From 6fa34c227c80a944282a249aa72360120f54a4fc Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 16 Dec 2025 20:42:03 +0100 Subject: [PATCH 01/63] Initial impl --- include/boost/redis/connection.hpp | 1 - .../boost/redis/detail/connection_state.hpp | 1 + include/boost/redis/detail/multiplexer.hpp | 7 ++++ include/boost/redis/detail/pubsub_state.hpp | 33 +++++++++++++++++++ include/boost/redis/impl/multiplexer.ipp | 17 +++++++--- include/boost/redis/impl/run_fsm.ipp | 12 ++++--- .../boost/redis/impl/setup_request_utils.hpp | 29 +++++++++------- include/boost/redis/request.hpp | 28 +++++++++++++++- 8 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 include/boost/redis/detail/pubsub_state.hpp diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index 3e8298f39..9fce9e31e 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -1059,7 +1059,6 @@ class basic_connection { 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() diff --git a/include/boost/redis/detail/connection_state.hpp b/include/boost/redis/detail/connection_state.hpp index b4d2e1a01..3570c7bbc 100644 --- a/include/boost/redis/detail/connection_state.hpp +++ b/include/boost/redis/detail/connection_state.hpp @@ -47,6 +47,7 @@ struct connection_state { config cfg{}; multiplexer mpx{}; std::string diagnostic{}; // Used by the setup request and Sentinel + request setup_req{}; request ping_req{}; // Sentinel stuff diff --git a/include/boost/redis/detail/multiplexer.hpp b/include/boost/redis/detail/multiplexer.hpp index 797c9493a..039152c3f 100644 --- a/include/boost/redis/detail/multiplexer.hpp +++ b/include/boost/redis/detail/multiplexer.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -206,8 +207,11 @@ class multiplexer { return usage_; } + void clear_pubsub_state() { pubsub_st_.clear(); } void set_config(config const& cfg); + const pubsub_state& get_pubsub_state() const { return pubsub_st_; } + private: void commit_usage(bool is_push, read_buffer::consume_result res); @@ -220,6 +224,9 @@ class multiplexer { [[nodiscard]] consume_result consume_impl(system::error_code& ec); + void complete_request(elem& elm); + + pubsub_state pubsub_st_; read_buffer read_buffer_; std::string write_buffer_; std::size_t write_offset_{}; // how many bytes of the write buffer have been written? diff --git a/include/boost/redis/detail/pubsub_state.hpp b/include/boost/redis/detail/pubsub_state.hpp new file mode 100644 index 000000000..765fd5bd4 --- /dev/null +++ b/include/boost/redis/detail/pubsub_state.hpp @@ -0,0 +1,33 @@ +// +// 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_PUBSUB_STATE_HPP +#define BOOST_REDIS_PUBSUB_STATE_HPP + +#include + +namespace boost::redis { + +class request; + +namespace detail { + +enum class pubsub_change_type; + +class pubsub_state { +public: + pubsub_state() = default; + void clear(); + void commit_change(pubsub_change_type type, std::string_view channel); + void compose_subscribe_request(request& to) const; +}; + +} // namespace detail +} // namespace boost::redis + +#endif diff --git a/include/boost/redis/impl/multiplexer.ipp b/include/boost/redis/impl/multiplexer.ipp index b6d118ce2..ff19f28ce 100644 --- a/include/boost/redis/impl/multiplexer.ipp +++ b/include/boost/redis/impl/multiplexer.ipp @@ -153,7 +153,7 @@ consume_result multiplexer::consume_impl(system::error_code& ec) reqs_.front()->commit_response(parser_.get_consumed()); if (reqs_.front()->get_remaining_responses() == 0) { // Done with this request. - reqs_.front()->notify_done(); + complete_request(*reqs_.front()); reqs_.pop_front(); } @@ -345,9 +345,8 @@ void multiplexer::release_push_requests() return !(ptr->is_written() && ptr->get_remaining_responses() == 0u); }); - std::for_each(point, std::end(reqs_), [](auto const& ptr) { - ptr->notify_done(); - }); + for (auto it = point; it != reqs_.end(); ++it) + complete_request(**it); reqs_.erase(point, std::end(reqs_)); } @@ -362,6 +361,16 @@ void multiplexer::set_config(config const& cfg) read_buffer_.set_config({cfg.read_buffer_append_size, cfg.max_read_size}); } +void multiplexer::complete_request(elem& elm) +{ + for (const auto& change : request_access::pubsub_changes(elm.get_request())) { + pubsub_st_.commit_change( + change.type, + elm.get_request().payload().substr(change.channel_offset, change.channel_size)); + } + elm.notify_done(); +} + auto make_elem(request const& req, any_adapter adapter) -> std::shared_ptr { return std::make_shared(req, std::move(adapter)); diff --git a/include/boost/redis/impl/run_fsm.ipp b/include/boost/redis/impl/run_fsm.ipp index 845f9d202..a99fb030e 100644 --- a/include/boost/redis/impl/run_fsm.ipp +++ b/include/boost/redis/impl/run_fsm.ipp @@ -101,10 +101,11 @@ run_action run_fsm::resume( return stored_ec_; } - // Compose the setup request. This only depends on the config, so it can be done just once - compose_setup_request(st.cfg); + // Setup the multiplexer + st.mpx.set_config(st.cfg); + st.mpx.clear_pubsub_state(); - // Compose the PING request. Same as above + // Compose the PING request. This only depends on the config, so it can be done just once compose_ping_request(st.cfg, st.ping_req); if (use_sentinel(st.cfg)) { @@ -159,10 +160,11 @@ run_action run_fsm::resume( // Initialization st.mpx.reset(); st.diagnostic.clear(); + compose_setup_request(st.cfg, st.mpx.get_pubsub_state(), st.setup_req); // Add the setup request to the multiplexer - if (st.cfg.setup.get_commands() != 0u) { - auto elm = make_elem(st.cfg.setup, make_any_adapter_impl(setup_adapter{st})); + if (st.setup_req.get_commands() != 0u) { + auto elm = make_elem(st.setup_req, make_any_adapter_impl(setup_adapter{st})); elm->set_done_callback([&elem_ref = *elm, &st] { on_setup_done(elem_ref, st); }); diff --git a/include/boost/redis/impl/setup_request_utils.hpp b/include/boost/redis/impl/setup_request_utils.hpp index c220d98e9..bed5e9748 100644 --- a/include/boost/redis/impl/setup_request_utils.hpp +++ b/include/boost/redis/impl/setup_request_utils.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include // use_sentinel #include @@ -22,14 +23,23 @@ namespace boost::redis::detail { // Modifies config::setup to make a request suitable to be sent // to the server using async_exec -inline void compose_setup_request(config& cfg) +inline void compose_setup_request(const config& cfg, const pubsub_state& pubsub_st, request& req) { - auto& req = cfg.setup; + // Clear any previous contents + req.clear(); - if (!cfg.use_setup) { + // Set the appropriate flags + request_access::set_priority(req, true); + req.get_config().cancel_if_unresponded = true; + req.get_config().cancel_on_connection_lost = true; + req.get_config().pubsub_state_restoration = false; + + if (cfg.use_setup) { + // We should use the provided request as-is + req.append(cfg.setup); + } else { // We're not using the setup request as-is, but should compose one based on // the values passed by the user - req.clear(); // Which parts of the command should we send? // Don't send AUTH if the user is the default and the password is empty. @@ -59,12 +69,8 @@ inline void compose_setup_request(config& cfg) if (use_sentinel(cfg)) req.push("ROLE"); - // In any case, the setup request should have the priority - // flag set so it's executed before any other request. - // The setup request should never be retried. - request_access::set_priority(req, true); - req.get_config().cancel_if_unresponded = true; - req.get_config().cancel_on_connection_lost = true; + // Add any subscription commands require to restore the PubSub state + pubsub_st.compose_subscribe_request(req); } class setup_adapter { @@ -83,7 +89,8 @@ class setup_adapter { // When using Sentinel, we add a ROLE command at the end. // We need to ensure that this instance is a master. - if (use_sentinel(st_->cfg) && response_idx_ == st_->cfg.setup.get_expected_responses() - 1u) { + // ROLE may be followed by subscribe requests, but these don't expect any response. + if (use_sentinel(st_->cfg) && response_idx_ == st_->setup_req.get_expected_responses() - 1u) { // ROLE's response should be an array of at least 1 element if (nd.depth == 0u) { if (nd.data_type != resp3::type::array) diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index fc3160c06..7c7843774 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -10,8 +10,10 @@ #include #include +#include #include #include +#include // NOTE: For some commands like hset it would be a good idea to assert // the value type is a pair. @@ -19,8 +21,24 @@ namespace boost::redis { namespace detail { + auto has_response(std::string_view cmd) -> bool; struct request_access; + +enum class pubsub_change_type +{ + subscribe, + unsubscribe, + psubscribe, + punsubscribe, +}; + +struct pubsub_change { + pubsub_change_type type; + std::size_t channel_offset; + std::size_t channel_size; +}; + } // namespace detail /** @brief Represents a Redis request. @@ -89,13 +107,16 @@ class request { * This field will be removed in subsequent releases. */ bool hello_with_priority = true; + + // TODO: document + bool pubsub_state_restoration = false; }; /** @brief Constructor * * @param cfg Configuration options. */ - explicit request(config cfg = config{false, false, true, true}) + explicit request(config cfg = config{false, false, true, true, false}) : cfg_{cfg} { } @@ -429,6 +450,7 @@ class request { std::size_t commands_ = 0; std::size_t expected_responses_ = 0; bool has_hello_priority_ = false; + std::vector pubsub_changes_{}; friend struct detail::request_access; }; @@ -438,6 +460,10 @@ namespace detail { struct request_access { inline static void set_priority(request& r, bool value) { r.has_hello_priority_ = value; } inline static bool has_priority(const request& r) { return r.has_hello_priority_; } + inline static const std::vector& pubsub_changes(const request& r) + { + return r.pubsub_changes_; + } }; // Creates a HELLO 3 request From cef6125dae647c78c70cde85ea2c34595534c206 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 16 Dec 2025 20:55:46 +0100 Subject: [PATCH 02/63] pubsub_state impl --- include/boost/redis/detail/pubsub_state.hpp | 5 +++ include/boost/redis/impl/pubsub_state.ipp | 42 +++++++++++++++++++++ include/boost/redis/src.hpp | 3 +- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 include/boost/redis/impl/pubsub_state.ipp diff --git a/include/boost/redis/detail/pubsub_state.hpp b/include/boost/redis/detail/pubsub_state.hpp index 765fd5bd4..09177ce71 100644 --- a/include/boost/redis/detail/pubsub_state.hpp +++ b/include/boost/redis/detail/pubsub_state.hpp @@ -9,6 +9,8 @@ #ifndef BOOST_REDIS_PUBSUB_STATE_HPP #define BOOST_REDIS_PUBSUB_STATE_HPP +#include +#include #include namespace boost::redis { @@ -20,6 +22,9 @@ namespace detail { enum class pubsub_change_type; class pubsub_state { + std::set channels_; + std::set pchannels_; + public: pubsub_state() = default; void clear(); diff --git a/include/boost/redis/impl/pubsub_state.ipp b/include/boost/redis/impl/pubsub_state.ipp new file mode 100644 index 000000000..bbeeda55d --- /dev/null +++ b/include/boost/redis/impl/pubsub_state.ipp @@ -0,0 +1,42 @@ +// +// 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 + +namespace boost::redis::detail { + +void pubsub_state::clear() +{ + channels_.clear(); + pchannels_.clear(); +} + +void pubsub_state::commit_change(pubsub_change_type type, std::string_view channel) +{ + std::string owning_channel{channel}; + switch (type) { + case pubsub_change_type::subscribe: channels_.insert(std::move(owning_channel)); break; + case pubsub_change_type::unsubscribe: channels_.erase(std::move(owning_channel)); break; + case pubsub_change_type::psubscribe: pchannels_.insert(std::move(owning_channel)); break; + case pubsub_change_type::punsubscribe: pchannels_.erase(std::move(owning_channel)); break; + default: BOOST_ASSERT(false); + } +} + +void pubsub_state::compose_subscribe_request(request& to) const +{ + to.push_range("SUBSCRIBE", channels_); + to.push_range("PBSUBSCRIBE", pchannels_); +} + +} // namespace boost::redis::detail diff --git a/include/boost/redis/src.hpp b/include/boost/redis/src.hpp index eb48981b9..9297968ff 100644 --- a/include/boost/redis/src.hpp +++ b/include/boost/redis/src.hpp @@ -9,9 +9,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -19,7 +21,6 @@ #include #include #include -#include #include #include #include From 67cb8e06cff82c1ccd5489fff186a3ba1d0e98dd Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 10:24:34 +0100 Subject: [PATCH 03/63] Initial request impl --- include/boost/redis/request.hpp | 32 ++--- .../boost/redis/resp3/impl/serialization.ipp | 17 +++ include/boost/redis/resp3/serialization.hpp | 125 +++++++++++++----- 3 files changed, 123 insertions(+), 51 deletions(-) diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index 7c7843774..296190684 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -25,20 +26,6 @@ namespace detail { auto has_response(std::string_view cmd) -> bool; struct request_access; -enum class pubsub_change_type -{ - subscribe, - unsubscribe, - psubscribe, - punsubscribe, -}; - -struct pubsub_change { - pubsub_change_type type; - std::size_t channel_offset; - std::size_t channel_size; -}; - } // namespace detail /** @brief Represents a Redis request. @@ -191,9 +178,10 @@ class request { void push(std::string_view cmd, Ts const&... args) { auto constexpr pack_size = sizeof...(Ts); + resp3::command_context ctx(cmd, pubsub_changes_, payload_); resp3::add_header(payload_, resp3::type::array, 1 + pack_size); - resp3::add_bulk(payload_, cmd); - resp3::add_bulk(payload_, std::tie(std::forward(args)...)); + resp3::boost_redis_to_bulk(payload_, cmd); + resp3::add_argument(ctx, std::tie(std::forward(args)...)); check_cmd(cmd); } @@ -262,12 +250,13 @@ class request { auto constexpr size = resp3::bulk_counter::size; auto const distance = std::distance(begin, end); + resp3::command_context ctx(cmd, pubsub_changes_, payload_); resp3::add_header(payload_, resp3::type::array, 2 + size * distance); - resp3::add_bulk(payload_, cmd); - resp3::add_bulk(payload_, key); + resp3::boost_redis_to_bulk(payload_, cmd); + resp3::add_argument(ctx, key); for (; begin != end; ++begin) - resp3::add_bulk(payload_, *begin); + resp3::add_argument(ctx, *begin); check_cmd(cmd); } @@ -331,11 +320,12 @@ class request { auto constexpr size = resp3::bulk_counter::size; auto const distance = std::distance(begin, end); + resp3::command_context ctx(cmd, pubsub_changes_, payload_); resp3::add_header(payload_, resp3::type::array, 1 + size * distance); - resp3::add_bulk(payload_, cmd); + resp3::boost_redis_to_bulk(payload_, cmd); for (; begin != end; ++begin) - resp3::add_bulk(payload_, *begin); + resp3::add_argument(ctx, *begin); check_cmd(cmd); } diff --git a/include/boost/redis/resp3/impl/serialization.ipp b/include/boost/redis/resp3/impl/serialization.ipp index 74e8c918b..f6ff9eaef 100644 --- a/include/boost/redis/resp3/impl/serialization.ipp +++ b/include/boost/redis/resp3/impl/serialization.ipp @@ -36,4 +36,21 @@ void add_blob(std::string& payload, std::string_view blob) } void add_separator(std::string& payload) { payload += parser::sep; } + +void command_context::add_argument(std::string_view value) +{ + // TODO: this is duplicated from boost_redis_to_bulk + // Add the value to the payload + *payload_ += to_code(resp3::type::blob_string); + *payload_ += std::to_string(value.size()); + *payload_ += resp3::parser::sep; + std::size_t offset = payload_->size(); + payload_->append(value.cbegin(), value.cend()); + *payload_ += resp3::parser::sep; + + // Record any pubsub change + if (cmd_change_ != ::boost::redis::detail::pubsub_change_type::none) + changes_->push_back({cmd_change_, offset, value.size()}); +} + } // namespace boost::redis::resp3 diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index 28835925f..ad1eee837 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -14,10 +14,61 @@ #include #include +#include #include +#include +#include +#include + +namespace boost::redis::detail { + +enum class pubsub_change_type +{ + none, + subscribe, + unsubscribe, + psubscribe, + punsubscribe, +}; + +struct pubsub_change { + pubsub_change_type type; + std::size_t channel_offset; + std::size_t channel_size; +}; + +} // namespace boost::redis::detail namespace boost::redis::resp3 { +class command_context { + detail::pubsub_change_type cmd_change_; + std::vector* changes_; + std::string* payload_; + +public: + // TODO: hide + command_context( + detail::pubsub_change_type t, + std::vector& changes, + std::string& payload) noexcept + : cmd_change_(t) + , changes_(&changes) + , payload_(&payload) + { } + + // TODO + command_context( + std::string_view cmd, + std::vector& changes, + std::string& payload) noexcept; + + void add_argument(std::string_view value); + + // TODO: hide + std::string& payload() { return *payload_; } +}; + /** @brief Adds a bulk to the request. * @relates boost::redis::request * @@ -48,46 +99,60 @@ void boost_redis_to_bulk(std::string& payload, T n) boost::redis::resp3::boost_redis_to_bulk(payload, std::string_view{s}); } -template -struct add_bulk_impl { - static void add(std::string& payload, T const& from) - { - using namespace boost::redis::resp3; - boost_redis_to_bulk(payload, from); - } -}; +namespace detail { -template -struct add_bulk_impl> { - static void add(std::string& payload, std::tuple const& t) - { - auto f = [&](auto const&... vs) { - using namespace boost::redis::resp3; - (boost_redis_to_bulk(payload, vs), ...); - }; - - std::apply(f, t); - } -}; +template +struct has_to_bulk_v1 : std::false_type { }; -template -struct add_bulk_impl> { - static void add(std::string& payload, std::pair const& from) - { +template +struct has_to_bulk_v1< + T, + decltype(boost_redis_to_bulk(std::declval(), std::declval()))> +: std::true_type { }; + +template +void add_scalar_argument(command_context ctx, T const& value) +{ + if constexpr (std::is_convertible_v) { + ctx.add_argument(value); + } else if constexpr (std::is_integral_v) { + ctx.add_argument(std::to_string(value)); + } else if constexpr (detail::has_to_bulk_v1::value) { using namespace boost::redis::resp3; - boost_redis_to_bulk(payload, from.first); - boost_redis_to_bulk(payload, from.second); + boost_redis_to_bulk(ctx.payload(), value); // TODO: this bypasses things + } else { + using namespace boost::redis::resp3; + boost_redis_to_bulk(ctx, value); } -}; +} -void add_header(std::string& payload, type t, std::size_t size); +} // namespace detail template -void add_bulk(std::string& payload, T const& data) +void add_argument(command_context ctx, T const& data) { - add_bulk_impl::add(payload, data); + detail::add_scalar_argument(ctx, data); } +template +void add_argument(command_context ctx, std::tuple const& t) +{ + auto f = [&](auto const&... vs) { + (detail::add_scalar_argument(ctx, vs), ...); + }; + + std::apply(f, t); +} + +template +void add_argument(command_context ctx, std::pair const& from) +{ + detail::add_scalar_argument(ctx, from.first); + detail::add_scalar_argument(ctx, from.second); +} + +void add_header(std::string& payload, type t, std::size_t size); + template struct bulk_counter; From f1e9772c5d9721f7785ba2d9660a5d63bd58d88c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 10:36:18 +0100 Subject: [PATCH 04/63] start_command --- include/boost/redis/impl/request.ipp | 34 +++++++++++++++++++++ include/boost/redis/request.hpp | 14 +++------ include/boost/redis/resp3/serialization.hpp | 6 ---- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/include/boost/redis/impl/request.ipp b/include/boost/redis/impl/request.ipp index 741c3b5d3..f378a7b06 100644 --- a/include/boost/redis/impl/request.ipp +++ b/include/boost/redis/impl/request.ipp @@ -5,7 +5,10 @@ */ #include +#include +#include +#include #include namespace boost::redis::detail { @@ -38,3 +41,34 @@ void boost::redis::request::append(const request& other) commands_ += other.commands_; expected_responses_ += other.expected_responses_; } + +boost::redis::resp3::command_context boost::redis::request::start_command( + std::string_view cmd, + std::size_t num_args) +{ + // Determine the pubsub change type that this command is performing + // TODO: this has overlap with has_response + auto change_type = detail::pubsub_change_type::none; + if (cfg_.pubsub_state_restoration) { + if (cmd == "SUBSCRIBE") + change_type = detail::pubsub_change_type::subscribe; + if (cmd == "PSUBSCRIBE") + change_type = detail::pubsub_change_type::psubscribe; + if (cmd == "UNSUBSCRIBE") + change_type = detail::pubsub_change_type::unsubscribe; + if (cmd == "PUNSUBSCRIBE") + change_type = detail::pubsub_change_type::punsubscribe; + } + + // Add the header + resp3::add_header( + payload_, + resp3::type::array, + num_args + 1u); // the command string is also an array member + + // Serialize the command string + resp3::boost_redis_to_bulk(payload_, cmd); + + // Compose the command context + return {change_type, pubsub_changes_, payload_}; +} diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index 296190684..32bc9e5a8 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -178,9 +178,7 @@ class request { void push(std::string_view cmd, Ts const&... args) { auto constexpr pack_size = sizeof...(Ts); - resp3::command_context ctx(cmd, pubsub_changes_, payload_); - resp3::add_header(payload_, resp3::type::array, 1 + pack_size); - resp3::boost_redis_to_bulk(payload_, cmd); + auto ctx = start_command(cmd, pack_size); resp3::add_argument(ctx, std::tie(std::forward(args)...)); check_cmd(cmd); @@ -250,9 +248,7 @@ class request { auto constexpr size = resp3::bulk_counter::size; auto const distance = std::distance(begin, end); - resp3::command_context ctx(cmd, pubsub_changes_, payload_); - resp3::add_header(payload_, resp3::type::array, 2 + size * distance); - resp3::boost_redis_to_bulk(payload_, cmd); + auto ctx = start_command(cmd, 1 + size * distance); resp3::add_argument(ctx, key); for (; begin != end; ++begin) @@ -320,9 +316,7 @@ class request { auto constexpr size = resp3::bulk_counter::size; auto const distance = std::distance(begin, end); - resp3::command_context ctx(cmd, pubsub_changes_, payload_); - resp3::add_header(payload_, resp3::type::array, 1 + size * distance); - resp3::boost_redis_to_bulk(payload_, cmd); + auto ctx = start_command(cmd, size * distance); for (; begin != end; ++begin) resp3::add_argument(ctx, *begin); @@ -424,6 +418,8 @@ class request { void append(const request& other); private: + resp3::command_context start_command(std::string_view cmd, std::size_t num_args); + void check_cmd(std::string_view cmd) { ++commands_; diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index ad1eee837..49a8e2b4c 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -57,12 +57,6 @@ class command_context { , payload_(&payload) { } - // TODO - command_context( - std::string_view cmd, - std::vector& changes, - std::string& payload) noexcept; - void add_argument(std::string_view value); // TODO: hide From 278a99f99371009defbe9cbdf62509e514ceadf3 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 12:50:57 +0100 Subject: [PATCH 05/63] parsing workaround --- .../boost/redis/resp3/impl/serialization.ipp | 26 +++++++++++++++++++ include/boost/redis/resp3/serialization.hpp | 7 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/include/boost/redis/resp3/impl/serialization.ipp b/include/boost/redis/resp3/impl/serialization.ipp index f6ff9eaef..da73e6687 100644 --- a/include/boost/redis/resp3/impl/serialization.ipp +++ b/include/boost/redis/resp3/impl/serialization.ipp @@ -6,6 +6,9 @@ #include #include +#include + +#include namespace boost::redis::resp3 { @@ -53,4 +56,27 @@ void command_context::add_argument(std::string_view value) changes_->push_back({cmd_change_, offset, value.size()}); } +void command_context::parse_last_argument(std::size_t offset) +{ + // No need to analyze arguments if this command is not related to PubSub + if (cmd_change_ == ::boost::redis::detail::pubsub_change_type::none) + return; + + // Parse the serialized argument + resp3::parser p; + system::error_code ec; + auto node = p.consume(std::string_view(*payload_).substr(offset), ec); + if (ec || !node.has_value()) + return; // something went very wrong during serialization + + // Add the change + std::string_view node_value = node->value; + BOOST_ASSERT(node_value.data() >= payload_->data()); + changes_->push_back({ + cmd_change_, + static_cast(node_value.data() - payload_->data()), + node_value.size(), + }); +} + } // namespace boost::redis::resp3 diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index 49a8e2b4c..f1ec1c58a 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -61,6 +61,9 @@ class command_context { // TODO: hide std::string& payload() { return *payload_; } + + // TODO: hide + void parse_last_argument(std::size_t offset); }; /** @brief Adds a bulk to the request. @@ -113,7 +116,9 @@ void add_scalar_argument(command_context ctx, T const& value) ctx.add_argument(std::to_string(value)); } else if constexpr (detail::has_to_bulk_v1::value) { using namespace boost::redis::resp3; - boost_redis_to_bulk(ctx.payload(), value); // TODO: this bypasses things + auto offset = ctx.payload().size(); + boost_redis_to_bulk(ctx.payload(), value); + ctx.parse_last_argument(offset); } else { using namespace boost::redis::resp3; boost_redis_to_bulk(ctx, value); From e502703dc0a9c2e0025309fe8d4373e095ce4687 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 12:58:43 +0100 Subject: [PATCH 06/63] clear and append --- include/boost/redis/impl/request.ipp | 4 ++++ include/boost/redis/request.hpp | 1 + 2 files changed, 5 insertions(+) diff --git a/include/boost/redis/impl/request.ipp b/include/boost/redis/impl/request.ipp index f378a7b06..de20882d2 100644 --- a/include/boost/redis/impl/request.ipp +++ b/include/boost/redis/impl/request.ipp @@ -38,6 +38,10 @@ request make_hello_request() void boost::redis::request::append(const request& other) { payload_ += other.payload_; + pubsub_changes_.insert( + pubsub_changes_.end(), + other.pubsub_changes_.begin(), + other.pubsub_changes_.end()); commands_ += other.commands_; expected_responses_ += other.expected_responses_; } diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index 32bc9e5a8..383a7809c 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -131,6 +131,7 @@ class request { void clear() { payload_.clear(); + pubsub_changes_.clear(); commands_ = 0; expected_responses_ = 0; has_hello_priority_ = false; From e820b456cab5736b990fcaaed08349fcb52ce802 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 13:11:50 +0100 Subject: [PATCH 07/63] some namespace changes --- include/boost/redis/impl/request.ipp | 2 +- include/boost/redis/request.hpp | 2 +- .../boost/redis/resp3/impl/serialization.ipp | 68 ++++++++++--------- include/boost/redis/resp3/serialization.hpp | 6 +- 4 files changed, 44 insertions(+), 34 deletions(-) diff --git a/include/boost/redis/impl/request.ipp b/include/boost/redis/impl/request.ipp index de20882d2..15fb57dbb 100644 --- a/include/boost/redis/impl/request.ipp +++ b/include/boost/redis/impl/request.ipp @@ -46,7 +46,7 @@ void boost::redis::request::append(const request& other) expected_responses_ += other.expected_responses_; } -boost::redis::resp3::command_context boost::redis::request::start_command( +boost::redis::command_context boost::redis::request::start_command( std::string_view cmd, std::size_t num_args) { diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index 383a7809c..a1aeba1d9 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -419,7 +419,7 @@ class request { void append(const request& other); private: - resp3::command_context start_command(std::string_view cmd, std::size_t num_args); + command_context start_command(std::string_view cmd, std::size_t num_args); void check_cmd(std::string_view cmd) { diff --git a/include/boost/redis/resp3/impl/serialization.ipp b/include/boost/redis/resp3/impl/serialization.ipp index da73e6687..4cc535bae 100644 --- a/include/boost/redis/resp3/impl/serialization.ipp +++ b/include/boost/redis/resp3/impl/serialization.ipp @@ -8,37 +8,11 @@ #include #include -#include - -namespace boost::redis::resp3 { - -void boost_redis_to_bulk(std::string& payload, std::string_view data) -{ - auto const str = std::to_string(data.size()); - - payload += to_code(type::blob_string); - payload.append(std::cbegin(str), std::cend(str)); - payload += parser::sep; - payload.append(std::cbegin(data), std::cend(data)); - payload += parser::sep; -} +#include -void add_header(std::string& payload, type t, std::size_t size) -{ - auto const str = std::to_string(size); - - payload += to_code(t); - payload.append(std::cbegin(str), std::cend(str)); - payload += parser::sep; -} - -void add_blob(std::string& payload, std::string_view blob) -{ - payload.append(std::cbegin(blob), std::cend(blob)); - payload += parser::sep; -} +#include -void add_separator(std::string& payload) { payload += parser::sep; } +namespace boost::redis { void command_context::add_argument(std::string_view value) { @@ -52,14 +26,14 @@ void command_context::add_argument(std::string_view value) *payload_ += resp3::parser::sep; // Record any pubsub change - if (cmd_change_ != ::boost::redis::detail::pubsub_change_type::none) + if (cmd_change_ != detail::pubsub_change_type::none) changes_->push_back({cmd_change_, offset, value.size()}); } void command_context::parse_last_argument(std::size_t offset) { // No need to analyze arguments if this command is not related to PubSub - if (cmd_change_ == ::boost::redis::detail::pubsub_change_type::none) + if (cmd_change_ == detail::pubsub_change_type::none) return; // Parse the serialized argument @@ -79,4 +53,36 @@ void command_context::parse_last_argument(std::size_t offset) }); } +} // namespace boost::redis + +namespace boost::redis::resp3 { + +void boost_redis_to_bulk(std::string& payload, std::string_view data) +{ + auto const str = std::to_string(data.size()); + + payload += to_code(type::blob_string); + payload.append(std::cbegin(str), std::cend(str)); + payload += parser::sep; + payload.append(std::cbegin(data), std::cend(data)); + payload += parser::sep; +} + +void add_header(std::string& payload, type t, std::size_t size) +{ + auto const str = std::to_string(size); + + payload += to_code(t); + payload.append(std::cbegin(str), std::cend(str)); + payload += parser::sep; +} + +void add_blob(std::string& payload, std::string_view blob) +{ + payload.append(std::cbegin(blob), std::cend(blob)); + payload += parser::sep; +} + +void add_separator(std::string& payload) { payload += parser::sep; } + } // namespace boost::redis::resp3 diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index f1ec1c58a..c6925d773 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -39,7 +39,7 @@ struct pubsub_change { } // namespace boost::redis::detail -namespace boost::redis::resp3 { +namespace boost::redis { class command_context { detail::pubsub_change_type cmd_change_; @@ -66,6 +66,10 @@ class command_context { void parse_last_argument(std::size_t offset); }; +} // namespace boost::redis + +namespace boost::redis::resp3 { + /** @brief Adds a bulk to the request. * @relates boost::redis::request * From 1a0d94c8f2575921e1566fc47fea82236e4e1c79 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 13:19:27 +0100 Subject: [PATCH 08/63] namespace moving --- include/boost/redis/request.hpp | 12 +++++----- include/boost/redis/resp3/serialization.hpp | 26 ++++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index a1aeba1d9..ebf31b5bc 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -180,7 +180,7 @@ class request { { auto constexpr pack_size = sizeof...(Ts); auto ctx = start_command(cmd, pack_size); - resp3::add_argument(ctx, std::tie(std::forward(args)...)); + detail::add_argument(ctx, std::tie(std::forward(args)...)); check_cmd(cmd); } @@ -247,13 +247,13 @@ class request { if (begin == end) return; - auto constexpr size = resp3::bulk_counter::size; + auto constexpr size = detail::bulk_counter::size; auto const distance = std::distance(begin, end); auto ctx = start_command(cmd, 1 + size * distance); - resp3::add_argument(ctx, key); + detail::add_argument(ctx, key); for (; begin != end; ++begin) - resp3::add_argument(ctx, *begin); + detail::add_argument(ctx, *begin); check_cmd(cmd); } @@ -315,12 +315,12 @@ class request { if (begin == end) return; - auto constexpr size = resp3::bulk_counter::size; + auto constexpr size = detail::bulk_counter::size; auto const distance = std::distance(begin, end); auto ctx = start_command(cmd, size * distance); for (; begin != end; ++begin) - resp3::add_argument(ctx, *begin); + detail::add_argument(ctx, *begin); check_cmd(cmd); } diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index c6925d773..4ac78215a 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -37,6 +37,8 @@ struct pubsub_change { std::size_t channel_size; }; +struct command_context_access; + } // namespace boost::redis::detail namespace boost::redis { @@ -46,6 +48,8 @@ class command_context { std::vector* changes_; std::string* payload_; + friend struct detail::command_context_access; + public: // TODO: hide command_context( @@ -100,7 +104,13 @@ void boost_redis_to_bulk(std::string& payload, T n) boost::redis::resp3::boost_redis_to_bulk(payload, std::string_view{s}); } -namespace detail { +void add_header(std::string& payload, type t, std::size_t size); +void add_blob(std::string& payload, std::string_view blob); +void add_separator(std::string& payload); + +} // namespace boost::redis::resp3 + +namespace boost::redis::detail { template struct has_to_bulk_v1 : std::false_type { }; @@ -129,8 +139,6 @@ void add_scalar_argument(command_context ctx, T const& value) } } -} // namespace detail - template void add_argument(command_context ctx, T const& data) { @@ -154,8 +162,6 @@ void add_argument(command_context ctx, std::pair const& from) detail::add_scalar_argument(ctx, from.second); } -void add_header(std::string& payload, type t, std::size_t size); - template struct bulk_counter; @@ -174,10 +180,10 @@ struct bulk_counter> { static constexpr auto size = sizeof...(T); }; -void add_blob(std::string& payload, std::string_view blob); -void add_separator(std::string& payload); +} // namespace boost::redis::detail -namespace detail { +// TODO: this belongs to tests +namespace boost::redis::resp3::detail { template void deserialize(std::string_view const& data, Adapter adapter, system::error_code& ec) @@ -212,8 +218,6 @@ void deserialize(std::string_view const& data, Adapter adapter) BOOST_THROW_EXCEPTION(system::system_error{ec}); } -} // namespace detail - -} // namespace boost::redis::resp3 +} // namespace boost::redis::resp3::detail #endif // BOOST_REDIS_RESP3_SERIALIZATION_HPP From 7e9ab02225692cc32affd6d115f879e0515d3f62 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 13:21:58 +0100 Subject: [PATCH 09/63] hide constructor --- include/boost/redis/impl/request.ipp | 2 +- include/boost/redis/resp3/serialization.hpp | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/include/boost/redis/impl/request.ipp b/include/boost/redis/impl/request.ipp index 15fb57dbb..3ef8c65ff 100644 --- a/include/boost/redis/impl/request.ipp +++ b/include/boost/redis/impl/request.ipp @@ -74,5 +74,5 @@ boost::redis::command_context boost::redis::request::start_command( resp3::boost_redis_to_bulk(payload_, cmd); // Compose the command context - return {change_type, pubsub_changes_, payload_}; + return detail::command_context_access::construct(change_type, pubsub_changes_, payload_); } diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index 4ac78215a..f3d5d85e7 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -50,8 +50,6 @@ class command_context { friend struct detail::command_context_access; -public: - // TODO: hide command_context( detail::pubsub_change_type t, std::vector& changes, @@ -61,6 +59,7 @@ class command_context { , payload_(&payload) { } +public: void add_argument(std::string_view value); // TODO: hide @@ -112,6 +111,16 @@ void add_separator(std::string& payload); namespace boost::redis::detail { +struct command_context_access { + static command_context construct( + detail::pubsub_change_type t, + std::vector& changes, + std::string& payload) + { + return {t, changes, payload}; + } +}; + template struct has_to_bulk_v1 : std::false_type { }; From 3a31d2317943cfbc52f6d744ad7e880fe294f16f Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 13:25:48 +0100 Subject: [PATCH 10/63] hide parse_last_argument --- include/boost/redis/resp3/serialization.hpp | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index f3d5d85e7..fd6de3088 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -59,14 +59,10 @@ class command_context { , payload_(&payload) { } + void parse_last_argument(std::size_t offset); + public: void add_argument(std::string_view value); - - // TODO: hide - std::string& payload() { return *payload_; } - - // TODO: hide - void parse_last_argument(std::size_t offset); }; } // namespace boost::redis @@ -119,6 +115,15 @@ struct command_context_access { { return {t, changes, payload}; } + + template + static void add_custom_argument(command_context ctx, const T& value) + { + using namespace boost::redis::resp3; + auto offset = ctx.payload_->size(); + boost_redis_to_bulk(*ctx.payload_, value); + ctx.parse_last_argument(offset); + } }; template @@ -138,10 +143,7 @@ void add_scalar_argument(command_context ctx, T const& value) } else if constexpr (std::is_integral_v) { ctx.add_argument(std::to_string(value)); } else if constexpr (detail::has_to_bulk_v1::value) { - using namespace boost::redis::resp3; - auto offset = ctx.payload().size(); - boost_redis_to_bulk(ctx.payload(), value); - ctx.parse_last_argument(offset); + detail::command_context_access::add_custom_argument(ctx, value); } else { using namespace boost::redis::resp3; boost_redis_to_bulk(ctx, value); From ddea2a9eb7ba5a37009494fda2a89f6c09d52088 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 13:50:23 +0100 Subject: [PATCH 11/63] Make the setting global --- include/boost/redis/config.hpp | 3 +++ include/boost/redis/connection.hpp | 7 +++++-- .../boost/redis/detail/connection_state.hpp | 2 ++ include/boost/redis/detail/exec_fsm.hpp | 13 ++++++++----- include/boost/redis/detail/pubsub_state.hpp | 1 + include/boost/redis/impl/exec_fsm.ipp | 18 ++++++++++++++---- include/boost/redis/impl/pubsub_state.ipp | 6 ++++++ include/boost/redis/impl/request.ipp | 18 ++++++++---------- .../boost/redis/impl/setup_request_utils.hpp | 1 - include/boost/redis/request.hpp | 5 +---- 10 files changed, 48 insertions(+), 26 deletions(-) diff --git a/include/boost/redis/config.hpp b/include/boost/redis/config.hpp index 4e9806794..75c0aea00 100644 --- a/include/boost/redis/config.hpp +++ b/include/boost/redis/config.hpp @@ -345,6 +345,9 @@ struct config { * @ref sentinel_config::addresses is not empty. */ sentinel_config sentinel{}; + + // (TODO: document properly) Do we want to re-subscribe to channels on reconnection? + bool restore_pubsub_state = false; }; } // namespace boost::redis diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index 9fce9e31e..b71532c8f 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -108,7 +108,10 @@ struct connection_impl { { while (true) { // Invoke the state machine - auto act = fsm_.resume(obj_->is_open(), self.get_cancellation_state().cancelled()); + auto act = fsm_.resume( + obj_->is_open(), + obj_->st_, + self.get_cancellation_state().cancelled()); // Do what the FSM said switch (act.type()) { @@ -203,7 +206,7 @@ struct connection_impl { }); return asio::async_compose( - exec_op{this, notifier, exec_fsm(st_.mpx, std::move(info))}, + exec_op{this, notifier, exec_fsm(std::move(info))}, token, writer_cv_); } diff --git a/include/boost/redis/detail/connection_state.hpp b/include/boost/redis/detail/connection_state.hpp index 3570c7bbc..bc4e9128f 100644 --- a/include/boost/redis/detail/connection_state.hpp +++ b/include/boost/redis/detail/connection_state.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -49,6 +50,7 @@ struct connection_state { std::string diagnostic{}; // Used by the setup request and Sentinel request setup_req{}; request ping_req{}; + pubsub_state pubsub_st{}; // Sentinel stuff lazy_random_engine eng{}; diff --git a/include/boost/redis/detail/exec_fsm.hpp b/include/boost/redis/detail/exec_fsm.hpp index 3727d07c3..7dc2f8c12 100644 --- a/include/boost/redis/detail/exec_fsm.hpp +++ b/include/boost/redis/detail/exec_fsm.hpp @@ -21,6 +21,8 @@ namespace boost::redis::detail { +struct connection_state; + // What should we do next? enum class exec_action_type { @@ -54,16 +56,17 @@ class exec_action { class exec_fsm { int resume_point_{0}; - multiplexer* mpx_{nullptr}; std::shared_ptr elem_; public: - exec_fsm(multiplexer& mpx, std::shared_ptr elem) noexcept - : mpx_(&mpx) - , elem_(std::move(elem)) + exec_fsm(std::shared_ptr elem) noexcept + : elem_(std::move(elem)) { } - exec_action resume(bool connection_is_open, asio::cancellation_type_t cancel_state); + exec_action resume( + bool connection_is_open, + connection_state& st, + asio::cancellation_type_t cancel_state); }; } // namespace boost::redis::detail diff --git a/include/boost/redis/detail/pubsub_state.hpp b/include/boost/redis/detail/pubsub_state.hpp index 09177ce71..f37723512 100644 --- a/include/boost/redis/detail/pubsub_state.hpp +++ b/include/boost/redis/detail/pubsub_state.hpp @@ -29,6 +29,7 @@ class pubsub_state { pubsub_state() = default; void clear(); void commit_change(pubsub_change_type type, std::string_view channel); + void commit_changes(const request& req); void compose_subscribe_request(request& to) const; }; diff --git a/include/boost/redis/impl/exec_fsm.ipp b/include/boost/redis/impl/exec_fsm.ipp index b7be8c608..663d2428c 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 @@ -28,7 +29,10 @@ 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, asio::cancellation_type_t cancel_state) +exec_action exec_fsm::resume( + bool connection_is_open, + connection_state& st, + asio::cancellation_type_t cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -47,7 +51,7 @@ exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t BOOST_REDIS_YIELD(resume_point_, 2, exec_action_type::setup_cancellation) // Add the request to the multiplexer - mpx_->add(elem_); + st.mpx.add(elem_); // Notify the writer task that there is work to do. If the task is not // listening (e.g. it's already writing or the connection is not healthy), @@ -61,8 +65,14 @@ exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t // If the request has completed (with error or not), we're done if (elem_->is_done()) { + // If the request completed successfully and we were configured to do so, + // record the changes applied to the pubsub state + if (st.cfg.restore_pubsub_state && !elem_->get_error()) + st.pubsub_st.commit_changes(elem_->get_request()); + + // Deallocate memory before finalizing exec_action act{elem_->get_error(), elem_->get_read_size()}; - elem_.reset(); // Deallocate memory before finalizing + elem_.reset(); return act; } @@ -71,7 +81,7 @@ exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t if ( (is_total_cancel(cancel_state) && elem_->is_waiting()) || is_partial_or_terminal_cancel(cancel_state)) { - mpx_->cancel(elem_); + st.mpx.cancel(elem_); elem_.reset(); // Deallocate memory before finalizing return exec_action{asio::error::operation_aborted}; } diff --git a/include/boost/redis/impl/pubsub_state.ipp b/include/boost/redis/impl/pubsub_state.ipp index bbeeda55d..1b882f322 100644 --- a/include/boost/redis/impl/pubsub_state.ipp +++ b/include/boost/redis/impl/pubsub_state.ipp @@ -33,6 +33,12 @@ void pubsub_state::commit_change(pubsub_change_type type, std::string_view chann } } +void pubsub_state::commit_changes(const request& req) +{ + for (const auto& ch : detail::request_access::pubsub_changes(req)) + commit_change(ch.type, req.payload().substr(ch.channel_offset, ch.channel_size)); +} + void pubsub_state::compose_subscribe_request(request& to) const { to.push_range("SUBSCRIBE", channels_); diff --git a/include/boost/redis/impl/request.ipp b/include/boost/redis/impl/request.ipp index 3ef8c65ff..99bbcb798 100644 --- a/include/boost/redis/impl/request.ipp +++ b/include/boost/redis/impl/request.ipp @@ -53,16 +53,14 @@ boost::redis::command_context boost::redis::request::start_command( // Determine the pubsub change type that this command is performing // TODO: this has overlap with has_response auto change_type = detail::pubsub_change_type::none; - if (cfg_.pubsub_state_restoration) { - if (cmd == "SUBSCRIBE") - change_type = detail::pubsub_change_type::subscribe; - if (cmd == "PSUBSCRIBE") - change_type = detail::pubsub_change_type::psubscribe; - if (cmd == "UNSUBSCRIBE") - change_type = detail::pubsub_change_type::unsubscribe; - if (cmd == "PUNSUBSCRIBE") - change_type = detail::pubsub_change_type::punsubscribe; - } + if (cmd == "SUBSCRIBE") + change_type = detail::pubsub_change_type::subscribe; + if (cmd == "PSUBSCRIBE") + change_type = detail::pubsub_change_type::psubscribe; + if (cmd == "UNSUBSCRIBE") + change_type = detail::pubsub_change_type::unsubscribe; + if (cmd == "PUNSUBSCRIBE") + change_type = detail::pubsub_change_type::punsubscribe; // Add the header resp3::add_header( diff --git a/include/boost/redis/impl/setup_request_utils.hpp b/include/boost/redis/impl/setup_request_utils.hpp index bed5e9748..556aa2dd4 100644 --- a/include/boost/redis/impl/setup_request_utils.hpp +++ b/include/boost/redis/impl/setup_request_utils.hpp @@ -32,7 +32,6 @@ inline void compose_setup_request(const config& cfg, const pubsub_state& pubsub_ request_access::set_priority(req, true); req.get_config().cancel_if_unresponded = true; req.get_config().cancel_on_connection_lost = true; - req.get_config().pubsub_state_restoration = false; if (cfg.use_setup) { // We should use the provided request as-is diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index ebf31b5bc..9ae3c5dbc 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -94,16 +94,13 @@ class request { * This field will be removed in subsequent releases. */ bool hello_with_priority = true; - - // TODO: document - bool pubsub_state_restoration = false; }; /** @brief Constructor * * @param cfg Configuration options. */ - explicit request(config cfg = config{false, false, true, true, false}) + explicit request(config cfg = config{false, false, true, true}) : cfg_{cfg} { } From 0c79084b9b93076ed926cdf4670c1e8fd3cf684c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 13:54:30 +0100 Subject: [PATCH 12/63] clean multiplexer up --- include/boost/redis/connection.hpp | 1 + include/boost/redis/detail/multiplexer.hpp | 7 ------- include/boost/redis/impl/multiplexer.ipp | 17 ++++------------- include/boost/redis/impl/run_fsm.ipp | 7 +++---- 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index b71532c8f..1fcf2d6b1 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -1062,6 +1062,7 @@ class basic_connection { 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() diff --git a/include/boost/redis/detail/multiplexer.hpp b/include/boost/redis/detail/multiplexer.hpp index 039152c3f..797c9493a 100644 --- a/include/boost/redis/detail/multiplexer.hpp +++ b/include/boost/redis/detail/multiplexer.hpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -207,11 +206,8 @@ class multiplexer { return usage_; } - void clear_pubsub_state() { pubsub_st_.clear(); } void set_config(config const& cfg); - const pubsub_state& get_pubsub_state() const { return pubsub_st_; } - private: void commit_usage(bool is_push, read_buffer::consume_result res); @@ -224,9 +220,6 @@ class multiplexer { [[nodiscard]] consume_result consume_impl(system::error_code& ec); - void complete_request(elem& elm); - - pubsub_state pubsub_st_; read_buffer read_buffer_; std::string write_buffer_; std::size_t write_offset_{}; // how many bytes of the write buffer have been written? diff --git a/include/boost/redis/impl/multiplexer.ipp b/include/boost/redis/impl/multiplexer.ipp index ff19f28ce..b6d118ce2 100644 --- a/include/boost/redis/impl/multiplexer.ipp +++ b/include/boost/redis/impl/multiplexer.ipp @@ -153,7 +153,7 @@ consume_result multiplexer::consume_impl(system::error_code& ec) reqs_.front()->commit_response(parser_.get_consumed()); if (reqs_.front()->get_remaining_responses() == 0) { // Done with this request. - complete_request(*reqs_.front()); + reqs_.front()->notify_done(); reqs_.pop_front(); } @@ -345,8 +345,9 @@ void multiplexer::release_push_requests() return !(ptr->is_written() && ptr->get_remaining_responses() == 0u); }); - for (auto it = point; it != reqs_.end(); ++it) - complete_request(**it); + std::for_each(point, std::end(reqs_), [](auto const& ptr) { + ptr->notify_done(); + }); reqs_.erase(point, std::end(reqs_)); } @@ -361,16 +362,6 @@ void multiplexer::set_config(config const& cfg) read_buffer_.set_config({cfg.read_buffer_append_size, cfg.max_read_size}); } -void multiplexer::complete_request(elem& elm) -{ - for (const auto& change : request_access::pubsub_changes(elm.get_request())) { - pubsub_st_.commit_change( - change.type, - elm.get_request().payload().substr(change.channel_offset, change.channel_size)); - } - elm.notify_done(); -} - auto make_elem(request const& req, any_adapter adapter) -> std::shared_ptr { return std::make_shared(req, std::move(adapter)); diff --git a/include/boost/redis/impl/run_fsm.ipp b/include/boost/redis/impl/run_fsm.ipp index a99fb030e..e8218bb76 100644 --- a/include/boost/redis/impl/run_fsm.ipp +++ b/include/boost/redis/impl/run_fsm.ipp @@ -101,9 +101,8 @@ run_action run_fsm::resume( return stored_ec_; } - // Setup the multiplexer - st.mpx.set_config(st.cfg); - st.mpx.clear_pubsub_state(); + // Clear any remainder from previous runs + st.pubsub_st.clear(); // Compose the PING request. This only depends on the config, so it can be done just once compose_ping_request(st.cfg, st.ping_req); @@ -160,7 +159,7 @@ run_action run_fsm::resume( // Initialization st.mpx.reset(); st.diagnostic.clear(); - compose_setup_request(st.cfg, st.mpx.get_pubsub_state(), st.setup_req); + compose_setup_request(st.cfg, st.pubsub_st, st.setup_req); // Add the setup request to the multiplexer if (st.setup_req.get_commands() != 0u) { From f611ced4dd7b0458488f929eb74910ba13b0ff35 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 14:03:52 +0100 Subject: [PATCH 13/63] Update the example --- example/cpp20_subscriber.cpp | 42 +++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/example/cpp20_subscriber.cpp b/example/cpp20_subscriber.cpp index 1f0dd86fe..dfb2c96ac 100644 --- a/example/cpp20_subscriber.cpp +++ b/example/cpp20_subscriber.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -46,38 +47,39 @@ using asio::signal_set; // Receives server pushes. auto receiver(std::shared_ptr conn) -> asio::awaitable { - request req; - req.push("SUBSCRIBE", "channel"); - generic_flat_response resp; conn->set_receive_response(resp); - // Loop while reconnection is enabled - while (conn->will_reconnect()) { - // Reconnect to the channels. - co_await conn->async_exec(req); + // Connect to the channels + request req; + req.push("SUBSCRIBE", "channel"); + co_await conn->async_exec(req); + + // Loop to read Redis push messages. + for (error_code ec;;) { + // Wait for pushes + co_await conn->async_receive2(asio::redirect_error(ec)); - // Loop to read Redis push messages. - for (error_code ec;;) { - // Wait for pushes - co_await conn->async_receive2(asio::redirect_error(ec)); - if (ec) - break; // Connection lost, break so we can reconnect to channels. + // Check for errors and cancellations + if (ec && (ec != asio::experimental::error::channel_cancelled || !conn->will_reconnect())) { + std::cerr << "Error during receive2: " << ec << std::endl; + break; + } - // The response must be consumed without suspending the - // coroutine i.e. without the use of async operations. - for (auto const& elem: resp.value().get_view()) - std::cout << elem.value << "\n"; + // The response must be consumed without suspending the + // coroutine i.e. without the use of async operations. + for (auto const& elem : resp.value().get_view()) + std::cout << elem.value << "\n"; - std::cout << std::endl; + std::cout << std::endl; - resp.value().clear(); - } + resp.value().clear(); } } auto co_main(config cfg) -> asio::awaitable { + cfg.restore_pubsub_state = true; auto ex = co_await asio::this_coro::executor; auto conn = std::make_shared(ex); asio::co_spawn(ex, receiver(conn), asio::detached); From 14af2c9ade0b22e13a5d399b64f96a58637508a3 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 14:09:48 +0100 Subject: [PATCH 14/63] Use the new extension point in json --- example/cpp20_json.cpp | 5 +++-- include/boost/redis/resp3/serialization.hpp | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/example/cpp20_json.cpp b/example/cpp20_json.cpp index 756d5275a..5250d4b7c 100644 --- a/example/cpp20_json.cpp +++ b/example/cpp20_json.cpp @@ -32,6 +32,7 @@ using boost::redis::ignore_t; using boost::redis::config; using boost::redis::connection; using boost::redis::resp3::node_view; +using boost::redis::command_context; // Struct that will be stored in Redis using json serialization. struct user { @@ -44,9 +45,9 @@ struct user { 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) +void boost_redis_to_bulk(command_context ctx, user const& u) { - resp3::boost_redis_to_bulk(to, boost::json::serialize(boost::json::value_from(u))); + ctx.add_argument(boost::json::serialize(boost::json::value_from(u))); } void boost_redis_from_bulk(user& u, node_view const& node, boost::system::error_code&) diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index fd6de3088..3253ba368 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -69,7 +69,7 @@ class command_context { namespace boost::redis::resp3 { -/** @brief Adds a bulk to the request. +/** @brief (Deprecated: use the new extension point, instead) Adds a bulk to the request. * @relates boost::redis::request * * This function is useful in serialization of your own data @@ -89,6 +89,7 @@ namespace boost::redis::resp3 { * * @param payload Storage on which data will be copied into. * @param data Data that will be serialized and stored in `payload`. + * TODO: mark this as deprecated */ void boost_redis_to_bulk(std::string& payload, std::string_view data); @@ -99,6 +100,12 @@ void boost_redis_to_bulk(std::string& payload, T n) boost::redis::resp3::boost_redis_to_bulk(payload, std::string_view{s}); } +// Use this new extension point, instead +inline void boost_redis_to_bulk(command_context ctx, std::string_view data) +{ + ctx.add_argument(data); +} + void add_header(std::string& payload, type t, std::size_t size); void add_blob(std::string& payload, std::string_view blob); void add_separator(std::string& payload); From 9ed38252883d75ab9bf7cf9ecf17e00f68b8ba4b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 19 Dec 2025 14:20:02 +0100 Subject: [PATCH 15/63] Restore add_bulk --- include/boost/redis/impl/request.ipp | 2 +- .../boost/redis/resp3/impl/serialization.ipp | 6 +++--- include/boost/redis/resp3/serialization.hpp | 21 +++++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/include/boost/redis/impl/request.ipp b/include/boost/redis/impl/request.ipp index 99bbcb798..5d0a39b59 100644 --- a/include/boost/redis/impl/request.ipp +++ b/include/boost/redis/impl/request.ipp @@ -69,7 +69,7 @@ boost::redis::command_context boost::redis::request::start_command( num_args + 1u); // the command string is also an array member // Serialize the command string - resp3::boost_redis_to_bulk(payload_, cmd); + resp3::add_bulk(payload_, cmd); // Compose the command context return detail::command_context_access::construct(change_type, pubsub_changes_, payload_); diff --git a/include/boost/redis/resp3/impl/serialization.ipp b/include/boost/redis/resp3/impl/serialization.ipp index 4cc535bae..189842c01 100644 --- a/include/boost/redis/resp3/impl/serialization.ipp +++ b/include/boost/redis/resp3/impl/serialization.ipp @@ -16,8 +16,8 @@ namespace boost::redis { void command_context::add_argument(std::string_view value) { - // TODO: this is duplicated from boost_redis_to_bulk - // Add the value to the payload + // Add the value to the payload. We can't use + // add_bulk directly because we need the value offset *payload_ += to_code(resp3::type::blob_string); *payload_ += std::to_string(value.size()); *payload_ += resp3::parser::sep; @@ -57,7 +57,7 @@ void command_context::parse_last_argument(std::size_t offset) namespace boost::redis::resp3 { -void boost_redis_to_bulk(std::string& payload, std::string_view data) +void add_bulk(std::string& payload, std::string_view data) { auto const str = std::to_string(data.size()); diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index 3253ba368..e7a5ae104 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -69,6 +69,11 @@ class command_context { namespace boost::redis::resp3 { +void add_header(std::string& payload, type t, std::size_t size); +void add_bulk(std::string& payload, std::string_view value); +void add_blob(std::string& payload, std::string_view blob); +void add_separator(std::string& payload); + /** @brief (Deprecated: use the new extension point, instead) Adds a bulk to the request. * @relates boost::redis::request * @@ -91,13 +96,15 @@ namespace boost::redis::resp3 { * @param data Data that will be serialized and stored in `payload`. * TODO: mark this as deprecated */ -void boost_redis_to_bulk(std::string& payload, std::string_view data); +inline void boost_redis_to_bulk(std::string& payload, std::string_view data) +{ + add_bulk(payload, data); +} template ::value>::type> void boost_redis_to_bulk(std::string& payload, T n) { - auto const s = std::to_string(n); - boost::redis::resp3::boost_redis_to_bulk(payload, std::string_view{s}); + add_bulk(payload, std::to_string(n)); } // Use this new extension point, instead @@ -106,10 +113,6 @@ inline void boost_redis_to_bulk(command_context ctx, std::string_view data) ctx.add_argument(data); } -void add_header(std::string& payload, type t, std::size_t size); -void add_blob(std::string& payload, std::string_view blob); -void add_separator(std::string& payload); - } // namespace boost::redis::resp3 namespace boost::redis::detail { @@ -124,7 +127,7 @@ struct command_context_access { } template - static void add_custom_argument(command_context ctx, const T& value) + static void add_v1_bulk(command_context ctx, const T& value) { using namespace boost::redis::resp3; auto offset = ctx.payload_->size(); @@ -150,7 +153,7 @@ void add_scalar_argument(command_context ctx, T const& value) } else if constexpr (std::is_integral_v) { ctx.add_argument(std::to_string(value)); } else if constexpr (detail::has_to_bulk_v1::value) { - detail::command_context_access::add_custom_argument(ctx, value); + detail::command_context_access::add_v1_bulk(ctx, value); } else { using namespace boost::redis::resp3; boost_redis_to_bulk(ctx, value); From 706a1cd85cd89fe87cb231925396cd0b124ef954 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 16:43:44 +0100 Subject: [PATCH 16/63] Revert customization point --- example/cpp20_json.cpp | 5 +- include/boost/redis/impl/request.ipp | 36 ---- include/boost/redis/request.hpp | 41 +++-- .../boost/redis/resp3/impl/serialization.ipp | 51 +----- include/boost/redis/resp3/serialization.hpp | 165 ++++-------------- 5 files changed, 66 insertions(+), 232 deletions(-) diff --git a/example/cpp20_json.cpp b/example/cpp20_json.cpp index 5250d4b7c..756d5275a 100644 --- a/example/cpp20_json.cpp +++ b/example/cpp20_json.cpp @@ -32,7 +32,6 @@ using boost::redis::ignore_t; using boost::redis::config; using boost::redis::connection; using boost::redis::resp3::node_view; -using boost::redis::command_context; // Struct that will be stored in Redis using json serialization. struct user { @@ -45,9 +44,9 @@ struct user { BOOST_DESCRIBE_STRUCT(user, (), (name, age, country)) // Boost.Redis customization points (example/json.hpp) -void boost_redis_to_bulk(command_context ctx, user const& u) +void boost_redis_to_bulk(std::string& to, user const& u) { - ctx.add_argument(boost::json::serialize(boost::json::value_from(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&) diff --git a/include/boost/redis/impl/request.ipp b/include/boost/redis/impl/request.ipp index 5d0a39b59..741c3b5d3 100644 --- a/include/boost/redis/impl/request.ipp +++ b/include/boost/redis/impl/request.ipp @@ -5,10 +5,7 @@ */ #include -#include -#include -#include #include namespace boost::redis::detail { @@ -38,39 +35,6 @@ request make_hello_request() void boost::redis::request::append(const request& other) { payload_ += other.payload_; - pubsub_changes_.insert( - pubsub_changes_.end(), - other.pubsub_changes_.begin(), - other.pubsub_changes_.end()); commands_ += other.commands_; expected_responses_ += other.expected_responses_; } - -boost::redis::command_context boost::redis::request::start_command( - std::string_view cmd, - std::size_t num_args) -{ - // Determine the pubsub change type that this command is performing - // TODO: this has overlap with has_response - auto change_type = detail::pubsub_change_type::none; - if (cmd == "SUBSCRIBE") - change_type = detail::pubsub_change_type::subscribe; - if (cmd == "PSUBSCRIBE") - change_type = detail::pubsub_change_type::psubscribe; - if (cmd == "UNSUBSCRIBE") - change_type = detail::pubsub_change_type::unsubscribe; - if (cmd == "PUNSUBSCRIBE") - change_type = detail::pubsub_change_type::punsubscribe; - - // Add the header - resp3::add_header( - payload_, - resp3::type::array, - num_args + 1u); // the command string is also an array member - - // Serialize the command string - resp3::add_bulk(payload_, cmd); - - // Compose the command context - return detail::command_context_access::construct(change_type, pubsub_changes_, payload_); -} diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index 9ae3c5dbc..9b83d44c9 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -10,9 +10,7 @@ #include #include -#include #include -#include #include #include @@ -22,10 +20,24 @@ namespace boost::redis { namespace detail { - auto has_response(std::string_view cmd) -> bool; struct request_access; +enum class pubsub_change_type +{ + none, + subscribe, + unsubscribe, + psubscribe, + punsubscribe, +}; + +struct pubsub_change { + pubsub_change_type type; + std::size_t channel_offset; + std::size_t channel_size; +}; + } // namespace detail /** @brief Represents a Redis request. @@ -176,8 +188,9 @@ class request { void push(std::string_view cmd, Ts const&... args) { auto constexpr pack_size = sizeof...(Ts); - auto ctx = start_command(cmd, pack_size); - detail::add_argument(ctx, std::tie(std::forward(args)...)); + resp3::add_header(payload_, resp3::type::array, 1 + pack_size); + resp3::add_bulk(payload_, cmd); + resp3::add_bulk(payload_, std::tie(std::forward(args)...)); check_cmd(cmd); } @@ -244,13 +257,14 @@ class request { if (begin == end) return; - auto constexpr size = detail::bulk_counter::size; + auto constexpr size = resp3::bulk_counter::size; auto const distance = std::distance(begin, end); - auto ctx = start_command(cmd, 1 + size * distance); - detail::add_argument(ctx, key); + resp3::add_header(payload_, resp3::type::array, 2 + size * distance); + resp3::add_bulk(payload_, cmd); + resp3::add_bulk(payload_, key); for (; begin != end; ++begin) - detail::add_argument(ctx, *begin); + resp3::add_bulk(payload_, *begin); check_cmd(cmd); } @@ -312,12 +326,13 @@ class request { if (begin == end) return; - auto constexpr size = detail::bulk_counter::size; + auto constexpr size = resp3::bulk_counter::size; auto const distance = std::distance(begin, end); - auto ctx = start_command(cmd, size * distance); + resp3::add_header(payload_, resp3::type::array, 1 + size * distance); + resp3::add_bulk(payload_, cmd); for (; begin != end; ++begin) - detail::add_argument(ctx, *begin); + resp3::add_bulk(payload_, *begin); check_cmd(cmd); } @@ -416,8 +431,6 @@ class request { void append(const request& other); private: - command_context start_command(std::string_view cmd, std::size_t num_args); - void check_cmd(std::string_view cmd) { ++commands_; diff --git a/include/boost/redis/resp3/impl/serialization.ipp b/include/boost/redis/resp3/impl/serialization.ipp index 189842c01..74e8c918b 100644 --- a/include/boost/redis/resp3/impl/serialization.ipp +++ b/include/boost/redis/resp3/impl/serialization.ipp @@ -6,58 +6,10 @@ #include #include -#include - -#include - -#include - -namespace boost::redis { - -void command_context::add_argument(std::string_view value) -{ - // Add the value to the payload. We can't use - // add_bulk directly because we need the value offset - *payload_ += to_code(resp3::type::blob_string); - *payload_ += std::to_string(value.size()); - *payload_ += resp3::parser::sep; - std::size_t offset = payload_->size(); - payload_->append(value.cbegin(), value.cend()); - *payload_ += resp3::parser::sep; - - // Record any pubsub change - if (cmd_change_ != detail::pubsub_change_type::none) - changes_->push_back({cmd_change_, offset, value.size()}); -} - -void command_context::parse_last_argument(std::size_t offset) -{ - // No need to analyze arguments if this command is not related to PubSub - if (cmd_change_ == detail::pubsub_change_type::none) - return; - - // Parse the serialized argument - resp3::parser p; - system::error_code ec; - auto node = p.consume(std::string_view(*payload_).substr(offset), ec); - if (ec || !node.has_value()) - return; // something went very wrong during serialization - - // Add the change - std::string_view node_value = node->value; - BOOST_ASSERT(node_value.data() >= payload_->data()); - changes_->push_back({ - cmd_change_, - static_cast(node_value.data() - payload_->data()), - node_value.size(), - }); -} - -} // namespace boost::redis namespace boost::redis::resp3 { -void add_bulk(std::string& payload, std::string_view data) +void boost_redis_to_bulk(std::string& payload, std::string_view data) { auto const str = std::to_string(data.size()); @@ -84,5 +36,4 @@ void add_blob(std::string& payload, std::string_view blob) } void add_separator(std::string& payload) { payload += parser::sep; } - } // namespace boost::redis::resp3 diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index e7a5ae104..28835925f 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -14,67 +14,11 @@ #include #include -#include #include -#include -#include -#include - -namespace boost::redis::detail { - -enum class pubsub_change_type -{ - none, - subscribe, - unsubscribe, - psubscribe, - punsubscribe, -}; - -struct pubsub_change { - pubsub_change_type type; - std::size_t channel_offset; - std::size_t channel_size; -}; - -struct command_context_access; - -} // namespace boost::redis::detail - -namespace boost::redis { - -class command_context { - detail::pubsub_change_type cmd_change_; - std::vector* changes_; - std::string* payload_; - - friend struct detail::command_context_access; - - command_context( - detail::pubsub_change_type t, - std::vector& changes, - std::string& payload) noexcept - : cmd_change_(t) - , changes_(&changes) - , payload_(&payload) - { } - - void parse_last_argument(std::size_t offset); - -public: - void add_argument(std::string_view value); -}; - -} // namespace boost::redis namespace boost::redis::resp3 { -void add_header(std::string& payload, type t, std::size_t size); -void add_bulk(std::string& payload, std::string_view value); -void add_blob(std::string& payload, std::string_view blob); -void add_separator(std::string& payload); - -/** @brief (Deprecated: use the new extension point, instead) Adds a bulk to the request. +/** @brief Adds a bulk to the request. * @relates boost::redis::request * * This function is useful in serialization of your own data @@ -94,93 +38,54 @@ void add_separator(std::string& payload); * * @param payload Storage on which data will be copied into. * @param data Data that will be serialized and stored in `payload`. - * TODO: mark this as deprecated */ -inline void boost_redis_to_bulk(std::string& payload, std::string_view data) -{ - add_bulk(payload, data); -} +void boost_redis_to_bulk(std::string& payload, std::string_view data); template ::value>::type> void boost_redis_to_bulk(std::string& payload, T n) { - add_bulk(payload, std::to_string(n)); + auto const s = std::to_string(n); + boost::redis::resp3::boost_redis_to_bulk(payload, std::string_view{s}); } -// Use this new extension point, instead -inline void boost_redis_to_bulk(command_context ctx, std::string_view data) -{ - ctx.add_argument(data); -} - -} // namespace boost::redis::resp3 - -namespace boost::redis::detail { - -struct command_context_access { - static command_context construct( - detail::pubsub_change_type t, - std::vector& changes, - std::string& payload) - { - return {t, changes, payload}; - } - - template - static void add_v1_bulk(command_context ctx, const T& value) +template +struct add_bulk_impl { + static void add(std::string& payload, T const& from) { using namespace boost::redis::resp3; - auto offset = ctx.payload_->size(); - boost_redis_to_bulk(*ctx.payload_, value); - ctx.parse_last_argument(offset); + boost_redis_to_bulk(payload, from); } }; -template -struct has_to_bulk_v1 : std::false_type { }; +template +struct add_bulk_impl> { + static void add(std::string& payload, std::tuple const& t) + { + auto f = [&](auto const&... vs) { + using namespace boost::redis::resp3; + (boost_redis_to_bulk(payload, vs), ...); + }; -template -struct has_to_bulk_v1< - T, - decltype(boost_redis_to_bulk(std::declval(), std::declval()))> -: std::true_type { }; + std::apply(f, t); + } +}; -template -void add_scalar_argument(command_context ctx, T const& value) -{ - if constexpr (std::is_convertible_v) { - ctx.add_argument(value); - } else if constexpr (std::is_integral_v) { - ctx.add_argument(std::to_string(value)); - } else if constexpr (detail::has_to_bulk_v1::value) { - detail::command_context_access::add_v1_bulk(ctx, value); - } else { +template +struct add_bulk_impl> { + static void add(std::string& payload, std::pair const& from) + { using namespace boost::redis::resp3; - boost_redis_to_bulk(ctx, value); + boost_redis_to_bulk(payload, from.first); + boost_redis_to_bulk(payload, from.second); } -} - -template -void add_argument(command_context ctx, T const& data) -{ - detail::add_scalar_argument(ctx, data); -} - -template -void add_argument(command_context ctx, std::tuple const& t) -{ - auto f = [&](auto const&... vs) { - (detail::add_scalar_argument(ctx, vs), ...); - }; +}; - std::apply(f, t); -} +void add_header(std::string& payload, type t, std::size_t size); -template -void add_argument(command_context ctx, std::pair const& from) +template +void add_bulk(std::string& payload, T const& data) { - detail::add_scalar_argument(ctx, from.first); - detail::add_scalar_argument(ctx, from.second); + add_bulk_impl::add(payload, data); } template @@ -201,10 +106,10 @@ struct bulk_counter> { static constexpr auto size = sizeof...(T); }; -} // namespace boost::redis::detail +void add_blob(std::string& payload, std::string_view blob); +void add_separator(std::string& payload); -// TODO: this belongs to tests -namespace boost::redis::resp3::detail { +namespace detail { template void deserialize(std::string_view const& data, Adapter adapter, system::error_code& ec) @@ -239,6 +144,8 @@ void deserialize(std::string_view const& data, Adapter adapter) BOOST_THROW_EXCEPTION(system::system_error{ec}); } -} // namespace boost::redis::resp3::detail +} // namespace detail + +} // namespace boost::redis::resp3 #endif // BOOST_REDIS_RESP3_SERIALIZATION_HPP From 3bb25f55da874e6960e8c81876aac2a8d6ff70c1 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 16:46:54 +0100 Subject: [PATCH 17/63] initial (pun)subscribe functions --- include/boost/redis/request.hpp | 267 ++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index 9b83d44c9..f3fb667c5 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -430,6 +430,273 @@ class request { */ void append(const request& other); + /** + * @brief Appends a SUBSCRIBE command to the end of the request. + * + * If `channels` contains `{"ch1", "ch2"}`, the resulting command + * is `SUBSCRIBE ch1 ch2`. + * + * SUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed channels is stored + * in the connection. Every time a reconnection happens, + * a suitable `SUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. + * + * PubSub store restoration only happens when using `subscribe`. + * Use @ref push or @ref push_range to disable it. + */ + void subscribe(std::initializer_list channels) + { + subscribe(channels.begin(), channels.end()); + } + + /** + * @brief Appends a SUBSCRIBE command to the end of the request. + * + * If `channels` contains `["ch1", "ch2"]`, the resulting command + * is `SUBSCRIBE ch1 ch2`. + * + * SUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed channels is stored + * in the connection. Every time a reconnection happens, + * a suitable `SUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + template + void subscribe(Range&& channels, decltype(std::cbegin(channels))* = nullptr) + { + subscribe(std::cbegin(channels), std::cend(channels)); + } + + /** + * @brief Appends a SUBSCRIBE command to the end of the request. + * + * [`channels_begin`, `channels_end`) should point to a valid + * range of elements convertible to `std::string_view`. + * If the range contains `["ch1", "ch2"]`, the resulting command + * is `SUBSCRIBE ch1 ch2`. + * + * SUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed channels is stored + * in the connection. Every time a reconnection happens, + * a suitable `SUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + template + void subscribe(ForwardIt channels_begin, ForwardIt channels_end) + { + push_range("SUBSCRIBE", channels_begin, channels_end); + } + + /** + * @brief Appends an UNSUBSCRIBE command to the end of the request. + * + * If `channels` contains `{"ch1", "ch2"}`, the resulting command + * is `UNSUBSCRIBE ch1 ch2`. + * + * UNSUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed channels tracked by the + * connection is updated. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + void unsubscribe(std::initializer_list channels) + { + unsubscribe(channels.begin(), channels.end()); + } + + /** + * @brief Appends an UNSUBSCRIBE command to the end of the request. + * + * If `channels` contains `["ch1", "ch2"]`, the resulting command + * is `UNSUBSCRIBE ch1 ch2`. + * + * UNSUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed channels tracked by the + * connection is updated. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + template + void unsubscribe(Range&& channels, decltype(std::cbegin(channels))* = nullptr) + { + unsubscribe(std::cbegin(channels), std::cend(channels)); + } + + /** + * @brief Appends an UNSUBSCRIBE command to the end of the request. + * + * [`channels_begin`, `channels_end`) should point to a valid + * range of elements convertible to `std::string_view`. + * If the range contains `["ch1", "ch2"]`, the resulting command + * is `UNSUBSCRIBE ch1 ch2`. + * + * UNSUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed channels tracked by the + * connection is updated. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + template + void unsubscribe(ForwardIt channels_begin, ForwardIt channels_end) + { + push_range("UNSUBSCRIBE", channels_begin, channels_end); + } + + /** + * @brief Appends a PSUBSCRIBE command to the end of the request. + * + * If `patterns` contains `{"news.*", "events.*"}`, the resulting command + * is `PSUBSCRIBE news.* events.*`. + * + * PSUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed patterns is stored + * in the connection. Every time a reconnection happens, + * a suitable `PSUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + void psubscribe(std::initializer_list patterns) + { + psubscribe(patterns.begin(), patterns.end()); + } + + /** + * @brief Appends a PSUBSCRIBE command to the end of the request. + * + * If `patterns` contains `["news.*", "events.*"]`, the resulting command + * is `PSUBSCRIBE news.* events.*`. + * + * PSUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed patterns is stored + * in the connection. Every time a reconnection happens, + * a suitable `PSUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + template + void psubscribe(Range&& patterns, decltype(std::cbegin(patterns))* = nullptr) + { + psubscribe(std::cbegin(patterns), std::cend(patterns)); + } + + /** + * @brief Appends a PSUBSCRIBE command to the end of the request. + * + * [`patterns_begin`, `patterns_end`) should point to a valid + * range of elements convertible to `std::string_view`. + * If the range contains `["news.*", "events.*"]`, the resulting command + * is `PSUBSCRIBE news.* events.*`. + * + * PSUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed patterns is stored + * in the connection. Every time a reconnection happens, + * a suitable `PSUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + template + void psubscribe(ForwardIt patterns_begin, ForwardIt patterns_end) + { + push_range("PSUBSCRIBE", patterns_begin, patterns_end); + } + + /** + * @brief Appends a PUNSUBSCRIBE command to the end of the request. + * + * If `patterns` contains `{"news.*", "events.*"}`, the resulting command + * is `PUNSUBSCRIBE news.* events.*`. + * + * PUNSUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed patterns tracked by the + * connection is updated. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + void punsubscribe(std::initializer_list patterns) + { + punsubscribe(patterns.begin(), patterns.end()); + } + + /** + * @brief Appends a PUNSUBSCRIBE command to the end of the request. + * + * If `patterns` contains `["news.*", "events.*"]`, the resulting command + * is `PUNSUBSCRIBE news.* events.*`. + * + * PUNSUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed patterns tracked by the + * connection is updated. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + template + void punsubscribe(Range&& patterns, decltype(std::cbegin(patterns))* = nullptr) + { + punsubscribe(std::cbegin(patterns), std::cend(patterns)); + } + + /** + * @brief Appends a PUNSUBSCRIBE command to the end of the request. + * + * [`patterns_begin`, `patterns_end`) should point to a valid + * range of elements convertible to `std::string_view`. + * If the range contains `["news.*", "events.*"]`, the resulting command + * is `PUNSUBSCRIBE news.* events.*`. + * + * PUNSUBSCRIBE commands created using this function are tracked + * to enable PubSub state restoration. After successfully executing + * the request, the list of subscribed patterns tracked by the + * connection is updated. + * + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. + * Use @ref push or @ref push_range to disable it. + */ + template + void punsubscribe(ForwardIt patterns_begin, ForwardIt patterns_end) + { + push_range("PUNSUBSCRIBE", patterns_begin, patterns_end); + } + private: void check_cmd(std::string_view cmd) { From c325e5873358c61b2fb8e97dd23c4ec37d8ed7a6 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 16:59:41 +0100 Subject: [PATCH 18/63] Proper implementation of pubsub methods --- include/boost/redis/impl/request.ipp | 13 +++++++++ include/boost/redis/request.hpp | 43 +++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/include/boost/redis/impl/request.ipp b/include/boost/redis/impl/request.ipp index 741c3b5d3..91afa1f88 100644 --- a/include/boost/redis/impl/request.ipp +++ b/include/boost/redis/impl/request.ipp @@ -5,7 +5,9 @@ */ #include +#include +#include #include namespace boost::redis::detail { @@ -38,3 +40,14 @@ void boost::redis::request::append(const request& other) commands_ += other.commands_; expected_responses_ += other.expected_responses_; } + +void boost::redis::request::add_pubsub_arg(detail::pubsub_change_type type, std::string_view value) +{ + // Add the argument + resp3::add_bulk(payload_, value); + + // Track the change. + // The final \r\n adds 2 bytes + std::size_t offset = payload_.size() - value.size() - 2u; + pubsub_changes_.push_back({type, offset, value.size()}); +} diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index f3fb667c5..d714d606b 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -496,7 +497,7 @@ class request { template void subscribe(ForwardIt channels_begin, ForwardIt channels_end) { - push_range("SUBSCRIBE", channels_begin, channels_end); + push_pubsub("SUBSCRIBE", detail::pubsub_change_type::subscribe, channels_begin, channels_end); } /** @@ -560,7 +561,11 @@ class request { template void unsubscribe(ForwardIt channels_begin, ForwardIt channels_end) { - push_range("UNSUBSCRIBE", channels_begin, channels_end); + push_pubsub( + "UNSUBSCRIBE", + detail::pubsub_change_type::unsubscribe, + channels_begin, + channels_end); } /** @@ -630,7 +635,11 @@ class request { template void psubscribe(ForwardIt patterns_begin, ForwardIt patterns_end) { - push_range("PSUBSCRIBE", patterns_begin, patterns_end); + push_pubsub( + "PSUBSCRIBE", + detail::pubsub_change_type::psubscribe, + patterns_begin, + patterns_end); } /** @@ -694,7 +703,11 @@ class request { template void punsubscribe(ForwardIt patterns_begin, ForwardIt patterns_end) { - push_range("PUNSUBSCRIBE", patterns_begin, patterns_end); + push_pubsub( + "PUNSUBSCRIBE", + detail::pubsub_change_type::punsubscribe, + patterns_begin, + patterns_end); } private: @@ -716,6 +729,28 @@ class request { bool has_hello_priority_ = false; std::vector pubsub_changes_{}; + void add_pubsub_arg(detail::pubsub_change_type type, std::string_view value); + + template + void push_pubsub( + std::string_view cmd, + detail::pubsub_change_type type, + ForwardIt channels_begin, + ForwardIt channels_end) + { + if (channels_begin == channels_end) + return; + + auto const distance = std::distance(channels_begin, channels_end); + resp3::add_header(payload_, resp3::type::array, 1 + distance); + resp3::add_bulk(payload_, cmd); + + for (; channels_begin != channels_end; ++channels_begin) + add_pubsub_arg(type, *channels_begin); + + ++commands_; // these commands don't have a response + } + friend struct detail::request_access; }; From 5ef0883af145625e8b49b6b4d91a96e872bfe1ce Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 17:00:23 +0100 Subject: [PATCH 19/63] Remove config::restore_pubsub_state --- example/cpp20_subscriber.cpp | 1 - include/boost/redis/config.hpp | 3 --- 2 files changed, 4 deletions(-) diff --git a/example/cpp20_subscriber.cpp b/example/cpp20_subscriber.cpp index dfb2c96ac..181174948 100644 --- a/example/cpp20_subscriber.cpp +++ b/example/cpp20_subscriber.cpp @@ -79,7 +79,6 @@ auto receiver(std::shared_ptr conn) -> asio::awaitable auto co_main(config cfg) -> asio::awaitable { - cfg.restore_pubsub_state = true; auto ex = co_await asio::this_coro::executor; auto conn = std::make_shared(ex); asio::co_spawn(ex, receiver(conn), asio::detached); diff --git a/include/boost/redis/config.hpp b/include/boost/redis/config.hpp index 75c0aea00..4e9806794 100644 --- a/include/boost/redis/config.hpp +++ b/include/boost/redis/config.hpp @@ -345,9 +345,6 @@ struct config { * @ref sentinel_config::addresses is not empty. */ sentinel_config sentinel{}; - - // (TODO: document properly) Do we want to re-subscribe to channels on reconnection? - bool restore_pubsub_state = false; }; } // namespace boost::redis From fa5a74fd87cced2edd2aa8069cdbed52d7224f60 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 17:01:06 +0100 Subject: [PATCH 20/63] cleanup --- include/boost/redis/impl/pubsub_state.ipp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/boost/redis/impl/pubsub_state.ipp b/include/boost/redis/impl/pubsub_state.ipp index 1b882f322..3d8a942e0 100644 --- a/include/boost/redis/impl/pubsub_state.ipp +++ b/include/boost/redis/impl/pubsub_state.ipp @@ -35,7 +35,7 @@ void pubsub_state::commit_change(pubsub_change_type type, std::string_view chann void pubsub_state::commit_changes(const request& req) { - for (const auto& ch : detail::request_access::pubsub_changes(req)) + for (const auto& ch : request_access::pubsub_changes(req)) commit_change(ch.type, req.payload().substr(ch.channel_offset, ch.channel_size)); } From a37b5c22af1782042c532118f29a1e91896adbda Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 17:02:41 +0100 Subject: [PATCH 21/63] Rename to subscription_tracker --- include/boost/redis/detail/connection_state.hpp | 4 ++-- .../{pubsub_state.hpp => subscription_tracker.hpp} | 8 ++++---- include/boost/redis/impl/pubsub_state.ipp | 10 +++++----- include/boost/redis/impl/setup_request_utils.hpp | 7 +++++-- 4 files changed, 16 insertions(+), 13 deletions(-) rename include/boost/redis/detail/{pubsub_state.hpp => subscription_tracker.hpp} (83%) diff --git a/include/boost/redis/detail/connection_state.hpp b/include/boost/redis/detail/connection_state.hpp index bc4e9128f..54fc9cf54 100644 --- a/include/boost/redis/detail/connection_state.hpp +++ b/include/boost/redis/detail/connection_state.hpp @@ -11,7 +11,7 @@ #include #include -#include +#include #include #include #include @@ -50,7 +50,7 @@ struct connection_state { std::string diagnostic{}; // Used by the setup request and Sentinel request setup_req{}; request ping_req{}; - pubsub_state pubsub_st{}; + subscription_tracker pubsub_st{}; // Sentinel stuff lazy_random_engine eng{}; diff --git a/include/boost/redis/detail/pubsub_state.hpp b/include/boost/redis/detail/subscription_tracker.hpp similarity index 83% rename from include/boost/redis/detail/pubsub_state.hpp rename to include/boost/redis/detail/subscription_tracker.hpp index f37723512..c24c43de7 100644 --- a/include/boost/redis/detail/pubsub_state.hpp +++ b/include/boost/redis/detail/subscription_tracker.hpp @@ -6,8 +6,8 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#ifndef BOOST_REDIS_PUBSUB_STATE_HPP -#define BOOST_REDIS_PUBSUB_STATE_HPP +#ifndef BOOST_REDIS_SUBSCRIPTION_TRACKER_HPP +#define BOOST_REDIS_SUBSCRIPTION_TRACKER_HPP #include #include @@ -21,12 +21,12 @@ namespace detail { enum class pubsub_change_type; -class pubsub_state { +class subscription_tracker { std::set channels_; std::set pchannels_; public: - pubsub_state() = default; + subscription_tracker() = default; void clear(); void commit_change(pubsub_change_type type, std::string_view channel); void commit_changes(const request& req); diff --git a/include/boost/redis/impl/pubsub_state.ipp b/include/boost/redis/impl/pubsub_state.ipp index 3d8a942e0..dc23c0032 100644 --- a/include/boost/redis/impl/pubsub_state.ipp +++ b/include/boost/redis/impl/pubsub_state.ipp @@ -6,7 +6,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include +#include #include #include @@ -15,13 +15,13 @@ namespace boost::redis::detail { -void pubsub_state::clear() +void subscription_tracker::clear() { channels_.clear(); pchannels_.clear(); } -void pubsub_state::commit_change(pubsub_change_type type, std::string_view channel) +void subscription_tracker::commit_change(pubsub_change_type type, std::string_view channel) { std::string owning_channel{channel}; switch (type) { @@ -33,13 +33,13 @@ void pubsub_state::commit_change(pubsub_change_type type, std::string_view chann } } -void pubsub_state::commit_changes(const request& req) +void subscription_tracker::commit_changes(const request& req) { for (const auto& ch : request_access::pubsub_changes(req)) commit_change(ch.type, req.payload().substr(ch.channel_offset, ch.channel_size)); } -void pubsub_state::compose_subscribe_request(request& to) const +void subscription_tracker::compose_subscribe_request(request& to) const { to.push_range("SUBSCRIBE", channels_); to.push_range("PBSUBSCRIBE", pchannels_); diff --git a/include/boost/redis/impl/setup_request_utils.hpp b/include/boost/redis/impl/setup_request_utils.hpp index 556aa2dd4..2fb0c6033 100644 --- a/include/boost/redis/impl/setup_request_utils.hpp +++ b/include/boost/redis/impl/setup_request_utils.hpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include #include // use_sentinel #include @@ -23,7 +23,10 @@ namespace boost::redis::detail { // Modifies config::setup to make a request suitable to be sent // to the server using async_exec -inline void compose_setup_request(const config& cfg, const pubsub_state& pubsub_st, request& req) +inline void compose_setup_request( + const config& cfg, + const subscription_tracker& pubsub_st, + request& req) { // Clear any previous contents req.clear(); From 7e3589564e8f07b43b15f6f21a0a56dd484edc88 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 17:03:43 +0100 Subject: [PATCH 22/63] build errors --- include/boost/redis/impl/exec_fsm.ipp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/boost/redis/impl/exec_fsm.ipp b/include/boost/redis/impl/exec_fsm.ipp index 663d2428c..2e7110a50 100644 --- a/include/boost/redis/impl/exec_fsm.ipp +++ b/include/boost/redis/impl/exec_fsm.ipp @@ -67,7 +67,7 @@ exec_action exec_fsm::resume( if (elem_->is_done()) { // If the request completed successfully and we were configured to do so, // record the changes applied to the pubsub state - if (st.cfg.restore_pubsub_state && !elem_->get_error()) + if (!elem_->get_error()) st.pubsub_st.commit_changes(elem_->get_request()); // Deallocate memory before finalizing From e12427d757a2b3cce1515ec850d3c7e4c0c70fca Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 17:14:56 +0100 Subject: [PATCH 23/63] Update example --- example/cpp20_subscriber.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/example/cpp20_subscriber.cpp b/example/cpp20_subscriber.cpp index 181174948..a879c3980 100644 --- a/example/cpp20_subscriber.cpp +++ b/example/cpp20_subscriber.cpp @@ -33,7 +33,7 @@ using asio::signal_set; * To test send messages with redis-cli * * $ redis-cli -3 - * 127.0.0.1:6379> PUBLISH channel some-message + * 127.0.0.1:6379> PUBLISH mychannel some-message * (integer) 3 * 127.0.0.1:6379> * @@ -50,11 +50,15 @@ auto receiver(std::shared_ptr conn) -> asio::awaitable generic_flat_response resp; conn->set_receive_response(resp); - // Connect to the channels + // Subscribe to the channel 'mychannel'. You can add any number of channels here. request req; - req.push("SUBSCRIBE", "channel"); + req.subscribe({"mychannel"}); co_await conn->async_exec(req); + // 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. + // Loop to read Redis push messages. for (error_code ec;;) { // Wait for pushes From c7aa9fe583405846483b70da7b378777dbba0274 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 17:17:45 +0100 Subject: [PATCH 24/63] Update discussion --- README.md | 47 +++++++++++-------- doc/modules/ROOT/pages/index.adoc | 47 +++++++++++-------- .../ROOT/pages/requests_responses.adoc | 2 +- 3 files changed, 55 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index af0b575b7..62a038963 100644 --- a/README.md +++ b/README.md @@ -93,32 +93,39 @@ The coroutine below shows how to use it ```cpp -auto -receiver(std::shared_ptr conn) -> net::awaitable +auto receiver(std::shared_ptr conn) -> asio::awaitable { - request req; - req.push("SUBSCRIBE", "channel"); - - flat_tree resp; + generic_flat_response resp; conn->set_receive_response(resp); - // Loop while reconnection is enabled - while (conn->will_reconnect()) { - - // Reconnect to channels. - co_await conn->async_exec(req); + // Subscribe to the channel 'mychannel'. You can add any number of channels here. + request req; + req.subscribe({"mychannel"}); + co_await conn->async_exec(req); + + // 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. + + // Loop to read Redis push messages. + for (error_code ec;;) { + // Wait for pushes + co_await conn->async_receive2(asio::redirect_error(ec)); + + // Check for errors and cancellations + if (ec && (ec != asio::experimental::error::channel_cancelled || !conn->will_reconnect())) { + std::cerr << "Error during receive2: " << ec << std::endl; + break; + } - // Loop reading Redis pushes. - for (error_code ec;;) { - co_await conn->async_receive2(resp, redirect_error(ec)); - if (ec) - break; // Connection lost, break so we can reconnect to channels. + // The response must be consumed without suspending the + // coroutine i.e. without the use of async operations. + for (auto const& elem : resp.value().get_view()) + std::cout << elem.value << "\n"; - // Use the response resp in some way and then clear it. - ... + std::cout << std::endl; - resp.clear(); - } + resp.value().clear(); } } ``` diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 36b83d493..b7194d1b9 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -104,32 +104,39 @@ The coroutine below shows how to use it [source,cpp] ---- -auto -receiver(std::shared_ptr conn) -> net::awaitable +auto receiver(std::shared_ptr conn) -> asio::awaitable { - request req; - req.push("SUBSCRIBE", "channel"); - - flat_tree resp; + generic_flat_response resp; conn->set_receive_response(resp); - // Loop while reconnection is enabled - while (conn->will_reconnect()) { - - // Reconnect to channels. - co_await conn->async_exec(req); + // Subscribe to the channel 'mychannel'. You can add any number of channels here. + request req; + req.subscribe({"mychannel"}); + co_await conn->async_exec(req); + + // 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. + + // Loop to read Redis push messages. + for (error_code ec;;) { + // Wait for pushes + co_await conn->async_receive2(asio::redirect_error(ec)); + + // Check for errors and cancellations + if (ec && (ec != asio::experimental::error::channel_cancelled || !conn->will_reconnect())) { + std::cerr << "Error during receive2: " << ec << std::endl; + break; + } - // Loop reading Redis pushes. - for (error_code ec;;) { - co_await conn->async_receive2(resp, redirect_error(ec)); - if (ec) - break; // Connection lost, break so we can reconnect to channels. + // The response must be consumed without suspending the + // coroutine i.e. without the use of async operations. + for (auto const& elem : resp.value().get_view()) + std::cout << elem.value << "\n"; - // Use the response here and then clear it. - ... + std::cout << std::endl; - resp.clear(); - } + resp.value().clear(); } } ---- diff --git a/doc/modules/ROOT/pages/requests_responses.adoc b/doc/modules/ROOT/pages/requests_responses.adoc index 7f40fed11..69728b63c 100644 --- a/doc/modules/ROOT/pages/requests_responses.adoc +++ b/doc/modules/ROOT/pages/requests_responses.adoc @@ -184,7 +184,7 @@ must **NOT** be included in the response tuple. For example, the following reque ---- request req; req.push("PING"); -req.push("SUBSCRIBE", "channel"); +req.subscribe({"channel"}); req.push("QUIT"); ---- From 5e51e511538adf14086314c9cf945814e8c2888b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 17:30:45 +0100 Subject: [PATCH 25/63] Update the chat room example --- example/cpp20_chat_room.cpp | 52 +++++++++++++++++++++++------------- example/cpp20_subscriber.cpp | 2 ++ 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/example/cpp20_chat_room.cpp b/example/cpp20_chat_room.cpp index 562225f56..4dd8180d7 100644 --- a/example/cpp20_chat_room.cpp +++ b/example/cpp20_chat_room.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -40,31 +41,44 @@ 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 { - request req; - req.push("SUBSCRIBE", "channel"); - + // Set the receive response, so pushes are stored in resp generic_flat_response resp; conn->set_receive_response(resp); - while (conn->will_reconnect()) { - // Subscribe to channels. - co_await conn->async_exec(req); + // 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); - // Loop reading Redis push messages. - for (error_code ec;;) { - co_await conn->async_receive2(redirect_error(ec)); - if (ec) - break; // Connection lost, break so we can reconnect to channels. + for (error_code ec;;) { + // Wait for pushes + co_await conn->async_receive2(asio::redirect_error(ec)); - for (auto const& elem: resp.value().get_view()) - std::cout << elem.value << "\n"; + // Check for errors and cancellations + if (ec && (ec != asio::experimental::error::channel_cancelled || !conn->will_reconnect())) { + std::cerr << "Error during receive2: " << ec << std::endl; + break; + } - std::cout << std::endl; + // The response must be consumed without suspending the + // coroutine i.e. without the use of async operations. + for (auto const& elem : resp.value().get_view()) + std::cout << elem.value << "\n"; - resp.value().clear(); - } + std::cout << std::endl; + + resp.value().clear(); } } @@ -81,6 +95,8 @@ auto publisher(std::shared_ptr in, std::shared_ptr awaitable { @@ -88,8 +104,8 @@ auto co_main(config cfg) -> awaitable auto conn = std::make_shared(ex); auto stream = std::make_shared(ex, ::dup(STDIN_FILENO)); - co_spawn(ex, receiver(conn), detached); - co_spawn(ex, publisher(stream, conn), detached); + 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}; diff --git a/example/cpp20_subscriber.cpp b/example/cpp20_subscriber.cpp index a879c3980..b3eb9cdd4 100644 --- a/example/cpp20_subscriber.cpp +++ b/example/cpp20_subscriber.cpp @@ -58,6 +58,8 @@ auto receiver(std::shared_ptr conn) -> asio::awaitable // 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 specialized request::subscribe() function (instead of request::push) + // to enable this behavior. // Loop to read Redis push messages. for (error_code ec;;) { From fc14d1a882ae2c4f27468b032d8190557a4fa674 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 17:32:54 +0100 Subject: [PATCH 26/63] rename file --- .../redis/impl/{pubsub_state.ipp => subscription_tracker.ipp} | 0 include/boost/redis/src.hpp | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename include/boost/redis/impl/{pubsub_state.ipp => subscription_tracker.ipp} (100%) diff --git a/include/boost/redis/impl/pubsub_state.ipp b/include/boost/redis/impl/subscription_tracker.ipp similarity index 100% rename from include/boost/redis/impl/pubsub_state.ipp rename to include/boost/redis/impl/subscription_tracker.ipp diff --git a/include/boost/redis/src.hpp b/include/boost/redis/src.hpp index 9297968ff..647e643de 100644 --- a/include/boost/redis/src.hpp +++ b/include/boost/redis/src.hpp @@ -13,13 +13,13 @@ #include #include #include -#include #include #include #include #include #include #include +#include #include #include #include From 4a222ce8d833f4950a3f593c2a987980d4a31b88 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 18:27:41 +0100 Subject: [PATCH 27/63] fix command typo --- include/boost/redis/impl/subscription_tracker.ipp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/boost/redis/impl/subscription_tracker.ipp b/include/boost/redis/impl/subscription_tracker.ipp index dc23c0032..9e1c064ca 100644 --- a/include/boost/redis/impl/subscription_tracker.ipp +++ b/include/boost/redis/impl/subscription_tracker.ipp @@ -42,7 +42,7 @@ void subscription_tracker::commit_changes(const request& req) void subscription_tracker::compose_subscribe_request(request& to) const { to.push_range("SUBSCRIBE", channels_); - to.push_range("PBSUBSCRIBE", pchannels_); + to.push_range("PSUBSCRIBE", pchannels_); } } // namespace boost::redis::detail From b08f2c8283cb9ee48cecc0fa1c7365804837f64e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Wed, 7 Jan 2026 18:28:07 +0100 Subject: [PATCH 28/63] Integ test --- test/test_conn_push2.cpp | 116 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/test/test_conn_push2.cpp b/test/test_conn_push2.cpp index 9e3d7f623..b2e80e0f2 100644 --- a/test/test_conn_push2.cpp +++ b/test/test_conn_push2.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -389,4 +390,119 @@ BOOST_AUTO_TEST_CASE(test_unsubscribe) BOOST_TEST(run_finished); } +class test_pubsub_state_restoration_ { + net::io_context ioc; + connection conn{ioc}; + request req; + response resp_str; + flat_tree resp_push; + bool exec_finished = false; + + 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(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(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(ec == error_code()); + BOOST_TEST(std::get<0>(resp_str).has_value()); + BOOST_TEST(find_client_info(std::get<0>(resp_str).value(), "sub") == "4"); + BOOST_TEST(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 + check_pubsub_restoration(); + }); + } + + void check_pubsub_restoration() + { + 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(ec == error_code()); + BOOST_TEST(std::get<0>(resp_str).has_value()); + BOOST_TEST(find_client_info(std::get<0>(resp_str).value(), "sub") == "3"); + BOOST_TEST(find_client_info(std::get<0>(resp_str).value(), "psub") == "4"); + exec_finished = true; + conn.cancel(); + + // TODO: verify the push response + }); + } + +public: + 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(ec == net::error::operation_aborted); + run_finished = true; + }); + + ioc.run_for(test_timeout); + + // Done + BOOST_TEST(exec_finished); + BOOST_TEST(run_finished); + } +}; + +BOOST_AUTO_TEST_CASE(test_pubsub_state_restoration) { test_pubsub_state_restoration_().run(); } + } // namespace From 89c516a1c11e4b8bccd90e81dec7ca81712a3826 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 16:24:38 +0100 Subject: [PATCH 29/63] verify subscriptions --- test/test_conn_push2.cpp | 50 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/test/test_conn_push2.cpp b/test/test_conn_push2.cpp index b2e80e0f2..9cf638e92 100644 --- a/test/test_conn_push2.cpp +++ b/test/test_conn_push2.cpp @@ -13,7 +13,9 @@ #include #include +#include #include +#include #define BOOST_TEST_MODULE conn_push #include @@ -36,6 +38,8 @@ using boost::redis::ignore; using boost::redis::ignore_t; using boost::system::error_code; using boost::redis::logger; +using boost::redis::resp3::node_view; +using boost::redis::resp3::type; using namespace std::chrono_literals; namespace { @@ -398,6 +402,38 @@ class test_pubsub_state_restoration_ { 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.get_view().begin(); it != resp_push.get_view().end();) { + // The root element should be a push + BOOST_TEST_REQUIRE(it->data_type == type::push); + BOOST_TEST_REQUIRE(it->aggregate_size >= 2u); + BOOST_TEST_REQUIRE((++it != resp_push.get_view().end())); + + // The next element should be the message type + std::string_view msg_type = it->value; + BOOST_TEST_REQUIRE((++it != resp_push.get_view().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.get_view().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(seen_channels == expected_channels, boost::test_tools::per_element()); + BOOST_TEST(seen_patterns == expected_patterns, boost::test_tools::per_element()); + } + void sub1() { // Subscribe to some channels and patterns @@ -441,6 +477,8 @@ class test_pubsub_state_restoration_ { conn.async_exec(req, resp_str, [this](error_code ec, std::size_t) { BOOST_TEST(ec == error_code()); + + // We are subscribed to 4 channels and 5 patterns BOOST_TEST(std::get<0>(resp_str).has_value()); BOOST_TEST(find_client_info(std::get<0>(resp_str).value(), "sub") == "4"); BOOST_TEST(find_client_info(std::get<0>(resp_str).value(), "psub") == "5"); @@ -458,11 +496,11 @@ class test_pubsub_state_restoration_ { conn.async_exec(req, ignore, [this](error_code, std::size_t) { // we don't know if this request will complete successfully or not - check_pubsub_restoration(); + client_info(); }); } - void check_pubsub_restoration() + void client_info() { req.clear(); req.push("CLIENT", "INFO"); @@ -470,13 +508,17 @@ class test_pubsub_state_restoration_ { conn.async_exec(req, resp_str, [this](error_code ec, std::size_t) { BOOST_TEST(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(find_client_info(std::get<0>(resp_str).value(), "sub") == "3"); BOOST_TEST(find_client_info(std::get<0>(resp_str).value(), "psub") == "4"); + + // We have received pushes confirming it + check_subscriptions(); + exec_finished = true; conn.cancel(); - - // TODO: verify the push response }); } From 941d6332023982f863691a1283a4ae5730aaed3d Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 16:28:43 +0100 Subject: [PATCH 30/63] Use restoring pubsub in some tests --- test/test_conn_echo_stress.cpp | 20 +++++++------------- test/test_conn_sentinel.cpp | 2 +- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/test/test_conn_echo_stress.cpp b/test/test_conn_echo_stress.cpp index 48157b0be..e1455b2aa 100644 --- a/test/test_conn_echo_stress.cpp +++ b/test/test_conn_echo_stress.cpp @@ -55,11 +55,7 @@ std::ostream& operator<<(std::ostream& os, usage const& u) namespace { -auto -receiver( - connection& conn, - flat_tree& resp, - std::size_t expected) -> net::awaitable +auto receiver(connection& conn, flat_tree& resp, std::size_t expected) -> net::awaitable { std::size_t push_counter = 0; while (push_counter != expected) { @@ -135,7 +131,7 @@ BOOST_AUTO_TEST_CASE(echo_stress) // Subscribe, then launch the coroutines request req; - req.push("SUBSCRIBE", "channel"); + req.subscribe({"channel"}); conn.async_exec(req, ignore, [&](error_code ec, std::size_t) { subscribe_finished = true; BOOST_TEST(ec == error_code()); @@ -150,13 +146,11 @@ BOOST_AUTO_TEST_CASE(echo_stress) BOOST_TEST(subscribe_finished); // Print statistics - std::cout - << "-------------------\n" - << "Usage data: \n" - << conn.get_usage() << "\n" - << "-------------------\n" - << "Reallocations: " << resp.get_reallocs() - << std::endl; + std::cout << "-------------------\n" + << "Usage data: \n" + << conn.get_usage() << "\n" + << "-------------------\n" + << "Reallocations: " << resp.get_reallocs() << std::endl; } } // namespace diff --git a/test/test_conn_sentinel.cpp b/test/test_conn_sentinel.cpp index 7caec74ec..f18c1de08 100644 --- a/test/test_conn_sentinel.cpp +++ b/test/test_conn_sentinel.cpp @@ -96,7 +96,7 @@ void test_receive() // Subscribe to a channel. This produces a push message on itself request req; - req.push("SUBSCRIBE", "sentinel_channel"); + req.subscribe({"sentinel_channel"}); bool exec_finished = false, receive_finished = false, run_finished = false; From 8e5308801e917450e0a38dd951fa6ba0d2359d8d Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 16:31:29 +0100 Subject: [PATCH 31/63] update request example --- include/boost/redis/request.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index d714d606b..0f8b1e89a 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -276,17 +276,17 @@ class request { * of arguments and don't have a key. For example: * * @code - * std::set channels - * { "channel1" , "channel2" , "channel3" }; + * std::set keys + * { "key1" , "key2" , "key3" }; * * request req; - * req.push("SUBSCRIBE", channels.cbegin(), channels.cend()); + * req.push("MGET", keys.begin(), keys.end()); * @endcode * * This will generate the following command: * * @code - * SUBSCRIBE channel1 channel2 channel3 + * MGET key1 key2 key3 * @endcode * * *If the passed range is empty, no command is added* and this From 6fc65f5f69e51a91abc0a47353f98ff4c31b3f74 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 17:27:29 +0100 Subject: [PATCH 32/63] remove change type none --- include/boost/redis/request.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index 0f8b1e89a..d73ec4cb4 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -26,7 +26,6 @@ struct request_access; enum class pubsub_change_type { - none, subscribe, unsubscribe, psubscribe, From c8ee2c1262e070187764964663f3c807bb31f247 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 17:37:32 +0100 Subject: [PATCH 33/63] first request test --- test/test_request.cpp | 71 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/test/test_request.cpp b/test/test_request.cpp index 63b59ca3c..82adfa6dd 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -6,16 +6,68 @@ #include +#include #include +#include #include +#include #include #include +#include -using boost::redis::request; +using namespace boost::redis; +using detail::pubsub_change; +using detail::pubsub_change_type; namespace { +// --- Utilities to check subscription tracking --- +const char* to_string(pubsub_change_type type) +{ + switch (type) { + case pubsub_change_type::subscribe: return "subscribe"; + case pubsub_change_type::unsubscribe: return "unsubscribe"; + case pubsub_change_type::psubscribe: return "psubscribe"; + case pubsub_change_type::punsubscribe: return "punsubscribe"; + default: return ""; + } +} + +// Like pubsub_change, but using a string instead of an offset +struct pubsub_change_str { + pubsub_change_type type; + std::string_view value; + + friend bool operator==(const pubsub_change_str& lhs, const pubsub_change_str& rhs) + { + return lhs.type == rhs.type && lhs.value == rhs.value; + } + + friend std::ostream& operator<<(std::ostream& os, const pubsub_change_str& value) + { + return os << "{ " << to_string(value.type) << ", " << value.value << " }"; + } +}; + +void check_pubsub_changes( + const request& req, + boost::span expected, + boost::source_location loc = BOOST_CURRENT_LOCATION) +{ + // Convert from offsets to strings + std::vector actual; + for (const auto& change : detail::request_access::pubsub_changes(req)) { + actual.push_back( + {change.type, req.payload().substr(change.channel_offset, change.channel_size)}); + } + + // Check + if (!BOOST_TEST_ALL_EQ(actual.begin(), actual.end(), expected.begin(), expected.end())) + std::cerr << "Called from " << loc << std::endl; +} + +// --- Generic functions to add commands --- void test_push_no_args() { request req1; @@ -38,6 +90,19 @@ void test_push_multiple_args() BOOST_TEST_EQ(req.payload(), res); } +// Subscription commands added with push are not tracked +void test_push_pubsub() +{ + request req; + req.push("SUBSCRIBE", "ch1"); + req.push("UNSUBSCRIBE", "ch2"); + req.push("PSUBSCRIBE", "ch3*"); + req.push("PUNSUBSCRIBE", "ch4*"); + + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + check_pubsub_changes(req, {}); +} + void test_push_range() { std::map in{ @@ -58,7 +123,7 @@ void test_push_range() BOOST_TEST_EQ(req2.payload(), expected); } -// Append +// --- append --- void test_append() { request req1; @@ -176,6 +241,8 @@ int main() test_push_no_args(); test_push_int(); test_push_multiple_args(); + test_push_pubsub(); + test_push_range(); test_append(); From c5f31d148d1e18b94d4e6e0f8996ddae3cd6a17e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 17:41:50 +0100 Subject: [PATCH 34/63] Implement request::append --- include/boost/redis/impl/request.ipp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/include/boost/redis/impl/request.ipp b/include/boost/redis/impl/request.ipp index 91afa1f88..72653897b 100644 --- a/include/boost/redis/impl/request.ipp +++ b/include/boost/redis/impl/request.ipp @@ -36,9 +36,23 @@ request make_hello_request() void boost::redis::request::append(const request& other) { + // Remember the old payload size, to update offsets + std::size_t old_offset = payload_.size(); + + // Add the payload payload_ += other.payload_; commands_ += other.commands_; expected_responses_ += other.expected_responses_; + + // Add the pubsub changes. Offsets need to be updated + pubsub_changes_.reserve(pubsub_changes_.size() + other.pubsub_changes_.size()); + for (const auto& change : other.pubsub_changes_) { + pubsub_changes_.push_back({ + change.type, + change.channel_offset + old_offset, + change.channel_size, + }); + } } void boost::redis::request::add_pubsub_arg(detail::pubsub_change_type type, std::string_view value) From 6ea8edb02bd1879e0a66106325634665cd031119 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 17:48:32 +0100 Subject: [PATCH 35/63] push_range oubsub --- test/test_request.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/test_request.cpp b/test/test_request.cpp index 82adfa6dd..407ff82dc 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -99,10 +99,17 @@ void test_push_pubsub() req.push("PSUBSCRIBE", "ch3*"); req.push("PUNSUBSCRIBE", "ch4*"); + char const* res = + "*2\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n" + "*2\r\n$11\r\nUNSUBSCRIBE\r\n$3\r\nch2\r\n" + "*2\r\n$10\r\nPSUBSCRIBE\r\n$4\r\nch3*\r\n" + "*2\r\n$12\r\nPUNSUBSCRIBE\r\n$4\r\nch4*\r\n"; + BOOST_TEST_EQ(req.payload(), res); BOOST_TEST_EQ(req.get_expected_responses(), 0u); check_pubsub_changes(req, {}); } +// --- push_range --- void test_push_range() { std::map in{ @@ -123,6 +130,27 @@ void test_push_range() BOOST_TEST_EQ(req2.payload(), expected); } +// Subscription commands added with push_range are not tracked +void test_push_range_pubsub() +{ + const std::vector channels1{"ch1", "ch2"}, channels2{"ch3"}, patterns1{"ch3*"}, + patterns2{"ch4*"}; + request req; + req.push_range("SUBSCRIBE", channels1); + req.push_range("UNSUBSCRIBE", channels2); + req.push_range("PSUBSCRIBE", patterns1); + req.push_range("PUNSUBSCRIBE", patterns2); + + char const* res = + "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n" + "*2\r\n$11\r\nUNSUBSCRIBE\r\n$3\r\nch3\r\n" + "*2\r\n$10\r\nPSUBSCRIBE\r\n$4\r\nch3*\r\n" + "*2\r\n$12\r\nPUNSUBSCRIBE\r\n$4\r\nch4*\r\n"; + BOOST_TEST_EQ(req.payload(), res); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + check_pubsub_changes(req, {}); +} + // --- append --- void test_append() { @@ -244,6 +272,7 @@ int main() test_push_pubsub(); test_push_range(); + test_push_range_pubsub(); test_append(); test_append_no_response(); From e621d4acaf9149dfb80fd008fa1cc7d6b62e3638 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 17:49:58 +0100 Subject: [PATCH 36/63] append existing tests --- test/test_request.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_request.cpp b/test/test_request.cpp index 407ff82dc..efa208395 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -170,6 +170,7 @@ void test_append() BOOST_TEST_EQ(req1.payload(), expected); BOOST_TEST_EQ(req1.get_commands(), 3u); BOOST_TEST_EQ(req1.get_expected_responses(), 3u); + check_pubsub_changes(req1, {}); } // Commands without responses are handled correctly @@ -191,6 +192,7 @@ void test_append_no_response() BOOST_TEST_EQ(req1.payload(), expected); BOOST_TEST_EQ(req1.get_commands(), 3u); BOOST_TEST_EQ(req1.get_expected_responses(), 2u); + check_pubsub_changes(req1, {}); } // Flags are not modified by append @@ -233,6 +235,7 @@ void test_append_target_empty() BOOST_TEST_EQ(req1.payload(), expected); BOOST_TEST_EQ(req1.get_commands(), 1u); BOOST_TEST_EQ(req1.get_expected_responses(), 1u); + check_pubsub_changes(req1, {}); } void test_append_source_empty() @@ -248,6 +251,7 @@ void test_append_source_empty() BOOST_TEST_EQ(req1.payload(), expected); BOOST_TEST_EQ(req1.get_commands(), 1u); BOOST_TEST_EQ(req1.get_expected_responses(), 1u); + check_pubsub_changes(req1, {}); } void test_append_both_empty() @@ -260,6 +264,7 @@ void test_append_both_empty() BOOST_TEST_EQ(req1.payload(), ""); BOOST_TEST_EQ(req1.get_commands(), 0u); BOOST_TEST_EQ(req1.get_expected_responses(), 0u); + check_pubsub_changes(req1, {}); } } // namespace From 83356c8a6aa13dbd3764c714082f845d789b69cf Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 17:57:59 +0100 Subject: [PATCH 37/63] append tests --- test/test_request.cpp | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/test_request.cpp b/test/test_request.cpp index efa208395..2cb59e369 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -267,6 +268,50 @@ void test_append_both_empty() check_pubsub_changes(req1, {}); } +// Append correctly handles requests with pubsub changes +void test_append_pubsub() +{ + request req1; + req1.subscribe({"ch1"}); + + auto req2 = std::make_unique(); + req2->unsubscribe({"ch2"}); + req2->psubscribe({"really_very_long_pattern_name*"}); + + req1.append(*req2); + req2.reset(); // make sure we don't leave dangling pointers + + constexpr std::string_view expected = + "*2\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n" + "*2\r\n$11\r\nUNSUBSCRIBE\r\n$3\r\nch2\r\n" + "*2\r\n$10\r\nPSUBSCRIBE\r\n$30\r\nreally_very_long_pattern_name*\r\n"; + BOOST_TEST_EQ(req1.payload(), expected); + const pubsub_change_str expected_changes[] = { + {pubsub_change_type::subscribe, "ch1" }, + {pubsub_change_type::unsubscribe, "ch2" }, + {pubsub_change_type::psubscribe, "really_very_long_pattern_name*"}, + }; + check_pubsub_changes(req1, expected_changes); +} + +// If the target is empty and the source has pubsub changes, that's OK +void test_append_pubsub_target_empty() +{ + request req1; + + request req2; + req2.punsubscribe({"ch2"}); + + req1.append(req2); + + constexpr std::string_view expected = "*2\r\n$12\r\nPUNSUBSCRIBE\r\n$3\r\nch2\r\n"; + BOOST_TEST_EQ(req1.payload(), expected); + const pubsub_change_str expected_changes[] = { + {pubsub_change_type::punsubscribe, "ch2"}, + }; + check_pubsub_changes(req1, expected_changes); +} + } // namespace int main() @@ -285,6 +330,8 @@ int main() test_append_target_empty(); test_append_source_empty(); test_append_both_empty(); + test_append_pubsub(); + test_append_pubsub_target_empty(); return boost::report_errors(); } From ab35372d25a2c04f5590dc1f3801bb3ce51e7709 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:12:16 +0100 Subject: [PATCH 38/63] subscribe tests --- test/test_request.cpp | 97 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/test/test_request.cpp b/test/test_request.cpp index 2cb59e369..f457a86a1 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include #include @@ -152,6 +154,95 @@ void test_push_range_pubsub() check_pubsub_changes(req, {}); } +// --- Functions that track subscriptions --- +void test_subscribe_iterators() +{ + const std::forward_list channels{"ch1", "ch2"}; + request req; + + req.subscribe(channels.begin(), channels.end()); + + constexpr std::string_view expected = "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; + BOOST_TEST_EQ(req.payload(), expected); + BOOST_TEST_EQ(req.get_commands(), 1u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + constexpr pubsub_change_str expected_changes[] = { + {pubsub_change_type::subscribe, "ch1"}, + {pubsub_change_type::subscribe, "ch2"}, + }; + check_pubsub_changes(req, expected_changes); +} + +// Like push_range, if the range is empty, this is a no-op +void test_subscribe_iterators_empty() +{ + const std::forward_list channels; + request req; + + req.subscribe(channels.begin(), channels.end()); + + BOOST_TEST_EQ(req.payload(), ""); + BOOST_TEST_EQ(req.get_commands(), 0u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + check_pubsub_changes(req, {}); +} + +// Iterators whose value_type is convertible to std::string_view work +void test_subscribe_iterators_convertible_string_view() +{ + const std::vector channels{"ch1", "ch2"}; + request req; + + req.subscribe(channels.begin(), channels.end()); + + constexpr std::string_view expected = "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; + BOOST_TEST_EQ(req.payload(), expected); + BOOST_TEST_EQ(req.get_commands(), 1u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + constexpr pubsub_change_str expected_changes[] = { + {pubsub_change_type::subscribe, "ch1"}, + {pubsub_change_type::subscribe, "ch2"}, + }; + check_pubsub_changes(req, expected_changes); +} + +// The range overload just dispatches to the iterator one +void test_subscribe_range() +{ + const std::vector channels{"ch1", "ch2"}; + request req; + + req.subscribe(channels); + + constexpr std::string_view expected = "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; + BOOST_TEST_EQ(req.payload(), expected); + BOOST_TEST_EQ(req.get_commands(), 1u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + constexpr pubsub_change_str expected_changes[] = { + {pubsub_change_type::subscribe, "ch1"}, + {pubsub_change_type::subscribe, "ch2"}, + }; + check_pubsub_changes(req, expected_changes); +} + +// The initializer_list overload just dispatches to the iterator one +void test_subscribe_initializer_list() +{ + request req; + + req.subscribe({"ch1", "ch2"}); + + constexpr std::string_view expected = "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; + BOOST_TEST_EQ(req.payload(), expected); + BOOST_TEST_EQ(req.get_commands(), 1u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + constexpr pubsub_change_str expected_changes[] = { + {pubsub_change_type::subscribe, "ch1"}, + {pubsub_change_type::subscribe, "ch2"}, + }; + check_pubsub_changes(req, expected_changes); +} + // --- append --- void test_append() { @@ -324,6 +415,12 @@ int main() test_push_range(); test_push_range_pubsub(); + test_subscribe_iterators(); + test_subscribe_iterators_empty(); + test_subscribe_iterators_convertible_string_view(); + test_subscribe_range(); + test_subscribe_initializer_list(); + test_append(); test_append_no_response(); test_append_flags(); From 85bf939c382468ac68c88fbf4e13b69b1a261a14 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:16:26 +0100 Subject: [PATCH 39/63] reduce duplication --- test/test_request.cpp | 82 +++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/test/test_request.cpp b/test/test_request.cpp index f457a86a1..2674adcb9 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -154,23 +155,38 @@ void test_push_range_pubsub() check_pubsub_changes(req, {}); } -// --- Functions that track subscriptions --- +// --- subscribe --- +struct subscribe_fixture { + request req; + + void check_two_channels(boost::source_location loc = BOOST_CURRENT_LOCATION) + { + constexpr std::string_view expected = "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; + if (!BOOST_TEST_EQ(req.payload(), expected)) + std::cerr << "Called from " << loc << std::endl; + + if (!BOOST_TEST_EQ(req.get_commands(), 1u)) + std::cerr << "Called from " << loc << std::endl; + + if (!BOOST_TEST_EQ(req.get_expected_responses(), 0u)) + std::cerr << "Called from " << loc << std::endl; + + constexpr pubsub_change_str expected_changes[] = { + {pubsub_change_type::subscribe, "ch1"}, + {pubsub_change_type::subscribe, "ch2"}, + }; + check_pubsub_changes(req, expected_changes, loc); + } +}; + void test_subscribe_iterators() { + subscribe_fixture fix; const std::forward_list channels{"ch1", "ch2"}; - request req; - req.subscribe(channels.begin(), channels.end()); + fix.req.subscribe(channels.begin(), channels.end()); - constexpr std::string_view expected = "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; - BOOST_TEST_EQ(req.payload(), expected); - BOOST_TEST_EQ(req.get_commands(), 1u); - BOOST_TEST_EQ(req.get_expected_responses(), 0u); - constexpr pubsub_change_str expected_changes[] = { - {pubsub_change_type::subscribe, "ch1"}, - {pubsub_change_type::subscribe, "ch2"}, - }; - check_pubsub_changes(req, expected_changes); + fix.check_two_channels(); } // Like push_range, if the range is empty, this is a no-op @@ -190,57 +206,33 @@ void test_subscribe_iterators_empty() // Iterators whose value_type is convertible to std::string_view work void test_subscribe_iterators_convertible_string_view() { + subscribe_fixture fix; const std::vector channels{"ch1", "ch2"}; - request req; - req.subscribe(channels.begin(), channels.end()); + fix.req.subscribe(channels.begin(), channels.end()); - constexpr std::string_view expected = "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; - BOOST_TEST_EQ(req.payload(), expected); - BOOST_TEST_EQ(req.get_commands(), 1u); - BOOST_TEST_EQ(req.get_expected_responses(), 0u); - constexpr pubsub_change_str expected_changes[] = { - {pubsub_change_type::subscribe, "ch1"}, - {pubsub_change_type::subscribe, "ch2"}, - }; - check_pubsub_changes(req, expected_changes); + fix.check_two_channels(); } // The range overload just dispatches to the iterator one void test_subscribe_range() { + subscribe_fixture fix; const std::vector channels{"ch1", "ch2"}; - request req; - req.subscribe(channels); + fix.req.subscribe(channels); - constexpr std::string_view expected = "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; - BOOST_TEST_EQ(req.payload(), expected); - BOOST_TEST_EQ(req.get_commands(), 1u); - BOOST_TEST_EQ(req.get_expected_responses(), 0u); - constexpr pubsub_change_str expected_changes[] = { - {pubsub_change_type::subscribe, "ch1"}, - {pubsub_change_type::subscribe, "ch2"}, - }; - check_pubsub_changes(req, expected_changes); + fix.check_two_channels(); } // The initializer_list overload just dispatches to the iterator one void test_subscribe_initializer_list() { - request req; + subscribe_fixture fix; - req.subscribe({"ch1", "ch2"}); + fix.req.subscribe({"ch1", "ch2"}); - constexpr std::string_view expected = "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; - BOOST_TEST_EQ(req.payload(), expected); - BOOST_TEST_EQ(req.get_commands(), 1u); - BOOST_TEST_EQ(req.get_expected_responses(), 0u); - constexpr pubsub_change_str expected_changes[] = { - {pubsub_change_type::subscribe, "ch1"}, - {pubsub_change_type::subscribe, "ch2"}, - }; - check_pubsub_changes(req, expected_changes); + fix.check_two_channels(); } // --- append --- From 8ef4dc82a51f05ec9aafab31598319e27dd0340e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:19:30 +0100 Subject: [PATCH 40/63] unsubscribe tests --- test/test_request.cpp | 87 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/test/test_request.cpp b/test/test_request.cpp index 2674adcb9..859647396 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -235,6 +235,87 @@ void test_subscribe_initializer_list() fix.check_two_channels(); } +// --- unsubscribe --- +struct unsubscribe_fixture { + request req; + + void check_two_channels(boost::source_location loc = BOOST_CURRENT_LOCATION) + { + constexpr std::string_view + expected = "*3\r\n$11\r\nUNSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; + if (!BOOST_TEST_EQ(req.payload(), expected)) + std::cerr << "Called from " << loc << std::endl; + + if (!BOOST_TEST_EQ(req.get_commands(), 1u)) + std::cerr << "Called from " << loc << std::endl; + + if (!BOOST_TEST_EQ(req.get_expected_responses(), 0u)) + std::cerr << "Called from " << loc << std::endl; + + constexpr pubsub_change_str expected_changes[] = { + {pubsub_change_type::unsubscribe, "ch1"}, + {pubsub_change_type::unsubscribe, "ch2"}, + }; + check_pubsub_changes(req, expected_changes, loc); + } +}; + +void test_unsubscribe_iterators() +{ + unsubscribe_fixture fix; + const std::forward_list channels{"ch1", "ch2"}; + + fix.req.unsubscribe(channels.begin(), channels.end()); + + fix.check_two_channels(); +} + +// Like push_range, if the range is empty, this is a no-op +void test_unsubscribe_iterators_empty() +{ + const std::forward_list channels; + request req; + + req.unsubscribe(channels.begin(), channels.end()); + + BOOST_TEST_EQ(req.payload(), ""); + BOOST_TEST_EQ(req.get_commands(), 0u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + check_pubsub_changes(req, {}); +} + +// Iterators whose value_type is convertible to std::string_view work +void test_unsubscribe_iterators_convertible_string_view() +{ + unsubscribe_fixture fix; + const std::vector channels{"ch1", "ch2"}; + + fix.req.unsubscribe(channels.begin(), channels.end()); + + fix.check_two_channels(); +} + +// The range overload just dispatches to the iterator one +void test_unsubscribe_range() +{ + unsubscribe_fixture fix; + const std::vector channels{"ch1", "ch2"}; + + fix.req.unsubscribe(channels); + + fix.check_two_channels(); +} + +// The initializer_list overload just dispatches to the iterator one +void test_unsubscribe_initializer_list() +{ + unsubscribe_fixture fix; + + fix.req.unsubscribe({"ch1", "ch2"}); + + fix.check_two_channels(); +} + // --- append --- void test_append() { @@ -413,6 +494,12 @@ int main() test_subscribe_range(); test_subscribe_initializer_list(); + test_unsubscribe_iterators(); + test_unsubscribe_iterators_empty(); + test_unsubscribe_iterators_convertible_string_view(); + test_unsubscribe_range(); + test_unsubscribe_initializer_list(); + test_append(); test_append_no_response(); test_append_flags(); From 1a3c7d73df541f24b8f4b9ba9ceb6517a72bc0bf Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:25:04 +0100 Subject: [PATCH 41/63] reduce duplication 2 --- test/test_request.cpp | 96 ++++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/test/test_request.cpp b/test/test_request.cpp index 859647396..fd66d2238 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -156,13 +156,17 @@ void test_push_range_pubsub() } // --- subscribe --- +// Most of the tests build the same request using different overloads. +// This fixture makes checking easier struct subscribe_fixture { request req; - void check_two_channels(boost::source_location loc = BOOST_CURRENT_LOCATION) + void check_impl( + std::string_view expected_payload, + pubsub_change_type expected_type, + boost::source_location loc = BOOST_CURRENT_LOCATION) { - constexpr std::string_view expected = "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; - if (!BOOST_TEST_EQ(req.payload(), expected)) + if (!BOOST_TEST_EQ(req.payload(), expected_payload)) std::cerr << "Called from " << loc << std::endl; if (!BOOST_TEST_EQ(req.get_commands(), 1u)) @@ -171,12 +175,44 @@ struct subscribe_fixture { if (!BOOST_TEST_EQ(req.get_expected_responses(), 0u)) std::cerr << "Called from " << loc << std::endl; - constexpr pubsub_change_str expected_changes[] = { - {pubsub_change_type::subscribe, "ch1"}, - {pubsub_change_type::subscribe, "ch2"}, + const pubsub_change_str expected_changes[] = { + {expected_type, "ch1"}, + {expected_type, "ch2"}, }; check_pubsub_changes(req, expected_changes, loc); } + + void check_subscribe(boost::source_location loc = BOOST_CURRENT_LOCATION) + { + check_impl( + "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n", + pubsub_change_type::subscribe, + loc); + } + + void check_unsubscribe(boost::source_location loc = BOOST_CURRENT_LOCATION) + { + check_impl( + "*3\r\n$11\r\nUNSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n", + pubsub_change_type::unsubscribe, + loc); + } + + void check_psubscribe(boost::source_location loc = BOOST_CURRENT_LOCATION) + { + check_impl( + "*3\r\n$10\r\nPSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n", + pubsub_change_type::psubscribe, + loc); + } + + void check_punsubscribe(boost::source_location loc = BOOST_CURRENT_LOCATION) + { + check_impl( + "*3\r\n$12\r\nPUNSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n", + pubsub_change_type::punsubscribe, + loc); + } }; void test_subscribe_iterators() @@ -186,7 +222,7 @@ void test_subscribe_iterators() fix.req.subscribe(channels.begin(), channels.end()); - fix.check_two_channels(); + fix.check_subscribe(); } // Like push_range, if the range is empty, this is a no-op @@ -211,7 +247,7 @@ void test_subscribe_iterators_convertible_string_view() fix.req.subscribe(channels.begin(), channels.end()); - fix.check_two_channels(); + fix.check_subscribe(); } // The range overload just dispatches to the iterator one @@ -222,7 +258,7 @@ void test_subscribe_range() fix.req.subscribe(channels); - fix.check_two_channels(); + fix.check_subscribe(); } // The initializer_list overload just dispatches to the iterator one @@ -232,42 +268,18 @@ void test_subscribe_initializer_list() fix.req.subscribe({"ch1", "ch2"}); - fix.check_two_channels(); + fix.check_subscribe(); } // --- unsubscribe --- -struct unsubscribe_fixture { - request req; - - void check_two_channels(boost::source_location loc = BOOST_CURRENT_LOCATION) - { - constexpr std::string_view - expected = "*3\r\n$11\r\nUNSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"; - if (!BOOST_TEST_EQ(req.payload(), expected)) - std::cerr << "Called from " << loc << std::endl; - - if (!BOOST_TEST_EQ(req.get_commands(), 1u)) - std::cerr << "Called from " << loc << std::endl; - - if (!BOOST_TEST_EQ(req.get_expected_responses(), 0u)) - std::cerr << "Called from " << loc << std::endl; - - constexpr pubsub_change_str expected_changes[] = { - {pubsub_change_type::unsubscribe, "ch1"}, - {pubsub_change_type::unsubscribe, "ch2"}, - }; - check_pubsub_changes(req, expected_changes, loc); - } -}; - void test_unsubscribe_iterators() { - unsubscribe_fixture fix; + subscribe_fixture fix; const std::forward_list channels{"ch1", "ch2"}; fix.req.unsubscribe(channels.begin(), channels.end()); - fix.check_two_channels(); + fix.check_unsubscribe(); } // Like push_range, if the range is empty, this is a no-op @@ -287,33 +299,33 @@ void test_unsubscribe_iterators_empty() // Iterators whose value_type is convertible to std::string_view work void test_unsubscribe_iterators_convertible_string_view() { - unsubscribe_fixture fix; + subscribe_fixture fix; const std::vector channels{"ch1", "ch2"}; fix.req.unsubscribe(channels.begin(), channels.end()); - fix.check_two_channels(); + fix.check_unsubscribe(); } // The range overload just dispatches to the iterator one void test_unsubscribe_range() { - unsubscribe_fixture fix; + subscribe_fixture fix; const std::vector channels{"ch1", "ch2"}; fix.req.unsubscribe(channels); - fix.check_two_channels(); + fix.check_unsubscribe(); } // The initializer_list overload just dispatches to the iterator one void test_unsubscribe_initializer_list() { - unsubscribe_fixture fix; + subscribe_fixture fix; fix.req.unsubscribe({"ch1", "ch2"}); - fix.check_two_channels(); + fix.check_unsubscribe(); } // --- append --- From 2c0aba0a27009c233920df57aaa078ede6105229 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:26:41 +0100 Subject: [PATCH 42/63] psubscribe --- test/test_request.cpp | 63 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/test_request.cpp b/test/test_request.cpp index fd66d2238..33275323b 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -328,6 +328,63 @@ void test_unsubscribe_initializer_list() fix.check_unsubscribe(); } +// --- psubscribe --- +void test_psubscribe_iterators() +{ + subscribe_fixture fix; + const std::forward_list channels{"ch1", "ch2"}; + + fix.req.psubscribe(channels.begin(), channels.end()); + + fix.check_psubscribe(); +} + +// Like push_range, if the range is empty, this is a no-op +void test_psubscribe_iterators_empty() +{ + const std::forward_list channels; + request req; + + req.psubscribe(channels.begin(), channels.end()); + + BOOST_TEST_EQ(req.payload(), ""); + BOOST_TEST_EQ(req.get_commands(), 0u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + check_pubsub_changes(req, {}); +} + +// Iterators whose value_type is convertible to std::string_view work +void test_psubscribe_iterators_convertible_string_view() +{ + subscribe_fixture fix; + const std::vector channels{"ch1", "ch2"}; + + fix.req.psubscribe(channels.begin(), channels.end()); + + fix.check_psubscribe(); +} + +// The range overload just dispatches to the iterator one +void test_psubscribe_range() +{ + subscribe_fixture fix; + const std::vector channels{"ch1", "ch2"}; + + fix.req.psubscribe(channels); + + fix.check_psubscribe(); +} + +// The initializer_list overload just dispatches to the iterator one +void test_psubscribe_initializer_list() +{ + subscribe_fixture fix; + + fix.req.psubscribe({"ch1", "ch2"}); + + fix.check_psubscribe(); +} + // --- append --- void test_append() { @@ -512,6 +569,12 @@ int main() test_unsubscribe_range(); test_unsubscribe_initializer_list(); + test_psubscribe_iterators(); + test_psubscribe_iterators_empty(); + test_psubscribe_iterators_convertible_string_view(); + test_psubscribe_range(); + test_psubscribe_initializer_list(); + test_append(); test_append_no_response(); test_append_flags(); From 738283d4cfb131c24857d4f071bbb21ef33cf789 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:28:00 +0100 Subject: [PATCH 43/63] punsubscribe --- test/test_request.cpp | 63 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/test_request.cpp b/test/test_request.cpp index 33275323b..c5e9a5623 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -385,6 +385,63 @@ void test_psubscribe_initializer_list() fix.check_psubscribe(); } +// --- punsubscribe --- +void test_punsubscribe_iterators() +{ + subscribe_fixture fix; + const std::forward_list channels{"ch1", "ch2"}; + + fix.req.punsubscribe(channels.begin(), channels.end()); + + fix.check_punsubscribe(); +} + +// Like push_range, if the range is empty, this is a no-op +void test_punsubscribe_iterators_empty() +{ + const std::forward_list channels; + request req; + + req.punsubscribe(channels.begin(), channels.end()); + + BOOST_TEST_EQ(req.payload(), ""); + BOOST_TEST_EQ(req.get_commands(), 0u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + check_pubsub_changes(req, {}); +} + +// Iterators whose value_type is convertible to std::string_view work +void test_punsubscribe_iterators_convertible_string_view() +{ + subscribe_fixture fix; + const std::vector channels{"ch1", "ch2"}; + + fix.req.punsubscribe(channels.begin(), channels.end()); + + fix.check_punsubscribe(); +} + +// The range overload just dispatches to the iterator one +void test_punsubscribe_range() +{ + subscribe_fixture fix; + const std::vector channels{"ch1", "ch2"}; + + fix.req.punsubscribe(channels); + + fix.check_punsubscribe(); +} + +// The initializer_list overload just dispatches to the iterator one +void test_punsubscribe_initializer_list() +{ + subscribe_fixture fix; + + fix.req.punsubscribe({"ch1", "ch2"}); + + fix.check_punsubscribe(); +} + // --- append --- void test_append() { @@ -575,6 +632,12 @@ int main() test_psubscribe_range(); test_psubscribe_initializer_list(); + test_punsubscribe_iterators(); + test_punsubscribe_iterators_empty(); + test_punsubscribe_iterators_convertible_string_view(); + test_punsubscribe_range(); + test_punsubscribe_initializer_list(); + test_append(); test_append_no_response(); test_append_flags(); From 10050ad9d709e0d75d1b3da122d5da1494f93d8e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:32:12 +0100 Subject: [PATCH 44/63] clear tests --- test/test_request.cpp | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/test_request.cpp b/test/test_request.cpp index c5e9a5623..ebb7eaf7d 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -602,6 +602,44 @@ void test_append_pubsub_target_empty() check_pubsub_changes(req1, expected_changes); } +// --- clear --- +void test_clear() +{ + // Create request with some commands and some pubsub changes + request req; + req.push("PING", "value"); + req.push("GET", "key"); + req.subscribe({"ch1", "ch2"}); + req.punsubscribe({"ch3*"}); + + // Clear removes the payload, the commands and the pubsub changes + req.clear(); + BOOST_TEST_EQ(req.payload(), ""); + BOOST_TEST_EQ(req.get_commands(), 0u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + check_pubsub_changes(req, {}); + + // Clearing again does nothing + req.clear(); + BOOST_TEST_EQ(req.payload(), ""); + BOOST_TEST_EQ(req.get_commands(), 0u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + check_pubsub_changes(req, {}); +} + +// Clearing an empty request doesn't cause trouble +void test_clear_empty() +{ + request req; + + req.clear(); + + BOOST_TEST_EQ(req.payload(), ""); + BOOST_TEST_EQ(req.get_commands(), 0u); + BOOST_TEST_EQ(req.get_expected_responses(), 0u); + check_pubsub_changes(req, {}); +} + } // namespace int main() @@ -647,5 +685,8 @@ int main() test_append_pubsub(); test_append_pubsub_target_empty(); + test_clear(); + test_clear_empty(); + return boost::report_errors(); } From 92980bd65cf411b385879b79a368416216449b30 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:36:27 +0100 Subject: [PATCH 45/63] mix pubsub and regular --- test/test_request.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/test_request.cpp b/test/test_request.cpp index ebb7eaf7d..9777e790c 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -442,6 +442,31 @@ void test_punsubscribe_initializer_list() fix.check_punsubscribe(); } +// Mixing regular commands and pubsub commands is OK +void test_mix_pubsub_regular() +{ + request req; + req.push("PING"); + req.subscribe({"ch1", "ch2"}); + req.push("GET", "key"); + req.punsubscribe({"ch4*"}); + + constexpr std::string_view expected = + "*1\r\n$4\r\nPING\r\n" + "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n" + "*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n" + "*2\r\n$12\r\nPUNSUBSCRIBE\r\n$4\r\nch4*\r\n"; + BOOST_TEST_EQ(req.payload(), expected); + BOOST_TEST_EQ(req.get_commands(), 4u); + BOOST_TEST_EQ(req.get_expected_responses(), 2u); + constexpr pubsub_change_str expected_changes[] = { + {pubsub_change_type::subscribe, "ch1" }, + {pubsub_change_type::subscribe, "ch2" }, + {pubsub_change_type::punsubscribe, "ch4*"}, + }; + check_pubsub_changes(req, expected_changes); +} + // --- append --- void test_append() { @@ -676,6 +701,8 @@ int main() test_punsubscribe_range(); test_punsubscribe_initializer_list(); + test_mix_pubsub_regular(); + test_append(); test_append_no_response(); test_append_flags(); From 0edc13485281a1a6fe865bab72a385e3b2acb995 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:46:35 +0100 Subject: [PATCH 46/63] tracker 1st test --- test/CMakeLists.txt | 1 + test/Jamfile | 1 + test/test_subscription_tracker.cpp | 45 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 test/test_subscription_tracker.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 40a1ea05d..9c6a97e0e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -55,6 +55,7 @@ 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) # Tests that require a real Redis server make_test(test_conn_quit) diff --git a/test/Jamfile b/test/Jamfile index 3aa72460f..ccd47060c 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -72,6 +72,7 @@ local tests = test_flat_tree test_generic_flat_response test_read_buffer + test_subscription_tracker ; # Build and run the tests diff --git a/test/test_subscription_tracker.cpp b/test/test_subscription_tracker.cpp new file mode 100644 index 000000000..8e8bfc4d3 --- /dev/null +++ b/test/test_subscription_tracker.cpp @@ -0,0 +1,45 @@ +// +// Copyright (c) 2025-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 + +using namespace boost::redis; +using detail::subscription_tracker; + +namespace { + +// State originated by SUBSCRIBE commands, only +void test_subscribe() +{ + subscription_tracker tracker; + request req1, req2, req_output, req_expected; + + // Add some changes to the tracker + req1.subscribe({"channel_a", "channel_b"}); + tracker.commit_changes(req1); + + req2.subscribe({"channel_c"}); + tracker.commit_changes(req2); + + // Check that we generate the correct response + tracker.compose_subscribe_request(req_output); + req_expected.push("SUBSCRIBE", "channel_a", "channel_b", "channel_c"); + BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); +} + +} // namespace + +int main() +{ + test_subscribe(); + + return boost::report_errors(); +} From adf8a6548897d7bc2cd8ce9b57a25518db0c3e9a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:49:38 +0100 Subject: [PATCH 47/63] psubscribe --- test/test_subscription_tracker.cpp | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/test_subscription_tracker.cpp b/test/test_subscription_tracker.cpp index 8e8bfc4d3..485f34b7f 100644 --- a/test/test_subscription_tracker.cpp +++ b/test/test_subscription_tracker.cpp @@ -35,11 +35,54 @@ void test_subscribe() BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); } +// State originated by PSUBSCRIBE commands, only +void test_psubscribe() +{ + subscription_tracker tracker; + request req1, req2, req_output, req_expected; + + // Add some changes to the tracker + req1.psubscribe({"channel_b*", "channel_c*"}); + tracker.commit_changes(req1); + + req2.psubscribe({"channel_a*"}); + tracker.commit_changes(req2); + + // Check that we generate the correct response + tracker.compose_subscribe_request(req_output); + req_expected.push("PSUBSCRIBE", "channel_a*", "channel_b*", "channel_c*"); // we sort them + BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); +} + +// We can mix SUBSCRIBE and PSUBSCRIBE operations +void test_subscribe_psubscribe() +{ + subscription_tracker tracker; + request req1, req2, req_output, req_expected; + + // Add some changes to the tracker + req1.psubscribe({"channel_a*", "channel_b*"}); + req1.subscribe({"ch1"}); + tracker.commit_changes(req1); + + req2.subscribe({"ch2"}); + req2.psubscribe({"channel_c*"}); + tracker.commit_changes(req2); + + // Check that we generate the correct response + tracker.compose_subscribe_request(req_output); + req_expected.push("SUBSCRIBE", "ch1", "ch2"); + req_expected.push("PSUBSCRIBE", "channel_a*", "channel_b*", "channel_c*"); + BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); +} + } // namespace int main() { test_subscribe(); + test_psubscribe(); + test_subscribe_psubscribe(); return boost::report_errors(); } From 7ec85cc40f708dffda66443caf39173e1e08ab3d Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:51:33 +0100 Subject: [PATCH 48/63] subs psubs same arg --- test/test_subscription_tracker.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/test_subscription_tracker.cpp b/test/test_subscription_tracker.cpp index 485f34b7f..4d6d24986 100644 --- a/test/test_subscription_tracker.cpp +++ b/test/test_subscription_tracker.cpp @@ -76,6 +76,22 @@ void test_subscribe_psubscribe() BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); } +// We can have subscribe and psubscribe commands with the same argument +void test_subscribe_psubscribe_same_arg() +{ + subscription_tracker tracker; + request req, req_output, req_expected; + + req.subscribe({"ch1"}); + req.psubscribe({"ch1"}); + tracker.commit_changes(req); + + tracker.compose_subscribe_request(req_output); + req_expected.push("SUBSCRIBE", "ch1"); + req_expected.push("PSUBSCRIBE", "ch1"); + BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); +} + } // namespace int main() @@ -83,6 +99,7 @@ int main() test_subscribe(); test_psubscribe(); test_subscribe_psubscribe(); + test_subscribe_psubscribe_same_arg(); return boost::report_errors(); } From 278c96bbe8798e8ad3c1aeac9407a656b7450b88 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 18:59:09 +0100 Subject: [PATCH 49/63] more tests --- test/test_subscription_tracker.cpp | 80 ++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/test/test_subscription_tracker.cpp b/test/test_subscription_tracker.cpp index 4d6d24986..86ccc2250 100644 --- a/test/test_subscription_tracker.cpp +++ b/test/test_subscription_tracker.cpp @@ -92,6 +92,83 @@ void test_subscribe_psubscribe_same_arg() BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); } +// An unsubscribe/punsubscribe balances a matching subscribe +void test_unsubscribe() +{ + subscription_tracker tracker; + request req1, req2, req_output, req_expected; + + // Add some changes to the tracker + req1.subscribe({"ch1", "ch2"}); + req1.psubscribe({"ch1*", "ch2*"}); + tracker.commit_changes(req1); + + // Unsubscribe from some channels + req2.punsubscribe({"ch2*"}); + req2.unsubscribe({"ch1"}); + tracker.commit_changes(req2); + + // Check that we generate the correct response + tracker.compose_subscribe_request(req_output); + req_expected.push("SUBSCRIBE", "ch2"); + req_expected.push("PSUBSCRIBE", "ch1*"); + BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); +} + +// After an unsubscribe, we can subscribe again +void test_resubscribe() +{ + subscription_tracker tracker; + request req, req_output, req_expected; + + // Subscribe to some channels + req.subscribe({"ch1", "ch2"}); + req.psubscribe({"ch1*", "ch2*"}); + tracker.commit_changes(req); + + // Unsubscribe from some channels + req.clear(); + req.punsubscribe({"ch2*"}); + req.unsubscribe({"ch1"}); + tracker.commit_changes(req); + + // Subscribe again + req.clear(); + req.subscribe({"ch1"}); + req.psubscribe({"ch2*"}); + tracker.commit_changes(req); + + // Check that we generate the correct response + tracker.compose_subscribe_request(req_output); + req_expected.push("SUBSCRIBE", "ch1", "ch2"); + req_expected.push("PSUBSCRIBE", "ch1*", "ch2*"); + BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); +} + +// Subscribing twice is not a problem +void test_subscribe_twice() +{ + subscription_tracker tracker; + request req, req_output, req_expected; + + // Subscribe to some channels + req.subscribe({"ch1", "ch2"}); + req.psubscribe({"ch1*", "ch2*"}); + tracker.commit_changes(req); + + // Subscribe to the same channels again + req.clear(); + req.subscribe({"ch2"}); + req.psubscribe({"ch1*"}); + tracker.commit_changes(req); + + // Check that we generate the correct response + tracker.compose_subscribe_request(req_output); + req_expected.push("SUBSCRIBE", "ch1", "ch2"); + req_expected.push("PSUBSCRIBE", "ch1*", "ch2*"); + BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); +} + } // namespace int main() @@ -100,6 +177,9 @@ int main() test_psubscribe(); test_subscribe_psubscribe(); test_subscribe_psubscribe_same_arg(); + test_unsubscribe(); + test_resubscribe(); + test_subscribe_twice(); return boost::report_errors(); } From 4d0eeea4185b87b30933b9e8c40ac912f4994c9a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:07:32 +0100 Subject: [PATCH 50/63] finished tracker test --- test/test_subscription_tracker.cpp | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/test/test_subscription_tracker.cpp b/test/test_subscription_tracker.cpp index 86ccc2250..7f5d5aefe 100644 --- a/test/test_subscription_tracker.cpp +++ b/test/test_subscription_tracker.cpp @@ -169,6 +169,93 @@ void test_subscribe_twice() BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); } +// Unsubscribing from channels we haven't subscribed to is not a problem +void test_lone_unsubscribe() +{ + subscription_tracker tracker; + request req, req_output, req_expected; + + // Subscribe to some channels + req.subscribe({"ch1", "ch2"}); + req.psubscribe({"ch1*", "ch2*"}); + tracker.commit_changes(req); + + // Unsubscribe from channels we haven't subscribed to + req.clear(); + req.unsubscribe({"other"}); + req.punsubscribe({"other*"}); + tracker.commit_changes(req); + + // Check that we generate the correct response + tracker.compose_subscribe_request(req_output); + req_expected.push("SUBSCRIBE", "ch1", "ch2"); + req_expected.push("PSUBSCRIBE", "ch1*", "ch2*"); + BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); +} + +// A state with no changes is not a problem +void test_empty() +{ + subscription_tracker tracker; + request req_output; + + tracker.compose_subscribe_request(req_output); + BOOST_TEST_EQ(req_output.payload(), ""); +} + +// If the output request is not empty, the commands are added to it, rather than replaced +void test_output_request_not_empty() +{ + subscription_tracker tracker; + request req, req_output, req_expected; + + // Subscribe to some channels + req.subscribe({"ch1", "ch2"}); + req.psubscribe({"ch1*", "ch2*"}); + tracker.commit_changes(req); + + // Compose the output request + req_output.push("PING", "hello"); + tracker.compose_subscribe_request(req_output); + + // Check that we generate the correct response + req_expected.push("PING", "hello"); + req_expected.push("SUBSCRIBE", "ch1", "ch2"); + req_expected.push("PSUBSCRIBE", "ch1*", "ch2*"); + BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); +} + +// Clear removes everything from the state +void test_clear() +{ + subscription_tracker tracker; + request req, req_output, req_expected; + + // Subscribe to some channels + req.subscribe({"ch1", "ch2"}); + req.psubscribe({"ch1*", "ch2*"}); + tracker.commit_changes(req); + + // Clear + tracker.clear(); + + // Nothing should be generated + tracker.compose_subscribe_request(req_output); + BOOST_TEST_EQ(req_output.payload(), ""); + + // We can reuse the tracker by now committing some more changes + req.clear(); + req.subscribe({"ch5"}); + req.psubscribe({"ch6*"}); + tracker.commit_changes(req); + + // Check that we generate the correct response + tracker.compose_subscribe_request(req_output); + req_expected.push("SUBSCRIBE", "ch5"); + req_expected.push("PSUBSCRIBE", "ch6*"); + BOOST_TEST_EQ(req_output.payload(), req_expected.payload()); +} + } // namespace int main() @@ -180,6 +267,10 @@ int main() test_unsubscribe(); test_resubscribe(); test_subscribe_twice(); + test_lone_unsubscribe(); + test_empty(); + test_output_request_not_empty(); + test_clear(); return boost::report_errors(); } From c57531ca26e94c9d13545495eb120fc31e581c24 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:11:53 +0100 Subject: [PATCH 51/63] Remove commit_change --- .../redis/detail/subscription_tracker.hpp | 4 ---- .../boost/redis/impl/subscription_tracker.ipp | 24 ++++++++----------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/include/boost/redis/detail/subscription_tracker.hpp b/include/boost/redis/detail/subscription_tracker.hpp index c24c43de7..865d4113e 100644 --- a/include/boost/redis/detail/subscription_tracker.hpp +++ b/include/boost/redis/detail/subscription_tracker.hpp @@ -11,7 +11,6 @@ #include #include -#include namespace boost::redis { @@ -19,8 +18,6 @@ class request; namespace detail { -enum class pubsub_change_type; - class subscription_tracker { std::set channels_; std::set pchannels_; @@ -28,7 +25,6 @@ class subscription_tracker { public: subscription_tracker() = default; void clear(); - void commit_change(pubsub_change_type type, std::string_view channel); void commit_changes(const request& req); void compose_subscribe_request(request& to) const; }; diff --git a/include/boost/redis/impl/subscription_tracker.ipp b/include/boost/redis/impl/subscription_tracker.ipp index 9e1c064ca..01d4d0532 100644 --- a/include/boost/redis/impl/subscription_tracker.ipp +++ b/include/boost/redis/impl/subscription_tracker.ipp @@ -21,22 +21,18 @@ void subscription_tracker::clear() pchannels_.clear(); } -void subscription_tracker::commit_change(pubsub_change_type type, std::string_view channel) -{ - std::string owning_channel{channel}; - switch (type) { - case pubsub_change_type::subscribe: channels_.insert(std::move(owning_channel)); break; - case pubsub_change_type::unsubscribe: channels_.erase(std::move(owning_channel)); break; - case pubsub_change_type::psubscribe: pchannels_.insert(std::move(owning_channel)); break; - case pubsub_change_type::punsubscribe: pchannels_.erase(std::move(owning_channel)); break; - default: BOOST_ASSERT(false); - } -} - void subscription_tracker::commit_changes(const request& req) { - for (const auto& ch : request_access::pubsub_changes(req)) - commit_change(ch.type, req.payload().substr(ch.channel_offset, ch.channel_size)); + for (const auto& ch : request_access::pubsub_changes(req)) { + std::string owning_channel{req.payload().substr(ch.channel_offset, ch.channel_size)}; + switch (ch.type) { + case pubsub_change_type::subscribe: channels_.insert(std::move(owning_channel)); break; + case pubsub_change_type::unsubscribe: channels_.erase(std::move(owning_channel)); break; + case pubsub_change_type::psubscribe: pchannels_.insert(std::move(owning_channel)); break; + case pubsub_change_type::punsubscribe: pchannels_.erase(std::move(owning_channel)); break; + default: BOOST_ASSERT(false); + } + } } void subscription_tracker::compose_subscribe_request(request& to) const From 16ddd828ac0c8e77bba348deee77f7de21639d5c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:12:23 +0100 Subject: [PATCH 52/63] rename --- include/boost/redis/impl/subscription_tracker.ipp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/boost/redis/impl/subscription_tracker.ipp b/include/boost/redis/impl/subscription_tracker.ipp index 01d4d0532..ec23675fb 100644 --- a/include/boost/redis/impl/subscription_tracker.ipp +++ b/include/boost/redis/impl/subscription_tracker.ipp @@ -24,12 +24,12 @@ void subscription_tracker::clear() void subscription_tracker::commit_changes(const request& req) { for (const auto& ch : request_access::pubsub_changes(req)) { - std::string owning_channel{req.payload().substr(ch.channel_offset, ch.channel_size)}; + std::string channel{req.payload().substr(ch.channel_offset, ch.channel_size)}; switch (ch.type) { - case pubsub_change_type::subscribe: channels_.insert(std::move(owning_channel)); break; - case pubsub_change_type::unsubscribe: channels_.erase(std::move(owning_channel)); break; - case pubsub_change_type::psubscribe: pchannels_.insert(std::move(owning_channel)); break; - case pubsub_change_type::punsubscribe: pchannels_.erase(std::move(owning_channel)); break; + case pubsub_change_type::subscribe: channels_.insert(std::move(channel)); break; + case pubsub_change_type::unsubscribe: channels_.erase(std::move(channel)); break; + case pubsub_change_type::psubscribe: pchannels_.insert(std::move(channel)); break; + case pubsub_change_type::punsubscribe: pchannels_.erase(std::move(channel)); break; default: BOOST_ASSERT(false); } } From 42b390fe28d47ea2c95fe244a108a1d1ef42010a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:16:02 +0100 Subject: [PATCH 53/63] Fix test_exec_fsm --- test/test_exec_fsm.cpp | 128 +++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/test/test_exec_fsm.cpp b/test/test_exec_fsm.cpp index f23853556..4fa26efb6 100644 --- a/test/test_exec_fsm.cpp +++ b/test/test_exec_fsm.cpp @@ -6,6 +6,7 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // +#include #include #include #include @@ -30,6 +31,7 @@ using detail::multiplexer; using detail::exec_action_type; using detail::consume_result; using detail::exec_action; +using detail::connection_state; using boost::system::error_code; using boost::asio::cancellation_type_t; @@ -121,35 +123,35 @@ struct elem_and_request { void test_success() { // Setup - multiplexer mpx; + connection_state st; elem_and_request input; - exec_fsm fsm(mpx, std::move(input.elm)); + exec_fsm fsm(std::move(input.elm)); error_code ec; // Initiate - auto act = fsm.resume(true, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write - BOOST_TEST_EQ(mpx.prepare_write(), 1u); // one request was placed in the packet to write - BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size())); + BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); // one request was placed in the packet to write + BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size())); // Simulate a successful read - read(mpx, "$5\r\nhello\r\n"); - auto req_status = mpx.consume(ec); + read(st.mpx, "$5\r\nhello\r\n"); + auto req_status = st.mpx.consume(ec); BOOST_TEST_EQ(ec, error_code()); BOOST_TEST_EQ(req_status.first, consume_result::got_response); BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action(error_code(), 11u)); // All memory should have been freed by now @@ -160,37 +162,37 @@ void test_success() void test_parse_error() { // Setup - multiplexer mpx; + connection_state st; elem_and_request input; - exec_fsm fsm(mpx, std::move(input.elm)); + exec_fsm fsm(std::move(input.elm)); error_code ec; // Initiate - auto act = fsm.resume(true, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write - BOOST_TEST_EQ(mpx.prepare_write(), 1u); // one request was placed in the packet to write - BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size())); + BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); // one request was placed in the packet to write + BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size())); // Simulate a read that will trigger an error. // The second field should be a number (rather than the empty string). // Note that although part of the buffer was consumed, the multiplexer // currently throws this information away. - read(mpx, "*2\r\n$5\r\nhello\r\n:\r\n"); - auto req_status = mpx.consume(ec); + read(st.mpx, "*2\r\n$5\r\nhello\r\n:\r\n"); + auto req_status = st.mpx.consume(ec); BOOST_TEST_EQ(ec, error::empty_field); BOOST_TEST_EQ(req_status.second, 15u); BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action(error::empty_field, 0u)); // All memory should have been freed by now @@ -201,17 +203,17 @@ void test_parse_error() void test_cancel_if_not_connected() { // Setup - multiplexer mpx; + connection_state st; request::config cfg; cfg.cancel_if_not_connected = true; elem_and_request input(cfg); - exec_fsm fsm(mpx, std::move(input.elm)); + exec_fsm fsm(std::move(input.elm)); // Initiate. We're not connected, so the request gets cancelled - auto act = fsm.resume(false, cancellation_type_t::none); + auto act = fsm.resume(false, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::immediate); - act = fsm.resume(false, cancellation_type_t::none); + act = fsm.resume(false, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action(error::not_connected)); // We didn't leave memory behind @@ -222,35 +224,35 @@ void test_cancel_if_not_connected() void test_not_connected() { // Setup - multiplexer mpx; + connection_state st; elem_and_request input; - exec_fsm fsm(mpx, std::move(input.elm)); + exec_fsm fsm(std::move(input.elm)); error_code ec; // Initiate - auto act = fsm.resume(false, cancellation_type_t::none); + auto act = fsm.resume(false, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); // We should now wait for a response - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write - BOOST_TEST_EQ(mpx.prepare_write(), 1u); // one request was placed in the packet to write - BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size())); + BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); // one request was placed in the packet to write + BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size())); // Simulate a successful read - read(mpx, "$5\r\nhello\r\n"); - auto req_status = mpx.consume(ec); + read(st.mpx, "$5\r\nhello\r\n"); + auto req_status = st.mpx.consume(ec); BOOST_TEST_EQ(ec, error_code()); BOOST_TEST_EQ(req_status.first, consume_result::got_response); BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action(error_code(), 11u)); // All memory should have been freed by now @@ -277,24 +279,24 @@ void test_cancel_waiting() for (const auto& tc : test_cases) { // Setup - multiplexer mpx; + connection_state st; elem_and_request input, input2; - exec_fsm fsm(mpx, std::move(input.elm)); + exec_fsm fsm(std::move(input.elm)); // Another request enters the multiplexer, so it's busy when we start - mpx.add(input2.elm); - BOOST_TEST_EQ_MSG(mpx.prepare_write(), 1u, tc.name); + st.mpx.add(input2.elm); + BOOST_TEST_EQ_MSG(st.mpx.prepare_write(), 1u, tc.name); // Initiate and wait - auto act = fsm.resume(true, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ_MSG(act, exec_action_type::setup_cancellation, tc.name); - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ_MSG(act, exec_action_type::notify_writer, tc.name); - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::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, tc.type); + act = fsm.resume(true, st, tc.type); BOOST_TEST_EQ_MSG(act, exec_action(asio::error::operation_aborted), tc.name); BOOST_TEST_EQ_MSG(input.weak_elm.expired(), true, tc.name); // we didn't leave memory behind } @@ -314,32 +316,32 @@ void test_cancel_notwaiting_terminal_partial() for (const auto& tc : test_cases) { // Setup - multiplexer mpx; + connection_state st; auto input = std::make_unique(); - exec_fsm fsm(mpx, std::move(input->elm)); + exec_fsm fsm(std::move(input->elm)); // Initiate - auto act = fsm.resume(false, cancellation_type_t::none); + auto act = fsm.resume(false, st, cancellation_type_t::none); BOOST_TEST_EQ_MSG(act, exec_action_type::setup_cancellation, tc.name); - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ_MSG(act, exec_action_type::notify_writer, tc.name); - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ_MSG(act, exec_action_type::wait_for_response, tc.name); // The multiplexer starts writing the request - BOOST_TEST_EQ_MSG(mpx.prepare_write(), 1u, tc.name); - BOOST_TEST_EQ_MSG(mpx.commit_write(mpx.get_write_buffer().size()), true, tc.name); + BOOST_TEST_EQ_MSG(st.mpx.prepare_write(), 1u, tc.name); + BOOST_TEST_EQ_MSG(st.mpx.commit_write(st.mpx.get_write_buffer().size()), true, tc.name); // A cancellation arrives - act = fsm.resume(true, tc.type); + act = fsm.resume(true, st, tc.type); BOOST_TEST_EQ(act, exec_action(asio::error::operation_aborted)); input.reset(); // Verify we don't access the request or response after completion error_code ec; // When the response to this request arrives, it gets ignored - read(mpx, "-ERR wrong command\r\n"); - auto res = mpx.consume(ec); + read(st.mpx, "-ERR wrong command\r\n"); + auto res = st.mpx.consume(ec); BOOST_TEST_EQ_MSG(ec, error_code(), tc.name); BOOST_TEST_EQ_MSG(res.first, consume_result::got_response, tc.name); @@ -352,38 +354,38 @@ void test_cancel_notwaiting_terminal_partial() void test_cancel_notwaiting_total() { // Setup - multiplexer mpx; + connection_state st; elem_and_request input; - exec_fsm fsm(mpx, std::move(input.elm)); + exec_fsm fsm(std::move(input.elm)); error_code ec; // Initiate - auto act = fsm.resume(true, cancellation_type_t::none); + auto act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::notify_writer); - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful write - BOOST_TEST_EQ(mpx.prepare_write(), 1u); - BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size())); + BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); + 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, asio::cancellation_type_t::total); + act = fsm.resume(true, st, asio::cancellation_type_t::total); BOOST_TEST_EQ(act, exec_action_type::wait_for_response); // Simulate a successful read - read(mpx, "$5\r\nhello\r\n"); - auto req_status = mpx.consume(ec); + read(st.mpx, "$5\r\nhello\r\n"); + auto req_status = st.mpx.consume(ec); BOOST_TEST_EQ(ec, error_code()); BOOST_TEST_EQ(req_status.first, consume_result::got_response); BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed BOOST_TEST_EQ(input.done_calls, 1u); // This will awaken the exec operation, and should complete the operation - act = fsm.resume(true, cancellation_type_t::none); + act = fsm.resume(true, st, cancellation_type_t::none); BOOST_TEST_EQ(act, exec_action(error_code(), 11u)); // All memory should have been freed by now From 55bb0d7e83a1d22656a8bebb16d2593b0b5a3828 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:24:20 +0100 Subject: [PATCH 54/63] rename tracker --- include/boost/redis/detail/connection_state.hpp | 2 +- include/boost/redis/impl/exec_fsm.ipp | 2 +- include/boost/redis/impl/run_fsm.ipp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/boost/redis/detail/connection_state.hpp b/include/boost/redis/detail/connection_state.hpp index 54fc9cf54..75ddf1772 100644 --- a/include/boost/redis/detail/connection_state.hpp +++ b/include/boost/redis/detail/connection_state.hpp @@ -50,7 +50,7 @@ struct connection_state { std::string diagnostic{}; // Used by the setup request and Sentinel request setup_req{}; request ping_req{}; - subscription_tracker pubsub_st{}; + subscription_tracker tracker{}; // Sentinel stuff lazy_random_engine eng{}; diff --git a/include/boost/redis/impl/exec_fsm.ipp b/include/boost/redis/impl/exec_fsm.ipp index 2e7110a50..3898d1e18 100644 --- a/include/boost/redis/impl/exec_fsm.ipp +++ b/include/boost/redis/impl/exec_fsm.ipp @@ -68,7 +68,7 @@ exec_action exec_fsm::resume( // If the request completed successfully and we were configured to do so, // record the changes applied to the pubsub state if (!elem_->get_error()) - st.pubsub_st.commit_changes(elem_->get_request()); + st.tracker.commit_changes(elem_->get_request()); // Deallocate memory before finalizing exec_action act{elem_->get_error(), elem_->get_read_size()}; diff --git a/include/boost/redis/impl/run_fsm.ipp b/include/boost/redis/impl/run_fsm.ipp index e8218bb76..d477d549d 100644 --- a/include/boost/redis/impl/run_fsm.ipp +++ b/include/boost/redis/impl/run_fsm.ipp @@ -102,7 +102,7 @@ run_action run_fsm::resume( } // Clear any remainder from previous runs - st.pubsub_st.clear(); + st.tracker.clear(); // Compose the PING request. This only depends on the config, so it can be done just once compose_ping_request(st.cfg, st.ping_req); @@ -159,7 +159,7 @@ run_action run_fsm::resume( // Initialization st.mpx.reset(); st.diagnostic.clear(); - compose_setup_request(st.cfg, st.pubsub_st, st.setup_req); + compose_setup_request(st.cfg, st.tracker, st.setup_req); // Add the setup request to the multiplexer if (st.setup_req.get_commands() != 0u) { From 1db2055b13734de977adc3d45ccf1aa9211fba70 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:27:23 +0100 Subject: [PATCH 55/63] exec success tracking --- test/test_exec_fsm.cpp | 58 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/test/test_exec_fsm.cpp b/test/test_exec_fsm.cpp index 4fa26efb6..f0a5ae875 100644 --- a/test/test_exec_fsm.cpp +++ b/test/test_exec_fsm.cpp @@ -104,11 +104,23 @@ struct elem_and_request { std::shared_ptr elm; std::weak_ptr weak_elm; // check that we free memory - elem_and_request(request::config cfg = {}) - : req(cfg) + static request make_request(request::config cfg) { + request req{cfg}; + // Empty requests are not valid. The request needs to be populated before creating the element req.push("get", "mykey"); + + return req; + } + + elem_and_request(request::config cfg = {}) + : elem_and_request(make_request(cfg)) + { } + + elem_and_request(request input_req) + : req(std::move(input_req)) + { elm = std::make_shared(req, any_adapter{}); elm->set_done_callback([this] { @@ -392,6 +404,47 @@ void test_cancel_notwaiting_total() BOOST_TEST_EQ(input.weak_elm.expired(), true); } +// If a request completes successfully and contained pubsub changes, these are committed +void test_subscription_tracking_success() +{ + // Setup + request req; + req.subscribe({"ch1", "ch2"}); + connection_state st; + elem_and_request input{std::move(req)}; + exec_fsm fsm(std::move(input.elm)); + + // Initiate + auto act = fsm.resume(true, st, cancellation_type_t::none); + BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); + act = fsm.resume(true, st, cancellation_type_t::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); + BOOST_TEST_EQ(act, exec_action_type::wait_for_response); + + // Simulate a successful write + BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); // one request was placed in the packet to write + BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size())); + + // 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); + BOOST_TEST_EQ(act, exec_action(error_code(), 0u)); + + // All memory should have been freed by now + BOOST_TEST(input.weak_elm.expired()); + + // The subscription has been added to the tracker + request tracker_req; + st.tracker.compose_subscribe_request(tracker_req); + + request expected_req; + expected_req.push("SUBSCRIBE", "ch1", "ch2"); + BOOST_TEST_EQ(tracker_req.payload(), expected_req.payload()); +} + } // namespace int main() @@ -403,6 +456,7 @@ int main() test_cancel_waiting(); test_cancel_notwaiting_terminal_partial(); test_cancel_notwaiting_total(); + test_subscription_tracking_success(); return boost::report_errors(); } From 1a3e7cfe2002721f866b2c409e66003141dd8092 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:32:04 +0100 Subject: [PATCH 56/63] exec tracking err --- test/test_exec_fsm.cpp | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/test_exec_fsm.cpp b/test/test_exec_fsm.cpp index f0a5ae875..16301554d 100644 --- a/test/test_exec_fsm.cpp +++ b/test/test_exec_fsm.cpp @@ -445,6 +445,43 @@ void test_subscription_tracking_success() BOOST_TEST_EQ(tracker_req.payload(), expected_req.payload()); } +// If the request errors, tracked subscriptions are not committed +void test_subscription_tracking_error() +{ + // Setup + request req; + req.subscribe({"ch1", "ch2"}); + connection_state st; + elem_and_request input{std::move(req)}; + exec_fsm fsm(std::move(input.elm)); + + // Initiate + auto act = fsm.resume(true, st, cancellation_type_t::none); + BOOST_TEST_EQ(act, exec_action_type::setup_cancellation); + act = fsm.resume(true, st, cancellation_type_t::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); + BOOST_TEST_EQ(act, exec_action_type::wait_for_response); + + // Simulate a write error, which would trigger a reconnection + BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); // one request was placed in the packet to write + 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)); + + // All memory should have been freed by now + BOOST_TEST(input.weak_elm.expired()); + + // The subscription has not been added to the tracker + request tracker_req; + st.tracker.compose_subscribe_request(tracker_req); + BOOST_TEST_EQ(tracker_req.payload(), ""); +} + } // namespace int main() @@ -457,6 +494,7 @@ int main() test_cancel_notwaiting_terminal_partial(); test_cancel_notwaiting_total(); test_subscription_tracking_success(); + test_subscription_tracking_error(); return boost::report_errors(); } From e80174410f7f4560fbdba622def3f2394ca26224 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:37:41 +0100 Subject: [PATCH 57/63] fix test compose_setup_request --- test/test_compose_setup_request.cpp | 160 ++++++++++++++++------------ 1 file changed, 92 insertions(+), 68 deletions(-) diff --git a/test/test_compose_setup_request.cpp b/test/test_compose_setup_request.cpp index 2f91a9470..7d9011b89 100644 --- a/test/test_compose_setup_request.cpp +++ b/test/test_compose_setup_request.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -18,112 +19,127 @@ #include #include +using namespace boost::redis; namespace asio = boost::asio; -namespace redis = boost::redis; -using redis::detail::compose_setup_request; +using detail::compose_setup_request; +using detail::subscription_tracker; using boost::system::error_code; namespace { void test_hello() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; cfg.clientname = ""; - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } void test_select() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; cfg.clientname = ""; cfg.database_index = 10; - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n" "*2\r\n$6\r\nSELECT\r\n$2\r\n10\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } void test_clientname() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } void test_auth() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; cfg.clientname = ""; cfg.username = "foo"; cfg.password = "bar"; - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } void test_auth_empty_password() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; cfg.clientname = ""; cfg.username = "foo"; - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$0\r\n\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } void test_auth_setname() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; cfg.clientname = "mytest"; cfg.username = "foo"; cfg.password = "bar"; - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*7\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$7\r\nSETNAME\r\n$" "6\r\nmytest\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } void test_use_setup() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; cfg.clientname = "mytest"; cfg.username = "foo"; cfg.password = "bar"; @@ -131,58 +147,64 @@ void test_use_setup() cfg.use_setup = true; cfg.setup.push("SELECT", 8); - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n" "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } // Regression check: we set the priority flag void test_use_setup_no_hello() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; cfg.use_setup = true; cfg.setup.clear(); cfg.setup.push("SELECT", 8); - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } // Regression check: we set the relevant cancellation flags in the request void test_use_setup_flags() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; cfg.use_setup = true; cfg.setup.clear(); cfg.setup.push("SELECT", 8); cfg.setup.get_config().cancel_if_unresponded = false; cfg.setup.get_config().cancel_on_connection_lost = false; - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } // When using Sentinel, a ROLE command is added. This works // both with the old HELLO and new setup strategies. void test_sentinel_auth() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; cfg.sentinel.addresses = { {"localhost", "26379"} }; @@ -190,36 +212,38 @@ void test_sentinel_auth() cfg.username = "foo"; cfg.password = "bar"; - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n" "*1\r\n$4\r\nROLE\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } void test_sentinel_use_setup() { - redis::config cfg; + subscription_tracker tracker; + request out; + config cfg; cfg.sentinel.addresses = { {"localhost", "26379"} }; cfg.use_setup = true; cfg.setup.push("SELECT", 42); - compose_setup_request(cfg); + compose_setup_request(cfg, tracker, out); std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n" "*2\r\n$6\r\nSELECT\r\n$2\r\n42\r\n" "*1\r\n$4\r\nROLE\r\n"; - BOOST_TEST_EQ(cfg.setup.payload(), expected); - BOOST_TEST(cfg.setup.has_hello_priority()); - BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded); - BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost); + BOOST_TEST_EQ(out.payload(), expected); + BOOST_TEST(out.has_hello_priority()); + BOOST_TEST(out.get_config().cancel_if_unresponded); + BOOST_TEST(out.get_config().cancel_on_connection_lost); } } // namespace From ded328db098cb1de059f992c55b4a615603f0039 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:45:48 +0100 Subject: [PATCH 58/63] reduce duplication --- test/test_compose_setup_request.cpp | 241 ++++++++++------------------ 1 file changed, 87 insertions(+), 154 deletions(-) diff --git a/test/test_compose_setup_request.cpp b/test/test_compose_setup_request.cpp index 7d9011b89..d085f002c 100644 --- a/test/test_compose_setup_request.cpp +++ b/test/test_compose_setup_request.cpp @@ -6,18 +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 using namespace boost::redis; namespace asio = boost::asio; @@ -27,223 +28,155 @@ using boost::system::error_code; namespace { -void test_hello() -{ +struct fixture { subscription_tracker tracker; request out; config cfg; - cfg.clientname = ""; - compose_setup_request(cfg, tracker, out); + void run(std::string_view expected_payload, boost::source_location loc = BOOST_CURRENT_LOCATION) + { + compose_setup_request(cfg, tracker, out); + + if (!BOOST_TEST_EQ(out.payload(), expected_payload)) + std::cerr << "Called from " << loc << std::endl; + + if (!BOOST_TEST(out.has_hello_priority())) + std::cerr << "Called from " << loc << std::endl; + + if (!BOOST_TEST(out.get_config().cancel_if_unresponded)) + std::cerr << "Called from " << loc << std::endl; + + if (!BOOST_TEST(out.get_config().cancel_on_connection_lost)) + std::cerr << "Called from " << loc << std::endl; + } +}; - std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); +void test_hello() +{ + fixture fix; + fix.cfg.clientname = ""; + + fix.run("*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"); } void test_select() { - subscription_tracker tracker; - request out; - config cfg; - cfg.clientname = ""; - cfg.database_index = 10; + fixture fix; + fix.cfg.clientname = ""; + fix.cfg.database_index = 10; - compose_setup_request(cfg, tracker, out); - - std::string_view const expected = + fix.run( "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n" - "*2\r\n$6\r\nSELECT\r\n$2\r\n10\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); + "*2\r\n$6\r\nSELECT\r\n$2\r\n10\r\n"); } void test_clientname() { - subscription_tracker tracker; - request out; - config cfg; - - compose_setup_request(cfg, tracker, out); + fixture fix; - std::string_view const - expected = "*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); + fix.run("*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n"); } void test_auth() { - subscription_tracker tracker; - request out; - config cfg; - cfg.clientname = ""; - cfg.username = "foo"; - cfg.password = "bar"; - - compose_setup_request(cfg, tracker, out); - - std::string_view const - expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); + fixture fix; + fix.cfg.clientname = ""; + fix.cfg.username = "foo"; + fix.cfg.password = "bar"; + + fix.run("*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"); } void test_auth_empty_password() { - subscription_tracker tracker; - request out; - config cfg; - cfg.clientname = ""; - cfg.username = "foo"; + fixture fix; + fix.cfg.clientname = ""; + fix.cfg.username = "foo"; - compose_setup_request(cfg, tracker, out); - - std::string_view const - expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$0\r\n\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); + fix.run("*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$0\r\n\r\n"); } void test_auth_setname() { - subscription_tracker tracker; - request out; - config cfg; - cfg.clientname = "mytest"; - cfg.username = "foo"; - cfg.password = "bar"; + fixture fix; + fix.cfg.clientname = "mytest"; + fix.cfg.username = "foo"; + fix.cfg.password = "bar"; - compose_setup_request(cfg, tracker, out); - - std::string_view const expected = + fix.run( "*7\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$7\r\nSETNAME\r\n$" - "6\r\nmytest\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); + "6\r\nmytest\r\n"); } void test_use_setup() { - subscription_tracker tracker; - request out; - config cfg; - cfg.clientname = "mytest"; - cfg.username = "foo"; - cfg.password = "bar"; - cfg.database_index = 4; - cfg.use_setup = true; - cfg.setup.push("SELECT", 8); - - compose_setup_request(cfg, tracker, out); - - std::string_view const expected = + fixture fix; + fix.cfg.clientname = "mytest"; + fix.cfg.username = "foo"; + fix.cfg.password = "bar"; + fix.cfg.database_index = 4; + fix.cfg.use_setup = true; + fix.cfg.setup.push("SELECT", 8); + + fix.run( "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n" - "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); + "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n"); } // Regression check: we set the priority flag void test_use_setup_no_hello() { - subscription_tracker tracker; - request out; - config cfg; - cfg.use_setup = true; - cfg.setup.clear(); - cfg.setup.push("SELECT", 8); + fixture fix; + fix.cfg.use_setup = true; + fix.cfg.setup.clear(); + fix.cfg.setup.push("SELECT", 8); - compose_setup_request(cfg, tracker, out); - - std::string_view const expected = "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); + fix.run("*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n"); } // Regression check: we set the relevant cancellation flags in the request void test_use_setup_flags() { - subscription_tracker tracker; - request out; - config cfg; - cfg.use_setup = true; - cfg.setup.clear(); - cfg.setup.push("SELECT", 8); - cfg.setup.get_config().cancel_if_unresponded = false; - cfg.setup.get_config().cancel_on_connection_lost = false; - - compose_setup_request(cfg, tracker, out); - - std::string_view const expected = "*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); + fixture fix; + fix.cfg.use_setup = true; + fix.cfg.setup.clear(); + fix.cfg.setup.push("SELECT", 8); + fix.cfg.setup.get_config().cancel_if_unresponded = false; + fix.cfg.setup.get_config().cancel_on_connection_lost = false; + + fix.run("*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n"); } // When using Sentinel, a ROLE command is added. This works // both with the old HELLO and new setup strategies. void test_sentinel_auth() { - subscription_tracker tracker; - request out; - config cfg; - cfg.sentinel.addresses = { + fixture fix; + fix.cfg.sentinel.addresses = { {"localhost", "26379"} }; - cfg.clientname = ""; - cfg.username = "foo"; - cfg.password = "bar"; - - compose_setup_request(cfg, tracker, out); + fix.cfg.clientname = ""; + fix.cfg.username = "foo"; + fix.cfg.password = "bar"; - std::string_view const expected = + fix.run( "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n" - "*1\r\n$4\r\nROLE\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); + "*1\r\n$4\r\nROLE\r\n"); } void test_sentinel_use_setup() { - subscription_tracker tracker; - request out; - config cfg; - cfg.sentinel.addresses = { + fixture fix; + fix.cfg.sentinel.addresses = { {"localhost", "26379"} }; - cfg.use_setup = true; - cfg.setup.push("SELECT", 42); - - compose_setup_request(cfg, tracker, out); + fix.cfg.use_setup = true; + fix.cfg.setup.push("SELECT", 42); - std::string_view const expected = + fix.run( "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n" "*2\r\n$6\r\nSELECT\r\n$2\r\n42\r\n" - "*1\r\n$4\r\nROLE\r\n"; - BOOST_TEST_EQ(out.payload(), expected); - BOOST_TEST(out.has_hello_priority()); - BOOST_TEST(out.get_config().cancel_if_unresponded); - BOOST_TEST(out.get_config().cancel_on_connection_lost); + "*1\r\n$4\r\nROLE\r\n"); } } // namespace From ba3f3295462b17449f9706f841526147c713c867 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:48:32 +0100 Subject: [PATCH 59/63] verify leftover cleanup --- test/test_compose_setup_request.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_compose_setup_request.cpp b/test/test_compose_setup_request.cpp index d085f002c..794fa3f15 100644 --- a/test/test_compose_setup_request.cpp +++ b/test/test_compose_setup_request.cpp @@ -35,6 +35,8 @@ struct fixture { void run(std::string_view expected_payload, boost::source_location loc = BOOST_CURRENT_LOCATION) { + out.push("PING", "leftover"); // verify that we clear the request + compose_setup_request(cfg, tracker, out); if (!BOOST_TEST_EQ(out.payload(), expected_payload)) From 37ca966e235b4bf44fd0288e41fb87ebafc9312a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 19:54:35 +0100 Subject: [PATCH 60/63] new tests --- test/test_compose_setup_request.cpp | 57 ++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/test/test_compose_setup_request.cpp b/test/test_compose_setup_request.cpp index 794fa3f15..566799502 100644 --- a/test/test_compose_setup_request.cpp +++ b/test/test_compose_setup_request.cpp @@ -149,8 +149,41 @@ void test_use_setup_flags() fix.run("*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n"); } +// If we have tracked subscriptions, these are added at the end +void test_tracked_subscriptions() +{ + fixture fix; + fix.cfg.clientname = ""; + + // Populate the tracker + request sub_req; + sub_req.subscribe({"ch1", "ch2"}); + fix.tracker.commit_changes(sub_req); + + fix.run( + "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n" + "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"); +} + +void test_tracked_subscriptions_use_setup() +{ + fixture fix; + fix.cfg.use_setup = true; + fix.cfg.setup.clear(); + fix.cfg.setup.push("PING", "value"); + + // Populate the tracker + request sub_req; + sub_req.subscribe({"ch1", "ch2"}); + fix.tracker.commit_changes(sub_req); + + fix.run( + "*2\r\n$4\r\nPING\r\n$5\r\nvalue\r\n" + "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"); +} + // When using Sentinel, a ROLE command is added. This works -// both with the old HELLO and new setup strategies. +// both with the old HELLO and new setup strategies, and with tracked subscriptions void test_sentinel_auth() { fixture fix; @@ -181,6 +214,25 @@ void test_sentinel_use_setup() "*1\r\n$4\r\nROLE\r\n"); } +void test_sentinel_tracked_subscriptions() +{ + fixture fix; + fix.cfg.clientname = ""; + fix.cfg.sentinel.addresses = { + {"localhost", "26379"} + }; + + // Populate the tracker + request sub_req; + sub_req.subscribe({"ch1", "ch2"}); + fix.tracker.commit_changes(sub_req); + + fix.run( + "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n" + "*1\r\n$4\r\nROLE\r\n" + "*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"); +} + } // namespace int main() @@ -194,8 +246,11 @@ int main() test_use_setup(); test_use_setup_no_hello(); test_use_setup_flags(); + test_tracked_subscriptions(); + test_tracked_subscriptions_use_setup(); test_sentinel_auth(); test_sentinel_use_setup(); + test_sentinel_tracked_subscriptions(); return boost::report_errors(); } \ No newline at end of file From 66a48591d3044cbfd55e45540c222080d1a5c593 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 20:11:43 +0100 Subject: [PATCH 61/63] static_assert --- include/boost/redis/request.hpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index d73ec4cb4..944494d56 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -10,9 +10,11 @@ #include #include +#include #include #include #include +#include #include // NOTE: For some commands like hset it would be a good idea to assert @@ -737,6 +739,12 @@ class request { ForwardIt channels_begin, ForwardIt channels_end) { + static_assert( + std::is_convertible_v< + typename std::iterator_traits::value_type, + std::string_view>, + "subscribe, psubscribe, unsubscribe and punsubscribe should be passed ranges of elements " + "convertible to std::string_view"); if (channels_begin == channels_end) return; From 30da6c5e2c3dd7b92df953c1af3be5d818c0dfaa Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 20:18:26 +0100 Subject: [PATCH 62/63] Update reference docs --- include/boost/redis/request.hpp | 145 +++++++++++++++++--------------- 1 file changed, 79 insertions(+), 66 deletions(-) diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index 944494d56..d3104a785 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -438,15 +438,16 @@ class request { * If `channels` contains `{"ch1", "ch2"}`, the resulting command * is `SUBSCRIBE ch1 ch2`. * - * SUBSCRIBE commands created using this function are tracked + * Subscriptions created using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed channels is stored - * in the connection. Every time a reconnection happens, - * a suitable `SUBSCRIBE` command is issued automatically, + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, * to restore the subscriptions that were active before the reconnection. * - * PubSub store restoration only happens when using `subscribe`. - * Use @ref push or @ref push_range to disable it. + * PubSub store restoration only happens when using @ref subscribe, + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ void subscribe(std::initializer_list channels) { @@ -459,16 +460,16 @@ class request { * If `channels` contains `["ch1", "ch2"]`, the resulting command * is `SUBSCRIBE ch1 ch2`. * - * SUBSCRIBE commands created using this function are tracked + * Subscriptions created using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed channels is stored - * in the connection. Every time a reconnection happens, - * a suitable `SUBSCRIBE` command is issued automatically, + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ template void subscribe(Range&& channels, decltype(std::cbegin(channels))* = nullptr) @@ -484,16 +485,16 @@ class request { * If the range contains `["ch1", "ch2"]`, the resulting command * is `SUBSCRIBE ch1 ch2`. * - * SUBSCRIBE commands created using this function are tracked + * Subscriptions created using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed channels is stored - * in the connection. Every time a reconnection happens, - * a suitable `SUBSCRIBE` command is issued automatically, + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ template void subscribe(ForwardIt channels_begin, ForwardIt channels_end) @@ -507,14 +508,16 @@ class request { * If `channels` contains `{"ch1", "ch2"}`, the resulting command * is `UNSUBSCRIBE ch1 ch2`. * - * UNSUBSCRIBE commands created using this function are tracked + * Subscriptions removed using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed channels tracked by the - * connection is updated. + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ void unsubscribe(std::initializer_list channels) { @@ -527,14 +530,16 @@ class request { * If `channels` contains `["ch1", "ch2"]`, the resulting command * is `UNSUBSCRIBE ch1 ch2`. * - * UNSUBSCRIBE commands created using this function are tracked + * Subscriptions removed using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed channels tracked by the - * connection is updated. + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ template void unsubscribe(Range&& channels, decltype(std::cbegin(channels))* = nullptr) @@ -550,14 +555,16 @@ class request { * If the range contains `["ch1", "ch2"]`, the resulting command * is `UNSUBSCRIBE ch1 ch2`. * - * UNSUBSCRIBE commands created using this function are tracked + * Subscriptions removed using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed channels tracked by the - * connection is updated. + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ template void unsubscribe(ForwardIt channels_begin, ForwardIt channels_end) @@ -575,16 +582,16 @@ class request { * If `patterns` contains `{"news.*", "events.*"}`, the resulting command * is `PSUBSCRIBE news.* events.*`. * - * PSUBSCRIBE commands created using this function are tracked + * Subscriptions created using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed patterns is stored - * in the connection. Every time a reconnection happens, - * a suitable `PSUBSCRIBE` command is issued automatically, + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ void psubscribe(std::initializer_list patterns) { @@ -597,16 +604,16 @@ class request { * If `patterns` contains `["news.*", "events.*"]`, the resulting command * is `PSUBSCRIBE news.* events.*`. * - * PSUBSCRIBE commands created using this function are tracked + * Subscriptions created using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed patterns is stored - * in the connection. Every time a reconnection happens, - * a suitable `PSUBSCRIBE` command is issued automatically, + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ template void psubscribe(Range&& patterns, decltype(std::cbegin(patterns))* = nullptr) @@ -622,16 +629,16 @@ class request { * If the range contains `["news.*", "events.*"]`, the resulting command * is `PSUBSCRIBE news.* events.*`. * - * PSUBSCRIBE commands created using this function are tracked + * Subscriptions created using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed patterns is stored - * in the connection. Every time a reconnection happens, - * a suitable `PSUBSCRIBE` command is issued automatically, + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ template void psubscribe(ForwardIt patterns_begin, ForwardIt patterns_end) @@ -649,14 +656,16 @@ class request { * If `patterns` contains `{"news.*", "events.*"}`, the resulting command * is `PUNSUBSCRIBE news.* events.*`. * - * PUNSUBSCRIBE commands created using this function are tracked + * Subscriptions removed using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed patterns tracked by the - * connection is updated. + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ void punsubscribe(std::initializer_list patterns) { @@ -669,14 +678,16 @@ class request { * If `patterns` contains `["news.*", "events.*"]`, the resulting command * is `PUNSUBSCRIBE news.* events.*`. * - * PUNSUBSCRIBE commands created using this function are tracked + * Subscriptions removed using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed patterns tracked by the - * connection is updated. + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ template void punsubscribe(Range&& patterns, decltype(std::cbegin(patterns))* = nullptr) @@ -692,14 +703,16 @@ class request { * If the range contains `["news.*", "events.*"]`, the resulting command * is `PUNSUBSCRIBE news.* events.*`. * - * PUNSUBSCRIBE commands created using this function are tracked + * Subscriptions removed using this function are tracked * to enable PubSub state restoration. After successfully executing - * the request, the list of subscribed patterns tracked by the - * connection is updated. + * the request, the connection will store any newly subscribed channels and patterns. + * Every time a reconnection happens, + * a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically, + * to restore the subscriptions that were active before the reconnection. * * PubSub store restoration only happens when using @ref subscribe, - * @ref unsubscribe, @ref psubscribe and @ref punsubscribe. - * Use @ref push or @ref push_range to disable it. + * @ref unsubscribe, @ref psubscribe or @ref punsubscribe. + * Subscription commands added by @ref push or @ref push_range are not tracked. */ template void punsubscribe(ForwardIt patterns_begin, ForwardIt patterns_end) From 3b9b7d2aa42eef477ab3ce6f04c21cfd88f05e81 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 8 Jan 2026 20:34:11 +0100 Subject: [PATCH 63/63] fix test_setup_adapter --- test/test_setup_adapter.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/test_setup_adapter.cpp b/test/test_setup_adapter.cpp index 694700eb2..fab4ce95e 100644 --- a/test/test_setup_adapter.cpp +++ b/test/test_setup_adapter.cpp @@ -30,7 +30,7 @@ void test_success() connection_state st; st.cfg.use_setup = true; st.cfg.setup.push("SELECT", 2); - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to HELLO @@ -55,7 +55,7 @@ void test_simple_error() // Setup connection_state st; st.cfg.use_setup = true; - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to HELLO contains an error @@ -73,7 +73,7 @@ void test_blob_error() connection_state st; st.cfg.use_setup = true; st.cfg.setup.push("SELECT", 1); - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to HELLO @@ -97,7 +97,7 @@ void test_null() // Setup connection_state st; st.cfg.use_setup = true; - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to HELLO @@ -129,7 +129,7 @@ void test_sentinel_master() st.cfg.sentinel.addresses = { {"localhost", "26379"} }; - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to HELLO @@ -164,7 +164,7 @@ void test_sentinel_replica() {"localhost", "26379"} }; st.cfg.sentinel.server_role = role::replica; - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to HELLO @@ -193,7 +193,7 @@ void test_sentinel_role_check_failed_master() st.cfg.sentinel.addresses = { {"localhost", "26379"} }; - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to HELLO @@ -222,7 +222,7 @@ void test_sentinel_role_check_failed_replica() {"localhost", "26379"} }; st.cfg.sentinel.server_role = role::replica; - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to HELLO @@ -252,7 +252,7 @@ void test_sentinel_role_error_node() st.cfg.sentinel.addresses = { {"localhost", "26379"} }; - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to ROLE @@ -273,7 +273,7 @@ void test_sentinel_role_not_array() st.cfg.sentinel.addresses = { {"localhost", "26379"} }; - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to ROLE @@ -294,7 +294,7 @@ void test_sentinel_role_empty_array() st.cfg.sentinel.addresses = { {"localhost", "26379"} }; - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to ROLE @@ -315,7 +315,7 @@ void test_sentinel_role_first_element_not_string() st.cfg.sentinel.addresses = { {"localhost", "26379"} }; - compose_setup_request(st.cfg); + compose_setup_request(st.cfg, st.tracker, st.setup_req); setup_adapter adapter{st}; // Response to ROLE