This is my formal review of the Capy and Corosio Libraries.
As context I have been working with the Capy and Corosio libraries for
a period of four months as part of a coroutine migration effort. I
have been involved in implementing various components of, and
applications within a tier-1 financial market system (deployed on
Linux) that has been in production for many years.
The platform currently uses Asio as an IO backbone, so using a library
that mirrors the behaviour and performance of the Asio counterparts
was important.
While this review is about the merits of Capy/Corosio, it is natural
that much of it may reference Asio as a comparator given that it is
our reference point for capability and performance given our existing
codebase is entirely based on the Asio library (largely using
callbacks).
Also note I come at this review from the position of a user picking up
a new library as part of a refactoring and navigating that library
based on the documentation and examples provided. I’m not trying to do
anything too clever and have been able to fall-back on existing tests
to assure behaviour (albeit updated to work with Capy/Corosio).
*1. What is your evaluation of the usefulness of the libraries?*
My goal, by using these libraries, was to build simple coroutine
scaffolding around pre-existing concepts within the codebase. This was
made possible by a familiar API and strong documentation. The
introductions provided a good high-level overview that allowed me,
given my limited knowledge and experience with coroutines, to get up
to speed quickly. These libraries will prove very useful for building
effective coroutine-based applications.
I have used capy and corosio to write TCP/UDP servers and clients
using each of the socket types, so the facilities provided by Corosio
are exactly what I would expect from a networking library.
As an aside I should note that all my previous experience has been
with Asio and callback code so while some may feel that style of code
is hard to reason about I find it more intuitive, mainly because of my
experiences to date.
This means, for example, that some coroutine specific patterns felt a
little clunky or counter-intuitive at first. Not long after my first
introduction to the libraries I attempted to rewrite some functions
that execute a callback periodically when an asynchronous timer
expires. This was when a new pattern was adopted: the periodic
callback needs to be run in a loop, since the alternative, re-entering
the same timer function indefinitely could result in a stack overflow.
For me, initially, the callback style code felt easier to reason
about.
I had a similar experience when refactoring server code that performs
read and write tasks. While the new pattern of performing the
asynchronous waits in a loop made the flow of the server easier to
understand from a high-level perspective, doing this as part of a
callback-chain felt more natural to me.
Why am I mentioning this as a general observation? Well because, as
someone who might be more representative of a typical user coming from
a callback-based Asio background they may face similar mental hurdles.
However, and this is the point, despite this: the mental jump from
callbacks to coroutines and then using a new library as well, I can
say that the experience has been a good one. So none of this is a
criticism, more just an observation that may lead to documentation
improvements to better aid users like me.
No doubt there will be alternative patterns and approaches I should
have taken but I haven’t looked into that in detail yet because I was
able to get code working that delivered the same or better performance
than the older implementation by using the documentation as a very
accessible guide.
*2. What is your evaluation of the design?*
The design of the library has been easy to use with limited background
knowledge of C++20 coroutines. In terms of the facilities from Capy,
having the capy-provided task type meant that coroutines could be
composed and executed in parallel to make up small working projects
with only few includes required.
Using run_async in more complicated code took some getting used to, in
comparison to my experience of Asio’s callback approach using, for
example, boost::asio::read_some( Socket, Callback ) where the
execution of the async operation is also the code that runs after the
operation is complete and no more than that.
Passing stop tokens through the coroutine stack automatically in the
background makes cancellation a seamless task. Running child tasks
within this stack is easy to orchestrate with the helpers e.g.
capy::this_coro, for getting access to the environment where the stop
token can be found.
So all-in-all, as advertised, this is more or less a familiar design
to Asio but with a focus on being coroutine-first.
*3. What is your evaluation of the implementation?*
My experience of the libraries is less of the implementation and more
of the usage of the API, so I haven’t read a lot of the source code.
Anecdotally from a testing and behavioural point of view clearly the
implementation is fit for purpose as all of our tests (refactored to
allow both Asio and Capy/Corosio variants) passed and performed
comparably or better, so no concerns here.
*4. What is your evaluation of the documentation?*
The documentation is incredibly strong. The way they begin from first
principles - networking, coroutine machinery and threading
synchronisation etc., means that C++ developers can reference the
documents for guidance on many related topics. A lot of canonical
patterns are given. I found it was a simple task to adapt these to
working code.
*5. Have you used either or both libraries? What was your experience?*
Yes, as noted, I have used both libraries for a period of about 4
months to-date. Prior to that I had no experience of working with
coroutines in C++. As a result I very much appreciated that the
documentation started from first principles of coroutines, allowing me
to write my own awaiters and promises to get a handle on the
machinery, before trying some of the same mock code with capy task and
run_async.
Creating my own awaiter involved creating a sleeping thread that then
posted the continuation on wake-up. To get this working I had to look
at the capy::delay_awaitable to see what was done with the handle and
io_env, so maybe the documentation could be improved for others
wanting to create their own.
In trying to write more complex code I was tripped up by the lambda
capture lifetime caused by temporary lambda tasks on multiple
occasions. However the documentation has since been updated and
explicitly warns users about this with sufficient detail to explain
exactly what happens in this scenario, and proven alternatives.
In the time I have been using Capy/Corosio the documentation has
continued to evolve as misunderstandings arise and can be addressed
and this has supported (for me) a positive experience of using the
libraries but more importantly the result of using the libraries has
been working, production quality code.
*6. Are the libraries ready for inclusion in Boost?*
Yes, the libraries are such that they provide all of the tools
necessary to write coroutine code with networking facilities. For my
use cases there is just one point to be made about the socket/endpoint
interfaces which is mentioned below.
In short, for Asio, we write resolver.resolve(host_name, port) and
then try each of the endpoints, each of which is potentially IPv4 or
IPv6, until one of them works, keeping the application code
independent of the particular IP version.
In some cases, it would be convenient if corosio supported a similar
mechanism that would replace this style of application code:
if( Endpoint.is_v4() )
{
Socket_.set_option
( boost::corosio::socket_option
::join_group_v4( Endpoint.v4_address() ) );
}
else
{
Socket_.set_option
( boost::corosio::socket_option
::join_group_v6( Endpoint.v6_address() ) );
}
with
Socket_.set_option( boost::corosio::socket_option::join_group( Endpoint ) );
Having said that, this is not a blocker.
I haven’t looked into the test coverage on these libraries so cannot
make any comments about this.
*7. If not, what changes would you recommend before acceptance?*
Nothing that would be considered a blocker.
*8. Do the libraries fit well within the existing Boost ecosystem?*
Yes, as a C++20 coroutine-first alternative to Asio. Capy in
particular could serve as a basis for other IO library developers
wishing to start from a foundation of coroutines if it was accepted
alone (recognising that the review is to treat both libraries as a
set).
*9. Are there API, naming, usability, extensibility, or implementation
concerns that should be addressed?*
I have no major concerns about the libraries. The authors have been
quick to address and fix any issues encountered with use.
Our code would directly depend on native support for websockets. I
have been told that this will be supported at a later date, but I
cannot find any github issues or timelines backing this up.
*Summary*
For the avoidance of doubt I vote to accept these libraries into Boost
on the basis that for my use cases they have proved fit for purpose. I
have no strong views on the APIs and proposed changes voiced in other
reviews as I’ve been tracking an evolving API for a while now and have
not encountered any blockers.