This article is really helpful, thank you. R On Tue, 17 Feb 2026 at 18:58, Ruben Perez via Boost <boost@lists.boost.org> 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/boost@lists.boost.org/thread/DNTZBH7OC... [2] https://github.com/anarthal/boost-redis/tree/feature/corosio [3] https://github.com/boostorg/redis/blob/b7b93c96dae038f7817d653a35e7225b83d30... [4] https://github.com/boostorg/redis/blob/b7b93c96dae038f7817d653a35e7225b83d30... [5] https://github.com/anarthal/boost-redis/blob/16464517dc1b47107504b0d94805483... [6] https://github.com/cppalliance/corosio/issues/128 [7] https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/async_co... [8] https://github.com/cppalliance/corosio/issues/124 [9] https://github.com/anarthal/boost-redis/blob/16464517dc1b47107504b0d94805483... [10] https://github.com/cppalliance/corosio/issues/122 [11] https://github.com/boostorg/mysql/issues/454 [12] https://github.com/cppalliance/capy/blob/8bcc0fa3753d7ebf80f28379865d987e8d2... [13] https://github.com/cppalliance/capy/blob/8bcc0fa3753d7ebf80f28379865d987e8d2... [14] https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/cancel_a... [15] https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/cancel_a... [16] https://github.com/cppalliance/corosio/issues/141 [17] https://github.com/anarthal/boost-redis/blob/16464517dc1b47107504b0d94805483... [18] https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/windows_... [19] https://github.com/cppalliance/capy/blob/8bcc0fa3753d7ebf80f28379865d987e8d2... [20] https://github.com/boostorg/beast/blob/develop/include/boost/beast/core/flat... [21] https://github.com/cppalliance/capy/blob/8bcc0fa3753d7ebf80f28379865d987e8d2... [22] https://github.com/cppalliance/capy/blob/8bcc0fa3753d7ebf80f28379865d987e8d2... [23] https://github.com/cppalliance/capy/issues/168 _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/FSX5H3MD...