From d767db9b4d7af80d29cdaace2ea3dfd57e3d1e91 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 9 Feb 2026 09:20:55 +0100 Subject: [PATCH 001/115] Initial impl --- include/boost/redis/corosio_connection.hpp | 946 +++++++++++++++++++++ 1 file changed, 946 insertions(+) create mode 100644 include/boost/redis/corosio_connection.hpp diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp new file mode 100644 index 00000000..6543b613 --- /dev/null +++ b/include/boost/redis/corosio_connection.hpp @@ -0,0 +1,946 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_COROSIO_CONNECTION_HPP +#define BOOST_REDIS_COROSIO_CONNECTION_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis { +namespace detail { + +// Given a timeout value, compute the expiry time. A zero timeout is considered to mean "no timeout" +inline std::chrono::steady_clock::time_point compute_expiry( + std::chrono::steady_clock::duration timeout) +{ + return timeout.count() == 0 ? (std::chrono::steady_clock::time_point::max)() + : std::chrono::steady_clock::now() + timeout; +} + +inline asio::cancellation_type_t token_to_cancel(std::stop_token tok) +{ + return tok.stop_requested() ? asio::cancellation_type_t::partial + : asio::cancellation_type_t::none; +} + +// TODO: actually implement +class corosio_redis_stream { + // asio::ssl::context ssl_ctx_; + // asio::ip::basic_resolver resolv_; + // asio::ssl::stream> stream_; + // #ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + // asio::basic_stream_socket unix_socket_; + // #endif + // typename asio::steady_timer::template rebind_executor::other timer_; + redis_stream_state st_; + +public: + // I/O + capy::io_task async_connect(const connect_params& params, buffered_logger& l); + + template + capy::io_task async_write_some(const ConstBufferSequence& buffers); + + template + capy::io_task async_read_some(const MutableBufferSequence& buffers); +}; + +struct corosio_connection_impl { + // using receive_channel_type = asio::experimental::channel< + // Executor, + // void(system::error_code, std::size_t)>; + + corosio_redis_stream stream_; + corosio::timer writer_timer_; // timer used for write timeouts + capy::async_event writer_event_; // set when there is new data to write + corosio::timer reader_timer_; // timer used for read timeouts + corosio::timer reconnect_timer_; // to wait the reconnection period + corosio::timer ping_timer_; // to wait between pings + // receive_channel_type receive_channel_; + asio::cancellation_signal run_signal_; + connection_state st_; + + corosio_connection_impl(capy::execution_context& ex, logger&& lgr) + : stream_{ex} + , writer_timer_{ex} + , writer_event_{ex} + , reader_timer_{ex} + , reconnect_timer_{ex} + , ping_timer_{ex} // , receive_channel_{ex, 256} + , st_{{std::move(lgr)}} + { + set_receive_adapter(any_adapter{ignore}); + writer_event_.expires_at((std::chrono::steady_clock::time_point::max)()); + } + + void cancel(operation op) + { + switch (op) { + case operation::exec: st_.mpx.cancel_waiting(); break; + case operation::receive: cancel_receive_v2(); break; + case operation::reconnection: + st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); + break; + case operation::run: + case operation::resolve: + case operation::connect: + case operation::ssl_handshake: + case operation::health_check: cancel_run(); break; + case operation::all: + st_.mpx.cancel_waiting(); // exec + cancel_receive_v2(); // receive + st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); // reconnect + cancel_run(); // run + break; + default: /* ignore */; + } + } + + void cancel_receive_v1(); + + void cancel_receive_v2() + { + st_.receive2_cancelled = true; + cancel_receive_v1(); + } + + // void cancel_run() + // { + // // Individual operations should see a terminal cancellation, regardless + // // of what we got requested. We take enough actions to ensure that this + // // doesn't prevent the object from being re-used (e.g. we reset the TLS stream). + // run_signal_.emit(asio::cancellation_type_t::terminal); + + // // Name resolution doesn't support per-operation cancellation + // stream_.cancel_resolve(); + + // // Receive is technically not part of run, but we also cancel it for + // // backwards compatibility. Note that this intentionally affects v1 receive, only. + // cancel_receive_v1(); + // } + + capy::io_task<> async_exec(request const& req, any_adapter adapter) + { + // Setup + capy::async_event request_done; + auto elem = make_elem(req, std::move(adapter)); + elem->set_done_callback([&request_done]() { + request_done.set(); + }); + exec_fsm fsm{elem}; + + // Invoke the FSM + while (true) { + // Invoke the state machine + auto act = fsm.resume(true, st_, token_to_cancel(co_await capy::this_coro::stop_token)); + + // Do what the FSM said + switch (act.type()) { + case exec_action_type::setup_cancellation: break; // ignored, not required by capy + case exec_action_type::immediate: break; // ignored, not required by capy + case exec_action_type::notify_writer: writer_event_.set(); break; + case exec_action_type::wait_for_response: co_await request_done.wait(); break; + case exec_action_type::done: co_return {act.error()}; + } + } + } + + void set_receive_adapter(any_adapter adapter) + { + st_.mpx.set_receive_adapter(std::move(adapter)); + } +}; + +template +struct receive2_op { + connection_impl* conn_; + receive_fsm fsm_{}; + + void drain_receive_channel() + { + // We don't expect any errors here. The only errors + // that might appear in the channel are due to cancellations, + // and these don't make sense with try_receive + auto f = [](system::error_code, std::size_t) { }; + while (conn_->receive_channel_.try_receive(f)) + ; + } + + template + void operator()(Self& self, system::error_code ec = {}, std::size_t /* push_bytes */ = 0u) + { + receive_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + + switch (act.type) { + case receive_action::action_type::setup_cancellation: + self.reset_cancellation_state(asio::enable_total_cancellation()); + (*this)(self); // this action does not require yielding + return; + case receive_action::action_type::wait: + conn_->receive_channel_.async_receive(std::move(self)); + return; + case receive_action::action_type::drain_channel: + drain_receive_channel(); + (*this)(self); // this action does not require yielding + return; + case receive_action::action_type::immediate: + asio::async_immediate(self.get_io_executor(), std::move(self)); + return; + case receive_action::action_type::done: self.complete(act.ec); return; + } + } +}; + +template +struct exec_one_op { + connection_impl* conn_; + const request* req_; + exec_one_fsm fsm_; + + explicit exec_one_op(connection_impl& conn, const request& req, any_adapter resp) + : conn_(&conn) + , req_(&req) + , fsm_(std::move(resp), req.get_expected_responses()) + { } + + template + void operator()(Self& self, system::error_code ec = {}, std::size_t bytes_written = 0u) + { + exec_one_action act = fsm_.resume( + conn_->st_.mpx.get_read_buffer(), + ec, + bytes_written, + self.get_cancellation_state().cancelled()); + + switch (act.type) { + case exec_one_action_type::done: self.complete(act.ec); return; + case exec_one_action_type::write: + asio::async_write(conn_->stream_, asio::buffer(req_->payload()), std::move(self)); + return; + case exec_one_action_type::read_some: + conn_->stream_.async_read_some( + conn_->st_.mpx.get_read_buffer().get_prepared(), + std::move(self)); + return; + } + } +}; + +template +auto async_exec_one( + connection_impl& conn, + const request& req, + any_adapter resp, + CompletionToken&& token) +{ + return asio::async_compose( + exec_one_op{conn, req, std::move(resp)}, + token, + conn); +} + +template +struct sentinel_resolve_op { + connection_impl* conn_; + sentinel_resolve_fsm fsm_; + + explicit sentinel_resolve_op(connection_impl& conn) + : conn_(&conn) + { } + + template + void operator()(Self& self, system::error_code ec = {}) + { + auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after + sentinel_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + + switch (act.get_type()) { + case sentinel_action::type::done: self.complete(act.error()); return; + case sentinel_action::type::connect: + conn->stream_.async_connect( + make_sentinel_connect_params(conn->st_.cfg, act.connect_addr()), + conn->st_.logger, + std::move(self)); + return; + case sentinel_action::type::request: + async_exec_one( + *conn, + conn->st_.cfg.sentinel.setup, + make_sentinel_adapter(conn->st_), + asio::cancel_after( + conn->reconnect_timer_, // should be safe to re-use this + conn->st_.cfg.sentinel.request_timeout, + std::move(self))); + return; + } + } +}; + +template +auto async_sentinel_resolve(connection_impl& conn, CompletionToken&& token) +{ + return asio::async_compose( + sentinel_resolve_op{conn}, + token, + conn); +} + +template +struct writer_op { + connection_impl* conn_; + writer_fsm fsm_; + + explicit writer_op(connection_impl& conn) noexcept + : conn_(&conn) + { } + + template + void operator()(Self& self, system::error_code ec = {}, std::size_t bytes_written = 0u) + { + auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after + auto act = fsm_.resume( + conn->st_, + ec, + bytes_written, + self.get_cancellation_state().cancelled()); + + switch (act.type()) { + case writer_action_type::done: self.complete(act.error()); return; + case writer_action_type::write_some: + conn->stream_.async_write_some( + asio::buffer(conn->st_.mpx.get_write_buffer()), + asio::cancel_at( + conn->writer_timer_, + compute_expiry(act.timeout()), + std::move(self))); + return; + case writer_action_type::wait: + conn->writer_cv_.expires_at(compute_expiry(act.timeout())); + conn->writer_cv_.async_wait(std::move(self)); + return; + } + } +}; + +template +struct reader_op { + connection_impl* conn_; + reader_fsm fsm_; + +public: + reader_op(connection_impl& conn) noexcept + : conn_{&conn} + { } + + template + void operator()(Self& self, system::error_code ec = {}, std::size_t n = 0) + { + for (;;) { + auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after + auto act = fsm_.resume(conn->st_, n, ec, self.get_cancellation_state().cancelled()); + + switch (act.get_type()) { + case reader_fsm::action::type::read_some: + conn->stream_.async_read_some( + asio::buffer(conn->st_.mpx.get_prepared_read_buffer()), + asio::cancel_at( + conn->reader_timer_, + compute_expiry(act.timeout()), + std::move(self))); + return; + case reader_fsm::action::type::notify_push_receiver: + if (conn->receive_channel_.try_send(ec, act.push_size())) { + continue; + } else { + conn->receive_channel_.async_send(ec, act.push_size(), std::move(self)); + } + return; + case reader_fsm::action::type::done: self.complete(act.error()); return; + } + } + } +}; + +template +class run_op { +private: + connection_impl* conn_; + run_fsm fsm_{}; + + template + auto reader(CompletionToken&& token) + { + return asio::async_compose( + reader_op{*conn_}, + std::forward(token), + conn_->writer_cv_); + } + + template + auto writer(CompletionToken&& token) + { + return asio::async_compose( + writer_op{*conn_}, + std::forward(token), + conn_->writer_cv_); + } + +public: + run_op(connection_impl* conn) noexcept + : conn_{conn} + { } + + // Called after the parallel group finishes + template + void operator()( + Self& self, + std::array order, + system::error_code reader_ec, + system::error_code writer_ec) + { + (*this)(self, order[0u] == 0u ? reader_ec : writer_ec); + } + + template + void operator()(Self& self, system::error_code ec = {}) + { + auto act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + + switch (act.type) { + case run_action_type::done: self.complete(act.ec); return; + case run_action_type::immediate: + asio::async_immediate(self.get_io_executor(), std::move(self)); + return; + case run_action_type::sentinel_resolve: + async_sentinel_resolve(*conn_, std::move(self)); + return; + case run_action_type::connect: + conn_->stream_.async_connect( + make_run_connect_params(conn_->st_), + conn_->st_.logger, + std::move(self)); + return; + case run_action_type::parallel_group: + asio::experimental::make_parallel_group( + [this](auto token) { + return this->reader(token); + }, + [this](auto token) { + return this->writer(token); + }) + .async_wait(asio::experimental::wait_for_one(), std::move(self)); + return; + case run_action_type::cancel_receive: + conn_->receive_channel_.cancel(); + (*this)(self); // this action does not require suspending + return; + case run_action_type::wait_for_reconnection: + conn_->reconnect_timer_.expires_after(conn_->st_.cfg.reconnect_wait_interval); + conn_->reconnect_timer_.async_wait(std::move(self)); + return; + default: BOOST_ASSERT(false); + } + } +}; + +logger make_stderr_logger(logger::level lvl, std::string prefix); + +template +class run_cancel_handler { + connection_impl* conn_; + +public: + explicit run_cancel_handler(connection_impl& conn) noexcept + : conn_(&conn) + { } + + void operator()(asio::cancellation_type_t cancel_type) const + { + // We support terminal and partial cancellation + constexpr auto mask = asio::cancellation_type_t::terminal | + asio::cancellation_type_t::partial; + + if ((cancel_type & mask) != asio::cancellation_type_t::none) { + conn_->cancel(operation::run); + } + } +}; + +} // namespace detail + +/** @brief A SSL connection to the Redis server. + * + * This class keeps a healthy connection to the Redis instance where + * commands can be sent at any time. For more details, please see the + * documentation of each individual function. + * + * @tparam Executor The executor type used to create any required I/O objects. + */ +template +class basic_connection { +public: + using this_type = basic_connection; + + /// (Deprecated) Type of the next layer + BOOST_DEPRECATED("This typedef is deprecated, and will be removed with next_layer().") + typedef asio::ssl::stream> next_layer_type; + + /// The type of the executor associated to this object. + using executor_type = Executor; + + /// Rebinds the socket type to another executor. + template + struct rebind_executor { + /// The connection type when rebound to the specified executor. + using other = basic_connection; + }; + + /** @brief Constructor from an executor. + * + * @param ex Executor used to create all internal I/O objects. + * @param ctx SSL context. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + explicit basic_connection( + executor_type ex, + asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, + logger lgr = {}) + : impl_( + std::make_unique>( + std::move(ex), + std::move(ctx), + std::move(lgr))) + { } + + /** @brief Constructor from an executor and a logger. + * + * @param ex Executor used to create all internal I/O objects. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + * + * An SSL context with default settings will be created. + */ + basic_connection(executor_type ex, logger lgr) + : basic_connection( + std::move(ex), + asio::ssl::context{asio::ssl::context::tlsv12_client}, + std::move(lgr)) + { } + + /** + * @brief Constructor from an `io_context`. + * + * @param ioc I/O context used to create all internal I/O objects. + * @param ctx SSL context. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + explicit basic_connection( + asio::io_context& ioc, + asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, + logger lgr = {}) + : basic_connection(ioc.get_executor(), std::move(ctx), std::move(lgr)) + { } + + /** + * @brief Constructor from an `io_context` and a logger. + * + * @param ioc I/O context used to create all internal I/O objects. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + basic_connection(asio::io_context& ioc, logger lgr) + : basic_connection( + ioc.get_executor(), + asio::ssl::context{asio::ssl::context::tlsv12_client}, + std::move(lgr)) + { } + + /// Returns the associated executor. + executor_type get_executor() noexcept { return impl_->writer_cv_.get_executor(); } + + /** @brief Starts the underlying connection operations. + * + * This function establishes a connection to the Redis server and keeps + * it healthy by performing the following operations: + * + * @li For Sentinel deployments (`config::sentinel::addresses` is not empty), + * contacts Sentinels to obtain the address of the configured master. + * @li For TCP connections, resolves the server hostname passed in + * @ref boost::redis::config::addr. + * @li Establishes a physical connection to the server. For TCP connections, + * connects to one of the endpoints obtained during name resolution. + * For UNIX domain socket connections, it connects to @ref boost::redis::config::unix_sockets. + * @li If @ref boost::redis::config::use_ssl is `true`, performs the TLS handshake. + * @li Executes the setup request, as defined by the passed @ref config object. + * By default, this is a `HELLO` command, but it can contain any other arbitrary + * commands. See the @ref config::setup docs for more info. + * @li Starts a health-check operation where `PING` commands are sent + * at intervals specified by + * @ref config::health_check_interval when the connection is idle. + * See the documentation of @ref config::health_check_interval for more info. + * @li Starts read and write operations. Requests issued using @ref async_exec + * before `async_run` is called will be written to the server immediately. + * + * When a connection is lost for any reason, a new one is + * established automatically. To disable reconnection + * set @ref boost::redis::config::reconnect_wait_interval to zero. + * + * The completion token must have the following signature + * + * @code + * void f(system::error_code); + * @endcode + * + * @par Per-operation cancellation + * This operation supports the following cancellation types: + * + * @li `asio::cancellation_type_t::terminal`. + * @li `asio::cancellation_type_t::partial`. + * + * In both cases, cancellation is equivalent to calling @ref basic_connection::cancel + * passing @ref operation::run as argument. + * + * After the operation completes, the token's associated cancellation slot + * may still have a cancellation handler associated to this connection. + * You should make sure to not invoke it after the connection has been destroyed. + * This is consistent with what other Asio I/O objects do. + * + * For example on how to call this function refer to + * cpp20_intro.cpp or any other example. + * + * @param cfg Configuration parameters. + * @param token Completion token. + */ + template > + auto async_run(config const& cfg, CompletionToken&& token = {}) + { + return asio::async_initiate( + run_initiation{impl_.get()}, + token, + &cfg); + } + + /** @brief Wait for server pushes asynchronously. + * + * This function suspends until at least one server push is received by the + * connection. On completion an unspecified number of pushes will + * have been added to the response object set with @ref + * set_receive_response. Use the functions in the response object + * to know how many messages they were received and consume them. + * + * To prevent receiving an unbound number of pushes the connection + * blocks further read operations on the socket when 256 pushes + * accumulate internally (we don't make any commitment to this + * exact number). When that happens any `async_exec`s and + * health-checks won't make any progress and the connection may + * eventually timeout. To avoid this, apps that expect server pushes + * should call this function continuously in a loop. + * + * This function should be used instead of the deprecated @ref async_receive. + * It differs from `async_receive` in the following: + * + * @li `async_receive` is designed to consume a single push message at a time. + * This can be inefficient when receiving lots of server pushes. + * `async_receive2` is batch-oriented. All pushes that are available + * when `async_receive2` is called will be marked as consumed. + * @li `async_receive` is cancelled when a reconnection happens (e.g. because + * of a network error). This enabled the user to re-establish subscriptions + * using @ref async_exec before waiting for pushes again. With the introduction of + * functions like @ref request::subscribe, subscriptions are automatically + * re-established on reconnection. Thus, `async_receive2` is not cancelled + * on reconnection. + * @li `async_receive` passes the number of bytes that each received + * push message contains. This information is unreliable and not very useful. + * Equivalent information is available using functions in the response object. + * @li `async_receive` might get cancelled if `async_run` is cancelled. + * This doesn't happen with `async_receive2`. + * + * This function does *not* remove messages from the response object + * passed to @ref set_receive_response - use the functions in the response + * object to achieve this. + * + * Only a single instance of `async_receive2` may be outstanding + * for a given connection at any time. Trying to start a second one + * will fail with @ref error::already_running. + * + * @note To avoid deadlocks the task (e.g. coroutine) calling + * `async_receive2` should not call `async_exec` in a way where + * they could block each other. This is, avoid the following pattern: + * + * @code + * asio::awaitable receiver() + * { + * // Do NOT do this!!! The receive buffer might get full while + * // async_exec runs, which will block all read operations until async_receive2 + * // is called. The two operations end up waiting each other, making the connection unresponsive. + * // If you need to do this, use two connections, instead. + * co_await conn.async_receive2(); + * co_await conn.async_exec(req, resp); + * } + * @endcode + * + * For an example see cpp20_subscriber.cpp. + * + * The completion token must have the following signature: + * + * @code + * void f(system::error_code); + * @endcode + * + * @par Per-operation cancellation + * This operation supports the following cancellation types: + * + * @li `asio::cancellation_type_t::terminal`. + * @li `asio::cancellation_type_t::partial`. + * @li `asio::cancellation_type_t::total`. + * + * @param token Completion token. + */ + template > + auto async_receive2(CompletionToken&& token = {}) + { + return asio::async_compose( + detail::receive2_op{impl_.get()}, + token, + *impl_); + } + + /** @brief Executes commands on the Redis server asynchronously. + * + * This function sends a request to the Redis server and waits for + * the responses to each individual command in the request. If the + * request contains only commands that don't expect a response, + * the completion occurs after it has been written to the + * underlying stream. Multiple concurrent calls to this function + * will be automatically queued by the implementation. + * + * For an example see cpp20_echo_server.cpp. + * + * The completion token must have the following signature: + * + * @code + * void f(system::error_code, std::size_t); + * @endcode + * + * Where the second parameter is the size of the response received + * in bytes. + * + * @par Per-operation cancellation + * This operation supports per-operation cancellation. Depending on the state of the request + * when cancellation is requested, we can encounter two scenarios: + * + * @li If the request hasn't been sent to the server yet, cancellation will prevent it + * from being sent to the server. In this situation, all cancellation types are supported + * (`asio::cancellation_type_t::terminal`, `asio::cancellation_type_t::partial` and + * `asio::cancellation_type_t::total`). + * @li If the request has been sent to the server but the response hasn't arrived yet, + * cancellation will cause `async_exec` to complete immediately. When the response + * arrives from the server, it will be ignored. In this situation, only + * `asio::cancellation_type_t::terminal` and `asio::cancellation_type_t::partial` + * are supported. Cancellation requests specifying `asio::cancellation_type_t::total` + * only will be ignored. + * + * In any case, connections can be safely used after cancelling `async_exec` operations. + * + * @par Object lifetimes + * Both `req` and `res` should be kept alive until the operation completes. + * No copies of the request object are made. + * + * @param req The request to be executed. + * @param resp The response object to parse data into. + * @param token Completion token. + */ + template < + class Response = ignore_t, + class CompletionToken = asio::default_completion_token_t> + auto async_exec(request const& req, Response& resp = ignore, CompletionToken&& token = {}) + { + return this->async_exec(req, any_adapter{resp}, std::forward(token)); + } + + /** @brief Executes commands on the Redis server asynchronously. + * + * This function sends a request to the Redis server and waits for + * the responses to each individual command in the request. If the + * request contains only commands that don't expect a response, + * the completion occurs after it has been written to the + * underlying stream. Multiple concurrent calls to this function + * will be automatically queued by the implementation. + * + * For an example see cpp20_echo_server.cpp. + * + * The completion token must have the following signature: + * + * @code + * void f(system::error_code, std::size_t); + * @endcode + * + * Where the second parameter is the size of the response received + * in bytes. + * + * @par Per-operation cancellation + * This operation supports per-operation cancellation. Depending on the state of the request + * when cancellation is requested, we can encounter two scenarios: + * + * @li If the request hasn't been sent to the server yet, cancellation will prevent it + * from being sent to the server. In this situation, all cancellation types are supported + * (`asio::cancellation_type_t::terminal`, `asio::cancellation_type_t::partial` and + * `asio::cancellation_type_t::total`). + * @li If the request has been sent to the server but the response hasn't arrived yet, + * cancellation will cause `async_exec` to complete immediately. When the response + * arrives from the server, it will be ignored. In this situation, only + * `asio::cancellation_type_t::terminal` and `asio::cancellation_type_t::partial` + * are supported. Cancellation requests specifying `asio::cancellation_type_t::total` + * only will be ignored. + * + * In any case, connections can be safely used after cancelling `async_exec` operations. + * + * @par Object lifetimes + * Both `req` and any response object referenced by `adapter` + * should be kept alive until the operation completes. + * No copies of the request object are made. + * + * @param req The request to be executed. + * @param adapter An adapter object referencing a response to place data into. + * @param token Completion token. + */ + template > + auto async_exec(request const& req, any_adapter adapter, CompletionToken&& token = {}) + { + return impl_->async_exec(req, std::move(adapter), std::forward(token)); + } + + /** @brief Cancel operations. + * + * @li `operation::exec`: cancels operations started with + * `async_exec`. Affects only requests that haven't been written + * yet. + * @li `operation::run`: cancels the `async_run` operation. + * @li `operation::receive`: cancels any ongoing calls to `async_receive`. + * @li `operation::all`: cancels all operations listed above. + * + * @param op The operation to be cancelled. + */ + void cancel(operation op = operation::all) { impl_->cancel(op); } + + /// Returns true if the connection will try to reconnect if an error is encountered. + bool will_reconnect() const noexcept { return impl_->will_reconnect(); } + + /// Sets the response object of @ref async_receive2 operations. + template + void set_receive_response(Response& resp) + { + impl_->set_receive_adapter(any_adapter{resp}); + } + + /// Returns connection usage information. + usage get_usage() const noexcept { return impl_->st_.mpx.get_usage(); } + +private: + using clock_type = std::chrono::steady_clock; + using clock_traits_type = asio::wait_traits; + using timer_type = asio::basic_waitable_timer; + + using receive_channel_type = asio::experimental::channel< + executor_type, + void(system::error_code, std::size_t)>; + + auto use_ssl() const noexcept { return impl_->cfg_.use_ssl; } + + // Used by both this class and connection + void set_stderr_logger(logger::level lvl, const config& cfg) + { + impl_->st_.logger.lgr = detail::make_stderr_logger(lvl, cfg.log_prefix); + } + + // Initiation for async_run. This is required because we need access + // to the final handler (rather than the completion token) within the initiation, + // to modify the handler's cancellation slot. + struct run_initiation { + detail::connection_impl* self; + + using executor_type = Executor; + executor_type get_executor() const noexcept { return self->get_executor(); } + + template + void operator()(Handler&& handler, config const* cfg) + { + self->st_.cfg = *cfg; + self->st_.mpx.set_config(*cfg); + + // If the token's slot has cancellation enabled, it should just emit + // the cancellation signal in our connection. This lets us unify the cancel() + // function and per-operation cancellation + auto slot = asio::get_associated_cancellation_slot(handler); + if (slot.is_connected()) { + slot.template emplace>(*self); + } + + // Overwrite the token's cancellation slot: the composed operation + // should use the signal's slot so we can generate cancellations in cancel() + auto token_with_slot = asio::bind_cancellation_slot( + self->run_signal_.slot(), + std::forward(handler)); + + asio::async_compose( + detail::run_op{self}, + token_with_slot, + self->writer_cv_); + } + }; + + friend class connection; + + std::unique_ptr> impl_; +}; + +} // namespace boost::redis + +#endif // BOOST_REDIS_CONNECTION_HPP From 30339dd3bc2747091371312886609502d39df340 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 9 Feb 2026 13:22:24 +0100 Subject: [PATCH 002/115] semaphore --- include/boost/redis/semaphore.hpp | 70 +++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 include/boost/redis/semaphore.hpp diff --git a/include/boost/redis/semaphore.hpp b/include/boost/redis/semaphore.hpp new file mode 100644 index 00000000..65e92cdc --- /dev/null +++ b/include/boost/redis/semaphore.hpp @@ -0,0 +1,70 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_SEMAPHORE_HPP +#define BOOST_REDIS_SEMAPHORE_HPP + +#include +#include +#include +#include + +#include +#include + +namespace boost::redis::detail { + +class semaphore { + std::size_t pending_bytes_{}; + std::size_t max_bytes_; + capy::async_event bytes_available_; + capy::async_event room_available_; + +public: + semaphore(std::size_t max_bytes) noexcept + : max_bytes_(max_bytes) + { + assert(max_bytes != 0u); + } + + /** Waits until at least one byte has been put in the semaphore. */ + capy::io_task<> take() + { + while (pending_bytes_ == 0u) { + // TODO: update this when cancellation is implemented for events + if ((co_await capy::this_coro::stop_token).stop_requested()) + co_return {capy::error::canceled}; + co_await bytes_available_.wait(); + } + pending_bytes_ = 0u; + bytes_available_.clear(); + room_available_.set(); + co_return {}; + } + + capy::io_task<> wait_for_space() + { + while (pending_bytes_ >= max_bytes_) { + // TODO: update this when cancellation is implemented for events + if ((co_await capy::this_coro::stop_token).stop_requested()) + co_return {capy::error::canceled}; + co_await room_available_.wait(); + } + co_return {}; + } + + void put(std::size_t bytes) + { + pending_bytes_ += bytes; + if (pending_bytes_ >= max_bytes_) + room_available_.clear(); + bytes_available_.set(); + } +}; + +} // namespace boost::redis::detail + +#endif // BOOST_REDIS_SEMAPHORE_HPP From 481545427c86772b321194886f1ab84fd49a8886 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 12:38:25 +0100 Subject: [PATCH 003/115] flow_controller --- .../{semaphore.hpp => flow_controller.hpp} | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) rename include/boost/redis/{semaphore.hpp => flow_controller.hpp} (61%) diff --git a/include/boost/redis/semaphore.hpp b/include/boost/redis/flow_controller.hpp similarity index 61% rename from include/boost/redis/semaphore.hpp rename to include/boost/redis/flow_controller.hpp index 65e92cdc..817b9553 100644 --- a/include/boost/redis/semaphore.hpp +++ b/include/boost/redis/flow_controller.hpp @@ -4,8 +4,8 @@ * accompanying file LICENSE.txt) */ -#ifndef BOOST_REDIS_SEMAPHORE_HPP -#define BOOST_REDIS_SEMAPHORE_HPP +#ifndef BOOST_REDIS_FLOW_CONTROLLER_HPP +#define BOOST_REDIS_FLOW_CONTROLLER_HPP #include #include @@ -17,27 +17,26 @@ namespace boost::redis::detail { -class semaphore { +class flow_controller { std::size_t pending_bytes_{}; std::size_t max_bytes_; capy::async_event bytes_available_; capy::async_event room_available_; public: - semaphore(std::size_t max_bytes) noexcept + flow_controller(std::size_t max_bytes) noexcept : max_bytes_(max_bytes) { assert(max_bytes != 0u); } - /** Waits until at least one byte has been put in the semaphore. */ + /** Waits until at least one byte has been put in the flow controller. */ capy::io_task<> take() { while (pending_bytes_ == 0u) { - // TODO: update this when cancellation is implemented for events - if ((co_await capy::this_coro::stop_token).stop_requested()) - co_return {capy::error::canceled}; - co_await bytes_available_.wait(); + auto [ec] = co_await bytes_available_.wait(); + if (ec) + co_return {ec}; } pending_bytes_ = 0u; bytes_available_.clear(); @@ -48,10 +47,9 @@ class semaphore { capy::io_task<> wait_for_space() { while (pending_bytes_ >= max_bytes_) { - // TODO: update this when cancellation is implemented for events - if ((co_await capy::this_coro::stop_token).stop_requested()) - co_return {capy::error::canceled}; - co_await room_available_.wait(); + auto [ec] = co_await bytes_available_.wait(); + if (ec) + co_return {ec}; } co_return {}; } @@ -67,4 +65,4 @@ class semaphore { } // namespace boost::redis::detail -#endif // BOOST_REDIS_SEMAPHORE_HPP +#endif // BOOST_REDIS_FLOW_CONTROLLER_HPP From 23e806c14131e85a0f9a6bc4b8a724e318a862d3 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 12:40:37 +0100 Subject: [PATCH 004/115] exec --- include/boost/redis/corosio_connection.hpp | 83 +++++++++++----------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp index 6543b613..19bc0744 100644 --- a/include/boost/redis/corosio_connection.hpp +++ b/include/boost/redis/corosio_connection.hpp @@ -30,14 +30,18 @@ #include #include +#include #include #include #include #include #include #include +#include #include +#include #include +#include #include #include @@ -77,13 +81,13 @@ class corosio_redis_stream { public: // I/O - capy::io_task async_connect(const connect_params& params, buffered_logger& l); + capy::io_task connect(const connect_params& params, buffered_logger& l); template - capy::io_task async_write_some(const ConstBufferSequence& buffers); + capy::io_task write_some(const ConstBufferSequence& buffers); template - capy::io_task async_read_some(const MutableBufferSequence& buffers); + capy::io_task read_some(const MutableBufferSequence& buffers); }; struct corosio_connection_impl { @@ -180,8 +184,13 @@ struct corosio_connection_impl { case exec_action_type::setup_cancellation: break; // ignored, not required by capy case exec_action_type::immediate: break; // ignored, not required by capy case exec_action_type::notify_writer: writer_event_.set(); break; - case exec_action_type::wait_for_response: co_await request_done.wait(); break; - case exec_action_type::done: co_return {act.error()}; + case exec_action_type::wait_for_response: + { + auto [ec] = co_await request_done.wait(); + ignore_unused(ec); // TODO: we should likely use this + break; + } + case exec_action_type::done: co_return {act.error()}; } } } @@ -232,52 +241,40 @@ struct receive2_op { } }; -template -struct exec_one_op { - connection_impl* conn_; - const request* req_; - exec_one_fsm fsm_; - - explicit exec_one_op(connection_impl& conn, const request& req, any_adapter resp) - : conn_(&conn) - , req_(&req) - , fsm_(std::move(resp), req.get_expected_responses()) - { } +capy::io_task<> async_exec_one(corosio_connection_impl& conn, const request& req, any_adapter resp) +{ + exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; + system::error_code ec; + std::size_t bytes = 0u; - template - void operator()(Self& self, system::error_code ec = {}, std::size_t bytes_written = 0u) - { - exec_one_action act = fsm_.resume( - conn_->st_.mpx.get_read_buffer(), + while (true) { + exec_one_action act = fsm.resume( + conn.st_.mpx.get_read_buffer(), ec, - bytes_written, - self.get_cancellation_state().cancelled()); + bytes, + token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type) { - case exec_one_action_type::done: self.complete(act.ec); return; + case exec_one_action_type::done: co_return {ec}; case exec_one_action_type::write: - asio::async_write(conn_->stream_, asio::buffer(req_->payload()), std::move(self)); - return; + { + auto [write_ec, write_bytes] = co_await capy::write( + conn.stream_, + capy::make_buffer(req.payload())); + ec = write_ec; + bytes = write_bytes; + break; + } case exec_one_action_type::read_some: - conn_->stream_.async_read_some( - conn_->st_.mpx.get_read_buffer().get_prepared(), - std::move(self)); - return; + { + auto [read_ec, read_bytes] = co_await conn.stream_.read_some( + conn.st_.mpx.get_read_buffer().get_prepared()); + ec = read_ec; + bytes = read_bytes; + break; + } } } -}; - -template -auto async_exec_one( - connection_impl& conn, - const request& req, - any_adapter resp, - CompletionToken&& token) -{ - return asio::async_compose( - exec_one_op{conn, req, std::move(resp)}, - token, - conn); } template From c4f979fb793afbbcf85b2061ebc2349395a85062 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 12:52:32 +0100 Subject: [PATCH 005/115] move flow_controller --- include/boost/redis/{ => detail}/flow_controller.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename include/boost/redis/{ => detail}/flow_controller.hpp (94%) diff --git a/include/boost/redis/flow_controller.hpp b/include/boost/redis/detail/flow_controller.hpp similarity index 94% rename from include/boost/redis/flow_controller.hpp rename to include/boost/redis/detail/flow_controller.hpp index 817b9553..9eaa8e7a 100644 --- a/include/boost/redis/flow_controller.hpp +++ b/include/boost/redis/detail/flow_controller.hpp @@ -4,8 +4,8 @@ * accompanying file LICENSE.txt) */ -#ifndef BOOST_REDIS_FLOW_CONTROLLER_HPP -#define BOOST_REDIS_FLOW_CONTROLLER_HPP +#ifndef BOOST_REDIS_DETAIL_FLOW_CONTROLLER_HPP +#define BOOST_REDIS_DETAIL_FLOW_CONTROLLER_HPP #include #include From 6aeb922d2ae3291ac7273144e0f3401cb8cc8376 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 12:52:40 +0100 Subject: [PATCH 006/115] receive2 --- include/boost/redis/corosio_connection.hpp | 134 ++++++++++----------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp index 19bc0744..a14d7a9c 100644 --- a/include/boost/redis/corosio_connection.hpp +++ b/include/boost/redis/corosio_connection.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -81,7 +82,7 @@ class corosio_redis_stream { public: // I/O - capy::io_task connect(const connect_params& params, buffered_logger& l); + capy::io_task<> connect(const connect_params& params, buffered_logger& l); template capy::io_task write_some(const ConstBufferSequence& buffers); @@ -104,6 +105,7 @@ struct corosio_connection_impl { // receive_channel_type receive_channel_; asio::cancellation_signal run_signal_; connection_state st_; + flow_controller controller_; corosio_connection_impl(capy::execution_context& ex, logger&& lgr) : stream_{ex} @@ -201,47 +203,37 @@ struct corosio_connection_impl { } }; -template -struct receive2_op { - connection_impl* conn_; - receive_fsm fsm_{}; - - void drain_receive_channel() - { - // We don't expect any errors here. The only errors - // that might appear in the channel are due to cancellations, - // and these don't make sense with try_receive - auto f = [](system::error_code, std::size_t) { }; - while (conn_->receive_channel_.try_receive(f)) - ; - } +inline capy::io_task<> receive2(corosio_connection_impl& conn) +{ + // Setup + receive_fsm fsm; + system::error_code ec; - template - void operator()(Self& self, system::error_code ec = {}, std::size_t /* push_bytes */ = 0u) - { - receive_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + while (true) { + receive_action act = fsm.resume( + conn.st_, + ec, + token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type) { - case receive_action::action_type::setup_cancellation: - self.reset_cancellation_state(asio::enable_total_cancellation()); - (*this)(self); // this action does not require yielding - return; + case receive_action::action_type::setup_cancellation: break; // not required here case receive_action::action_type::wait: - conn_->receive_channel_.async_receive(std::move(self)); - return; - case receive_action::action_type::drain_channel: - drain_receive_channel(); - (*this)(self); // this action does not require yielding - return; - case receive_action::action_type::immediate: - asio::async_immediate(self.get_io_executor(), std::move(self)); - return; - case receive_action::action_type::done: self.complete(act.ec); return; + { + auto [controller_ec] = co_await conn.controller_.take(); + ec = controller_ec; + break; + } + case receive_action::action_type::drain_channel: break; // not required + case receive_action::action_type::immediate: break; // not required + case receive_action::action_type::done: co_return {act.ec}; } } -}; +} -capy::io_task<> async_exec_one(corosio_connection_impl& conn, const request& req, any_adapter resp) +inline capy::io_task<> async_exec_one( + corosio_connection_impl& conn, + const request& req, + any_adapter resp) { exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; system::error_code ec; @@ -277,41 +269,56 @@ capy::io_task<> async_exec_one(corosio_connection_impl& conn, const request& req } } -template -struct sentinel_resolve_op { - connection_impl* conn_; - sentinel_resolve_fsm fsm_; - - explicit sentinel_resolve_op(connection_impl& conn) - : conn_(&conn) - { } +inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) +{ + // Setup + sentinel_resolve_fsm fsm; + system::error_code ec; - template - void operator()(Self& self, system::error_code ec = {}) - { - auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after - sentinel_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + while (true) { + sentinel_action act = fsm.resume( + conn.st_, + ec, + token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.get_type()) { - case sentinel_action::type::done: self.complete(act.error()); return; + case sentinel_action::type::done: co_return {act.error()}; case sentinel_action::type::connect: - conn->stream_.async_connect( - make_sentinel_connect_params(conn->st_.cfg, act.connect_addr()), - conn->st_.logger, - std::move(self)); - return; + { + auto [connect_ec] = co_await conn.stream_.connect( + make_sentinel_connect_params(conn.st_.cfg, act.connect_addr()), + conn.st_.logger); + ec = connect_ec; + break; + } case sentinel_action::type::request: - async_exec_one( - *conn, - conn->st_.cfg.sentinel.setup, - make_sentinel_adapter(conn->st_), + { + auto [exec_ec] = co_await async_exec_one( + conn, + conn.st_.cfg.sentinel.setup, + make_sentinel_adapter(conn.st_), asio::cancel_after( conn->reconnect_timer_, // should be safe to re-use this conn->st_.cfg.sentinel.request_timeout, std::move(self))); - return; + break; + } } } +} + +template +struct sentinel_resolve_op { + connection_impl* conn_; + sentinel_resolve_fsm fsm_; + + explicit sentinel_resolve_op(connection_impl& conn) + : conn_(&conn) + { } + + template + void operator()(Self& self, system::error_code ec = {}) + { } }; template @@ -740,14 +747,7 @@ class basic_connection { * * @param token Completion token. */ - template > - auto async_receive2(CompletionToken&& token = {}) - { - return asio::async_compose( - detail::receive2_op{impl_.get()}, - token, - *impl_); - } + capy::task<> receive(CompletionToken&& token = {}) { return detail::receive2(*impl_); } /** @brief Executes commands on the Redis server asynchronously. * From f0671faf915e0cef82dc74292e05361c5450e931 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 13:01:57 +0100 Subject: [PATCH 007/115] Sentinel resolve --- include/boost/redis/corosio_connection.hpp | 51 +++++++++------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp index a14d7a9c..7989bafa 100644 --- a/include/boost/redis/corosio_connection.hpp +++ b/include/boost/redis/corosio_connection.hpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +52,7 @@ #include #include #include +#include #include namespace boost::redis { @@ -269,6 +272,18 @@ inline capy::io_task<> async_exec_one( } } +inline capy::io_task<> cancel_after( + capy::io_task<> task, + corosio::timer& timer, + std::chrono::steady_clock::duration timeout) +{ + timer.expires_after(timeout); + auto [winner_index, result] = co_await capy::when_any(std::move(task), timer.wait()); + co_return { + winner_index == 0u ? std::get>(result).ec + : std::error_code(make_error_code(asio::error::operation_aborted))}; +} + inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) { // Setup @@ -293,43 +308,17 @@ inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) } case sentinel_action::type::request: { - auto [exec_ec] = co_await async_exec_one( - conn, - conn.st_.cfg.sentinel.setup, - make_sentinel_adapter(conn.st_), - asio::cancel_after( - conn->reconnect_timer_, // should be safe to re-use this - conn->st_.cfg.sentinel.request_timeout, - std::move(self))); + auto [request_ec] = co_await cancel_after( + async_exec_one(conn, conn.st_.cfg.sentinel.setup, make_sentinel_adapter(conn.st_)), + conn.reconnect_timer_, + conn.st_.cfg.sentinel.request_timeout); + ec = request_ec; break; } } } } -template -struct sentinel_resolve_op { - connection_impl* conn_; - sentinel_resolve_fsm fsm_; - - explicit sentinel_resolve_op(connection_impl& conn) - : conn_(&conn) - { } - - template - void operator()(Self& self, system::error_code ec = {}) - { } -}; - -template -auto async_sentinel_resolve(connection_impl& conn, CompletionToken&& token) -{ - return asio::async_compose( - sentinel_resolve_op{conn}, - token, - conn); -} - template struct writer_op { connection_impl* conn_; From c22bffd528dfa9f9982c9f2ef046e1bc185f0785 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 16:40:08 +0100 Subject: [PATCH 008/115] Finish prototype --- include/boost/redis/corosio_connection.hpp | 474 ++++++--------------- 1 file changed, 138 insertions(+), 336 deletions(-) diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp index 7989bafa..9061ace3 100644 --- a/include/boost/redis/corosio_connection.hpp +++ b/include/boost/redis/corosio_connection.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -99,9 +100,10 @@ struct corosio_connection_impl { // Executor, // void(system::error_code, std::size_t)>; + capy::async_event run_cancelled_event_; corosio_redis_stream stream_; corosio::timer writer_timer_; // timer used for write timeouts - capy::async_event writer_event_; // set when there is new data to write + corosio::timer writer_cv_; // set when there is new data to write corosio::timer reader_timer_; // timer used for read timeouts corosio::timer reconnect_timer_; // to wait the reconnection period corosio::timer ping_timer_; // to wait between pings @@ -113,63 +115,32 @@ struct corosio_connection_impl { corosio_connection_impl(capy::execution_context& ex, logger&& lgr) : stream_{ex} , writer_timer_{ex} - , writer_event_{ex} + , writer_cv_{ex} , reader_timer_{ex} , reconnect_timer_{ex} , ping_timer_{ex} // , receive_channel_{ex, 256} , st_{{std::move(lgr)}} { set_receive_adapter(any_adapter{ignore}); - writer_event_.expires_at((std::chrono::steady_clock::time_point::max)()); + writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); } - void cancel(operation op) + void cancel() { - switch (op) { - case operation::exec: st_.mpx.cancel_waiting(); break; - case operation::receive: cancel_receive_v2(); break; - case operation::reconnection: - st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); - break; - case operation::run: - case operation::resolve: - case operation::connect: - case operation::ssl_handshake: - case operation::health_check: cancel_run(); break; - case operation::all: - st_.mpx.cancel_waiting(); // exec - cancel_receive_v2(); // receive - st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); // reconnect - cancel_run(); // run - break; - default: /* ignore */; - } - } + // exec + st_.mpx.cancel_waiting(); - void cancel_receive_v1(); - - void cancel_receive_v2() - { + // receive (TODO: do we really need this?) st_.receive2_cancelled = true; - cancel_receive_v1(); - } - // void cancel_run() - // { - // // Individual operations should see a terminal cancellation, regardless - // // of what we got requested. We take enough actions to ensure that this - // // doesn't prevent the object from being re-used (e.g. we reset the TLS stream). - // run_signal_.emit(asio::cancellation_type_t::terminal); + // reconnect (TODO: do we really need this?) + st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); - // // Name resolution doesn't support per-operation cancellation - // stream_.cancel_resolve(); - - // // Receive is technically not part of run, but we also cancel it for - // // backwards compatibility. Note that this intentionally affects v1 receive, only. - // cancel_receive_v1(); - // } + // run + run_cancelled_event_.set(); + } - capy::io_task<> async_exec(request const& req, any_adapter adapter) + capy::io_task<> exec(request const& req, any_adapter adapter) { // Setup capy::async_event request_done; @@ -188,7 +159,7 @@ struct corosio_connection_impl { switch (act.type()) { case exec_action_type::setup_cancellation: break; // ignored, not required by capy case exec_action_type::immediate: break; // ignored, not required by capy - case exec_action_type::notify_writer: writer_event_.set(); break; + case exec_action_type::notify_writer: writer_cv_.cancel(); break; case exec_action_type::wait_for_response: { auto [ec] = co_await request_done.wait(); @@ -206,7 +177,7 @@ struct corosio_connection_impl { } }; -inline capy::io_task<> receive2(corosio_connection_impl& conn) +inline capy::io_task<> receive(corosio_connection_impl& conn) { // Setup receive_fsm fsm; @@ -272,16 +243,27 @@ inline capy::io_task<> async_exec_one( } } -inline capy::io_task<> cancel_after( - capy::io_task<> task, +template +inline capy::io_task cancel_at( + capy::io_task task, corosio::timer& timer, - std::chrono::steady_clock::duration timeout) + std::chrono::steady_clock::time_point timeout) { - timer.expires_after(timeout); + timer.expires_at(timeout); auto [winner_index, result] = co_await capy::when_any(std::move(task), timer.wait()); - co_return { - winner_index == 0u ? std::get>(result).ec - : std::error_code(make_error_code(asio::error::operation_aborted))}; + if (winner_index == 0u) + co_return std::move(result); + else + co_return {make_error_code(asio::error::operation_aborted)}; +} + +template +inline capy::io_task cancel_after( + capy::io_task task, + corosio::timer& timer, + std::chrono::steady_clock::duration timeout) +{ + return cancel_at(std::move(task), timer, std::chrono::steady_clock::now() + timeout); } inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) @@ -319,187 +301,112 @@ inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) } } -template -struct writer_op { - connection_impl* conn_; - writer_fsm fsm_; - - explicit writer_op(connection_impl& conn) noexcept - : conn_(&conn) - { } +inline capy::io_task<> writer(corosio_connection_impl& conn) +{ + // Setup + writer_fsm fsm; + system::error_code ec; + std::size_t bytes_written = 0u; - template - void operator()(Self& self, system::error_code ec = {}, std::size_t bytes_written = 0u) - { - auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after - auto act = fsm_.resume( - conn->st_, + while (true) { + writer_action act = fsm.resume( + conn.st_, ec, bytes_written, - self.get_cancellation_state().cancelled()); + token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type()) { - case writer_action_type::done: self.complete(act.error()); return; + case writer_action_type::done: co_return {act.error()}; case writer_action_type::write_some: - conn->stream_.async_write_some( - asio::buffer(conn->st_.mpx.get_write_buffer()), - asio::cancel_at( - conn->writer_timer_, - compute_expiry(act.timeout()), - std::move(self))); - return; + { + auto [write_ec, write_bytes] = co_await cancel_at( + conn.stream_.write_some(capy::make_buffer(conn.st_.mpx.get_write_buffer())), + conn.writer_timer_, + compute_expiry(act.timeout())); + ec = write_ec; + bytes_written = write_bytes; + break; + } case writer_action_type::wait: - conn->writer_cv_.expires_at(compute_expiry(act.timeout())); - conn->writer_cv_.async_wait(std::move(self)); - return; + { + conn.writer_cv_.expires_at(compute_expiry(act.timeout())); + auto [wait_ec] = co_await conn.writer_cv_.wait(); + ec = wait_ec; + bytes_written = 0u; + break; + } } } -}; +} -template -struct reader_op { - connection_impl* conn_; - reader_fsm fsm_; +inline capy::io_task<> reader(corosio_connection_impl& conn) +{ + reader_fsm fsm; + std::size_t n = 0u; + system::error_code ec; -public: - reader_op(connection_impl& conn) noexcept - : conn_{&conn} - { } + for (;;) { + auto act = fsm.resume(conn.st_, n, ec, token_to_cancel(co_await capy::this_coro::stop_token)); - template - void operator()(Self& self, system::error_code ec = {}, std::size_t n = 0) - { - for (;;) { - auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after - auto act = fsm_.resume(conn->st_, n, ec, self.get_cancellation_state().cancelled()); - - switch (act.get_type()) { - case reader_fsm::action::type::read_some: - conn->stream_.async_read_some( - asio::buffer(conn->st_.mpx.get_prepared_read_buffer()), - asio::cancel_at( - conn->reader_timer_, - compute_expiry(act.timeout()), - std::move(self))); - return; - case reader_fsm::action::type::notify_push_receiver: - if (conn->receive_channel_.try_send(ec, act.push_size())) { - continue; - } else { - conn->receive_channel_.async_send(ec, act.push_size(), std::move(self)); - } - return; - case reader_fsm::action::type::done: self.complete(act.error()); return; + switch (act.get_type()) { + case reader_fsm::action::type::read_some: + { + auto [read_ec, read_bytes] = co_await cancel_at( + conn.stream_.read_some(capy::make_buffer(conn.st_.mpx.get_prepared_read_buffer())), + conn.reader_timer_, + compute_expiry(act.timeout())); + ec = read_ec; + n = read_bytes; + break; } + case reader_fsm::action::type::notify_push_receiver: + { + // TODO: re-work this + auto [notify_ec] = co_await conn.controller_.wait_for_space(); + if (notify_ec) + ec = notify_ec; + else + conn.controller_.put(act.push_size()); + } + case reader_fsm::action::type::done: co_return {act.error()}; } } -}; - -template -class run_op { -private: - connection_impl* conn_; - run_fsm fsm_{}; - - template - auto reader(CompletionToken&& token) - { - return asio::async_compose( - reader_op{*conn_}, - std::forward(token), - conn_->writer_cv_); - } - - template - auto writer(CompletionToken&& token) - { - return asio::async_compose( - writer_op{*conn_}, - std::forward(token), - conn_->writer_cv_); - } - -public: - run_op(connection_impl* conn) noexcept - : conn_{conn} - { } +} - // Called after the parallel group finishes - template - void operator()( - Self& self, - std::array order, - system::error_code reader_ec, - system::error_code writer_ec) - { - (*this)(self, order[0u] == 0u ? reader_ec : writer_ec); - } +inline capy::io_task<> run(corosio_connection_impl& conn) +{ + run_fsm fsm; + system::error_code ec; - template - void operator()(Self& self, system::error_code ec = {}) - { - auto act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + while (true) { + auto act = fsm.resume(conn.st_, ec, token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type) { - case run_action_type::done: self.complete(act.ec); return; - case run_action_type::immediate: - asio::async_immediate(self.get_io_executor(), std::move(self)); - return; - case run_action_type::sentinel_resolve: - async_sentinel_resolve(*conn_, std::move(self)); - return; + case run_action_type::done: co_return {act.ec}; + case run_action_type::immediate: break; // no longer required + case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve(conn)).ec; break; case run_action_type::connect: - conn_->stream_.async_connect( - make_run_connect_params(conn_->st_), - conn_->st_.logger, - std::move(self)); - return; + ec = (co_await conn.stream_.connect(make_run_connect_params(conn.st_), conn.st_.logger)) + .ec; + break; case run_action_type::parallel_group: - asio::experimental::make_parallel_group( - [this](auto token) { - return this->reader(token); - }, - [this](auto token) { - return this->writer(token); - }) - .async_wait(asio::experimental::wait_for_one(), std::move(self)); - return; - case run_action_type::cancel_receive: - conn_->receive_channel_.cancel(); - (*this)(self); // this action does not require suspending - return; + { + auto [winner_index, result] = co_await capy::when_any(reader(conn), writer(conn)); + ignore_unused(winner_index); + ec = std::get<0>(result).ec; + break; + } + case run_action_type::cancel_receive: break; // no longer required case run_action_type::wait_for_reconnection: - conn_->reconnect_timer_.expires_after(conn_->st_.cfg.reconnect_wait_interval); - conn_->reconnect_timer_.async_wait(std::move(self)); - return; - default: BOOST_ASSERT(false); + conn.reconnect_timer_.expires_after(conn.st_.cfg.reconnect_wait_interval); + ec = (co_await conn.reconnect_timer_.wait()).ec; + break; } } -}; +} logger make_stderr_logger(logger::level lvl, std::string prefix); -template -class run_cancel_handler { - connection_impl* conn_; - -public: - explicit run_cancel_handler(connection_impl& conn) noexcept - : conn_(&conn) - { } - - void operator()(asio::cancellation_type_t cancel_type) const - { - // We support terminal and partial cancellation - constexpr auto mask = asio::cancellation_type_t::terminal | - asio::cancellation_type_t::partial; - - if ((cancel_type & mask) != asio::cancellation_type_t::none) { - conn_->cancel(operation::run); - } - } -}; - } // namespace detail /** @brief A SSL connection to the Redis server. @@ -510,25 +417,8 @@ class run_cancel_handler { * * @tparam Executor The executor type used to create any required I/O objects. */ -template -class basic_connection { +class connection { public: - using this_type = basic_connection; - - /// (Deprecated) Type of the next layer - BOOST_DEPRECATED("This typedef is deprecated, and will be removed with next_layer().") - typedef asio::ssl::stream> next_layer_type; - - /// The type of the executor associated to this object. - using executor_type = Executor; - - /// Rebinds the socket type to another executor. - template - struct rebind_executor { - /// The connection type when rebound to the specified executor. - using other = basic_connection; - }; - /** @brief Constructor from an executor. * * @param ex Executor used to create all internal I/O objects. @@ -537,8 +427,8 @@ class basic_connection { * and customize logging. By default, `logger::level::info` messages * and higher are logged to `stderr`. */ - explicit basic_connection( - executor_type ex, + explicit connection( + capy::execution_context& ex, asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, logger lgr = {}) : impl_( @@ -557,47 +447,13 @@ class basic_connection { * * An SSL context with default settings will be created. */ - basic_connection(executor_type ex, logger lgr) - : basic_connection( + connection(capy::execution_context& ex, logger lgr) + : connection( std::move(ex), asio::ssl::context{asio::ssl::context::tlsv12_client}, std::move(lgr)) { } - /** - * @brief Constructor from an `io_context`. - * - * @param ioc I/O context used to create all internal I/O objects. - * @param ctx SSL context. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - */ - explicit basic_connection( - asio::io_context& ioc, - asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, - logger lgr = {}) - : basic_connection(ioc.get_executor(), std::move(ctx), std::move(lgr)) - { } - - /** - * @brief Constructor from an `io_context` and a logger. - * - * @param ioc I/O context used to create all internal I/O objects. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - */ - basic_connection(asio::io_context& ioc, logger lgr) - : basic_connection( - ioc.get_executor(), - asio::ssl::context{asio::ssl::context::tlsv12_client}, - std::move(lgr)) - { } - - /// Returns the associated executor. - executor_type get_executor() noexcept { return impl_->writer_cv_.get_executor(); } - /** @brief Starts the underlying connection operations. * * This function establishes a connection to the Redis server and keeps @@ -651,13 +507,17 @@ class basic_connection { * @param cfg Configuration parameters. * @param token Completion token. */ - template > - auto async_run(config const& cfg, CompletionToken&& token = {}) + capy::io_task<> run(config const& cfg) { - return asio::async_initiate( - run_initiation{impl_.get()}, - token, - &cfg); + impl_->st_.cfg = cfg; + impl_->st_.mpx.set_config(cfg); + impl_->run_cancelled_event_.clear(); + + auto [winner_index, result] = co_await capy::when_any( + detail::run(*impl_), + impl_->run_cancelled_event_.wait()); + + co_return {winner_index == 0u ? std::get<0>(result).ec : capy::error::canceled}; } /** @brief Wait for server pushes asynchronously. @@ -736,7 +596,7 @@ class basic_connection { * * @param token Completion token. */ - capy::task<> receive(CompletionToken&& token = {}) { return detail::receive2(*impl_); } + capy::io_task<> receive() { return detail::receive(*impl_); } /** @brief Executes commands on the Redis server asynchronously. * @@ -783,12 +643,10 @@ class basic_connection { * @param resp The response object to parse data into. * @param token Completion token. */ - template < - class Response = ignore_t, - class CompletionToken = asio::default_completion_token_t> - auto async_exec(request const& req, Response& resp = ignore, CompletionToken&& token = {}) + template + capy::io_task<> exec(request const& req, Response& resp = ignore) { - return this->async_exec(req, any_adapter{resp}, std::forward(token)); + return exec(req, any_adapter{resp}); } /** @brief Executes commands on the Redis server asynchronously. @@ -837,10 +695,9 @@ class basic_connection { * @param adapter An adapter object referencing a response to place data into. * @param token Completion token. */ - template > - auto async_exec(request const& req, any_adapter adapter, CompletionToken&& token = {}) + capy::io_task<> exec(request const& req, any_adapter adapter) { - return impl_->async_exec(req, std::move(adapter), std::forward(token)); + return impl_->exec(req, std::move(adapter)); } /** @brief Cancel operations. @@ -854,77 +711,22 @@ class basic_connection { * * @param op The operation to be cancelled. */ - void cancel(operation op = operation::all) { impl_->cancel(op); } - - /// Returns true if the connection will try to reconnect if an error is encountered. - bool will_reconnect() const noexcept { return impl_->will_reconnect(); } + void cancel() { impl_->cancel(); } /// Sets the response object of @ref async_receive2 operations. - template - void set_receive_response(Response& resp) - { - impl_->set_receive_adapter(any_adapter{resp}); - } + void set_receive_response(any_adapter resp) { impl_->set_receive_adapter(std::move(resp)); } /// Returns connection usage information. usage get_usage() const noexcept { return impl_->st_.mpx.get_usage(); } private: - using clock_type = std::chrono::steady_clock; - using clock_traits_type = asio::wait_traits; - using timer_type = asio::basic_waitable_timer; - - using receive_channel_type = asio::experimental::channel< - executor_type, - void(system::error_code, std::size_t)>; - - auto use_ssl() const noexcept { return impl_->cfg_.use_ssl; } - // Used by both this class and connection void set_stderr_logger(logger::level lvl, const config& cfg) { impl_->st_.logger.lgr = detail::make_stderr_logger(lvl, cfg.log_prefix); } - // Initiation for async_run. This is required because we need access - // to the final handler (rather than the completion token) within the initiation, - // to modify the handler's cancellation slot. - struct run_initiation { - detail::connection_impl* self; - - using executor_type = Executor; - executor_type get_executor() const noexcept { return self->get_executor(); } - - template - void operator()(Handler&& handler, config const* cfg) - { - self->st_.cfg = *cfg; - self->st_.mpx.set_config(*cfg); - - // If the token's slot has cancellation enabled, it should just emit - // the cancellation signal in our connection. This lets us unify the cancel() - // function and per-operation cancellation - auto slot = asio::get_associated_cancellation_slot(handler); - if (slot.is_connected()) { - slot.template emplace>(*self); - } - - // Overwrite the token's cancellation slot: the composed operation - // should use the signal's slot so we can generate cancellations in cancel() - auto token_with_slot = asio::bind_cancellation_slot( - self->run_signal_.slot(), - std::forward(handler)); - - asio::async_compose( - detail::run_op{self}, - token_with_slot, - self->writer_cv_); - } - }; - - friend class connection; - - std::unique_ptr> impl_; + std::unique_ptr impl_; }; } // namespace boost::redis From 6f91ae42ea268105789c9d560c2f0a522d6cb30b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 16:49:27 +0100 Subject: [PATCH 009/115] example --- example/cpp20_intro.cpp | 42 ++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/example/cpp20_intro.cpp b/example/cpp20_intro.cpp index 9c1637a1..ec2a6aa9 100644 --- a/example/cpp20_intro.cpp +++ b/example/cpp20_intro.cpp @@ -4,28 +4,20 @@ * accompanying file LICENSE.txt) */ -#include +#include +#include -#include -#include -#include +#include +#include +#include #include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) +namespace capy = boost::capy; +using namespace boost::redis; -namespace asio = boost::asio; -using boost::redis::request; -using boost::redis::response; -using boost::redis::config; -using boost::redis::connection; - -// Called from the main function (see main.cpp) -auto co_main(config cfg) -> asio::awaitable +capy::task run_request(connection& conn) { - auto conn = std::make_shared(co_await asio::this_coro::executor); - conn->async_run(cfg, asio::consign(asio::detached, conn)); - // A request containing only a ping command. request req; req.push("PING", "Hello world"); @@ -34,10 +26,18 @@ auto co_main(config cfg) -> asio::awaitable response resp; // Executes the request. - co_await conn->async_exec(req, resp); - conn->cancel(); - - std::cout << "PING: " << std::get<0>(resp).value() << std::endl; + auto [ec] = co_await conn.exec(req, resp); + if (ec) + co_return; + std::cout << "PING value: " << std::get<0>(resp).value() << std::endl; } -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) +capy::task co_main() +{ + // Create a connection + connection conn{(co_await capy::this_coro::executor).context()}; + + auto r = co_await capy::when_any(run_request(conn), conn.run(config{})); + + static_cast(r); +} From 0953d3a1e5f1d7068728f7c17049c2b3e3a94e15 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 20:21:16 +0100 Subject: [PATCH 010/115] implement connect --- include/boost/redis/corosio_connection.hpp | 132 ++++++++++++++++----- 1 file changed, 100 insertions(+), 32 deletions(-) diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp index 9061ace3..5a1680f4 100644 --- a/include/boost/redis/corosio_connection.hpp +++ b/include/boost/redis/corosio_connection.hpp @@ -44,7 +44,11 @@ #include #include #include +#include +#include +#include #include +#include #include #include @@ -73,20 +77,107 @@ inline asio::cancellation_type_t token_to_cancel(std::stop_token tok) : asio::cancellation_type_t::none; } -// TODO: actually implement +template +inline capy::io_task cancel_at( + capy::io_task task, + corosio::timer& timer, + std::chrono::steady_clock::time_point timeout) +{ + timer.expires_at(timeout); + auto [winner_index, result] = co_await capy::when_any(std::move(task), timer.wait()); + if (winner_index == 0u) + co_return std::move(result); + else + co_return {make_error_code(asio::error::operation_aborted)}; +} + +template +inline capy::io_task cancel_after( + capy::io_task task, + corosio::timer& timer, + std::chrono::steady_clock::duration timeout) +{ + return cancel_at(std::move(task), timer, std::chrono::steady_clock::now() + timeout); +} + class corosio_redis_stream { - // asio::ssl::context ssl_ctx_; - // asio::ip::basic_resolver resolv_; - // asio::ssl::stream> stream_; - // #ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS - // asio::basic_stream_socket unix_socket_; - // #endif - // typename asio::steady_timer::template rebind_executor::other timer_; + // TODO: UNIX sockets + corosio::tcp_socket socket_; + std::unique_ptr stream_; + corosio::timer timer_; + corosio::resolver resolv_; redis_stream_state st_; public: + explicit corosio_redis_stream(capy::execution_context& ctx) + : socket_(ctx) + , stream_(nullptr) // TODO + , timer_(ctx) + , resolv_(ctx) + { } + // I/O - capy::io_task<> connect(const connect_params& params, buffered_logger& l); + capy::io_task<> connect(const connect_params& params, buffered_logger& l) + { + connect_fsm fsm{l}; + system::error_code ec; + corosio::resolver_results endpoints; + + auto act = fsm.resume(ec, st_, asio::cancellation_type_t::none); + + while (true) { + switch (act.type) { + case connect_action_type::unix_socket_close: + BOOST_ASSERT(false); + co_return {system::error_code(asio::error::operation_not_supported)}; + case connect_action_type::unix_socket_connect: + BOOST_ASSERT(false); + co_return {system::error_code(asio::error::operation_not_supported)}; + case connect_action_type::tcp_resolve: + { + auto result = co_await cancel_after( + [&] -> capy::io_task { + co_return co_await resolv_.resolve( + params.addr.tcp_address().host, + params.addr.tcp_address().port); + }(), + timer_, + params.resolve_timeout); + ec = result.ec; + endpoints = std::move(result.t1); + act = fsm.resume(ec, endpoints, st_, asio::cancellation_type_t::none); + break; + } + case connect_action_type::ssl_stream_reset: + stream_->reset(); + act = fsm.resume(ec, st_, asio::cancellation_type_t::none); + break; + case connect_action_type::ssl_handshake: + ec = (co_await cancel_after( + stream_->handshake(corosio::tls_stream::handshake_type::client), + timer_, + params.ssl_handshake_timeout)) + .ec; + act = fsm.resume(ec, st_, asio::cancellation_type_t::none); + break; + case connect_action_type::done: co_return {act.ec}; + case connect_action_type::tcp_connect: + { + // TODO: range connect + auto result = co_await cancel_after( + [&] -> capy::io_task<> { + co_return co_await socket_.connect(*endpoints.begin()); + }(), + timer_, + params.connect_timeout); + ec = result.ec; + act = fsm.resume(ec, *endpoints.begin(), st_, asio::cancellation_type_t::none); + break; + } + default: BOOST_ASSERT(false); + } + } + } template capy::io_task write_some(const ConstBufferSequence& buffers); @@ -243,29 +334,6 @@ inline capy::io_task<> async_exec_one( } } -template -inline capy::io_task cancel_at( - capy::io_task task, - corosio::timer& timer, - std::chrono::steady_clock::time_point timeout) -{ - timer.expires_at(timeout); - auto [winner_index, result] = co_await capy::when_any(std::move(task), timer.wait()); - if (winner_index == 0u) - co_return std::move(result); - else - co_return {make_error_code(asio::error::operation_aborted)}; -} - -template -inline capy::io_task cancel_after( - capy::io_task task, - corosio::timer& timer, - std::chrono::steady_clock::duration timeout) -{ - return cancel_at(std::move(task), timer, std::chrono::steady_clock::now() + timeout); -} - inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) { // Setup From c1cc27ecc19e2c92e8c3cc0c47fd9a609a884899 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 20:24:27 +0100 Subject: [PATCH 011/115] read and write --- include/boost/redis/corosio_connection.hpp | 25 ++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp index 5a1680f4..18b8157e 100644 --- a/include/boost/redis/corosio_connection.hpp +++ b/include/boost/redis/corosio_connection.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -179,11 +180,27 @@ class corosio_redis_stream { } } - template - capy::io_task write_some(const ConstBufferSequence& buffers); + template + capy::io_task write_some(const BuffType& buffers) + { + switch (st_.type) { + case transport_type::tcp: co_return co_await socket_.write_some(buffers); + case transport_type::tcp_tls: co_return co_await stream_->write_some(buffers); + case transport_type::unix_socket: + default: BOOST_ASSERT(false); co_return {}; + } + } - template - capy::io_task read_some(const MutableBufferSequence& buffers); + template + capy::io_task read_some(const BuffType& buffers) + { + switch (st_.type) { + case transport_type::tcp: co_return co_await socket_.read_some(buffers); + case transport_type::tcp_tls: co_return co_await stream_->read_some(buffers); + case transport_type::unix_socket: + default: BOOST_ASSERT(false); co_return {}; + } + } }; struct corosio_connection_impl { From b623d14310f79b05d903f23ba2b18ba602b184c0 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 20:38:19 +0100 Subject: [PATCH 012/115] connect_fsm to corosio --- include/boost/redis/corosio_connection.hpp | 10 +-- include/boost/redis/detail/connect_fsm.hpp | 20 +++-- include/boost/redis/impl/connect_fsm.ipp | 85 ++++++---------------- 3 files changed, 37 insertions(+), 78 deletions(-) diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp index 18b8157e..29cb5771 100644 --- a/include/boost/redis/corosio_connection.hpp +++ b/include/boost/redis/corosio_connection.hpp @@ -124,7 +124,7 @@ class corosio_redis_stream { system::error_code ec; corosio::resolver_results endpoints; - auto act = fsm.resume(ec, st_, asio::cancellation_type_t::none); + auto act = fsm.resume(ec, st_); while (true) { switch (act.type) { @@ -146,12 +146,12 @@ class corosio_redis_stream { params.resolve_timeout); ec = result.ec; endpoints = std::move(result.t1); - act = fsm.resume(ec, endpoints, st_, asio::cancellation_type_t::none); + act = fsm.resume(ec, endpoints, st_); break; } case connect_action_type::ssl_stream_reset: stream_->reset(); - act = fsm.resume(ec, st_, asio::cancellation_type_t::none); + act = fsm.resume(ec, st_); break; case connect_action_type::ssl_handshake: ec = (co_await cancel_after( @@ -159,7 +159,7 @@ class corosio_redis_stream { timer_, params.ssl_handshake_timeout)) .ec; - act = fsm.resume(ec, st_, asio::cancellation_type_t::none); + act = fsm.resume(ec, st_); break; case connect_action_type::done: co_return {act.ec}; case connect_action_type::tcp_connect: @@ -172,7 +172,7 @@ class corosio_redis_stream { timer_, params.connect_timeout); ec = result.ec; - act = fsm.resume(ec, *endpoints.begin(), st_, asio::cancellation_type_t::none); + act = fsm.resume(ec, *endpoints.begin(), st_); break; } default: BOOST_ASSERT(false); diff --git a/include/boost/redis/detail/connect_fsm.hpp b/include/boost/redis/detail/connect_fsm.hpp index 35235566..a4139b9b 100644 --- a/include/boost/redis/detail/connect_fsm.hpp +++ b/include/boost/redis/detail/connect_fsm.hpp @@ -9,10 +9,13 @@ #ifndef BOOST_REDIS_CONNECT_FSM_HPP #define BOOST_REDIS_CONNECT_FSM_HPP -#include #include +#include +#include #include +#include + // Sans-io algorithm for redis_stream::async_connect, as a finite state machine namespace boost::redis::detail { @@ -69,20 +72,15 @@ class connect_fsm { connect_action resume( system::error_code ec, - const asio::ip::tcp::resolver::results_type& resolver_results, - redis_stream_state& st, - asio::cancellation_type_t cancel_state); + std::span resolver_results, + redis_stream_state& st); connect_action resume( system::error_code ec, - const asio::ip::tcp::endpoint& selected_endpoint, - redis_stream_state& st, - asio::cancellation_type_t cancel_state); + const corosio::endpoint& selected_endpoint, + redis_stream_state& st); - connect_action resume( - system::error_code ec, - redis_stream_state& st, - asio::cancellation_type_t cancel_state); + connect_action resume(system::error_code ec, redis_stream_state& st); }; // namespace boost::redis::detail diff --git a/include/boost/redis/impl/connect_fsm.ipp b/include/boost/redis/impl/connect_fsm.ipp index f2699dda..2ef7ccd4 100644 --- a/include/boost/redis/impl/connect_fsm.ipp +++ b/include/boost/redis/impl/connect_fsm.ipp @@ -9,86 +9,63 @@ #include #include #include -#include #include -#include -#include #include #include +#include +#include #include namespace boost::redis::detail { // Logging -inline void format_tcp_endpoint(const asio::ip::tcp::endpoint& ep, std::string& to) +inline void format_tcp_endpoint(const corosio::endpoint& ep, std::string& to) { // This formatting is inspired by Asio's endpoint operator<< - const auto& addr = ep.address(); - if (addr.is_v6()) + if (ep.is_v6()) { to += '['; - to += addr.to_string(); - if (addr.is_v6()) + to += ep.v6_address().to_string(); to += ']'; + } else { + to += ep.v4_address().to_string(); + } to += ':'; to += std::to_string(ep.port()); } template <> -struct log_traits { - static inline void log(std::string& to, const asio::ip::tcp::endpoint& value) +struct log_traits { + static inline void log(std::string& to, const corosio::endpoint& value) { format_tcp_endpoint(value, to); } }; template <> -struct log_traits { - static inline void log(std::string& to, const asio::ip::tcp::resolver::results_type& value) +struct log_traits> { + static inline void log(std::string& to, std::span value) { auto iter = value.cbegin(); auto end = value.cend(); if (iter != end) { - format_tcp_endpoint(iter->endpoint(), to); + format_tcp_endpoint(iter->get_endpoint(), to); ++iter; for (; iter != end; ++iter) { to += ", "; - format_tcp_endpoint(iter->endpoint(), to); + format_tcp_endpoint(iter->get_endpoint(), to); } } } }; -inline system::error_code translate_timeout_error( - system::error_code io_ec, - asio::cancellation_type_t cancel_state, - error code_if_cancelled) -{ - // Translates cancellations and timeout errors into a single error_code. - // - Cancellation state set, and an I/O error: the entire operation was cancelled. - // The I/O code (probably operation_aborted) is appropriate. - // - Cancellation state set, and no I/O error: same as above, but the cancellation - // arrived after the operation completed and before the handler was called. Set the code here. - // - No cancellation state set, I/O error set to operation_aborted: since we use cancel_after, - // this means a timeout. - // - Otherwise, respect the I/O error. - if ((cancel_state & asio::cancellation_type_t::terminal) != asio::cancellation_type_t::none) { - return io_ec ? io_ec : asio::error::operation_aborted; - } - return io_ec == asio::error::operation_aborted ? code_if_cancelled : io_ec; -} - connect_action connect_fsm::resume( system::error_code ec, - const asio::ip::tcp::resolver::results_type& resolver_results, - redis_stream_state& st, - asio::cancellation_type_t cancel_state) + std::span resolver_results, + redis_stream_state& st) { - // Translate error codes - ec = translate_timeout_error(ec, cancel_state, error::resolve_timeout); - // Log it if (ec) { log_info(*lgr_, "Connect: hostname resolution failed: ", ec); @@ -97,18 +74,14 @@ connect_action connect_fsm::resume( } // Delegate to the regular resume function - return resume(ec, st, cancel_state); + return resume(ec, st); } connect_action connect_fsm::resume( system::error_code ec, - const asio::ip::tcp::endpoint& selected_endpoint, - redis_stream_state& st, - asio::cancellation_type_t cancel_state) + const corosio::endpoint& selected_endpoint, + redis_stream_state& st) { - // Translate error codes - ec = translate_timeout_error(ec, cancel_state, error::connect_timeout); - // Log it if (ec) { log_info(*lgr_, "Connect: TCP connect failed: ", ec); @@ -117,13 +90,10 @@ connect_action connect_fsm::resume( } // Delegate to the regular resume function - return resume(ec, st, cancel_state); + return resume(ec, st); } -connect_action connect_fsm::resume( - system::error_code ec, - redis_stream_state& st, - asio::cancellation_type_t cancel_state) +connect_action connect_fsm::resume(system::error_code ec, redis_stream_state& st) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -135,12 +105,6 @@ connect_action connect_fsm::resume( // Connect to the socket BOOST_REDIS_YIELD(resume_point_, 2, connect_action_type::unix_socket_connect) - // Fix error codes. If we were cancelled and the code is operation_aborted, - // it is because per-operation cancellation was activated. If we were not cancelled - // but the operation failed with operation_aborted, it's a timeout. - // Also check for cancellations that didn't cause a failure - ec = translate_timeout_error(ec, cancel_state, error::connect_timeout); - // Log it if (ec) { log_info(*lgr_, "Connect: UNIX socket connect failed: ", ec); @@ -169,7 +133,7 @@ connect_action connect_fsm::resume( // endpoints, and is a specialized resume() that will call this function BOOST_REDIS_YIELD(resume_point_, 4, connect_action_type::tcp_resolve) - // If this failed, we can't continue (error code translation already performed here) + // If this failed, we can't continue if (ec) { return ec; } @@ -178,7 +142,7 @@ connect_action connect_fsm::resume( // This has a specialized resume(), too BOOST_REDIS_YIELD(resume_point_, 5, connect_action_type::tcp_connect) - // If this failed, we can't continue (error code translation already performed here) + // If this failed, we can't continue if (ec) { return ec; } @@ -190,9 +154,6 @@ connect_action connect_fsm::resume( // Perform the TLS handshake BOOST_REDIS_YIELD(resume_point_, 6, connect_action_type::ssl_handshake) - // Translate error codes - ec = translate_timeout_error(ec, cancel_state, error::ssl_handshake_timeout); - // Log it if (ec) { log_info(*lgr_, "Connect: SSL handshake failed: ", ec); From 785216e05226854afb15acaf2dd16076ca3b5fd4 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 10 Feb 2026 21:04:16 +0100 Subject: [PATCH 013/115] final touches --- include/boost/redis/corosio_connection.hpp | 62 ++++++++++------------ include/boost/redis/detail/read_buffer.hpp | 3 +- include/boost/redis/impl/read_buffer.ipp | 5 +- include/boost/redis/src.hpp | 2 +- 4 files changed, 33 insertions(+), 39 deletions(-) diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp index 29cb5771..78a6948e 100644 --- a/include/boost/redis/corosio_connection.hpp +++ b/include/boost/redis/corosio_connection.hpp @@ -45,10 +45,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -87,7 +89,7 @@ inline capy::io_task cancel_at( timer.expires_at(timeout); auto [winner_index, result] = co_await capy::when_any(std::move(task), timer.wait()); if (winner_index == 0u) - co_return std::move(result); + co_return std::get<0>(std::move(result)); else co_return {make_error_code(asio::error::operation_aborted)}; } @@ -104,15 +106,15 @@ inline capy::io_task cancel_after( class corosio_redis_stream { // TODO: UNIX sockets corosio::tcp_socket socket_; - std::unique_ptr stream_; + corosio::openssl_stream stream_; // TODO: make this configurable corosio::timer timer_; corosio::resolver resolv_; redis_stream_state st_; public: - explicit corosio_redis_stream(capy::execution_context& ctx) + explicit corosio_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) : socket_(ctx) - , stream_(nullptr) // TODO + , stream_(&socket_, std::move(tls_ctx)) , timer_(ctx) , resolv_(ctx) { } @@ -150,12 +152,12 @@ class corosio_redis_stream { break; } case connect_action_type::ssl_stream_reset: - stream_->reset(); + stream_.reset(); act = fsm.resume(ec, st_); break; case connect_action_type::ssl_handshake: ec = (co_await cancel_after( - stream_->handshake(corosio::tls_stream::handshake_type::client), + stream_.handshake(corosio::tls_stream::handshake_type::client), timer_, params.ssl_handshake_timeout)) .ec; @@ -185,7 +187,7 @@ class corosio_redis_stream { { switch (st_.type) { case transport_type::tcp: co_return co_await socket_.write_some(buffers); - case transport_type::tcp_tls: co_return co_await stream_->write_some(buffers); + case transport_type::tcp_tls: co_return co_await stream_.write_some(buffers); case transport_type::unix_socket: default: BOOST_ASSERT(false); co_return {}; } @@ -196,7 +198,7 @@ class corosio_redis_stream { { switch (st_.type) { case transport_type::tcp: co_return co_await socket_.read_some(buffers); - case transport_type::tcp_tls: co_return co_await stream_->read_some(buffers); + case transport_type::tcp_tls: co_return co_await stream_.read_some(buffers); case transport_type::unix_socket: default: BOOST_ASSERT(false); co_return {}; } @@ -204,10 +206,6 @@ class corosio_redis_stream { }; struct corosio_connection_impl { - // using receive_channel_type = asio::experimental::channel< - // Executor, - // void(system::error_code, std::size_t)>; - capy::async_event run_cancelled_event_; corosio_redis_stream stream_; corosio::timer writer_timer_; // timer used for write timeouts @@ -215,18 +213,20 @@ struct corosio_connection_impl { corosio::timer reader_timer_; // timer used for read timeouts corosio::timer reconnect_timer_; // to wait the reconnection period corosio::timer ping_timer_; // to wait between pings - // receive_channel_type receive_channel_; - asio::cancellation_signal run_signal_; - connection_state st_; flow_controller controller_; + connection_state st_; - corosio_connection_impl(capy::execution_context& ex, logger&& lgr) - : stream_{ex} - , writer_timer_{ex} - , writer_cv_{ex} - , reader_timer_{ex} - , reconnect_timer_{ex} - , ping_timer_{ex} // , receive_channel_{ex, 256} + corosio_connection_impl( + capy::execution_context& ctx, + corosio::tls_context&& ssl_ctx, + logger&& lgr) + : stream_{ctx, std::move(ssl_ctx)} + , writer_timer_{ctx} + , writer_cv_{ctx} + , reader_timer_{ctx} + , reconnect_timer_{ctx} + , ping_timer_{ctx} + , controller_{1024u * 1024u * 16u} // 16MB, TODO: make it configurable , st_{{std::move(lgr)}} { set_receive_adapter(any_adapter{ignore}); @@ -342,7 +342,7 @@ inline capy::io_task<> async_exec_one( case exec_one_action_type::read_some: { auto [read_ec, read_bytes] = co_await conn.stream_.read_some( - conn.st_.mpx.get_read_buffer().get_prepared()); + capy::make_buffer(conn.st_.mpx.get_read_buffer().get_prepared())); ec = read_ec; bytes = read_bytes; break; @@ -513,14 +513,11 @@ class connection { * and higher are logged to `stderr`. */ explicit connection( - capy::execution_context& ex, - asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, + capy::execution_context& ctx, + corosio::tls_context ssl_ctx = {}, logger lgr = {}) : impl_( - std::make_unique>( - std::move(ex), - std::move(ctx), - std::move(lgr))) + std::make_unique(ctx, std::move(ssl_ctx), std::move(lgr))) { } /** @brief Constructor from an executor and a logger. @@ -532,11 +529,8 @@ class connection { * * An SSL context with default settings will be created. */ - connection(capy::execution_context& ex, logger lgr) - : connection( - std::move(ex), - asio::ssl::context{asio::ssl::context::tlsv12_client}, - std::move(lgr)) + connection(capy::execution_context& ctx, logger lgr) + : connection(ctx, {}, std::move(lgr)) { } /** @brief Starts the underlying connection operations. diff --git a/include/boost/redis/detail/read_buffer.hpp b/include/boost/redis/detail/read_buffer.hpp index 965845ec..d54eb511 100644 --- a/include/boost/redis/detail/read_buffer.hpp +++ b/include/boost/redis/detail/read_buffer.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -19,7 +20,7 @@ namespace boost::redis::detail { class read_buffer { public: - using span_type = span; + using span_type = std::span; struct consume_result { std::size_t consumed; diff --git a/include/boost/redis/impl/read_buffer.ipp b/include/boost/redis/impl/read_buffer.ipp index b3705549..78f63b2a 100644 --- a/include/boost/redis/impl/read_buffer.ipp +++ b/include/boost/redis/impl/read_buffer.ipp @@ -37,7 +37,7 @@ void read_buffer::commit(std::size_t read_size) auto read_buffer::get_prepared() noexcept -> span_type { auto const size = buffer_.size(); - return make_span(buffer_.data() + append_buf_begin_, size - append_buf_begin_); + return span_type(buffer_.data() + append_buf_begin_, size - append_buf_begin_); } auto read_buffer::get_commited() const noexcept -> std::string_view @@ -51,8 +51,7 @@ void read_buffer::clear() append_buf_begin_ = 0; } -read_buffer::consume_result -read_buffer::consume(std::size_t size) +read_buffer::consume_result read_buffer::consume(std::size_t size) { // For convenience, if the requested size is larger than the // committed buffer we cap it to the maximum. diff --git a/include/boost/redis/src.hpp b/include/boost/redis/src.hpp index 196b2960..de5b7c6a 100644 --- a/include/boost/redis/src.hpp +++ b/include/boost/redis/src.hpp @@ -5,7 +5,7 @@ */ #include -#include +// #include #include #include #include From df473bd7db10ef5bc2c338d4452b269368184499 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 18:30:45 +0100 Subject: [PATCH 014/115] rework main --- example/cpp20_intro.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/example/cpp20_intro.cpp b/example/cpp20_intro.cpp index ec2a6aa9..6e1ea4f6 100644 --- a/example/cpp20_intro.cpp +++ b/example/cpp20_intro.cpp @@ -7,14 +7,18 @@ #include #include +#include #include #include #include +#include +#include #include namespace capy = boost::capy; using namespace boost::redis; +namespace corosio = boost::corosio; capy::task run_request(connection& conn) { @@ -41,3 +45,19 @@ capy::task co_main() static_cast(r); } + +struct handler { + void operator()() { std::cout << "Done\n"; } + void operator()(std::exception_ptr exc) + { + if (exc) + std::rethrow_exception(exc); + } +}; + +int main() +{ + corosio::io_context ctx; + capy::run_async(ctx.get_executor())(co_main()); + ctx.run(); +} From 2417d5c6b1617648feb9dd892062584a73eaf60b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 18:56:34 +0100 Subject: [PATCH 015/115] Fixes --- include/boost/redis/corosio_connection.hpp | 3 ++- include/boost/redis/impl/reader_fsm.ipp | 3 ++- include/boost/redis/impl/writer_fsm.ipp | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp index 78a6948e..73316371 100644 --- a/include/boost/redis/corosio_connection.hpp +++ b/include/boost/redis/corosio_connection.hpp @@ -76,7 +76,7 @@ inline std::chrono::steady_clock::time_point compute_expiry( inline asio::cancellation_type_t token_to_cancel(std::stop_token tok) { - return tok.stop_requested() ? asio::cancellation_type_t::partial + return tok.stop_requested() ? asio::cancellation_type_t::terminal : asio::cancellation_type_t::none; } @@ -167,6 +167,7 @@ class corosio_redis_stream { case connect_action_type::tcp_connect: { // TODO: range connect + socket_.open(); auto result = co_await cancel_after( [&] -> capy::io_task<> { co_return co_await socket_.connect(*endpoints.begin()); diff --git a/include/boost/redis/impl/reader_fsm.ipp b/include/boost/redis/impl/reader_fsm.ipp index 5c1539eb..56e4005e 100644 --- a/include/boost/redis/impl/reader_fsm.ipp +++ b/include/boost/redis/impl/reader_fsm.ipp @@ -13,6 +13,7 @@ #include #include +#include namespace boost::redis::detail { @@ -48,7 +49,7 @@ reader_fsm::action reader_fsm::resume( // Translate timeout errors caused by operation_aborted to more legible ones. // A timeout here means that we didn't receive data in time. // Note that cancellation is already handled by the above statement. - if (ec == asio::error::operation_aborted) { + if (ec == capy::cond::canceled) { ec = error::pong_timeout; } diff --git a/include/boost/redis/impl/writer_fsm.ipp b/include/boost/redis/impl/writer_fsm.ipp index f4d3f779..d0638c23 100644 --- a/include/boost/redis/impl/writer_fsm.ipp +++ b/include/boost/redis/impl/writer_fsm.ipp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -81,12 +82,12 @@ writer_action writer_fsm::resume( // Check for cancellations and translate error codes if (is_terminal_cancel(cancel_state)) ec = asio::error::operation_aborted; - else if (ec == asio::error::operation_aborted) + else if (ec == capy::cond::canceled) ec = error::write_timeout; // Check for errors if (ec) { - if (ec == asio::error::operation_aborted) { + if (ec == capy::cond::canceled) { log_debug(st.logger, "Writer task: cancelled (1)."); } else { log_debug(st.logger, "Writer task error: ", ec); From 25ec65c4da9a7903682b5c6bb7b286d7f32e70a8 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 19:30:30 +0100 Subject: [PATCH 016/115] port 1st test --- test/test_conn_setup.cpp | 525 +++++++++++++++++++++------------------ 1 file changed, 279 insertions(+), 246 deletions(-) diff --git a/test/test_conn_setup.cpp b/test/test_conn_setup.cpp index 9997cb6b..cff6289a 100644 --- a/test/test_conn_setup.cpp +++ b/test/test_conn_setup.cpp @@ -6,283 +6,316 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include -#include -#include -#include - -#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include #include -#include - -#include "common.hpp" +#include -#include #include #include +#include namespace asio = boost::asio; namespace redis = boost::redis; +using namespace redis; using namespace std::chrono_literals; -using boost::system::error_code; +namespace capy = boost::capy; +namespace corosio = boost::corosio; namespace { -void test_auth_success() -{ - // Setup - asio::io_context ioc; - redis::connection conn{ioc}; - - // This request should return the username we're logged in as - redis::request req; - req.push("ACL", "WHOAMI"); - redis::response resp; - - // These credentials are set up in main, before tests are run - auto cfg = make_test_config(); - cfg.username = "myuser"; - cfg.password = "mypass"; - - bool exec_finished = false, run_finished = false; - - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - exec_finished = true; - BOOST_TEST_EQ(ec, error_code()); - conn.cancel(); - }); - - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, asio::error::operation_aborted); - }); - - ioc.run_for(test_timeout); - - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); - BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser"); -} - -// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) -void test_auth_failure() -{ - // Setup - std::string logs; - asio::io_context ioc; - redis::connection conn{ioc, make_string_logger(logs)}; - - // Disable reconnection so the hello error causes the connection to exit - auto cfg = make_test_config(); - cfg.username = "myuser"; - cfg.password = "wrongpass"; // wrong - cfg.reconnect_wait_interval = 0s; - - bool run_finished = false; - - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, redis::error::resp3_hello); - }); - - ioc.run_for(test_timeout); - - BOOST_TEST(run_finished); - - // Check the log - if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) { - std::cerr << "Log was: \n" << logs << std::endl; - } -} - -void test_database_index() +void run_coroutine_test(capy::task test) { - // Setup - asio::io_context ioc; - redis::connection conn(ioc); - - // Use a non-default database index - auto cfg = make_test_config(); - cfg.database_index = 2; - - redis::request req; - req.push("CLIENT", "INFO"); - - redis::response resp; - - bool exec_finished = false, run_finished = false; - - conn.async_exec(req, resp, [&](error_code ec, std::size_t n) { - BOOST_TEST_EQ(ec, error_code()); - std::clog << "async_exec has completed: " << n << std::endl; - conn.cancel(); - exec_finished = true; - }); - - conn.async_run(cfg, {}, [&run_finished](error_code) { - std::clog << "async_run has exited." << std::endl; - run_finished = true; - }); - - ioc.run_for(test_timeout); - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "2"); + // TODO: test timeout + corosio::io_context ctx; + capy::run_async(ctx.get_executor())(std::move(test)); + ctx.run(); } -// The user configured an empty setup request. No request should be sent -void test_setup_empty() +capy::task create_user( + std::string_view port, + std::string_view username, + std::string_view password) { - // Setup - asio::io_context ioc; - redis::connection conn(ioc); - - auto cfg = make_test_config(); - cfg.use_setup = true; - cfg.setup.clear(); - - redis::request req; - req.push("CLIENT", "INFO"); - - redis::response resp; + redis::connection conn{(co_await capy::this_coro::executor).context()}; - bool exec_finished = false, run_finished = false; + auto exec_fn = [&]() -> capy::task { + // Enable the user and grant them permissions on everything + redis::request req; + req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.cancel(); - exec_finished = true; - }); + auto [ec] = co_await conn.exec(req, redis::ignore); + BOOST_TEST_EQ(ec, std::error_code()); + }; - conn.async_run(cfg, {}, [&run_finished](error_code) { - run_finished = true; - }); + auto run_fn = [&]() -> capy::task { + config cfg; + cfg.addr.port = port; - ioc.run_for(test_timeout); - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 -} + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; -// We can use the setup member to run commands at startup -void test_setup_hello() -{ - // Setup - asio::io_context ioc; - redis::connection conn(ioc); - - auto cfg = make_test_config(); - cfg.use_setup = true; - cfg.setup.clear(); - cfg.setup.push("HELLO", "3", "AUTH", "myuser", "mypass"); - cfg.setup.push("SELECT", 8); - - redis::request req; - req.push("CLIENT", "INFO"); - - redis::response resp; - - bool exec_finished = false, run_finished = false; - - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.cancel(); - exec_finished = true; - }); - - conn.async_run(cfg, {}, [&run_finished](error_code) { - run_finished = true; - }); - - ioc.run_for(test_timeout); - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "3"); // using RESP3 - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "user"), "myuser"); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); + co_await capy::when_any(exec_fn(), run_fn()); } -// Running a pipeline without a HELLO is okay (regression check: we set the priority flag) -void test_setup_no_hello() +capy::task<> test_auth_success() { // Setup - asio::io_context ioc; - redis::connection conn(ioc); - - auto cfg = make_test_config(); - cfg.use_setup = true; - cfg.setup.clear(); - cfg.setup.push("SELECT", 8); - - redis::request req; - req.push("CLIENT", "INFO"); - - redis::response resp; - - bool exec_finished = false, run_finished = false; - - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.cancel(); - exec_finished = true; - }); - - conn.async_run(cfg, {}, [&run_finished](error_code) { - run_finished = true; - }); - - ioc.run_for(test_timeout); - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP3 - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); + redis::connection conn{(co_await capy::this_coro::executor).context()}; + + auto request_fn = [&] -> capy::task { + // This request should return the username we're logged in as + redis::request req; + req.push("ACL", "WHOAMI"); + redis::response resp; + + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser"); + }; + + auto run_fn = [&] -> capy::task { + // These credentials are set up in main, before tests are run + config cfg; + cfg.username = "myuser"; + cfg.password = "mypass"; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(request_fn(), run_fn()); } -// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) -void test_setup_failure() -{ - // Setup - std::string logs; - asio::io_context ioc; - redis::connection conn{ioc, make_string_logger(logs)}; - - // Disable reconnection so the hello error causes the connection to exit - auto cfg = make_test_config(); - cfg.use_setup = true; - cfg.setup.clear(); - cfg.setup.push("GET", "two", "args"); // GET only accepts one arg, so this will fail - cfg.reconnect_wait_interval = 0s; - - bool run_finished = false; - - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, redis::error::resp3_hello); - }); - - ioc.run_for(test_timeout); - - BOOST_TEST(run_finished); - - // Check the log - if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) { - std::cerr << "Log was:\n" << logs << std::endl; - } -} +// // Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) +// void test_auth_failure() +// { +// // Setup +// std::string logs; +// asio::io_context ioc; +// redis::connection conn{ioc, make_string_logger(logs)}; + +// // Disable reconnection so the hello error causes the connection to exit +// auto cfg = make_test_config(); +// cfg.username = "myuser"; +// cfg.password = "wrongpass"; // wrong +// cfg.reconnect_wait_interval = 0s; + +// bool run_finished = false; + +// conn.async_run(cfg, [&](error_code ec) { +// run_finished = true; +// BOOST_TEST_EQ(ec, redis::error::resp3_hello); +// }); + +// ioc.run_for(test_timeout); + +// BOOST_TEST(run_finished); + +// // Check the log +// if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) { +// std::cerr << "Log was: \n" << logs << std::endl; +// } +// } + +// void test_database_index() +// { +// // Setup +// asio::io_context ioc; +// redis::connection conn(ioc); + +// // Use a non-default database index +// auto cfg = make_test_config(); +// cfg.database_index = 2; + +// redis::request req; +// req.push("CLIENT", "INFO"); + +// redis::response resp; + +// bool exec_finished = false, run_finished = false; + +// conn.async_exec(req, resp, [&](error_code ec, std::size_t n) { +// BOOST_TEST_EQ(ec, error_code()); +// std::clog << "async_exec has completed: " << n << std::endl; +// conn.cancel(); +// exec_finished = true; +// }); + +// conn.async_run(cfg, {}, [&run_finished](error_code) { +// std::clog << "async_run has exited." << std::endl; +// run_finished = true; +// }); + +// ioc.run_for(test_timeout); +// BOOST_TEST(exec_finished); +// BOOST_TEST(run_finished); +// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "2"); +// } + +// // The user configured an empty setup request. No request should be sent +// void test_setup_empty() +// { +// // Setup +// asio::io_context ioc; +// redis::connection conn(ioc); + +// auto cfg = make_test_config(); +// cfg.use_setup = true; +// cfg.setup.clear(); + +// redis::request req; +// req.push("CLIENT", "INFO"); + +// redis::response resp; + +// bool exec_finished = false, run_finished = false; + +// conn.async_exec(req, resp, [&](error_code ec, std::size_t) { +// BOOST_TEST_EQ(ec, error_code()); +// conn.cancel(); +// exec_finished = true; +// }); + +// conn.async_run(cfg, {}, [&run_finished](error_code) { +// run_finished = true; +// }); + +// ioc.run_for(test_timeout); +// BOOST_TEST(exec_finished); +// BOOST_TEST(run_finished); +// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 +// } + +// // We can use the setup member to run commands at startup +// void test_setup_hello() +// { +// // Setup +// asio::io_context ioc; +// redis::connection conn(ioc); + +// auto cfg = make_test_config(); +// cfg.use_setup = true; +// cfg.setup.clear(); +// cfg.setup.push("HELLO", "3", "AUTH", "myuser", "mypass"); +// cfg.setup.push("SELECT", 8); + +// redis::request req; +// req.push("CLIENT", "INFO"); + +// redis::response resp; + +// bool exec_finished = false, run_finished = false; + +// conn.async_exec(req, resp, [&](error_code ec, std::size_t) { +// BOOST_TEST_EQ(ec, error_code()); +// conn.cancel(); +// exec_finished = true; +// }); + +// conn.async_run(cfg, {}, [&run_finished](error_code) { +// run_finished = true; +// }); + +// ioc.run_for(test_timeout); +// BOOST_TEST(exec_finished); +// BOOST_TEST(run_finished); +// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "3"); // using RESP3 +// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "user"), "myuser"); +// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); +// } + +// // Running a pipeline without a HELLO is okay (regression check: we set the priority flag) +// void test_setup_no_hello() +// { +// // Setup +// asio::io_context ioc; +// redis::connection conn(ioc); + +// auto cfg = make_test_config(); +// cfg.use_setup = true; +// cfg.setup.clear(); +// cfg.setup.push("SELECT", 8); + +// redis::request req; +// req.push("CLIENT", "INFO"); + +// redis::response resp; + +// bool exec_finished = false, run_finished = false; + +// conn.async_exec(req, resp, [&](error_code ec, std::size_t) { +// BOOST_TEST_EQ(ec, error_code()); +// conn.cancel(); +// exec_finished = true; +// }); + +// conn.async_run(cfg, {}, [&run_finished](error_code) { +// run_finished = true; +// }); + +// ioc.run_for(test_timeout); +// BOOST_TEST(exec_finished); +// BOOST_TEST(run_finished); +// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP3 +// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); +// } + +// // Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) +// void test_setup_failure() +// { +// // Setup +// std::string logs; +// asio::io_context ioc; +// redis::connection conn{ioc, make_string_logger(logs)}; + +// // Disable reconnection so the hello error causes the connection to exit +// auto cfg = make_test_config(); +// cfg.use_setup = true; +// cfg.setup.clear(); +// cfg.setup.push("GET", "two", "args"); // GET only accepts one arg, so this will fail +// cfg.reconnect_wait_interval = 0s; + +// bool run_finished = false; + +// conn.async_run(cfg, [&](error_code ec) { +// run_finished = true; +// BOOST_TEST_EQ(ec, redis::error::resp3_hello); +// }); + +// ioc.run_for(test_timeout); + +// BOOST_TEST(run_finished); + +// // Check the log +// if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) { +// std::cerr << "Log was:\n" << logs << std::endl; +// } +// } } // namespace int main() { - create_user("6379", "myuser", "mypass"); - - test_auth_success(); - test_auth_failure(); - test_database_index(); - test_setup_empty(); - test_setup_hello(); - test_setup_no_hello(); - test_setup_failure(); + run_coroutine_test(create_user("6379", "myuser", "mypass")); + + run_coroutine_test(test_auth_success()); + + // run_coroutine_test(test_auth_failure()); + // run_coroutine_test(test_database_index()); + // run_coroutine_test(test_setup_empty()); + // run_coroutine_test(test_setup_hello()); + // run_coroutine_test(test_setup_no_hello()); + // run_coroutine_test(test_setup_failure()); return boost::report_errors(); } From 69fb961c2ec2a3c4f3aabffaf14ecb6ac20218d7 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 19:47:13 +0100 Subject: [PATCH 017/115] test_conn_setup --- test/common.cpp | 75 -------- test/common.hpp | 31 +--- test/test_conn_setup.cpp | 388 ++++++++++++++++++--------------------- 3 files changed, 180 insertions(+), 314 deletions(-) diff --git a/test/common.cpp b/test/common.cpp index 54b3697f..997132ae 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -1,42 +1,15 @@ #include -#include -#include -#include #include #include "common.hpp" #include #include -#include -#include #include -namespace net = boost::asio; using namespace std::chrono_literals; -struct run_callback { - std::shared_ptr conn; - boost::redis::operation op; - boost::system::error_code expected; - - void operator()(boost::system::error_code const& ec) const - { - std::cout << "async_run: " << ec.message() << std::endl; - conn->cancel(op); - } -}; - -void run( - std::shared_ptr conn, - boost::redis::config cfg, - boost::system::error_code ec, - boost::redis::operation op) -{ - conn->async_run(cfg, run_callback{conn, op, ec}); -} - static std::string safe_getenv(const char* name, const char* default_value) { // MSVC doesn't like getenv @@ -61,22 +34,6 @@ boost::redis::config make_test_config() return cfg; } -#ifdef BOOST_ASIO_HAS_CO_AWAIT -void run_coroutine_test(net::awaitable op, std::chrono::steady_clock::duration timeout) -{ - net::io_context ioc; - bool finished = false; - net::co_spawn(ioc, std::move(op), [&finished](std::exception_ptr p) { - if (p) - std::rethrow_exception(p); - finished = true; - }); - ioc.run_for(timeout); - if (!finished) - throw std::runtime_error("Coroutine test did not finish"); -} -#endif // BOOST_ASIO_HAS_CO_AWAIT - // Finds a value in the output of the CLIENT INFO command // format: key1=value1 key2=value2 std::string_view find_client_info(std::string_view client_info, std::string_view key) @@ -92,38 +49,6 @@ std::string_view find_client_info(std::string_view client_info, std::string_view return client_info.substr(pos_begin, pos_end - pos_begin); } -void create_user(std::string_view port, std::string_view username, std::string_view password) -{ - // Setup - net::io_context ioc; - boost::redis::connection conn{ioc}; - - boost::redis::config cfg; - cfg.addr.port = port; - - // Enable the user and grant them permissions on everything - boost::redis::request req; - req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); - - bool run_finished = false, exec_finished = false; - - conn.async_run(cfg, [&](boost::system::error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); - }); - - conn.async_exec(req, boost::redis::ignore, [&](boost::system::error_code ec, std::size_t) { - exec_finished = true; - BOOST_TEST_EQ(ec, boost::system::error_code()); - conn.cancel(); - }); - - ioc.run_for(test_timeout); - - BOOST_TEST(run_finished); - BOOST_TEST(exec_finished); -} - boost::redis::logger make_string_logger(std::string& to) { return { diff --git a/test/common.hpp b/test/common.hpp index 569a7bcd..077b956b 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -1,17 +1,9 @@ #pragma once -#include -#include +#include #include -#include - -#include -#include -#include -#include #include -#include #include #include @@ -20,30 +12,15 @@ // integral number. inline constexpr std::chrono::seconds test_timeout{30}; -#ifdef BOOST_ASIO_HAS_CO_AWAIT -inline auto redir(boost::system::error_code& ec) -{ - return boost::asio::redirect_error(boost::asio::use_awaitable, ec); -} -void run_coroutine_test( - boost::asio::awaitable, - std::chrono::steady_clock::duration timeout = test_timeout); -#endif // BOOST_ASIO_HAS_CO_AWAIT - boost::redis::config make_test_config(); std::string get_server_hostname(); -void run( - std::shared_ptr conn, - boost::redis::config cfg = make_test_config(), - boost::system::error_code ec = boost::asio::error::operation_aborted, - boost::redis::operation op = boost::redis::operation::receive); - // Finds a value in the output of the CLIENT INFO command // format: key1=value1 key2=value2 std::string_view find_client_info(std::string_view client_info, std::string_view key); -// Connects to the Redis server at the given port and creates a user -void create_user(std::string_view port, std::string_view username, std::string_view password); +// TODO: bring back +// // Connects to the Redis server at the given port and creates a user +// void create_user(std::string_view port, std::string_view username, std::string_view password); boost::redis::logger make_string_logger(std::string& to); diff --git a/test/test_conn_setup.cpp b/test/test_conn_setup.cpp index cff6289a..5e03b397 100644 --- a/test/test_conn_setup.cpp +++ b/test/test_conn_setup.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include @@ -19,12 +21,19 @@ #include #include +#include #include #include #include namespace asio = boost::asio; namespace redis = boost::redis; + +// Declarations from common.cpp (avoid including common.hpp to prevent asio connection conflict) +boost::redis::config make_test_config(); +std::string get_server_hostname(); +std::string_view find_client_info(std::string_view client_info, std::string_view key); +boost::redis::logger make_string_logger(std::string& to); using namespace redis; using namespace std::chrono_literals; namespace capy = boost::capy; @@ -96,211 +105,167 @@ capy::task<> test_auth_success() co_await capy::when_any(request_fn(), run_fn()); } -// // Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) -// void test_auth_failure() -// { -// // Setup -// std::string logs; -// asio::io_context ioc; -// redis::connection conn{ioc, make_string_logger(logs)}; - -// // Disable reconnection so the hello error causes the connection to exit -// auto cfg = make_test_config(); -// cfg.username = "myuser"; -// cfg.password = "wrongpass"; // wrong -// cfg.reconnect_wait_interval = 0s; - -// bool run_finished = false; - -// conn.async_run(cfg, [&](error_code ec) { -// run_finished = true; -// BOOST_TEST_EQ(ec, redis::error::resp3_hello); -// }); - -// ioc.run_for(test_timeout); - -// BOOST_TEST(run_finished); - -// // Check the log -// if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) { -// std::cerr << "Log was: \n" << logs << std::endl; -// } -// } - -// void test_database_index() -// { -// // Setup -// asio::io_context ioc; -// redis::connection conn(ioc); - -// // Use a non-default database index -// auto cfg = make_test_config(); -// cfg.database_index = 2; - -// redis::request req; -// req.push("CLIENT", "INFO"); - -// redis::response resp; - -// bool exec_finished = false, run_finished = false; - -// conn.async_exec(req, resp, [&](error_code ec, std::size_t n) { -// BOOST_TEST_EQ(ec, error_code()); -// std::clog << "async_exec has completed: " << n << std::endl; -// conn.cancel(); -// exec_finished = true; -// }); - -// conn.async_run(cfg, {}, [&run_finished](error_code) { -// std::clog << "async_run has exited." << std::endl; -// run_finished = true; -// }); - -// ioc.run_for(test_timeout); -// BOOST_TEST(exec_finished); -// BOOST_TEST(run_finished); -// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "2"); -// } - -// // The user configured an empty setup request. No request should be sent -// void test_setup_empty() -// { -// // Setup -// asio::io_context ioc; -// redis::connection conn(ioc); - -// auto cfg = make_test_config(); -// cfg.use_setup = true; -// cfg.setup.clear(); - -// redis::request req; -// req.push("CLIENT", "INFO"); - -// redis::response resp; - -// bool exec_finished = false, run_finished = false; - -// conn.async_exec(req, resp, [&](error_code ec, std::size_t) { -// BOOST_TEST_EQ(ec, error_code()); -// conn.cancel(); -// exec_finished = true; -// }); - -// conn.async_run(cfg, {}, [&run_finished](error_code) { -// run_finished = true; -// }); - -// ioc.run_for(test_timeout); -// BOOST_TEST(exec_finished); -// BOOST_TEST(run_finished); -// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 -// } - -// // We can use the setup member to run commands at startup -// void test_setup_hello() -// { -// // Setup -// asio::io_context ioc; -// redis::connection conn(ioc); - -// auto cfg = make_test_config(); -// cfg.use_setup = true; -// cfg.setup.clear(); -// cfg.setup.push("HELLO", "3", "AUTH", "myuser", "mypass"); -// cfg.setup.push("SELECT", 8); - -// redis::request req; -// req.push("CLIENT", "INFO"); - -// redis::response resp; - -// bool exec_finished = false, run_finished = false; - -// conn.async_exec(req, resp, [&](error_code ec, std::size_t) { -// BOOST_TEST_EQ(ec, error_code()); -// conn.cancel(); -// exec_finished = true; -// }); - -// conn.async_run(cfg, {}, [&run_finished](error_code) { -// run_finished = true; -// }); - -// ioc.run_for(test_timeout); -// BOOST_TEST(exec_finished); -// BOOST_TEST(run_finished); -// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "3"); // using RESP3 -// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "user"), "myuser"); -// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); -// } - -// // Running a pipeline without a HELLO is okay (regression check: we set the priority flag) -// void test_setup_no_hello() -// { -// // Setup -// asio::io_context ioc; -// redis::connection conn(ioc); - -// auto cfg = make_test_config(); -// cfg.use_setup = true; -// cfg.setup.clear(); -// cfg.setup.push("SELECT", 8); - -// redis::request req; -// req.push("CLIENT", "INFO"); - -// redis::response resp; - -// bool exec_finished = false, run_finished = false; - -// conn.async_exec(req, resp, [&](error_code ec, std::size_t) { -// BOOST_TEST_EQ(ec, error_code()); -// conn.cancel(); -// exec_finished = true; -// }); - -// conn.async_run(cfg, {}, [&run_finished](error_code) { -// run_finished = true; -// }); - -// ioc.run_for(test_timeout); -// BOOST_TEST(exec_finished); -// BOOST_TEST(run_finished); -// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP3 -// BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); -// } - -// // Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) -// void test_setup_failure() -// { -// // Setup -// std::string logs; -// asio::io_context ioc; -// redis::connection conn{ioc, make_string_logger(logs)}; - -// // Disable reconnection so the hello error causes the connection to exit -// auto cfg = make_test_config(); -// cfg.use_setup = true; -// cfg.setup.clear(); -// cfg.setup.push("GET", "two", "args"); // GET only accepts one arg, so this will fail -// cfg.reconnect_wait_interval = 0s; - -// bool run_finished = false; - -// conn.async_run(cfg, [&](error_code ec) { -// run_finished = true; -// BOOST_TEST_EQ(ec, redis::error::resp3_hello); -// }); - -// ioc.run_for(test_timeout); - -// BOOST_TEST(run_finished); - -// // Check the log -// if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) { -// std::cerr << "Log was:\n" << logs << std::endl; -// } -// } +// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) +capy::task<> test_auth_failure() +{ + // Setup + std::string logs; + redis::connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + + // Disable reconnection so the hello error causes the connection to exit + auto cfg = make_test_config(); + cfg.username = "myuser"; + cfg.password = "wrongpass"; // wrong + cfg.reconnect_wait_interval = 0s; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(redis::error::resp3_hello)); + + // Check the log + if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) { + std::cerr << "Log was: \n" << logs << std::endl; + } +} + +capy::task<> test_database_index() +{ + // Setup + redis::connection conn{(co_await capy::this_coro::executor).context()}; + + auto request_fn = [&] -> capy::task { + redis::request req; + req.push("CLIENT", "INFO"); + redis::response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "2"); + }; + + auto run_fn = [&] -> capy::task { + // Use a non-default database index + auto cfg = make_test_config(); + cfg.database_index = 2; + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(request_fn(), run_fn()); +} + +// The user configured an empty setup request. No request should be sent +capy::task<> test_setup_empty() +{ + // Setup + redis::connection conn{(co_await capy::this_coro::executor).context()}; + + auto request_fn = [&] -> capy::task { + redis::request req; + req.push("CLIENT", "INFO"); + redis::response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 + }; + + auto run_fn = [&] -> capy::task { + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(request_fn(), run_fn()); +} + +// We can use the setup member to run commands at startup +capy::task<> test_setup_hello() +{ + // Setup + redis::connection conn{(co_await capy::this_coro::executor).context()}; + + auto request_fn = [&] -> capy::task { + redis::request req; + req.push("CLIENT", "INFO"); + redis::response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "3"); // using RESP3 + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "user"), "myuser"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); + }; + + auto run_fn = [&] -> capy::task { + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("HELLO", "3", "AUTH", "myuser", "mypass"); + cfg.setup.push("SELECT", 8); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(request_fn(), run_fn()); +} + +// Running a pipeline without a HELLO is okay (regression check: we set the priority flag) +capy::task<> test_setup_no_hello() +{ + // Setup + redis::connection conn{(co_await capy::this_coro::executor).context()}; + + auto request_fn = [&] -> capy::task { + redis::request req; + req.push("CLIENT", "INFO"); + redis::response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); + }; + + auto run_fn = [&] -> capy::task { + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("SELECT", 8); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(request_fn(), run_fn()); +} + +// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) +capy::task<> test_setup_failure() +{ + // Setup + std::string logs; + redis::connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + + // Disable reconnection so the hello error causes the connection to exit + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("GET", "two", "args"); // GET only accepts one arg, so this will fail + cfg.reconnect_wait_interval = 0s; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(redis::error::resp3_hello)); + + // Check the log + if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) { + std::cerr << "Log was:\n" << logs << std::endl; + } +} } // namespace @@ -309,13 +274,12 @@ int main() run_coroutine_test(create_user("6379", "myuser", "mypass")); run_coroutine_test(test_auth_success()); - - // run_coroutine_test(test_auth_failure()); - // run_coroutine_test(test_database_index()); - // run_coroutine_test(test_setup_empty()); - // run_coroutine_test(test_setup_hello()); - // run_coroutine_test(test_setup_no_hello()); - // run_coroutine_test(test_setup_failure()); + run_coroutine_test(test_auth_failure()); + run_coroutine_test(test_database_index()); + run_coroutine_test(test_setup_empty()); + run_coroutine_test(test_setup_hello()); + run_coroutine_test(test_setup_no_hello()); + run_coroutine_test(test_setup_failure()); return boost::report_errors(); } From a86626eb354ca7b596360a470a3cb2e0760859be Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 19:57:07 +0100 Subject: [PATCH 018/115] Remove old connection --- example/cpp20_intro.cpp | 2 +- include/boost/redis/connection.hpp | 1380 +++++-------------- include/boost/redis/corosio_connection.hpp | 814 ----------- include/boost/redis/detail/redis_stream.hpp | 251 ---- test/test_conn_setup.cpp | 2 +- 5 files changed, 382 insertions(+), 2067 deletions(-) delete mode 100644 include/boost/redis/corosio_connection.hpp delete mode 100644 include/boost/redis/detail/redis_stream.hpp diff --git a/example/cpp20_intro.cpp b/example/cpp20_intro.cpp index 6e1ea4f6..4f0b4d5e 100644 --- a/example/cpp20_intro.cpp +++ b/example/cpp20_intro.cpp @@ -5,7 +5,7 @@ */ #include -#include +#include #include #include diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index 1801dd4d..2e7999d9 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -4,8 +4,8 @@ * accompanying file LICENSE.txt) */ -#ifndef BOOST_REDIS_CONNECTION_HPP -#define BOOST_REDIS_CONNECTION_HPP +#ifndef BOOST_REDIS_COROSIO_CONNECTION_HPP +#define BOOST_REDIS_COROSIO_CONNECTION_HPP #include #include @@ -13,10 +13,10 @@ #include #include #include +#include #include #include #include -#include #include #include #include @@ -28,37 +28,38 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include #include +#include #include +#include #include namespace boost::redis { @@ -72,494 +73,425 @@ inline std::chrono::steady_clock::time_point compute_expiry( : std::chrono::steady_clock::now() + timeout; } -template -struct connection_impl { - using clock_type = std::chrono::steady_clock; - using clock_traits_type = asio::wait_traits; - using timer_type = asio::basic_waitable_timer; - - using receive_channel_type = asio::experimental::channel< - Executor, - void(system::error_code, std::size_t)>; - using exec_notifier_type = asio::experimental::channel< - Executor, - void(system::error_code, std::size_t)>; - - redis_stream stream_; - timer_type writer_timer_; // timer used for write timeouts - timer_type writer_cv_; // condition variable, cancelled when there is new data to write - timer_type reader_timer_; // timer used for read timeouts - timer_type reconnect_timer_; // to wait the reconnection period - timer_type ping_timer_; // to wait between pings - receive_channel_type receive_channel_; - asio::cancellation_signal run_signal_; - connection_state st_; +inline asio::cancellation_type_t token_to_cancel(std::stop_token tok) +{ + return tok.stop_requested() ? asio::cancellation_type_t::terminal + : asio::cancellation_type_t::none; +} - using executor_type = Executor; +template +inline capy::io_task cancel_at( + capy::io_task task, + corosio::timer& timer, + std::chrono::steady_clock::time_point timeout) +{ + timer.expires_at(timeout); + auto [winner_index, result] = co_await capy::when_any(std::move(task), timer.wait()); + if (winner_index == 0u) + co_return std::get<0>(std::move(result)); + else + co_return {make_error_code(asio::error::operation_aborted)}; +} - executor_type get_executor() noexcept { return writer_cv_.get_executor(); } +template +inline capy::io_task cancel_after( + capy::io_task task, + corosio::timer& timer, + std::chrono::steady_clock::duration timeout) +{ + return cancel_at(std::move(task), timer, std::chrono::steady_clock::now() + timeout); +} - struct exec_op { - connection_impl* obj_ = nullptr; - std::shared_ptr notifier_ = nullptr; - exec_fsm fsm_; +class corosio_redis_stream { + // TODO: UNIX sockets + corosio::tcp_socket socket_; + corosio::openssl_stream stream_; // TODO: make this configurable + corosio::timer timer_; + corosio::resolver resolv_; + redis_stream_state st_; - template - void operator()(Self& self, system::error_code = {}, std::size_t = 0) - { - while (true) { - // Invoke the state machine - auto act = fsm_.resume( - obj_->is_open(), - obj_->st_, - self.get_cancellation_state().cancelled()); +public: + explicit corosio_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) + : socket_(ctx) + , stream_(&socket_, std::move(tls_ctx)) + , timer_(ctx) + , resolv_(ctx) + { } - // Do what the FSM said - switch (act.type()) { - case exec_action_type::setup_cancellation: - self.reset_cancellation_state(asio::enable_total_cancellation()); - continue; // this action does not require yielding - case exec_action_type::immediate: - asio::async_immediate(self.get_io_executor(), std::move(self)); - return; - case exec_action_type::notify_writer: - obj_->writer_cv_.cancel(); - continue; // this action does not require yielding - case exec_action_type::wait_for_response: - notifier_->async_receive(std::move(self)); - return; - case exec_action_type::done: - notifier_.reset(); - self.complete(act.error(), act.bytes_read()); - return; + // I/O + capy::io_task<> connect(const connect_params& params, buffered_logger& l) + { + connect_fsm fsm{l}; + system::error_code ec; + corosio::resolver_results endpoints; + + auto act = fsm.resume(ec, st_); + + while (true) { + switch (act.type) { + case connect_action_type::unix_socket_close: + BOOST_ASSERT(false); + co_return {system::error_code(asio::error::operation_not_supported)}; + case connect_action_type::unix_socket_connect: + BOOST_ASSERT(false); + co_return {system::error_code(asio::error::operation_not_supported)}; + case connect_action_type::tcp_resolve: + { + auto result = co_await cancel_after( + [&] -> capy::io_task { + co_return co_await resolv_.resolve( + params.addr.tcp_address().host, + params.addr.tcp_address().port); + }(), + timer_, + params.resolve_timeout); + ec = result.ec; + endpoints = std::move(result.t1); + act = fsm.resume(ec, endpoints, st_); + break; + } + case connect_action_type::ssl_stream_reset: + stream_.reset(); + act = fsm.resume(ec, st_); + break; + case connect_action_type::ssl_handshake: + ec = (co_await cancel_after( + stream_.handshake(corosio::tls_stream::handshake_type::client), + timer_, + params.ssl_handshake_timeout)) + .ec; + act = fsm.resume(ec, st_); + break; + case connect_action_type::done: co_return {act.ec}; + case connect_action_type::tcp_connect: + { + // TODO: range connect + socket_.open(); + auto result = co_await cancel_after( + [&] -> capy::io_task<> { + co_return co_await socket_.connect(*endpoints.begin()); + }(), + timer_, + params.connect_timeout); + ec = result.ec; + act = fsm.resume(ec, *endpoints.begin(), st_); + break; } + default: BOOST_ASSERT(false); } } - }; + } - connection_impl(Executor&& ex, asio::ssl::context&& ctx, logger&& lgr) - : stream_{ex, std::move(ctx)} - , writer_timer_{ex} - , writer_cv_{ex} - , reader_timer_{ex} - , reconnect_timer_{ex} - , ping_timer_{ex} - , receive_channel_{ex, 256} - , st_{{std::move(lgr)}} + template + capy::io_task write_some(const BuffType& buffers) { - set_receive_adapter(any_adapter{ignore}); - writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); + switch (st_.type) { + case transport_type::tcp: co_return co_await socket_.write_some(buffers); + case transport_type::tcp_tls: co_return co_await stream_.write_some(buffers); + case transport_type::unix_socket: + default: BOOST_ASSERT(false); co_return {}; + } } - void cancel(operation op) + template + capy::io_task read_some(const BuffType& buffers) { - switch (op) { - case operation::exec: st_.mpx.cancel_waiting(); break; - case operation::receive: cancel_receive_v2(); break; - case operation::reconnection: - st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); - break; - case operation::run: - case operation::resolve: - case operation::connect: - case operation::ssl_handshake: - case operation::health_check: cancel_run(); break; - case operation::all: - st_.mpx.cancel_waiting(); // exec - cancel_receive_v2(); // receive - st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); // reconnect - cancel_run(); // run - break; - default: /* ignore */; + switch (st_.type) { + case transport_type::tcp: co_return co_await socket_.read_some(buffers); + case transport_type::tcp_tls: co_return co_await stream_.read_some(buffers); + case transport_type::unix_socket: + default: BOOST_ASSERT(false); co_return {}; } } +}; - void cancel_receive_v1() { receive_channel_.cancel(); } +struct corosio_connection_impl { + capy::async_event run_cancelled_event_; + corosio_redis_stream stream_; + corosio::timer writer_timer_; // timer used for write timeouts + corosio::timer writer_cv_; // set when there is new data to write + corosio::timer reader_timer_; // timer used for read timeouts + corosio::timer reconnect_timer_; // to wait the reconnection period + corosio::timer ping_timer_; // to wait between pings + flow_controller controller_; + connection_state st_; - void cancel_receive_v2() + corosio_connection_impl( + capy::execution_context& ctx, + corosio::tls_context&& ssl_ctx, + logger&& lgr) + : stream_{ctx, std::move(ssl_ctx)} + , writer_timer_{ctx} + , writer_cv_{ctx} + , reader_timer_{ctx} + , reconnect_timer_{ctx} + , ping_timer_{ctx} + , controller_{1024u * 1024u * 16u} // 16MB, TODO: make it configurable + , st_{{std::move(lgr)}} { - st_.receive2_cancelled = true; - cancel_receive_v1(); + set_receive_adapter(any_adapter{ignore}); + writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); } - void cancel_run() + void cancel() { - // Individual operations should see a terminal cancellation, regardless - // of what we got requested. We take enough actions to ensure that this - // doesn't prevent the object from being re-used (e.g. we reset the TLS stream). - run_signal_.emit(asio::cancellation_type_t::terminal); + // exec + st_.mpx.cancel_waiting(); - // Name resolution doesn't support per-operation cancellation - stream_.cancel_resolve(); - - // Receive is technically not part of run, but we also cancel it for - // backwards compatibility. Note that this intentionally affects v1 receive, only. - cancel_receive_v1(); - } + // receive (TODO: do we really need this?) + st_.receive2_cancelled = true; - bool is_open() const noexcept { return stream_.is_open(); } + // reconnect (TODO: do we really need this?) + st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); - bool will_reconnect() const noexcept - { - return st_.cfg.reconnect_wait_interval != std::chrono::seconds::zero(); + // run + run_cancelled_event_.set(); } - template - auto async_exec(request const& req, any_adapter adapter, CompletionToken&& token) + capy::io_task<> exec(request const& req, any_adapter adapter) { - auto notifier = std::make_shared(get_executor(), 1); - auto info = make_elem(req, std::move(adapter)); - - info->set_done_callback([notifier]() { - notifier->try_send(std::error_code{}, 0); + // Setup + capy::async_event request_done; + auto elem = make_elem(req, std::move(adapter)); + elem->set_done_callback([&request_done]() { + request_done.set(); }); - - return asio::async_compose( - exec_op{this, notifier, exec_fsm(std::move(info))}, - token, - writer_cv_); + exec_fsm fsm{elem}; + + // Invoke the FSM + while (true) { + // Invoke the state machine + auto act = fsm.resume(true, st_, token_to_cancel(co_await capy::this_coro::stop_token)); + + // Do what the FSM said + switch (act.type()) { + case exec_action_type::setup_cancellation: break; // ignored, not required by capy + case exec_action_type::immediate: break; // ignored, not required by capy + case exec_action_type::notify_writer: writer_cv_.cancel(); break; + case exec_action_type::wait_for_response: + { + auto [ec] = co_await request_done.wait(); + ignore_unused(ec); // TODO: we should likely use this + break; + } + case exec_action_type::done: co_return {act.error()}; + } + } } void set_receive_adapter(any_adapter adapter) { st_.mpx.set_receive_adapter(std::move(adapter)); } - - std::size_t receive(system::error_code& ec) - { - std::size_t size = 0; - - auto f = [&](system::error_code const& ec2, std::size_t n) { - ec = ec2; - size = n; - }; - - auto const res = receive_channel_.try_receive(f); - if (ec) - return 0; - - if (!res) - ec = error::sync_receive_push_failed; - - return size; - } }; -template -struct receive2_op { - connection_impl* conn_; - receive_fsm fsm_{}; - - void drain_receive_channel() - { - // We don't expect any errors here. The only errors - // that might appear in the channel are due to cancellations, - // and these don't make sense with try_receive - auto f = [](system::error_code, std::size_t) { }; - while (conn_->receive_channel_.try_receive(f)) - ; - } +inline capy::io_task<> receive(corosio_connection_impl& conn) +{ + // Setup + receive_fsm fsm; + system::error_code ec; - template - void operator()(Self& self, system::error_code ec = {}, std::size_t /* push_bytes */ = 0u) - { - receive_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + while (true) { + receive_action act = fsm.resume( + conn.st_, + ec, + token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type) { - case receive_action::action_type::setup_cancellation: - self.reset_cancellation_state(asio::enable_total_cancellation()); - (*this)(self); // this action does not require yielding - return; + case receive_action::action_type::setup_cancellation: break; // not required here case receive_action::action_type::wait: - conn_->receive_channel_.async_receive(std::move(self)); - return; - case receive_action::action_type::drain_channel: - drain_receive_channel(); - (*this)(self); // this action does not require yielding - return; - case receive_action::action_type::immediate: - asio::async_immediate(self.get_io_executor(), std::move(self)); - return; - case receive_action::action_type::done: self.complete(act.ec); return; + { + auto [controller_ec] = co_await conn.controller_.take(); + ec = controller_ec; + break; + } + case receive_action::action_type::drain_channel: break; // not required + case receive_action::action_type::immediate: break; // not required + case receive_action::action_type::done: co_return {act.ec}; } } -}; - -template -struct exec_one_op { - connection_impl* conn_; - const request* req_; - exec_one_fsm fsm_; +} - explicit exec_one_op(connection_impl& conn, const request& req, any_adapter resp) - : conn_(&conn) - , req_(&req) - , fsm_(std::move(resp), req.get_expected_responses()) - { } +inline capy::io_task<> async_exec_one( + corosio_connection_impl& conn, + const request& req, + any_adapter resp) +{ + exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; + system::error_code ec; + std::size_t bytes = 0u; - template - void operator()(Self& self, system::error_code ec = {}, std::size_t bytes_written = 0u) - { - exec_one_action act = fsm_.resume( - conn_->st_.mpx.get_read_buffer(), + while (true) { + exec_one_action act = fsm.resume( + conn.st_.mpx.get_read_buffer(), ec, - bytes_written, - self.get_cancellation_state().cancelled()); + bytes, + token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type) { - case exec_one_action_type::done: self.complete(act.ec); return; + case exec_one_action_type::done: co_return {ec}; case exec_one_action_type::write: - asio::async_write(conn_->stream_, asio::buffer(req_->payload()), std::move(self)); - return; + { + auto [write_ec, write_bytes] = co_await capy::write( + conn.stream_, + capy::make_buffer(req.payload())); + ec = write_ec; + bytes = write_bytes; + break; + } case exec_one_action_type::read_some: - conn_->stream_.async_read_some( - conn_->st_.mpx.get_read_buffer().get_prepared(), - std::move(self)); - return; + { + auto [read_ec, read_bytes] = co_await conn.stream_.read_some( + capy::make_buffer(conn.st_.mpx.get_read_buffer().get_prepared())); + ec = read_ec; + bytes = read_bytes; + break; + } } } -}; - -template -auto async_exec_one( - connection_impl& conn, - const request& req, - any_adapter resp, - CompletionToken&& token) -{ - return asio::async_compose( - exec_one_op{conn, req, std::move(resp)}, - token, - conn); } -template -struct sentinel_resolve_op { - connection_impl* conn_; - sentinel_resolve_fsm fsm_; - - explicit sentinel_resolve_op(connection_impl& conn) - : conn_(&conn) - { } +inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) +{ + // Setup + sentinel_resolve_fsm fsm; + system::error_code ec; - template - void operator()(Self& self, system::error_code ec = {}) - { - auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after - sentinel_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + while (true) { + sentinel_action act = fsm.resume( + conn.st_, + ec, + token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.get_type()) { - case sentinel_action::type::done: self.complete(act.error()); return; + case sentinel_action::type::done: co_return {act.error()}; case sentinel_action::type::connect: - conn->stream_.async_connect( - make_sentinel_connect_params(conn->st_.cfg, act.connect_addr()), - conn->st_.logger, - std::move(self)); - return; + { + auto [connect_ec] = co_await conn.stream_.connect( + make_sentinel_connect_params(conn.st_.cfg, act.connect_addr()), + conn.st_.logger); + ec = connect_ec; + break; + } case sentinel_action::type::request: - async_exec_one( - *conn, - conn->st_.cfg.sentinel.setup, - make_sentinel_adapter(conn->st_), - asio::cancel_after( - conn->reconnect_timer_, // should be safe to re-use this - conn->st_.cfg.sentinel.request_timeout, - std::move(self))); - return; + { + auto [request_ec] = co_await cancel_after( + async_exec_one(conn, conn.st_.cfg.sentinel.setup, make_sentinel_adapter(conn.st_)), + conn.reconnect_timer_, + conn.st_.cfg.sentinel.request_timeout); + ec = request_ec; + break; + } } } -}; - -template -auto async_sentinel_resolve(connection_impl& conn, CompletionToken&& token) -{ - return asio::async_compose( - sentinel_resolve_op{conn}, - token, - conn); } -template -struct writer_op { - connection_impl* conn_; - writer_fsm fsm_; - - explicit writer_op(connection_impl& conn) noexcept - : conn_(&conn) - { } - - template - void operator()(Self& self, system::error_code ec = {}, std::size_t bytes_written = 0u) - { - auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after - auto act = fsm_.resume( - conn->st_, +inline capy::io_task<> writer(corosio_connection_impl& conn) +{ + // Setup + writer_fsm fsm; + system::error_code ec; + std::size_t bytes_written = 0u; + + while (true) { + writer_action act = fsm.resume( + conn.st_, ec, bytes_written, - self.get_cancellation_state().cancelled()); + token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type()) { - case writer_action_type::done: self.complete(act.error()); return; + case writer_action_type::done: co_return {act.error()}; case writer_action_type::write_some: - conn->stream_.async_write_some( - asio::buffer(conn->st_.mpx.get_write_buffer()), - asio::cancel_at( - conn->writer_timer_, - compute_expiry(act.timeout()), - std::move(self))); - return; + { + auto [write_ec, write_bytes] = co_await cancel_at( + conn.stream_.write_some(capy::make_buffer(conn.st_.mpx.get_write_buffer())), + conn.writer_timer_, + compute_expiry(act.timeout())); + ec = write_ec; + bytes_written = write_bytes; + break; + } case writer_action_type::wait: - conn->writer_cv_.expires_at(compute_expiry(act.timeout())); - conn->writer_cv_.async_wait(std::move(self)); - return; + { + conn.writer_cv_.expires_at(compute_expiry(act.timeout())); + auto [wait_ec] = co_await conn.writer_cv_.wait(); + ec = wait_ec; + bytes_written = 0u; + break; + } } } -}; - -template -struct reader_op { - connection_impl* conn_; - reader_fsm fsm_; +} -public: - reader_op(connection_impl& conn) noexcept - : conn_{&conn} - { } +inline capy::io_task<> reader(corosio_connection_impl& conn) +{ + reader_fsm fsm; + std::size_t n = 0u; + system::error_code ec; - template - void operator()(Self& self, system::error_code ec = {}, std::size_t n = 0) - { - for (;;) { - auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after - auto act = fsm_.resume(conn->st_, n, ec, self.get_cancellation_state().cancelled()); + for (;;) { + auto act = fsm.resume(conn.st_, n, ec, token_to_cancel(co_await capy::this_coro::stop_token)); - switch (act.get_type()) { - case reader_fsm::action::type::read_some: - conn->stream_.async_read_some( - asio::buffer(conn->st_.mpx.get_prepared_read_buffer()), - asio::cancel_at( - conn->reader_timer_, - compute_expiry(act.timeout()), - std::move(self))); - return; - case reader_fsm::action::type::notify_push_receiver: - if (conn->receive_channel_.try_send(ec, act.push_size())) { - continue; - } else { - conn->receive_channel_.async_send(ec, act.push_size(), std::move(self)); - } - return; - case reader_fsm::action::type::done: self.complete(act.error()); return; + switch (act.get_type()) { + case reader_fsm::action::type::read_some: + { + auto [read_ec, read_bytes] = co_await cancel_at( + conn.stream_.read_some(capy::make_buffer(conn.st_.mpx.get_prepared_read_buffer())), + conn.reader_timer_, + compute_expiry(act.timeout())); + ec = read_ec; + n = read_bytes; + break; + } + case reader_fsm::action::type::notify_push_receiver: + { + // TODO: re-work this + auto [notify_ec] = co_await conn.controller_.wait_for_space(); + if (notify_ec) + ec = notify_ec; + else + conn.controller_.put(act.push_size()); } + case reader_fsm::action::type::done: co_return {act.error()}; } } -}; - -template -class run_op { -private: - connection_impl* conn_; - run_fsm fsm_{}; - - template - auto reader(CompletionToken&& token) - { - return asio::async_compose( - reader_op{*conn_}, - std::forward(token), - conn_->writer_cv_); - } - - template - auto writer(CompletionToken&& token) - { - return asio::async_compose( - writer_op{*conn_}, - std::forward(token), - conn_->writer_cv_); - } - -public: - run_op(connection_impl* conn) noexcept - : conn_{conn} - { } +} - // Called after the parallel group finishes - template - void operator()( - Self& self, - std::array order, - system::error_code reader_ec, - system::error_code writer_ec) - { - (*this)(self, order[0u] == 0u ? reader_ec : writer_ec); - } +inline capy::io_task<> run(corosio_connection_impl& conn) +{ + run_fsm fsm; + system::error_code ec; - template - void operator()(Self& self, system::error_code ec = {}) - { - auto act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + while (true) { + auto act = fsm.resume(conn.st_, ec, token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type) { - case run_action_type::done: self.complete(act.ec); return; - case run_action_type::immediate: - asio::async_immediate(self.get_io_executor(), std::move(self)); - return; - case run_action_type::sentinel_resolve: - async_sentinel_resolve(*conn_, std::move(self)); - return; + case run_action_type::done: co_return {act.ec}; + case run_action_type::immediate: break; // no longer required + case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve(conn)).ec; break; case run_action_type::connect: - conn_->stream_.async_connect( - make_run_connect_params(conn_->st_), - conn_->st_.logger, - std::move(self)); - return; + ec = (co_await conn.stream_.connect(make_run_connect_params(conn.st_), conn.st_.logger)) + .ec; + break; case run_action_type::parallel_group: - asio::experimental::make_parallel_group( - [this](auto token) { - return this->reader(token); - }, - [this](auto token) { - return this->writer(token); - }) - .async_wait(asio::experimental::wait_for_one(), std::move(self)); - return; - case run_action_type::cancel_receive: - conn_->receive_channel_.cancel(); - (*this)(self); // this action does not require suspending - return; + { + auto [winner_index, result] = co_await capy::when_any(reader(conn), writer(conn)); + ignore_unused(winner_index); + ec = std::get<0>(result).ec; + break; + } + case run_action_type::cancel_receive: break; // no longer required case run_action_type::wait_for_reconnection: - conn_->reconnect_timer_.expires_after(conn_->st_.cfg.reconnect_wait_interval); - conn_->reconnect_timer_.async_wait(std::move(self)); - return; - default: BOOST_ASSERT(false); + conn.reconnect_timer_.expires_after(conn.st_.cfg.reconnect_wait_interval); + ec = (co_await conn.reconnect_timer_.wait()).ec; + break; } } -}; +} logger make_stderr_logger(logger::level lvl, std::string prefix); -template -class run_cancel_handler { - connection_impl* conn_; - -public: - explicit run_cancel_handler(connection_impl& conn) noexcept - : conn_(&conn) - { } - - void operator()(asio::cancellation_type_t cancel_type) const - { - // We support terminal and partial cancellation - constexpr auto mask = asio::cancellation_type_t::terminal | - asio::cancellation_type_t::partial; - - if ((cancel_type & mask) != asio::cancellation_type_t::none) { - conn_->cancel(operation::run); - } - } -}; - } // namespace detail /** @brief A SSL connection to the Redis server. @@ -570,25 +502,8 @@ class run_cancel_handler { * * @tparam Executor The executor type used to create any required I/O objects. */ -template -class basic_connection { +class connection { public: - using this_type = basic_connection; - - /// (Deprecated) Type of the next layer - BOOST_DEPRECATED("This typedef is deprecated, and will be removed with next_layer().") - typedef asio::ssl::stream> next_layer_type; - - /// The type of the executor associated to this object. - using executor_type = Executor; - - /// Rebinds the socket type to another executor. - template - struct rebind_executor { - /// The connection type when rebound to the specified executor. - using other = basic_connection; - }; - /** @brief Constructor from an executor. * * @param ex Executor used to create all internal I/O objects. @@ -597,15 +512,12 @@ class basic_connection { * and customize logging. By default, `logger::level::info` messages * and higher are logged to `stderr`. */ - explicit basic_connection( - executor_type ex, - asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, + explicit connection( + capy::execution_context& ctx, + corosio::tls_context ssl_ctx = {}, logger lgr = {}) : impl_( - std::make_unique>( - std::move(ex), - std::move(ctx), - std::move(lgr))) + std::make_unique(ctx, std::move(ssl_ctx), std::move(lgr))) { } /** @brief Constructor from an executor and a logger. @@ -617,47 +529,10 @@ class basic_connection { * * An SSL context with default settings will be created. */ - basic_connection(executor_type ex, logger lgr) - : basic_connection( - std::move(ex), - asio::ssl::context{asio::ssl::context::tlsv12_client}, - std::move(lgr)) - { } - - /** - * @brief Constructor from an `io_context`. - * - * @param ioc I/O context used to create all internal I/O objects. - * @param ctx SSL context. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - */ - explicit basic_connection( - asio::io_context& ioc, - asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, - logger lgr = {}) - : basic_connection(ioc.get_executor(), std::move(ctx), std::move(lgr)) - { } - - /** - * @brief Constructor from an `io_context` and a logger. - * - * @param ioc I/O context used to create all internal I/O objects. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - */ - basic_connection(asio::io_context& ioc, logger lgr) - : basic_connection( - ioc.get_executor(), - asio::ssl::context{asio::ssl::context::tlsv12_client}, - std::move(lgr)) + connection(capy::execution_context& ctx, logger lgr) + : connection(ctx, {}, std::move(lgr)) { } - /// Returns the associated executor. - executor_type get_executor() noexcept { return impl_->writer_cv_.get_executor(); } - /** @brief Starts the underlying connection operations. * * This function establishes a connection to the Redis server and keeps @@ -711,100 +586,17 @@ class basic_connection { * @param cfg Configuration parameters. * @param token Completion token. */ - template > - auto async_run(config const& cfg, CompletionToken&& token = {}) + capy::io_task<> run(config const& cfg) { - return asio::async_initiate( - run_initiation{impl_.get()}, - token, - &cfg); - } + impl_->st_.cfg = cfg; + impl_->st_.mpx.set_config(cfg); + impl_->run_cancelled_event_.clear(); - /** - * @brief (Deprecated) Starts the underlying connection operations. - * @copydetail async_run - * - * This function accepts an extra logger parameter. The passed `logger::lvl` - * will be used, but `logger::fn` will be ignored. Instead, a function - * that logs to `stderr` using `config::prefix` will be used. - * This keeps backwards compatibility with previous versions. - * Any logger configured in the constructor will be overriden. - * - * @par Deprecated - * The logger should be passed to the connection's constructor instead of using this - * function. Use the overload without a logger parameter, instead. This function is - * deprecated and will be removed in subsequent releases. - * - * @param cfg Configuration parameters. - * @param l Logger. - * @param token Completion token. - */ - template > - BOOST_DEPRECATED( - "The async_run overload taking a logger argument is deprecated. " - "Please pass the logger to the connection's constructor, instead, " - "and use the other async_run overloads.") - auto async_run(config const& cfg, logger l, CompletionToken&& token = {}) - { - set_stderr_logger(l.lvl, cfg); - return async_run(cfg, std::forward(token)); - } + auto [winner_index, result] = co_await capy::when_any( + detail::run(*impl_), + impl_->run_cancelled_event_.wait()); - /** - * @brief (Deprecated) Starts the underlying connection operations. - * @copydetail async_run - * - * Uses a default-constructed config object to run the connection. - * - * @par Deprecated - * This function is deprecated and will be removed in subsequent releases. - * Use the overload taking an explicit config object, instead. - * - * @param token Completion token. - */ - template > - BOOST_DEPRECATED( - "Running without an explicit config object is deprecated." - "Please create a config object and pass it to async_run.") - auto async_run(CompletionToken&& token = {}) - { - return async_run(config{}, std::forward(token)); - } - - /** @brief (Deprecated) Receives server side pushes asynchronously. - * - * When pushes arrive and there is no `async_receive` operation in - * progress, pushed data, requests, and responses will be paused - * until `async_receive` is called again. Apps will usually want - * to call `async_receive` in a loop. - * - * For an example see cpp20_subscriber.cpp. The completion token must - * have the following signature - * - * @code - * void f(system::error_code, std::size_t); - * @endcode - * - * Where the second parameter is the size of the push received in - * bytes. - * - * @par Per-operation cancellation - * This operation supports the following cancellation types: - * - * @li `asio::cancellation_type_t::terminal`. - * @li `asio::cancellation_type_t::partial`. - * @li `asio::cancellation_type_t::total`. - * - * Calling `basic_connection::cancel(operation::receive)` will - * also cancel any ongoing receive operations. - * - * @param token Completion token. - */ - template > - BOOST_DEPRECATED("Please use async_receive2 instead.") - auto async_receive(CompletionToken&& token = {}) - { - return impl_->receive_channel_.async_receive(std::forward(token)); + co_return {winner_index == 0u ? std::get<0>(result).ec : capy::error::canceled}; } /** @brief Wait for server pushes asynchronously. @@ -883,27 +675,7 @@ class basic_connection { * * @param token Completion token. */ - template > - auto async_receive2(CompletionToken&& token = {}) - { - return asio::async_compose( - detail::receive2_op{impl_.get()}, - token, - *impl_); - } - - /** @brief (Deprecated) Receives server pushes synchronously without blocking. - * - * Receives a server push synchronously by calling `try_receive` on - * the underlying channel. If the operation fails because - * `try_receive` returns `false`, `ec` will be set to - * @ref boost::redis::error::sync_receive_push_failed. - * - * @param ec Contains the error if any occurred. - * @returns The number of bytes read from the socket. - */ - BOOST_DEPRECATED("Please, use async_receive2 instead.") - std::size_t receive(system::error_code& ec) { return impl_->receive(ec); } + capy::io_task<> receive() { return detail::receive(*impl_); } /** @brief Executes commands on the Redis server asynchronously. * @@ -950,12 +722,10 @@ class basic_connection { * @param resp The response object to parse data into. * @param token Completion token. */ - template < - class Response = ignore_t, - class CompletionToken = asio::default_completion_token_t> - auto async_exec(request const& req, Response& resp = ignore, CompletionToken&& token = {}) + template + capy::io_task<> exec(request const& req, Response& resp = ignore) { - return this->async_exec(req, any_adapter{resp}, std::forward(token)); + return exec(req, any_adapter{resp}); } /** @brief Executes commands on the Redis server asynchronously. @@ -1004,10 +774,9 @@ class basic_connection { * @param adapter An adapter object referencing a response to place data into. * @param token Completion token. */ - template > - auto async_exec(request const& req, any_adapter adapter, CompletionToken&& token = {}) + capy::io_task<> exec(request const& req, any_adapter adapter) { - return impl_->async_exec(req, std::move(adapter), std::forward(token)); + return impl_->exec(req, std::move(adapter)); } /** @brief Cancel operations. @@ -1021,411 +790,22 @@ class basic_connection { * * @param op The operation to be cancelled. */ - void cancel(operation op = operation::all) { impl_->cancel(op); } - - /// Returns true if the connection will try to reconnect if an error is encountered. - bool will_reconnect() const noexcept { return impl_->will_reconnect(); } - - /** - * @brief (Deprecated) Returns the ssl context. - * - * `ssl::context` has no const methods, so this function should not be called. - * Any TLS configuration should be set up by passing an `ssl::context` - * to the connection's constructor. - * - * @returns The SSL context. - */ - BOOST_DEPRECATED( - "ssl::context has no const methods, so this function should not be called. Set up any " - "required TLS configuration before passing the ssl::context to the connection's constructor.") - asio::ssl::context const& get_ssl_context() const noexcept - { - return impl_->stream_.get_ssl_context(); - } - - /** - * @brief (Deprecated) Resets the underlying stream. - * - * This function is no longer necessary and is currently a no-op. - */ - BOOST_DEPRECATED( - "This function is no longer necessary and is currently a no-op. connection resets the stream " - "internally as required. This function will be removed in subsequent releases") - void reset_stream() { } - - /** - * @brief (Deprecated) Returns a reference to the next layer. - * - * This function returns a dummy object for connections using UNIX domain sockets. - * - * @par Deprecated - * Accessing the underlying stream is deprecated and will be removed in the next release. - * Use the other member functions to interact with the connection. - * - * @returns A reference to the underlying SSL stream object. - */ - BOOST_DEPRECATED( - "Accessing the underlying stream is deprecated and will be removed in the next release. Use " - "the other member functions to interact with the connection.") - auto& next_layer() noexcept { return impl_->stream_.next_layer(); } - - /** - * @brief (Deprecated) Returns a reference to the next layer. - * - * This function returns a dummy object for connections using UNIX domain sockets. - * - * @par Deprecated - * Accessing the underlying stream is deprecated and will be removed in the next release. - * Use the other member functions to interact with the connection. - * - * @returns A reference to the underlying SSL stream object. - */ - BOOST_DEPRECATED( - "Accessing the underlying stream is deprecated and will be removed in the next release. Use " - "the other member functions to interact with the connection.") - auto const& next_layer() const noexcept { return impl_->stream_.next_layer(); } + void cancel() { impl_->cancel(); } /// Sets the response object of @ref async_receive2 operations. - template - void set_receive_response(Response& resp) - { - impl_->set_receive_adapter(any_adapter{resp}); - } + void set_receive_response(any_adapter resp) { impl_->set_receive_adapter(std::move(resp)); } /// Returns connection usage information. usage get_usage() const noexcept { return impl_->st_.mpx.get_usage(); } private: - using clock_type = std::chrono::steady_clock; - using clock_traits_type = asio::wait_traits; - using timer_type = asio::basic_waitable_timer; - - using receive_channel_type = asio::experimental::channel< - executor_type, - void(system::error_code, std::size_t)>; - - auto use_ssl() const noexcept { return impl_->cfg_.use_ssl; } - // Used by both this class and connection void set_stderr_logger(logger::level lvl, const config& cfg) { impl_->st_.logger.lgr = detail::make_stderr_logger(lvl, cfg.log_prefix); } - // Initiation for async_run. This is required because we need access - // to the final handler (rather than the completion token) within the initiation, - // to modify the handler's cancellation slot. - struct run_initiation { - detail::connection_impl* self; - - using executor_type = Executor; - executor_type get_executor() const noexcept { return self->get_executor(); } - - template - void operator()(Handler&& handler, config const* cfg) - { - self->st_.cfg = *cfg; - self->st_.mpx.set_config(*cfg); - - // If the token's slot has cancellation enabled, it should just emit - // the cancellation signal in our connection. This lets us unify the cancel() - // function and per-operation cancellation - auto slot = asio::get_associated_cancellation_slot(handler); - if (slot.is_connected()) { - slot.template emplace>(*self); - } - - // Overwrite the token's cancellation slot: the composed operation - // should use the signal's slot so we can generate cancellations in cancel() - auto token_with_slot = asio::bind_cancellation_slot( - self->run_signal_.slot(), - std::forward(handler)); - - asio::async_compose( - detail::run_op{self}, - token_with_slot, - self->writer_cv_); - } - }; - - friend class connection; - - std::unique_ptr> impl_; -}; - -/** @brief A basic_connection that type erases the executor. - * - * This connection type uses `asio::any_io_executor` and - * `asio::any_completion_token` to reduce compilation times. - * - * For documentation of each member function see - * @ref boost::redis::basic_connection. - */ -class connection { -public: - /// Executor type. - using executor_type = asio::any_io_executor; - - /** @brief Constructor from an executor. - * - * @param ex Executor used to create all internal I/O objects. - * @param ctx SSL context. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - */ - explicit connection( - executor_type ex, - asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, - logger lgr = {}); - - /** @brief Constructor from an executor and a logger. - * - * @param ex Executor used to create all internal I/O objects. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - * - * An SSL context with default settings will be created. - */ - connection(executor_type ex, logger lgr) - : connection( - std::move(ex), - asio::ssl::context{asio::ssl::context::tlsv12_client}, - std::move(lgr)) - { } - - /** - * @brief Constructor from an `io_context`. - * - * @param ioc I/O context used to create all internal I/O objects. - * @param ctx SSL context. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - */ - explicit connection( - asio::io_context& ioc, - asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, - logger lgr = {}) - : connection(ioc.get_executor(), std::move(ctx), std::move(lgr)) - { } - - /** - * @brief Constructor from an `io_context` and a logger. - * - * @param ioc I/O context used to create all internal I/O objects. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - */ - connection(asio::io_context& ioc, logger lgr) - : connection( - ioc.get_executor(), - asio::ssl::context{asio::ssl::context::tlsv12_client}, - std::move(lgr)) - { } - - /// Returns the underlying executor. - executor_type get_executor() noexcept { return impl_.get_executor(); } - - /** - * @brief Calls @ref boost::redis::basic_connection::async_run. - * - * @param cfg Configuration parameters. - * @param token Completion token. - */ - template - auto async_run(config const& cfg, CompletionToken&& token = {}) - { - return asio::async_initiate( - initiation{this}, - token, - &cfg); - } - - /** - * @brief (Deprecated) Calls @ref boost::redis::basic_connection::async_run. - * - * This function accepts an extra logger parameter. The passed logger - * will be used by the connection, overwriting any logger passed to the connection's - * constructor. - * - * @par Deprecated - * The logger should be passed to the connection's constructor instead of using this - * function. Use the overload without a logger parameter, instead. This function is - * deprecated and will be removed in subsequent releases. - * - * @param cfg Configuration parameters. - * @param l Logger. - * @param token Completion token. - */ - template - BOOST_DEPRECATED( - "The async_run overload taking a logger argument is deprecated. " - "Please pass the logger to the connection's constructor, instead, " - "and use the other async_run overloads.") - auto async_run(config const& cfg, logger l, CompletionToken&& token = {}) - { - return asio::async_initiate( - initiation{this}, - token, - &cfg, - std::move(l)); - } - - /// @copydoc basic_connection::async_receive - template - BOOST_DEPRECATED("Please use async_receive2 instead.") - auto async_receive(CompletionToken&& token = {}) - { - return impl_.async_receive(std::forward(token)); - } - - /// @copydoc basic_connection::async_receive2 - template - auto async_receive2(CompletionToken&& token = {}) - { - return asio::async_initiate( - initiation{this}, - token); - } - - /// @copydoc basic_connection::receive - BOOST_DEPRECATED("Please use async_receive2 instead.") - std::size_t receive(system::error_code& ec) { return impl_.impl_->receive(ec); } - - /** - * @brief Calls @ref boost::redis::basic_connection::async_exec. - * - * @param req The request to be executed. - * @param resp The response object to parse data into. - * @param token Completion token. - */ - template - auto async_exec(request const& req, Response& resp = ignore, CompletionToken&& token = {}) - { - return async_exec(req, any_adapter{resp}, std::forward(token)); - } - - /** - * @brief Calls @ref boost::redis::basic_connection::async_exec. - * - * @param req The request to be executed. - * @param adapter An adapter object referencing a response to place data into. - * @param token Completion token. - */ - template - auto async_exec(request const& req, any_adapter adapter, CompletionToken&& token = {}) - { - return asio::async_initiate( - initiation{this}, - token, - &req, - std::move(adapter)); - } - - /// @copydoc basic_connection::cancel - void cancel(operation op = operation::all); - - /// @copydoc basic_connection::will_reconnect - bool will_reconnect() const noexcept { return impl_.will_reconnect(); } - - /// (Deprecated) Calls @ref boost::redis::basic_connection::next_layer. - BOOST_DEPRECATED( - "Accessing the underlying stream is deprecated and will be removed in the next release. Use " - "the other member functions to interact with the connection.") - asio::ssl::stream& next_layer() noexcept - { - return impl_.impl_->stream_.next_layer(); - } - - /// (Deprecated) Calls @ref boost::redis::basic_connection::next_layer. - BOOST_DEPRECATED( - "Accessing the underlying stream is deprecated and will be removed in the next release. Use " - "the other member functions to interact with the connection.") - asio::ssl::stream const& next_layer() const noexcept - { - return impl_.impl_->stream_.next_layer(); - } - - /// @copydoc basic_connection::reset_stream - BOOST_DEPRECATED( - "This function is no longer necessary and is currently a no-op. connection resets the stream " - "internally as required. This function will be removed in subsequent releases") - void reset_stream() { } - - /// @copydoc basic_connection::set_receive_response - template - void set_receive_response(Response& response) - { - impl_.set_receive_response(response); - } - - /// @copydoc basic_connection::get_usage - usage get_usage() const noexcept { return impl_.get_usage(); } - - /// @copydoc basic_connection::get_ssl_context - BOOST_DEPRECATED( - "ssl::context has no const methods, so this function should not be called. Set up any " - "required TLS configuration before passing the ssl::context to the connection's constructor.") - asio::ssl::context const& get_ssl_context() const noexcept - { - return impl_.impl_->stream_.get_ssl_context(); - } - -private: - // Function object to initiate the async ops that use asio::any_completion_handler. - // Required for asio::cancel_after to work. - // Since all ops have different arguments, a single struct with different overloads is enough. - struct initiation { - connection* self; - - using executor_type = asio::any_io_executor; - executor_type get_executor() const noexcept { return self->get_executor(); } - - template - void operator()(Handler&& handler, config const* cfg, logger l) - { - self->async_run_impl(*cfg, std::move(l), std::forward(handler)); - } - - template - void operator()(Handler&& handler, config const* cfg) - { - self->async_run_impl(*cfg, std::forward(handler)); - } - - template - void operator()(Handler&& handler, request const* req, any_adapter&& adapter) - { - self->async_exec_impl(*req, std::move(adapter), std::forward(handler)); - } - - template - void operator()(Handler&& handler) - { - self->async_receive2_impl(std::forward(handler)); - } - }; - - void async_run_impl( - config const& cfg, - logger&& l, - asio::any_completion_handler token); - - void async_run_impl( - config const& cfg, - asio::any_completion_handler token); - - void async_exec_impl( - request const& req, - any_adapter&& adapter, - asio::any_completion_handler token); - - void async_receive2_impl(asio::any_completion_handler token); - - basic_connection impl_; + std::unique_ptr impl_; }; } // namespace boost::redis diff --git a/include/boost/redis/corosio_connection.hpp b/include/boost/redis/corosio_connection.hpp deleted file mode 100644 index 73316371..00000000 --- a/include/boost/redis/corosio_connection.hpp +++ /dev/null @@ -1,814 +0,0 @@ -/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_COROSIO_CONNECTION_HPP -#define BOOST_REDIS_COROSIO_CONNECTION_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace boost::redis { -namespace detail { - -// Given a timeout value, compute the expiry time. A zero timeout is considered to mean "no timeout" -inline std::chrono::steady_clock::time_point compute_expiry( - std::chrono::steady_clock::duration timeout) -{ - return timeout.count() == 0 ? (std::chrono::steady_clock::time_point::max)() - : std::chrono::steady_clock::now() + timeout; -} - -inline asio::cancellation_type_t token_to_cancel(std::stop_token tok) -{ - return tok.stop_requested() ? asio::cancellation_type_t::terminal - : asio::cancellation_type_t::none; -} - -template -inline capy::io_task cancel_at( - capy::io_task task, - corosio::timer& timer, - std::chrono::steady_clock::time_point timeout) -{ - timer.expires_at(timeout); - auto [winner_index, result] = co_await capy::when_any(std::move(task), timer.wait()); - if (winner_index == 0u) - co_return std::get<0>(std::move(result)); - else - co_return {make_error_code(asio::error::operation_aborted)}; -} - -template -inline capy::io_task cancel_after( - capy::io_task task, - corosio::timer& timer, - std::chrono::steady_clock::duration timeout) -{ - return cancel_at(std::move(task), timer, std::chrono::steady_clock::now() + timeout); -} - -class corosio_redis_stream { - // TODO: UNIX sockets - corosio::tcp_socket socket_; - corosio::openssl_stream stream_; // TODO: make this configurable - corosio::timer timer_; - corosio::resolver resolv_; - redis_stream_state st_; - -public: - explicit corosio_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) - : socket_(ctx) - , stream_(&socket_, std::move(tls_ctx)) - , timer_(ctx) - , resolv_(ctx) - { } - - // I/O - capy::io_task<> connect(const connect_params& params, buffered_logger& l) - { - connect_fsm fsm{l}; - system::error_code ec; - corosio::resolver_results endpoints; - - auto act = fsm.resume(ec, st_); - - while (true) { - switch (act.type) { - case connect_action_type::unix_socket_close: - BOOST_ASSERT(false); - co_return {system::error_code(asio::error::operation_not_supported)}; - case connect_action_type::unix_socket_connect: - BOOST_ASSERT(false); - co_return {system::error_code(asio::error::operation_not_supported)}; - case connect_action_type::tcp_resolve: - { - auto result = co_await cancel_after( - [&] -> capy::io_task { - co_return co_await resolv_.resolve( - params.addr.tcp_address().host, - params.addr.tcp_address().port); - }(), - timer_, - params.resolve_timeout); - ec = result.ec; - endpoints = std::move(result.t1); - act = fsm.resume(ec, endpoints, st_); - break; - } - case connect_action_type::ssl_stream_reset: - stream_.reset(); - act = fsm.resume(ec, st_); - break; - case connect_action_type::ssl_handshake: - ec = (co_await cancel_after( - stream_.handshake(corosio::tls_stream::handshake_type::client), - timer_, - params.ssl_handshake_timeout)) - .ec; - act = fsm.resume(ec, st_); - break; - case connect_action_type::done: co_return {act.ec}; - case connect_action_type::tcp_connect: - { - // TODO: range connect - socket_.open(); - auto result = co_await cancel_after( - [&] -> capy::io_task<> { - co_return co_await socket_.connect(*endpoints.begin()); - }(), - timer_, - params.connect_timeout); - ec = result.ec; - act = fsm.resume(ec, *endpoints.begin(), st_); - break; - } - default: BOOST_ASSERT(false); - } - } - } - - template - capy::io_task write_some(const BuffType& buffers) - { - switch (st_.type) { - case transport_type::tcp: co_return co_await socket_.write_some(buffers); - case transport_type::tcp_tls: co_return co_await stream_.write_some(buffers); - case transport_type::unix_socket: - default: BOOST_ASSERT(false); co_return {}; - } - } - - template - capy::io_task read_some(const BuffType& buffers) - { - switch (st_.type) { - case transport_type::tcp: co_return co_await socket_.read_some(buffers); - case transport_type::tcp_tls: co_return co_await stream_.read_some(buffers); - case transport_type::unix_socket: - default: BOOST_ASSERT(false); co_return {}; - } - } -}; - -struct corosio_connection_impl { - capy::async_event run_cancelled_event_; - corosio_redis_stream stream_; - corosio::timer writer_timer_; // timer used for write timeouts - corosio::timer writer_cv_; // set when there is new data to write - corosio::timer reader_timer_; // timer used for read timeouts - corosio::timer reconnect_timer_; // to wait the reconnection period - corosio::timer ping_timer_; // to wait between pings - flow_controller controller_; - connection_state st_; - - corosio_connection_impl( - capy::execution_context& ctx, - corosio::tls_context&& ssl_ctx, - logger&& lgr) - : stream_{ctx, std::move(ssl_ctx)} - , writer_timer_{ctx} - , writer_cv_{ctx} - , reader_timer_{ctx} - , reconnect_timer_{ctx} - , ping_timer_{ctx} - , controller_{1024u * 1024u * 16u} // 16MB, TODO: make it configurable - , st_{{std::move(lgr)}} - { - set_receive_adapter(any_adapter{ignore}); - writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); - } - - void cancel() - { - // exec - st_.mpx.cancel_waiting(); - - // receive (TODO: do we really need this?) - st_.receive2_cancelled = true; - - // reconnect (TODO: do we really need this?) - st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); - - // run - run_cancelled_event_.set(); - } - - capy::io_task<> exec(request const& req, any_adapter adapter) - { - // Setup - capy::async_event request_done; - auto elem = make_elem(req, std::move(adapter)); - elem->set_done_callback([&request_done]() { - request_done.set(); - }); - exec_fsm fsm{elem}; - - // Invoke the FSM - while (true) { - // Invoke the state machine - auto act = fsm.resume(true, st_, token_to_cancel(co_await capy::this_coro::stop_token)); - - // Do what the FSM said - switch (act.type()) { - case exec_action_type::setup_cancellation: break; // ignored, not required by capy - case exec_action_type::immediate: break; // ignored, not required by capy - case exec_action_type::notify_writer: writer_cv_.cancel(); break; - case exec_action_type::wait_for_response: - { - auto [ec] = co_await request_done.wait(); - ignore_unused(ec); // TODO: we should likely use this - break; - } - case exec_action_type::done: co_return {act.error()}; - } - } - } - - void set_receive_adapter(any_adapter adapter) - { - st_.mpx.set_receive_adapter(std::move(adapter)); - } -}; - -inline capy::io_task<> receive(corosio_connection_impl& conn) -{ - // Setup - receive_fsm fsm; - system::error_code ec; - - while (true) { - receive_action act = fsm.resume( - conn.st_, - ec, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type) { - case receive_action::action_type::setup_cancellation: break; // not required here - case receive_action::action_type::wait: - { - auto [controller_ec] = co_await conn.controller_.take(); - ec = controller_ec; - break; - } - case receive_action::action_type::drain_channel: break; // not required - case receive_action::action_type::immediate: break; // not required - case receive_action::action_type::done: co_return {act.ec}; - } - } -} - -inline capy::io_task<> async_exec_one( - corosio_connection_impl& conn, - const request& req, - any_adapter resp) -{ - exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; - system::error_code ec; - std::size_t bytes = 0u; - - while (true) { - exec_one_action act = fsm.resume( - conn.st_.mpx.get_read_buffer(), - ec, - bytes, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type) { - case exec_one_action_type::done: co_return {ec}; - case exec_one_action_type::write: - { - auto [write_ec, write_bytes] = co_await capy::write( - conn.stream_, - capy::make_buffer(req.payload())); - ec = write_ec; - bytes = write_bytes; - break; - } - case exec_one_action_type::read_some: - { - auto [read_ec, read_bytes] = co_await conn.stream_.read_some( - capy::make_buffer(conn.st_.mpx.get_read_buffer().get_prepared())); - ec = read_ec; - bytes = read_bytes; - break; - } - } - } -} - -inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) -{ - // Setup - sentinel_resolve_fsm fsm; - system::error_code ec; - - while (true) { - sentinel_action act = fsm.resume( - conn.st_, - ec, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.get_type()) { - case sentinel_action::type::done: co_return {act.error()}; - case sentinel_action::type::connect: - { - auto [connect_ec] = co_await conn.stream_.connect( - make_sentinel_connect_params(conn.st_.cfg, act.connect_addr()), - conn.st_.logger); - ec = connect_ec; - break; - } - case sentinel_action::type::request: - { - auto [request_ec] = co_await cancel_after( - async_exec_one(conn, conn.st_.cfg.sentinel.setup, make_sentinel_adapter(conn.st_)), - conn.reconnect_timer_, - conn.st_.cfg.sentinel.request_timeout); - ec = request_ec; - break; - } - } - } -} - -inline capy::io_task<> writer(corosio_connection_impl& conn) -{ - // Setup - writer_fsm fsm; - system::error_code ec; - std::size_t bytes_written = 0u; - - while (true) { - writer_action act = fsm.resume( - conn.st_, - ec, - bytes_written, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type()) { - case writer_action_type::done: co_return {act.error()}; - case writer_action_type::write_some: - { - auto [write_ec, write_bytes] = co_await cancel_at( - conn.stream_.write_some(capy::make_buffer(conn.st_.mpx.get_write_buffer())), - conn.writer_timer_, - compute_expiry(act.timeout())); - ec = write_ec; - bytes_written = write_bytes; - break; - } - case writer_action_type::wait: - { - conn.writer_cv_.expires_at(compute_expiry(act.timeout())); - auto [wait_ec] = co_await conn.writer_cv_.wait(); - ec = wait_ec; - bytes_written = 0u; - break; - } - } - } -} - -inline capy::io_task<> reader(corosio_connection_impl& conn) -{ - reader_fsm fsm; - std::size_t n = 0u; - system::error_code ec; - - for (;;) { - auto act = fsm.resume(conn.st_, n, ec, token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.get_type()) { - case reader_fsm::action::type::read_some: - { - auto [read_ec, read_bytes] = co_await cancel_at( - conn.stream_.read_some(capy::make_buffer(conn.st_.mpx.get_prepared_read_buffer())), - conn.reader_timer_, - compute_expiry(act.timeout())); - ec = read_ec; - n = read_bytes; - break; - } - case reader_fsm::action::type::notify_push_receiver: - { - // TODO: re-work this - auto [notify_ec] = co_await conn.controller_.wait_for_space(); - if (notify_ec) - ec = notify_ec; - else - conn.controller_.put(act.push_size()); - } - case reader_fsm::action::type::done: co_return {act.error()}; - } - } -} - -inline capy::io_task<> run(corosio_connection_impl& conn) -{ - run_fsm fsm; - system::error_code ec; - - while (true) { - auto act = fsm.resume(conn.st_, ec, token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type) { - case run_action_type::done: co_return {act.ec}; - case run_action_type::immediate: break; // no longer required - case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve(conn)).ec; break; - case run_action_type::connect: - ec = (co_await conn.stream_.connect(make_run_connect_params(conn.st_), conn.st_.logger)) - .ec; - break; - case run_action_type::parallel_group: - { - auto [winner_index, result] = co_await capy::when_any(reader(conn), writer(conn)); - ignore_unused(winner_index); - ec = std::get<0>(result).ec; - break; - } - case run_action_type::cancel_receive: break; // no longer required - case run_action_type::wait_for_reconnection: - conn.reconnect_timer_.expires_after(conn.st_.cfg.reconnect_wait_interval); - ec = (co_await conn.reconnect_timer_.wait()).ec; - break; - } - } -} - -logger make_stderr_logger(logger::level lvl, std::string prefix); - -} // namespace detail - -/** @brief A SSL connection to the Redis server. - * - * This class keeps a healthy connection to the Redis instance where - * commands can be sent at any time. For more details, please see the - * documentation of each individual function. - * - * @tparam Executor The executor type used to create any required I/O objects. - */ -class connection { -public: - /** @brief Constructor from an executor. - * - * @param ex Executor used to create all internal I/O objects. - * @param ctx SSL context. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - */ - explicit connection( - capy::execution_context& ctx, - corosio::tls_context ssl_ctx = {}, - logger lgr = {}) - : impl_( - std::make_unique(ctx, std::move(ssl_ctx), std::move(lgr))) - { } - - /** @brief Constructor from an executor and a logger. - * - * @param ex Executor used to create all internal I/O objects. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - * - * An SSL context with default settings will be created. - */ - connection(capy::execution_context& ctx, logger lgr) - : connection(ctx, {}, std::move(lgr)) - { } - - /** @brief Starts the underlying connection operations. - * - * This function establishes a connection to the Redis server and keeps - * it healthy by performing the following operations: - * - * @li For Sentinel deployments (`config::sentinel::addresses` is not empty), - * contacts Sentinels to obtain the address of the configured master. - * @li For TCP connections, resolves the server hostname passed in - * @ref boost::redis::config::addr. - * @li Establishes a physical connection to the server. For TCP connections, - * connects to one of the endpoints obtained during name resolution. - * For UNIX domain socket connections, it connects to @ref boost::redis::config::unix_sockets. - * @li If @ref boost::redis::config::use_ssl is `true`, performs the TLS handshake. - * @li Executes the setup request, as defined by the passed @ref config object. - * By default, this is a `HELLO` command, but it can contain any other arbitrary - * commands. See the @ref config::setup docs for more info. - * @li Starts a health-check operation where `PING` commands are sent - * at intervals specified by - * @ref config::health_check_interval when the connection is idle. - * See the documentation of @ref config::health_check_interval for more info. - * @li Starts read and write operations. Requests issued using @ref async_exec - * before `async_run` is called will be written to the server immediately. - * - * When a connection is lost for any reason, a new one is - * established automatically. To disable reconnection - * set @ref boost::redis::config::reconnect_wait_interval to zero. - * - * The completion token must have the following signature - * - * @code - * void f(system::error_code); - * @endcode - * - * @par Per-operation cancellation - * This operation supports the following cancellation types: - * - * @li `asio::cancellation_type_t::terminal`. - * @li `asio::cancellation_type_t::partial`. - * - * In both cases, cancellation is equivalent to calling @ref basic_connection::cancel - * passing @ref operation::run as argument. - * - * After the operation completes, the token's associated cancellation slot - * may still have a cancellation handler associated to this connection. - * You should make sure to not invoke it after the connection has been destroyed. - * This is consistent with what other Asio I/O objects do. - * - * For example on how to call this function refer to - * cpp20_intro.cpp or any other example. - * - * @param cfg Configuration parameters. - * @param token Completion token. - */ - capy::io_task<> run(config const& cfg) - { - impl_->st_.cfg = cfg; - impl_->st_.mpx.set_config(cfg); - impl_->run_cancelled_event_.clear(); - - auto [winner_index, result] = co_await capy::when_any( - detail::run(*impl_), - impl_->run_cancelled_event_.wait()); - - co_return {winner_index == 0u ? std::get<0>(result).ec : capy::error::canceled}; - } - - /** @brief Wait for server pushes asynchronously. - * - * This function suspends until at least one server push is received by the - * connection. On completion an unspecified number of pushes will - * have been added to the response object set with @ref - * set_receive_response. Use the functions in the response object - * to know how many messages they were received and consume them. - * - * To prevent receiving an unbound number of pushes the connection - * blocks further read operations on the socket when 256 pushes - * accumulate internally (we don't make any commitment to this - * exact number). When that happens any `async_exec`s and - * health-checks won't make any progress and the connection may - * eventually timeout. To avoid this, apps that expect server pushes - * should call this function continuously in a loop. - * - * This function should be used instead of the deprecated @ref async_receive. - * It differs from `async_receive` in the following: - * - * @li `async_receive` is designed to consume a single push message at a time. - * This can be inefficient when receiving lots of server pushes. - * `async_receive2` is batch-oriented. All pushes that are available - * when `async_receive2` is called will be marked as consumed. - * @li `async_receive` is cancelled when a reconnection happens (e.g. because - * of a network error). This enabled the user to re-establish subscriptions - * using @ref async_exec before waiting for pushes again. With the introduction of - * functions like @ref request::subscribe, subscriptions are automatically - * re-established on reconnection. Thus, `async_receive2` is not cancelled - * on reconnection. - * @li `async_receive` passes the number of bytes that each received - * push message contains. This information is unreliable and not very useful. - * Equivalent information is available using functions in the response object. - * @li `async_receive` might get cancelled if `async_run` is cancelled. - * This doesn't happen with `async_receive2`. - * - * This function does *not* remove messages from the response object - * passed to @ref set_receive_response - use the functions in the response - * object to achieve this. - * - * Only a single instance of `async_receive2` may be outstanding - * for a given connection at any time. Trying to start a second one - * will fail with @ref error::already_running. - * - * @note To avoid deadlocks the task (e.g. coroutine) calling - * `async_receive2` should not call `async_exec` in a way where - * they could block each other. This is, avoid the following pattern: - * - * @code - * asio::awaitable receiver() - * { - * // Do NOT do this!!! The receive buffer might get full while - * // async_exec runs, which will block all read operations until async_receive2 - * // is called. The two operations end up waiting each other, making the connection unresponsive. - * // If you need to do this, use two connections, instead. - * co_await conn.async_receive2(); - * co_await conn.async_exec(req, resp); - * } - * @endcode - * - * For an example see cpp20_subscriber.cpp. - * - * The completion token must have the following signature: - * - * @code - * void f(system::error_code); - * @endcode - * - * @par Per-operation cancellation - * This operation supports the following cancellation types: - * - * @li `asio::cancellation_type_t::terminal`. - * @li `asio::cancellation_type_t::partial`. - * @li `asio::cancellation_type_t::total`. - * - * @param token Completion token. - */ - capy::io_task<> receive() { return detail::receive(*impl_); } - - /** @brief Executes commands on the Redis server asynchronously. - * - * This function sends a request to the Redis server and waits for - * the responses to each individual command in the request. If the - * request contains only commands that don't expect a response, - * the completion occurs after it has been written to the - * underlying stream. Multiple concurrent calls to this function - * will be automatically queued by the implementation. - * - * For an example see cpp20_echo_server.cpp. - * - * The completion token must have the following signature: - * - * @code - * void f(system::error_code, std::size_t); - * @endcode - * - * Where the second parameter is the size of the response received - * in bytes. - * - * @par Per-operation cancellation - * This operation supports per-operation cancellation. Depending on the state of the request - * when cancellation is requested, we can encounter two scenarios: - * - * @li If the request hasn't been sent to the server yet, cancellation will prevent it - * from being sent to the server. In this situation, all cancellation types are supported - * (`asio::cancellation_type_t::terminal`, `asio::cancellation_type_t::partial` and - * `asio::cancellation_type_t::total`). - * @li If the request has been sent to the server but the response hasn't arrived yet, - * cancellation will cause `async_exec` to complete immediately. When the response - * arrives from the server, it will be ignored. In this situation, only - * `asio::cancellation_type_t::terminal` and `asio::cancellation_type_t::partial` - * are supported. Cancellation requests specifying `asio::cancellation_type_t::total` - * only will be ignored. - * - * In any case, connections can be safely used after cancelling `async_exec` operations. - * - * @par Object lifetimes - * Both `req` and `res` should be kept alive until the operation completes. - * No copies of the request object are made. - * - * @param req The request to be executed. - * @param resp The response object to parse data into. - * @param token Completion token. - */ - template - capy::io_task<> exec(request const& req, Response& resp = ignore) - { - return exec(req, any_adapter{resp}); - } - - /** @brief Executes commands on the Redis server asynchronously. - * - * This function sends a request to the Redis server and waits for - * the responses to each individual command in the request. If the - * request contains only commands that don't expect a response, - * the completion occurs after it has been written to the - * underlying stream. Multiple concurrent calls to this function - * will be automatically queued by the implementation. - * - * For an example see cpp20_echo_server.cpp. - * - * The completion token must have the following signature: - * - * @code - * void f(system::error_code, std::size_t); - * @endcode - * - * Where the second parameter is the size of the response received - * in bytes. - * - * @par Per-operation cancellation - * This operation supports per-operation cancellation. Depending on the state of the request - * when cancellation is requested, we can encounter two scenarios: - * - * @li If the request hasn't been sent to the server yet, cancellation will prevent it - * from being sent to the server. In this situation, all cancellation types are supported - * (`asio::cancellation_type_t::terminal`, `asio::cancellation_type_t::partial` and - * `asio::cancellation_type_t::total`). - * @li If the request has been sent to the server but the response hasn't arrived yet, - * cancellation will cause `async_exec` to complete immediately. When the response - * arrives from the server, it will be ignored. In this situation, only - * `asio::cancellation_type_t::terminal` and `asio::cancellation_type_t::partial` - * are supported. Cancellation requests specifying `asio::cancellation_type_t::total` - * only will be ignored. - * - * In any case, connections can be safely used after cancelling `async_exec` operations. - * - * @par Object lifetimes - * Both `req` and any response object referenced by `adapter` - * should be kept alive until the operation completes. - * No copies of the request object are made. - * - * @param req The request to be executed. - * @param adapter An adapter object referencing a response to place data into. - * @param token Completion token. - */ - capy::io_task<> exec(request const& req, any_adapter adapter) - { - return impl_->exec(req, std::move(adapter)); - } - - /** @brief Cancel operations. - * - * @li `operation::exec`: cancels operations started with - * `async_exec`. Affects only requests that haven't been written - * yet. - * @li `operation::run`: cancels the `async_run` operation. - * @li `operation::receive`: cancels any ongoing calls to `async_receive`. - * @li `operation::all`: cancels all operations listed above. - * - * @param op The operation to be cancelled. - */ - void cancel() { impl_->cancel(); } - - /// Sets the response object of @ref async_receive2 operations. - void set_receive_response(any_adapter resp) { impl_->set_receive_adapter(std::move(resp)); } - - /// Returns connection usage information. - usage get_usage() const noexcept { return impl_->st_.mpx.get_usage(); } - -private: - // Used by both this class and connection - void set_stderr_logger(logger::level lvl, const config& cfg) - { - impl_->st_.logger.lgr = detail::make_stderr_logger(lvl, cfg.log_prefix); - } - - std::unique_ptr impl_; -}; - -} // namespace boost::redis - -#endif // BOOST_REDIS_CONNECTION_HPP diff --git a/include/boost/redis/detail/redis_stream.hpp b/include/boost/redis/detail/redis_stream.hpp deleted file mode 100644 index 349cc650..00000000 --- a/include/boost/redis/detail/redis_stream.hpp +++ /dev/null @@ -1,251 +0,0 @@ -/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com), - * Ruben Perez Hidalgo (rubenperez038 at gmail dot com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ -#ifndef BOOST_REDIS_REDIS_STREAM_HPP -#define BOOST_REDIS_REDIS_STREAM_HPP - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace boost { -namespace redis { -namespace detail { - -template -class redis_stream { - asio::ssl::context ssl_ctx_; - asio::ip::basic_resolver resolv_; - asio::ssl::stream> stream_; -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS - asio::basic_stream_socket unix_socket_; -#endif - typename asio::steady_timer::template rebind_executor::other timer_; - redis_stream_state st_; - - void reset_stream() { stream_ = {resolv_.get_executor(), ssl_ctx_}; } - - struct connect_op { - redis_stream& obj_; - connect_fsm fsm_; - connect_params params_; - - template - void execute_action(Self& self, connect_action act) - { - // Prevent use-after-move errors - auto& obj = this->obj_; - auto params = this->params_; - - switch (act.type) { - case connect_action_type::unix_socket_close: -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS - { - system::error_code ec; - obj.unix_socket_.close(ec); - (*this)(self, ec); // This is a sync action - } -#else - BOOST_ASSERT(false); -#endif - return; - case connect_action_type::unix_socket_connect: -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS - obj.unix_socket_.async_connect( - params.addr.unix_socket(), - asio::cancel_after(obj.timer_, params.connect_timeout, std::move(self))); -#else - BOOST_ASSERT(false); -#endif - return; - - case connect_action_type::tcp_resolve: - obj.resolv_.async_resolve( - params.addr.tcp_address().host, - params.addr.tcp_address().port, - asio::cancel_after(obj.timer_, params.resolve_timeout, std::move(self))); - return; - case connect_action_type::ssl_stream_reset: - obj.reset_stream(); - // this action does not require yielding. Execute the next action immediately - (*this)(self); - return; - case connect_action_type::ssl_handshake: - obj.stream_.async_handshake( - asio::ssl::stream_base::client, - asio::cancel_after(obj.timer_, params.ssl_handshake_timeout, std::move(self))); - return; - case connect_action_type::done: self.complete(act.ec); break; - // Connect should use the specialized handler, where resolver results are available - case connect_action_type::tcp_connect: - default: BOOST_ASSERT(false); - } - } - - // This overload will be used for connects - template - void operator()( - Self& self, - system::error_code ec, - const asio::ip::tcp::endpoint& selected_endpoint) - { - auto act = fsm_.resume( - ec, - selected_endpoint, - obj_.st_, - self.get_cancellation_state().cancelled()); - execute_action(self, act); - } - - // This overload will be used for resolves - template - void operator()( - Self& self, - system::error_code ec, - asio::ip::tcp::resolver::results_type endpoints) - { - auto act = fsm_.resume(ec, endpoints, obj_.st_, self.get_cancellation_state().cancelled()); - if (act.type == connect_action_type::tcp_connect) { - auto& obj = this->obj_; // prevent use-after-move errors - asio::async_connect( - obj.stream_.next_layer(), - std::move(endpoints), - asio::cancel_after(obj.timer_, params_.connect_timeout, std::move(self))); - } else { - execute_action(self, act); - } - } - - template - void operator()(Self& self, system::error_code ec = {}) - { - auto act = fsm_.resume(ec, obj_.st_, self.get_cancellation_state().cancelled()); - execute_action(self, act); - } - }; - -public: - explicit redis_stream(Executor ex, asio::ssl::context&& ssl_ctx) - : ssl_ctx_{std::move(ssl_ctx)} - , resolv_{ex} - , stream_{ex, ssl_ctx_} -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS - , unix_socket_{ex} -#endif - , timer_{std::move(ex)} - { } - - // Executor. Required to satisfy the AsyncStream concept - using executor_type = Executor; - executor_type get_executor() noexcept { return resolv_.get_executor(); } - - // Accessors - const auto& get_ssl_context() const noexcept { return ssl_ctx_; } - bool is_open() const - { -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS - if (st_.type == transport_type::unix_socket) - return unix_socket_.is_open(); -#endif - return stream_.next_layer().is_open(); - } - auto& next_layer() { return stream_; } - const auto& next_layer() const { return stream_; } - - // I/O - template - auto async_connect(const connect_params& params, buffered_logger& l, CompletionToken&& token) - { - this->st_.type = params.addr.type(); - return asio::async_compose( - connect_op{*this, connect_fsm{l}, params}, - token); - } - - // These functions should only be used with callbacks (e.g. within async_compose function bodies) - template - void async_write_some(const ConstBufferSequence& buffers, CompletionToken&& token) - { - switch (st_.type) { - case transport_type::tcp: - { - stream_.next_layer().async_write_some(buffers, std::forward(token)); - break; - } - case transport_type::tcp_tls: - { - stream_.async_write_some(buffers, std::forward(token)); - break; - } -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS - case transport_type::unix_socket: - { - unix_socket_.async_write_some(buffers, std::forward(token)); - break; - } -#endif - default: BOOST_ASSERT(false); - } - } - - template - void async_read_some(const MutableBufferSequence& buffers, CompletionToken&& token) - { - switch (st_.type) { - case transport_type::tcp: - { - return stream_.next_layer().async_read_some( - buffers, - std::forward(token)); - break; - } - case transport_type::tcp_tls: - { - return stream_.async_read_some(buffers, std::forward(token)); - break; - } -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS - case transport_type::unix_socket: - { - unix_socket_.async_read_some(buffers, std::forward(token)); - break; - } -#endif - default: BOOST_ASSERT(false); - } - } - - // Cancels resolve operations. Resolve operations don't support per-operation - // cancellation, but resolvers have a cancel() function. Resolve operations are - // in general blocking and run in a separate thread. cancel() has effect only - // if the operation hasn't started yet. Still, trying is better than nothing - void cancel_resolve() { resolv_.cancel(); } -}; - -} // namespace detail -} // namespace redis -} // namespace boost - -#endif diff --git a/test/test_conn_setup.cpp b/test/test_conn_setup.cpp index 5e03b397..c2becb62 100644 --- a/test/test_conn_setup.cpp +++ b/test/test_conn_setup.cpp @@ -7,7 +7,7 @@ // #include -#include +#include #include #include From 7589bbb17f1594460cdb06828a5144c3d98482da Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 19:58:59 +0100 Subject: [PATCH 019/115] trim asio includes --- include/boost/redis/detail/connect_fsm.hpp | 1 - include/boost/redis/impl/connect_fsm.ipp | 1 - include/boost/redis/impl/run_fsm.ipp | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/include/boost/redis/detail/connect_fsm.hpp b/include/boost/redis/detail/connect_fsm.hpp index a4139b9b..b7c84eed 100644 --- a/include/boost/redis/detail/connect_fsm.hpp +++ b/include/boost/redis/detail/connect_fsm.hpp @@ -9,7 +9,6 @@ #ifndef BOOST_REDIS_CONNECT_FSM_HPP #define BOOST_REDIS_CONNECT_FSM_HPP -#include #include #include #include diff --git a/include/boost/redis/impl/connect_fsm.ipp b/include/boost/redis/impl/connect_fsm.ipp index 2ef7ccd4..8230b233 100644 --- a/include/boost/redis/impl/connect_fsm.ipp +++ b/include/boost/redis/impl/connect_fsm.ipp @@ -11,7 +11,6 @@ #include #include -#include #include #include diff --git a/include/boost/redis/impl/run_fsm.ipp b/include/boost/redis/impl/run_fsm.ipp index d477d549..a7e11336 100644 --- a/include/boost/redis/impl/run_fsm.ipp +++ b/include/boost/redis/impl/run_fsm.ipp @@ -21,7 +21,7 @@ #include #include -#include // for BOOST_ASIO_HAS_LOCAL_SOCKETS +// #include // for BOOST_ASIO_HAS_LOCAL_SOCKETS #include namespace boost::redis::detail { From 09b949768f391043ed2f68b6a553f678e2a3b967 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 20:02:45 +0100 Subject: [PATCH 020/115] run_coroutine_test => common --- test/common.cpp | 10 +++++++ test/common.hpp | 4 +++ test/test_conn_setup.cpp | 65 ++++++++++++++++------------------------ 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/test/common.cpp b/test/common.cpp index 997132ae..4b1b8655 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -1,6 +1,8 @@ #include +#include #include +#include #include "common.hpp" @@ -58,3 +60,11 @@ boost::redis::logger make_string_logger(std::string& to) to += '\n'; }}; } + +void run_coroutine_test(boost::capy::task test) +{ + // TODO: test timeout + boost::corosio::io_context ctx; + boost::capy::run_async(ctx.get_executor())(std::move(test)); + ctx.run(); +} diff --git a/test/common.hpp b/test/common.hpp index 077b956b..dc3831aa 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -3,6 +3,8 @@ #include #include +#include + #include #include #include @@ -24,3 +26,5 @@ std::string_view find_client_info(std::string_view client_info, std::string_view // void create_user(std::string_view port, std::string_view username, std::string_view password); boost::redis::logger make_string_logger(std::string& to); + +void run_coroutine_test(boost::capy::task test); diff --git a/test/test_conn_setup.cpp b/test/test_conn_setup.cpp index c2becb62..fec28bc9 100644 --- a/test/test_conn_setup.cpp +++ b/test/test_conn_setup.cpp @@ -19,7 +19,8 @@ #include #include #include -#include + +#include "common.hpp" #include #include @@ -27,41 +28,27 @@ #include namespace asio = boost::asio; -namespace redis = boost::redis; - -// Declarations from common.cpp (avoid including common.hpp to prevent asio connection conflict) -boost::redis::config make_test_config(); -std::string get_server_hostname(); -std::string_view find_client_info(std::string_view client_info, std::string_view key); -boost::redis::logger make_string_logger(std::string& to); -using namespace redis; + +using namespace boost::redis; using namespace std::chrono_literals; namespace capy = boost::capy; namespace corosio = boost::corosio; namespace { -void run_coroutine_test(capy::task test) -{ - // TODO: test timeout - corosio::io_context ctx; - capy::run_async(ctx.get_executor())(std::move(test)); - ctx.run(); -} - capy::task create_user( std::string_view port, std::string_view username, std::string_view password) { - redis::connection conn{(co_await capy::this_coro::executor).context()}; + connection conn{(co_await capy::this_coro::executor).context()}; auto exec_fn = [&]() -> capy::task { // Enable the user and grant them permissions on everything - redis::request req; + request req; req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); - auto [ec] = co_await conn.exec(req, redis::ignore); + auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, std::error_code()); }; @@ -79,13 +66,13 @@ capy::task create_user( capy::task<> test_auth_success() { // Setup - redis::connection conn{(co_await capy::this_coro::executor).context()}; + connection conn{(co_await capy::this_coro::executor).context()}; auto request_fn = [&] -> capy::task { // This request should return the username we're logged in as - redis::request req; + request req; req.push("ACL", "WHOAMI"); - redis::response resp; + response resp; auto [ec] = co_await conn.exec(req, resp); BOOST_TEST_EQ(ec, std::error_code()); @@ -110,7 +97,7 @@ capy::task<> test_auth_failure() { // Setup std::string logs; - redis::connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; // Disable reconnection so the hello error causes the connection to exit auto cfg = make_test_config(); @@ -119,7 +106,7 @@ capy::task<> test_auth_failure() cfg.reconnect_wait_interval = 0s; auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(redis::error::resp3_hello)); + BOOST_TEST_EQ(ec, std::error_code(error::resp3_hello)); // Check the log if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) { @@ -130,12 +117,12 @@ capy::task<> test_auth_failure() capy::task<> test_database_index() { // Setup - redis::connection conn{(co_await capy::this_coro::executor).context()}; + connection conn{(co_await capy::this_coro::executor).context()}; auto request_fn = [&] -> capy::task { - redis::request req; + request req; req.push("CLIENT", "INFO"); - redis::response resp; + response resp; auto [ec] = co_await conn.exec(req, resp); @@ -158,12 +145,12 @@ capy::task<> test_database_index() capy::task<> test_setup_empty() { // Setup - redis::connection conn{(co_await capy::this_coro::executor).context()}; + connection conn{(co_await capy::this_coro::executor).context()}; auto request_fn = [&] -> capy::task { - redis::request req; + request req; req.push("CLIENT", "INFO"); - redis::response resp; + response resp; auto [ec] = co_await conn.exec(req, resp); @@ -186,12 +173,12 @@ capy::task<> test_setup_empty() capy::task<> test_setup_hello() { // Setup - redis::connection conn{(co_await capy::this_coro::executor).context()}; + connection conn{(co_await capy::this_coro::executor).context()}; auto request_fn = [&] -> capy::task { - redis::request req; + request req; req.push("CLIENT", "INFO"); - redis::response resp; + response resp; auto [ec] = co_await conn.exec(req, resp); @@ -218,12 +205,12 @@ capy::task<> test_setup_hello() capy::task<> test_setup_no_hello() { // Setup - redis::connection conn{(co_await capy::this_coro::executor).context()}; + connection conn{(co_await capy::this_coro::executor).context()}; auto request_fn = [&] -> capy::task { - redis::request req; + request req; req.push("CLIENT", "INFO"); - redis::response resp; + response resp; auto [ec] = co_await conn.exec(req, resp); @@ -249,7 +236,7 @@ capy::task<> test_setup_failure() { // Setup std::string logs; - redis::connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; // Disable reconnection so the hello error causes the connection to exit auto cfg = make_test_config(); @@ -259,7 +246,7 @@ capy::task<> test_setup_failure() cfg.reconnect_wait_interval = 0s; auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(redis::error::resp3_hello)); + BOOST_TEST_EQ(ec, std::error_code(error::resp3_hello)); // Check the log if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) { From 653819e3fe76c6dd16f804f7e7bc5b12299d9b9a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 20:06:00 +0100 Subject: [PATCH 021/115] test timeout --- test/common.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test/common.cpp b/test/common.cpp index 4b1b8655..8d73ee36 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -9,6 +9,7 @@ #include #include #include +#include using namespace std::chrono_literals; @@ -63,8 +64,18 @@ boost::redis::logger make_string_logger(std::string& to) void run_coroutine_test(boost::capy::task test) { - // TODO: test timeout + // Set a timeout to the tests, so they don't hang on error + bool finished = false; + auto wrapper_fn = [test = std::move(test), &finished]() mutable -> boost::capy::task { + co_await std::move(test); + finished = true; + }; + + // Actually run the test boost::corosio::io_context ctx; - boost::capy::run_async(ctx.get_executor())(std::move(test)); - ctx.run(); + boost::capy::run_async(ctx.get_executor())(wrapper_fn()); + ctx.run_for(test_timeout); + + // Check that it finished + BOOST_TEST(finished); } From 0a5e4a1636221b4027a280116b1a80fe19bcb123 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 20:31:55 +0100 Subject: [PATCH 022/115] Move to compiled --- include/boost/redis/connection.hpp | 402 +---------------------- include/boost/redis/impl/connection.ipp | 414 ++++++++++++++++++++++-- include/boost/redis/src.hpp | 2 +- 3 files changed, 394 insertions(+), 424 deletions(-) diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index 2e7999d9..f1e9782b 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -65,43 +65,6 @@ namespace boost::redis { namespace detail { -// Given a timeout value, compute the expiry time. A zero timeout is considered to mean "no timeout" -inline std::chrono::steady_clock::time_point compute_expiry( - std::chrono::steady_clock::duration timeout) -{ - return timeout.count() == 0 ? (std::chrono::steady_clock::time_point::max)() - : std::chrono::steady_clock::now() + timeout; -} - -inline asio::cancellation_type_t token_to_cancel(std::stop_token tok) -{ - return tok.stop_requested() ? asio::cancellation_type_t::terminal - : asio::cancellation_type_t::none; -} - -template -inline capy::io_task cancel_at( - capy::io_task task, - corosio::timer& timer, - std::chrono::steady_clock::time_point timeout) -{ - timer.expires_at(timeout); - auto [winner_index, result] = co_await capy::when_any(std::move(task), timer.wait()); - if (winner_index == 0u) - co_return std::get<0>(std::move(result)); - else - co_return {make_error_code(asio::error::operation_aborted)}; -} - -template -inline capy::io_task cancel_after( - capy::io_task task, - corosio::timer& timer, - std::chrono::steady_clock::duration timeout) -{ - return cancel_at(std::move(task), timer, std::chrono::steady_clock::now() + timeout); -} - class corosio_redis_stream { // TODO: UNIX sockets corosio::tcp_socket socket_; @@ -119,68 +82,7 @@ class corosio_redis_stream { { } // I/O - capy::io_task<> connect(const connect_params& params, buffered_logger& l) - { - connect_fsm fsm{l}; - system::error_code ec; - corosio::resolver_results endpoints; - - auto act = fsm.resume(ec, st_); - - while (true) { - switch (act.type) { - case connect_action_type::unix_socket_close: - BOOST_ASSERT(false); - co_return {system::error_code(asio::error::operation_not_supported)}; - case connect_action_type::unix_socket_connect: - BOOST_ASSERT(false); - co_return {system::error_code(asio::error::operation_not_supported)}; - case connect_action_type::tcp_resolve: - { - auto result = co_await cancel_after( - [&] -> capy::io_task { - co_return co_await resolv_.resolve( - params.addr.tcp_address().host, - params.addr.tcp_address().port); - }(), - timer_, - params.resolve_timeout); - ec = result.ec; - endpoints = std::move(result.t1); - act = fsm.resume(ec, endpoints, st_); - break; - } - case connect_action_type::ssl_stream_reset: - stream_.reset(); - act = fsm.resume(ec, st_); - break; - case connect_action_type::ssl_handshake: - ec = (co_await cancel_after( - stream_.handshake(corosio::tls_stream::handshake_type::client), - timer_, - params.ssl_handshake_timeout)) - .ec; - act = fsm.resume(ec, st_); - break; - case connect_action_type::done: co_return {act.ec}; - case connect_action_type::tcp_connect: - { - // TODO: range connect - socket_.open(); - auto result = co_await cancel_after( - [&] -> capy::io_task<> { - co_return co_await socket_.connect(*endpoints.begin()); - }(), - timer_, - params.connect_timeout); - ec = result.ec; - act = fsm.resume(ec, *endpoints.begin(), st_); - break; - } - default: BOOST_ASSERT(false); - } - } - } + capy::io_task<> connect(const connect_params& params, buffered_logger& l); template capy::io_task write_some(const BuffType& buffers) @@ -219,279 +121,15 @@ struct corosio_connection_impl { corosio_connection_impl( capy::execution_context& ctx, corosio::tls_context&& ssl_ctx, - logger&& lgr) - : stream_{ctx, std::move(ssl_ctx)} - , writer_timer_{ctx} - , writer_cv_{ctx} - , reader_timer_{ctx} - , reconnect_timer_{ctx} - , ping_timer_{ctx} - , controller_{1024u * 1024u * 16u} // 16MB, TODO: make it configurable - , st_{{std::move(lgr)}} - { - set_receive_adapter(any_adapter{ignore}); - writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); - } - - void cancel() - { - // exec - st_.mpx.cancel_waiting(); - - // receive (TODO: do we really need this?) - st_.receive2_cancelled = true; - - // reconnect (TODO: do we really need this?) - st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); - - // run - run_cancelled_event_.set(); - } - - capy::io_task<> exec(request const& req, any_adapter adapter) - { - // Setup - capy::async_event request_done; - auto elem = make_elem(req, std::move(adapter)); - elem->set_done_callback([&request_done]() { - request_done.set(); - }); - exec_fsm fsm{elem}; + logger&& lgr); - // Invoke the FSM - while (true) { - // Invoke the state machine - auto act = fsm.resume(true, st_, token_to_cancel(co_await capy::this_coro::stop_token)); + void cancel(); - // Do what the FSM said - switch (act.type()) { - case exec_action_type::setup_cancellation: break; // ignored, not required by capy - case exec_action_type::immediate: break; // ignored, not required by capy - case exec_action_type::notify_writer: writer_cv_.cancel(); break; - case exec_action_type::wait_for_response: - { - auto [ec] = co_await request_done.wait(); - ignore_unused(ec); // TODO: we should likely use this - break; - } - case exec_action_type::done: co_return {act.error()}; - } - } - } + capy::io_task<> exec(request const& req, any_adapter adapter); - void set_receive_adapter(any_adapter adapter) - { - st_.mpx.set_receive_adapter(std::move(adapter)); - } + void set_receive_adapter(any_adapter adapter); }; -inline capy::io_task<> receive(corosio_connection_impl& conn) -{ - // Setup - receive_fsm fsm; - system::error_code ec; - - while (true) { - receive_action act = fsm.resume( - conn.st_, - ec, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type) { - case receive_action::action_type::setup_cancellation: break; // not required here - case receive_action::action_type::wait: - { - auto [controller_ec] = co_await conn.controller_.take(); - ec = controller_ec; - break; - } - case receive_action::action_type::drain_channel: break; // not required - case receive_action::action_type::immediate: break; // not required - case receive_action::action_type::done: co_return {act.ec}; - } - } -} - -inline capy::io_task<> async_exec_one( - corosio_connection_impl& conn, - const request& req, - any_adapter resp) -{ - exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; - system::error_code ec; - std::size_t bytes = 0u; - - while (true) { - exec_one_action act = fsm.resume( - conn.st_.mpx.get_read_buffer(), - ec, - bytes, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type) { - case exec_one_action_type::done: co_return {ec}; - case exec_one_action_type::write: - { - auto [write_ec, write_bytes] = co_await capy::write( - conn.stream_, - capy::make_buffer(req.payload())); - ec = write_ec; - bytes = write_bytes; - break; - } - case exec_one_action_type::read_some: - { - auto [read_ec, read_bytes] = co_await conn.stream_.read_some( - capy::make_buffer(conn.st_.mpx.get_read_buffer().get_prepared())); - ec = read_ec; - bytes = read_bytes; - break; - } - } - } -} - -inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) -{ - // Setup - sentinel_resolve_fsm fsm; - system::error_code ec; - - while (true) { - sentinel_action act = fsm.resume( - conn.st_, - ec, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.get_type()) { - case sentinel_action::type::done: co_return {act.error()}; - case sentinel_action::type::connect: - { - auto [connect_ec] = co_await conn.stream_.connect( - make_sentinel_connect_params(conn.st_.cfg, act.connect_addr()), - conn.st_.logger); - ec = connect_ec; - break; - } - case sentinel_action::type::request: - { - auto [request_ec] = co_await cancel_after( - async_exec_one(conn, conn.st_.cfg.sentinel.setup, make_sentinel_adapter(conn.st_)), - conn.reconnect_timer_, - conn.st_.cfg.sentinel.request_timeout); - ec = request_ec; - break; - } - } - } -} - -inline capy::io_task<> writer(corosio_connection_impl& conn) -{ - // Setup - writer_fsm fsm; - system::error_code ec; - std::size_t bytes_written = 0u; - - while (true) { - writer_action act = fsm.resume( - conn.st_, - ec, - bytes_written, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type()) { - case writer_action_type::done: co_return {act.error()}; - case writer_action_type::write_some: - { - auto [write_ec, write_bytes] = co_await cancel_at( - conn.stream_.write_some(capy::make_buffer(conn.st_.mpx.get_write_buffer())), - conn.writer_timer_, - compute_expiry(act.timeout())); - ec = write_ec; - bytes_written = write_bytes; - break; - } - case writer_action_type::wait: - { - conn.writer_cv_.expires_at(compute_expiry(act.timeout())); - auto [wait_ec] = co_await conn.writer_cv_.wait(); - ec = wait_ec; - bytes_written = 0u; - break; - } - } - } -} - -inline capy::io_task<> reader(corosio_connection_impl& conn) -{ - reader_fsm fsm; - std::size_t n = 0u; - system::error_code ec; - - for (;;) { - auto act = fsm.resume(conn.st_, n, ec, token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.get_type()) { - case reader_fsm::action::type::read_some: - { - auto [read_ec, read_bytes] = co_await cancel_at( - conn.stream_.read_some(capy::make_buffer(conn.st_.mpx.get_prepared_read_buffer())), - conn.reader_timer_, - compute_expiry(act.timeout())); - ec = read_ec; - n = read_bytes; - break; - } - case reader_fsm::action::type::notify_push_receiver: - { - // TODO: re-work this - auto [notify_ec] = co_await conn.controller_.wait_for_space(); - if (notify_ec) - ec = notify_ec; - else - conn.controller_.put(act.push_size()); - } - case reader_fsm::action::type::done: co_return {act.error()}; - } - } -} - -inline capy::io_task<> run(corosio_connection_impl& conn) -{ - run_fsm fsm; - system::error_code ec; - - while (true) { - auto act = fsm.resume(conn.st_, ec, token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type) { - case run_action_type::done: co_return {act.ec}; - case run_action_type::immediate: break; // no longer required - case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve(conn)).ec; break; - case run_action_type::connect: - ec = (co_await conn.stream_.connect(make_run_connect_params(conn.st_), conn.st_.logger)) - .ec; - break; - case run_action_type::parallel_group: - { - auto [winner_index, result] = co_await capy::when_any(reader(conn), writer(conn)); - ignore_unused(winner_index); - ec = std::get<0>(result).ec; - break; - } - case run_action_type::cancel_receive: break; // no longer required - case run_action_type::wait_for_reconnection: - conn.reconnect_timer_.expires_after(conn.st_.cfg.reconnect_wait_interval); - ec = (co_await conn.reconnect_timer_.wait()).ec; - break; - } - } -} - -logger make_stderr_logger(logger::level lvl, std::string prefix); - } // namespace detail /** @brief A SSL connection to the Redis server. @@ -515,10 +153,7 @@ class connection { explicit connection( capy::execution_context& ctx, corosio::tls_context ssl_ctx = {}, - logger lgr = {}) - : impl_( - std::make_unique(ctx, std::move(ssl_ctx), std::move(lgr))) - { } + logger lgr = {}); /** @brief Constructor from an executor and a logger. * @@ -529,9 +164,7 @@ class connection { * * An SSL context with default settings will be created. */ - connection(capy::execution_context& ctx, logger lgr) - : connection(ctx, {}, std::move(lgr)) - { } + connection(capy::execution_context& ctx, logger lgr); /** @brief Starts the underlying connection operations. * @@ -586,18 +219,7 @@ class connection { * @param cfg Configuration parameters. * @param token Completion token. */ - capy::io_task<> run(config const& cfg) - { - impl_->st_.cfg = cfg; - impl_->st_.mpx.set_config(cfg); - impl_->run_cancelled_event_.clear(); - - auto [winner_index, result] = co_await capy::when_any( - detail::run(*impl_), - impl_->run_cancelled_event_.wait()); - - co_return {winner_index == 0u ? std::get<0>(result).ec : capy::error::canceled}; - } + capy::io_task<> run(config const& cfg); /** @brief Wait for server pushes asynchronously. * @@ -675,7 +297,7 @@ class connection { * * @param token Completion token. */ - capy::io_task<> receive() { return detail::receive(*impl_); } + capy::io_task<> receive(); /** @brief Executes commands on the Redis server asynchronously. * @@ -799,12 +421,6 @@ class connection { usage get_usage() const noexcept { return impl_->st_.mpx.get_usage(); } private: - // Used by both this class and connection - void set_stderr_logger(logger::level lvl, const config& cfg) - { - impl_->st_.logger.lgr = detail::make_stderr_logger(lvl, cfg.log_prefix); - } - std::unique_ptr impl_; }; diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/connection.ipp index 9db49af3..fa89fb16 100644 --- a/include/boost/redis/impl/connection.ipp +++ b/include/boost/redis/impl/connection.ipp @@ -5,58 +5,412 @@ */ #include -#include + +#include +#include #include -#include -#include #include namespace boost::redis { +namespace detail { + +// Given a timeout value, compute the expiry time. A zero timeout is considered to mean "no timeout" +inline std::chrono::steady_clock::time_point compute_expiry( + std::chrono::steady_clock::duration timeout) +{ + return timeout.count() == 0 ? (std::chrono::steady_clock::time_point::max)() + : std::chrono::steady_clock::now() + timeout; +} + +inline asio::cancellation_type_t token_to_cancel(std::stop_token tok) +{ + return tok.stop_requested() ? asio::cancellation_type_t::terminal + : asio::cancellation_type_t::none; +} + +template +capy::io_task cancel_at( + capy::io_task task, + corosio::timer& timer, + std::chrono::steady_clock::time_point timeout) +{ + timer.expires_at(timeout); + auto [winner_index, result] = co_await capy::when_any(std::move(task), timer.wait()); + if (winner_index == 0u) + co_return std::get<0>(std::move(result)); + else + co_return {make_error_code(asio::error::operation_aborted)}; +} + +template +capy::io_task cancel_after( + capy::io_task task, + corosio::timer& timer, + std::chrono::steady_clock::duration timeout) +{ + return cancel_at(std::move(task), timer, std::chrono::steady_clock::now() + timeout); +} + +capy::io_task<> corosio_redis_stream::connect(const connect_params& params, buffered_logger& l) +{ + connect_fsm fsm{l}; + system::error_code ec; + corosio::resolver_results endpoints; -logger detail::make_stderr_logger(logger::level lvl, std::string prefix) + auto act = fsm.resume(ec, st_); + + while (true) { + switch (act.type) { + case connect_action_type::unix_socket_close: + BOOST_ASSERT(false); + co_return {system::error_code(asio::error::operation_not_supported)}; + case connect_action_type::unix_socket_connect: + BOOST_ASSERT(false); + co_return {system::error_code(asio::error::operation_not_supported)}; + case connect_action_type::tcp_resolve: + { + auto result = co_await cancel_after( + [&] -> capy::io_task { + co_return co_await resolv_.resolve( + params.addr.tcp_address().host, + params.addr.tcp_address().port); + }(), + timer_, + params.resolve_timeout); + ec = result.ec; + endpoints = std::move(result.t1); + act = fsm.resume(ec, endpoints, st_); + break; + } + case connect_action_type::ssl_stream_reset: + stream_.reset(); + act = fsm.resume(ec, st_); + break; + case connect_action_type::ssl_handshake: + ec = (co_await cancel_after( + stream_.handshake(corosio::tls_stream::handshake_type::client), + timer_, + params.ssl_handshake_timeout)) + .ec; + act = fsm.resume(ec, st_); + break; + case connect_action_type::done: co_return {act.ec}; + case connect_action_type::tcp_connect: + { + // TODO: range connect + socket_.open(); + auto result = co_await cancel_after( + [&] -> capy::io_task<> { + co_return co_await socket_.connect(*endpoints.begin()); + }(), + timer_, + params.connect_timeout); + ec = result.ec; + act = fsm.resume(ec, *endpoints.begin(), st_); + break; + } + default: BOOST_ASSERT(false); + } + } +} + +corosio_connection_impl::corosio_connection_impl( + capy::execution_context& ctx, + corosio::tls_context&& ssl_ctx, + logger&& lgr) +: stream_{ctx, std::move(ssl_ctx)} +, writer_timer_{ctx} +, writer_cv_{ctx} +, reader_timer_{ctx} +, reconnect_timer_{ctx} +, ping_timer_{ctx} +, controller_{1024u * 1024u * 16u} // 16MB, TODO: make it configurable +, st_{{std::move(lgr)}} +{ + set_receive_adapter(any_adapter{ignore}); + writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); +} + +void corosio_connection_impl::cancel() { - return logger(lvl, [prefix = std::move(prefix)](logger::level, std::string_view msg) { - log_to_file(stderr, msg, prefix.c_str()); + // exec + st_.mpx.cancel_waiting(); + + // receive (TODO: do we really need this?) + st_.receive2_cancelled = true; + + // reconnect (TODO: do we really need this?) + st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); + + // run + run_cancelled_event_.set(); +} + +capy::io_task<> corosio_connection_impl::exec(request const& req, any_adapter adapter) +{ + // Setup + capy::async_event request_done; + auto elem = make_elem(req, std::move(adapter)); + elem->set_done_callback([&request_done]() { + request_done.set(); }); + exec_fsm fsm{elem}; + + // Invoke the FSM + while (true) { + // Invoke the state machine + auto act = fsm.resume(true, st_, token_to_cancel(co_await capy::this_coro::stop_token)); + + // Do what the FSM said + switch (act.type()) { + case exec_action_type::setup_cancellation: break; // ignored, not required by capy + case exec_action_type::immediate: break; // ignored, not required by capy + case exec_action_type::notify_writer: writer_cv_.cancel(); break; + case exec_action_type::wait_for_response: + { + auto [ec] = co_await request_done.wait(); + ignore_unused(ec); // TODO: we should likely use this + break; + } + case exec_action_type::done: co_return {act.error()}; + } + } } -connection::connection(executor_type ex, asio::ssl::context ctx, logger lgr) -: impl_{std::move(ex), std::move(ctx), std::move(lgr)} -{ } +void corosio_connection_impl::set_receive_adapter(any_adapter adapter) +{ + st_.mpx.set_receive_adapter(std::move(adapter)); +} + +inline capy::io_task<> receive(corosio_connection_impl& conn) +{ + // Setup + receive_fsm fsm; + system::error_code ec; + + while (true) { + receive_action act = fsm.resume( + conn.st_, + ec, + token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.type) { + case receive_action::action_type::setup_cancellation: break; // not required here + case receive_action::action_type::wait: + { + auto [controller_ec] = co_await conn.controller_.take(); + ec = controller_ec; + break; + } + case receive_action::action_type::drain_channel: break; // not required + case receive_action::action_type::immediate: break; // not required + case receive_action::action_type::done: co_return {act.ec}; + } + } +} + +inline capy::io_task<> async_exec_one( + corosio_connection_impl& conn, + const request& req, + any_adapter resp) +{ + exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; + system::error_code ec; + std::size_t bytes = 0u; + + while (true) { + exec_one_action act = fsm.resume( + conn.st_.mpx.get_read_buffer(), + ec, + bytes, + token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.type) { + case exec_one_action_type::done: co_return {ec}; + case exec_one_action_type::write: + { + auto [write_ec, write_bytes] = co_await capy::write( + conn.stream_, + capy::make_buffer(req.payload())); + ec = write_ec; + bytes = write_bytes; + break; + } + case exec_one_action_type::read_some: + { + auto [read_ec, read_bytes] = co_await conn.stream_.read_some( + capy::make_buffer(conn.st_.mpx.get_read_buffer().get_prepared())); + ec = read_ec; + bytes = read_bytes; + break; + } + } + } +} + +inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) +{ + // Setup + sentinel_resolve_fsm fsm; + system::error_code ec; + + while (true) { + sentinel_action act = fsm.resume( + conn.st_, + ec, + token_to_cancel(co_await capy::this_coro::stop_token)); -void connection::async_run_impl( - config const& cfg, - logger&& l, - asio::any_completion_handler token) + switch (act.get_type()) { + case sentinel_action::type::done: co_return {act.error()}; + case sentinel_action::type::connect: + { + auto [connect_ec] = co_await conn.stream_.connect( + make_sentinel_connect_params(conn.st_.cfg, act.connect_addr()), + conn.st_.logger); + ec = connect_ec; + break; + } + case sentinel_action::type::request: + { + auto [request_ec] = co_await cancel_after( + async_exec_one(conn, conn.st_.cfg.sentinel.setup, make_sentinel_adapter(conn.st_)), + conn.reconnect_timer_, + conn.st_.cfg.sentinel.request_timeout); + ec = request_ec; + break; + } + } + } +} + +inline capy::io_task<> writer(corosio_connection_impl& conn) { - // Avoid calling the basic_connection::async_run overload taking a logger - // because it generates deprecated messages when building this file - impl_.set_stderr_logger(l.lvl, cfg); - impl_.async_run(cfg, std::move(token)); + // Setup + writer_fsm fsm; + system::error_code ec; + std::size_t bytes_written = 0u; + + while (true) { + writer_action act = fsm.resume( + conn.st_, + ec, + bytes_written, + token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.type()) { + case writer_action_type::done: co_return {act.error()}; + case writer_action_type::write_some: + { + auto [write_ec, write_bytes] = co_await cancel_at( + conn.stream_.write_some(capy::make_buffer(conn.st_.mpx.get_write_buffer())), + conn.writer_timer_, + compute_expiry(act.timeout())); + ec = write_ec; + bytes_written = write_bytes; + break; + } + case writer_action_type::wait: + { + conn.writer_cv_.expires_at(compute_expiry(act.timeout())); + auto [wait_ec] = co_await conn.writer_cv_.wait(); + ec = wait_ec; + bytes_written = 0u; + break; + } + } + } } -void connection::async_run_impl( - config const& cfg, - asio::any_completion_handler token) +inline capy::io_task<> reader(corosio_connection_impl& conn) { - impl_.async_run(cfg, std::move(token)); + reader_fsm fsm; + std::size_t n = 0u; + system::error_code ec; + + for (;;) { + auto act = fsm.resume(conn.st_, n, ec, token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.get_type()) { + case reader_fsm::action::type::read_some: + { + auto [read_ec, read_bytes] = co_await cancel_at( + conn.stream_.read_some(capy::make_buffer(conn.st_.mpx.get_prepared_read_buffer())), + conn.reader_timer_, + compute_expiry(act.timeout())); + ec = read_ec; + n = read_bytes; + break; + } + case reader_fsm::action::type::notify_push_receiver: + { + // TODO: re-work this + auto [notify_ec] = co_await conn.controller_.wait_for_space(); + if (notify_ec) + ec = notify_ec; + else + conn.controller_.put(act.push_size()); + } + case reader_fsm::action::type::done: co_return {act.error()}; + } + } } -void connection::async_exec_impl( - request const& req, - any_adapter&& adapter, - asio::any_completion_handler token) +inline capy::io_task<> run(corosio_connection_impl& conn) { - impl_.async_exec(req, std::move(adapter), std::move(token)); + run_fsm fsm; + system::error_code ec; + + while (true) { + auto act = fsm.resume(conn.st_, ec, token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.type) { + case run_action_type::done: co_return {act.ec}; + case run_action_type::immediate: break; // no longer required + case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve(conn)).ec; break; + case run_action_type::connect: + ec = (co_await conn.stream_.connect(make_run_connect_params(conn.st_), conn.st_.logger)) + .ec; + break; + case run_action_type::parallel_group: + { + auto [winner_index, result] = co_await capy::when_any(reader(conn), writer(conn)); + ignore_unused(winner_index); + ec = std::get<0>(result).ec; + break; + } + case run_action_type::cancel_receive: break; // no longer required + case run_action_type::wait_for_reconnection: + conn.reconnect_timer_.expires_after(conn.st_.cfg.reconnect_wait_interval); + ec = (co_await conn.reconnect_timer_.wait()).ec; + break; + } + } } -void connection::async_receive2_impl( - asio::any_completion_handler token) +} // namespace detail + +connection::connection(capy::execution_context& ctx, corosio::tls_context ssl_ctx, logger lgr) +: impl_(std::make_unique(ctx, std::move(ssl_ctx), std::move(lgr))) +{ } + +connection::connection(capy::execution_context& ctx, logger lgr) +: connection(ctx, {}, std::move(lgr)) +{ } + +capy::io_task<> connection::run(config const& cfg) { - impl_.async_receive2(std::move(token)); + impl_->st_.cfg = cfg; + impl_->st_.mpx.set_config(cfg); + impl_->run_cancelled_event_.clear(); + + auto [winner_index, result] = co_await capy::when_any( + detail::run(*impl_), + impl_->run_cancelled_event_.wait()); + + co_return {winner_index == 0u ? std::get<0>(result).ec : capy::error::canceled}; } -void connection::cancel(operation op) { impl_.cancel(op); } +capy::io_task<> connection::receive() { return detail::receive(*impl_); } } // namespace boost::redis diff --git a/include/boost/redis/src.hpp b/include/boost/redis/src.hpp index de5b7c6a..196b2960 100644 --- a/include/boost/redis/src.hpp +++ b/include/boost/redis/src.hpp @@ -5,7 +5,7 @@ */ #include -// #include +#include #include #include #include From 29fa87df87903e535510105a856c63db83c2b922 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 20:41:48 +0100 Subject: [PATCH 023/115] operation_aborted => canceled --- doc/modules/ROOT/pages/cancellation.adoc | 2 +- include/boost/redis/connection.hpp | 3 +-- include/boost/redis/impl/connection.ipp | 7 ++++--- include/boost/redis/impl/exec_fsm.ipp | 4 ++-- include/boost/redis/impl/exec_one_fsm.ipp | 6 +++--- include/boost/redis/impl/multiplexer.ipp | 6 +++--- include/boost/redis/impl/reader_fsm.ipp | 8 ++++---- include/boost/redis/impl/receive_fsm.ipp | 4 ++-- include/boost/redis/impl/run_fsm.ipp | 10 +++++----- include/boost/redis/impl/sentinel_resolve_fsm.ipp | 6 +++--- include/boost/redis/impl/writer_fsm.ipp | 6 +++--- 11 files changed, 31 insertions(+), 31 deletions(-) diff --git a/doc/modules/ROOT/pages/cancellation.adoc b/doc/modules/ROOT/pages/cancellation.adoc index 78ed2007..33f3fba4 100644 --- a/doc/modules/ROOT/pages/cancellation.adoc +++ b/doc/modules/ROOT/pages/cancellation.adoc @@ -52,7 +52,7 @@ error after sending the request, but before receiving a response? In this situation there is no way to know if the request was processed by the server or not. By default, the library will consider the request as failed, -and `async_exec` will complete with an `asio::error::operation_aborted` +and `async_exec` will complete with an `capy::error::canceled` error code. Some requests can be executed several times and result in the same outcome diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index f1e9782b..10d87b04 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -29,11 +29,10 @@ #include #include -#include +#include #include #include #include -#include #include #include #include diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/connection.ipp index fa89fb16..fee92b7f 100644 --- a/include/boost/redis/impl/connection.ipp +++ b/include/boost/redis/impl/connection.ipp @@ -10,6 +10,7 @@ #include #include +#include #include namespace boost::redis { @@ -40,7 +41,7 @@ capy::io_task cancel_at( if (winner_index == 0u) co_return std::get<0>(std::move(result)); else - co_return {make_error_code(asio::error::operation_aborted)}; + co_return {make_error_code(capy::error::canceled)}; } template @@ -64,10 +65,10 @@ capy::io_task<> corosio_redis_stream::connect(const connect_params& params, buff switch (act.type) { case connect_action_type::unix_socket_close: BOOST_ASSERT(false); - co_return {system::error_code(asio::error::operation_not_supported)}; + co_return {std::make_error_code(std::errc::operation_not_supported)}; case connect_action_type::unix_socket_connect: BOOST_ASSERT(false); - co_return {system::error_code(asio::error::operation_not_supported)}; + co_return {std::make_error_code(std::errc::operation_not_supported)}; case connect_action_type::tcp_resolve: { auto result = co_await cancel_after( diff --git a/include/boost/redis/impl/exec_fsm.ipp b/include/boost/redis/impl/exec_fsm.ipp index 3898d1e1..bede881e 100644 --- a/include/boost/redis/impl/exec_fsm.ipp +++ b/include/boost/redis/impl/exec_fsm.ipp @@ -14,8 +14,8 @@ #include #include -#include #include +#include namespace boost::redis::detail { @@ -83,7 +83,7 @@ exec_action exec_fsm::resume( is_partial_or_terminal_cancel(cancel_state)) { st.mpx.cancel(elem_); elem_.reset(); // Deallocate memory before finalizing - return exec_action{asio::error::operation_aborted}; + return exec_action{capy::error::canceled}; } } } diff --git a/include/boost/redis/impl/exec_one_fsm.ipp b/include/boost/redis/impl/exec_one_fsm.ipp index b4a7250b..b7cab7bf 100644 --- a/include/boost/redis/impl/exec_one_fsm.ipp +++ b/include/boost/redis/impl/exec_one_fsm.ipp @@ -18,7 +18,7 @@ #include #include -#include +#include #include #include @@ -40,7 +40,7 @@ exec_one_action exec_one_fsm::resume( // Errors and cancellations if (is_terminal_cancel(cancel_state)) - return system::error_code{asio::error::operation_aborted}; + return system::error_code{capy::error::canceled}; if (ec) return ec; @@ -61,7 +61,7 @@ exec_one_action exec_one_fsm::resume( // Errors and cancellations if (is_terminal_cancel(cancel_state)) - return system::error_code{asio::error::operation_aborted}; + return system::error_code{capy::error::canceled}; if (ec) return ec; diff --git a/include/boost/redis/impl/multiplexer.ipp b/include/boost/redis/impl/multiplexer.ipp index b6d118ce..931dbf56 100644 --- a/include/boost/redis/impl/multiplexer.ipp +++ b/include/boost/redis/impl/multiplexer.ipp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include @@ -243,7 +243,7 @@ std::size_t multiplexer::cancel_waiting() auto const ret = std::distance(point, std::end(reqs_)); std::for_each(point, std::end(reqs_), [](auto const& ptr) { - ptr->notify_error({asio::error::operation_aborted}); + ptr->notify_error({capy::error::canceled}); }); reqs_.erase(point, std::end(reqs_)); @@ -276,7 +276,7 @@ void multiplexer::cancel_on_conn_lost() auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), cond); std::for_each(point, std::end(reqs_), [](auto const& ptr) { - ptr->notify_error({asio::error::operation_aborted}); + ptr->notify_error({capy::error::canceled}); }); reqs_.erase(point, std::end(reqs_)); diff --git a/include/boost/redis/impl/reader_fsm.ipp b/include/boost/redis/impl/reader_fsm.ipp index 56e4005e..5ce13b37 100644 --- a/include/boost/redis/impl/reader_fsm.ipp +++ b/include/boost/redis/impl/reader_fsm.ipp @@ -12,7 +12,7 @@ #include #include -#include +#include #include namespace boost::redis::detail { @@ -43,10 +43,10 @@ reader_fsm::action reader_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Reader task: cancelled (1)"); - return system::error_code(asio::error::operation_aborted); + return system::error_code(capy::error::canceled); } - // Translate timeout errors caused by operation_aborted to more legible ones. + // Translate timeout errors caused by canceled to more legible ones. // A timeout here means that we didn't receive data in time. // Note that cancellation is already handled by the above statement. if (ec == capy::cond::canceled) { @@ -92,7 +92,7 @@ reader_fsm::action reader_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Reader task: cancelled (2)"); - return system::error_code(asio::error::operation_aborted); + return system::error_code(capy::error::canceled); } // Check for other errors diff --git a/include/boost/redis/impl/receive_fsm.ipp b/include/boost/redis/impl/receive_fsm.ipp index ca98dcd8..2cefed6a 100644 --- a/include/boost/redis/impl/receive_fsm.ipp +++ b/include/boost/redis/impl/receive_fsm.ipp @@ -11,7 +11,7 @@ #include #include -#include +#include #include #include @@ -63,7 +63,7 @@ receive_action receive_fsm::resume( // Check for cancellations if (is_any_cancel(cancel_state) || st.receive2_cancelled) { st.receive2_running = false; - return system::error_code(asio::error::operation_aborted); + return system::error_code(capy::error::canceled); } // If we get any unknown errors, propagate them (shouldn't happen, but just in case) diff --git a/include/boost/redis/impl/run_fsm.ipp b/include/boost/redis/impl/run_fsm.ipp index a7e11336..c855034b 100644 --- a/include/boost/redis/impl/run_fsm.ipp +++ b/include/boost/redis/impl/run_fsm.ipp @@ -20,7 +20,7 @@ #include #include -#include +#include // #include // for BOOST_ASIO_HAS_LOCAL_SOCKETS #include @@ -124,7 +124,7 @@ run_action run_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (4)"); - return {asio::error::operation_aborted}; + return {capy::error::canceled}; } // Check for errors @@ -139,7 +139,7 @@ run_action run_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (1)"); - return system::error_code(asio::error::operation_aborted); + return system::error_code(capy::error::canceled); } if (ec) { @@ -191,7 +191,7 @@ run_action run_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (2)"); - return system::error_code(asio::error::operation_aborted); + return system::error_code(capy::error::canceled); } sleep_and_reconnect: @@ -207,7 +207,7 @@ sleep_and_reconnect: // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (3)"); - return system::error_code(asio::error::operation_aborted); + return system::error_code(capy::error::canceled); } } } diff --git a/include/boost/redis/impl/sentinel_resolve_fsm.ipp b/include/boost/redis/impl/sentinel_resolve_fsm.ipp index c4359e63..ff963555 100644 --- a/include/boost/redis/impl/sentinel_resolve_fsm.ipp +++ b/include/boost/redis/impl/sentinel_resolve_fsm.ipp @@ -20,7 +20,7 @@ #include #include -#include +#include #include #include @@ -69,7 +69,7 @@ sentinel_action sentinel_resolve_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Sentinel resolve: cancelled (1)"); - return system::error_code(asio::error::operation_aborted); + return system::error_code(capy::error::canceled); } // Check for errors @@ -86,7 +86,7 @@ sentinel_action sentinel_resolve_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Sentinel resolve: cancelled (2)"); - return system::error_code(asio::error::operation_aborted); + return system::error_code(capy::error::canceled); } // Check for errors diff --git a/include/boost/redis/impl/writer_fsm.ipp b/include/boost/redis/impl/writer_fsm.ipp index d0638c23..82cee689 100644 --- a/include/boost/redis/impl/writer_fsm.ipp +++ b/include/boost/redis/impl/writer_fsm.ipp @@ -19,7 +19,7 @@ #include #include -#include +#include #include #include #include @@ -81,7 +81,7 @@ writer_action writer_fsm::resume( // Check for cancellations and translate error codes if (is_terminal_cancel(cancel_state)) - ec = asio::error::operation_aborted; + ec = capy::error::canceled; else if (ec == capy::cond::canceled) ec = error::write_timeout; @@ -107,7 +107,7 @@ writer_action writer_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Writer task: cancelled (2)."); - return system::error_code(asio::error::operation_aborted); + return system::error_code(capy::error::canceled); } // If we weren't notified, it's because there is no data and we should send a health check From 7fc4de773b739fc8f63022e7c684392997e6cd3b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 21:02:11 +0100 Subject: [PATCH 024/115] misc fixes --- include/boost/redis/impl/connection.ipp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/connection.ipp index fee92b7f..a385b7cb 100644 --- a/include/boost/redis/impl/connection.ipp +++ b/include/boost/redis/impl/connection.ipp @@ -100,6 +100,7 @@ capy::io_task<> corosio_redis_stream::connect(const connect_params& params, buff case connect_action_type::tcp_connect: { // TODO: range connect + socket_.close(); socket_.open(); auto result = co_await cancel_after( [&] -> capy::io_task<> { @@ -351,6 +352,7 @@ inline capy::io_task<> reader(corosio_connection_impl& conn) ec = notify_ec; else conn.controller_.put(act.push_size()); + break; } case reader_fsm::action::type::done: co_return {act.error()}; } From 9d71bf6d6c3f440440d642045eb1f1c5ca5b8efc Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 12 Feb 2026 21:02:33 +0100 Subject: [PATCH 025/115] test_conn_check_health --- test/test_conn_check_health.cpp | 307 +++++++++++++------------------- 1 file changed, 127 insertions(+), 180 deletions(-) diff --git a/test/test_conn_check_health.cpp b/test/test_conn_check_health.cpp index 5d53b084..7868a52f 100644 --- a/test/test_conn_check_health.cpp +++ b/test/test_conn_check_health.cpp @@ -9,211 +9,155 @@ #include #include -#include -#include -#include +#include +#include +#include +#include #include +#include #include "common.hpp" #include -#include #include +#include -namespace net = boost::asio; -namespace redis = boost::redis; -using error_code = boost::system::error_code; -using connection = boost::redis::connection; -using boost::redis::request; -using boost::redis::ignore; -using boost::redis::generic_response; +namespace capy = boost::capy; +using namespace boost::redis; +using error_code = std::error_code; using namespace std::chrono_literals; namespace { // The health checker detects dead connections and triggers reconnection -void test_reconnection() +capy::task test_reconnection() { // Setup - net::io_context ioc; - connection conn{ioc}; + connection conn{(co_await capy::this_coro::executor).context()}; - // This request will block forever, causing the connection to become unresponsive - request req1; - req1.push("BLPOP", "any", 0); + auto exec_fn = [&]() -> capy::task { + // This request will block forever, causing the connection to become unresponsive + request req1; + req1.push("BLPOP", "any", 0); - // This request should be executed after reconnection - request req2; - req2.push("PING", "after_reconnection"); - req2.get_config().cancel_if_unresponded = false; - req2.get_config().cancel_on_connection_lost = false; + // This request should be executed after reconnection + request req2; + req2.push("PING", "after_reconnection"); + req2.get_config().cancel_if_unresponded = false; + req2.get_config().cancel_on_connection_lost = false; - // Make the test run faster - auto cfg = make_test_config(); - cfg.health_check_interval = 500ms; - - bool run_finished = false, exec1_finished = false, exec2_finished = false; - - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); - }); - - // This request will complete after the health checker deems the connection - // as unresponsive and triggers a reconnection (it's configured to be cancelled - // on connection lost). - conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { - exec1_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + // This request will complete after the health checker deems the connection + // as unresponsive and triggers a reconnection (it's configured to be cancelled + // on connection lost). + auto [ec1] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec1, error_code(capy::error::canceled)); // Execute the second request. This one will succeed after reconnection - conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) { - exec2_finished = true; - BOOST_TEST_EQ(ec2, error_code()); - conn.cancel(); - }); - }); + auto [ec2] = co_await conn.exec(req2, ignore); + BOOST_TEST_EQ(ec2, error_code()); + }; - ioc.run_for(test_timeout); + auto run_fn = [&]() -> capy::task { + // Make the test run faster + auto cfg = make_test_config(); + cfg.health_check_interval = 500ms; - BOOST_TEST(run_finished); - BOOST_TEST(exec1_finished); - BOOST_TEST(exec2_finished); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(exec_fn(), run_fn()); } // We use the correct error code when a ping times out -void test_error_code() +capy::task test_error_code() { - // Setup - net::io_context ioc; - connection conn{ioc}; + connection conn{(co_await capy::this_coro::executor).context()}; - // This request will block forever, causing the connection to become unresponsive - request req; - req.push("BLPOP", "any", 0); + auto exec_fn = [&]() -> capy::task { + // This request will block forever, causing the connection to become unresponsive + request req; + req.push("BLPOP", "any", 0); - // Make the test run faster - auto cfg = make_test_config(); - cfg.health_check_interval = 200ms; - cfg.reconnect_wait_interval = 0s; + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, capy::error::canceled); + }; - bool run_finished = false, exec_finished = false; + auto run_fn = [&]() -> capy::task { + // Make the test run faster + auto cfg = make_test_config(); + cfg.health_check_interval = 200ms; + cfg.reconnect_wait_interval = 0s; - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; + auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, boost::redis::error::pong_timeout); - }); - - // This request will complete after the health checker deems the connection - // as unresponsive and triggers a reconnection (it's configured to be cancelled - // if unresponded). - conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { - exec_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); - }); + }; - ioc.run_for(test_timeout); - - BOOST_TEST(run_finished); - BOOST_TEST(exec_finished); + co_await capy::when_any(exec_fn(), run_fn()); } // A ping interval of zero disables timeouts (and doesn't cause trouble) -void test_disabled() +capy::task test_disabled() { - // Setup - net::io_context ioc; - connection conn{ioc}; + connection conn{(co_await capy::this_coro::executor).context()}; - // Run a couple of requests to verify that the connection works fine - request req1; - req1.push("PING", "health_check_disabled_1"); + auto exec_fn = [&]() -> capy::task { + // Run a couple of requests to verify that the connection works fine + request req1; + req1.push("PING", "health_check_disabled_1"); - request req2; - req1.push("PING", "health_check_disabled_2"); + request req2; + req1.push("PING", "health_check_disabled_2"); - auto cfg = make_test_config(); - cfg.health_check_interval = 0s; - - bool run_finished = false, exec1_finished = false, exec2_finished = false; + auto [ec1] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec1, std::error_code()); - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); - }); + auto [ec2] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec2, std::error_code()); + }; - conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { - exec1_finished = true; - BOOST_TEST_EQ(ec, error_code()); - conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) { - exec2_finished = true; - BOOST_TEST_EQ(ec2, error_code()); - conn.cancel(); - }); - }); + auto run_fn = [&]() -> capy::task { + auto cfg = make_test_config(); + cfg.health_check_interval = 0s; + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; - ioc.run_for(test_timeout); + co_await capy::when_any(exec_fn(), run_fn()); +} - BOOST_TEST(run_finished); - BOOST_TEST(exec1_finished); - BOOST_TEST(exec2_finished); +// Generates a sufficiently unique name for channels so +// tests may be run in parallel for different configurations +std::string make_unique_id() +{ + auto t = std::chrono::high_resolution_clock::now(); + return "test-flexible-health-checks-" + std::to_string(t.time_since_epoch().count()); } // Receiving data is sufficient to consider our connection healthy. // Sends a blocking request that causes PINGs to not be answered, // and subscribes to a channel to receive pushes periodically. // This simulates situations of heavy load, where PINGs may not be answered on time. -class test_flexible { - net::io_context ioc; - connection conn1{ioc}; // The one that simulates a heavy load condition - connection conn2{ioc}; // Publishes messages - net::steady_timer timer{ioc}; - request publish_req; - bool run1_finished = false, run2_finished = false, exec_finished{false}, - publisher_finished{false}; - - // Starts publishing messages to the channel - void start_publish() - { - conn2.async_exec(publish_req, ignore, [this](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); +capy::task test_flexible() +{ + // Setup + connection conn1{(co_await capy::this_coro::executor).context()}; + connection conn2{(co_await capy::this_coro::executor).context()}; + auto cfg = make_test_config(); + cfg.health_check_interval = 500ms; + std::string channel_name = make_unique_id(); - if (exec_finished) { - // The blocking request finished, we're done - conn2.cancel(); - publisher_finished = true; - } else { - // Wait for some time and publish again - timer.expires_after(100ms); - timer.async_wait([this](error_code ec) { - BOOST_TEST_EQ(ec, error_code()); - start_publish(); - }); - } - }); - } - - // Generates a sufficiently unique name for channels so - // tests may be run in parallel for different configurations - static std::string make_unique_id() - { - auto t = std::chrono::high_resolution_clock::now(); - return "test-flexible-health-checks-" + std::to_string(t.time_since_epoch().count()); - } - -public: - test_flexible() = default; - - void run() - { - // Setup - auto cfg = make_test_config(); - cfg.health_check_interval = 500ms; - generic_response resp; + auto run1_fn = [&] -> capy::task { + auto [ec] = co_await conn1.run(cfg); + BOOST_TEST_EQ(ec, capy::error::canceled); + }; - std::string channel_name = make_unique_id(); - publish_req.push("PUBLISH", channel_name, "test_health_check_flexible"); + auto run2_fn = [&] -> capy::task { + auto [ec] = co_await conn2.run(cfg); + BOOST_TEST_EQ(ec, capy::error::canceled); + }; + auto exec_fn = [&] -> capy::task { // This request will block for much longer than the health check // interval. If we weren't receiving pushes, the connection would be considered dead. // If this request finishes successfully, the health checker is doing good @@ -223,42 +167,45 @@ class test_flexible { blocking_req.get_config().cancel_if_unresponded = true; blocking_req.get_config().cancel_on_connection_lost = true; - conn1.async_run(cfg, [&](error_code ec) { - run1_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); - }); + // BLPOP will return NIL, so we can't use ignore + generic_response resp; + auto [ec] = co_await conn1.exec(blocking_req, resp); + BOOST_TEST_EQ(ec, error_code()); + }; + + auto publish_fn = [&] -> capy::task { + request publish_req; + publish_req.push("PUBLISH", channel_name, "test_health_check_flexible"); - conn2.async_run(cfg, [&](error_code ec) { - run2_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); - }); + boost::corosio::timer timer{(co_await capy::this_coro::executor).context()}; - // BLPOP will return NIL, so we can't use ignore - conn1.async_exec(blocking_req, resp, [&](error_code ec, std::size_t) { - exec_finished = true; + while (true) { + // Publish a message + auto [ec] = co_await conn2.exec(publish_req, ignore); + if (ec == capy::error::canceled) + co_return; BOOST_TEST_EQ(ec, error_code()); - conn1.cancel(); - }); - - start_publish(); - ioc.run_for(test_timeout); + // Wait for some time and publish again + timer.expires_after(100ms); + auto [ec2] = co_await timer.wait(); + if (ec2 == capy::error::canceled) + co_return; + BOOST_TEST_EQ(ec2, error_code()); + } + }; - BOOST_TEST(run1_finished); - BOOST_TEST(run2_finished); - BOOST_TEST(exec_finished); - BOOST_TEST(publisher_finished); - } -}; + co_await capy::when_any(run1_fn(), run2_fn(), exec_fn(), publish_fn()); +} } // namespace int main() { - test_reconnection(); - test_error_code(); - test_disabled(); - test_flexible().run(); + run_coroutine_test(test_reconnection()); + run_coroutine_test(test_error_code()); + run_coroutine_test(test_disabled()); + run_coroutine_test(test_flexible()); return boost::report_errors(); } \ No newline at end of file From 16464517dc1b47107504b0d9480548314fd1071a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 14 Feb 2026 21:40:17 +0100 Subject: [PATCH 026/115] CMake and missing files --- CMakeLists.txt | 14 ++++++-- example/main.cpp | 44 ++++++++++++------------- include/boost/redis/impl/connection.ipp | 2 +- test/CMakeLists.txt | 2 +- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 967f1ccd..e72a8cd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,11 @@ if (BOOST_REDIS_MAIN_PROJECT) json endian compat + url + filesystem + scope + capy + corosio ) foreach(dep IN LISTS deps) @@ -90,6 +95,8 @@ if (BOOST_REDIS_MAIN_PROJECT) INTERFACE Boost::system Boost::asio + Boost::capy + Boost::corosio Threads::Threads OpenSSL::Crypto OpenSSL::SSL @@ -118,6 +125,9 @@ else() Boost::mp11 Boost::system Boost::throw_exception + Boost::capy + Boost::corosio + boost_corosio_openssl Threads::Threads OpenSSL::Crypto OpenSSL::SSL @@ -130,7 +140,7 @@ if (BOOST_REDIS_MAIN_PROJECT) endif() # Most tests require a running Redis server, so we only run them if we're the main project -if(BOOST_REDIS_MAIN_PROJECT AND BUILD_TESTING) +# if(BOOST_REDIS_MAIN_PROJECT AND BUILD_TESTING) # Tests and common utilities add_subdirectory(test) @@ -139,4 +149,4 @@ if(BOOST_REDIS_MAIN_PROJECT AND BUILD_TESTING) # Examples add_subdirectory(example) -endif() +# endif() diff --git a/example/main.cpp b/example/main.cpp index 43bcbc6b..1dbaa5ca 100644 --- a/example/main.cpp +++ b/example/main.cpp @@ -21,28 +21,28 @@ using boost::redis::logger; extern asio::awaitable co_main(config); -auto main(int argc, char* argv[]) -> int -{ - try { - config cfg; - - if (argc == 3) { - cfg.addr.host = argv[1]; - cfg.addr.port = argv[2]; - } - - asio::io_context ioc; - asio::co_spawn(ioc, co_main(cfg), [](std::exception_ptr p) { - if (p) - std::rethrow_exception(p); - }); - ioc.run(); - - } catch (std::exception const& e) { - std::cerr << "(main) " << e.what() << std::endl; - return 1; - } -} +// auto main(int argc, char* argv[]) -> int +// { +// try { +// config cfg; + +// if (argc == 3) { +// cfg.addr.host = argv[1]; +// cfg.addr.port = argv[2]; +// } + +// asio::io_context ioc; +// asio::co_spawn(ioc, co_main(cfg), [](std::exception_ptr p) { +// if (p) +// std::rethrow_exception(p); +// }); +// ioc.run(); + +// } catch (std::exception const& e) { +// std::cerr << "(main) " << e.what() << std::endl; +// return 1; +// } +// } #else // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/connection.ipp index a385b7cb..b5f1f9f4 100644 --- a/include/boost/redis/impl/connection.ipp +++ b/include/boost/redis/impl/connection.ipp @@ -41,7 +41,7 @@ capy::io_task cancel_at( if (winner_index == 0u) co_return std::get<0>(std::move(result)); else - co_return {make_error_code(capy::error::canceled)}; + co_return {capy::error::canceled}; } template diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 859dfc51..46b85ff3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,7 +29,7 @@ macro(make_test TEST_NAME) Boost::unit_test_framework ) target_compile_definitions(${EXE_NAME} PRIVATE BOOST_ALLOW_DEPRECATED=1) # we need to still test deprecated fns - add_test(${EXE_NAME} ${EXE_NAME}) + add_test(NAME ${EXE_NAME} COMMAND ${EXE_NAME}) endmacro() # Unit tests From d7ddfdaf1ab7fb47460f2f4b605fa9767dfe31e8 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 14 Apr 2026 20:39:52 +0200 Subject: [PATCH 027/115] Fix span member bug --- include/boost/redis/detail/read_buffer.hpp | 1 - include/boost/redis/impl/connect_fsm.ipp | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/include/boost/redis/detail/read_buffer.hpp b/include/boost/redis/detail/read_buffer.hpp index d54eb511..8d9af548 100644 --- a/include/boost/redis/detail/read_buffer.hpp +++ b/include/boost/redis/detail/read_buffer.hpp @@ -7,7 +7,6 @@ #ifndef BOOST_REDIS_READ_BUFFER_HPP #define BOOST_REDIS_READ_BUFFER_HPP -#include #include #include diff --git a/include/boost/redis/impl/connect_fsm.ipp b/include/boost/redis/impl/connect_fsm.ipp index 8230b233..ebf95531 100644 --- a/include/boost/redis/impl/connect_fsm.ipp +++ b/include/boost/redis/impl/connect_fsm.ipp @@ -46,8 +46,8 @@ template <> struct log_traits> { static inline void log(std::string& to, std::span value) { - auto iter = value.cbegin(); - auto end = value.cend(); + auto iter = value.begin(); + auto end = value.end(); if (iter != end) { format_tcp_endpoint(iter->get_endpoint(), to); From aa3d8f3ca537e11cb7918cfb9728961800c4edb5 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 14 Apr 2026 21:04:23 +0200 Subject: [PATCH 028/115] Latest capy fixes --- CMakeLists.txt | 3 +- include/boost/redis/impl/connection.ipp | 78 +++++++++---------------- test/test_conn_check_health.cpp | 40 +++++++++---- 3 files changed, 59 insertions(+), 62 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e72a8cd4..efc4cbdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,7 @@ if (BOOST_REDIS_MAIN_PROJECT) Boost::asio Boost::capy Boost::corosio + Boost::corosio_openssl Threads::Threads OpenSSL::Crypto OpenSSL::SSL @@ -127,7 +128,7 @@ else() Boost::throw_exception Boost::capy Boost::corosio - boost_corosio_openssl + Boost::corosio_openssl Threads::Threads OpenSSL::Crypto OpenSSL::SSL diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/connection.ipp index b5f1f9f4..e32517ca 100644 --- a/include/boost/redis/impl/connection.ipp +++ b/include/boost/redis/impl/connection.ipp @@ -7,8 +7,12 @@ #include #include +#include +#include +#include #include +#include #include #include #include @@ -30,27 +34,16 @@ inline asio::cancellation_type_t token_to_cancel(std::stop_token tok) : asio::cancellation_type_t::none; } -template -capy::io_task cancel_at( - capy::io_task task, - corosio::timer& timer, - std::chrono::steady_clock::time_point timeout) -{ - timer.expires_at(timeout); - auto [winner_index, result] = co_await capy::when_any(std::move(task), timer.wait()); - if (winner_index == 0u) - co_return std::get<0>(std::move(result)); - else - co_return {capy::error::canceled}; -} - -template -capy::io_task cancel_after( - capy::io_task task, - corosio::timer& timer, +// Run an operation with a timeout, with a zero timeout meaning 'no timeout' +template +capy::task> maybe_timeout( + Aw&& aw, std::chrono::steady_clock::duration timeout) { - return cancel_at(std::move(task), timer, std::chrono::steady_clock::now() + timeout); + if (timeout.count() == 0) + co_return co_await std::move(aw); + else + co_return co_await capy::timeout(std::move(aw), timeout); } capy::io_task<> corosio_redis_stream::connect(const connect_params& params, buffered_logger& l) @@ -71,16 +64,11 @@ capy::io_task<> corosio_redis_stream::connect(const connect_params& params, buff co_return {std::make_error_code(std::errc::operation_not_supported)}; case connect_action_type::tcp_resolve: { - auto result = co_await cancel_after( - [&] -> capy::io_task { - co_return co_await resolv_.resolve( - params.addr.tcp_address().host, - params.addr.tcp_address().port); - }(), - timer_, + auto result = co_await capy::timeout( + resolv_.resolve(params.addr.tcp_address().host, params.addr.tcp_address().port), params.resolve_timeout); ec = result.ec; - endpoints = std::move(result.t1); + endpoints = std::move(std::get<0>(result.values)); act = fsm.resume(ec, endpoints, st_); break; } @@ -89,9 +77,8 @@ capy::io_task<> corosio_redis_stream::connect(const connect_params& params, buff act = fsm.resume(ec, st_); break; case connect_action_type::ssl_handshake: - ec = (co_await cancel_after( + ec = (co_await capy::timeout( stream_.handshake(corosio::tls_stream::handshake_type::client), - timer_, params.ssl_handshake_timeout)) .ec; act = fsm.resume(ec, st_); @@ -102,11 +89,8 @@ capy::io_task<> corosio_redis_stream::connect(const connect_params& params, buff // TODO: range connect socket_.close(); socket_.open(); - auto result = co_await cancel_after( - [&] -> capy::io_task<> { - co_return co_await socket_.connect(*endpoints.begin()); - }(), - timer_, + auto result = co_await capy::timeout( + socket_.connect(*endpoints.begin()), params.connect_timeout); ec = result.ec; act = fsm.resume(ec, *endpoints.begin(), st_); @@ -275,9 +259,8 @@ inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) } case sentinel_action::type::request: { - auto [request_ec] = co_await cancel_after( + auto [request_ec] = co_await capy::timeout( async_exec_one(conn, conn.st_.cfg.sentinel.setup, make_sentinel_adapter(conn.st_)), - conn.reconnect_timer_, conn.st_.cfg.sentinel.request_timeout); ec = request_ec; break; @@ -304,10 +287,9 @@ inline capy::io_task<> writer(corosio_connection_impl& conn) case writer_action_type::done: co_return {act.error()}; case writer_action_type::write_some: { - auto [write_ec, write_bytes] = co_await cancel_at( + auto [write_ec, write_bytes] = co_await maybe_timeout( conn.stream_.write_some(capy::make_buffer(conn.st_.mpx.get_write_buffer())), - conn.writer_timer_, - compute_expiry(act.timeout())); + act.timeout()); ec = write_ec; bytes_written = write_bytes; break; @@ -336,10 +318,9 @@ inline capy::io_task<> reader(corosio_connection_impl& conn) switch (act.get_type()) { case reader_fsm::action::type::read_some: { - auto [read_ec, read_bytes] = co_await cancel_at( + auto [read_ec, read_bytes] = co_await maybe_timeout( conn.stream_.read_some(capy::make_buffer(conn.st_.mpx.get_prepared_read_buffer())), - conn.reader_timer_, - compute_expiry(act.timeout())); + act.timeout()); ec = read_ec; n = read_bytes; break; @@ -377,9 +358,9 @@ inline capy::io_task<> run(corosio_connection_impl& conn) break; case run_action_type::parallel_group: { - auto [winner_index, result] = co_await capy::when_any(reader(conn), writer(conn)); - ignore_unused(winner_index); - ec = std::get<0>(result).ec; + auto result = co_await capy::when_any(reader(conn), writer(conn)); + BOOST_ASSERT(result.index() == 0u); // reader and writer always finish with an error + ec = std::get<0>(result); break; } case run_action_type::cancel_receive: break; // no longer required @@ -407,11 +388,10 @@ capy::io_task<> connection::run(config const& cfg) impl_->st_.mpx.set_config(cfg); impl_->run_cancelled_event_.clear(); - auto [winner_index, result] = co_await capy::when_any( - detail::run(*impl_), - impl_->run_cancelled_event_.wait()); + auto result = co_await capy::when_any(detail::run(*impl_), impl_->run_cancelled_event_.wait()); - co_return {winner_index == 0u ? std::get<0>(result).ec : capy::error::canceled}; + // If run finished first, return its result (run never returns successfully). Otherwise, return a cancellation + co_return {result.index() == 0u ? std::get<0>(result) : capy::error::canceled}; } capy::io_task<> connection::receive() { return detail::receive(*impl_); } diff --git a/test/test_conn_check_health.cpp b/test/test_conn_check_health.cpp index 7868a52f..1ef46d0d 100644 --- a/test/test_conn_check_health.cpp +++ b/test/test_conn_check_health.cpp @@ -35,7 +35,7 @@ capy::task test_reconnection() // Setup connection conn{(co_await capy::this_coro::executor).context()}; - auto exec_fn = [&]() -> capy::task { + auto exec_fn = [&]() -> capy::io_task<> { // This request will block forever, causing the connection to become unresponsive request req1; req1.push("BLPOP", "any", 0); @@ -55,15 +55,19 @@ capy::task test_reconnection() // Execute the second request. This one will succeed after reconnection auto [ec2] = co_await conn.exec(req2, ignore); BOOST_TEST_EQ(ec2, error_code()); + + co_return {}; }; - auto run_fn = [&]() -> capy::task { + auto run_fn = [&]() -> capy::io_task<> { // Make the test run faster auto cfg = make_test_config(); cfg.health_check_interval = 500ms; auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + + co_return {}; }; co_await capy::when_any(exec_fn(), run_fn()); @@ -74,16 +78,18 @@ capy::task test_error_code() { connection conn{(co_await capy::this_coro::executor).context()}; - auto exec_fn = [&]() -> capy::task { + auto exec_fn = [&]() -> capy::io_task<> { // This request will block forever, causing the connection to become unresponsive request req; req.push("BLPOP", "any", 0); auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, capy::error::canceled); + + co_return {}; }; - auto run_fn = [&]() -> capy::task { + auto run_fn = [&]() -> capy::io_task<> { // Make the test run faster auto cfg = make_test_config(); cfg.health_check_interval = 200ms; @@ -91,6 +97,8 @@ capy::task test_error_code() auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, boost::redis::error::pong_timeout); + + co_return {}; }; co_await capy::when_any(exec_fn(), run_fn()); @@ -101,7 +109,7 @@ capy::task test_disabled() { connection conn{(co_await capy::this_coro::executor).context()}; - auto exec_fn = [&]() -> capy::task { + auto exec_fn = [&]() -> capy::io_task<> { // Run a couple of requests to verify that the connection works fine request req1; req1.push("PING", "health_check_disabled_1"); @@ -114,13 +122,17 @@ capy::task test_disabled() auto [ec2] = co_await conn.exec(req1, ignore); BOOST_TEST_EQ(ec2, std::error_code()); + + co_return {}; }; - auto run_fn = [&]() -> capy::task { + auto run_fn = [&]() -> capy::io_task<> { auto cfg = make_test_config(); cfg.health_check_interval = 0s; auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + + co_return {}; }; co_await capy::when_any(exec_fn(), run_fn()); @@ -147,17 +159,19 @@ capy::task test_flexible() cfg.health_check_interval = 500ms; std::string channel_name = make_unique_id(); - auto run1_fn = [&] -> capy::task { + auto run1_fn = [&]() -> capy::io_task<> { auto [ec] = co_await conn1.run(cfg); BOOST_TEST_EQ(ec, capy::error::canceled); + co_return {}; }; - auto run2_fn = [&] -> capy::task { + auto run2_fn = [&]() -> capy::io_task<> { auto [ec] = co_await conn2.run(cfg); BOOST_TEST_EQ(ec, capy::error::canceled); + co_return {}; }; - auto exec_fn = [&] -> capy::task { + auto exec_fn = [&]() -> capy::io_task<> { // This request will block for much longer than the health check // interval. If we weren't receiving pushes, the connection would be considered dead. // If this request finishes successfully, the health checker is doing good @@ -171,9 +185,11 @@ capy::task test_flexible() generic_response resp; auto [ec] = co_await conn1.exec(blocking_req, resp); BOOST_TEST_EQ(ec, error_code()); + + co_return {}; }; - auto publish_fn = [&] -> capy::task { + auto publish_fn = [&]() -> capy::io_task<> { request publish_req; publish_req.push("PUBLISH", channel_name, "test_health_check_flexible"); @@ -183,14 +199,14 @@ capy::task test_flexible() // Publish a message auto [ec] = co_await conn2.exec(publish_req, ignore); if (ec == capy::error::canceled) - co_return; + co_return {}; BOOST_TEST_EQ(ec, error_code()); // Wait for some time and publish again timer.expires_after(100ms); auto [ec2] = co_await timer.wait(); if (ec2 == capy::error::canceled) - co_return; + co_return {}; BOOST_TEST_EQ(ec2, error_code()); } }; From 5f46eb3daf2a4ee72d0614e62eb18e88ca60473b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 17 Apr 2026 12:16:29 +0200 Subject: [PATCH 029/115] Fix when_any changes --- include/boost/redis/impl/connection.ipp | 35 +++++++++++++++++-------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/connection.ipp index e32517ca..f188f359 100644 --- a/include/boost/redis/impl/connection.ipp +++ b/include/boost/redis/impl/connection.ipp @@ -15,7 +15,9 @@ #include #include #include +#include #include +#include namespace boost::redis { namespace detail { @@ -37,7 +39,7 @@ inline asio::cancellation_type_t token_to_cancel(std::stop_token tok) // Run an operation with a timeout, with a zero timeout meaning 'no timeout' template capy::task> maybe_timeout( - Aw&& aw, + Aw aw, std::chrono::steady_clock::duration timeout) { if (timeout.count() == 0) @@ -269,7 +271,8 @@ inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) } } -inline capy::io_task<> writer(corosio_connection_impl& conn) +// This signature is required because capy::when_any is equivalent to wait_for_one_success +inline capy::io_task writer(corosio_connection_impl& conn) { // Setup writer_fsm fsm; @@ -284,7 +287,7 @@ inline capy::io_task<> writer(corosio_connection_impl& conn) token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type()) { - case writer_action_type::done: co_return {act.error()}; + case writer_action_type::done: co_return {{}, act.error()}; case writer_action_type::write_some: { auto [write_ec, write_bytes] = co_await maybe_timeout( @@ -306,7 +309,7 @@ inline capy::io_task<> writer(corosio_connection_impl& conn) } } -inline capy::io_task<> reader(corosio_connection_impl& conn) +inline capy::io_task reader(corosio_connection_impl& conn) { reader_fsm fsm; std::size_t n = 0u; @@ -335,12 +338,12 @@ inline capy::io_task<> reader(corosio_connection_impl& conn) conn.controller_.put(act.push_size()); break; } - case reader_fsm::action::type::done: co_return {act.error()}; + case reader_fsm::action::type::done: co_return {{}, act.error()}; } } } -inline capy::io_task<> run(corosio_connection_impl& conn) +inline capy::io_task run(corosio_connection_impl& conn) { run_fsm fsm; system::error_code ec; @@ -349,7 +352,7 @@ inline capy::io_task<> run(corosio_connection_impl& conn) auto act = fsm.resume(conn.st_, ec, token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type) { - case run_action_type::done: co_return {act.ec}; + case run_action_type::done: co_return {{}, act.ec}; case run_action_type::immediate: break; // no longer required case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve(conn)).ec; break; case run_action_type::connect: @@ -359,8 +362,11 @@ inline capy::io_task<> run(corosio_connection_impl& conn) case run_action_type::parallel_group: { auto result = co_await capy::when_any(reader(conn), writer(conn)); - BOOST_ASSERT(result.index() == 0u); // reader and writer always finish with an error - ec = std::get<0>(result); + ec = std::visit( + [](std::error_code value) { + return value; + }, + result); break; } case run_action_type::cancel_receive: break; // no longer required @@ -390,8 +396,15 @@ capy::io_task<> connection::run(config const& cfg) auto result = co_await capy::when_any(detail::run(*impl_), impl_->run_cancelled_event_.wait()); - // If run finished first, return its result (run never returns successfully). Otherwise, return a cancellation - co_return {result.index() == 0u ? std::get<0>(result) : capy::error::canceled}; + struct visitor { + // Either error or run finished 1st + std::error_code operator()(std::error_code val) const { return val; } + + // The event finishes 1st + std::error_code operator()(std::tuple<>) const { return capy::error::canceled; } + }; + + co_return std::visit(visitor{}, result); } capy::io_task<> connection::receive() { return detail::receive(*impl_); } From 26cacb7d04445bb7c01b9b867620895e4ef827f5 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 10:07:49 +0200 Subject: [PATCH 030/115] Fix timeout checking --- include/boost/redis/impl/reader_fsm.ipp | 4 ++-- include/boost/redis/impl/writer_fsm.ipp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/boost/redis/impl/reader_fsm.ipp b/include/boost/redis/impl/reader_fsm.ipp index 5ce13b37..bcade27c 100644 --- a/include/boost/redis/impl/reader_fsm.ipp +++ b/include/boost/redis/impl/reader_fsm.ipp @@ -12,8 +12,8 @@ #include #include -#include #include +#include namespace boost::redis::detail { @@ -49,7 +49,7 @@ reader_fsm::action reader_fsm::resume( // Translate timeout errors caused by canceled to more legible ones. // A timeout here means that we didn't receive data in time. // Note that cancellation is already handled by the above statement. - if (ec == capy::cond::canceled) { + if (ec == capy::cond::timeout) { ec = error::pong_timeout; } diff --git a/include/boost/redis/impl/writer_fsm.ipp b/include/boost/redis/impl/writer_fsm.ipp index 82cee689..081dbb72 100644 --- a/include/boost/redis/impl/writer_fsm.ipp +++ b/include/boost/redis/impl/writer_fsm.ipp @@ -19,9 +19,9 @@ #include #include -#include #include #include +#include #include #include @@ -82,7 +82,7 @@ writer_action writer_fsm::resume( // Check for cancellations and translate error codes if (is_terminal_cancel(cancel_state)) ec = capy::error::canceled; - else if (ec == capy::cond::canceled) + else if (ec == capy::cond::timeout) ec = error::write_timeout; // Check for errors From 5057e6154c4c19ef3b78c07302bded3be4d8f999 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 10:20:09 +0200 Subject: [PATCH 031/115] Use range connect --- include/boost/redis/impl/connection.ipp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/connection.ipp index f188f359..43238c8b 100644 --- a/include/boost/redis/impl/connection.ipp +++ b/include/boost/redis/impl/connection.ipp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -88,14 +89,11 @@ capy::io_task<> corosio_redis_stream::connect(const connect_params& params, buff case connect_action_type::done: co_return {act.ec}; case connect_action_type::tcp_connect: { - // TODO: range connect - socket_.close(); - socket_.open(); auto result = co_await capy::timeout( - socket_.connect(*endpoints.begin()), + corosio::connect(socket_, std::move(endpoints)), params.connect_timeout); ec = result.ec; - act = fsm.resume(ec, *endpoints.begin(), st_); + act = fsm.resume(ec, result.get<1>(), st_); break; } default: BOOST_ASSERT(false); From f0c8f1cbbad28060f3572830ada8744b86a63b83 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 10:31:55 +0200 Subject: [PATCH 032/115] Rename to co_connection --- example/cpp20_intro.cpp | 6 +-- .../{connection.hpp => co_connection.hpp} | 40 ++++++++-------- .../{connection.ipp => co_connection.ipp} | 48 ++++++++++--------- include/boost/redis/src.hpp | 2 +- test/test_conn_check_health.cpp | 26 +++++----- test/test_conn_setup.cpp | 16 +++---- 6 files changed, 70 insertions(+), 68 deletions(-) rename include/boost/redis/{connection.hpp => co_connection.hpp} (95%) rename include/boost/redis/impl/{connection.ipp => co_connection.ipp} (89%) diff --git a/example/cpp20_intro.cpp b/example/cpp20_intro.cpp index 4f0b4d5e..f1130192 100644 --- a/example/cpp20_intro.cpp +++ b/example/cpp20_intro.cpp @@ -4,8 +4,8 @@ * accompanying file LICENSE.txt) */ +#include #include -#include #include #include @@ -20,7 +20,7 @@ namespace capy = boost::capy; using namespace boost::redis; namespace corosio = boost::corosio; -capy::task run_request(connection& conn) +capy::task run_request(co_connection& conn) { // A request containing only a ping command. request req; @@ -39,7 +39,7 @@ capy::task run_request(connection& conn) capy::task co_main() { // Create a connection - connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{(co_await capy::this_coro::executor).context()}; auto r = co_await capy::when_any(run_request(conn), conn.run(config{})); diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/co_connection.hpp similarity index 95% rename from include/boost/redis/connection.hpp rename to include/boost/redis/co_connection.hpp index 10d87b04..17e40612 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/co_connection.hpp @@ -1,11 +1,13 @@ -/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// -#ifndef BOOST_REDIS_COROSIO_CONNECTION_HPP -#define BOOST_REDIS_COROSIO_CONNECTION_HPP +#ifndef BOOST_REDIS_CO_CONNECTION_HPP +#define BOOST_REDIS_CO_CONNECTION_HPP #include #include @@ -29,10 +31,10 @@ #include #include -#include #include #include #include +#include #include #include #include @@ -50,7 +52,6 @@ #include #include #include -#include #include #include @@ -64,7 +65,7 @@ namespace boost::redis { namespace detail { -class corosio_redis_stream { +class co_redis_stream { // TODO: UNIX sockets corosio::tcp_socket socket_; corosio::openssl_stream stream_; // TODO: make this configurable @@ -73,7 +74,7 @@ class corosio_redis_stream { redis_stream_state st_; public: - explicit corosio_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) + explicit co_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) : socket_(ctx) , stream_(&socket_, std::move(tls_ctx)) , timer_(ctx) @@ -106,9 +107,9 @@ class corosio_redis_stream { } }; -struct corosio_connection_impl { +struct co_connection_impl { capy::async_event run_cancelled_event_; - corosio_redis_stream stream_; + co_redis_stream stream_; corosio::timer writer_timer_; // timer used for write timeouts corosio::timer writer_cv_; // set when there is new data to write corosio::timer reader_timer_; // timer used for read timeouts @@ -117,10 +118,7 @@ struct corosio_connection_impl { flow_controller controller_; connection_state st_; - corosio_connection_impl( - capy::execution_context& ctx, - corosio::tls_context&& ssl_ctx, - logger&& lgr); + co_connection_impl(capy::execution_context& ctx, corosio::tls_context&& ssl_ctx, logger&& lgr); void cancel(); @@ -139,7 +137,7 @@ struct corosio_connection_impl { * * @tparam Executor The executor type used to create any required I/O objects. */ -class connection { +class co_connection { public: /** @brief Constructor from an executor. * @@ -149,7 +147,7 @@ class connection { * and customize logging. By default, `logger::level::info` messages * and higher are logged to `stderr`. */ - explicit connection( + explicit co_connection( capy::execution_context& ctx, corosio::tls_context ssl_ctx = {}, logger lgr = {}); @@ -163,7 +161,7 @@ class connection { * * An SSL context with default settings will be created. */ - connection(capy::execution_context& ctx, logger lgr); + co_connection(capy::execution_context& ctx, logger lgr); /** @brief Starts the underlying connection operations. * @@ -420,7 +418,7 @@ class connection { usage get_usage() const noexcept { return impl_->st_.mpx.get_usage(); } private: - std::unique_ptr impl_; + std::unique_ptr impl_; }; } // namespace boost::redis diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/co_connection.ipp similarity index 89% rename from include/boost/redis/impl/connection.ipp rename to include/boost/redis/impl/co_connection.ipp index 43238c8b..406e0bd2 100644 --- a/include/boost/redis/impl/connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -1,10 +1,12 @@ -/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// -#include +#include #include #include @@ -49,7 +51,7 @@ capy::task> maybe_timeout( co_return co_await capy::timeout(std::move(aw), timeout); } -capy::io_task<> corosio_redis_stream::connect(const connect_params& params, buffered_logger& l) +capy::io_task<> co_redis_stream::connect(const connect_params& params, buffered_logger& l) { connect_fsm fsm{l}; system::error_code ec; @@ -101,7 +103,7 @@ capy::io_task<> corosio_redis_stream::connect(const connect_params& params, buff } } -corosio_connection_impl::corosio_connection_impl( +co_connection_impl::co_connection_impl( capy::execution_context& ctx, corosio::tls_context&& ssl_ctx, logger&& lgr) @@ -118,7 +120,7 @@ corosio_connection_impl::corosio_connection_impl( writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); } -void corosio_connection_impl::cancel() +void co_connection_impl::cancel() { // exec st_.mpx.cancel_waiting(); @@ -133,7 +135,7 @@ void corosio_connection_impl::cancel() run_cancelled_event_.set(); } -capy::io_task<> corosio_connection_impl::exec(request const& req, any_adapter adapter) +capy::io_task<> co_connection_impl::exec(request const& req, any_adapter adapter) { // Setup capy::async_event request_done; @@ -164,12 +166,12 @@ capy::io_task<> corosio_connection_impl::exec(request const& req, any_adapter ad } } -void corosio_connection_impl::set_receive_adapter(any_adapter adapter) +void co_connection_impl::set_receive_adapter(any_adapter adapter) { st_.mpx.set_receive_adapter(std::move(adapter)); } -inline capy::io_task<> receive(corosio_connection_impl& conn) +inline capy::io_task<> receive(co_connection_impl& conn) { // Setup receive_fsm fsm; @@ -197,7 +199,7 @@ inline capy::io_task<> receive(corosio_connection_impl& conn) } inline capy::io_task<> async_exec_one( - corosio_connection_impl& conn, + co_connection_impl& conn, const request& req, any_adapter resp) { @@ -235,7 +237,7 @@ inline capy::io_task<> async_exec_one( } } -inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) +inline capy::io_task<> sentinel_resolve(co_connection_impl& conn) { // Setup sentinel_resolve_fsm fsm; @@ -270,7 +272,7 @@ inline capy::io_task<> sentinel_resolve(corosio_connection_impl& conn) } // This signature is required because capy::when_any is equivalent to wait_for_one_success -inline capy::io_task writer(corosio_connection_impl& conn) +inline capy::io_task writer(co_connection_impl& conn) { // Setup writer_fsm fsm; @@ -307,7 +309,7 @@ inline capy::io_task writer(corosio_connection_impl& conn) } } -inline capy::io_task reader(corosio_connection_impl& conn) +inline capy::io_task reader(co_connection_impl& conn) { reader_fsm fsm; std::size_t n = 0u; @@ -341,7 +343,7 @@ inline capy::io_task reader(corosio_connection_impl& conn) } } -inline capy::io_task run(corosio_connection_impl& conn) +inline capy::io_task run(co_connection_impl& conn) { run_fsm fsm; system::error_code ec; @@ -378,15 +380,15 @@ inline capy::io_task run(corosio_connection_impl& conn) } // namespace detail -connection::connection(capy::execution_context& ctx, corosio::tls_context ssl_ctx, logger lgr) -: impl_(std::make_unique(ctx, std::move(ssl_ctx), std::move(lgr))) +co_connection::co_connection(capy::execution_context& ctx, corosio::tls_context ssl_ctx, logger lgr) +: impl_(std::make_unique(ctx, std::move(ssl_ctx), std::move(lgr))) { } -connection::connection(capy::execution_context& ctx, logger lgr) -: connection(ctx, {}, std::move(lgr)) +co_connection::co_connection(capy::execution_context& ctx, logger lgr) +: co_connection(ctx, {}, std::move(lgr)) { } -capy::io_task<> connection::run(config const& cfg) +capy::io_task<> co_connection::run(config const& cfg) { impl_->st_.cfg = cfg; impl_->st_.mpx.set_config(cfg); @@ -405,6 +407,6 @@ capy::io_task<> connection::run(config const& cfg) co_return std::visit(visitor{}, result); } -capy::io_task<> connection::receive() { return detail::receive(*impl_); } +capy::io_task<> co_connection::receive() { return detail::receive(*impl_); } } // namespace boost::redis diff --git a/include/boost/redis/src.hpp b/include/boost/redis/src.hpp index 196b2960..a626d696 100644 --- a/include/boost/redis/src.hpp +++ b/include/boost/redis/src.hpp @@ -4,8 +4,8 @@ * accompanying file LICENSE.txt) */ +#include #include -#include #include #include #include diff --git a/test/test_conn_check_health.cpp b/test/test_conn_check_health.cpp index 1ef46d0d..4ec56443 100644 --- a/test/test_conn_check_health.cpp +++ b/test/test_conn_check_health.cpp @@ -1,10 +1,12 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#include +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include #include #include #include @@ -33,7 +35,7 @@ namespace { capy::task test_reconnection() { // Setup - connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{(co_await capy::this_coro::executor).context()}; auto exec_fn = [&]() -> capy::io_task<> { // This request will block forever, causing the connection to become unresponsive @@ -76,7 +78,7 @@ capy::task test_reconnection() // We use the correct error code when a ping times out capy::task test_error_code() { - connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{(co_await capy::this_coro::executor).context()}; auto exec_fn = [&]() -> capy::io_task<> { // This request will block forever, causing the connection to become unresponsive @@ -107,7 +109,7 @@ capy::task test_error_code() // A ping interval of zero disables timeouts (and doesn't cause trouble) capy::task test_disabled() { - connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{(co_await capy::this_coro::executor).context()}; auto exec_fn = [&]() -> capy::io_task<> { // Run a couple of requests to verify that the connection works fine @@ -153,8 +155,8 @@ std::string make_unique_id() capy::task test_flexible() { // Setup - connection conn1{(co_await capy::this_coro::executor).context()}; - connection conn2{(co_await capy::this_coro::executor).context()}; + co_connection conn1{(co_await capy::this_coro::executor).context()}; + co_connection conn2{(co_await capy::this_coro::executor).context()}; auto cfg = make_test_config(); cfg.health_check_interval = 500ms; std::string channel_name = make_unique_id(); diff --git a/test/test_conn_setup.cpp b/test/test_conn_setup.cpp index fec28bc9..1738e2c5 100644 --- a/test/test_conn_setup.cpp +++ b/test/test_conn_setup.cpp @@ -41,7 +41,7 @@ capy::task create_user( std::string_view username, std::string_view password) { - connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{(co_await capy::this_coro::executor).context()}; auto exec_fn = [&]() -> capy::task { // Enable the user and grant them permissions on everything @@ -66,7 +66,7 @@ capy::task create_user( capy::task<> test_auth_success() { // Setup - connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{(co_await capy::this_coro::executor).context()}; auto request_fn = [&] -> capy::task { // This request should return the username we're logged in as @@ -97,7 +97,7 @@ capy::task<> test_auth_failure() { // Setup std::string logs; - connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + co_connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; // Disable reconnection so the hello error causes the connection to exit auto cfg = make_test_config(); @@ -117,7 +117,7 @@ capy::task<> test_auth_failure() capy::task<> test_database_index() { // Setup - connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{(co_await capy::this_coro::executor).context()}; auto request_fn = [&] -> capy::task { request req; @@ -145,7 +145,7 @@ capy::task<> test_database_index() capy::task<> test_setup_empty() { // Setup - connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{(co_await capy::this_coro::executor).context()}; auto request_fn = [&] -> capy::task { request req; @@ -173,7 +173,7 @@ capy::task<> test_setup_empty() capy::task<> test_setup_hello() { // Setup - connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{(co_await capy::this_coro::executor).context()}; auto request_fn = [&] -> capy::task { request req; @@ -205,7 +205,7 @@ capy::task<> test_setup_hello() capy::task<> test_setup_no_hello() { // Setup - connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{(co_await capy::this_coro::executor).context()}; auto request_fn = [&] -> capy::task { request req; @@ -236,7 +236,7 @@ capy::task<> test_setup_failure() { // Setup std::string logs; - connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + co_connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; // Disable reconnection so the hello error causes the connection to exit auto cfg = make_test_config(); From c75863b6f6e1d586b28517b223c01b5b0f1a8a4b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 10:37:48 +0200 Subject: [PATCH 033/115] Rename exmaple and tests --- example/corosio_intro.cpp | 65 ++++++ test/CMakeLists.txt | 2 + test/test_co_conn_check_health.cpp | 229 +++++++++++++++++++ test/test_co_conn_setup.cpp | 272 ++++++++++++++++++++++ test/test_conn_check_health.cpp | 331 +++++++++++++++------------ test/test_conn_setup.cpp | 348 +++++++++++++++-------------- 6 files changed, 933 insertions(+), 314 deletions(-) create mode 100644 example/corosio_intro.cpp create mode 100644 test/test_co_conn_check_health.cpp create mode 100644 test/test_co_conn_setup.cpp diff --git a/example/corosio_intro.cpp b/example/corosio_intro.cpp new file mode 100644 index 00000000..2a7ee0f4 --- /dev/null +++ b/example/corosio_intro.cpp @@ -0,0 +1,65 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +// TODO: re-add this to CMake! + +namespace capy = boost::capy; +using namespace boost::redis; +namespace corosio = boost::corosio; + +capy::task run_request(co_connection& conn) +{ + // A request containing only a ping command. + request req; + req.push("PING", "Hello world"); + + // Response where the PONG response will be stored. + response resp; + + // Executes the request. + auto [ec] = co_await conn.exec(req, resp); + if (ec) + co_return; + std::cout << "PING value: " << std::get<0>(resp).value() << std::endl; +} + +capy::task co_main() +{ + // Create a connection + co_connection conn{(co_await capy::this_coro::executor).context()}; + + auto r = co_await capy::when_any(run_request(conn), conn.run(config{})); + + static_cast(r); +} + +struct handler { + void operator()() { std::cout << "Done\n"; } + void operator()(std::exception_ptr exc) + { + if (exc) + std::rethrow_exception(exc); + } +}; + +int main() +{ + corosio::io_context ctx; + capy::run_async(ctx.get_executor())(co_main()); + ctx.run(); +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 46b85ff3..05836b50 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -65,6 +65,7 @@ make_test(test_conn_exec_error) make_test(test_run) make_test(test_conn_run_cancel) make_test(test_conn_check_health) +make_test(test_co_conn_check_health) make_test(test_conn_exec) make_test(test_conn_push) make_test(test_conn_push2) @@ -74,6 +75,7 @@ make_test(test_conn_exec_cancel) make_test(test_conn_echo_stress) make_test(test_conn_move) make_test(test_conn_setup) +make_test(test_co_conn_setup) make_test(test_issue_50) make_test(test_conversions) make_test(test_conn_tls) diff --git a/test/test_co_conn_check_health.cpp b/test/test_co_conn_check_health.cpp new file mode 100644 index 00000000..4ec56443 --- /dev/null +++ b/test/test_co_conn_check_health.cpp @@ -0,0 +1,229 @@ +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "common.hpp" + +#include +#include +#include + +namespace capy = boost::capy; +using namespace boost::redis; +using error_code = std::error_code; +using namespace std::chrono_literals; + +namespace { + +// The health checker detects dead connections and triggers reconnection +capy::task test_reconnection() +{ + // Setup + co_connection conn{(co_await capy::this_coro::executor).context()}; + + auto exec_fn = [&]() -> capy::io_task<> { + // This request will block forever, causing the connection to become unresponsive + request req1; + req1.push("BLPOP", "any", 0); + + // This request should be executed after reconnection + request req2; + req2.push("PING", "after_reconnection"); + req2.get_config().cancel_if_unresponded = false; + req2.get_config().cancel_on_connection_lost = false; + + // This request will complete after the health checker deems the connection + // as unresponsive and triggers a reconnection (it's configured to be cancelled + // on connection lost). + auto [ec1] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec1, error_code(capy::error::canceled)); + + // Execute the second request. This one will succeed after reconnection + auto [ec2] = co_await conn.exec(req2, ignore); + BOOST_TEST_EQ(ec2, error_code()); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + // Make the test run faster + auto cfg = make_test_config(); + cfg.health_check_interval = 500ms; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + + co_return {}; + }; + + co_await capy::when_any(exec_fn(), run_fn()); +} + +// We use the correct error code when a ping times out +capy::task test_error_code() +{ + co_connection conn{(co_await capy::this_coro::executor).context()}; + + auto exec_fn = [&]() -> capy::io_task<> { + // This request will block forever, causing the connection to become unresponsive + request req; + req.push("BLPOP", "any", 0); + + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, capy::error::canceled); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + // Make the test run faster + auto cfg = make_test_config(); + cfg.health_check_interval = 200ms; + cfg.reconnect_wait_interval = 0s; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, boost::redis::error::pong_timeout); + + co_return {}; + }; + + co_await capy::when_any(exec_fn(), run_fn()); +} + +// A ping interval of zero disables timeouts (and doesn't cause trouble) +capy::task test_disabled() +{ + co_connection conn{(co_await capy::this_coro::executor).context()}; + + auto exec_fn = [&]() -> capy::io_task<> { + // Run a couple of requests to verify that the connection works fine + request req1; + req1.push("PING", "health_check_disabled_1"); + + request req2; + req1.push("PING", "health_check_disabled_2"); + + auto [ec1] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec1, std::error_code()); + + auto [ec2] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec2, std::error_code()); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_test_config(); + cfg.health_check_interval = 0s; + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + + co_return {}; + }; + + co_await capy::when_any(exec_fn(), run_fn()); +} + +// Generates a sufficiently unique name for channels so +// tests may be run in parallel for different configurations +std::string make_unique_id() +{ + auto t = std::chrono::high_resolution_clock::now(); + return "test-flexible-health-checks-" + std::to_string(t.time_since_epoch().count()); +} + +// Receiving data is sufficient to consider our connection healthy. +// Sends a blocking request that causes PINGs to not be answered, +// and subscribes to a channel to receive pushes periodically. +// This simulates situations of heavy load, where PINGs may not be answered on time. +capy::task test_flexible() +{ + // Setup + co_connection conn1{(co_await capy::this_coro::executor).context()}; + co_connection conn2{(co_await capy::this_coro::executor).context()}; + auto cfg = make_test_config(); + cfg.health_check_interval = 500ms; + std::string channel_name = make_unique_id(); + + auto run1_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn1.run(cfg); + BOOST_TEST_EQ(ec, capy::error::canceled); + co_return {}; + }; + + auto run2_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn2.run(cfg); + BOOST_TEST_EQ(ec, capy::error::canceled); + co_return {}; + }; + + auto exec_fn = [&]() -> capy::io_task<> { + // This request will block for much longer than the health check + // interval. If we weren't receiving pushes, the connection would be considered dead. + // If this request finishes successfully, the health checker is doing good + request blocking_req; + blocking_req.push("SUBSCRIBE", channel_name); + blocking_req.push("BLPOP", "any", 2); + blocking_req.get_config().cancel_if_unresponded = true; + blocking_req.get_config().cancel_on_connection_lost = true; + + // BLPOP will return NIL, so we can't use ignore + generic_response resp; + auto [ec] = co_await conn1.exec(blocking_req, resp); + BOOST_TEST_EQ(ec, error_code()); + + co_return {}; + }; + + auto publish_fn = [&]() -> capy::io_task<> { + request publish_req; + publish_req.push("PUBLISH", channel_name, "test_health_check_flexible"); + + boost::corosio::timer timer{(co_await capy::this_coro::executor).context()}; + + while (true) { + // Publish a message + auto [ec] = co_await conn2.exec(publish_req, ignore); + if (ec == capy::error::canceled) + co_return {}; + BOOST_TEST_EQ(ec, error_code()); + + // Wait for some time and publish again + timer.expires_after(100ms); + auto [ec2] = co_await timer.wait(); + if (ec2 == capy::error::canceled) + co_return {}; + BOOST_TEST_EQ(ec2, error_code()); + } + }; + + co_await capy::when_any(run1_fn(), run2_fn(), exec_fn(), publish_fn()); +} + +} // namespace + +int main() +{ + run_coroutine_test(test_reconnection()); + run_coroutine_test(test_error_code()); + run_coroutine_test(test_disabled()); + run_coroutine_test(test_flexible()); + + return boost::report_errors(); +} \ No newline at end of file diff --git a/test/test_co_conn_setup.cpp b/test/test_co_conn_setup.cpp new file mode 100644 index 00000000..1738e2c5 --- /dev/null +++ b/test/test_co_conn_setup.cpp @@ -0,0 +1,272 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" + +#include +#include +#include +#include + +namespace asio = boost::asio; + +using namespace boost::redis; +using namespace std::chrono_literals; +namespace capy = boost::capy; +namespace corosio = boost::corosio; + +namespace { + +capy::task create_user( + std::string_view port, + std::string_view username, + std::string_view password) +{ + co_connection conn{(co_await capy::this_coro::executor).context()}; + + auto exec_fn = [&]() -> capy::task { + // Enable the user and grant them permissions on everything + request req; + req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); + + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, std::error_code()); + }; + + auto run_fn = [&]() -> capy::task { + config cfg; + cfg.addr.port = port; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(exec_fn(), run_fn()); +} + +capy::task<> test_auth_success() +{ + // Setup + co_connection conn{(co_await capy::this_coro::executor).context()}; + + auto request_fn = [&] -> capy::task { + // This request should return the username we're logged in as + request req; + req.push("ACL", "WHOAMI"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser"); + }; + + auto run_fn = [&] -> capy::task { + // These credentials are set up in main, before tests are run + config cfg; + cfg.username = "myuser"; + cfg.password = "mypass"; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(request_fn(), run_fn()); +} + +// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) +capy::task<> test_auth_failure() +{ + // Setup + std::string logs; + co_connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + + // Disable reconnection so the hello error causes the connection to exit + auto cfg = make_test_config(); + cfg.username = "myuser"; + cfg.password = "wrongpass"; // wrong + cfg.reconnect_wait_interval = 0s; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(error::resp3_hello)); + + // Check the log + if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) { + std::cerr << "Log was: \n" << logs << std::endl; + } +} + +capy::task<> test_database_index() +{ + // Setup + co_connection conn{(co_await capy::this_coro::executor).context()}; + + auto request_fn = [&] -> capy::task { + request req; + req.push("CLIENT", "INFO"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "2"); + }; + + auto run_fn = [&] -> capy::task { + // Use a non-default database index + auto cfg = make_test_config(); + cfg.database_index = 2; + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(request_fn(), run_fn()); +} + +// The user configured an empty setup request. No request should be sent +capy::task<> test_setup_empty() +{ + // Setup + co_connection conn{(co_await capy::this_coro::executor).context()}; + + auto request_fn = [&] -> capy::task { + request req; + req.push("CLIENT", "INFO"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 + }; + + auto run_fn = [&] -> capy::task { + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(request_fn(), run_fn()); +} + +// We can use the setup member to run commands at startup +capy::task<> test_setup_hello() +{ + // Setup + co_connection conn{(co_await capy::this_coro::executor).context()}; + + auto request_fn = [&] -> capy::task { + request req; + req.push("CLIENT", "INFO"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "3"); // using RESP3 + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "user"), "myuser"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); + }; + + auto run_fn = [&] -> capy::task { + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("HELLO", "3", "AUTH", "myuser", "mypass"); + cfg.setup.push("SELECT", 8); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(request_fn(), run_fn()); +} + +// Running a pipeline without a HELLO is okay (regression check: we set the priority flag) +capy::task<> test_setup_no_hello() +{ + // Setup + co_connection conn{(co_await capy::this_coro::executor).context()}; + + auto request_fn = [&] -> capy::task { + request req; + req.push("CLIENT", "INFO"); + response resp; + + auto [ec] = co_await conn.exec(req, resp); + + BOOST_TEST_EQ(ec, std::error_code()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); + }; + + auto run_fn = [&] -> capy::task { + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("SELECT", 8); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + }; + + co_await capy::when_any(request_fn(), run_fn()); +} + +// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) +capy::task<> test_setup_failure() +{ + // Setup + std::string logs; + co_connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + + // Disable reconnection so the hello error causes the connection to exit + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("GET", "two", "args"); // GET only accepts one arg, so this will fail + cfg.reconnect_wait_interval = 0s; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, std::error_code(error::resp3_hello)); + + // Check the log + if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) { + std::cerr << "Log was:\n" << logs << std::endl; + } +} + +} // namespace + +int main() +{ + run_coroutine_test(create_user("6379", "myuser", "mypass")); + + run_coroutine_test(test_auth_success()); + run_coroutine_test(test_auth_failure()); + run_coroutine_test(test_database_index()); + run_coroutine_test(test_setup_empty()); + run_coroutine_test(test_setup_hello()); + run_coroutine_test(test_setup_no_hello()); + run_coroutine_test(test_setup_failure()); + + return boost::report_errors(); +} diff --git a/test/test_conn_check_health.cpp b/test/test_conn_check_health.cpp index 4ec56443..5d53b084 100644 --- a/test/test_conn_check_health.cpp +++ b/test/test_conn_check_health.cpp @@ -1,179 +1,219 @@ -// -// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), -// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// - -#include +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include #include #include #include -#include -#include -#include -#include +#include +#include +#include #include -#include #include "common.hpp" #include +#include #include -#include -namespace capy = boost::capy; -using namespace boost::redis; -using error_code = std::error_code; +namespace net = boost::asio; +namespace redis = boost::redis; +using error_code = boost::system::error_code; +using connection = boost::redis::connection; +using boost::redis::request; +using boost::redis::ignore; +using boost::redis::generic_response; using namespace std::chrono_literals; namespace { // The health checker detects dead connections and triggers reconnection -capy::task test_reconnection() +void test_reconnection() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; + net::io_context ioc; + connection conn{ioc}; - auto exec_fn = [&]() -> capy::io_task<> { - // This request will block forever, causing the connection to become unresponsive - request req1; - req1.push("BLPOP", "any", 0); + // This request will block forever, causing the connection to become unresponsive + request req1; + req1.push("BLPOP", "any", 0); - // This request should be executed after reconnection - request req2; - req2.push("PING", "after_reconnection"); - req2.get_config().cancel_if_unresponded = false; - req2.get_config().cancel_on_connection_lost = false; + // This request should be executed after reconnection + request req2; + req2.push("PING", "after_reconnection"); + req2.get_config().cancel_if_unresponded = false; + req2.get_config().cancel_on_connection_lost = false; - // This request will complete after the health checker deems the connection - // as unresponsive and triggers a reconnection (it's configured to be cancelled - // on connection lost). - auto [ec1] = co_await conn.exec(req1, ignore); - BOOST_TEST_EQ(ec1, error_code(capy::error::canceled)); + // Make the test run faster + auto cfg = make_test_config(); + cfg.health_check_interval = 500ms; - // Execute the second request. This one will succeed after reconnection - auto [ec2] = co_await conn.exec(req2, ignore); - BOOST_TEST_EQ(ec2, error_code()); + bool run_finished = false, exec1_finished = false, exec2_finished = false; - co_return {}; - }; + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, net::error::operation_aborted); + }); - auto run_fn = [&]() -> capy::io_task<> { - // Make the test run faster - auto cfg = make_test_config(); - cfg.health_check_interval = 500ms; + // This request will complete after the health checker deems the connection + // as unresponsive and triggers a reconnection (it's configured to be cancelled + // on connection lost). + conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { + exec1_finished = true; + BOOST_TEST_EQ(ec, net::error::operation_aborted); - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + // Execute the second request. This one will succeed after reconnection + conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) { + exec2_finished = true; + BOOST_TEST_EQ(ec2, error_code()); + conn.cancel(); + }); + }); - co_return {}; - }; + ioc.run_for(test_timeout); - co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST(run_finished); + BOOST_TEST(exec1_finished); + BOOST_TEST(exec2_finished); } // We use the correct error code when a ping times out -capy::task test_error_code() +void test_error_code() { - co_connection conn{(co_await capy::this_coro::executor).context()}; - - auto exec_fn = [&]() -> capy::io_task<> { - // This request will block forever, causing the connection to become unresponsive - request req; - req.push("BLPOP", "any", 0); + // Setup + net::io_context ioc; + connection conn{ioc}; - auto [ec] = co_await conn.exec(req, ignore); - BOOST_TEST_EQ(ec, capy::error::canceled); + // This request will block forever, causing the connection to become unresponsive + request req; + req.push("BLPOP", "any", 0); - co_return {}; - }; + // Make the test run faster + auto cfg = make_test_config(); + cfg.health_check_interval = 200ms; + cfg.reconnect_wait_interval = 0s; - auto run_fn = [&]() -> capy::io_task<> { - // Make the test run faster - auto cfg = make_test_config(); - cfg.health_check_interval = 200ms; - cfg.reconnect_wait_interval = 0s; + bool run_finished = false, exec_finished = false; - auto [ec] = co_await conn.run(cfg); + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; BOOST_TEST_EQ(ec, boost::redis::error::pong_timeout); + }); + + // This request will complete after the health checker deems the connection + // as unresponsive and triggers a reconnection (it's configured to be cancelled + // if unresponded). + conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, net::error::operation_aborted); + }); - co_return {}; - }; + ioc.run_for(test_timeout); - co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST(run_finished); + BOOST_TEST(exec_finished); } // A ping interval of zero disables timeouts (and doesn't cause trouble) -capy::task test_disabled() +void test_disabled() { - co_connection conn{(co_await capy::this_coro::executor).context()}; - - auto exec_fn = [&]() -> capy::io_task<> { - // Run a couple of requests to verify that the connection works fine - request req1; - req1.push("PING", "health_check_disabled_1"); + // Setup + net::io_context ioc; + connection conn{ioc}; - request req2; - req1.push("PING", "health_check_disabled_2"); + // Run a couple of requests to verify that the connection works fine + request req1; + req1.push("PING", "health_check_disabled_1"); - auto [ec1] = co_await conn.exec(req1, ignore); - BOOST_TEST_EQ(ec1, std::error_code()); + request req2; + req1.push("PING", "health_check_disabled_2"); - auto [ec2] = co_await conn.exec(req1, ignore); - BOOST_TEST_EQ(ec2, std::error_code()); + auto cfg = make_test_config(); + cfg.health_check_interval = 0s; - co_return {}; - }; + bool run_finished = false, exec1_finished = false, exec2_finished = false; - auto run_fn = [&]() -> capy::io_task<> { - auto cfg = make_test_config(); - cfg.health_check_interval = 0s; - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, net::error::operation_aborted); + }); - co_return {}; - }; + conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { + exec1_finished = true; + BOOST_TEST_EQ(ec, error_code()); + conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) { + exec2_finished = true; + BOOST_TEST_EQ(ec2, error_code()); + conn.cancel(); + }); + }); - co_await capy::when_any(exec_fn(), run_fn()); -} + ioc.run_for(test_timeout); -// Generates a sufficiently unique name for channels so -// tests may be run in parallel for different configurations -std::string make_unique_id() -{ - auto t = std::chrono::high_resolution_clock::now(); - return "test-flexible-health-checks-" + std::to_string(t.time_since_epoch().count()); + BOOST_TEST(run_finished); + BOOST_TEST(exec1_finished); + BOOST_TEST(exec2_finished); } // Receiving data is sufficient to consider our connection healthy. // Sends a blocking request that causes PINGs to not be answered, // and subscribes to a channel to receive pushes periodically. // This simulates situations of heavy load, where PINGs may not be answered on time. -capy::task test_flexible() -{ - // Setup - co_connection conn1{(co_await capy::this_coro::executor).context()}; - co_connection conn2{(co_await capy::this_coro::executor).context()}; - auto cfg = make_test_config(); - cfg.health_check_interval = 500ms; - std::string channel_name = make_unique_id(); +class test_flexible { + net::io_context ioc; + connection conn1{ioc}; // The one that simulates a heavy load condition + connection conn2{ioc}; // Publishes messages + net::steady_timer timer{ioc}; + request publish_req; + bool run1_finished = false, run2_finished = false, exec_finished{false}, + publisher_finished{false}; + + // Starts publishing messages to the channel + void start_publish() + { + conn2.async_exec(publish_req, ignore, [this](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); - auto run1_fn = [&]() -> capy::io_task<> { - auto [ec] = co_await conn1.run(cfg); - BOOST_TEST_EQ(ec, capy::error::canceled); - co_return {}; - }; + if (exec_finished) { + // The blocking request finished, we're done + conn2.cancel(); + publisher_finished = true; + } else { + // Wait for some time and publish again + timer.expires_after(100ms); + timer.async_wait([this](error_code ec) { + BOOST_TEST_EQ(ec, error_code()); + start_publish(); + }); + } + }); + } + + // Generates a sufficiently unique name for channels so + // tests may be run in parallel for different configurations + static std::string make_unique_id() + { + auto t = std::chrono::high_resolution_clock::now(); + return "test-flexible-health-checks-" + std::to_string(t.time_since_epoch().count()); + } + +public: + test_flexible() = default; + + void run() + { + // Setup + auto cfg = make_test_config(); + cfg.health_check_interval = 500ms; + generic_response resp; - auto run2_fn = [&]() -> capy::io_task<> { - auto [ec] = co_await conn2.run(cfg); - BOOST_TEST_EQ(ec, capy::error::canceled); - co_return {}; - }; + std::string channel_name = make_unique_id(); + publish_req.push("PUBLISH", channel_name, "test_health_check_flexible"); - auto exec_fn = [&]() -> capy::io_task<> { // This request will block for much longer than the health check // interval. If we weren't receiving pushes, the connection would be considered dead. // If this request finishes successfully, the health checker is doing good @@ -183,47 +223,42 @@ capy::task test_flexible() blocking_req.get_config().cancel_if_unresponded = true; blocking_req.get_config().cancel_on_connection_lost = true; - // BLPOP will return NIL, so we can't use ignore - generic_response resp; - auto [ec] = co_await conn1.exec(blocking_req, resp); - BOOST_TEST_EQ(ec, error_code()); - - co_return {}; - }; - - auto publish_fn = [&]() -> capy::io_task<> { - request publish_req; - publish_req.push("PUBLISH", channel_name, "test_health_check_flexible"); + conn1.async_run(cfg, [&](error_code ec) { + run1_finished = true; + BOOST_TEST_EQ(ec, net::error::operation_aborted); + }); - boost::corosio::timer timer{(co_await capy::this_coro::executor).context()}; + conn2.async_run(cfg, [&](error_code ec) { + run2_finished = true; + BOOST_TEST_EQ(ec, net::error::operation_aborted); + }); - while (true) { - // Publish a message - auto [ec] = co_await conn2.exec(publish_req, ignore); - if (ec == capy::error::canceled) - co_return {}; + // BLPOP will return NIL, so we can't use ignore + conn1.async_exec(blocking_req, resp, [&](error_code ec, std::size_t) { + exec_finished = true; BOOST_TEST_EQ(ec, error_code()); + conn1.cancel(); + }); - // Wait for some time and publish again - timer.expires_after(100ms); - auto [ec2] = co_await timer.wait(); - if (ec2 == capy::error::canceled) - co_return {}; - BOOST_TEST_EQ(ec2, error_code()); - } - }; + start_publish(); - co_await capy::when_any(run1_fn(), run2_fn(), exec_fn(), publish_fn()); -} + ioc.run_for(test_timeout); + + BOOST_TEST(run1_finished); + BOOST_TEST(run2_finished); + BOOST_TEST(exec_finished); + BOOST_TEST(publisher_finished); + } +}; } // namespace int main() { - run_coroutine_test(test_reconnection()); - run_coroutine_test(test_error_code()); - run_coroutine_test(test_disabled()); - run_coroutine_test(test_flexible()); + test_reconnection(); + test_error_code(); + test_disabled(); + test_flexible().run(); return boost::report_errors(); } \ No newline at end of file diff --git a/test/test_conn_setup.cpp b/test/test_conn_setup.cpp index 1738e2c5..9997cb6b 100644 --- a/test/test_conn_setup.cpp +++ b/test/test_conn_setup.cpp @@ -6,98 +6,71 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include #include -#include #include +#include +#include -#include -#include -#include -#include -#include -#include -#include +#include #include +#include #include "common.hpp" #include #include #include -#include namespace asio = boost::asio; - -using namespace boost::redis; +namespace redis = boost::redis; using namespace std::chrono_literals; -namespace capy = boost::capy; -namespace corosio = boost::corosio; +using boost::system::error_code; namespace { -capy::task create_user( - std::string_view port, - std::string_view username, - std::string_view password) +void test_auth_success() { - co_connection conn{(co_await capy::this_coro::executor).context()}; + // Setup + asio::io_context ioc; + redis::connection conn{ioc}; - auto exec_fn = [&]() -> capy::task { - // Enable the user and grant them permissions on everything - request req; - req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); + // This request should return the username we're logged in as + redis::request req; + req.push("ACL", "WHOAMI"); + redis::response resp; - auto [ec] = co_await conn.exec(req, ignore); - BOOST_TEST_EQ(ec, std::error_code()); - }; + // These credentials are set up in main, before tests are run + auto cfg = make_test_config(); + cfg.username = "myuser"; + cfg.password = "mypass"; - auto run_fn = [&]() -> capy::task { - config cfg; - cfg.addr.port = port; + bool exec_finished = false, run_finished = false; - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); - }; + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, error_code()); + conn.cancel(); + }); - co_await capy::when_any(exec_fn(), run_fn()); -} + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, asio::error::operation_aborted); + }); -capy::task<> test_auth_success() -{ - // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; - - auto request_fn = [&] -> capy::task { - // This request should return the username we're logged in as - request req; - req.push("ACL", "WHOAMI"); - response resp; - - auto [ec] = co_await conn.exec(req, resp); - BOOST_TEST_EQ(ec, std::error_code()); - BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser"); - }; - - auto run_fn = [&] -> capy::task { - // These credentials are set up in main, before tests are run - config cfg; - cfg.username = "myuser"; - cfg.password = "mypass"; - - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); - }; - - co_await capy::when_any(request_fn(), run_fn()); + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); + BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser"); } // Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) -capy::task<> test_auth_failure() +void test_auth_failure() { // Setup std::string logs; - co_connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + asio::io_context ioc; + redis::connection conn{ioc, make_string_logger(logs)}; // Disable reconnection so the hello error causes the connection to exit auto cfg = make_test_config(); @@ -105,8 +78,16 @@ capy::task<> test_auth_failure() cfg.password = "wrongpass"; // wrong cfg.reconnect_wait_interval = 0s; - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(error::resp3_hello)); + bool run_finished = false; + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, redis::error::resp3_hello); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(run_finished); // Check the log if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) { @@ -114,129 +95,156 @@ capy::task<> test_auth_failure() } } -capy::task<> test_database_index() +void test_database_index() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; + asio::io_context ioc; + redis::connection conn(ioc); - auto request_fn = [&] -> capy::task { - request req; - req.push("CLIENT", "INFO"); - response resp; + // Use a non-default database index + auto cfg = make_test_config(); + cfg.database_index = 2; + + redis::request req; + req.push("CLIENT", "INFO"); + + redis::response resp; - auto [ec] = co_await conn.exec(req, resp); + bool exec_finished = false, run_finished = false; - BOOST_TEST_EQ(ec, std::error_code()); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "2"); - }; + conn.async_exec(req, resp, [&](error_code ec, std::size_t n) { + BOOST_TEST_EQ(ec, error_code()); + std::clog << "async_exec has completed: " << n << std::endl; + conn.cancel(); + exec_finished = true; + }); - auto run_fn = [&] -> capy::task { - // Use a non-default database index - auto cfg = make_test_config(); - cfg.database_index = 2; - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); - }; + conn.async_run(cfg, {}, [&run_finished](error_code) { + std::clog << "async_run has exited." << std::endl; + run_finished = true; + }); - co_await capy::when_any(request_fn(), run_fn()); + ioc.run_for(test_timeout); + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "2"); } // The user configured an empty setup request. No request should be sent -capy::task<> test_setup_empty() +void test_setup_empty() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; + asio::io_context ioc; + redis::connection conn(ioc); + + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + + redis::request req; + req.push("CLIENT", "INFO"); - auto request_fn = [&] -> capy::task { - request req; - req.push("CLIENT", "INFO"); - response resp; + redis::response resp; - auto [ec] = co_await conn.exec(req, resp); + bool exec_finished = false, run_finished = false; - BOOST_TEST_EQ(ec, std::error_code()); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 - }; + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.cancel(); + exec_finished = true; + }); - auto run_fn = [&] -> capy::task { - auto cfg = make_test_config(); - cfg.use_setup = true; - cfg.setup.clear(); - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); - }; + conn.async_run(cfg, {}, [&run_finished](error_code) { + run_finished = true; + }); - co_await capy::when_any(request_fn(), run_fn()); + ioc.run_for(test_timeout); + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 } // We can use the setup member to run commands at startup -capy::task<> test_setup_hello() +void test_setup_hello() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; - - auto request_fn = [&] -> capy::task { - request req; - req.push("CLIENT", "INFO"); - response resp; - - auto [ec] = co_await conn.exec(req, resp); - - BOOST_TEST_EQ(ec, std::error_code()); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "3"); // using RESP3 - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "user"), "myuser"); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); - }; - - auto run_fn = [&] -> capy::task { - auto cfg = make_test_config(); - cfg.use_setup = true; - cfg.setup.clear(); - cfg.setup.push("HELLO", "3", "AUTH", "myuser", "mypass"); - cfg.setup.push("SELECT", 8); - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); - }; - - co_await capy::when_any(request_fn(), run_fn()); + asio::io_context ioc; + redis::connection conn(ioc); + + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("HELLO", "3", "AUTH", "myuser", "mypass"); + cfg.setup.push("SELECT", 8); + + redis::request req; + req.push("CLIENT", "INFO"); + + redis::response resp; + + bool exec_finished = false, run_finished = false; + + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.cancel(); + exec_finished = true; + }); + + conn.async_run(cfg, {}, [&run_finished](error_code) { + run_finished = true; + }); + + ioc.run_for(test_timeout); + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "3"); // using RESP3 + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "user"), "myuser"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); } // Running a pipeline without a HELLO is okay (regression check: we set the priority flag) -capy::task<> test_setup_no_hello() +void test_setup_no_hello() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; - - auto request_fn = [&] -> capy::task { - request req; - req.push("CLIENT", "INFO"); - response resp; - - auto [ec] = co_await conn.exec(req, resp); - - BOOST_TEST_EQ(ec, std::error_code()); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 - BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); - }; - - auto run_fn = [&] -> capy::task { - auto cfg = make_test_config(); - cfg.use_setup = true; - cfg.setup.clear(); - cfg.setup.push("SELECT", 8); - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); - }; - - co_await capy::when_any(request_fn(), run_fn()); + asio::io_context ioc; + redis::connection conn(ioc); + + auto cfg = make_test_config(); + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("SELECT", 8); + + redis::request req; + req.push("CLIENT", "INFO"); + + redis::response resp; + + bool exec_finished = false, run_finished = false; + + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.cancel(); + exec_finished = true; + }); + + conn.async_run(cfg, {}, [&run_finished](error_code) { + run_finished = true; + }); + + ioc.run_for(test_timeout); + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP3 + BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); } // Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) -capy::task<> test_setup_failure() +void test_setup_failure() { // Setup std::string logs; - co_connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + asio::io_context ioc; + redis::connection conn{ioc, make_string_logger(logs)}; // Disable reconnection so the hello error causes the connection to exit auto cfg = make_test_config(); @@ -245,8 +253,16 @@ capy::task<> test_setup_failure() cfg.setup.push("GET", "two", "args"); // GET only accepts one arg, so this will fail cfg.reconnect_wait_interval = 0s; - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(error::resp3_hello)); + bool run_finished = false; + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, redis::error::resp3_hello); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(run_finished); // Check the log if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) { @@ -258,15 +274,15 @@ capy::task<> test_setup_failure() int main() { - run_coroutine_test(create_user("6379", "myuser", "mypass")); - - run_coroutine_test(test_auth_success()); - run_coroutine_test(test_auth_failure()); - run_coroutine_test(test_database_index()); - run_coroutine_test(test_setup_empty()); - run_coroutine_test(test_setup_hello()); - run_coroutine_test(test_setup_no_hello()); - run_coroutine_test(test_setup_failure()); + create_user("6379", "myuser", "mypass"); + + test_auth_success(); + test_auth_failure(); + test_database_index(); + test_setup_empty(); + test_setup_hello(); + test_setup_no_hello(); + test_setup_failure(); return boost::report_errors(); } From af04ac3e4397fa1941cb31401b3c1080c35515bf Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 10:44:24 +0200 Subject: [PATCH 034/115] executor ctors --- include/boost/redis/co_connection.hpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/include/boost/redis/co_connection.hpp b/include/boost/redis/co_connection.hpp index 17e40612..54ed2b08 100644 --- a/include/boost/redis/co_connection.hpp +++ b/include/boost/redis/co_connection.hpp @@ -152,6 +152,11 @@ class co_connection { corosio::tls_context ssl_ctx = {}, logger lgr = {}); + template + co_connection(const Ex& ex, corosio::tls_context ssl_ctx = {}, logger lgr = {}) + : co_connection(ex.context(), std::move(ssl_ctx), std::move(lgr)) + { } + /** @brief Constructor from an executor and a logger. * * @param ex Executor used to create all internal I/O objects. @@ -163,6 +168,11 @@ class co_connection { */ co_connection(capy::execution_context& ctx, logger lgr); + template + co_connection(const Ex& ex, logger lgr) + : co_connection(ex.context(), corosio::tls_context{}, std::move(lgr)) + { } + /** @brief Starts the underlying connection operations. * * This function establishes a connection to the Redis server and keeps From 508209b575a9f7734c7c25bc35604468eff5bff6 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 10:46:40 +0200 Subject: [PATCH 035/115] test cleanup --- test/test_co_conn_check_health.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/test/test_co_conn_check_health.cpp b/test/test_co_conn_check_health.cpp index 4ec56443..0c11e79a 100644 --- a/test/test_co_conn_check_health.cpp +++ b/test/test_co_conn_check_health.cpp @@ -11,12 +11,12 @@ #include #include +#include #include #include #include #include #include -#include #include "common.hpp" @@ -35,7 +35,7 @@ namespace { capy::task test_reconnection() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{co_await capy::this_coro::executor}; auto exec_fn = [&]() -> capy::io_task<> { // This request will block forever, causing the connection to become unresponsive @@ -78,7 +78,7 @@ capy::task test_reconnection() // We use the correct error code when a ping times out capy::task test_error_code() { - co_connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{co_await capy::this_coro::executor}; auto exec_fn = [&]() -> capy::io_task<> { // This request will block forever, causing the connection to become unresponsive @@ -109,7 +109,7 @@ capy::task test_error_code() // A ping interval of zero disables timeouts (and doesn't cause trouble) capy::task test_disabled() { - co_connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{co_await capy::this_coro::executor}; auto exec_fn = [&]() -> capy::io_task<> { // Run a couple of requests to verify that the connection works fine @@ -155,8 +155,8 @@ std::string make_unique_id() capy::task test_flexible() { // Setup - co_connection conn1{(co_await capy::this_coro::executor).context()}; - co_connection conn2{(co_await capy::this_coro::executor).context()}; + co_connection conn1{co_await capy::this_coro::executor}; + co_connection conn2{co_await capy::this_coro::executor}; auto cfg = make_test_config(); cfg.health_check_interval = 500ms; std::string channel_name = make_unique_id(); @@ -195,8 +195,6 @@ capy::task test_flexible() request publish_req; publish_req.push("PUBLISH", channel_name, "test_health_check_flexible"); - boost::corosio::timer timer{(co_await capy::this_coro::executor).context()}; - while (true) { // Publish a message auto [ec] = co_await conn2.exec(publish_req, ignore); @@ -205,8 +203,7 @@ capy::task test_flexible() BOOST_TEST_EQ(ec, error_code()); // Wait for some time and publish again - timer.expires_after(100ms); - auto [ec2] = co_await timer.wait(); + auto [ec2] = co_await capy::delay(100ms); if (ec2 == capy::error::canceled) co_return {}; BOOST_TEST_EQ(ec2, error_code()); From d369c837323f04af13903c7943f531e2bdf0392f Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 10:52:28 +0200 Subject: [PATCH 036/115] Fix test conn setup --- test/test_co_conn_setup.cpp | 84 +++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/test/test_co_conn_setup.cpp b/test/test_co_conn_setup.cpp index 1738e2c5..adde684c 100644 --- a/test/test_co_conn_setup.cpp +++ b/test/test_co_conn_setup.cpp @@ -6,8 +6,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include -#include #include #include @@ -41,34 +41,39 @@ capy::task create_user( std::string_view username, std::string_view password) { - co_connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{co_await capy::this_coro::executor}; - auto exec_fn = [&]() -> capy::task { + auto exec_fn = [&]() -> capy::io_task<> { // Enable the user and grant them permissions on everything request req; req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, std::error_code()); + + co_return {}; }; - auto run_fn = [&]() -> capy::task { + auto run_fn = [&]() -> capy::io_task<> { config cfg; cfg.addr.port = port; auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + + co_return {}; }; - co_await capy::when_any(exec_fn(), run_fn()); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } capy::task<> test_auth_success() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{co_await capy::this_coro::executor}; - auto request_fn = [&] -> capy::task { + auto request_fn = [&]() -> capy::io_task<> { // This request should return the username we're logged in as request req; req.push("ACL", "WHOAMI"); @@ -77,9 +82,11 @@ capy::task<> test_auth_success() auto [ec] = co_await conn.exec(req, resp); BOOST_TEST_EQ(ec, std::error_code()); BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser"); + + co_return {}; }; - auto run_fn = [&] -> capy::task { + auto run_fn = [&]() -> capy::io_task<> { // These credentials are set up in main, before tests are run config cfg; cfg.username = "myuser"; @@ -87,9 +94,12 @@ capy::task<> test_auth_success() auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + + co_return {}; }; - co_await capy::when_any(request_fn(), run_fn()); + auto result = co_await capy::when_any(request_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } // Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) @@ -97,7 +107,7 @@ capy::task<> test_auth_failure() { // Setup std::string logs; - co_connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + co_connection conn{co_await capy::this_coro::executor, make_string_logger(logs)}; // Disable reconnection so the hello error causes the connection to exit auto cfg = make_test_config(); @@ -117,9 +127,9 @@ capy::task<> test_auth_failure() capy::task<> test_database_index() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{co_await capy::this_coro::executor}; - auto request_fn = [&] -> capy::task { + auto request_fn = [&]() -> capy::io_task<> { request req; req.push("CLIENT", "INFO"); response resp; @@ -128,26 +138,31 @@ capy::task<> test_database_index() BOOST_TEST_EQ(ec, std::error_code()); BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "2"); + + co_return {}; }; - auto run_fn = [&] -> capy::task { + auto run_fn = [&]() -> capy::io_task<> { // Use a non-default database index auto cfg = make_test_config(); cfg.database_index = 2; auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + + co_return {}; }; - co_await capy::when_any(request_fn(), run_fn()); + auto result = co_await capy::when_any(request_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } // The user configured an empty setup request. No request should be sent capy::task<> test_setup_empty() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{co_await capy::this_coro::executor}; - auto request_fn = [&] -> capy::task { + auto request_fn = [&]() -> capy::io_task<> { request req; req.push("CLIENT", "INFO"); response resp; @@ -156,26 +171,31 @@ capy::task<> test_setup_empty() BOOST_TEST_EQ(ec, std::error_code()); BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 + + co_return {}; }; - auto run_fn = [&] -> capy::task { + auto run_fn = [&]() -> capy::io_task<> { auto cfg = make_test_config(); cfg.use_setup = true; cfg.setup.clear(); auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + + co_return {}; }; - co_await capy::when_any(request_fn(), run_fn()); + auto result = co_await capy::when_any(request_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } // We can use the setup member to run commands at startup capy::task<> test_setup_hello() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{co_await capy::this_coro::executor}; - auto request_fn = [&] -> capy::task { + auto request_fn = [&]() -> capy::io_task<> { request req; req.push("CLIENT", "INFO"); response resp; @@ -186,9 +206,11 @@ capy::task<> test_setup_hello() BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "3"); // using RESP3 BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "user"), "myuser"); BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); + + co_return {}; }; - auto run_fn = [&] -> capy::task { + auto run_fn = [&]() -> capy::io_task<> { auto cfg = make_test_config(); cfg.use_setup = true; cfg.setup.clear(); @@ -196,18 +218,21 @@ capy::task<> test_setup_hello() cfg.setup.push("SELECT", 8); auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + + co_return {}; }; - co_await capy::when_any(request_fn(), run_fn()); + auto result = co_await capy::when_any(request_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } // Running a pipeline without a HELLO is okay (regression check: we set the priority flag) capy::task<> test_setup_no_hello() { // Setup - co_connection conn{(co_await capy::this_coro::executor).context()}; + co_connection conn{co_await capy::this_coro::executor}; - auto request_fn = [&] -> capy::task { + auto request_fn = [&]() -> capy::io_task<> { request req; req.push("CLIENT", "INFO"); response resp; @@ -217,18 +242,23 @@ capy::task<> test_setup_no_hello() BOOST_TEST_EQ(ec, std::error_code()); BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "resp"), "2"); // using RESP2 BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8"); + + co_return {}; }; - auto run_fn = [&] -> capy::task { + auto run_fn = [&]() -> capy::io_task<> { auto cfg = make_test_config(); cfg.use_setup = true; cfg.setup.clear(); cfg.setup.push("SELECT", 8); auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + + co_return {}; }; - co_await capy::when_any(request_fn(), run_fn()); + auto result = co_await capy::when_any(request_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } // Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297) @@ -236,7 +266,7 @@ capy::task<> test_setup_failure() { // Setup std::string logs; - co_connection conn{(co_await capy::this_coro::executor).context(), make_string_logger(logs)}; + co_connection conn{co_await capy::this_coro::executor, make_string_logger(logs)}; // Disable reconnection so the hello error causes the connection to exit auto cfg = make_test_config(); From bf1e366a1e9fc6e139fe6dc4ad1ce1bc2072a677 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 10:53:21 +0200 Subject: [PATCH 037/115] test rename --- test/CMakeLists.txt | 4 ++-- ...test_co_conn_check_health.cpp => test_co_check_health.cpp} | 0 test/{test_co_conn_setup.cpp => test_co_setup.cpp} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename test/{test_co_conn_check_health.cpp => test_co_check_health.cpp} (100%) rename test/{test_co_conn_setup.cpp => test_co_setup.cpp} (100%) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 05836b50..eb538d6a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -65,7 +65,7 @@ make_test(test_conn_exec_error) make_test(test_run) make_test(test_conn_run_cancel) make_test(test_conn_check_health) -make_test(test_co_conn_check_health) +make_test(test_co_check_health) make_test(test_conn_exec) make_test(test_conn_push) make_test(test_conn_push2) @@ -75,7 +75,7 @@ make_test(test_conn_exec_cancel) make_test(test_conn_echo_stress) make_test(test_conn_move) make_test(test_conn_setup) -make_test(test_co_conn_setup) +make_test(test_co_setup) make_test(test_issue_50) make_test(test_conversions) make_test(test_conn_tls) diff --git a/test/test_co_conn_check_health.cpp b/test/test_co_check_health.cpp similarity index 100% rename from test/test_co_conn_check_health.cpp rename to test/test_co_check_health.cpp diff --git a/test/test_co_conn_setup.cpp b/test/test_co_setup.cpp similarity index 100% rename from test/test_co_conn_setup.cpp rename to test/test_co_setup.cpp From 1524652b5aa80c044995fb323b066b6ac9025d2c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 11:08:20 +0200 Subject: [PATCH 038/115] cleanup example --- example/CMakeLists.txt | 2 ++ example/corosio_intro.cpp | 52 ++++++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 55fde857..3ce3e8d8 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -25,6 +25,8 @@ make_testable_example(cpp20_unix_sockets examples_main) make_testable_example(cpp20_timeouts examples_main) make_testable_example(cpp20_sentinel examples_main) +make_testable_example(corosio_intro) + make_example(cpp20_subscriber examples_main) make_example(cpp20_streams examples_main) make_example(cpp20_echo_server examples_main) diff --git a/example/corosio_intro.cpp b/example/corosio_intro.cpp index 2a7ee0f4..33c3b39b 100644 --- a/example/corosio_intro.cpp +++ b/example/corosio_intro.cpp @@ -16,13 +16,11 @@ #include #include -// TODO: re-add this to CMake! - namespace capy = boost::capy; using namespace boost::redis; namespace corosio = boost::corosio; -capy::task run_request(co_connection& conn) +capy::io_task<> run_request(co_connection& conn) { // A request containing only a ping command. request req; @@ -33,33 +31,47 @@ capy::task run_request(co_connection& conn) // Executes the request. auto [ec] = co_await conn.exec(req, resp); - if (ec) - co_return; - std::cout << "PING value: " << std::get<0>(resp).value() << std::endl; + if (ec) { + std::cout << "Error executing PING: " << ec << std::endl; + } else { + std::cout << "PING value: " << std::get<0>(resp).value() << std::endl; + } + + co_return {}; } capy::task co_main() { // Create a connection - co_connection conn{(co_await capy::this_coro::executor).context()}; - - auto r = co_await capy::when_any(run_request(conn), conn.run(config{})); + co_connection conn{co_await capy::this_coro::executor}; - static_cast(r); + // Run the connection and the PING request, in parallel + co_await capy::when_any(run_request(conn), conn.run(config{})); } -struct handler { - void operator()() { std::cout << "Done\n"; } - void operator()(std::exception_ptr exc) - { - if (exc) - std::rethrow_exception(exc); - } -}; - int main() { + // The I/O context, required for all I/O operations corosio::io_context ctx; - capy::run_async(ctx.get_executor())(co_main()); + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + exit(1); + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine ctx.run(); } From 41328027f0178e38cfb77a1f5eeb5ddbf2e7970e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 11:10:13 +0200 Subject: [PATCH 039/115] Recover old connection --- include/boost/redis/connection.hpp | 1461 +++++++++++++++++++++++ include/boost/redis/impl/connection.ipp | 62 + 2 files changed, 1523 insertions(+) create mode 100644 include/boost/redis/connection.hpp create mode 100644 include/boost/redis/impl/connection.ipp diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp new file mode 100644 index 00000000..46184c2c --- /dev/null +++ b/include/boost/redis/connection.hpp @@ -0,0 +1,1461 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_CONNECTION_HPP +#define BOOST_REDIS_CONNECTION_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace boost::redis { +namespace detail { + +// Given a timeout value, compute the expiry time. A zero timeout is considered to mean "no timeout" +inline std::chrono::steady_clock::time_point compute_expiry( + std::chrono::steady_clock::duration timeout) +{ + return timeout.count() == 0 ? (std::chrono::steady_clock::time_point::max)() + : std::chrono::steady_clock::now() + timeout; +} + +template +struct connection_impl { + using clock_type = std::chrono::steady_clock; + using clock_traits_type = asio::wait_traits; + using timer_type = asio::basic_waitable_timer; + + using receive_channel_type = asio::experimental::channel< + Executor, + void(system::error_code, std::size_t)>; + using exec_notifier_type = asio::experimental::channel< + Executor, + void(system::error_code, std::size_t)>; + + redis_stream stream_; + timer_type writer_timer_; // timer used for write timeouts + timer_type writer_cv_; // condition variable, cancelled when there is new data to write + timer_type reader_timer_; // timer used for read timeouts + timer_type reconnect_timer_; // to wait the reconnection period + timer_type ping_timer_; // to wait between pings + receive_channel_type receive_channel_; + asio::cancellation_signal run_signal_; + connection_state st_; + + using executor_type = Executor; + + executor_type get_executor() noexcept { return writer_cv_.get_executor(); } + + struct exec_op { + connection_impl* obj_ = nullptr; + std::shared_ptr notifier_ = nullptr; + exec_fsm fsm_; + + template + void operator()(Self& self, system::error_code = {}, std::size_t = 0) + { + while (true) { + // Invoke the state machine + auto act = fsm_.resume( + obj_->is_open(), + obj_->st_, + self.get_cancellation_state().cancelled()); + + // Do what the FSM said + switch (act.type()) { + case exec_action_type::setup_cancellation: + self.reset_cancellation_state(asio::enable_total_cancellation()); + continue; // this action does not require yielding + case exec_action_type::immediate: + asio::async_immediate(self.get_io_executor(), std::move(self)); + return; + case exec_action_type::notify_writer: + obj_->writer_cv_.cancel(); + continue; // this action does not require yielding + case exec_action_type::wait_for_response: + notifier_->async_receive(std::move(self)); + return; + case exec_action_type::done: + notifier_.reset(); + self.complete(act.error(), act.bytes_read()); + return; + } + } + } + }; + + connection_impl(Executor&& ex, asio::ssl::context&& ctx, logger&& lgr) + : stream_{ex, std::move(ctx)} + , writer_timer_{ex} + , writer_cv_{ex} + , reader_timer_{ex} + , reconnect_timer_{ex} + , ping_timer_{ex} + , receive_channel_{ex, 256} + , st_{{std::move(lgr)}} + { + set_receive_adapter(any_adapter{ignore}); + writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); + } + + void cancel(operation op) + { + switch (op) { + case operation::exec: st_.mpx.cancel_waiting(); break; + case operation::receive: cancel_receive_v2(); break; + case operation::reconnection: + st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); + break; + case operation::run: + case operation::resolve: + case operation::connect: + case operation::ssl_handshake: + case operation::health_check: cancel_run(); break; + case operation::all: + st_.mpx.cancel_waiting(); // exec + cancel_receive_v2(); // receive + st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); // reconnect + cancel_run(); // run + break; + default: /* ignore */; + } + } + + void cancel_receive_v1() { receive_channel_.cancel(); } + + void cancel_receive_v2() + { + st_.receive2_cancelled = true; + cancel_receive_v1(); + } + + void cancel_run() + { + // Individual operations should see a terminal cancellation, regardless + // of what we got requested. We take enough actions to ensure that this + // doesn't prevent the object from being re-used (e.g. we reset the TLS stream). + run_signal_.emit(asio::cancellation_type_t::terminal); + + // Name resolution doesn't support per-operation cancellation + stream_.cancel_resolve(); + + // Receive is technically not part of run, but we also cancel it for + // backwards compatibility. Note that this intentionally affects v1 receive, only. + cancel_receive_v1(); + } + + bool is_open() const noexcept { return stream_.is_open(); } + + bool will_reconnect() const noexcept + { + return st_.cfg.reconnect_wait_interval != std::chrono::seconds::zero(); + } + + template + auto async_exec(request const& req, any_adapter adapter, CompletionToken&& token) + { + auto notifier = std::make_shared(get_executor(), 1); + auto info = make_elem(req, std::move(adapter)); + + info->set_done_callback([notifier]() { + notifier->try_send(std::error_code{}, 0); + }); + + return asio::async_compose( + exec_op{this, notifier, exec_fsm(std::move(info))}, + token, + writer_cv_); + } + + void set_receive_adapter(any_adapter adapter) + { + st_.mpx.set_receive_adapter(std::move(adapter)); + } + + std::size_t receive(system::error_code& ec) + { + std::size_t size = 0; + + auto f = [&](system::error_code const& ec2, std::size_t n) { + ec = ec2; + size = n; + }; + + auto const res = receive_channel_.try_receive(f); + if (ec) + return 0; + + if (!res) + ec = error::sync_receive_push_failed; + + return size; + } +}; + +template +struct receive2_op { + connection_impl* conn_; + receive_fsm fsm_{}; + + void drain_receive_channel() + { + // We don't expect any errors here. The only errors + // that might appear in the channel are due to cancellations, + // and these don't make sense with try_receive + auto f = [](system::error_code, std::size_t) { }; + while (conn_->receive_channel_.try_receive(f)) + ; + } + + template + void operator()(Self& self, system::error_code ec = {}, std::size_t /* push_bytes */ = 0u) + { + receive_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + + switch (act.type) { + case receive_action::action_type::setup_cancellation: + self.reset_cancellation_state(asio::enable_total_cancellation()); + (*this)(self); // this action does not require yielding + return; + case receive_action::action_type::wait: + conn_->receive_channel_.async_receive(std::move(self)); + return; + case receive_action::action_type::drain_channel: + drain_receive_channel(); + (*this)(self); // this action does not require yielding + return; + case receive_action::action_type::immediate: + asio::async_immediate(self.get_io_executor(), std::move(self)); + return; + case receive_action::action_type::done: self.complete(act.ec); return; + } + } +}; + +template +struct exec_one_op { + connection_impl* conn_; + const request* req_; + exec_one_fsm fsm_; + + explicit exec_one_op(connection_impl& conn, const request& req, any_adapter resp) + : conn_(&conn) + , req_(&req) + , fsm_(std::move(resp), req.get_expected_responses()) + { } + + template + void operator()(Self& self, system::error_code ec = {}, std::size_t bytes_written = 0u) + { + exec_one_action act = fsm_.resume( + conn_->st_.mpx.get_read_buffer(), + ec, + bytes_written, + self.get_cancellation_state().cancelled()); + + switch (act.type) { + case exec_one_action_type::done: self.complete(act.ec); return; + case exec_one_action_type::write: + asio::async_write(conn_->stream_, asio::buffer(req_->payload()), std::move(self)); + return; + case exec_one_action_type::read_some: + conn_->stream_.async_read_some( + conn_->st_.mpx.get_read_buffer().get_prepared(), + std::move(self)); + return; + } + } +}; + +template +auto async_exec_one( + connection_impl& conn, + const request& req, + any_adapter resp, + CompletionToken&& token) +{ + return asio::async_compose( + exec_one_op{conn, req, std::move(resp)}, + token, + conn); +} + +template +struct sentinel_resolve_op { + connection_impl* conn_; + sentinel_resolve_fsm fsm_; + + explicit sentinel_resolve_op(connection_impl& conn) + : conn_(&conn) + { } + + template + void operator()(Self& self, system::error_code ec = {}) + { + auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after + sentinel_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + + switch (act.get_type()) { + case sentinel_action::type::done: self.complete(act.error()); return; + case sentinel_action::type::connect: + conn->stream_.async_connect( + make_sentinel_connect_params(conn->st_.cfg, act.connect_addr()), + conn->st_.logger, + std::move(self)); + return; + case sentinel_action::type::request: + async_exec_one( + *conn, + conn->st_.cfg.sentinel.setup, + make_sentinel_adapter(conn->st_), + asio::cancel_after( + conn->reconnect_timer_, // should be safe to re-use this + conn->st_.cfg.sentinel.request_timeout, + std::move(self))); + return; + } + } +}; + +template +auto async_sentinel_resolve(connection_impl& conn, CompletionToken&& token) +{ + return asio::async_compose( + sentinel_resolve_op{conn}, + token, + conn); +} + +template +struct writer_op { + connection_impl* conn_; + writer_fsm fsm_; + + explicit writer_op(connection_impl& conn) noexcept + : conn_(&conn) + { } + + template + void operator()(Self& self, system::error_code ec = {}, std::size_t bytes_written = 0u) + { + auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after + auto act = fsm_.resume( + conn->st_, + ec, + bytes_written, + self.get_cancellation_state().cancelled()); + + switch (act.type()) { + case writer_action_type::done: self.complete(act.error()); return; + case writer_action_type::write_some: + conn->stream_.async_write_some( + asio::buffer(conn->st_.mpx.get_write_buffer()), + asio::cancel_at( + conn->writer_timer_, + compute_expiry(act.timeout()), + std::move(self))); + return; + case writer_action_type::wait: + conn->writer_cv_.expires_at(compute_expiry(act.timeout())); + conn->writer_cv_.async_wait(std::move(self)); + return; + } + } +}; + +template +struct reader_op { + connection_impl* conn_; + reader_fsm fsm_; + +public: + reader_op(connection_impl& conn) noexcept + : conn_{&conn} + { } + + template + void operator()(Self& self, system::error_code ec = {}, std::size_t n = 0) + { + for (;;) { + auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after + auto act = fsm_.resume(conn->st_, n, ec, self.get_cancellation_state().cancelled()); + + switch (act.get_type()) { + case reader_fsm::action::type::read_some: + conn->stream_.async_read_some( + asio::buffer(conn->st_.mpx.get_prepared_read_buffer()), + asio::cancel_at( + conn->reader_timer_, + compute_expiry(act.timeout()), + std::move(self))); + return; + case reader_fsm::action::type::notify_push_receiver: + if (conn->receive_channel_.try_send(ec, act.push_size())) { + continue; + } else { + conn->receive_channel_.async_send(ec, act.push_size(), std::move(self)); + } + return; + case reader_fsm::action::type::done: self.complete(act.error()); return; + } + } + } +}; + +template +class run_op { +private: + connection_impl* conn_; + run_fsm fsm_{}; + + template + auto reader(CompletionToken&& token) + { + return asio::async_compose( + reader_op{*conn_}, + std::forward(token), + conn_->writer_cv_); + } + + template + auto writer(CompletionToken&& token) + { + return asio::async_compose( + writer_op{*conn_}, + std::forward(token), + conn_->writer_cv_); + } + +public: + run_op(connection_impl* conn) noexcept + : conn_{conn} + { } + + // Called after the parallel group finishes + template + void operator()( + Self& self, + std::array order, + system::error_code reader_ec, + system::error_code writer_ec) + { + (*this)(self, order[0u] == 0u ? reader_ec : writer_ec); + } + + template + void operator()(Self& self, system::error_code ec = {}) + { + auto act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + + switch (act.type) { + case run_action_type::done: self.complete(act.ec); return; + case run_action_type::immediate: + asio::async_immediate(self.get_io_executor(), std::move(self)); + return; + case run_action_type::sentinel_resolve: + async_sentinel_resolve(*conn_, std::move(self)); + return; + case run_action_type::connect: + conn_->stream_.async_connect( + make_run_connect_params(conn_->st_), + conn_->st_.logger, + std::move(self)); + return; + case run_action_type::parallel_group: + asio::experimental::make_parallel_group( + [this](auto token) { + return this->reader(token); + }, + [this](auto token) { + return this->writer(token); + }) + .async_wait(asio::experimental::wait_for_one(), std::move(self)); + return; + case run_action_type::cancel_receive: + conn_->receive_channel_.cancel(); + (*this)(self); // this action does not require suspending + return; + case run_action_type::wait_for_reconnection: + conn_->reconnect_timer_.expires_after(conn_->st_.cfg.reconnect_wait_interval); + conn_->reconnect_timer_.async_wait(std::move(self)); + return; + default: BOOST_ASSERT(false); + } + } +}; + +logger make_stderr_logger(logger::level lvl, std::string prefix); + +template +class run_cancel_handler { + connection_impl* conn_; + +public: + explicit run_cancel_handler(connection_impl& conn) noexcept + : conn_(&conn) + { } + + void operator()(asio::cancellation_type_t cancel_type) const + { + // We support terminal and partial cancellation + constexpr auto mask = asio::cancellation_type_t::terminal | + asio::cancellation_type_t::partial; + + if ((cancel_type & mask) != asio::cancellation_type_t::none) { + conn_->cancel(operation::run); + } + } +}; + +} // namespace detail + +/** @brief A SSL connection to the Redis server. + * + * This class keeps a healthy connection to the Redis instance where + * commands can be sent at any time. For more details, please see the + * documentation of each individual function. + * + * @tparam Executor The executor type used to create any required I/O objects. + */ +template +class basic_connection { +public: + using this_type = basic_connection; + + /// (Deprecated) Type of the next layer + BOOST_DEPRECATED("This typedef is deprecated, and will be removed with next_layer().") + typedef asio::ssl::stream> next_layer_type; + + /// The type of the executor associated to this object. + using executor_type = Executor; + + /// Rebinds the socket type to another executor. + template + struct rebind_executor { + /// The connection type when rebound to the specified executor. + using other = basic_connection; + }; + + /** @brief Constructor from an executor. + * + * @param ex Executor used to create all internal I/O objects. + * @param ctx SSL context. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + explicit basic_connection( + executor_type ex, + asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, + logger lgr = {}) + : impl_( + std::make_unique>( + std::move(ex), + std::move(ctx), + std::move(lgr))) + { } + + /** @brief Constructor from an executor and a logger. + * + * @param ex Executor used to create all internal I/O objects. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + * + * An SSL context with default settings will be created. + */ + basic_connection(executor_type ex, logger lgr) + : basic_connection( + std::move(ex), + asio::ssl::context{asio::ssl::context::tlsv12_client}, + std::move(lgr)) + { } + + /** + * @brief Constructor from an `io_context`. + * + * @param ioc I/O context used to create all internal I/O objects. + * @param ctx SSL context. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + explicit basic_connection( + asio::io_context& ioc, + asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, + logger lgr = {}) + : basic_connection(ioc.get_executor(), std::move(ctx), std::move(lgr)) + { } + + /** + * @brief Constructor from an `io_context` and a logger. + * + * @param ioc I/O context used to create all internal I/O objects. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + basic_connection(asio::io_context& ioc, logger lgr) + : basic_connection( + ioc.get_executor(), + asio::ssl::context{asio::ssl::context::tlsv12_client}, + std::move(lgr)) + { } + + /// Returns the associated executor. + executor_type get_executor() noexcept { return impl_->writer_cv_.get_executor(); } + + /** @brief Starts the underlying connection operations. + * + * This function establishes a connection to the Redis server and keeps + * it healthy by performing the following operations: + * + * @li For Sentinel deployments (`config::sentinel::addresses` is not empty), + * contacts Sentinels to obtain the address of the configured master. + * @li For TCP connections, resolves the server hostname passed in + * @ref boost::redis::config::addr. + * @li Establishes a physical connection to the server. For TCP connections, + * connects to one of the endpoints obtained during name resolution. + * For UNIX domain socket connections, it connects to @ref boost::redis::config::unix_socket. + * @li If @ref boost::redis::config::use_ssl is `true`, performs the TLS handshake. + * @li Executes the setup request, as defined by the passed @ref config object. + * By default, this is a `HELLO` command, but it can contain any other arbitrary + * commands. See the @ref config::setup docs for more info. + * @li Starts a health-check operation where `PING` commands are sent + * at intervals specified by + * @ref config::health_check_interval when the connection is idle. + * See the documentation of @ref config::health_check_interval for more info. + * @li Starts read and write operations. Requests issued using @ref async_exec + * before `async_run` is called will be written to the server immediately. + * + * When a connection is lost for any reason, a new one is + * established automatically. To disable reconnection + * set @ref boost::redis::config::reconnect_wait_interval to zero. + * + * The completion token must have the following signature + * + * @code + * void f(system::error_code); + * @endcode + * + * @par Per-operation cancellation + * This operation supports the following cancellation types: + * + * @li `asio::cancellation_type_t::terminal`. + * @li `asio::cancellation_type_t::partial`. + * + * In both cases, cancellation is equivalent to calling @ref basic_connection::cancel + * passing @ref operation::run as argument. + * + * After the operation completes, the token's associated cancellation slot + * may still have a cancellation handler associated to this connection. + * You should make sure to not invoke it after the connection has been destroyed. + * This is consistent with what other Asio I/O objects do. + * + * For example on how to call this function refer to + * cpp20_intro.cpp or any other example. + * + * @param cfg Configuration parameters. + * @param token Completion token. + */ + template > + auto async_run(config const& cfg, CompletionToken&& token = {}) + { + return asio::async_initiate( + run_initiation{impl_.get()}, + token, + &cfg); + } + + /** + * @brief (Deprecated) Starts the underlying connection operations. + * @copydetail async_run + * + * This function accepts an extra logger parameter. The passed `logger::lvl` + * will be used, but `logger::fn` will be ignored. Instead, a function + * that logs to `stderr` using `config::prefix` will be used. + * This keeps backwards compatibility with previous versions. + * Any logger configured in the constructor will be overriden. + * + * @par Deprecated + * The logger should be passed to the connection's constructor instead of using this + * function. Use the overload without a logger parameter, instead. This function is + * deprecated and will be removed in subsequent releases. + * + * @param cfg Configuration parameters. + * @param l Logger. + * @param token Completion token. + */ + template > + BOOST_DEPRECATED( + "The async_run overload taking a logger argument is deprecated. " + "Please pass the logger to the connection's constructor, instead, " + "and use the other async_run overloads.") + auto async_run(config const& cfg, logger l, CompletionToken&& token = {}) + { + set_stderr_logger(l.lvl, cfg); + return async_run(cfg, std::forward(token)); + } + + /** + * @brief (Deprecated) Starts the underlying connection operations. + * @copydetail async_run + * + * Uses a default-constructed config object to run the connection. + * + * @par Deprecated + * This function is deprecated and will be removed in subsequent releases. + * Use the overload taking an explicit config object, instead. + * + * @param token Completion token. + */ + template > + BOOST_DEPRECATED( + "Running without an explicit config object is deprecated." + "Please create a config object and pass it to async_run.") + auto async_run(CompletionToken&& token = {}) + { + return async_run(config{}, std::forward(token)); + } + + /** @brief (Deprecated) Receives server side pushes asynchronously. + * + * When pushes arrive and there is no `async_receive` operation in + * progress, pushed data, requests, and responses will be paused + * until `async_receive` is called again. Apps will usually want + * to call `async_receive` in a loop. + * + * For an example see cpp20_subscriber.cpp. The completion token must + * have the following signature + * + * @code + * void f(system::error_code, std::size_t); + * @endcode + * + * Where the second parameter is the size of the push received in + * bytes. + * + * @par Per-operation cancellation + * This operation supports the following cancellation types: + * + * @li `asio::cancellation_type_t::terminal`. + * @li `asio::cancellation_type_t::partial`. + * @li `asio::cancellation_type_t::total`. + * + * Calling `basic_connection::cancel(operation::receive)` will + * also cancel any ongoing receive operations. + * + * @param token Completion token. + */ + template > + BOOST_DEPRECATED("Please use async_receive2 instead.") + auto async_receive(CompletionToken&& token = {}) + { + return impl_->receive_channel_.async_receive(std::forward(token)); + } + + /** @brief Wait for server pushes asynchronously. + * + * This function suspends until at least one server push is received by the + * connection. On completion an unspecified number of pushes will + * have been added to the response object set with @ref + * set_receive_response. Use the functions in the response object + * to know how many messages they were received and consume them. + * + * To prevent receiving an unbound number of pushes the connection + * blocks further read operations on the socket when 256 pushes + * accumulate internally (we don't make any commitment to this + * exact number). When that happens any `async_exec`s and + * health-checks won't make any progress and the connection may + * eventually timeout. To avoid this, apps that expect server pushes + * should call this function continuously in a loop. + * + * This function should be used instead of the deprecated @ref async_receive. + * It differs from `async_receive` in the following: + * + * @li `async_receive` is designed to consume a single push message at a time. + * This can be inefficient when receiving lots of server pushes. + * `async_receive2` is batch-oriented. All pushes that are available + * when `async_receive2` is called will be marked as consumed. + * @li `async_receive` is cancelled when a reconnection happens (e.g. because + * of a network error). This enabled the user to re-establish subscriptions + * using @ref async_exec before waiting for pushes again. With the introduction of + * functions like @ref request::subscribe, subscriptions are automatically + * re-established on reconnection. Thus, `async_receive2` is not cancelled + * on reconnection. + * @li `async_receive` passes the number of bytes that each received + * push message contains. This information is unreliable and not very useful. + * Equivalent information is available using functions in the response object. + * @li `async_receive` might get cancelled if `async_run` is cancelled. + * This doesn't happen with `async_receive2`. + * + * This function does *not* remove messages from the response object + * passed to @ref set_receive_response - use the functions in the response + * object to achieve this. + * + * Only a single instance of `async_receive2` may be outstanding + * for a given connection at any time. Trying to start a second one + * will fail with @ref error::already_running. + * + * @note To avoid deadlocks the task (e.g. coroutine) calling + * `async_receive2` should not call `async_exec` in a way where + * they could block each other. This is, avoid the following pattern: + * + * @code + * asio::awaitable receiver() + * { + * // Do NOT do this!!! The receive buffer might get full while + * // async_exec runs, which will block all read operations until async_receive2 + * // is called. The two operations end up waiting each other, making the connection unresponsive. + * // If you need to do this, use two connections, instead. + * co_await conn.async_receive2(); + * co_await conn.async_exec(req, resp); + * } + * @endcode + * + * For an example see cpp20_subscriber.cpp. + * + * The completion token must have the following signature: + * + * @code + * void f(system::error_code); + * @endcode + * + * @par Per-operation cancellation + * This operation supports the following cancellation types: + * + * @li `asio::cancellation_type_t::terminal`. + * @li `asio::cancellation_type_t::partial`. + * @li `asio::cancellation_type_t::total`. + * + * @param token Completion token. + */ + template > + auto async_receive2(CompletionToken&& token = {}) + { + return asio::async_compose( + detail::receive2_op{impl_.get()}, + token, + *impl_); + } + + /** @brief (Deprecated) Receives server pushes synchronously without blocking. + * + * Receives a server push synchronously by calling `try_receive` on + * the underlying channel. If the operation fails because + * `try_receive` returns `false`, `ec` will be set to + * @ref boost::redis::error::sync_receive_push_failed. + * + * @param ec Contains the error if any occurred. + * @returns The number of bytes read from the socket. + */ + BOOST_DEPRECATED("Please, use async_receive2 instead.") + std::size_t receive(system::error_code& ec) { return impl_->receive(ec); } + + /** @brief Executes commands on the Redis server asynchronously. + * + * This function sends a request to the Redis server and waits for + * the responses to each individual command in the request. If the + * request contains only commands that don't expect a response, + * the completion occurs after it has been written to the + * underlying stream. Multiple concurrent calls to this function + * will be automatically queued by the implementation. + * + * For an example see cpp20_echo_server.cpp. + * + * The completion token must have the following signature: + * + * @code + * void f(system::error_code, std::size_t); + * @endcode + * + * Where the second parameter is the size of the response received + * in bytes. + * + * @par Per-operation cancellation + * This operation supports per-operation cancellation. Depending on the state of the request + * when cancellation is requested, we can encounter two scenarios: + * + * @li If the request hasn't been sent to the server yet, cancellation will prevent it + * from being sent to the server. In this situation, all cancellation types are supported + * (`asio::cancellation_type_t::terminal`, `asio::cancellation_type_t::partial` and + * `asio::cancellation_type_t::total`). + * @li If the request has been sent to the server but the response hasn't arrived yet, + * cancellation will cause `async_exec` to complete immediately. When the response + * arrives from the server, it will be ignored. In this situation, only + * `asio::cancellation_type_t::terminal` and `asio::cancellation_type_t::partial` + * are supported. Cancellation requests specifying `asio::cancellation_type_t::total` + * only will be ignored. + * + * In any case, connections can be safely used after cancelling `async_exec` operations. + * + * @par Object lifetimes + * Both `req` and `res` should be kept alive until the operation completes. + * No copies of the request object are made. + * + * @param req The request to be executed. + * @param resp The response object to parse data into. + * @param token Completion token. + */ + template < + class Response = ignore_t, + class CompletionToken = asio::default_completion_token_t> + auto async_exec(request const& req, Response& resp = ignore, CompletionToken&& token = {}) + { + return this->async_exec(req, any_adapter{resp}, std::forward(token)); + } + + /** @brief Executes commands on the Redis server asynchronously. + * + * This function sends a request to the Redis server and waits for + * the responses to each individual command in the request. If the + * request contains only commands that don't expect a response, + * the completion occurs after it has been written to the + * underlying stream. Multiple concurrent calls to this function + * will be automatically queued by the implementation. + * + * For an example see cpp20_echo_server.cpp. + * + * The completion token must have the following signature: + * + * @code + * void f(system::error_code, std::size_t); + * @endcode + * + * Where the second parameter is the size of the response received + * in bytes. + * + * @par Per-operation cancellation + * This operation supports per-operation cancellation. Depending on the state of the request + * when cancellation is requested, we can encounter two scenarios: + * + * @li If the request hasn't been sent to the server yet, cancellation will prevent it + * from being sent to the server. In this situation, all cancellation types are supported + * (`asio::cancellation_type_t::terminal`, `asio::cancellation_type_t::partial` and + * `asio::cancellation_type_t::total`). + * @li If the request has been sent to the server but the response hasn't arrived yet, + * cancellation will cause `async_exec` to complete immediately. When the response + * arrives from the server, it will be ignored. In this situation, only + * `asio::cancellation_type_t::terminal` and `asio::cancellation_type_t::partial` + * are supported. Cancellation requests specifying `asio::cancellation_type_t::total` + * only will be ignored. + * + * In any case, connections can be safely used after cancelling `async_exec` operations. + * + * @par Object lifetimes + * Both `req` and any response object referenced by `adapter` + * should be kept alive until the operation completes. + * No copies of the request object are made. + * + * @param req The request to be executed. + * @param adapter An adapter object referencing a response to place data into. + * @param token Completion token. + */ + template > + auto async_exec(request const& req, any_adapter adapter, CompletionToken&& token = {}) + { + return impl_->async_exec(req, std::move(adapter), std::forward(token)); + } + + /** @brief Cancel operations. + * + * @li `operation::exec`: cancels operations started with + * `async_exec`. Affects only requests that haven't been written + * yet. + * @li `operation::run`: cancels the `async_run` operation. + * @li `operation::receive`: cancels any ongoing calls to `async_receive`. + * @li `operation::all`: cancels all operations listed above. + * + * @param op The operation to be cancelled. + */ + void cancel(operation op = operation::all) { impl_->cancel(op); } + + /// Returns true if the connection will try to reconnect if an error is encountered. + bool will_reconnect() const noexcept { return impl_->will_reconnect(); } + + /** + * @brief (Deprecated) Returns the ssl context. + * + * `ssl::context` has no const methods, so this function should not be called. + * Any TLS configuration should be set up by passing an `ssl::context` + * to the connection's constructor. + * + * @returns The SSL context. + */ + BOOST_DEPRECATED( + "ssl::context has no const methods, so this function should not be called. Set up any " + "required TLS configuration before passing the ssl::context to the connection's constructor.") + asio::ssl::context const& get_ssl_context() const noexcept + { + return impl_->stream_.get_ssl_context(); + } + + /** + * @brief (Deprecated) Resets the underlying stream. + * + * This function is no longer necessary and is currently a no-op. + */ + BOOST_DEPRECATED( + "This function is no longer necessary and is currently a no-op. connection resets the stream " + "internally as required. This function will be removed in subsequent releases") + void reset_stream() { } + + /** + * @brief (Deprecated) Returns a reference to the next layer. + * + * This function returns a dummy object for connections using UNIX domain sockets. + * + * @par Deprecated + * Accessing the underlying stream is deprecated and will be removed in the next release. + * Use the other member functions to interact with the connection. + * + * @returns A reference to the underlying SSL stream object. + */ + BOOST_DEPRECATED( + "Accessing the underlying stream is deprecated and will be removed in the next release. Use " + "the other member functions to interact with the connection.") + auto& next_layer() noexcept { return impl_->stream_.next_layer(); } + + /** + * @brief (Deprecated) Returns a reference to the next layer. + * + * This function returns a dummy object for connections using UNIX domain sockets. + * + * @par Deprecated + * Accessing the underlying stream is deprecated and will be removed in the next release. + * Use the other member functions to interact with the connection. + * + * @returns A reference to the underlying SSL stream object. + */ + BOOST_DEPRECATED( + "Accessing the underlying stream is deprecated and will be removed in the next release. Use " + "the other member functions to interact with the connection.") + auto const& next_layer() const noexcept { return impl_->stream_.next_layer(); } + + /** + * @brief Sets the response object of @ref async_receive2 operations. + * + * Pushes received by the connection (concretely, by @ref async_run) + * will be stored in `resp`. This happens even if @ref async_receive2 + * is not being called. + * + * `resp` should be able to accommodate the following message types: + * + * @li Any kind of RESP3 pushes that the application might expect. + * This usually involves Pub/Sub messages of type `message`, + * `subscribe`, `unsubscribe`, `psubscribe` and `punsubscribe`. + * See this page + * for more info. + * @li Any errors caused by failed `SUBSCRIBE` commands. + * Because of protocol oddities, these are placed in the receive buffer, + * rather than handed to `async_exec`. + * @li If your application is using `MONITOR`, simple strings. + * + * Because receive responses need to accommodate many different kind + * of messages, it's advised to use one of the generic responses + * (like @ref generic_flat_response). If a response can't accommodate + * one of the received types, @ref async_run will exit with an error. + * + * Messages received before this function is called are discarded. + * + * @par Object lifetimes + * `resp` should be kept alive until @ref async_run completes. + */ + template + void set_receive_response(Response& resp) + { + impl_->set_receive_adapter(any_adapter{resp}); + } + + /// Returns connection usage information. + usage get_usage() const noexcept { return impl_->st_.mpx.get_usage(); } + +private: + using clock_type = std::chrono::steady_clock; + using clock_traits_type = asio::wait_traits; + using timer_type = asio::basic_waitable_timer; + + using receive_channel_type = asio::experimental::channel< + executor_type, + void(system::error_code, std::size_t)>; + + auto use_ssl() const noexcept { return impl_->cfg_.use_ssl; } + + // Used by both this class and connection + void set_stderr_logger(logger::level lvl, const config& cfg) + { + impl_->st_.logger.lgr = detail::make_stderr_logger(lvl, cfg.log_prefix); + } + + // Initiation for async_run. This is required because we need access + // to the final handler (rather than the completion token) within the initiation, + // to modify the handler's cancellation slot. + struct run_initiation { + detail::connection_impl* self; + + using executor_type = Executor; + executor_type get_executor() const noexcept { return self->get_executor(); } + + template + void operator()(Handler&& handler, config const* cfg) + { + self->st_.cfg = *cfg; + self->st_.mpx.set_config(*cfg); + + // If the token's slot has cancellation enabled, it should just emit + // the cancellation signal in our connection. This lets us unify the cancel() + // function and per-operation cancellation + auto slot = asio::get_associated_cancellation_slot(handler); + if (slot.is_connected()) { + slot.template emplace>(*self); + } + + // Overwrite the token's cancellation slot: the composed operation + // should use the signal's slot so we can generate cancellations in cancel() + auto token_with_slot = asio::bind_cancellation_slot( + self->run_signal_.slot(), + std::forward(handler)); + + asio::async_compose( + detail::run_op{self}, + token_with_slot, + self->writer_cv_); + } + }; + + friend class connection; + + std::unique_ptr> impl_; +}; + +/** @brief A basic_connection that type erases the executor. + * + * This connection type uses `asio::any_io_executor` and + * `asio::any_completion_token` to reduce compilation times. + * + * For documentation of each member function see + * @ref boost::redis::basic_connection. + */ +class connection { +public: + /// Executor type. + using executor_type = asio::any_io_executor; + + /** @brief Constructor from an executor. + * + * @param ex Executor used to create all internal I/O objects. + * @param ctx SSL context. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + explicit connection( + executor_type ex, + asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, + logger lgr = {}); + + /** @brief Constructor from an executor and a logger. + * + * @param ex Executor used to create all internal I/O objects. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + * + * An SSL context with default settings will be created. + */ + connection(executor_type ex, logger lgr) + : connection( + std::move(ex), + asio::ssl::context{asio::ssl::context::tlsv12_client}, + std::move(lgr)) + { } + + /** + * @brief Constructor from an `io_context`. + * + * @param ioc I/O context used to create all internal I/O objects. + * @param ctx SSL context. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + explicit connection( + asio::io_context& ioc, + asio::ssl::context ctx = asio::ssl::context{asio::ssl::context::tlsv12_client}, + logger lgr = {}) + : connection(ioc.get_executor(), std::move(ctx), std::move(lgr)) + { } + + /** + * @brief Constructor from an `io_context` and a logger. + * + * @param ioc I/O context used to create all internal I/O objects. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ + connection(asio::io_context& ioc, logger lgr) + : connection( + ioc.get_executor(), + asio::ssl::context{asio::ssl::context::tlsv12_client}, + std::move(lgr)) + { } + + /// Returns the underlying executor. + executor_type get_executor() noexcept { return impl_.get_executor(); } + + /** + * @brief Calls @ref boost::redis::basic_connection::async_run. + * + * @param cfg Configuration parameters. + * @param token Completion token. + */ + template + auto async_run(config const& cfg, CompletionToken&& token = {}) + { + return asio::async_initiate( + initiation{this}, + token, + &cfg); + } + + /** + * @brief (Deprecated) Calls @ref boost::redis::basic_connection::async_run. + * + * This function accepts an extra logger parameter. The passed logger + * will be used by the connection, overwriting any logger passed to the connection's + * constructor. + * + * @par Deprecated + * The logger should be passed to the connection's constructor instead of using this + * function. Use the overload without a logger parameter, instead. This function is + * deprecated and will be removed in subsequent releases. + * + * @param cfg Configuration parameters. + * @param l Logger. + * @param token Completion token. + */ + template + BOOST_DEPRECATED( + "The async_run overload taking a logger argument is deprecated. " + "Please pass the logger to the connection's constructor, instead, " + "and use the other async_run overloads.") + auto async_run(config const& cfg, logger l, CompletionToken&& token = {}) + { + return asio::async_initiate( + initiation{this}, + token, + &cfg, + std::move(l)); + } + + /// @copydoc basic_connection::async_receive + template + BOOST_DEPRECATED("Please use async_receive2 instead.") + auto async_receive(CompletionToken&& token = {}) + { + return impl_.async_receive(std::forward(token)); + } + + /// @copydoc basic_connection::async_receive2 + template + auto async_receive2(CompletionToken&& token = {}) + { + return asio::async_initiate( + initiation{this}, + token); + } + + /// @copydoc basic_connection::receive + BOOST_DEPRECATED("Please use async_receive2 instead.") + std::size_t receive(system::error_code& ec) { return impl_.impl_->receive(ec); } + + /** + * @brief Calls @ref boost::redis::basic_connection::async_exec. + * + * @param req The request to be executed. + * @param resp The response object to parse data into. + * @param token Completion token. + */ + template + auto async_exec(request const& req, Response& resp = ignore, CompletionToken&& token = {}) + { + return async_exec(req, any_adapter{resp}, std::forward(token)); + } + + /** + * @brief Calls @ref boost::redis::basic_connection::async_exec. + * + * @param req The request to be executed. + * @param adapter An adapter object referencing a response to place data into. + * @param token Completion token. + */ + template + auto async_exec(request const& req, any_adapter adapter, CompletionToken&& token = {}) + { + return asio::async_initiate( + initiation{this}, + token, + &req, + std::move(adapter)); + } + + /// @copydoc basic_connection::cancel + void cancel(operation op = operation::all); + + /// @copydoc basic_connection::will_reconnect + bool will_reconnect() const noexcept { return impl_.will_reconnect(); } + + /// (Deprecated) Calls @ref boost::redis::basic_connection::next_layer. + BOOST_DEPRECATED( + "Accessing the underlying stream is deprecated and will be removed in the next release. Use " + "the other member functions to interact with the connection.") + asio::ssl::stream& next_layer() noexcept + { + return impl_.impl_->stream_.next_layer(); + } + + /// (Deprecated) Calls @ref boost::redis::basic_connection::next_layer. + BOOST_DEPRECATED( + "Accessing the underlying stream is deprecated and will be removed in the next release. Use " + "the other member functions to interact with the connection.") + asio::ssl::stream const& next_layer() const noexcept + { + return impl_.impl_->stream_.next_layer(); + } + + /// @copydoc basic_connection::reset_stream + BOOST_DEPRECATED( + "This function is no longer necessary and is currently a no-op. connection resets the stream " + "internally as required. This function will be removed in subsequent releases") + void reset_stream() { } + + /// @copydoc basic_connection::set_receive_response + template + void set_receive_response(Response& response) + { + impl_.set_receive_response(response); + } + + /// @copydoc basic_connection::get_usage + usage get_usage() const noexcept { return impl_.get_usage(); } + + /// @copydoc basic_connection::get_ssl_context + BOOST_DEPRECATED( + "ssl::context has no const methods, so this function should not be called. Set up any " + "required TLS configuration before passing the ssl::context to the connection's constructor.") + asio::ssl::context const& get_ssl_context() const noexcept + { + return impl_.impl_->stream_.get_ssl_context(); + } + +private: + // Function object to initiate the async ops that use asio::any_completion_handler. + // Required for asio::cancel_after to work. + // Since all ops have different arguments, a single struct with different overloads is enough. + struct initiation { + connection* self; + + using executor_type = asio::any_io_executor; + executor_type get_executor() const noexcept { return self->get_executor(); } + + template + void operator()(Handler&& handler, config const* cfg, logger l) + { + self->async_run_impl(*cfg, std::move(l), std::forward(handler)); + } + + template + void operator()(Handler&& handler, config const* cfg) + { + self->async_run_impl(*cfg, std::forward(handler)); + } + + template + void operator()(Handler&& handler, request const* req, any_adapter&& adapter) + { + self->async_exec_impl(*req, std::move(adapter), std::forward(handler)); + } + + template + void operator()(Handler&& handler) + { + self->async_receive2_impl(std::forward(handler)); + } + }; + + void async_run_impl( + config const& cfg, + logger&& l, + asio::any_completion_handler token); + + void async_run_impl( + config const& cfg, + asio::any_completion_handler token); + + void async_exec_impl( + request const& req, + any_adapter&& adapter, + asio::any_completion_handler token); + + void async_receive2_impl(asio::any_completion_handler token); + + basic_connection impl_; +}; + +} // namespace boost::redis + +#endif // BOOST_REDIS_CONNECTION_HPP diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/connection.ipp new file mode 100644 index 00000000..9db49af3 --- /dev/null +++ b/include/boost/redis/impl/connection.ipp @@ -0,0 +1,62 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include + +#include +#include +#include +#include + +namespace boost::redis { + +logger detail::make_stderr_logger(logger::level lvl, std::string prefix) +{ + return logger(lvl, [prefix = std::move(prefix)](logger::level, std::string_view msg) { + log_to_file(stderr, msg, prefix.c_str()); + }); +} + +connection::connection(executor_type ex, asio::ssl::context ctx, logger lgr) +: impl_{std::move(ex), std::move(ctx), std::move(lgr)} +{ } + +void connection::async_run_impl( + config const& cfg, + logger&& l, + asio::any_completion_handler token) +{ + // Avoid calling the basic_connection::async_run overload taking a logger + // because it generates deprecated messages when building this file + impl_.set_stderr_logger(l.lvl, cfg); + impl_.async_run(cfg, std::move(token)); +} + +void connection::async_run_impl( + config const& cfg, + asio::any_completion_handler token) +{ + impl_.async_run(cfg, std::move(token)); +} + +void connection::async_exec_impl( + request const& req, + any_adapter&& adapter, + asio::any_completion_handler token) +{ + impl_.async_exec(req, std::move(adapter), std::move(token)); +} + +void connection::async_receive2_impl( + asio::any_completion_handler token) +{ + impl_.async_receive2(std::move(token)); +} + +void connection::cancel(operation op) { impl_.cancel(op); } + +} // namespace boost::redis From da0faf287472d043e6edd62889cf2c03ea24efd5 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 11:17:21 +0200 Subject: [PATCH 040/115] src split --- benchmarks/CMakeLists.txt | 2 +- include/boost/redis/src/asio.hpp | 8 ++++++ include/boost/redis/src/corosio.hpp | 8 ++++++ include/boost/redis/src/proto.hpp | 26 +++++++++++++++++++ test/CMakeLists.txt | 11 ++++++-- .../{boost_redis.cpp => boost_redis_asio.cpp} | 2 +- test/boost_redis_corosio.cpp | 7 +++++ test/boost_redis_proto.cpp | 7 +++++ 8 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 include/boost/redis/src/asio.hpp create mode 100644 include/boost/redis/src/corosio.hpp create mode 100644 include/boost/redis/src/proto.hpp rename test/{boost_redis.cpp => boost_redis_asio.cpp} (83%) create mode 100644 test/boost_redis_corosio.cpp create mode 100644 test/boost_redis_proto.cpp diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index e40a7dae..ddb56601 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(benchmarks_options INTERFACE) -target_link_libraries(benchmarks_options INTERFACE boost_redis_src) +target_link_libraries(benchmarks_options INTERFACE boost_redis_asio) target_link_libraries(benchmarks_options INTERFACE boost_redis_project_options) target_compile_features(benchmarks_options INTERFACE cxx_std_20) diff --git a/include/boost/redis/src/asio.hpp b/include/boost/redis/src/asio.hpp new file mode 100644 index 00000000..e15516a1 --- /dev/null +++ b/include/boost/redis/src/asio.hpp @@ -0,0 +1,8 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +// #include // TODO diff --git a/include/boost/redis/src/corosio.hpp b/include/boost/redis/src/corosio.hpp new file mode 100644 index 00000000..1489db18 --- /dev/null +++ b/include/boost/redis/src/corosio.hpp @@ -0,0 +1,8 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include diff --git a/include/boost/redis/src/proto.hpp b/include/boost/redis/src/proto.hpp new file mode 100644 index 00000000..004a4f60 --- /dev/null +++ b/include/boost/redis/src/proto.hpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dddcb9f6..88a5b32a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -10,8 +10,15 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL target_compile_options(boost_redis_project_options INTERFACE -Wall -Wextra -Werror) endif() -add_library(boost_redis_src STATIC boost_redis.cpp) -target_link_libraries(boost_redis_src PRIVATE boost_redis_project_options) +# Source libraries +add_library(boost_redis_proto STATIC boost_redis_proto.cpp) +target_link_libraries(boost_redis_proto PRIVATE boost_redis_project_options) + +add_library(boost_redis_asio STATIC boost_redis_asio.cpp) +target_link_libraries(boost_redis_asio PRIVATE boost_redis_proto) + +add_library(boost_redis_corosio STATIC boost_redis_corosio.cpp) +target_link_libraries(boost_redis_corosio PRIVATE boost_redis_proto) # Test utils add_library(boost_redis_tests_common STATIC common.cpp sansio_utils.cpp) diff --git a/test/boost_redis.cpp b/test/boost_redis_asio.cpp similarity index 83% rename from test/boost_redis.cpp rename to test/boost_redis_asio.cpp index dddc80f2..9c0bff14 100644 --- a/test/boost_redis.cpp +++ b/test/boost_redis_asio.cpp @@ -4,4 +4,4 @@ * accompanying file LICENSE.txt) */ -#include +#include diff --git a/test/boost_redis_corosio.cpp b/test/boost_redis_corosio.cpp new file mode 100644 index 00000000..395cd877 --- /dev/null +++ b/test/boost_redis_corosio.cpp @@ -0,0 +1,7 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include diff --git a/test/boost_redis_proto.cpp b/test/boost_redis_proto.cpp new file mode 100644 index 00000000..2c76307d --- /dev/null +++ b/test/boost_redis_proto.cpp @@ -0,0 +1,7 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include From 18436c7418c01bb92c42c9fcfb5388f8d954b139 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 11:28:12 +0200 Subject: [PATCH 041/115] split test utils --- test/CMakeLists.txt | 18 +++++++--- test/asio_common.cpp | 85 ++++++++++++++++++++++++++++++++++++++++++++ test/asio_common.hpp | 36 +++++++++++++++++++ test/common.cpp | 42 ---------------------- test/common.hpp | 7 ++-- 5 files changed, 136 insertions(+), 52 deletions(-) create mode 100644 test/asio_common.cpp create mode 100644 test/asio_common.hpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 88a5b32a..0a7ee76c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -12,17 +12,25 @@ endif() # Source libraries add_library(boost_redis_proto STATIC boost_redis_proto.cpp) -target_link_libraries(boost_redis_proto PRIVATE boost_redis_project_options) +target_link_libraries(boost_redis_proto PUBLIC boost_redis_project_options) add_library(boost_redis_asio STATIC boost_redis_asio.cpp) -target_link_libraries(boost_redis_asio PRIVATE boost_redis_proto) +target_link_libraries(boost_redis_asio PUBLIC boost_redis_proto) add_library(boost_redis_corosio STATIC boost_redis_corosio.cpp) -target_link_libraries(boost_redis_corosio PRIVATE boost_redis_proto) +target_link_libraries(boost_redis_corosio PUBLIC boost_redis_proto) # Test utils -add_library(boost_redis_tests_common STATIC common.cpp sansio_utils.cpp) -target_link_libraries(boost_redis_tests_common PRIVATE boost_redis_project_options) +add_library(boost_redis_tests_proto STATIC common.cpp sansio_utils.cpp) +target_link_libraries(boost_redis_tests_proto PUBLIC boost_redis_proto) +target_compile_definitions(boost_redis_tests_proto PUBLIC BOOST_ALLOW_DEPRECATED=1) # we need to still test deprecated fns + +add_library(boost_redis_tests_asio STATIC asio_common.cpp) +target_link_libraries(boost_redis_tests_asio PUBLIC boost_redis_asio) + +# add_library(boost_redis_tests_corosio STATIC corosio_common.cpp) +# target_link_libraries(boost_redis_tests_corosio PRIVATE boost_redis_corosio) + macro(make_test TEST_NAME) set(EXE_NAME "boost_redis_${TEST_NAME}") diff --git a/test/asio_common.cpp b/test/asio_common.cpp new file mode 100644 index 00000000..cfac9022 --- /dev/null +++ b/test/asio_common.cpp @@ -0,0 +1,85 @@ +#include +#include + +#include +#include +#include + +#include "asio_common.hpp" + +#include +#include +#include +#include +#include + +namespace net = boost::asio; + +struct run_callback { + std::shared_ptr conn; + boost::redis::operation op; + boost::system::error_code expected; + + void operator()(boost::system::error_code const& ec) const + { + std::cout << "async_run: " << ec.message() << std::endl; + conn->cancel(op); + } +}; + +void run( + std::shared_ptr conn, + boost::redis::config cfg, + boost::system::error_code ec, + boost::redis::operation op) +{ + conn->async_run(cfg, run_callback{conn, op, ec}); +} + +#ifdef BOOST_ASIO_HAS_CO_AWAIT +void run_coroutine_test(net::awaitable op, std::chrono::steady_clock::duration timeout) +{ + net::io_context ioc; + bool finished = false; + net::co_spawn(ioc, std::move(op), [&finished](std::exception_ptr p) { + if (p) + std::rethrow_exception(p); + finished = true; + }); + ioc.run_for(timeout); + if (!finished) + throw std::runtime_error("Coroutine test did not finish"); +} +#endif // BOOST_ASIO_HAS_CO_AWAIT + +void create_user(std::string_view port, std::string_view username, std::string_view password) +{ + // Setup + net::io_context ioc; + boost::redis::connection conn{ioc}; + + boost::redis::config cfg; + cfg.addr.port = port; + + // Enable the user and grant them permissions on everything + boost::redis::request req; + req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); + + bool run_finished = false, exec_finished = false; + + conn.async_run(cfg, [&](boost::system::error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, net::error::operation_aborted); + }); + + conn.async_exec(req, boost::redis::ignore, [&](boost::system::error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, boost::system::error_code()); + conn.cancel(); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(run_finished); + BOOST_TEST(exec_finished); +} diff --git a/test/asio_common.hpp b/test/asio_common.hpp new file mode 100644 index 00000000..94e04d9a --- /dev/null +++ b/test/asio_common.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "common.hpp" + +#include +#include +#include + +#ifdef BOOST_ASIO_HAS_CO_AWAIT +inline auto redir(boost::system::error_code& ec) +{ + return boost::asio::redirect_error(boost::asio::use_awaitable, ec); +} +void run_coroutine_test( + boost::asio::awaitable, + std::chrono::steady_clock::duration timeout = test_timeout); +#endif // BOOST_ASIO_HAS_CO_AWAIT + +void run( + std::shared_ptr conn, + boost::redis::config cfg = make_test_config(), + boost::system::error_code ec = boost::asio::error::operation_aborted, + boost::redis::operation op = boost::redis::operation::receive); + +// Connects to the Redis server at the given port and creates a user +void create_user(std::string_view port, std::string_view username, std::string_view password); diff --git a/test/common.cpp b/test/common.cpp index a951d80f..1023fc16 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -1,39 +1,15 @@ #include -#include #include -#include #include "common.hpp" #include #include #include -#include using namespace std::chrono_literals; -struct run_callback { - std::shared_ptr conn; - boost::redis::operation op; - boost::system::error_code expected; - - void operator()(boost::system::error_code const& ec) const - { - std::cout << "async_run: " << ec.message() << std::endl; - conn->cancel(op); - } -}; - -void run( - std::shared_ptr conn, - boost::redis::config cfg, - boost::system::error_code ec, - boost::redis::operation op) -{ - conn->async_run(cfg, run_callback{conn, op, ec}); -} - std::string safe_getenv(const char* name, const char* default_value) { // MSVC doesn't like getenv @@ -82,21 +58,3 @@ boost::redis::logger make_string_logger(std::string& to) to += '\n'; }}; } - -void run_coroutine_test(boost::capy::task test) -{ - // Set a timeout to the tests, so they don't hang on error - bool finished = false; - auto wrapper_fn = [test = std::move(test), &finished]() mutable -> boost::capy::task { - co_await std::move(test); - finished = true; - }; - - // Actually run the test - boost::corosio::io_context ctx; - boost::capy::run_async(ctx.get_executor())(wrapper_fn()); - ctx.run_for(test_timeout); - - // Check that it finished - BOOST_TEST(finished); -} diff --git a/test/common.hpp b/test/common.hpp index b1c1b821..6ebefa69 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -3,9 +3,10 @@ #include #include -#include +#include #include +#include #include #include @@ -21,10 +22,6 @@ std::string get_server_hostname(); // format: key1=value1 key2=value2 std::string_view find_client_info(std::string_view client_info, std::string_view key); -// TODO: bring back -// // Connects to the Redis server at the given port and creates a user -// void create_user(std::string_view port, std::string_view username, std::string_view password); - boost::redis::logger make_string_logger(std::string& to); std::string safe_getenv(const char* name, const char* default_value); From 4167d5bfedc4ed372e7e648a9637b0448060bf97 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 11:29:01 +0200 Subject: [PATCH 042/115] Recover Boost span --- include/boost/redis/detail/read_buffer.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/boost/redis/detail/read_buffer.hpp b/include/boost/redis/detail/read_buffer.hpp index 8d9af548..965845ec 100644 --- a/include/boost/redis/detail/read_buffer.hpp +++ b/include/boost/redis/detail/read_buffer.hpp @@ -7,10 +7,10 @@ #ifndef BOOST_REDIS_READ_BUFFER_HPP #define BOOST_REDIS_READ_BUFFER_HPP +#include #include #include -#include #include #include #include @@ -19,7 +19,7 @@ namespace boost::redis::detail { class read_buffer { public: - using span_type = std::span; + using span_type = span; struct consume_result { std::size_t consumed; From 726128ead72894c88659e7894f9c641362173404 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 11:29:15 +0200 Subject: [PATCH 043/115] Recover Redis stream --- include/boost/redis/detail/redis_stream.hpp | 251 ++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 include/boost/redis/detail/redis_stream.hpp diff --git a/include/boost/redis/detail/redis_stream.hpp b/include/boost/redis/detail/redis_stream.hpp new file mode 100644 index 00000000..349cc650 --- /dev/null +++ b/include/boost/redis/detail/redis_stream.hpp @@ -0,0 +1,251 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com), + * Ruben Perez Hidalgo (rubenperez038 at gmail dot com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ +#ifndef BOOST_REDIS_REDIS_STREAM_HPP +#define BOOST_REDIS_REDIS_STREAM_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace boost { +namespace redis { +namespace detail { + +template +class redis_stream { + asio::ssl::context ssl_ctx_; + asio::ip::basic_resolver resolv_; + asio::ssl::stream> stream_; +#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + asio::basic_stream_socket unix_socket_; +#endif + typename asio::steady_timer::template rebind_executor::other timer_; + redis_stream_state st_; + + void reset_stream() { stream_ = {resolv_.get_executor(), ssl_ctx_}; } + + struct connect_op { + redis_stream& obj_; + connect_fsm fsm_; + connect_params params_; + + template + void execute_action(Self& self, connect_action act) + { + // Prevent use-after-move errors + auto& obj = this->obj_; + auto params = this->params_; + + switch (act.type) { + case connect_action_type::unix_socket_close: +#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + { + system::error_code ec; + obj.unix_socket_.close(ec); + (*this)(self, ec); // This is a sync action + } +#else + BOOST_ASSERT(false); +#endif + return; + case connect_action_type::unix_socket_connect: +#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + obj.unix_socket_.async_connect( + params.addr.unix_socket(), + asio::cancel_after(obj.timer_, params.connect_timeout, std::move(self))); +#else + BOOST_ASSERT(false); +#endif + return; + + case connect_action_type::tcp_resolve: + obj.resolv_.async_resolve( + params.addr.tcp_address().host, + params.addr.tcp_address().port, + asio::cancel_after(obj.timer_, params.resolve_timeout, std::move(self))); + return; + case connect_action_type::ssl_stream_reset: + obj.reset_stream(); + // this action does not require yielding. Execute the next action immediately + (*this)(self); + return; + case connect_action_type::ssl_handshake: + obj.stream_.async_handshake( + asio::ssl::stream_base::client, + asio::cancel_after(obj.timer_, params.ssl_handshake_timeout, std::move(self))); + return; + case connect_action_type::done: self.complete(act.ec); break; + // Connect should use the specialized handler, where resolver results are available + case connect_action_type::tcp_connect: + default: BOOST_ASSERT(false); + } + } + + // This overload will be used for connects + template + void operator()( + Self& self, + system::error_code ec, + const asio::ip::tcp::endpoint& selected_endpoint) + { + auto act = fsm_.resume( + ec, + selected_endpoint, + obj_.st_, + self.get_cancellation_state().cancelled()); + execute_action(self, act); + } + + // This overload will be used for resolves + template + void operator()( + Self& self, + system::error_code ec, + asio::ip::tcp::resolver::results_type endpoints) + { + auto act = fsm_.resume(ec, endpoints, obj_.st_, self.get_cancellation_state().cancelled()); + if (act.type == connect_action_type::tcp_connect) { + auto& obj = this->obj_; // prevent use-after-move errors + asio::async_connect( + obj.stream_.next_layer(), + std::move(endpoints), + asio::cancel_after(obj.timer_, params_.connect_timeout, std::move(self))); + } else { + execute_action(self, act); + } + } + + template + void operator()(Self& self, system::error_code ec = {}) + { + auto act = fsm_.resume(ec, obj_.st_, self.get_cancellation_state().cancelled()); + execute_action(self, act); + } + }; + +public: + explicit redis_stream(Executor ex, asio::ssl::context&& ssl_ctx) + : ssl_ctx_{std::move(ssl_ctx)} + , resolv_{ex} + , stream_{ex, ssl_ctx_} +#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + , unix_socket_{ex} +#endif + , timer_{std::move(ex)} + { } + + // Executor. Required to satisfy the AsyncStream concept + using executor_type = Executor; + executor_type get_executor() noexcept { return resolv_.get_executor(); } + + // Accessors + const auto& get_ssl_context() const noexcept { return ssl_ctx_; } + bool is_open() const + { +#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + if (st_.type == transport_type::unix_socket) + return unix_socket_.is_open(); +#endif + return stream_.next_layer().is_open(); + } + auto& next_layer() { return stream_; } + const auto& next_layer() const { return stream_; } + + // I/O + template + auto async_connect(const connect_params& params, buffered_logger& l, CompletionToken&& token) + { + this->st_.type = params.addr.type(); + return asio::async_compose( + connect_op{*this, connect_fsm{l}, params}, + token); + } + + // These functions should only be used with callbacks (e.g. within async_compose function bodies) + template + void async_write_some(const ConstBufferSequence& buffers, CompletionToken&& token) + { + switch (st_.type) { + case transport_type::tcp: + { + stream_.next_layer().async_write_some(buffers, std::forward(token)); + break; + } + case transport_type::tcp_tls: + { + stream_.async_write_some(buffers, std::forward(token)); + break; + } +#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + case transport_type::unix_socket: + { + unix_socket_.async_write_some(buffers, std::forward(token)); + break; + } +#endif + default: BOOST_ASSERT(false); + } + } + + template + void async_read_some(const MutableBufferSequence& buffers, CompletionToken&& token) + { + switch (st_.type) { + case transport_type::tcp: + { + return stream_.next_layer().async_read_some( + buffers, + std::forward(token)); + break; + } + case transport_type::tcp_tls: + { + return stream_.async_read_some(buffers, std::forward(token)); + break; + } +#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + case transport_type::unix_socket: + { + unix_socket_.async_read_some(buffers, std::forward(token)); + break; + } +#endif + default: BOOST_ASSERT(false); + } + } + + // Cancels resolve operations. Resolve operations don't support per-operation + // cancellation, but resolvers have a cancel() function. Resolve operations are + // in general blocking and run in a separate thread. cancel() has effect only + // if the operation hasn't started yet. Still, trying is better than nothing + void cancel_resolve() { resolv_.cancel(); } +}; + +} // namespace detail +} // namespace redis +} // namespace boost + +#endif From 59a93c87a79eb1e772646bc0a51300c590f8c403 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 11:52:12 +0200 Subject: [PATCH 044/115] Recover src --- include/boost/redis/src.hpp | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/include/boost/redis/src.hpp b/include/boost/redis/src.hpp index 6e82bfa6..3712ea9f 100644 --- a/include/boost/redis/src.hpp +++ b/include/boost/redis/src.hpp @@ -4,25 +4,6 @@ * accompanying file LICENSE.txt) */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +// Retained for backwards-compatibility (TODO: test) +#include +#include From 53adde21b7d58c1d1b529088a5d333f913bf3ca1 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 12:01:35 +0200 Subject: [PATCH 045/115] Split connect_fsm --- include/boost/redis/co_connection.hpp | 3 +- include/boost/redis/detail/co_connect_fsm.hpp | 82 ++++++++ include/boost/redis/detail/connect_fsm.hpp | 31 ++- include/boost/redis/detail/connect_params.hpp | 2 +- include/boost/redis/detail/transport_type.hpp | 24 +++ include/boost/redis/impl/co_connect_fsm.ipp | 178 ++++++++++++++++++ include/boost/redis/impl/co_connection.ipp | 25 ++- include/boost/redis/impl/connect_fsm.ipp | 90 ++++++--- include/boost/redis/src/asio.hpp | 2 +- include/boost/redis/src/corosio.hpp | 2 +- 10 files changed, 383 insertions(+), 56 deletions(-) create mode 100644 include/boost/redis/detail/co_connect_fsm.hpp create mode 100644 include/boost/redis/detail/transport_type.hpp create mode 100644 include/boost/redis/impl/co_connect_fsm.ipp diff --git a/include/boost/redis/co_connection.hpp b/include/boost/redis/co_connection.hpp index 54ed2b08..649f863d 100644 --- a/include/boost/redis/co_connection.hpp +++ b/include/boost/redis/co_connection.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -71,7 +72,7 @@ class co_redis_stream { corosio::openssl_stream stream_; // TODO: make this configurable corosio::timer timer_; corosio::resolver resolv_; - redis_stream_state st_; + co_redis_stream_state st_; public: explicit co_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) diff --git a/include/boost/redis/detail/co_connect_fsm.hpp b/include/boost/redis/detail/co_connect_fsm.hpp new file mode 100644 index 00000000..f85cad4d --- /dev/null +++ b/include/boost/redis/detail/co_connect_fsm.hpp @@ -0,0 +1,82 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_REDIS_CO_CONNECT_FSM_HPP +#define BOOST_REDIS_CO_CONNECT_FSM_HPP + +#include + +#include +#include +#include + +#include + +// Sans-io algorithm for redis_stream::async_connect, as a finite state machine + +namespace boost::redis::detail { + +struct buffered_logger; + +struct co_redis_stream_state { + transport_type type{transport_type::tcp}; + bool ssl_stream_used{false}; +}; + +// What should we do next? +enum class co_connect_action_type +{ + unix_socket_close, // Close the UNIX socket, to discard state + unix_socket_connect, // Connect to the UNIX socket + tcp_resolve, // Name resolution + tcp_connect, // TCP connect + ssl_stream_reset, // Re-create the SSL stream, to discard state + ssl_handshake, // SSL handshake + done, // Complete the async op +}; + +struct co_connect_action { + co_connect_action_type type; + system::error_code ec; + + co_connect_action(co_connect_action_type type) noexcept + : type{type} + { } + + co_connect_action(system::error_code ec) noexcept + : type{co_connect_action_type::done} + , ec{ec} + { } +}; + +class co_connect_fsm { + int resume_point_{0}; + buffered_logger* lgr_{nullptr}; + +public: + co_connect_fsm(buffered_logger& lgr) noexcept + : lgr_(&lgr) + { } + + co_connect_action resume( + system::error_code ec, + std::span resolver_results, + co_redis_stream_state& st); + + co_connect_action resume( + system::error_code ec, + const corosio::endpoint& selected_endpoint, + co_redis_stream_state& st); + + co_connect_action resume(system::error_code ec, co_redis_stream_state& st); + +}; // namespace boost::redis::detail + +} // namespace boost::redis::detail + +#endif diff --git a/include/boost/redis/detail/connect_fsm.hpp b/include/boost/redis/detail/connect_fsm.hpp index b7c84eed..d04b7647 100644 --- a/include/boost/redis/detail/connect_fsm.hpp +++ b/include/boost/redis/detail/connect_fsm.hpp @@ -9,11 +9,11 @@ #ifndef BOOST_REDIS_CONNECT_FSM_HPP #define BOOST_REDIS_CONNECT_FSM_HPP -#include -#include -#include +#include -#include +#include +#include +#include // Sans-io algorithm for redis_stream::async_connect, as a finite state machine @@ -21,14 +21,6 @@ namespace boost::redis::detail { struct buffered_logger; -// What transport is redis_stream using? -enum class transport_type -{ - tcp, // plaintext TCP - tcp_tls, // TLS over TCP - unix_socket, // UNIX domain sockets -}; - struct redis_stream_state { transport_type type{transport_type::tcp}; bool ssl_stream_used{false}; @@ -71,15 +63,20 @@ class connect_fsm { connect_action resume( system::error_code ec, - std::span resolver_results, - redis_stream_state& st); + const asio::ip::tcp::resolver::results_type& resolver_results, + redis_stream_state& st, + asio::cancellation_type_t cancel_state); connect_action resume( system::error_code ec, - const corosio::endpoint& selected_endpoint, - redis_stream_state& st); + const asio::ip::tcp::endpoint& selected_endpoint, + redis_stream_state& st, + asio::cancellation_type_t cancel_state); - connect_action resume(system::error_code ec, redis_stream_state& st); + connect_action resume( + system::error_code ec, + redis_stream_state& st, + asio::cancellation_type_t cancel_state); }; // namespace boost::redis::detail diff --git a/include/boost/redis/detail/connect_params.hpp b/include/boost/redis/detail/connect_params.hpp index e0dfa01d..d1d32b00 100644 --- a/include/boost/redis/detail/connect_params.hpp +++ b/include/boost/redis/detail/connect_params.hpp @@ -12,7 +12,7 @@ // Parameters used by redis_stream::async_connect #include -#include +#include #include #include diff --git a/include/boost/redis/detail/transport_type.hpp b/include/boost/redis/detail/transport_type.hpp new file mode 100644 index 00000000..dc386561 --- /dev/null +++ b/include/boost/redis/detail/transport_type.hpp @@ -0,0 +1,24 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_REDIS_TRANSPORT_TYPE_HPP +#define BOOST_REDIS_TRANSPORT_TYPE_HPP + +namespace boost::redis::detail { + +// What transport are we using? +enum class transport_type +{ + tcp, // plaintext TCP + tcp_tls, // TLS over TCP + unix_socket, // UNIX domain sockets +}; + +} // namespace boost::redis::detail + +#endif diff --git a/include/boost/redis/impl/co_connect_fsm.ipp b/include/boost/redis/impl/co_connect_fsm.ipp new file mode 100644 index 00000000..7df61cd7 --- /dev/null +++ b/include/boost/redis/impl/co_connect_fsm.ipp @@ -0,0 +1,178 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace boost::redis::detail { + +// Logging +inline void format_tcp_endpoint(const corosio::endpoint& ep, std::string& to) +{ + // This formatting is inspired by Asio's endpoint operator<< + if (ep.is_v6()) { + to += '['; + to += ep.v6_address().to_string(); + to += ']'; + } else { + to += ep.v4_address().to_string(); + } + to += ':'; + to += std::to_string(ep.port()); +} + +template <> +struct log_traits { + static inline void log(std::string& to, const corosio::endpoint& value) + { + format_tcp_endpoint(value, to); + } +}; + +template <> +struct log_traits> { + static inline void log(std::string& to, std::span value) + { + auto iter = value.begin(); + auto end = value.end(); + + if (iter != end) { + format_tcp_endpoint(iter->get_endpoint(), to); + ++iter; + for (; iter != end; ++iter) { + to += ", "; + format_tcp_endpoint(iter->get_endpoint(), to); + } + } + } +}; + +co_connect_action co_connect_fsm::resume( + system::error_code ec, + std::span resolver_results, + co_redis_stream_state& st) +{ + // Log it + if (ec) { + log_info(*lgr_, "Connect: hostname resolution failed: ", ec); + } else { + log_debug(*lgr_, "Connect: hostname resolution results: ", resolver_results); + } + + // Delegate to the regular resume function + return resume(ec, st); +} + +co_connect_action co_connect_fsm::resume( + system::error_code ec, + const corosio::endpoint& selected_endpoint, + co_redis_stream_state& st) +{ + // Log it + if (ec) { + log_info(*lgr_, "Connect: TCP connect failed: ", ec); + } else { + log_debug(*lgr_, "Connect: TCP connect succeeded. Selected endpoint: ", selected_endpoint); + } + + // Delegate to the regular resume function + return resume(ec, st); +} + +co_connect_action co_connect_fsm::resume(system::error_code ec, co_redis_stream_state& st) +{ + switch (resume_point_) { + BOOST_REDIS_CORO_INITIAL + + if (st.type == transport_type::unix_socket) { + // Reset the socket, to discard any previous state. Ignore any errors + BOOST_REDIS_YIELD(resume_point_, 1, co_connect_action_type::unix_socket_close) + + // Connect to the socket + BOOST_REDIS_YIELD(resume_point_, 2, co_connect_action_type::unix_socket_connect) + + // Log it + if (ec) { + log_info(*lgr_, "Connect: UNIX socket connect failed: ", ec); + } else { + log_debug(*lgr_, "Connect: UNIX socket connect succeeded"); + } + + // If this failed, we can't continue + if (ec) { + return ec; + } + + // Done + return system::error_code(); + } else { + // ssl::stream doesn't support being re-used. If we're to use + // TLS and the stream has been used, re-create it. + // Must be done before anything else is done on the stream. + // We don't need to close the TCP socket if using plaintext TCP + // because range-connect closes open sockets, while individual connect doesn't + if (st.type == transport_type::tcp_tls && st.ssl_stream_used) { + BOOST_REDIS_YIELD(resume_point_, 3, co_connect_action_type::ssl_stream_reset) + } + + // Resolve names. The continuation needs access to the returned + // endpoints, and is a specialized resume() that will call this function + BOOST_REDIS_YIELD(resume_point_, 4, co_connect_action_type::tcp_resolve) + + // If this failed, we can't continue + if (ec) { + return ec; + } + + // Now connect to the endpoints returned by the resolver. + // This has a specialized resume(), too + BOOST_REDIS_YIELD(resume_point_, 5, co_connect_action_type::tcp_connect) + + // If this failed, we can't continue + if (ec) { + return ec; + } + + if (st.type == transport_type::tcp_tls) { + // Mark the SSL stream as used + st.ssl_stream_used = true; + + // Perform the TLS handshake + BOOST_REDIS_YIELD(resume_point_, 6, co_connect_action_type::ssl_handshake) + + // Log it + if (ec) { + log_info(*lgr_, "Connect: SSL handshake failed: ", ec); + } else { + log_debug(*lgr_, "Connect: SSL handshake succeeded"); + } + + // If this failed, we can't continue + if (ec) { + return ec; + } + } + + // Done + return system::error_code(); + } + } + + BOOST_ASSERT(false); + return system::error_code(); +} + +} // namespace boost::redis::detail diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index 406e0bd2..44765743 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -7,6 +7,7 @@ // #include +#include #include #include @@ -53,7 +54,7 @@ capy::task> maybe_timeout( capy::io_task<> co_redis_stream::connect(const connect_params& params, buffered_logger& l) { - connect_fsm fsm{l}; + co_connect_fsm fsm{l}; system::error_code ec; corosio::resolver_results endpoints; @@ -61,13 +62,13 @@ capy::io_task<> co_redis_stream::connect(const connect_params& params, buffered_ while (true) { switch (act.type) { - case connect_action_type::unix_socket_close: + case co_connect_action_type::unix_socket_close: BOOST_ASSERT(false); co_return {std::make_error_code(std::errc::operation_not_supported)}; - case connect_action_type::unix_socket_connect: + case co_connect_action_type::unix_socket_connect: BOOST_ASSERT(false); co_return {std::make_error_code(std::errc::operation_not_supported)}; - case connect_action_type::tcp_resolve: + case co_connect_action_type::tcp_resolve: { auto result = co_await capy::timeout( resolv_.resolve(params.addr.tcp_address().host, params.addr.tcp_address().port), @@ -77,19 +78,19 @@ capy::io_task<> co_redis_stream::connect(const connect_params& params, buffered_ act = fsm.resume(ec, endpoints, st_); break; } - case connect_action_type::ssl_stream_reset: + case co_connect_action_type::ssl_stream_reset: stream_.reset(); act = fsm.resume(ec, st_); break; - case connect_action_type::ssl_handshake: + case co_connect_action_type::ssl_handshake: ec = (co_await capy::timeout( stream_.handshake(corosio::tls_stream::handshake_type::client), params.ssl_handshake_timeout)) .ec; act = fsm.resume(ec, st_); break; - case connect_action_type::done: co_return {act.ec}; - case connect_action_type::tcp_connect: + case co_connect_action_type::done: co_return {act.ec}; + case co_connect_action_type::tcp_connect: { auto result = co_await capy::timeout( corosio::connect(socket_, std::move(endpoints)), @@ -227,8 +228,10 @@ inline capy::io_task<> async_exec_one( } case exec_one_action_type::read_some: { + // https://github.com/cppalliance/capy/issues/147 + auto buff = conn.st_.mpx.get_read_buffer().get_prepared(); auto [read_ec, read_bytes] = co_await conn.stream_.read_some( - capy::make_buffer(conn.st_.mpx.get_read_buffer().get_prepared())); + capy::mutable_buffer(buff.data(), buff.size())); ec = read_ec; bytes = read_bytes; break; @@ -321,8 +324,10 @@ inline capy::io_task reader(co_connection_impl& conn) switch (act.get_type()) { case reader_fsm::action::type::read_some: { + // https://github.com/cppalliance/capy/issues/147 + auto buff = conn.st_.mpx.get_prepared_read_buffer(); auto [read_ec, read_bytes] = co_await maybe_timeout( - conn.stream_.read_some(capy::make_buffer(conn.st_.mpx.get_prepared_read_buffer())), + conn.stream_.read_some(capy::mutable_buffer(buff.data(), buff.size())), act.timeout()); ec = read_ec; n = read_bytes; diff --git a/include/boost/redis/impl/connect_fsm.ipp b/include/boost/redis/impl/connect_fsm.ipp index ebf95531..f2699dda 100644 --- a/include/boost/redis/impl/connect_fsm.ipp +++ b/include/boost/redis/impl/connect_fsm.ipp @@ -9,62 +9,86 @@ #include #include #include +#include #include +#include +#include +#include #include -#include -#include #include namespace boost::redis::detail { // Logging -inline void format_tcp_endpoint(const corosio::endpoint& ep, std::string& to) +inline void format_tcp_endpoint(const asio::ip::tcp::endpoint& ep, std::string& to) { // This formatting is inspired by Asio's endpoint operator<< - if (ep.is_v6()) { + const auto& addr = ep.address(); + if (addr.is_v6()) to += '['; - to += ep.v6_address().to_string(); + to += addr.to_string(); + if (addr.is_v6()) to += ']'; - } else { - to += ep.v4_address().to_string(); - } to += ':'; to += std::to_string(ep.port()); } template <> -struct log_traits { - static inline void log(std::string& to, const corosio::endpoint& value) +struct log_traits { + static inline void log(std::string& to, const asio::ip::tcp::endpoint& value) { format_tcp_endpoint(value, to); } }; template <> -struct log_traits> { - static inline void log(std::string& to, std::span value) +struct log_traits { + static inline void log(std::string& to, const asio::ip::tcp::resolver::results_type& value) { - auto iter = value.begin(); - auto end = value.end(); + auto iter = value.cbegin(); + auto end = value.cend(); if (iter != end) { - format_tcp_endpoint(iter->get_endpoint(), to); + format_tcp_endpoint(iter->endpoint(), to); ++iter; for (; iter != end; ++iter) { to += ", "; - format_tcp_endpoint(iter->get_endpoint(), to); + format_tcp_endpoint(iter->endpoint(), to); } } } }; +inline system::error_code translate_timeout_error( + system::error_code io_ec, + asio::cancellation_type_t cancel_state, + error code_if_cancelled) +{ + // Translates cancellations and timeout errors into a single error_code. + // - Cancellation state set, and an I/O error: the entire operation was cancelled. + // The I/O code (probably operation_aborted) is appropriate. + // - Cancellation state set, and no I/O error: same as above, but the cancellation + // arrived after the operation completed and before the handler was called. Set the code here. + // - No cancellation state set, I/O error set to operation_aborted: since we use cancel_after, + // this means a timeout. + // - Otherwise, respect the I/O error. + if ((cancel_state & asio::cancellation_type_t::terminal) != asio::cancellation_type_t::none) { + return io_ec ? io_ec : asio::error::operation_aborted; + } + return io_ec == asio::error::operation_aborted ? code_if_cancelled : io_ec; +} + connect_action connect_fsm::resume( system::error_code ec, - std::span resolver_results, - redis_stream_state& st) + const asio::ip::tcp::resolver::results_type& resolver_results, + redis_stream_state& st, + asio::cancellation_type_t cancel_state) { + // Translate error codes + ec = translate_timeout_error(ec, cancel_state, error::resolve_timeout); + // Log it if (ec) { log_info(*lgr_, "Connect: hostname resolution failed: ", ec); @@ -73,14 +97,18 @@ connect_action connect_fsm::resume( } // Delegate to the regular resume function - return resume(ec, st); + return resume(ec, st, cancel_state); } connect_action connect_fsm::resume( system::error_code ec, - const corosio::endpoint& selected_endpoint, - redis_stream_state& st) + const asio::ip::tcp::endpoint& selected_endpoint, + redis_stream_state& st, + asio::cancellation_type_t cancel_state) { + // Translate error codes + ec = translate_timeout_error(ec, cancel_state, error::connect_timeout); + // Log it if (ec) { log_info(*lgr_, "Connect: TCP connect failed: ", ec); @@ -89,10 +117,13 @@ connect_action connect_fsm::resume( } // Delegate to the regular resume function - return resume(ec, st); + return resume(ec, st, cancel_state); } -connect_action connect_fsm::resume(system::error_code ec, redis_stream_state& st) +connect_action connect_fsm::resume( + system::error_code ec, + redis_stream_state& st, + asio::cancellation_type_t cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -104,6 +135,12 @@ connect_action connect_fsm::resume(system::error_code ec, redis_stream_state& st // Connect to the socket BOOST_REDIS_YIELD(resume_point_, 2, connect_action_type::unix_socket_connect) + // Fix error codes. If we were cancelled and the code is operation_aborted, + // it is because per-operation cancellation was activated. If we were not cancelled + // but the operation failed with operation_aborted, it's a timeout. + // Also check for cancellations that didn't cause a failure + ec = translate_timeout_error(ec, cancel_state, error::connect_timeout); + // Log it if (ec) { log_info(*lgr_, "Connect: UNIX socket connect failed: ", ec); @@ -132,7 +169,7 @@ connect_action connect_fsm::resume(system::error_code ec, redis_stream_state& st // endpoints, and is a specialized resume() that will call this function BOOST_REDIS_YIELD(resume_point_, 4, connect_action_type::tcp_resolve) - // If this failed, we can't continue + // If this failed, we can't continue (error code translation already performed here) if (ec) { return ec; } @@ -141,7 +178,7 @@ connect_action connect_fsm::resume(system::error_code ec, redis_stream_state& st // This has a specialized resume(), too BOOST_REDIS_YIELD(resume_point_, 5, connect_action_type::tcp_connect) - // If this failed, we can't continue + // If this failed, we can't continue (error code translation already performed here) if (ec) { return ec; } @@ -153,6 +190,9 @@ connect_action connect_fsm::resume(system::error_code ec, redis_stream_state& st // Perform the TLS handshake BOOST_REDIS_YIELD(resume_point_, 6, connect_action_type::ssl_handshake) + // Translate error codes + ec = translate_timeout_error(ec, cancel_state, error::ssl_handshake_timeout); + // Log it if (ec) { log_info(*lgr_, "Connect: SSL handshake failed: ", ec); diff --git a/include/boost/redis/src/asio.hpp b/include/boost/redis/src/asio.hpp index e15516a1..5f68727f 100644 --- a/include/boost/redis/src/asio.hpp +++ b/include/boost/redis/src/asio.hpp @@ -4,5 +4,5 @@ * accompanying file LICENSE.txt) */ +#include #include -// #include // TODO diff --git a/include/boost/redis/src/corosio.hpp b/include/boost/redis/src/corosio.hpp index 1489db18..4739762d 100644 --- a/include/boost/redis/src/corosio.hpp +++ b/include/boost/redis/src/corosio.hpp @@ -4,5 +4,5 @@ * accompanying file LICENSE.txt) */ +#include #include -#include From 3c9f32296c1c4d46e90c8035709a0bfc2532e17d Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 12:06:36 +0200 Subject: [PATCH 046/115] Recover read_buffer --- include/boost/redis/impl/read_buffer.ipp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/boost/redis/impl/read_buffer.ipp b/include/boost/redis/impl/read_buffer.ipp index 78f63b2a..b3705549 100644 --- a/include/boost/redis/impl/read_buffer.ipp +++ b/include/boost/redis/impl/read_buffer.ipp @@ -37,7 +37,7 @@ void read_buffer::commit(std::size_t read_size) auto read_buffer::get_prepared() noexcept -> span_type { auto const size = buffer_.size(); - return span_type(buffer_.data() + append_buf_begin_, size - append_buf_begin_); + return make_span(buffer_.data() + append_buf_begin_, size - append_buf_begin_); } auto read_buffer::get_commited() const noexcept -> std::string_view @@ -51,7 +51,8 @@ void read_buffer::clear() append_buf_begin_ = 0; } -read_buffer::consume_result read_buffer::consume(std::size_t size) +read_buffer::consume_result +read_buffer::consume(std::size_t size) { // For convenience, if the requested size is larger than the // committed buffer we cap it to the maximum. From 9a8a34ef6119cb7c08be6ca44ff88aa165f1be39 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 12:44:16 +0200 Subject: [PATCH 047/115] Use generic codes (1) --- include/boost/redis/impl/exec_fsm.ipp | 5 +++-- include/boost/redis/impl/exec_one_fsm.ipp | 6 +++--- include/boost/redis/impl/multiplexer.ipp | 6 +++--- include/boost/redis/impl/reader_fsm.ipp | 11 +++++------ include/boost/redis/impl/receive_fsm.ipp | 3 +-- include/boost/redis/impl/run_fsm.ipp | 11 +++++------ include/boost/redis/impl/sentinel_resolve_fsm.ipp | 5 ++--- include/boost/redis/impl/writer_fsm.ipp | 11 +++++------ 8 files changed, 27 insertions(+), 31 deletions(-) diff --git a/include/boost/redis/impl/exec_fsm.ipp b/include/boost/redis/impl/exec_fsm.ipp index bede881e..71de2f32 100644 --- a/include/boost/redis/impl/exec_fsm.ipp +++ b/include/boost/redis/impl/exec_fsm.ipp @@ -12,10 +12,11 @@ #include #include #include +#include #include #include -#include +#include namespace boost::redis::detail { @@ -83,7 +84,7 @@ exec_action exec_fsm::resume( is_partial_or_terminal_cancel(cancel_state)) { st.mpx.cancel(elem_); elem_.reset(); // Deallocate memory before finalizing - return exec_action{capy::error::canceled}; + return exec_action{make_error_code(system::errc::operation_canceled)}; } } } diff --git a/include/boost/redis/impl/exec_one_fsm.ipp b/include/boost/redis/impl/exec_one_fsm.ipp index b7cab7bf..c2e236cd 100644 --- a/include/boost/redis/impl/exec_one_fsm.ipp +++ b/include/boost/redis/impl/exec_one_fsm.ipp @@ -18,8 +18,8 @@ #include #include -#include #include +#include #include #include @@ -40,7 +40,7 @@ exec_one_action exec_one_fsm::resume( // Errors and cancellations if (is_terminal_cancel(cancel_state)) - return system::error_code{capy::error::canceled}; + return make_error_code(system::errc::operation_canceled); if (ec) return ec; @@ -61,7 +61,7 @@ exec_one_action exec_one_fsm::resume( // Errors and cancellations if (is_terminal_cancel(cancel_state)) - return system::error_code{capy::error::canceled}; + return make_error_code(system::errc::operation_canceled); if (ec) return ec; diff --git a/include/boost/redis/impl/multiplexer.ipp b/include/boost/redis/impl/multiplexer.ipp index 931dbf56..255c35f9 100644 --- a/include/boost/redis/impl/multiplexer.ipp +++ b/include/boost/redis/impl/multiplexer.ipp @@ -5,10 +5,10 @@ */ #include +#include #include #include -#include #include #include @@ -243,7 +243,7 @@ std::size_t multiplexer::cancel_waiting() auto const ret = std::distance(point, std::end(reqs_)); std::for_each(point, std::end(reqs_), [](auto const& ptr) { - ptr->notify_error({capy::error::canceled}); + ptr->notify_error({make_error_code(system::errc::operation_canceled)}); }); reqs_.erase(point, std::end(reqs_)); @@ -276,7 +276,7 @@ void multiplexer::cancel_on_conn_lost() auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), cond); std::for_each(point, std::end(reqs_), [](auto const& ptr) { - ptr->notify_error({capy::error::canceled}); + ptr->notify_error({make_error_code(system::errc::operation_canceled)}); }); reqs_.erase(point, std::end(reqs_)); diff --git a/include/boost/redis/impl/reader_fsm.ipp b/include/boost/redis/impl/reader_fsm.ipp index f7d51c7b..f09eba7f 100644 --- a/include/boost/redis/impl/reader_fsm.ipp +++ b/include/boost/redis/impl/reader_fsm.ipp @@ -12,8 +12,7 @@ #include #include -#include -#include +#include namespace boost::redis::detail { @@ -43,13 +42,13 @@ reader_fsm::action reader_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Reader task: cancelled (1)"); - return system::error_code(capy::error::canceled); + return system::error_code(make_error_code(system::errc::operation_canceled)); } - // Translate timeout errors caused by canceled to more legible ones. + // Translate timeout errors caused by operation_aborted to more legible ones. // A timeout here means that we didn't receive data in time. // Note that cancellation is already handled by the above statement. - if (ec == capy::cond::timeout) { + if (ec == asio::error::operation_aborted) { // TODO ec = error::pong_timeout; } @@ -96,7 +95,7 @@ reader_fsm::action reader_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Reader task: cancelled (2)"); - return system::error_code(capy::error::canceled); + return system::error_code(make_error_code(system::errc::operation_canceled)); } // Check for other errors. diff --git a/include/boost/redis/impl/receive_fsm.ipp b/include/boost/redis/impl/receive_fsm.ipp index 2cefed6a..b3a9f9d3 100644 --- a/include/boost/redis/impl/receive_fsm.ipp +++ b/include/boost/redis/impl/receive_fsm.ipp @@ -11,7 +11,6 @@ #include #include -#include #include #include @@ -63,7 +62,7 @@ receive_action receive_fsm::resume( // Check for cancellations if (is_any_cancel(cancel_state) || st.receive2_cancelled) { st.receive2_running = false; - return system::error_code(capy::error::canceled); + return make_error_code(system::errc::operation_canceled); } // If we get any unknown errors, propagate them (shouldn't happen, but just in case) diff --git a/include/boost/redis/impl/run_fsm.ipp b/include/boost/redis/impl/run_fsm.ipp index 9a2c6f63..8ba7a1be 100644 --- a/include/boost/redis/impl/run_fsm.ipp +++ b/include/boost/redis/impl/run_fsm.ipp @@ -20,8 +20,7 @@ #include #include -#include -// #include // for BOOST_ASIO_HAS_LOCAL_SOCKETS +// #include // for BOOST_ASIO_HAS_LOCAL_SOCKETS, TODO #include namespace boost::redis::detail { @@ -126,7 +125,7 @@ run_action run_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (4)"); - return {capy::error::canceled}; + return {make_error_code(system::errc::operation_canceled)}; } // Check for errors @@ -141,7 +140,7 @@ run_action run_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (1)"); - return system::error_code(capy::error::canceled); + return make_error_code(system::errc::operation_canceled); } if (ec) { @@ -193,7 +192,7 @@ run_action run_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (2)"); - return system::error_code(capy::error::canceled); + return make_error_code(system::errc::operation_canceled); } sleep_and_reconnect: @@ -209,7 +208,7 @@ sleep_and_reconnect: // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Run: cancelled (3)"); - return system::error_code(capy::error::canceled); + return make_error_code(system::errc::operation_canceled); } } } diff --git a/include/boost/redis/impl/sentinel_resolve_fsm.ipp b/include/boost/redis/impl/sentinel_resolve_fsm.ipp index ff963555..d8cfbb26 100644 --- a/include/boost/redis/impl/sentinel_resolve_fsm.ipp +++ b/include/boost/redis/impl/sentinel_resolve_fsm.ipp @@ -20,7 +20,6 @@ #include #include -#include #include #include @@ -69,7 +68,7 @@ sentinel_action sentinel_resolve_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Sentinel resolve: cancelled (1)"); - return system::error_code(capy::error::canceled); + return make_error_code(system::errc::operation_canceled); } // Check for errors @@ -86,7 +85,7 @@ sentinel_action sentinel_resolve_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Sentinel resolve: cancelled (2)"); - return system::error_code(capy::error::canceled); + return make_error_code(system::errc::operation_canceled); } // Check for errors diff --git a/include/boost/redis/impl/writer_fsm.ipp b/include/boost/redis/impl/writer_fsm.ipp index 92c5afed..9cd689ee 100644 --- a/include/boost/redis/impl/writer_fsm.ipp +++ b/include/boost/redis/impl/writer_fsm.ipp @@ -21,8 +21,7 @@ #include #include -#include -#include +#include #include #include @@ -82,13 +81,13 @@ writer_action writer_fsm::resume( // Check for cancellations and translate error codes if (is_terminal_cancel(cancel_state)) - ec = capy::error::canceled; - else if (ec == capy::cond::timeout) + ec = make_error_code(system::errc::operation_canceled); + else if (ec == make_error_code(system::errc::operation_canceled)) // TODO ec = error::write_timeout; // Check for errors if (ec) { - if (ec == capy::cond::canceled) { + if (ec == system::errc::operation_canceled) { log_debug(st.logger, "Writer task: cancelled (1)."); } else { log_err(st.logger, "Error writing data to the server: ", ec); @@ -108,7 +107,7 @@ writer_action writer_fsm::resume( // Check for cancellations if (is_terminal_cancel(cancel_state)) { log_debug(st.logger, "Writer task: cancelled (2)."); - return system::error_code(capy::error::canceled); + return make_error_code(system::errc::operation_canceled); } // If we weren't notified, it's because there is no data and we should send a health check From 78302095d4f315c8862cfeb423b088927f259ba1 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 13:06:40 +0200 Subject: [PATCH 048/115] writr uses error conditions --- include/boost/redis/connection.hpp | 3 ++- include/boost/redis/detail/writer_fsm.hpp | 6 +++++- include/boost/redis/impl/co_connection.ipp | 5 +++-- include/boost/redis/impl/writer_fsm.ipp | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index 46184c2c..b47c4851 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -382,7 +382,8 @@ auto async_sentinel_resolve(connection_impl& conn, CompletionToken&& t template struct writer_op { connection_impl* conn_; - writer_fsm fsm_; + // asio timeouts => operation_aborted + writer_fsm fsm_{system::error_code(asio::error::operation_aborted).default_error_condition()}; explicit writer_op(connection_impl& conn) noexcept : conn_(&conn) diff --git a/include/boost/redis/detail/writer_fsm.hpp b/include/boost/redis/detail/writer_fsm.hpp index be285834..1a0c82d9 100644 --- a/include/boost/redis/detail/writer_fsm.hpp +++ b/include/boost/redis/detail/writer_fsm.hpp @@ -15,6 +15,7 @@ #include #include +#include // Sans-io algorithm for the writer task, as a finite state machine @@ -75,10 +76,13 @@ class writer_action { }; class writer_fsm { + std::error_condition timeout_cond_; int resume_point_{0}; public: - writer_fsm() = default; + writer_fsm(std::error_condition timeout_cond) noexcept + : timeout_cond_(timeout_cond) + { } writer_action resume( connection_state& st, diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index 44765743..ec538158 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -277,8 +278,8 @@ inline capy::io_task<> sentinel_resolve(co_connection_impl& conn) // This signature is required because capy::when_any is equivalent to wait_for_one_success inline capy::io_task writer(co_connection_impl& conn) { - // Setup - writer_fsm fsm; + // Setup. + writer_fsm fsm{capy::cond::timeout}; system::error_code ec; std::size_t bytes_written = 0u; diff --git a/include/boost/redis/impl/writer_fsm.ipp b/include/boost/redis/impl/writer_fsm.ipp index 9cd689ee..4d13f9b9 100644 --- a/include/boost/redis/impl/writer_fsm.ipp +++ b/include/boost/redis/impl/writer_fsm.ipp @@ -82,7 +82,7 @@ writer_action writer_fsm::resume( // Check for cancellations and translate error codes if (is_terminal_cancel(cancel_state)) ec = make_error_code(system::errc::operation_canceled); - else if (ec == make_error_code(system::errc::operation_canceled)) // TODO + else if (ec == timeout_cond_) ec = error::write_timeout; // Check for errors From d6968f4adfe31ee537d499f17e99c0c06b30db32 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 13:11:14 +0200 Subject: [PATCH 049/115] Change reader too --- include/boost/redis/connection.hpp | 3 ++- include/boost/redis/detail/reader_fsm.hpp | 6 +++++- include/boost/redis/impl/co_connection.ipp | 2 +- include/boost/redis/impl/reader_fsm.ipp | 5 ++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index b47c4851..4a6952d8 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -420,7 +420,8 @@ struct writer_op { template struct reader_op { connection_impl* conn_; - reader_fsm fsm_; + // asio timeouts => operation_aborted + reader_fsm fsm_{system::error_code(asio::error::operation_aborted).default_error_condition()}; public: reader_op(connection_impl& conn) noexcept diff --git a/include/boost/redis/detail/reader_fsm.hpp b/include/boost/redis/detail/reader_fsm.hpp index 3e7a7032..10931fd3 100644 --- a/include/boost/redis/detail/reader_fsm.hpp +++ b/include/boost/redis/detail/reader_fsm.hpp @@ -15,6 +15,7 @@ #include #include +#include namespace boost::redis::detail { @@ -85,9 +86,12 @@ class reader_fsm { system::error_code ec, asio::cancellation_type_t cancel_state); - reader_fsm() = default; + reader_fsm(std::error_condition timeout_cond) noexcept + : timeout_cond_(timeout_cond) + { } private: + std::error_condition timeout_cond_; int resume_point_{0}; std::pair res_{consume_result::needs_more, 0u}; }; diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index ec538158..9f108dd0 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -315,7 +315,7 @@ inline capy::io_task writer(co_connection_impl& conn) inline capy::io_task reader(co_connection_impl& conn) { - reader_fsm fsm; + reader_fsm fsm{capy::cond::timeout}; std::size_t n = 0u; system::error_code ec; diff --git a/include/boost/redis/impl/reader_fsm.ipp b/include/boost/redis/impl/reader_fsm.ipp index f09eba7f..b2815b82 100644 --- a/include/boost/redis/impl/reader_fsm.ipp +++ b/include/boost/redis/impl/reader_fsm.ipp @@ -12,7 +12,6 @@ #include #include -#include namespace boost::redis::detail { @@ -45,10 +44,10 @@ reader_fsm::action reader_fsm::resume( return system::error_code(make_error_code(system::errc::operation_canceled)); } - // Translate timeout errors caused by operation_aborted to more legible ones. + // Translate timeout errors to more legible ones. // A timeout here means that we didn't receive data in time. // Note that cancellation is already handled by the above statement. - if (ec == asio::error::operation_aborted) { // TODO + if (ec == timeout_cond_) { ec = error::pong_timeout; } From 4cc3873889a11f493dd70389c016edf749f4c880 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 13:15:24 +0200 Subject: [PATCH 050/115] Remove Asio dependency to detect UNIX sockets --- include/boost/redis/connection.hpp | 2 +- include/boost/redis/detail/redis_stream.hpp | 9 +++++++++ include/boost/redis/detail/run_fsm.hpp | 5 ++++- include/boost/redis/impl/co_connection.ipp | 3 ++- include/boost/redis/impl/run_fsm.ipp | 10 ++++------ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index 4a6952d8..7350e3f7 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -461,7 +461,7 @@ template class run_op { private: connection_impl* conn_; - run_fsm fsm_{}; + run_fsm fsm_{unix_sockets_supported()}; template auto reader(CompletionToken&& token) diff --git a/include/boost/redis/detail/redis_stream.hpp b/include/boost/redis/detail/redis_stream.hpp index 349cc650..ddbbe2b9 100644 --- a/include/boost/redis/detail/redis_stream.hpp +++ b/include/boost/redis/detail/redis_stream.hpp @@ -34,6 +34,15 @@ namespace boost { namespace redis { namespace detail { +constexpr bool unix_sockets_supported() +{ +#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + return true; +#else + return false; +#endif +} + template class redis_stream { asio::ssl::context ssl_ctx_; diff --git a/include/boost/redis/detail/run_fsm.hpp b/include/boost/redis/detail/run_fsm.hpp index b125fa74..d58a2578 100644 --- a/include/boost/redis/detail/run_fsm.hpp +++ b/include/boost/redis/detail/run_fsm.hpp @@ -48,11 +48,14 @@ struct run_action { }; class run_fsm { + bool unix_sockets_supported_; int resume_point_{0}; system::error_code stored_ec_; public: - run_fsm() = default; + run_fsm(bool unix_sockets_supported) noexcept + : unix_sockets_supported_(unix_sockets_supported) + { } run_action resume( connection_state& st, diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index 9f108dd0..a47932f4 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -351,7 +351,8 @@ inline capy::io_task reader(co_connection_impl& conn) inline capy::io_task run(co_connection_impl& conn) { - run_fsm fsm; + constexpr bool unix_sockets_supported = false; // TODO + run_fsm fsm{unix_sockets_supported}; system::error_code ec; while (true) { diff --git a/include/boost/redis/impl/run_fsm.ipp b/include/boost/redis/impl/run_fsm.ipp index 8ba7a1be..692a6430 100644 --- a/include/boost/redis/impl/run_fsm.ipp +++ b/include/boost/redis/impl/run_fsm.ipp @@ -20,21 +20,19 @@ #include #include -// #include // for BOOST_ASIO_HAS_LOCAL_SOCKETS, TODO #include namespace boost::redis::detail { -inline system::error_code check_config(const config& cfg) +inline system::error_code check_config(const config& cfg, bool unix_sockets_supported) { if (!cfg.unix_socket.empty()) { if (cfg.use_ssl) return error::unix_sockets_ssl_unsupported; if (use_sentinel(cfg)) return error::sentinel_unix_sockets_unsupported; -#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS - return error::unix_sockets_unsupported; -#endif + if (!unix_sockets_supported) + return error::unix_sockets_unsupported; } return system::error_code{}; } @@ -94,7 +92,7 @@ run_action run_fsm::resume( BOOST_REDIS_CORO_INITIAL // Check config - ec = check_config(st.cfg); + ec = check_config(st.cfg, unix_sockets_supported_); if (ec) { log_err(st.logger, "Invalid configuration: ", ec); stored_ec_ = ec; From 1d258b11b1f3a2537285f40bfbadb77f998a2804 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 13:16:50 +0200 Subject: [PATCH 051/115] Remove unused timer --- include/boost/redis/co_connection.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/boost/redis/co_connection.hpp b/include/boost/redis/co_connection.hpp index 649f863d..bb728e09 100644 --- a/include/boost/redis/co_connection.hpp +++ b/include/boost/redis/co_connection.hpp @@ -70,7 +70,6 @@ class co_redis_stream { // TODO: UNIX sockets corosio::tcp_socket socket_; corosio::openssl_stream stream_; // TODO: make this configurable - corosio::timer timer_; corosio::resolver resolv_; co_redis_stream_state st_; @@ -78,7 +77,6 @@ class co_redis_stream { explicit co_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) : socket_(ctx) , stream_(&socket_, std::move(tls_ctx)) - , timer_(ctx) , resolv_(ctx) { } From d0cd02adaa94a45b1eff8284bc599aabc8eed8d0 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 13:42:43 +0200 Subject: [PATCH 052/115] Remove Asio dependency regarding cancellations --- include/boost/redis/connection.hpp | 44 ++++++++++++++--- .../boost/redis/detail/cancellation_type.hpp | 47 +++++++++++++++++++ include/boost/redis/detail/exec_fsm.hpp | 4 +- include/boost/redis/detail/exec_one_fsm.hpp | 4 +- include/boost/redis/detail/reader_fsm.hpp | 4 +- include/boost/redis/detail/receive_fsm.hpp | 5 +- include/boost/redis/detail/run_fsm.hpp | 4 +- .../redis/detail/sentinel_resolve_fsm.hpp | 4 +- include/boost/redis/detail/writer_fsm.hpp | 5 +- include/boost/redis/impl/co_connection.ipp | 5 +- include/boost/redis/impl/exec_fsm.ipp | 17 ++----- include/boost/redis/impl/exec_one_fsm.ipp | 3 +- .../boost/redis/impl/is_terminal_cancel.hpp | 8 ++-- include/boost/redis/impl/reader_fsm.ipp | 4 +- include/boost/redis/impl/receive_fsm.ipp | 12 ++--- include/boost/redis/impl/run_fsm.ipp | 3 +- .../boost/redis/impl/sentinel_resolve_fsm.ipp | 2 +- include/boost/redis/impl/writer_fsm.ipp | 3 +- 18 files changed, 118 insertions(+), 60 deletions(-) create mode 100644 include/boost/redis/detail/cancellation_type.hpp diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index 7350e3f7..c5137252 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -72,6 +73,22 @@ inline std::chrono::steady_clock::time_point compute_expiry( : std::chrono::steady_clock::now() + timeout; } +// Translates Asio's cancellation type into the common one used by the FSMs. +// Enumerator values match but are not guaranteed to remain like this. +// This function also filters any unknown Asio cancellation types. +// TODO: unit test +constexpr cancellation_type to_redis_cancellation(asio::cancellation_type_t t) noexcept +{ + int res = 0; + if ((t & asio::cancellation_type::terminal) != asio::cancellation_type_t::none) + res |= static_cast(cancellation_type::terminal); + if ((t & asio::cancellation_type::partial) != asio::cancellation_type_t::none) + res |= static_cast(cancellation_type::partial); + if ((t & asio::cancellation_type::total) != asio::cancellation_type_t::none) + res |= static_cast(cancellation_type::total); + return static_cast(res); +} + template struct connection_impl { using clock_type = std::chrono::steady_clock; @@ -112,7 +129,7 @@ struct connection_impl { auto act = fsm_.resume( obj_->is_open(), obj_->st_, - self.get_cancellation_state().cancelled()); + to_redis_cancellation(self.get_cancellation_state().cancelled())); // Do what the FSM said switch (act.type()) { @@ -263,7 +280,10 @@ struct receive2_op { template void operator()(Self& self, system::error_code ec = {}, std::size_t /* push_bytes */ = 0u) { - receive_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + receive_action act = fsm_.resume( + conn_->st_, + ec, + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.type) { case receive_action::action_type::setup_cancellation: @@ -304,7 +324,7 @@ struct exec_one_op { conn_->st_.mpx.get_read_buffer(), ec, bytes_written, - self.get_cancellation_state().cancelled()); + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.type) { case exec_one_action_type::done: self.complete(act.ec); return; @@ -346,7 +366,10 @@ struct sentinel_resolve_op { void operator()(Self& self, system::error_code ec = {}) { auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after - sentinel_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + sentinel_action act = fsm_.resume( + conn_->st_, + ec, + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.get_type()) { case sentinel_action::type::done: self.complete(act.error()); return; @@ -397,7 +420,7 @@ struct writer_op { conn->st_, ec, bytes_written, - self.get_cancellation_state().cancelled()); + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.type()) { case writer_action_type::done: self.complete(act.error()); return; @@ -433,7 +456,11 @@ struct reader_op { { for (;;) { auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after - auto act = fsm_.resume(conn->st_, n, ec, self.get_cancellation_state().cancelled()); + auto act = fsm_.resume( + conn->st_, + n, + ec, + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.get_type()) { case reader_fsm::action::type::read_some: @@ -500,7 +527,10 @@ class run_op { template void operator()(Self& self, system::error_code ec = {}) { - auto act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled()); + auto act = fsm_.resume( + conn_->st_, + ec, + to_redis_cancellation(self.get_cancellation_state().cancelled())); switch (act.type) { case run_action_type::done: self.complete(act.ec); return; diff --git a/include/boost/redis/detail/cancellation_type.hpp b/include/boost/redis/detail/cancellation_type.hpp new file mode 100644 index 00000000..1249bf43 --- /dev/null +++ b/include/boost/redis/detail/cancellation_type.hpp @@ -0,0 +1,47 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_REDIS_CANCELLATION_TYPE_HPP +#define BOOST_REDIS_CANCELLATION_TYPE_HPP + +// A minimal port of asio::cancellation_type_t to avoid +// depending on Asio in the protocol state machines + +namespace boost::redis::detail { + +enum class cancellation_type : int +{ + none = 0, + terminal = 1, + partial = 2, + total = 4, +}; + +constexpr bool contains(cancellation_type value, cancellation_type query) +{ + return (static_cast(value) & static_cast(query)) != 0; +} + +constexpr bool contains_terminal(cancellation_type value) +{ + return contains(value, cancellation_type::terminal); +} + +constexpr bool contains_partial(cancellation_type value) +{ + return contains(value, cancellation_type::partial); +} + +constexpr bool contains_total(cancellation_type value) +{ + return contains(value, cancellation_type::total); +} + +} // namespace boost::redis::detail + +#endif diff --git a/include/boost/redis/detail/exec_fsm.hpp b/include/boost/redis/detail/exec_fsm.hpp index 7dc2f8c1..a9603830 100644 --- a/include/boost/redis/detail/exec_fsm.hpp +++ b/include/boost/redis/detail/exec_fsm.hpp @@ -9,9 +9,9 @@ #ifndef BOOST_REDIS_EXEC_FSM_HPP #define BOOST_REDIS_EXEC_FSM_HPP +#include #include -#include #include #include @@ -66,7 +66,7 @@ class exec_fsm { exec_action resume( bool connection_is_open, connection_state& st, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; } // namespace boost::redis::detail diff --git a/include/boost/redis/detail/exec_one_fsm.hpp b/include/boost/redis/detail/exec_one_fsm.hpp index 4ad3bd3e..cb6061c2 100644 --- a/include/boost/redis/detail/exec_one_fsm.hpp +++ b/include/boost/redis/detail/exec_one_fsm.hpp @@ -10,9 +10,9 @@ #define BOOST_REDIS_EXEC_ONE_FSM_HPP #include +#include #include -#include #include #include @@ -61,7 +61,7 @@ class exec_one_fsm { read_buffer& buffer, system::error_code ec, std::size_t bytes_transferred, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; } // namespace boost::redis::detail diff --git a/include/boost/redis/detail/reader_fsm.hpp b/include/boost/redis/detail/reader_fsm.hpp index 10931fd3..29ddf8a6 100644 --- a/include/boost/redis/detail/reader_fsm.hpp +++ b/include/boost/redis/detail/reader_fsm.hpp @@ -7,10 +7,10 @@ #ifndef BOOST_REDIS_READER_FSM_HPP #define BOOST_REDIS_READER_FSM_HPP +#include #include #include -#include #include #include @@ -84,7 +84,7 @@ class reader_fsm { connection_state& st, std::size_t bytes_read, system::error_code ec, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); reader_fsm(std::error_condition timeout_cond) noexcept : timeout_cond_(timeout_cond) diff --git a/include/boost/redis/detail/receive_fsm.hpp b/include/boost/redis/detail/receive_fsm.hpp index 16989dce..d9812dfd 100644 --- a/include/boost/redis/detail/receive_fsm.hpp +++ b/include/boost/redis/detail/receive_fsm.hpp @@ -9,7 +9,8 @@ #ifndef BOOST_REDIS_RECEIVE_FSM_HPP #define BOOST_REDIS_RECEIVE_FSM_HPP -#include +#include + #include // Sans-io algorithm for async_receive2, as a finite state machine @@ -50,7 +51,7 @@ class receive_fsm { receive_action resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; } // namespace boost::redis::detail diff --git a/include/boost/redis/detail/run_fsm.hpp b/include/boost/redis/detail/run_fsm.hpp index d58a2578..95a34248 100644 --- a/include/boost/redis/detail/run_fsm.hpp +++ b/include/boost/redis/detail/run_fsm.hpp @@ -9,9 +9,9 @@ #ifndef BOOST_REDIS_RUN_FSM_HPP #define BOOST_REDIS_RUN_FSM_HPP +#include #include -#include #include // Sans-io algorithm for async_run, as a finite state machine @@ -60,7 +60,7 @@ class run_fsm { run_action resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; connect_params make_run_connect_params(const connection_state& st); diff --git a/include/boost/redis/detail/sentinel_resolve_fsm.hpp b/include/boost/redis/detail/sentinel_resolve_fsm.hpp index de8d4db6..05b092cc 100644 --- a/include/boost/redis/detail/sentinel_resolve_fsm.hpp +++ b/include/boost/redis/detail/sentinel_resolve_fsm.hpp @@ -11,9 +11,9 @@ #include #include +#include #include -#include #include #include @@ -82,7 +82,7 @@ class sentinel_resolve_fsm { sentinel_action resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; connect_params make_sentinel_connect_params(const config& cfg, const address& sentinel_addr); diff --git a/include/boost/redis/detail/writer_fsm.hpp b/include/boost/redis/detail/writer_fsm.hpp index 1a0c82d9..807501a1 100644 --- a/include/boost/redis/detail/writer_fsm.hpp +++ b/include/boost/redis/detail/writer_fsm.hpp @@ -9,7 +9,8 @@ #ifndef BOOST_REDIS_WRITER_FSM_HPP #define BOOST_REDIS_WRITER_FSM_HPP -#include +#include + #include #include @@ -88,7 +89,7 @@ class writer_fsm { connection_state& st, system::error_code ec, std::size_t bytes_written, - asio::cancellation_type_t cancel_state); + cancellation_type cancel_state); }; } // namespace boost::redis::detail diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index a47932f4..5a6c378b 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -35,10 +35,9 @@ inline std::chrono::steady_clock::time_point compute_expiry( : std::chrono::steady_clock::now() + timeout; } -inline asio::cancellation_type_t token_to_cancel(std::stop_token tok) +inline cancellation_type token_to_cancel(std::stop_token tok) { - return tok.stop_requested() ? asio::cancellation_type_t::terminal - : asio::cancellation_type_t::none; + return tok.stop_requested() ? cancellation_type::terminal : cancellation_type::none; } // Run an operation with a timeout, with a zero timeout meaning 'no timeout' diff --git a/include/boost/redis/impl/exec_fsm.ipp b/include/boost/redis/impl/exec_fsm.ipp index 71de2f32..335299f5 100644 --- a/include/boost/redis/impl/exec_fsm.ipp +++ b/include/boost/redis/impl/exec_fsm.ipp @@ -9,6 +9,7 @@ #ifndef BOOST_REDIS_EXEC_FSM_IPP #define BOOST_REDIS_EXEC_FSM_IPP +#include #include #include #include @@ -20,20 +21,10 @@ namespace boost::redis::detail { -inline bool is_partial_or_terminal_cancel(asio::cancellation_type_t type) -{ - return !!(type & (asio::cancellation_type_t::partial | asio::cancellation_type_t::terminal)); -} - -inline bool is_total_cancel(asio::cancellation_type_t type) -{ - return !!(type & asio::cancellation_type_t::total); -} - exec_action exec_fsm::resume( bool connection_is_open, connection_state& st, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -80,8 +71,8 @@ exec_action exec_fsm::resume( // Total cancellation can only be handled if the request hasn't been sent yet. // Partial and terminal cancellation can always be served if ( - (is_total_cancel(cancel_state) && elem_->is_waiting()) || - is_partial_or_terminal_cancel(cancel_state)) { + (contains_total(cancel_state) && elem_->is_waiting()) || + contains_partial(cancel_state) || contains_terminal(cancel_state)) { st.mpx.cancel(elem_); elem_.reset(); // Deallocate memory before finalizing return exec_action{make_error_code(system::errc::operation_canceled)}; diff --git a/include/boost/redis/impl/exec_one_fsm.ipp b/include/boost/redis/impl/exec_one_fsm.ipp index c2e236cd..d222081b 100644 --- a/include/boost/redis/impl/exec_one_fsm.ipp +++ b/include/boost/redis/impl/exec_one_fsm.ipp @@ -17,7 +17,6 @@ #include #include -#include #include #include #include @@ -30,7 +29,7 @@ exec_one_action exec_one_fsm::resume( read_buffer& buffer, system::error_code ec, std::size_t bytes_transferred, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL diff --git a/include/boost/redis/impl/is_terminal_cancel.hpp b/include/boost/redis/impl/is_terminal_cancel.hpp index 308e3647..b997c4ca 100644 --- a/include/boost/redis/impl/is_terminal_cancel.hpp +++ b/include/boost/redis/impl/is_terminal_cancel.hpp @@ -9,15 +9,15 @@ #ifndef BOOST_REDIS_IS_TERMINAL_CANCEL_HPP #define BOOST_REDIS_IS_TERMINAL_CANCEL_HPP -#include +#include namespace boost::redis::detail { -constexpr bool is_terminal_cancel(asio::cancellation_type_t cancel_state) +constexpr bool is_terminal_cancel(cancellation_type cancel_state) { - return (cancel_state & asio::cancellation_type_t::terminal) != asio::cancellation_type_t::none; + return contains_terminal(cancel_state); } } // namespace boost::redis::detail -#endif \ No newline at end of file +#endif diff --git a/include/boost/redis/impl/reader_fsm.ipp b/include/boost/redis/impl/reader_fsm.ipp index b2815b82..e73e00b7 100644 --- a/include/boost/redis/impl/reader_fsm.ipp +++ b/include/boost/redis/impl/reader_fsm.ipp @@ -11,15 +11,13 @@ #include #include -#include - namespace boost::redis::detail { reader_fsm::action reader_fsm::resume( connection_state& st, std::size_t bytes_read, system::error_code ec, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL diff --git a/include/boost/redis/impl/receive_fsm.ipp b/include/boost/redis/impl/receive_fsm.ipp index b3a9f9d3..73a1f178 100644 --- a/include/boost/redis/impl/receive_fsm.ipp +++ b/include/boost/redis/impl/receive_fsm.ipp @@ -6,6 +6,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include @@ -16,13 +17,6 @@ namespace boost::redis::detail { -constexpr bool is_any_cancel(asio::cancellation_type_t type) -{ - return !!( - type & (asio::cancellation_type_t::terminal | asio::cancellation_type_t::partial | - asio::cancellation_type_t::total)); -} - // We use the receive2_cancelled flag rather than will_reconnect() to // avoid entanglement between async_run and async_receive2 cancellations. // If we had used will_reconnect(), async_receive2 would be cancelled @@ -30,7 +24,7 @@ constexpr bool is_any_cancel(asio::cancellation_type_t type) receive_action receive_fsm::resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -60,7 +54,7 @@ receive_action receive_fsm::resume( } // Check for cancellations - if (is_any_cancel(cancel_state) || st.receive2_cancelled) { + if (cancel_state != cancellation_type::none || st.receive2_cancelled) { st.receive2_running = false; return make_error_code(system::errc::operation_canceled); } diff --git a/include/boost/redis/impl/run_fsm.ipp b/include/boost/redis/impl/run_fsm.ipp index 692a6430..d8656ef5 100644 --- a/include/boost/redis/impl/run_fsm.ipp +++ b/include/boost/redis/impl/run_fsm.ipp @@ -19,7 +19,6 @@ #include #include -#include #include namespace boost::redis::detail { @@ -86,7 +85,7 @@ struct log_traits { run_action run_fsm::resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL diff --git a/include/boost/redis/impl/sentinel_resolve_fsm.ipp b/include/boost/redis/impl/sentinel_resolve_fsm.ipp index d8cfbb26..e6ac36d7 100644 --- a/include/boost/redis/impl/sentinel_resolve_fsm.ipp +++ b/include/boost/redis/impl/sentinel_resolve_fsm.ipp @@ -42,7 +42,7 @@ void log_sentinel_error(connection_state& st, std::size_t current_idx, const Arg sentinel_action sentinel_resolve_fsm::resume( connection_state& st, system::error_code ec, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL diff --git a/include/boost/redis/impl/writer_fsm.ipp b/include/boost/redis/impl/writer_fsm.ipp index 4d13f9b9..201967c4 100644 --- a/include/boost/redis/impl/writer_fsm.ipp +++ b/include/boost/redis/impl/writer_fsm.ipp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -57,7 +56,7 @@ writer_action writer_fsm::resume( connection_state& st, system::error_code ec, std::size_t bytes_written, - asio::cancellation_type_t cancel_state) + cancellation_type cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL From de7e4ebd001e5299b11d9947ae308d362b0360ec Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 13:43:53 +0200 Subject: [PATCH 053/115] Remove obsolete write --- include/boost/redis/detail/write.hpp | 54 ---------------------------- 1 file changed, 54 deletions(-) delete mode 100644 include/boost/redis/detail/write.hpp diff --git a/include/boost/redis/detail/write.hpp b/include/boost/redis/detail/write.hpp deleted file mode 100644 index 69b136f4..00000000 --- a/include/boost/redis/detail/write.hpp +++ /dev/null @@ -1,54 +0,0 @@ -/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_WRITE_HPP -#define BOOST_REDIS_WRITE_HPP - -#include - -#include - -namespace boost::redis::detail { - -/** @brief Writes a request synchronously. - * - * @param stream Stream to write the request to. - * @param req Request to write. - */ -template -auto write(SyncWriteStream& stream, request const& req) -{ - return asio::write(stream, asio::buffer(req.payload())); -} - -template -auto write(SyncWriteStream& stream, request const& req, system::error_code& ec) -{ - return asio::write(stream, asio::buffer(req.payload()), ec); -} - -/** @brief Writes a request asynchronously. - * - * @param stream Stream to write the request to. - * @param req Request to write. - * @param token Asio completion token. - */ -template < - class AsyncWriteStream, - class CompletionToken = - asio::default_completion_token_t > -auto async_write( - AsyncWriteStream& stream, - request const& req, - CompletionToken&& token = - asio::default_completion_token_t{}) -{ - return asio::async_write(stream, asio::buffer(req.payload()), token); -} - -} // namespace boost::redis::detail - -#endif // BOOST_REDIS_WRITE_HPP From e9e7d5df9b337df58099a38ee4a99c9726422fd7 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 13:49:11 +0200 Subject: [PATCH 054/115] Remove co_connection::cancel --- include/boost/redis/co_connection.hpp | 17 ----------- include/boost/redis/impl/co_connect_fsm.ipp | 1 - include/boost/redis/impl/co_connection.ipp | 33 ++------------------- 3 files changed, 3 insertions(+), 48 deletions(-) diff --git a/include/boost/redis/co_connection.hpp b/include/boost/redis/co_connection.hpp index bb728e09..c84e94b1 100644 --- a/include/boost/redis/co_connection.hpp +++ b/include/boost/redis/co_connection.hpp @@ -31,7 +31,6 @@ #include #include -#include #include #include #include @@ -107,7 +106,6 @@ class co_redis_stream { }; struct co_connection_impl { - capy::async_event run_cancelled_event_; co_redis_stream stream_; corosio::timer writer_timer_; // timer used for write timeouts corosio::timer writer_cv_; // set when there is new data to write @@ -119,8 +117,6 @@ struct co_connection_impl { co_connection_impl(capy::execution_context& ctx, corosio::tls_context&& ssl_ctx, logger&& lgr); - void cancel(); - capy::io_task<> exec(request const& req, any_adapter adapter); void set_receive_adapter(any_adapter adapter); @@ -407,19 +403,6 @@ class co_connection { return impl_->exec(req, std::move(adapter)); } - /** @brief Cancel operations. - * - * @li `operation::exec`: cancels operations started with - * `async_exec`. Affects only requests that haven't been written - * yet. - * @li `operation::run`: cancels the `async_run` operation. - * @li `operation::receive`: cancels any ongoing calls to `async_receive`. - * @li `operation::all`: cancels all operations listed above. - * - * @param op The operation to be cancelled. - */ - void cancel() { impl_->cancel(); } - /// Sets the response object of @ref async_receive2 operations. void set_receive_response(any_adapter resp) { impl_->set_receive_adapter(std::move(resp)); } diff --git a/include/boost/redis/impl/co_connect_fsm.ipp b/include/boost/redis/impl/co_connect_fsm.ipp index 7df61cd7..d26f6ff5 100644 --- a/include/boost/redis/impl/co_connect_fsm.ipp +++ b/include/boost/redis/impl/co_connect_fsm.ipp @@ -22,7 +22,6 @@ namespace boost::redis::detail { // Logging inline void format_tcp_endpoint(const corosio::endpoint& ep, std::string& to) { - // This formatting is inspired by Asio's endpoint operator<< if (ep.is_v6()) { to += '['; to += ep.v6_address().to_string(); diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index 5a6c378b..8849c340 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -121,21 +121,6 @@ co_connection_impl::co_connection_impl( writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); } -void co_connection_impl::cancel() -{ - // exec - st_.mpx.cancel_waiting(); - - // receive (TODO: do we really need this?) - st_.receive2_cancelled = true; - - // reconnect (TODO: do we really need this?) - st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); - - // run - run_cancelled_event_.set(); -} - capy::io_task<> co_connection_impl::exec(request const& req, any_adapter adapter) { // Setup @@ -348,7 +333,7 @@ inline capy::io_task reader(co_connection_impl& conn) } } -inline capy::io_task run(co_connection_impl& conn) +inline capy::io_task<> run(co_connection_impl& conn) { constexpr bool unix_sockets_supported = false; // TODO run_fsm fsm{unix_sockets_supported}; @@ -358,7 +343,7 @@ inline capy::io_task run(co_connection_impl& conn) auto act = fsm.resume(conn.st_, ec, token_to_cancel(co_await capy::this_coro::stop_token)); switch (act.type) { - case run_action_type::done: co_return {{}, act.ec}; + case run_action_type::done: co_return {act.ec}; case run_action_type::immediate: break; // no longer required case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve(conn)).ec; break; case run_action_type::connect: @@ -398,19 +383,7 @@ capy::io_task<> co_connection::run(config const& cfg) { impl_->st_.cfg = cfg; impl_->st_.mpx.set_config(cfg); - impl_->run_cancelled_event_.clear(); - - auto result = co_await capy::when_any(detail::run(*impl_), impl_->run_cancelled_event_.wait()); - - struct visitor { - // Either error or run finished 1st - std::error_code operator()(std::error_code val) const { return val; } - - // The event finishes 1st - std::error_code operator()(std::tuple<>) const { return capy::error::canceled; } - }; - - co_return std::visit(visitor{}, result); + return detail::run(*impl_); } capy::io_task<> co_connection::receive() { return detail::receive(*impl_); } From 3741cc6782e1e1d0921cb7436e6296797e195a5a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 13:57:07 +0200 Subject: [PATCH 055/115] Recover run_coroutine_test --- test/corosio_common.cpp | 36 ++++++++++++++++++++++++++++++++++++ test/corosio_common.hpp | 21 +++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 test/corosio_common.cpp create mode 100644 test/corosio_common.hpp diff --git a/test/corosio_common.cpp b/test/corosio_common.cpp new file mode 100644 index 00000000..f4c1389f --- /dev/null +++ b/test/corosio_common.cpp @@ -0,0 +1,36 @@ +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +#include + +void boost::redis::detail::run_coroutine_test(capy::task test, source_location loc) +{ + // Set a timeout to the tests, so they don't hang on error + bool finished = false; + auto wrapper_fn = [test = std::move(test), &finished]() mutable -> capy::task { + co_await std::move(test); + finished = true; + }; + + // Actually run the test + corosio::io_context ctx; + capy::run_async(ctx.get_executor())(wrapper_fn()); + ctx.run_for(test_timeout); + + // Check that it finished + if (!BOOST_TEST(finished)) + std::cerr << " Called from " << loc << std::endl; +} \ No newline at end of file diff --git a/test/corosio_common.hpp b/test/corosio_common.hpp new file mode 100644 index 00000000..5aa38209 --- /dev/null +++ b/test/corosio_common.hpp @@ -0,0 +1,21 @@ +// +// Copyright (c) 2026 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_REDIS_TEST_COROSIO_COMMON_HPP +#define BOOST_REDIS_TEST_COROSIO_COMMON_HPP + +#include +#include + +namespace boost::redis::detail { + +void run_coroutine_test(capy::task test, source_location loc = BOOST_CURRENT_LOCATION); + +} + +#endif From dcfd5a0236868526d3c4737a1fc8d57474494122 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 13:59:42 +0200 Subject: [PATCH 056/115] proper cmake --- test/CMakeLists.txt | 23 ++++++++++------------- test/test_co_check_health.cpp | 2 ++ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0a7ee76c..96bf96e3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -26,24 +26,21 @@ target_link_libraries(boost_redis_tests_proto PUBLIC boost_redis_proto) target_compile_definitions(boost_redis_tests_proto PUBLIC BOOST_ALLOW_DEPRECATED=1) # we need to still test deprecated fns add_library(boost_redis_tests_asio STATIC asio_common.cpp) -target_link_libraries(boost_redis_tests_asio PUBLIC boost_redis_asio) +target_link_libraries(boost_redis_tests_asio PUBLIC boost_redis_asio boost_redis_tests_proto) -# add_library(boost_redis_tests_corosio STATIC corosio_common.cpp) -# target_link_libraries(boost_redis_tests_corosio PRIVATE boost_redis_corosio) +add_library(boost_redis_tests_corosio STATIC corosio_common.cpp) +target_link_libraries(boost_redis_tests_corosio PUBLIC boost_redis_corosio boost_redis_tests_proto) - -macro(make_test TEST_NAME) +function(make_test TEST_NAME) set(EXE_NAME "boost_redis_${TEST_NAME}") add_executable(${EXE_NAME} ${TEST_NAME}.cpp) - target_link_libraries(${EXE_NAME} PRIVATE - boost_redis_src - boost_redis_tests_common - boost_redis_project_options - Boost::unit_test_framework - ) + if (ARGN) + target_link_libraries(${EXE_NAME} PRIVATE ${ARGN}) + endif() + target_compile_definitions(${EXE_NAME} PRIVATE BOOST_ALLOW_DEPRECATED=1) # we need to still test deprecated fns add_test(NAME ${EXE_NAME} COMMAND ${EXE_NAME}) -endmacro() +endfunction() # Unit tests make_test(test_low_level) @@ -79,7 +76,7 @@ make_test(test_conn_exec_error) make_test(test_run) make_test(test_conn_run_cancel) make_test(test_conn_check_health) -make_test(test_co_check_health) +make_test(test_co_check_health boost_redis_tests_corosio) make_test(test_conn_exec) make_test(test_conn_push) make_test(test_conn_push2) diff --git a/test/test_co_check_health.cpp b/test/test_co_check_health.cpp index 0c11e79a..d12fa43f 100644 --- a/test/test_co_check_health.cpp +++ b/test/test_co_check_health.cpp @@ -19,6 +19,7 @@ #include #include "common.hpp" +#include "corosio_common.hpp" #include #include @@ -26,6 +27,7 @@ namespace capy = boost::capy; using namespace boost::redis; +using detail::run_coroutine_test; using error_code = std::error_code; using namespace std::chrono_literals; From dc6d1bb80cccab09834b3b15ed300ba25b8d40f5 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 14:02:20 +0200 Subject: [PATCH 057/115] use boost::redis::test --- test/corosio_common.cpp | 2 +- test/corosio_common.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/corosio_common.cpp b/test/corosio_common.cpp index f4c1389f..fda980d3 100644 --- a/test/corosio_common.cpp +++ b/test/corosio_common.cpp @@ -16,7 +16,7 @@ #include -void boost::redis::detail::run_coroutine_test(capy::task test, source_location loc) +void boost::redis::test::run_coroutine_test(capy::task test, source_location loc) { // Set a timeout to the tests, so they don't hang on error bool finished = false; diff --git a/test/corosio_common.hpp b/test/corosio_common.hpp index 5aa38209..511a1138 100644 --- a/test/corosio_common.hpp +++ b/test/corosio_common.hpp @@ -12,7 +12,7 @@ #include #include -namespace boost::redis::detail { +namespace boost::redis::test { void run_coroutine_test(capy::task test, source_location loc = BOOST_CURRENT_LOCATION); From 49f5108b45f3b95e819b28d80d560ee59a745139 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 14:11:11 +0200 Subject: [PATCH 058/115] use error conditions --- test/common.hpp | 9 ++++++++- test/test_co_check_health.cpp | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/test/common.hpp b/test/common.hpp index 6ebefa69..93a975ff 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -3,10 +3,11 @@ #include #include +#include #include +#include #include -#include #include #include @@ -25,3 +26,9 @@ std::string_view find_client_info(std::string_view client_info, std::string_view boost::redis::logger make_string_logger(std::string& to); std::string safe_getenv(const char* name, const char* default_value); + +// Reduce verbosity in tests +constexpr boost::system::error_condition canceled_condition() +{ + return boost::system::errc::operation_canceled; +} diff --git a/test/test_co_check_health.cpp b/test/test_co_check_health.cpp index d12fa43f..c7701787 100644 --- a/test/test_co_check_health.cpp +++ b/test/test_co_check_health.cpp @@ -27,7 +27,7 @@ namespace capy = boost::capy; using namespace boost::redis; -using detail::run_coroutine_test; +using namespace boost::redis::test; using error_code = std::error_code; using namespace std::chrono_literals; @@ -54,7 +54,7 @@ capy::task test_reconnection() // as unresponsive and triggers a reconnection (it's configured to be cancelled // on connection lost). auto [ec1] = co_await conn.exec(req1, ignore); - BOOST_TEST_EQ(ec1, error_code(capy::error::canceled)); + BOOST_TEST_EQ(ec1, canceled_condition()); // Execute the second request. This one will succeed after reconnection auto [ec2] = co_await conn.exec(req2, ignore); @@ -69,7 +69,7 @@ capy::task test_reconnection() cfg.health_check_interval = 500ms; auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; @@ -88,7 +88,7 @@ capy::task test_error_code() req.push("BLPOP", "any", 0); auto [ec] = co_await conn.exec(req, ignore); - BOOST_TEST_EQ(ec, capy::error::canceled); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; @@ -100,7 +100,7 @@ capy::task test_error_code() cfg.reconnect_wait_interval = 0s; auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, boost::redis::error::pong_timeout); + BOOST_TEST_EQ(ec, error::pong_timeout); co_return {}; }; @@ -134,7 +134,7 @@ capy::task test_disabled() auto cfg = make_test_config(); cfg.health_check_interval = 0s; auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; @@ -165,13 +165,13 @@ capy::task test_flexible() auto run1_fn = [&]() -> capy::io_task<> { auto [ec] = co_await conn1.run(cfg); - BOOST_TEST_EQ(ec, capy::error::canceled); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; auto run2_fn = [&]() -> capy::io_task<> { auto [ec] = co_await conn2.run(cfg); - BOOST_TEST_EQ(ec, capy::error::canceled); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; @@ -200,13 +200,13 @@ capy::task test_flexible() while (true) { // Publish a message auto [ec] = co_await conn2.exec(publish_req, ignore); - if (ec == capy::error::canceled) + if (ec == canceled_condition()) co_return {}; BOOST_TEST_EQ(ec, error_code()); // Wait for some time and publish again auto [ec2] = co_await capy::delay(100ms); - if (ec2 == capy::error::canceled) + if (ec2 == canceled_condition()) co_return {}; BOOST_TEST_EQ(ec2, error_code()); } From 3d05217bf94df1244a69b8ef8ed26d6e356ab5d2 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 21 Apr 2026 14:48:11 +0200 Subject: [PATCH 059/115] Proper condition logging --- test/common.cpp | 12 ++++++++++++ test/common.hpp | 31 +++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/test/common.cpp b/test/common.cpp index 1023fc16..aa9f4d17 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -1,11 +1,13 @@ #include +#include #include #include "common.hpp" #include #include +#include #include using namespace std::chrono_literals; @@ -58,3 +60,13 @@ boost::redis::logger make_string_logger(std::string& to) to += '\n'; }}; } + +std::ostream& operator<<(std::ostream& os, const condition_wrapper& val) +{ + return os << val.value.category().name() << ':' << val.value.value() << " (" + << val.value.message() << ')'; +} + +// TODO: this should use std::errc::operation_canceled +// https://github.com/cppalliance/capy/issues/267 +condition_wrapper canceled_condition() { return {boost::capy::cond::canceled}; } \ No newline at end of file diff --git a/test/common.hpp b/test/common.hpp index 93a975ff..cfa64b2b 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -8,8 +8,10 @@ #include #include +#include #include #include +#include // The timeout for tests involving communication to a real server. // Some tests use a longer timeout by multiplying this value by some @@ -27,8 +29,29 @@ boost::redis::logger make_string_logger(std::string& to); std::string safe_getenv(const char* name, const char* default_value); +// std::error_condition doesn't implement operator<< and is difficult to use in tests +struct condition_wrapper { + std::error_condition value; + + friend bool operator==(const condition_wrapper& lhs, const condition_wrapper& rhs) noexcept + { + return lhs.value == rhs.value; + } + + template + friend bool operator==(const T& lhs, const condition_wrapper& rhs) noexcept + { + return lhs == rhs.value; + } + + template + friend bool operator==(const condition_wrapper& lhs, const T& rhs) noexcept + { + return lhs.value == rhs; + } + + friend std::ostream& operator<<(std::ostream& os, const condition_wrapper& val); +}; + // Reduce verbosity in tests -constexpr boost::system::error_condition canceled_condition() -{ - return boost::system::errc::operation_canceled; -} +condition_wrapper canceled_condition(); From 665262ce61c2fd41bcee2011656e93d1140d9599 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 23 Apr 2026 12:17:57 +0200 Subject: [PATCH 060/115] Use pimpl --- include/boost/redis/co_connection.hpp | 115 +++------------------ include/boost/redis/impl/co_connection.ipp | 101 ++++++++++++++++++ 2 files changed, 113 insertions(+), 103 deletions(-) diff --git a/include/boost/redis/co_connection.hpp b/include/boost/redis/co_connection.hpp index c84e94b1..abc60fa4 100644 --- a/include/boost/redis/co_connection.hpp +++ b/include/boost/redis/co_connection.hpp @@ -9,118 +9,24 @@ #ifndef BOOST_REDIS_CO_CONNECTION_HPP #define BOOST_REDIS_CO_CONNECTION_HPP -#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include #include -#include #include #include -#include -#include -#include -#include -#include #include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include #include -#include -#include -#include #include namespace boost::redis { namespace detail { -class co_redis_stream { - // TODO: UNIX sockets - corosio::tcp_socket socket_; - corosio::openssl_stream stream_; // TODO: make this configurable - corosio::resolver resolv_; - co_redis_stream_state st_; - -public: - explicit co_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) - : socket_(ctx) - , stream_(&socket_, std::move(tls_ctx)) - , resolv_(ctx) - { } - - // I/O - capy::io_task<> connect(const connect_params& params, buffered_logger& l); - - template - capy::io_task write_some(const BuffType& buffers) - { - switch (st_.type) { - case transport_type::tcp: co_return co_await socket_.write_some(buffers); - case transport_type::tcp_tls: co_return co_await stream_.write_some(buffers); - case transport_type::unix_socket: - default: BOOST_ASSERT(false); co_return {}; - } - } - - template - capy::io_task read_some(const BuffType& buffers) - { - switch (st_.type) { - case transport_type::tcp: co_return co_await socket_.read_some(buffers); - case transport_type::tcp_tls: co_return co_await stream_.read_some(buffers); - case transport_type::unix_socket: - default: BOOST_ASSERT(false); co_return {}; - } - } -}; - -struct co_connection_impl { - co_redis_stream stream_; - corosio::timer writer_timer_; // timer used for write timeouts - corosio::timer writer_cv_; // set when there is new data to write - corosio::timer reader_timer_; // timer used for read timeouts - corosio::timer reconnect_timer_; // to wait the reconnection period - corosio::timer ping_timer_; // to wait between pings - flow_controller controller_; - connection_state st_; - - co_connection_impl(capy::execution_context& ctx, corosio::tls_context&& ssl_ctx, logger&& lgr); - - capy::io_task<> exec(request const& req, any_adapter adapter); - - void set_receive_adapter(any_adapter adapter); -}; +struct co_connection_impl; } // namespace detail @@ -168,6 +74,12 @@ class co_connection { : co_connection(ex.context(), corosio::tls_context{}, std::move(lgr)) { } + co_connection(co_connection&&) noexcept; + co_connection& operator=(co_connection&&) noexcept; + co_connection(const co_connection&) = delete; + co_connection& operator=(const co_connection&) = delete; + ~co_connection(); + /** @brief Starts the underlying connection operations. * * This function establishes a connection to the Redis server and keeps @@ -273,7 +185,7 @@ class co_connection { * @code * asio::awaitable receiver() * { - * // Do NOT do this!!! The receive buffer might get full while + * // Do NOT do this!!! The receive buffer might get full while * // async_exec runs, which will block all read operations until async_receive2 * // is called. The two operations end up waiting each other, making the connection unresponsive. * // If you need to do this, use two connections, instead. @@ -398,16 +310,13 @@ class co_connection { * @param adapter An adapter object referencing a response to place data into. * @param token Completion token. */ - capy::io_task<> exec(request const& req, any_adapter adapter) - { - return impl_->exec(req, std::move(adapter)); - } + capy::io_task<> exec(request const& req, any_adapter adapter); /// Sets the response object of @ref async_receive2 operations. - void set_receive_response(any_adapter resp) { impl_->set_receive_adapter(std::move(resp)); } + void set_receive_response(any_adapter resp); /// Returns connection usage information. - usage get_usage() const noexcept { return impl_->st_.mpx.get_usage(); } + usage get_usage() const noexcept; private: std::unique_ptr impl_; @@ -415,4 +324,4 @@ class co_connection { } // namespace boost::redis -#endif // BOOST_REDIS_CONNECTION_HPP +#endif // BOOST_REDIS_CO_CONNECTION_HPP diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index 8849c340..541466f4 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -8,17 +8,44 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include #include #include +#include +#include +#include +#include #include #include +#include +#include #include #include +#include +#include +#include +#include +#include +#include #include #include +#include +#include #include #include #include @@ -27,6 +54,64 @@ namespace boost::redis { namespace detail { +class co_redis_stream { + // TODO: UNIX sockets + corosio::tcp_socket socket_; + corosio::openssl_stream stream_; // TODO: make this configurable + corosio::resolver resolv_; + co_redis_stream_state st_; + +public: + explicit co_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) + : socket_(ctx) + , stream_(&socket_, std::move(tls_ctx)) + , resolv_(ctx) + { } + + // I/O + capy::io_task<> connect(const connect_params& params, buffered_logger& l); + + template + capy::io_task write_some(const BuffType& buffers) + { + switch (st_.type) { + case transport_type::tcp: co_return co_await socket_.write_some(buffers); + case transport_type::tcp_tls: co_return co_await stream_.write_some(buffers); + case transport_type::unix_socket: + default: BOOST_ASSERT(false); co_return {}; + } + } + + template + capy::io_task read_some(const BuffType& buffers) + { + switch (st_.type) { + case transport_type::tcp: co_return co_await socket_.read_some(buffers); + case transport_type::tcp_tls: co_return co_await stream_.read_some(buffers); + case transport_type::unix_socket: + default: BOOST_ASSERT(false); co_return {}; + } + } +}; + +struct co_connection_impl { + capy::async_event run_cancelled_event_; + co_redis_stream stream_; + corosio::timer writer_timer_; // timer used for write timeouts + corosio::timer writer_cv_; // set when there is new data to write + corosio::timer reader_timer_; // timer used for read timeouts + corosio::timer reconnect_timer_; // to wait the reconnection period + corosio::timer ping_timer_; // to wait between pings + flow_controller controller_; + connection_state st_; + + co_connection_impl(capy::execution_context& ctx, corosio::tls_context&& ssl_ctx, logger&& lgr); + + capy::io_task<> exec(request const& req, any_adapter adapter); + + void set_receive_adapter(any_adapter adapter); +}; + // Given a timeout value, compute the expiry time. A zero timeout is considered to mean "no timeout" inline std::chrono::steady_clock::time_point compute_expiry( std::chrono::steady_clock::duration timeout) @@ -379,6 +464,10 @@ co_connection::co_connection(capy::execution_context& ctx, logger lgr) : co_connection(ctx, {}, std::move(lgr)) { } +co_connection::co_connection(co_connection&&) noexcept = default; +co_connection& co_connection::operator=(co_connection&&) noexcept = default; +co_connection::~co_connection() = default; + capy::io_task<> co_connection::run(config const& cfg) { impl_->st_.cfg = cfg; @@ -388,4 +477,16 @@ capy::io_task<> co_connection::run(config const& cfg) capy::io_task<> co_connection::receive() { return detail::receive(*impl_); } +capy::io_task<> co_connection::exec(request const& req, any_adapter adapter) +{ + return impl_->exec(req, std::move(adapter)); +} + +void co_connection::set_receive_response(any_adapter resp) +{ + impl_->set_receive_adapter(std::move(resp)); +} + +usage co_connection::get_usage() const noexcept { return impl_->st_.mpx.get_usage(); } + } // namespace boost::redis From 6589d3c6cdca08c49bbcc3ff0cdb6949e268401e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 23 Apr 2026 12:28:00 +0200 Subject: [PATCH 061/115] cleanup co_connection --- include/boost/redis/impl/co_connection.ipp | 637 ++++++++++----------- 1 file changed, 310 insertions(+), 327 deletions(-) diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index 541466f4..530ab09b 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -47,13 +47,37 @@ #include #include #include -#include #include #include namespace boost::redis { namespace detail { +// Given a timeout value, compute the expiry time. A zero timeout is considered to mean "no timeout" +inline std::chrono::steady_clock::time_point compute_expiry( + std::chrono::steady_clock::duration timeout) +{ + return timeout.count() == 0 ? (std::chrono::steady_clock::time_point::max)() + : std::chrono::steady_clock::now() + timeout; +} + +inline cancellation_type token_to_cancel(std::stop_token tok) +{ + return tok.stop_requested() ? cancellation_type::terminal : cancellation_type::none; +} + +// Run an operation with a timeout, with a zero timeout meaning 'no timeout' +template +capy::task> maybe_timeout( + Aw aw, + std::chrono::steady_clock::duration timeout) +{ + if (timeout.count() == 0) + co_return co_await std::move(aw); + else + co_return co_await capy::timeout(std::move(aw), timeout); +} + class co_redis_stream { // TODO: UNIX sockets corosio::tcp_socket socket_; @@ -69,7 +93,57 @@ public: { } // I/O - capy::io_task<> connect(const connect_params& params, buffered_logger& l); + capy::io_task<> connect(const connect_params& params, buffered_logger& l) + { + co_connect_fsm fsm{l}; + system::error_code ec; + corosio::resolver_results endpoints; + + auto act = fsm.resume(ec, st_); + + while (true) { + switch (act.type) { + case co_connect_action_type::unix_socket_close: + BOOST_ASSERT(false); + co_return {std::make_error_code(std::errc::operation_not_supported)}; + case co_connect_action_type::unix_socket_connect: + BOOST_ASSERT(false); + co_return {std::make_error_code(std::errc::operation_not_supported)}; + case co_connect_action_type::tcp_resolve: + { + auto result = co_await capy::timeout( + resolv_.resolve(params.addr.tcp_address().host, params.addr.tcp_address().port), + params.resolve_timeout); + ec = result.ec; + endpoints = std::move(std::get<0>(result.values)); + act = fsm.resume(ec, endpoints, st_); + break; + } + case co_connect_action_type::ssl_stream_reset: + stream_.reset(); + act = fsm.resume(ec, st_); + break; + case co_connect_action_type::ssl_handshake: + ec = (co_await capy::timeout( + stream_.handshake(corosio::tls_stream::handshake_type::client), + params.ssl_handshake_timeout)) + .ec; + act = fsm.resume(ec, st_); + break; + case co_connect_action_type::done: co_return {act.ec}; + case co_connect_action_type::tcp_connect: + { + auto result = co_await capy::timeout( + corosio::connect(socket_, std::move(endpoints)), + params.connect_timeout); + ec = result.ec; + act = fsm.resume(ec, result.get<1>(), st_); + break; + } + default: BOOST_ASSERT(false); + } + } + } template capy::io_task write_some(const BuffType& buffers) @@ -105,354 +179,268 @@ struct co_connection_impl { flow_controller controller_; connection_state st_; - co_connection_impl(capy::execution_context& ctx, corosio::tls_context&& ssl_ctx, logger&& lgr); - - capy::io_task<> exec(request const& req, any_adapter adapter); - - void set_receive_adapter(any_adapter adapter); -}; - -// Given a timeout value, compute the expiry time. A zero timeout is considered to mean "no timeout" -inline std::chrono::steady_clock::time_point compute_expiry( - std::chrono::steady_clock::duration timeout) -{ - return timeout.count() == 0 ? (std::chrono::steady_clock::time_point::max)() - : std::chrono::steady_clock::now() + timeout; -} - -inline cancellation_type token_to_cancel(std::stop_token tok) -{ - return tok.stop_requested() ? cancellation_type::terminal : cancellation_type::none; -} - -// Run an operation with a timeout, with a zero timeout meaning 'no timeout' -template -capy::task> maybe_timeout( - Aw aw, - std::chrono::steady_clock::duration timeout) -{ - if (timeout.count() == 0) - co_return co_await std::move(aw); - else - co_return co_await capy::timeout(std::move(aw), timeout); -} - -capy::io_task<> co_redis_stream::connect(const connect_params& params, buffered_logger& l) -{ - co_connect_fsm fsm{l}; - system::error_code ec; - corosio::resolver_results endpoints; - - auto act = fsm.resume(ec, st_); - - while (true) { - switch (act.type) { - case co_connect_action_type::unix_socket_close: - BOOST_ASSERT(false); - co_return {std::make_error_code(std::errc::operation_not_supported)}; - case co_connect_action_type::unix_socket_connect: - BOOST_ASSERT(false); - co_return {std::make_error_code(std::errc::operation_not_supported)}; - case co_connect_action_type::tcp_resolve: - { - auto result = co_await capy::timeout( - resolv_.resolve(params.addr.tcp_address().host, params.addr.tcp_address().port), - params.resolve_timeout); - ec = result.ec; - endpoints = std::move(std::get<0>(result.values)); - act = fsm.resume(ec, endpoints, st_); - break; - } - case co_connect_action_type::ssl_stream_reset: - stream_.reset(); - act = fsm.resume(ec, st_); - break; - case co_connect_action_type::ssl_handshake: - ec = (co_await capy::timeout( - stream_.handshake(corosio::tls_stream::handshake_type::client), - params.ssl_handshake_timeout)) - .ec; - act = fsm.resume(ec, st_); - break; - case co_connect_action_type::done: co_return {act.ec}; - case co_connect_action_type::tcp_connect: - { - auto result = co_await capy::timeout( - corosio::connect(socket_, std::move(endpoints)), - params.connect_timeout); - ec = result.ec; - act = fsm.resume(ec, result.get<1>(), st_); - break; - } - default: BOOST_ASSERT(false); - } + co_connection_impl(capy::execution_context& ctx, corosio::tls_context&& ssl_ctx, logger&& lgr) + : stream_{ctx, std::move(ssl_ctx)} + , writer_timer_{ctx} + , writer_cv_{ctx} + , reader_timer_{ctx} + , reconnect_timer_{ctx} + , ping_timer_{ctx} + , controller_{1024u * 1024u * 16u} // 16MB, TODO: make it configurable + , st_{{std::move(lgr)}} + { + set_receive_adapter(any_adapter{ignore}); + writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); } -} -co_connection_impl::co_connection_impl( - capy::execution_context& ctx, - corosio::tls_context&& ssl_ctx, - logger&& lgr) -: stream_{ctx, std::move(ssl_ctx)} -, writer_timer_{ctx} -, writer_cv_{ctx} -, reader_timer_{ctx} -, reconnect_timer_{ctx} -, ping_timer_{ctx} -, controller_{1024u * 1024u * 16u} // 16MB, TODO: make it configurable -, st_{{std::move(lgr)}} -{ - set_receive_adapter(any_adapter{ignore}); - writer_cv_.expires_at((std::chrono::steady_clock::time_point::max)()); -} + void set_receive_adapter(any_adapter adapter) + { + st_.mpx.set_receive_adapter(std::move(adapter)); + } -capy::io_task<> co_connection_impl::exec(request const& req, any_adapter adapter) -{ - // Setup - capy::async_event request_done; - auto elem = make_elem(req, std::move(adapter)); - elem->set_done_callback([&request_done]() { - request_done.set(); - }); - exec_fsm fsm{elem}; - - // Invoke the FSM - while (true) { - // Invoke the state machine - auto act = fsm.resume(true, st_, token_to_cancel(co_await capy::this_coro::stop_token)); - - // Do what the FSM said - switch (act.type()) { - case exec_action_type::setup_cancellation: break; // ignored, not required by capy - case exec_action_type::immediate: break; // ignored, not required by capy - case exec_action_type::notify_writer: writer_cv_.cancel(); break; - case exec_action_type::wait_for_response: - { - auto [ec] = co_await request_done.wait(); - ignore_unused(ec); // TODO: we should likely use this - break; + capy::io_task<> exec(request const& req, any_adapter adapter) + { + // Setup + capy::async_event request_done; + auto elem = make_elem(req, std::move(adapter)); + elem->set_done_callback([&request_done]() { + request_done.set(); + }); + exec_fsm fsm{elem}; + + // Invoke the FSM + while (true) { + // Invoke the state machine + auto act = fsm.resume(true, st_, token_to_cancel(co_await capy::this_coro::stop_token)); + + // Do what the FSM said + switch (act.type()) { + case exec_action_type::setup_cancellation: break; // ignored, not required by capy + case exec_action_type::immediate: break; // ignored, not required by capy + case exec_action_type::notify_writer: writer_cv_.cancel(); break; + case exec_action_type::wait_for_response: + { + auto [ec] = co_await request_done.wait(); + ignore_unused(ec); // TODO: we should likely use this + break; + } + case exec_action_type::done: co_return {act.error()}; } - case exec_action_type::done: co_return {act.error()}; } } -} - -void co_connection_impl::set_receive_adapter(any_adapter adapter) -{ - st_.mpx.set_receive_adapter(std::move(adapter)); -} -inline capy::io_task<> receive(co_connection_impl& conn) -{ - // Setup - receive_fsm fsm; - system::error_code ec; - - while (true) { - receive_action act = fsm.resume( - conn.st_, - ec, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type) { - case receive_action::action_type::setup_cancellation: break; // not required here - case receive_action::action_type::wait: - { - auto [controller_ec] = co_await conn.controller_.take(); - ec = controller_ec; - break; + capy::io_task<> receive() + { + // Setup + receive_fsm fsm; + system::error_code ec; + + while (true) { + receive_action act = fsm.resume( + st_, + ec, + token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.type) { + case receive_action::action_type::setup_cancellation: break; // not required here + case receive_action::action_type::wait: + { + auto [controller_ec] = co_await controller_.take(); + ec = controller_ec; + break; + } + case receive_action::action_type::drain_channel: break; // not required + case receive_action::action_type::immediate: break; // not required + case receive_action::action_type::done: co_return {act.ec}; } - case receive_action::action_type::drain_channel: break; // not required - case receive_action::action_type::immediate: break; // not required - case receive_action::action_type::done: co_return {act.ec}; } } -} -inline capy::io_task<> async_exec_one( - co_connection_impl& conn, - const request& req, - any_adapter resp) -{ - exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; - system::error_code ec; - std::size_t bytes = 0u; - - while (true) { - exec_one_action act = fsm.resume( - conn.st_.mpx.get_read_buffer(), - ec, - bytes, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type) { - case exec_one_action_type::done: co_return {ec}; - case exec_one_action_type::write: - { - auto [write_ec, write_bytes] = co_await capy::write( - conn.stream_, - capy::make_buffer(req.payload())); - ec = write_ec; - bytes = write_bytes; - break; - } - case exec_one_action_type::read_some: - { - // https://github.com/cppalliance/capy/issues/147 - auto buff = conn.st_.mpx.get_read_buffer().get_prepared(); - auto [read_ec, read_bytes] = co_await conn.stream_.read_some( - capy::mutable_buffer(buff.data(), buff.size())); - ec = read_ec; - bytes = read_bytes; - break; + capy::io_task<> exec_one(const request& req, any_adapter resp) + { + exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; + system::error_code ec; + std::size_t bytes = 0u; + + while (true) { + exec_one_action act = fsm.resume( + st_.mpx.get_read_buffer(), + ec, + bytes, + token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.type) { + case exec_one_action_type::done: co_return {ec}; + case exec_one_action_type::write: + { + auto [write_ec, write_bytes] = co_await capy::write( + stream_, + capy::make_buffer(req.payload())); + ec = write_ec; + bytes = write_bytes; + break; + } + case exec_one_action_type::read_some: + { + // https://github.com/cppalliance/capy/issues/147 + auto buff = st_.mpx.get_read_buffer().get_prepared(); + auto [read_ec, read_bytes] = co_await stream_.read_some( + capy::mutable_buffer(buff.data(), buff.size())); + ec = read_ec; + bytes = read_bytes; + break; + } } } } -} -inline capy::io_task<> sentinel_resolve(co_connection_impl& conn) -{ - // Setup - sentinel_resolve_fsm fsm; - system::error_code ec; - - while (true) { - sentinel_action act = fsm.resume( - conn.st_, - ec, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.get_type()) { - case sentinel_action::type::done: co_return {act.error()}; - case sentinel_action::type::connect: - { - auto [connect_ec] = co_await conn.stream_.connect( - make_sentinel_connect_params(conn.st_.cfg, act.connect_addr()), - conn.st_.logger); - ec = connect_ec; - break; - } - case sentinel_action::type::request: - { - auto [request_ec] = co_await capy::timeout( - async_exec_one(conn, conn.st_.cfg.sentinel.setup, make_sentinel_adapter(conn.st_)), - conn.st_.cfg.sentinel.request_timeout); - ec = request_ec; - break; + capy::io_task<> sentinel_resolve() + { + // Setup + sentinel_resolve_fsm fsm; + system::error_code ec; + + while (true) { + sentinel_action act = fsm.resume( + st_, + ec, + token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.get_type()) { + case sentinel_action::type::done: co_return {act.error()}; + case sentinel_action::type::connect: + { + auto [connect_ec] = co_await stream_.connect( + make_sentinel_connect_params(st_.cfg, act.connect_addr()), + st_.logger); + ec = connect_ec; + break; + } + case sentinel_action::type::request: + { + auto [request_ec] = co_await capy::timeout( + exec_one(st_.cfg.sentinel.setup, make_sentinel_adapter(st_)), + st_.cfg.sentinel.request_timeout); + ec = request_ec; + break; + } } } } -} -// This signature is required because capy::when_any is equivalent to wait_for_one_success -inline capy::io_task writer(co_connection_impl& conn) -{ - // Setup. - writer_fsm fsm{capy::cond::timeout}; - system::error_code ec; - std::size_t bytes_written = 0u; - - while (true) { - writer_action act = fsm.resume( - conn.st_, - ec, - bytes_written, - token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type()) { - case writer_action_type::done: co_return {{}, act.error()}; - case writer_action_type::write_some: - { - auto [write_ec, write_bytes] = co_await maybe_timeout( - conn.stream_.write_some(capy::make_buffer(conn.st_.mpx.get_write_buffer())), - act.timeout()); - ec = write_ec; - bytes_written = write_bytes; - break; - } - case writer_action_type::wait: - { - conn.writer_cv_.expires_at(compute_expiry(act.timeout())); - auto [wait_ec] = co_await conn.writer_cv_.wait(); - ec = wait_ec; - bytes_written = 0u; - break; + // This signature is required because capy::when_any is equivalent to wait_for_one_success + capy::io_task writer() + { + // Setup. + writer_fsm fsm{capy::cond::timeout}; + system::error_code ec; + std::size_t bytes_written = 0u; + + while (true) { + writer_action act = fsm.resume( + st_, + ec, + bytes_written, + token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.type()) { + case writer_action_type::done: co_return {{}, act.error()}; + case writer_action_type::write_some: + { + auto [write_ec, write_bytes] = co_await maybe_timeout( + stream_.write_some(capy::make_buffer(st_.mpx.get_write_buffer())), + act.timeout()); + ec = write_ec; + bytes_written = write_bytes; + break; + } + case writer_action_type::wait: + { + writer_cv_.expires_at(compute_expiry(act.timeout())); + auto [wait_ec] = co_await writer_cv_.wait(); + ec = wait_ec; + bytes_written = 0u; + break; + } } } } -} -inline capy::io_task reader(co_connection_impl& conn) -{ - reader_fsm fsm{capy::cond::timeout}; - std::size_t n = 0u; - system::error_code ec; - - for (;;) { - auto act = fsm.resume(conn.st_, n, ec, token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.get_type()) { - case reader_fsm::action::type::read_some: - { - // https://github.com/cppalliance/capy/issues/147 - auto buff = conn.st_.mpx.get_prepared_read_buffer(); - auto [read_ec, read_bytes] = co_await maybe_timeout( - conn.stream_.read_some(capy::mutable_buffer(buff.data(), buff.size())), - act.timeout()); - ec = read_ec; - n = read_bytes; - break; - } - case reader_fsm::action::type::notify_push_receiver: - { - // TODO: re-work this - auto [notify_ec] = co_await conn.controller_.wait_for_space(); - if (notify_ec) - ec = notify_ec; - else - conn.controller_.put(act.push_size()); - break; + capy::io_task reader() + { + reader_fsm fsm{capy::cond::timeout}; + std::size_t n = 0u; + system::error_code ec; + + for (;;) { + auto act = fsm.resume(st_, n, ec, token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.get_type()) { + case reader_fsm::action::type::read_some: + { + // https://github.com/cppalliance/capy/issues/147 + auto buff = st_.mpx.get_prepared_read_buffer(); + auto [read_ec, read_bytes] = co_await maybe_timeout( + stream_.read_some(capy::mutable_buffer(buff.data(), buff.size())), + act.timeout()); + ec = read_ec; + n = read_bytes; + break; + } + case reader_fsm::action::type::notify_push_receiver: + { + // TODO: re-work this + auto [notify_ec] = co_await controller_.wait_for_space(); + if (notify_ec) + ec = notify_ec; + else + controller_.put(act.push_size()); + break; + } + case reader_fsm::action::type::done: co_return {{}, act.error()}; } - case reader_fsm::action::type::done: co_return {{}, act.error()}; } } -} -inline capy::io_task<> run(co_connection_impl& conn) -{ - constexpr bool unix_sockets_supported = false; // TODO - run_fsm fsm{unix_sockets_supported}; - system::error_code ec; - - while (true) { - auto act = fsm.resume(conn.st_, ec, token_to_cancel(co_await capy::this_coro::stop_token)); - - switch (act.type) { - case run_action_type::done: co_return {act.ec}; - case run_action_type::immediate: break; // no longer required - case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve(conn)).ec; break; - case run_action_type::connect: - ec = (co_await conn.stream_.connect(make_run_connect_params(conn.st_), conn.st_.logger)) - .ec; - break; - case run_action_type::parallel_group: - { - auto result = co_await capy::when_any(reader(conn), writer(conn)); - ec = std::visit( - [](std::error_code value) { - return value; - }, - result); - break; + capy::io_task<> run(const config& cfg) + { + constexpr bool unix_sockets_supported = false; // TODO + run_fsm fsm{unix_sockets_supported}; + system::error_code ec; + + // Setup + st_.cfg = cfg; + st_.mpx.set_config(cfg); + + while (true) { + auto act = fsm.resume(st_, ec, token_to_cancel(co_await capy::this_coro::stop_token)); + + switch (act.type) { + case run_action_type::done: co_return {act.ec}; + case run_action_type::immediate: break; // no longer required + case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve()).ec; break; + case run_action_type::connect: + ec = (co_await stream_.connect(make_run_connect_params(st_), st_.logger)).ec; + break; + case run_action_type::parallel_group: + { + auto result = co_await capy::when_any(reader(), writer()); + ec = std::visit( + [](std::error_code value) { + return value; + }, + result); + break; + } + case run_action_type::cancel_receive: break; // no longer required + case run_action_type::wait_for_reconnection: + reconnect_timer_.expires_after(st_.cfg.reconnect_wait_interval); + ec = (co_await reconnect_timer_.wait()).ec; + break; } - case run_action_type::cancel_receive: break; // no longer required - case run_action_type::wait_for_reconnection: - conn.reconnect_timer_.expires_after(conn.st_.cfg.reconnect_wait_interval); - ec = (co_await conn.reconnect_timer_.wait()).ec; - break; } } -} +}; } // namespace detail @@ -468,14 +456,9 @@ co_connection::co_connection(co_connection&&) noexcept = default; co_connection& co_connection::operator=(co_connection&&) noexcept = default; co_connection::~co_connection() = default; -capy::io_task<> co_connection::run(config const& cfg) -{ - impl_->st_.cfg = cfg; - impl_->st_.mpx.set_config(cfg); - return detail::run(*impl_); -} +capy::io_task<> co_connection::run(config const& cfg) { return impl_->run(cfg); } -capy::io_task<> co_connection::receive() { return detail::receive(*impl_); } +capy::io_task<> co_connection::receive() { return impl_->receive(); } capy::io_task<> co_connection::exec(request const& req, any_adapter adapter) { From 1c7d8527f658c9cba5f086e28ac49408013049e0 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 15:17:41 +0200 Subject: [PATCH 062/115] UNIX support --- include/boost/redis/detail/co_connect_fsm.hpp | 21 +-- include/boost/redis/impl/co_connect_fsm.ipp | 37 ++--- include/boost/redis/impl/co_connection.ipp | 126 ++++++++++++------ 3 files changed, 103 insertions(+), 81 deletions(-) diff --git a/include/boost/redis/detail/co_connect_fsm.hpp b/include/boost/redis/detail/co_connect_fsm.hpp index f85cad4d..ee74dada 100644 --- a/include/boost/redis/detail/co_connect_fsm.hpp +++ b/include/boost/redis/detail/co_connect_fsm.hpp @@ -23,19 +23,12 @@ namespace boost::redis::detail { struct buffered_logger; -struct co_redis_stream_state { - transport_type type{transport_type::tcp}; - bool ssl_stream_used{false}; -}; - // What should we do next? enum class co_connect_action_type { - unix_socket_close, // Close the UNIX socket, to discard state unix_socket_connect, // Connect to the UNIX socket tcp_resolve, // Name resolution tcp_connect, // TCP connect - ssl_stream_reset, // Re-create the SSL stream, to discard state ssl_handshake, // SSL handshake done, // Complete the async op }; @@ -57,23 +50,21 @@ struct co_connect_action { class co_connect_fsm { int resume_point_{0}; buffered_logger* lgr_{nullptr}; + transport_type type_; public: - co_connect_fsm(buffered_logger& lgr) noexcept + co_connect_fsm(buffered_logger& lgr, transport_type type) noexcept : lgr_(&lgr) + , type_(type) { } co_connect_action resume( system::error_code ec, - std::span resolver_results, - co_redis_stream_state& st); + std::span resolver_results); - co_connect_action resume( - system::error_code ec, - const corosio::endpoint& selected_endpoint, - co_redis_stream_state& st); + co_connect_action resume(system::error_code ec, const corosio::endpoint& selected_endpoint); - co_connect_action resume(system::error_code ec, co_redis_stream_state& st); + co_connect_action resume(system::error_code ec); }; // namespace boost::redis::detail diff --git a/include/boost/redis/impl/co_connect_fsm.ipp b/include/boost/redis/impl/co_connect_fsm.ipp index d26f6ff5..b4582184 100644 --- a/include/boost/redis/impl/co_connect_fsm.ipp +++ b/include/boost/redis/impl/co_connect_fsm.ipp @@ -61,8 +61,7 @@ struct log_traits> { co_connect_action co_connect_fsm::resume( system::error_code ec, - std::span resolver_results, - co_redis_stream_state& st) + std::span resolver_results) { // Log it if (ec) { @@ -72,13 +71,12 @@ co_connect_action co_connect_fsm::resume( } // Delegate to the regular resume function - return resume(ec, st); + return resume(ec); } co_connect_action co_connect_fsm::resume( system::error_code ec, - const corosio::endpoint& selected_endpoint, - co_redis_stream_state& st) + const corosio::endpoint& selected_endpoint) { // Log it if (ec) { @@ -88,20 +86,17 @@ co_connect_action co_connect_fsm::resume( } // Delegate to the regular resume function - return resume(ec, st); + return resume(ec); } -co_connect_action co_connect_fsm::resume(system::error_code ec, co_redis_stream_state& st) +co_connect_action co_connect_fsm::resume(system::error_code ec) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL - if (st.type == transport_type::unix_socket) { - // Reset the socket, to discard any previous state. Ignore any errors - BOOST_REDIS_YIELD(resume_point_, 1, co_connect_action_type::unix_socket_close) - + if (type_ == transport_type::unix_socket) { // Connect to the socket - BOOST_REDIS_YIELD(resume_point_, 2, co_connect_action_type::unix_socket_connect) + BOOST_REDIS_YIELD(resume_point_, 1, co_connect_action_type::unix_socket_connect) // Log it if (ec) { @@ -118,18 +113,9 @@ co_connect_action co_connect_fsm::resume(system::error_code ec, co_redis_stream_ // Done return system::error_code(); } else { - // ssl::stream doesn't support being re-used. If we're to use - // TLS and the stream has been used, re-create it. - // Must be done before anything else is done on the stream. - // We don't need to close the TCP socket if using plaintext TCP - // because range-connect closes open sockets, while individual connect doesn't - if (st.type == transport_type::tcp_tls && st.ssl_stream_used) { - BOOST_REDIS_YIELD(resume_point_, 3, co_connect_action_type::ssl_stream_reset) - } - // Resolve names. The continuation needs access to the returned // endpoints, and is a specialized resume() that will call this function - BOOST_REDIS_YIELD(resume_point_, 4, co_connect_action_type::tcp_resolve) + BOOST_REDIS_YIELD(resume_point_, 2, co_connect_action_type::tcp_resolve) // If this failed, we can't continue if (ec) { @@ -138,17 +124,14 @@ co_connect_action co_connect_fsm::resume(system::error_code ec, co_redis_stream_ // Now connect to the endpoints returned by the resolver. // This has a specialized resume(), too - BOOST_REDIS_YIELD(resume_point_, 5, co_connect_action_type::tcp_connect) + BOOST_REDIS_YIELD(resume_point_, 3, co_connect_action_type::tcp_connect) // If this failed, we can't continue if (ec) { return ec; } - if (st.type == transport_type::tcp_tls) { - // Mark the SSL stream as used - st.ssl_stream_used = true; - + if (type_ == transport_type::tcp_tls) { // Perform the TLS handshake BOOST_REDIS_YIELD(resume_point_, 6, co_connect_action_type::ssl_handshake) diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index 530ab09b..3c2e239d 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -26,7 +27,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -35,16 +38,19 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -79,65 +85,117 @@ capy::task> maybe_timeout( } class co_redis_stream { - // TODO: UNIX sockets - corosio::tcp_socket socket_; - corosio::openssl_stream stream_; // TODO: make this configurable - corosio::resolver resolv_; - co_redis_stream_state st_; + struct tcp_state { + corosio::resolver resolv; + corosio::tcp_socket sock; + + explicit tcp_state(capy::execution_context& ctx) + : resolv(ctx) + , sock(ctx) + { } + }; + + // Required to create the other objects + capy::execution_context& ctx_; + corosio::tls_context tls_ctx_; + + // Constructed lazily as required + std::optional tcp_; + std::optional unix_; + std::optional tls_; + + // Contains the stream that will end up being used + capy::any_stream stream_; + + void setup(transport_type type) + { + if (type == transport_type::unix_socket) { + if (unix_.has_value()) { + // UNIX sockets don't use range connect. + // We need to close and re-open the socket before establishing another connection + unix_->close(); + unix_->open(); + } else { + unix_.emplace(ctx_); + } + stream_ = capy::any_stream(&*unix_); + } else { + // TCP (with or without TLS) + // Allocate the object if not there. + // TCP uses range connect, so we don't need to close and reopen the socket + if (!tcp_.has_value()) + tcp_.emplace(ctx_); + + // If using TLS, allocate or reset the stream + if (type == transport_type::tcp_tls) { + if (tls_.has_value()) + tls_->reset(); + else + tls_.emplace(capy::any_stream(&tcp_->sock), tls_ctx_); + stream_ = capy::any_stream(&*tls_); + } else { + stream_ = capy::any_stream(&tcp_->sock); + } + } + } public: explicit co_redis_stream(capy::execution_context& ctx, corosio::tls_context tls_ctx) - : socket_(ctx) - , stream_(&socket_, std::move(tls_ctx)) - , resolv_(ctx) + : ctx_(ctx) + , tls_ctx_(std::move(tls_ctx)) { } // I/O capy::io_task<> connect(const connect_params& params, buffered_logger& l) { - co_connect_fsm fsm{l}; + co_connect_fsm fsm{l, params.addr.type()}; system::error_code ec; corosio::resolver_results endpoints; - auto act = fsm.resume(ec, st_); + setup(params.addr.type()); + + auto act = fsm.resume(ec); while (true) { switch (act.type) { - case co_connect_action_type::unix_socket_close: - BOOST_ASSERT(false); - co_return {std::make_error_code(std::errc::operation_not_supported)}; case co_connect_action_type::unix_socket_connect: - BOOST_ASSERT(false); - co_return {std::make_error_code(std::errc::operation_not_supported)}; + { + auto result = co_await capy::timeout( + unix_->connect(corosio::local_endpoint(params.addr.unix_socket())), + params.connect_timeout); + ec = result.ec; + act = fsm.resume(ec); + break; + } case co_connect_action_type::tcp_resolve: { auto result = co_await capy::timeout( - resolv_.resolve(params.addr.tcp_address().host, params.addr.tcp_address().port), + tcp_->resolv.resolve( + params.addr.tcp_address().host, + params.addr.tcp_address().port), params.resolve_timeout); ec = result.ec; endpoints = std::move(std::get<0>(result.values)); - act = fsm.resume(ec, endpoints, st_); + act = fsm.resume(ec, endpoints); break; } - case co_connect_action_type::ssl_stream_reset: - stream_.reset(); - act = fsm.resume(ec, st_); - break; case co_connect_action_type::ssl_handshake: + { ec = (co_await capy::timeout( - stream_.handshake(corosio::tls_stream::handshake_type::client), + tls_->handshake(corosio::tls_stream::handshake_type::client), params.ssl_handshake_timeout)) .ec; - act = fsm.resume(ec, st_); + act = fsm.resume(ec); break; + } case co_connect_action_type::done: co_return {act.ec}; case co_connect_action_type::tcp_connect: { auto result = co_await capy::timeout( - corosio::connect(socket_, std::move(endpoints)), + corosio::connect(tcp_->sock, std::move(endpoints)), params.connect_timeout); ec = result.ec; - act = fsm.resume(ec, result.get<1>(), st_); + act = fsm.resume(ec, result.get<1>()); break; } default: BOOST_ASSERT(false); @@ -146,25 +204,15 @@ public: } template - capy::io_task write_some(const BuffType& buffers) + auto write_some(BuffType&& buffers) { - switch (st_.type) { - case transport_type::tcp: co_return co_await socket_.write_some(buffers); - case transport_type::tcp_tls: co_return co_await stream_.write_some(buffers); - case transport_type::unix_socket: - default: BOOST_ASSERT(false); co_return {}; - } + return stream_.write_some(std::forward(buffers)); } template - capy::io_task read_some(const BuffType& buffers) + auto read_some(BuffType&& buffers) { - switch (st_.type) { - case transport_type::tcp: co_return co_await socket_.read_some(buffers); - case transport_type::tcp_tls: co_return co_await stream_.read_some(buffers); - case transport_type::unix_socket: - default: BOOST_ASSERT(false); co_return {}; - } + return stream_.read_some(std::forward(buffers)); } }; From 39ebad546ec0404e330b067d574d24744b448afb Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 15:36:19 +0200 Subject: [PATCH 063/115] UNIX test --- include/boost/redis/impl/co_connection.ipp | 4 +- test/CMakeLists.txt | 1 + test/test_co_unix_sockets.cpp | 169 +++++++++++++++++++++ 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 test/test_co_unix_sockets.cpp diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index 3c2e239d..6ae2596a 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -45,7 +45,6 @@ #include #include #include -#include #include #include @@ -452,7 +451,8 @@ struct co_connection_impl { capy::io_task<> run(const config& cfg) { - constexpr bool unix_sockets_supported = false; // TODO + // corosio only runs in systems that support UNIX sockets + constexpr bool unix_sockets_supported = true; run_fsm fsm{unix_sockets_supported}; system::error_code ec; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 96bf96e3..05dc223c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -91,6 +91,7 @@ make_test(test_issue_50) make_test(test_conversions) make_test(test_conn_tls) make_test(test_unix_sockets) +make_test(test_co_unix_sockets) make_test(test_conn_cancel_after) make_test(test_conn_sentinel) diff --git a/test/test_co_unix_sockets.cpp b/test/test_co_unix_sockets.cpp new file mode 100644 index 00000000..01132b6f --- /dev/null +++ b/test/test_co_unix_sockets.cpp @@ -0,0 +1,169 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +#include +#include +#include +#include + +namespace capy = boost::capy; +using namespace boost::redis; +using namespace boost::redis::test; +using error_code = std::error_code; +using namespace std::string_view_literals; + +namespace { + +constexpr std::string_view unix_socket_path = "/tmp/redis-socks/redis.sock"; + +// Executing commands using UNIX sockets works +capy::task test_exec() +{ + co_connection conn{co_await capy::this_coro::executor}; + auto cfg = make_test_config(); + cfg.unix_socket = unix_socket_path; + + request req; + req.push("PING", "unix"); + response res; + + auto exec_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.exec(req, res); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(std::get<0>(res).value(), "unix"sv); +} + +// If the connection is lost when using a UNIX socket, we can reconnect +capy::task test_reconnection() +{ + co_connection conn{co_await capy::this_coro::executor}; + auto cfg = make_test_config(); + cfg.unix_socket = unix_socket_path; + + request ping_request; + ping_request.get_config().cancel_if_not_connected = false; + ping_request.get_config().cancel_if_unresponded = false; + ping_request.get_config().cancel_on_connection_lost = false; + ping_request.push("PING", "some_value"); + + request quit_request; + quit_request.push("QUIT"); + + auto exec_fn = [&]() -> capy::io_task<> { + auto [quit_ec] = co_await conn.exec(quit_request, ignore); + BOOST_TEST_EQ(quit_ec, error_code()); + + auto [ping_ec] = co_await conn.exec(ping_request, ignore); + BOOST_TEST_EQ(ping_ec, error_code()); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + co_await capy::when_any(exec_fn(), run_fn()); +} + +// We can freely switch between UNIX sockets and other transports +capy::task test_switch_between_transports() +{ + co_connection conn{co_await capy::this_coro::executor}; + request req; + req.push("PING", "hello"); + + // Create configurations for TLS and UNIX connections + auto tcp_tls_cfg = make_test_config(); + tcp_tls_cfg.use_ssl = true; + tcp_tls_cfg.addr.port = "16380"; + auto unix_cfg = make_test_config(); + unix_cfg.unix_socket = unix_socket_path; + + auto run_once = [&](const config& cfg) -> capy::task<> { + auto when_any_res = co_await capy::when_any( + [&]() -> capy::io_task<> { + response res; + auto [ec] = co_await conn.exec(req, res); + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(std::get<0>(res).value(), "hello"); + co_return {}; + }(), + [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }()); + BOOST_TEST_EQ(when_any_res.index(), 1); + }; + + // Run the connection with TCP/TLS + std::cerr << "test_switch_between_transports: TLS 1\n"; + co_await run_once(tcp_tls_cfg); + + // Switch to UNIX + std::cerr << "test_switch_between_transports: UNIX\n"; + co_await run_once(unix_cfg); + + // Go back to TCP/TLS + std::cerr << "test_switch_between_transports: TLS 2\n"; + co_await run_once(tcp_tls_cfg); +} + +// Trying to enable TLS and UNIX sockets at the same time +// is an error and makes run exit immediately +capy::task test_error_unix_tls() +{ + co_connection conn{co_await capy::this_coro::executor}; + auto cfg = make_test_config(); + cfg.use_ssl = true; + cfg.addr.port = "16380"; + cfg.unix_socket = unix_socket_path; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); +} + +} // namespace + +int main() +{ + run_coroutine_test(test_exec()); + run_coroutine_test(test_reconnection()); + run_coroutine_test(test_switch_between_transports()); + run_coroutine_test(test_error_unix_tls()); + + return boost::report_errors(); +} From b70b51262ecd76c68f89d3c01dd113d7301d9908 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 15:50:00 +0200 Subject: [PATCH 064/115] recover test_exec_one_fsm --- test/test_exec_one_fsm.cpp | 72 +++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/test/test_exec_one_fsm.cpp b/test/test_exec_one_fsm.cpp index 1e5d7c02..1069e591 100644 --- a/test/test_exec_one_fsm.cpp +++ b/test/test_exec_one_fsm.cpp @@ -7,15 +7,15 @@ // #include +#include #include #include #include #include #include -#include -#include #include +#include #include #include "print_node.hpp" @@ -26,13 +26,13 @@ #include using namespace boost::redis; -namespace asio = boost::asio; using detail::exec_one_fsm; using detail::exec_one_action; using detail::exec_one_action_type; using detail::read_buffer; +using detail::cancellation_type; using boost::system::error_code; -using boost::asio::cancellation_type_t; +namespace errc = boost::system::errc; using parse_event = any_adapter::parse_event; using resp3::type; @@ -112,17 +112,17 @@ void test_success() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Read the entire response in one go constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n"; copy_to(buff, payload); - act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none); + act = fsm.resume(buff, error_code(), payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::done); // Verify the adapter calls @@ -147,11 +147,11 @@ void test_no_expected_response() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM shouldn't ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // No adapter calls should be done @@ -167,25 +167,25 @@ void test_short_reads() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Read fragments constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n"; copy_to(buff, payload.substr(0, 6u)); - act = fsm.resume(buff, error_code(), 6u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 6u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); copy_to(buff, payload.substr(6, 10u)); - act = fsm.resume(buff, error_code(), 10u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 10u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); copy_to(buff, payload.substr(16)); - act = fsm.resume(buff, error_code(), payload.substr(16).size(), cancellation_type_t::none); + act = fsm.resume(buff, error_code(), payload.substr(16).size(), cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::done); // Verify the adapter calls @@ -210,12 +210,12 @@ void test_write_error() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // Write error - act = fsm.resume(buff, asio::error::connection_reset, 10u, cancellation_type_t::none); - BOOST_TEST_EQ(act, error_code(asio::error::connection_reset)); + act = fsm.resume(buff, make_error_code(errc::io_error), 10u, cancellation_type::none); + BOOST_TEST_EQ(act, make_error_code(errc::io_error)); } void test_write_cancel() @@ -226,12 +226,12 @@ void test_write_cancel() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // Edge case where the operation finished successfully but with the cancellation state set - act = fsm.resume(buff, error_code(), 10u, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fsm.resume(buff, error_code(), 10u, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); } // Errors in read @@ -243,16 +243,16 @@ void test_read_error() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Read error - act = fsm.resume(buff, asio::error::network_reset, 0u, cancellation_type_t::none); - BOOST_TEST_EQ(act, error_code(asio::error::network_reset)); + act = fsm.resume(buff, make_error_code(errc::io_error), 0u, cancellation_type::none); + BOOST_TEST_EQ(act, make_error_code(errc::io_error)); } void test_read_cancelled() @@ -263,17 +263,17 @@ void test_read_cancelled() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Edge case where the operation finished successfully but with the cancellation state set copy_to(buff, "$5\r\n"); - act = fsm.resume(buff, error_code(), 4u, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fsm.resume(buff, error_code(), 4u, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); } // Buffer too small @@ -286,11 +286,11 @@ void test_buffer_prepare_error() buff.set_config({4096u, 8u}); // max size is 8 bytes // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // When preparing the buffer, we encounter an error - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::exceeds_maximum_read_buffer_size)); } @@ -303,17 +303,17 @@ void test_parse_error() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // The response contains an invalid message constexpr std::string_view payload = "$bad\r\n"; copy_to(buff, payload); - act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none); + act = fsm.resume(buff, error_code(), payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::not_a_number)); } @@ -329,17 +329,17 @@ void test_adapter_error() read_buffer buff; // Write the request - auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none); + auto act = fsm.resume(buff, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::write); // FSM should now ask for data - act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none); + act = fsm.resume(buff, error_code(), 25u, cancellation_type::none); BOOST_TEST_EQ(act, exec_one_action_type::read_some); // Read the entire response in one go constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n"; copy_to(buff, payload); - act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none); + act = fsm.resume(buff, error_code(), payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::empty_field)); } From 587ac507a2273be02dcd9472565846f136ced716 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 16:02:22 +0200 Subject: [PATCH 065/115] Recover test_reader_fsm --- test/test_reader_fsm.cpp | 129 ++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/test/test_reader_fsm.cpp b/test/test_reader_fsm.cpp index b5cc60c0..9b510b0b 100644 --- a/test/test_reader_fsm.cpp +++ b/test/test_reader_fsm.cpp @@ -6,6 +6,7 @@ // #include +#include #include #include #include @@ -14,9 +15,9 @@ #include #include -#include -#include #include +#include +#include #include #include "sansio_utils.hpp" @@ -24,14 +25,15 @@ #include #include #include +#include using namespace boost::redis; -namespace net = boost::asio; using boost::system::error_code; -using net::cancellation_type_t; +namespace errc = boost::system::errc; using detail::reader_fsm; using detail::multiplexer; using detail::connection_state; +using detail::cancellation_type; using action = detail::reader_fsm::action; using namespace std::chrono_literals; @@ -108,11 +110,12 @@ struct fixture : detail::log_fixture { void test_push() { + // Timeout condition is arbitrary fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -124,19 +127,19 @@ void test_push() copy_to(fix.st.mpx, payload); // Deliver the 1st push - act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(11u)); // Deliver the 2st push - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(12u)); // Deliver the 3rd push - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(13u)); // All pushes were delivered so the fsm should demand more data - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Check logging @@ -150,10 +153,10 @@ void test_push() void test_read_needs_more() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Split the incoming message in three random parts and deliver @@ -162,22 +165,22 @@ void test_read_needs_more() // Passes the first part to the fsm. copy_to(fix.st.mpx, msg[0]); - act = fsm.resume(fix.st, msg[0].size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, msg[0].size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Passes the second part to the fsm. copy_to(fix.st.mpx, msg[1]); - act = fsm.resume(fix.st, msg[1].size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, msg[1].size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Passes the third and last part to the fsm, next it should ask us // to deliver the message. copy_to(fix.st.mpx, msg[2]); - act = fsm.resume(fix.st, msg[2].size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, msg[2].size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(msg[0].size() + msg[1].size() + msg[2].size())); // All pushes were delivered so the fsm should demand more data - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Check logging @@ -197,11 +200,11 @@ void test_read_needs_more() void test_health_checks_disabled() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; fix.st.cfg.health_check_interval = 0s; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(0s)); // Split the message into two so we cover both the regular read and the needs more branch @@ -209,16 +212,16 @@ void test_health_checks_disabled() // Passes the first part to the fsm. copy_to(fix.st.mpx, msg[0]); - act = fsm.resume(fix.st, msg[0].size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, msg[0].size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(0s)); // Push delivery complete copy_to(fix.st.mpx, msg[1]); - act = fsm.resume(fix.st, msg[1].size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, msg[1].size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(25u)); // All pushes were delivered so the fsm should demand more data - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(0s)); // Check logging @@ -235,10 +238,10 @@ void test_health_checks_disabled() void test_read_error() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -246,7 +249,7 @@ void test_read_error() copy_to(fix.st.mpx, payload); // Deliver the data - act = fsm.resume(fix.st, payload.size(), {error::empty_field}, cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), {error::empty_field}, cancellation_type::none); BOOST_TEST_EQ(act, error_code{error::empty_field}); // Check logging @@ -263,14 +266,14 @@ void test_read_error() void test_read_timeout() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); - // Timeout - act = fsm.resume(fix.st, 0, {net::error::operation_aborted}, cancellation_type_t::none); + // Timeout: an error code that matches the timeout condition + act = fsm.resume(fix.st, 0, make_error_code(errc::broken_pipe), cancellation_type::none); BOOST_TEST_EQ(act, error_code{error::pong_timeout}); // Check logging @@ -286,10 +289,10 @@ void test_read_timeout() void test_parse_error() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -297,7 +300,7 @@ void test_parse_error() copy_to(fix.st.mpx, payload); // Deliver the data - act = fsm.resume(fix.st, payload.size(), {}, cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), {}, cancellation_type::none); BOOST_TEST_EQ(act, error_code{error::not_a_number}); // Check logging @@ -317,7 +320,7 @@ void test_setup_request_error() { // Setup fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; request req; req.push("PING"); // should have 1 command auto elem = std::make_shared( @@ -333,7 +336,7 @@ void test_setup_request_error() BOOST_TEST(fix.st.mpx.commit_write(fix.st.mpx.get_write_buffer().size())); // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -341,7 +344,7 @@ void test_setup_request_error() copy_to(fix.st.mpx, payload); // Deliver the data - act = fsm.resume(fix.st, payload.size(), {}, cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), {}, cancellation_type::none); BOOST_TEST_EQ(act, error_code{error::resp3_hello}); // Check logging @@ -355,10 +358,10 @@ void test_setup_request_error() void test_push_deliver_error() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -366,11 +369,11 @@ void test_push_deliver_error() copy_to(fix.st.mpx, payload); // Deliver the data - act = fsm.resume(fix.st, payload.size(), {}, cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), {}, cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(11u)); // Resumes from notifying a push with an error. - act = fsm.resume(fix.st, 0, error::empty_field, cancellation_type_t::none); + act = fsm.resume(fix.st, 0, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, error_code{error::empty_field}); // Check logging @@ -389,16 +392,16 @@ void test_max_read_buffer_size() fix.st.cfg.read_buffer_append_size = 5; fix.st.cfg.max_read_size = 7; fix.st.mpx.set_config(fix.st.cfg); - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Passes the first part to the fsm. std::string const part1 = ">3\r\n"; copy_to(fix.st.mpx, part1); - act = fsm.resume(fix.st, part1.size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, part1.size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::exceeds_maximum_read_buffer_size)); // Check logging @@ -416,21 +419,22 @@ void test_max_read_buffer_size() void test_cancel_read() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); - // The read was cancelled (maybe after delivering some bytes) + // The read was cancelled (maybe after delivering some bytes). + // Cancellation state wins to timeout. constexpr std::string_view payload = ">1\r\n"; copy_to(fix.st.mpx, payload); act = fsm.resume( fix.st, payload.size(), - net::error::operation_aborted, - cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(net::error::operation_aborted)); + make_error_code(errc::broken_pipe), + cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Check logging fix.check_log({ @@ -442,18 +446,18 @@ void test_cancel_read() void test_cancel_read_edge() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // Deliver a push, and notify a cancellation. // This can happen if the cancellation signal arrives before the read handler runs constexpr std::string_view payload = ">1\r\n+msg1\r\n"; copy_to(fix.st.mpx, payload); - act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(net::error::operation_aborted)); + act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Check logging fix.check_log({ @@ -465,10 +469,10 @@ void test_cancel_read_edge() void test_cancel_push_delivery() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -479,12 +483,13 @@ void test_cancel_push_delivery() copy_to(fix.st.mpx, payload); // Deliver the 1st push - act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(11u)); - // We got a cancellation while delivering it - act = fsm.resume(fix.st, 0, net::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(net::error::operation_aborted)); + // We got a cancellation while delivering it. + // The pass-through ec is superseded by the cancellation state. + act = fsm.resume(fix.st, 0, make_error_code(errc::io_error), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Check logging fix.check_log({ @@ -497,10 +502,10 @@ void test_cancel_push_delivery() void test_cancel_push_delivery_edge() { fixture fix; - reader_fsm fsm; + reader_fsm fsm{std::errc::broken_pipe}; // Initiate - auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::none); + auto act = fsm.resume(fix.st, 0, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::read_some(6s)); // The fsm is asking for data. @@ -511,13 +516,13 @@ void test_cancel_push_delivery_edge() copy_to(fix.st.mpx, payload); // Deliver the 1st push - act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type_t::none); + act = fsm.resume(fix.st, payload.size(), error_code(), cancellation_type::none); BOOST_TEST_EQ(act, action::notify_push_receiver(11u)); // We got a cancellation after delivering it. // This can happen if the cancellation signal arrives before the channel send handler runs - act = fsm.resume(fix.st, 0, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(net::error::operation_aborted)); + act = fsm.resume(fix.st, 0, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Check logging fix.check_log({ From 1204f706df51c5ea8f5fbb823005a4c8ff361506 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 16:19:22 +0200 Subject: [PATCH 066/115] writer tests --- test/test_writer_fsm.cpp | 133 ++++++++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 51 deletions(-) diff --git a/test/test_writer_fsm.cpp b/test/test_writer_fsm.cpp index 0c151a3f..cc7e3e0a 100644 --- a/test/test_writer_fsm.cpp +++ b/test/test_writer_fsm.cpp @@ -6,16 +6,19 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include +#include #include #include -#include #include #include +#include #include +#include #include #include "sansio_utils.hpp" @@ -24,8 +27,11 @@ #include #include #include +#include using namespace boost::redis; +using boost::system::error_code; +namespace errc = boost::system::errc; namespace asio = boost::asio; using detail::writer_fsm; using detail::multiplexer; @@ -33,8 +39,7 @@ using detail::writer_action_type; using detail::consume_result; using detail::writer_action; using detail::connection_state; -using boost::system::error_code; -using boost::asio::cancellation_type_t; +using detail::cancellation_type; using namespace std::chrono_literals; // Operators @@ -109,7 +114,8 @@ struct test_elem { struct fixture : detail::log_fixture { connection_state st{{make_logger()}}; - writer_fsm fsm; + // Timeout condition is arbitrary + writer_fsm fsm{std::errc::broken_pipe}; fixture() { @@ -129,13 +135,12 @@ void test_single_request() fix.st.mpx.add(item1.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); // The write completes successfully. The request is written, and we go back to sleep. - act = fix.fsm - .resume(fix.st, error_code(), item1.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item1.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); BOOST_TEST(item1.elm->is_written()); @@ -143,13 +148,12 @@ void test_single_request() fix.st.mpx.add(item2.elm); // The wait is cancelled to signal we've got a new request - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item2.elm->is_staged()); // Write successful - act = fix.fsm - .resume(fix.st, error_code(), item2.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item2.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); BOOST_TEST(item2.elm->is_written()); @@ -171,7 +175,7 @@ void test_request_arrives_while_writing() fix.st.mpx.add(item1.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); @@ -180,15 +184,13 @@ void test_request_arrives_while_writing() // The write completes successfully. The request is written, // and we start writing the new one - act = fix.fsm - .resume(fix.st, error_code(), item1.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item1.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_written()); BOOST_TEST(item2.elm->is_staged()); // Write successful - act = fix.fsm - .resume(fix.st, error_code(), item2.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item2.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); BOOST_TEST(item2.elm->is_written()); @@ -207,19 +209,50 @@ void test_no_request_at_startup() test_elem item; // Start. There is no request, so we wait - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // A request arrives fix.st.mpx.add(item.elm); - // The wait is cancelled to signal we've got a new request - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type_t::none); + // The wait is cancelled (with a non-empty ec signal) to indicate new data + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type::none); + BOOST_TEST_EQ(act, writer_action::write_some(4s)); + BOOST_TEST(item.elm->is_staged()); + + // Write successful + act = fix.fsm.resume(fix.st, error_code(), item.req.payload().size(), cancellation_type::none); + BOOST_TEST_EQ(act, writer_action::wait(4s)); + BOOST_TEST(item.elm->is_written()); + + // Logs + fix.check_log({ + {logger::level::debug, "Writer task: 24 bytes written."}, + }); +} + +// If the wait is signaled with any error, we considered it a notification. +// Important because Asio and Corosio might signal this differently +void test_wait_canceled_other_code() +{ + // Setup + fixture fix; + test_elem item; + + // Start. There is no request, so we wait + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); + BOOST_TEST_EQ(act, writer_action::wait(4s)); + + // A request arrives + fix.st.mpx.add(item.elm); + + // The wait is cancelled (with a non-empty ec signal) to indicate new data + act = fix.fsm.resume(fix.st, boost::capy::error::canceled, 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item.elm->is_staged()); // Write successful - act = fix.fsm.resume(fix.st, error_code(), item.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); BOOST_TEST(item.elm->is_written()); @@ -240,27 +273,27 @@ void test_short_writes() fix.st.mpx.add(item1.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); // We write a few bytes. It's not the entire message, so we write again - act = fix.fsm.resume(fix.st, error_code(), 2u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 2u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); // We write some more bytes, but still not the entire message. - act = fix.fsm.resume(fix.st, error_code(), 5u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 5u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); // A zero size write doesn't cause trouble - act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item1.elm->is_staged()); // Complete writing the message (the entire payload is 24 bytes long) - act = fix.fsm.resume(fix.st, error_code(), 17u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 17u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); BOOST_TEST(item1.elm->is_written()); @@ -282,16 +315,16 @@ void test_ping() constexpr std::string_view ping_payload = "*2\r\n$4\r\nPING\r\n$8\r\nping_msg\r\n"; // Start. There is no request, so we wait - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // No request arrives during the wait interval so a ping is added - act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST_EQ(fix.st.mpx.get_write_buffer(), ping_payload); // Write successful - act = fix.fsm.resume(fix.st, error_code(), ping_payload.size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), ping_payload.size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // Simulate a successful response to the PING @@ -320,12 +353,12 @@ void test_health_checks_disabled() fix.st.mpx.add(item.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(0s)); BOOST_TEST(item.elm->is_staged()); // The write completes successfully. The request is written, and we go back to sleep. - act = fix.fsm.resume(fix.st, error_code(), item.req.payload().size(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), item.req.payload().size(), cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(0s)); BOOST_TEST(item.elm->is_written()); @@ -343,16 +376,16 @@ void test_ping_error() error_code ec; // Start. There is no request, so we wait - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // No request arrives during the wait interval so a ping is added - act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); // Write successful const auto ping_size = fix.st.mpx.get_write_buffer().size(); - act = fix.fsm.resume(fix.st, error_code(), ping_size, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), ping_size, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // Simulate an error response to the PING @@ -381,14 +414,14 @@ void test_write_error() fix.st.mpx.add(item.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item.elm->is_staged()); // The write completes with an error (possibly with partial success). // The request is still staged, and the writer exits. // Use an error we control so we can check logs - act = fix.fsm.resume(fix.st, error::empty_field, 2u, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, 2u, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::empty_field)); BOOST_TEST(item.elm->is_staged()); @@ -410,12 +443,12 @@ void test_write_timeout() fix.st.mpx.add(item.elm); // Start. A write is triggered, and the request is marked as staged - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item.elm->is_staged()); - // The write times out, so it completes with operation_aborted - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type_t::none); + // The write times out. An ec matching the timeout condition is reported. + act = fix.fsm.resume(fix.st, make_error_code(errc::broken_pipe), 0u, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::write_timeout)); BOOST_TEST(item.elm->is_staged()); @@ -439,13 +472,13 @@ void test_cancel_write() fix.st.mpx.add(item.elm); // Start. A write is triggered - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item.elm->is_staged()); // Write cancelled and failed with operation_aborted - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 2u, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 2u, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); BOOST_TEST(item.elm->is_staged()); // Logs @@ -466,14 +499,14 @@ void test_cancel_write_edge() fix.st.mpx.add(item.elm); // Start. A write is triggered - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::write_some(4s)); BOOST_TEST(item.elm->is_staged()); // Write cancelled but without error act = fix.fsm - .resume(fix.st, error_code(), item.req.payload().size(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + .resume(fix.st, error_code(), item.req.payload().size(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); BOOST_TEST(item.elm->is_written()); // Logs @@ -491,19 +524,16 @@ void test_cancel_wait() test_elem item; // Start. There is no request, so we wait - auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), 0u, cancellation_type::none); BOOST_TEST_EQ(act, writer_action::wait(4s)); // Sanity check: the writer doesn't touch the multiplexer after a cancellation fix.st.mpx.add(item.elm); - // Cancel the wait, setting the cancellation state - act = fix.fsm.resume( - fix.st, - asio::error::operation_aborted, - 0u, - asio::cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + // Cancel the wait, setting the cancellation state. + // The pass-through ec is superseded by the cancellation state. + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, 0u, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); BOOST_TEST(item.elm->is_waiting()); // Logs @@ -519,6 +549,7 @@ int main() test_single_request(); test_request_arrives_while_writing(); test_no_request_at_startup(); + test_wait_canceled_other_code(); test_short_writes(); test_health_checks_disabled(); From 0cb0643f563b86025e77258b098171da492096e3 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 16:22:57 +0200 Subject: [PATCH 067/115] recover a read error --- test/test_reader_fsm.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_reader_fsm.cpp b/test/test_reader_fsm.cpp index 9b510b0b..7b66ea84 100644 --- a/test/test_reader_fsm.cpp +++ b/test/test_reader_fsm.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -432,7 +433,7 @@ void test_cancel_read() act = fsm.resume( fix.st, payload.size(), - make_error_code(errc::broken_pipe), + boost::asio::error::operation_aborted, cancellation_type::terminal); BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); @@ -488,7 +489,7 @@ void test_cancel_push_delivery() // We got a cancellation while delivering it. // The pass-through ec is superseded by the cancellation state. - act = fsm.resume(fix.st, 0, make_error_code(errc::io_error), cancellation_type::terminal); + act = fsm.resume(fix.st, 0, boost::asio::error::operation_aborted, cancellation_type::terminal); BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Check logging From 0c285b05936890300b1ed52436cd5e43374c4cad Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 16:25:45 +0200 Subject: [PATCH 068/115] Recover test_run_fsm --- test/test_run_fsm.cpp | 234 +++++++++++++++++++++--------------------- 1 file changed, 116 insertions(+), 118 deletions(-) diff --git a/test/test_run_fsm.cpp b/test/test_run_fsm.cpp index 25558fd5..92d3c032 100644 --- a/test/test_run_fsm.cpp +++ b/test/test_run_fsm.cpp @@ -7,6 +7,7 @@ // #include +#include #include #include #include @@ -14,8 +15,8 @@ #include #include -#include // for BOOST_ASIO_HAS_LOCAL_SOCKETS #include +#include #include #include "sansio_utils.hpp" @@ -24,13 +25,14 @@ #include using namespace boost::redis; +namespace errc = boost::system::errc; namespace asio = boost::asio; using detail::run_fsm; using detail::multiplexer; using detail::run_action_type; using detail::run_action; +using detail::cancellation_type; using boost::system::error_code; -using boost::asio::cancellation_type_t; using namespace std::chrono_literals; // Operators @@ -85,8 +87,9 @@ struct fixture : detail::log_fixture { return res; } - fixture(config&& cfg = default_config()) + fixture(config&& cfg = default_config(), bool unix_sockets_supported = true) : st{{make_logger()}, std::move(cfg)} + , fsm{unix_sockets_supported} { } }; @@ -98,18 +101,17 @@ config config_no_reconnect() } // Config errors -#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS void test_config_error_unix() { // Setup config cfg; cfg.unix_socket = "/var/sock"; - fixture fix{std::move(cfg)}; + fixture fix{std::move(cfg), false}; // Launching the operation fails immediately - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::immediate); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::unix_sockets_unsupported)); // Log @@ -119,7 +121,6 @@ void test_config_error_unix() "are not supported by the system. [boost.redis:24]"}, }); } -#endif void test_config_error_unix_ssl() { @@ -130,9 +131,9 @@ void test_config_error_unix_ssl() fixture fix{std::move(cfg)}; // Launching the operation fails immediately - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::immediate); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::unix_sockets_ssl_unsupported)); // Log @@ -154,9 +155,9 @@ void test_config_error_unix_sentinel() fixture fix{std::move(cfg)}; // Launching the operation fails immediately - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::immediate); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::sentinel_unix_sockets_unsupported)); // Log @@ -174,17 +175,17 @@ void test_connect_error() fixture fix; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect errors. We sleep and try to connect again - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // This time we succeed and we launch the parallel group - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Log @@ -207,17 +208,17 @@ void test_connect_error_ssl() fix.st.cfg.use_ssl = true; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect errors. We sleep and try to connect again - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // This time we succeed and we launch the parallel group - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Log @@ -231,7 +232,6 @@ void test_connect_error_ssl() }); } -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS void test_connect_error_unix() { // Setup @@ -239,17 +239,17 @@ void test_connect_error_unix() fix.st.cfg.unix_socket = "/tmp/sock"; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect errors. We sleep and try to connect again - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // This time we succeed and we launch the parallel group - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Log @@ -262,7 +262,6 @@ void test_connect_error_unix() // clang-format on }); } -#endif // An error in connect without reconnection enabled makes the operation finish void test_connect_error_no_reconnect() @@ -271,11 +270,11 @@ void test_connect_error_no_reconnect() fixture fix{config_no_reconnect()}; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect errors. The operation finishes - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::connect_timeout)); // Log @@ -294,12 +293,15 @@ void test_connect_cancel() fixture fix; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect cancelled. The operation finishes - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume( + fix.st, + make_error_code(errc::operation_canceled), + cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -315,12 +317,12 @@ void test_connect_cancel_edge() fixture fix; // Launch the operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Connect cancelled. The operation finishes - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -337,19 +339,19 @@ void test_parallel_group_error() fixture fix; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits with an error. We sleep and connect again - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Log @@ -368,15 +370,15 @@ void test_parallel_group_error_no_reconnect() fixture fix{config_no_reconnect()}; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits with an error. We cancel the receive operation and exit - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::empty_field)); // Log @@ -394,16 +396,16 @@ void test_parallel_group_cancel() fixture fix; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits because the operation gets cancelled. Any receive operation gets cancelled - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -419,16 +421,16 @@ void test_parallel_group_cancel_no_reconnect() fixture fix{config_no_reconnect()}; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits because the operation gets cancelled. Any receive operation gets cancelled - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -445,20 +447,20 @@ void test_wait_cancel() fixture fix; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits with an error. We sleep - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); // We get cancelled during the sleep - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -474,20 +476,20 @@ void test_wait_cancel_edge() fixture fix; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits with an error. We sleep - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); // We get cancelled during the sleep - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -503,32 +505,32 @@ void test_several_reconnections() fixture fix; // Run the operation. Connect errors and we sleep - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); // Connect again, this time successfully. We launch the tasks - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // This exits with an error. We sleep and connect again - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Exit with cancellation - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -555,9 +557,9 @@ void test_setup_ping_requests() fixture fix{std::move(cfg)}; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // At this point, the requests are set up @@ -568,13 +570,13 @@ void test_setup_ping_requests() BOOST_TEST_EQ(fix.st.setup_req.payload(), expected_setup); // Reconnect - act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // The requests haven't been modified @@ -591,9 +593,9 @@ void test_setup_request_success() fix.st.cfg.setup.push("HELLO", 3); // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // At this point, the setup request should be already queued. Simulate the writer @@ -625,9 +627,9 @@ void test_setup_request_empty() fix.st.cfg.setup.clear(); // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Nothing was added to the multiplexer @@ -652,9 +654,9 @@ void test_setup_request_server_error() fix.st.cfg.setup.push("HELLO", 3); // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // At this point, the setup request should be already queued. Simulate the writer @@ -687,9 +689,9 @@ void test_setup_request_other_error() fix.st.cfg.reconnect_wait_interval = 0s; // Run the operation. We connect and launch the tasks - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // At this point, the setup request should be already queued. @@ -705,9 +707,9 @@ void test_setup_request_other_error() BOOST_TEST(res.first == detail::consume_result::got_response); // This will cause the writer to exit - act = fix.fsm.resume(fix.st, error::not_a_number, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::not_a_number, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::not_a_number)); // Check log @@ -730,21 +732,21 @@ void test_sentinel_reconnection() }; // Resolve succeeds, and a connection is attempted - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); fix.st.cfg.addr = {"host1", "1000"}; - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // This errors, so we sleep and resolve again - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); fix.st.cfg.addr = {"host2", "2000"}; - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::parallel_group); // Sentinel involves always a setup request containing the role check. Run it. @@ -757,19 +759,19 @@ void test_sentinel_reconnection() BOOST_TEST(res.first == detail::consume_result::got_response); // The parallel group errors, so we sleep and resolve again - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::cancel_receive); - act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); fix.st.cfg.addr = {"host3", "3000"}; - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Cancel - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -795,18 +797,18 @@ void test_sentinel_resolve_error() }; // Start the Sentinel resolve operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); // It fails with an error, so we go to sleep - act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection); // Retrying it succeeds - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); fix.st.cfg.addr = {"myhost", "10000"}; - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::connect); // Log @@ -825,11 +827,11 @@ void test_sentinel_resolve_error_no_reconnect() }; // Start the Sentinel resolve operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); // It fails with an error, so we exit - act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed)); // Log @@ -845,10 +847,10 @@ void test_sentinel_resolve_cancel() }; // Start the Sentinel resolve operation - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, run_action_type::sentinel_resolve); - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Log fix.check_log({ @@ -860,17 +862,13 @@ void test_sentinel_resolve_cancel() int main() { -#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS test_config_error_unix(); -#endif test_config_error_unix_ssl(); test_config_error_unix_sentinel(); test_connect_error(); test_connect_error_ssl(); -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS test_connect_error_unix(); -#endif test_connect_error_no_reconnect(); test_connect_cancel(); test_connect_cancel_edge(); From ed219e301015eebe30cfabed2b5181b1c2e881be Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 16:27:58 +0200 Subject: [PATCH 069/115] Recover test_sentinel_esolve --- test/test_sentinel_resolve_fsm.cpp | 120 +++++++++++++++-------------- 1 file changed, 62 insertions(+), 58 deletions(-) diff --git a/test/test_sentinel_resolve_fsm.cpp b/test/test_sentinel_resolve_fsm.cpp index d3046819..fe6e9400 100644 --- a/test/test_sentinel_resolve_fsm.cpp +++ b/test/test_sentinel_resolve_fsm.cpp @@ -7,6 +7,7 @@ // #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include #include @@ -23,6 +25,8 @@ using namespace boost::redis; namespace asio = boost::asio; +namespace errc = boost::system::errc; +using detail::cancellation_type; using detail::sentinel_resolve_fsm; using detail::sentinel_action; using detail::connection_state; @@ -109,11 +113,11 @@ void test_success() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); // Now send the request - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ // clang-format off @@ -125,7 +129,7 @@ void test_success() }); // We received a valid request, so we're done - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -160,11 +164,11 @@ void test_success_replica() fix.st.eng.get().seed(static_cast(183984887232u)); // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); // Now send the request - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ // clang-format off @@ -181,7 +185,7 @@ void test_success_replica() }); // We received a valid request, so we're done - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The address of one of the replicas is stored @@ -205,15 +209,15 @@ void test_one_connect_error() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); // This errors, so we connect to the 2nd sentinel - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); // Now send the request - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", @@ -221,7 +225,7 @@ void test_one_connect_error() }); // We received a valid request, so we're done - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -247,21 +251,21 @@ void test_one_request_network_error() fixture fix; // Initiate, connect to the 1st Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); // It fails, so we connect to the 2nd sentinel. This one succeeds - act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -288,9 +292,9 @@ void test_one_request_parse_error() fixture fix; // Initiate, connect to the 1st Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "+OK\r\n", @@ -298,15 +302,15 @@ void test_one_request_parse_error() }); // This fails parsing, so we connect to the 2nd sentinel. This one succeeds - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -334,9 +338,9 @@ void test_one_request_error_node() fixture fix; // Initiate, connect to the 1st Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "-ERR needs authentication\r\n", @@ -344,15 +348,15 @@ void test_one_request_error_node() }); // This fails, so we connect to the 2nd sentinel. This one succeeds - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -379,9 +383,9 @@ void test_one_master_unknown() fixture fix; // Initiate, connect to the 1st Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "_\r\n", @@ -390,15 +394,15 @@ void test_one_master_unknown() // It doesn't know about our master, so we connect to the 2nd sentinel. // This one succeeds - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The master's address is stored @@ -426,9 +430,9 @@ void test_one_no_replicas() fix.st.cfg.sentinel.server_role = role::replica; // Initiate, connect to the 1st Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", @@ -437,9 +441,9 @@ void test_one_no_replicas() }); // This errors, so we connect to the 2nd sentinel. This one succeeds - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ // clang-format off @@ -450,7 +454,7 @@ void test_one_no_replicas() "*0\r\n", // clang-format on }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code()); // The replica's address is stored @@ -477,9 +481,9 @@ void test_error() fixture fix; // 1st Sentinel doesn't know about the master - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "_\r\n", @@ -487,13 +491,13 @@ void test_error() }); // Move to the 2nd Sentinel, which fails to connect - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host2", "2000"})); // Move to the 3rd Sentinel, which has authentication misconfigured - act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type::none); BOOST_TEST_EQ(act, (address{"host3", "3000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "-ERR unauthorized\r\n", @@ -501,7 +505,7 @@ void test_error() }); // Sentinel list exhausted - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed)); // The Sentinel list is not updated @@ -542,16 +546,16 @@ void test_error_replica() fix.st.cfg.sentinel.server_role = role::replica; // Initiate, connect to the only Sentinel, and send the request - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", "*0\r\n", }); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed)); // Logs @@ -576,12 +580,12 @@ void test_cancel_connect() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); // Cancellation - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Logs fix.check_log({ @@ -597,12 +601,12 @@ void test_cancel_connect_edge() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); // Cancellation (without error code) - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Logs fix.check_log({ @@ -618,12 +622,12 @@ void test_cancel_request() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); - act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Logs fix.check_log({ @@ -640,12 +644,12 @@ void test_cancel_request_edge() fixture fix; // Initiate. We should connect to the 1st sentinel - auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, (address{"host1", "1000"})); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::none); BOOST_TEST_EQ(act, sentinel_action::request()); - act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal); - BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted)); + act = fix.fsm.resume(fix.st, error_code(), cancellation_type::terminal); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); // Logs fix.check_log({ From c889d4dcde1d2744e354ac065ca22ba85cbd482b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 16:38:23 +0200 Subject: [PATCH 070/115] Recover test_exec_fsm --- .../boost/redis/detail/cancellation_type.hpp | 5 + test/test_exec_fsm.cpp | 97 ++++++++++--------- 2 files changed, 55 insertions(+), 47 deletions(-) diff --git a/include/boost/redis/detail/cancellation_type.hpp b/include/boost/redis/detail/cancellation_type.hpp index 1249bf43..fbdba6c3 100644 --- a/include/boost/redis/detail/cancellation_type.hpp +++ b/include/boost/redis/detail/cancellation_type.hpp @@ -22,6 +22,11 @@ enum class cancellation_type : int total = 4, }; +constexpr cancellation_type operator|(cancellation_type lhs, cancellation_type rhs) +{ + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + constexpr bool contains(cancellation_type value, cancellation_type query) { return (static_cast(value) & static_cast(query)) != 0; diff --git a/test/test_exec_fsm.cpp b/test/test_exec_fsm.cpp index 16301554..99711d1e 100644 --- a/test/test_exec_fsm.cpp +++ b/test/test_exec_fsm.cpp @@ -6,15 +6,17 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include +#include #include -#include #include #include #include +#include #include #include "sansio_utils.hpp" @@ -26,6 +28,8 @@ using namespace boost::redis; namespace asio = boost::asio; +namespace errc = boost::system::errc; +using detail::cancellation_type; using detail::exec_fsm; using detail::multiplexer; using detail::exec_action_type; @@ -33,7 +37,6 @@ using detail::consume_result; using detail::exec_action; using detail::connection_state; using boost::system::error_code; -using boost::asio::cancellation_type_t; #define BOOST_REDIS_EXEC_SWITCH_CASE(elem) \ case exec_action_type::elem: return "exec_action_type::" #elem @@ -141,13 +144,13 @@ void test_success() error_code ec; // Initiate - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write @@ -163,7 +166,7 @@ void test_success() BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error_code(), 11u)); // All memory should have been freed by now @@ -180,13 +183,13 @@ void test_parse_error() error_code ec; // Initiate - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write @@ -204,7 +207,7 @@ void test_parse_error() BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error::empty_field, 0u)); // All memory should have been freed by now @@ -222,10 +225,10 @@ void test_cancel_if_not_connected() exec_fsm fsm(std::move(input.elm)); // Initiate. We're not connected, so the request gets cancelled - auto act = fsm.resume(false, st, cancellation_type_t::none); + auto act = fsm.resume(false, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::immediate); - act = fsm.resume(false, st, cancellation_type_t::none); + act = fsm.resume(false, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error::not_connected)); // We didn't leave memory behind @@ -242,13 +245,13 @@ void test_not_connected() error_code ec; // Initiate - auto act = fsm.resume(false, st, cancellation_type_t::none); + auto act = fsm.resume(false, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write @@ -264,7 +267,7 @@ void test_not_connected() BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error_code(), 11u)); // All memory should have been freed by now @@ -280,13 +283,13 @@ void test_cancel_waiting() { constexpr struct { const char* name; - asio::cancellation_type_t type; + cancellation_type type; } test_cases[] = { - {"terminal", asio::cancellation_type_t::terminal }, - {"partial", asio::cancellation_type_t::partial }, - {"total", asio::cancellation_type_t::total }, - {"mixed", asio::cancellation_type_t::partial | asio::cancellation_type_t::terminal}, - {"all", asio::cancellation_type_t::all }, + {"terminal", cancellation_type::terminal }, + {"partial", cancellation_type::partial }, + {"total", cancellation_type::total }, + {"mixed", cancellation_type::partial | cancellation_type::terminal }, + {"all", cancellation_type::terminal | cancellation_type::partial | cancellation_type::total}, }; for (const auto& tc : test_cases) { @@ -300,16 +303,16 @@ void test_cancel_waiting() BOOST_TEST_EQ_MSG(st.mpx.prepare_write(), 1u, tc.name); // Initiate and wait - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::setup_cancellation, tc.name); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::notify_writer, tc.name); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::wait_for_response, tc.name); // We get notified because the request got cancelled act = fsm.resume(true, st, tc.type); - BOOST_TEST_EQ_MSG(act, exec_action(asio::error::operation_aborted), tc.name); + BOOST_TEST_EQ_MSG(act, make_error_code(errc::operation_canceled), tc.name); BOOST_TEST_EQ_MSG(input.weak_elm.expired(), true, tc.name); // we didn't leave memory behind } } @@ -320,10 +323,10 @@ void test_cancel_notwaiting_terminal_partial() { constexpr struct { const char* name; - asio::cancellation_type_t type; + cancellation_type type; } test_cases[] = { - {"terminal", asio::cancellation_type_t::terminal}, - {"partial", asio::cancellation_type_t::partial }, + {"terminal", cancellation_type::terminal}, + {"partial", cancellation_type::partial }, }; for (const auto& tc : test_cases) { @@ -333,12 +336,12 @@ void test_cancel_notwaiting_terminal_partial() exec_fsm fsm(std::move(input->elm)); // Initiate - auto act = fsm.resume(false, st, cancellation_type_t::none); + auto act = fsm.resume(false, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::setup_cancellation, tc.name); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::notify_writer, tc.name); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ_MSG(act, exec_action_type::wait_for_response, tc.name); // The multiplexer starts writing the request @@ -347,7 +350,7 @@ void test_cancel_notwaiting_terminal_partial() // A cancellation arrives act = fsm.resume(true, st, tc.type); - BOOST_TEST_EQ(act, exec_action(asio::error::operation_aborted)); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled)); input.reset(); // Verify we don't access the request or response after completion error_code ec; @@ -372,12 +375,12 @@ void test_cancel_notwaiting_total() error_code ec; // Initiate - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write @@ -385,7 +388,7 @@ void test_cancel_notwaiting_total() BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size())); // We got requested a cancellation here, but we can't honor it - act = fsm.resume(true, st, asio::cancellation_type_t::total); + act = fsm.resume(true, st, cancellation_type::total); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful read @@ -397,7 +400,7 @@ void test_cancel_notwaiting_total() BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error_code(), 11u)); // All memory should have been freed by now @@ -415,13 +418,13 @@ void test_subscription_tracking_success() exec_fsm fsm(std::move(input.elm)); // Initiate - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write @@ -430,7 +433,7 @@ void test_subscription_tracking_success() // The request doesn't have a response, so this will // awaken the exec operation, and should complete the operation - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action(error_code(), 0u)); // All memory should have been freed by now @@ -456,13 +459,13 @@ void test_subscription_tracking_error() exec_fsm fsm(std::move(input.elm)); // Initiate - auto act = fsm.resume(true, st, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, st, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a write error, which would trigger a reconnection @@ -470,8 +473,8 @@ void test_subscription_tracking_error() st.mpx.cancel_on_conn_lost(); // This awakens the request - act = fsm.resume(true, st, cancellation_type_t::none); - BOOST_TEST_EQ(act, exec_action(asio::error::operation_aborted, 0u)); + act = fsm.resume(true, st, cancellation_type::none); + BOOST_TEST_EQ(act, make_error_code(errc::operation_canceled, 0u)); // All memory should have been freed by now BOOST_TEST(input.weak_elm.expired()); From b1ff4ddcd9211418de36c19d693dae0f6d600803 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 20:09:55 +0200 Subject: [PATCH 071/115] co_logging --- test/test_co_logging.cpp | 138 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 test/test_co_logging.cpp diff --git a/test/test_co_logging.cpp b/test/test_co_logging.cpp new file mode 100644 index 00000000..e2708e7e --- /dev/null +++ b/test/test_co_logging.cpp @@ -0,0 +1,138 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "corosio_common.hpp" + +#include +#include +#include + +using boost::system::error_code; +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; +using namespace boost::redis::test; + +namespace { + +config make_invalid_config() +{ + config cfg; + cfg.use_ssl = true; + cfg.unix_socket = "/tmp/sock"; + return cfg; +} + +struct fixture { + std::vector messages; + logger make_logger(logger::level lvl) + { + return {lvl, [this](logger::level, std::string_view msg) { + messages.emplace_back(msg); + }}; + } +}; + +capy::task<> test_connection_constructor_executor_1() +{ + // Setup + fixture fix; + co_connection conn{co_await capy::this_coro::executor, fix.make_logger(logger::level::info)}; + + // Produce some logging + auto [ec] = co_await conn.run(make_invalid_config()); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); + + // Some logging was produced + BOOST_TEST_EQ(fix.messages.size(), 1u); +} + +capy::task<> test_connection_constructor_context_1() +{ + // Setup + fixture fix; + auto ex = co_await capy::this_coro::executor; + co_connection conn{ex.context(), fix.make_logger(logger::level::info)}; + + // Produce some logging + auto [ec] = co_await conn.run(make_invalid_config()); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); + + // Some logging was produced + BOOST_TEST_EQ(fix.messages.size(), 1u); +} + +capy::task<> test_connection_constructor_executor_2() +{ + // Setup + fixture fix; + co_connection conn{ + co_await capy::this_coro::executor, + corosio::tls_context{}, + fix.make_logger(logger::level::info)}; + + // Produce some logging + auto [ec] = co_await conn.run(make_invalid_config()); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); + + // Some logging was produced + BOOST_TEST_EQ(fix.messages.size(), 1u); +} + +capy::task<> test_connection_constructor_context_2() +{ + // Setup + fixture fix; + auto ex = co_await capy::this_coro::executor; + co_connection conn{ex.context(), corosio::tls_context{}, fix.make_logger(logger::level::info)}; + + // Produce some logging + auto [ec] = co_await conn.run(make_invalid_config()); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); + + // Some logging was produced + BOOST_TEST_EQ(fix.messages.size(), 1u); +} + +capy::task<> test_disable_logging() +{ + // Setup + fixture fix; + co_connection conn{co_await capy::this_coro::executor, fix.make_logger(logger::level::disabled)}; + + // Produce some logging + auto [ec] = co_await conn.run(make_invalid_config()); + BOOST_TEST_EQ(ec, error::unix_sockets_ssl_unsupported); + + // No logging should have been produced + BOOST_TEST_EQ(fix.messages.size(), 0u); +} + +} // namespace + +int main() +{ + run_coroutine_test(test_connection_constructor_executor_1()); + run_coroutine_test(test_connection_constructor_executor_2()); + run_coroutine_test(test_connection_constructor_context_1()); + run_coroutine_test(test_connection_constructor_context_2()); + run_coroutine_test(test_disable_logging()); + + return boost::report_errors(); +} From 375dcccc3a4b987473d7d94d692c21d446de1c6e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 20:19:34 +0200 Subject: [PATCH 072/115] Recover all integ tests --- test/CMakeLists.txt | 99 ++++++++++++++++++----------------- test/test_co_setup.cpp | 4 +- test/test_conn_exec.cpp | 2 +- test/test_conn_exec_error.cpp | 2 +- test/test_conn_quit.cpp | 2 +- test/test_conn_reconnect.cpp | 2 +- test/test_conn_sentinel.cpp | 2 +- test/test_conn_setup.cpp | 2 +- test/test_conversions.cpp | 2 +- 9 files changed, 59 insertions(+), 58 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 05dc223c..84c3cd8f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -43,57 +43,58 @@ function(make_test TEST_NAME) endfunction() # Unit tests -make_test(test_low_level) -make_test(test_request) -make_test(test_serialization) -make_test(test_low_level_sync_sans_io) -make_test(test_any_adapter) -make_test(test_log_to_file) -make_test(test_conn_logging) -make_test(test_exec_fsm) -make_test(test_exec_one_fsm) -make_test(test_writer_fsm) -make_test(test_reader_fsm) -make_test(test_connect_fsm) -make_test(test_sentinel_resolve_fsm) -make_test(test_receive_fsm) -make_test(test_run_fsm) -make_test(test_compose_setup_request) -make_test(test_setup_adapter) -make_test(test_multiplexer) -make_test(test_parse_sentinel_response) -make_test(test_update_sentinel_list) -make_test(test_flat_tree) -make_test(test_generic_flat_response) -make_test(test_read_buffer) -make_test(test_subscription_tracker) -make_test(test_push_parser) +make_test(test_low_level boost_redis_tests_proto) +make_test(test_request boost_redis_tests_proto) +make_test(test_serialization boost_redis_tests_proto) +make_test(test_low_level_sync_sans_io boost_redis_tests_proto) +make_test(test_any_adapter boost_redis_tests_proto) +make_test(test_log_to_file boost_redis_tests_proto) +make_test(test_conn_logging boost_redis_tests_asio) +make_test(test_co_logging boost_redis_tests_corosio) +make_test(test_exec_fsm boost_redis_tests_proto) +make_test(test_exec_one_fsm boost_redis_tests_proto) +make_test(test_writer_fsm boost_redis_tests_proto) +make_test(test_reader_fsm boost_redis_tests_proto) +make_test(test_connect_fsm boost_redis_tests_asio) +make_test(test_sentinel_resolve_fsm boost_redis_tests_proto) +make_test(test_receive_fsm boost_redis_tests_proto) +make_test(test_run_fsm boost_redis_tests_proto) +make_test(test_compose_setup_request boost_redis_tests_proto) +make_test(test_setup_adapter boost_redis_tests_proto) +make_test(test_multiplexer boost_redis_tests_proto) +make_test(test_parse_sentinel_response boost_redis_tests_proto) +make_test(test_update_sentinel_list boost_redis_tests_proto) +make_test(test_flat_tree boost_redis_tests_proto) +make_test(test_generic_flat_response boost_redis_tests_proto) +make_test(test_read_buffer boost_redis_tests_proto) +make_test(test_subscription_tracker boost_redis_tests_proto) +make_test(test_push_parser boost_redis_tests_proto) # Tests that require a real Redis server -make_test(test_conn_quit) -make_test(test_conn_exec_retry) -make_test(test_conn_exec_error) -make_test(test_run) -make_test(test_conn_run_cancel) -make_test(test_conn_check_health) -make_test(test_co_check_health boost_redis_tests_corosio) -make_test(test_conn_exec) -make_test(test_conn_push) -make_test(test_conn_push2) -make_test(test_conn_monitor) -make_test(test_conn_reconnect) -make_test(test_conn_exec_cancel) -make_test(test_conn_echo_stress) -make_test(test_conn_move) -make_test(test_conn_setup) -make_test(test_co_setup) -make_test(test_issue_50) -make_test(test_conversions) -make_test(test_conn_tls) -make_test(test_unix_sockets) -make_test(test_co_unix_sockets) -make_test(test_conn_cancel_after) -make_test(test_conn_sentinel) +make_test(test_conn_quit boost_redis_tests_asio) +make_test(test_conn_exec_retry boost_redis_tests_asio) +make_test(test_conn_exec_error boost_redis_tests_asio) +make_test(test_run boost_redis_tests_asio) +make_test(test_conn_run_cancel boost_redis_tests_asio) +make_test(test_conn_check_health boost_redis_tests_asio) +make_test(test_co_check_health boost_redis_tests_corosio) +make_test(test_conn_exec boost_redis_tests_asio) +make_test(test_conn_push boost_redis_tests_asio) +make_test(test_conn_push2 boost_redis_tests_asio) +make_test(test_conn_monitor boost_redis_tests_asio) +make_test(test_conn_reconnect boost_redis_tests_asio) +make_test(test_conn_exec_cancel boost_redis_tests_asio) +make_test(test_conn_echo_stress boost_redis_tests_asio) +make_test(test_conn_move boost_redis_tests_asio) +make_test(test_conn_setup boost_redis_tests_asio) +make_test(test_co_setup boost_redis_tests_corosio) +make_test(test_issue_50 boost_redis_tests_asio) +make_test(test_conversions boost_redis_tests_asio) +make_test(test_conn_tls boost_redis_tests_asio) +make_test(test_unix_sockets boost_redis_tests_asio) +make_test(test_co_unix_sockets boost_redis_tests_corosio) +make_test(test_conn_cancel_after boost_redis_tests_asio) +make_test(test_conn_sentinel boost_redis_tests_asio) # Coverage set( diff --git a/test/test_co_setup.cpp b/test/test_co_setup.cpp index adde684c..b1f6aae7 100644 --- a/test/test_co_setup.cpp +++ b/test/test_co_setup.cpp @@ -21,15 +21,15 @@ #include #include "common.hpp" +#include "corosio_common.hpp" #include #include #include #include -namespace asio = boost::asio; - using namespace boost::redis; +using namespace boost::redis::test; using namespace std::chrono_literals; namespace capy = boost::capy; namespace corosio = boost::corosio; diff --git a/test/test_conn_exec.cpp b/test/test_conn_exec.cpp index 715d844f..1b472e43 100644 --- a/test/test_conn_exec.cpp +++ b/test/test_conn_exec.cpp @@ -15,7 +15,7 @@ #define BOOST_TEST_MODULE conn_exec #include -#include "common.hpp" +#include "asio_common.hpp" #include diff --git a/test/test_conn_exec_error.cpp b/test/test_conn_exec_error.cpp index b77f1299..ee832e6d 100644 --- a/test/test_conn_exec_error.cpp +++ b/test/test_conn_exec_error.cpp @@ -9,7 +9,7 @@ #define BOOST_TEST_MODULE conn_exec_error #include -#include "common.hpp" +#include "asio_common.hpp" #include #include diff --git a/test/test_conn_quit.cpp b/test/test_conn_quit.cpp index 5a614a37..ffa45057 100644 --- a/test/test_conn_quit.cpp +++ b/test/test_conn_quit.cpp @@ -12,7 +12,7 @@ #define BOOST_TEST_MODULE conn_quit #include -#include "common.hpp" +#include "asio_common.hpp" #include diff --git a/test/test_conn_reconnect.cpp b/test/test_conn_reconnect.cpp index 7740c9b5..e245dc7b 100644 --- a/test/test_conn_reconnect.cpp +++ b/test/test_conn_reconnect.cpp @@ -12,7 +12,7 @@ #define BOOST_TEST_MODULE conn_reconnect #include -#include "common.hpp" +#include "asio_common.hpp" #include diff --git a/test/test_conn_sentinel.cpp b/test/test_conn_sentinel.cpp index f18c1de0..e903ee45 100644 --- a/test/test_conn_sentinel.cpp +++ b/test/test_conn_sentinel.cpp @@ -18,7 +18,7 @@ #include #include -#include "common.hpp" +#include "asio_common.hpp" #include "print_node.hpp" #include diff --git a/test/test_conn_setup.cpp b/test/test_conn_setup.cpp index 9997cb6b..91829419 100644 --- a/test/test_conn_setup.cpp +++ b/test/test_conn_setup.cpp @@ -15,7 +15,7 @@ #include #include -#include "common.hpp" +#include "asio_common.hpp" #include #include diff --git a/test/test_conversions.cpp b/test/test_conversions.cpp index ac684506..53667a8a 100644 --- a/test/test_conversions.cpp +++ b/test/test_conversions.cpp @@ -12,7 +12,7 @@ #define BOOST_TEST_MODULE conversions #include -#include "common.hpp" +#include "asio_common.hpp" namespace net = boost::asio; using boost::redis::connection; From be091f79eab2ba3f24f97e2ed7d10312037fe72e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 22:20:00 +0200 Subject: [PATCH 073/115] Sanitize tests --- test/test_co_setup.cpp | 12 +++++------ test/test_conn_cancel_after.cpp | 6 +++--- test/test_conn_check_health.cpp | 12 +++++------ test/test_conn_echo_stress.cpp | 2 +- test/test_conn_exec_cancel.cpp | 14 ++++++------- test/test_conn_exec_error.cpp | 2 +- test/test_conn_exec_retry.cpp | 6 +++--- test/test_conn_monitor.cpp | 2 +- test/test_conn_move.cpp | 4 ++-- test/test_conn_push.cpp | 10 ++++----- test/test_conn_push2.cpp | 36 ++++++++++++++++----------------- test/test_conn_quit.cpp | 2 +- test/test_conn_run_cancel.cpp | 2 +- test/test_conn_sentinel.cpp | 14 ++++++------- test/test_conn_setup.cpp | 2 +- test/test_conn_tls.cpp | 6 +++--- test/test_unix_sockets.cpp | 10 ++++----- 17 files changed, 71 insertions(+), 71 deletions(-) diff --git a/test/test_co_setup.cpp b/test/test_co_setup.cpp index b1f6aae7..e2541349 100644 --- a/test/test_co_setup.cpp +++ b/test/test_co_setup.cpp @@ -59,7 +59,7 @@ capy::task create_user( cfg.addr.port = port; auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; @@ -93,7 +93,7 @@ capy::task<> test_auth_success() cfg.password = "mypass"; auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; @@ -147,7 +147,7 @@ capy::task<> test_database_index() auto cfg = make_test_config(); cfg.database_index = 2; auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; @@ -180,7 +180,7 @@ capy::task<> test_setup_empty() cfg.use_setup = true; cfg.setup.clear(); auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; @@ -217,7 +217,7 @@ capy::task<> test_setup_hello() cfg.setup.push("HELLO", "3", "AUTH", "myuser", "mypass"); cfg.setup.push("SELECT", 8); auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; @@ -252,7 +252,7 @@ capy::task<> test_setup_no_hello() cfg.setup.clear(); cfg.setup.push("SELECT", 8); auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, std::error_code(capy::error::canceled)); + BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; }; diff --git a/test/test_conn_cancel_after.cpp b/test/test_conn_cancel_after.cpp index b7f35800..9c293083 100644 --- a/test/test_conn_cancel_after.cpp +++ b/test/test_conn_cancel_after.cpp @@ -40,7 +40,7 @@ void test_run() // Call the function with a very short timeout conn.async_run(make_test_config(), asio::cancel_after(1ms, [&](error_code ec) { - BOOST_TEST_EQ(ec, asio::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; })); @@ -63,7 +63,7 @@ void test_exec() // Call the function with a very short timeout. // The connection is not being run, so these can't succeed conn.async_exec(req, ignore, asio::cancel_after(1ms, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, asio::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); exec_finished = true; })); @@ -105,7 +105,7 @@ void test_receive2() // Call the function with a very short timeout. conn.async_receive2(asio::cancel_after(1ms, [&](error_code ec) { - BOOST_TEST_EQ(ec, asio::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); receive_finished = true; })); diff --git a/test/test_conn_check_health.cpp b/test/test_conn_check_health.cpp index 5d53b084..f66ae2e7 100644 --- a/test/test_conn_check_health.cpp +++ b/test/test_conn_check_health.cpp @@ -56,7 +56,7 @@ void test_reconnection() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // This request will complete after the health checker deems the connection @@ -64,7 +64,7 @@ void test_reconnection() // on connection lost). conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { exec1_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); // Execute the second request. This one will succeed after reconnection conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) { @@ -109,7 +109,7 @@ void test_error_code() // if unresponded). conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { exec_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -139,7 +139,7 @@ void test_disabled() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { @@ -225,12 +225,12 @@ class test_flexible { conn1.async_run(cfg, [&](error_code ec) { run1_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); conn2.async_run(cfg, [&](error_code ec) { run2_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // BLPOP will return NIL, so we can't use ignore diff --git a/test/test_conn_echo_stress.cpp b/test/test_conn_echo_stress.cpp index e1455b2a..0dfa065d 100644 --- a/test/test_conn_echo_stress.cpp +++ b/test/test_conn_echo_stress.cpp @@ -121,7 +121,7 @@ BOOST_AUTO_TEST_CASE(echo_stress) bool run_finished = false, subscribe_finished = false; conn.async_run(cfg, logger{logger::level::crit}, [&run_finished](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); std::clog << "async_run finished" << std::endl; }); diff --git a/test/test_conn_exec_cancel.cpp b/test/test_conn_exec_cancel.cpp index 939cd361..0bb49255 100644 --- a/test/test_conn_exec_cancel.cpp +++ b/test/test_conn_exec_cancel.cpp @@ -70,7 +70,7 @@ void test_cancel_pending() req, ignore, net::bind_cancellation_slot(sig.slot(), [&](error_code ec, std::size_t sz) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); BOOST_TEST_EQ(sz, 0u); called = true; })); @@ -116,7 +116,7 @@ void test_cancel_written() // Run the connection conn.async_run(cfg, [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -127,14 +127,14 @@ void test_cancel_written() auto blpop_cb = [&](error_code ec, std::size_t) { req1.reset(); r1.reset(); - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); exec1_finished = true; }; conn.async_exec(*req1, *r1, net::cancel_after(500ms, blpop_cb)); // The first PING will be cancelled, too. Use partial cancellation here. auto req2_cb = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); exec2_finished = true; }; conn.async_exec( @@ -203,7 +203,7 @@ void test_cancel_on_connection_lost_written() // Run the connection auto cfg = make_test_config(); conn.async_run(cfg, [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -219,7 +219,7 @@ void test_cancel_on_connection_lost_written() }); conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); exec1_finished = true; }); @@ -251,7 +251,7 @@ void test_cancel_operation_exec() // Run the connection conn.async_run(make_test_config(), [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); diff --git a/test/test_conn_exec_error.cpp b/test/test_conn_exec_error.cpp index ee832e6d..09c53284 100644 --- a/test/test_conn_exec_error.cpp +++ b/test/test_conn_exec_error.cpp @@ -308,7 +308,7 @@ BOOST_AUTO_TEST_CASE(issue_287_generic_response_error_then_success) bool run_finished = false, exec_finished = false; conn.async_run(cfg, [&](error_code ec) { - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); run_finished = true; }); diff --git a/test/test_conn_exec_retry.cpp b/test/test_conn_exec_retry.cpp index e9d941c5..ff3d9f21 100644 --- a/test/test_conn_exec_retry.cpp +++ b/test/test_conn_exec_retry.cpp @@ -70,13 +70,13 @@ BOOST_AUTO_TEST_CASE(request_cancel_if_unresponded_true) auto c2 = [&](error_code ec, std::size_t) { c2_called = true; std::cout << "c2" << std::endl; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }; auto c1 = [&](error_code ec, std::size_t) { c1_called = true; std::cout << "c1" << std::endl; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }; auto c0 = [&](error_code ec, std::size_t) { @@ -150,7 +150,7 @@ BOOST_AUTO_TEST_CASE(request_cancel_if_unresponded_false) auto c1 = [&](error_code ec, std::size_t) { c1_called = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }; auto c0 = [&](error_code ec, std::size_t) { diff --git a/test/test_conn_monitor.cpp b/test/test_conn_monitor.cpp index 090c5455..0feaac01 100644 --- a/test/test_conn_monitor.cpp +++ b/test/test_conn_monitor.cpp @@ -91,7 +91,7 @@ class test_monitor { // Run the connection conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // Issue the monitor, then start generating traffic diff --git a/test/test_conn_move.cpp b/test/test_conn_move.cpp index e7726d6f..56025faa 100644 --- a/test/test_conn_move.cpp +++ b/test/test_conn_move.cpp @@ -43,7 +43,7 @@ void test_conn_move_construct() // Run the connection conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // Launch a PING @@ -78,7 +78,7 @@ void test_conn_move_assign_while_running() // Run the connection conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // Launch a PING. When it finishes, conn will be moved-from, and conn2 will be valid diff --git a/test/test_conn_push.cpp b/test/test_conn_push.cpp index 0ba3258f..b40f8995 100644 --- a/test/test_conn_push.cpp +++ b/test/test_conn_push.cpp @@ -68,7 +68,7 @@ void test_async_receive_waiting_for_push() }); conn.async_run(make_test_config(), [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -112,7 +112,7 @@ void test_async_receive_push_available() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -165,7 +165,7 @@ void test_sync_receive() }); conn.async_run(make_test_config(), [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -266,7 +266,7 @@ struct test_async_receive_cancelled_on_reconnection_impl { conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -361,7 +361,7 @@ void test_consecutive_receives() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); diff --git a/test/test_conn_push2.cpp b/test/test_conn_push2.cpp index c76191dd..495e742d 100644 --- a/test/test_conn_push2.cpp +++ b/test/test_conn_push2.cpp @@ -79,7 +79,7 @@ void test_async_receive2_waiting_for_push() }); conn.async_run(make_test_config(), [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -123,7 +123,7 @@ void test_async_receive2_push_available() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -155,7 +155,7 @@ void test_async_receive2_batch() // 2. Receive both of them // 3. Check that receive2 has consumed them by calling it again auto on_receive2 = [&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); receive_finished = true; conn.cancel(); }; @@ -173,7 +173,7 @@ void test_async_receive2_batch() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -241,7 +241,7 @@ void test_async_receive2_subsequent_calls() start_subscribe1(); conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -267,7 +267,7 @@ void test_async_receive2_per_operation_cancellation( bool receive_finished = false; conn.async_receive2(net::bind_cancellation_slot(sig.slot(), [&](error_code ec) { - if (!BOOST_TEST_EQ(ec, net::error::operation_aborted)) + if (!BOOST_TEST_EQ(ec, canceled_condition())) std::cerr << "With cancellation type " << name << std::endl; receive_finished = true; })); @@ -290,7 +290,7 @@ void test_async_receive2_connection_cancel() bool receive_finished = false; conn.async_receive2([&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); receive_finished = true; }); @@ -354,7 +354,7 @@ void test_async_receive2_reconnection() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -398,7 +398,7 @@ void test_exec_push_interleaved() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -436,14 +436,14 @@ void test_push_adapter_error() // We cancel receive when run exits conn.async_receive2([&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); receive_finished = true; }); // The request is cancelled because the PING response isn't processed // by the time the error is generated conn.async_exec(req, ignore, [&exec_finished](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); exec_finished = true; }); @@ -483,7 +483,7 @@ void test_push_adapter_error_reconnection() // async_receive2 is cancelled every reconnection cycle conn.async_receive2([&](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); push_received = true; }); @@ -497,12 +497,12 @@ void test_push_adapter_error_reconnection() // The request is cancelled because the PING response isn't processed // by the time the error is generated conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); conn.async_exec(req2, resp, on_exec2); }); conn.async_run(make_test_config(), [&run_finished](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -524,7 +524,7 @@ void test_push_consumer() std::function launch_push_consumer = [&]() { conn.async_receive2([&](error_code ec) { if (ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); push_consumer_finished = true; resp.clear(); return; @@ -592,7 +592,7 @@ void test_push_consumer() conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -658,7 +658,7 @@ void test_unsubscribe() conn.async_exec(req_subscribe, resp_subscribe, on_subscribe); conn.async_run(make_test_config(), [&run_finished](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); @@ -816,7 +816,7 @@ struct test_pubsub_state_restoration_impl { // Start running bool run_finished = false; conn.async_run(make_test_config(), [&run_finished](error_code ec) { - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); run_finished = true; }); diff --git a/test/test_conn_quit.cpp b/test/test_conn_quit.cpp index ffa45057..cd70ebfd 100644 --- a/test/test_conn_quit.cpp +++ b/test/test_conn_quit.cpp @@ -51,7 +51,7 @@ BOOST_AUTO_TEST_CASE(test_async_run_exits) auto c3 = [&](error_code ec, std::size_t) { c3_called = true; std::clog << "c3: " << ec.message() << std::endl; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }; auto c2 = [&](error_code ec, std::size_t) { diff --git a/test/test_conn_run_cancel.cpp b/test/test_conn_run_cancel.cpp index 053c58f5..5f1fe8ae 100644 --- a/test/test_conn_run_cancel.cpp +++ b/test/test_conn_run_cancel.cpp @@ -46,7 +46,7 @@ void test_per_operation_cancellation(std::string_view name, net::cancellation_ty // Run the connection auto run_cb = [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }; conn.async_run(make_test_config(), net::bind_cancellation_slot(sig.slot(), run_cb)); diff --git a/test/test_conn_sentinel.cpp b/test/test_conn_sentinel.cpp index e903ee45..782c66e9 100644 --- a/test/test_conn_sentinel.cpp +++ b/test/test_conn_sentinel.cpp @@ -67,7 +67,7 @@ void test_exec() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -113,7 +113,7 @@ void test_receive() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -171,7 +171,7 @@ void test_reconnect() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -209,7 +209,7 @@ void test_sentinel_not_reachable() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -254,7 +254,7 @@ void test_auth() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -299,7 +299,7 @@ void test_tls() conn.async_run(cfg, {}, [&](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -346,7 +346,7 @@ void test_replica() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); diff --git a/test/test_conn_setup.cpp b/test/test_conn_setup.cpp index 91829419..1eaa7139 100644 --- a/test/test_conn_setup.cpp +++ b/test/test_conn_setup.cpp @@ -54,7 +54,7 @@ void test_auth_success() conn.async_run(cfg, [&](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, asio::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); ioc.run_for(test_timeout); diff --git a/test/test_conn_tls.cpp b/test/test_conn_tls.cpp index 9e588884..aa438386 100644 --- a/test/test_conn_tls.cpp +++ b/test/test_conn_tls.cpp @@ -82,7 +82,7 @@ BOOST_AUTO_TEST_CASE(exec_default_ssl_context) conn.async_run(cfg, {}, [&](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -125,7 +125,7 @@ BOOST_AUTO_TEST_CASE(exec_custom_ssl_context) conn.async_run(cfg, {}, [&](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }); ioc.run_for(test_timeout); @@ -157,7 +157,7 @@ BOOST_AUTO_TEST_CASE(reconnection) // Run the connection conn.async_run(make_test_config(), [&](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_CHECK_EQUAL(ec, canceled_condition()); }); // The PING is the end of the callback chain diff --git a/test/test_unix_sockets.cpp b/test/test_unix_sockets.cpp index ec53b583..eefaa1f4 100644 --- a/test/test_unix_sockets.cpp +++ b/test/test_unix_sockets.cpp @@ -48,7 +48,7 @@ void test_exec() // Run the connection conn.async_run(cfg, {}, [&run_finished](error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // Execute a request @@ -93,7 +93,7 @@ void test_reconnection() // Run the connection conn.async_run(cfg, {}, [&](error_code ec) { run_finished = true; - BOOST_TEST(ec == net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); // The PING is the end of the callback chain @@ -138,13 +138,13 @@ void test_switch_between_transports() auto on_run_tls_2 = [&](error_code ec) { finished = true; std::cout << "Run (TCP/TLS 2) finished\n"; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }; // After UNIX sockets, switch back to TCP/tLS auto on_run_unix = [&](error_code ec) { std::cout << "Run (UNIX) finished\n"; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); // Change to using TCP with TLS again conn.async_run(unix_cfg, {}, on_run_tls_2); @@ -159,7 +159,7 @@ void test_switch_between_transports() // After TCP/TLS, change to UNIX sockets auto on_run_tls_1 = [&](error_code ec) { std::cout << "Run (TCP/TLS 1) finished\n"; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); conn.async_run(unix_cfg, {}, on_run_unix); conn.async_exec(req, res2, [&](error_code ec, std::size_t) { From 20b24fc5a847ce00e6c6d5863354eafa26018d94 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 22:22:13 +0200 Subject: [PATCH 074/115] More cleanup --- test/asio_common.cpp | 3 ++- test/test_conn_reconnect.cpp | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test/asio_common.cpp b/test/asio_common.cpp index cfac9022..70e56b1a 100644 --- a/test/asio_common.cpp +++ b/test/asio_common.cpp @@ -6,6 +6,7 @@ #include #include "asio_common.hpp" +#include "common.hpp" #include #include @@ -69,7 +70,7 @@ void create_user(std::string_view port, std::string_view username, std::string_v conn.async_run(cfg, [&](boost::system::error_code ec) { run_finished = true; - BOOST_TEST_EQ(ec, net::error::operation_aborted); + BOOST_TEST_EQ(ec, canceled_condition()); }); conn.async_exec(req, boost::redis::ignore, [&](boost::system::error_code ec, std::size_t) { diff --git a/test/test_conn_reconnect.cpp b/test/test_conn_reconnect.cpp index e245dc7b..7c58745b 100644 --- a/test/test_conn_reconnect.cpp +++ b/test/test_conn_reconnect.cpp @@ -9,6 +9,8 @@ #include +#include "common.hpp" + #define BOOST_TEST_MODULE conn_reconnect #include @@ -109,7 +111,7 @@ auto async_test_reconnect_timeout() -> net::awaitable std::cout << "ccc" << std::endl; - BOOST_CHECK_EQUAL(ec1, boost::asio::error::operation_aborted); + BOOST_CHECK_EQUAL(ec1, canceled_condition()); } BOOST_AUTO_TEST_CASE(test_reconnect_and_idle) From e07ebeaf77b1a2504dd674ab4cdeed2c1549183d Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 22:54:51 +0200 Subject: [PATCH 075/115] FSM invocation cleanup --- include/boost/redis/impl/co_connection.ipp | 99 ++++++++-------------- 1 file changed, 36 insertions(+), 63 deletions(-) diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index 6ae2596a..bd75508b 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -7,6 +7,7 @@ // #include +#include #include #include #include @@ -45,9 +46,9 @@ #include #include #include +#include #include -#include #include #include #include @@ -66,7 +67,7 @@ inline std::chrono::steady_clock::time_point compute_expiry( : std::chrono::steady_clock::now() + timeout; } -inline cancellation_type token_to_cancel(std::stop_token tok) +inline cancellation_type to_cancel(std::stop_token tok) { return tok.stop_requested() ? cancellation_type::terminal : cancellation_type::none; } @@ -258,7 +259,7 @@ struct co_connection_impl { // Invoke the FSM while (true) { // Invoke the state machine - auto act = fsm.resume(true, st_, token_to_cancel(co_await capy::this_coro::stop_token)); + auto act = fsm.resume(true, st_, to_cancel(co_await capy::this_coro::stop_token)); // Do what the FSM said switch (act.type()) { @@ -283,10 +284,7 @@ struct co_connection_impl { system::error_code ec; while (true) { - receive_action act = fsm.resume( - st_, - ec, - token_to_cancel(co_await capy::this_coro::stop_token)); + receive_action act = fsm.resume(st_, ec, to_cancel(co_await capy::this_coro::stop_token)); switch (act.type) { case receive_action::action_type::setup_cancellation: break; // not required here @@ -306,35 +304,27 @@ struct co_connection_impl { capy::io_task<> exec_one(const request& req, any_adapter resp) { exec_one_fsm fsm{std::move(resp), req.get_expected_responses()}; - system::error_code ec; - std::size_t bytes = 0u; + auto& rdbuff = st_.mpx.get_read_buffer(); - while (true) { - exec_one_action act = fsm.resume( - st_.mpx.get_read_buffer(), - ec, - bytes, - token_to_cancel(co_await capy::this_coro::stop_token)); + // First invocation + auto act = fsm.resume(rdbuff, system::error_code(), 0u, cancellation_type::none); + while (true) { switch (act.type) { - case exec_one_action_type::done: co_return {ec}; + case exec_one_action_type::done: co_return {act.ec}; case exec_one_action_type::write: { - auto [write_ec, write_bytes] = co_await capy::write( - stream_, - capy::make_buffer(req.payload())); - ec = write_ec; - bytes = write_bytes; + auto [ec, bytes] = co_await capy::write(stream_, capy::make_buffer(req.payload())); + act = fsm.resume(rdbuff, ec, bytes, to_cancel(co_await capy::this_coro::stop_token)); break; } case exec_one_action_type::read_some: { // https://github.com/cppalliance/capy/issues/147 - auto buff = st_.mpx.get_read_buffer().get_prepared(); - auto [read_ec, read_bytes] = co_await stream_.read_some( + auto buff = rdbuff.get_prepared(); + auto [ec, bytes] = co_await stream_.read_some( capy::mutable_buffer(buff.data(), buff.size())); - ec = read_ec; - bytes = read_bytes; + act = fsm.resume(rdbuff, ec, bytes, to_cancel(co_await capy::this_coro::stop_token)); break; } } @@ -345,30 +335,25 @@ struct co_connection_impl { { // Setup sentinel_resolve_fsm fsm; - system::error_code ec; + auto act = fsm.resume(st_, system::error_code(), cancellation_type::none); while (true) { - sentinel_action act = fsm.resume( - st_, - ec, - token_to_cancel(co_await capy::this_coro::stop_token)); - switch (act.get_type()) { case sentinel_action::type::done: co_return {act.error()}; case sentinel_action::type::connect: { - auto [connect_ec] = co_await stream_.connect( + auto [ec] = co_await stream_.connect( make_sentinel_connect_params(st_.cfg, act.connect_addr()), st_.logger); - ec = connect_ec; + act = fsm.resume(st_, ec, to_cancel(co_await capy::this_coro::stop_token)); break; } case sentinel_action::type::request: { - auto [request_ec] = co_await capy::timeout( + auto [ec] = co_await capy::timeout( exec_one(st_.cfg.sentinel.setup, make_sentinel_adapter(st_)), st_.cfg.sentinel.request_timeout); - ec = request_ec; + act = fsm.resume(st_, ec, to_cancel(co_await capy::this_coro::stop_token)); break; } } @@ -380,33 +365,24 @@ struct co_connection_impl { { // Setup. writer_fsm fsm{capy::cond::timeout}; - system::error_code ec; - std::size_t bytes_written = 0u; + auto act = fsm.resume(st_, system::error_code(), 0u, cancellation_type::none); while (true) { - writer_action act = fsm.resume( - st_, - ec, - bytes_written, - token_to_cancel(co_await capy::this_coro::stop_token)); - switch (act.type()) { case writer_action_type::done: co_return {{}, act.error()}; case writer_action_type::write_some: { - auto [write_ec, write_bytes] = co_await maybe_timeout( + auto [ec, bytes] = co_await maybe_timeout( stream_.write_some(capy::make_buffer(st_.mpx.get_write_buffer())), act.timeout()); - ec = write_ec; - bytes_written = write_bytes; + act = fsm.resume(st_, ec, bytes, to_cancel(co_await capy::this_coro::stop_token)); break; } case writer_action_type::wait: { writer_cv_.expires_at(compute_expiry(act.timeout())); - auto [wait_ec] = co_await writer_cv_.wait(); - ec = wait_ec; - bytes_written = 0u; + auto [ec] = co_await writer_cv_.wait(); + act = fsm.resume(st_, ec, 0u, to_cancel(co_await capy::this_coro::stop_token)); break; } } @@ -416,32 +392,27 @@ struct co_connection_impl { capy::io_task reader() { reader_fsm fsm{capy::cond::timeout}; - std::size_t n = 0u; - system::error_code ec; + auto act = fsm.resume(st_, 0u, system::error_code(), cancellation_type::none); for (;;) { - auto act = fsm.resume(st_, n, ec, token_to_cancel(co_await capy::this_coro::stop_token)); - switch (act.get_type()) { case reader_fsm::action::type::read_some: { // https://github.com/cppalliance/capy/issues/147 auto buff = st_.mpx.get_prepared_read_buffer(); - auto [read_ec, read_bytes] = co_await maybe_timeout( + auto [ec, bytes] = co_await maybe_timeout( stream_.read_some(capy::mutable_buffer(buff.data(), buff.size())), act.timeout()); - ec = read_ec; - n = read_bytes; + act = fsm.resume(st_, bytes, ec, to_cancel(co_await capy::this_coro::stop_token)); break; } case reader_fsm::action::type::notify_push_receiver: { // TODO: re-work this - auto [notify_ec] = co_await controller_.wait_for_space(); - if (notify_ec) - ec = notify_ec; - else + auto [ec] = co_await controller_.wait_for_space(); + if (!ec) controller_.put(act.push_size()); + act = fsm.resume(st_, 0u, ec, to_cancel(co_await capy::this_coro::stop_token)); break; } case reader_fsm::action::type::done: co_return {{}, act.error()}; @@ -461,11 +432,10 @@ struct co_connection_impl { st_.mpx.set_config(cfg); while (true) { - auto act = fsm.resume(st_, ec, token_to_cancel(co_await capy::this_coro::stop_token)); + auto act = fsm.resume(st_, ec, to_cancel(co_await capy::this_coro::stop_token)); switch (act.type) { case run_action_type::done: co_return {act.ec}; - case run_action_type::immediate: break; // no longer required case run_action_type::sentinel_resolve: ec = (co_await sentinel_resolve()).ec; break; case run_action_type::connect: ec = (co_await stream_.connect(make_run_connect_params(st_), st_.logger)).ec; @@ -480,7 +450,10 @@ struct co_connection_impl { result); break; } - case run_action_type::cancel_receive: break; // no longer required + case run_action_type::cancel_receive: + case run_action_type::immediate: + ec = system::error_code(); + break; // no longer required case run_action_type::wait_for_reconnection: reconnect_timer_.expires_after(st_.cfg.reconnect_wait_interval); ec = (co_await reconnect_timer_.wait()).ec; From 6c97811556f762748e5c6960a27ec40bba343186 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 24 Apr 2026 23:19:34 +0200 Subject: [PATCH 076/115] Rework push controller --- .../boost/redis/detail/flow_controller.hpp | 30 ++++++++++++------- include/boost/redis/impl/co_connection.ipp | 12 ++++---- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/include/boost/redis/detail/flow_controller.hpp b/include/boost/redis/detail/flow_controller.hpp index 9eaa8e7a..fe433e0e 100644 --- a/include/boost/redis/detail/flow_controller.hpp +++ b/include/boost/redis/detail/flow_controller.hpp @@ -27,6 +27,7 @@ class flow_controller { flow_controller(std::size_t max_bytes) noexcept : max_bytes_(max_bytes) { + bytes_available_.set(); assert(max_bytes != 0u); } @@ -44,22 +45,31 @@ class flow_controller { co_return {}; } - capy::io_task<> wait_for_space() + bool try_put(std::size_t bytes) { - while (pending_bytes_ >= max_bytes_) { - auto [ec] = co_await bytes_available_.wait(); - if (ec) - co_return {ec}; - } - co_return {}; - } + // Do we have space? + if (!room_available_.is_set()) + return false; - void put(std::size_t bytes) - { + // Add the bytes. We might surpass the limit slightly, but this is OK + // because we've already read the bytes. + // The following messages will wait pending_bytes_ += bytes; if (pending_bytes_ >= max_bytes_) room_available_.clear(); bytes_available_.set(); + + return true; + } + + capy::io_task<> put(std::size_t bytes) + { + while (!try_put(bytes)) { + auto [ec] = co_await room_available_.wait(); + if (ec) + co_return {ec}; + } + co_return {}; } }; diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index bd75508b..3a46c86e 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -408,11 +408,13 @@ struct co_connection_impl { } case reader_fsm::action::type::notify_push_receiver: { - // TODO: re-work this - auto [ec] = co_await controller_.wait_for_space(); - if (!ec) - controller_.put(act.push_size()); - act = fsm.resume(st_, 0u, ec, to_cancel(co_await capy::this_coro::stop_token)); + auto cancel = to_cancel(co_await capy::this_coro::stop_token); + if (controller_.try_put(act.push_size())) { + act = fsm.resume(st_, 0u, system::error_code(), cancel); + } else { + auto [ec] = co_await controller_.put(act.push_size()); + act = fsm.resume(st_, 0u, ec, to_cancel(co_await capy::this_coro::stop_token)); + } break; } case reader_fsm::action::type::done: co_return {{}, act.error()}; From 7c2a652902c1fdf6578580cc8ff2bc619a6040f0 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 11:22:29 +0200 Subject: [PATCH 077/115] Cleanup --- include/boost/redis/impl/co_connection.ipp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index 3a46c86e..ff40d448 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -413,7 +413,7 @@ struct co_connection_impl { act = fsm.resume(st_, 0u, system::error_code(), cancel); } else { auto [ec] = co_await controller_.put(act.push_size()); - act = fsm.resume(st_, 0u, ec, to_cancel(co_await capy::this_coro::stop_token)); + act = fsm.resume(st_, 0u, ec, cancel); } break; } From 4caa0cca0757195262f11a518439d561472f263b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 11:23:27 +0200 Subject: [PATCH 078/115] Copy test push --- test/test_co_push2.cpp | 853 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 853 insertions(+) create mode 100644 test/test_co_push2.cpp diff --git a/test/test_co_push2.cpp b/test/test_co_push2.cpp new file mode 100644 index 00000000..495e742d --- /dev/null +++ b/test/test_co_push2.cpp @@ -0,0 +1,853 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace net = boost::asio; +using namespace boost::redis; +using namespace std::chrono_literals; +using boost::system::error_code; +using resp3::flat_tree; +using resp3::node_view; +using resp3::type; + +// Covers all receive functionality except for the deprecated +// async_receive and receive functions. + +namespace { + +// async_receive2 is outstanding when a push is received +void test_async_receive2_waiting_for_push() +{ + resp3::flat_tree resp; + net::io_context ioc; + connection conn{ioc}; + conn.set_receive_response(resp); + + request req1; + req1.push("PING", "Message1"); + req1.push("SUBSCRIBE", "test_async_receive_waiting_for_push"); + + request req2; + req2.push("PING", "Message2"); + + bool run_finished = false, push_received = false, exec1_finished = false, exec2_finished = false; + + auto on_exec2 = [&](error_code ec2, std::size_t) { + BOOST_TEST_EQ(ec2, error_code()); + exec2_finished = true; + conn.cancel(); + }; + + conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + exec1_finished = true; + }); + + conn.async_receive2([&](error_code ec) { + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + push_received = true; + conn.async_exec(req2, ignore, on_exec2); + }); + + conn.async_run(make_test_config(), [&](error_code ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + run_finished = true; + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(push_received); + BOOST_TEST(exec1_finished); + BOOST_TEST(exec2_finished); + BOOST_TEST(run_finished); +} + +// A push is already available when async_receive2 is called +void test_async_receive2_push_available() +{ + net::io_context ioc; + connection conn{ioc}; + resp3::flat_tree resp; + conn.set_receive_response(resp); + + // SUBSCRIBE doesn't have a response, but causes a push to be delivered. + // Add a PING so the overall request has a response. + // This ensures that when async_exec completes, the push has been delivered + request req; + req.push("SUBSCRIBE", "test_async_receive_push_available"); + req.push("PING", "message"); + + bool push_received = false, exec_finished = false, run_finished = false; + + auto on_receive = [&](error_code ec, std::size_t) { + push_received = true; + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + conn.cancel(); + }; + + conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, error_code()); + conn.async_receive(on_receive); + }); + + conn.async_run(make_test_config(), [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(push_received); + BOOST_TEST(run_finished); +} + +// async_receive2 blocks only once if several messages are received in a batch +void test_async_receive2_batch() +{ + // Setup + net::io_context ioc; + connection conn{ioc}; + resp3::flat_tree resp; + conn.set_receive_response(resp); + + // Cause two messages to be delivered. The PING ensures that + // the pushes have been read when exec completes + request req; + req.push("SUBSCRIBE", "test_async_receive2_batch"); + req.push("SUBSCRIBE", "test_async_receive2_batch"); + req.push("PING", "message"); + + bool receive_finished = false, run_finished = false; + + // 1. Trigger pushes + // 2. Receive both of them + // 3. Check that receive2 has consumed them by calling it again + auto on_receive2 = [&](error_code ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + receive_finished = true; + conn.cancel(); + }; + + auto on_receive1 = [&](error_code ec) { + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 2u); + conn.async_receive2(net::cancel_after(50ms, on_receive2)); + }; + + conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.async_receive2(on_receive1); + }); + + conn.async_run(make_test_config(), [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(receive_finished); + BOOST_TEST(run_finished); +} + +// async_receive2 can be called several times in a row +void test_async_receive2_subsequent_calls() +{ + struct impl { + net::io_context ioc{}; + connection conn{ioc}; + resp3::flat_tree resp{}; + request req{}; + bool receive_finished = false, run_finished = false; + + // Send a SUBSCRIBE, which will trigger a push + void start_subscribe1() + { + conn.async_exec(req, ignore, [this](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + start_receive1(); + }); + } + + // Receive the push + void start_receive1() + { + conn.async_receive2([this](error_code ec) { + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + resp.clear(); + start_subscribe2(); + }); + } + + // Send another SUBSCRIBE, which will trigger another push + void start_subscribe2() + { + conn.async_exec(req, ignore, [this](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + start_receive2(); + }); + } + + // End + void start_receive2() + { + conn.async_receive2([this](error_code ec) { + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + receive_finished = true; + conn.cancel(); + }); + } + + void run() + { + // Setup + conn.set_receive_response(resp); + req.push("SUBSCRIBE", "test_async_receive2_subsequent_calls"); + + start_subscribe1(); + conn.async_run(make_test_config(), [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(receive_finished); + BOOST_TEST(run_finished); + } + }; + + impl{}.run(); +} + +// async_receive2 can be cancelled using per-operation cancellation, +// and supports all cancellation types +void test_async_receive2_per_operation_cancellation( + std::string_view name, + net::cancellation_type_t type) +{ + // Setup + net::io_context ioc; + connection conn{ioc}; + net::cancellation_signal sig; + bool receive_finished = false; + + conn.async_receive2(net::bind_cancellation_slot(sig.slot(), [&](error_code ec) { + if (!BOOST_TEST_EQ(ec, canceled_condition())) + std::cerr << "With cancellation type " << name << std::endl; + receive_finished = true; + })); + + sig.emit(type); + + ioc.run_for(test_timeout); + + if (!BOOST_TEST(receive_finished)) + std::cerr << "With cancellation type " << name << std::endl; +} + +// connection::cancel() cancels async_receive2 +void test_async_receive2_connection_cancel() +{ + // Setup + net::io_context ioc; + connection conn{ioc}; + net::cancellation_signal sig; + bool receive_finished = false; + + conn.async_receive2([&](error_code ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + receive_finished = true; + }); + + conn.cancel(); + + ioc.run_for(test_timeout); + + BOOST_TEST(receive_finished); +} + +// Reconnection doesn't cancel async_receive2 +void test_async_receive2_reconnection() +{ + // Setup + net::io_context ioc; + connection conn{ioc}; + resp3::flat_tree resp; + conn.set_receive_response(resp); + + // Causes the reconnection + request req_quit; + req_quit.push("QUIT"); + + // When this completes, the reconnection has happened + request req_ping; + req_ping.get_config().cancel_if_unresponded = false; + req_ping.push("PING", "test_async_receive2_connection"); + + // Generates a push + request req_subscribe; + req_subscribe.push("SUBSCRIBE", "test_async_receive2_connection"); + + bool exec_finished = false, receive_finished = false, run_finished = false; + + // Launch a receive operation, and in parallel + // 1. Trigger a reconnection + // 2. Wait for the reconnection and check that receive hasn't been cancelled + // 3. Trigger a push to make receive complete + auto on_subscribe = [&](error_code ec, std::size_t) { + // Will finish before receive2 because the command doesn't have a response + BOOST_TEST_EQ(ec, error_code()); + exec_finished = true; + }; + + auto on_ping = [&](error_code ec, std::size_t) { + // Reconnection has already happened here + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_NOT(receive_finished); + conn.async_exec(req_subscribe, ignore, on_subscribe); + }; + + conn.async_exec(req_quit, ignore, [&](error_code, std::size_t) { + conn.async_exec(req_ping, ignore, on_ping); + }); + + conn.async_receive2([&](error_code ec) { + BOOST_TEST_EQ(ec, error_code()); + receive_finished = true; + conn.cancel(); + }); + + conn.async_run(make_test_config(), [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(receive_finished); + BOOST_TEST(run_finished); +} + +// A push may be interleaved between regular responses. +// It is handed to the receive adapter (filtered out). +void test_exec_push_interleaved() +{ + net::io_context ioc; + connection conn{ioc}; + resp3::flat_tree receive_resp; + conn.set_receive_response(receive_resp); + + request req; + req.push("PING", "msg1"); + req.push("SUBSCRIBE", "test_exec_push_interleaved"); + req.push("PING", "msg2"); + + response resp; + + bool exec_finished = false, push_received = false, run_finished = false; + + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "msg1"); + BOOST_TEST_EQ(std::get<1>(resp).value(), "msg2"); + conn.cancel(); + }); + + conn.async_receive2([&](error_code ec) { + push_received = true; + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(receive_resp.get_total_msgs(), 1u); + }); + + conn.async_run(make_test_config(), [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(push_received); + BOOST_TEST(run_finished); +} + +// An adapter that always errors +struct response_error_tag { }; +response_error_tag error_tag_obj; + +struct response_error_adapter { + void on_init() { } + void on_done() { } + void on_node(node_view const&, error_code& ec) { ec = error::incompatible_size; } +}; + +auto boost_redis_adapt(response_error_tag&) { return response_error_adapter{}; } + +// If the push adapter returns an error, the connection is torn down +void test_push_adapter_error() +{ + net::io_context ioc; + connection conn{ioc}; + conn.set_receive_response(error_tag_obj); + + request req; + req.push("PING"); + req.push("SUBSCRIBE", "channel"); + req.push("PING"); + + bool receive_finished = false, exec_finished = false, run_finished = false; + + // We cancel receive when run exits + conn.async_receive2([&](error_code ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + receive_finished = true; + }); + + // The request is cancelled because the PING response isn't processed + // by the time the error is generated + conn.async_exec(req, ignore, [&exec_finished](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, canceled_condition()); + exec_finished = true; + }); + + auto cfg = make_test_config(); + cfg.reconnect_wait_interval = 0s; // so we can validate the generated error + conn.async_run(cfg, [&](error_code ec) { + BOOST_TEST_EQ(ec, error::incompatible_size); + run_finished = true; + conn.cancel(); + }); + + ioc.run_for(test_timeout); + BOOST_TEST(receive_finished); + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); +} + +// A push response error triggers a reconnection +void test_push_adapter_error_reconnection() +{ + net::io_context ioc; + connection conn{ioc}; + conn.set_receive_response(error_tag_obj); + + request req; + req.push("PING"); + req.push("SUBSCRIBE", "channel"); + req.push("PING"); + + request req2; + req2.push("PING", "msg2"); + req2.get_config().cancel_if_unresponded = false; + + response resp; + + bool push_received = false, exec_finished = false, run_finished = false; + + // async_receive2 is cancelled every reconnection cycle + conn.async_receive2([&](error_code ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + push_received = true; + }); + + auto on_exec2 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "msg2"); + exec_finished = true; + conn.cancel(); + }; + + // The request is cancelled because the PING response isn't processed + // by the time the error is generated + conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, canceled_condition()); + conn.async_exec(req2, resp, on_exec2); + }); + + conn.async_run(make_test_config(), [&run_finished](error_code ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + run_finished = true; + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(push_received); + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); +} + +// Tests the usual push consumer pattern that we recommend in the examples +void test_push_consumer() +{ + net::io_context ioc; + connection conn{ioc}; + resp3::flat_tree resp; + bool push_consumer_finished{false}; + + std::function launch_push_consumer = [&]() { + conn.async_receive2([&](error_code ec) { + if (ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + push_consumer_finished = true; + resp.clear(); + return; + } + launch_push_consumer(); + }); + }; + + conn.set_receive_response(resp); + + request req1; + req1.get_config().cancel_on_connection_lost = false; + req1.push("PING", "Message1"); + + request req2; + req2.get_config().cancel_on_connection_lost = false; + req2.push("SUBSCRIBE", "channel"); + + bool exec_finished = false, run_finished = false; + + auto c10 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + exec_finished = true; + conn.cancel(); + }; + auto c9 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.async_exec(req2, ignore, c10); + }; + auto c8 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.async_exec(req1, ignore, c9); + }; + auto c7 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.async_exec(req2, ignore, c8); + }; + auto c6 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.async_exec(req2, ignore, c7); + }; + auto c5 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.async_exec(req1, ignore, c6); + }; + auto c4 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.async_exec(req2, ignore, c5); + }; + auto c3 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.async_exec(req1, ignore, c4); + }; + auto c2 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.async_exec(req2, ignore, c3); + }; + auto c1 = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + conn.async_exec(req2, ignore, c2); + }; + + conn.async_exec(req1, ignore, c1); + launch_push_consumer(); + + conn.async_run(make_test_config(), [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); + BOOST_TEST(push_consumer_finished); +} + +// UNSUBSCRIBE and PUNSUBSCRIBE work +void test_unsubscribe() +{ + net::io_context ioc; + connection conn{ioc}; + + // Subscribe to 3 channels and 2 patterns. Use CLIENT INFO to verify this took effect + request req_subscribe; + req_subscribe.push("SUBSCRIBE", "ch1", "ch2", "ch3"); + req_subscribe.push("PSUBSCRIBE", "ch1*", "ch2*"); + req_subscribe.push("CLIENT", "INFO"); + + // Then, unsubscribe from some of them, and verify again + request req_unsubscribe; + req_unsubscribe.push("UNSUBSCRIBE", "ch1"); + req_unsubscribe.push("PUNSUBSCRIBE", "ch2*"); + req_unsubscribe.push("CLIENT", "INFO"); + + // Finally, ping to verify that the connection is still usable + request req_ping; + req_ping.push("PING", "test_unsubscribe"); + + response resp_subscribe, resp_unsubscribe, resp_ping; + + bool subscribe_finished = false, unsubscribe_finished = false, ping_finished = false, + run_finished = false; + + auto on_ping = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + ping_finished = true; + BOOST_TEST(std::get<0>(resp_ping).has_value()); + BOOST_TEST_EQ(std::get<0>(resp_ping).value(), "test_unsubscribe"); + conn.cancel(); + }; + + auto on_unsubscribe = [&](error_code ec, std::size_t) { + unsubscribe_finished = true; + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST(std::get<0>(resp_unsubscribe).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_unsubscribe).value(), "sub"), "2"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_unsubscribe).value(), "psub"), "1"); + conn.async_exec(req_ping, resp_ping, on_ping); + }; + + auto on_subscribe = [&](error_code ec, std::size_t) { + subscribe_finished = true; + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST(std::get<0>(resp_subscribe).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "sub"), "3"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "psub"), "2"); + conn.async_exec(req_unsubscribe, resp_unsubscribe, on_unsubscribe); + }; + + conn.async_exec(req_subscribe, resp_subscribe, on_subscribe); + + conn.async_run(make_test_config(), [&run_finished](error_code ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + run_finished = true; + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(subscribe_finished); + BOOST_TEST(unsubscribe_finished); + BOOST_TEST(ping_finished); + BOOST_TEST(run_finished); +} + +struct test_pubsub_state_restoration_impl { + net::io_context ioc; + connection conn{ioc}; + request req{}; + response resp_str{}; + flat_tree resp_push{}; + bool exec_finished = false; + + void check_subscriptions() + { + // Checks for the expected subscriptions and patterns after restoration + std::set seen_channels, seen_patterns; + for (auto it = resp_push.begin(); it != resp_push.end();) { + // The root element should be a push + BOOST_TEST_EQ(it->data_type, type::push); + BOOST_TEST_GE(it->aggregate_size, 2u); + BOOST_TEST(++it != resp_push.end()); + + // The next element should be the message type + std::string_view msg_type = it->value; + BOOST_TEST(++it != resp_push.end()); + + // The next element is the channel or pattern + if (msg_type == "subscribe") + seen_channels.insert(it->value); + else if (msg_type == "psubscribe") + seen_patterns.insert(it->value); + + // Skip the rest of the nodes + while (it != resp_push.end() && it->depth != 0u) + ++it; + } + + const std::string_view expected_channels[] = {"ch1", "ch3", "ch5"}; + const std::string_view expected_patterns[] = {"ch1*", "ch3*", "ch4*", "ch8*"}; + + BOOST_TEST_ALL_EQ( + seen_channels.begin(), + seen_channels.end(), + std::begin(expected_channels), + std::end(expected_channels)); + BOOST_TEST_ALL_EQ( + seen_patterns.begin(), + seen_patterns.end(), + std::begin(expected_patterns), + std::end(expected_patterns)); + } + + void sub1() + { + // Subscribe to some channels and patterns + req.clear(); + req.subscribe({"ch1", "ch2", "ch3"}); // active: 1, 2, 3 + req.psubscribe({"ch1*", "ch2*", "ch3*", "ch4*"}); // active: 1, 2, 3, 4 + conn.async_exec(req, ignore, [this](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + unsub(); + }); + } + + void unsub() + { + // Unsubscribe from some channels and patterns. + // Unsubscribing from a channel/pattern that we weren't subscribed to is OK. + req.clear(); + req.unsubscribe({"ch2", "ch1", "ch5"}); // active: 3 + req.punsubscribe({"ch2*", "ch4*", "ch9*"}); // active: 1, 3 + conn.async_exec(req, ignore, [this](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + sub2(); + }); + } + + void sub2() + { + // Subscribe to other channels/patterns. + // Re-subscribing to channels/patterns we unsubscribed from is OK. + // Subscribing to the same channel/pattern twice is OK. + req.clear(); + req.subscribe({"ch1", "ch3", "ch5"}); // active: 1, 3, 5 + req.psubscribe({"ch3*", "ch4*", "ch8*"}); // active: 1, 3, 4, 8 + + // Subscriptions created by push() don't survive reconnection + req.push("SUBSCRIBE", "ch10"); // active: 1, 3, 5, 10 + req.push("PSUBSCRIBE", "ch10*"); // active: 1, 3, 4, 8, 10 + + // Validate that we're subscribed to what we expect + req.push("CLIENT", "INFO"); + + conn.async_exec(req, resp_str, [this](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + + // We are subscribed to 4 channels and 5 patterns + BOOST_TEST(std::get<0>(resp_str).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "sub"), "4"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "psub"), "5"); + + resp_push.clear(); + + quit(); + }); + } + + void quit() + { + req.clear(); + req.push("QUIT"); + + conn.async_exec(req, ignore, [this](error_code, std::size_t) { + // we don't know if this request will complete successfully or not + client_info(); + }); + } + + void client_info() + { + req.clear(); + req.push("CLIENT", "INFO"); + req.get_config().cancel_if_unresponded = false; + + conn.async_exec(req, resp_str, [this](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + + // We are subscribed to 3 channels and 4 patterns (1 of each didn't survive reconnection) + BOOST_TEST(std::get<0>(resp_str).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "sub"), "3"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "psub"), "4"); + + // We have received pushes confirming it + check_subscriptions(); + + exec_finished = true; + conn.cancel(); + }); + } + + void run() + { + conn.set_receive_response(resp_push); + + // Start the request chain + sub1(); + + // Start running + bool run_finished = false; + conn.async_run(make_test_config(), [&run_finished](error_code ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + run_finished = true; + }); + + ioc.run_for(test_timeout); + + // Done + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); + } +}; +void test_pubsub_state_restoration() { test_pubsub_state_restoration_impl{}.run(); } + +} // namespace + +int main() +{ + test_async_receive2_waiting_for_push(); + test_async_receive2_push_available(); + test_async_receive2_batch(); + test_async_receive2_subsequent_calls(); + test_async_receive2_per_operation_cancellation("terminal", net::cancellation_type_t::terminal); + test_async_receive2_per_operation_cancellation("partial", net::cancellation_type_t::partial); + test_async_receive2_per_operation_cancellation("total", net::cancellation_type_t::total); + test_async_receive2_connection_cancel(); + test_async_receive2_reconnection(); + test_exec_push_interleaved(); + test_push_adapter_error(); + test_push_adapter_error_reconnection(); + test_push_consumer(); + test_unsubscribe(); + test_pubsub_state_restoration(); + + return boost::report_errors(); +} From f6f9c00242dbab62288559769126f0225e477b8e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 12:32:49 +0200 Subject: [PATCH 079/115] Fix set_receive_response --- include/boost/redis/co_connection.hpp | 9 ++++++++- include/boost/redis/impl/co_connection.ipp | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/include/boost/redis/co_connection.hpp b/include/boost/redis/co_connection.hpp index abc60fa4..8fd2c06e 100644 --- a/include/boost/redis/co_connection.hpp +++ b/include/boost/redis/co_connection.hpp @@ -9,6 +9,7 @@ #ifndef BOOST_REDIS_CO_CONNECTION_HPP #define BOOST_REDIS_CO_CONNECTION_HPP +#include #include #include #include @@ -313,12 +314,18 @@ class co_connection { capy::io_task<> exec(request const& req, any_adapter adapter); /// Sets the response object of @ref async_receive2 operations. - void set_receive_response(any_adapter resp); + template + void set_receive_response(Response& resp) + { + set_receive_adapter(boost_redis_adapt(resp)); + } /// Returns connection usage information. usage get_usage() const noexcept; private: + void set_receive_adapter(any_adapter adapter); + std::unique_ptr impl_; }; diff --git a/include/boost/redis/impl/co_connection.ipp b/include/boost/redis/impl/co_connection.ipp index ff40d448..30cf6493 100644 --- a/include/boost/redis/impl/co_connection.ipp +++ b/include/boost/redis/impl/co_connection.ipp @@ -488,7 +488,7 @@ capy::io_task<> co_connection::exec(request const& req, any_adapter adapter) return impl_->exec(req, std::move(adapter)); } -void co_connection::set_receive_response(any_adapter resp) +void co_connection::set_receive_adapter(any_adapter resp) { impl_->set_receive_adapter(std::move(resp)); } From 807b9364d1199fd608a8611efa9d59f8f8401492 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 12:32:58 +0200 Subject: [PATCH 080/115] Migrate test_co_push2 --- test/test_co_push2.cpp | 917 +++++++++++++++++------------------------ 1 file changed, 369 insertions(+), 548 deletions(-) diff --git a/test/test_co_push2.cpp b/test/test_co_push2.cpp index 495e742d..e68bae42 100644 --- a/test/test_co_push2.cpp +++ b/test/test_co_push2.cpp @@ -1,312 +1,237 @@ -/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#include +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include #include -#include -#include #include #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include #include "common.hpp" +#include "corosio_common.hpp" -#include -#include -#include #include #include #include #include +#include -namespace net = boost::asio; +namespace capy = boost::capy; using namespace boost::redis; +using namespace boost::redis::test; using namespace std::chrono_literals; -using boost::system::error_code; +using error_code = std::error_code; using resp3::flat_tree; using resp3::node_view; using resp3::type; -// Covers all receive functionality except for the deprecated -// async_receive and receive functions. +// Covers all receive functionality for the new co_connection API. namespace { -// async_receive2 is outstanding when a push is received -void test_async_receive2_waiting_for_push() +// receive() is outstanding when a push is received +capy::task<> test_receive_waiting_for_push() { resp3::flat_tree resp; - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; conn.set_receive_response(resp); request req1; req1.push("PING", "Message1"); - req1.push("SUBSCRIBE", "test_async_receive_waiting_for_push"); + req1.push("SUBSCRIBE", "test_receive_waiting_for_push"); request req2; req2.push("PING", "Message2"); - bool run_finished = false, push_received = false, exec1_finished = false, exec2_finished = false; - - auto on_exec2 = [&](error_code ec2, std::size_t) { - BOOST_TEST_EQ(ec2, error_code()); - exec2_finished = true; - conn.cancel(); + auto exec1_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.exec(req1, ignore); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; }; - conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) { + auto receive_then_exec2_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); BOOST_TEST_EQ(ec, error_code()); - exec1_finished = true; - }); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + + auto [ec2] = co_await conn.exec(req2, ignore); + BOOST_TEST_EQ(ec2, error_code()); + co_return {}; + }; - conn.async_receive2([&](error_code ec) { + auto work_fn = [&]() -> capy::io_task<> { + auto [ec, a, b] = co_await capy::when_all(exec1_fn(), receive_then_exec2_fn()); BOOST_TEST_EQ(ec, error_code()); - BOOST_TEST_EQ(resp.get_total_msgs(), 1u); - push_received = true; - conn.async_exec(req2, ignore, on_exec2); - }); + co_return {}; + }; - conn.async_run(make_test_config(), [&](error_code ec) { + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); BOOST_TEST_EQ(ec, canceled_condition()); - run_finished = true; - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(push_received); - BOOST_TEST(exec1_finished); - BOOST_TEST(exec2_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(work_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Work finished 1st } -// A push is already available when async_receive2 is called -void test_async_receive2_push_available() +// A push is already available when receive() is called +capy::task<> test_receive_push_available() { - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; resp3::flat_tree resp; conn.set_receive_response(resp); // SUBSCRIBE doesn't have a response, but causes a push to be delivered. // Add a PING so the overall request has a response. - // This ensures that when async_exec completes, the push has been delivered + // This ensures that when exec completes, the push has been delivered request req; - req.push("SUBSCRIBE", "test_async_receive_push_available"); + req.push("SUBSCRIBE", "test_receive_push_available"); req.push("PING", "message"); - bool push_received = false, exec_finished = false, run_finished = false; - - auto on_receive = [&](error_code ec, std::size_t) { - push_received = true; + auto exec_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, error_code()); + + auto [ec2] = co_await conn.receive(); + BOOST_TEST_EQ(ec2, error_code()); BOOST_TEST_EQ(resp.get_total_msgs(), 1u); - conn.cancel(); + co_return {}; }; - conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { - exec_finished = true; - BOOST_TEST_EQ(ec, error_code()); - conn.async_receive(on_receive); - }); - - conn.async_run(make_test_config(), [&](error_code ec) { - run_finished = true; + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); BOOST_TEST_EQ(ec, canceled_condition()); - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(push_received); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } -// async_receive2 blocks only once if several messages are received in a batch -void test_async_receive2_batch() +// receive() blocks only once if several messages are received in a batch +capy::task<> test_receive_batch() { - // Setup - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; resp3::flat_tree resp; conn.set_receive_response(resp); // Cause two messages to be delivered. The PING ensures that // the pushes have been read when exec completes request req; - req.push("SUBSCRIBE", "test_async_receive2_batch"); - req.push("SUBSCRIBE", "test_async_receive2_batch"); + req.push("SUBSCRIBE", "test_receive_batch"); + req.push("SUBSCRIBE", "test_receive_batch"); req.push("PING", "message"); - bool receive_finished = false, run_finished = false; - - // 1. Trigger pushes - // 2. Receive both of them - // 3. Check that receive2 has consumed them by calling it again - auto on_receive2 = [&](error_code ec) { - BOOST_TEST_EQ(ec, canceled_condition()); - receive_finished = true; - conn.cancel(); - }; - - auto on_receive1 = [&](error_code ec) { + auto exec_fn = [&]() -> capy::io_task<> { + // 1. Trigger pushes + auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, error_code()); + + // 2. Receive both of them + auto [ec2] = co_await conn.receive(); + BOOST_TEST_EQ(ec2, error_code()); BOOST_TEST_EQ(resp.get_total_msgs(), 2u); - conn.async_receive2(net::cancel_after(50ms, on_receive2)); - }; - conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.async_receive2(on_receive1); - }); + // 3. Check that receive has consumed them by calling it again with a deadline + auto [ec3] = co_await capy::timeout(conn.receive(), 50ms); + BOOST_TEST_EQ(ec3, capy::cond::timeout); + co_return {}; + }; - conn.async_run(make_test_config(), [&](error_code ec) { - run_finished = true; + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); BOOST_TEST_EQ(ec, canceled_condition()); - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(receive_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } -// async_receive2 can be called several times in a row -void test_async_receive2_subsequent_calls() +// receive() can be called several times in a row +capy::task<> test_receive_subsequent_calls() { - struct impl { - net::io_context ioc{}; - connection conn{ioc}; - resp3::flat_tree resp{}; - request req{}; - bool receive_finished = false, run_finished = false; + co_connection conn{co_await capy::this_coro::executor}; + resp3::flat_tree resp; + conn.set_receive_response(resp); + + request req; + req.push("SUBSCRIBE", "test_receive_subsequent_calls"); + auto exec_fn = [&]() -> capy::io_task<> { // Send a SUBSCRIBE, which will trigger a push - void start_subscribe1() - { - conn.async_exec(req, ignore, [this](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - start_receive1(); - }); - } + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, error_code()); // Receive the push - void start_receive1() - { - conn.async_receive2([this](error_code ec) { - BOOST_TEST_EQ(ec, error_code()); - BOOST_TEST_EQ(resp.get_total_msgs(), 1u); - resp.clear(); - start_subscribe2(); - }); - } + auto [ec2] = co_await conn.receive(); + BOOST_TEST_EQ(ec2, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + resp.clear(); // Send another SUBSCRIBE, which will trigger another push - void start_subscribe2() - { - conn.async_exec(req, ignore, [this](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - start_receive2(); - }); - } - - // End - void start_receive2() - { - conn.async_receive2([this](error_code ec) { - BOOST_TEST_EQ(ec, error_code()); - BOOST_TEST_EQ(resp.get_total_msgs(), 1u); - receive_finished = true; - conn.cancel(); - }); - } - - void run() - { - // Setup - conn.set_receive_response(resp); - req.push("SUBSCRIBE", "test_async_receive2_subsequent_calls"); - - start_subscribe1(); - conn.async_run(make_test_config(), [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, canceled_condition()); - }); + auto [ec3] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec3, error_code()); - ioc.run_for(test_timeout); - - BOOST_TEST(receive_finished); - BOOST_TEST(run_finished); - } + // Receive the push + auto [ec4] = co_await conn.receive(); + BOOST_TEST_EQ(ec4, error_code()); + BOOST_TEST_EQ(resp.get_total_msgs(), 1u); + co_return {}; }; - impl{}.run(); -} - -// async_receive2 can be cancelled using per-operation cancellation, -// and supports all cancellation types -void test_async_receive2_per_operation_cancellation( - std::string_view name, - net::cancellation_type_t type) -{ - // Setup - net::io_context ioc; - connection conn{ioc}; - net::cancellation_signal sig; - bool receive_finished = false; - - conn.async_receive2(net::bind_cancellation_slot(sig.slot(), [&](error_code ec) { - if (!BOOST_TEST_EQ(ec, canceled_condition())) - std::cerr << "With cancellation type " << name << std::endl; - receive_finished = true; - })); - - sig.emit(type); - - ioc.run_for(test_timeout); + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; - if (!BOOST_TEST(receive_finished)) - std::cerr << "With cancellation type " << name << std::endl; + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } -// connection::cancel() cancels async_receive2 -void test_async_receive2_connection_cancel() +// receive() can be cancelled via stop token +capy::task<> test_receive_cancellation() { - // Setup - net::io_context ioc; - connection conn{ioc}; - net::cancellation_signal sig; - bool receive_finished = false; + co_connection conn{co_await capy::this_coro::executor}; - conn.async_receive2([&](error_code ec) { + auto receive_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); BOOST_TEST_EQ(ec, canceled_condition()); - receive_finished = true; - }); - - conn.cancel(); + co_return {}; + }; - ioc.run_for(test_timeout); + // RP TODO: use immediate + // RP TODO: check return code + auto trigger_fn = [&]() -> capy::io_task<> { + // Complete immediately with success to cancel siblings + co_return {}; + }; - BOOST_TEST(receive_finished); + auto result = co_await capy::when_any(receive_fn(), trigger_fn()); + BOOST_TEST_EQ(result.index(), 2u); // trigger finished 1st } -// Reconnection doesn't cancel async_receive2 -void test_async_receive2_reconnection() +// Reconnection doesn't cancel receive() +capy::task<> test_receive_reconnection() { - // Setup - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; resp3::flat_tree resp; conn.set_receive_response(resp); @@ -317,59 +242,58 @@ void test_async_receive2_reconnection() // When this completes, the reconnection has happened request req_ping; req_ping.get_config().cancel_if_unresponded = false; - req_ping.push("PING", "test_async_receive2_connection"); + req_ping.push("PING", "test_receive_reconnection"); // Generates a push request req_subscribe; - req_subscribe.push("SUBSCRIBE", "test_async_receive2_connection"); + req_subscribe.push("SUBSCRIBE", "test_receive_reconnection"); - bool exec_finished = false, receive_finished = false, run_finished = false; + bool receive_finished = false; - // Launch a receive operation, and in parallel - // 1. Trigger a reconnection - // 2. Wait for the reconnection and check that receive hasn't been cancelled - // 3. Trigger a push to make receive complete - auto on_subscribe = [&](error_code ec, std::size_t) { - // Will finish before receive2 because the command doesn't have a response + auto receive_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); BOOST_TEST_EQ(ec, error_code()); - exec_finished = true; + receive_finished = true; + co_return {}; }; - auto on_ping = [&](error_code ec, std::size_t) { - // Reconnection has already happened here - BOOST_TEST_EQ(ec, error_code()); + // Trigger a reconnection, then trigger a push to make receive complete + auto trigger_fn = [&]() -> capy::io_task<> { + auto [ec_quit] = co_await conn.exec(req_quit, ignore); + // QUIT may complete with success or an error; we don't care + static_cast(ec_quit); + + // Reconnection has happened by the time PING completes + auto [ec_ping] = co_await conn.exec(req_ping, ignore); + BOOST_TEST_EQ(ec_ping, error_code()); BOOST_TEST_NOT(receive_finished); - conn.async_exec(req_subscribe, ignore, on_subscribe); - }; - conn.async_exec(req_quit, ignore, [&](error_code, std::size_t) { - conn.async_exec(req_ping, ignore, on_ping); - }); + auto [ec_sub] = co_await conn.exec(req_subscribe, ignore); + BOOST_TEST_EQ(ec_sub, error_code()); + co_return {}; + }; - conn.async_receive2([&](error_code ec) { + auto work_fn = [&]() -> capy::io_task<> { + auto [ec, a, b] = co_await capy::when_all(receive_fn(), trigger_fn()); BOOST_TEST_EQ(ec, error_code()); - receive_finished = true; - conn.cancel(); - }); + co_return {}; + }; - conn.async_run(make_test_config(), [&](error_code ec) { - run_finished = true; + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); BOOST_TEST_EQ(ec, canceled_condition()); - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(receive_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(work_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Work finished 1st } // A push may be interleaved between regular responses. // It is handed to the receive adapter (filtered out). -void test_exec_push_interleaved() +capy::task<> test_exec_push_interleaved() { - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; resp3::flat_tree receive_resp; conn.set_receive_response(receive_resp); @@ -380,32 +304,35 @@ void test_exec_push_interleaved() response resp; - bool exec_finished = false, push_received = false, run_finished = false; - - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - exec_finished = true; + auto exec_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.exec(req, resp); BOOST_TEST_EQ(ec, error_code()); BOOST_TEST_EQ(std::get<0>(resp).value(), "msg1"); BOOST_TEST_EQ(std::get<1>(resp).value(), "msg2"); - conn.cancel(); - }); + co_return {}; + }; - conn.async_receive2([&](error_code ec) { - push_received = true; + auto receive_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); BOOST_TEST_EQ(ec, error_code()); BOOST_TEST_EQ(receive_resp.get_total_msgs(), 1u); - }); + co_return {}; + }; - conn.async_run(make_test_config(), [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, canceled_condition()); - }); + auto work_fn = [&]() -> capy::io_task<> { + auto [ec, a, b] = co_await capy::when_all(exec_fn(), receive_fn()); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; - ioc.run_for(test_timeout); + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(push_received); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(work_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Work finished 1st } // An adapter that always errors @@ -421,10 +348,9 @@ struct response_error_adapter { auto boost_redis_adapt(response_error_tag&) { return response_error_adapter{}; } // If the push adapter returns an error, the connection is torn down -void test_push_adapter_error() +capy::task<> test_push_adapter_error() { - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; conn.set_receive_response(error_tag_obj); request req; @@ -432,40 +358,36 @@ void test_push_adapter_error() req.push("SUBSCRIBE", "channel"); req.push("PING"); - bool receive_finished = false, exec_finished = false, run_finished = false; - - // We cancel receive when run exits - conn.async_receive2([&](error_code ec) { + auto receive_fn = [&]() -> capy::io_task<> { + // Will be cancelled by when_any + auto [ec] = co_await conn.receive(); BOOST_TEST_EQ(ec, canceled_condition()); - receive_finished = true; - }); + co_return {}; + }; // The request is cancelled because the PING response isn't processed // by the time the error is generated - conn.async_exec(req, ignore, [&exec_finished](error_code ec, std::size_t) { + auto exec_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, canceled_condition()); - exec_finished = true; - }); + co_return {}; + }; - auto cfg = make_test_config(); - cfg.reconnect_wait_interval = 0s; // so we can validate the generated error - conn.async_run(cfg, [&](error_code ec) { + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_test_config(); + cfg.reconnect_wait_interval = 0s; // so we can validate the generated error + auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, error::incompatible_size); - run_finished = true; - conn.cancel(); - }); - - ioc.run_for(test_timeout); - BOOST_TEST(receive_finished); - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); + co_return {}; + }; + + co_await capy::when_any(receive_fn(), exec_fn(), run_fn()); } // A push response error triggers a reconnection -void test_push_adapter_error_reconnection() +capy::task<> test_push_adapter_error_reconnection() { - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; conn.set_receive_response(error_tag_obj); request req; @@ -479,60 +401,40 @@ void test_push_adapter_error_reconnection() response resp; - bool push_received = false, exec_finished = false, run_finished = false; - - // async_receive2 is cancelled every reconnection cycle - conn.async_receive2([&](error_code ec) { + // receive() will be cancelled by when_any + auto receive_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); BOOST_TEST_EQ(ec, canceled_condition()); - push_received = true; - }); - - auto on_exec2 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - BOOST_TEST_EQ(std::get<0>(resp).value(), "msg2"); - exec_finished = true; - conn.cancel(); + co_return {}; }; // The request is cancelled because the PING response isn't processed - // by the time the error is generated - conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, canceled_condition()); - conn.async_exec(req2, resp, on_exec2); - }); + // by the time the error is generated. The second exec succeeds after reconnection. + auto exec_fn = [&]() -> capy::io_task<> { + auto [ec1] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec1, canceled_condition()); - conn.async_run(make_test_config(), [&run_finished](error_code ec) { - BOOST_TEST_EQ(ec, canceled_condition()); - run_finished = true; - }); + auto [ec2] = co_await conn.exec(req2, resp); + BOOST_TEST_EQ(ec2, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "msg2"); + co_return {}; + }; - ioc.run_for(test_timeout); + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; - BOOST_TEST(push_received); - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(receive_fn(), exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 2u); // exec finished after the receive cancel } // Tests the usual push consumer pattern that we recommend in the examples -void test_push_consumer() +capy::task<> test_push_consumer() { - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; resp3::flat_tree resp; - bool push_consumer_finished{false}; - - std::function launch_push_consumer = [&]() { - conn.async_receive2([&](error_code ec) { - if (ec) { - BOOST_TEST_EQ(ec, canceled_condition()); - push_consumer_finished = true; - resp.clear(); - return; - } - launch_push_consumer(); - }); - }; - conn.set_receive_response(resp); request req1; @@ -543,70 +445,41 @@ void test_push_consumer() req2.get_config().cancel_on_connection_lost = false; req2.push("SUBSCRIBE", "channel"); - bool exec_finished = false, run_finished = false; - - auto c10 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - exec_finished = true; - conn.cancel(); - }; - auto c9 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.async_exec(req2, ignore, c10); - }; - auto c8 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.async_exec(req1, ignore, c9); - }; - auto c7 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.async_exec(req2, ignore, c8); - }; - auto c6 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.async_exec(req2, ignore, c7); - }; - auto c5 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.async_exec(req1, ignore, c6); - }; - auto c4 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.async_exec(req2, ignore, c5); - }; - auto c3 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.async_exec(req1, ignore, c4); - }; - auto c2 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.async_exec(req2, ignore, c3); - }; - auto c1 = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - conn.async_exec(req2, ignore, c2); + auto consumer_fn = [&]() -> capy::io_task<> { + while (true) { + auto [ec] = co_await conn.receive(); + resp.clear(); + if (ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + } + } }; - conn.async_exec(req1, ignore, c1); - launch_push_consumer(); + auto exec_fn = [&]() -> capy::io_task<> { + const request* sequence[] = + {&req1, &req2, &req2, &req1, &req2, &req1, &req2, &req2, &req1, &req2}; + for (const auto* r : sequence) { + auto [ec] = co_await conn.exec(*r, ignore); + BOOST_TEST_EQ(ec, error_code()); + } + co_return {}; + }; - conn.async_run(make_test_config(), [&](error_code ec) { - run_finished = true; + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); BOOST_TEST_EQ(ec, canceled_condition()); - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); - BOOST_TEST(push_consumer_finished); + auto result = co_await capy::when_any(consumer_fn(), exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 2u); // exec finished 1st } // UNSUBSCRIBE and PUNSUBSCRIBE work -void test_unsubscribe() +capy::task<> test_unsubscribe() { - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; // Subscribe to 3 channels and 2 patterns. Use CLIENT INFO to verify this took effect request req_subscribe; @@ -626,125 +499,100 @@ void test_unsubscribe() response resp_subscribe, resp_unsubscribe, resp_ping; - bool subscribe_finished = false, unsubscribe_finished = false, ping_finished = false, - run_finished = false; - - auto on_ping = [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - ping_finished = true; - BOOST_TEST(std::get<0>(resp_ping).has_value()); - BOOST_TEST_EQ(std::get<0>(resp_ping).value(), "test_unsubscribe"); - conn.cancel(); - }; + auto exec_fn = [&]() -> capy::io_task<> { + auto [ec_sub] = co_await conn.exec(req_subscribe, resp_subscribe); + BOOST_TEST_EQ(ec_sub, error_code()); + BOOST_TEST(std::get<0>(resp_subscribe).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "sub"), "3"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "psub"), "2"); - auto on_unsubscribe = [&](error_code ec, std::size_t) { - unsubscribe_finished = true; - BOOST_TEST_EQ(ec, error_code()); + auto [ec_unsub] = co_await conn.exec(req_unsubscribe, resp_unsubscribe); + BOOST_TEST_EQ(ec_unsub, error_code()); BOOST_TEST(std::get<0>(resp_unsubscribe).has_value()); BOOST_TEST_EQ(find_client_info(std::get<0>(resp_unsubscribe).value(), "sub"), "2"); BOOST_TEST_EQ(find_client_info(std::get<0>(resp_unsubscribe).value(), "psub"), "1"); - conn.async_exec(req_ping, resp_ping, on_ping); - }; - auto on_subscribe = [&](error_code ec, std::size_t) { - subscribe_finished = true; - BOOST_TEST_EQ(ec, error_code()); - BOOST_TEST(std::get<0>(resp_subscribe).has_value()); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "sub"), "3"); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "psub"), "2"); - conn.async_exec(req_unsubscribe, resp_unsubscribe, on_unsubscribe); + auto [ec_ping] = co_await conn.exec(req_ping, resp_ping); + BOOST_TEST_EQ(ec_ping, error_code()); + BOOST_TEST(std::get<0>(resp_ping).has_value()); + BOOST_TEST_EQ(std::get<0>(resp_ping).value(), "test_unsubscribe"); + co_return {}; }; - conn.async_exec(req_subscribe, resp_subscribe, on_subscribe); - - conn.async_run(make_test_config(), [&run_finished](error_code ec) { + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); BOOST_TEST_EQ(ec, canceled_condition()); - run_finished = true; - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(subscribe_finished); - BOOST_TEST(unsubscribe_finished); - BOOST_TEST(ping_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } -struct test_pubsub_state_restoration_impl { - net::io_context ioc; - connection conn{ioc}; - request req{}; - response resp_str{}; - flat_tree resp_push{}; - bool exec_finished = false; - - void check_subscriptions() - { - // Checks for the expected subscriptions and patterns after restoration - std::set seen_channels, seen_patterns; - for (auto it = resp_push.begin(); it != resp_push.end();) { - // The root element should be a push - BOOST_TEST_EQ(it->data_type, type::push); - BOOST_TEST_GE(it->aggregate_size, 2u); - BOOST_TEST(++it != resp_push.end()); - - // The next element should be the message type - std::string_view msg_type = it->value; - BOOST_TEST(++it != resp_push.end()); - - // The next element is the channel or pattern - if (msg_type == "subscribe") - seen_channels.insert(it->value); - else if (msg_type == "psubscribe") - seen_patterns.insert(it->value); - - // Skip the rest of the nodes - while (it != resp_push.end() && it->depth != 0u) - ++it; - } - - const std::string_view expected_channels[] = {"ch1", "ch3", "ch5"}; - const std::string_view expected_patterns[] = {"ch1*", "ch3*", "ch4*", "ch8*"}; - - BOOST_TEST_ALL_EQ( - seen_channels.begin(), - seen_channels.end(), - std::begin(expected_channels), - std::end(expected_channels)); - BOOST_TEST_ALL_EQ( - seen_patterns.begin(), - seen_patterns.end(), - std::begin(expected_patterns), - std::end(expected_patterns)); +void check_subscriptions(flat_tree const& resp_push) +{ + // Checks for the expected subscriptions and patterns after restoration + std::set seen_channels, seen_patterns; + for (auto it = resp_push.begin(); it != resp_push.end();) { + // The root element should be a push + BOOST_TEST_EQ(it->data_type, type::push); + BOOST_TEST_GE(it->aggregate_size, 2u); + BOOST_TEST(++it != resp_push.end()); + + // The next element should be the message type + std::string_view msg_type = it->value; + BOOST_TEST(++it != resp_push.end()); + + // The next element is the channel or pattern + if (msg_type == "subscribe") + seen_channels.insert(it->value); + else if (msg_type == "psubscribe") + seen_patterns.insert(it->value); + + // Skip the rest of the nodes + while (it != resp_push.end() && it->depth != 0u) + ++it; } - void sub1() - { + const std::string_view expected_channels[] = {"ch1", "ch3", "ch5"}; + const std::string_view expected_patterns[] = {"ch1*", "ch3*", "ch4*", "ch8*"}; + + BOOST_TEST_ALL_EQ( + seen_channels.begin(), + seen_channels.end(), + std::begin(expected_channels), + std::end(expected_channels)); + BOOST_TEST_ALL_EQ( + seen_patterns.begin(), + seen_patterns.end(), + std::begin(expected_patterns), + std::end(expected_patterns)); +} + +capy::task<> test_pubsub_state_restoration() +{ + co_connection conn{co_await capy::this_coro::executor}; + request req; + response resp_str; + flat_tree resp_push; + conn.set_receive_response(resp_push); + + auto exec_fn = [&]() -> capy::io_task<> { // Subscribe to some channels and patterns req.clear(); req.subscribe({"ch1", "ch2", "ch3"}); // active: 1, 2, 3 req.psubscribe({"ch1*", "ch2*", "ch3*", "ch4*"}); // active: 1, 2, 3, 4 - conn.async_exec(req, ignore, [this](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - unsub(); - }); - } + auto [ec1] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec1, error_code()); - void unsub() - { // Unsubscribe from some channels and patterns. // Unsubscribing from a channel/pattern that we weren't subscribed to is OK. req.clear(); req.unsubscribe({"ch2", "ch1", "ch5"}); // active: 3 req.punsubscribe({"ch2*", "ch4*", "ch9*"}); // active: 1, 3 - conn.async_exec(req, ignore, [this](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - sub2(); - }); - } + auto [ec2] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec2, error_code()); - void sub2() - { // Subscribe to other channels/patterns. // Re-subscribing to channels/patterns we unsubscribed from is OK. // Subscribing to the same channel/pattern twice is OK. @@ -759,95 +607,68 @@ struct test_pubsub_state_restoration_impl { // Validate that we're subscribed to what we expect req.push("CLIENT", "INFO"); - conn.async_exec(req, resp_str, [this](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - - // We are subscribed to 4 channels and 5 patterns - BOOST_TEST(std::get<0>(resp_str).has_value()); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "sub"), "4"); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "psub"), "5"); + auto [ec3] = co_await conn.exec(req, resp_str); + BOOST_TEST_EQ(ec3, error_code()); - resp_push.clear(); + // We are subscribed to 4 channels and 5 patterns + BOOST_TEST(std::get<0>(resp_str).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "sub"), "4"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "psub"), "5"); - quit(); - }); - } + resp_push.clear(); - void quit() - { + // Trigger a reconnection req.clear(); req.push("QUIT"); + auto result = co_await conn.exec(req, ignore); + static_cast(result); + // we don't know if this request will complete successfully or not - conn.async_exec(req, ignore, [this](error_code, std::size_t) { - // we don't know if this request will complete successfully or not - client_info(); - }); - } - - void client_info() - { + // Verify state after reconnection req.clear(); req.push("CLIENT", "INFO"); req.get_config().cancel_if_unresponded = false; - conn.async_exec(req, resp_str, [this](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - - // We are subscribed to 3 channels and 4 patterns (1 of each didn't survive reconnection) - BOOST_TEST(std::get<0>(resp_str).has_value()); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "sub"), "3"); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "psub"), "4"); - - // We have received pushes confirming it - check_subscriptions(); + auto [ec4] = co_await conn.exec(req, resp_str); + BOOST_TEST_EQ(ec4, error_code()); - exec_finished = true; - conn.cancel(); - }); - } - - void run() - { - conn.set_receive_response(resp_push); + // We are subscribed to 3 channels and 4 patterns (1 of each didn't survive reconnection) + BOOST_TEST(std::get<0>(resp_str).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "sub"), "3"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "psub"), "4"); - // Start the request chain - sub1(); + // We have received pushes confirming it + check_subscriptions(resp_push); - // Start running - bool run_finished = false; - conn.async_run(make_test_config(), [&run_finished](error_code ec) { - BOOST_TEST_EQ(ec, canceled_condition()); - run_finished = true; - }); + co_return {}; + }; - ioc.run_for(test_timeout); + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; - // Done - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); - } -}; -void test_pubsub_state_restoration() { test_pubsub_state_restoration_impl{}.run(); } + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} } // namespace int main() { - test_async_receive2_waiting_for_push(); - test_async_receive2_push_available(); - test_async_receive2_batch(); - test_async_receive2_subsequent_calls(); - test_async_receive2_per_operation_cancellation("terminal", net::cancellation_type_t::terminal); - test_async_receive2_per_operation_cancellation("partial", net::cancellation_type_t::partial); - test_async_receive2_per_operation_cancellation("total", net::cancellation_type_t::total); - test_async_receive2_connection_cancel(); - test_async_receive2_reconnection(); - test_exec_push_interleaved(); - test_push_adapter_error(); - test_push_adapter_error_reconnection(); - test_push_consumer(); - test_unsubscribe(); - test_pubsub_state_restoration(); + run_coroutine_test(test_receive_waiting_for_push()); + run_coroutine_test(test_receive_push_available()); + run_coroutine_test(test_receive_batch()); + run_coroutine_test(test_receive_subsequent_calls()); + run_coroutine_test(test_receive_cancellation()); + run_coroutine_test(test_receive_reconnection()); + run_coroutine_test(test_exec_push_interleaved()); + run_coroutine_test(test_push_adapter_error()); + run_coroutine_test(test_push_adapter_error_reconnection()); + run_coroutine_test(test_push_consumer()); + run_coroutine_test(test_unsubscribe()); + run_coroutine_test(test_pubsub_state_restoration()); return boost::report_errors(); } From ff96a3d74ec7b68fa42413033e388943df4d937d Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 12:34:27 +0200 Subject: [PATCH 081/115] Build fixes --- include/boost/redis/co_connection.hpp | 2 +- test/test_co_push2.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/boost/redis/co_connection.hpp b/include/boost/redis/co_connection.hpp index 8fd2c06e..63f90136 100644 --- a/include/boost/redis/co_connection.hpp +++ b/include/boost/redis/co_connection.hpp @@ -317,7 +317,7 @@ class co_connection { template void set_receive_response(Response& resp) { - set_receive_adapter(boost_redis_adapt(resp)); + set_receive_adapter(any_adapter(resp)); } /// Returns connection usage information. diff --git a/test/test_co_push2.cpp b/test/test_co_push2.cpp index e68bae42..cbbe8ed2 100644 --- a/test/test_co_push2.cpp +++ b/test/test_co_push2.cpp @@ -150,7 +150,7 @@ capy::task<> test_receive_batch() // 3. Check that receive has consumed them by calling it again with a deadline auto [ec3] = co_await capy::timeout(conn.receive(), 50ms); - BOOST_TEST_EQ(ec3, capy::cond::timeout); + BOOST_TEST_EQ(ec3, condition_wrapper{capy::cond::timeout}); co_return {}; }; From b23ef9b716b1a1e0d1998c963f62e966a4ca8d48 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 12:42:08 +0200 Subject: [PATCH 082/115] Fix receive bug --- include/boost/redis/detail/flow_controller.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/boost/redis/detail/flow_controller.hpp b/include/boost/redis/detail/flow_controller.hpp index fe433e0e..aa156652 100644 --- a/include/boost/redis/detail/flow_controller.hpp +++ b/include/boost/redis/detail/flow_controller.hpp @@ -27,7 +27,7 @@ class flow_controller { flow_controller(std::size_t max_bytes) noexcept : max_bytes_(max_bytes) { - bytes_available_.set(); + room_available_.set(); assert(max_bytes != 0u); } From a7d8d0ba70d7c38cd05f7fcfb909b487ab6f0704 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 12:53:20 +0200 Subject: [PATCH 083/115] Improve test locality --- test/test_co_push2.cpp | 248 ++++++++++++++++++++--------------------- 1 file changed, 119 insertions(+), 129 deletions(-) diff --git a/test/test_co_push2.cpp b/test/test_co_push2.cpp index cbbe8ed2..d24c1fdc 100644 --- a/test/test_co_push2.cpp +++ b/test/test_co_push2.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -51,15 +52,12 @@ capy::task<> test_receive_waiting_for_push() co_connection conn{co_await capy::this_coro::executor}; conn.set_receive_response(resp); - request req1; - req1.push("PING", "Message1"); - req1.push("SUBSCRIBE", "test_receive_waiting_for_push"); - - request req2; - req2.push("PING", "Message2"); - auto exec1_fn = [&]() -> capy::io_task<> { - auto [ec] = co_await conn.exec(req1, ignore); + request req; + req.push("PING", "Message1"); + req.push("SUBSCRIBE", "test_receive_waiting_for_push"); + + auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, error_code()); co_return {}; }; @@ -69,7 +67,10 @@ capy::task<> test_receive_waiting_for_push() BOOST_TEST_EQ(ec, error_code()); BOOST_TEST_EQ(resp.get_total_msgs(), 1u); - auto [ec2] = co_await conn.exec(req2, ignore); + request req; + req.push("PING", "Message2"); + + auto [ec2] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec2, error_code()); co_return {}; }; @@ -97,14 +98,14 @@ capy::task<> test_receive_push_available() resp3::flat_tree resp; conn.set_receive_response(resp); - // SUBSCRIBE doesn't have a response, but causes a push to be delivered. - // Add a PING so the overall request has a response. - // This ensures that when exec completes, the push has been delivered - request req; - req.push("SUBSCRIBE", "test_receive_push_available"); - req.push("PING", "message"); - auto exec_fn = [&]() -> capy::io_task<> { + // SUBSCRIBE doesn't have a response, but causes a push to be delivered. + // Add a PING so the overall request has a response. + // This ensures that when exec completes, the push has been delivered + request req; + req.push("SUBSCRIBE", "test_receive_push_available"); + req.push("PING", "message"); + auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, error_code()); @@ -131,15 +132,14 @@ capy::task<> test_receive_batch() resp3::flat_tree resp; conn.set_receive_response(resp); - // Cause two messages to be delivered. The PING ensures that - // the pushes have been read when exec completes - request req; - req.push("SUBSCRIBE", "test_receive_batch"); - req.push("SUBSCRIBE", "test_receive_batch"); - req.push("PING", "message"); - auto exec_fn = [&]() -> capy::io_task<> { // 1. Trigger pushes + // This causes two messages to be delivered. The PING ensures that + // the pushes have been read when exec completes + request req; + req.push("SUBSCRIBE", "test_receive_batch"); + req.push("SUBSCRIBE", "test_receive_batch"); + req.push("PING", "message"); auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, error_code()); @@ -171,11 +171,10 @@ capy::task<> test_receive_subsequent_calls() resp3::flat_tree resp; conn.set_receive_response(resp); - request req; - req.push("SUBSCRIBE", "test_receive_subsequent_calls"); - auto exec_fn = [&]() -> capy::io_task<> { // Send a SUBSCRIBE, which will trigger a push + request req; + req.push("SUBSCRIBE", "test_receive_subsequent_calls"); auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, error_code()); @@ -217,14 +216,7 @@ capy::task<> test_receive_cancellation() co_return {}; }; - // RP TODO: use immediate - // RP TODO: check return code - auto trigger_fn = [&]() -> capy::io_task<> { - // Complete immediately with success to cancel siblings - co_return {}; - }; - - auto result = co_await capy::when_any(receive_fn(), trigger_fn()); + auto result = co_await capy::when_any(receive_fn(), capy::ready()); BOOST_TEST_EQ(result.index(), 2u); // trigger finished 1st } @@ -235,19 +227,6 @@ capy::task<> test_receive_reconnection() resp3::flat_tree resp; conn.set_receive_response(resp); - // Causes the reconnection - request req_quit; - req_quit.push("QUIT"); - - // When this completes, the reconnection has happened - request req_ping; - req_ping.get_config().cancel_if_unresponded = false; - req_ping.push("PING", "test_receive_reconnection"); - - // Generates a push - request req_subscribe; - req_subscribe.push("SUBSCRIBE", "test_receive_reconnection"); - bool receive_finished = false; auto receive_fn = [&]() -> capy::io_task<> { @@ -259,15 +238,23 @@ capy::task<> test_receive_reconnection() // Trigger a reconnection, then trigger a push to make receive complete auto trigger_fn = [&]() -> capy::io_task<> { + // Causes a reconnection + request req_quit; + req_quit.push("QUIT"); auto [ec_quit] = co_await conn.exec(req_quit, ignore); - // QUIT may complete with success or an error; we don't care - static_cast(ec_quit); + static_cast(ec_quit); // QUIT may complete with success or an error; we don't care // Reconnection has happened by the time PING completes + request req_ping; + req_ping.get_config().cancel_if_unresponded = false; + req_ping.push("PING", "test_receive_reconnection"); auto [ec_ping] = co_await conn.exec(req_ping, ignore); BOOST_TEST_EQ(ec_ping, error_code()); BOOST_TEST_NOT(receive_finished); + // Generates a push + request req_subscribe; + req_subscribe.push("SUBSCRIBE", "test_receive_reconnection"); auto [ec_sub] = co_await conn.exec(req_subscribe, ignore); BOOST_TEST_EQ(ec_sub, error_code()); co_return {}; @@ -297,14 +284,14 @@ capy::task<> test_exec_push_interleaved() resp3::flat_tree receive_resp; conn.set_receive_response(receive_resp); - request req; - req.push("PING", "msg1"); - req.push("SUBSCRIBE", "test_exec_push_interleaved"); - req.push("PING", "msg2"); + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING", "msg1"); + req.push("SUBSCRIBE", "test_exec_push_interleaved"); + req.push("PING", "msg2"); - response resp; + response resp; - auto exec_fn = [&]() -> capy::io_task<> { auto [ec] = co_await conn.exec(req, resp); BOOST_TEST_EQ(ec, error_code()); BOOST_TEST_EQ(std::get<0>(resp).value(), "msg1"); @@ -353,11 +340,6 @@ capy::task<> test_push_adapter_error() co_connection conn{co_await capy::this_coro::executor}; conn.set_receive_response(error_tag_obj); - request req; - req.push("PING"); - req.push("SUBSCRIBE", "channel"); - req.push("PING"); - auto receive_fn = [&]() -> capy::io_task<> { // Will be cancelled by when_any auto [ec] = co_await conn.receive(); @@ -368,6 +350,11 @@ capy::task<> test_push_adapter_error() // The request is cancelled because the PING response isn't processed // by the time the error is generated auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING"); + req.push("SUBSCRIBE", "channel"); + req.push("PING"); + auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, canceled_condition()); co_return {}; @@ -390,17 +377,6 @@ capy::task<> test_push_adapter_error_reconnection() co_connection conn{co_await capy::this_coro::executor}; conn.set_receive_response(error_tag_obj); - request req; - req.push("PING"); - req.push("SUBSCRIBE", "channel"); - req.push("PING"); - - request req2; - req2.push("PING", "msg2"); - req2.get_config().cancel_if_unresponded = false; - - response resp; - // receive() will be cancelled by when_any auto receive_fn = [&]() -> capy::io_task<> { auto [ec] = co_await conn.receive(); @@ -408,12 +384,24 @@ capy::task<> test_push_adapter_error_reconnection() co_return {}; }; - // The request is cancelled because the PING response isn't processed - // by the time the error is generated. The second exec succeeds after reconnection. auto exec_fn = [&]() -> capy::io_task<> { + // The request is cancelled because the PING response isn't processed + // by the time the error is generated + request req; + req.push("PING"); + req.push("SUBSCRIBE", "channel"); + req.push("PING"); + auto [ec1] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec1, canceled_condition()); + // This one will succeed after reconnection + request req2; + req2.push("PING", "msg2"); + req2.get_config().cancel_if_unresponded = false; + + response resp; + auto [ec2] = co_await conn.exec(req2, resp); BOOST_TEST_EQ(ec2, error_code()); BOOST_TEST_EQ(std::get<0>(resp).value(), "msg2"); @@ -437,14 +425,6 @@ capy::task<> test_push_consumer() resp3::flat_tree resp; conn.set_receive_response(resp); - request req1; - req1.get_config().cancel_on_connection_lost = false; - req1.push("PING", "Message1"); - - request req2; - req2.get_config().cancel_on_connection_lost = false; - req2.push("SUBSCRIBE", "channel"); - auto consumer_fn = [&]() -> capy::io_task<> { while (true) { auto [ec] = co_await conn.receive(); @@ -457,6 +437,14 @@ capy::task<> test_push_consumer() }; auto exec_fn = [&]() -> capy::io_task<> { + request req1; + req1.get_config().cancel_on_connection_lost = false; + req1.push("PING", "Message1"); + + request req2; + req2.get_config().cancel_on_connection_lost = false; + req2.push("SUBSCRIBE", "channel"); + const request* sequence[] = {&req1, &req2, &req2, &req1, &req2, &req1, &req2, &req2, &req1, &req2}; for (const auto* r : sequence) { @@ -481,37 +469,37 @@ capy::task<> test_unsubscribe() { co_connection conn{co_await capy::this_coro::executor}; - // Subscribe to 3 channels and 2 patterns. Use CLIENT INFO to verify this took effect - request req_subscribe; - req_subscribe.push("SUBSCRIBE", "ch1", "ch2", "ch3"); - req_subscribe.push("PSUBSCRIBE", "ch1*", "ch2*"); - req_subscribe.push("CLIENT", "INFO"); - - // Then, unsubscribe from some of them, and verify again - request req_unsubscribe; - req_unsubscribe.push("UNSUBSCRIBE", "ch1"); - req_unsubscribe.push("PUNSUBSCRIBE", "ch2*"); - req_unsubscribe.push("CLIENT", "INFO"); - - // Finally, ping to verify that the connection is still usable - request req_ping; - req_ping.push("PING", "test_unsubscribe"); + auto exec_fn = [&]() -> capy::io_task<> { + response resp_subscribe, resp_unsubscribe, resp_ping; - response resp_subscribe, resp_unsubscribe, resp_ping; + // Subscribe to 3 channels and 2 patterns. Use CLIENT INFO to verify this took effect + request req_subscribe; + req_subscribe.push("SUBSCRIBE", "ch1", "ch2", "ch3"); + req_subscribe.push("PSUBSCRIBE", "ch1*", "ch2*"); + req_subscribe.push("CLIENT", "INFO"); - auto exec_fn = [&]() -> capy::io_task<> { auto [ec_sub] = co_await conn.exec(req_subscribe, resp_subscribe); BOOST_TEST_EQ(ec_sub, error_code()); BOOST_TEST(std::get<0>(resp_subscribe).has_value()); BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "sub"), "3"); BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "psub"), "2"); + // Then, unsubscribe from some of them, and verify again + request req_unsubscribe; + req_unsubscribe.push("UNSUBSCRIBE", "ch1"); + req_unsubscribe.push("PUNSUBSCRIBE", "ch2*"); + req_unsubscribe.push("CLIENT", "INFO"); + auto [ec_unsub] = co_await conn.exec(req_unsubscribe, resp_unsubscribe); BOOST_TEST_EQ(ec_unsub, error_code()); BOOST_TEST(std::get<0>(resp_unsubscribe).has_value()); BOOST_TEST_EQ(find_client_info(std::get<0>(resp_unsubscribe).value(), "sub"), "2"); BOOST_TEST_EQ(find_client_info(std::get<0>(resp_unsubscribe).value(), "psub"), "1"); + // Finally, ping to verify that the connection is still usable + request req_ping; + req_ping.push("PING", "test_unsubscribe"); + auto [ec_ping] = co_await conn.exec(req_ping, resp_ping); BOOST_TEST_EQ(ec_ping, error_code()); BOOST_TEST(std::get<0>(resp_ping).has_value()); @@ -572,70 +560,72 @@ void check_subscriptions(flat_tree const& resp_push) capy::task<> test_pubsub_state_restoration() { co_connection conn{co_await capy::this_coro::executor}; - request req; - response resp_str; flat_tree resp_push; conn.set_receive_response(resp_push); auto exec_fn = [&]() -> capy::io_task<> { // Subscribe to some channels and patterns - req.clear(); - req.subscribe({"ch1", "ch2", "ch3"}); // active: 1, 2, 3 - req.psubscribe({"ch1*", "ch2*", "ch3*", "ch4*"}); // active: 1, 2, 3, 4 - auto [ec1] = co_await conn.exec(req, ignore); + request req1; + req1.subscribe({"ch1", "ch2", "ch3"}); // active: 1, 2, 3 + req1.psubscribe({"ch1*", "ch2*", "ch3*", "ch4*"}); // active: 1, 2, 3, 4 + auto [ec1] = co_await conn.exec(req1, ignore); BOOST_TEST_EQ(ec1, error_code()); // Unsubscribe from some channels and patterns. // Unsubscribing from a channel/pattern that we weren't subscribed to is OK. - req.clear(); - req.unsubscribe({"ch2", "ch1", "ch5"}); // active: 3 - req.punsubscribe({"ch2*", "ch4*", "ch9*"}); // active: 1, 3 - auto [ec2] = co_await conn.exec(req, ignore); + request req2; + req2.unsubscribe({"ch2", "ch1", "ch5"}); // active: 3 + req2.punsubscribe({"ch2*", "ch4*", "ch9*"}); // active: 1, 3 + auto [ec2] = co_await conn.exec(req2, ignore); BOOST_TEST_EQ(ec2, error_code()); // Subscribe to other channels/patterns. // Re-subscribing to channels/patterns we unsubscribed from is OK. // Subscribing to the same channel/pattern twice is OK. - req.clear(); - req.subscribe({"ch1", "ch3", "ch5"}); // active: 1, 3, 5 - req.psubscribe({"ch3*", "ch4*", "ch8*"}); // active: 1, 3, 4, 8 + request req3; + req3.subscribe({"ch1", "ch3", "ch5"}); // active: 1, 3, 5 + req3.psubscribe({"ch3*", "ch4*", "ch8*"}); // active: 1, 3, 4, 8 // Subscriptions created by push() don't survive reconnection - req.push("SUBSCRIBE", "ch10"); // active: 1, 3, 5, 10 - req.push("PSUBSCRIBE", "ch10*"); // active: 1, 3, 4, 8, 10 + req3.push("SUBSCRIBE", "ch10"); // active: 1, 3, 5, 10 + req3.push("PSUBSCRIBE", "ch10*"); // active: 1, 3, 4, 8, 10 // Validate that we're subscribed to what we expect - req.push("CLIENT", "INFO"); + req3.push("CLIENT", "INFO"); + + response resp3; - auto [ec3] = co_await conn.exec(req, resp_str); + auto [ec3] = co_await conn.exec(req3, resp3); BOOST_TEST_EQ(ec3, error_code()); // We are subscribed to 4 channels and 5 patterns - BOOST_TEST(std::get<0>(resp_str).has_value()); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "sub"), "4"); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "psub"), "5"); + BOOST_TEST(std::get<0>(resp3).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp3).value(), "sub"), "4"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp3).value(), "psub"), "5"); resp_push.clear(); // Trigger a reconnection - req.clear(); - req.push("QUIT"); - auto result = co_await conn.exec(req, ignore); + request req4; + req4.push("QUIT"); + auto result = co_await conn.exec(req4, ignore); static_cast(result); // we don't know if this request will complete successfully or not // Verify state after reconnection - req.clear(); - req.push("CLIENT", "INFO"); - req.get_config().cancel_if_unresponded = false; + request req5; + req5.push("CLIENT", "INFO"); + req5.get_config().cancel_if_unresponded = false; - auto [ec4] = co_await conn.exec(req, resp_str); - BOOST_TEST_EQ(ec4, error_code()); + response resp5; + + auto [ec5] = co_await conn.exec(req5, resp5); + BOOST_TEST_EQ(ec5, error_code()); // We are subscribed to 3 channels and 4 patterns (1 of each didn't survive reconnection) - BOOST_TEST(std::get<0>(resp_str).has_value()); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "sub"), "3"); - BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "psub"), "4"); + BOOST_TEST(std::get<0>(resp5).has_value()); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp5).value(), "sub"), "3"); + BOOST_TEST_EQ(find_client_info(std::get<0>(resp5).value(), "psub"), "4"); // We have received pushes confirming it check_subscriptions(resp_push); From a6ba309b8062d000f00a1afbd8787c02f6370126 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 12:59:25 +0200 Subject: [PATCH 084/115] Copy test_co_exec_cancel --- test/test_co_exec_cancel.cpp | 228 +++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 test/test_co_exec_cancel.cpp diff --git a/test/test_co_exec_cancel.cpp b/test/test_co_exec_cancel.cpp new file mode 100644 index 00000000..ba0042ad --- /dev/null +++ b/test/test_co_exec_cancel.cpp @@ -0,0 +1,228 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" + +#include +#include + +using namespace std::chrono_literals; + +namespace net = boost::asio; +using error_code = boost::system::error_code; +using boost::redis::operation; +using boost::redis::error; +using boost::redis::request; +using boost::redis::response; +using boost::redis::generic_response; +using boost::redis::ignore; +using boost::redis::ignore_t; +using boost::redis::logger; +using boost::redis::connection; +using namespace std::chrono_literals; + +namespace { + +// We can cancel requests that haven't been written yet. +// All cancellation types are supported here. +void test_cancel_pending() +{ + struct { + const char* name; + net::cancellation_type_t cancel_type; + } test_cases[] = { + {"terminal", net::cancellation_type_t::terminal}, + {"partial", net::cancellation_type_t::partial }, + {"total", net::cancellation_type_t::total }, + }; + + for (const auto& tc : test_cases) { + std::cerr << "Running test case: " << tc.name << std::endl; + + // Setup + net::io_context ctx; + connection conn(ctx); + request req; + req.push("get", "mykey"); + + // Issue a request without calling async_run(), so the request stays waiting forever + net::cancellation_signal sig; + bool called = false; + conn.async_exec( + req, + ignore, + net::bind_cancellation_slot(sig.slot(), [&](error_code ec, std::size_t sz) { + BOOST_TEST_EQ(ec, canceled_condition()); + BOOST_TEST_EQ(sz, 0u); + called = true; + })); + + // Issue a cancellation + sig.emit(tc.cancel_type); + + // Prevent the test for deadlocking in case of failure + ctx.run_for(test_timeout); + BOOST_TEST(called); + } +} + +// We can cancel requests that have been written but which +// responses haven't been received yet. +// Terminal and partial cancellation types are supported here. +void test_cancel_written() +{ + // Setup + net::io_context ctx; + connection conn{ctx}; + auto cfg = make_test_config(); + cfg.health_check_interval = std::chrono::seconds::zero(); + bool run_finished = false, exec1_finished = false, exec2_finished = false, + exec3_finished = false; + + // Will be cancelled after it has been written but before the + // response arrives. Create everything in dynamic memory to verify + // we don't try to access things after completion. + auto req1 = std::make_unique(); + req1->push("BLPOP", "any", 1); + auto r1 = std::make_unique>(); + + // Will be cancelled too because it's sent after BLPOP. + // Tests that partial cancellation is supported, too. + request req2; + req2.push("PING", "partial_cancellation"); + + // Will finish successfully once the response to the BLPOP arrives + request req3; + req3.push("PING", "after_blpop"); + response r3; + + // Run the connection + conn.async_run(cfg, [&](error_code ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + run_finished = true; + }); + + // The request will be cancelled before it receives a response. + // Our BLPOP will wait for longer than the timeout we're using. + // Clear allocated memory to check we don't access the request or + // response when the server response arrives. + auto blpop_cb = [&](error_code ec, std::size_t) { + req1.reset(); + r1.reset(); + BOOST_TEST_EQ(ec, canceled_condition()); + exec1_finished = true; + }; + conn.async_exec(*req1, *r1, net::cancel_after(500ms, blpop_cb)); + + // The first PING will be cancelled, too. Use partial cancellation here. + auto req2_cb = [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, canceled_condition()); + exec2_finished = true; + }; + conn.async_exec( + req2, + ignore, + net::cancel_after(500ms, net::cancellation_type_t::partial, req2_cb)); + + // The second PING's response will be received after the BLPOP's response, + // but it will be processed successfully. + conn.async_exec(req3, r3, [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(std::get<0>(r3).value(), "after_blpop"); + conn.cancel(); + exec3_finished = true; + }); + + ctx.run_for(test_timeout); + BOOST_TEST(run_finished); + BOOST_TEST(exec1_finished); + BOOST_TEST(exec2_finished); + BOOST_TEST(exec3_finished); +} + +// connection::cancel(operation::exec) works. Pending requests are cancelled, +// but written requests are not +void test_cancel_operation_exec() +{ + // Setup + net::io_context ctx; + connection conn{ctx}; + bool run_finished = false, exec0_finished = false, exec1_finished = false, + exec2_finished = false; + + request req0; + req0.push("PING", "before_blpop"); + + request req1; + req1.push("BLPOP", "any", 1); + generic_response r1; + + request req2; + req2.push("PING", "after_blpop"); + + // Run the connection + conn.async_run(make_test_config(), [&](error_code ec) { + BOOST_TEST_EQ(ec, canceled_condition()); + run_finished = true; + }); + + // Execute req0 and req1. They will be coalesced together. + // When req0 completes, we know that req1 will be waiting its response + conn.async_exec(req0, ignore, [&](error_code ec, std::size_t) { + BOOST_TEST_EQ(ec, error_code()); + exec0_finished = true; + conn.cancel(operation::exec); + }); + + // By default, ignore will issue an error when a NULL is received. + // ATM, this causes the connection to be torn down. Using a generic_response avoids this. + // See https://github.com/boostorg/redis/issues/314 + conn.async_exec(req1, r1, [&](error_code ec, std::size_t) { + // No error should occur since the cancellation should be ignored + std::cout << "async_exec (1): " << ec.message() << std::endl; + BOOST_TEST_EQ(ec, error_code()); + exec1_finished = true; + + // The connection remains usable + conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) { + BOOST_TEST_EQ(ec2, error_code()); + exec2_finished = true; + conn.cancel(); + }); + }); + + ctx.run_for(test_timeout); + BOOST_TEST(run_finished); + BOOST_TEST(exec0_finished); + BOOST_TEST(exec1_finished); + BOOST_TEST(exec2_finished); +} + +} // namespace + +int main() +{ + test_cancel_pending(); + test_cancel_written(); + test_cancel_operation_exec(); + + return boost::report_errors(); +} From b94d68989d88c60f7307c50a14b42acee90b7205 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 13:09:21 +0200 Subject: [PATCH 085/115] co_exec_cance --- test/test_co_exec_cancel.cpp | 262 ++++++++++------------------------- 1 file changed, 71 insertions(+), 191 deletions(-) diff --git a/test/test_co_exec_cancel.cpp b/test/test_co_exec_cancel.cpp index ba0042ad..54212077 100644 --- a/test/test_co_exec_cancel.cpp +++ b/test/test_co_exec_cancel.cpp @@ -1,228 +1,108 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#include +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include #include #include #include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include -#include #include "common.hpp" +#include "corosio_common.hpp" -#include -#include - -using namespace std::chrono_literals; +#include +#include +#include +#include -namespace net = boost::asio; -using error_code = boost::system::error_code; -using boost::redis::operation; -using boost::redis::error; -using boost::redis::request; -using boost::redis::response; -using boost::redis::generic_response; -using boost::redis::ignore; -using boost::redis::ignore_t; -using boost::redis::logger; -using boost::redis::connection; +namespace capy = boost::capy; +using namespace boost::redis; +using namespace boost::redis::test; using namespace std::chrono_literals; +using error_code = std::error_code; namespace { -// We can cancel requests that haven't been written yet. -// All cancellation types are supported here. -void test_cancel_pending() +// We can cancel requests that haven't been written yet +capy::task<> test_cancel_pending() { - struct { - const char* name; - net::cancellation_type_t cancel_type; - } test_cases[] = { - {"terminal", net::cancellation_type_t::terminal}, - {"partial", net::cancellation_type_t::partial }, - {"total", net::cancellation_type_t::total }, - }; - - for (const auto& tc : test_cases) { - std::cerr << "Running test case: " << tc.name << std::endl; + co_connection conn{co_await capy::this_coro::executor}; - // Setup - net::io_context ctx; - connection conn(ctx); + // Issue a request without calling run, so the request stays waiting forever + auto exec_fn = [&]() -> capy::io_task<> { request req; req.push("get", "mykey"); + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; - // Issue a request without calling async_run(), so the request stays waiting forever - net::cancellation_signal sig; - bool called = false; - conn.async_exec( - req, - ignore, - net::bind_cancellation_slot(sig.slot(), [&](error_code ec, std::size_t sz) { - BOOST_TEST_EQ(ec, canceled_condition()); - BOOST_TEST_EQ(sz, 0u); - called = true; - })); - - // Issue a cancellation - sig.emit(tc.cancel_type); - - // Prevent the test for deadlocking in case of failure - ctx.run_for(test_timeout); - BOOST_TEST(called); - } + auto result = co_await capy::when_any(exec_fn(), capy::ready()); + BOOST_TEST_EQ(result.index(), 2u); // Trigger finished 1st } -// We can cancel requests that have been written but which +// We can cancel requests that have been written but whose // responses haven't been received yet. -// Terminal and partial cancellation types are supported here. -void test_cancel_written() +capy::task<> test_cancel_written() { - // Setup - net::io_context ctx; - connection conn{ctx}; - auto cfg = make_test_config(); - cfg.health_check_interval = std::chrono::seconds::zero(); - bool run_finished = false, exec1_finished = false, exec2_finished = false, - exec3_finished = false; - - // Will be cancelled after it has been written but before the - // response arrives. Create everything in dynamic memory to verify - // we don't try to access things after completion. - auto req1 = std::make_unique(); - req1->push("BLPOP", "any", 1); - auto r1 = std::make_unique>(); - - // Will be cancelled too because it's sent after BLPOP. - // Tests that partial cancellation is supported, too. - request req2; - req2.push("PING", "partial_cancellation"); - - // Will finish successfully once the response to the BLPOP arrives - request req3; - req3.push("PING", "after_blpop"); - response r3; - - // Run the connection - conn.async_run(cfg, [&](error_code ec) { - BOOST_TEST_EQ(ec, canceled_condition()); - run_finished = true; - }); - - // The request will be cancelled before it receives a response. - // Our BLPOP will wait for longer than the timeout we're using. - // Clear allocated memory to check we don't access the request or - // response when the server response arrives. - auto blpop_cb = [&](error_code ec, std::size_t) { + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + // Will be cancelled after it has been written but before the response arrives. + // Our BLPOP will block server-side for longer than the deadline below. + auto req1 = std::make_unique(); + req1->push("BLPOP", "any", 1); + auto resp1 = std::make_unique>(); + auto [ec1] = co_await capy::timeout(conn.exec(*req1, *resp1), 500ms); + BOOST_TEST_EQ(ec1, condition_wrapper{capy::cond::timeout}); + + // Destroy request and response to verify that we don't reference them after cancellation req1.reset(); - r1.reset(); - BOOST_TEST_EQ(ec, canceled_condition()); - exec1_finished = true; + resp1.reset(); + + // The connection remains usable. The PING's response will be received + // after the BLPOP's response, but it will be processed successfully. + request req2; + req2.push("PING", "after_blpop"); + response resp2; + auto [ec2] = co_await conn.exec(req2, resp2); + BOOST_TEST_EQ(ec2, error_code()); + BOOST_TEST_EQ(std::get<0>(resp2).value(), "after_blpop"); + co_return {}; }; - conn.async_exec(*req1, *r1, net::cancel_after(500ms, blpop_cb)); - // The first PING will be cancelled, too. Use partial cancellation here. - auto req2_cb = [&](error_code ec, std::size_t) { + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_test_config(); + cfg.health_check_interval = 0s; + + auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, canceled_condition()); - exec2_finished = true; + co_return {}; }; - conn.async_exec( - req2, - ignore, - net::cancel_after(500ms, net::cancellation_type_t::partial, req2_cb)); - - // The second PING's response will be received after the BLPOP's response, - // but it will be processed successfully. - conn.async_exec(req3, r3, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - BOOST_TEST_EQ(std::get<0>(r3).value(), "after_blpop"); - conn.cancel(); - exec3_finished = true; - }); - - ctx.run_for(test_timeout); - BOOST_TEST(run_finished); - BOOST_TEST(exec1_finished); - BOOST_TEST(exec2_finished); - BOOST_TEST(exec3_finished); -} - -// connection::cancel(operation::exec) works. Pending requests are cancelled, -// but written requests are not -void test_cancel_operation_exec() -{ - // Setup - net::io_context ctx; - connection conn{ctx}; - bool run_finished = false, exec0_finished = false, exec1_finished = false, - exec2_finished = false; - request req0; - req0.push("PING", "before_blpop"); - - request req1; - req1.push("BLPOP", "any", 1); - generic_response r1; - - request req2; - req2.push("PING", "after_blpop"); - - // Run the connection - conn.async_run(make_test_config(), [&](error_code ec) { - BOOST_TEST_EQ(ec, canceled_condition()); - run_finished = true; - }); - - // Execute req0 and req1. They will be coalesced together. - // When req0 completes, we know that req1 will be waiting its response - conn.async_exec(req0, ignore, [&](error_code ec, std::size_t) { - BOOST_TEST_EQ(ec, error_code()); - exec0_finished = true; - conn.cancel(operation::exec); - }); - - // By default, ignore will issue an error when a NULL is received. - // ATM, this causes the connection to be torn down. Using a generic_response avoids this. - // See https://github.com/boostorg/redis/issues/314 - conn.async_exec(req1, r1, [&](error_code ec, std::size_t) { - // No error should occur since the cancellation should be ignored - std::cout << "async_exec (1): " << ec.message() << std::endl; - BOOST_TEST_EQ(ec, error_code()); - exec1_finished = true; - - // The connection remains usable - conn.async_exec(req2, ignore, [&](error_code ec2, std::size_t) { - BOOST_TEST_EQ(ec2, error_code()); - exec2_finished = true; - conn.cancel(); - }); - }); - - ctx.run_for(test_timeout); - BOOST_TEST(run_finished); - BOOST_TEST(exec0_finished); - BOOST_TEST(exec1_finished); - BOOST_TEST(exec2_finished); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } } // namespace int main() { - test_cancel_pending(); - test_cancel_written(); - test_cancel_operation_exec(); + run_coroutine_test(test_cancel_pending()); + run_coroutine_test(test_cancel_written()); return boost::report_errors(); } From 59d65569c9e905b91316bca7124cf6b03d9e09f4 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 13:13:26 +0200 Subject: [PATCH 086/115] Copy test_co_move --- test/test_co_move.cpp | 112 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 test/test_co_move.cpp diff --git a/test/test_co_move.cpp b/test/test_co_move.cpp new file mode 100644 index 00000000..56025faa --- /dev/null +++ b/test/test_co_move.cpp @@ -0,0 +1,112 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "common.hpp" + +#include +#include + +using boost::system::error_code; +namespace net = boost::asio; +using namespace boost::redis; + +namespace { + +// Move constructing a connection doesn't leave dangling pointers +void test_conn_move_construct() +{ + // Setup + net::io_context ioc; + connection conn_prev(ioc); + connection conn(std::move(conn_prev)); + request req; + req.push("PING", "something"); + response res; + + bool run_finished = false, exec_finished = false; + + // Run the connection + conn.async_run(make_test_config(), [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + // Launch a PING + conn.async_exec(req, res, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, error_code()); + conn.cancel(); + }); + + ioc.run_for(test_timeout); + + // Check + BOOST_TEST(run_finished); + BOOST_TEST(exec_finished); + BOOST_TEST_EQ(std::get<0>(res).value(), "something"); +} + +// Moving a connection is safe even when it's running, +// and it doesn't leave dangling pointers +void test_conn_move_assign_while_running() +{ + // Setup + net::io_context ioc; + connection conn(ioc); + connection conn2(ioc); // will be assigned to + request req; + req.push("PING", "something"); + response res; + + bool run_finished = false, exec_finished = false; + + // Run the connection + conn.async_run(make_test_config(), [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + // Launch a PING. When it finishes, conn will be moved-from, and conn2 will be valid + conn.async_exec(req, res, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, error_code()); + conn2.cancel(); + }); + + // While the operations are running, perform a move + net::post(net::bind_executor(ioc.get_executor(), [&] { + conn2 = std::move(conn); + })); + + ioc.run_for(test_timeout); + + // Check + BOOST_TEST(run_finished); + BOOST_TEST(exec_finished); + BOOST_TEST_EQ(std::get<0>(res).value(), "something"); +} + +} // namespace + +int main() +{ + test_conn_move_construct(); + test_conn_move_assign_while_running(); + + return boost::report_errors(); +} From da98d7aad4a38ec13398618b91342e692003f80a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 13:15:16 +0200 Subject: [PATCH 087/115] Migrate test_co_mve --- test/test_co_move.cpp | 126 ++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 66 deletions(-) diff --git a/test/test_co_move.cpp b/test/test_co_move.cpp index 56025faa..9719a1d4 100644 --- a/test/test_co_move.cpp +++ b/test/test_co_move.cpp @@ -6,107 +6,101 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include +#include #include #include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include "common.hpp" +#include "corosio_common.hpp" -#include #include +#include +#include -using boost::system::error_code; -namespace net = boost::asio; +namespace capy = boost::capy; using namespace boost::redis; +using namespace boost::redis::test; +using namespace std::chrono_literals; +using error_code = std::error_code; namespace { // Move constructing a connection doesn't leave dangling pointers -void test_conn_move_construct() +capy::task<> test_conn_move_construct() { - // Setup - net::io_context ioc; - connection conn_prev(ioc); - connection conn(std::move(conn_prev)); - request req; - req.push("PING", "something"); - response res; - - bool run_finished = false, exec_finished = false; - - // Run the connection - conn.async_run(make_test_config(), [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, canceled_condition()); - }); + co_connection conn_prev{co_await capy::this_coro::executor}; + co_connection conn{std::move(conn_prev)}; + + response resp; - // Launch a PING - conn.async_exec(req, res, [&](error_code ec, std::size_t) { - exec_finished = true; + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING", "something"); + auto [ec] = co_await conn.exec(req, resp); BOOST_TEST_EQ(ec, error_code()); - conn.cancel(); - }); + co_return {}; + }; - ioc.run_for(test_timeout); + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; - // Check - BOOST_TEST(run_finished); - BOOST_TEST(exec_finished); - BOOST_TEST_EQ(std::get<0>(res).value(), "something"); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st + BOOST_TEST_EQ(std::get<0>(resp).value(), "something"); } // Moving a connection is safe even when it's running, // and it doesn't leave dangling pointers -void test_conn_move_assign_while_running() +capy::task<> test_conn_move_assign_while_running() { - // Setup - net::io_context ioc; - connection conn(ioc); - connection conn2(ioc); // will be assigned to - request req; - req.push("PING", "something"); - response res; - - bool run_finished = false, exec_finished = false; - - // Run the connection - conn.async_run(make_test_config(), [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, canceled_condition()); - }); + co_connection conn{co_await capy::this_coro::executor}; + co_connection conn2{co_await capy::this_coro::executor}; // will be assigned to - // Launch a PING. When it finishes, conn will be moved-from, and conn2 will be valid - conn.async_exec(req, res, [&](error_code ec, std::size_t) { - exec_finished = true; - BOOST_TEST_EQ(ec, error_code()); - conn2.cancel(); - }); + response resp; - // While the operations are running, perform a move - net::post(net::bind_executor(ioc.get_executor(), [&] { + auto exec_fn = [&]() -> capy::io_task<> { + // Wait briefly to ensure run is in flight + auto [delay_ec] = co_await capy::delay(50ms); + BOOST_TEST_EQ(delay_ec, error_code()); + + // Perform the move while run is in progress conn2 = std::move(conn); - })); - ioc.run_for(test_timeout); + // Launch a PING on the moved-to connection + request req; + req.push("PING", "something"); + auto [ec] = co_await conn2.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; - // Check - BOOST_TEST(run_finished); - BOOST_TEST(exec_finished); - BOOST_TEST_EQ(std::get<0>(res).value(), "something"); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st + BOOST_TEST_EQ(std::get<0>(resp).value(), "something"); } } // namespace int main() { - test_conn_move_construct(); - test_conn_move_assign_while_running(); + run_coroutine_test(test_conn_move_construct()); + run_coroutine_test(test_conn_move_assign_while_running()); return boost::report_errors(); } From ce93adc192a7d4ff19b81415ba7acaffb58532c0 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 13:17:30 +0200 Subject: [PATCH 088/115] Improve the test --- test/test_co_move.cpp | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/test/test_co_move.cpp b/test/test_co_move.cpp index 9719a1d4..3ff0acf3 100644 --- a/test/test_co_move.cpp +++ b/test/test_co_move.cpp @@ -10,7 +10,6 @@ #include #include -#include #include #include #include @@ -66,21 +65,22 @@ capy::task<> test_conn_move_assign_while_running() co_connection conn{co_await capy::this_coro::executor}; co_connection conn2{co_await capy::this_coro::executor}; // will be assigned to - response resp; - auto exec_fn = [&]() -> capy::io_task<> { - // Wait briefly to ensure run is in flight - auto [delay_ec] = co_await capy::delay(50ms); - BOOST_TEST_EQ(delay_ec, error_code()); + // Ensure that run is in flight + request req; + req.push("PING", "test_co_move"); + auto [ec] = co_await conn.exec(req); + BOOST_TEST_EQ(ec, error_code()); // Perform the move while run is in progress conn2 = std::move(conn); - // Launch a PING on the moved-to connection - request req; - req.push("PING", "something"); - auto [ec] = co_await conn2.exec(req, resp); - BOOST_TEST_EQ(ec, error_code()); + // Checked that the moved-to connection is still usable + response resp; + auto [ec2] = co_await conn2.exec(req, resp); + BOOST_TEST_EQ(ec2, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "test_co_move"); + co_return {}; }; @@ -92,7 +92,6 @@ capy::task<> test_conn_move_assign_while_running() auto result = co_await capy::when_any(exec_fn(), run_fn()); BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st - BOOST_TEST_EQ(std::get<0>(resp).value(), "something"); } } // namespace From 3a0a8df534c42a5ae10b66ac2fcfa2c7f8f47e54 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 13:21:59 +0200 Subject: [PATCH 089/115] test_co_run_cancel --- test/test_co_run_cancel.cpp | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test/test_co_run_cancel.cpp diff --git a/test/test_co_run_cancel.cpp b/test/test_co_run_cancel.cpp new file mode 100644 index 00000000..144fc779 --- /dev/null +++ b/test/test_co_run_cancel.cpp @@ -0,0 +1,49 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include + +#include +#include +#include + +#include "common.hpp" +#include "corosio_common.hpp" + +using boost::system::error_code; +namespace capy = boost::capy; +using namespace boost::redis; +using namespace boost::redis::test; + +namespace { + +// The user configured an empty setup request. No request should be sent +capy::task<> test_cancel_run() +{ + // Setup + co_connection conn{co_await capy::this_coro::executor}; + + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(config{}); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; + + auto result = co_await capy::when_any(run_fn(), capy::ready()); + BOOST_TEST_EQ(result.index(), 2u); // Ready finished 1st +} + +} // namespace + +int main() +{ + run_coroutine_test(test_cancel_run()); + + return boost::report_errors(); +} From ec93ec2de01114a00ee1b7f850ad4b26dc5c0296 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 13:22:23 +0200 Subject: [PATCH 090/115] Copy test_co_sentinel --- test/test_co_sentinel.cpp | 491 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 test/test_co_sentinel.cpp diff --git a/test/test_co_sentinel.cpp b/test/test_co_sentinel.cpp new file mode 100644 index 00000000..782c66e9 --- /dev/null +++ b/test/test_co_sentinel.cpp @@ -0,0 +1,491 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "asio_common.hpp" +#include "print_node.hpp" + +#include + +namespace net = boost::asio; +using namespace boost::redis; +using namespace std::chrono_literals; +using boost::system::error_code; + +namespace { + +// We can execute requests normally when using Sentinel run +void test_exec() +{ + // Setup + net::io_context ioc; + connection conn{ioc}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26379"}, + {"localhost", "26380"}, + {"localhost", "26381"}, + }; + cfg.sentinel.master_name = "mymaster"; + + // Verify that we're connected to the master + request req; + req.push("ROLE"); + + generic_response resp; + + bool exec_finished = false, run_finished = false; + + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, error_code()); + + // ROLE outputs an array, 1st element should be 'master' + BOOST_TEST(resp.has_value()); + BOOST_TEST_GE(resp.value().size(), 2u); + BOOST_TEST_EQ(resp.value().at(1u).value, "master"); + + conn.cancel(); + }); + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); +} + +// We can use receive normally when using Sentinel run +void test_receive() +{ + // Setup + net::io_context ioc; + connection conn{ioc}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26379"}, + {"localhost", "26380"}, + {"localhost", "26381"}, + }; + cfg.sentinel.master_name = "mymaster"; + + resp3::tree resp; + conn.set_receive_response(resp); + + // Subscribe to a channel. This produces a push message on itself + request req; + req.subscribe({"sentinel_channel"}); + + bool exec_finished = false, receive_finished = false, run_finished = false; + + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, error_code()); + }); + + conn.async_receive2([&](error_code ec2) { + receive_finished = true; + BOOST_TEST_EQ(ec2, error_code()); + conn.cancel(); + }); + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(receive_finished); + BOOST_TEST(run_finished); + + // We subscribed to channel 'sentinel_channel', and have 1 active subscription + const resp3::node expected[] = { + {resp3::type::push, 3u, 0u, "" }, + {resp3::type::blob_string, 1u, 1u, "subscribe" }, + {resp3::type::blob_string, 1u, 1u, "sentinel_channel"}, + {resp3::type::number, 1u, 1u, "1" }, + }; + + BOOST_TEST_ALL_EQ(resp.begin(), resp.end(), std::begin(expected), std::end(expected)); +} + +// If connectivity to the Redis master fails, we can reconnect +void test_reconnect() +{ + // Setup + net::io_context ioc; + connection conn{ioc}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26379"}, + {"localhost", "26380"}, + {"localhost", "26381"}, + }; + cfg.sentinel.master_name = "mymaster"; + + // Will cause the connection to fail + request req_quit; + req_quit.push("QUIT"); + + // Will succeed if the reconnection succeeds + request req_ping; + req_ping.push("PING", "sentinel_reconnect"); + req_ping.get_config().cancel_if_unresponded = false; + + bool quit_finished = false, ping_finished = false, run_finished = false; + + conn.async_exec(req_quit, ignore, [&](error_code ec1, std::size_t) { + quit_finished = true; + BOOST_TEST_EQ(ec1, error_code()); + conn.async_exec(req_ping, ignore, [&](error_code ec2, std::size_t) { + ping_finished = true; + BOOST_TEST_EQ(ec2, error_code()); + conn.cancel(); + }); + }); + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(quit_finished); + BOOST_TEST(ping_finished); + BOOST_TEST(run_finished); +} + +// If a Sentinel is not reachable, we try the next one +void test_sentinel_not_reachable() +{ + // Setup + net::io_context ioc; + connection conn{ioc}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "45678"}, // invalid + {"localhost", "26381"}, + }; + cfg.sentinel.master_name = "mymaster"; + + // Verify that we're connected to the master, listening at port 6380 + request req; + req.push("PING", "test_sentinel_not_reachable"); + + bool exec_finished = false, run_finished = false; + + conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, error_code()); + conn.cancel(); + }); + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); +} + +// Both Sentinels and masters may be protected with authorization +void test_auth() +{ + // Setup + net::io_context ioc; + connection conn{ioc}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26379"}, + }; + cfg.sentinel.master_name = "mymaster"; + cfg.sentinel.setup.push("HELLO", 3, "AUTH", "sentinel_user", "sentinel_pass"); + + cfg.use_setup = true; + cfg.setup.clear(); + cfg.setup.push("HELLO", 3, "AUTH", "redis_user", "redis_pass"); + + // Verify that we're authenticated correctly + request req; + req.push("ACL", "WHOAMI"); + + response resp; + + bool exec_finished = false, run_finished = false; + + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST(std::get<0>(resp).has_value()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "redis_user"); + conn.cancel(); + }); + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); +} + +// TLS might be used with Sentinels. In our setup, nodes don't use TLS, +// but this setting is independent from Sentinel. +void test_tls() +{ + // Setup + net::io_context ioc; + net::ssl::context ssl_ctx{net::ssl::context::tlsv13_client}; + + // The custom server uses a certificate signed by a CA + // that is not trusted by default - skip verification. + ssl_ctx.set_verify_mode(net::ssl::verify_none); + + connection conn{ioc, std::move(ssl_ctx)}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "36379"}, + {"localhost", "36380"}, + {"localhost", "36381"}, + }; + cfg.sentinel.master_name = "mymaster"; + cfg.sentinel.use_ssl = true; + + request req; + req.push("PING", "test_sentinel_tls"); + + bool exec_finished = false, run_finished = false; + + conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST(ec == error_code()); + conn.cancel(); + }); + + conn.async_run(cfg, {}, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); +} + +// We can also connect to replicas +void test_replica() +{ + // Setup + net::io_context ioc; + connection conn{ioc}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26379"}, + {"localhost", "26380"}, + {"localhost", "26381"}, + }; + cfg.sentinel.master_name = "mymaster"; + cfg.sentinel.server_role = role::replica; + + // Verify that we're connected to a replica + request req; + req.push("ROLE"); + + generic_response resp; + + bool exec_finished = false, run_finished = false; + + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST_EQ(ec, error_code()); + + // ROLE outputs an array, 1st element should be 'slave' + BOOST_TEST(resp.has_value()); + BOOST_TEST_GE(resp.value().size(), 2u); + BOOST_TEST_EQ(resp.value().at(1u).value, "slave"); + + conn.cancel(); + }); + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); +} + +// If no Sentinel is reachable, an error is issued. +// This tests disabling reconnection with Sentinel, too. +void test_error_no_sentinel_reachable() +{ + // Setup + std::string logs; + net::io_context ioc; + connection conn{ioc, make_string_logger(logs)}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "43210"}, + {"localhost", "43211"}, + }; + cfg.sentinel.master_name = "mymaster"; + cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error + + bool run_finished = false; + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(run_finished); + + if ( + !BOOST_TEST_NE( + logs.find("Sentinel at localhost:43210: connection establishment error"), + std::string::npos) || + !BOOST_TEST_NE( + logs.find("Sentinel at localhost:43211: connection establishment error"), + std::string::npos)) { + std::cerr << "Log was:\n" << logs << std::endl; + } +} + +// If Sentinel doesn't know about the configured master, +// the appropriate error is returned +void test_error_unknown_master() +{ + // Setup + std::string logs; + net::io_context ioc; + connection conn{ioc, make_string_logger(logs)}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26380"}, + }; + cfg.sentinel.master_name = "unknown_master"; + cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error + + bool run_finished = false; + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(run_finished); + + if (!BOOST_TEST_NE( + logs.find("Sentinel at localhost:26380: doesn't know about the configured master"), + std::string::npos)) { + std::cerr << "Log was:\n" << logs << std::endl; + } +} + +// The same applies when connecting to replicas, too +void test_error_unknown_master_replica() +{ + // Setup + std::string logs; + net::io_context ioc; + connection conn{ioc, make_string_logger(logs)}; + + config cfg; + cfg.sentinel.addresses = { + {"localhost", "26380"}, + }; + cfg.sentinel.master_name = "unknown_master"; + cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error + cfg.sentinel.server_role = role::replica; + + bool run_finished = false; + + conn.async_run(cfg, [&](error_code ec) { + run_finished = true; + BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(run_finished); + + if (!BOOST_TEST_NE( + logs.find("Sentinel at localhost:26380: doesn't know about the configured master"), + std::string::npos)) { + std::cerr << "Log was:\n" << logs << std::endl; + } +} + +} // namespace + +int main() +{ + // Create the required users in the master, replicas and sentinels + create_user("6379", "redis_user", "redis_pass"); + create_user("6380", "redis_user", "redis_pass"); + create_user("6381", "redis_user", "redis_pass"); + create_user("26379", "sentinel_user", "sentinel_pass"); + create_user("26380", "sentinel_user", "sentinel_pass"); + create_user("26381", "sentinel_user", "sentinel_pass"); + + // Actual tests + test_exec(); + test_receive(); + test_reconnect(); + test_sentinel_not_reachable(); + test_auth(); + test_tls(); + test_replica(); + + test_error_no_sentinel_reachable(); + test_error_unknown_master(); + test_error_unknown_master_replica(); + + return boost::report_errors(); +} From 851f922da4293bd1a061ca802c9ee251adfbfb69 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 13:23:11 +0200 Subject: [PATCH 091/115] Copy test_co_tls --- test/test_co_tls.cpp | 183 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 test/test_co_tls.cpp diff --git a/test/test_co_tls.cpp b/test/test_co_tls.cpp new file mode 100644 index 00000000..aa438386 --- /dev/null +++ b/test/test_co_tls.cpp @@ -0,0 +1,183 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#define BOOST_TEST_MODULE conn_tls +#include + +#include "common.hpp" + +namespace net = boost::asio; +using namespace boost::redis; +using namespace std::chrono_literals; +using boost::system::error_code; + +namespace { + +// Loads the CA certificate that signed the certificate used by the server. +// Should be in /tmp/ +std::string load_ca_certificate() +{ + auto ca_path = safe_getenv("BOOST_REDIS_CA_PATH", "/opt/ci-tls/ca.crt"); + std::ifstream f(ca_path); + if (!f) { + throw boost::system::system_error( + errno, + boost::system::system_category(), + "Failed to open CA certificate file '" + ca_path + "'"); + } + + return std::string(std::istreambuf_iterator(f), std::istreambuf_iterator()); +} + +config make_tls_config() +{ + config cfg; + cfg.use_ssl = true; + cfg.addr.host = get_server_hostname(); + cfg.addr.port = "16379"; + return cfg; +} + +// Using the default TLS context allows establishing TLS connections and execute requests +BOOST_AUTO_TEST_CASE(exec_default_ssl_context) +{ + auto const cfg = make_tls_config(); + constexpr std::string_view ping_value = "Kabuf"; + + request req; + req.push("PING", ping_value); + + response resp; + + net::io_context ioc; + connection conn{ioc}; + + // The custom server uses a certificate signed by a CA + // that is not trusted by default - skip verification. + conn.next_layer().set_verify_mode(net::ssl::verify_none); + + bool exec_finished = false, run_finished = false; + + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST(ec == error_code()); + conn.cancel(); + }); + + conn.async_run(cfg, {}, [&](error_code ec) { + run_finished = true; + BOOST_CHECK_EQUAL(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); + BOOST_TEST(std::get<0>(resp).value() == ping_value); +} + +// Users can pass a custom context with TLS config +BOOST_AUTO_TEST_CASE(exec_custom_ssl_context) +{ + std::string ca_pem = load_ca_certificate(); + auto const cfg = make_tls_config(); + constexpr std::string_view ping_value = "Kabuf"; + + request req; + req.push("PING", ping_value); + + response resp; + + net::io_context ioc; + net::ssl::context ctx{net::ssl::context::tls_client}; + + // Configure the SSL context to trust the CA that signed the server's certificate. + // The test certificate uses "redis" as its common name, regardless of the actual server's hostname + ctx.add_certificate_authority(net::buffer(ca_pem)); + ctx.set_verify_mode(net::ssl::verify_peer); + ctx.set_verify_callback(net::ssl::host_name_verification("redis")); + + connection conn{ioc, std::move(ctx)}; + + bool exec_finished = false, run_finished = false; + + conn.async_exec(req, resp, [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST(ec == error_code()); + conn.cancel(); + }); + + conn.async_run(cfg, {}, [&](error_code ec) { + run_finished = true; + BOOST_CHECK_EQUAL(ec, canceled_condition()); + }); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); + BOOST_TEST(std::get<0>(resp).value() == ping_value); +} + +// After an error, a TLS connection can recover. +// Force an error using QUIT, then issue a regular request to verify that we could reconnect +BOOST_AUTO_TEST_CASE(reconnection) +{ + // Setup + net::io_context ioc; + net::steady_timer timer{ioc}; + connection conn{ioc}; + + request ping_request; + ping_request.push("PING", "some_value"); + ping_request.get_config().cancel_if_unresponded = false; + ping_request.get_config().cancel_on_connection_lost = false; + + request quit_request; + quit_request.push("QUIT"); + + bool exec_finished = false, run_finished = false; + + // Run the connection + conn.async_run(make_test_config(), [&](error_code ec) { + run_finished = true; + BOOST_CHECK_EQUAL(ec, canceled_condition()); + }); + + // The PING is the end of the callback chain + auto ping_callback = [&](error_code ec, std::size_t) { + exec_finished = true; + BOOST_TEST(ec == error_code()); + conn.cancel(); + }; + + auto quit_callback = [&](error_code ec, std::size_t) { + BOOST_TEST(ec == error_code()); + conn.async_exec(ping_request, ignore, ping_callback); + }; + + conn.async_exec(quit_request, ignore, quit_callback); + + ioc.run_for(test_timeout); + + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); +} + +} // namespace \ No newline at end of file From 9eed916b08a284c8582697e836059a801c608104 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 13:59:26 +0200 Subject: [PATCH 092/115] co_sentinel --- test/test_co_sentinel.cpp | 475 ++++++++++++++++++-------------------- 1 file changed, 219 insertions(+), 256 deletions(-) diff --git a/test/test_co_sentinel.cpp b/test/test_co_sentinel.cpp index 782c66e9..c0dde09c 100644 --- a/test/test_co_sentinel.cpp +++ b/test/test_co_sentinel.cpp @@ -6,37 +6,81 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include -#include +#include #include +#include #include #include #include #include #include -#include +#include +#include +#include +#include +#include #include +#include -#include "asio_common.hpp" +#include "common.hpp" +#include "corosio_common.hpp" #include "print_node.hpp" +#include +#include #include +#include +#include +#include -namespace net = boost::asio; +namespace capy = boost::capy; +namespace corosio = boost::corosio; using namespace boost::redis; +using namespace boost::redis::test; using namespace std::chrono_literals; -using boost::system::error_code; +using error_code = std::error_code; namespace { -// We can execute requests normally when using Sentinel run -void test_exec() +// RP TODO: this is duplicate +// Connects to the Redis server at the given port and creates a user +capy::task create_user( + std::string_view port, + std::string_view username, + std::string_view password) { - // Setup - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; + auto exec_fn = [&]() -> capy::io_task<> { + // Enable the user and grant them permissions on everything + request req; + req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); + + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, error_code()); + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + config cfg; + cfg.addr.port = port; + + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +config make_sentinel_config() +{ config cfg; cfg.sentinel.addresses = { {"localhost", "26379"}, @@ -44,17 +88,22 @@ void test_exec() {"localhost", "26381"}, }; cfg.sentinel.master_name = "mymaster"; + return cfg; +} - // Verify that we're connected to the master - request req; - req.push("ROLE"); +// We can execute requests normally when using Sentinel +capy::task<> test_exec() +{ + co_connection conn{co_await capy::this_coro::executor}; generic_response resp; - bool exec_finished = false, run_finished = false; + auto exec_fn = [&]() -> capy::io_task<> { + // Verify that we're connected to the master + request req; + req.push("ROLE"); - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - exec_finished = true; + auto [ec] = co_await conn.exec(req, resp); BOOST_TEST_EQ(ec, error_code()); // ROLE outputs an array, 1st element should be 'master' @@ -62,65 +111,55 @@ void test_exec() BOOST_TEST_GE(resp.value().size(), 2u); BOOST_TEST_EQ(resp.value().at(1u).value, "master"); - conn.cancel(); - }); + co_return {}; + }; - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_sentinel_config()); BOOST_TEST_EQ(ec, canceled_condition()); - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } -// We can use receive normally when using Sentinel run -void test_receive() +// We can use receive normally when using Sentinel +capy::task<> test_receive() { - // Setup - net::io_context ioc; - connection conn{ioc}; - - config cfg; - cfg.sentinel.addresses = { - {"localhost", "26379"}, - {"localhost", "26380"}, - {"localhost", "26381"}, - }; - cfg.sentinel.master_name = "mymaster"; - + co_connection conn{co_await capy::this_coro::executor}; resp3::tree resp; conn.set_receive_response(resp); - // Subscribe to a channel. This produces a push message on itself - request req; - req.subscribe({"sentinel_channel"}); - - bool exec_finished = false, receive_finished = false, run_finished = false; + auto exec_fn = [&]() -> capy::io_task<> { + // Subscribe to a channel. This produces a push message on itself + request req; + req.subscribe({"sentinel_channel"}); + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - exec_finished = true; + auto receive_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.receive(); BOOST_TEST_EQ(ec, error_code()); - }); + co_return {}; + }; - conn.async_receive2([&](error_code ec2) { - receive_finished = true; - BOOST_TEST_EQ(ec2, error_code()); - conn.cancel(); - }); + auto work_fn = [&]() -> capy::io_task<> { + auto [ec, a, b] = co_await capy::when_all(exec_fn(), receive_fn()); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_sentinel_config()); BOOST_TEST_EQ(ec, canceled_condition()); - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(receive_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(work_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Work finished 1st // We subscribed to channel 'sentinel_channel', and have 1 active subscription const resp3::node expected[] = { @@ -134,59 +173,41 @@ void test_receive() } // If connectivity to the Redis master fails, we can reconnect -void test_reconnect() +capy::task<> test_reconnect() { - // Setup - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; - config cfg; - cfg.sentinel.addresses = { - {"localhost", "26379"}, - {"localhost", "26380"}, - {"localhost", "26381"}, - }; - cfg.sentinel.master_name = "mymaster"; - - // Will cause the connection to fail - request req_quit; - req_quit.push("QUIT"); + auto exec_fn = [&]() -> capy::io_task<> { + // Will cause the connection to fail + request req_quit; + req_quit.push("QUIT"); + auto [ec1] = co_await conn.exec(req_quit, ignore); + BOOST_TEST_EQ(ec1, error_code()); - // Will succeed if the reconnection succeeds - request req_ping; - req_ping.push("PING", "sentinel_reconnect"); - req_ping.get_config().cancel_if_unresponded = false; + // Will succeed if the reconnection succeeds + request req_ping; + req_ping.push("PING", "sentinel_reconnect"); + req_ping.get_config().cancel_if_unresponded = false; + auto [ec2] = co_await conn.exec(req_ping, ignore); + BOOST_TEST_EQ(ec2, error_code()); - bool quit_finished = false, ping_finished = false, run_finished = false; + co_return {}; + }; - conn.async_exec(req_quit, ignore, [&](error_code ec1, std::size_t) { - quit_finished = true; - BOOST_TEST_EQ(ec1, error_code()); - conn.async_exec(req_ping, ignore, [&](error_code ec2, std::size_t) { - ping_finished = true; - BOOST_TEST_EQ(ec2, error_code()); - conn.cancel(); - }); - }); - - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_sentinel_config()); BOOST_TEST_EQ(ec, canceled_condition()); - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(quit_finished); - BOOST_TEST(ping_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } // If a Sentinel is not reachable, we try the next one -void test_sentinel_not_reachable() +capy::task<> test_sentinel_not_reachable() { - // Setup - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; config cfg; cfg.sentinel.addresses = { @@ -195,35 +216,29 @@ void test_sentinel_not_reachable() }; cfg.sentinel.master_name = "mymaster"; - // Verify that we're connected to the master, listening at port 6380 - request req; - req.push("PING", "test_sentinel_not_reachable"); - - bool exec_finished = false, run_finished = false; - - conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { - exec_finished = true; + auto exec_fn = [&]() -> capy::io_task<> { + // Verify that we're connected to the master + request req; + req.push("PING", "test_sentinel_not_reachable"); + auto [ec] = co_await conn.exec(req, ignore); BOOST_TEST_EQ(ec, error_code()); - conn.cancel(); - }); + co_return {}; + }; - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, canceled_condition()); - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } // Both Sentinels and masters may be protected with authorization -void test_auth() +capy::task<> test_auth() { - // Setup - net::io_context ioc; - connection conn{ioc}; + co_connection conn{co_await capy::this_coro::executor}; config cfg; cfg.sentinel.addresses = { @@ -236,46 +251,40 @@ void test_auth() cfg.setup.clear(); cfg.setup.push("HELLO", 3, "AUTH", "redis_user", "redis_pass"); - // Verify that we're authenticated correctly - request req; - req.push("ACL", "WHOAMI"); - - response resp; + auto exec_fn = [&]() -> capy::io_task<> { + // Verify that we're authenticated correctly + request req; + req.push("ACL", "WHOAMI"); + response resp; - bool exec_finished = false, run_finished = false; - - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - exec_finished = true; + auto [ec] = co_await conn.exec(req, resp); BOOST_TEST_EQ(ec, error_code()); BOOST_TEST(std::get<0>(resp).has_value()); BOOST_TEST_EQ(std::get<0>(resp).value(), "redis_user"); - conn.cancel(); - }); - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, canceled_condition()); - }); + co_return {}; + }; - ioc.run_for(test_timeout); + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } // TLS might be used with Sentinels. In our setup, nodes don't use TLS, // but this setting is independent from Sentinel. -void test_tls() +capy::task<> test_tls() { - // Setup - net::io_context ioc; - net::ssl::context ssl_ctx{net::ssl::context::tlsv13_client}; - // The custom server uses a certificate signed by a CA // that is not trusted by default - skip verification. - ssl_ctx.set_verify_mode(net::ssl::verify_none); + corosio::tls_context tls_ctx; + tls_ctx.set_verify_mode(corosio::tls_verify_mode::none); - connection conn{ioc, std::move(ssl_ctx)}; + co_connection conn{co_await capy::this_coro::executor, std::move(tls_ctx)}; config cfg; cfg.sentinel.addresses = { @@ -286,54 +295,35 @@ void test_tls() cfg.sentinel.master_name = "mymaster"; cfg.sentinel.use_ssl = true; - request req; - req.push("PING", "test_sentinel_tls"); - - bool exec_finished = false, run_finished = false; - - conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { - exec_finished = true; - BOOST_TEST(ec == error_code()); - conn.cancel(); - }); + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING", "test_sentinel_tls"); + auto [ec] = co_await conn.exec(req, ignore); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; - conn.async_run(cfg, {}, [&](error_code ec) { - run_finished = true; + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, canceled_condition()); - }); - - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } // We can also connect to replicas -void test_replica() +capy::task<> test_replica() { - // Setup - net::io_context ioc; - connection conn{ioc}; - - config cfg; - cfg.sentinel.addresses = { - {"localhost", "26379"}, - {"localhost", "26380"}, - {"localhost", "26381"}, - }; - cfg.sentinel.master_name = "mymaster"; - cfg.sentinel.server_role = role::replica; - - // Verify that we're connected to a replica - request req; - req.push("ROLE"); - - generic_response resp; - - bool exec_finished = false, run_finished = false; - - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - exec_finished = true; + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + // Verify that we're connected to a replica + request req; + req.push("ROLE"); + generic_response resp; + auto [ec] = co_await conn.exec(req, resp); BOOST_TEST_EQ(ec, error_code()); // ROLE outputs an array, 1st element should be 'slave' @@ -341,28 +331,29 @@ void test_replica() BOOST_TEST_GE(resp.value().size(), 2u); BOOST_TEST_EQ(resp.value().at(1u).value, "slave"); - conn.cancel(); - }); + co_return {}; + }; - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; + auto run_fn = [&]() -> capy::io_task<> { + auto cfg = make_sentinel_config(); + cfg.sentinel.server_role = role::replica; + + auto [ec] = co_await conn.run(cfg); BOOST_TEST_EQ(ec, canceled_condition()); - }); - ioc.run_for(test_timeout); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st } // If no Sentinel is reachable, an error is issued. // This tests disabling reconnection with Sentinel, too. -void test_error_no_sentinel_reachable() +capy::task<> test_error_no_sentinel_reachable() { - // Setup std::string logs; - net::io_context ioc; - connection conn{ioc, make_string_logger(logs)}; + co_connection conn{co_await capy::this_coro::executor, make_string_logger(logs)}; config cfg; cfg.sentinel.addresses = { @@ -372,16 +363,8 @@ void test_error_no_sentinel_reachable() cfg.sentinel.master_name = "mymaster"; cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error - bool run_finished = false; - - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); - }); - - ioc.run_for(test_timeout); - - BOOST_TEST(run_finished); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); if ( !BOOST_TEST_NE( @@ -396,12 +379,10 @@ void test_error_no_sentinel_reachable() // If Sentinel doesn't know about the configured master, // the appropriate error is returned -void test_error_unknown_master() +capy::task<> test_error_unknown_master() { - // Setup std::string logs; - net::io_context ioc; - connection conn{ioc, make_string_logger(logs)}; + co_connection conn{co_await capy::this_coro::executor, make_string_logger(logs)}; config cfg; cfg.sentinel.addresses = { @@ -410,16 +391,8 @@ void test_error_unknown_master() cfg.sentinel.master_name = "unknown_master"; cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error - bool run_finished = false; - - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); - }); - - ioc.run_for(test_timeout); - - BOOST_TEST(run_finished); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); if (!BOOST_TEST_NE( logs.find("Sentinel at localhost:26380: doesn't know about the configured master"), @@ -429,12 +402,10 @@ void test_error_unknown_master() } // The same applies when connecting to replicas, too -void test_error_unknown_master_replica() +capy::task<> test_error_unknown_master_replica() { - // Setup std::string logs; - net::io_context ioc; - connection conn{ioc, make_string_logger(logs)}; + co_connection conn{co_await capy::this_coro::executor, make_string_logger(logs)}; config cfg; cfg.sentinel.addresses = { @@ -444,16 +415,8 @@ void test_error_unknown_master_replica() cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error cfg.sentinel.server_role = role::replica; - bool run_finished = false; - - conn.async_run(cfg, [&](error_code ec) { - run_finished = true; - BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); - }); - - ioc.run_for(test_timeout); - - BOOST_TEST(run_finished); + auto [ec] = co_await conn.run(cfg); + BOOST_TEST_EQ(ec, error::sentinel_resolve_failed); if (!BOOST_TEST_NE( logs.find("Sentinel at localhost:26380: doesn't know about the configured master"), @@ -467,25 +430,25 @@ void test_error_unknown_master_replica() int main() { // Create the required users in the master, replicas and sentinels - create_user("6379", "redis_user", "redis_pass"); - create_user("6380", "redis_user", "redis_pass"); - create_user("6381", "redis_user", "redis_pass"); - create_user("26379", "sentinel_user", "sentinel_pass"); - create_user("26380", "sentinel_user", "sentinel_pass"); - create_user("26381", "sentinel_user", "sentinel_pass"); + run_coroutine_test(create_user("6379", "redis_user", "redis_pass")); + run_coroutine_test(create_user("6380", "redis_user", "redis_pass")); + run_coroutine_test(create_user("6381", "redis_user", "redis_pass")); + run_coroutine_test(create_user("26379", "sentinel_user", "sentinel_pass")); + run_coroutine_test(create_user("26380", "sentinel_user", "sentinel_pass")); + run_coroutine_test(create_user("26381", "sentinel_user", "sentinel_pass")); // Actual tests - test_exec(); - test_receive(); - test_reconnect(); - test_sentinel_not_reachable(); - test_auth(); - test_tls(); - test_replica(); - - test_error_no_sentinel_reachable(); - test_error_unknown_master(); - test_error_unknown_master_replica(); + run_coroutine_test(test_exec()); + run_coroutine_test(test_receive()); + run_coroutine_test(test_reconnect()); + run_coroutine_test(test_sentinel_not_reachable()); + run_coroutine_test(test_auth()); + run_coroutine_test(test_tls()); + run_coroutine_test(test_replica()); + + run_coroutine_test(test_error_no_sentinel_reachable()); + run_coroutine_test(test_error_unknown_master()); + run_coroutine_test(test_error_unknown_master_replica()); return boost::report_errors(); } From ba26f061caa0b0867580a9563c2a4e4d4c97d36e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 14:01:50 +0200 Subject: [PATCH 093/115] co_tls --- test/test_co_tls.cpp | 227 +++++++++++++++++++++---------------------- 1 file changed, 113 insertions(+), 114 deletions(-) diff --git a/test/test_co_tls.cpp b/test/test_co_tls.cpp index aa438386..605d40d7 100644 --- a/test/test_co_tls.cpp +++ b/test/test_co_tls.cpp @@ -1,36 +1,45 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#include +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include #include - -#include -#include -#include +#include +#include + +#include +#include +#include +#include +#include +#include #include +#include "common.hpp" +#include "corosio_common.hpp" + #include -#include #include +#include #include #include -#define BOOST_TEST_MODULE conn_tls -#include +#include +#include -#include "common.hpp" - -namespace net = boost::asio; +namespace capy = boost::capy; +namespace corosio = boost::corosio; using namespace boost::redis; +using namespace boost::redis::test; using namespace std::chrono_literals; -using boost::system::error_code; +using error_code = std::error_code; namespace { // Loads the CA certificate that signed the certificate used by the server. -// Should be in /tmp/ std::string load_ca_certificate() { auto ca_path = safe_getenv("BOOST_REDIS_CA_PATH", "/opt/ci-tls/ca.crt"); @@ -54,130 +63,120 @@ config make_tls_config() return cfg; } -// Using the default TLS context allows establishing TLS connections and execute requests -BOOST_AUTO_TEST_CASE(exec_default_ssl_context) +// Using a default TLS context with verification disabled +// allows establishing TLS connections and execute requests +capy::task<> test_exec_default_ssl_context() { - auto const cfg = make_tls_config(); constexpr std::string_view ping_value = "Kabuf"; - request req; - req.push("PING", ping_value); - - response resp; - - net::io_context ioc; - connection conn{ioc}; - // The custom server uses a certificate signed by a CA // that is not trusted by default - skip verification. - conn.next_layer().set_verify_mode(net::ssl::verify_none); + corosio::tls_context tls_ctx; + auto ec_mode = tls_ctx.set_verify_mode(corosio::tls_verify_mode::none); + BOOST_TEST_EQ(ec_mode, error_code()); - bool exec_finished = false, run_finished = false; + co_connection conn{co_await capy::this_coro::executor, std::move(tls_ctx)}; - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - exec_finished = true; - BOOST_TEST(ec == error_code()); - conn.cancel(); - }); + response resp; - conn.async_run(cfg, {}, [&](error_code ec) { - run_finished = true; - BOOST_CHECK_EQUAL(ec, canceled_condition()); - }); + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING", ping_value); + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; - ioc.run_for(test_timeout); + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_tls_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); - BOOST_TEST(std::get<0>(resp).value() == ping_value); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st + BOOST_TEST_EQ(std::get<0>(resp).value(), ping_value); } // Users can pass a custom context with TLS config -BOOST_AUTO_TEST_CASE(exec_custom_ssl_context) +capy::task<> test_exec_custom_ssl_context() { - std::string ca_pem = load_ca_certificate(); - auto const cfg = make_tls_config(); constexpr std::string_view ping_value = "Kabuf"; - request req; - req.push("PING", ping_value); - - response resp; - - net::io_context ioc; - net::ssl::context ctx{net::ssl::context::tls_client}; - - // Configure the SSL context to trust the CA that signed the server's certificate. - // The test certificate uses "redis" as its common name, regardless of the actual server's hostname - ctx.add_certificate_authority(net::buffer(ca_pem)); - ctx.set_verify_mode(net::ssl::verify_peer); - ctx.set_verify_callback(net::ssl::host_name_verification("redis")); + std::string ca_pem = load_ca_certificate(); - connection conn{ioc, std::move(ctx)}; + // Configure the TLS context to trust the CA that signed the server's certificate. + // The test certificate uses "redis" as its common name, + // regardless of the actual server's hostname. + corosio::tls_context tls_ctx; + auto ec_ca = tls_ctx.add_certificate_authority(ca_pem); + BOOST_TEST_EQ(ec_ca, error_code()); + auto ec_mode = tls_ctx.set_verify_mode(corosio::tls_verify_mode::require_peer); + BOOST_TEST_EQ(ec_mode, error_code()); + tls_ctx.set_hostname("redis"); - bool exec_finished = false, run_finished = false; + co_connection conn{co_await capy::this_coro::executor, std::move(tls_ctx)}; - conn.async_exec(req, resp, [&](error_code ec, std::size_t) { - exec_finished = true; - BOOST_TEST(ec == error_code()); - conn.cancel(); - }); + response resp; - conn.async_run(cfg, {}, [&](error_code ec) { - run_finished = true; - BOOST_CHECK_EQUAL(ec, canceled_condition()); - }); + auto exec_fn = [&]() -> capy::io_task<> { + request req; + req.push("PING", ping_value); + auto [ec] = co_await conn.exec(req, resp); + BOOST_TEST_EQ(ec, error_code()); + co_return {}; + }; - ioc.run_for(test_timeout); + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_tls_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; + }; - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); - BOOST_TEST(std::get<0>(resp).value() == ping_value); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st + BOOST_TEST_EQ(std::get<0>(resp).value(), ping_value); } -// After an error, a TLS connection can recover. -// Force an error using QUIT, then issue a regular request to verify that we could reconnect -BOOST_AUTO_TEST_CASE(reconnection) +// After an error, a connection can recover. +// Force an error using QUIT, then issue a regular request to verify that we could reconnect. +capy::task<> test_reconnection() { - // Setup - net::io_context ioc; - net::steady_timer timer{ioc}; - connection conn{ioc}; - - request ping_request; - ping_request.push("PING", "some_value"); - ping_request.get_config().cancel_if_unresponded = false; - ping_request.get_config().cancel_on_connection_lost = false; - - request quit_request; - quit_request.push("QUIT"); - - bool exec_finished = false, run_finished = false; - - // Run the connection - conn.async_run(make_test_config(), [&](error_code ec) { - run_finished = true; - BOOST_CHECK_EQUAL(ec, canceled_condition()); - }); - - // The PING is the end of the callback chain - auto ping_callback = [&](error_code ec, std::size_t) { - exec_finished = true; - BOOST_TEST(ec == error_code()); - conn.cancel(); + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + request quit_request; + quit_request.push("QUIT"); + auto [ec_quit] = co_await conn.exec(quit_request, ignore); + BOOST_TEST_EQ(ec_quit, error_code()); + + request ping_request; + ping_request.push("PING", "some_value"); + ping_request.get_config().cancel_if_unresponded = false; + ping_request.get_config().cancel_on_connection_lost = false; + auto [ec_ping] = co_await conn.exec(ping_request, ignore); + BOOST_TEST_EQ(ec_ping, error_code()); + + co_return {}; }; - auto quit_callback = [&](error_code ec, std::size_t) { - BOOST_TEST(ec == error_code()); - conn.async_exec(ping_request, ignore, ping_callback); + auto run_fn = [&]() -> capy::io_task<> { + auto [ec] = co_await conn.run(make_test_config()); + BOOST_TEST_EQ(ec, canceled_condition()); + co_return {}; }; - conn.async_exec(quit_request, ignore, quit_callback); + auto result = co_await capy::when_any(exec_fn(), run_fn()); + BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st +} + +} // namespace - ioc.run_for(test_timeout); +int main() +{ + run_coroutine_test(test_exec_default_ssl_context()); + run_coroutine_test(test_exec_custom_ssl_context()); + run_coroutine_test(test_reconnection()); - BOOST_TEST(exec_finished); - BOOST_TEST(run_finished); + return boost::report_errors(); } - -} // namespace \ No newline at end of file From 2a1210c48835523df963e57246fde2da4a409ff6 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 14:04:30 +0200 Subject: [PATCH 094/115] Improve TLS tests --- test/test_co_tls.cpp | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/test/test_co_tls.cpp b/test/test_co_tls.cpp index 605d40d7..949cb71b 100644 --- a/test/test_co_tls.cpp +++ b/test/test_co_tls.cpp @@ -63,27 +63,21 @@ config make_tls_config() return cfg; } -// Using a default TLS context with verification disabled +// Using the default TLS context (the one created if nothing is passed to the ctor) // allows establishing TLS connections and execute requests -capy::task<> test_exec_default_ssl_context() +capy::task<> test_exec_default_tls_context() { - constexpr std::string_view ping_value = "Kabuf"; - - // The custom server uses a certificate signed by a CA - // that is not trusted by default - skip verification. - corosio::tls_context tls_ctx; - auto ec_mode = tls_ctx.set_verify_mode(corosio::tls_verify_mode::none); - BOOST_TEST_EQ(ec_mode, error_code()); - - co_connection conn{co_await capy::this_coro::executor, std::move(tls_ctx)}; - - response resp; + co_connection conn{co_await capy::this_coro::executor}; auto exec_fn = [&]() -> capy::io_task<> { request req; - req.push("PING", ping_value); + req.push("PING", "test_co_tls"); + response resp; + auto [ec] = co_await conn.exec(req, resp); BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), "test_co_tls"); + co_return {}; }; @@ -95,21 +89,16 @@ capy::task<> test_exec_default_ssl_context() auto result = co_await capy::when_any(exec_fn(), run_fn()); BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st - BOOST_TEST_EQ(std::get<0>(resp).value(), ping_value); } // Users can pass a custom context with TLS config capy::task<> test_exec_custom_ssl_context() { - constexpr std::string_view ping_value = "Kabuf"; - - std::string ca_pem = load_ca_certificate(); - // Configure the TLS context to trust the CA that signed the server's certificate. // The test certificate uses "redis" as its common name, // regardless of the actual server's hostname. corosio::tls_context tls_ctx; - auto ec_ca = tls_ctx.add_certificate_authority(ca_pem); + auto ec_ca = tls_ctx.add_certificate_authority(load_ca_certificate()); BOOST_TEST_EQ(ec_ca, error_code()); auto ec_mode = tls_ctx.set_verify_mode(corosio::tls_verify_mode::require_peer); BOOST_TEST_EQ(ec_mode, error_code()); @@ -117,13 +106,16 @@ capy::task<> test_exec_custom_ssl_context() co_connection conn{co_await capy::this_coro::executor, std::move(tls_ctx)}; - response resp; - auto exec_fn = [&]() -> capy::io_task<> { + constexpr std::string_view ping_value = "Kabuf"; request req; req.push("PING", ping_value); + response resp; + auto [ec] = co_await conn.exec(req, resp); BOOST_TEST_EQ(ec, error_code()); + BOOST_TEST_EQ(std::get<0>(resp).value(), ping_value); + co_return {}; }; @@ -135,7 +127,6 @@ capy::task<> test_exec_custom_ssl_context() auto result = co_await capy::when_any(exec_fn(), run_fn()); BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st - BOOST_TEST_EQ(std::get<0>(resp).value(), ping_value); } // After an error, a connection can recover. @@ -174,7 +165,7 @@ capy::task<> test_reconnection() int main() { - run_coroutine_test(test_exec_default_ssl_context()); + run_coroutine_test(test_exec_default_tls_context()); run_coroutine_test(test_exec_custom_ssl_context()); run_coroutine_test(test_reconnection()); From 77c31d322b0de6857d3e02e461e222a6d768e04b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 14:10:05 +0200 Subject: [PATCH 095/115] Refactor duplicate fn --- test/corosio_common.cpp | 43 ++++++++++++++++++++++++++++++++- test/corosio_common.hpp | 8 +++++- test/test_co_sentinel.cpp | 51 +++++++++------------------------------ test/test_co_setup.cpp | 32 ------------------------ 4 files changed, 60 insertions(+), 74 deletions(-) diff --git a/test/corosio_common.cpp b/test/corosio_common.cpp index fda980d3..cafdb059 100644 --- a/test/corosio_common.cpp +++ b/test/corosio_common.cpp @@ -6,8 +6,11 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include + #include #include +#include #include #include @@ -16,6 +19,8 @@ #include +namespace capy = boost::capy; + void boost::redis::test::run_coroutine_test(capy::task test, source_location loc) { // Set a timeout to the tests, so they don't hang on error @@ -33,4 +38,40 @@ void boost::redis::test::run_coroutine_test(capy::task test, source_locati // Check that it finished if (!BOOST_TEST(finished)) std::cerr << " Called from " << loc << std::endl; -} \ No newline at end of file +} + +capy::task boost::redis::test::create_user( + std::string_view port, + std::string_view username, + std::string_view password, + source_location loc) +{ + co_connection conn{co_await capy::this_coro::executor}; + + auto exec_fn = [&]() -> capy::io_task<> { + // Enable the user and grant them permissions on everything + request req; + req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); + + auto [ec] = co_await conn.exec(req, ignore); + if (!BOOST_TEST_EQ(ec, std::error_code())) + std::cerr << " Called from " << loc << std::endl; + + co_return {}; + }; + + auto run_fn = [&]() -> capy::io_task<> { + config cfg; + cfg.addr.port = port; + + auto [ec] = co_await conn.run(cfg); + if (!BOOST_TEST_EQ(ec, canceled_condition())) + std::cerr << " Called from " << loc << std::endl; + + co_return {}; + }; + + auto result = co_await capy::when_any(exec_fn(), run_fn()); + if (!BOOST_TEST_EQ(result.index(), 1u)) // Exec finished 1st + std::cerr << " Called from " << loc << std::endl; +} diff --git a/test/corosio_common.hpp b/test/corosio_common.hpp index 511a1138..fe1988b9 100644 --- a/test/corosio_common.hpp +++ b/test/corosio_common.hpp @@ -16,6 +16,12 @@ namespace boost::redis::test { void run_coroutine_test(capy::task test, source_location loc = BOOST_CURRENT_LOCATION); -} +capy::task<> create_user( + std::string_view port, + std::string_view username, + std::string_view password, + source_location loc = BOOST_CURRENT_LOCATION); + +} // namespace boost::redis::test #endif diff --git a/test/test_co_sentinel.cpp b/test/test_co_sentinel.cpp index c0dde09c..1a08647e 100644 --- a/test/test_co_sentinel.cpp +++ b/test/test_co_sentinel.cpp @@ -45,40 +45,6 @@ using error_code = std::error_code; namespace { -// RP TODO: this is duplicate -// Connects to the Redis server at the given port and creates a user -capy::task create_user( - std::string_view port, - std::string_view username, - std::string_view password) -{ - co_connection conn{co_await capy::this_coro::executor}; - - auto exec_fn = [&]() -> capy::io_task<> { - // Enable the user and grant them permissions on everything - request req; - req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); - - auto [ec] = co_await conn.exec(req, ignore); - BOOST_TEST_EQ(ec, error_code()); - - co_return {}; - }; - - auto run_fn = [&]() -> capy::io_task<> { - config cfg; - cfg.addr.port = port; - - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, canceled_condition()); - - co_return {}; - }; - - auto result = co_await capy::when_any(exec_fn(), run_fn()); - BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st -} - config make_sentinel_config() { config cfg; @@ -425,17 +391,22 @@ capy::task<> test_error_unknown_master_replica() } } +capy::task<> create_all_users() +{ + co_await create_user("6379", "redis_user", "redis_pass"); + co_await create_user("6380", "redis_user", "redis_pass"); + co_await create_user("6381", "redis_user", "redis_pass"); + co_await create_user("26379", "sentinel_user", "sentinel_pass"); + co_await create_user("26380", "sentinel_user", "sentinel_pass"); + co_await create_user("26381", "sentinel_user", "sentinel_pass"); +} + } // namespace int main() { // Create the required users in the master, replicas and sentinels - run_coroutine_test(create_user("6379", "redis_user", "redis_pass")); - run_coroutine_test(create_user("6380", "redis_user", "redis_pass")); - run_coroutine_test(create_user("6381", "redis_user", "redis_pass")); - run_coroutine_test(create_user("26379", "sentinel_user", "sentinel_pass")); - run_coroutine_test(create_user("26380", "sentinel_user", "sentinel_pass")); - run_coroutine_test(create_user("26381", "sentinel_user", "sentinel_pass")); + run_coroutine_test(create_all_users()); // Actual tests run_coroutine_test(test_exec()); diff --git a/test/test_co_setup.cpp b/test/test_co_setup.cpp index e2541349..6398fe9a 100644 --- a/test/test_co_setup.cpp +++ b/test/test_co_setup.cpp @@ -36,38 +36,6 @@ namespace corosio = boost::corosio; namespace { -capy::task create_user( - std::string_view port, - std::string_view username, - std::string_view password) -{ - co_connection conn{co_await capy::this_coro::executor}; - - auto exec_fn = [&]() -> capy::io_task<> { - // Enable the user and grant them permissions on everything - request req; - req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all"); - - auto [ec] = co_await conn.exec(req, ignore); - BOOST_TEST_EQ(ec, std::error_code()); - - co_return {}; - }; - - auto run_fn = [&]() -> capy::io_task<> { - config cfg; - cfg.addr.port = port; - - auto [ec] = co_await conn.run(cfg); - BOOST_TEST_EQ(ec, canceled_condition()); - - co_return {}; - }; - - auto result = co_await capy::when_any(exec_fn(), run_fn()); - BOOST_TEST_EQ(result.index(), 1u); // Exec finished 1st -} - capy::task<> test_auth_success() { // Setup From af641c064f091a4be432b86f0f4f55622f75ece9 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 14:12:31 +0200 Subject: [PATCH 096/115] cmake --- test/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 84c3cd8f..dfb0bddb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -76,25 +76,31 @@ make_test(test_conn_exec_retry boost_redis_tests_asio) make_test(test_conn_exec_error boost_redis_tests_asio) make_test(test_run boost_redis_tests_asio) make_test(test_conn_run_cancel boost_redis_tests_asio) +make_test(test_co_run_cancel boost_redis_tests_corosio) make_test(test_conn_check_health boost_redis_tests_asio) make_test(test_co_check_health boost_redis_tests_corosio) make_test(test_conn_exec boost_redis_tests_asio) make_test(test_conn_push boost_redis_tests_asio) make_test(test_conn_push2 boost_redis_tests_asio) +make_test(test_co_push2 boost_redis_tests_corosio) make_test(test_conn_monitor boost_redis_tests_asio) make_test(test_conn_reconnect boost_redis_tests_asio) make_test(test_conn_exec_cancel boost_redis_tests_asio) +make_test(test_co_exec_cancel boost_redis_tests_corosio) make_test(test_conn_echo_stress boost_redis_tests_asio) make_test(test_conn_move boost_redis_tests_asio) +make_test(test_co_move boost_redis_tests_corosio) make_test(test_conn_setup boost_redis_tests_asio) make_test(test_co_setup boost_redis_tests_corosio) make_test(test_issue_50 boost_redis_tests_asio) make_test(test_conversions boost_redis_tests_asio) make_test(test_conn_tls boost_redis_tests_asio) +make_test(test_co_tls boost_redis_tests_corosio) make_test(test_unix_sockets boost_redis_tests_asio) make_test(test_co_unix_sockets boost_redis_tests_corosio) make_test(test_conn_cancel_after boost_redis_tests_asio) make_test(test_conn_sentinel boost_redis_tests_asio) +make_test(test_co_sentinel boost_redis_tests_corosio) # Coverage set( From 5ece8066c015ad22cc43cb48edf7c470541a1ae7 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 15:23:04 +0200 Subject: [PATCH 097/115] Recover asio example --- example/cpp20_intro.cpp | 60 ++++++++++++++--------------------------- example/main.cpp | 44 +++++++++++++++--------------- 2 files changed, 42 insertions(+), 62 deletions(-) diff --git a/example/cpp20_intro.cpp b/example/cpp20_intro.cpp index f1130192..9c1637a1 100644 --- a/example/cpp20_intro.cpp +++ b/example/cpp20_intro.cpp @@ -4,24 +4,28 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include -#include -#include -#include -#include -#include +#include +#include +#include -#include #include -namespace capy = boost::capy; -using namespace boost::redis; -namespace corosio = boost::corosio; +#if defined(BOOST_ASIO_HAS_CO_AWAIT) -capy::task run_request(co_connection& conn) +namespace asio = boost::asio; +using boost::redis::request; +using boost::redis::response; +using boost::redis::config; +using boost::redis::connection; + +// Called from the main function (see main.cpp) +auto co_main(config cfg) -> asio::awaitable { + auto conn = std::make_shared(co_await asio::this_coro::executor); + conn->async_run(cfg, asio::consign(asio::detached, conn)); + // A request containing only a ping command. request req; req.push("PING", "Hello world"); @@ -30,34 +34,10 @@ capy::task run_request(co_connection& conn) response resp; // Executes the request. - auto [ec] = co_await conn.exec(req, resp); - if (ec) - co_return; - std::cout << "PING value: " << std::get<0>(resp).value() << std::endl; -} - -capy::task co_main() -{ - // Create a connection - co_connection conn{(co_await capy::this_coro::executor).context()}; + co_await conn->async_exec(req, resp); + conn->cancel(); - auto r = co_await capy::when_any(run_request(conn), conn.run(config{})); - - static_cast(r); + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; } -struct handler { - void operator()() { std::cout << "Done\n"; } - void operator()(std::exception_ptr exc) - { - if (exc) - std::rethrow_exception(exc); - } -}; - -int main() -{ - corosio::io_context ctx; - capy::run_async(ctx.get_executor())(co_main()); - ctx.run(); -} +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/example/main.cpp b/example/main.cpp index 1dbaa5ca..43bcbc6b 100644 --- a/example/main.cpp +++ b/example/main.cpp @@ -21,28 +21,28 @@ using boost::redis::logger; extern asio::awaitable co_main(config); -// auto main(int argc, char* argv[]) -> int -// { -// try { -// config cfg; - -// if (argc == 3) { -// cfg.addr.host = argv[1]; -// cfg.addr.port = argv[2]; -// } - -// asio::io_context ioc; -// asio::co_spawn(ioc, co_main(cfg), [](std::exception_ptr p) { -// if (p) -// std::rethrow_exception(p); -// }); -// ioc.run(); - -// } catch (std::exception const& e) { -// std::cerr << "(main) " << e.what() << std::endl; -// return 1; -// } -// } +auto main(int argc, char* argv[]) -> int +{ + try { + config cfg; + + if (argc == 3) { + cfg.addr.host = argv[1]; + cfg.addr.port = argv[2]; + } + + asio::io_context ioc; + asio::co_spawn(ioc, co_main(cfg), [](std::exception_ptr p) { + if (p) + std::rethrow_exception(p); + }); + ioc.run(); + + } catch (std::exception const& e) { + std::cerr << "(main) " << e.what() << std::endl; + return 1; + } +} #else // defined(BOOST_ASIO_HAS_CO_AWAIT) From 21217712d673d92d60710611d162c66a4412adfd Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 15:26:37 +0200 Subject: [PATCH 098/115] Sanitize example cmake --- example/CMakeLists.txt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 3ce3e8d8..e7505049 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -1,10 +1,8 @@ add_library(examples_main STATIC main.cpp) -target_link_libraries(examples_main PRIVATE boost_redis_project_options) +target_link_libraries(examples_main PUBLIC boost_redis_asio) function(make_example EXAMPLE_NAME) add_executable(${EXAMPLE_NAME} ${EXAMPLE_NAME}.cpp) - target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_redis_src) - target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_redis_project_options) if (ARGN) target_link_libraries(${EXAMPLE_NAME} PRIVATE ${ARGN}) endif() @@ -15,8 +13,8 @@ function(make_testable_example EXAMPLE_NAME) add_test(${EXAMPLE_NAME} ${EXAMPLE_NAME} $ENV{BOOST_REDIS_TEST_SERVER} 6379) endfunction() -make_testable_example(cpp17_intro) -make_testable_example(cpp17_intro_sync) +make_testable_example(cpp17_intro boost_redis_asio) +make_testable_example(cpp17_intro_sync boost_redis_asio) make_testable_example(cpp20_intro examples_main) make_testable_example(cpp20_containers examples_main) @@ -25,13 +23,13 @@ make_testable_example(cpp20_unix_sockets examples_main) make_testable_example(cpp20_timeouts examples_main) make_testable_example(cpp20_sentinel examples_main) -make_testable_example(corosio_intro) - make_example(cpp20_subscriber examples_main) make_example(cpp20_streams examples_main) make_example(cpp20_echo_server examples_main) make_example(cpp20_intro_tls examples_main) +make_testable_example(corosio_intro boost_redis_corosio) + # We test the protobuf example only on gcc. if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") find_package(Protobuf) From 447163678c6507322a540c4a73acff8cae7fbc80 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 17:12:35 +0200 Subject: [PATCH 099/115] Corosio subscriber --- example/CMakeLists.txt | 2 + example/corosio_subscriber.cpp | 129 +++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 example/corosio_subscriber.cpp diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index e7505049..4c48df93 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -30,6 +30,8 @@ make_example(cpp20_intro_tls examples_main) make_testable_example(corosio_intro boost_redis_corosio) +make_example(corosio_subscriber boost_redis_corosio) + # We test the protobuf example only on gcc. if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") find_package(Protobuf) diff --git a/example/corosio_subscriber.cpp b/example/corosio_subscriber.cpp new file mode 100644 index 00000000..489e0a68 --- /dev/null +++ b/example/corosio_subscriber.cpp @@ -0,0 +1,129 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; + +/* This example will subscribe and read pushes indefinitely. + * + * To test send messages with redis-cli + * + * $ redis-cli -3 + * 127.0.0.1:6379> PUBLISH mychannel some-message + * (integer) 3 + * 127.0.0.1:6379> + * + * To test reconnection try, for example, to close all clients currently + * connected to the Redis instance + * + * $ redis-cli + * > CLIENT kill TYPE pubsub + */ + +// Receives server pushes. +capy::io_task<> receiver(co_connection& conn) +{ + generic_flat_response resp; + conn.set_receive_response(resp); + + // Subscribe to the channel 'mychannel'. You can add any number of channels here. + request req; + req.subscribe({"mychannel"}); + auto [sub_ec] = co_await conn.exec(req); + if (sub_ec) { + std::cerr << "Error subscribing: " << sub_ec << std::endl; + co_return {}; + } + + // You're now subscribed to 'mychannel'. Pushes sent over this channel will be stored + // in resp. If the connection encounters a network error and reconnects to the server, + // it will automatically subscribe to 'mychannel' again. This is transparent to the user. + // You need to use the specialized request::subscribe() function (instead of request::push) + // to enable this behavior. + + // Loop to read Redis push messages. The loop terminates when receive() reports an error + // (e.g. cancellation when the surrounding when_any cascade tears down the run loop). + while (true) { + // Wait for pushes + auto [ec] = co_await conn.receive(); + + // Check for errors and cancellations + if (ec) { + std::cerr << "Error during receive: " << ec << std::endl; + co_return {}; + } + + // This can happen if a SUBSCRIBE command errored (e.g. insufficient permissions) + if (resp.has_error()) { + std::cerr << "The receive response contains an error: " << resp.error().diagnostic + << std::endl; + co_return {}; + } + + // The response must be consumed without suspending the + // coroutine i.e. without the use of async operations. + for (push_view elem : push_parser(resp.value())) { + std::cout << "Received message from channel " << elem.channel << ": " << elem.payload + << "\n"; + } + + resp.value().clear(); + } +} + +capy::task co_main() +{ + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection and the receiver loop in parallel. + // when_any will cancel the surviving task once the other completes. + co_await capy::when_any(receiver(conn), conn.run(config{})); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + exit(1); + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} From 9c82bc77aa9ce972bb75a2ff702c08d484e08238 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 17:26:42 +0200 Subject: [PATCH 100/115] Install tabs --- doc/package-lock.json | 379 +++++++++++++++++++++++++++++++----------- doc/package.json | 3 +- 2 files changed, 284 insertions(+), 98 deletions(-) diff --git a/doc/package-lock.json b/doc/package-lock.json index 2139949a..3a6b0c3f 100644 --- a/doc/package-lock.json +++ b/doc/package-lock.json @@ -5,7 +5,8 @@ "packages": { "": { "dependencies": { - "@cppalliance/antora-cpp-reference-extension": "^0.0.6", + "@asciidoctor/tabs": "^1.0.0-beta.6", + "@cppalliance/antora-cpp-reference-extension": "^0.1.0", "antora": "^3.1.10" } }, @@ -67,15 +68,6 @@ "node": ">=16.0.0" } }, - "node_modules/@antora/content-aggregator/node_modules/@antora/expand-path-helper": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-3.0.0.tgz", - "integrity": "sha512-7PdEIhk97v85/CSm3HynCsX14TR6oIVz1s233nNLsiWubE8tTnpPt4sNRJR+hpmIZ6Bx9c6QDp3XIoiyu/WYYA==", - "license": "MPL-2.0", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@antora/content-aggregator/node_modules/isomorphic-git": { "version": "1.25.10", "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.25.10.tgz", @@ -129,12 +121,12 @@ } }, "node_modules/@antora/expand-path-helper": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-2.0.0.tgz", - "integrity": "sha512-CSMBGC+tI21VS2kGW3PV7T2kQTM5eT3f2GTPVLttwaNYbNxDve08en/huzszHJfxo11CcEs26Ostr0F2c1QqeA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-3.0.0.tgz", + "integrity": "sha512-7PdEIhk97v85/CSm3HynCsX14TR6oIVz1s233nNLsiWubE8tTnpPt4sNRJR+hpmIZ6Bx9c6QDp3XIoiyu/WYYA==", "license": "MPL-2.0", "engines": { - "node": ">=10.17.0" + "node": ">=16.0.0" } }, "node_modules/@antora/file-publisher": { @@ -152,15 +144,6 @@ "node": ">=16.0.0" } }, - "node_modules/@antora/file-publisher/node_modules/@antora/expand-path-helper": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-3.0.0.tgz", - "integrity": "sha512-7PdEIhk97v85/CSm3HynCsX14TR6oIVz1s233nNLsiWubE8tTnpPt4sNRJR+hpmIZ6Bx9c6QDp3XIoiyu/WYYA==", - "license": "MPL-2.0", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@antora/logger": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/@antora/logger/-/logger-3.1.10.tgz", @@ -176,15 +159,6 @@ "node": ">=16.0.0" } }, - "node_modules/@antora/logger/node_modules/@antora/expand-path-helper": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-3.0.0.tgz", - "integrity": "sha512-7PdEIhk97v85/CSm3HynCsX14TR6oIVz1s233nNLsiWubE8tTnpPt4sNRJR+hpmIZ6Bx9c6QDp3XIoiyu/WYYA==", - "license": "MPL-2.0", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@antora/navigation-builder": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/@antora/navigation-builder/-/navigation-builder-3.1.10.tgz", @@ -310,15 +284,6 @@ "node": ">=16.0.0" } }, - "node_modules/@antora/ui-loader/node_modules/@antora/expand-path-helper": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-3.0.0.tgz", - "integrity": "sha512-7PdEIhk97v85/CSm3HynCsX14TR6oIVz1s233nNLsiWubE8tTnpPt4sNRJR+hpmIZ6Bx9c6QDp3XIoiyu/WYYA==", - "license": "MPL-2.0", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@antora/user-require-helper": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@antora/user-require-helper/-/user-require-helper-3.0.0.tgz", @@ -331,15 +296,6 @@ "node": ">=16.0.0" } }, - "node_modules/@antora/user-require-helper/node_modules/@antora/expand-path-helper": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-3.0.0.tgz", - "integrity": "sha512-7PdEIhk97v85/CSm3HynCsX14TR6oIVz1s233nNLsiWubE8tTnpPt4sNRJR+hpmIZ6Bx9c6QDp3XIoiyu/WYYA==", - "license": "MPL-2.0", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@asciidoctor/core": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-2.2.8.tgz", @@ -355,19 +311,28 @@ "yarn": ">=1.1.0" } }, + "node_modules/@asciidoctor/tabs": { + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@asciidoctor/tabs/-/tabs-1.0.0-beta.6.tgz", + "integrity": "sha512-gGZnW7UfRXnbiyKNd9PpGKtSuD8+DsqaaTSbQ1dHVkZ76NaolLhdQg8RW6/xqN3pX1vWZEcF4e81+Oe9rNRWxg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@cppalliance/antora-cpp-reference-extension": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@cppalliance/antora-cpp-reference-extension/-/antora-cpp-reference-extension-0.0.6.tgz", - "integrity": "sha512-Weud5Cn9KAoU3+fSA4IZM7THAEA8VPhclH7EfU6SiKSp/Iy92vSItpZioTmVrn0DIVo9tIxxrJXBp5GpSFk6hg==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@cppalliance/antora-cpp-reference-extension/-/antora-cpp-reference-extension-0.1.0.tgz", + "integrity": "sha512-3VD/gAFebR06GiBWAy2PgEHNqyRNrvAE0FfFvotLvA0RQmHn0q+ct+j0z53N64yxuvJVj8Hl0bRRPdhh2BGjXg==", "license": "BSL-1.0", "dependencies": { - "@antora/expand-path-helper": "^2.0.0", - "axios": "^1.7.2", + "@antora/expand-path-helper": "^3.0.0", + "axios": "^1.13.2", "cache-directory": "^2.0.0", - "fast-glob": "^3.3.2", - "isomorphic-git": "^1.27.1", + "fast-glob": "^3.3.3", + "isomorphic-git": "^1.35.0", "js-yaml": "^4.1.0", - "semver": "^7.6.3" + "semver": "^7.7.3" } }, "node_modules/@iarna/toml": { @@ -479,15 +444,30 @@ "node": ">=8.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" } }, "node_modules/b4a": { @@ -596,6 +576,24 @@ "node": ">=4" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -609,6 +607,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/clean-git-ref": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", @@ -706,6 +720,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -872,9 +903,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -891,10 +922,25 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -1022,6 +1068,18 @@ "uglify-js": "^3.1.4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1050,9 +1108,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1122,6 +1180,18 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1152,10 +1222,31 @@ "node": ">=0.12.0" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/isomorphic-git": { - "version": "1.31.0", - "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.31.0.tgz", - "integrity": "sha512-huMXDX1pMuBc0+GQSp7zD4ejqmrhqwWlK2X7p2dZyb7p2DKZWva4kRc4aH+0EE92W1j3+D7N4VRv35RCMWAeCQ==", + "version": "1.37.5", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.37.5.tgz", + "integrity": "sha512-wek54c5uFvd3WsxewLWt6h0GXKWQh0P8rRXns9bN1rHNjcgCb3+0lmyAsP594NeTtQFeCJQVS9b0kjbkD1l5qg==", "license": "MIT", "dependencies": { "async-lock": "^1.4.1", @@ -1165,10 +1256,9 @@ "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", - "path-browserify": "^1.0.1", "pify": "^4.0.1", - "readable-stream": "^3.4.0", - "sha.js": "^2.4.9", + "readable-stream": "^4.0.0", + "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { @@ -1178,6 +1268,22 @@ "node": ">=14.17" } }, + "node_modules/isomorphic-git/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -1362,12 +1468,6 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1499,6 +1599,15 @@ "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", "license": "MIT" }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -1524,10 +1633,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pump": { "version": "3.0.3", @@ -1681,9 +1793,9 @@ "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1692,17 +1804,41 @@ "node": ">=10" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/should-proxy": { @@ -1844,6 +1980,20 @@ "real-require": "^0.2.0" } }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1856,6 +2006,20 @@ "node": ">=8.0" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -1899,6 +2063,27 @@ "node": ">=10.13.0" } }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/doc/package.json b/doc/package.json index 955a7b5e..40de9972 100644 --- a/doc/package.json +++ b/doc/package.json @@ -1,6 +1,7 @@ { "dependencies": { - "@cppalliance/antora-cpp-reference-extension": "^0.0.6", + "@asciidoctor/tabs": "^1.0.0-beta.6", + "@cppalliance/antora-cpp-reference-extension": "^0.1.0", "antora": "^3.1.10" } } From 202d7fd42849e9493701b6c8719b5dec680e6859 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 17:35:38 +0200 Subject: [PATCH 101/115] Corosio subscriber docs --- doc/modules/ROOT/pages/pushes.adoc | 67 ++++++++++++++++++++++++++++++ doc/redis-playbook.yml | 2 + 2 files changed, 69 insertions(+) diff --git a/doc/modules/ROOT/pages/pushes.adoc b/doc/modules/ROOT/pages/pushes.adoc index cae1187d..974d738e 100644 --- a/doc/modules/ROOT/pages/pushes.adoc +++ b/doc/modules/ROOT/pages/pushes.adoc @@ -15,6 +15,12 @@ The most common case is https://redis.io/docs/latest/develop/pubsub/[Pub/Sub messages] triggered by `PUBLISH`. The following example shows a typical receiver: +[tabs] +======== +Asio:: ++ +-- + [source,cpp] ---- auto receiver(std::shared_ptr conn) -> asio::awaitable @@ -62,6 +68,67 @@ auto receiver(std::shared_ptr conn) -> asio::awaitable } } ---- +-- + +Corosio:: ++ +-- + +[source,cpp] +---- +capy::io_task<> receiver(co_connection& conn) +{ + generic_flat_response resp; + conn.set_receive_response(resp); + + // Subscribe to the channel 'mychannel'. You can add any number of channels here. + request req; + req.subscribe({"mychannel"}); + auto [sub_ec] = co_await conn.exec(req); + if (sub_ec) { + std::cerr << "Error subscribing: " << sub_ec << std::endl; + co_return {}; + } + + // You're now subscribed to 'mychannel'. Pushes sent over this channel will be stored + // in resp. If the connection encounters a network error and reconnects to the server, + // it will automatically subscribe to 'mychannel' again. This is transparent to the user. + // You need to use the specialized request::subscribe() function (instead of request::push) + // to enable this behavior. + + // Loop to read Redis push messages. The loop terminates when receive() reports an error + // (e.g. cancellation when the surrounding when_any cascade tears down the run loop). + while (true) { + // Wait for pushes + auto [ec] = co_await conn.receive(); + + // Check for errors and cancellations + if (ec) { + std::cerr << "Error during receive: " << ec << std::endl; + co_return {}; + } + + // This can happen if a SUBSCRIBE command errored (e.g. insufficient permissions) + if (resp.has_error()) { + std::cerr << "The receive response contains an error: " << resp.error().diagnostic + << std::endl; + co_return {}; + } + + // The response must be consumed without suspending the + // coroutine i.e. without the use of async operations. + for (push_view elem : push_parser(resp.value())) { + std::cout << "Received message from channel " << elem.channel << ": " << elem.payload + << "\n"; + } + + resp.value().clear(); + } +} +---- +-- + +======== Summary of the steps: diff --git a/doc/redis-playbook.yml b/doc/redis-playbook.yml index 54a901e8..7400a02c 100644 --- a/doc/redis-playbook.yml +++ b/doc/redis-playbook.yml @@ -25,6 +25,8 @@ asciidoc: attributes: # Scrolling problems appear without this page-pagination: '' + extensions: + - '@asciidoctor/tabs' content: sources: From 22e20cae4c89b1c44969cab508243787a82fb7e4 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 17:52:04 +0200 Subject: [PATCH 102/115] Copy the other examples --- example/corosio_chat_room.cpp | 135 ++++++++++++++++++++++++++++ example/corosio_containers.cpp | 146 +++++++++++++++++++++++++++++++ example/corosio_echo_server.cpp | 72 +++++++++++++++ example/corosio_intro_tls.cpp | 57 ++++++++++++ example/corosio_json.cpp | 82 +++++++++++++++++ example/corosio_protobuf.cpp | 89 +++++++++++++++++++ example/corosio_sentinel.cpp | 60 +++++++++++++ example/corosio_spdlog.cpp | 99 +++++++++++++++++++++ example/corosio_timeouts.cpp | 49 +++++++++++ example/corosio_unix_sockets.cpp | 60 +++++++++++++ 10 files changed, 849 insertions(+) create mode 100644 example/corosio_chat_room.cpp create mode 100644 example/corosio_containers.cpp create mode 100644 example/corosio_echo_server.cpp create mode 100644 example/corosio_intro_tls.cpp create mode 100644 example/corosio_json.cpp create mode 100644 example/corosio_protobuf.cpp create mode 100644 example/corosio_sentinel.cpp create mode 100644 example/corosio_spdlog.cpp create mode 100644 example/corosio_timeouts.cpp create mode 100644 example/corosio_unix_sockets.cpp diff --git a/example/corosio_chat_room.cpp b/example/corosio_chat_room.cpp new file mode 100644 index 00000000..8c056b0b --- /dev/null +++ b/example/corosio_chat_room.cpp @@ -0,0 +1,135 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) +#if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR) + +namespace asio = boost::asio; +using asio::posix::stream_descriptor; +using asio::signal_set; +using boost::asio::async_read_until; +using boost::asio::awaitable; +using boost::asio::co_spawn; +using boost::asio::consign; +using boost::asio::detached; +using boost::asio::dynamic_buffer; +using boost::redis::config; +using boost::redis::connection; +using boost::redis::generic_flat_response; +using boost::redis::request; +using boost::system::error_code; +using boost::redis::push_parser; +using boost::redis::push_view; +using namespace std::chrono_literals; + +// Chat over Redis pubsub. To test, run this program from multiple +// terminals and type messages to stdin. + +namespace { + +auto rethrow_on_error = [](std::exception_ptr exc) { + if (exc) + std::rethrow_exception(exc); +}; + +auto receiver(std::shared_ptr conn) -> awaitable +{ + // Set the receive response, so pushes are stored in resp + generic_flat_response resp; + conn->set_receive_response(resp); + + // Subscribe to the channel 'channel'. Using request::subscribe() + // (instead of request::push()) makes the connection re-subscribe + // to 'channel' whenever it re-connects to the server. + request req; + req.subscribe({"channel"}); + co_await conn->async_exec(req); + + while (conn->will_reconnect()) { + // Wait for pushes + auto [ec] = co_await conn->async_receive2(asio::as_tuple); + + // Check for errors and cancellations + if (ec) { + std::cerr << "Error during receive: " << ec << std::endl; + break; + } + + // This can happen if a SUBSCRIBE command errored (e.g. insufficient permissions) + if (resp.has_error()) { + std::cerr << "The receive response contains an error: " << resp.error().diagnostic + << std::endl; + break; + } + + // The response must be consumed without suspending the + // coroutine i.e. without the use of async operations. + for (push_view elem : push_parser(resp.value())) { + std::cout << "Received message from channel " << elem.channel << ": " << elem.payload + << "\n"; + } + + std::cout << std::endl; + + resp.value().clear(); + } +} + +// Publishes stdin messages to a Redis channel. +auto publisher(std::shared_ptr in, std::shared_ptr conn) + -> awaitable +{ + for (std::string msg;;) { + auto n = co_await async_read_until(*in, dynamic_buffer(msg, 1024), "\n"); + request req; + req.push("PUBLISH", "channel", msg); + co_await conn->async_exec(req); + msg.erase(0, n); + } +} + +} // namespace + +// Called from the main function (see main.cpp) +auto co_main(config cfg) -> awaitable +{ + auto ex = co_await asio::this_coro::executor; + auto conn = std::make_shared(ex); + auto stream = std::make_shared(ex, ::dup(STDIN_FILENO)); + + co_spawn(ex, receiver(conn), rethrow_on_error); + co_spawn(ex, publisher(stream, conn), rethrow_on_error); + conn->async_run(cfg, consign(detached, conn)); + + signal_set sig_set{ex, SIGINT, SIGTERM}; + co_await sig_set.async_wait(); + conn->cancel(); + stream->cancel(); +} + +#else // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR) +auto co_main(config const&) -> awaitable +{ + std::cout << "Requires support for posix streams." << std::endl; + co_return; +} +#endif // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR) +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/example/corosio_containers.cpp b/example/corosio_containers.cpp new file mode 100644 index 00000000..7c648e38 --- /dev/null +++ b/example/corosio_containers.cpp @@ -0,0 +1,146 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include + +#include +#include +#include + +#include +#include +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +namespace asio = boost::asio; +using boost::redis::request; +using boost::redis::response; +using boost::redis::ignore_t; +using boost::redis::ignore; +using boost::redis::config; +using boost::redis::connection; +using boost::asio::awaitable; +using boost::asio::detached; +using boost::asio::consign; + +template +std::ostream& operator<<(std::ostream& os, std::optional const& opt) +{ + if (opt.has_value()) + std::cout << opt.value(); + else + std::cout << "null"; + + return os; +} + +void print(std::map const& cont) +{ + for (auto const& e : cont) + std::cout << e.first << ": " << e.second << "\n"; +} + +template +void print(std::vector const& cont) +{ + for (auto const& e : cont) + std::cout << e << " "; + std::cout << "\n"; +} + +// Stores the content of some STL containers in Redis. +auto store(std::shared_ptr conn) -> awaitable +{ + std::vector vec{1, 2, 3, 4, 5, 6}; + + std::map map{ + {"key1", "value1"}, + {"key2", "value2"}, + {"key3", "value3"} + }; + + request req; + req.push_range("RPUSH", "rpush-key", vec); + req.push_range("HSET", "hset-key", map); + req.push("SET", "key", "value"); + + co_await conn->async_exec(req, ignore); +} + +auto hgetall(std::shared_ptr conn) -> awaitable +{ + // A request contains multiple commands. + request req; + req.push("HGETALL", "hset-key"); + + // Responses as tuple elements. + response> resp; + + // Executes the request and reads the response. + co_await conn->async_exec(req, resp); + + print(std::get<0>(resp).value()); +} + +auto mget(std::shared_ptr conn) -> awaitable +{ + // A request contains multiple commands. + request req; + req.push("MGET", "key", "non-existing-key"); + + // Responses as tuple elements. + response>> resp; + + // Executes the request and reads the response. + co_await conn->async_exec(req, resp); + + print(std::get<0>(resp).value()); +} + +// Retrieves in a transaction. +auto transaction(std::shared_ptr conn) -> awaitable +{ + request req; + req.push("MULTI"); + req.push("LRANGE", "rpush-key", 0, -1); // Retrieves + req.push("HGETALL", "hset-key"); // Retrieves + req.push("MGET", "key", "non-existing-key"); + req.push("EXEC"); + + response< + ignore_t, // multi + ignore_t, // lrange + ignore_t, // hgetall + ignore_t, // mget + response< + std::optional>, + std::optional>, + std::optional>>> // exec + > + resp; + + co_await conn->async_exec(req, resp); + + print(std::get<0>(std::get<4>(resp).value()).value().value()); + print(std::get<1>(std::get<4>(resp).value()).value().value()); + print(std::get<2>(std::get<4>(resp).value()).value().value()); +} + +// Called from the main function (see main.cpp) +awaitable co_main(config cfg) +{ + auto conn = std::make_shared(co_await asio::this_coro::executor); + conn->async_run(cfg, consign(detached, conn)); + + co_await store(conn); + co_await transaction(conn); + co_await hgetall(conn); + co_await mget(conn); + conn->cancel(); +} + +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/example/corosio_echo_server.cpp b/example/corosio_echo_server.cpp new file mode 100644 index 00000000..f4e27751 --- /dev/null +++ b/example/corosio_echo_server.cpp @@ -0,0 +1,72 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +namespace asio = boost::asio; +using boost::asio::signal_set; +using boost::redis::request; +using boost::redis::response; +using boost::redis::config; +using boost::system::error_code; +using boost::redis::connection; +using namespace std::chrono_literals; + +auto echo_server_session(asio::ip::tcp::socket socket, std::shared_ptr conn) + -> asio::awaitable +{ + request req; + response resp; + + for (std::string buffer;;) { + auto n = co_await asio::async_read_until(socket, asio::dynamic_buffer(buffer, 1024), "\n"); + req.push("PING", buffer); + co_await conn->async_exec(req, resp); + co_await asio::async_write(socket, asio::buffer(std::get<0>(resp).value())); + std::get<0>(resp).value().clear(); + req.clear(); + buffer.erase(0, n); + } +} + +// Listens for tcp connections. +auto listener(std::shared_ptr conn) -> asio::awaitable +{ + try { + auto ex = co_await asio::this_coro::executor; + asio::ip::tcp::acceptor acc(ex, {asio::ip::tcp::v4(), 55555}); + for (;;) + asio::co_spawn(ex, echo_server_session(co_await acc.async_accept(), conn), asio::detached); + } catch (std::exception const& e) { + std::clog << "Listener: " << e.what() << std::endl; + } +} + +// Called from the main function (see main.cpp) +auto co_main(config cfg) -> asio::awaitable +{ + auto ex = co_await asio::this_coro::executor; + auto conn = std::make_shared(ex); + asio::co_spawn(ex, listener(conn), asio::detached); + conn->async_run(cfg, asio::consign(asio::detached, conn)); + + signal_set sig_set(ex, SIGINT, SIGTERM); + co_await sig_set.async_wait(); + conn->cancel(); +} + +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/example/corosio_intro_tls.cpp b/example/corosio_intro_tls.cpp new file mode 100644 index 00000000..6321c9e8 --- /dev/null +++ b/example/corosio_intro_tls.cpp @@ -0,0 +1,57 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include + +#include +#include +#include +#include + +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +namespace asio = boost::asio; +using boost::redis::request; +using boost::redis::response; +using boost::redis::config; +using boost::redis::logger; +using boost::redis::connection; + +auto verify_certificate(bool, asio::ssl::verify_context&) -> bool +{ + std::cout << "set_verify_callback" << std::endl; + return true; +} + +auto co_main(config cfg) -> asio::awaitable +{ + cfg.use_ssl = true; + cfg.username = "aedis"; + cfg.password = "aedis"; + cfg.addr.host = "db.occase.de"; + cfg.addr.port = "6380"; + + asio::ssl::context ctx{asio::ssl::context::tlsv12_client}; + ctx.set_verify_mode(asio::ssl::verify_peer); + ctx.set_verify_callback(verify_certificate); + + auto conn = std::make_shared(co_await asio::this_coro::executor, std::move(ctx)); + conn->async_run(cfg, asio::consign(asio::detached, conn)); + + request req; + req.push("PING"); + + response resp; + + co_await conn->async_exec(req, resp); + conn->cancel(); + + std::cout << "Response: " << std::get<0>(resp).value() << std::endl; +} + +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/example/corosio_json.cpp b/example/corosio_json.cpp new file mode 100644 index 00000000..756d5275 --- /dev/null +++ b/example/corosio_json.cpp @@ -0,0 +1,82 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include + +#include +#include +#include +#include + +#include +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +#include + +#include +#include +#include +#include + +namespace asio = boost::asio; +namespace resp3 = boost::redis::resp3; +using namespace boost::describe; +using boost::redis::request; +using boost::redis::response; +using boost::redis::ignore_t; +using boost::redis::config; +using boost::redis::connection; +using boost::redis::resp3::node_view; + +// Struct that will be stored in Redis using json serialization. +struct user { + std::string name; + std::string age; + std::string country; +}; + +// The type must be described for serialization to work. +BOOST_DESCRIBE_STRUCT(user, (), (name, age, country)) + +// Boost.Redis customization points (example/json.hpp) +void boost_redis_to_bulk(std::string& to, user const& u) +{ + resp3::boost_redis_to_bulk(to, boost::json::serialize(boost::json::value_from(u))); +} + +void boost_redis_from_bulk(user& u, node_view const& node, boost::system::error_code&) +{ + u = boost::json::value_to(boost::json::parse(node.value)); +} + +auto co_main(config cfg) -> asio::awaitable +{ + auto ex = co_await asio::this_coro::executor; + auto conn = std::make_shared(ex); + conn->async_run(cfg, asio::consign(asio::detached, conn)); + + // user object that will be stored in Redis in json format. + user const u{"Joao", "58", "Brazil"}; + + // Stores and retrieves in the same request. + request req; + req.push("SET", "json-key", u); // Stores in Redis. + req.push("GET", "json-key"); // Retrieves from Redis. + + response resp; + + co_await conn->async_exec(req, resp); + conn->cancel(); + + // Prints the first ping + std::cout << "Name: " << std::get<1>(resp).value().name << "\n" + << "Age: " << std::get<1>(resp).value().age << "\n" + << "Country: " << std::get<1>(resp).value().country << "\n"; +} + +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/example/corosio_protobuf.cpp b/example/corosio_protobuf.cpp new file mode 100644 index 00000000..e23415a0 --- /dev/null +++ b/example/corosio_protobuf.cpp @@ -0,0 +1,89 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include + +#include +#include +#include +#include + +#include + +// See the definition in person.proto. This header is automatically +// generated by CMakeLists.txt. +#include "person.pb.h" + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +namespace asio = boost::asio; +namespace resp3 = boost::redis::resp3; +using boost::redis::request; +using boost::redis::response; +using boost::redis::operation; +using boost::redis::ignore_t; +using boost::redis::config; +using boost::redis::connection; +using boost::redis::resp3::node_view; + +// The protobuf type described in example/person.proto +using tutorial::person; + +// Boost.Redis customization points (example/protobuf.hpp) +namespace tutorial { + +// Below I am using a Boost.Redis to indicate a protobuf error, this +// is ok for an example, users however might want to define their own +// error codes. +void boost_redis_to_bulk(std::string& to, person const& u) +{ + std::string tmp; + if (!u.SerializeToString(&tmp)) + throw boost::system::system_error(boost::redis::error::invalid_data_type); + + resp3::boost_redis_to_bulk(to, tmp); +} + +void boost_redis_from_bulk(person& u, node_view const& node, boost::system::error_code& ec) +{ + std::string const tmp{node.value}; + if (!u.ParseFromString(tmp)) + ec = boost::redis::error::invalid_data_type; +} + +} // namespace tutorial + +using tutorial::boost_redis_to_bulk; +using tutorial::boost_redis_from_bulk; + +asio::awaitable co_main(config cfg) +{ + auto ex = co_await asio::this_coro::executor; + auto conn = std::make_shared(ex); + conn->async_run(cfg, asio::consign(asio::detached, conn)); + + person p; + p.set_name("Louis"); + p.set_id(3); + p.set_email("No email yet."); + + request req; + req.push("SET", "protobuf-key", p); + req.push("GET", "protobuf-key"); + + response resp; + + // Sends the request and receives the response. + co_await conn->async_exec(req, resp); + conn->cancel(); + + std::cout << "Name: " << std::get<1>(resp).value().name() << "\n" + << "Age: " << std::get<1>(resp).value().id() << "\n" + << "Email: " << std::get<1>(resp).value().email() << "\n"; +} + +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/example/corosio_sentinel.cpp b/example/corosio_sentinel.cpp new file mode 100644 index 00000000..5e51413b --- /dev/null +++ b/example/corosio_sentinel.cpp @@ -0,0 +1,60 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include + +#include +#include +#include + +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +namespace asio = boost::asio; +using boost::redis::request; +using boost::redis::response; +using boost::redis::config; +using boost::redis::connection; + +// Called from the main function (see main.cpp) +auto co_main(config cfg) -> asio::awaitable +{ + // Boost.Redis has built-in support for Sentinel deployments. + // To enable it, set the fields in config shown here. + // sentinel.addresses should contain a list of (hostname, port) pairs + // where Sentinels are listening. IPs can also be used. + cfg.sentinel.addresses = { + {"localhost", "26379"}, + {"localhost", "26380"}, + {"localhost", "26381"}, + }; + + // Set master_name to the identifier that you configured + // in the "sentinel monitor" statement of your sentinel.conf file + cfg.sentinel.master_name = "mymaster"; + + // async_run will contact the Sentinels, obtain the master address, + // connect to it and keep the connection healthy. If a failover happens, + // the address will be resolved again and the new elected master will be contacted. + auto conn = std::make_shared(co_await asio::this_coro::executor); + conn->async_run(cfg, asio::consign(asio::detached, conn)); + + // You can now use the connection normally, as you would use a connection to a single master. + request req; + req.push("PING", "Hello world"); + response resp; + + // Execute the request. + co_await conn->async_exec(req, resp); + conn->cancel(); + + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; +} + +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/example/corosio_spdlog.cpp b/example/corosio_spdlog.cpp new file mode 100644 index 00000000..d5be9120 --- /dev/null +++ b/example/corosio_spdlog.cpp @@ -0,0 +1,99 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace asio = boost::asio; +namespace redis = boost::redis; + +// Maps a Boost.Redis log level to a spdlog log level +static spdlog::level::level_enum to_spdlog_level(redis::logger::level lvl) +{ + switch (lvl) { + // spdlog doesn't include the emerg and alert syslog levels, + // so we convert them to the highest supported level. + // Similarly, notice is similar to info + case redis::logger::level::emerg: + case redis::logger::level::alert: + case redis::logger::level::crit: return spdlog::level::critical; + case redis::logger::level::err: return spdlog::level::err; + case redis::logger::level::warning: return spdlog::level::warn; + case redis::logger::level::notice: + case redis::logger::level::info: return spdlog::level::info; + case redis::logger::level::debug: + default: return spdlog::level::debug; + } +} + +// This function glues Boost.Redis logging and spdlog. +// It should have the signature shown here. It will be invoked +// by Boost.Redis whenever a message is to be logged. +static void do_log(redis::logger::level level, std::string_view msg) +{ + spdlog::log(to_spdlog_level(level), "(Boost.Redis) {}", msg); +} + +auto main(int argc, char** argv) -> int +{ + try { + // Create an execution context, required to create any I/O objects + asio::io_context ioc; + + // Create a connection to connect to Redis, and pass it a custom logger. + // Boost.Redis will call do_log whenever it needs to log a message. + // Note that the function will only be called for messages with level >= info + // (i.e. filtering is done by Boost.Redis). + redis::connection conn{ + ioc, + redis::logger{redis::logger::level::info, do_log} + }; + + // Configuration to connect to the server. Adjust as required + redis::config cfg; + if (argc == 3) { + cfg.addr.host = argv[1]; + cfg.addr.port = argv[2]; + } + + // Run the connection with the specified configuration. + // This will establish the connection and keep it healthy + conn.async_run(cfg, asio::detached); + + // Execute a request + redis::request req; + req.push("PING", "Hello world"); + + redis::response resp; + + conn.async_exec(req, resp, [&](boost::system::error_code ec, std::size_t /* bytes_read*/) { + if (ec) { + spdlog::error("Request failed: {}", ec.what()); + exit(1); + } else { + spdlog::info("PING: {}", std::get<0>(resp).value()); + } + conn.cancel(); + }); + + // Actually run our example. Nothing will happen until we call run() + ioc.run(); + + } catch (std::exception const& e) { + spdlog::error("Error: {}", e.what()); + return 1; + } +} diff --git a/example/corosio_timeouts.cpp b/example/corosio_timeouts.cpp new file mode 100644 index 00000000..98168339 --- /dev/null +++ b/example/corosio_timeouts.cpp @@ -0,0 +1,49 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include + +#include +#include +#include +#include + +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +namespace asio = boost::asio; +using boost::redis::request; +using boost::redis::response; +using boost::redis::config; +using boost::redis::connection; +using namespace std::chrono_literals; + +// Called from the main function (see main.cpp) +auto co_main(config cfg) -> asio::awaitable +{ + auto conn = std::make_shared(co_await asio::this_coro::executor); + conn->async_run(cfg, asio::consign(asio::detached, conn)); + + // A request containing only a ping command. + request req; + req.push("PING", "Hello world"); + + // Response where the PONG response will be stored. + response resp; + + // Executes the request with a timeout. If the server is down, + // async_exec will wait until it's back again, so it, + // may suspend for a long time. + // For this reason, it's good practice to set a timeout to requests with cancel_after. + // If the request hasn't completed after 10 seconds, an exception will be thrown. + co_await conn->async_exec(req, resp, asio::cancel_after(10s)); + conn->cancel(); + + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; +} + +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/example/corosio_unix_sockets.cpp b/example/corosio_unix_sockets.cpp new file mode 100644 index 00000000..3c69ea12 --- /dev/null +++ b/example/corosio_unix_sockets.cpp @@ -0,0 +1,60 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +#include + +#include +#include +#include + +#include + +namespace asio = boost::asio; +using boost::redis::request; +using boost::redis::response; +using boost::redis::config; +using boost::redis::logger; +using boost::redis::connection; + +#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS + +auto co_main(config cfg) -> asio::awaitable +{ + // If unix_socket is set to a non-empty string, UNIX domain sockets will be used + // instead of TCP. Set this value to the path where your server is listening. + // UNIX domain socket connections work in the same way as TCP connections. + cfg.unix_socket = "/tmp/redis-socks/redis.sock"; + + auto conn = std::make_shared(co_await asio::this_coro::executor); + conn->async_run(cfg, asio::consign(asio::detached, conn)); + + request req; + req.push("PING"); + + response resp; + + co_await conn->async_exec(req, resp); + conn->cancel(); + + std::cout << "Response: " << std::get<0>(resp).value() << std::endl; +} + +#else + +auto co_main(config) -> asio::awaitable +{ + std::cout << "Sorry, your system does not support UNIX domain sockets\n"; + co_return; +} + +#endif + +#endif From 6e62a1667578de1b84e4ae85e7f7fbd0c8c737c8 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:00:41 +0200 Subject: [PATCH 103/115] Initial impl --- example/corosio_chat_room.cpp | 151 +++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 66 deletions(-) diff --git a/example/corosio_chat_room.cpp b/example/corosio_chat_room.cpp index 8c056b0b..16cde625 100644 --- a/example/corosio_chat_room.cpp +++ b/example/corosio_chat_room.cpp @@ -4,80 +4,72 @@ * accompanying file LICENSE.txt) */ -#include +#include +#include #include - -#include -#include -#include -#include -#include -#include -#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include #include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR) - -namespace asio = boost::asio; -using asio::posix::stream_descriptor; -using asio::signal_set; -using boost::asio::async_read_until; -using boost::asio::awaitable; -using boost::asio::co_spawn; -using boost::asio::consign; -using boost::asio::detached; -using boost::asio::dynamic_buffer; -using boost::redis::config; -using boost::redis::connection; -using boost::redis::generic_flat_response; -using boost::redis::request; -using boost::system::error_code; -using boost::redis::push_parser; -using boost::redis::push_view; -using namespace std::chrono_literals; +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; // Chat over Redis pubsub. To test, run this program from multiple -// terminals and type messages to stdin. +// terminals and type messages to stdin. Press Ctrl+D to exit. namespace { -auto rethrow_on_error = [](std::exception_ptr exc) { - if (exc) - std::rethrow_exception(exc); -}; - -auto receiver(std::shared_ptr conn) -> awaitable +// Receives server pushes. +capy::io_task<> receiver(co_connection& conn) { // Set the receive response, so pushes are stored in resp generic_flat_response resp; - conn->set_receive_response(resp); + conn.set_receive_response(resp); // Subscribe to the channel 'channel'. Using request::subscribe() // (instead of request::push()) makes the connection re-subscribe // to 'channel' whenever it re-connects to the server. request req; req.subscribe({"channel"}); - co_await conn->async_exec(req); + auto [sub_ec] = co_await conn.exec(req); + if (sub_ec) { + std::cerr << "Error subscribing: " << sub_ec << std::endl; + co_return {}; + } - while (conn->will_reconnect()) { + // Loop to read Redis push messages. The loop terminates when receive() reports + // an error (e.g. cancellation when the surrounding when_any cascade tears down + // the run loop). + while (true) { // Wait for pushes - auto [ec] = co_await conn->async_receive2(asio::as_tuple); + auto [ec] = co_await conn.receive(); // Check for errors and cancellations if (ec) { std::cerr << "Error during receive: " << ec << std::endl; - break; + co_return {}; } // This can happen if a SUBSCRIBE command errored (e.g. insufficient permissions) if (resp.has_error()) { std::cerr << "The receive response contains an error: " << resp.error().diagnostic << std::endl; - break; + co_return {}; } // The response must be consumed without suspending the @@ -94,42 +86,69 @@ auto receiver(std::shared_ptr conn) -> awaitable } // Publishes stdin messages to a Redis channel. -auto publisher(std::shared_ptr in, std::shared_ptr conn) - -> awaitable +// Returns when stdin reaches EOF (e.g. Ctrl+D), which then cancels the surrounding +// when_any siblings. +capy::io_task<> publisher(corosio::stream_file& in, co_connection& conn) { for (std::string msg;;) { - auto n = co_await async_read_until(*in, dynamic_buffer(msg, 1024), "\n"); + auto [ec, n] = co_await capy::read_until(in, capy::string_dynamic_buffer(&msg), "\n"); + if (ec) { + // EOF, cancellation or other read error: exit cleanly. + std::cerr << "Error reading from stdin: " << ec << ": " << ec.message() << std::endl; + co_return {}; + } + request req; req.push("PUBLISH", "channel", msg); - co_await conn->async_exec(req); + auto [pub_ec] = co_await conn.exec(req); + if (pub_ec) { + co_return {}; + } msg.erase(0, n); } } } // namespace -// Called from the main function (see main.cpp) -auto co_main(config cfg) -> awaitable +capy::task co_main(config cfg) { - auto ex = co_await asio::this_coro::executor; - auto conn = std::make_shared(ex); - auto stream = std::make_shared(ex, ::dup(STDIN_FILENO)); - - co_spawn(ex, receiver(conn), rethrow_on_error); - co_spawn(ex, publisher(stream, conn), rethrow_on_error); - conn->async_run(cfg, consign(detached, conn)); - - signal_set sig_set{ex, SIGINT, SIGTERM}; - co_await sig_set.async_wait(); - conn->cancel(); - stream->cancel(); + auto ex = co_await capy::this_coro::executor; + + // Create a connection + co_connection conn{ex}; + + // Wrap a duplicated stdin file descriptor in a corosio stream + corosio::stream_file in{ex}; + in.assign(::dup(STDIN_FILENO)); + + // Run the connection, the receiver and the publisher in parallel. + // The first task to complete (typically the publisher on EOF) cancels the others. + co_await capy::when_any(receiver(conn), publisher(in, conn), conn.run(cfg)); } -#else // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR) -auto co_main(config const&) -> awaitable +int main() { - std::cout << "Requires support for posix streams." << std::endl; - co_return; + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + exit(1); + } + exit(1); + })(co_main(config{})); + + // Executes all pending work, including the main coroutine + ctx.run(); } -#endif // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR) -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) From 1927a2dbf672a8b222ed3dd4a588c1d6f1476f6b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:20:37 +0200 Subject: [PATCH 104/115] cmake and cleanup --- example/CMakeLists.txt | 25 ++++-- example/corosio_chat_room.cpp | 154 ---------------------------------- example/corosio_protobuf.cpp | 89 -------------------- example/corosio_streams.cpp | 97 +++++++++++++++++++++ 4 files changed, 113 insertions(+), 252 deletions(-) delete mode 100644 example/corosio_chat_room.cpp delete mode 100644 example/corosio_protobuf.cpp create mode 100644 example/corosio_streams.cpp diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 4c48df93..575d4fba 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -17,20 +17,26 @@ make_testable_example(cpp17_intro boost_redis_asio) make_testable_example(cpp17_intro_sync boost_redis_asio) make_testable_example(cpp20_intro examples_main) +make_testable_example(corosio_intro boost_redis_corosio) make_testable_example(cpp20_containers examples_main) +make_testable_example(corosio_containers boost_redis_corosio) make_testable_example(cpp20_json examples_main Boost::json Boost::container_hash) +make_testable_example(corosio_json boost_redis_corosio Boost::json Boost::container_hash) make_testable_example(cpp20_unix_sockets examples_main) +make_testable_example(corosio_unix_sockets boost_redis_corosio) make_testable_example(cpp20_timeouts examples_main) +make_testable_example(corosio_timeouts boost_redis_corosio) make_testable_example(cpp20_sentinel examples_main) +make_testable_example(corosio_sentinel boost_redis_corosio) -make_example(cpp20_subscriber examples_main) -make_example(cpp20_streams examples_main) -make_example(cpp20_echo_server examples_main) -make_example(cpp20_intro_tls examples_main) - -make_testable_example(corosio_intro boost_redis_corosio) - -make_example(corosio_subscriber boost_redis_corosio) +make_example(cpp20_subscriber examples_main) +make_example(corosio_subscriber boost_redis_corosio) +make_example(cpp20_streams examples_main) +make_example(corosio_streams boost_redis_corosio) +make_example(cpp20_echo_server examples_main) +make_example(corosio_echo_server boost_redis_corosio) +make_example(cpp20_intro_tls examples_main) +make_example(corosio_intro_tls boost_redis_corosio) # We test the protobuf example only on gcc. if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") @@ -50,7 +56,8 @@ endif() # We build and test the spdlog integration example only if the library is found find_package(spdlog) if (spdlog_FOUND) - make_testable_example(cpp17_spdlog spdlog::spdlog) + make_testable_example(cpp17_spdlog spdlog::spdlog) + make_testable_example(corosio_spdlog spdlog::spdlog boost_redis_corosio) else() message(STATUS "Skipping the spdlog example because the spdlog package couldn't be found") endif() diff --git a/example/corosio_chat_room.cpp b/example/corosio_chat_room.cpp deleted file mode 100644 index 16cde625..00000000 --- a/example/corosio_chat_room.cpp +++ /dev/null @@ -1,154 +0,0 @@ -/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace capy = boost::capy; -namespace corosio = boost::corosio; -using namespace boost::redis; - -// Chat over Redis pubsub. To test, run this program from multiple -// terminals and type messages to stdin. Press Ctrl+D to exit. - -namespace { - -// Receives server pushes. -capy::io_task<> receiver(co_connection& conn) -{ - // Set the receive response, so pushes are stored in resp - generic_flat_response resp; - conn.set_receive_response(resp); - - // Subscribe to the channel 'channel'. Using request::subscribe() - // (instead of request::push()) makes the connection re-subscribe - // to 'channel' whenever it re-connects to the server. - request req; - req.subscribe({"channel"}); - auto [sub_ec] = co_await conn.exec(req); - if (sub_ec) { - std::cerr << "Error subscribing: " << sub_ec << std::endl; - co_return {}; - } - - // Loop to read Redis push messages. The loop terminates when receive() reports - // an error (e.g. cancellation when the surrounding when_any cascade tears down - // the run loop). - while (true) { - // Wait for pushes - auto [ec] = co_await conn.receive(); - - // Check for errors and cancellations - if (ec) { - std::cerr << "Error during receive: " << ec << std::endl; - co_return {}; - } - - // This can happen if a SUBSCRIBE command errored (e.g. insufficient permissions) - if (resp.has_error()) { - std::cerr << "The receive response contains an error: " << resp.error().diagnostic - << std::endl; - co_return {}; - } - - // The response must be consumed without suspending the - // coroutine i.e. without the use of async operations. - for (push_view elem : push_parser(resp.value())) { - std::cout << "Received message from channel " << elem.channel << ": " << elem.payload - << "\n"; - } - - std::cout << std::endl; - - resp.value().clear(); - } -} - -// Publishes stdin messages to a Redis channel. -// Returns when stdin reaches EOF (e.g. Ctrl+D), which then cancels the surrounding -// when_any siblings. -capy::io_task<> publisher(corosio::stream_file& in, co_connection& conn) -{ - for (std::string msg;;) { - auto [ec, n] = co_await capy::read_until(in, capy::string_dynamic_buffer(&msg), "\n"); - if (ec) { - // EOF, cancellation or other read error: exit cleanly. - std::cerr << "Error reading from stdin: " << ec << ": " << ec.message() << std::endl; - co_return {}; - } - - request req; - req.push("PUBLISH", "channel", msg); - auto [pub_ec] = co_await conn.exec(req); - if (pub_ec) { - co_return {}; - } - msg.erase(0, n); - } -} - -} // namespace - -capy::task co_main(config cfg) -{ - auto ex = co_await capy::this_coro::executor; - - // Create a connection - co_connection conn{ex}; - - // Wrap a duplicated stdin file descriptor in a corosio stream - corosio::stream_file in{ex}; - in.assign(::dup(STDIN_FILENO)); - - // Run the connection, the receiver and the publisher in parallel. - // The first task to complete (typically the publisher on EOF) cancels the others. - co_await capy::when_any(receiver(conn), publisher(in, conn), conn.run(cfg)); -} - -int main() -{ - // The I/O context, required for all I/O operations - corosio::io_context ctx; - - // Schedules the main coroutine for execution - capy::run_async( - ctx.get_executor(), - []() { - // Runs when the main coroutine finishes normally - std::cout << "Done\n"; - }, - [](std::exception_ptr exc) { - // Runs when the main coroutine finishes with an exception - try { - std::rethrow_exception(exc); - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << std::endl; - exit(1); - } - exit(1); - })(co_main(config{})); - - // Executes all pending work, including the main coroutine - ctx.run(); -} diff --git a/example/corosio_protobuf.cpp b/example/corosio_protobuf.cpp deleted file mode 100644 index e23415a0..00000000 --- a/example/corosio_protobuf.cpp +++ /dev/null @@ -1,89 +0,0 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#include -#include - -#include -#include -#include -#include - -#include - -// See the definition in person.proto. This header is automatically -// generated by CMakeLists.txt. -#include "person.pb.h" - -#if defined(BOOST_ASIO_HAS_CO_AWAIT) - -namespace asio = boost::asio; -namespace resp3 = boost::redis::resp3; -using boost::redis::request; -using boost::redis::response; -using boost::redis::operation; -using boost::redis::ignore_t; -using boost::redis::config; -using boost::redis::connection; -using boost::redis::resp3::node_view; - -// The protobuf type described in example/person.proto -using tutorial::person; - -// Boost.Redis customization points (example/protobuf.hpp) -namespace tutorial { - -// Below I am using a Boost.Redis to indicate a protobuf error, this -// is ok for an example, users however might want to define their own -// error codes. -void boost_redis_to_bulk(std::string& to, person const& u) -{ - std::string tmp; - if (!u.SerializeToString(&tmp)) - throw boost::system::system_error(boost::redis::error::invalid_data_type); - - resp3::boost_redis_to_bulk(to, tmp); -} - -void boost_redis_from_bulk(person& u, node_view const& node, boost::system::error_code& ec) -{ - std::string const tmp{node.value}; - if (!u.ParseFromString(tmp)) - ec = boost::redis::error::invalid_data_type; -} - -} // namespace tutorial - -using tutorial::boost_redis_to_bulk; -using tutorial::boost_redis_from_bulk; - -asio::awaitable co_main(config cfg) -{ - auto ex = co_await asio::this_coro::executor; - auto conn = std::make_shared(ex); - conn->async_run(cfg, asio::consign(asio::detached, conn)); - - person p; - p.set_name("Louis"); - p.set_id(3); - p.set_email("No email yet."); - - request req; - req.push("SET", "protobuf-key", p); - req.push("GET", "protobuf-key"); - - response resp; - - // Sends the request and receives the response. - co_await conn->async_exec(req, resp); - conn->cancel(); - - std::cout << "Name: " << std::get<1>(resp).value().name() << "\n" - << "Age: " << std::get<1>(resp).value().id() << "\n" - << "Email: " << std::get<1>(resp).value().email() << "\n"; -} - -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/example/corosio_streams.cpp b/example/corosio_streams.cpp new file mode 100644 index 00000000..1cef143f --- /dev/null +++ b/example/corosio_streams.cpp @@ -0,0 +1,97 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include + +#include +#include +#include +#include +#include + +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +#include +#include +#include +#include + +namespace net = boost::asio; +using boost::redis::config; +using boost::redis::generic_response; +using boost::redis::operation; +using boost::redis::request; +using boost::redis::connection; +using net::signal_set; + +auto stream_reader(std::shared_ptr conn) -> net::awaitable +{ + std::string redisStreamKey_; + request req; + generic_response resp; + + std::string stream_id{"$"}; + std::string const field = "myfield"; + + for (;;) { + req.push("XREAD", "BLOCK", "0", "STREAMS", "test-topic", stream_id); + co_await conn->async_exec(req, resp); + + //std::cout << "Response: "; + //for (auto i = 0UL; i < resp->size(); ++i) { + // std::cout << resp->at(i).value << ", "; + //} + //std::cout << std::endl; + + // The following approach was taken in order to be able to + // deal with the responses, as generated by redis in the case + // that there are multiple stream 'records' within a single + // generic_response. The nesting and number of values in + // resp.value() are different, depending on the contents + // of the stream in redis. Uncomment the above commented-out + // code for examples while running the XADD command. + + std::size_t item_index = 0; + while (item_index < std::size(resp.value())) { + auto const& val = resp.value().at(item_index).value; + + if (field.compare(val) == 0) { + // We've hit a myfield field. + // The streamId is located at item_index - 2 + // The payload is located at item_index + 1 + stream_id = resp.value().at(item_index - 2).value; + std::cout << "StreamId: " << stream_id << ", " + << "MyField: " << resp.value().at(item_index + 1).value << std::endl; + ++item_index; // We can increase so we don't read this again + } + + ++item_index; + } + + req.clear(); + resp.value().clear(); + } +} + +// Run this in another terminal: +// redis-cli -r 100000 -i 0.0001 XADD "test-topic" "*" "myfield" "myfieldvalue1" +auto co_main(config cfg) -> net::awaitable +{ + auto ex = co_await net::this_coro::executor; + auto conn = std::make_shared(ex); + net::co_spawn(ex, stream_reader(conn), net::detached); + + // Disable health checks. + cfg.health_check_interval = std::chrono::seconds::zero(); + conn->async_run(cfg, net::consign(net::detached, conn)); + + signal_set sig_set(ex, SIGINT, SIGTERM); + co_await sig_set.async_wait(); + conn->cancel(); +} +#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) From 09b1c7ccef51afbb31e7e5820f99c168664b3e88 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:20:50 +0200 Subject: [PATCH 105/115] Containers example --- example/corosio_containers.cpp | 122 +++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 37 deletions(-) diff --git a/example/corosio_containers.cpp b/example/corosio_containers.cpp index 7c648e38..175335b5 100644 --- a/example/corosio_containers.cpp +++ b/example/corosio_containers.cpp @@ -4,28 +4,30 @@ * accompanying file LICENSE.txt) */ -#include - -#include -#include -#include - +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include #include #include +#include +#include #include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) - -namespace asio = boost::asio; -using boost::redis::request; -using boost::redis::response; -using boost::redis::ignore_t; -using boost::redis::ignore; -using boost::redis::config; -using boost::redis::connection; -using boost::asio::awaitable; -using boost::asio::detached; -using boost::asio::consign; +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; template std::ostream& operator<<(std::ostream& os, std::optional const& opt) @@ -53,7 +55,7 @@ void print(std::vector const& cont) } // Stores the content of some STL containers in Redis. -auto store(std::shared_ptr conn) -> awaitable +capy::task<> store(co_connection& conn) { std::vector vec{1, 2, 3, 4, 5, 6}; @@ -68,10 +70,14 @@ auto store(std::shared_ptr conn) -> awaitable req.push_range("HSET", "hset-key", map); req.push("SET", "key", "value"); - co_await conn->async_exec(req, ignore); + auto [ec] = co_await conn.exec(req, ignore); + if (ec) { + std::cerr << "Error in store: " << ec << std::endl; + exit(1); + } } -auto hgetall(std::shared_ptr conn) -> awaitable +capy::task<> hgetall(co_connection& conn) { // A request contains multiple commands. request req; @@ -81,12 +87,16 @@ auto hgetall(std::shared_ptr conn) -> awaitable response> resp; // Executes the request and reads the response. - co_await conn->async_exec(req, resp); + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error in hgetall: " << ec << std::endl; + exit(1); + } print(std::get<0>(resp).value()); } -auto mget(std::shared_ptr conn) -> awaitable +capy::task<> mget(co_connection& conn) { // A request contains multiple commands. request req; @@ -96,13 +106,17 @@ auto mget(std::shared_ptr conn) -> awaitable response>> resp; // Executes the request and reads the response. - co_await conn->async_exec(req, resp); + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error in mget: " << ec << std::endl; + exit(1); + } print(std::get<0>(resp).value()); } // Retrieves in a transaction. -auto transaction(std::shared_ptr conn) -> awaitable +capy::task<> transaction(co_connection& conn) { request req; req.push("MULTI"); @@ -123,24 +137,58 @@ auto transaction(std::shared_ptr conn) -> awaitable > resp; - co_await conn->async_exec(req, resp); + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error in transaction: " << ec << std::endl; + exit(1); + } print(std::get<0>(std::get<4>(resp).value()).value().value()); print(std::get<1>(std::get<4>(resp).value()).value().value()); print(std::get<2>(std::get<4>(resp).value()).value().value()); } -// Called from the main function (see main.cpp) -awaitable co_main(config cfg) +capy::task co_main(config cfg) { - auto conn = std::make_shared(co_await asio::this_coro::executor); - conn->async_run(cfg, consign(detached, conn)); - - co_await store(conn); - co_await transaction(conn); - co_await hgetall(conn); - co_await mget(conn); - conn->cancel(); + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection in parallel with the work coroutine. + // when_any will cancel run() once the work completes. + co_await capy::when_any( + [&]() -> capy::io_task<> { + co_await store(conn); + co_await transaction(conn); + co_await hgetall(conn); + co_await mget(conn); + co_return {}; + }(), + conn.run(cfg)); } -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + exit(1); + } + exit(1); + })(co_main(config{})); + + // Executes all pending work, including the main coroutine + ctx.run(); +} From cc920170e24818bc4217a54ea809b99ad35baf53 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:31:11 +0200 Subject: [PATCH 106/115] echo_server --- example/corosio_echo_server.cpp | 153 +++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 42 deletions(-) diff --git a/example/corosio_echo_server.cpp b/example/corosio_echo_server.cpp index f4e27751..8617ca4d 100644 --- a/example/corosio_echo_server.cpp +++ b/example/corosio_echo_server.cpp @@ -1,42 +1,71 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) * * Distributed under the Boost Software License, Version 1.0. (See * accompanying file LICENSE.txt) */ -#include +#include +#include +#include +#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; -namespace asio = boost::asio; -using boost::asio::signal_set; -using boost::redis::request; -using boost::redis::response; -using boost::redis::config; -using boost::system::error_code; -using boost::redis::connection; -using namespace std::chrono_literals; - -auto echo_server_session(asio::ip::tcp::socket socket, std::shared_ptr conn) - -> asio::awaitable +// Echoes lines received over TCP back to the sender, going through Redis (PING). +capy::io_task<> echo_server_session(corosio::tcp_socket socket, co_connection& conn) { request req; response resp; for (std::string buffer;;) { - auto n = co_await asio::async_read_until(socket, asio::dynamic_buffer(buffer, 1024), "\n"); + auto [read_ec, n] = co_await capy::read_until( + socket, + capy::string_dynamic_buffer(&buffer), + "\n"); + if (read_ec) { + std::cerr << "Error reading from session socket: " << read_ec << std::endl; + co_return {}; + } + req.push("PING", buffer); - co_await conn->async_exec(req, resp); - co_await asio::async_write(socket, asio::buffer(std::get<0>(resp).value())); + auto [exec_ec] = co_await conn.exec(req, resp); + if (exec_ec) { + std::cerr << "Error executing PING: " << exec_ec << std::endl; + co_return {}; + } + + auto const& reply = std::get<0>(resp).value(); + auto [write_ec, written] = co_await capy::write( + socket, + capy::make_buffer(reply.data(), reply.size())); + if (write_ec) { + std::cerr << "Error writing to session socket: " << write_ec << std::endl; + co_return {}; + } + std::get<0>(resp).value().clear(); req.clear(); buffer.erase(0, n); @@ -44,29 +73,69 @@ auto echo_server_session(asio::ip::tcp::socket socket, std::shared_ptr conn) -> asio::awaitable +capy::io_task<> listener(co_connection& conn) { - try { - auto ex = co_await asio::this_coro::executor; - asio::ip::tcp::acceptor acc(ex, {asio::ip::tcp::v4(), 55555}); - for (;;) - asio::co_spawn(ex, echo_server_session(co_await acc.async_accept(), conn), asio::detached); - } catch (std::exception const& e) { - std::clog << "Listener: " << e.what() << std::endl; + auto ex = co_await capy::this_coro::executor; + corosio::tcp_acceptor acc{ex, corosio::endpoint(static_cast(55555))}; + + for (;;) { + corosio::tcp_socket peer{ex}; + auto [ec] = co_await acc.accept(peer); + if (ec) { + std::clog << "Listener: " << ec.message() << std::endl; + co_return {}; + } + + // Spawn the session as a detached task on the same executor. + // The new task runs independently and does not inherit the listener's stop token. + capy::run_async(ex)(echo_server_session(std::move(peer), conn)); } } -// Called from the main function (see main.cpp) -auto co_main(config cfg) -> asio::awaitable +// Wait for SIGINT or SIGTERM; completing this task triggers a graceful shutdown +// by cancelling the surviving siblings via the when_any cascade. +capy::io_task<> signal_handler() { - auto ex = co_await asio::this_coro::executor; - auto conn = std::make_shared(ex); - asio::co_spawn(ex, listener(conn), asio::detached); - conn->async_run(cfg, asio::consign(asio::detached, conn)); - - signal_set sig_set(ex, SIGINT, SIGTERM); - co_await sig_set.async_wait(); - conn->cancel(); + corosio::signal_set signals{(co_await capy::this_coro::executor).context(), SIGINT, SIGTERM}; + auto [ec, signum] = co_await signals.wait(); + if (!ec) + std::cout << "Received signal " << signum << ", shutting down\n"; + co_return {}; +}; + +capy::task co_main() +{ + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection, the listener and the signal waiter in parallel. + // when_any will cancel the surviving tasks once any one of them completes. + co_await capy::when_any(listener(conn), conn.run(config{}), signal_handler()); } -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + exit(1); + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} From 73335d1512b800992b93a8b9a4969f3394eb1490 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:36:23 +0200 Subject: [PATCH 107/115] TLS intro --- example/corosio_intro_tls.cpp | 100 +++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/example/corosio_intro_tls.cpp b/example/corosio_intro_tls.cpp index 6321c9e8..5517db71 100644 --- a/example/corosio_intro_tls.cpp +++ b/example/corosio_intro_tls.cpp @@ -1,57 +1,95 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) * * Distributed under the Boost Software License, Version 1.0. (See * accompanying file LICENSE.txt) */ -#include +#include +#include +#include +#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; -namespace asio = boost::asio; -using boost::redis::request; -using boost::redis::response; -using boost::redis::config; -using boost::redis::logger; -using boost::redis::connection; - -auto verify_certificate(bool, asio::ssl::verify_context&) -> bool +capy::io_task<> run_request(co_connection& conn) { - std::cout << "set_verify_callback" << std::endl; - return true; + request req; + req.push("PING"); + + response resp; + + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cout << "Error executing PING: " << ec << std::endl; + } else { + std::cout << "Response: " << std::get<0>(resp).value() << std::endl; + } + + co_return {}; } -auto co_main(config cfg) -> asio::awaitable +capy::task co_main() { + // Configure a TLS connection to the public test server + config cfg; cfg.use_ssl = true; cfg.username = "aedis"; cfg.password = "aedis"; cfg.addr.host = "db.occase.de"; cfg.addr.port = "6380"; - asio::ssl::context ctx{asio::ssl::context::tlsv12_client}; - ctx.set_verify_mode(asio::ssl::verify_peer); - ctx.set_verify_callback(verify_certificate); + // Configure the TLS context + corosio::tls_context tls_ctx; + if (auto ec = tls_ctx.set_verify_mode(corosio::tls_verify_mode::require_peer)) { + std::cerr << "Error in set_verify_mode: " << ec << std::endl; + exit(1); + } + tls_ctx.set_hostname("db.occase.de"); - auto conn = std::make_shared(co_await asio::this_coro::executor, std::move(ctx)); - conn->async_run(cfg, asio::consign(asio::detached, conn)); + // Create a connection using the configured TLS context + co_connection conn{co_await capy::this_coro::executor, std::move(tls_ctx)}; - request req; - req.push("PING"); + // Run the connection and the PING request, in parallel + co_await capy::when_any(run_request(conn), conn.run(cfg)); +} - response resp; +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; - co_await conn->async_exec(req, resp); - conn->cancel(); + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + exit(1); + } + exit(1); + })(co_main()); - std::cout << "Response: " << std::get<0>(resp).value() << std::endl; + // Executes all pending work, including the main coroutine + ctx.run(); } - -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) From 352e6213e25579846a8a0a0acaefd80701f8738f Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:38:30 +0200 Subject: [PATCH 108/115] JSON example --- example/corosio_json.cpp | 93 +++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/example/corosio_json.cpp b/example/corosio_json.cpp index 756d5275..33c2f238 100644 --- a/example/corosio_json.cpp +++ b/example/corosio_json.cpp @@ -1,36 +1,36 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) * * Distributed under the Boost Software License, Version 1.0. (See * accompanying file LICENSE.txt) */ -#include - -#include -#include -#include -#include - -#include -#include - -#if defined(BOOST_ASIO_HAS_CO_AWAIT) - +#include +#include +#include #include - +#include + +#include +#include +#include +#include +#include +#include +#include #include #include #include #include -namespace asio = boost::asio; +#include +#include +#include + +namespace capy = boost::capy; +namespace corosio = boost::corosio; namespace resp3 = boost::redis::resp3; using namespace boost::describe; -using boost::redis::request; -using boost::redis::response; -using boost::redis::ignore_t; -using boost::redis::config; -using boost::redis::connection; +using namespace boost::redis; using boost::redis::resp3::node_view; // Struct that will be stored in Redis using json serialization. @@ -54,12 +54,8 @@ void boost_redis_from_bulk(user& u, node_view const& node, boost::system::error_ u = boost::json::value_to(boost::json::parse(node.value)); } -auto co_main(config cfg) -> asio::awaitable +capy::io_task<> run_request(co_connection& conn) { - auto ex = co_await asio::this_coro::executor; - auto conn = std::make_shared(ex); - conn->async_run(cfg, asio::consign(asio::detached, conn)); - // user object that will be stored in Redis in json format. user const u{"Joao", "58", "Brazil"}; @@ -70,13 +66,52 @@ auto co_main(config cfg) -> asio::awaitable response resp; - co_await conn->async_exec(req, resp); - conn->cancel(); + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error executing request: " << ec << std::endl; + exit(1); + } - // Prints the first ping std::cout << "Name: " << std::get<1>(resp).value().name << "\n" << "Age: " << std::get<1>(resp).value().age << "\n" << "Country: " << std::get<1>(resp).value().country << "\n"; + + co_return {}; +} + +capy::task co_main() +{ + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection and the JSON request in parallel. + // when_any will cancel run() once the request completes. + co_await capy::when_any(run_request(conn), conn.run(config{})); } -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + exit(1); + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} From 0e2b8aad29e2602effa261252c32af99089df417 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:39:35 +0200 Subject: [PATCH 109/115] sentinel example --- example/corosio_sentinel.cpp | 89 ++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/example/corosio_sentinel.cpp b/example/corosio_sentinel.cpp index 5e51413b..bdffc8b5 100644 --- a/example/corosio_sentinel.cpp +++ b/example/corosio_sentinel.cpp @@ -6,29 +6,51 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include +#include +#include +#include +#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include +#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; -namespace asio = boost::asio; -using boost::redis::request; -using boost::redis::response; -using boost::redis::config; -using boost::redis::connection; +capy::io_task<> run_request(co_connection& conn) +{ + // You can use the connection normally, as you would use a connection to a single master. + request req; + req.push("PING", "Hello world"); + response resp; + + // Execute the request. + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error executing PING: " << ec << std::endl; + exit(1); + } + + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; + co_return {}; +} -// Called from the main function (see main.cpp) -auto co_main(config cfg) -> asio::awaitable +capy::task co_main() { // Boost.Redis has built-in support for Sentinel deployments. // To enable it, set the fields in config shown here. // sentinel.addresses should contain a list of (hostname, port) pairs // where Sentinels are listening. IPs can also be used. + config cfg; cfg.sentinel.addresses = { {"localhost", "26379"}, {"localhost", "26380"}, @@ -39,22 +61,39 @@ auto co_main(config cfg) -> asio::awaitable // in the "sentinel monitor" statement of your sentinel.conf file cfg.sentinel.master_name = "mymaster"; - // async_run will contact the Sentinels, obtain the master address, + // run() will contact the Sentinels, obtain the master address, // connect to it and keep the connection healthy. If a failover happens, // the address will be resolved again and the new elected master will be contacted. - auto conn = std::make_shared(co_await asio::this_coro::executor); - conn->async_run(cfg, asio::consign(asio::detached, conn)); + co_connection conn{co_await capy::this_coro::executor}; - // You can now use the connection normally, as you would use a connection to a single master. - request req; - req.push("PING", "Hello world"); - response resp; + // Run the connection and the PING request in parallel. + // when_any will cancel run() once the request completes. + co_await capy::when_any(run_request(conn), conn.run(cfg)); +} - // Execute the request. - co_await conn->async_exec(req, resp); - conn->cancel(); +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; - std::cout << "PING: " << std::get<0>(resp).value() << std::endl; -} + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + exit(1); + } + exit(1); + })(co_main()); -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) + // Executes all pending work, including the main coroutine + ctx.run(); +} From 85c467b8ab4d40235b12979a0c8c996563e12459 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:41:08 +0200 Subject: [PATCH 110/115] spdlog example --- example/corosio_spdlog.cpp | 101 ++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 36 deletions(-) diff --git a/example/corosio_spdlog.cpp b/example/corosio_spdlog.cpp index d5be9120..0c32d7a8 100644 --- a/example/corosio_spdlog.cpp +++ b/example/corosio_spdlog.cpp @@ -6,18 +6,26 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include +#include +#include #include +#include +#include -#include -#include +#include +#include +#include +#include +#include +#include -#include -#include +#include #include +#include #include -namespace asio = boost::asio; +namespace capy = boost::capy; +namespace corosio = boost::corosio; namespace redis = boost::redis; // Maps a Boost.Redis log level to a spdlog log level @@ -47,21 +55,42 @@ static void do_log(redis::logger::level level, std::string_view msg) spdlog::log(to_spdlog_level(level), "(Boost.Redis) {}", msg); } -auto main(int argc, char** argv) -> int +capy::io_task<> run_request(redis::co_connection& conn) { - try { - // Create an execution context, required to create any I/O objects - asio::io_context ioc; + // Execute a request + redis::request req; + req.push("PING", "Hello world"); + redis::response resp; + + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + spdlog::error("Request failed: {}", ec.message()); + exit(1); + } + + spdlog::info("PING: {}", std::get<0>(resp).value()); + co_return {}; +} + +capy::task co_main(redis::config cfg) +{ + // Create a connection to connect to Redis, and pass it a custom logger. + // Boost.Redis will call do_log whenever it needs to log a message. + // Note that the function will only be called for messages with level >= info + // (i.e. filtering is done by Boost.Redis). + redis::co_connection conn{ + co_await capy::this_coro::executor, + redis::logger{redis::logger::level::info, do_log} + }; - // Create a connection to connect to Redis, and pass it a custom logger. - // Boost.Redis will call do_log whenever it needs to log a message. - // Note that the function will only be called for messages with level >= info - // (i.e. filtering is done by Boost.Redis). - redis::connection conn{ - ioc, - redis::logger{redis::logger::level::info, do_log} - }; + // Run the connection and the PING request in parallel. + // when_any will cancel run() once the request completes. + co_await capy::when_any(run_request(conn), conn.run(cfg)); +} +int main(int argc, char** argv) +{ + try { // Configuration to connect to the server. Adjust as required redis::config cfg; if (argc == 3) { @@ -69,28 +98,28 @@ auto main(int argc, char** argv) -> int cfg.addr.port = argv[2]; } - // Run the connection with the specified configuration. - // This will establish the connection and keep it healthy - conn.async_run(cfg, asio::detached); - - // Execute a request - redis::request req; - req.push("PING", "Hello world"); - - redis::response resp; + // Create an execution context, required to create any I/O objects + corosio::io_context ctx; - conn.async_exec(req, resp, [&](boost::system::error_code ec, std::size_t /* bytes_read*/) { - if (ec) { - spdlog::error("Request failed: {}", ec.what()); + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + spdlog::error("Error: {}", e.what()); + exit(1); + } exit(1); - } else { - spdlog::info("PING: {}", std::get<0>(resp).value()); - } - conn.cancel(); - }); + })(co_main(cfg)); // Actually run our example. Nothing will happen until we call run() - ioc.run(); + ctx.run(); } catch (std::exception const& e) { spdlog::error("Error: {}", e.what()); From 15b4712b62eec79a227b7a73ab4f8e1e5e3af37f Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:42:58 +0200 Subject: [PATCH 111/115] exit 1 cleanup --- example/corosio_containers.cpp | 1 - example/corosio_echo_server.cpp | 1 - example/corosio_intro.cpp | 1 - example/corosio_intro_tls.cpp | 1 - example/corosio_json.cpp | 1 - example/corosio_sentinel.cpp | 1 - example/corosio_spdlog.cpp | 1 - example/corosio_subscriber.cpp | 1 - 8 files changed, 8 deletions(-) diff --git a/example/corosio_containers.cpp b/example/corosio_containers.cpp index 175335b5..ba33a428 100644 --- a/example/corosio_containers.cpp +++ b/example/corosio_containers.cpp @@ -184,7 +184,6 @@ int main() std::rethrow_exception(exc); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; - exit(1); } exit(1); })(co_main(config{})); diff --git a/example/corosio_echo_server.cpp b/example/corosio_echo_server.cpp index 8617ca4d..66c13622 100644 --- a/example/corosio_echo_server.cpp +++ b/example/corosio_echo_server.cpp @@ -131,7 +131,6 @@ int main() std::rethrow_exception(exc); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; - exit(1); } exit(1); })(co_main()); diff --git a/example/corosio_intro.cpp b/example/corosio_intro.cpp index 33c3b39b..05488356 100644 --- a/example/corosio_intro.cpp +++ b/example/corosio_intro.cpp @@ -67,7 +67,6 @@ int main() std::rethrow_exception(exc); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; - exit(1); } exit(1); })(co_main()); diff --git a/example/corosio_intro_tls.cpp b/example/corosio_intro_tls.cpp index 5517db71..c563a3fd 100644 --- a/example/corosio_intro_tls.cpp +++ b/example/corosio_intro_tls.cpp @@ -85,7 +85,6 @@ int main() std::rethrow_exception(exc); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; - exit(1); } exit(1); })(co_main()); diff --git a/example/corosio_json.cpp b/example/corosio_json.cpp index 33c2f238..4b9c7322 100644 --- a/example/corosio_json.cpp +++ b/example/corosio_json.cpp @@ -107,7 +107,6 @@ int main() std::rethrow_exception(exc); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; - exit(1); } exit(1); })(co_main()); diff --git a/example/corosio_sentinel.cpp b/example/corosio_sentinel.cpp index bdffc8b5..80218b22 100644 --- a/example/corosio_sentinel.cpp +++ b/example/corosio_sentinel.cpp @@ -89,7 +89,6 @@ int main() std::rethrow_exception(exc); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; - exit(1); } exit(1); })(co_main()); diff --git a/example/corosio_spdlog.cpp b/example/corosio_spdlog.cpp index 0c32d7a8..080ced3f 100644 --- a/example/corosio_spdlog.cpp +++ b/example/corosio_spdlog.cpp @@ -113,7 +113,6 @@ int main(int argc, char** argv) std::rethrow_exception(exc); } catch (const std::exception& e) { spdlog::error("Error: {}", e.what()); - exit(1); } exit(1); })(co_main(cfg)); diff --git a/example/corosio_subscriber.cpp b/example/corosio_subscriber.cpp index 489e0a68..c0f96f9c 100644 --- a/example/corosio_subscriber.cpp +++ b/example/corosio_subscriber.cpp @@ -119,7 +119,6 @@ int main() std::rethrow_exception(exc); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; - exit(1); } exit(1); })(co_main()); From 9b184334fc94cc4404c575ebcf5463b99fc92ba8 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:43:47 +0200 Subject: [PATCH 112/115] UNIX sockets example --- example/corosio_unix_sockets.cpp | 94 ++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/example/corosio_unix_sockets.cpp b/example/corosio_unix_sockets.cpp index 3c69ea12..958cdc79 100644 --- a/example/corosio_unix_sockets.cpp +++ b/example/corosio_unix_sockets.cpp @@ -5,56 +5,82 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) +#include +#include +#include +#include -#include - -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include +#include -namespace asio = boost::asio; -using boost::redis::request; -using boost::redis::response; -using boost::redis::config; -using boost::redis::logger; -using boost::redis::connection; - -#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; -auto co_main(config cfg) -> asio::awaitable +capy::io_task<> run_request(co_connection& conn) { - // If unix_socket is set to a non-empty string, UNIX domain sockets will be used - // instead of TCP. Set this value to the path where your server is listening. - // UNIX domain socket connections work in the same way as TCP connections. - cfg.unix_socket = "/tmp/redis-socks/redis.sock"; - - auto conn = std::make_shared(co_await asio::this_coro::executor); - conn->async_run(cfg, asio::consign(asio::detached, conn)); - request req; req.push("PING"); response resp; - co_await conn->async_exec(req, resp); - conn->cancel(); + auto [ec] = co_await conn.exec(req, resp); + if (ec) { + std::cerr << "Error executing PING: " << ec << std::endl; + exit(1); + } std::cout << "Response: " << std::get<0>(resp).value() << std::endl; + co_return {}; } -#else - -auto co_main(config) -> asio::awaitable +capy::task co_main() { - std::cout << "Sorry, your system does not support UNIX domain sockets\n"; - co_return; -} + // If unix_socket is set to a non-empty string, UNIX domain sockets will be used + // instead of TCP. Set this value to the path where your server is listening. + // UNIX domain socket connections work in the same way as TCP connections. + config cfg; + cfg.unix_socket = "/tmp/redis-socks/redis.sock"; -#endif + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; -#endif + // Run the connection and the PING request in parallel. + // when_any will cancel run() once the request completes. + co_await capy::when_any(run_request(conn), conn.run(cfg)); +} + +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} From 55b487166586fe24b008ececf330730212e4d041 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 18:45:55 +0200 Subject: [PATCH 113/115] timeout example --- example/corosio_timeouts.cpp | 90 ++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/example/corosio_timeouts.cpp b/example/corosio_timeouts.cpp index 98168339..a44d6211 100644 --- a/example/corosio_timeouts.cpp +++ b/example/corosio_timeouts.cpp @@ -1,33 +1,35 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) * * Distributed under the Boost Software License, Version 1.0. (See * accompanying file LICENSE.txt) */ -#include +#include +#include +#include +#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include -#if defined(BOOST_ASIO_HAS_CO_AWAIT) - -namespace asio = boost::asio; -using boost::redis::request; -using boost::redis::response; -using boost::redis::config; -using boost::redis::connection; +namespace capy = boost::capy; +namespace corosio = boost::corosio; +using namespace boost::redis; using namespace std::chrono_literals; -// Called from the main function (see main.cpp) -auto co_main(config cfg) -> asio::awaitable +capy::io_task<> run_request(co_connection& conn) { - auto conn = std::make_shared(co_await asio::this_coro::executor); - conn->async_run(cfg, asio::consign(asio::detached, conn)); - // A request containing only a ping command. request req; req.push("PING", "Hello world"); @@ -36,14 +38,52 @@ auto co_main(config cfg) -> asio::awaitable response resp; // Executes the request with a timeout. If the server is down, - // async_exec will wait until it's back again, so it, - // may suspend for a long time. - // For this reason, it's good practice to set a timeout to requests with cancel_after. - // If the request hasn't completed after 10 seconds, an exception will be thrown. - co_await conn->async_exec(req, resp, asio::cancel_after(10s)); - conn->cancel(); + // exec will wait until it's back again, so it may suspend for a long time. + // For this reason, it's good practice to set a timeout to requests with capy::timeout. + // If the request hasn't completed after 10 seconds, exec is cancelled + // and the awaitable returns an error. + auto [ec] = co_await capy::timeout(conn.exec(req, resp), 10s); + if (ec) { + std::cerr << "Error executing PING: " << ec << std::endl; + exit(1); + } std::cout << "PING: " << std::get<0>(resp).value() << std::endl; + co_return {}; +} + +capy::task co_main() +{ + // Create a connection + co_connection conn{co_await capy::this_coro::executor}; + + // Run the connection and the PING request in parallel. + // when_any will cancel run() once the request completes. + co_await capy::when_any(run_request(conn), conn.run(config{})); } -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) +int main() +{ + // The I/O context, required for all I/O operations + corosio::io_context ctx; + + // Schedules the main coroutine for execution + capy::run_async( + ctx.get_executor(), + []() { + // Runs when the main coroutine finishes normally + std::cout << "Done\n"; + }, + [](std::exception_ptr exc) { + // Runs when the main coroutine finishes with an exception + try { + std::rethrow_exception(exc); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + exit(1); + })(co_main()); + + // Executes all pending work, including the main coroutine + ctx.run(); +} From 7b9e7a2a154b427dd6ea238a16642b06c93c70f9 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 20:51:04 +0200 Subject: [PATCH 114/115] Reference docs --- include/boost/redis/co_connection.hpp | 382 ++++++++++++++++---------- 1 file changed, 232 insertions(+), 150 deletions(-) diff --git a/include/boost/redis/co_connection.hpp b/include/boost/redis/co_connection.hpp index 63f90136..def8bd83 100644 --- a/include/boost/redis/co_connection.hpp +++ b/include/boost/redis/co_connection.hpp @@ -31,54 +31,125 @@ struct co_connection_impl; } // namespace detail -/** @brief A SSL connection to the Redis server. +/** @brief A connection to a Redis server, designed for capy coroutines. * * This class keeps a healthy connection to the Redis instance where * commands can be sent at any time. For more details, please see the * documentation of each individual function. * - * @tparam Executor The executor type used to create any required I/O objects. + * Each I/O member function returns a `capy::io_task` that should + * be `co_await`ed from a coroutine running on a capy executor. + * This class uses corosio for sockets, TLS and timers, and does not depend on Boost.Asio. + * Cancellation follows the usual capy patterns, and is driven by the parent task's + * `std::stop_token`. When stop is requested, operations complete with + * an error matching `capy::cond::canceled`. + * + * @par Thread safety + * Distinct objects: safe. + * Shared objects: unsafe. + * This class is not thread-safe: for a single object, if you + * call its member functions concurrently from separate threads, you will get a race condition. + * Use a strand if if you require thread safety. */ class co_connection { public: - /** @brief Constructor from an executor. - * - * @param ex Executor used to create all internal I/O objects. - * @param ctx SSL context. - * @param lgr Logger configuration. It can be used to filter messages by level - * and customize logging. By default, `logger::level::info` messages - * and higher are logged to `stderr`. - */ + /** @brief Constructor from an execution context. + * + * @param ctx Execution context used to create all internal I/O objects. + * @param tls_ctx TLS context, used to create any required TLS streams. + * Used only when @ref config::use_ssl is `true`. + * The connection is usable with TLS even if not specified - + * a default-constructed TLS context is used in this case. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ explicit co_connection( capy::execution_context& ctx, - corosio::tls_context ssl_ctx = {}, + corosio::tls_context tls_ctx = {}, logger lgr = {}); + /** @brief Constructor from a capy executor. + * + * Equivalent to constructing from `ex.context()`. + * + * @param ex Executor whose context will own this connection. + * @param tls_ctx TLS context, used to create any required TLS streams. + * Used only when @ref config::use_ssl is `true`. + * The connection is usable with TLS even if not specified - + * a default-constructed TLS context is used in this case. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ template - co_connection(const Ex& ex, corosio::tls_context ssl_ctx = {}, logger lgr = {}) - : co_connection(ex.context(), std::move(ssl_ctx), std::move(lgr)) + co_connection(const Ex& ex, corosio::tls_context tls_ctx = {}, logger lgr = {}) + : co_connection(ex.context(), std::move(tls_ctx), std::move(lgr)) { } - /** @brief Constructor from an executor and a logger. + /** @brief Constructor from an execution context and a logger. + * + * A TLS context with default settings will be created. * - * @param ex Executor used to create all internal I/O objects. + * @param ctx Execution context used to create all internal I/O objects. * @param lgr Logger configuration. It can be used to filter messages by level * and customize logging. By default, `logger::level::info` messages * and higher are logged to `stderr`. - * - * An SSL context with default settings will be created. */ co_connection(capy::execution_context& ctx, logger lgr); + /** @brief Constructor from a capy executor and a logger. + * + * Equivalent to constructing from `ex.context()`. + * A TLS context with default settings will be created. + * + * @param ex Executor whose context will own this connection. + * @param lgr Logger configuration. It can be used to filter messages by level + * and customize logging. By default, `logger::level::info` messages + * and higher are logged to `stderr`. + */ template co_connection(const Ex& ex, logger lgr) : co_connection(ex.context(), corosio::tls_context{}, std::move(lgr)) { } + /** @brief Move constructor. + * + * Transfers ownership of the connection's internal state from `other` to + * the new object. Any operations that were in flight on `other` continue to make + * progress on the same internal state, now owned by `*this`. + * + * After the move, `other` is in a valid but unspecified state, + * and can only be assigned to or destroyed. + * + * @par Exception safety + * No-throw guarantee. + */ co_connection(co_connection&&) noexcept; + + /** @brief Move assignment operator. + * + * Releases the resources currently owned by `*this` (if any) and + * transfers ownership of `other`'s internal state to `*this`. + * + * Any operations that were in flight on `other` continue to make + * progress on the same internal state, now owned by `*this`. + * If `*this` has any operation in flight, the behavior is undefined. + * + * After the move, `other` is in a valid but unspecified state, + * and can only be assigned to or destroyed. + * + * @par Exception safety + * No-throw guarantee. + * + * @return Reference to `*this`. + */ co_connection& operator=(co_connection&&) noexcept; + co_connection(const co_connection&) = delete; co_connection& operator=(const co_connection&) = delete; + + /// Destructor. ~co_connection(); /** @brief Starts the underlying connection operations. @@ -86,14 +157,14 @@ class co_connection { * This function establishes a connection to the Redis server and keeps * it healthy by performing the following operations: * - * @li For Sentinel deployments (`config::sentinel::addresses` is not empty), + * @li For Sentinel deployments (@ref config::sentinel::addresses is not empty), * contacts Sentinels to obtain the address of the configured master. * @li For TCP connections, resolves the server hostname passed in - * @ref boost::redis::config::addr. + * @ref config::addr. * @li Establishes a physical connection to the server. For TCP connections, * connects to one of the endpoints obtained during name resolution. - * For UNIX domain socket connections, it connects to @ref boost::redis::config::unix_sockets. - * @li If @ref boost::redis::config::use_ssl is `true`, performs the TLS handshake. + * For UNIX domain socket connections, it connects to @ref config::unix_socket. + * @li If @ref config::use_ssl is `true`, performs the TLS handshake. * @li Executes the setup request, as defined by the passed @ref config object. * By default, this is a `HELLO` command, but it can contain any other arbitrary * commands. See the @ref config::setup docs for more info. @@ -101,163 +172,146 @@ class co_connection { * at intervals specified by * @ref config::health_check_interval when the connection is idle. * See the documentation of @ref config::health_check_interval for more info. - * @li Starts read and write operations. Requests issued using @ref async_exec - * before `async_run` is called will be written to the server immediately. + * @li Starts read and write operations. Requests submitted via @ref exec + * before this task is ready to send data will be queued and written to + * the server as soon as the connection is up. * - * When a connection is lost for any reason, a new one is - * established automatically. To disable reconnection - * set @ref boost::redis::config::reconnect_wait_interval to zero. + * When a connection is lost for any reason, a new one is + * established automatically. To disable reconnection + * set @ref config::reconnect_wait_interval to zero. * - * The completion token must have the following signature + * Awaiting the returned task yields a `capy::io_result` containing + * a single `std::error_code`: * - * @code - * void f(system::error_code); - * @endcode + * @code + * auto [ec] = co_await conn.run(cfg); + * @endcode * - * @par Per-operation cancellation - * This operation supports the following cancellation types: + * If the passed configuration contains a critical error + * (e.g. TLS is enabled together with UNIX sockets), + * the operation completes immediately with a non-empty error code. * - * @li `asio::cancellation_type_t::terminal`. - * @li `asio::cancellation_type_t::partial`. + * If reconnection is diabled, the operation completes + * once an event that would otherwise trigger a reconnection is encountered. + * An informative error code is returned. * - * In both cases, cancellation is equivalent to calling @ref basic_connection::cancel - * passing @ref operation::run as argument. + * If reconnection is enabled, the operation only completes when cancelled. * - * After the operation completes, the token's associated cancellation slot - * may still have a cancellation handler associated to this connection. - * You should make sure to not invoke it after the connection has been destroyed. - * This is consistent with what other Asio I/O objects do. + * @par Cancellation + * The usual capy cancellation semantics apply. The operation can be used + * in constructs like `capy::timeout` and `capy::with_any`. * - * For example on how to call this function refer to - * cpp20_intro.cpp or any other example. + * For an example on how to call this function refer to + * corosio_intro.cpp or any other corosio example. * * @param cfg Configuration parameters. - * @param token Completion token. + * + * @return A task that yields a `capy::io_result` holding the + * operation's `std::error_code` on completion. */ capy::io_task<> run(config const& cfg); - /** @brief Wait for server pushes asynchronously. + /** @brief Wait for server pushes. * * This function suspends until at least one server push is received by the - * connection. On completion an unspecified number of pushes will - * have been added to the response object set with @ref - * set_receive_response. Use the functions in the response object - * to know how many messages they were received and consume them. - * - * To prevent receiving an unbound number of pushes the connection - * blocks further read operations on the socket when 256 pushes - * accumulate internally (we don't make any commitment to this - * exact number). When that happens any `async_exec`s and - * health-checks won't make any progress and the connection may - * eventually timeout. To avoid this, apps that expect server pushes - * should call this function continuously in a loop. - * - * This function should be used instead of the deprecated @ref async_receive. - * It differs from `async_receive` in the following: - * - * @li `async_receive` is designed to consume a single push message at a time. - * This can be inefficient when receiving lots of server pushes. - * `async_receive2` is batch-oriented. All pushes that are available - * when `async_receive2` is called will be marked as consumed. - * @li `async_receive` is cancelled when a reconnection happens (e.g. because - * of a network error). This enabled the user to re-establish subscriptions - * using @ref async_exec before waiting for pushes again. With the introduction of - * functions like @ref request::subscribe, subscriptions are automatically - * re-established on reconnection. Thus, `async_receive2` is not cancelled - * on reconnection. - * @li `async_receive` passes the number of bytes that each received - * push message contains. This information is unreliable and not very useful. - * Equivalent information is available using functions in the response object. - * @li `async_receive` might get cancelled if `async_run` is cancelled. - * This doesn't happen with `async_receive2`. + * connection. On completion, an unspecified number of pushes will have been + * added to the response object set with @ref set_receive_response. Use the + * functions in the response object to know how many messages were received + * and consume them. + * + * To prevent receiving an unbounded number of pushes, the connection blocks + * further read operations on the socket when its internal receive buffer + * fills up. When that happens, in-flight @ref exec calls and health checks + * won't make any progress and the connection may eventually time out. To + * avoid this, applications that expect server pushes should call this + * function continuously in a loop. * * This function does *not* remove messages from the response object * passed to @ref set_receive_response - use the functions in the response * object to achieve this. * - * Only a single instance of `async_receive2` may be outstanding - * for a given connection at any time. Trying to start a second one - * will fail with @ref error::already_running. + * Only a single instance of `receive` may be outstanding for a given + * connection at any time. Launching a second `receive` fails + * with @ref error::already_running. * - * @note To avoid deadlocks the task (e.g. coroutine) calling - * `async_receive2` should not call `async_exec` in a way where - * they could block each other. This is, avoid the following pattern: + * `receive` does not complete when reconnection happens or + * when @ref run completes. + * + * @note To avoid deadlocks, the task calling `receive` should not also call + * `exec` in a way where they can block each other. That is, avoid the + * following pattern: * * @code - * asio::awaitable receiver() + * capy::task receiver(co_connection& conn) * { * // Do NOT do this!!! The receive buffer might get full while - * // async_exec runs, which will block all read operations until async_receive2 - * // is called. The two operations end up waiting each other, making the connection unresponsive. - * // If you need to do this, use two connections, instead. - * co_await conn.async_receive2(); - * co_await conn.async_exec(req, resp); + * // exec runs, which will block all read operations until receive + * // is called. The two operations end up waiting for each other, + * // making the connection unresponsive. If you need this pattern, + * // use two connections instead. + * co_await conn.receive(); + * co_await conn.exec(req, resp); * } * @endcode * - * For an example see cpp20_subscriber.cpp. + * For an example see corosio_subscriber.cpp. * - * The completion token must have the following signature: + * Awaiting the returned task yields a `capy::io_result` containing + * a single `std::error_code`: * * @code - * void f(system::error_code); + * auto [ec] = co_await conn.receive(); * @endcode * - * @par Per-operation cancellation - * This operation supports the following cancellation types: - * - * @li `asio::cancellation_type_t::terminal`. - * @li `asio::cancellation_type_t::partial`. - * @li `asio::cancellation_type_t::total`. + * @par Cancellation + * The usual capy cancellation semantics apply. The operation can be used + * in constructs like `capy::timeout` and `capy::with_any`. * - * @param token Completion token. + * @return A task that yields a `capy::io_result` holding the + * operation's `std::error_code` on completion. */ capy::io_task<> receive(); - /** @brief Executes commands on the Redis server asynchronously. + /** @brief Executes commands on the Redis server. * * This function sends a request to the Redis server and waits for * the responses to each individual command in the request. If the * request contains only commands that don't expect a response, - * the completion occurs after it has been written to the - * underlying stream. Multiple concurrent calls to this function - * will be automatically queued by the implementation. + * the completion occurs after it has been written to the underlying + * stream. Multiple concurrent calls to this function will be + * automatically queued by the implementation. * - * For an example see cpp20_echo_server.cpp. + * For an example see corosio_intro.cpp. * - * The completion token must have the following signature: + * Awaiting the returned task yields a `capy::io_result` containing + * a single `std::error_code`: * * @code - * void f(system::error_code, std::size_t); + * auto [ec] = co_await conn.exec(req, resp); * @endcode * - * Where the second parameter is the size of the response received - * in bytes. - * - * @par Per-operation cancellation - * This operation supports per-operation cancellation. Depending on the state of the request - * when cancellation is requested, we can encounter two scenarios: + * @par Cancellation + * The usual capy cancellation semantics apply. The operation can be used + * in constructs like `capy::timeout` and `capy::with_any`. * - * @li If the request hasn't been sent to the server yet, cancellation will prevent it - * from being sent to the server. In this situation, all cancellation types are supported - * (`asio::cancellation_type_t::terminal`, `asio::cancellation_type_t::partial` and - * `asio::cancellation_type_t::total`). - * @li If the request has been sent to the server but the response hasn't arrived yet, - * cancellation will cause `async_exec` to complete immediately. When the response - * arrives from the server, it will be ignored. In this situation, only - * `asio::cancellation_type_t::terminal` and `asio::cancellation_type_t::partial` - * are supported. Cancellation requests specifying `asio::cancellation_type_t::total` - * only will be ignored. + * What happens to the request depends on its state when cancellation is requested: * - * In any case, connections can be safely used after cancelling `async_exec` operations. + * @li If the request hasn't been sent to the server yet, cancellation will + * prevent it from being sent. + * @li If the request has been sent but the response hasn't arrived yet, + * cancellation causes `exec` to complete immediately. When the response + * eventually arrives from the server, it will be ignored. * * @par Object lifetimes - * Both `req` and `res` should be kept alive until the operation completes. - * No copies of the request object are made. + * Both `req` and `resp` should be kept alive until the operation completes. + * No copies of the request object are made. After `exec` completes, the objects + * can be safely destroyed, even if `exec` was cancelled. * * @param req The request to be executed. * @param resp The response object to parse data into. - * @param token Completion token. + * + * @return A task that yields a `capy::io_result` holding the + * operation's `std::error_code` on completion. */ template capy::io_task<> exec(request const& req, Response& resp = ignore) @@ -265,42 +319,40 @@ class co_connection { return exec(req, any_adapter{resp}); } - /** @brief Executes commands on the Redis server asynchronously. + /** @brief Executes commands on the Redis server. + * + * This is the type-erased version of `exec`. + * It has the same semantics as the typed `exec` overload + * Same as the other @ref exec overload, but takes a type-erased + * @ref any_adapter instead of a typed response. * * This function sends a request to the Redis server and waits for * the responses to each individual command in the request. If the * request contains only commands that don't expect a response, - * the completion occurs after it has been written to the - * underlying stream. Multiple concurrent calls to this function - * will be automatically queued by the implementation. + * the completion occurs after it has been written to the underlying + * stream. Multiple concurrent calls to this function will be + * automatically queued by the implementation. * - * For an example see cpp20_echo_server.cpp. + * For an example see corosio_intro.cpp. * - * The completion token must have the following signature: + * Awaiting the returned task yields a `capy::io_result` containing + * a single `std::error_code`: * * @code - * void f(system::error_code, std::size_t); + * auto [ec] = co_await conn.exec(req, resp); * @endcode * - * Where the second parameter is the size of the response received - * in bytes. - * - * @par Per-operation cancellation - * This operation supports per-operation cancellation. Depending on the state of the request - * when cancellation is requested, we can encounter two scenarios: + * @par Cancellation + * The usual capy cancellation semantics apply. The operation can be used + * in constructs like `capy::timeout` and `capy::with_any`. * - * @li If the request hasn't been sent to the server yet, cancellation will prevent it - * from being sent to the server. In this situation, all cancellation types are supported - * (`asio::cancellation_type_t::terminal`, `asio::cancellation_type_t::partial` and - * `asio::cancellation_type_t::total`). - * @li If the request has been sent to the server but the response hasn't arrived yet, - * cancellation will cause `async_exec` to complete immediately. When the response - * arrives from the server, it will be ignored. In this situation, only - * `asio::cancellation_type_t::terminal` and `asio::cancellation_type_t::partial` - * are supported. Cancellation requests specifying `asio::cancellation_type_t::total` - * only will be ignored. + * What happens to the request depends on its state when cancellation is requested: * - * In any case, connections can be safely used after cancelling `async_exec` operations. + * @li If the request hasn't been sent to the server yet, cancellation will + * prevent it from being sent. + * @li If the request has been sent but the response hasn't arrived yet, + * cancellation causes `exec` to complete immediately. When the response + * eventually arrives from the server, it will be ignored. * * @par Object lifetimes * Both `req` and any response object referenced by `adapter` @@ -309,11 +361,41 @@ class co_connection { * * @param req The request to be executed. * @param adapter An adapter object referencing a response to place data into. - * @param token Completion token. + * + * @return A task that yields a @ref boost::capy::io_result holding the + * operation's `std::error_code` on completion. */ capy::io_task<> exec(request const& req, any_adapter adapter); - /// Sets the response object of @ref async_receive2 operations. + /** + * @brief Sets the response object for @ref receive operations. + * + * Pushes received by the connection (concretely, by @ref run) + * will be stored in `resp`. This happens even if @ref receive + * is not being called. + * + * `resp` should be able to accommodate the following message types: + * + * @li Any kind of RESP3 pushes that the application might expect. + * This usually involves Pub/Sub messages of type `message`, + * `subscribe`, `unsubscribe`, `psubscribe` and `punsubscribe`. + * See this page + * for more info. + * @li Any errors caused by failed `SUBSCRIBE` commands. Because of protocol + * oddities, these are placed in the receive buffer rather than handed to + * @ref exec. + * @li If your application is using `MONITOR`, simple strings. + * + * Because receive responses need to accommodate many different kinds + * of messages, it's advised to use one of the generic responses + * (like @ref generic_flat_response). If a response can't accommodate + * one of the received types, @ref run will exit with an error. + * + * Messages received before this function is called are discarded. + * + * @par Object lifetimes + * `resp` should be kept alive until @ref run completes. + */ template void set_receive_response(Response& resp) { From 8bd2dc6f543b1471df6a881524fd6ce06a6e7f5d Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 25 Apr 2026 21:09:51 +0200 Subject: [PATCH 115/115] Reference docs fixes --- doc/modules/ROOT/pages/reference.adoc | 2 ++ doc/mrdocs.cpp | 1 + include/boost/redis/co_connection.hpp | 27 +++++++++++++-------------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/doc/modules/ROOT/pages/reference.adoc b/doc/modules/ROOT/pages/reference.adoc index 085ea3cb..404e57a2 100644 --- a/doc/modules/ROOT/pages/reference.adoc +++ b/doc/modules/ROOT/pages/reference.adoc @@ -23,6 +23,8 @@ xref:reference:boost/redis/connection.adoc[`connection`] xref:reference:boost/redis/basic_connection.adoc[`basic_connection`] +xref:reference:boost/redis/co_connection.adoc[`co_connection`] + xref:reference:boost/redis/address.adoc[`address`] xref:reference:boost/redis/role.adoc[`role`] diff --git a/doc/mrdocs.cpp b/doc/mrdocs.cpp index 1543a509..eb52412d 100644 --- a/doc/mrdocs.cpp +++ b/doc/mrdocs.cpp @@ -8,3 +8,4 @@ #define BOOST_ALLOW_DEPRECATED // avoid mrdocs errors with the BOOST_DEPRECATED macro #include +#include diff --git a/include/boost/redis/co_connection.hpp b/include/boost/redis/co_connection.hpp index def8bd83..b4046f4e 100644 --- a/include/boost/redis/co_connection.hpp +++ b/include/boost/redis/co_connection.hpp @@ -33,16 +33,18 @@ struct co_connection_impl; /** @brief A connection to a Redis server, designed for capy coroutines. * - * This class keeps a healthy connection to the Redis instance where - * commands can be sent at any time. For more details, please see the - * documentation of each individual function. + * This class keeps a healthy connection to the Redis instance where + * commands can be sent at any time. For more details, please see the + * documentation of each individual function. * - * Each I/O member function returns a `capy::io_task` that should - * be `co_await`ed from a coroutine running on a capy executor. - * This class uses corosio for sockets, TLS and timers, and does not depend on Boost.Asio. - * Cancellation follows the usual capy patterns, and is driven by the parent task's - * `std::stop_token`. When stop is requested, operations complete with - * an error matching `capy::cond::canceled`. + * Each I/O member function returns a `capy::io_task` that should + * be awaited from a coroutine running on a capy executor. + * This class uses corosio for sockets, TLS and timers, and does not depend on Boost.Asio. + * Cancellation follows the usual capy patterns, and is driven by the parent task's + * `std::stop_token`. When stop is requested, operations complete with + * an error matching `capy::cond::canceled`. + * + * This type is movable but not copyable. * * @par Thread safety * Distinct objects: safe. @@ -146,9 +148,6 @@ class co_connection { */ co_connection& operator=(co_connection&&) noexcept; - co_connection(const co_connection&) = delete; - co_connection& operator=(const co_connection&) = delete; - /// Destructor. ~co_connection(); @@ -157,7 +156,7 @@ class co_connection { * This function establishes a connection to the Redis server and keeps * it healthy by performing the following operations: * - * @li For Sentinel deployments (@ref config::sentinel::addresses is not empty), + * @li For Sentinel deployments (`config::sentinel::addresses` is not empty), * contacts Sentinels to obtain the address of the configured master. * @li For TCP connections, resolves the server hostname passed in * @ref config::addr. @@ -227,7 +226,7 @@ class co_connection { * function continuously in a loop. * * This function does *not* remove messages from the response object - * passed to @ref set_receive_response - use the functions in the response + * passed to @ref set_receive_response. Use the functions in the response * object to achieve this. * * Only a single instance of `receive` may be outstanding for a given