Re: Porting Boost.Redis to Corosio and Capy
Richard Hodges via Boost <[email protected]>
| Newsgroups | gmane.comp.lib.boost.devel |
|---|---|
| Message-ID | <CALvx3hayWSfg-yA8d5KrSbDH9=F1LodcNhQ6SgzqbUueWA-p0g@mail.gmail.com> |
This article is really helpful, thank you. R On Tue, 17 Feb 2026 at 18:58, Ruben Perez via Boost <[email protected]> wrote: > Hi all, > > As you might remember, last week I endorsed Vinnie's Capy+Corosio > submission and promised to talk about how I had managed to port > Boost.Redis to use these libraries [1] [2]. This is the post. It's > somehow a mini-review. Warning label: it might be a little lengthy. > > Disclaimer: I'm affiliated with the C++ Alliance. I'm trying to write > this as a potential user, rather than as an Alliance member. > > > Why Boost-Redis > =============== > > As you might know, I authored Boost.MySQL and co-maintain Boost.Redis > with Marcelo. I chose Boost.Redis because it has a smaller API surface > than MySQL, while still exercising similar functionality regarding > networking. > > Note that Boost.Redis is a high-level library. In addition to parsing > and serializing messages, it manages connection lifecycle, > automatically reconnects on failure, pipelines multiple requests into > a single round trip when possible, and handles health-check pings to > detect dead connections. > > > Library requirements > ==================== > > A library like this requires lots of networking and concurrency > primitives, like: > > * Reading and writing data. > * Establishing TCP connections. > * Establishing UNIX domain socket connections. > * TLS. > * Cancellation. > * Running tasks in parallel. > * Waiting for events to happen (e.g. "block my task until the user > submits a new request"). > * Waiting periods of time (timers). > * Setting timeouts to operations. > > Some of these primitives (like timers) are only present in Corosio, so > using Capy alone isn't an option. > This is one of the reasons why I endorse having them reviewed > together. I understand the rationale > behind keeping them separate, but users like me benefit the most if > both of them are present. > > > Porting strategy > ================ > > If you've ever written universal Asio async operations, you might know > that Asio's composition mechanism, > async_compose, is hard. It works, but it leads to heavily templated > code, lengthy compiler errors, > and code difficult to test. > > For this reason, most of Boost.Redis code today is written as > finite-state machines (FSMs) > that decouple logic from I/O. The FSM is a hand-wired generator that > yields actions > (i.e. "what should I do next? read from the server? write data? wait?"). > The I/O layer just executes these actions. [3] is the FSM for the task > that reads > data from the server, with [4] being its I/O layer. This is the code > as it is today. > > This disposition has made the port pretty straightforward. You just > need to replace > the I/O layer with Corosio's primitives and go. The reader becomes [5]. > Code managing connection establishment required slightly more work, > but nothing unsurmountable. > > I've ported some integration tests, too [9] to prove that the port works. > There is still much to do on my side here, though. > > The port is currently functional, but it could use some refactoring. > Coroutines enable separate compilation where universal Asio doesn't, > and we could make improvements here. More on this later though. > > > > Mapping Asio to Capy/Corosio > ============================ > > Let's go over the primitives I've exercised and how well things have gone. > > * Reading and writing data: I'd say this is Capy's specialty. > Primitives map cleanly. > Capy has a clean stream model here, with optional type erasure, that > makes things simple. > Nothing to object. > * Establishing TCP connections: most of it maps well. Some considerations > here: > * You need Corosio to achieve this. With the current design, I think > this is reasonable today. > * Only IPv4 is currently supported [6]. Socket opening semantics need > better definition. > * Range connect [7] is missing [8]. This is practical because hostname > resolutions yields > a range of endpoints, rather than a single one. This is however > trivially implementable > by users thanks to coroutines, so it isn't critical. > * Establishing UNIX domain socket connections: this is not supported > at the moment [10]. > * TLS. I'm particularly happy here. Corosio defines a tls_stream base > class, with concrete > implementations, like openssl_stream or wolfssl_stream. This > traditional OOP approach > is possible because of coroutines. > Many of our users run Redis in trusted networks, inside Docker > containers, over plain TCP. > They shouldn't have to link OpenSSL if they don't use it. In the > current Asio-based version, > this is not possible without heavy templating or config macros. We've > been requested to > implement this a number of times, but it's ugly. With Corosio we could do > it. > * Cancellation. Capy's model here is simpler than Asio's: > * Asio has three cancellation types with potentially different semantics: > terminal, partial and total. Capy has just one. I think this is the > right choice. > None of the libraries I work on really require more than this. And > uncommon cases > can be modeled as a combination of when_all, when_any, and async_event > primitives. > * Asio's cancellation handlers are stateless. Cancellation state is > held by async_compose. > Cancellation state is required because a cancellation might arrive > after an operation > finishes, but before its completion handler runs. The consequence is > that you need to > check your operation's cancellation state manually after every async > primitive. > If you forget one, you get an insidious bug [11]. > Capy cancellation uses std::stop_token, which is stateful. > This mostly removes this problem, which is good. > * Asio's cancellation handlers are not thread-safe by default. > This leads to insidious surprises. Capy cancellation is thread-safe by > default. > I don't know if there is any performance penalty here, but it's > probably the best choice. > * Note: correct and universal cancellation semantics are mandatory to > get the next point right. > Primitives like when_any don't make sense if one of the operations > can't be reliably cancelled. > * Running tasks in parallel. Also happy here. Capy has when_any and > when_all, similar > to Asio's parallel groups. Parallel groups in Asio are still > experimental and may > create problems with certain config macros, so it's a win here, too. > * Note: when_any and when_all are actually a subset of what can be > achieved with Asio's parallel group. > I don't think this is a problem though. It is the most useful subset. > Anything else can be > expressed in terms of them + async_event. > * Synchronization primitives (i.e. "waiting for events to happen"). > Capy has async_mutex [12] and async_event [13]. Asio has none of these, > and you need to either implement them using channels (experimental), or > timers (you set the timer expiration to infinity, then cancel the > timer to signal the event). > These are both workarounds and don't capture user intention. > I can't stress enough the importance of having a working async_event, > especially > for higher-level libraries like Redis. It's a fundamental building block. > * Waiting periods of time (timers). These are similar to Asio's > steady_timer. > Other timer types are not supported. None of the libraries I work on > need anything > else, so I'm satisfied. > * Something I'd explore is a standalone sleep(steady_clock::duration) > function. > It might be ergonomic in some cases. > * Setting timeouts to operations. Asio implements this with the > cancel_after [14] > and cancel_at [15] completion tokens, which we use in Redis. These are > absent from > Corosio [16]. > I've done a quick-and-dirty implementation [17], but this should be > extended by the > library to support custom awaitables (and not only capy::task). > > > Separate compilation > ==================== > > One of the key benefits of the proposed model is separate compilation > (note how the port's connection implementation lives in an .ipp file, > which ends up in a single TU). > > The libraries have no config macros, which I like. > Having them creates a lot of problems for separate compilation > (this is why we have .ipp files instead of regular TUs in Redis today). > > Type erasure has probably some performance implications, which I haven't > evaluated. A database library like Boost.Redis is probably not going > to see any significant impact here. We get performance from reducing > round-trips to the server. This kind of performance impact > is probably more relevant to domains like high-frequency trading, > but I can't add anything here. > > > Error handling > ============== > > Asio has a built-in error_code to exception transformation. > If you have an async_write operation accepting a completion handler > of void(error_code, std::size_t), when using coroutines, you get: > > // error_code is swallowed. This throws if the error_code indicates failure > std::size_t bytes = co_await async_write(...); > > This is good for tutorials but probably not what you want for production, > since I/O errors are not exceptional. So you end up writing: > > // doesn't throw, check ec manually > auto [ec, bytes ] = co_await async_write(..., asio::as_tuple); > > Capy doesn't support this. I think this is the right choice, > because it's simpler and leads to code following best practice. > > > Executors and associated properties > =================================== > > In Asio, every completion token has a set of associated properties: > executor, immediate executor, allocator and cancellation slot. > When writing composed operations, you need to make sure to propagate this. > This is not trivial with callbacks, since passing a lambda by default > doesn't achieve it. > > Capy stores most of this in a an io_env structure. Task frames hold a > pointer > to such structure. The result is that propagation happens by default. > I haven't had to think much about this, which is a good sign. > > Capy has functions to launch coroutines with a different environment. > I haven't had to use these yet, but I will likely need them when writing > multi-threaded examples for Redis. Having them is required. > > > Windows-specific I/O > ==================== > > While Redis doesn't, MySQL supports communication over Windows named pipes. > We don't have this working in Boost.MySQL yet, but Asio's > windows::stream_handle > [18] probably helps here. It's not a high-priority thing, but it would be > nice > having it on the horizon. > > > Buffers > ======= > > Capy provides many utilities to work with buffers. I think the most useful > utilities here are the concrete buffer types, like > beyond my needs, but I want to highlight the concrete buffer types, > like circular_dynamic_buffer [19]. > > I miss here an equivalent to Beast's flat_buffer [20] [23]. At the moment, > concrete buffer types are: > > * circular_dynamic_buffer. As I said, very useful. > * flat_dynamic_buffer. This is non-allocating, and never frees space > with memmove. This is a problem if the server sends a stream of small > messages, > since there is no guarantee of message boundaries. > * dynamic_string_buffer and dynamic_vector_buffer. These are similar to > Asio's. > Protocols handling many small messages can't use these because they cause > data to move with every consumed message. > > Buffer sequence type erasure is split between Corosio and Capy. There > are two classes doing essentially the same job, and when I tried to > write a generator that returns a buffer sequence as a concrete type, I > found the story fragmented. A single unified type would simplify > things. I don't have a strong opinion on where it should live, but > having one answer instead of two would help. > > > Asio interoperability > ===================== > > The ASIO interop layer has friction. The executor adapter translates a > dispatch call into an `async_post` internally [22], which isn't > correct. When you dispatch in ASIO, the expectation is that if you're > already on the right executor, the function runs inline. Turning that > into a post adds unnecessary overhead and breaks the expected > semantics. The boundary between ASIO and Corosio needs to be smoother > if we want incremental adoption, and we do want incremental adoption > because no one is going to rewrite their entire stack at once. > > > Final thoughts > ============== > > The port succeeded, and this is enough to earn my endorsement. Most > coroutine code is cleaner than the one written for universal Asio, > which is good. I've found bugs, but Vinnie's team has addressed them > already. > > I intend to port the rest of the tests and likely Boost.MySQL at some > point, which will exercise more parts of the API. > > I hope to see these libraries in Boost in the future. > > Congratulations reading (or skipping) until here, you're a really > tenacious reader. > > Regards, > Ruben. > > > [1] > https://lists.boost.org/archives/list/[email protected]/thread/DNTZBH7OCFWG4LERWCFEPBVDVKVHAR6A/ > [2] https://github.com/anarthal/boost-redis/tree/feature/corosio > [3] > https://github.com/boostorg/redis/blob/b7b93c96dae038f7817d653a35e7225b83d30ae6/include/boost/redis/impl/writer_fsm.ipp > [4] > https://github.com/boostorg/redis/blob/b7b93c96dae038f7817d653a35e7225b83d30ae6/include/boost/redis/connection.hpp#L419-L456 > [5] > https://github.com/anarthal/boost-redis/blob/16464517dc1b47107504b0d9480548314fd1071a/include/boost/redis/impl/connection.ipp#L327-L360 > [6] https://github.com/cppalliance/corosio/issues/128 > [7] > https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/async_connect.html > [8] https://github.com/cppalliance/corosio/issues/124 > [9] > https://github.com/anarthal/boost-redis/blob/16464517dc1b47107504b0d9480548314fd1071a/test/test_conn_setup.cpp > [10] https://github.com/cppalliance/corosio/issues/122 > [11] https://github.com/boostorg/mysql/issues/454 > [12] > https://github.com/cppalliance/capy/blob/8bcc0fa3753d7ebf80f28379865d987e8d22629f/include/boost/capy/ex/async_mutex.hpp > [13] > https://github.com/cppalliance/capy/blob/8bcc0fa3753d7ebf80f28379865d987e8d22629f/include/boost/capy/ex/async_event.hpp > [14] > https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/cancel_after.html > [15] > https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/cancel_at.html > [16] https://github.com/cppalliance/corosio/issues/141 > [17] > https://github.com/anarthal/boost-redis/blob/16464517dc1b47107504b0d9480548314fd1071a/include/boost/redis/impl/connection.ipp#L33-L54 > [18] > https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/windows__stream_handle.html > [19] > https://github.com/cppalliance/capy/blob/8bcc0fa3753d7ebf80f28379865d987e8d22629f/include/boost/capy/buffers/circular_dynamic_buffer.hpp > [20] > https://github.com/boostorg/beast/blob/develop/include/boost/beast/core/flat_buffer.hpp > [21] > https://github.com/cppalliance/capy/blob/8bcc0fa3753d7ebf80f28379865d987e8d22629f/include/boost/capy/buffers/flat_dynamic_buffer.hpp > [22] > https://github.com/cppalliance/capy/blob/8bcc0fa3753d7ebf80f28379865d987e8d22629f/example/asio/api/uni_stream.hpp#L57 > [23] https://github.com/cppalliance/capy/issues/168 > _______________________________________________ > Boost mailing list -- [email protected] > To unsubscribe send an email to [email protected] > https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ > Archived at: > https://lists.boost.org/archives/list/[email protected]/message/FSX5H3MDQSLO3VZFEOUINUZPYQFCIASB/ > _______________________________________________ Boost mailing list -- [email protected] To unsubscribe send an email to [email protected] https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/[email protected]/message/DQMVXEPHUOCHIVSKJFBTKPPKOCGEX3MU/