2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-19 16:52:08 +00:00

Compare commits

..

213 Commits

Author SHA1 Message Date
Anarthal (Rubén Pérez)
18ee72830b Makes flat_tree a proper container (#383)
Removes flat_tree::get_view()
Adds flat_tree{iterator, reverse_iterator, begin, end, rbegin, rend, data, operator[], at, front, back, size, empty}

close #362
2026-01-18 22:08:18 +01:00
Anarthal (Rubén Pérez)
89e44dc017 Makes async_receive2 not cancel on reconnection (#381)
async_receive2 is now only cancelled after calling connection::cancel() or using per-operation cancellation
Adds a restriction to only have one outstanding async_receive2 operation per connection
Adds error::already_running
Adds support for asio::cancel_after for async_receive2
Deprecates cancel(operation::receive)
Adds more documentation to async_receive2

close #331
2026-01-18 16:14:53 +01:00
Anarthal (Rubén Pérez)
002b616dd9 Replaces --with-system with --with-headers when building Boost in CI (#382) 2026-01-18 15:44:55 +01:00
Anarthal (Rubén Pérez)
c11a5194d8 Reworks test_conn_push and test_conn_push2 (#380)
Adds a test to verify that async_receive (v1) is cancelled on reconnection
Adds a test to verify that a reconnection is triggered when the receive adapter generates an error
Makes the unsubscribe and push adapter tests live only in test_conn_push2, since they test functionality common to async_receive and async_receive2
Migrates all tests to lightweight_test

Entails no functional change
2026-01-14 21:54:34 +01:00
Anarthal (Rubén Pérez)
bea547481a Adds support for PubSub state restoration (#375)
Adds request::{subscribe, unsubscribe, psubscribe, punsubscribe}. When requests created with these functions are executed successfully, the created subscriptions are tracked and restore on re-connection.

close #367
2026-01-09 21:08:54 +01:00
Anarthal (Rubén Pérez)
3b07119e54 Makes flat_tree aware of incremental parsing to avoid race conditions with pushes (#378)
Adds the concept of a "temporary working area" to flat_tree. Nodes in this area belong to a partially parsed message, and are hidden from the user. Now flat_tree can be used as the receive response without explicitly handling partial messages.
Changes flat_tree::get_view return type from const vector& to span.
Adds flat_tree::capacity.
Splits generic_flat_response tests to a separate file and adds extra cases.

close #369
2026-01-07 09:55:23 +01:00
Anarthal (Rubén Pérez)
7750a6b126 Splits read_buffer tests to a separate file (#368) 2025-12-03 08:53:03 +01:00
Anarthal (Rubén Pérez)
2bbf0090b5 Mark consume_one as deprecated (#365)
close #353
2025-12-02 11:20:56 +01:00
Anarthal (Rubén Pérez)
02632b31c6 Removes ostream inclusion from public headers (#364)
close #361
2025-12-02 11:20:32 +01:00
Anarthal (Rubén Pérez)
6005ebd04a Fixes std::tuple serialization and adds tests (#363)
* Fixes a problem that caused passing ranges containing tuples into `request::push_range` to generate invalid commands.
* Adds test_serialization
* Updates request reference docs to reflect the requirements of the types passed to push and push_range

close #360
2025-12-02 11:20:01 +01:00
Anarthal (Rubén Pérez)
755d14a10d Renames test_setup_request_utils to test_compose_setup_request (#359) 2025-11-29 22:26:37 +01:00
Anarthal (Rubén Pérez)
d9e4b2c720 Improves flat_tree implementation (#358)
* Makes flat_tree implementation use a custom buffer. This allows:
  * Never dangling nodes (previously, node values could dangle after calling reserve() or if notify_done() wasn't called).
  * Reduced memory consumption
  * Increased runtime speed
* Changes flat_tree assignment to the usual signature and semantics
* Fixes a bug causing an assertion to trigger when copy-constructing an empty flat_tree.
* Changes basic_node operator== and operator!= return type 
* Adds generic_flat_response, basic_tree, tree, view_tree, flat_tree to the reference page.
* Adds a missing resp3:: qualifier to all names in the reference page that belong to the resp3 namespace.
* Adds reference documentation to flat_tree.
* Mentions generic_flat_response in the discussion.
* Adds operator!= for basic_node to basic_node's reference page.
* Adds test_flat_tree.

close #357 
close #354 
close #352
2025-11-29 21:35:53 +01:00
Marcelo
91afb4a279 Fixes SBO problem and adds some tests (#356) 2025-11-20 14:38:49 +01:00
Anarthal (Rubén Pérez)
bdd9c327c1 Adds Sentinel support (#345)
close #237
close #269
close #268
close #229
2025-11-19 22:31:19 +01:00
Marcelo
00f3ec9b78 Merge pull request #340 from boostorg/263-marcelo
Concludes the work on the generic_flat_response started by Nikolai Vladimirov
2025-11-18 14:10:23 +01:00
Marcelo Zimbres
b365b96228 Fixup Adds generic_flat_response typedef 2025-11-10 23:21:16 +01:00
Marcelo Zimbres
2c1f1c4c50 Concludes the work started by Nikolai Vladimirov on the generic_flat_response 2025-11-09 21:37:39 +01:00
Nikolai Vladimirov
6ff474008f Cosmetic changes 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
bd799aff96 create flat_response_value::add_node() 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
c284960549 Replaced generic_response with generic_flat_response in tests 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
b1420d3d1d Adjust code base to new changes 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
84fa39918f Added API for generic_flat_response, replaced generic_response with new response type in tests 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
019a080b65 Get rid of type erasion in details::prepare_done 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
53e5ae0cd4 Avoid turning throwing consume_one into template 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
1d9f9ab0e3 Moved implementation of push_back to general_aggregate 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
5444e077f9 Defined done_fn_type in adapters.hpp 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
ecd1573257 Call prepare_done function to form response 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
9419c857fd Corrected and cleaner implementation with comments 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
b78cf818e0 reserve function 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
7d959c1039 Addressed some comments 2025-11-08 21:37:40 +01:00
Nikolai Vladimirov
ccb17f89cd Draft version without consume_one implementation 2025-11-08 21:37:40 +01:00
Anarthal (Rubén Pérez)
c9375a44eb Adds release notes for Boost 1.89 and Boost 1.90 (#347) 2025-11-03 14:53:32 +01:00
Anarthal (Rubén Pérez)
c88f9f4666 Fixes a potential use-after-move bug in async_connect (#343) 2025-10-30 14:49:57 +01:00
Anarthal (Rubén Pérez)
682bec618d Adds request::append() (#342)
close #341
2025-10-27 15:18:46 +01:00
Anarthal (Rubén Pérez)
6791e759f9 Adds support for PUNSUBSCRIBE (#339)
Adds a test covering UNSUBSCRIBE and PUNSUBSCRIBE

close #306
2025-10-25 00:41:49 +02:00
Anarthal (Rubén Pérez)
0460b38e14 Simplifies setup_request_utils.hpp (#338)
Removes unused clear_response
Removes include in connection.hpp
Moves the header to impl/ and makes functions inline
2025-10-23 22:37:50 +02:00
Anarthal (Rubén Pérez)
033f6aaa62 Moves all logging logic to FSMs (#335)
Replaces connection_logger by buffered_logger and log_utils.hpp
Adds a system to log arbitrary values without defining new logger methods
2025-10-22 11:59:59 +02:00
Anarthal (Rubén Pérez)
42411be444 Changes request default's cancel_on_connection_lost to false (#334)
Changes request default constructor to set cancel_on_connection_lost to false, matching request::config's default initializer
Overwrites this flag to true for the setup request
Adds unit tests for the latter
2025-10-20 16:37:02 +02:00
Anarthal (Rubén Pérez)
6be0d122fb Moves the setup request execution to run_fsm (#333)
Adds unit tests to cover setup request execution in run_fsm
Entails no functional change
2025-10-20 15:56:46 +02:00
Anarthal (Rubén Pérez)
2b09ecbd78 Makes health checks flexible so they don't tear down connections under heavy load (#328)
Adds error::write_timeout

close #104
2025-10-20 15:29:20 +02:00
Anarthal (Rubén Pérez)
da09787d29 Moves logging into reader_fsm (#332)
* Removes logging all the reader actions, and logs specific messages inside the reader_fsm instead
* Adds constructors to reader actions
* Makes reader_fsm use connection_state
* Refactors reader_fsm tests
* Moves exec_fsm action printing to test code
2025-10-15 17:36:54 +02:00
Anarthal (Rubén Pérez)
f683e368dd Implements async_run as a FSM and adds tests (#330)
* Implements async_run as a FSM and adds tests
* Places all sans-io variables in connection_impl in a connection_state struct

Entails no functional change.
2025-10-13 22:19:39 +02:00
Anarthal (Rubén Pérez)
28ed27ce72 Changes cancel_on_connection_lost default to false and deprecates it (#329)
* Changes cancel_on_connection_lost default to false
* Deprecates cancel_on_connection_lost and cancel_if_not_connected
* Fixes a TODO in test_conn_exec_cancel

close #323
2025-10-11 13:30:41 +02:00
Anarthal (Rubén Pérez)
35fa68b926 Improves the cancellation discussion page (#327)
Fixes inaccuracies in request::config::cancel_if_unresponded
2025-10-09 12:11:43 +02:00
Anarthal (Rubén Pérez)
228b31917c Implements the writer as an FSM and adds tests (#325)
* Refactors the writer task into a FSM and adds unit tests.
* Adds a testing utility to check logging.

Entails no functional change (other than cosmetic word fixes to the logs).
2025-10-09 11:31:36 +02:00
Anarthal (Rubén Pérez)
d3e335942f Adds support for asio::cancel_after (#324)
* Adds support for asio::cancel_after in connection::{async_exec, async_run}
* Adds cancel_after tests
* Adds an example on using asio::cancel_after
* Adds a discussion page on timeouts and the `cancel_if_unresponded` flag

close #226
2025-10-06 18:11:25 +02:00
Anarthal (Rubén Pérez)
0c159280ba Implements connect as a FSM and fixes cancellation (#320)
Implements redis_stream::async_connect as a FSM
Adds per-operation cancellation handling code
Adds tests
2025-10-06 12:39:28 +02:00
Anarthal (Rubén Pérez)
1812be87bf Makes the CI run part of the examples (#322)
Removes a conditional from CMake that was causing examples to never be run under CI.
2025-10-03 20:08:58 +02:00
Anarthal (Rubén Pérez)
5771128f2d Adds per-operation cancellation support to async_run and cleans up cancellation (#321)
* Adds support for terminal and partial cancellation to async_run.
* Makes basic_connection::cancel use per-operation cancellation under the hood.
* Fixes a number of race conditions during cancellation which could cause the cancellation to be ignored. This could happen if the cancellation is delivered while an async handler is pending execution.
* Deprecates operation::{resolve, connect, ssl_handshake, reconnection, health_check}, in favor of operation::run. Calling basic_connection::cancel with these values (excepting reconnection) is now equivalent to using operation::run.
* Fixes a problem in the health checker that caused ping timeouts to be reported as cancellations.
* Sanitizes how the parallel group computes its final error code.
* Simplifies the reader, writer and health checker to not care about connection cancellation. This is now the responsibility of the parallel group.
* Removes an unnecessary setup_cancellation action in the reader FSM.
* Adds documentation regarding per-operation cancellation to async_receive.
* Adds additional health checker tests.
* Adds async_run per-operation cancellation tests.
* Adds reader FSM cancellation tests.
* Makes test_conn_exec_retry tests more resilient.
* Removes leftovers in the UNIX and TLS reconnection tests. These were required due to race conditions that have already been fixed.

close #318 
close #319
2025-10-03 18:48:30 +02:00
Marcelo
2babb79425 Merge pull request #311 from boostorg/refactoring_clean_code
Simplifies the read_buffer and add rotated bytes to usage.
2025-09-29 21:50:19 +02:00
Marcelo Zimbres
a70bdf6574 Simplifies the read_buffer and adds rotated bytes to usage.
Data rotation in the read buffer creates latency, we know it
is preset but so far its magnitude was unknown. This PR adds
it as a new field to the usage struct. For example, the
test_conn_echo_stress outputs now

   Commands sent: 780,002
   Bytes sent: 32,670,085
   Responses received: 780,001
   Pushes received: 750,001
   Bytes received (response): 3,210,147
   Bytes received (push): 32,250,036
   Bytes rotated: 3,109,190,184

In total approximately 34Mb are received but 3Gb are
rotated.
2025-09-28 13:02:12 +02:00
Anarthal (Rubén Pérez)
e414b3941a Simplifies the health checker (#317)
Modifies the health checker to use asio::cancel_after rather than a separate parallel group task
2025-09-27 19:08:19 +02:00
Anarthal (Rubén Pérez)
beab3f69ed Reduces the time elapsed by test_conn_health_check and adds test_conn_monitor (#316)
Splits test_conn_health_check into a test to verify that the health checker works and another to verify that MONITOR is handled correctly.
Tests no longer wait or use CLIENT PAUSE
2025-09-27 18:13:02 +02:00
Anarthal (Rubén Pérez)
f955dc01d2 Adds reliable cancellation support to async_exec (#310)
* Terminal cancellation in async_exec no longer tears down the connection when the cancelled request has been sent to the server.
* Adds support for partial cancellation in async_exec with the same semantics.
2025-09-26 12:50:42 +02:00
Anarthal (Rubén Pérez)
bcf120bd8f Increases antora log level and fixes a broken link (#315)
close #313
2025-09-24 12:53:42 +02:00
Anarthal (Rubén Pérez)
203e9298ed Fixes a race condition when cancelling requests on connection lost (#309)
Changes how cancel_on_conn_lost is used to ensure it is called only once and after the reader and writer tasks have exited.
This fixes a problem in test_conn_reconnect
Adds a test for multiplexer::reset()
Adds stronger invariants to the multiplexer functions to be called by the reader and writer
Removes test_issue_181, since the same functionality is being covered by unit tests already
Removes basic_connection::run_is_canceled
2025-09-22 13:04:28 +02:00
Anarthal (Rubén Pérez)
8da18379ba Replaces tribool by an enum and adds coverage for multiplexer (#301)
* In the context of the multiplexer, replaces tribool by consume_result to enhance readability and make values smaller
* Splits the multiplexer tests out of test_low_level_sync_sans_io into a separate test file
* Increases testing coverage for the multiplexer class

Entails no functional change.
2025-09-18 13:55:29 +02:00
Anarthal (Rubén Pérez)
40417a13b2 Deprecates request::config::hello_with_priority (#305) 2025-09-16 00:49:33 +02:00
Anarthal (Rubén Pérez)
74909be47d Removes CMakePresets.json (#304) 2025-09-15 14:19:04 +02:00
Anarthal (Rubén Pérez)
6a1a07f95a Adds custom setup requests (#303)
Adds config::setup and config::use_setup, to run arbitrary Redis commands on connection establishment
Improves docs for config::{username, password, clientname, database_index}
Splits all connection establishment tests into test_conn_hello

close #302
2025-09-15 14:15:11 +02:00
Anarthal (Rubén Pérez)
0cf2441ed2 Removes handshaker in favor of asio::deferred (#291)
Refactors the handshake process to use asio::deferred instead of a custom composed operation.
Fixes logging on HELLO error (close #297)
Fixes a potential problem on reconnection after a HELLO error is encountered (close #290)
Fixes a race condition in the health checker that could cause it to never exit on cancellation
Adds support for users with a username different than "default" and an empty password (close #298)
Adds integration testing for authentication
Adds unit testing for the hello utility functions
2025-09-04 16:48:00 +02:00
Anarthal (Rubén Pérez)
2133ed747b Fixes an exception when parsing intercalated errors and success messages into generic_response (#289)
close #287
2025-09-01 20:18:35 +02:00
Anarthal (Rubén Pérez)
1f6c6bd64d Adds Valkey CIs and docs (#296)
* Adds Valkey CIs

Modifies existing builds to use different database flavors and versions:
* Redis 7.4.5
* Redis 8.2.1
* Valkey 8.1.3

* Update docs
2025-09-01 20:14:26 +02:00
Marcelo
da2f0101d0 Merge pull request #286 from boostorg/parser_events
Add parse event init, node and done.
2025-08-10 17:04:26 +02:00
Marcelo Zimbres
a76a621b0b More code review changes. 2025-08-02 13:38:19 +02:00
Marcelo Zimbres
16bf57cf33 Add parse event init, node and done. 2025-07-26 22:44:53 +02:00
Anarthal (Rubén Pérez)
88d8f3c0ca Makes all objects in connection have a stable address (#285)
Moves read_buffer memory reservation to the connection's constructor
Makes read_buffer memory reservation size be a power of 2
2025-07-25 22:51:14 +02:00
Marcelo
20ab2c7e70 Merge pull request #283 from boostorg/refactoring_clean_code
Removes async_append_some
2025-07-22 20:51:50 +02:00
Marcelo Zimbres
8ee2213efe Code review changes. 2025-07-20 18:38:02 +02:00
Marcelo Zimbres
97d71d1d6b Removes async_append_some. 2025-07-17 12:37:17 +02:00
Anarthal (Rubén Pérez)
76129bbcb8 Merge pull request #282 from boostorg/develop
Migrates the documentation to Asciidoc/MrDocs (#276)
2025-07-08 21:07:52 +02:00
Anarthal (Rubén Pérez)
adf17f2b3b Migrates the documentation to Asciidoc/MrDocs (#276)
Fixes some typos

close #247
2025-07-02 23:27:33 +02:00
Marcelo
6499818911 Merge pull request #280 from boostorg/develop
Sync master with develop for boost release
2025-07-02 13:27:48 +02:00
Marcelo Zimbres
963ae8d145 Disables msvc-14.2 builds 2025-07-02 13:09:58 +02:00
Anarthal (Rubén Pérez)
fe4ba64a97 Makes the reconnection loop retry on errors (#275)
After an error is encountered during name resolution, connection establishment, SSL handshake, Redis hello, reading or writing, async_run will now keep retrying if the configuration requests it

close #255
2025-06-29 19:02:22 +02:00
Marcelo
b82224700e Merge pull request #274 from boostorg/refactoring_clean_code
Adds a sans-io fsm to the read operation.
2025-06-23 22:40:42 +02:00
Marcelo Zimbres
620b1e9510 Adds a sans-io fsm for read operation. 2025-06-23 22:24:46 +02:00
Anarthal (Rubén Pérez)
f04d97ffa5 Updates the Logger interface to allow extensibility and type erasure (#273)
Removes all the logger::on_xxx functions
Removes the Logger template parameter to async_run
Adds a logger constructor that allows passing a std::function to customize logging behavior
Adds constructors to connection and basic_connection taking a logger
Deprecates config::logger_prefix
Deprecates the async_run overload taking a logger parameter
Deprecates the basic_connection::async_run overload not taking any config object
Deprecates the basic_connection::next_layer_type typedef
Makes the default log level logger::info
Makes the logging thread-safe
Cleans up deprecated functionality from examples
Adds docs on logging
Adds an example on how to integrate spdlog into Boost.Redis logging

close #213
2025-06-23 12:07:21 +02:00
Anarthal (Rubén Pérez)
7304d99bf6 Adds support for UNIX sockets (#272)
close #246
2025-06-20 13:23:39 +02:00
Anarthal (Rubén Pérez)
89a42dbf74 async_exec now uses a sans-io strategy (#250)
async_exec now supports partial and total cancellation when the request is pending
Added docs on async_exec's cancellation support
Added detail::exec_fsm and macros for coroutine emulation
Requests are now removed from the multiplexer when an error is encountered
2025-06-10 20:42:59 +02:00
Anarthal (Rubén Pérez)
a39d130240 Adds a redis_stream class that encapsulates connection, reading and writing (#266)
Deprecates get_ssl_context for connection and basic_connection
Deprecates reset_stream for connection and basic_connection
2025-06-07 19:15:42 +02:00
Anarthal (Rubén Pérez)
6d5b550bb3 Restores the TLS tests in CI (#267)
Renews test certificates
Recovers and refactors test_conn_tls.cpp
Adds a test for TLS reconnection
2025-06-07 18:37:09 +02:00
Anarthal (Rubén Pérez)
4a2085c800 Uses ignore as default for connection::async_exec (#265)
Updates test_conn_echo_stress.cpp to use this default

close #264
2025-06-07 13:21:20 +02:00
Anarthal (Rubén Pérez)
2fc54bc73b Increases the robustness of integration tests (#259)
Updates multiplexer to make requests complete with error_code() rather than error_code(0)
Integration tests now use io_context::run_for to run with a timeout and avoid deadlocks
Tests now use concrete lambdas where generic ones are not required
Tests now use BOOST_TEST with operator== to print values on error
Tests now use anonymous namespaces to detect dead code
Adds run_coroutine_test and removed start from common.hpp
Updates test_reconnect to perform relevant checks
Refactors how test_issue_50 launches its coroutines
Groups tests in CMake as unit or integration
Updates Jamfile tests to contain all unit tests and only unit tests
2025-06-06 12:48:40 +02:00
Anarthal (Rubén Pérez)
0c8c6fcc09 Fixes a memory corruption in logger and adds sanitizer builds to CI (#261)
logger now owns the prefix string
This fixes a use-after-stack-return memory error in async_run
Adds a clang-19 and a gcc-14 build to CI with the address and undefined behavior sanitizer enabled

close #260
2025-06-01 13:40:37 +02:00
Marcelo
479068e778 Merge pull request #258 from boostorg/refactoring_clean_code
Refactors reader_op to simplify sans-io.
2025-05-29 14:52:04 +02:00
Marcelo Zimbres
35d50700b9 Refactors reader_op to simplify sans-io. 2025-05-25 12:04:28 +02:00
Anarthal (Rubén Pérez)
b58e4f94de Marks the next_layer() functions as deprecated (#256)
CI builds now use BOOST_ALLOW_DEPRECATED to prevent warnings
2025-05-25 11:05:07 +02:00
Anarthal (Rubén Pérez)
e8b13bd7a0 Enables -Wall and -Werror in CIs (#254)
Removed warnings in:

* bench/echo_server_client.cpp (parameter shadowing)
* include/boost/redis/adapter/detail/adapters.hpp (unused parameter)
* include/boost/redis/connection.hpp (unused parameter)
* include/boost/redis/resp3/impl/parser.ipp (signed to unsigned conversion)
* test/test_conversions.cpp (signed to unsigned comparison)
* test/test_issue_50.cpp (superfluous move)
* test/test_low_level_sync_sans_io.cpp (signed to unsigned comparison)
2025-05-20 20:30:15 +02:00
Anarthal (Rubén Pérez)
c1c50e3e24 Reconnection no longer causes the writer to perform busy waiting (#253)
Reconnection now uses a separate timer to wait

close #252
2025-05-19 16:46:56 +02:00
Anarthal (Rubén Pérez)
328ad97a79 Adds automatic formatting with clang-format 2025-05-12 13:49:55 +02:00
Marcelo
1060733b84 Merge pull request #248 from boostorg/refactoring_clean_code
Sans-io multiplexing
2025-05-10 20:52:10 +02:00
Marcelo Zimbres
f9d0679be5 Splits the multiplexer out of the connection. 2025-05-04 11:39:34 +02:00
Marcelo
11e54de8f8 Merge pull request #244 from boostorg/develop
Sync master with develop
2025-04-26 20:07:46 +02:00
Marcelo Zimbres
109248d53b Removes unnecessary dynamic buffer. 2025-04-26 19:24:21 +02:00
Marcelo
f7b4ec291e Merge pull request #239 from anarthal/bugfix/238-int-conversions
Responses now admit all integer types again
2025-04-07 22:48:48 +02:00
Ruben Perez
c4e31e5f2f Floating point tests 2025-04-07 11:38:44 +02:00
Ruben Perez
f32da1b0e3 tests for boolean 2025-04-07 11:30:28 +02:00
Ruben Perez
8b98081ccf Fixed the problem 2025-04-07 11:21:56 +02:00
Ruben Perez
77ade122ea Failing test 2025-04-07 11:12:07 +02:00
Marcelo
746fb62619 Merge pull request #235 from boostorg/develop
Develop to master
2025-02-20 16:10:41 +01:00
Marcelo
ac4e6e29b3 Merge pull request #234 from boostorg/233-missing-implementation-for-stdvectorstdoptionalstdstring
Adds support for optional fields
2025-02-20 12:19:05 +01:00
Marcelo Zimbres
412f5535ea Adds support for optional fields 2025-02-19 13:09:34 +01:00
Marcelo
31ceed9f8f Merge pull request #228 from boostorg/225-support-default-completion-tokens-in-redisconnection
Uses asio::deferred_t as default completion type.
2025-02-08 22:26:11 +01:00
Marcelo Zimbres
e7c1b9ed5f Removes include of ciso646.h. 2025-02-08 22:00:48 +01:00
Marcelo Zimbres
35cfdae3f3 Unifies the signatures of async functions. 2025-02-08 20:44:48 +01:00
Marcelo Zimbres
3a74575ada Uses asio::deferred_t as default completion type. 2025-02-08 20:44:48 +01:00
Marcelo
6cdbd64eb8 Merge pull request #232 from striezel-stash/remove-executable-flags
Remove executable flags from key and certificate files
2025-02-08 20:39:17 +01:00
Marcelo
c21ac7569a Merge pull request #231 from striezel-stash/fix-typos
Fix some typos
2025-02-02 22:35:47 +01:00
Dirk Stolle
7dd5a77d4f Remove executable flags from key and certificate files
See <https://github.com/boostorg/admin/issues/47#issuecomment-2575165830>.

[ci skip]
2025-01-27 11:12:25 +01:00
Dirk Stolle
a53278aa2d Fix some typos 2025-01-27 11:04:57 +01:00
Marcelo
6abef73cb1 Merge pull request #227 from boostorg/refactoring_clean_code
Refactoring
2024-12-23 21:31:33 +01:00
Marcelo Zimbres
9a48633bdf Removes connection_base class. 2024-12-22 21:20:51 +01:00
Marcelo Zimbres
c615902559 Simplifications
- Removes cancellation support from async_run.
- Simplifies async operations.
- Removes async_run_lean.
- Remove support for implicit cancellation from health-checker.
- Reuses async_run parallel group for health-checker tasks.
- Moves the resolver from the runner to the connection.
- Moves the connector from the runner to the connection_base class.
- Moves the ssl handshaker to the connection_base class.
- Moves the health-check to the connection_base class.
- Simplifies cancel operations.
- Improvements in logging.
- Remove ssl handshaker.
- Removes run_op from runner and renames runner to resp3_handshaker.
2024-12-22 12:27:48 +01:00
Marcelo
d910557db4 Merge pull request #201 from anarthal/feature/type-erased-response
Adds a type-erased response adapter to the public API
2024-12-22 11:43:39 +01:00
Ruben Perez
5089eef376 Cleanup 2024-12-02 11:41:14 +01:00
Ruben Perez
8f4e980fd5 Merge branch 'develop' into feature/type-erased-response 2024-12-02 11:32:16 +01:00
Marcelo
8df535f50e Merge pull request #221 from boostorg/develop
Fixes for Boost 1.87
2024-11-03 13:15:52 +01:00
Marcelo Zimbres
26197e8300 Merge branch 'master' into develop 2024-11-02 17:57:59 +01:00
Marcelo Zimbres
f592073330 Remove unused coverage report script. 2024-11-02 17:48:50 +01:00
Marcelo Zimbres
32bc39a63a Change log level to debug. 2024-11-02 17:48:50 +01:00
Marcelo Zimbres
ca2c4efa35 Fix error assignment. 2024-11-02 17:48:50 +01:00
Marcelo Zimbres
7f9ac6f0fc Update release notes. 2024-11-02 17:48:50 +01:00
Marcelo Zimbres
d68c4583ed Update to support Asio latest changes. 2024-11-02 12:37:01 +01:00
Marcelo Zimbres
943cf0d740 Simplifies the connect operations. 2024-10-21 22:30:18 +02:00
Marcelo Zimbres
53cabf0745 Simplifies resolve timeout operation. 2024-10-21 22:30:18 +02:00
Marcelo Zimbres
c309c5776b Disentangles resolve and connect from run operations. 2024-10-21 22:30:18 +02:00
Marcelo Zimbres
a535862d51 Updates the copyright notice. 2024-10-21 22:30:18 +02:00
Marcelo Zimbres
dfecf0bd0a Fixes the adapter of empty nested responses.
See https://github.com/boostorg/redis/issues/210.
2024-10-21 22:30:18 +02:00
Marcelo Zimbres
ce4e5cbffa Improves reconnection responsiveness.
As the user points out in

   https://github.com/boostorg/redis/issues/205

wait_for_all might be the cause of delay in the
reconnection. I could not observe any improvement in my
local setup but wait_for_one_error look the correct token
and all tests pass so I am willing to change it.
2024-10-21 22:30:18 +02:00
René Ferdinand Rivera Morell
c370e930c5 Add support for modular build structure. (#204)
* Fixes issue 181.

* Refactors add_hello and adds unit tests.

* Adds endian to the list of dependencies

* Make the library modular usable.

* Some fixes in the article about the costs of async abstractions [skip ci]

* Switch to library requirements instead of source. As source puts extra source in install targets.

* Add missing import-search for cconfig/predef checks.

* Add requires-b2 check to top-level build file.

* Bump B2 require to 5.2

* Move inter-lib dependencies to a project variable and into the build targets.

* Update build deps.

* Fix spurious semi-colon.

---------

Co-authored-by: Marcelo Zimbres <mzimbres@gmail.com>
2024-10-21 22:30:18 +02:00
Marcelo Zimbres
e30bb373aa Removes gcc-10 from the list of supported compilers.
See https://github.com/boostorg/redis/issues/203.
2024-10-21 22:30:18 +02:00
Marcelo Zimbres
b8a52e5e61 Simplifies the connect operations. 2024-10-21 22:12:48 +02:00
Marcelo Zimbres
302d50e262 Simplifies resolve timeout operation. 2024-10-21 22:12:48 +02:00
Marcelo Zimbres
86015cbdde Disentangles resolve and connect from run operations. 2024-10-21 22:12:48 +02:00
Marcelo Zimbres
84ead6a3e4 Updates the copyright notice. 2024-10-21 22:12:48 +02:00
Marcelo Zimbres
dbfb72d853 Fixes the adapter of empty nested responses.
See https://github.com/boostorg/redis/issues/210.
2024-10-06 21:16:40 +02:00
Marcelo Zimbres
9039c3cd90 Improves reconnection responsiveness.
As the user points out in

   https://github.com/boostorg/redis/issues/205

wait_for_all might be the cause of delay in the
reconnection. I could not observe any improvement in my
local setup but wait_for_one_error look the correct token
and all tests pass so I am willing to change it.
2024-09-15 11:04:48 +02:00
René Ferdinand Rivera Morell
abde1afcb0 Add support for modular build structure. (#204)
* Fixes issue 181.

* Refactors add_hello and adds unit tests.

* Adds endian to the list of dependencies

* Make the library modular usable.

* Some fixes in the article about the costs of async abstractions [skip ci]

* Switch to library requirements instead of source. As source puts extra source in install targets.

* Add missing import-search for cconfig/predef checks.

* Add requires-b2 check to top-level build file.

* Bump B2 require to 5.2

* Move inter-lib dependencies to a project variable and into the build targets.

* Update build deps.

* Fix spurious semi-colon.

---------

Co-authored-by: Marcelo Zimbres <mzimbres@gmail.com>
2024-08-18 22:46:53 +02:00
Ruben Perez
3adac158ab Missing include 2024-08-07 10:56:23 +02:00
Ruben Perez
63aad808b6 Added test to Jamfile 2024-08-07 10:51:13 +02:00
Ruben Perez
90a07a1e07 test_any_adapter 2024-08-07 10:50:44 +02:00
Ruben Perez
585988bb61 Test for connection::async_exec 2024-08-07 10:37:47 +02:00
Ruben Perez
b5e90ea5f4 Fixed async_initiate signature 2024-08-07 10:19:42 +02:00
Ruben Perez
42d1250b53 reference fixes 2024-08-07 09:58:30 +02:00
Ruben Perez
299e7ca3a8 Reference docs (2) 2024-08-07 09:52:36 +02:00
Ruben Perez
0f56460056 reference docs (1) 2024-08-07 09:41:04 +02:00
Ruben Perez
6e7c42be7f get_impl is now private 2024-08-07 09:34:17 +02:00
Ruben Perez
6c6bcd03c0 casts to any_adapter 2024-08-07 09:28:34 +02:00
Ruben Perez
3098fca125 Immediate executor fix 2024-08-07 09:25:38 +02:00
Ruben Perez
25929578b9 Fixed compl handler signatures 2024-08-07 09:15:33 +02:00
Ruben Perez
1957cc7106 Merge branch 'develop' into feature/type-erased-response 2024-08-07 09:04:03 +02:00
Ruben Perez
a8c4f7520f Move any_adapter 2024-08-07 09:02:26 +02:00
Marcelo Zimbres
e8dd4d69eb Removes gcc-10 from the list of supported compilers.
See https://github.com/boostorg/redis/issues/203.
2024-07-28 15:03:07 +02:00
Ruben Perez
d1822761a5 Added any_adapter 2024-06-09 13:24:58 +02:00
Marcelo Zimbres
a4d2bb983d Merge branch 'master' into develop 2024-05-01 16:44:39 +02:00
Ruben Perez
30ba3c3eb6 Leftovers cleanup 2024-04-03 16:02:55 +02:00
Ruben Perez
90f3d6cd92 docker-compose without build 2024-04-03 16:02:55 +02:00
Ruben Perez
ef6dea1666 Missing package & env var 2024-04-03 16:02:55 +02:00
Ruben Perez
90b001a54e Correct volume mount 2024-04-03 16:02:55 +02:00
Ruben Perez
ec1ca876eb Incorrect directory for docker-compose 2024-04-03 16:02:55 +02:00
Ruben Perez
1b90346b7c Removed interactive flag 2024-04-03 16:02:55 +02:00
Ruben Perez
8f46d1eaa9 Removed need 2024-04-03 16:02:55 +02:00
Ruben Perez
a4e9a60f34 Removed external service 2024-04-03 16:02:55 +02:00
Ruben Perez
e9b16a3140 Removed sudo 2024-04-03 16:02:55 +02:00
Ruben Perez
ced4f9bd02 Container service 2024-04-03 16:02:55 +02:00
Ruben Perez
43878d68a3 missing sudo 2024-04-03 16:02:55 +02:00
Ruben Perez
29892f2837 Attempt to run without container 2024-04-03 16:02:55 +02:00
Ruben Perez
b71dff6dd0 Missing ports in docker-compose 2024-04-03 16:02:55 +02:00
Ruben Perez
a9204fcc91 Include cleanup 2024-04-03 16:02:55 +02:00
Ruben Perez
1788ebc80b make instead of ninja 2024-04-03 16:02:55 +02:00
Ruben Perez
5656571aa4 Missing packages 2024-04-03 16:02:55 +02:00
Ruben Perez
aa6622ffa2 Missing git 2024-04-03 16:02:55 +02:00
Ruben Perez
f459d5d89b ca-certificates 2024-04-03 16:02:55 +02:00
Ruben Perez
35276b6acb Docker compose with build step 2024-04-03 16:02:55 +02:00
Ruben Perez
122ffb20b2 Permission change 2024-04-03 16:02:55 +02:00
Ruben Perez
1f4b709b21 Missing ca-certificates 2024-04-03 16:02:55 +02:00
Ruben Perez
7d52065a87 Add git 2024-04-03 16:02:55 +02:00
Ruben Perez
f0d92c16a5 Debugging compose 2024-04-03 16:02:55 +02:00
Ruben Perez
1df18258d8 Missing backslashes in ci.yml 2024-04-03 16:02:55 +02:00
Ruben Perez
71c60a5a89 sudo removal 2024-04-03 16:02:55 +02:00
Ruben Perez
098fbd68d2 CI with Docker compose 2024-04-03 16:02:55 +02:00
Ruben Perez
4f2b12adbc Remove debug statement in test_conn_tls 2024-04-03 16:02:55 +02:00
Ruben Perez
e7cec45cb2 Test TLS files 2024-04-03 16:02:55 +02:00
Ruben Perez
b19067cfed get_server_hostname utility function 2024-04-03 16:02:55 +02:00
Ruben Perez
0af1c2e73d Fixed unused variable warning in connection_base 2024-04-03 16:02:55 +02:00
Ruben Perez
da48368d53 Restored TLS tests & rework 2024-04-03 16:02:55 +02:00
Marcelo Zimbres
b6e1280075 Fixes narrowing conversion.
NOTE: I had to disable the TLS tests because I shotdown the server I was
running on my domain occase.de. Once this ticket is merged I will open a
new one to fix that and reenable the tests.
2024-03-20 23:08:15 +01:00
Marcelo Zimbres
5d553f5d71 Some fixes in the article about the costs of async abstractions [skip ci] 2024-03-20 23:08:15 +01:00
Marcelo Zimbres
5c46b62958 Fixes narrowing conversion.
NOTE: I had to disable the TLS tests because I shotdown the server I was
running on my domain occase.de. Once this ticket is merged I will open a
new one to fix that and reenable the tests.
2024-03-20 14:58:06 +01:00
Marcelo Zimbres
6cde6eab44 Some fixes in the article about the costs of async abstractions [skip ci] 2024-03-20 14:58:06 +01:00
Marcelo Zimbres
78792199ef Adds endian to the list of dependencies 2024-02-17 21:34:01 +01:00
Marcelo Zimbres
f5793ac9bc Refactors add_hello and adds unit tests. 2024-02-17 21:34:01 +01:00
Marcelo Zimbres
dfc2bd1ac2 Fixes issue 181. 2024-02-17 21:34:01 +01:00
Marcelo Zimbres
0445e74fa3 Fixes the CMake file. 2024-01-21 21:52:31 +01:00
Marcelo Zimbres
234f961e87 Provides a way of passing a custom ssl context to the connection. 2024-01-21 21:52:31 +01:00
Marcelo Zimbres
8bb0004188 Adds missing ssl-context getters. 2024-01-21 21:52:31 +01:00
Marcelo Zimbres
4257b2eaec In-tree cmake builds instead of FindBoost. 2024-01-21 21:52:31 +01:00
Marcelo Zimbres
96da11a2cc Article about the costs of async abstractions. 2024-01-21 21:52:31 +01:00
Marcelo Zimbres
3861c5de74 Accepts as valid responses to staged requests.
Before these changes the request had to be marked as written in order to
interpret incoming responses as belonging to that request. On fast
networks however, like on localhost and underload the responses might
arrive before the write operation completed.
2024-01-21 21:52:31 +01:00
Marcelo
168ee6148a Merge pull request #162 from boostorg/develop
Merge latest css improvements into master
2023-12-14 23:22:03 +01:00
Marcelo Zimbres
723e72797f Output test error. 2023-12-02 20:23:16 +01:00
Marcelo
7caea928af Merge pull request #167 from boostorg/165-support-containers-in-ci
165 support containers in ci
2023-11-12 15:08:42 +01:00
Marcelo Zimbres
71b9a4f428 Reduces the number of messages in the stress test. 2023-11-12 13:00:43 +01:00
Marcelo Zimbres
d89a976729 Build gcc11 builds in container. 2023-11-11 14:12:33 +01:00
Marcelo Zimbres
154d0b106d Obtains the Redis host from env variables. 2023-11-04 12:41:53 +01:00
Marcelo Zimbres
2b12525206 Adds container to matrix. 2023-10-30 22:59:04 +01:00
Marcelo Zimbres
0bcbf6d4e4 Fix the build of boost dependencies. 2023-10-28 09:29:51 +02:00
Marcelo
6389daa783 Merge pull request #161 from anarthal/develop
Docs styles minor tweaks
2023-10-11 12:19:16 +02:00
Ruben Perez
ab2d6cdea8 Docs styles minor tweaks
Magnifying glass showing twice.
Sidebar scrolling with the content.
Header not being hidden correctly for small devices.

close #160
2023-10-11 11:57:43 +02:00
Marcelo
63ce40e365 Merge pull request #159 from anarthal/feature/158-boost-docs-formatting-problems
Fixed CSS formatting for Boost docs
2023-10-10 23:10:37 +02:00
Ruben Perez
f2a005a8c4 Fixed CSS formatting for Boost docs 2023-10-10 22:36:09 +02:00
Marcelo Zimbres
0c06be66de Fixes Boost.Redis version.
[skip ci]
2023-10-08 10:04:54 +02:00
193 changed files with 28577 additions and 9043 deletions

176
.clang-format Normal file
View File

@@ -0,0 +1,176 @@
---
Language: Cpp
ColumnLimit: 100
IndentWidth: 3
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: true
AfterClass: false
AfterControlStatement: false
AfterEnum: true
AfterFunction: true
AfterNamespace: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: true
AccessModifierOffset: -3
BinPackArguments: false
BinPackParameters: false
AlignAfterOpenBracket: AlwaysBreak
PointerAlignment: Left
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<boost/redis.*>|<boost/redis/.*>'
Priority: -10
SortPriority: 1
- Regex: '^<boost/.*>'
Priority: -8
SortPriority: 3
- Regex: "^<.*"
Priority: -6
SortPriority: 5
- Regex: ".*"
Priority: -5
SortPriority: 4
IndentCaseLabels: true
AllowShortCaseLabelsOnASingleLine: true
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlignArrayOfStructures: Left
DerivePointerAlignment: false
PenaltyBreakAssignment: 2000000
PenaltyBreakBeforeFirstCallParameter: 0
PenaltyBreakOpenParenthesis: 0
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyBreakScopeResolution: 1000000
PenaltyReturnTypeOnItsOwnLine: 200000000
# Defaults (based on Google)
AlignConsecutiveMacros: false
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: true
AllowAllConstructorInitializersOnNextLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortFunctionsOnASingleLine: All
AllowShortLambdasOnASingleLine: Empty
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: ExceptShortType
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
BreakTemplateDeclarations: Leave
BreakBeforeBinaryOperators: None
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: true
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
CommentPragmas: '(^ IWYU pragma:)|(^\\copydoc )|(^ ?\\n)'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 0
ContinuationIndentWidth: 3
Cpp11BracedListStyle: true
DeriveLineEnding: true
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeIsMainRegex: null
IncludeIsMainSourceRegex: ""
IndentGotoLabels: false
IndentPPDirectives: None
IndentWrappedFunctionNames: false
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ""
MacroBlockEnd: ""
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Never
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- "c++"
- "C++"
CanonicalDelimiter: ""
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
CanonicalDelimiter: ""
BasedOnStyle: google
ReflowComments: false
SortIncludes: true
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: false
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
Standard: Auto
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseCRLF: false
UseTab: Never
AlignConsecutiveShortCaseStatements:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true
AlignCaseColons: false
---

View File

@@ -21,15 +21,15 @@ jobs:
fail-fast: false
matrix:
include:
- { toolset: msvc-14.2, os: windows-2019, generator: "Visual Studio 16 2019", cxxstd: '17', build-type: 'Debug', build-shared-libs: 1 }
- { toolset: msvc-14.2, os: windows-2019, generator: "Visual Studio 16 2019", cxxstd: '17', build-type: 'Release', build-shared-libs: 0 }
#- { toolset: msvc-14.2, os: windows-2019, generator: "Visual Studio 16 2019", cxxstd: '17', build-type: 'Debug', build-shared-libs: 1 }
#- { toolset: msvc-14.2, os: windows-2019, generator: "Visual Studio 16 2019", cxxstd: '17', build-type: 'Release', build-shared-libs: 0 }
- { toolset: msvc-14.3, os: windows-2022, generator: "Visual Studio 17 2022", cxxstd: '20', build-type: 'Debug', build-shared-libs: 0 }
- { toolset: msvc-14.3, os: windows-2022, generator: "Visual Studio 17 2022", cxxstd: '20', build-type: 'Release', build-shared-libs: 1 }
env:
CMAKE_BUILD_PARALLEL_LEVEL: 4
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Boost
run: python3 tools/ci.py setup-boost --source-dir=$(pwd)
@@ -100,13 +100,13 @@ jobs:
fail-fast: false
matrix:
include:
- { toolset: msvc-14.2, os: windows-2019 }
#- { toolset: msvc-14.2, os: windows-2019 }
- { toolset: msvc-14.3, os: windows-2022 }
env:
OPENSSL_ROOT: "C:\\Program Files\\OpenSSL"
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup user-config.jam
run: cp tools/user-config.jam "${HOMEDRIVE}${HOMEPATH}/"
@@ -131,70 +131,170 @@ jobs:
fail-fast: false
matrix:
include:
- { toolset: gcc-11, install: g++-11, os: ubuntu-22.04, cxxstd: '17', build-type: 'Debug', ldflags: '' }
- { toolset: gcc-11, install: g++-11, os: ubuntu-22.04, cxxstd: '20', build-type: 'Release', ldflags: '' }
- { toolset: clang-11, install: clang-11, os: ubuntu-22.04, cxxstd: '17', build-type: 'Debug', ldflags: '' }
- { toolset: clang-11, install: clang-11, os: ubuntu-22.04, cxxstd: '20', build-type: 'Debug', ldflags: '' }
- { toolset: clang-13, install: clang-13, os: ubuntu-22.04, cxxstd: '17', build-type: 'Release', ldflags: '' }
- { toolset: clang-13, install: clang-13, os: ubuntu-22.04, cxxstd: '20', build-type: 'Release', ldflags: '' }
- { toolset: clang-14, install: 'clang-14 libc++-14-dev libc++abi-14-dev', os: ubuntu-22.04, cxxstd: '17', build-type: 'Debug', cxxflags: '-stdlib=libc++', ldflags: '-lc++' }
- { toolset: clang-14, install: 'clang-14 libc++-14-dev libc++abi-14-dev', os: ubuntu-22.04, cxxstd: '20', build-type: 'Release', cxxflags: '-stdlib=libc++', ldflags: '-lc++' }
runs-on: ${{ matrix.os }}
- toolset: gcc-11
install: g++-11
container: ubuntu:22.04
cxxstd: '17'
build-type: 'Debug'
ldflags: ''
server: "redis:7.4.5-alpine"
- toolset: gcc-11
install: g++-11
container: ubuntu:22.04
cxxstd: '20'
build-type: 'Release'
ldflags: ''
server: "redis:7.4.5-alpine"
- toolset: clang-11
install: clang-11
container: ubuntu:22.04
cxxstd: '17'
build-type: 'Debug'
ldflags: ''
server: "redis:7.4.5-alpine"
- toolset: clang-11
install: clang-11
container: ubuntu:22.04
cxxstd: '20'
build-type: 'Debug'
ldflags: ''
server: "redis:7.4.5-alpine"
- toolset: clang-13
install: clang-13
container: ubuntu:22.04
cxxstd: '17'
build-type: 'Release'
ldflags: ''
server: "redis:8.2.1-alpine"
- toolset: clang-13
install: clang-13
container: ubuntu:22.04
cxxstd: '20'
build-type: 'Release'
ldflags: ''
server: "redis:8.2.1-alpine"
- toolset: clang-14
install: 'clang-14 libc++-14-dev libc++abi-14-dev'
container: ubuntu:22.04
cxxstd: '17'
build-type: 'Debug'
cxxflags: '-stdlib=libc++'
ldflags: '-lc++'
server: "redis:8.2.1-alpine"
- toolset: clang-14
install: 'clang-14 libc++-14-dev libc++abi-14-dev'
container: ubuntu:22.04
cxxstd: '20'
build-type: 'Release'
cxxflags: '-stdlib=libc++'
ldflags: '-lc++'
server: "redis:8.2.1-alpine"
- toolset: clang-19
install: 'clang-19'
container: ubuntu:24.04
cxxstd: '23'
build-type: 'Debug'
cxxflags: '-fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all'
ldflags: '-fsanitize=address -fsanitize=undefined'
server: "redis:8.2.1-alpine"
- toolset: gcc-14
install: 'g++-14'
container: ubuntu:24.04
cxxstd: '23'
build-type: 'Debug'
cxxflags: '-DBOOST_ASIO_DISABLE_LOCAL_SOCKETS=1' # If a system had no UNIX socket support, we build correctly
server: "valkey/valkey:8.1.3-alpine"
- toolset: gcc-14
install: 'g++-14'
container: ubuntu:24.04
cxxstd: '23'
build-type: 'Debug'
cxxflags: '-fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all'
ldflags: '-fsanitize=address -fsanitize=undefined'
server: "valkey/valkey:8.1.3-alpine"
runs-on: ubuntu-latest
env:
CXXFLAGS: ${{matrix.cxxflags}} -Wall -Wextra
LDFLAGS: ${{matrix.ldflags}}
CMAKE_BUILD_PARALLEL_LEVEL: 4
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up the required containers
run: |
BUILDER_IMAGE=${{ matrix.container }} SERVER_IMAGE=${{ matrix.server }} docker compose -f tools/docker-compose.yml up -d --wait || (docker compose logs; exit 1)
- name: Install dependencies
run: sudo apt-get -y install cmake protobuf-compiler redis-server python3 ${{ matrix.install }}
run: |
docker exec builder apt-get update
docker exec builder apt-get -y --no-install-recommends install \
git \
g++ \
libssl-dev \
make \
ca-certificates \
cmake \
protobuf-compiler \
python3 \
${{ matrix.install }}
- name: Setup Boost
run: ./tools/ci.py setup-boost --source-dir=$(pwd)
run: docker exec builder /boost-redis/tools/ci.py setup-boost --source-dir=/boost-redis
- name: Build a Boost distribution using B2
run: |
./tools/ci.py build-b2-distro \
docker exec builder /boost-redis/tools/ci.py build-b2-distro \
--toolset ${{ matrix.toolset }}
- name: Build a Boost distribution using CMake
run: |
./tools/ci.py build-cmake-distro \
docker exec builder /boost-redis/tools/ci.py build-cmake-distro \
--build-type ${{ matrix.build-type }} \
--cxxstd ${{ matrix.cxxstd }} \
--toolset ${{ matrix.toolset }}
- name: Build the project tests
run: |
./tools/ci.py build-cmake-standalone-tests \
docker exec builder /boost-redis/tools/ci.py build-cmake-standalone-tests \
--build-type ${{ matrix.build-type }} \
--cxxstd ${{ matrix.cxxstd }} \
--toolset ${{ matrix.toolset }}
- name: Run the project tests
run: |
./tools/ci.py run-cmake-standalone-tests \
docker exec builder /boost-redis/tools/ci.py run-cmake-standalone-tests \
--build-type ${{ matrix.build-type }}
- name: Run add_subdirectory tests
run: |
./tools/ci.py run-cmake-add-subdirectory-tests \
docker exec builder /boost-redis/tools/ci.py run-cmake-add-subdirectory-tests \
--build-type ${{ matrix.build-type }} \
--cxxstd ${{ matrix.cxxstd }} \
--toolset ${{ matrix.toolset }}
- name: Run find_package tests with the built cmake distribution
run: |
./tools/ci.py run-cmake-find-package-tests \
docker exec builder /boost-redis/tools/ci.py run-cmake-find-package-tests \
--build-type ${{ matrix.build-type }} \
--cxxstd ${{ matrix.cxxstd }} \
--toolset ${{ matrix.toolset }}
- name: Run find_package tests with the built b2 distribution
run: |
./tools/ci.py run-cmake-b2-find-package-tests \
docker exec builder /boost-redis/tools/ci.py run-cmake-b2-find-package-tests \
--build-type ${{ matrix.build-type }} \
--cxxstd ${{ matrix.cxxstd }} \
--toolset ${{ matrix.toolset }}
@@ -209,15 +309,32 @@ jobs:
fail-fast: false
matrix:
include:
- { toolset: gcc-11, install: g++-11, cxxstd: "11,17,20" } # Having C++11 shouldn't break the build
- { toolset: clang-14, install: clang-14, cxxstd: "17,20" }
runs-on: ubuntu-22.04
- toolset: gcc-11
install: g++-11
cxxstd: "11,17,20" # Having C++11 shouldn't break the build
os: ubuntu-latest
container: ubuntu:22.04
- toolset: clang-14
install: clang-14
os: ubuntu-latest
container: ubuntu:22.04
cxxstd: "17,20"
runs-on: ${{ matrix.os }}
container: ${{matrix.container}}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup container environment
if: matrix.container
run: |
apt-get update
apt-get -y install sudo python3 git g++ libssl-dev
- name: Install dependencies
run: sudo apt-get -y install python3 ${{ matrix.install }}
run: |
sudo apt-get update
sudo apt-get -y install python3 ${{ matrix.install }}
- name: Setup Boost
run: ./tools/ci.py setup-boost --source-dir=$(pwd)
@@ -228,3 +345,24 @@ jobs:
--toolset ${{ matrix.toolset }} \
--cxxstd ${{ matrix.cxxstd }} \
--variant debug,release
# Checks that we don't have any errors in docs
check-docs:
name: Check docs
defaults:
run:
shell: bash
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Boost
run: ./tools/ci.py setup-boost --source-dir=$(pwd)
- name: Build docs
run: |
cd ~/boost-root
./b2 libs/redis/doc
[ -f ~/boost-root/libs/redis/doc/html/index.html ]

View File

@@ -1,51 +0,0 @@
name: Coverage
on:
push:
branches:
- develop
jobs:
posix:
defaults:
run:
shell: bash
runs-on: ubuntu-22.04
env:
CXX: g++-11
CXXFLAGS: -g -O0 -std=c++20 --coverage -fkeep-inline-functions -fkeep-static-functions
LDFLAGS: --coverage
CMAKE_BUILD_PARALLEL_LEVEL: 4
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install dependencies
run: sudo apt-get --no-install-recommends -y install cmake lcov g++-11 redis-server python3 libgd-perl
- name: Setup Boost
run: ./tools/ci.py setup-boost --source-dir=$(pwd)
- name: Build Boost
run: ./tools/ci.py build-b2-distro --toolset=gcc-11
# Having our library there confuses the coverage reports
- name: Remove Boost.Redis from the b2 distro
run: rm -rf ~/boost-b2-distro/include/boost/redis
- name: Run CMake
run: cmake -DCMAKE_PREFIX_PATH=$HOME/boost-b2-distro --preset coverage .
- name: Build
run: cmake --build --preset coverage
- name: Test
run: ctest --preset coverage
- name: Make the coverage file
run: cmake --build --preset coverage --target coverage
- name: Upload to codecov
run: |
bash <(curl -s https://codecov.io/bash) -f ./build/coverage/coverage.info

View File

@@ -19,13 +19,77 @@ target_compile_features(boost_redis INTERFACE cxx_std_17)
# Dependencies
if (BOOST_REDIS_MAIN_PROJECT)
# If we're the root project, error if a dependency is not found
find_package(Boost 1.83 REQUIRED COMPONENTS headers)
# TODO: Understand why we have to list all dependencies below
# instead of
#set(BOOST_INCLUDE_LIBRARIES redis)
#set(BOOST_EXCLUDE_LIBRARIES redis)
#add_subdirectory(../.. boostorg/boost EXCLUDE_FROM_ALL)
set(deps
system
assert
config
throw_exception
asio
variant2
mp11
winapi
predef
align
context
core
static_assert
pool
date_time
smart_ptr
exception
integer
move
type_traits
algorithm
utility
io
lexical_cast
numeric/conversion
mpl
range
tokenizer
tuple
array
bind
concept_check
function
iterator
regex
unordered
preprocessor
container
conversion
container_hash
detail
optional
function_types
fusion
intrusive
describe
typeof
functional
test
json
endian
compat
)
foreach(dep IN LISTS deps)
add_subdirectory(../${dep} boostorg/${dep})
endforeach()
find_package(Threads REQUIRED)
find_package(OpenSSL REQUIRED)
target_link_libraries(boost_redis
INTERFACE
Boost::headers
Boost::system
Boost::asio
Threads::Threads
OpenSSL::Crypto
OpenSSL::SSL

View File

@@ -1,178 +0,0 @@
{
"version": 2,
"cmakeMinimumRequired": {
"major": 3,
"minor": 14,
"patch": 0
},
"configurePresets": [
{
"name": "cmake-pedantic",
"hidden": true,
"warnings": {
"dev": true,
"deprecated": true,
"uninitialized": false,
"unusedCli": true,
"systemVars": false
},
"errors": {
"dev": true,
"deprecated": true
}
},
{
"name": "coverage",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/coverage",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Coverage",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra",
"CMAKE_CXX_FLAGS_COVERAGE": "-Og -g --coverage -fkeep-inline-functions -fkeep-static-functions",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"CMAKE_EXE_LINKER_FLAGS_COVERAGE": "--coverage",
"CMAKE_SHARED_LINKER_FLAGS_COVERAGE": "--coverage",
"PROJECT_BINARY_DIR": "${sourceDir}/build/coverage",
"COVERAGE_HTML_COMMAND": ""
}
},
{
"name": "g++-11",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/g++-11",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -fsanitize=address",
"CMAKE_CXX_COMPILER": "g++-11",
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11"
}
},
{
"name": "g++-11-release",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/g++-11-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra",
"CMAKE_CXX_COMPILER": "g++-11",
"CMAKE_SHARED_LINKER_FLAGS": "",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/g++-11-release"
}
},
{
"name": "clang++-13",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/clang++-13",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -fsanitize=address",
"CMAKE_CXX_COMPILER": "clang++-13",
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/clang++-13"
}
},
{
"name": "clang++-14",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/clang++-14",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -fsanitize=address",
"CMAKE_CXX_COMPILER": "clang++-14",
"CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/clang++-14"
}
},
{
"name": "libc++-14-cpp17",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/libc++-14-cpp17",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -stdlib=libc++ -std=c++17",
"CMAKE_EXE_LINKER_FLAGS": "-lc++",
"CMAKE_CXX_COMPILER": "clang++-14",
"CMAKE_SHARED_LINKER_FLAGS": "",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/libc++-14-cpp17"
}
},
{
"name": "libc++-14-cpp20",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["cmake-pedantic"],
"binaryDir": "${sourceDir}/build/libc++-14-cpp20",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -stdlib=libc++ -std=c++17",
"CMAKE_EXE_LINKER_FLAGS": "-lc++",
"CMAKE_CXX_COMPILER": "clang++-14",
"CMAKE_SHARED_LINKER_FLAGS": "",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"PROJECT_BINARY_DIR": "${sourceDir}/build/libc++-14-cpp20"
}
},
{
"name": "clang-tidy",
"generator": "Unix Makefiles",
"hidden": false,
"inherits": ["g++-11"],
"binaryDir": "${sourceDir}/build/clang-tidy",
"cacheVariables": {
"CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=${sourceDir}/include/*",
"CMAKE_CXX_STANDARD": "20"
}
}
],
"buildPresets": [
{ "name": "coverage", "configurePreset": "coverage" },
{ "name": "g++-11", "configurePreset": "g++-11" },
{ "name": "g++-11-release", "configurePreset": "g++-11-release" },
{ "name": "clang++-13", "configurePreset": "clang++-13" },
{ "name": "clang++-14", "configurePreset": "clang++-14" },
{ "name": "libc++-14-cpp17", "configurePreset": "libc++-14-cpp17" },
{ "name": "libc++-14-cpp20", "configurePreset": "libc++-14-cpp20" },
{ "name": "clang-tidy", "configurePreset": "clang-tidy" }
],
"testPresets": [
{
"name": "test",
"hidden": true,
"output": {"outputOnFailure": true},
"execution": {"noTestsAction": "error", "stopOnFailure": true}
},
{ "name": "coverage", "configurePreset": "coverage", "inherits": ["test"] },
{ "name": "g++-11", "configurePreset": "g++-11", "inherits": ["test"] },
{ "name": "g++-11-release", "configurePreset": "g++-11-release", "inherits": ["test"] },
{ "name": "clang++-13", "configurePreset": "clang++-13", "inherits": ["test"] },
{ "name": "clang++-14", "configurePreset": "clang++-14", "inherits": ["test"] },
{ "name": "libc++-14-cpp17", "configurePreset": "libc++-14-cpp17", "inherits": ["test"] },
{ "name": "libc++-14-cpp20", "configurePreset": "libc++-14-cpp20", "inherits": ["test"] },
{ "name": "clang-tidy", "configurePreset": "clang-tidy", "inherits": ["test"] }
]
}

1016
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,9 @@
* accompanying file LICENSE.txt)
*/
#include <iostream>
#include <boost/asio.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace net = boost::asio;
@@ -26,9 +27,9 @@ auto example(boost::asio::ip::tcp::endpoint ep, std::string msg, int n) -> net::
auto dbuffer = net::dynamic_buffer(buffer);
for (int i = 0; i < n; ++i) {
co_await net::async_write(socket, net::buffer(msg));
auto n = co_await net::async_read_until(socket, dbuffer, "\n");
auto bytes_read = co_await net::async_read_until(socket, dbuffer, "\n");
//std::printf("> %s", buffer.data());
dbuffer.consume(n);
dbuffer.consume(bytes_read);
}
//std::printf("Ok: %s", msg.data());
@@ -62,6 +63,10 @@ int main(int argc, char* argv[])
std::cerr << e.what() << std::endl;
}
}
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
auto main() -> int {std::cout << "Requires coroutine support." << std::endl; return 1;}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
auto main() -> int
{
std::cout << "Requires coroutine support." << std::endl;
return 1;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -8,9 +8,10 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/asio.hpp>
#include <cstdio>
#include <iostream>
#include <boost/asio.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace net = boost::asio;
@@ -27,37 +28,41 @@ constexpr net::use_awaitable_t<executor_type> use_awaitable;
awaitable_type echo(tcp_socket socket)
{
try {
char data[1024];
for (;;) {
std::size_t n = co_await socket.async_read_some(net::buffer(data), use_awaitable);
co_await async_write(socket, net::buffer(data, n), use_awaitable);
}
} catch (std::exception const&) {
//std::printf("echo Exception: %s\n", e.what());
}
try {
char data[1024];
for (;;) {
std::size_t n = co_await socket.async_read_some(net::buffer(data), use_awaitable);
co_await async_write(socket, net::buffer(data, n), use_awaitable);
}
} catch (std::exception const&) {
//std::printf("echo Exception: %s\n", e.what());
}
}
awaitable_type listener()
{
auto ex = co_await this_coro::executor;
tcp_acceptor acceptor(ex, {tcp::v4(), 55555});
for (;;) {
tcp_socket socket = co_await acceptor.async_accept(use_awaitable);
co_spawn(ex, echo(std::move(socket)), detached);
}
auto ex = co_await this_coro::executor;
tcp_acceptor acceptor(ex, {tcp::v4(), 55555});
for (;;) {
tcp_socket socket = co_await acceptor.async_accept(use_awaitable);
co_spawn(ex, echo(std::move(socket)), detached);
}
}
int main()
{
try {
net::io_context io_context{BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO};
co_spawn(io_context, listener(), detached);
io_context.run();
} catch (std::exception const& e) {
std::printf("Exception: %s\n", e.what());
}
try {
net::io_context io_context{BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO};
co_spawn(io_context, listener(), detached);
io_context.run();
} catch (std::exception const& e) {
std::printf("Exception: %s\n", e.what());
}
}
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
auto main() -> int {std::cout << "Requires coroutine support." << std::endl; return 1;}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
auto main() -> int
{
std::cout << "Requires coroutine support." << std::endl;
return 1;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

28
build.jam Normal file
View File

@@ -0,0 +1,28 @@
# Copyright René Ferdinand Rivera Morell 2024
# 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)
require-b2 5.2 ;
constant boost_dependencies :
/boost/asio//boost_asio
/boost/assert//boost_assert
/boost/core//boost_core
/boost/mp11//boost_mp11
/boost/system//boost_system
/boost/throw_exception//boost_throw_exception ;
project /boost/redis
: common-requirements
<include>include
;
explicit
[ alias boost_redis : : : : <library>$(boost_dependencies) ]
[ alias all : boost_redis test ]
;
call-if : boost-library redis
;

26
doc/CMakeLists.txt Normal file
View File

@@ -0,0 +1,26 @@
#
# Copyright (c) 2025 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)
#
# Root CMakeLists.txt so MrDocs knows how to build our code
cmake_minimum_required(VERSION 3.8...3.22)
# Project
project(boost_redis_mrdocs LANGUAGES CXX)
# MrDocs forces CMAKE_EXPORT_COMPILE_COMMANDS=ON, incorrectly
# causing all targets to be dumped to the compilation database.
# Disable this setting so we can set EXPORT_COMPILE_COMMANDS
# only to our target of interest
set(CMAKE_EXPORT_COMPILE_COMMANDS OFF)
# Add Boost
add_subdirectory($ENV{BOOST_SRC_DIR} deps/boost)
# Add the target for mrdocs to analyze
add_executable(mrdocs mrdocs.cpp)
target_link_libraries(mrdocs PRIVATE Boost::redis)
set_target_properties(mrdocs PROPERTIES EXPORT_COMPILE_COMMANDS ON)

View File

@@ -1,226 +0,0 @@
<doxygenlayout version="1.0">
<!-- Generated by doxygen 1.9.1 -->
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="yes" title="Contents"/>
<tab type="pages" visible="yes" title="" intro=""/>
<tab type="modules" visible="no" title="Reference" intro=""/>
<tab type="namespaces" visible="no" title="">
<tab type="namespacelist" visible="yes" title="" intro=""/>
<tab type="namespacemembers" visible="yes" title="" intro=""/>
</tab>
<tab type="interfaces" visible="no" title="">
<tab type="interfacelist" visible="yes" title="" intro=""/>
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
<tab type="interfacehierarchy" visible="yes" title="" intro=""/>
</tab>
<tab type="classes" visible="no" title="">
<tab type="classlist" visible="no" title="" intro=""/>
<tab type="classindex" visible="$ALPHABETICAL_INDEX" title=""/>
<tab type="hierarchy" visible="no" title="" intro=""/>
<tab type="classmembers" visible="no" title="" intro=""/>
</tab>
<tab type="structs" visible="no" title="">
<tab type="structlist" visible="no" title="" intro=""/>
<tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
</tab>
<tab type="exceptions" visible="no" title="">
<tab type="exceptionlist" visible="no" title="" intro=""/>
<tab type="exceptionindex" visible="$ALPHABETICAL_INDEX" title=""/>
<tab type="exceptionhierarchy" visible="yes" title="" intro=""/>
</tab>
<tab type="files" visible="no" title="">
<tab type="filelist" visible="no" title="" intro=""/>
<tab type="globals" visible="no" title="" intro=""/>
</tab>
<tab type="examples" visible="yes" title="" intro=""/>
</navindex>
<!-- Layout definition for a class page -->
<class>
<briefdescription visible="yes"/>
<includes visible="$SHOW_INCLUDE_FILES"/>
<inheritancegraph visible="$CLASS_GRAPH"/>
<collaborationgraph visible="$COLLABORATION_GRAPH"/>
<memberdecl>
<nestedclasses visible="yes" title=""/>
<publictypes title=""/>
<services title=""/>
<interfaces title=""/>
<publicslots title=""/>
<signals title=""/>
<publicmethods title=""/>
<publicstaticmethods title=""/>
<publicattributes title=""/>
<publicstaticattributes title=""/>
<protectedtypes title=""/>
<protectedslots title=""/>
<protectedmethods title=""/>
<protectedstaticmethods title=""/>
<protectedattributes title=""/>
<protectedstaticattributes title=""/>
<packagetypes title=""/>
<packagemethods title=""/>
<packagestaticmethods title=""/>
<packageattributes title=""/>
<packagestaticattributes title=""/>
<properties title=""/>
<events title=""/>
<privatetypes title=""/>
<privateslots title=""/>
<privatemethods title=""/>
<privatestaticmethods title=""/>
<privateattributes title=""/>
<privatestaticattributes title=""/>
<friends title=""/>
<related title="" subtitle=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription title=""/>
<memberdef>
<inlineclasses title=""/>
<typedefs title=""/>
<enums title=""/>
<services title=""/>
<interfaces title=""/>
<constructors title=""/>
<functions title=""/>
<related title=""/>
<variables title=""/>
<properties title=""/>
<events title=""/>
</memberdef>
<allmemberslink visible="yes"/>
<usedfiles visible="$SHOW_USED_FILES"/>
<authorsection visible="yes"/>
</class>
<!-- Layout definition for a namespace page -->
<namespace>
<briefdescription visible="yes"/>
<memberdecl>
<nestednamespaces visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<structs visible="yes" title=""/>
<exceptions visible="yes" title=""/>
<typedefs title=""/>
<sequences title=""/>
<dictionaries title=""/>
<enums title=""/>
<functions title=""/>
<variables title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription title=""/>
<memberdef>
<inlineclasses title=""/>
<typedefs title=""/>
<sequences title=""/>
<dictionaries title=""/>
<enums title=""/>
<functions title=""/>
<variables title=""/>
</memberdef>
<authorsection visible="yes"/>
</namespace>
<!-- Layout definition for a file page -->
<file>
<briefdescription visible="yes"/>
<includes visible="$SHOW_INCLUDE_FILES"/>
<includegraph visible="$INCLUDE_GRAPH"/>
<includedbygraph visible="$INCLUDED_BY_GRAPH"/>
<sourcelink visible="yes"/>
<memberdecl>
<interfaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<structs visible="yes" title=""/>
<exceptions visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<defines title=""/>
<typedefs title=""/>
<sequences title=""/>
<dictionaries title=""/>
<enums title=""/>
<functions title=""/>
<variables title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription title=""/>
<memberdef>
<inlineclasses title=""/>
<defines title=""/>
<typedefs title=""/>
<sequences title=""/>
<dictionaries title=""/>
<enums title=""/>
<functions title=""/>
<variables title=""/>
</memberdef>
<authorsection/>
</file>
<!-- Layout definition for a group page -->
<group>
<briefdescription visible="yes"/>
<groupgraph visible="$GROUP_GRAPHS"/>
<memberdecl>
<nestedgroups visible="yes" title=""/>
<dirs visible="yes" title=""/>
<files visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<defines title=""/>
<typedefs title=""/>
<sequences title=""/>
<dictionaries title=""/>
<enums title=""/>
<enumvalues title=""/>
<functions title=""/>
<variables title=""/>
<signals title=""/>
<publicslots title=""/>
<protectedslots title=""/>
<privateslots title=""/>
<events title=""/>
<properties title=""/>
<friends title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription title=""/>
<memberdef>
<pagedocs/>
<inlineclasses title=""/>
<defines title=""/>
<typedefs title=""/>
<sequences title=""/>
<dictionaries title=""/>
<enums title=""/>
<enumvalues title=""/>
<functions title=""/>
<variables title=""/>
<signals title=""/>
<publicslots title=""/>
<protectedslots title=""/>
<privateslots title=""/>
<events title=""/>
<properties title=""/>
<friends title=""/>
</memberdef>
<authorsection visible="yes"/>
</group>
<!-- Layout definition for a directory page -->
<directory>
<briefdescription visible="yes"/>
<directorygraph visible="yes"/>
<memberdecl>
<dirs visible="yes"/>
<files visible="yes"/>
</memberdecl>
<detaileddescription title=""/>
</directory>
</doxygenlayout>

View File

@@ -1,80 +1,17 @@
project redis/doc ;
#
# Copyright (c) 2025 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)
#
import doxygen ;
import path ;
import sequence ;
# Adapted from Boost.Unordered
make html/index.html : build_antora.sh : @run-script ;
# All paths must be absolute to work well with the Doxygen rules.
path-constant this_dir : . ;
path-constant target_dir : html ;
path-constant redis_root_dir : .. ;
path-constant include_dir : ../include ;
path-constant examples_dir : ../example ;
path-constant readme : ../README.md ;
path-constant layout_file : DoxygenLayout.xml ;
local stylesheet_files = [ path.glob $(this_dir) : *.css ] ;
local includes = [ path.glob-tree $(include_dir) : *.hpp *.cpp ] ;
local examples = [ path.glob-tree $(examples_dir) : *.hpp *.cpp ] ;
# If passed directly, several HTML_EXTRA_STYLESHEET tags are generated,
# which is not correct.
local stylesheet_arg = [ sequence.join "\"$(stylesheet_files)\"" : " " ] ;
# The doxygen rule requires the target name to end in .html to generate HTML files
doxygen doc.html
:
$(includes) $(examples) $(readme)
:
<doxygen:param>"PROJECT_NAME=Boost.Redis"
<doxygen:param>PROJECT_NUMBER="1.4.2"
<doxygen:param>PROJECT_BRIEF="A redis client library"
<doxygen:param>"STRIP_FROM_PATH=\"$(redis_root_dir)\""
<doxygen:param>"STRIP_FROM_INC_PATH=\"$(include_dir)\""
<doxygen:param>BUILTIN_STL_SUPPORT=YES
<doxygen:param>INLINE_SIMPLE_STRUCTS=YES
<doxygen:param>HIDE_UNDOC_MEMBERS=YES
<doxygen:param>HIDE_UNDOC_CLASSES=YES
<doxygen:param>SHOW_HEADERFILE=YES
<doxygen:param>SORT_BRIEF_DOCS=YES
<doxygen:param>SORT_MEMBERS_CTORS_1ST=YES
<doxygen:param>SHOW_FILES=NO
<doxygen:param>SHOW_NAMESPACES=NO
<doxygen:param>"LAYOUT_FILE=\"$(layout_file)\""
<doxygen:param>WARN_IF_INCOMPLETE_DOC=YES
<doxygen:param>FILE_PATTERNS="*.hpp *.cpp"
<doxygen:param>EXCLUDE_SYMBOLS=std
<doxygen:param>"USE_MDFILE_AS_MAINPAGE=\"$(readme)\""
<doxygen:param>SOURCE_BROWSER=YES
<doxygen:param>"HTML_EXTRA_STYLESHEET=$(stylesheet_arg)"
<doxygen:param>HTML_TIMESTAMP=YES
<doxygen:param>GENERATE_TREEVIEW=YES
<doxygen:param>FULL_SIDEBAR=NO
<doxygen:param>ENUM_VALUES_PER_LINE=0
<doxygen:param>OBFUSCATE_EMAILS=YES
<doxygen:param>USE_MATHJAX=YES
<doxygen:param>MATHJAX_VERSION=MathJax_2
<doxygen:param>MATHJAX_RELPATH="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/"
<doxygen:param>MACRO_EXPANSION=YES
<doxygen:param>HAVE_DOT=NO
<doxygen:param>CLASS_GRAPH=NO
<doxygen:param>DIRECTORY_GRAPH=NO
;
explicit doc.html ;
# The doxygen rule only informs b2 about the main HTML file, and not about
# all the doc directory that gets generated. Using the install rule copies
# only a single file, which is incorrect. This is a workaround to copy
# the generated docs to the doc/html directory, where they should be.
make copyhtml.tag : doc.html : @copy_html_dir ;
explicit copyhtml.tag ;
actions copy_html_dir
# Runs the Antora script
actions run-script
{
rm -rf $(target_dir)
mkdir -p $(target_dir)
cp -r $(<:D)/html/doc/* $(target_dir)/
echo "Stamped" > "$(<)"
bash -x $(>)
}
# These are used to inform the build system of the
@@ -83,5 +20,5 @@ actions copy_html_dir
alias boostdoc ;
explicit boostdoc ;
alias boostrelease : copyhtml.tag ;
alias boostrelease : html/index.html ;
explicit boostrelease ;

15
doc/antora.yml Normal file
View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025 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)
#
name: redis
title: Boost.Redis
version: ~
nav:
- modules/ROOT/nav.adoc
ext:
cpp-reference:
config: doc/mrdocs.yml

19
doc/build_antora.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
#
# Copyright (c) 2025 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)
#
set -e
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd "$SCRIPT_DIR"
# Required by our CMake.
# Prevents Antora from cloning Boost again
export BOOST_SRC_DIR=$(realpath $SCRIPT_DIR/../../..)
npm ci
npx antora --log-format=pretty --stacktrace --log-level info redis-playbook.yml

View File

@@ -1,115 +0,0 @@
/**
Doxygen Awesome
https://github.com/jothepro/doxygen-awesome-css
MIT License
Copyright (c) 2021 jothepro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
html {
/* side nav width. MUST be = `TREEVIEW_WIDTH`.
* Make sure it is wide enough to contain the page title (logo + title + version)
*/
--side-nav-fixed-width: 335px;
--menu-display: none;
--top-height: 120px;
--toc-sticky-top: -25px;
--toc-max-height: calc(100vh - 2 * var(--spacing-medium) - 25px);
}
#projectname {
white-space: nowrap;
}
@media screen and (min-width: 768px) {
html {
--searchbar-background: var(--page-background-color);
}
#side-nav {
min-width: var(--side-nav-fixed-width);
max-width: var(--side-nav-fixed-width);
top: var(--top-height);
overflow: visible;
}
#nav-tree, #side-nav {
height: calc(100vh - var(--top-height)) !important;
}
#nav-tree {
padding: 0;
}
#top {
display: block;
border-bottom: none;
height: var(--top-height);
margin-bottom: calc(0px - var(--top-height));
max-width: var(--side-nav-fixed-width);
overflow: hidden;
background: var(--side-nav-background);
}
#main-nav {
float: left;
padding-right: 0;
}
.ui-resizable-handle {
cursor: default;
width: 1px !important;
box-shadow: 0 calc(-2 * var(--top-height)) 0 0 var(--separator-color);
}
#nav-path {
position: fixed;
right: 0;
left: var(--side-nav-fixed-width);
bottom: 0;
width: auto;
}
#doc-content {
height: calc(100vh - 31px) !important;
padding-bottom: calc(3 * var(--spacing-large));
padding-top: calc(var(--top-height) - 80px);
box-sizing: border-box;
margin-left: var(--side-nav-fixed-width) !important;
}
#MSearchBox {
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)));
}
#MSearchField {
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)) - 65px);
}
#MSearchResultsWindow {
left: var(--spacing-medium) !important;
right: auto;
}
}

File diff suppressed because it is too large Load Diff

12
doc/modules/ROOT/nav.adoc Normal file
View File

@@ -0,0 +1,12 @@
* xref:index.adoc[Introduction]
* xref:requests_responses.adoc[]
* xref:cancellation.adoc[]
* xref:serialization.adoc[]
* xref:logging.adoc[]
* xref:sentinel.adoc[]
* xref:benchmarks.adoc[]
* xref:comparison.adoc[]
* xref:examples.adoc[]
* xref:reference.adoc[Reference]
* xref:acknowledgements.adoc[]
* xref:changelog.adoc[]

View File

@@ -0,0 +1,37 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.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)
//
= Acknowledgements
Acknowledgement to people that helped shape Boost.Redis
* Richard Hodges (https://github.com/madmongo1[madmongo1]): For very helpful support with Asio, the design of asynchronous programs, etc.
* Vinícius dos Santos Oliveira (https://github.com/vinipsmaker[vinipsmaker]): For useful discussion about how Boost.Redis consumes buffers in the read operation.
* Petr Dannhofer (https://github.com/Eddie-cz[Eddie-cz]): For helping me understand how the `AUTH` and `HELLO` command can influence each other.
* Mohammad Nejati (https://github.com/ashtum[ashtum]): For pointing out scenarios where calls to `async_exec` should fail when the connection is lost.
* Klemens Morgenstern (https://github.com/klemens-morgenstern[klemens-morgenstern]): For useful discussion about timeouts, cancellation, synchronous interfaces and general help with Asio.
* Vinnie Falco (https://github.com/vinniefalco[vinniefalco]): For general suggestions about how to improve the code and the documentation.
* Bram Veldhoen (https://github.com/bveldhoen[bveldhoen]): For contributing a Redis-streams example.
Also many thanks to all individuals that participated in the Boost
review
* Zach Laine: https://lists.boost.org/Archives/boost/2023/01/253883.php
* Vinnie Falco: https://lists.boost.org/Archives/boost/2023/01/253886.php
* Christian Mazakas: https://lists.boost.org/Archives/boost/2023/01/253900.php
* Ruben Perez: https://lists.boost.org/Archives/boost/2023/01/253915.php
* Dmitry Arkhipov: https://lists.boost.org/Archives/boost/2023/01/253925.php
* Alan de Freitas: https://lists.boost.org/Archives/boost/2023/01/253927.php
* Mohammad Nejati: https://lists.boost.org/Archives/boost/2023/01/253929.php
* Sam Hartsfield: https://lists.boost.org/Archives/boost/2023/01/253931.php
* Miguel Portilla: https://lists.boost.org/Archives/boost/2023/01/253935.php
* Robert A.H. Leahy: https://lists.boost.org/Archives/boost/2023/01/253928.php
The Reviews can be found at:
https://lists.boost.org/Archives/boost/2023/01/date.php. The thread
with the ACCEPT from the review manager can be found here:
https://lists.boost.org/Archives/boost/2023/01/253944.php.

View File

@@ -0,0 +1,95 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.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)
//
= Echo server benchmark
This document benchmarks the performance of TCP echo servers I
implemented in different languages using different Redis clients. The
main motivations for choosing an echo server are
* Simple to implement and does not require expertise level in most languages.
* I/O bound: Echo servers have very low CPU consumption in general
and therefore are excellent to measure how a program handles concurrent requests.
* It simulates very well a typical backend in regard to concurrency.
I also imposed some constraints on the implementations
* It should be simple enough and not require writing too much code.
* Favor the use standard idioms and avoid optimizations that require expert level.
* Avoid the use of complex things like connection and thread pool.
To reproduce these results run one of the echo-server programs in one
terminal and the
https://github.com/boostorg/redis/blob/42880e788bec6020dd018194075a211ad9f339e8/benchmarks/cpp/asio/echo_server_client.cpp[echo-server-client] in another.
== Without Redis
First I tested a pure TCP echo server, i.e. one that sends the messages
directly to the client without interacting with Redis. The result can
be seen below
image::https://boostorg.github.io/redis/tcp-echo-direct.png[]
The tests were performed with a 1000 concurrent TCP connections on the
localhost where latency is 0.07ms on average on my machine. On higher
latency networks the difference among libraries is expected to
decrease.
* I expected Libuv to have similar performance to Asio and Tokio.
* I did expect nodejs to come a little behind given it is is
javascript code. Otherwise I did expect it to have similar
performance to libuv since it is the framework behind it.
* Go did surprise me: faster than nodejs and libuv!
The code used in the benchmarks can be found at
* https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/cpp/asio/echo_server_direct.cpp[Asio]: A variation of https://github.com/chriskohlhoff/asio/blob/4915cfd8a1653c157a1480162ae5601318553eb8/asio/src/examples/cpp20/coroutines/echo_server.cpp[this Asio example].
* https://github.com/boostorg/redis/tree/835a1decf477b09317f391eddd0727213cdbe12b/benchmarks/c/libuv[Libuv]: Taken from https://github.com/libuv/libuv/blob/06948c6ee502862524f233af4e2c3e4ca876f5f6/docs/code/tcp-echo-server/main.c[this Libuv example].
* https://github.com/boostorg/redis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/rust/echo_server_direct[Tokio]: Taken from https://docs.rs/tokio/latest/tokio/[here].
* https://github.com/boostorg/redis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/nodejs/echo_server_direct[Nodejs]
* https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/go/echo_server_direct.go[Go]
== With Redis
This is similar to the echo server described above but messages are
echoed by Redis and not by the echo-server itself, which acts
as a proxy between the client and the Redis server. The results
can be seen below
image::https://boostorg.github.io/redis/tcp-echo-over-redis.png[]
The tests were performed on a network where latency is 35ms on
average, otherwise it uses the same number of TCP connections
as the previous example.
As the reader can see, the Libuv and the Rust test are not depicted
in the graph, the reasons are
* https://github.com/redis-rs/redis-rs[redis-rs]: This client
comes so far behind that it can't even be represented together
with the other benchmarks without making them look insignificant.
I don't know for sure why it is so slow, I suppose it has
something to do with its lack of automatic
https://redis.io/docs/manual/pipelining/[pipelining] support.
In fact, the more TCP connections I launch the worse its
performance gets.
* Libuv: I left it out because it would require me writing to much
c code. More specifically, I would have to use hiredis and
implement support for pipelines manually.
The code used in the benchmarks can be found at
* https://github.com/boostorg/redis[Boost.Redis]: https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/examples/echo_server.cpp[code]
* https://github.com/redis/node-redis[node-redis]: https://github.com/boostorg/redis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/nodejs/echo_server_over_redis[code]
* https://github.com/go-redis/redis[go-redis]: https://github.com/boostorg/redis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/go/echo_server_over_redis.go[code]
== Conclusion
Redis clients have to support automatic pipelining to have competitive performance. For updates to this document follow https://github.com/boostorg/redis[].

View File

@@ -0,0 +1,79 @@
//
// 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)
//
= Cancellation and timeouts
By default, running a request with xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`async_exec`]
will wait until a connection to the Redis server is established by `async_run`.
This may take a very long time if the server is down.
For this reason, it is usually a good idea to set a timeout to `async_exec`
operations using the
https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/cancel_after.html[`asio::cancel_after`]
completion token:
[source,cpp]
----
using namespace std::chrono_literals;
// Compose a request with a SET command
request req;
req.push("SET", "my_key", 42);
// If the request hasn't completed after 10 seconds, it will be cancelled
// and an exception will be thrown.
co_await conn.async_exec(req, ignore, asio::cancel_after(10s));
----
See our {site-url}/example/cpp20_timeouts.cpp[example on timeouts]
for a full code listing.
You can also use `cancel_after` with other completion styles, like
callbacks and futures.
`cancel_after` works because `async_exec` supports the per-operation
cancellation mechanism. This is used by Boost.Asio to implement features
like `cancel_after` and parallel groups. All asynchronous operations
in the library support this mechanism. Please consult the documentation
for individual operations for more info.
== Retrying idempotent requests
We mentioned that `async_exec` waits until the server is up
before sending the request. But what happens if there is a communication
error after sending the request, but before receiving a response?
In this situation there is no way to know if the request was processed by the server or not.
By default, the library will consider the request as failed,
and `async_exec` will complete with an `asio::error::operation_aborted`
error code.
Some requests can be executed several times and result in the same outcome
as executing them only once. We say that these requests are _idempotent_.
The `SET` command is idempotent, while `INCR` is not.
If you know that a `request` object contains only idempotent commands,
you can instruct Boost.Redis to retry the request on failure, even
if the library is unsure about the server having processed the request or not.
You can do so by setting `cancel_if_unresponded`
in xref:reference:boost/redis/request/config.adoc[`request::config`]
to false:
[source,cpp]
----
// Compose a request
request req;
req.push("SET", "my_key", 42); // idempotent
req.get_config().cancel_if_unresponded = false; // Retry the request even if it was written but not responded
// Makes sure that the key is set, even in the presence of network errors.
// This may suspend for an unspecified period of time if the server is down.
co_await conn.async_exec(req, ignore);
----

View File

@@ -0,0 +1,537 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.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)
//
= Changelog
== Boost 1.90
* (Pull request https://github.com/boostorg/redis/pull/310[310])
Improves the per-operation support in `async_exec()`. Requests can now
be cancelled at any point, and cancellations don't interfere with other
requests anyhow. In previous versions, a cancellation could cause
`async_run()` to be cancelled, making cancellations unpredictable.
* (Issue https://github.com/boostorg/redis/issues/226[226])
Added support for the `asio::cancel_after` and `asio::cancel_at`
completion tokens in `async_exec()` and `async_run()`.
* (Issue https://github.com/boostorg/redis/issues/319[319])
Added support for per-operation cancellation in `async_run()`.
* (Pull request https://github.com/boostorg/redis/pull/329[329]
and https://github.com/boostorg/redis/pull/334[334])
The `cancel_on_connection_lost` and `cancel_if_not_connected`
flags in `request::config` have been deprecated, and will be removed
in subsequent releases. To limit the time span that `async_exec`
might take, use `asio::cancel_after`, instead.
`cancel_on_connection_lost` default has been changed to `false`.
This shouldn't cause much impact, since `cancel_on_connection_lost`
is not a reliable way to limit the time span that `async_exec` might take.
* (Pull request https://github.com/boostorg/redis/pull/321[321])
Calling `cancel` with `operation::resolve`, `operation::connect`,
`operation::ssl_handshake`, `operation::reconnection` and
`operation::health_check` is now deprecated.
These enumerators will be removed in subsequent releases.
Users should employ `cancel(operation::run)`, instead.
* (Issue github.com/boostorg/redis/issues/302[302] and
pull request https://github.com/boostorg/redis/pull/303[303])
Added support for custom setup requests using `config::setup`
and `config::use_setup`. When setting these fields, users can
replace the library-generated `HELLO` request by any other arbitrary request.
This request is executed every time a new physical connection with the server
is established. This feature can be used to interact with systems that don't
support `HELLO`, to handle authentication and to connect to replicas.
* (Pull request https://github.com/boostorg/redis/pull/305[305])
`request::config::hello_with_priority` and `request::has_hello_priority()` have
been deprecated and will be removed in subsequent releases.
This flag is not well specified and should only be used by the library.
If you need to execute a request before any other, use `config::setup`, instead.
* (Issue https://github.com/boostorg/redis/issues/296[296])
Valkey long-term support: we guarantee Valkey compatibility
starting with this release. Previous releases may also work,
but have not been tested with this database system.
* (Issue https://github.com/boostorg/redis/issues/341[341])
Adds a `request::append()` function, to concatenate request objects.
* (Issue https://github.com/boostorg/redis/issues/104[104])
The health checker algorithm has been redesigned to avoid
false positives under heavy loads. `PING` commands are now
only issued when the connection is idle, instead of periodically.
* (Pull request https://github.com/boostorg/redis/pull/283[283])
Added `config::read_buffer_append_size`, which allows to control
the expansion of the connection's read buffer.
* (Pull request https://github.com/boostorg/redis/pull/311[311])
Added `usage::bytes_rotated`, which measures data copying when
reading and parsing data from the server.
* (Issue https://github.com/boostorg/redis/issues/298[298])
Added support for authenticating users with an empty password
but a non-default username.
* (Issue https://github.com/boostorg/redis/issues/318[318])
Fixed a number of race conditions in the `cancel()` function
of `connection` and `basic_connection` that could cause
cancellations to be ignored.
* (Issue https://github.com/boostorg/redis/issues/290[290])
Fixed a problem that could cause an error during `HELLO`
to make subsequent `HELLO` attempts during reconnection to fail.
* (Issue https://github.com/boostorg/redis/issues/297[297])
Errors during `HELLO` are now correctly logged.
* (Issue https://github.com/boostorg/redis/issues/287[287])
Fixed a bug causing an exception to be thrown when parsing
a response that contains an intermediate error into a `generic_response`.
== Boost 1.89
* (Pull request https://github.com/boostorg/redis/pull/256[256],
https://github.com/boostorg/redis/pull/266[266] and
https://github.com/boostorg/redis/pull/273[273])
The following members in `connection` and `basic_connection` are now deprecated
and will be removed in subsequent releases:
* `next_layer()` and `next_layer_type`: there is no reason to access the underlying stream object directly.
Connection member functions should be used, instead.
* `get_ssl_context()`: SSL contexts should never be modified after an `asio::ssl::stream`
object has been created from them. Properties should be set before passing the context
to the constructor. There is no reason to access the SSL context after that.
* `reset_stream()`: connection internals have been refactored to reset the SSL stream
automatically when required. This function is now a no-op.
* The `async_run()` overload taking no parameters: use the `async_run`
overload taking a `config` object explicitly, instead.
* (Issue https://github.com/boostorg/redis/issues/213[213])
The logging interface has been re-written:
* Logging can now be customized by passing a function object
to the `logger` constructor. This allows integration with
third-party logging libraries, like spdlog.
This new logging interface is public and will be maintained long-term.
* The old, unstable interface consisting of `logger::on_xxx` functions has been removed.
* `connection` and `basic_connection` constructors now accept a `logger` object.
This is now the preferred way to configure logging.
The `async_run()` overload taking a `logger` object is now deprecated, and will
be removed in subsequent releases.
* `config::log_prefix` is now deprecated, and will
be removed in subsequent releases. Users can achieve the same effect
by passing a custom logging function to the `logger` constructor.
* The default logging function, which prints to `stderr`,
is now based on `printf` and is thus thread-safe.
The old function used `std::cerr` and could result to interleaved
output in multi-threaded programs.
* The default log level is now `logger::level::info`,
down from `logger::level::debug`. This results in less verbose output by default.
* (Issue https://github.com/boostorg/redis/issues/272[272])
Added support for connecting to Redis using UNIX domain sockets.
This feature can be accessed using `config::unix_socket`.
* (Issue https://github.com/boostorg/redis/issues/255[255])
Fixed an issue that caused `async_run` to complete when a connection
establishment error is encountered, even if `config::reconnect_wait_interval`
specified reconnection.
* (Issue https://github.com/boostorg/redis/issues/265[265])
`connection::async_exec` now uses `ignore` as the default response,
rather than having no default response. This matches
`basic_connection::async_exec` behavior.
* (Issue https://github.com/boostorg/redis/issues/260[260])
Fixed a memory corruption affecting the logger's prefix
configured in `config::log_prefix`.
* (Pull request https://github.com/boostorg/redis/pull/254[254])
Fixed some warnings regarding unused variables, name shadowing
and narrowing conversions.
* (Issue https://github.com/boostorg/redis/issues/252[252])
Fixed a bug that causes reconnection to ignore the reconnection
wait time configured in `config::reconnect_wait_interval`.
* (Issue https://github.com/boostorg/redis/issues/238[238])
Fixed a bug introduced in Boost 1.88 that caused `response<T>` to
not compile for some integral types.
* (Issue https://github.com/boostorg/redis/issues/247[247])
The documentation has been modernized, and a more complete
reference section has been added.
* Part of the internals have been refactored to allow for better
testing and reduce compile times.
== Boost 1.88
* (Issue https://github.com/boostorg/redis/issues/233[233])
To deal with keys that might not exits in the Redis server, the
library supports `std::optional`, for example
`response<std::optional<std::vector<std::string>>>`. In some cases
however, such as the https://redis.io/docs/latest/commands/mget/[MGET] command,
each element in the vector might be non exiting, now it is possible
to specify a response as `response<std::optional<std::vector<std::optional<std::string>>>>`.
* (Issue https://github.com/boostorg/redis/issues/225[225])
Use `deferred` as the connection default completion token.
* (Issue https://github.com/boostorg/redis/issues/128[128])
Adds a new `async_exec` overload that allows passing response
adapters. This makes it possible to receive Redis responses directly
in custom data structures thereby avoiding unnecessary data copying.
Thanks to Ruben Perez (@anarthal) for implementing this feature.
* There are also other multiple small improvements in this release,
users can refer to the git history for more details.
== Boost 1.87
* (Issue https://github.com/boostorg/redis/issues/205[205])
Improves reaction time to disconnection by using `wait_for_one_error`
instead of `wait_for_all`. The function `connection::async_run` was
also changed to return EOF to the user when that error is received
from the server. That is a breaking change.
* (Issue https://github.com/boostorg/redis/issues/210[210])
Fixes the adapter of empty nested responses.
* (Issues https://github.com/boostorg/redis/issues/211[211] and https://github.com/boostorg/redis/issues/212[212])
Fixes the reconnect loop that would hang under certain conditions,
see the linked issues for more details.
* (Issue https://github.com/boostorg/redis/issues/219[219])
Changes the default log level from `disabled` to `debug`.
== Boost 1.85
* (Issue https://github.com/boostorg/redis/issues/170[170])
Under load and on low-latency networks it is possible to start
receiving responses before the write operation completed and while
the request is still marked as staged and not written. This messes
up with the heuristics that classifies responses as unsolicited or
not.
* (Issue https://github.com/boostorg/redis/issues/168[168]).
Provides a way of passing a custom SSL context to the connection.
The design here differs from that of Boost.Beast and Boost.MySql
since in Boost.Redis the connection owns the context instead of only
storing a reference to a user provided one. This is ok so because
apps need only one connection for their entire application, which
makes the overhead of one ssl-context per connection negligible.
* (Issue https://github.com/boostorg/redis/issues/181[181]).
See a detailed description of this bug in
https://github.com/boostorg/redis/issues/181#issuecomment-1913346983[this comment].
* (Issue https://github.com/boostorg/redis/issues/182[182]).
Sets `"default"` as the default value of `config::username`. This
makes it simpler to use the `requirepass` configuration in Redis.
* (Issue https://github.com/boostorg/redis/issues/189[189]).
Fixes narrowing conversion by using `std::size_t` instead of
`std::uint64_t` for the sizes of bulks and aggregates. The code
relies now on `std::from_chars` returning an error if a value
greater than 32 is received on platforms on which the size
of `std::size_t` is 32.
== Boost 1.84 (First release in Boost)
* Deprecates the `async_receive` overload that takes a response. Users
should now first call `set_receive_response` to avoid constantly and
unnecessarily setting the same response.
* Uses `std::function` to type erase the response adapter. This change
should not influence users in any way but allowed important
simplification in the connections internals. This resulted in
massive performance improvement.
* The connection has a new member `get_usage()` that returns the
connection usage information, such as number of bytes written,
received etc.
* There are massive performance improvements in the consuming of
server pushes which are now communicated with an `asio::channel` and
therefore can be buffered which avoids blocking the socket read-loop.
Batch reads are also supported by means of `channel.try_send` and
buffered messages can be consumed synchronously with
`connection::receive`. The function `boost::redis::cancel_one` has
been added to simplify processing multiple server pushes contained
in the same `generic_response`. *IMPORTANT*: These changes may
result in more than one push in the response when
`connection::async_receive` resumes. The user must therefore be
careful when calling `resp.clear()`: either ensure that all message
have been processed or just use `consume_one`.
== v1.4.2 (incorporates changes to conform the boost review and more)
* Adds `boost::redis::config::database_index` to make it possible to
choose a database before starting running commands e.g. after an
automatic reconnection.
* Massive performance improvement. One of my tests went from
140k req/s to 390k/s. This was possible after a parser
simplification that reduced the number of reschedules and buffer
rotations.
* Adds Redis stream example.
* Renames the project to Boost.Redis and moves the code into namespace
`boost::redis`.
* As pointed out in the reviews the `to_bulk` and `from_bulk` names were too
generic for ADL customization points. They gained the prefix `boost_redis_`.
* Moves `boost::redis::resp3::request` to `boost::redis::request`.
* Adds new typedef `boost::redis::response` that should be used instead of
`std::tuple`.
* Adds new typedef `boost::redis::generic_response` that should be used instead
of `std::vector<resp3::node<std::string>>`.
* Renames `redis::ignore` to `redis::ignore_t`.
* Changes `async_exec` to receive a `redis::response` instead of an adapter,
namely, instead of passing `adapt(resp)` users should pass `resp` directly.
* Introduces `boost::redis::adapter::result` to store responses to commands
including possible resp3 errors without losing the error diagnostic part. To
access values now use `std::get<N>(resp).value()` instead of
`std::get<N>(resp)`.
* Implements full-duplex communication. Before these changes the connection
would wait for a response to arrive before sending the next one. Now requests
are continuously coalesced and written to the socket. `request::coalesce`
became unnecessary and was removed. I could measure significative performance
gains with these changes.
* Improves serialization examples using Boost.Describe to serialize to JSON and protobuf. See
cpp20_json.cpp and cpp20_protobuf.cpp for more details.
* Upgrades to Boost 1.81.0.
* Fixes build with pass:[libc++].
* Adds high-level functionality to the connection classes. For
example, `boost::redis::connection::async_run` will automatically
resolve, connect, reconnect and perform health checks.
== v1.4.0-1
* Renames `retry_on_connection_lost` to `cancel_if_unresponded`. (v1.4.1)
* Removes dependency on Boost.Hana, `boost::string_view`, Boost.Variant2 and Boost.Spirit.
* Fixes build and setup CI on windows.
== v1.3.0-1
* Upgrades to Boost 1.80.0
* Removes automatic sending of the `HELLO` command. This can't be
implemented properly without bloating the connection class. It is
now a user responsibility to send HELLO. Requests that contain it have
priority over other requests and will be moved to the front of the
queue, see `aedis::request::config`
* Automatic name resolving and connecting have been removed from
`aedis::connection::async_run`. Users have to do this step manually
now. The reason for this change is that having them built-in doesn't
offer enough flexibility that is need for boost users.
* Removes healthy checks and idle timeout. This functionality must now
be implemented by users, see the examples. This is
part of making Aedis useful to a larger audience and suitable for
the Boost review process.
* The `aedis::connection` is now using a typeddef to a
`net::ip::tcp::socket` and `aedis::ssl::connection` to
`net::ssl::stream<net::ip::tcp::socket>`. Users that need to use
other stream type must now specialize `aedis::basic_connection`.
* Adds a low level example of async code.
== v1.2.0
* `aedis::adapt` supports now tuples created with `std::tie`.
`aedis::ignore` is now an alias to the type of `std::ignore`.
* Provides allocator support for the internal queue used in the
`aedis::connection` class.
* Changes the behaviour of `async_run` to complete with success if
asio::error::eof is received. This makes it easier to write
composed operations with awaitable operators.
* Adds allocator support in the `aedis::request` (a
contribution from Klemens Morgenstern).
* Renames `aedis::request::push_range2` to `push_range`. The
suffix 2 was used for disambiguation. Klemens fixed it with SFINAE.
* Renames `fail_on_connection_lost` to
`aedis::request::config::cancel_on_connection_lost`. Now, it will
only cause connections to be canceled when `async_run` completes.
* Introduces `aedis::request::config::cancel_if_not_connected` which will
cause a request to be canceled if `async_exec` is called before a
connection has been established.
* Introduces new request flag `aedis::request::config::retry` that if
set to true will cause the request to not be canceled when it was
sent to Redis but remained unresponded after `async_run` completed.
It provides a way to avoid executing commands twice.
* Removes the `aedis::connection::async_run` overload that takes
request and adapter as parameters.
* Changes the way `aedis::adapt()` behaves with
`std::vector<aedis::resp3::node<T>>`. Receiving RESP3 simple errors,
blob errors or null won't causes an error but will be treated as
normal response. It is the user responsibility to check the content
in the vector.
* Fixes a bug in `connection::cancel(operation::exec)`. Now this
call will only cancel non-written requests.
* Implements per-operation implicit cancellation support for
`aedis::connection::async_exec`. The following call will `co_await (conn.async_exec(...) || timer.async_wait(...))`
will cancel the request as long as it has not been written.
* Changes `aedis::connection::async_run` completion signature to
`f(error_code)`. This is how is was in the past, the second
parameter was not helpful.
* Renames `operation::receive_push` to `aedis::operation::receive`.
== v1.1.0-1
* Removes `coalesce_requests` from the `aedis::connection::config`, it
became a request property now, see `aedis::request::config::coalesce`.
* Removes `max_read_size` from the `aedis::connection::config`. The maximum
read size can be specified now as a parameter of the
`aedis::adapt()` function.
* Removes `aedis::sync` class, see intro_sync.cpp for how to perform
synchronous and thread safe calls. This is possible in Boost. 1.80
only as it requires `boost::asio::deferred`.
* Moves from `boost::optional` to `std::optional`. This is part of
moving to pass:[C++17].
* Changes the behaviour of the second `aedis::connection::async_run` overload
so that it always returns an error when the connection is lost.
* Adds TLS support, see intro_tls.cpp.
* Adds an example that shows how to resolve addresses over sentinels,
see subscriber_sentinel.cpp.
* Adds a `aedis::connection::timeouts::resp3_handshake_timeout`. This is
timeout used to send the `HELLO` command.
* Adds `aedis::endpoint` where in addition to host and port, users can
optionally provide username, password and the expected server role
(see `aedis::error::unexpected_server_role`).
* `aedis::connection::async_run` checks whether the server role received in
the hello command is equal to the expected server role specified in
`aedis::endpoint`. To skip this check let the role variable empty.
* Removes reconnect functionality from `aedis::connection`. It
is possible in simple reconnection strategies but bloats the class
in more complex scenarios, for example, with sentinel,
authentication and TLS. This is trivial to implement in a separate
coroutine. As a result the `enum event` and `async_receive_event`
have been removed from the class too.
* Fixes a bug in `connection::async_receive_push` that prevented
passing any response adapter other that `adapt(std::vector<node>)`.
* Changes the behaviour of `aedis::adapt()` that caused RESP3 errors
to be ignored. One consequence of it is that `connection::async_run`
would not exit with failure in servers that required authentication.
* Changes the behaviour of `connection::async_run` that would cause it
to complete with success when an error in the
`connection::async_exec` occurred.
* Ports the buildsystem from autotools to CMake.
== v1.0.0
* Adds experimental cmake support for windows users.
* Adds new class `aedis::sync` that wraps an `aedis::connection` in
a thread-safe and synchronous API. All free functions from the
`sync.hpp` are now member functions of `aedis::sync`.
* Split `aedis::connection::async_receive_event` in two functions, one
to receive events and another for server side pushes, see
`aedis::connection::async_receive_push`.
* Removes collision between `aedis::adapter::adapt` and
`aedis::adapt`.
* Adds `connection::operation` enum to replace `cancel_*` member
functions with a single cancel function that gets the operations
that should be cancelled as argument.
* Bugfix: a bug on reconnect from a state where the `connection` object
had unsent commands. It could cause `async_exec` to never
complete under certain conditions.
* Bugfix: Documentation of `adapt()` functions were missing from
Doxygen.
== v0.3.0
* Adds `experimental::exec` and `receive_event` functions to offer a
thread safe and synchronous way of executing requests across
threads. See `intro_sync.cpp` and `subscriber_sync.cpp` for
examples.
* `connection::async_read_push` was renamed to `async_receive_event`.
* `connection::async_receive_event` is now being used to communicate
internal events to the user, such as resolve, connect, push etc. For
examples see cpp20_subscriber.cpp and `connection::event`.
* The `aedis` directory has been moved to `include` to look more
similar to Boost libraries. Users should now replace `-I/aedis-path`
with `-I/aedis-path/include` in the compiler flags.
* The `AUTH` and `HELLO` commands are now sent automatically. This change was
necessary to implement reconnection. The username and password
used in `AUTH` should be provided by the user on
`connection::config`.
* Adds support for reconnection. See `connection::enable_reconnect`.
* Fixes a bug in the `connection::async_run(host, port)` overload
that was causing crashes on reconnection.
* Fixes the executor usage in the connection class. Before these
changes it was imposing `any_io_executor` on users.
* `connection::async_receiver_event` is not cancelled anymore when
`connection::async_run` exits. This change makes user code simpler.
* `connection::async_exec` with host and port overload has been
removed. Use the other `connection::async_run` overload.
* The host and port parameters from `connection::async_run` have been
move to `connection::config` to better support authentication and
failover.
* Many simplifications in the `chat_room` example.
* Fixes build in clang the compilers and makes some improvements in
the documentation.
== v0.2.0-1
* Fixes a bug that happens on very high load. (v0.2.1)
* Major rewrite of the high-level API. There is no more need to use the low-level API anymore.
* No more callbacks: Sending requests follows the ASIO asynchronous model.
* Support for reconnection: Pending requests are not canceled when a connection is lost and are re-sent when a new one is established.
* The library is not sending HELLO-3 on user behalf anymore. This is important to support AUTH properly.
== v0.1.0-2
* Adds reconnect coroutine in the `echo_server` example. (v0.1.2)
* Corrects `client::async_wait_for_data` with `make_parallel_group` to launch operation. (v0.1.2)
* Improvements in the documentation. (v0.1.2)
* Avoids dynamic memory allocation in the client class after reconnection. (v0.1.2)
* Improves the documentation and adds some features to the high-level client. (v.0.1.1)
* Improvements in the design and documentation.
== v0.0.1
* First release to collect design feedback.

View File

@@ -0,0 +1,121 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.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)
//
= Comparison with other Redis clients
## Comparison
The main reason for why I started writing Boost.Redis was to have a client
compatible with the Asio asynchronous model. As I made progresses I could
also address what I considered weaknesses in other libraries. Due to
time constraints I won't be able to give a detailed comparison with
each client listed in the
https://redis.io/docs/clients/#cpp[official list].
Instead, I will focus on the most popular pass:[C++] client on github in number of stars, namely:
https://github.com/sewenew/redis-plus-plus[]
### Boost.Redis vs Redis-plus-plus
Before we start it is important to mention some of the things
redis-plus-plus does not support
* The latest version of the communication protocol RESP3. Without that it is impossible to support some important Redis features like client side caching, among other things.
* Coroutines.
* Reading responses directly in user data structures to avoid creating temporaries.
* Error handling with support for error-code.
* Cancellation.
The remaining points will be addressed individually. Let us first
have a look at what sending a command a pipeline and a transaction
look like
[source,cpp]
----
auto redis = Redis("tcp://127.0.0.1:6379");
// Send commands
redis.set("key", "val");
auto val = redis.get("key"); // val is of type OptionalString.
if (val)
std::cout << *val << std::endl;
// Sending pipelines
auto pipe = redis.pipeline();
auto pipe_replies = pipe.set("key", "value")
.get("key")
.rename("key", "new-key")
.rpush("list", {"a", "b", "c"})
.lrange("list", 0, -1)
.exec();
// Parse reply with reply type and index.
auto set_cmd_result = pipe_replies.get<bool>(0);
// ...
// Sending a transaction
auto tx = redis.transaction();
auto tx_replies = tx.incr("num0")
.incr("num1")
.mget({"num0", "num1"})
.exec();
auto incr_result0 = tx_replies.get<long long>(0);
// ...
----
Some of the problems with this API are
* Heterogeneous treatment of commands, pipelines and transaction. This makes auto-pipelining impossible.
* Any Api that sends individual commands has a very restricted scope of usability and should be avoided for performance reasons.
* The API imposes exceptions on users, no error-code overload is provided.
* No way to reuse the buffer for new calls to e.g. redis.get in order to avoid further dynamic memory allocations.
* Error handling of resolve and connection not clear.
According to the documentation, pipelines in redis-plus-plus have
the following characteristics
> +NOTE+: By default, creating a Pipeline object is NOT cheap, since
> it creates a new connection.
This is clearly a downside in the API as pipelines should be the
default way of communicating and not an exception, paying such a
high price for each pipeline imposes a severe cost in performance.
Transactions also suffer from the very same problem:
> +NOTE+: Creating a Transaction object is NOT cheap, since it
> creates a new connection.
In Boost.Redis there is no difference between sending one command, a
pipeline or a transaction because requests are decoupled
from the I/O objects.
> redis-plus-plus also supports async interface, however, async
> support for Transaction and Subscriber is still on the way.
>
> The async interface depends on third-party event library, and so
> far, only libuv is supported.
Async code in redis-plus-plus looks like the following
[source,cpp]
----
auto async_redis = AsyncRedis(opts, pool_opts);
Future<string> ping_res = async_redis.ping();
cout << ping_res.get() << endl;
----
As the reader can see, the async interface is based on futures
which is also known to have a bad performance. The biggest
problem however with this async design is that it makes it
impossible to write asynchronous programs correctly since it
starts an async operation on every command sent instead of
enqueueing a message and triggering a write when it can be sent.
It is also not clear how are pipelines realised with this design
(if at all).

View File

@@ -0,0 +1,27 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.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)
//
= Examples
The examples below show how to use the features discussed throughout this documentation:
* {site-url}/example/cpp20_intro.cpp[cpp20_intro.cpp]: Does not use awaitable operators.
* {site-url}/example/cpp20_intro_tls.cpp[cpp20_intro_tls.cpp]: Communicates over TLS.
* {site-url}/example/cpp20_unix_sockets.cpp[cpp20_unix_sockets.cpp]: Communicates over UNIX domain sockets.
* {site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp]: Shows how to send and receive STL containers and how to use transactions.
* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp]: Shows how to serialize types using Boost.Json.
* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: Shows how to serialize types using protobuf.
* {site-url}/example/cpp20_sentinel.cpp[cpp20_sentinel.cpp]: Shows how to use the library with a Sentinel deployment.
* {site-url}/example/cpp20_subscriber.cpp[cpp20_subscriber.cpp]: Shows how to implement pubsub with reconnection re-subscription.
* {site-url}/example/cpp20_echo_server.cpp[cpp20_echo_server.cpp]: A simple TCP echo server.
* {site-url}/example/cpp20_chat_room.cpp[cpp20_chat_room.cpp]: A command line chat built on Redis pubsub.
* {site-url}/example/cpp17_intro.cpp[cpp17_intro.cpp]: Uses callbacks and requires pass:[C++17].
* {site-url}/example/cpp17_intro_sync.cpp[cpp17_intro_sync.cpp]: Runs `async_run` in a separate thread and performs synchronous calls to `async_exec`.
* {site-url}/example/cpp17_spdlog.cpp[cpp17_spdlog.cpp]: Shows how to use third-party logging libraries like `spdlog` with Boost.Redis.
The main function used in some async examples has been factored out in
the {site-url}/example/main.cpp[main.cpp] file.

View File

@@ -0,0 +1,154 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.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)
//
[#intro]
= Introduction
Boost.Redis is a high-level https://redis.io/[Redis] and https://valkey.io/[Valkey]
client library built on top of
https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html[Boost.Asio]
that implements the Redis protocol
https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md[RESP3].
== Requirements
The requirements for using Boost.Redis are:
* Boost 1.84 or higher. Boost.Redis is included in Boost installations since Boost 1.84.
* pass:[C++17] or higher. Supported compilers include gcc 11 and later, clang 11 and later, and Visual Studio 16 (2019) and later.
* Redis 6 or higher, or Valkey (any version). The database server must support RESP3.
We intend to maintain compatibility with both Redis and Valkey in the long-run.
* OpenSSL.
The documentation assumes basic-level knowledge about https://redis.io/docs/[Redis] and https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html[Boost.Asio].
== Building the library
To use the library it is necessary to include the following:
[source,cpp]
----
#include <boost/redis/src.hpp>
----
in exactly one source file in your applications. Otherwise, the library is header-only.
Boost.Redis unconditionally requires OpenSSL. Targets using Boost.Redis need to link
to the OpenSSL libraries.
== Tutorial
The code below uses a short-lived connection to
https://redis.io/commands/ping/[ping] a Redis server:
[source,cpp]
----
#include <boost/redis/connection.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
namespace net = boost::asio;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
using boost::redis::connection;
auto co_main(config const& cfg) -> net::awaitable<void>
{
auto conn = std::make_shared<connection>(co_await net::this_coro::executor);
conn->async_run(cfg, {}, net::consign(net::detached, conn));
// A request containing only a ping command.
request req;
req.push("PING", "Hello world");
// Response object.
response<std::string> resp;
// Executes the request.
co_await conn->async_exec(req, resp);
conn->cancel();
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
}
----
The roles played by the `async_run` and `async_exec` functions are:
* xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`connection::async_exec`]: executes the commands contained in the
request and stores the individual responses in the response object. Can
be called from multiple places in your code concurrently.
* xref:reference:boost/redis/basic_connection/async_run-04.adoc[`connection::async_run`]: keeps the connection healthy. It takes care of hostname resolution, session establishment, health-checks, reconnection and coordination of low-level read and write operations. It should be called only once per connection, regardless of the number of requests to execute.
== Server pushes
Redis servers can also send a variety of pushes to the client. Some of
them are:
* https://redis.io/docs/manual/pubsub/[Pubsub messages].
* https://redis.io/docs/manual/keyspace-notifications/[Keyspace notifications].
* https://redis.io/docs/manual/client-side-caching/[Client-side caching].
The connection class supports server pushes by means of the
xref:reference:boost/redis/basic_connection/async_receive.adoc[`connection::async_receive2`] function, which can be
called in the same connection that is being used to execute commands.
The coroutine below shows how to use it
[source,cpp]
----
auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
{
generic_flat_response resp;
conn->set_receive_response(resp);
// Subscribe to the channel 'mychannel'. You can add any number of channels here.
request req;
req.subscribe({"mychannel"});
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.
// You need to use specialized request::subscribe() function (instead of request::push)
// to enable this behavior.
// Loop to read Redis push messages.
while (conn->will_reconnect()) {
// Wait for pushes
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
// Check for errors and cancellations
if (ec) {
std::cerr << "Error during receive: " << ec << std::endl;
break;
}
// The response must be consumed without suspending the
// coroutine i.e. without the use of async operations.
for (auto const& elem : resp.value())
std::cout << elem.value << "\n";
std::cout << std::endl;
resp.value().clear();
}
}
----
== Further reading
Here is a list of topics that you might be interested in:
* xref:cancellation.adoc[Setting timeouts to requests and managing cancellation].
* xref:requests_responses.adoc[More on requests and responses].
* xref:serialization.adoc[Serializing and parsing into custom types].
* xref:logging.adoc[Configuring logging].
* xref:examples.adoc[Examples].

View File

@@ -0,0 +1,45 @@
//
// 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)
//
= Logging
xref:reference:boost/redis/basic_connection/async_run-04.adoc[`connection::async_run`]
is a complex algorithm, with features like built-in reconnection.
This can make configuration problems, like a misconfigured hostname, difficult to debug -
Boost.Redis will keep retrying to connect to the same hostname over and over.
For this reason, Boost.Redis incorporates a lightweight logging solution, and
*will log some status messages to stderr by default*.
Logging can be customized by passing a
xref:reference:boost/redis/logger.adoc[`logger`] object to the connection's constructor. For example, logging can be disabled by writing:
[source,cpp]
----
asio::io_context ioc;
connection conn {ioc, logger{logger::level::disabled}};
----
Every message logged by the library is attached a
https://en.wikipedia.org/wiki/Syslog#Severity_level[syslog-like severity]
tag (a xref:reference:boost/redis/logger/level.adoc[`logger::level`]).
You can filter messages by severity by creating a `logger` with a specific level:
[source,cpp]
----
asio::io_context ioc;
// Logs to stderr messages with severity >= level::error.
// This will hide all informational output.
connection conn {ioc, logger{logger::level::error}};
----
The `logger` constructor accepts a `std::function<void(logger::level, std::string_view)>`
as second argument. If supplied, Boost.Redis will call this function when logging
instead of printing to stderr. This can be used to integrate third-party logging
libraries. See our {site-url}/example/cpp17_spdlog.cpp[spdlog integration example]
for sample code.

View File

@@ -0,0 +1,104 @@
//
// 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)
//
[#reference]
= Reference
[width=100%,cols="5*"]
|===
| *Connections*
| *Requests and responses*
| *Adapters*
| *RESP3 protocol*
| *Unstable low-level APIs*
|
xref:reference:boost/redis/connection.adoc[`connection`]
xref:reference:boost/redis/basic_connection.adoc[`basic_connection`]
xref:reference:boost/redis/address.adoc[`address`]
xref:reference:boost/redis/role.adoc[`role`]
xref:reference:boost/redis/config.adoc[`config`]
xref:reference:boost/redis/sentinel_config.adoc[`sentinel_config`]
xref:reference:boost/redis/error.adoc[`error`]
xref:reference:boost/redis/logger.adoc[`logger`]
xref:reference:boost/redis/logger/level.adoc[`logger::level`]
xref:reference:boost/redis/operation.adoc[`operation`]
xref:reference:boost/redis/usage.adoc[`usage`]
|
xref:reference:boost/redis/ignore_t.adoc[`ignore_t`]
xref:reference:boost/redis/ignore.adoc[`ignore`]
xref:reference:boost/redis/request.adoc[`request`]
xref:reference:boost/redis/request/config.adoc[`request::config`]
xref:reference:boost/redis/response.adoc[`response`]
xref:reference:boost/redis/generic_response.adoc[`generic_response`]
xref:reference:boost/redis/generic_flat_response.adoc[`generic_flat_response`]
xref:reference:boost/redis/consume_one-08.adoc[`consume_one`]
|
xref:reference:boost/redis/adapter/boost_redis_adapt.adoc[`boost_redis_adapt`]
xref:reference:boost/redis/adapter/ignore.adoc[`adapter::ignore`]
xref:reference:boost/redis/adapter/error.adoc[`adapter::error`]
xref:reference:boost/redis/adapter/result.adoc[`adapter::result`]
xref:reference:boost/redis/any_adapter.adoc[`any_adapter`]
|
xref:reference:boost/redis/resp3/basic_node.adoc[`resp3::basic_node`]
xref:reference:boost/redis/resp3/node.adoc[`resp3::node`]
xref:reference:boost/redis/resp3/node_view.adoc[`resp3::node_view`]
xref:reference:boost/redis/resp3/basic_tree.adoc[`resp3::basic_tree`]
xref:reference:boost/redis/resp3/tree.adoc[`resp3::tree`]
xref:reference:boost/redis/resp3/view_tree.adoc[`resp3::view_tree`]
xref:reference:boost/redis/resp3/flat_tree.adoc[`resp3::flat_tree`]
xref:reference:boost/redis/resp3/boost_redis_to_bulk-08.adoc[`boost_redis_to_bulk`]
xref:reference:boost/redis/resp3/type.adoc[`resp3::type`]
xref:reference:boost/redis/resp3/is_aggregate.adoc[`resp3::is_aggregate`]
|
xref:reference:boost/redis/adapter/adapt2.adoc[`adapter::adapt2`]
xref:reference:boost/redis/resp3/parser.adoc[`resp3::parser`]
xref:reference:boost/redis/resp3/parse.adoc[`resp3::parse`]
|===

View File

@@ -0,0 +1,303 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.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)
//
= Requests and responses
== Requests
Redis requests are composed of one or more commands. In the
Redis documentation, requests are called
https://redis.io/topics/pipelining[pipelines]. For example:
[source,cpp]
----
// Some example containers.
std::list<std::string> list {...};
std::map<std::string, mystruct> map { ...};
// The request can contain multiple commands.
request req;
// Command with variable length of arguments.
req.push("SET", "key", "some value", "EX", "2");
// Pushes a list.
req.push_range("SUBSCRIBE", list);
// Same as above but as an iterator range.
req.push_range("SUBSCRIBE", std::cbegin(list), std::cend(list));
// Pushes a map.
req.push_range("HSET", "key", map);
----
Sending a request to Redis is performed by
xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`connection::async_exec`]
as already stated. Requests accept a xref:reference:boost/redis/request/config.adoc[`boost::redis::request::config`]
object when constructed that dictates how requests are handled in situations like
reconnection. The reader is advised to read it carefully.
## Responses
Boost.Redis uses the following strategy to deal with Redis responses:
* xref:reference:boost/redis/response.adoc[`boost::redis::response`] should be used
when the request's number of commands is known at compile-time.
* xref:reference:boost/redis/generic_response.adoc[`boost::redis::generic_response`] should be
used when the number of commands is dynamic.
For example, the request below has three commands:
[source,cpp]
----
request req;
req.push("PING");
req.push("INCR", "key");
req.push("QUIT");
----
Therefore, its response will also contain three elements.
The following response object can be used:
[source,cpp]
----
response<std::string, int, std::string>
----
The response behaves as a `std::tuple` and must
have as many elements as the request has commands (exceptions below).
It is also necessary that each tuple element is capable of storing the
response to the command it refers to, otherwise an error will occur.
To ignore responses to individual commands in the request use the tag
xref:reference:boost/redis/ignore_t.adoc[`boost::redis::ignore_t`]. For example:
[source,cpp]
----
// Ignore the second and last responses.
response<std::string, ignore_t, std::string, ignore_t>
----
The following table provides the RESP3-types returned by some Redis
commands:
[cols="3*"]
|===
| *Command* | *RESP3 type* | *Documentation*
| `lpush` | Number | https://redis.io/commands/lpush[]
| `lrange` | Array | https://redis.io/commands/lrange[]
| `set` | Simple-string, null or blob-string | https://redis.io/commands/set[]
| `get` | Blob-string | https://redis.io/commands/get[]
| `smembers` | Set | https://redis.io/commands/smembers[]
| `hgetall` | Map | https://redis.io/commands/hgetall[]
|===
To map these RESP3 types into a pass:[C++] data structure use the table below:
[cols="3*"]
|===
| *RESP3 type* | *Possible pass:[C++] type* | *Type*
| Simple-string | `std::string` | Simple
| Simple-error | `std::string` | Simple
| Blob-string | `std::string`, `std::vector` | Simple
| Blob-error | `std::string`, `std::vector` | Simple
| Number | `long long`, `int`, `std::size_t`, `std::string` | Simple
| Double | `double`, `std::string` | Simple
| Null | `std::optional<T>` | Simple
| Array | `std::vector`, `std::list`, `std::array`, `std::deque` | Aggregate
| Map | `std::vector`, `std::map`, `std::unordered_map` | Aggregate
| Set | `std::vector`, `std::set`, `std::unordered_set` | Aggregate
| Push | `std::vector`, `std::map`, `std::unordered_map` | Aggregate
|===
For example, the response to the request
[source,cpp]
----
request req;
req.push("HELLO", 3);
req.push_range("RPUSH", "key1", vec);
req.push_range("HSET", "key2", map);
req.push("LRANGE", "key3", 0, -1);
req.push("HGETALL", "key4");
req.push("QUIT");
----
Can be read in the following response object:
[source,cpp]
----
response<
redis::ignore_t, // hello
int, // rpush
int, // hset
std::vector<T>, // lrange
std::map<U, V>, // hgetall
std::string // quit
> resp;
----
To execute the request and read the response use
xref:reference:boost/redis/basic_connection/async_exec-02.adoc[`async_exec`]:
[source,cpp]
----
co_await conn->async_exec(req, resp);
----
If the intention is to ignore responses altogether, use
xref:reference:boost/redis/ignore.adoc[`ignore`]:
[source,cpp]
----
// Ignores the response
co_await conn->async_exec(req, ignore);
----
Responses that contain nested aggregates or heterogeneous data
types will be given special treatment later in xref:#the-general-case[the general case]. As
of this writing, not all RESP3 types are used by the Redis server,
which means in practice users will be concerned with a reduced
subset of the RESP3 specification.
### Pushes
Commands that have no response, like
* `"SUBSCRIBE"`
* `"PSUBSCRIBE"`
* `"UNSUBSCRIBE"`
must **NOT** be included in the response tuple. For example, the following request
[source,cpp]
----
request req;
req.push("PING");
req.subscribe({"channel"});
req.push("QUIT");
----
must be read in the response object `response<std::string, std::string>`.
### Null
It is not uncommon for apps to access keys that do not exist or that
have already expired in the Redis server. To deal with these use cases,
wrap the type within a `std::optional`:
[source,cpp]
----
response<
std::optional<A>,
std::optional<B>,
...
> resp;
----
Everything else stays the same.
### Transactions
To read responses to transactions we must first observe that Redis
will queue the transaction commands and send their individual
responses as elements of an array. The array itself is the response to
the `EXEC` command. For example, to read the response to this request
[source,cpp]
----
req.push("MULTI");
req.push("GET", "key1");
req.push("LRANGE", "key2", 0, -1);
req.push("HGETALL", "key3");
req.push("EXEC");
----
Use the following response type:
[source,cpp]
----
response<
ignore_t, // multi
ignore_t, // QUEUED
ignore_t, // QUEUED
ignore_t, // QUEUED
response<
std::optional<std::string>, // get
std::optional<std::vector<std::string>>, // lrange
std::optional<std::map<std::string, std::string>> // hgetall
> // exec
> resp;
----
For a complete example, see {site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp].
[#the-general-case]
### The general case
There are cases where responses to Redis
commands won't fit in the model presented above. Some examples are:
* Commands (like `set`) whose responses don't have a fixed
RESP3 type. Expecting an `int` and receiving a blob-string
results in an error.
* RESP3 aggregates that contain nested aggregates can't be read in STL containers.
* Transactions with a dynamic number of commands can't be read in a `response`.
To deal with these cases Boost.Redis provides the xref:reference:boost/redis/resp3/node.adoc[`boost::redis::resp3::node`] type
abstraction, that is the most general form of an element in a
response, be it a simple RESP3 type or the element of an aggregate. It
is defined like:
[source,cpp]
----
template <class String>
struct basic_node {
// The RESP3 type of the data in this node.
type data_type;
// The number of elements of an aggregate (or 1 for simple data).
std::size_t aggregate_size;
// The depth of this node in the response tree.
std::size_t depth;
// The actual data. For aggregate types this is always empty.
String value;
};
----
Any response to a Redis command can be parsed into a
xref:reference:boost/redis/generic_response.adoc[boost::redis::generic_response]
and its counterpart xref:reference:boost/redis/generic_flat_response.adoc[boost::redis::generic_flat_response].
The vector can be seen as a pre-order view of the response tree.
Using it is not different than using other types:
[source,cpp]
----
// Receives any RESP3 simple or aggregate data type.
boost::redis::generic_response resp;
co_await conn->async_exec(req, resp);
----
For example, suppose we want to retrieve a hash data structure
from Redis with `HGETALL`, some of the options are
* `boost::redis::generic_response` and `boost::redis::generic_flat_response`: always works.
* `std::vector<std::string>`: efficient and flat, all elements as string.
* `std::map<std::string, std::string>`: efficient if you need the data as a `std::map`.
* `std::map<U, V>`: efficient if you are storing serialized data. Avoids temporaries and requires `boost_redis_from_bulk` for `U` and `V`.
In addition to the above users can also use unordered versions of the
containers. The same reasoning applies to sets e.g. `SMEMBERS`
and other data structures in general.

View File

@@ -0,0 +1,152 @@
//
// 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)
//
= Sentinel
Boost.Redis supports Redis Sentinel deployments. Sentinel handling
in `connection` is built-in: xref:reference:boost/redis/basic_connection/async_run-04.adoc[`async_run`]
automatically connects to Sentinels, resolves the master's address, and connects to the master.
Configuration is done using xref:reference:boost/redis/sentinel_config.adoc[`config::sentinel`]:
[source,cpp]
----
config cfg;
// To enable Sentinel, set this field to a non-empty list
// of (hostname, port) pairs where Sentinels are listening
cfg.sentinel.addresses = {
{"sentinel1.example.com", "26379"},
{"sentinel2.example.com", "26379"},
{"sentinel3.example.com", "26379"},
};
// Set master_name to the identifier that you configured
// in the "sentinel monitor" statement of your sentinel.conf file
cfg.sentinel.master_name = "mymaster";
----
Once set, the connection object can be used normally. See our
our {site-url}/example/cpp20_sentinel.cpp[Sentinel example]
for a full program.
== Connecting to replicas
By default, the library connects to the Redis master.
You can connect to one of its replicas by using
xref:reference:boost/redis/sentinel_config/server_role.adoc[`config::sentinel::server_role`].
This can be used to balance load, if all your commands read data from
the server and never write to it. The particular replica will be chosen randomly.
[source,cpp]
----
config cfg;
// Set up Sentinel
cfg.sentinel.addresses = {
{"sentinel1.example.com", "26379"},
{"sentinel2.example.com", "26379"},
{"sentinel3.example.com", "26379"},
};
cfg.sentinel.master_name = "mymaster";
// Ask the library to connect to a random replica of 'mymaster', rather than the master node
cfg.sentinel.server_role = role::replica;
----
== Sentinel authentication
If your Sentinels require authentication,
you can use xref:reference:boost/redis/sentinel_config/setup.adoc[`config::sentinel::setup`]
to provide credentials.
This request is executed immediately after connecting to Sentinels, and
before any other command:
[source,cpp]
----
// Set up Sentinel
config cfg;
cfg.sentinel.addresses = {
{"sentinel1.example.com", "26379"},
{"sentinel2.example.com", "26379"},
{"sentinel3.example.com", "26379"},
};
cfg.sentinel.master_name = "mymaster";
// By default, setup contains a 'HELLO 3' command.
// Override it to add an AUTH clause to it with out credentials.
cfg.sentinel.setup.clear();
cfg.sentinel.setup.push("HELLO", 3, "AUTH", "sentinel_user", "sentinel_password");
// cfg.sentinel.setup applies to Sentinels, only.
// Use cfg.setup to authenticate to masters/replicas.
cfg.use_setup = true; // Required for cfg.setup to be used, for historic reasons
cfg.setup.clear();
cfg.setup.push("HELLO", 3, "AUTH", "master_user", "master_password");
----
== Using TLS with Sentinels
You might use TLS with Sentinels only, masters/replicas only, or both by adjusting
xref:reference:boost/redis/sentinel_config/use_ssl.adoc[`config::sentinel::use_ssl`]
and xref:reference:boost/redis/config/use_ssl.adoc[`config::use_ssl`]:
[source,cpp]
----
// Set up Sentinel
config cfg;
cfg.sentinel.addresses = {
{"sentinel1.example.com", "26379"},
{"sentinel2.example.com", "26379"},
{"sentinel3.example.com", "26379"},
};
cfg.sentinel.master_name = "mymaster";
// Adjust these switches to enable/disable TLS
cfg.use_ssl = true; // Applies to masters and replicas
cfg.sentinel.use_ssl = true; // Applies to Sentinels
----
== Sentinel algorithm
This section details how `async_run` interacts with Sentinel.
Most of the algorithm follows
https://redis.io/docs/latest/develop/reference/sentinel-clients/[the official Sentinel client guidelines].
Some of these details may vary between library versions.
* Connections maintain an internal list of Sentinels, bootstrapped from
xref:reference:boost/redis/sentinel_config/addresses.adoc[`config::sentinel::addresses`].
* The first Sentinel in the list is contacted by performing the following:
** A physical connection is established.
** The setup request is executed.
** The master's address is resolved using
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL GET-MASTER-NAME-BY-ADDR`].
** If `config::sentinel::server_role` is `role::replica`, replica addresses are obtained using
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL REPLICAS`].
One replica is chosen randomly.
** The address of other Sentinels also monitoring this master are retrieved using
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL SENTINELS`].
* If a Sentinel is unreachable, doesn't know about the configured master,
or returns an error while executing the above requests, the next Sentinel in the list is tried.
* If all Sentinels have been tried without success, `config::reconnect_wait_interval`
is waited, and the process starts again.
* After a successful Sentinel response, the internal Sentinel list is updated
with any newly discovered Sentinels.
Sentinels in `config::sentinel::addresses` are always kept in the list,
even if they weren't present in the output of `SENTINEL SENTINELS`.
* The retrieved address is used
to establish a connection with the master or replica.
A `ROLE` command is added at the end of the setup request.
This is used to detect situations where a Sentinel returns outdated
information due to a failover in process. If `ROLE` doesn't output
the expected role (`"master"` or `"slave"`, depending on `config::sentinel::server_role`)
`config::reconnect_wait_interval` is waited and Sentinel is contacted again.
* The connection to the master/replica is run like any other connection.
If network errors or timeouts happen, `config::reconnect_wait_interval`
is waited and Sentinel is contacted again.

View File

@@ -0,0 +1,26 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.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)
//
= Serializing and parsing into custom types
Boost.Redis supports serialization of user defined types by means of
the following customization points
[source,cpp]
----
// Serialize
void boost_redis_to_bulk(std::string& to, mystruct const& obj);
// Deserialize
void boost_redis_from_bulk(mystruct& u, node_view const& node, boost::system::error_code&);
----
These functions are accessed over ADL and therefore they must be
imported in the global namespace by the user. The following examples might be of interest:
* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp]: serializes and parses JSON objects.
* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: serializes and parses https://protobuf.dev/[protobuf] objects.

10
doc/mrdocs.cpp Normal file
View File

@@ -0,0 +1,10 @@
//
// Copyright (c) 2025 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)
//
#define BOOST_ALLOW_DEPRECATED // avoid mrdocs errors with the BOOST_DEPRECATED macro
#include <boost/redis.hpp>

31
doc/mrdocs.yml Normal file
View File

@@ -0,0 +1,31 @@
#
# Copyright (c) 2025 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)
#
source-root: ../include
compilation-database: ./CMakeLists.txt
include-symbols:
- "boost::redis::**"
exclude-symbols:
- "boost::redis::detail::**"
- "boost::redis::adapter::detail::**"
- "boost::redis::resp3::detail::**"
- "boost::redis::basic_connection::run_is_canceled"
- "boost::redis::basic_connection::this_type"
- "boost::redis::any_adapter::impl_t"
- "boost::redis::any_adapter::fn_type"
- "boost::redis::any_adapter::create_impl"
- "boost::redis::any_adapter::impl_"
- "boost::redis::request::payload"
- "boost::redis::request::has_hello_priority"
see-below:
- "boost::redis::adapter::ignore"
sort-members: false
base-url: https://github.com/boostorg/redis/blob/master/include/
use-system-libc: true
warn-as-error: true
warn-if-undocumented: false
warn-no-paramdoc: false

View File

@@ -0,0 +1,671 @@
# On the costs of asynchronous abstractions
The biggest force behind the evolution of
[Boost.Redis](https://github.com/boostorg/redis) was my struggling in
coming up with a high-level connection abstraction that was capable of
multiplexing Redis commands from independent sources while
concurrently handling server pushes. This journey taught me many
important lessons, many of which are related to the design and
performance of asynchronous programs based on Boost.Asio.
In this article I will share some of the lessons learned, specially
those related to the performance costs of _abstractions_ such as
`async_read_until` that tend to overschedule into the event-loop. In
this context I will also briefly comment on how the topics discussed
here influenced my views on the proposed
[P2300](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2300r7.html)
(a.k.a. Senders and Receivers), which is likely to become the basis of
networking in upcoming C++ standards.
Although the analysis presented in this article uses the Redis communication
protocol for illustration I expect it to be useful in general since
[RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) shares
many similarities with other widely used protocols such as HTTP.
## Parsing `\r\n`-delimited messages
The Redis server communicates with its clients by exchanging data
serialized in
[RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) format.
Among the data types supported by this specification, the
`\r\n`-delimited messages are some of the most frequent in a typical
session. The table below shows some examples
Command | Response | Wire format | RESP3 name
---------|----------|---------------|---------------------
PING | PONG | `+PONG\r\n` | simple-string
INCR | 42 | `:42\r\n` | number
GET | null | `_\r\n` | null
Redis also supports command pipelines, which provide a way of
optimizing round-trip times by batching commands. A pipeline composed
by the commands shown in the previous table look like this
```
| Sent in a |
| single write |
+--------+ | | +-------+
| | --------> PING + INCR + GET --------> | |
| | | |
| Client | | Redis |
| | | |
| | <-------- "+PONG\r\n:42\r\n_\r\n" <-------- | |
+--------+ |<------>|<---->|<-->| +-------+
| |
| Responses |
```
Messages that use delimiters are so common in networking that a
facility called `async_read_until` for reading them incrementally from
a socket is already part of Boost.Asio. The coroutine below uses it to
print message contents to the screen
```cpp
awaitable<void> parse_resp3_simple_msgs(tcp::socket socket)
{
for (std::string buffer;;) {
auto n = co_await async_read_until(socket, dynamic_buffer(buffer), "\r\n");
std::cout << buffer.substr(1, n - 3) << std::endl;
// Consume the buffer.
buffer.erase(0, n);
}
}
```
If we pay attention to the buffer content as it is parsed by the code
above we can see it is rotated fairly often, for example
```
"+PONG\r\n:100\r\n+OK\r\n_\r\n"
":100\r\n+OK\r\n_\r\n"
"+OK\r\n_\r\n"
"_\r\n"
""
```
When I first realized these, apparently excessive, buffer rotations I
was concerned they would impact the performance of Boost.Redis in a
severe way. To measure the magnitude of this impact I came up with an
experimental implementation of Asio's `dynamic_buffer` that consumed
the buffer less eagerly than the `std::string::erase` function used
above. For that, the implementation increased a buffer offset up
to a certain threshold and only then triggered a (larger) rotation.
This is illustrated in the diagram below
```
|<---- offset threshold ---->|
| |
"+PONG\r\n:100\r\n+OK\r\n_\r\n+PONG\r\n"
| # Initial offset
"+PONG\r\n:100\r\n+OK\r\n_\r\n+PONG\r\n"
|<------>| # After 1st message
"+PONG\r\n:100\r\n+OK\r\n_\r\n+PONG\r\n"
|<-------------->| # After 2nd message
"+PONG\r\n:100\r\n+OK\r\n_\r\n+PONG\r\n"
|<--------------------->| # After 3rd message
"+PONG\r\n:100\r\n+OK\r\n_\r\n+PONG\r\n"
|<-------------------------->| # Threshold crossed after the 4th message
"+PONG\r\n"
| # After rotation
```
After comparing the performance differences between the two versions I
was surprised there wasn't any! But that was also very suspicious
since some RESP3 aggregate types contain a considerable number of
separators. For example, a map with two pairs `[(key1, value1),
(key2, value2)]` encoded in RESP3 requires ten rotations in total
```
"%2\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n"
"$4\r\nkey1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n"
"key1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n"
"$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n"
...
```
It was evident something more costly was shadowing the buffer
rotations. But it couldn't be the search for the separator since it
performs equivalently to rotations. It is also easy to show that the
overhead is not related to any IO operation since the problem persists
if the buffer is never consumed (which causes the function to be
called with the same string repeatedly). Once these two factors
are removed from the table, we are driven into the conclusion that
calling `async_read_until` has an intrinsic cost, let us see what
that is.
### Async operations that complete synchronously considered harmful
Assume the scenario described earlier where `async_read_until` is used
to parse multiple `\r\n`-delimited messages. The following is a
detailed description of what happens behind the scenes
1. `async_read_until` calls `socket.async_read_some` repeatedly
until the separator `\r\n` shows up in the buffer
```
"<read1>" # Read 1: needs more data.
"<read1><read2>" # Read 2: needs more data.
"<read1><read2>" # Read 3: needs more data.
"<read1><read2><read3>" # Read 4: needs more data.
"<read1><read2><read3>\r\n<bonus bytes>" # separator found, done.
```
2. The last call to `socket.async_read_some` happens to read past
the separator `\r\n` (depicted as `<bonus bytes>` above),
resulting in bonus (maybe incomplete) messages in the buffer
```
| 1st async_read_some | 2nd async_read_some |
| | |
"+message content here \r\n:100\r\n+OK\r\n_\r\n+incomplete respo"
| | | |
| Message wanted |<-- bonus msgs --->|<--incomplete-->|
| | msg |
| | |
| |<---------- bonus bytes ----------->|
```
3. The buffer is consumed and `async_read_until` is called again.
However, since the buffer already contains the next message this
is an IO-less call
```
":100\r\n+OK\r\n_\r\n+not enough byt"
| | |
| No IO required | Need more |
| to parse these | data |
| messages. | |
```
The fact that step 3. doesn't perform any IO implies the operation can
complete synchronously, but because this is an asynchronous function
Boost.Asio by default won't call the continuation before the
function returns. The implementation must therefore enqueue it for
execution, as depicted below
```
OP5 ---> OP4 ---> OP3 ---> OP2 ---> OP1 # Reschedules the continuation
|
OP1 schedules its continuation |
+-----------------------------------+
|
|
OP6 ---> OP5 ---> OP4 ---> OP3 ---> OP2 # Reschedules the continuation
|
OP2 schedules its continuation |
+-----------------------------------+
|
|
OP7 ---> OP6 ---> OP5 ---> OP4 ---> OP3
```
When summed up, the excessive rescheduling of continuations lead to
performance degradation at scale. But since this is an event-loop
there is no way around rescheduling as doing otherwise would mean
allowing a task to monopolize the event-loop, preventing other tasks
from making progress. The best that can be done is to avoid
_overscheduling_, so let us determine how much rescheduling is too
much.
## The intrinsic latency of an event-loop
An event-loop is a design pattern originally used to handle events
external to the application, such as GUIs, networking and other forms
of IO. If we take this literally, it becomes evident that the way
`async_read_until` works is incompatible with an event-loop since
_searching for the separator_ is not an external event and as such
should not have to be enqueued for execution.
Once we constrain ourselves to events that have an external origin,
such as anything related to IO and including any form of IPC, the
scheduling overhead is reduced considerably since the latency
of the transport layer eclipses whatever time it takes to schedule the
continuation, for example, according to
[these](https://www.boost.org/doc/libs/develop/libs/cobalt/doc/html/index.html#posting_to_an_executor)
benchmarks, the time it takes to schedule a task in the
`asio::io_context ` is approximately `50ns`.
To give the reader an idea about the magnitude of this number, if
rescheduling alone were to account for 1% of the runtime of an app
that uses asynchronous IO to move around data in chunks of size 128kb,
then this app would have a throughput of approximately 24Gbs. At such
high throughput multiple other factors kick in before any scheduling
overhead even starts to manifest.
It is therefore safe to say that only asynchronous operations that
don't perform or are not bound to any IO are ever likely to
overschedule in the sense described above. Those cases can be usually
avoided, this is what worked for Boost.Redis
1. `async_read_until` was replaced with calls to
`socket.async_read_some` and an incremental parser that does not
do any IO.
2. Channel `try_` functions are used to check if send and receive
operations can be called without suspension. For example,
`try_send` before `async_send` and `try_receive` before
`async_receive` ([see also](https://github.com/chriskohlhoff/asio/commit/fe4fd7acf145335eeefdd19708483c46caeb45e5)
`try_send_via_dispatch` for a more aggressive optimization).
3. Coalescing of individual requests into a single payload to reduce
the number of necessary writes on the socket, this is only
possible because Redis supports pipelining (good protocols
help!).
4. Increased the socket read sizes to 4kb to reduce the number of
reads (which is outweighed by the costs of rotating data in the
buffer).
5. Dropped the `resp3::async_read` abstraction. When I started
developing Boost.Redis there was convincing precedent for having
a `resp3::async_read` function to read complete RESP3 messages
from a socket
Name | Description
---------------------------------------|-------------------
`asio::ip::tcp::async_read` | Reads `n` bytes from a stream.
`beast::http::async_read` | Reads a complete HTTP message.
`beast::websocket::stream::async_read` | Reads a complete Websocket message.
`redis::async_read` | Reads a complete RESP3 message.
It turns out however that this function is also vulnerable to
immediate completions since in command pipelines multiple
responses show up in the buffer after a call to
`socket.async_read_some`. When that happens each call to
`resp3::async_read` is IO-less.
Sometimes it is not possible to avoid asynchronous operations that
complete synchronously, in the following sections we will see how to
favor throughput over fairness in Boost.Asio.
### Calling the continuation inline
In Boost.Asio it is possible to customize how an algorithm executes
the continuation when an immediate completion occurs, this includes
the ability of calling it inline, thereby avoiding the costs of
excessive rescheduling. Here is how it works
```cpp
// (default) The continuation is enqueued for execution, regardless of
// whether it is immediate or not.
async_read_until(socket, buffer, "\r\n", continuation);
// Immediate completions are executed in exec2 (otherwise equal to the
// version above). The completion is called inline if exec2 is the
// same executor that is running the operation.
async_read_until(socket, buffer, "\r\n", bind_immediate_executor(exec2, completion));
```
To compare the performance of both cases I have written a small
function that calls `async_read_until` in a loop with a buffer that is
never consumed so that all completions are immediate. The version
below uses the default behaviour
```cpp
void read_safe(tcp::socket& s, std::string& buffer)
{
auto continuation = [&s, &buffer](auto ec, auto n)
{
read_safe(s, buffer); // Recursive call
};
// This won't cause stack exhaustion because the continuation is
// not called inline but posted in the event loop.
async_read_until(s, dynamic_buffer(buffer), "\r\n", continuation);
}
```
To optimize away some of the rescheduling the version below uses the
`bind_immediate_executor` customization to call the continuation
reentrantly and then breaks the stack from time to time to avoid
exhausting it
```cpp
void read_reentrant(tcp::socket& s, std::string& buffer)
{
auto cont = [&](auto, auto)
{
read_reentrant(s, buffer); // Recursive call
};
// Breaks the callstack after 16 inline calls.
if (counter % 16 == 0) {
post(s.get_executor(), [cont](){cont({}, 0);});
return;
}
// Continuation called reentrantly.
async_read_until(s, dynamic_buffer(buffer), "\r\n",
bind_immediate_executor(s.get_executor(), cont));
}
```
The diagram below shows what the reentrant chain of calls in the code
above look like from the event-loop point of view
```
OP5 ---> OP4 ---> OP3 ---> OP2 ---> OP1a # Completes immediately
|
|
... |
OP1b # Completes immediately
|
Waiting for OP5 to |
reschedule its |
continuation OP1c # Completes immediately
|
|
... |
OP1d # Break the call-stack
|
+-----------------------------------+
|
OP6 ---> OP5 ---> OP4 ---> OP3 ---> OP2
```
Unsurprisingly, the reentrant code is 3x faster than the one that
relies on the default behaviour (don't forget that this is a best case
scenario, in the general case not all completions are immediate).
Although faster, this strategy has some downsides
- The overall operation is not as fast as possible since it still
has to reschedule from time to time to break the call stack. The
less it reschedules the higher the risk of exhausting it.
- It is too easy to forget to break the stack. For example, the
programmer might decide to branch somewhere into another chain of
asynchronous calls that also use this strategy. To avoid
exhaustion all such branches would have to be safeguarded with a
manual rescheduling i.e. `post`.
- Requires additional layers of complexity such as
`bind_immediate_executor` in addition to `bind_executor`.
- Non-compliat with more strict
[guidelines](https://en.wikipedia.org/wiki/The_Power_of_10:_Rules_for_Developing_Safety-Critical_Code)
that prohibits reentrat code.
- There is no simple way of choosing the maximum allowed number of
reentrant calls for each function in a way that covers different
use cases and users. Library writers and users would be tempted
into using a small value reducing the performance advantage.
- If the socket is always ready for reading the task will
monopolize IO for up to `16` interactions which might cause
stutter in unrelated tasks as depicted below
```
Unfairness
+----+----+----+ +----+----+----+ +----+----+----+
Socket-1 | | | | | | | | | | | |
+----+----+----+----+----+----+----+----+----+----+----+----+
Socket-2 | | | | | |
+----+ +----+ +----+
```
From the aesthetic point of view the code above is also unpleasant as
it breaks the function asynchronous contract by injecting a reentrant
behaviour. It gives me the same kind of feeling I have about
[recursive
mutexes](http://www.zaval.org/resources/library/butenhof1.html).
Note: It is worth mentioning here that a similar
[strategy](https://github.com/NVIDIA/stdexec/blob/6f23dd5b1d523541ce28af32fc2603403ebd36ed/include/exec/trampoline_scheduler.hpp#L52)
is used to break the call stack of repeating algorithms in
[stdexec](https://github.com/NVIDIA/stdexec), but in this time
based on
[P2300](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2300r7.html)
and not on Boost.Asio.
### Coroutine tail-calls
In the previous section we have seen how to avoid overscheduling by
instructing the asynchronous operation to call the completion inline
on immediate completion. It turns out however that coroutine support
for _tail-calls_ provide a way to completely sidestep this problem.
This feature is described by
[Lewis Baker](https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer)
as follows
> A tail-call is one where the current stack-frame is popped before
> the call and the current functions return address becomes the
> return-address for the callee. ie. the callee will return directly
> the the [sic] caller of this function.
This means (at least in principle) that a library capable of using
tail-calls when an immediate completion occurs neither has to
reschedule the continuation nor call it inline. To test how this
feature compares to the other styles I have used Boost.Cobalt. The
code looks as follows
```cpp
// Warning: risks unfairness and starvation of other tasks.
task<void> read_until_unfair()
{
for (int i = 0; i != repeat; ++i) {
co_await async_read_until(s, dynamic_buffer(buffer), "\r\n", cobalt::use_op);
}
}
```
The result of this comparison as listed in the table below
Time/s | Style | Configuration | Library
-------|-----------|-----------------------------|-------------
1,0 | Coroutine | `await_ready` optimization | Boost.Cobalt
4.8 | Callback | Reentant | Boost.Asio
10.3 | Coroutine | `use_op` | Boost.Cobalt
14.9 | Callback | Regular | Boost.Asio
15.6 | Coroutine | `asio::deferred` | Boost.Asio
As the reader can see, `cobalt::use_op` ranks 3rd and is considerably
faster (10.3 vs 15.6) than the Asio equivalent that uses
default-rescheduling. However, by trading rescheduling with tail-calls
the code above can now monopolize the event-loop, resulting in
unfairness if the socket happens to receive data at a higher rate
than other tasks. If by chance data is received continuously
on a socket that is always ready for reading, other tasks will starve
```
Starvation
+----+----+----+----+----+----+----+----+----+----+----+----+
Socket-1 | | | | | | | | | | | | |
+----+----+----+----+----+----+----+----+----+----+----+----+
Socket-2 Starving ...
```
To avoid this problem the programmer is forced to reschedule from time
to time, in the same way we did for the reentrant calls
```cpp
task<void> read_until_fair()
{
for (int i = 0; i != repeat; ++i) {
if (repeat % 16 == 0) {
// Reschedules to address unfairness and starvation of
// other tasks.
co_await post(cobalt::use_op);
continue;
}
co_await async_read_until(s, dynamic_buffer(buffer), "\r\n", cobalt::use_op);
}
}
```
Delegating fairness-safety to applications is a dangerous game.
This is a
[problem](https://tokio.rs/blog/2020-04-preemption) the Tokio
community had to deal with before Tokio runtime started enforcing
rescheduling (after 256 successful operations)
> If data is received faster than it can be processed, it is possible
> that more data will have already been received by the time the
> processing of a data chunk completes. In this case, .await will
> never yield control back to the scheduler, other tasks will not be
> scheduled, resulting in starvation and large latency variance.
> Currently, the answer to this problem is that the user of Tokio is
> responsible for adding yield points in both the application and
> libraries. In practice, very few actually do this and end up being
> vulnerable to this sort of problem.
### Safety in P2300 (Senders and Receivers)
As of this writing, the C++ standards committee (WG21) has been
pursuing the standardization of a networking library for almost 20
years. One of the biggest obstacles that prevented it from happening
was a disagreement on what the _asynchronous model_ that underlies
networking should look like. Until 2021 that model was basically
Boost.Asio _executors_, but in this
[poll](https://www.reddit.com/r/cpp/comments/q6tgod/c_committee_polling_results_for_asynchronous/)
the committee decided to abandon that front and concentrate efforts on
the new [P2300](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2300r7.html)
proposal, also known as _senders and receivers_. The decision was
quite [abrupt](https://isocpp.org/files/papers/P2464R0.html)
> The original plan about a week earlier than the actual writing of
> this paper was to write a paper that makes a case for standardizing
> the Networking TS.
and opinions turned out to be very strong against Boost.Asio (see
[this](https://api.csswg.org/bikeshed/?force=1&url=https://raw.githubusercontent.com/brycelelbach/wg21_p2459_2022_january_library_evolution_poll_outcomes/main/2022_january_library_evolution_poll_outcomes.bs)
for how each voter backed their vote)
> The whole concept is completely useless, there's no composed code
> you can write with it.
The part of that debate that interests us most here is stated in
[P2471](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2471r1.pdf),
that compares Boost.Asio with P2300
> Yes, default rescheduling each operation and default not
> rescheduling each operation, is a poor trade off. IMO both options
> are poor. The one good option that I know of that can prevent stack
> exhaustion is first-class tail-recursion in library or language
> ASIO has chosen to require that every async operation must schedule
> the completion on a scheduler (every read, every write, etc..).
> sender/receiver has not decided to
> require that the completion be scheduled.
> This is why I consider tail-call the only good solution. Scheduling
> solutions are all inferior (give thanks to Lewis for this shift in
> my understanding :) ).
Although tail-calls solve the problem of stack-exhaustion as we have
seen above, it makes the code vulnerable to unfairness and starvation
and therefore it is not an alternative to default-rescheduling as the
quotation above is implying. To deal with the lack of
default-rescheduling, libraries and applications built on top of P2300
have to address the aforementioned problems, layer after layer. For
example,
[stdexec](https://github.com/NVIDIA/stdexec) has invented something
called
_[trampoline-scheduler](https://github.com/NVIDIA/stdexec/blob/e7cd275273525dbc693f4bf5f6dc4d4181b639e4/include/exec/trampoline_scheduler.hpp)_
to protect repeating algorithms such as `repeat_effect_until` from
exhausting the stack. This construct however is built around
reentracy, allowing
[sixteen](https://github.com/NVIDIA/stdexec/blob/83cdb92d316e8b3bca1357e2cf49fc39e9bed403/include/exec/trampoline_scheduler.hpp#L52)
levels of inline calls by default. While in Boost.Asio it is possible to use
reentracy as an optimization for a corner cases, here it is made its
_modus operandi_, the downsides of this approach have already been stated in a
previous section so I won't repeat it here.
Also the fact that a special scheduler is needed by specific
algorithms is a problem on its own since it contradicts one of the
main selling points of P2300 which is that of being _generic_. For
example, [P2464R0](https://isocpp.org/files/papers/P2464R0.html) uses
the code below as an example
```cpp
void
run_that_io_operation(
scheduler auto sched,
sender_of<network_buffer> auto wrapping_continuation)
{
// snip
}
```
and states
> I have no idea what the sched's concrete type is. I have no idea
> what the wrapping_continuation's concrete type is. They're none of
> my business, ...
Hence, by being generic, the algorithms built on top of P2300 are also
unsafe (against stack-exhaustion, unfairness and starvation). Otherwise,
if library writers require a specific scheduler to ensure safety, then
the algorithms become automatically non-generic, pick your poison!
The proposers of P2300 claim that it doesn't address safety because it
should be seen as the low-level building blocks of asynchronous
programming and that its the role of higher-level libraries, to deal
with that. This claim however does not hold since, as we have just
seen, Boost.Asio also provides those building blocks but does so in a
safe way. In fact during the whole development of Boost.Redis I never
had to think about these kinds of problems because safety is built
from the ground up.
### Avoiding coroutine suspension with `await_ready`
Now let us get back to the first place in the table above, which uses
the `await_ready` optimization from Boost.Cobalt. This API provides
users with the ability to avoid coroutine suspension altogether in
case the separator is already present in the buffer. It works by
defining a `struct` with the following interface
```cpp
struct read_until : cobalt::op<error_code, std::size_t> {
...
void ready(cobalt::handler<error_code, std::size_t> handler) override
{
// Search for the separator in buffer and call the handler if found
}
void initiate(cobalt::completion_handler<error_code, std::size_t> complete) override
{
// Regular call to async_read_until.
async_read_until(socket, buffer, delim, std::move(complete));
}
};
```
and the code that uses it
```cpp
for (int i = 0; i != repeat; ++i) {
co_await read_until(socket, dynamic_buffer(buffer));
}
```
In essence, what the code above does is to skip a call to
`async_read_unil` by first checking with the ready function whether
the forthcoming operation is going to complete immediately. The
nice thing about it is that the programmer can use this optimization
only when a performance bottleneck is detected, without planing for it
in advance. The drawback however is that it requires reimplementing
the search for the separator in the body of the `ready` function,
defeating the purpose of using `async_read_until` in first place as
(again) it would have been simpler to reformulate the operation in
terms of `socket.async_read_some` directly.
## Acknowledgements
Thanks to Klemens Morgenstern for answering questions about
Boost.Cobalt.

1955
doc/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

6
doc/package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"dependencies": {
"@cppalliance/antora-cpp-reference-extension": "^0.0.6",
"antora": "^3.1.10"
}
}

45
doc/redis-playbook.yml Normal file
View File

@@ -0,0 +1,45 @@
#
# Copyright (c) 2025 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)
#
site:
url: https://github.com/boostorg/redis/blob/master
title: Boost.Redis
robots: allow
start_page: redis::index.adoc
antora:
extensions:
- require: '@cppalliance/antora-cpp-reference-extension'
dependencies:
- name: 'boost'
repo: 'https://github.com/boostorg/boost.git'
tag: 'develop'
variable: 'BOOST_SRC_DIR'
system-env: 'BOOST_SRC_DIR'
asciidoc:
attributes:
# Scrolling problems appear without this
page-pagination: ''
content:
sources:
- url: ..
start_path: doc
ui:
bundle:
url: https://github.com/boostorg/website-v2-docs/releases/download/ui-master/ui-bundle.zip
snapshot: true
output_dir: _
output:
dir: html
runtime:
log:
failure_level: error

View File

@@ -10,13 +10,14 @@ macro(make_example EXAMPLE_NAME STANDARD)
if (${STANDARD} STREQUAL "20")
target_link_libraries(${EXAMPLE_NAME} PRIVATE examples_main)
endif()
if (${EXAMPLE_NAME} STREQUAL "cpp20_json")
target_link_libraries(${EXAMPLE_NAME} PRIVATE Boost::json Boost::container_hash)
endif()
endmacro()
macro(make_testable_example EXAMPLE_NAME STANDARD)
make_example(${EXAMPLE_NAME} ${STANDARD})
if (BOOST_REDIS_INTEGRATION_TESTS)
add_test(${EXAMPLE_NAME} ${EXAMPLE_NAME})
endif()
add_test(${EXAMPLE_NAME} ${EXAMPLE_NAME} $ENV{BOOST_REDIS_TEST_SERVER} 6379)
endmacro()
make_testable_example(cpp17_intro 17)
@@ -25,12 +26,14 @@ make_testable_example(cpp17_intro_sync 17)
make_testable_example(cpp20_intro 20)
make_testable_example(cpp20_containers 20)
make_testable_example(cpp20_json 20)
make_testable_example(cpp20_intro_tls 20)
make_testable_example(cpp20_unix_sockets 20)
make_testable_example(cpp20_timeouts 20)
make_testable_example(cpp20_sentinel 20)
make_example(cpp20_subscriber 20)
make_example(cpp20_streams 20)
make_example(cpp20_echo_server 20)
make_example(cpp20_resolve_with_sentinel 20)
make_example(cpp20_intro_tls 20)
# We test the protobuf example only on gcc.
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
@@ -46,4 +49,13 @@ endif()
if (NOT MSVC)
make_example(cpp20_chat_room 20)
endif()
endif()
# We build and test the spdlog integration example only if the library is found
find_package(spdlog)
if (spdlog_FOUND)
make_testable_example(cpp17_spdlog 17)
target_link_libraries(cpp17_spdlog PRIVATE spdlog::spdlog)
else()
message(STATUS "Skipping the spdlog example because the spdlog package couldn't be found")
endif()

View File

@@ -5,7 +5,9 @@
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
namespace asio = boost::asio;
@@ -14,7 +16,7 @@ using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
auto main(int argc, char * argv[]) -> int
auto main(int argc, char* argv[]) -> int
{
try {
config cfg;
@@ -32,7 +34,7 @@ auto main(int argc, char * argv[]) -> int
asio::io_context ioc;
connection conn{ioc};
conn.async_run(cfg, {}, asio::detached);
conn.async_run(cfg, asio::detached);
conn.async_exec(req, resp, [&](auto ec, auto) {
if (!ec)
@@ -47,4 +49,3 @@ auto main(int argc, char * argv[]) -> int
return 1;
}
}

View File

@@ -6,15 +6,15 @@
#include "sync_connection.hpp"
#include <string>
#include <iostream>
#include <string>
using boost::redis::sync_connection;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
auto main(int argc, char * argv[]) -> int
auto main(int argc, char* argv[]) -> int
{
try {
config cfg;

99
example/cpp17_spdlog.cpp Normal file
View File

@@ -0,0 +1,99 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/redis/connection.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/detached.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <iostream>
#include <spdlog/spdlog.h>
#include <string_view>
namespace asio = boost::asio;
namespace redis = boost::redis;
// Maps a Boost.Redis log level to a spdlog log level
static spdlog::level::level_enum to_spdlog_level(redis::logger::level lvl)
{
switch (lvl) {
// spdlog doesn't include the emerg and alert syslog levels,
// so we convert them to the highest supported level.
// Similarly, notice is similar to info
case redis::logger::level::emerg:
case redis::logger::level::alert:
case redis::logger::level::crit: return spdlog::level::critical;
case redis::logger::level::err: return spdlog::level::err;
case redis::logger::level::warning: return spdlog::level::warn;
case redis::logger::level::notice:
case redis::logger::level::info: return spdlog::level::info;
case redis::logger::level::debug:
default: return spdlog::level::debug;
}
}
// This function glues Boost.Redis logging and spdlog.
// It should have the signature shown here. It will be invoked
// by Boost.Redis whenever a message is to be logged.
static void do_log(redis::logger::level level, std::string_view msg)
{
spdlog::log(to_spdlog_level(level), "(Boost.Redis) {}", msg);
}
auto main(int argc, char** argv) -> int
{
try {
// Create an execution context, required to create any I/O objects
asio::io_context ioc;
// Create a connection to connect to Redis, and pass it a custom logger.
// Boost.Redis will call do_log whenever it needs to log a message.
// Note that the function will only be called for messages with level >= info
// (i.e. filtering is done by Boost.Redis).
redis::connection conn{
ioc,
redis::logger{redis::logger::level::info, do_log}
};
// Configuration to connect to the server. Adjust as required
redis::config cfg;
if (argc == 3) {
cfg.addr.host = argv[1];
cfg.addr.port = argv[2];
}
// Run the connection with the specified configuration.
// This will establish the connection and keep it healthy
conn.async_run(cfg, asio::detached);
// Execute a request
redis::request req;
req.push("PING", "Hello world");
redis::response<std::string> resp;
conn.async_exec(req, resp, [&](boost::system::error_code ec, std::size_t /* bytes_read*/) {
if (ec) {
spdlog::error("Request failed: {}", ec.what());
exit(1);
} else {
spdlog::info("PING: {}", std::get<0>(resp).value());
}
conn.cancel();
});
// Actually run our example. Nothing will happen until we call run()
ioc.run();
} catch (std::exception const& e) {
spdlog::error("Error: {}", e.what());
return 1;
}
}

View File

@@ -1,38 +1,38 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/posix/stream_descriptor.hpp>
#include <unistd.h>
#include <boost/asio/read_until.hpp>
#include <boost/asio/signal_set.hpp>
#include <exception>
#include <iostream>
#include <unistd.h>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
namespace asio = boost::asio;
using stream_descriptor = asio::deferred_t::as_default_on_t<asio::posix::stream_descriptor>;
using signal_set = asio::deferred_t::as_default_on_t<asio::signal_set>;
using asio::posix::stream_descriptor;
using asio::signal_set;
using boost::asio::async_read_until;
using boost::asio::awaitable;
using boost::asio::co_spawn;
using boost::asio::consign;
using boost::asio::deferred;
using boost::asio::detached;
using boost::asio::dynamic_buffer;
using boost::asio::redirect_error;
using boost::asio::use_awaitable;
using boost::redis::config;
using boost::redis::connection;
using boost::redis::generic_response;
using boost::redis::ignore;
using boost::redis::generic_flat_response;
using boost::redis::request;
using boost::system::error_code;
using namespace std::chrono_literals;
@@ -40,47 +40,62 @@ using namespace std::chrono_literals;
// Chat over Redis pubsub. To test, run this program from multiple
// terminals and type messages to stdin.
auto
receiver(std::shared_ptr<connection> conn) -> awaitable<void>
{
request req;
req.push("SUBSCRIBE", "channel");
namespace {
generic_response resp;
auto rethrow_on_error = [](std::exception_ptr exc) {
if (exc)
std::rethrow_exception(exc);
};
auto receiver(std::shared_ptr<connection> conn) -> awaitable<void>
{
// Set the receive response, so pushes are stored in resp
generic_flat_response resp;
conn->set_receive_response(resp);
// Subscribe to the channel 'channel'. Using request::subscribe()
// (instead of request::push()) makes the connection re-subscribe
// to 'channel' whenever it re-connects to the server.
request req;
req.subscribe({"channel"});
co_await conn->async_exec(req);
while (conn->will_reconnect()) {
// Wait for pushes
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
// Subscribe to channels.
co_await conn->async_exec(req, ignore, deferred);
// Loop reading Redis push messages.
for (error_code ec;;) {
co_await conn->async_receive(redirect_error(use_awaitable, ec));
if (ec)
break; // Connection lost, break so we can reconnect to channels.
std::cout
<< resp.value().at(1).value
<< " " << resp.value().at(2).value
<< " " << resp.value().at(3).value
<< std::endl;
resp.value().clear();
// Check for errors and cancellations
if (ec) {
std::cerr << "Error during receive: " << 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())
std::cout << elem.value << "\n";
std::cout << std::endl;
resp.value().clear();
}
}
// Publishes stdin messages to a Redis channel.
auto publisher(std::shared_ptr<stream_descriptor> in, std::shared_ptr<connection> conn) -> awaitable<void>
auto publisher(std::shared_ptr<stream_descriptor> in, std::shared_ptr<connection> conn)
-> awaitable<void>
{
for (std::string msg;;) {
auto n = co_await async_read_until(*in, dynamic_buffer(msg, 1024), "\n");
request req;
req.push("PUBLISH", "channel", msg);
co_await conn->async_exec(req, ignore, deferred);
co_await conn->async_exec(req);
msg.erase(0, n);
}
}
} // namespace
// Called from the main function (see main.cpp)
auto co_main(config cfg) -> awaitable<void>
{
@@ -88,9 +103,9 @@ auto co_main(config cfg) -> awaitable<void>
auto conn = std::make_shared<connection>(ex);
auto stream = std::make_shared<stream_descriptor>(ex, ::dup(STDIN_FILENO));
co_spawn(ex, receiver(conn), detached);
co_spawn(ex, publisher(stream, conn), detached);
conn->async_run(cfg, {}, consign(detached, conn));
co_spawn(ex, receiver(conn), rethrow_on_error);
co_spawn(ex, publisher(stream, conn), rethrow_on_error);
conn->async_run(cfg, consign(detached, conn));
signal_set sig_set{ex, SIGINT, SIGTERM};
co_await sig_set.async_wait();
@@ -98,11 +113,11 @@ auto co_main(config cfg) -> awaitable<void>
stream->cancel();
}
#else // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
#else // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
auto co_main(config const&) -> awaitable<void>
{
std::cout << "Requires support for posix streams." << std::endl;
co_return;
}
#endif // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -1,16 +1,18 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
#include <map>
#include <vector>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
@@ -22,36 +24,51 @@ using boost::redis::ignore;
using boost::redis::config;
using boost::redis::connection;
using boost::asio::awaitable;
using boost::asio::deferred;
using boost::asio::detached;
using boost::asio::consign;
template <class T>
std::ostream& operator<<(std::ostream& os, std::optional<T> const& opt)
{
if (opt.has_value())
std::cout << opt.value();
else
std::cout << "null";
return os;
}
void print(std::map<std::string, std::string> const& cont)
{
for (auto const& e: cont)
for (auto const& e : cont)
std::cout << e.first << ": " << e.second << "\n";
}
void print(std::vector<int> const& cont)
template <class T>
void print(std::vector<T> const& cont)
{
for (auto const& e: cont) std::cout << e << " ";
for (auto const& e : cont)
std::cout << e << " ";
std::cout << "\n";
}
// Stores the content of some STL containers in Redis.
auto store(std::shared_ptr<connection> conn) -> awaitable<void>
{
std::vector<int> vec
{1, 2, 3, 4, 5, 6};
std::vector<int> vec{1, 2, 3, 4, 5, 6};
std::map<std::string, std::string> map
{{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}};
std::map<std::string, std::string> map{
{"key1", "value1"},
{"key2", "value2"},
{"key3", "value3"}
};
request req;
req.push_range("RPUSH", "rpush-key", vec);
req.push_range("HSET", "hset-key", map);
req.push("SET", "key", "value");
co_await conn->async_exec(req, ignore, deferred);
co_await conn->async_exec(req, ignore);
}
auto hgetall(std::shared_ptr<connection> conn) -> awaitable<void>
@@ -64,7 +81,22 @@ auto hgetall(std::shared_ptr<connection> conn) -> awaitable<void>
response<std::map<std::string, std::string>> resp;
// Executes the request and reads the response.
co_await conn->async_exec(req, resp, deferred);
co_await conn->async_exec(req, resp);
print(std::get<0>(resp).value());
}
auto mget(std::shared_ptr<connection> conn) -> awaitable<void>
{
// A request contains multiple commands.
request req;
req.push("MGET", "key", "non-existing-key");
// Responses as tuple elements.
response<std::vector<std::optional<std::string>>> resp;
// Executes the request and reads the response.
co_await conn->async_exec(req, resp);
print(std::get<0>(resp).value());
}
@@ -74,33 +106,41 @@ auto transaction(std::shared_ptr<connection> conn) -> awaitable<void>
{
request req;
req.push("MULTI");
req.push("LRANGE", "rpush-key", 0, -1); // Retrieves
req.push("HGETALL", "hset-key"); // Retrieves
req.push("LRANGE", "rpush-key", 0, -1); // Retrieves
req.push("HGETALL", "hset-key"); // Retrieves
req.push("MGET", "key", "non-existing-key");
req.push("EXEC");
response<
ignore_t, // multi
ignore_t, // lrange
ignore_t, // hgetall
response<std::optional<std::vector<int>>, std::optional<std::map<std::string, std::string>>> // exec
> resp;
ignore_t, // multi
ignore_t, // lrange
ignore_t, // hgetall
ignore_t, // mget
response<
std::optional<std::vector<int>>,
std::optional<std::map<std::string, std::string>>,
std::optional<std::vector<std::optional<std::string>>>> // exec
>
resp;
co_await conn->async_exec(req, resp, deferred);
co_await conn->async_exec(req, resp);
print(std::get<0>(std::get<3>(resp).value()).value().value());
print(std::get<1>(std::get<3>(resp).value()).value().value());
print(std::get<0>(std::get<4>(resp).value()).value().value());
print(std::get<1>(std::get<4>(resp).value()).value().value());
print(std::get<2>(std::get<4>(resp).value()).value().value());
}
// Called from the main function (see main.cpp)
awaitable<void> co_main(config cfg)
{
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
conn->async_run(cfg, {}, consign(detached, conn));
conn->async_run(cfg, consign(detached, conn));
co_await store(conn);
co_await transaction(conn);
co_await hgetall(conn);
co_await mget(conn);
conn->cancel();
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -5,19 +5,20 @@
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/signal_set.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace asio = boost::asio;
using tcp_socket = asio::deferred_t::as_default_on_t<asio::ip::tcp::socket>;
using tcp_acceptor = asio::deferred_t::as_default_on_t<asio::ip::tcp::acceptor>;
using signal_set = asio::deferred_t::as_default_on_t<asio::signal_set>;
using boost::asio::signal_set;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
@@ -25,7 +26,8 @@ using boost::system::error_code;
using boost::redis::connection;
using namespace std::chrono_literals;
auto echo_server_session(tcp_socket socket, std::shared_ptr<connection> conn) -> asio::awaitable<void>
auto echo_server_session(asio::ip::tcp::socket socket, std::shared_ptr<connection> conn)
-> asio::awaitable<void>
{
request req;
response<std::string> resp;
@@ -33,7 +35,7 @@ auto echo_server_session(tcp_socket socket, std::shared_ptr<connection> conn) ->
for (std::string buffer;;) {
auto n = co_await asio::async_read_until(socket, asio::dynamic_buffer(buffer, 1024), "\n");
req.push("PING", buffer);
co_await conn->async_exec(req, resp, asio::deferred);
co_await conn->async_exec(req, resp);
co_await asio::async_write(socket, asio::buffer(std::get<0>(resp).value()));
std::get<0>(resp).value().clear();
req.clear();
@@ -46,7 +48,7 @@ auto listener(std::shared_ptr<connection> conn) -> asio::awaitable<void>
{
try {
auto ex = co_await asio::this_coro::executor;
tcp_acceptor acc(ex, {asio::ip::tcp::v4(), 55555});
asio::ip::tcp::acceptor acc(ex, {asio::ip::tcp::v4(), 55555});
for (;;)
asio::co_spawn(ex, echo_server_session(co_await acc.async_accept(), conn), asio::detached);
} catch (std::exception const& e) {
@@ -60,11 +62,11 @@ auto co_main(config cfg) -> asio::awaitable<void>
auto ex = co_await asio::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
asio::co_spawn(ex, listener(conn), asio::detached);
conn->async_run(cfg, {}, asio::consign(asio::detached, conn));
conn->async_run(cfg, asio::consign(asio::detached, conn));
signal_set sig_set(ex, SIGINT, SIGTERM);
co_await sig_set.async_wait();
conn->cancel();
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -5,10 +5,11 @@
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
@@ -23,7 +24,7 @@ using boost::redis::connection;
auto co_main(config cfg) -> asio::awaitable<void>
{
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
conn->async_run(cfg, {}, asio::consign(asio::detached, conn));
conn->async_run(cfg, asio::consign(asio::detached, conn));
// A request containing only a ping command.
request req;
@@ -33,10 +34,10 @@ auto co_main(config cfg) -> asio::awaitable<void>
response<std::string> resp;
// Executes the request.
co_await conn->async_exec(req, resp, asio::deferred);
co_await conn->async_exec(req, resp);
conn->cancel();
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -5,10 +5,12 @@
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
@@ -34,21 +36,22 @@ auto co_main(config cfg) -> asio::awaitable<void>
cfg.addr.host = "db.occase.de";
cfg.addr.port = "6380";
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
conn->async_run(cfg, {}, asio::consign(asio::detached, conn));
asio::ssl::context ctx{asio::ssl::context::tlsv12_client};
ctx.set_verify_mode(asio::ssl::verify_peer);
ctx.set_verify_callback(verify_certificate);
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor, std::move(ctx));
conn->async_run(cfg, asio::consign(asio::detached, conn));
request req;
req.push("PING");
response<std::string> resp;
conn->next_layer().set_verify_mode(asio::ssl::verify_peer);
conn->next_layer().set_verify_callback(verify_certificate);
co_await conn->async_exec(req, resp, asio::deferred);
co_await conn->async_exec(req, resp);
conn->cancel();
std::cout << "Response: " << std::get<0>(resp).value() << std::endl;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -5,33 +5,35 @@
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/detached.hpp>
#include <boost/describe.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <string>
#include <boost/describe.hpp>
#include <iostream>
#include <string>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#define BOOST_JSON_NO_LIB
#define BOOST_CONTAINER_NO_LIB
#include <boost/json/serialize.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/value_from.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/json/src.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value_from.hpp>
#include <boost/json/value_to.hpp>
namespace asio = boost::asio;
namespace resp3 = boost::redis::resp3;
using namespace boost::describe;
using boost::redis::request;
using boost::redis::response;
using boost::redis::ignore_t;
using boost::redis::config;
using boost::redis::connection;
using boost::redis::resp3::node_view;
// Struct that will be stored in Redis using json serialization.
// Struct that will be stored in Redis using json serialization.
struct user {
std::string name;
std::string age;
@@ -43,35 +45,38 @@ 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)
{ boost::redis::resp3::boost_redis_to_bulk(to, 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, std::string_view sv, boost::system::error_code&)
{ u = boost::json::value_to<user>(boost::json::parse(sv)); }
void boost_redis_from_bulk(user& u, node_view const& node, boost::system::error_code&)
{
u = boost::json::value_to<user>(boost::json::parse(node.value));
}
auto co_main(config cfg) -> asio::awaitable<void>
{
auto ex = co_await asio::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
conn->async_run(cfg, {}, asio::consign(asio::detached, conn));
conn->async_run(cfg, asio::consign(asio::detached, conn));
// user object that will be stored in Redis in json format.
user const u{"Joao", "58", "Brazil"};
// Stores and retrieves in the same request.
request req;
req.push("SET", "json-key", u); // Stores in Redis.
req.push("GET", "json-key"); // Retrieves from Redis.
req.push("SET", "json-key", u); // Stores in Redis.
req.push("GET", "json-key"); // Retrieves from Redis.
response<ignore_t, user> resp;
co_await conn->async_exec(req, resp, asio::deferred);
co_await conn->async_exec(req, resp);
conn->cancel();
// Prints the first ping
std::cout
<< "Name: " << std::get<1>(resp).value().name << "\n"
<< "Age: " << std::get<1>(resp).value().age << "\n"
<< "Country: " << std::get<1>(resp).value().country << "\n";
std::cout << "Name: " << std::get<1>(resp).value().name << "\n"
<< "Age: " << std::get<1>(resp).value().age << "\n"
<< "Country: " << std::get<1>(resp).value().country << "\n";
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -6,11 +6,12 @@
#include <boost/redis/connection.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/system/errc.hpp>
#include <iostream>
// See the definition in person.proto. This header is automatically
@@ -20,19 +21,20 @@
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace asio = boost::asio;
namespace resp3 = boost::redis::resp3;
using boost::redis::request;
using boost::redis::response;
using boost::redis::operation;
using boost::redis::ignore_t;
using boost::redis::config;
using boost::redis::connection;
using boost::redis::resp3::node_view;
// The protobuf type described in example/person.proto
using tutorial::person;
// Boost.Redis customization points (example/protobuf.hpp)
namespace tutorial
{
namespace tutorial {
// Below I am using a Boost.Redis to indicate a protobuf error, this
// is ok for an example, users however might want to define their own
@@ -43,17 +45,17 @@ void boost_redis_to_bulk(std::string& to, person const& u)
if (!u.SerializeToString(&tmp))
throw boost::system::system_error(boost::redis::error::invalid_data_type);
boost::redis::resp3::boost_redis_to_bulk(to, tmp);
resp3::boost_redis_to_bulk(to, tmp);
}
void boost_redis_from_bulk(person& u, std::string_view sv, boost::system::error_code& ec)
void boost_redis_from_bulk(person& u, node_view const& node, boost::system::error_code& ec)
{
std::string const tmp {sv};
std::string const tmp{node.value};
if (!u.ParseFromString(tmp))
ec = boost::redis::error::invalid_data_type;
}
} // tutorial
} // namespace tutorial
using tutorial::boost_redis_to_bulk;
using tutorial::boost_redis_from_bulk;
@@ -62,7 +64,7 @@ asio::awaitable<void> co_main(config cfg)
{
auto ex = co_await asio::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
conn->async_run(cfg, {}, asio::consign(asio::detached, conn));
conn->async_run(cfg, asio::consign(asio::detached, conn));
person p;
p.set_name("Louis");
@@ -76,13 +78,12 @@ asio::awaitable<void> co_main(config cfg)
response<ignore_t, person> resp;
// Sends the request and receives the response.
co_await conn->async_exec(req, resp, asio::deferred);
co_await conn->async_exec(req, resp);
conn->cancel();
std::cout
<< "Name: " << std::get<1>(resp).value().name() << "\n"
<< "Age: " << std::get<1>(resp).value().id() << "\n"
<< "Email: " << std::get<1>(resp).value().email() << "\n";
std::cout << "Name: " << std::get<1>(resp).value().name() << "\n"
<< "Age: " << std::get<1>(resp).value().id() << "\n"
<< "Email: " << std::get<1>(resp).value().email() << "\n";
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -1,75 +0,0 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace asio = boost::asio;
using endpoints = asio::ip::tcp::resolver::results_type;
using boost::redis::request;
using boost::redis::response;
using boost::redis::ignore_t;
using boost::redis::config;
using boost::redis::address;
using boost::redis::connection;
auto redir(boost::system::error_code& ec)
{ return asio::redirect_error(asio::use_awaitable, ec); }
// For more info see
// - https://redis.io/docs/manual/sentinel.
// - https://redis.io/docs/reference/sentinel-clients.
auto resolve_master_address(std::vector<address> const& addresses) -> asio::awaitable<address>
{
request req;
req.push("SENTINEL", "get-master-addr-by-name", "mymaster");
req.push("QUIT");
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
response<std::optional<std::array<std::string, 2>>, ignore_t> resp;
for (auto addr : addresses) {
boost::system::error_code ec;
config cfg;
cfg.addr = addr;
// TODO: async_run and async_exec should be lauched in
// parallel here so we can wait for async_run completion
// before eventually calling it again.
conn->async_run(cfg, {}, asio::consign(asio::detached, conn));
co_await conn->async_exec(req, resp, redir(ec));
conn->cancel();
conn->reset_stream();
if (!ec && std::get<0>(resp))
co_return address{std::get<0>(resp).value().value().at(0), std::get<0>(resp).value().value().at(1)};
}
co_return address{};
}
auto co_main(config cfg) -> asio::awaitable<void>
{
// A list of sentinel addresses from which only one is responsive.
// This simulates sentinels that are down.
std::vector<address> const addresses
{ address{"foo", "26379"}
, address{"bar", "26379"}
, cfg.addr
};
auto const ep = co_await resolve_master_address(addresses);
std::clog
<< "Host: " << ep.host << "\n"
<< "Port: " << ep.port << "\n"
<< std::flush;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -0,0 +1,60 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/redis/connection.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace asio = boost::asio;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
using boost::redis::connection;
// Called from the main function (see main.cpp)
auto co_main(config cfg) -> asio::awaitable<void>
{
// Boost.Redis has built-in support for Sentinel deployments.
// To enable it, set the fields in config shown here.
// sentinel.addresses should contain a list of (hostname, port) pairs
// where Sentinels are listening. IPs can also be used.
cfg.sentinel.addresses = {
{"localhost", "26379"},
{"localhost", "26380"},
{"localhost", "26381"},
};
// Set master_name to the identifier that you configured
// in the "sentinel monitor" statement of your sentinel.conf file
cfg.sentinel.master_name = "mymaster";
// async_run will contact the Sentinels, obtain the master address,
// connect to it and keep the connection healthy. If a failover happens,
// the address will be resolved again and the new elected master will be contacted.
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
conn->async_run(cfg, asio::consign(asio::detached, conn));
// You can now use the connection normally, as you would use a connection to a single master.
request req;
req.push("PING", "Hello world");
response<std::string> resp;
// Execute the request.
co_await conn->async_exec(req, resp);
conn->cancel();
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -5,12 +5,13 @@
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/signal_set.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
@@ -26,57 +27,55 @@ using boost::redis::generic_response;
using boost::redis::operation;
using boost::redis::request;
using boost::redis::connection;
using signal_set = net::deferred_t::as_default_on_t<net::signal_set>;
using net::signal_set;
auto stream_reader(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
std::string redisStreamKey_;
request req;
generic_response resp;
std::string redisStreamKey_;
request req;
generic_response resp;
std::string stream_id{"$"};
std::string const field = "myfield";
std::string stream_id{"$"};
std::string const field = "myfield";
for (;;) {
req.push("XREAD", "BLOCK", "0", "STREAMS", "test-topic", stream_id);
co_await conn->async_exec(req, resp, net::deferred);
for (;;) {
req.push("XREAD", "BLOCK", "0", "STREAMS", "test-topic", stream_id);
co_await conn->async_exec(req, resp);
//std::cout << "Response: ";
//for (auto i = 0UL; i < resp->size(); ++i) {
// std::cout << resp->at(i).value << ", ";
//}
//std::cout << std::endl;
//std::cout << "Response: ";
//for (auto i = 0UL; i < resp->size(); ++i) {
// std::cout << resp->at(i).value << ", ";
//}
//std::cout << std::endl;
// The following approach was taken in order to be able to
// deal with the responses, as generated by redis in the case
// that there are multiple stream 'records' within a single
// generic_response. The nesting and number of values in
// resp.value() are different, depending on the contents
// of the stream in redis. Uncomment the above commented-out
// code for examples while running the XADD command.
// The following approach was taken in order to be able to
// deal with the responses, as generated by redis in the case
// that there are multiple stream 'records' within a single
// generic_response. The nesting and number of values in
// resp.value() are different, depending on the contents
// of the stream in redis. Uncomment the above commented-out
// code for examples while running the XADD command.
std::size_t item_index = 0;
while (item_index < std::size(resp.value())) {
auto const& val = resp.value().at(item_index).value;
std::size_t item_index = 0;
while (item_index < std::size(resp.value())) {
auto const& val = resp.value().at(item_index).value;
if (field.compare(val) == 0) {
// We've hit a myfield field.
// The streamId is located at item_index - 2
// The payload is located at item_index + 1
stream_id = resp.value().at(item_index - 2).value;
std::cout
<< "StreamId: " << stream_id << ", "
<< "MyField: " << resp.value().at(item_index + 1).value
<< std::endl;
++item_index; // We can increase so we don't read this again
}
if (field.compare(val) == 0) {
// We've hit a myfield field.
// The streamId is located at item_index - 2
// The payload is located at item_index + 1
stream_id = resp.value().at(item_index - 2).value;
std::cout << "StreamId: " << stream_id << ", "
<< "MyField: " << resp.value().at(item_index + 1).value << std::endl;
++item_index; // We can increase so we don't read this again
}
++item_index;
}
++item_index;
}
req.clear();
resp.value().clear();
}
req.clear();
resp.value().clear();
}
}
// Run this in another terminal:
@@ -89,10 +88,10 @@ auto co_main(config cfg) -> net::awaitable<void>
// Disable health checks.
cfg.health_check_interval = std::chrono::seconds::zero();
conn->async_run(cfg, {}, net::consign(net::detached, conn));
conn->async_run(cfg, net::consign(net::detached, conn));
signal_set sig_set(ex, SIGINT, SIGTERM);
co_await sig_set.async_wait();
conn->cancel();
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -1,19 +1,18 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/signal_set.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
@@ -21,22 +20,18 @@
namespace asio = boost::asio;
using namespace std::chrono_literals;
using boost::redis::request;
using boost::redis::generic_response;
using boost::redis::consume_one;
using boost::redis::logger;
using boost::redis::generic_flat_response;
using boost::redis::config;
using boost::redis::ignore;
using boost::redis::error;
using boost::system::error_code;
using boost::redis::connection;
using signal_set = asio::deferred_t::as_default_on_t<asio::signal_set>;
using asio::signal_set;
/* This example will subscribe and read pushes indefinitely.
*
* 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>
*
@@ -48,41 +43,41 @@ using signal_set = asio::deferred_t::as_default_on_t<asio::signal_set>;
*/
// Receives server pushes.
auto
receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
{
request req;
req.push("SUBSCRIBE", "channel");
generic_response resp;
generic_flat_response resp;
conn->set_receive_response(resp);
// Loop while reconnection is enabled
// 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.
// You need to use specialized request::subscribe() function (instead of request::push)
// to enable this behavior.
// Loop to read Redis push messages.
while (conn->will_reconnect()) {
// Wait for pushes
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
// Reconnect to the channels.
co_await conn->async_exec(req, ignore, asio::deferred);
// Loop reading Redis pushs messages.
for (error_code ec;;) {
// First tries to read any buffered pushes.
conn->receive(ec);
if (ec == error::sync_receive_push_failed) {
ec = {};
co_await conn->async_receive(asio::redirect_error(asio::use_awaitable, ec));
}
if (ec)
break; // Connection lost, break so we can reconnect to channels.
std::cout
<< resp.value().at(1).value
<< " " << resp.value().at(2).value
<< " " << resp.value().at(3).value
<< std::endl;
consume_one(resp);
// Check for errors and cancellations
if (ec) {
std::cerr << "Error during receive: " << 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())
std::cout << elem.value << "\n";
std::cout << std::endl;
resp.value().clear();
}
}
@@ -91,7 +86,7 @@ auto co_main(config cfg) -> asio::awaitable<void>
auto ex = co_await asio::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
asio::co_spawn(ex, receiver(conn), asio::detached);
conn->async_run(cfg, {}, asio::consign(asio::detached, conn));
conn->async_run(cfg, asio::consign(asio::detached, conn));
signal_set sig_set(ex, SIGINT, SIGTERM);
co_await sig_set.async_wait();
@@ -99,4 +94,4 @@ auto co_main(config cfg) -> asio::awaitable<void>
conn->cancel();
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -0,0 +1,49 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace asio = boost::asio;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
using boost::redis::connection;
using namespace std::chrono_literals;
// Called from the main function (see main.cpp)
auto co_main(config cfg) -> asio::awaitable<void>
{
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
conn->async_run(cfg, asio::consign(asio::detached, conn));
// A request containing only a ping command.
request req;
req.push("PING", "Hello world");
// Response where the PONG response will be stored.
response<std::string> resp;
// Executes the request with a timeout. If the server is down,
// async_exec will wait until it's back again, so it,
// may suspend for a long time.
// For this reason, it's good practice to set a timeout to requests with cancel_after.
// If the request hasn't completed after 10 seconds, an exception will be thrown.
co_await conn->async_exec(req, resp, asio::cancel_after(10s));
conn->cancel();
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -0,0 +1,60 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/asio/awaitable.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <boost/redis/connection.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/this_coro.hpp>
#include <iostream>
namespace asio = boost::asio;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
using boost::redis::logger;
using boost::redis::connection;
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
auto co_main(config cfg) -> asio::awaitable<void>
{
// If unix_socket is set to a non-empty string, UNIX domain sockets will be used
// instead of TCP. Set this value to the path where your server is listening.
// UNIX domain socket connections work in the same way as TCP connections.
cfg.unix_socket = "/tmp/redis-socks/redis.sock";
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
conn->async_run(cfg, asio::consign(asio::detached, conn));
request req;
req.push("PING");
response<std::string> resp;
co_await conn->async_exec(req, resp);
conn->cancel();
std::cout << "Response: " << std::get<0>(resp).value() << std::endl;
}
#else
auto co_main(config) -> asio::awaitable<void>
{
std::cout << "Sorry, your system does not support UNIX domain sockets\n";
co_return;
}
#endif
#endif

View File

@@ -4,11 +4,13 @@
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/connection.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <iostream>
namespace asio = boost::asio;
@@ -19,7 +21,7 @@ using boost::redis::logger;
extern asio::awaitable<void> co_main(config);
auto main(int argc, char * argv[]) -> int
auto main(int argc, char* argv[]) -> int
{
try {
config cfg;
@@ -42,7 +44,7 @@ auto main(int argc, char * argv[]) -> int
}
}
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
auto main() -> int
{
@@ -50,4 +52,4 @@ auto main() -> int
return 0;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -1,5 +1,5 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -7,16 +7,17 @@
#include <boost/redis/connection.hpp>
#include <boost/redis/request.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/use_future.hpp>
#include <thread>
#include <chrono>
#include <thread>
using namespace std::chrono_literals;
namespace boost::redis
{
namespace boost::redis {
class sync_connection {
public:
@@ -25,33 +26,32 @@ public:
, conn_{std::make_shared<connection>(ioc_)}
{ }
~sync_connection()
{
thread_.join();
}
~sync_connection() { thread_.join(); }
void run(config cfg)
{
// Starts a thread that will can io_context::run on which the
// connection will run.
thread_ = std::thread{[this, cfg]() {
conn_->async_run(cfg, {}, asio::detached);
conn_->async_run(cfg, asio::detached);
ioc_.run();
}};
}
void stop()
{
asio::dispatch(ioc_, [this]() { conn_->cancel(); });
asio::dispatch(ioc_, [this]() {
conn_->cancel();
});
}
template <class Response>
auto exec(request const& req, Response& resp)
{
asio::dispatch(
conn_->get_executor(),
asio::deferred([this, &req, &resp]() { return conn_->async_exec(req, resp, asio::deferred); }))
(asio::use_future).get();
asio::dispatch(conn_->get_executor(), asio::deferred([this, &req, &resp]() {
return conn_->async_exec(req, resp, asio::deferred);
}))(asio::use_future)
.get();
}
private:
@@ -60,4 +60,4 @@ private:
std::thread thread_;
};
}
} // namespace boost::redis

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -8,12 +8,12 @@
#define BOOST_REDIS_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/connection.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
/** @defgroup high-level-api Reference
*
@@ -25,4 +25,4 @@
* This page contains the documentation of the Aedis low-level API.
*/
#endif // BOOST_REDIS_HPP
#endif // BOOST_REDIS_HPP

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -7,74 +7,40 @@
#ifndef BOOST_REDIS_ADAPTER_ADAPT_HPP
#define BOOST_REDIS_ADAPTER_ADAPT_HPP
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/response.hpp>
#include <boost/redis/adapter/detail/result_traits.hpp>
#include <boost/redis/adapter/detail/response_traits.hpp>
#include <boost/mp11.hpp>
#include <boost/system.hpp>
#include <boost/redis/adapter/detail/result_traits.hpp>
#include <boost/redis/ignore.hpp>
#include <tuple>
#include <limits>
#include <string_view>
#include <variant>
namespace boost::redis::adapter
{
namespace boost::redis::adapter {
/** @brief Adapts a type to be used as a response.
*
* The type T must be either
*
* 1. a response<T1, T2, T3, ...> or
* 2. std::vector<node<String>>
* @li a `response<T1, T2, T3, ...>`
* @li `std::vector<node<String>>`
*
* The types T1, T2, etc can be any STL container, any integer type
* and `std::string`.
*
* @param t Tuple containing the responses.
* @tparam T The response type.
*/
template<class T>
template <class T>
auto boost_redis_adapt(T& t) noexcept
{
return detail::response_traits<T>::adapt(t);
}
/** @brief Adapts user data to read operations.
* @ingroup low-level-api
/** @brief Adapts a type to be used as the response to an individual command.
*
* STL containers, \c resp3::response and built-in types are supported and
* can be used in conjunction with \c std::optional<T>.
*
* Example usage:
*
* @code
* std::unordered_map<std::string, std::string> cont;
* co_await async_read(socket, buffer, adapt(cont));
* @endcode
*
* For a transaction
*
* @code
* sr.push(command::multi);
* sr.push(command::ping, ...);
* sr.push(command::incr, ...);
* sr.push_range(command::rpush, ...);
* sr.push(command::lrange, ...);
* sr.push(command::incr, ...);
* sr.push(command::exec);
*
* co_await async_write(socket, buffer(request));
*
* // Reads the response to a transaction
* resp3::response<std::string, int, int, std::vector<std::string>, int> execs;
* co_await resp3::async_read(socket, dynamic_buffer(buffer), adapt(execs));
* @endcode
* It can be used with low-level APIs, like @ref boost::redis::resp3::parser.
*/
template<class T>
template <class T>
auto adapt2(T& t = redis::ignore) noexcept
{ return detail::result_traits<T>::adapt(t); }
{
return detail::result_traits<T>::adapt(t);
}
} // boost::redis::adapter
} // namespace boost::redis::adapter
#endif // BOOST_REDIS_ADAPTER_ADAPT_HPP
#endif // BOOST_REDIS_ADAPTER_ADAPT_HPP

View File

@@ -0,0 +1,125 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_ANY_ADAPTER_HPP
#define BOOST_REDIS_ANY_ADAPTER_HPP
#include <boost/redis/adapter/adapt.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/system/error_code.hpp>
#include <functional>
#include <type_traits>
namespace boost::redis {
/** @brief A type-erased reference to a response.
*
* A type-erased response adapter. It can be executed using @ref connection::async_exec.
* Using this type instead of raw response references enables separate compilation.
*
* Given a response object `resp` that can be passed to `async_exec`, the following two
* statements have the same effect:
*
* @code
* co_await conn.async_exec(req, resp);
* co_await conn.async_exec(req, any_response(resp));
* @endcode
*/
class any_adapter {
public:
/** @brief Parse events that an adapter must support.
*/
enum class parse_event
{
/// Called before the parser starts processing data
init,
/// Called for each and every node of RESP3 data
node,
/// Called when done processing a complete RESP3 message
done
};
/// The type erased implementation type.
using impl_t = std::function<void(parse_event, resp3::node_view const&, system::error_code&)>;
template <class T>
static auto create_impl(T& resp) -> impl_t;
/// Contructs from a type erased adaper
any_adapter(impl_t fn = [](parse_event, resp3::node_view const&, system::error_code&) { })
: impl_{std::move(fn)}
{ }
/**
* @brief Constructor.
*
* Creates a type-erased response adapter from `resp` by calling
* `boost_redis_adapt`. `T` must be a valid Redis response type.
* Any type passed to @ref connection::async_exec qualifies.
*
* This object stores a reference to `resp`, which must be kept alive
* while `*this` is being used.
*/
template <class T, class = std::enable_if_t<!std::is_same_v<T, any_adapter>>>
explicit any_adapter(T& resp)
: impl_(create_impl(resp))
{ }
/// Calls the implementation with the arguments `impl_(parse_event::init, ...);`
void on_init()
{
system::error_code ec;
impl_(parse_event::init, {}, ec);
};
/// Calls the implementation with the arguments `impl_(parse_event::done, ...);`
void on_done()
{
system::error_code ec;
impl_(parse_event::done, {}, ec);
};
/// Calls the implementation with the arguments `impl_(parse_event::node, ...);`
void on_node(resp3::node_view const& nd, system::error_code& ec)
{
impl_(parse_event::node, nd, ec);
};
private:
impl_t impl_;
};
namespace detail {
template <class Adapter>
any_adapter::impl_t make_any_adapter_impl(Adapter&& value)
{
return [adapter = std::move(value)](
any_adapter::parse_event ev,
resp3::node_view const& nd,
system::error_code& ec) mutable {
switch (ev) {
case any_adapter::parse_event::init: adapter.on_init(); break;
case any_adapter::parse_event::node: adapter.on_node(nd, ec); break;
case any_adapter::parse_event::done: adapter.on_done(); break;
}
};
}
} // namespace detail
} // namespace boost::redis
template <class T>
auto boost::redis::any_adapter::create_impl(T& resp) -> impl_t
{
using adapter::boost_redis_adapt;
return detail::make_any_adapter_impl(boost_redis_adapt(resp));
}
#endif

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -7,80 +7,135 @@
#ifndef BOOST_REDIS_ADAPTER_ADAPTERS_HPP
#define BOOST_REDIS_ADAPTER_ADAPTERS_HPP
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/adapter/result.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/response.hpp>
#include <boost/assert.hpp>
#include <set>
#include <optional>
#include <unordered_set>
#include <forward_list>
#include <system_error>
#include <map>
#include <unordered_map>
#include <list>
#include <deque>
#include <vector>
#include <array>
#include <string_view>
#include <charconv>
#include <deque>
#include <forward_list>
#include <list>
#include <map>
#include <optional>
#include <set>
#include <string_view>
#include <system_error>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <vector>
// See https://stackoverflow.com/a/31658120/1077832
#include<ciso646>
#ifdef _LIBCPP_VERSION
#else
#include <cstdlib>
#endif
namespace boost::redis::adapter::detail
{
namespace boost::redis::adapter::detail {
// Serialization.
// Exclude bools, char and charXY_t types
template <class T> struct is_integral_number : std::is_integral<T> { };
template <> struct is_integral_number<bool> : std::false_type { };
template <> struct is_integral_number<char> : std::false_type { };
template <> struct is_integral_number<char16_t> : std::false_type { };
template <> struct is_integral_number<char32_t> : std::false_type { };
template <> struct is_integral_number<wchar_t> : std::false_type { };
#ifdef __cpp_char8_t
template <> struct is_integral_number<char8_t> : std::false_type { };
#endif
template <class T, bool = is_integral_number<T>::value>
struct converter;
template <class T>
auto boost_redis_from_bulk(T& i, std::string_view sv, system::error_code& ec) -> typename std::enable_if<std::is_integral<T>::value, void>::type
{
auto const res = std::from_chars(sv.data(), sv.data() + std::size(sv), i);
if (res.ec != std::errc())
ec = redis::error::not_a_number;
}
struct converter<T, true> {
template <class String>
static void apply(T& i, resp3::basic_node<String> const& node, system::error_code& ec)
{
auto const res = std::from_chars(node.value.data(), node.value.data() + node.value.size(), i);
if (res.ec != std::errc())
ec = redis::error::not_a_number;
}
};
inline
void boost_redis_from_bulk(bool& t, std::string_view sv, system::error_code&)
{
t = *sv.data() == 't';
}
template <>
struct converter<bool, false> {
template <class String>
static void apply(bool& t, resp3::basic_node<String> const& node, system::error_code&)
{
t = *node.value.data() == 't';
}
};
inline
void boost_redis_from_bulk(double& d, std::string_view sv, system::error_code& ec)
{
template <>
struct converter<double, false> {
template <class String>
static void apply(double& d, resp3::basic_node<String> const& node, system::error_code& ec)
{
#ifdef _LIBCPP_VERSION
// The string in sv is not null terminated and we also don't know
// if there is enough space at the end for a null char. The easiest
// thing to do is to create a temporary.
std::string const tmp{sv.data(), sv.data() + std::size(sv)};
char* end{};
d = std::strtod(tmp.data(), &end);
if (d == HUGE_VAL || d == 0)
ec = redis::error::not_a_double;
// The string in node.value is not null terminated and we also
// don't know if there is enough space at the end for a null
// char. The easiest thing to do is to create a temporary.
std::string const tmp{node.value.data(), node.value.data() + node.value.size()};
char* end{};
d = std::strtod(tmp.data(), &end);
if (d == HUGE_VAL || d == 0)
ec = redis::error::not_a_double;
#else
auto const res = std::from_chars(sv.data(), sv.data() + std::size(sv), d);
if (res.ec != std::errc())
ec = redis::error::not_a_double;
#endif // _LIBCPP_VERSION
}
auto const res = std::from_chars(node.value.data(), node.value.data() + node.value.size(), d);
if (res.ec != std::errc())
ec = redis::error::not_a_double;
#endif // _LIBCPP_VERSION
}
};
template <class CharT, class Traits, class Allocator>
void
boost_redis_from_bulk(
std::basic_string<CharT, Traits, Allocator>& s,
std::string_view sv,
system::error_code&)
struct converter<std::basic_string<CharT, Traits, Allocator>, false> {
template <class String>
static void apply(
std::basic_string<CharT, Traits, Allocator>& s,
resp3::basic_node<String> const& node,
system::error_code&)
{
s.append(node.value.data(), node.value.size());
}
};
template <class T>
struct from_bulk_impl {
template <class String>
static void apply(T& t, resp3::basic_node<String> const& node, system::error_code& ec)
{
converter<T>::apply(t, node, ec);
}
};
template <class T>
struct from_bulk_impl<std::optional<T>> {
template <class String>
static void apply(
std::optional<T>& op,
resp3::basic_node<String> const& node,
system::error_code& ec)
{
if (node.data_type != resp3::type::null) {
op.emplace(T{});
converter<T>::apply(op.value(), node, ec);
}
}
};
template <class T, class String>
void boost_redis_from_bulk(T& t, resp3::basic_node<String> const& node, system::error_code& ec)
{
s.append(sv.data(), sv.size());
from_bulk_impl<T>::apply(t, node, ec);
}
//================================================
@@ -91,38 +146,155 @@ private:
Result* result_;
public:
explicit general_aggregate(Result* c = nullptr): result_(c) {}
explicit general_aggregate(Result* c = nullptr)
: result_(c)
{ }
void on_init() { }
void on_done() { }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code&)
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
switch (nd.data_type) {
case resp3::type::blob_error:
case resp3::type::simple_error:
*result_ = error{nd.data_type, std::string{std::cbegin(nd.value), std::cend(nd.value)}};
*result_ = error{
nd.data_type,
std::string{std::cbegin(nd.value), std::cend(nd.value)}
};
break;
default:
result_->value().push_back({nd.data_type, nd.aggregate_size, nd.depth, std::string{std::cbegin(nd.value), std::cend(nd.value)}});
if (result_->has_value()) {
(**result_).push_back({
nd.data_type,
nd.aggregate_size,
nd.depth,
std::string{std::cbegin(nd.value), std::cend(nd.value)}
});
}
}
}
};
template <>
class general_aggregate<resp3::tree> {
private:
resp3::tree* tree_ = nullptr;
public:
explicit general_aggregate(resp3::tree* c = nullptr)
: tree_(c)
{ }
void on_init() { }
void on_done() { }
template <class String>
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
resp3::node tmp;
tmp.data_type = nd.data_type;
tmp.aggregate_size = nd.aggregate_size;
tmp.depth = nd.depth;
tmp.value = std::string{std::cbegin(nd.value), std::cend(nd.value)};
tree_->push_back(std::move(tmp));
}
};
template <>
class general_aggregate<generic_flat_response> {
private:
generic_flat_response* tree_ = nullptr;
public:
explicit general_aggregate(generic_flat_response* c = nullptr)
: tree_(c)
{ }
void on_init()
{
if (tree_->has_value()) {
tree_->value().notify_init();
}
}
void on_done()
{
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
if (tree_->has_value()) {
tree_->value().notify_done();
}
}
template <class String>
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
switch (nd.data_type) {
case resp3::type::blob_error:
case resp3::type::simple_error:
*tree_ = error{
nd.data_type,
std::string{std::cbegin(nd.value), std::cend(nd.value)}
};
break;
default:
if (tree_->has_value()) {
(**tree_).push(nd);
}
}
}
};
template <>
class general_aggregate<resp3::flat_tree> {
private:
resp3::flat_tree* tree_ = nullptr;
public:
explicit general_aggregate(resp3::flat_tree* c = nullptr)
: tree_(c)
{ }
void on_init() { tree_->notify_init(); }
void on_done() { tree_->notify_done(); }
template <class String>
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
tree_->push(nd);
}
};
template <class Node>
class general_simple {
private:
Node* result_;
public:
explicit general_simple(Node* t = nullptr) : result_(t) {}
explicit general_simple(Node* t = nullptr)
: result_(t)
{ }
void on_init() { }
void on_done() { }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code&)
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
switch (nd.data_type) {
case resp3::type::blob_error:
case resp3::type::simple_error:
*result_ = error{nd.data_type, std::string{std::cbegin(nd.value), std::cend(nd.value)}};
*result_ = error{
nd.data_type,
std::string{std::cbegin(nd.value), std::cend(nd.value)}
};
break;
default:
result_->value().data_type = nd.data_type;
@@ -136,17 +308,20 @@ public:
template <class Result>
class simple_impl {
public:
void on_value_available(Result&) {}
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& n, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& node, system::error_code& ec)
{
if (is_aggregate(n.data_type)) {
if (is_aggregate(node.data_type)) {
ec = redis::error::expects_resp3_simple_type;
return;
}
boost_redis_from_bulk(result, n.value, ec);
boost_redis_from_bulk(result, node, ec);
}
};
@@ -156,11 +331,13 @@ private:
typename Result::iterator hint_;
public:
void on_value_available(Result& result)
{ hint_ = std::end(result); }
void on_value_available(Result& result) { hint_ = std::end(result); }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
if (nd.data_type != resp3::type::set)
@@ -171,12 +348,12 @@ public:
BOOST_ASSERT(nd.aggregate_size == 1);
if (nd.depth < 1) {
ec = redis::error::expects_resp3_set;
return;
ec = redis::error::expects_resp3_set;
return;
}
typename Result::key_type obj;
boost_redis_from_bulk(obj, nd.value, ec);
boost_redis_from_bulk(obj, nd, ec);
hint_ = result.insert(hint_, std::move(obj));
}
};
@@ -188,32 +365,34 @@ private:
bool on_key_ = true;
public:
void on_value_available(Result& result)
{ current_ = std::end(result); }
void on_value_available(Result& result) { current_ = std::end(result); }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
if (element_multiplicity(nd.data_type) != 2)
ec = redis::error::expects_resp3_map;
ec = redis::error::expects_resp3_map;
return;
}
BOOST_ASSERT(nd.aggregate_size == 1);
if (nd.depth < 1) {
ec = redis::error::expects_resp3_map;
return;
ec = redis::error::expects_resp3_map;
return;
}
if (on_key_) {
typename Result::key_type obj;
boost_redis_from_bulk(obj, nd.value, ec);
boost_redis_from_bulk(obj, nd, ec);
current_ = result.insert(current_, {std::move(obj), {}});
} else {
typename Result::mapped_type obj;
boost_redis_from_bulk(obj, nd.value, ec);
boost_redis_from_bulk(obj, nd, ec);
current_->second = std::move(obj);
}
@@ -224,17 +403,20 @@ public:
template <class Result>
class vector_impl {
public:
void on_value_available(Result& ) { }
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
auto const m = element_multiplicity(nd.data_type);
result.reserve(result.size() + m * nd.aggregate_size);
} else {
result.push_back({});
boost_redis_from_bulk(result.back(), nd.value, ec);
boost_redis_from_bulk(result.back(), nd, ec);
}
}
};
@@ -245,13 +427,16 @@ private:
int i_ = -1;
public:
void on_value_available(Result& ) { }
void on_value_available(Result&) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (is_aggregate(nd.data_type)) {
if (i_ != -1) {
if (i_ != -1) {
ec = redis::error::nested_aggregate_not_supported;
return;
}
@@ -267,7 +452,7 @@ public:
}
BOOST_ASSERT(nd.aggregate_size == 1);
boost_redis_from_bulk(result.at(i_), nd.value, ec);
boost_redis_from_bulk(result.at(i_), nd, ec);
}
++i_;
@@ -276,21 +461,23 @@ public:
template <class Result>
struct list_impl {
void on_value_available(Result&) { }
void on_value_available(Result& ) { }
void on_init() { }
void on_done() { }
template <class String>
void operator()(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(Result& result, resp3::basic_node<String> const& nd, system::error_code& ec)
{
if (!is_aggregate(nd.data_type)) {
BOOST_ASSERT(nd.aggregate_size == 1);
if (nd.depth < 1) {
ec = redis::error::expects_resp3_aggregate;
return;
}
BOOST_ASSERT(nd.aggregate_size == 1);
if (nd.depth < 1) {
ec = redis::error::expects_resp3_aggregate;
return;
}
result.push_back({});
boost_redis_from_bulk(result.back(), nd.value, ec);
result.push_back({});
boost_redis_from_bulk(result.back(), nd, ec);
}
}
};
@@ -298,56 +485,84 @@ struct list_impl {
//---------------------------------------------------
template <class T>
struct impl_map { using type = simple_impl<T>; };
struct impl_map {
using type = simple_impl<T>;
};
template <class Key, class Compare, class Allocator>
struct impl_map<std::set<Key, Compare, Allocator>> { using type = set_impl<std::set<Key, Compare, Allocator>>; };
struct impl_map<std::set<Key, Compare, Allocator>> {
using type = set_impl<std::set<Key, Compare, Allocator>>;
};
template <class Key, class Compare, class Allocator>
struct impl_map<std::multiset<Key, Compare, Allocator>> { using type = set_impl<std::multiset<Key, Compare, Allocator>>; };
struct impl_map<std::multiset<Key, Compare, Allocator>> {
using type = set_impl<std::multiset<Key, Compare, Allocator>>;
};
template <class Key, class Hash, class KeyEqual, class Allocator>
struct impl_map<std::unordered_set<Key, Hash, KeyEqual, Allocator>> { using type = set_impl<std::unordered_set<Key, Hash, KeyEqual, Allocator>>; };
struct impl_map<std::unordered_set<Key, Hash, KeyEqual, Allocator>> {
using type = set_impl<std::unordered_set<Key, Hash, KeyEqual, Allocator>>;
};
template <class Key, class Hash, class KeyEqual, class Allocator>
struct impl_map<std::unordered_multiset<Key, Hash, KeyEqual, Allocator>> { using type = set_impl<std::unordered_multiset<Key, Hash, KeyEqual, Allocator>>; };
struct impl_map<std::unordered_multiset<Key, Hash, KeyEqual, Allocator>> {
using type = set_impl<std::unordered_multiset<Key, Hash, KeyEqual, Allocator>>;
};
template <class Key, class T, class Compare, class Allocator>
struct impl_map<std::map<Key, T, Compare, Allocator>> { using type = map_impl<std::map<Key, T, Compare, Allocator>>; };
struct impl_map<std::map<Key, T, Compare, Allocator>> {
using type = map_impl<std::map<Key, T, Compare, Allocator>>;
};
template <class Key, class T, class Compare, class Allocator>
struct impl_map<std::multimap<Key, T, Compare, Allocator>> { using type = map_impl<std::multimap<Key, T, Compare, Allocator>>; };
struct impl_map<std::multimap<Key, T, Compare, Allocator>> {
using type = map_impl<std::multimap<Key, T, Compare, Allocator>>;
};
template <class Key, class Hash, class KeyEqual, class Allocator>
struct impl_map<std::unordered_map<Key, Hash, KeyEqual, Allocator>> { using type = map_impl<std::unordered_map<Key, Hash, KeyEqual, Allocator>>; };
struct impl_map<std::unordered_map<Key, Hash, KeyEqual, Allocator>> {
using type = map_impl<std::unordered_map<Key, Hash, KeyEqual, Allocator>>;
};
template <class Key, class Hash, class KeyEqual, class Allocator>
struct impl_map<std::unordered_multimap<Key, Hash, KeyEqual, Allocator>> { using type = map_impl<std::unordered_multimap<Key, Hash, KeyEqual, Allocator>>; };
struct impl_map<std::unordered_multimap<Key, Hash, KeyEqual, Allocator>> {
using type = map_impl<std::unordered_multimap<Key, Hash, KeyEqual, Allocator>>;
};
template <class T, class Allocator>
struct impl_map<std::vector<T, Allocator>> { using type = vector_impl<std::vector<T, Allocator>>; };
struct impl_map<std::vector<T, Allocator>> {
using type = vector_impl<std::vector<T, Allocator>>;
};
template <class T, std::size_t N>
struct impl_map<std::array<T, N>> { using type = array_impl<std::array<T, N>>; };
struct impl_map<std::array<T, N>> {
using type = array_impl<std::array<T, N>>;
};
template <class T, class Allocator>
struct impl_map<std::list<T, Allocator>> { using type = list_impl<std::list<T, Allocator>>; };
struct impl_map<std::list<T, Allocator>> {
using type = list_impl<std::list<T, Allocator>>;
};
template <class T, class Allocator>
struct impl_map<std::deque<T, Allocator>> { using type = list_impl<std::deque<T, Allocator>>; };
struct impl_map<std::deque<T, Allocator>> {
using type = list_impl<std::deque<T, Allocator>>;
};
//---------------------------------------------------
template <class>
class wrapper;
template <class Result>
class wrapper<result<Result>> {
template <class T>
class wrapper<result<T>> {
public:
using response_type = result<Result>;
using response_type = result<T>;
private:
response_type* result_;
typename impl_map<Result>::type impl_;
typename impl_map<T>::type impl_;
bool called_once_ = false;
template <class String>
bool set_if_resp3_error(resp3::basic_node<String> const& nd) noexcept
@@ -356,35 +571,41 @@ private:
case resp3::type::null:
case resp3::type::simple_error:
case resp3::type::blob_error:
*result_ = error{nd.data_type, {std::cbegin(nd.value), std::cend(nd.value)}};
*result_ = error{
nd.data_type,
{std::cbegin(nd.value), std::cend(nd.value)}
};
return true;
default:
return false;
default: return false;
}
}
public:
explicit wrapper(response_type* t = nullptr) : result_(t)
explicit wrapper(response_type* t = nullptr)
: result_(t)
{
if (result_) {
result_->value() = Result{};
result_->value() = T{};
impl_.on_value_available(result_->value());
}
}
void on_init() { impl_.on_init(); }
void on_done() { impl_.on_done(); }
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(resp3::basic_node<String> const& nd, system::error_code& ec)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
if (result_->has_error())
return;
if (set_if_resp3_error(nd))
if (!std::exchange(called_once_, true) && set_if_resp3_error(nd))
return;
BOOST_ASSERT(result_);
impl_(result_->value(), nd, ec);
impl_.on_node(result_->value(), nd, ec);
}
};
@@ -396,6 +617,7 @@ public:
private:
response_type* result_;
typename impl_map<T>::type impl_{};
bool called_once_ = false;
template <class String>
bool set_if_resp3_error(resp3::basic_node<String> const& nd) noexcept
@@ -403,21 +625,25 @@ private:
switch (nd.data_type) {
case resp3::type::blob_error:
case resp3::type::simple_error:
*result_ = error{nd.data_type, {std::cbegin(nd.value), std::cend(nd.value)}};
*result_ = error{
nd.data_type,
{std::cbegin(nd.value), std::cend(nd.value)}
};
return true;
default:
return false;
default: return false;
}
}
public:
explicit wrapper(response_type* o = nullptr) : result_(o) {}
explicit wrapper(response_type* o = nullptr)
: result_(o)
{ }
void on_init() { impl_.on_init(); }
void on_done() { impl_.on_done(); }
template <class String>
void
operator()(
resp3::basic_node<String> const& nd,
system::error_code& ec)
void on_node(resp3::basic_node<String> const& nd, system::error_code& ec)
{
BOOST_ASSERT_MSG(!!result_, "Unexpected null pointer");
@@ -427,18 +653,18 @@ public:
if (set_if_resp3_error(nd))
return;
if (nd.data_type == resp3::type::null)
if (!std::exchange(called_once_, true) && nd.data_type == resp3::type::null)
return;
if (!result_->value().has_value()) {
result_->value() = T{};
impl_.on_value_available(result_->value().value());
result_->value() = T{};
impl_.on_value_available(result_->value().value());
}
impl_(result_->value().value(), nd, ec);
impl_.on_node(result_->value().value(), nd, ec);
}
};
} // boost::redis::adapter::detail
} // namespace boost::redis::adapter::detail
#endif // BOOST_REDIS_ADAPTER_ADAPTERS_HPP
#endif // BOOST_REDIS_ADAPTER_ADAPTERS_HPP

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -7,37 +7,20 @@
#ifndef BOOST_REDIS_ADAPTER_DETAIL_RESPONSE_TRAITS_HPP
#define BOOST_REDIS_ADAPTER_DETAIL_RESPONSE_TRAITS_HPP
#include <boost/redis/adapter/detail/result_traits.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/response.hpp>
#include <boost/redis/adapter/detail/result_traits.hpp>
#include <boost/mp11.hpp>
#include <boost/system.hpp>
#include <tuple>
#include <limits>
#include <string_view>
#include <tuple>
#include <variant>
namespace boost::redis::adapter::detail
{
class ignore_adapter {
public:
template <class String>
void operator()(std::size_t, resp3::basic_node<String> const& nd, system::error_code& ec)
{
switch (nd.data_type) {
case resp3::type::simple_error: ec = redis::error::resp3_simple_error; break;
case resp3::type::blob_error: ec = redis::error::resp3_blob_error; break;
case resp3::type::null: ec = redis::error::resp3_null; break;
default:;
}
}
[[nodiscard]]
auto get_supported_response_size() const noexcept
{ return static_cast<std::size_t>(-1);}
};
namespace boost::redis::adapter::detail {
template <class Response>
class static_adapter {
@@ -48,47 +31,44 @@ private:
using adapters_array_type = std::array<variant_type, size>;
adapters_array_type adapters_;
std::size_t i_ = 0;
public:
explicit static_adapter(Response& r)
{
assigner<size - 1>::assign(adapters_, r);
}
explicit static_adapter(Response& r) { assigner<size - 1>::assign(adapters_, r); }
[[nodiscard]]
auto get_supported_response_size() const noexcept
{ return size;}
template <class String>
void operator()(std::size_t i, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_init()
{
using std::visit;
// I am usure whether this should be an error or an assertion.
BOOST_ASSERT(i < adapters_.size());
visit([&](auto& arg){arg(nd, ec);}, adapters_.at(i));
visit(
[&](auto& arg) {
arg.on_init();
},
adapters_.at(i_));
}
};
template <class Vector>
class vector_adapter {
private:
using adapter_type = typename result_traits<Vector>::adapter_type;
adapter_type adapter_;
public:
explicit vector_adapter(Vector& v)
: adapter_{internal_adapt(v)}
{ }
[[nodiscard]]
auto
get_supported_response_size() const noexcept
{ return static_cast<std::size_t>(-1);}
void on_done()
{
using std::visit;
visit(
[&](auto& arg) {
arg.on_done();
},
adapters_.at(i_));
i_ += 1;
}
template <class String>
void operator()(std::size_t, resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(resp3::basic_node<String> const& nd, system::error_code& ec)
{
adapter_(nd, ec);
using std::visit;
// I am usure whether this should be an error or an assertion.
BOOST_ASSERT(i_ < adapters_.size());
visit(
[&](auto& arg) {
arg.on_node(nd, ec);
},
adapters_.at(i_));
}
};
@@ -98,62 +78,59 @@ struct response_traits;
template <>
struct response_traits<ignore_t> {
using response_type = ignore_t;
using adapter_type = detail::ignore_adapter;
using adapter_type = ignore;
static auto adapt(response_type&) noexcept
{ return detail::ignore_adapter{}; }
static auto adapt(response_type&) noexcept { return ignore{}; }
};
template <>
struct response_traits<result<ignore_t>> {
using response_type = result<ignore_t>;
using adapter_type = detail::ignore_adapter;
using adapter_type = ignore;
static auto adapt(response_type&) noexcept
{ return detail::ignore_adapter{}; }
static auto adapt(response_type&) noexcept { return ignore{}; }
};
template <class String, class Allocator>
struct response_traits<result<std::vector<resp3::basic_node<String>, Allocator>>> {
using response_type = result<std::vector<resp3::basic_node<String>, Allocator>>;
using adapter_type = vector_adapter<response_type>;
struct response_traits<result<resp3::basic_tree<String, Allocator>>> {
using response_type = result<resp3::basic_tree<String, Allocator>>;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept
{ return adapter_type{v}; }
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <class ...Ts>
template <class String>
struct response_traits<resp3::basic_tree<String>> {
using response_type = resp3::basic_tree<String>;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <>
struct response_traits<resp3::flat_tree> {
using response_type = resp3::flat_tree;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <>
struct response_traits<generic_flat_response> {
using response_type = generic_flat_response;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <class... Ts>
struct response_traits<response<Ts...>> {
using response_type = response<Ts...>;
using adapter_type = static_adapter<response_type>;
static auto adapt(response_type& r) noexcept
{ return adapter_type{r}; }
static auto adapt(response_type& r) noexcept { return adapter_type{r}; }
};
template <class Adapter>
class wrapper {
public:
explicit wrapper(Adapter adapter) : adapter_{adapter} {}
} // namespace boost::redis::adapter::detail
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code& ec)
{ return adapter_(0, nd, ec); }
[[nodiscard]]
auto get_supported_response_size() const noexcept
{ return adapter_.get_supported_response_size();}
private:
Adapter adapter_;
};
template <class Adapter>
auto make_adapter_wrapper(Adapter adapter)
{
return wrapper{adapter};
}
} // boost::redis::adapter::detail
#endif // BOOST_REDIS_ADAPTER_DETAIL_RESPONSE_TRAITS_HPP
#endif // BOOST_REDIS_ADAPTER_DETAIL_RESPONSE_TRAITS_HPP

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -7,21 +7,23 @@
#ifndef BOOST_REDIS_ADAPTER_RESPONSE_TRAITS_HPP
#define BOOST_REDIS_ADAPTER_RESPONSE_TRAITS_HPP
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/adapter/detail/adapters.hpp>
#include <boost/redis/adapter/result.hpp>
#include <boost/redis/adapter/ignore.hpp>
#include <boost/redis/adapter/result.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/resp3/tree.hpp>
#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/mp11.hpp>
#include <vector>
#include <tuple>
#include <string_view>
#include <tuple>
#include <variant>
#include <vector>
namespace boost::redis::adapter::detail
{
namespace boost::redis::adapter::detail {
/* Traits class for response objects.
*
@@ -30,7 +32,7 @@ namespace boost::redis::adapter::detail
*/
template <class Result>
struct result_traits {
using adapter_type = adapter::detail::wrapper<typename std::decay<Result>::type>;
using adapter_type = wrapper<typename std::decay<Result>::type>;
static auto adapt(Result& r) noexcept { return adapter_type{&r}; }
};
@@ -56,36 +58,59 @@ struct result_traits<result<resp3::basic_node<T>>> {
};
template <class String, class Allocator>
struct result_traits<result<std::vector<resp3::basic_node<String>, Allocator>>> {
struct result_traits<result<resp3::basic_tree<String, Allocator>>> {
using response_type = result<std::vector<resp3::basic_node<String>, Allocator>>;
using adapter_type = adapter::detail::general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <class String>
struct result_traits<resp3::basic_tree<String>> {
using response_type = resp3::basic_tree<String>;
using adapter_type = adapter::detail::general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <>
struct result_traits<generic_flat_response> {
using response_type = generic_flat_response;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <>
struct result_traits<resp3::flat_tree> {
using response_type = resp3::flat_tree;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <class T>
using adapter_t = typename result_traits<std::decay_t<T>>::adapter_type;
template<class T>
template <class T>
auto internal_adapt(T& t) noexcept
{ return result_traits<std::decay_t<T>>::adapt(t); }
{
return result_traits<std::decay_t<T>>::adapt(t);
}
template <std::size_t N>
struct assigner {
template <class T1, class T2>
static void assign(T1& dest, T2& from)
{
dest[N].template emplace<N>(internal_adapt(std::get<N>(from)));
assigner<N - 1>::assign(dest, from);
}
template <class T1, class T2>
static void assign(T1& dest, T2& from)
{
dest[N].template emplace<N>(internal_adapt(std::get<N>(from)));
assigner<N - 1>::assign(dest, from);
}
};
template <>
struct assigner<0> {
template <class T1, class T2>
static void assign(T1& dest, T2& from)
{
dest[0].template emplace<0>(internal_adapt(std::get<0>(from)));
}
template <class T1, class T2>
static void assign(T1& dest, T2& from)
{
dest[0].template emplace<0>(internal_adapt(std::get<0>(from)));
}
};
template <class Tuple>
@@ -94,16 +119,16 @@ class static_aggregate_adapter;
template <class Tuple>
class static_aggregate_adapter<result<Tuple>> {
private:
using adapters_array_type =
std::array<
mp11::mp_rename<
mp11::mp_transform<
adapter_t, Tuple>,
std::variant>,
std::tuple_size<Tuple>::value>;
using adapters_array_type = std::array<
mp11::mp_rename<mp11::mp_transform<adapter_t, Tuple>, std::variant>,
std::tuple_size<Tuple>::value>;
// Tuple element we are currently on.
std::size_t i_ = 0;
// Nested aggregate element counter.
std::size_t aggregate_size_ = 0;
adapters_array_type adapters_;
result<Tuple>* res_ = nullptr;
@@ -117,47 +142,73 @@ public:
}
template <class String>
void count(resp3::basic_node<String> const& nd)
void count(resp3::basic_node<String> const& elem)
{
if (nd.depth == 1) {
if (is_aggregate(nd.data_type))
aggregate_size_ = element_multiplicity(nd.data_type) * nd.aggregate_size;
else
++i_;
return;
if (elem.depth == 1 && is_aggregate(elem.data_type)) {
aggregate_size_ = element_multiplicity(elem.data_type) * elem.aggregate_size;
}
if (--aggregate_size_ == 0)
++i_;
if (aggregate_size_ == 0) {
i_ += 1;
} else {
aggregate_size_ -= 1;
}
}
void on_init()
{
using std::visit;
for (auto& adapter : adapters_) {
visit(
[&](auto& arg) {
arg.on_init();
},
adapter);
}
}
void on_done()
{
using std::visit;
for (auto& adapter : adapters_) {
visit(
[&](auto& arg) {
arg.on_done();
},
adapter);
}
}
template <class String>
void operator()(resp3::basic_node<String> const& nd, system::error_code& ec)
void on_node(resp3::basic_node<String> const& elem, system::error_code& ec)
{
using std::visit;
if (nd.depth == 0) {
auto const real_aggr_size = nd.aggregate_size * element_multiplicity(nd.data_type);
if (elem.depth == 0) {
auto const multiplicity = element_multiplicity(elem.data_type);
auto const real_aggr_size = elem.aggregate_size * multiplicity;
if (real_aggr_size != std::tuple_size<Tuple>::value)
ec = redis::error::incompatible_size;
return;
}
visit([&](auto& arg){arg(nd, ec);}, adapters_[i_]);
count(nd);
visit(
[&](auto& arg) {
arg.on_node(elem, ec);
},
adapters_.at(i_));
count(elem);
}
};
template <class... Ts>
struct result_traits<result<std::tuple<Ts...>>>
{
struct result_traits<result<std::tuple<Ts...>>> {
using response_type = result<std::tuple<Ts...>>;
using adapter_type = static_aggregate_adapter<response_type>;
static auto adapt(response_type& r) noexcept { return adapter_type{&r}; }
};
} // boost::redis::adapter::detail
} // namespace boost::redis::adapter::detail
#endif // BOOST_REDIS_ADAPTER_RESPONSE_TRAITS_HPP
#endif // BOOST_REDIS_ADAPTER_RESPONSE_TRAITS_HPP

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -7,31 +7,32 @@
#ifndef BOOST_REDIS_ADAPTER_IGNORE_HPP
#define BOOST_REDIS_ADAPTER_IGNORE_HPP
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/system/error_code.hpp>
#include <string>
namespace boost::redis::adapter
{
namespace boost::redis::adapter {
/** @brief An adapter that ignores responses
* @ingroup high-level-api
/** @brief An adapter that ignores responses.
*
* RESP3 errors won't be ignored.
*/
struct ignore {
void operator()(resp3::basic_node<std::string_view> const& nd, system::error_code& ec)
void on_init() { }
void on_done() { }
void on_node(resp3::basic_node<std::string_view> const& nd, system::error_code& ec)
{
switch (nd.data_type) {
case resp3::type::simple_error: ec = redis::error::resp3_simple_error; break;
case resp3::type::blob_error: ec = redis::error::resp3_blob_error; break;
case resp3::type::null: ec = redis::error::resp3_null; break;
default:;
case resp3::type::blob_error: ec = redis::error::resp3_blob_error; break;
case resp3::type::null: ec = redis::error::resp3_null; break;
default: ;
}
}
};
} // boost::redis::adapter
} // namespace boost::redis::adapter
#endif // BOOST_REDIS_ADAPTER_IGNORE_HPP
#endif // BOOST_REDIS_ADAPTER_IGNORE_HPP

View File

@@ -1,5 +1,5 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -8,17 +8,17 @@
#ifndef BOOST_REDIS_ADAPTER_RESULT_HPP
#define BOOST_REDIS_ADAPTER_RESULT_HPP
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/detail/resp3_type_to_error.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/system/result.hpp>
#include <string>
namespace boost::redis::adapter
{
namespace boost::redis::adapter {
/** @brief Stores any resp3 error
* @ingroup high-level-api
*/
/// Stores any resp3 error.
struct error {
/// RESP3 error data type.
resp3::type data_type = resp3::type::invalid;
@@ -44,38 +44,24 @@ inline bool operator==(error const& a, error const& b)
* @param a Left hand side error object.
* @param b Right hand side error object.
*/
inline bool operator!=(error const& a, error const& b)
{
return !(a == b);
}
inline bool operator!=(error const& a, error const& b) { return !(a == b); }
/** @brief Stores response to individual Redis commands
* @ingroup high-level-api
*/
/// Stores response to individual Redis commands.
template <class Value>
using result = system::result<Value, error>;
BOOST_NORETURN inline void
throw_exception_from_error(error const & e, boost::source_location const &)
/**
* @brief Allows using @ref error with `boost::system::result`.
* @param e The error to throw.
* @relates error
*/
BOOST_NORETURN inline void throw_exception_from_error(error const& e, boost::source_location const&)
{
system::error_code ec;
switch (e.data_type) {
case resp3::type::simple_error:
ec = redis::error::resp3_simple_error;
break;
case resp3::type::blob_error:
ec = redis::error::resp3_blob_error;
break;
case resp3::type::null:
ec = redis::error::resp3_null;
break;
default:
BOOST_ASSERT_MSG(false, "Unexpected data type.");
}
throw system::system_error(ec, e.diagnostic);
throw system::system_error(
system::error_code(detail::resp3_type_to_error(e.data_type)),
e.diagnostic);
}
} // boost::redis::adapter
} // namespace boost::redis::adapter
#endif // BOOST_REDIS_ADAPTER_RESULT_HPP
#endif // BOOST_REDIS_ADAPTER_RESULT_HPP

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -7,16 +7,17 @@
#ifndef BOOST_REDIS_CONFIG_HPP
#define BOOST_REDIS_CONFIG_HPP
#include <string>
#include <boost/redis/request.hpp>
#include <chrono>
#include <limits>
#include <optional>
#include <string>
#include <vector>
namespace boost::redis
{
namespace boost::redis {
/** @brief Address of a Redis server
* @ingroup high-level-api
*/
/// Address of a Redis server.
struct address {
/// Redis host.
std::string host = "127.0.0.1";
@@ -24,62 +25,328 @@ struct address {
std::string port = "6379";
};
/** @brief Configure parameters used by the connection classes
* @ingroup high-level-api
/** @brief Compares two addresses for equality.
* @relates address
*
* @param a Left hand side address.
* @param b Right hand side address.
*/
struct config {
/// Uses SSL instead of a plain connection.
inline bool operator==(address const& a, address const& b)
{
return a.host == b.host && a.port == b.port;
}
/** @brief Compares two addresses for inequality.
* @relates address
*
* @param a Left hand side address.
* @param b Right hand side address.
*/
inline bool operator!=(address const& a, address const& b) { return !(a == b); }
/// Identifies the possible roles of a Redis server.
enum class role
{
/// The server is a master.
master,
/// The server is a replica.
replica,
};
/// Configuration values to use when using Sentinel.
struct sentinel_config {
/**
* @brief A list of (hostname, port) pairs where the Sentinels are listening.
*
* Sentinels in this list will be contacted in order, until a successful
* connection is made. At this point, the `SENTINEL SENTINELS` command
* will be used to retrieve any additional Sentinels monitoring the configured master.
* Thus, it is not required to keep this list comprehensive - if Sentinels are added
* later, they will be detected at runtime.
*
* Sentinel will only be used if this value is not empty.
*
* Numeric IP addresses are also allowed as hostnames.
*/
std::vector<address> addresses{};
/**
* @brief The name of the master to connect to, as configured in the
* `sentinel monitor` statement in `sentinel.conf`.
*
* This field is required even when connecting to replicas.
*/
std::string master_name{};
/**
* @brief Whether connections to Sentinels should use TLS or not.
* Does not affect connections to masters.
*
* When set to `true`, physical connections to Sentinels will be established
* using TLS. This setting does *not* influence how masters and replicas are contacted.
* To use TLS when connecting to these, set @ref config::use_ssl to `true`.
*/
bool use_ssl = false;
/// Address of the Redis server.
/**
* @brief A request to be sent to Sentinels upon connection establishment.
*
* This request is executed every time a Sentinel is contacted, and before
* commands like `SENTINEL GET-MASTER-NAME-BY-ADDR` are run.
* By default, this field contains a `HELLO 3` command.
* You can use this request to set up any authorization required by Sentinels.
*
* This request should ensure that the connection is upgraded to RESP3
* by executing `HELLO 3` or similar. RESP2 is not supported yet.
*/
request setup = detail::make_hello_request();
/**
* @brief Time span that the Sentinel resolve operation is allowed to elapse.
* Does not affect connections to masters and replicas, controlled by @ref config::resolve_timeout.
*/
std::chrono::steady_clock::duration resolve_timeout = std::chrono::milliseconds{500};
/**
* @brief Time span that the Sentinel connect operation is allowed to elapse.
* Does not affect connections to masters and replicas, controlled by @ref config::connect_timeout.
*/
std::chrono::steady_clock::duration connect_timeout = std::chrono::milliseconds{500};
/**
* @brief Time span that the Sentinel TLS handshake operation is allowed to elapse.
* Does not affect connections to masters and replicas, controlled by @ref config::ssl_handshake_timeout.
*/
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{5};
/**
* @brief Time span that the Sentinel request/response exchange is allowed to elapse.
* Includes executing the commands in @ref setup and the commands required to
* resolve the server's address.
*/
std::chrono::steady_clock::duration request_timeout = std::chrono::seconds{5};
/**
* @brief Whether to connect to a Redis master or to a replica.
*
* The library resolves and connects to the Redis master, by default.
* Set this value to @ref role::replica to connect to one of the replicas
* of the master identified by @ref master_name.
* The particular replica will be chosen randomly.
*/
role server_role = role::master;
};
/// Configure parameters used by the connection classes.
struct config {
/**
* @brief Whether to use TLS instead of plaintext connections.
*
* When using Sentinel, configures whether to use TLS when connecting to masters and replicas.
* Use @ref sentinel_config::use_ssl to control TLS for Sentinels.
*/
bool use_ssl = false;
/// For TCP connections, hostname and port of the Redis server. Ignored when using Sentinel.
address addr = address{"127.0.0.1", "6379"};
/** @brief Username passed to the
* [HELLO](https://redis.io/commands/hello/) command. If left
* empty `HELLO` will be sent without authentication parameters.
/**
* @brief The UNIX domain socket path where the server is listening.
*
* If non-empty, communication with the server will happen using
* UNIX domain sockets, and @ref addr will be ignored.
*
* UNIX domain sockets can't be used with SSL: if `unix_socket` is non-empty,
* @ref use_ssl must be `false`. UNIX domain sockets can't be used with Sentinel, either.
*
* UNIX domain sockets can't be used with Sentinel.
*/
std::string username;
std::string unix_socket;
/** @brief Password passed to the
* [HELLO](https://redis.io/commands/hello/) command. If left
* empty `HELLO` will be sent without authentication parameters.
/** @brief Username used for authentication during connection establishment.
*
* If @ref use_setup is false (the default), during connection establishment,
* authentication is performed by sending a `HELLO` command.
* This field contains the username to employ.
*
* If the username equals the literal `"default"` (the default)
* and no password is specified, the `HELLO` command is sent
* without authentication parameters.
*
* When using Sentinel, this setting applies to masters and replicas.
* Use @ref sentinel_config::setup to configure authorization for Sentinels.
*/
std::string username = "default";
/** @brief Password used for authentication during connection establishment.
*
* If @ref use_setup is false (the default), during connection establishment,
* authentication is performed by sending a `HELLO` command.
* This field contains the password to employ.
*
* If the username equals the literal `"default"` (the default)
* and no password is specified, the `HELLO` command is sent
* without authentication parameters.
*
* When using Sentinel, this setting applies to masters and replicas.
* Use @ref sentinel_config::setup to configure authorization for Sentinels.
*/
std::string password;
/// Client name parameter of the [HELLO](https://redis.io/commands/hello/) command.
/** @brief Client name parameter to use during connection establishment.
*
* If @ref use_setup is false (the default), during connection establishment,
* a `HELLO` command is sent. If this field is not empty, the `HELLO` command
* will contain a `SETNAME` subcommand containing this value.
*
* When using Sentinel, this setting applies to masters and replicas.
* Use @ref sentinel_config::setup to configure this value for Sentinels.
*/
std::string clientname = "Boost.Redis";
/// Database that will be passed to the [SELECT](https://redis.io/commands/hello/) command.
/** @brief Database index to pass to the `SELECT` command during connection establishment.
*
* If @ref use_setup is false (the default), and this field is set to a
* non-empty optional, and its value is different than zero,
* a `SELECT` command will be issued during connection establishment to set the logical
* database index. By default, no `SELECT` command is sent.
*
* When using Sentinel, this setting applies to masters and replicas.
*/
std::optional<int> database_index = 0;
/// Message used by the health-checker in `boost::redis::connection::async_run`.
/// Message used by `PING` commands sent by the health checker.
std::string health_check_id = "Boost.Redis";
/// Logger prefix, see `boost::redis::logger`.
/**
* @brief (Deprecated) Sets the logger prefix, a string printed before log messages.
*
* Setting a prefix in this struct is deprecated. If you need to change how log messages
* look like, please construct a logger object passing a formatting function, and use that
* logger in connection's constructor. This member will be removed in subsequent releases.
*/
std::string log_prefix = "(Boost.Redis) ";
/// Time the resolve operation is allowed to last.
/**
* @brief Time span that the resolve operation is allowed to elapse.
* When using Sentinel, this setting applies to masters and replicas.
*/
std::chrono::steady_clock::duration resolve_timeout = std::chrono::seconds{10};
/// Time the connect operation is allowed to last.
/**
* @brief Time span that the connect operation is allowed to elapse.
* When using Sentinel, this setting applies to masters and replicas.
*/
std::chrono::steady_clock::duration connect_timeout = std::chrono::seconds{10};
/// Time the SSL handshake operation is allowed to last.
/**
* @brief Time span that the SSL handshake operation is allowed to elapse.
* When using Sentinel, this setting applies to masters and replicas.
*/
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{10};
/** Health checks interval.
*
* To disable health-checks pass zero as duration.
/** @brief Time span between successive health checks.
* Set to zero to disable health-checks.
*
* When this value is set to a non-zero duration, @ref basic_connection::async_run
* will issue `PING` commands whenever no command is sent to the server for more
* than `health_check_interval`. You can configure the message passed to the `PING`
* command using @ref health_check_id.
*
* Enabling health checks also sets timeouts to individual network
* operations. The connection is considered dead if:
*
* @li No byte can be written to the server after `health_check_interval`.
* @li No byte is read from the server after `2 * health_check_interval`.
*
* If the health checker finds that the connection is unresponsive, it will be closed,
* and a reconnection will be triggered, as if a network error had occurred.
*
* The exact timeout values are *not* part of the interface, and might change
* in future versions.
*
* When using Sentinel, this setting applies to masters and replicas.
* Sentinels are not health-checked.
*/
std::chrono::steady_clock::duration health_check_interval = std::chrono::seconds{2};
/** @brief Time waited before trying a reconnection.
*
* To disable reconnection pass zero as duration.
/** @brief Time span to wait between successive connection retries.
* Set to zero to disable reconnection.
*
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
* If none of the configured Sentinels can be contacted, this time span will
* be waited before trying again. After a connection error with a master or replica
* is encountered, this time span will be waited before contacting Sentinels again.
*/
std::chrono::steady_clock::duration reconnect_wait_interval = std::chrono::seconds{1};
/** @brief Maximum size of the socket read-buffer in bytes.
*
* Sets a limit on how much data is allowed to be read into the
* read buffer. It can be used to prevent DDOS.
*
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
*/
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)();
/** @brief Grow size of the read buffer.
*
* The size by which the read buffer grows when more space is
* needed. This can help avoiding some memory allocations. Once the
* maximum size is reached no more memory allocations are made
* since the buffer is reused.
*
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
*/
std::size_t read_buffer_append_size = 4096;
/** @brief Enables using a custom requests during connection establishment.
*
* If set to true, the @ref setup member will be sent to the server immediately after
* connection establishment. Every time a reconnection happens, the setup
* request will be executed before any other request.
* It can be used to perform authentication,
* subscribe to channels or select a database index.
*
* When set to true, *the custom setup request replaces the built-in HELLO
* request generated by the library*. The @ref username, @ref password,
* @ref clientname and @ref database_index fields *will be ignored*.
*
* By default, @ref setup contains a `"HELLO 3"` command, which upgrades the
* protocol to RESP3. You might modify this request as you like,
* but you should ensure that the resulting connection uses RESP3.
*
* To prevent sending any setup request at all, set this field to true
* and @ref setup to an empty request. This can be used to interface with
* systems that don't support `HELLO`.
*
* By default, this field is false, and @ref setup will not be used.
*
* When using Sentinel, this setting applies to masters and replicas.
* Use @ref sentinel_config::setup for Sentinels.
*/
bool use_setup = false;
/** @brief Request to be executed after connection establishment.
*
* This member is only used if @ref use_setup is `true`. Please consult
* @ref use_setup docs for more info.
*
* By default, `setup` contains a `"HELLO 3"` command.
*
* When using Sentinel, this setting applies to masters and replicas.
* Use @ref sentinel_config::setup for Sentinels.
*/
request setup = detail::make_hello_request();
/**
* @brief Configuration values for Sentinel. Sentinel is enabled only if
* @ref sentinel_config::addresses is not empty.
*/
sentinel_config sentinel{};
};
} // boost::redis
} // namespace boost::redis
#endif // BOOST_REDIS_CONFIG_HPP
#endif // BOOST_REDIS_CONFIG_HPP

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
//
// 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_CONNECT_FSM_HPP
#define BOOST_REDIS_CONNECT_FSM_HPP
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/system/error_code.hpp>
// Sans-io algorithm for redis_stream::async_connect, as a finite state machine
namespace boost::redis::detail {
struct buffered_logger;
// What transport is redis_stream using?
enum class transport_type
{
tcp, // plaintext TCP
tcp_tls, // TLS over TCP
unix_socket, // UNIX domain sockets
};
struct redis_stream_state {
transport_type type{transport_type::tcp};
bool ssl_stream_used{false};
};
// What should we do next?
enum class connect_action_type
{
unix_socket_close, // Close the UNIX socket, to discard state
unix_socket_connect, // Connect to the UNIX socket
tcp_resolve, // Name resolution
tcp_connect, // TCP connect
ssl_stream_reset, // Re-create the SSL stream, to discard state
ssl_handshake, // SSL handshake
done, // Complete the async op
};
struct connect_action {
connect_action_type type;
system::error_code ec;
connect_action(connect_action_type type) noexcept
: type{type}
{ }
connect_action(system::error_code ec) noexcept
: type{connect_action_type::done}
, ec{ec}
{ }
};
class connect_fsm {
int resume_point_{0};
buffered_logger* lgr_{nullptr};
public:
connect_fsm(buffered_logger& lgr) noexcept
: lgr_(&lgr)
{ }
connect_action resume(
system::error_code ec,
const asio::ip::tcp::resolver::results_type& resolver_results,
redis_stream_state& st,
asio::cancellation_type_t cancel_state);
connect_action resume(
system::error_code ec,
const asio::ip::tcp::endpoint& selected_endpoint,
redis_stream_state& st,
asio::cancellation_type_t cancel_state);
connect_action resume(
system::error_code ec,
redis_stream_state& st,
asio::cancellation_type_t cancel_state);
}; // namespace boost::redis::detail
} // namespace boost::redis::detail
#endif

View File

@@ -0,0 +1,65 @@
//
// 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_CONNECT_PARAMS_HPP
#define BOOST_REDIS_CONNECT_PARAMS_HPP
// Parameters used by redis_stream::async_connect
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connect_fsm.hpp>
#include <chrono>
#include <string_view>
namespace boost::redis::detail {
// Fully identifies where a server is listening. Reference type.
class any_address_view {
transport_type type_;
union {
const address* tcp_;
std::string_view unix_;
};
public:
any_address_view(const address& addr, bool use_ssl) noexcept
: type_(use_ssl ? transport_type::tcp_tls : transport_type::tcp)
, tcp_(&addr)
{ }
explicit any_address_view(std::string_view unix_socket) noexcept
: type_(transport_type::unix_socket)
, unix_(unix_socket)
{ }
transport_type type() const { return type_; }
const address& tcp_address() const
{
BOOST_ASSERT(type_ == transport_type::tcp || type_ == transport_type::tcp_tls);
return *tcp_;
}
std::string_view unix_socket() const
{
BOOST_ASSERT(type_ == transport_type::unix_socket);
return unix_;
}
};
struct connect_params {
any_address_view addr;
std::chrono::steady_clock::duration resolve_timeout;
std::chrono::steady_clock::duration connect_timeout;
std::chrono::steady_clock::duration ssl_handshake_timeout;
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -1,958 +0,0 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_CONNECTION_BASE_HPP
#define BOOST_REDIS_CONNECTION_BASE_HPP
#include <boost/redis/adapter/adapt.hpp>
#include <boost/redis/detail/helper.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/operation.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/detail/runner.hpp>
#include <boost/redis/usage.hpp>
#include <boost/system.hpp>
#include <boost/asio/basic_stream_socket.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/write.hpp>
#include <boost/assert.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/experimental/channel.hpp>
#include <algorithm>
#include <array>
#include <chrono>
#include <deque>
#include <memory>
#include <string_view>
#include <type_traits>
#include <functional>
namespace boost::redis::detail
{
template <class DynamicBuffer>
std::string_view buffer_view(DynamicBuffer buf) noexcept
{
char const* start = static_cast<char const*>(buf.data(0, buf.size()).data());
return std::string_view{start, std::size(buf)};
}
template <class AsyncReadStream, class DynamicBuffer>
class append_some_op {
private:
AsyncReadStream& stream_;
DynamicBuffer buf_;
std::size_t size_ = 0;
std::size_t tmp_ = 0;
asio::coroutine coro_{};
public:
append_some_op(AsyncReadStream& stream, DynamicBuffer buf, std::size_t size)
: stream_ {stream}
, buf_ {std::move(buf)}
, size_{size}
{ }
template <class Self>
void operator()( Self& self
, system::error_code ec = {}
, std::size_t n = 0)
{
BOOST_ASIO_CORO_REENTER (coro_)
{
tmp_ = buf_.size();
buf_.grow(size_);
BOOST_ASIO_CORO_YIELD
stream_.async_read_some(buf_.data(tmp_, size_), std::move(self));
if (ec) {
self.complete(ec, 0);
return;
}
buf_.shrink(buf_.size() - tmp_ - n);
self.complete({}, n);
}
}
};
template <class AsyncReadStream, class DynamicBuffer, class CompletionToken>
auto
async_append_some(
AsyncReadStream& stream,
DynamicBuffer buffer,
std::size_t size,
CompletionToken&& token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code, std::size_t)
>(append_some_op<AsyncReadStream, DynamicBuffer> {stream, buffer, size}, token, stream);
}
template <class Conn>
struct exec_op {
using req_info_type = typename Conn::req_info;
using adapter_type = typename Conn::adapter_type;
Conn* conn_ = nullptr;
std::shared_ptr<req_info_type> info_ = nullptr;
asio::coroutine coro{};
template <class Self>
void operator()(Self& self , system::error_code ec = {})
{
BOOST_ASIO_CORO_REENTER (coro)
{
// Check whether the user wants to wait for the connection to
// be stablished.
if (info_->req_->get_config().cancel_if_not_connected && !conn_->is_open()) {
BOOST_ASIO_CORO_YIELD
asio::post(std::move(self));
return self.complete(error::not_connected, 0);
}
conn_->add_request_info(info_);
EXEC_OP_WAIT:
BOOST_ASIO_CORO_YIELD
info_->async_wait(std::move(self));
BOOST_ASSERT(ec == asio::error::operation_aborted);
if (info_->ec_) {
self.complete(info_->ec_, 0);
return;
}
if (info_->stop_requested()) {
// Don't have to call remove_request as it has already
// been by cancel(exec).
return self.complete(ec, 0);
}
if (is_cancelled(self)) {
if (info_->is_written()) {
using c_t = asio::cancellation_type;
auto const c = self.get_cancellation_state().cancelled();
if ((c & c_t::terminal) != c_t::none) {
// Cancellation requires closing the connection
// otherwise it stays in inconsistent state.
conn_->cancel(operation::run);
return self.complete(ec, 0);
} else {
// Can't implement other cancelation types, ignoring.
self.get_cancellation_state().clear();
// TODO: Find out a better way to ignore
// cancelation.
goto EXEC_OP_WAIT;
}
} else {
// Cancelation can be honored.
conn_->remove_request(info_);
self.complete(ec, 0);
return;
}
}
self.complete(info_->ec_, info_->read_size_);
}
}
};
template <class Conn, class Logger>
struct run_op {
Conn* conn = nullptr;
Logger logger_;
asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 2> order = {}
, system::error_code ec0 = {}
, system::error_code ec1 = {})
{
BOOST_ASIO_CORO_REENTER (coro)
{
conn->reset();
BOOST_ASIO_CORO_YIELD
asio::experimental::make_parallel_group(
[this](auto token) { return conn->reader(logger_, token);},
[this](auto token) { return conn->writer(logger_, token);}
).async_wait(
asio::experimental::wait_for_one(),
std::move(self));
if (is_cancelled(self)) {
logger_.trace("run-op: canceled. Exiting ...");
self.complete(asio::error::operation_aborted);
return;
}
logger_.on_run(ec0, ec1);
switch (order[0]) {
case 0: self.complete(ec0); break;
case 1: self.complete(ec1); break;
default: BOOST_ASSERT(false);
}
}
}
};
template <class Conn, class Logger>
struct writer_op {
Conn* conn_;
Logger logger_;
asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, system::error_code ec = {}
, std::size_t n = 0)
{
ignore_unused(n);
BOOST_ASIO_CORO_REENTER (coro) for (;;)
{
while (conn_->coalesce_requests()) {
if (conn_->use_ssl())
BOOST_ASIO_CORO_YIELD asio::async_write(conn_->next_layer(), asio::buffer(conn_->write_buffer_), std::move(self));
else
BOOST_ASIO_CORO_YIELD asio::async_write(conn_->next_layer().next_layer(), asio::buffer(conn_->write_buffer_), std::move(self));
logger_.on_write(ec, conn_->write_buffer_);
if (ec) {
logger_.trace("writer-op: error. Exiting ...");
conn_->cancel(operation::run);
self.complete(ec);
return;
}
if (is_cancelled(self)) {
logger_.trace("writer-op: canceled. Exiting ...");
self.complete(asio::error::operation_aborted);
return;
}
conn_->on_write();
// A socket.close() may have been called while a
// successful write might had already been queued, so we
// have to check here before proceeding.
if (!conn_->is_open()) {
logger_.trace("writer-op: canceled (2). Exiting ...");
self.complete({});
return;
}
}
BOOST_ASIO_CORO_YIELD
conn_->writer_timer_.async_wait(std::move(self));
if (!conn_->is_open() || is_cancelled(self)) {
logger_.trace("writer-op: canceled (3). Exiting ...");
// Notice this is not an error of the op, stoping was
// requested from the outside, so we complete with
// success.
self.complete({});
return;
}
}
}
};
template <class Conn, class Logger>
struct reader_op {
using parse_result = typename Conn::parse_result;
using parse_ret_type = typename Conn::parse_ret_type;
Conn* conn_;
Logger logger_;
parse_ret_type res_{parse_result::resp, 0};
asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, system::error_code ec = {}
, std::size_t n = 0)
{
ignore_unused(n);
BOOST_ASIO_CORO_REENTER (coro) for (;;)
{
// Appends some data to the buffer if necessary.
if ((res_.first == parse_result::needs_more) || std::empty(conn_->read_buffer_)) {
if (conn_->use_ssl()) {
BOOST_ASIO_CORO_YIELD
async_append_some(
conn_->next_layer(),
conn_->dbuf_,
conn_->get_suggested_buffer_growth(),
std::move(self));
} else {
BOOST_ASIO_CORO_YIELD
async_append_some(
conn_->next_layer().next_layer(),
conn_->dbuf_,
conn_->get_suggested_buffer_growth(),
std::move(self));
}
logger_.on_read(ec, n);
// EOF is not treated as error.
if (ec == asio::error::eof) {
logger_.trace("reader-op: EOF received. Exiting ...");
conn_->cancel(operation::run);
return self.complete({}); // EOFINAE: EOF is not an error.
}
// The connection is not viable after an error.
if (ec) {
logger_.trace("reader-op: error. Exiting ...");
conn_->cancel(operation::run);
self.complete(ec);
return;
}
// Somebody might have canceled implicitly or explicitly
// while we were suspended and after queueing so we have to
// check.
if (!conn_->is_open() || is_cancelled(self)) {
logger_.trace("reader-op: canceled. Exiting ...");
self.complete(ec);
return;
}
}
res_ = conn_->on_read(buffer_view(conn_->dbuf_), ec);
if (ec) {
logger_.trace("reader-op: parse error. Exiting ...");
conn_->cancel(operation::run);
self.complete(ec);
return;
}
if (res_.first == parse_result::push) {
if (!conn_->receive_channel_.try_send(ec, res_.second)) {
BOOST_ASIO_CORO_YIELD
conn_->receive_channel_.async_send(ec, res_.second, std::move(self));
}
if (ec) {
logger_.trace("reader-op: error. Exiting ...");
conn_->cancel(operation::run);
self.complete(ec);
return;
}
if (!conn_->is_open() || is_cancelled(self)) {
logger_.trace("reader-op: canceled (2). Exiting ...");
self.complete(asio::error::operation_aborted);
return;
}
}
}
}
};
/** @brief Base class for high level Redis asynchronous connections.
* @ingroup high-level-api
*
* @tparam Executor The executor type.
*
*/
template <class Executor>
class connection_base {
public:
/// Executor type
using executor_type = Executor;
/// Type of the next layer
using next_layer_type = asio::ssl::stream<asio::basic_stream_socket<asio::ip::tcp, Executor>>;
using clock_type = std::chrono::steady_clock;
using clock_traits_type = asio::wait_traits<clock_type>;
using timer_type = asio::basic_waitable_timer<clock_type, clock_traits_type, executor_type>;
using this_type = connection_base<Executor>;
/// Constructs from an executor.
connection_base(
executor_type ex,
asio::ssl::context::method method,
std::size_t max_read_size)
: ctx_{method}
, stream_{std::make_unique<next_layer_type>(ex, ctx_)}
, writer_timer_{ex}
, receive_channel_{ex, 256}
, runner_{ex, {}}
, dbuf_{read_buffer_, max_read_size}
{
set_receive_response(ignore);
writer_timer_.expires_at((std::chrono::steady_clock::time_point::max)());
}
/// Returns the ssl context.
auto const& get_ssl_context() const noexcept
{ return ctx_;}
/// Returns the ssl context.
auto& get_ssl_context() noexcept
{ return ctx_;}
/// Resets the underlying stream.
void reset_stream()
{
stream_ = std::make_unique<next_layer_type>(writer_timer_.get_executor(), ctx_);
}
/// Returns a reference to the next layer.
auto& next_layer() noexcept { return *stream_; }
/// Returns a const reference to the next layer.
auto const& next_layer() const noexcept { return *stream_; }
/// Returns the associated executor.
auto get_executor() {return writer_timer_.get_executor();}
/// Cancels specific operations.
void cancel(operation op)
{
runner_.cancel(op);
if (op == operation::all) {
cancel_impl(operation::run);
cancel_impl(operation::receive);
cancel_impl(operation::exec);
return;
}
cancel_impl(op);
}
template <class Response, class CompletionToken>
auto async_exec(request const& req, Response& resp, CompletionToken token)
{
using namespace boost::redis::adapter;
auto f = boost_redis_adapt(resp);
BOOST_ASSERT_MSG(req.get_expected_responses() <= f.get_supported_response_size(), "Request and response have incompatible sizes.");
auto info = std::make_shared<req_info>(req, f, get_executor());
return asio::async_compose
< CompletionToken
, void(system::error_code, std::size_t)
>(exec_op<this_type>{this, info}, token, writer_timer_);
}
template <class Response, class CompletionToken>
[[deprecated("Set the response with set_receive_response and use the other overload.")]]
auto async_receive(Response& response, CompletionToken token)
{
set_receive_response(response);
return receive_channel_.async_receive(std::move(token));
}
template <class CompletionToken>
auto async_receive(CompletionToken token)
{ return receive_channel_.async_receive(std::move(token)); }
std::size_t receive(system::error_code& ec)
{
std::size_t size = 0;
auto f = [&](system::error_code const& ec2, std::size_t n)
{
ec = ec2;
size = n;
};
auto const res = receive_channel_.try_receive(f);
if (ec)
return 0;
if (!res)
ec = error::sync_receive_push_failed;
return size;
}
template <class Logger, class CompletionToken>
auto async_run(config const& cfg, Logger l, CompletionToken token)
{
runner_.set_config(cfg);
l.set_prefix(runner_.get_config().log_prefix);
return runner_.async_run(*this, l, std::move(token));
}
template <class Response>
void set_receive_response(Response& response)
{
using namespace boost::redis::adapter;
auto g = boost_redis_adapt(response);
receive_adapter_ = adapter::detail::make_adapter_wrapper(g);
}
usage get_usage() const noexcept
{ return usage_; }
private:
using receive_channel_type = asio::experimental::channel<executor_type, void(system::error_code, std::size_t)>;
using runner_type = runner<executor_type>;
using adapter_type = std::function<void(std::size_t, resp3::basic_node<std::string_view> const&, system::error_code&)>;
using receiver_adapter_type = std::function<void(resp3::basic_node<std::string_view> const&, system::error_code&)>;
auto use_ssl() const noexcept
{ return runner_.get_config().use_ssl;}
auto cancel_on_conn_lost() -> std::size_t
{
// Must return false if the request should be removed.
auto cond = [](auto const& ptr)
{
BOOST_ASSERT(ptr != nullptr);
if (ptr->is_written()) {
return !ptr->req_->get_config().cancel_if_unresponded;
} else {
return !ptr->req_->get_config().cancel_on_connection_lost;
}
};
auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), cond);
auto const ret = std::distance(point, std::end(reqs_));
std::for_each(point, std::end(reqs_), [](auto const& ptr) {
ptr->stop();
});
reqs_.erase(point, std::end(reqs_));
std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
return ptr->reset_status();
});
return ret;
}
auto cancel_unwritten_requests() -> std::size_t
{
auto f = [](auto const& ptr)
{
BOOST_ASSERT(ptr != nullptr);
return ptr->is_written();
};
auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), f);
auto const ret = std::distance(point, std::end(reqs_));
std::for_each(point, std::end(reqs_), [](auto const& ptr) {
ptr->stop();
});
reqs_.erase(point, std::end(reqs_));
return ret;
}
void cancel_impl(operation op)
{
switch (op) {
case operation::exec:
{
cancel_unwritten_requests();
} break;
case operation::run:
{
close();
writer_timer_.cancel();
receive_channel_.cancel();
cancel_on_conn_lost();
} break;
case operation::receive:
{
receive_channel_.cancel();
} break;
default: /* ignore */;
}
}
void on_write()
{
// We have to clear the payload right after writing it to use it
// as a flag that informs there is no ongoing write.
write_buffer_.clear();
// Notice this must come before the for-each below.
cancel_push_requests();
// There is small optimization possible here: traverse only the
// partition of unwritten requests instead of them all.
std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
BOOST_ASSERT_MSG(ptr != nullptr, "Expects non-null pointer.");
if (ptr->is_staged())
ptr->mark_written();
});
}
struct req_info {
public:
using node_type = resp3::basic_node<std::string_view>;
using wrapped_adapter_type = std::function<void(node_type const&, system::error_code&)>;
enum class action
{
stop,
proceed,
none,
};
explicit req_info(request const& req, adapter_type adapter, executor_type ex)
: timer_{ex}
, action_{action::none}
, req_{&req}
, adapter_{}
, expected_responses_{req.get_expected_responses()}
, status_{status::none}
, ec_{{}}
, read_size_{0}
{
timer_.expires_at((std::chrono::steady_clock::time_point::max)());
adapter_ = [this, adapter](node_type const& nd, system::error_code& ec)
{
auto const i = req_->get_expected_responses() - expected_responses_;
adapter(i, nd, ec);
};
}
auto proceed()
{
timer_.cancel();
action_ = action::proceed;
}
void stop()
{
timer_.cancel();
action_ = action::stop;
}
[[nodiscard]] auto is_waiting_write() const noexcept
{ return !is_written() && !is_staged(); }
[[nodiscard]] auto is_written() const noexcept
{ return status_ == status::written; }
[[nodiscard]] auto is_staged() const noexcept
{ return status_ == status::staged; }
void mark_written() noexcept
{ status_ = status::written; }
void mark_staged() noexcept
{ status_ = status::staged; }
void reset_status() noexcept
{ status_ = status::none; }
[[nodiscard]] auto stop_requested() const noexcept
{ return action_ == action::stop;}
template <class CompletionToken>
auto async_wait(CompletionToken token)
{
return timer_.async_wait(std::move(token));
}
//private:
enum class status
{ none
, staged
, written
};
timer_type timer_;
action action_;
request const* req_;
wrapped_adapter_type adapter_;
// Contains the number of commands that haven't been read yet.
std::size_t expected_responses_;
status status_;
system::error_code ec_;
std::size_t read_size_;
};
void remove_request(std::shared_ptr<req_info> const& info)
{
reqs_.erase(std::remove(std::begin(reqs_), std::end(reqs_), info));
}
using reqs_type = std::deque<std::shared_ptr<req_info>>;
template <class, class> friend struct reader_op;
template <class, class> friend struct writer_op;
template <class, class> friend struct run_op;
template <class> friend struct exec_op;
template <class, class, class> friend struct run_all_op;
void cancel_push_requests()
{
auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
return !(ptr->is_staged() && ptr->req_->get_expected_responses() == 0);
});
std::for_each(point, std::end(reqs_), [](auto const& ptr) {
ptr->proceed();
});
reqs_.erase(point, std::end(reqs_));
}
[[nodiscard]] bool is_writing() const noexcept
{
return !write_buffer_.empty();
}
void add_request_info(std::shared_ptr<req_info> const& info)
{
reqs_.push_back(info);
if (info->req_->has_hello_priority()) {
auto rend = std::partition_point(std::rbegin(reqs_), std::rend(reqs_), [](auto const& e) {
return e->is_waiting_write();
});
std::rotate(std::rbegin(reqs_), std::rbegin(reqs_) + 1, rend);
}
if (is_open() && !is_writing())
writer_timer_.cancel();
}
template <class CompletionToken, class Logger>
auto reader(Logger l, CompletionToken&& token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(reader_op<this_type, Logger>{this, l}, token, writer_timer_);
}
template <class CompletionToken, class Logger>
auto writer(Logger l, CompletionToken&& token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(writer_op<this_type, Logger>{this, l}, token, writer_timer_);
}
template <class Logger, class CompletionToken>
auto async_run_lean(config const& cfg, Logger l, CompletionToken token)
{
runner_.set_config(cfg);
l.set_prefix(runner_.get_config().log_prefix);
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(run_op<this_type, Logger>{this, l}, token, writer_timer_);
}
[[nodiscard]] bool coalesce_requests()
{
// Coalesces the requests and marks them staged. After a
// successful write staged requests will be marked as written.
auto const point = std::partition_point(std::cbegin(reqs_), std::cend(reqs_), [](auto const& ri) {
return !ri->is_waiting_write();
});
std::for_each(point, std::cend(reqs_), [this](auto const& ri) {
// Stage the request.
write_buffer_ += ri->req_->payload();
ri->mark_staged();
usage_.commands_sent += ri->expected_responses_;
});
usage_.bytes_sent += std::size(write_buffer_);
return point != std::cend(reqs_);
}
bool is_waiting_response() const noexcept
{
return !std::empty(reqs_) && reqs_.front()->is_written();
}
void close()
{
if (stream_->next_layer().is_open()) {
system::error_code ec;
stream_->next_layer().close(ec);
}
}
auto is_open() const noexcept { return stream_->next_layer().is_open(); }
auto& lowest_layer() noexcept { return stream_->lowest_layer(); }
auto is_next_push()
{
// We handle unsolicited events in the following way
//
// 1. Its resp3 type is a push.
//
// 2. A non-push type is received with an empty requests
// queue. I have noticed this is possible (e.g. -MISCONF).
// I expect them to have type push so we can distinguish
// them from responses to commands, but it is a
// simple-error. If we are lucky enough to receive them
// when the command queue is empty we can treat them as
// server pushes, otherwise it is impossible to handle
// them properly
//
// 3. The request does not expect any response but we got
// one. This may happen if for example, subscribe with
// wrong syntax.
//
// Useful links:
//
// - https://github.com/redis/redis/issues/11784
// - https://github.com/redis/redis/issues/6426
//
BOOST_ASSERT(!read_buffer_.empty());
return
(resp3::to_type(read_buffer_.front()) == resp3::type::push)
|| reqs_.empty()
|| (!reqs_.empty() && reqs_.front()->expected_responses_ == 0)
|| !is_waiting_response(); // Added to deal with MONITOR.
}
auto get_suggested_buffer_growth() const noexcept
{
return parser_.get_suggested_buffer_growth(4096);
}
enum class parse_result { needs_more, push, resp };
using parse_ret_type = std::pair<parse_result, std::size_t>;
parse_ret_type on_finish_parsing(parse_result t)
{
if (t == parse_result::push) {
usage_.pushes_received += 1;
usage_.push_bytes_received += parser_.get_consumed();
} else {
usage_.responses_received += 1;
usage_.response_bytes_received += parser_.get_consumed();
}
on_push_ = false;
dbuf_.consume(parser_.get_consumed());
auto const res = std::make_pair(t, parser_.get_consumed());
parser_.reset();
return res;
}
parse_ret_type on_read(std::string_view data, system::error_code& ec)
{
// We arrive here in two states:
//
// 1. While we are parsing a message. In this case we
// don't want to determine the type of the message in the
// buffer (i.e. response vs push) but leave it untouched
// until the parsing of a complete message ends.
//
// 2. On a new message, in which case we have to determine
// whether the next messag is a push or a response.
//
if (!on_push_) // Prepare for new message.
on_push_ = is_next_push();
if (on_push_) {
if (!resp3::parse(parser_, data, receive_adapter_, ec))
return std::make_pair(parse_result::needs_more, 0);
if (ec)
return std::make_pair(parse_result::push, 0);
return on_finish_parsing(parse_result::push);
}
BOOST_ASSERT_MSG(is_waiting_response(), "Not waiting for a response (using MONITOR command perhaps?)");
BOOST_ASSERT(!reqs_.empty());
BOOST_ASSERT(reqs_.front() != nullptr);
BOOST_ASSERT(reqs_.front()->expected_responses_ != 0);
if (!resp3::parse(parser_, data, reqs_.front()->adapter_, ec))
return std::make_pair(parse_result::needs_more, 0);
if (ec) {
reqs_.front()->ec_ = ec;
reqs_.front()->proceed();
return std::make_pair(parse_result::resp, 0);
}
reqs_.front()->read_size_ += parser_.get_consumed();
if (--reqs_.front()->expected_responses_ == 0) {
// Done with this request.
reqs_.front()->proceed();
reqs_.pop_front();
}
return on_finish_parsing(parse_result::resp);
}
void reset()
{
write_buffer_.clear();
read_buffer_.clear();
parser_.reset();
on_push_ = false;
}
asio::ssl::context ctx_;
std::unique_ptr<next_layer_type> stream_;
// Notice we use a timer to simulate a condition-variable. It is
// also more suitable than a channel and the notify operation does
// not suspend.
timer_type writer_timer_;
receive_channel_type receive_channel_;
runner_type runner_;
receiver_adapter_type receive_adapter_;
using dyn_buffer_type = asio::dynamic_string_buffer<char, std::char_traits<char>, std::allocator<char>>;
std::string read_buffer_;
dyn_buffer_type dbuf_;
std::string write_buffer_;
reqs_type reqs_;
resp3::parser parser_{};
bool on_push_ = false;
usage usage_;
};
} // boost::redis::detail
#endif // BOOST_REDIS_CONNECTION_BASE_HPP

View File

@@ -0,0 +1,64 @@
//
// 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_CONNECTION_STATE_HPP
#define BOOST_REDIS_CONNECTION_STATE_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/detail/subscription_tracker.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/response.hpp>
#include <random>
#include <string>
#include <vector>
namespace boost::redis::detail {
// A random engine that gets seeded lazily.
// Seeding with std::random_device is not trivial and might fail.
class lazy_random_engine {
bool seeded_{};
std::minstd_rand eng_;
public:
lazy_random_engine() = default;
std::minstd_rand& get()
{
if (!seeded_) {
eng_.seed(static_cast<std::minstd_rand::result_type>(std::random_device{}()));
seeded_ = true;
}
return eng_;
}
};
// Contains all the members in connection that don't depend on the Executor.
// Makes implementing sans-io algorithms easier
struct connection_state {
buffered_logger logger;
config cfg{};
multiplexer mpx{};
std::string diagnostic{}; // Used by the setup request and Sentinel
request setup_req{};
request ping_req{};
subscription_tracker tracker{};
bool receive2_running{false}, receive2_cancelled{false};
// Sentinel stuff
lazy_random_engine eng{};
std::vector<address> sentinels{};
std::vector<resp3::node> sentinel_resp_nodes{}; // for parsing
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -1,133 +0,0 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_CONNECTOR_HPP
#define BOOST_REDIS_CONNECTOR_HPP
#include <boost/redis/detail/helper.hpp>
#include <boost/redis/error.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/steady_timer.hpp>
#include <string>
#include <chrono>
namespace boost::redis::detail
{
template <class Connector, class Stream>
struct connect_op {
Connector* ctor_ = nullptr;
Stream* stream = nullptr;
asio::ip::tcp::resolver::results_type const* res_ = nullptr;
asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 2> const& order = {}
, system::error_code const& ec1 = {}
, asio::ip::tcp::endpoint const& ep= {}
, system::error_code const& ec2 = {})
{
BOOST_ASIO_CORO_REENTER (coro)
{
ctor_->timer_.expires_after(ctor_->timeout_);
BOOST_ASIO_CORO_YIELD
asio::experimental::make_parallel_group(
[this](auto token)
{
auto f = [](system::error_code const&, auto const&) { return true; };
return asio::async_connect(*stream, *res_, f, token);
},
[this](auto token) { return ctor_->timer_.async_wait(token);}
).async_wait(
asio::experimental::wait_for_one(),
std::move(self));
if (is_cancelled(self)) {
self.complete(asio::error::operation_aborted);
return;
}
switch (order[0]) {
case 0: {
ctor_->endpoint_ = ep;
self.complete(ec1);
} break;
case 1:
{
if (ec2) {
self.complete(ec2);
} else {
self.complete(error::connect_timeout);
}
} break;
default: BOOST_ASSERT(false);
}
}
}
};
template <class Executor>
class connector {
public:
using timer_type =
asio::basic_waitable_timer<
std::chrono::steady_clock,
asio::wait_traits<std::chrono::steady_clock>,
Executor>;
connector(Executor ex)
: timer_{ex}
{}
void set_config(config const& cfg)
{ timeout_ = cfg.connect_timeout; }
template <class Stream, class CompletionToken>
auto
async_connect(
Stream& stream,
asio::ip::tcp::resolver::results_type const& res,
CompletionToken&& token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(connect_op<connector, Stream>{this, &stream, &res}, token, timer_);
}
std::size_t cancel(operation op)
{
switch (op) {
case operation::connect:
case operation::all:
timer_.cancel();
break;
default: /* ignore */;
}
return 0;
}
auto const& endpoint() const noexcept { return endpoint_;}
private:
template <class, class> friend struct connect_op;
timer_type timer_;
std::chrono::steady_clock::duration timeout_ = std::chrono::seconds{2};
asio::ip::tcp::endpoint endpoint_;
};
} // boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -0,0 +1,36 @@
//
// 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_DETAIL_COROUTINE_HPP
#define BOOST_REDIS_DETAIL_COROUTINE_HPP
// asio::coroutine uses __COUNTER__ internally, which can trigger
// ODR violations if we use them in header-only code. These manifest as
// extremely hard-to-debug bugs only present in release builds.
// Use this instead when doing coroutines in non-template code.
// Adapted from Boost.MySQL.
// Coroutine state is represented as an integer (resume_point_var).
// Every yield gets assigned a unique value (resume_point_id).
// Yielding sets the next resume point, returns, and sets a case label for re-entering.
// Coroutines need to switch on resume_point_var to re-enter.
// Enclosing this in a scope allows placing the macro inside a brace-less for/while loop
// The empty scope after the case label is required because labels can't be at the end of a compound statement
#define BOOST_REDIS_YIELD(resume_point_var, resume_point_id, ...) \
{ \
resume_point_var = resume_point_id; \
return {__VA_ARGS__}; \
case resume_point_id: \
{ \
} \
}
#define BOOST_REDIS_CORO_INITIAL case 0:
#endif

View File

@@ -0,0 +1,74 @@
//
// 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_EXEC_FSM_HPP
#define BOOST_REDIS_EXEC_FSM_HPP
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <memory>
// Sans-io algorithm for async_exec, as a finite state machine
namespace boost::redis::detail {
struct connection_state;
// What should we do next?
enum class exec_action_type
{
setup_cancellation, // Set up the cancellation types supported by the composed operation
immediate, // Invoke asio::async_immediate to avoid re-entrancy problems
done, // Call the final handler
notify_writer, // Notify the writer task
wait_for_response, // Wait to be notified
};
class exec_action {
exec_action_type type_;
system::error_code ec_;
std::size_t bytes_read_;
public:
exec_action(exec_action_type type) noexcept
: type_{type}
{ }
exec_action(system::error_code ec, std::size_t bytes_read = 0u) noexcept
: type_{exec_action_type::done}
, ec_{ec}
, bytes_read_{bytes_read}
{ }
exec_action_type type() const { return type_; }
system::error_code error() const { return ec_; }
std::size_t bytes_read() const { return bytes_read_; }
};
class exec_fsm {
int resume_point_{0};
std::shared_ptr<multiplexer::elem> elem_;
public:
exec_fsm(std::shared_ptr<multiplexer::elem> elem) noexcept
: elem_(std::move(elem))
{ }
exec_action resume(
bool connection_is_open,
connection_state& st,
asio::cancellation_type_t cancel_state);
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -0,0 +1,69 @@
//
// 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_EXEC_ONE_FSM_HPP
#define BOOST_REDIS_EXEC_ONE_FSM_HPP
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/resp3/parser.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
// Sans-io algorithm for async_exec_one, as a finite state machine
namespace boost::redis::detail {
class read_buffer;
// What should we do next?
enum class exec_one_action_type
{
done, // Call the final handler
write, // Write the request
read_some, // Read into the read buffer
};
struct exec_one_action {
exec_one_action_type type;
system::error_code ec;
exec_one_action(exec_one_action_type type) noexcept
: type{type}
{ }
exec_one_action(system::error_code ec) noexcept
: type{exec_one_action_type::done}
, ec{ec}
{ }
};
class exec_one_fsm {
int resume_point_{0};
any_adapter adapter_;
std::size_t remaining_responses_;
resp3::parser parser_;
public:
exec_one_fsm(any_adapter resp, std::size_t expected_responses)
: adapter_(std::move(resp))
, remaining_responses_(expected_responses)
{ }
exec_one_action resume(
read_buffer& buffer,
system::error_code ec,
std::size_t bytes_transferred,
asio::cancellation_type_t cancel_state);
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -1,124 +0,0 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_SSL_CONNECTOR_HPP
#define BOOST_REDIS_SSL_CONNECTOR_HPP
#include <boost/redis/detail/helper.hpp>
#include <boost/redis/error.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/ssl.hpp>
#include <string>
#include <chrono>
namespace boost::redis::detail
{
template <class Handshaker, class Stream>
struct handshake_op {
Handshaker* hsher_ = nullptr;
Stream* stream_ = nullptr;
asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 2> const& order = {}
, system::error_code const& ec1 = {}
, system::error_code const& ec2 = {})
{
BOOST_ASIO_CORO_REENTER (coro)
{
hsher_->timer_.expires_after(hsher_->timeout_);
BOOST_ASIO_CORO_YIELD
asio::experimental::make_parallel_group(
[this](auto token) { return stream_->async_handshake(asio::ssl::stream_base::client, token); },
[this](auto token) { return hsher_->timer_.async_wait(token);}
).async_wait(
asio::experimental::wait_for_one(),
std::move(self));
if (is_cancelled(self)) {
self.complete(asio::error::operation_aborted);
return;
}
switch (order[0]) {
case 0: {
self.complete(ec1);
} break;
case 1:
{
if (ec2) {
self.complete(ec2);
} else {
self.complete(error::ssl_handshake_timeout);
}
} break;
default: BOOST_ASSERT(false);
}
}
}
};
template <class Executor>
class handshaker {
public:
using timer_type =
asio::basic_waitable_timer<
std::chrono::steady_clock,
asio::wait_traits<std::chrono::steady_clock>,
Executor>;
handshaker(Executor ex)
: timer_{ex}
{}
template <class Stream, class CompletionToken>
auto
async_handshake(Stream& stream, CompletionToken&& token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(handshake_op<handshaker, Stream>{this, &stream}, token, timer_);
}
std::size_t cancel(operation op)
{
switch (op) {
case operation::ssl_handshake:
case operation::all:
timer_.cancel();
break;
default: /* ignore */;
}
return 0;
}
constexpr bool is_dummy() const noexcept
{return false;}
void set_config(config const& cfg)
{ timeout_ = cfg.ssl_handshake_timeout; }
private:
template <class, class> friend struct handshake_op;
timer_type timer_;
std::chrono::steady_clock::duration timeout_;
};
} // boost::redis::detail
#endif // BOOST_REDIS_SSL_CONNECTOR_HPP

View File

@@ -1,252 +0,0 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_HEALTH_CHECKER_HPP
#define BOOST_REDIS_HEALTH_CHECKER_HPP
// Has to included before promise.hpp to build on msvc.
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/redis/operation.hpp>
#include <boost/redis/detail/helper.hpp>
#include <boost/redis/config.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
#include <memory>
#include <chrono>
namespace boost::redis::detail {
template <class HealthChecker, class Connection, class Logger>
class ping_op {
public:
HealthChecker* checker_ = nullptr;
Connection* conn_ = nullptr;
Logger logger_;
asio::coroutine coro_{};
template <class Self>
void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
{
BOOST_ASIO_CORO_REENTER (coro_) for (;;)
{
if (checker_->checker_has_exited_) {
logger_.trace("ping_op: checker has exited. Exiting ...");
self.complete({});
return;
}
BOOST_ASIO_CORO_YIELD
conn_->async_exec(checker_->req_, checker_->resp_, std::move(self));
if (ec || is_cancelled(self)) {
logger_.trace("ping_op: error/cancelled (1).");
checker_->wait_timer_.cancel();
self.complete(!!ec ? ec : asio::error::operation_aborted);
return;
}
// Wait before pinging again.
checker_->ping_timer_.expires_after(checker_->ping_interval_);
BOOST_ASIO_CORO_YIELD
checker_->ping_timer_.async_wait(std::move(self));
if (ec || is_cancelled(self)) {
logger_.trace("ping_op: error/cancelled (2).");
self.complete(!!ec ? ec : asio::error::operation_aborted);
return;
}
}
}
};
template <class HealthChecker, class Connection, class Logger>
class check_timeout_op {
public:
HealthChecker* checker_ = nullptr;
Connection* conn_ = nullptr;
Logger logger_;
asio::coroutine coro_{};
template <class Self>
void operator()(Self& self, system::error_code ec = {})
{
BOOST_ASIO_CORO_REENTER (coro_) for (;;)
{
checker_->wait_timer_.expires_after(2 * checker_->ping_interval_);
BOOST_ASIO_CORO_YIELD
checker_->wait_timer_.async_wait(std::move(self));
if (ec || is_cancelled(self)) {
logger_.trace("check-timeout-op: error/canceled. Exiting ...");
self.complete(!!ec ? ec : asio::error::operation_aborted);
return;
}
if (checker_->resp_.has_error()) {
logger_.trace("check-timeout-op: Response error. Exiting ...");
self.complete({});
return;
}
if (checker_->resp_.value().empty()) {
logger_.trace("check-timeout-op: Response has no value. Exiting ...");
checker_->ping_timer_.cancel();
conn_->cancel(operation::run);
checker_->checker_has_exited_ = true;
self.complete(error::pong_timeout);
return;
}
if (checker_->resp_.has_value()) {
checker_->resp_.value().clear();
}
}
}
};
template <class HealthChecker, class Connection, class Logger>
class check_health_op {
public:
HealthChecker* checker_ = nullptr;
Connection* conn_ = nullptr;
Logger logger_;
asio::coroutine coro_{};
template <class Self>
void
operator()(
Self& self,
std::array<std::size_t, 2> order = {},
system::error_code ec1 = {},
system::error_code ec2 = {})
{
BOOST_ASIO_CORO_REENTER (coro_)
{
if (checker_->ping_interval_ == std::chrono::seconds::zero()) {
logger_.trace("check-health-op: timeout disabled.");
BOOST_ASIO_CORO_YIELD
asio::post(std::move(self));
self.complete({});
return;
}
BOOST_ASIO_CORO_YIELD
asio::experimental::make_parallel_group(
[this](auto token) { return checker_->async_ping(*conn_, logger_, token); },
[this](auto token) { return checker_->async_check_timeout(*conn_, logger_, token);}
).async_wait(
asio::experimental::wait_for_one(),
std::move(self));
logger_.on_check_health(ec1, ec2);
if (is_cancelled(self)) {
logger_.trace("check-health-op: canceled. Exiting ...");
self.complete(asio::error::operation_aborted);
return;
}
switch (order[0]) {
case 0: self.complete(ec1); return;
case 1: self.complete(ec2); return;
default: BOOST_ASSERT(false);
}
}
}
};
template <class Executor>
class health_checker {
private:
using timer_type =
asio::basic_waitable_timer<
std::chrono::steady_clock,
asio::wait_traits<std::chrono::steady_clock>,
Executor>;
public:
health_checker(Executor ex)
: ping_timer_{ex}
, wait_timer_{ex}
{
req_.push("PING", "Boost.Redis");
}
void set_config(config const& cfg)
{
req_.clear();
req_.push("PING", cfg.health_check_id);
ping_interval_ = cfg.health_check_interval;
}
template <
class Connection,
class Logger,
class CompletionToken = asio::default_completion_token_t<Executor>
>
auto
async_check_health(
Connection& conn,
Logger l,
CompletionToken token = CompletionToken{})
{
checker_has_exited_ = false;
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(check_health_op<health_checker, Connection, Logger>{this, &conn, l}, token, conn);
}
std::size_t cancel(operation op)
{
switch (op) {
case operation::health_check:
case operation::all:
ping_timer_.cancel();
wait_timer_.cancel();
break;
default: /* ignore */;
}
return 0;
}
private:
template <class Connection, class Logger, class CompletionToken>
auto async_ping(Connection& conn, Logger l, CompletionToken token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(ping_op<health_checker, Connection, Logger>{this, &conn, l}, token, conn, ping_timer_);
}
template <class Connection, class Logger, class CompletionToken>
auto async_check_timeout(Connection& conn, Logger l, CompletionToken token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(check_timeout_op<health_checker, Connection, Logger>{this, &conn, l}, token, conn, wait_timer_);
}
template <class, class, class> friend class ping_op;
template <class, class, class> friend class check_timeout_op;
template <class, class, class> friend class check_health_op;
timer_type ping_timer_;
timer_type wait_timer_;
redis::request req_;
redis::generic_response resp_;
std::chrono::steady_clock::duration ping_interval_ = std::chrono::seconds{5};
bool checker_has_exited_ = false;
};
} // boost::redis::detail
#endif // BOOST_REDIS_HEALTH_CHECKER_HPP

View File

@@ -1,37 +0,0 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_HELPER_HPP
#define BOOST_REDIS_HELPER_HPP
#include <boost/asio/cancellation_type.hpp>
namespace boost::redis::detail
{
template <class T>
auto is_cancelled(T const& self)
{
return self.get_cancellation_state().cancelled() != asio::cancellation_type_t::none;
}
#define BOOST_REDIS_CHECK_OP0(X)\
if (ec || redis::detail::is_cancelled(self)) {\
X\
self.complete(!!ec ? ec : asio::error::operation_aborted);\
return;\
}
#define BOOST_REDIS_CHECK_OP1(X)\
if (ec || redis::detail::is_cancelled(self)) {\
X\
self.complete(!!ec ? ec : asio::error::operation_aborted, {});\
return;\
}
} // boost::redis::detail
#endif // BOOST_REDIS_HELPER_HPP

View File

@@ -0,0 +1,239 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_MULTIPLEXER_HPP
#define BOOST_REDIS_MULTIPLEXER_HPP
#include <boost/redis/adapter/adapt.hpp>
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/detail/read_buffer.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/parser.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/usage.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <deque>
#include <functional>
#include <memory>
#include <string_view>
#include <utility>
namespace boost::redis {
class request;
namespace detail {
// Return type of the multiplexer::consume_next function
enum class consume_result
{
needs_more, // consume_next didn't have enough data
got_response, // got a response to a regular command, vs. a push
got_push, // got a response to a push
};
class multiplexer {
public:
struct elem {
public:
explicit elem(request const& req, any_adapter adapter);
void set_done_callback(std::function<void()> f) noexcept { done_ = std::move(f); };
auto notify_done() noexcept -> void
{
status_ = status::done;
done_();
}
auto notify_error(system::error_code ec) noexcept -> void;
[[nodiscard]]
auto is_waiting() const noexcept
{
return status_ == status::waiting;
}
[[nodiscard]]
auto is_written() const noexcept
{
return status_ == status::written;
}
[[nodiscard]]
auto is_staged() const noexcept
{
return status_ == status::staged;
}
[[nodiscard]]
bool is_done() const noexcept
{
return status_ == status::done;
}
void mark_written() noexcept { status_ = status::written; }
void mark_staged() noexcept { status_ = status::staged; }
void mark_waiting() noexcept { status_ = status::waiting; }
auto get_error() const -> system::error_code const& { return ec_; }
auto get_request() const -> request const& { return *req_; }
auto get_read_size() const -> std::size_t { return read_size_; }
auto get_remaining_responses() const -> std::size_t { return remaining_responses_; }
auto commit_response(std::size_t read_size) -> void;
auto get_adapter() -> any_adapter& { return adapter_; }
// Marks the element as an abandoned request. An abandoned request
// won't cause problems when its response arrives, but that response will be ignored.
void mark_abandoned();
[[nodiscard]]
bool is_abandoned() const
{
return !req_;
}
private:
enum class status
{
waiting, // the request hasn't been written yet
staged, // we've issued the write for this request, but it hasn't finished yet
written, // the request has been written successfully
done, // the request has completed and the done callback has been invoked
};
request const* req_;
any_adapter adapter_;
std::function<void()> done_;
// Contains the number of commands that haven't been read yet.
std::size_t remaining_responses_;
status status_;
system::error_code ec_;
std::size_t read_size_;
};
multiplexer();
// To be called before a write operation. Coalesces all available requests
// into a single buffer. Returns the number of coalesced requests.
// Must be called before cancel_on_conn_lost() because it might change
// request status.
[[nodiscard]]
auto prepare_write() -> std::size_t;
// To be called after a write operation.
// Returns true once all the bytes in the buffer generated by prepare_write
// have been written.
// Must be called before cancel_on_conn_lost() because it might change
// request status.
auto commit_write(std::size_t bytes_written) -> bool;
// To be called after a successful read operation.
// Must be called before cancel_on_conn_lost() because it might change
// request status.
[[nodiscard]]
auto consume(system::error_code& ec) -> std::pair<consume_result, std::size_t>;
auto add(std::shared_ptr<elem> const& ptr) -> void;
void cancel(std::shared_ptr<elem> const& ptr);
auto reset() -> void;
[[nodiscard]]
auto const& get_parser() const noexcept
{
return parser_;
}
auto cancel_waiting() -> std::size_t;
// To be called exactly once to clean up state after a connection becomes unhealthy.
// Requests are canceled or returned to the waiting state to be re-sent to the server,
// depending on their configuration. After this function is called, prepare_write,
// commit_write and consume_next must not be called until a reset() happens.
// Otherwise, race conditions like the following might happen
// (see https://github.com/boostorg/redis/pull/309 and https://github.com/boostorg/redis/issues/181):
//
// - This function runs and cancels a request, then consume_next runs. It tries to access
// a request and adapter that might have been destroyed.
// - This function runs and returns a request to waiting, then prepare_write runs.
// It incorrectly sets the request state to staged, causing de synchronization between requests and responses.
void cancel_on_conn_lost();
[[nodiscard]]
auto get_write_buffer() const noexcept -> std::string_view
{
return std::string_view{write_buffer_}.substr(write_offset_);
}
[[nodiscard]]
auto get_read_buffer() noexcept -> read_buffer&
{
return read_buffer_;
}
[[nodiscard]]
auto get_prepared_read_buffer() noexcept -> read_buffer::span_type;
[[nodiscard]]
auto prepare_read() noexcept -> system::error_code;
void commit_read(std::size_t read_size);
[[nodiscard]]
auto get_read_buffer_size() const noexcept -> std::size_t;
void set_receive_adapter(any_adapter adapter);
[[nodiscard]]
auto get_usage() const noexcept -> usage
{
return usage_;
}
void set_config(config const& cfg);
private:
void commit_usage(bool is_push, read_buffer::consume_result res);
[[nodiscard]]
auto is_next_push(std::string_view data) const noexcept -> bool;
// Completes requests that don't expect a response
void release_push_requests();
[[nodiscard]]
consume_result consume_impl(system::error_code& ec);
read_buffer read_buffer_;
std::string write_buffer_;
std::size_t write_offset_{}; // how many bytes of the write buffer have been written?
std::deque<std::shared_ptr<elem>> reqs_;
resp3::parser parser_{};
bool on_push_ = false;
bool cancel_run_called_ = false;
usage usage_;
any_adapter receive_adapter_;
};
auto make_elem(request const& req, any_adapter adapter) -> std::shared_ptr<multiplexer::elem>;
} // namespace detail
} // namespace boost::redis
#endif // BOOST_REDIS_MULTIPLEXER_HPP

View File

@@ -0,0 +1,69 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_READ_BUFFER_HPP
#define BOOST_REDIS_READ_BUFFER_HPP
#include <boost/core/span.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <string_view>
#include <utility>
#include <vector>
namespace boost::redis::detail {
class read_buffer {
public:
using span_type = span<char>;
struct consume_result {
std::size_t consumed;
std::size_t rotated;
};
// See config.hpp for the meaning of these parameters.
struct config {
std::size_t read_buffer_append_size = 4096u;
std::size_t max_read_size = static_cast<std::size_t>(-1);
};
// Prepare the buffer to receive more data.
[[nodiscard]]
auto prepare() -> system::error_code;
[[nodiscard]]
auto get_prepared() noexcept -> span_type;
void commit(std::size_t read_size);
[[nodiscard]]
auto get_commited() const noexcept -> std::string_view;
void clear();
// Consumes committed data by rotating the remaining data to the
// front of the buffer.
auto consume(std::size_t size) -> consume_result;
void reserve(std::size_t n);
friend bool operator==(read_buffer const& lhs, read_buffer const& rhs);
friend bool operator!=(read_buffer const& lhs, read_buffer const& rhs);
void set_config(config const& cfg) noexcept { cfg_ = cfg; };
private:
config cfg_ = config{};
std::vector<char> buffer_;
std::size_t append_buf_begin_ = 0;
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_READ_BUFFER_HPP

View File

@@ -0,0 +1,97 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_READER_FSM_HPP
#define BOOST_REDIS_READER_FSM_HPP
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <cstddef>
namespace boost::redis::detail {
class read_buffer;
class reader_fsm {
public:
class action {
public:
enum class type
{
read_some,
notify_push_receiver,
done,
};
action(system::error_code ec) noexcept
: type_(type::done)
, ec_(ec)
{ }
static action read_some(std::chrono::steady_clock::duration timeout) { return {timeout}; }
static action notify_push_receiver(std::size_t bytes) { return {bytes}; }
type get_type() const { return type_; }
system::error_code error() const
{
BOOST_ASSERT(type_ == type::done);
return ec_;
}
std::chrono::steady_clock::duration timeout() const
{
BOOST_ASSERT(type_ == type::read_some);
return timeout_;
}
std::size_t push_size() const
{
BOOST_ASSERT(type_ == type::notify_push_receiver);
return push_size_;
}
private:
action(std::size_t push_size) noexcept
: type_(type::notify_push_receiver)
, push_size_(push_size)
{ }
action(std::chrono::steady_clock::duration t) noexcept
: type_(type::read_some)
, timeout_(t)
{ }
type type_;
union {
system::error_code ec_;
std::chrono::steady_clock::duration timeout_;
std::size_t push_size_{};
};
};
action resume(
connection_state& st,
std::size_t bytes_read,
system::error_code ec,
asio::cancellation_type_t cancel_state);
reader_fsm() = default;
private:
int resume_point_{0};
std::pair<consume_result, std::size_t> res_{consume_result::needs_more, 0u};
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_READER_FSM_HPP

View File

@@ -0,0 +1,58 @@
//
// Copyright (c) 2018-2026 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef BOOST_REDIS_RECEIVE_FSM_HPP
#define BOOST_REDIS_RECEIVE_FSM_HPP
#include <boost/asio/cancellation_type.hpp>
#include <boost/system/error_code.hpp>
// Sans-io algorithm for async_receive2, as a finite state machine
namespace boost::redis::detail {
struct connection_state;
struct receive_action {
enum class action_type
{
setup_cancellation, // Set up the cancellation types supported by the composed operation
wait, // Wait for a message to appear in the receive channel
drain_channel, // Empty the receive channel
immediate, // Call async_immediate
done, // Complete
};
action_type type;
system::error_code ec;
receive_action(action_type type) noexcept
: type{type}
{ }
receive_action(system::error_code ec) noexcept
: type{action_type::done}
, ec{ec}
{ }
};
class receive_fsm {
int resume_point_{0};
public:
receive_fsm() = default;
receive_action resume(
connection_state& st,
system::error_code ec,
asio::cancellation_type_t cancel_state);
};
} // namespace boost::redis::detail
#endif

View File

@@ -0,0 +1,251 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
* Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_REDIS_STREAM_HPP
#define BOOST_REDIS_REDIS_STREAM_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connect_fsm.hpp>
#include <boost/redis/detail/connect_params.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/basic_waitable_timer.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/ip/basic_resolver.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/local/stream_protocol.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/assert.hpp>
#include <boost/system/error_code.hpp>
#include <utility>
namespace boost {
namespace redis {
namespace detail {
template <class Executor>
class redis_stream {
asio::ssl::context ssl_ctx_;
asio::ip::basic_resolver<asio::ip::tcp, Executor> resolv_;
asio::ssl::stream<asio::basic_stream_socket<asio::ip::tcp, Executor>> stream_;
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
asio::basic_stream_socket<asio::local::stream_protocol, Executor> unix_socket_;
#endif
typename asio::steady_timer::template rebind_executor<Executor>::other timer_;
redis_stream_state st_;
void reset_stream() { stream_ = {resolv_.get_executor(), ssl_ctx_}; }
struct connect_op {
redis_stream& obj_;
connect_fsm fsm_;
connect_params params_;
template <class Self>
void execute_action(Self& self, connect_action act)
{
// Prevent use-after-move errors
auto& obj = this->obj_;
auto params = this->params_;
switch (act.type) {
case connect_action_type::unix_socket_close:
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
{
system::error_code ec;
obj.unix_socket_.close(ec);
(*this)(self, ec); // This is a sync action
}
#else
BOOST_ASSERT(false);
#endif
return;
case connect_action_type::unix_socket_connect:
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
obj.unix_socket_.async_connect(
params.addr.unix_socket(),
asio::cancel_after(obj.timer_, params.connect_timeout, std::move(self)));
#else
BOOST_ASSERT(false);
#endif
return;
case connect_action_type::tcp_resolve:
obj.resolv_.async_resolve(
params.addr.tcp_address().host,
params.addr.tcp_address().port,
asio::cancel_after(obj.timer_, params.resolve_timeout, std::move(self)));
return;
case connect_action_type::ssl_stream_reset:
obj.reset_stream();
// this action does not require yielding. Execute the next action immediately
(*this)(self);
return;
case connect_action_type::ssl_handshake:
obj.stream_.async_handshake(
asio::ssl::stream_base::client,
asio::cancel_after(obj.timer_, params.ssl_handshake_timeout, std::move(self)));
return;
case connect_action_type::done: self.complete(act.ec); break;
// Connect should use the specialized handler, where resolver results are available
case connect_action_type::tcp_connect:
default: BOOST_ASSERT(false);
}
}
// This overload will be used for connects
template <class Self>
void operator()(
Self& self,
system::error_code ec,
const asio::ip::tcp::endpoint& selected_endpoint)
{
auto act = fsm_.resume(
ec,
selected_endpoint,
obj_.st_,
self.get_cancellation_state().cancelled());
execute_action(self, act);
}
// This overload will be used for resolves
template <class Self>
void operator()(
Self& self,
system::error_code ec,
asio::ip::tcp::resolver::results_type endpoints)
{
auto act = fsm_.resume(ec, endpoints, obj_.st_, self.get_cancellation_state().cancelled());
if (act.type == connect_action_type::tcp_connect) {
auto& obj = this->obj_; // prevent use-after-move errors
asio::async_connect(
obj.stream_.next_layer(),
std::move(endpoints),
asio::cancel_after(obj.timer_, params_.connect_timeout, std::move(self)));
} else {
execute_action(self, act);
}
}
template <class Self>
void operator()(Self& self, system::error_code ec = {})
{
auto act = fsm_.resume(ec, obj_.st_, self.get_cancellation_state().cancelled());
execute_action(self, act);
}
};
public:
explicit redis_stream(Executor ex, asio::ssl::context&& ssl_ctx)
: ssl_ctx_{std::move(ssl_ctx)}
, resolv_{ex}
, stream_{ex, ssl_ctx_}
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
, unix_socket_{ex}
#endif
, timer_{std::move(ex)}
{ }
// Executor. Required to satisfy the AsyncStream concept
using executor_type = Executor;
executor_type get_executor() noexcept { return resolv_.get_executor(); }
// Accessors
const auto& get_ssl_context() const noexcept { return ssl_ctx_; }
bool is_open() const
{
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
if (st_.type == transport_type::unix_socket)
return unix_socket_.is_open();
#endif
return stream_.next_layer().is_open();
}
auto& next_layer() { return stream_; }
const auto& next_layer() const { return stream_; }
// I/O
template <class CompletionToken>
auto async_connect(const connect_params& params, buffered_logger& l, CompletionToken&& token)
{
this->st_.type = params.addr.type();
return asio::async_compose<CompletionToken, void(system::error_code)>(
connect_op{*this, connect_fsm{l}, params},
token);
}
// These functions should only be used with callbacks (e.g. within async_compose function bodies)
template <class ConstBufferSequence, class CompletionToken>
void async_write_some(const ConstBufferSequence& buffers, CompletionToken&& token)
{
switch (st_.type) {
case transport_type::tcp:
{
stream_.next_layer().async_write_some(buffers, std::forward<CompletionToken>(token));
break;
}
case transport_type::tcp_tls:
{
stream_.async_write_some(buffers, std::forward<CompletionToken>(token));
break;
}
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
case transport_type::unix_socket:
{
unix_socket_.async_write_some(buffers, std::forward<CompletionToken>(token));
break;
}
#endif
default: BOOST_ASSERT(false);
}
}
template <class MutableBufferSequence, class CompletionToken>
void async_read_some(const MutableBufferSequence& buffers, CompletionToken&& token)
{
switch (st_.type) {
case transport_type::tcp:
{
return stream_.next_layer().async_read_some(
buffers,
std::forward<CompletionToken>(token));
break;
}
case transport_type::tcp_tls:
{
return stream_.async_read_some(buffers, std::forward<CompletionToken>(token));
break;
}
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
case transport_type::unix_socket:
{
unix_socket_.async_read_some(buffers, std::forward<CompletionToken>(token));
break;
}
#endif
default: BOOST_ASSERT(false);
}
}
// Cancels resolve operations. Resolve operations don't support per-operation
// cancellation, but resolvers have a cancel() function. Resolve operations are
// in general blocking and run in a separate thread. cancel() has effect only
// if the operation hasn't started yet. Still, trying is better than nothing
void cancel_resolve() { resolv_.cancel(); }
};
} // namespace detail
} // namespace redis
} // namespace boost
#endif

View File

@@ -1,137 +0,0 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_RESOLVER_HPP
#define BOOST_REDIS_RESOLVER_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/detail/helper.hpp>
#include <boost/redis/error.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/steady_timer.hpp>
#include <string>
#include <chrono>
namespace boost::redis::detail
{
template <class Resolver>
struct resolve_op {
Resolver* resv_ = nullptr;
asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 2> order = {}
, system::error_code ec1 = {}
, asio::ip::tcp::resolver::results_type res = {}
, system::error_code ec2 = {})
{
BOOST_ASIO_CORO_REENTER (coro)
{
resv_->timer_.expires_after(resv_->timeout_);
BOOST_ASIO_CORO_YIELD
asio::experimental::make_parallel_group(
[this](auto token)
{
return resv_->resv_.async_resolve(resv_->addr_.host, resv_->addr_.port, token);
},
[this](auto token) { return resv_->timer_.async_wait(token);}
).async_wait(
asio::experimental::wait_for_one(),
std::move(self));
if (is_cancelled(self)) {
self.complete(asio::error::operation_aborted);
return;
}
switch (order[0]) {
case 0: {
// Resolver completed first.
resv_->results_ = res;
self.complete(ec1);
} break;
case 1: {
if (ec2) {
// Timer completed first with error, perhaps a
// cancellation going on.
self.complete(ec2);
} else {
// Timer completed first without an error, this is a
// resolve timeout.
self.complete(error::resolve_timeout);
}
} break;
default: BOOST_ASSERT(false);
}
}
}
};
template <class Executor>
class resolver {
public:
using timer_type =
asio::basic_waitable_timer<
std::chrono::steady_clock,
asio::wait_traits<std::chrono::steady_clock>,
Executor>;
resolver(Executor ex) : resv_{ex} , timer_{ex} {}
template <class CompletionToken>
auto async_resolve(CompletionToken&& token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(resolve_op<resolver>{this}, token, resv_);
}
std::size_t cancel(operation op)
{
switch (op) {
case operation::resolve:
case operation::all:
resv_.cancel();
timer_.cancel();
break;
default: /* ignore */;
}
return 0;
}
auto const& results() const noexcept
{ return results_;}
void set_config(config const& cfg)
{
addr_ = cfg.addr;
timeout_ = cfg.resolve_timeout;
}
private:
using resolver_type = asio::ip::basic_resolver<asio::ip::tcp, Executor>;
template <class> friend struct resolve_op;
resolver_type resv_;
timer_type timer_;
address addr_;
std::chrono::steady_clock::duration timeout_;
asio::ip::tcp::resolver::results_type results_;
};
} // boost::redis::detail
#endif // BOOST_REDIS_RESOLVER_HPP

View File

@@ -0,0 +1,29 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_RESP3_TYPE_TO_ERROR_HPP
#define BOOST_RESP3_TYPE_TO_ERROR_HPP
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/assert.hpp>
namespace boost::redis::detail {
inline error resp3_type_to_error(resp3::type t)
{
switch (t) {
case resp3::type::simple_error: return error::resp3_simple_error;
case resp3::type::blob_error: return error::resp3_blob_error;
case resp3::type::null: return error::resp3_null;
default: BOOST_ASSERT_MSG(false, "Unexpected data type."); return error::resp3_simple_error;
}
}
} // namespace boost::redis::detail
#endif // BOOST_REDIS_ADAPTER_RESULT_HPP

View File

@@ -0,0 +1,67 @@
//
// 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_RUN_FSM_HPP
#define BOOST_REDIS_RUN_FSM_HPP
#include <boost/redis/detail/connect_params.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/system/error_code.hpp>
// Sans-io algorithm for async_run, as a finite state machine
namespace boost::redis::detail {
// Forward decls
struct connection_state;
// What should we do next?
enum class run_action_type
{
done, // Call the final handler
immediate, // Call asio::async_immediate
connect, // Transport connection establishment
sentinel_resolve, // Contact Sentinels to resolve the master's address
parallel_group, // Run the reader, writer and friends
cancel_receive, // Cancel the receiver channel
wait_for_reconnection, // Sleep for the reconnection period
};
struct run_action {
run_action_type type;
system::error_code ec;
run_action(run_action_type type) noexcept
: type{type}
{ }
run_action(system::error_code ec) noexcept
: type{run_action_type::done}
, ec{ec}
{ }
};
class run_fsm {
int resume_point_{0};
system::error_code stored_ec_;
public:
run_fsm() = default;
run_action resume(
connection_state& st,
system::error_code ec,
asio::cancellation_type_t cancel_state);
};
connect_params make_run_connect_params(const connection_state& st);
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -1,276 +0,0 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_RUNNER_HPP
#define BOOST_REDIS_RUNNER_HPP
#include <boost/redis/detail/health_checker.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/response.hpp>
#include <boost/redis/detail/helper.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/operation.hpp>
#include <boost/redis/detail/connector.hpp>
#include <boost/redis/detail/resolver.hpp>
#include <boost/redis/detail/handshaker.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/steady_timer.hpp>
#include <string>
#include <memory>
#include <chrono>
namespace boost::redis::detail
{
template <class Runner, class Connection, class Logger>
struct hello_op {
Runner* runner_ = nullptr;
Connection* conn_ = nullptr;
Logger logger_;
asio::coroutine coro_{};
template <class Self>
void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
{
BOOST_ASIO_CORO_REENTER (coro_)
{
runner_->hello_req_.clear();
if (runner_->hello_resp_.has_value())
runner_->hello_resp_.value().clear();
runner_->add_hello();
BOOST_ASIO_CORO_YIELD
conn_->async_exec(runner_->hello_req_, runner_->hello_resp_, std::move(self));
logger_.on_hello(ec, runner_->hello_resp_);
if (ec || runner_->has_error_in_response() || is_cancelled(self)) {
logger_.trace("hello-op: error/canceled. Exiting ...");
conn_->cancel(operation::run);
self.complete(!!ec ? ec : asio::error::operation_aborted);
return;
}
self.complete({});
}
}
};
template <class Runner, class Connection, class Logger>
class runner_op {
private:
Runner* runner_ = nullptr;
Connection* conn_ = nullptr;
Logger logger_;
asio::coroutine coro_{};
public:
runner_op(Runner* runner, Connection* conn, Logger l)
: runner_{runner}
, conn_{conn}
, logger_{l}
{}
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 3> order = {}
, system::error_code ec0 = {}
, system::error_code ec1 = {}
, system::error_code ec2 = {}
, std::size_t = 0)
{
BOOST_ASIO_CORO_REENTER (coro_)
{
BOOST_ASIO_CORO_YIELD
asio::experimental::make_parallel_group(
[this](auto token) { return runner_->async_run_all(*conn_, logger_, token); },
[this](auto token) { return runner_->health_checker_.async_check_health(*conn_, logger_, token); },
[this](auto token) { return runner_->async_hello(*conn_, logger_, token); }
).async_wait(
asio::experimental::wait_for_all(),
std::move(self));
logger_.on_runner(ec0, ec1, ec2);
if (is_cancelled(self)) {
self.complete(asio::error::operation_aborted);
return;
}
if (ec0 == error::connect_timeout || ec0 == error::resolve_timeout) {
self.complete(ec0);
return;
}
if (order[0] == 2 && !!ec2) {
self.complete(ec2);
return;
}
if (order[0] == 1 && ec1 == error::pong_timeout) {
self.complete(ec1);
return;
}
self.complete(ec0);
}
}
};
template <class Runner, class Connection, class Logger>
struct run_all_op {
Runner* runner_ = nullptr;
Connection* conn_ = nullptr;
Logger logger_;
asio::coroutine coro_{};
template <class Self>
void operator()(Self& self, system::error_code ec = {}, std::size_t = 0)
{
BOOST_ASIO_CORO_REENTER (coro_)
{
BOOST_ASIO_CORO_YIELD
runner_->resv_.async_resolve(std::move(self));
logger_.on_resolve(ec, runner_->resv_.results());
BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);)
BOOST_ASIO_CORO_YIELD
runner_->ctor_.async_connect(conn_->next_layer().next_layer(), runner_->resv_.results(), std::move(self));
logger_.on_connect(ec, runner_->ctor_.endpoint());
BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);)
if (conn_->use_ssl()) {
BOOST_ASIO_CORO_YIELD
runner_->hsher_.async_handshake(conn_->next_layer(), std::move(self));
logger_.on_ssl_handshake(ec);
BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);)
}
BOOST_ASIO_CORO_YIELD
conn_->async_run_lean(runner_->cfg_, logger_, std::move(self));
BOOST_REDIS_CHECK_OP0(;)
self.complete(ec);
}
}
};
template <class Executor>
class runner {
public:
runner(Executor ex, config cfg)
: resv_{ex}
, ctor_{ex}
, hsher_{ex}
, health_checker_{ex}
, cfg_{cfg}
{ }
std::size_t cancel(operation op)
{
resv_.cancel(op);
ctor_.cancel(op);
hsher_.cancel(op);
health_checker_.cancel(op);
return 0U;
}
void set_config(config const& cfg)
{
cfg_ = cfg;
resv_.set_config(cfg);
ctor_.set_config(cfg);
hsher_.set_config(cfg);
health_checker_.set_config(cfg);
}
template <class Connection, class Logger, class CompletionToken>
auto async_run(Connection& conn, Logger l, CompletionToken token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(runner_op<runner, Connection, Logger>{this, &conn, l}, token, conn);
}
config const& get_config() const noexcept {return cfg_;}
private:
using resolver_type = resolver<Executor>;
using connector_type = connector<Executor>;
using handshaker_type = detail::handshaker<Executor>;
using health_checker_type = health_checker<Executor>;
using timer_type = typename connector_type::timer_type;
template <class, class, class> friend struct run_all_op;
template <class, class, class> friend class runner_op;
template <class, class, class> friend struct hello_op;
template <class Connection, class Logger, class CompletionToken>
auto async_run_all(Connection& conn, Logger l, CompletionToken token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(run_all_op<runner, Connection, Logger>{this, &conn, l}, token, conn);
}
template <class Connection, class Logger, class CompletionToken>
auto async_hello(Connection& conn, Logger l, CompletionToken token)
{
return asio::async_compose
< CompletionToken
, void(system::error_code)
>(hello_op<runner, Connection, Logger>{this, &conn, l}, token, conn);
}
void add_hello()
{
if (!cfg_.username.empty() && !cfg_.password.empty() && !cfg_.clientname.empty())
hello_req_.push("HELLO", "3", "AUTH", cfg_.username, cfg_.password, "SETNAME", cfg_.clientname);
else if (cfg_.username.empty() && cfg_.password.empty() && cfg_.clientname.empty())
hello_req_.push("HELLO", "3");
else if (cfg_.clientname.empty())
hello_req_.push("HELLO", "3", "AUTH", cfg_.username, cfg_.password);
else
hello_req_.push("HELLO", "3", "SETNAME", cfg_.clientname);
if (cfg_.database_index && cfg_.database_index.value() != 0)
hello_req_.push("SELECT", cfg_.database_index.value());
}
bool has_error_in_response() const noexcept
{
if (!hello_resp_.has_value())
return true;
auto f = [](auto const& e)
{
switch (e.data_type) {
case resp3::type::simple_error:
case resp3::type::blob_error: return true;
default: return false;
}
};
return std::any_of(std::cbegin(hello_resp_.value()), std::cend(hello_resp_.value()), f);
}
resolver_type resv_;
connector_type ctor_;
handshaker_type hsher_;
health_checker_type health_checker_;
request hello_req_;
generic_response hello_resp_;
config cfg_;
};
} // boost::redis::detail
#endif // BOOST_REDIS_RUNNER_HPP

View File

@@ -0,0 +1,93 @@
//
// 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_SENTINEL_RESOLVE_FSM_HPP
#define BOOST_REDIS_SENTINEL_RESOLVE_FSM_HPP
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connect_params.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/assert.hpp>
#include <boost/system/error_code.hpp>
// Sans-io algorithm for async_sentinel_resolve, as a finite state machine
namespace boost::redis::detail {
// Forward decls
struct connection_state;
class sentinel_action {
public:
enum class type
{
done, // Call the final handler
connect, // Transport connection establishment
request, // Send the Sentinel request
};
sentinel_action(system::error_code ec) noexcept
: type_(type::done)
, ec_(ec)
{ }
sentinel_action(const address& addr) noexcept
: type_(type::connect)
, connect_(&addr)
{ }
static sentinel_action request() { return {type::request}; }
type get_type() const { return type_; }
[[nodiscard]]
system::error_code error() const
{
BOOST_ASSERT(type_ == type::done);
return ec_;
}
const address& connect_addr() const
{
BOOST_ASSERT(type_ == type::connect);
return *connect_;
}
private:
type type_;
union {
system::error_code ec_;
const address* connect_;
};
sentinel_action(type type) noexcept
: type_(type)
{ }
};
class sentinel_resolve_fsm {
int resume_point_{0};
std::size_t idx_{0u};
public:
sentinel_resolve_fsm() = default;
sentinel_action resume(
connection_state& st,
system::error_code ec,
asio::cancellation_type_t cancel_state);
};
connect_params make_sentinel_connect_params(const config& cfg, const address& sentinel_addr);
any_adapter make_sentinel_adapter(connection_state& st);
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -0,0 +1,35 @@
//
// 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_SUBSCRIPTION_TRACKER_HPP
#define BOOST_REDIS_SUBSCRIPTION_TRACKER_HPP
#include <set>
#include <string>
namespace boost::redis {
class request;
namespace detail {
class subscription_tracker {
std::set<std::string> channels_;
std::set<std::string> pchannels_;
public:
subscription_tracker() = default;
void clear();
void commit_changes(const request& req);
void compose_subscribe_request(request& to) const;
};
} // namespace detail
} // namespace boost::redis
#endif

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -7,40 +7,39 @@
#ifndef BOOST_REDIS_WRITE_HPP
#define BOOST_REDIS_WRITE_HPP
#include <boost/asio/write.hpp>
#include <boost/redis/request.hpp>
#include <boost/asio/write.hpp>
namespace boost::redis::detail {
/** \brief Writes a request synchronously.
* \ingroup low-level-api
/** @brief Writes a request synchronously.
*
* \param stream Stream to write the request to.
* \param req Request to write.
* @param stream Stream to write the request to.
* @param req Request to write.
*/
template<class SyncWriteStream>
template <class SyncWriteStream>
auto write(SyncWriteStream& stream, request const& req)
{
return asio::write(stream, asio::buffer(req.payload()));
}
template<class SyncWriteStream>
template <class SyncWriteStream>
auto write(SyncWriteStream& stream, request const& req, system::error_code& ec)
{
return asio::write(stream, asio::buffer(req.payload()), ec);
}
/** \brief Writes a request asynchronously.
* \ingroup low-level-api
/** @brief Writes a request asynchronously.
*
* \param stream Stream to write the request to.
* \param req Request to write.
* \param token Asio completion token.
* @param stream Stream to write the request to.
* @param req Request to write.
* @param token Asio completion token.
*/
template<
template <
class AsyncWriteStream,
class CompletionToken = asio::default_completion_token_t<typename AsyncWriteStream::executor_type>
>
class CompletionToken =
asio::default_completion_token_t<typename AsyncWriteStream::executor_type> >
auto async_write(
AsyncWriteStream& stream,
request const& req,
@@ -50,6 +49,6 @@ auto async_write(
return asio::async_write(stream, asio::buffer(req.payload()), token);
}
} // boost::redis::detail
} // namespace boost::redis::detail
#endif // BOOST_REDIS_WRITE_HPP
#endif // BOOST_REDIS_WRITE_HPP

View File

@@ -0,0 +1,92 @@
//
// 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_WRITER_FSM_HPP
#define BOOST_REDIS_WRITER_FSM_HPP
#include <boost/asio/cancellation_type.hpp>
#include <boost/assert.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <cstddef>
// Sans-io algorithm for the writer task, as a finite state machine
namespace boost::redis::detail {
// Forward decls
struct connection_state;
// What should we do next?
enum class writer_action_type
{
done, // Call the final handler
write_some, // Issue a write on the stream
wait, // Wait until there is data to be written
};
class writer_action {
writer_action_type type_;
union {
system::error_code ec_;
std::chrono::steady_clock::duration timeout_;
};
writer_action(writer_action_type type, std::chrono::steady_clock::duration t) noexcept
: type_{type}
, timeout_{t}
{ }
public:
writer_action_type type() const { return type_; }
writer_action(system::error_code ec) noexcept
: type_{writer_action_type::done}
, ec_{ec}
{ }
static writer_action write_some(std::chrono::steady_clock::duration timeout)
{
return {writer_action_type::write_some, timeout};
}
static writer_action wait(std::chrono::steady_clock::duration timeout)
{
return {writer_action_type::wait, timeout};
}
system::error_code error() const
{
BOOST_ASSERT(type_ == writer_action_type::done);
return ec_;
}
std::chrono::steady_clock::duration timeout() const
{
BOOST_ASSERT(type_ == writer_action_type::write_some || type_ == writer_action_type::wait);
return timeout_;
}
};
class writer_fsm {
int resume_point_{0};
public:
writer_fsm() = default;
writer_action resume(
connection_state& st,
system::error_code ec,
std::size_t bytes_written,
asio::cancellation_type_t cancel_state);
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -11,9 +11,7 @@
namespace boost::redis {
/** \brief Generic errors.
* \ingroup high-level-api
*/
/// Generic errors.
enum class error
{
/// Invalid RESP3 type.
@@ -70,33 +68,70 @@ enum class error
/// Connect timeout
connect_timeout,
/// Connect timeout
/// The server didn't answer the health checks on time and didn't send any data during the health check period.
pong_timeout,
/// SSL handshake timeout
ssl_handshake_timeout,
/// Can't receive push synchronously without blocking
/// (Deprecated) Can't receive push synchronously without blocking
sync_receive_push_failed,
/// Incompatible node depth.
incompatible_node_depth,
/// The setup request sent during connection establishment failed (the name is historical).
resp3_hello,
/// The configuration specified a UNIX socket address, but UNIX sockets are not supported by the system.
unix_sockets_unsupported,
/// The configuration specified UNIX sockets with SSL, which is not supported.
unix_sockets_ssl_unsupported,
/// Reading data from the socket would exceed the maximum size allowed of the read buffer.
exceeds_maximum_read_buffer_size,
/// Timeout while writing data to the server.
write_timeout,
/// The configuration specified UNIX sockets with Sentinel, which is not supported.
sentinel_unix_sockets_unsupported,
/// No Sentinel could be used to obtain the address of the Redis server.
/// Sentinels might be unreachable, have authentication misconfigured or may not know about
/// the configured master. Turn logging on for details.
sentinel_resolve_failed,
/// The contacted server is not a master as expected.
/// This is likely a transient failure caused by a Sentinel failover in progress.
role_check_failed,
/// Expects a RESP3 string, but got a different data type.
expects_resp3_string,
/// Expects a RESP3 array, but got a different data type.
expects_resp3_array,
/// A @ref basic_connection::async_receive2 operation is already running.
/// Only one of such operations might be running at any point in time.
already_running,
};
/** \internal
* \brief Creates a error_code object from an error.
* \param e Error code.
* \ingroup any
/**
* @brief Creates a error_code object from an error.
*
* @param e Error code.
*/
auto make_error_code(error e) -> system::error_code;
} // boost::redis
} // namespace boost::redis
namespace std {
template<>
struct is_error_code_enum<::boost::redis::error> : std::true_type {};
template <>
struct is_error_code_enum<::boost::redis::error> : std::true_type { };
} // std
} // namespace std
#endif // BOOST_REDIS_ERROR_HPP
#endif // BOOST_REDIS_ERROR_HPP

View File

@@ -1,5 +1,5 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -13,30 +13,27 @@
#include <tuple>
#include <type_traits>
namespace boost::redis
{
namespace boost::redis {
/** @brief Type used to ignore responses.
* @ingroup high-level-api
*
* For example
* For example:
*
* @code
* response<ignore_t, std::string, ignore_t> resp;
* @endcode
*
* will ignore the first and third responses. RESP3 errors won't be
* This will ignore the first and third responses. RESP3 errors won't be
* ignore but will cause `async_exec` to complete with an error.
*/
using ignore_t = std::decay_t<decltype(std::ignore)>;
/** @brief Global ignore object.
* @ingroup high-level-api
*
* Can be used to ignore responses to a request
* Can be used to ignore responses to a request. For example:
*
* @code
* conn->async_exec(req, ignore, ...);
* co_await conn.async_exec(req, ignore);
* @endcode
*
* RESP3 errors won't be ignore but will cause `async_exec` to
@@ -44,6 +41,6 @@ using ignore_t = std::decay_t<decltype(std::ignore)>;
*/
extern ignore_t ignore;
} // boost::redis
} // namespace boost::redis
#endif // BOOST_REDIS_IGNORE_HPP
#endif // BOOST_REDIS_IGNORE_HPP

View File

@@ -0,0 +1,218 @@
//
// 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 <boost/redis/config.hpp>
#include <boost/redis/detail/connect_fsm.hpp>
#include <boost/redis/detail/coroutine.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/impl/log_utils.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/assert.hpp>
#include <string>
namespace boost::redis::detail {
// Logging
inline void format_tcp_endpoint(const asio::ip::tcp::endpoint& ep, std::string& to)
{
// This formatting is inspired by Asio's endpoint operator<<
const auto& addr = ep.address();
if (addr.is_v6())
to += '[';
to += addr.to_string();
if (addr.is_v6())
to += ']';
to += ':';
to += std::to_string(ep.port());
}
template <>
struct log_traits<asio::ip::tcp::endpoint> {
static inline void log(std::string& to, const asio::ip::tcp::endpoint& value)
{
format_tcp_endpoint(value, to);
}
};
template <>
struct log_traits<asio::ip::tcp::resolver::results_type> {
static inline void log(std::string& to, const asio::ip::tcp::resolver::results_type& value)
{
auto iter = value.cbegin();
auto end = value.cend();
if (iter != end) {
format_tcp_endpoint(iter->endpoint(), to);
++iter;
for (; iter != end; ++iter) {
to += ", ";
format_tcp_endpoint(iter->endpoint(), to);
}
}
}
};
inline system::error_code translate_timeout_error(
system::error_code io_ec,
asio::cancellation_type_t cancel_state,
error code_if_cancelled)
{
// Translates cancellations and timeout errors into a single error_code.
// - Cancellation state set, and an I/O error: the entire operation was cancelled.
// The I/O code (probably operation_aborted) is appropriate.
// - Cancellation state set, and no I/O error: same as above, but the cancellation
// arrived after the operation completed and before the handler was called. Set the code here.
// - No cancellation state set, I/O error set to operation_aborted: since we use cancel_after,
// this means a timeout.
// - Otherwise, respect the I/O error.
if ((cancel_state & asio::cancellation_type_t::terminal) != asio::cancellation_type_t::none) {
return io_ec ? io_ec : asio::error::operation_aborted;
}
return io_ec == asio::error::operation_aborted ? code_if_cancelled : io_ec;
}
connect_action connect_fsm::resume(
system::error_code ec,
const asio::ip::tcp::resolver::results_type& resolver_results,
redis_stream_state& st,
asio::cancellation_type_t cancel_state)
{
// Translate error codes
ec = translate_timeout_error(ec, cancel_state, error::resolve_timeout);
// Log it
if (ec) {
log_info(*lgr_, "Connect: hostname resolution failed: ", ec);
} else {
log_debug(*lgr_, "Connect: hostname resolution results: ", resolver_results);
}
// Delegate to the regular resume function
return resume(ec, st, cancel_state);
}
connect_action connect_fsm::resume(
system::error_code ec,
const asio::ip::tcp::endpoint& selected_endpoint,
redis_stream_state& st,
asio::cancellation_type_t cancel_state)
{
// Translate error codes
ec = translate_timeout_error(ec, cancel_state, error::connect_timeout);
// Log it
if (ec) {
log_info(*lgr_, "Connect: TCP connect failed: ", ec);
} else {
log_debug(*lgr_, "Connect: TCP connect succeeded. Selected endpoint: ", selected_endpoint);
}
// Delegate to the regular resume function
return resume(ec, st, cancel_state);
}
connect_action connect_fsm::resume(
system::error_code ec,
redis_stream_state& st,
asio::cancellation_type_t cancel_state)
{
switch (resume_point_) {
BOOST_REDIS_CORO_INITIAL
if (st.type == transport_type::unix_socket) {
// Reset the socket, to discard any previous state. Ignore any errors
BOOST_REDIS_YIELD(resume_point_, 1, connect_action_type::unix_socket_close)
// Connect to the socket
BOOST_REDIS_YIELD(resume_point_, 2, connect_action_type::unix_socket_connect)
// Fix error codes. If we were cancelled and the code is operation_aborted,
// it is because per-operation cancellation was activated. If we were not cancelled
// but the operation failed with operation_aborted, it's a timeout.
// Also check for cancellations that didn't cause a failure
ec = translate_timeout_error(ec, cancel_state, error::connect_timeout);
// Log it
if (ec) {
log_info(*lgr_, "Connect: UNIX socket connect failed: ", ec);
} else {
log_debug(*lgr_, "Connect: UNIX socket connect succeeded");
}
// If this failed, we can't continue
if (ec) {
return ec;
}
// Done
return system::error_code();
} else {
// ssl::stream doesn't support being re-used. If we're to use
// TLS and the stream has been used, re-create it.
// Must be done before anything else is done on the stream.
// We don't need to close the TCP socket if using plaintext TCP
// because range-connect closes open sockets, while individual connect doesn't
if (st.type == transport_type::tcp_tls && st.ssl_stream_used) {
BOOST_REDIS_YIELD(resume_point_, 3, connect_action_type::ssl_stream_reset)
}
// Resolve names. The continuation needs access to the returned
// endpoints, and is a specialized resume() that will call this function
BOOST_REDIS_YIELD(resume_point_, 4, connect_action_type::tcp_resolve)
// If this failed, we can't continue (error code translation already performed here)
if (ec) {
return ec;
}
// Now connect to the endpoints returned by the resolver.
// This has a specialized resume(), too
BOOST_REDIS_YIELD(resume_point_, 5, connect_action_type::tcp_connect)
// If this failed, we can't continue (error code translation already performed here)
if (ec) {
return ec;
}
if (st.type == transport_type::tcp_tls) {
// Mark the SSL stream as used
st.ssl_stream_used = true;
// Perform the TLS handshake
BOOST_REDIS_YIELD(resume_point_, 6, connect_action_type::ssl_handshake)
// Translate error codes
ec = translate_timeout_error(ec, cancel_state, error::ssl_handshake_timeout);
// Log it
if (ec) {
log_info(*lgr_, "Connect: SSL handshake failed: ", ec);
} else {
log_debug(*lgr_, "Connect: SSL handshake succeeded");
}
// If this failed, we can't continue
if (ec) {
return ec;
}
}
// Done
return system::error_code();
}
}
BOOST_ASSERT(false);
return system::error_code();
}
} // namespace boost::redis::detail

View File

@@ -1,39 +1,62 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/redis/impl/log_to_file.hpp>
#include <cstddef>
#include <cstdio>
#include <string_view>
#include <utility>
namespace boost::redis {
connection::connection(
executor_type ex,
asio::ssl::context::method method,
std::size_t max_read_size)
: impl_{ex, method, max_read_size}
logger detail::make_stderr_logger(logger::level lvl, std::string prefix)
{
return logger(lvl, [prefix = std::move(prefix)](logger::level, std::string_view msg) {
log_to_file(stderr, msg, prefix.c_str());
});
}
connection::connection(executor_type ex, asio::ssl::context ctx, logger lgr)
: impl_{std::move(ex), std::move(ctx), std::move(lgr)}
{ }
connection::connection(
asio::io_context& ioc,
asio::ssl::context::method method,
std::size_t max_read_size)
: impl_{ioc.get_executor(), method, max_read_size}
{ }
void
connection::async_run_impl(
void connection::async_run_impl(
config const& cfg,
logger l,
logger&& l,
asio::any_completion_handler<void(boost::system::error_code)> token)
{
impl_.async_run(cfg, l, std::move(token));
// Avoid calling the basic_connection::async_run overload taking a logger
// because it generates deprecated messages when building this file
impl_.set_stderr_logger(l.lvl, cfg);
impl_.async_run(cfg, std::move(token));
}
void connection::cancel(operation op)
void connection::async_run_impl(
config const& cfg,
asio::any_completion_handler<void(boost::system::error_code)> token)
{
impl_.cancel(op);
impl_.async_run(cfg, std::move(token));
}
} // namespace boost::redis
void connection::async_exec_impl(
request const& req,
any_adapter&& adapter,
asio::any_completion_handler<void(boost::system::error_code, std::size_t)> token)
{
impl_.async_exec(req, std::move(adapter), std::move(token));
}
void connection::async_receive2_impl(
asio::any_completion_handler<void(boost::system::error_code)> token)
{
impl_.async_receive2(std::move(token));
}
void connection::cancel(operation op) { impl_.cancel(op); }
} // namespace boost::redis

View File

@@ -1,65 +1,92 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/error.hpp>
#include <boost/assert.hpp>
namespace boost::redis {
namespace detail {
struct error_category_impl : system::error_category {
virtual ~error_category_impl() = default;
auto name() const noexcept -> char const* override
{
return "boost.redis";
}
auto name() const noexcept -> char const* override { return "boost.redis"; }
auto message(int ev) const -> std::string override
{
switch(static_cast<error>(ev)) {
case error::invalid_data_type: return "Invalid resp3 type.";
case error::not_a_number: return "Can't convert string to number (maybe forgot to upgrade to RESP3?).";
case error::exceeeds_max_nested_depth: return "Exceeds the maximum number of nested responses.";
case error::unexpected_bool_value: return "Unexpected bool value.";
case error::empty_field: return "Expected field value is empty.";
case error::expects_resp3_simple_type: return "Expects a resp3 simple type.";
case error::expects_resp3_aggregate: return "Expects resp3 aggregate.";
case error::expects_resp3_map: return "Expects resp3 map.";
case error::expects_resp3_set: return "Expects resp3 set.";
case error::nested_aggregate_not_supported: return "Nested aggregate not_supported.";
case error::resp3_simple_error: return "Got RESP3 simple-error.";
case error::resp3_blob_error: return "Got RESP3 blob-error.";
case error::incompatible_size: return "Aggregate container has incompatible size.";
case error::not_a_double: return "Not a double.";
case error::resp3_null: return "Got RESP3 null.";
case error::not_connected: return "Not connected.";
case error::resolve_timeout: return "Resolve timeout.";
case error::connect_timeout: return "Connect timeout.";
case error::pong_timeout: return "Pong timeout.";
case error::ssl_handshake_timeout: return "SSL handshake timeout.";
case error::sync_receive_push_failed: return "Can't receive server push synchronously without blocking.";
case error::incompatible_node_depth: return "Incompatible node depth.";
default: BOOST_ASSERT(false); return "Boost.Redis error.";
switch (static_cast<error>(ev)) {
case error::invalid_data_type: return "Invalid resp3 type.";
case error::not_a_number:
return "Can't convert string to number (maybe forgot to upgrade to RESP3?).";
case error::exceeeds_max_nested_depth:
return "Exceeds the maximum number of nested responses.";
case error::unexpected_bool_value: return "Unexpected bool value.";
case error::empty_field: return "Expected field value is empty.";
case error::expects_resp3_simple_type: return "Expects a resp3 simple type.";
case error::expects_resp3_aggregate: return "Expects resp3 aggregate.";
case error::expects_resp3_map: return "Expects resp3 map.";
case error::expects_resp3_set: return "Expects resp3 set.";
case error::nested_aggregate_not_supported: return "Nested aggregate not_supported.";
case error::resp3_simple_error: return "Got RESP3 simple-error.";
case error::resp3_blob_error: return "Got RESP3 blob-error.";
case error::incompatible_size: return "Aggregate container has incompatible size.";
case error::not_a_double: return "Not a double.";
case error::resp3_null: return "Got RESP3 null.";
case error::not_connected: return "Not connected.";
case error::resolve_timeout: return "Resolve timeout.";
case error::connect_timeout: return "Connect timeout.";
case error::pong_timeout: return "Pong timeout.";
case error::ssl_handshake_timeout: return "SSL handshake timeout.";
case error::sync_receive_push_failed:
return "Can't receive server push synchronously without blocking.";
case error::incompatible_node_depth: return "Incompatible node depth.";
case error::resp3_hello:
return "The server response to the setup request sent during connection establishment "
"contains an error.";
case error::unix_sockets_unsupported:
return "The configuration specified a UNIX socket address, but UNIX sockets are not "
"supported by the system.";
case error::unix_sockets_ssl_unsupported:
return "The configuration specified UNIX sockets with SSL, which is not supported.";
case error::exceeds_maximum_read_buffer_size:
return "Reading data from the socket would exceed the maximum size allowed of the read "
"buffer.";
case error::write_timeout: return "Timeout while writing data to the server.";
case error::sentinel_unix_sockets_unsupported:
return "The configuration specified UNIX sockets with Sentinel, which is not "
"supported.";
case error::sentinel_resolve_failed:
return "No Sentinel could be used to obtain the address of the Redis server.";
case error::role_check_failed:
return "The contacted server does not have the expected role. "
"This is likely a transient failure caused by a Sentinel failover in progress.";
case error::expects_resp3_string:
return "Expects a RESP3 string, but got a different data type.";
case error::expects_resp3_array:
return "Expects a RESP3 array, but got a different data type.";
case error::already_running:
return "An async_receive2 operation is already running. Only one of such operations "
"might be running at any point in time.";
default: BOOST_ASSERT(false); return "Boost.Redis error.";
}
}
};
auto category() -> system::error_category const&
{
static error_category_impl instance;
return instance;
static error_category_impl instance;
return instance;
}
} // detail
} // namespace detail
auto make_error_code(error e) -> system::error_code
{
return system::error_code{static_cast<int>(e), detail::category()};
return system::error_code{static_cast<int>(e), detail::category()};
}
} // boost::redis::detail
} // namespace boost::redis

View File

@@ -0,0 +1,98 @@
//
// 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_EXEC_FSM_IPP
#define BOOST_REDIS_EXEC_FSM_IPP
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/coroutine.hpp>
#include <boost/redis/detail/exec_fsm.hpp>
#include <boost/redis/request.hpp>
#include <boost/asio/error.hpp>
#include <boost/assert.hpp>
namespace boost::redis::detail {
inline bool is_partial_or_terminal_cancel(asio::cancellation_type_t type)
{
return !!(type & (asio::cancellation_type_t::partial | asio::cancellation_type_t::terminal));
}
inline bool is_total_cancel(asio::cancellation_type_t type)
{
return !!(type & asio::cancellation_type_t::total);
}
exec_action exec_fsm::resume(
bool connection_is_open,
connection_state& st,
asio::cancellation_type_t cancel_state)
{
switch (resume_point_) {
BOOST_REDIS_CORO_INITIAL
// Check whether the user wants to wait for the connection to
// be established.
if (elem_->get_request().get_config().cancel_if_not_connected && !connection_is_open) {
BOOST_REDIS_YIELD(resume_point_, 1, exec_action_type::immediate)
elem_.reset(); // Deallocate memory before finalizing
return system::error_code(error::not_connected);
}
// No more immediate errors. Set up the supported cancellation types.
// This is required to get partial and total cancellations.
// This is a potentially allocating operation, so do it as late as we can.
BOOST_REDIS_YIELD(resume_point_, 2, exec_action_type::setup_cancellation)
// Add the request to the multiplexer
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),
// this is a no-op. Since this is sync, no cancellation can happen here.
BOOST_REDIS_YIELD(resume_point_, 3, exec_action_type::notify_writer)
while (true) {
// Wait until we get notified. This will return once the request completes,
// or upon any kind of cancellation
BOOST_REDIS_YIELD(resume_point_, 4, exec_action_type::wait_for_response)
// 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 (!elem_->get_error())
st.tracker.commit_changes(elem_->get_request());
// Deallocate memory before finalizing
exec_action act{elem_->get_error(), elem_->get_read_size()};
elem_.reset();
return act;
}
// Total cancellation can only be handled if the request hasn't been sent yet.
// Partial and terminal cancellation can always be served
if (
(is_total_cancel(cancel_state) && elem_->is_waiting()) ||
is_partial_or_terminal_cancel(cancel_state)) {
st.mpx.cancel(elem_);
elem_.reset(); // Deallocate memory before finalizing
return exec_action{asio::error::operation_aborted};
}
}
}
// We should never get here
BOOST_ASSERT(false);
return exec_action{system::error_code()};
}
} // namespace boost::redis::detail
#endif

View File

@@ -0,0 +1,95 @@
//
// 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_EXEC_ONE_FSM_IPP
#define BOOST_REDIS_EXEC_ONE_FSM_IPP
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/detail/coroutine.hpp>
#include <boost/redis/detail/exec_one_fsm.hpp>
#include <boost/redis/detail/read_buffer.hpp>
#include <boost/redis/impl/is_terminal_cancel.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/parser.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/assert.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
namespace boost::redis::detail {
exec_one_action exec_one_fsm::resume(
read_buffer& buffer,
system::error_code ec,
std::size_t bytes_transferred,
asio::cancellation_type_t cancel_state)
{
switch (resume_point_) {
BOOST_REDIS_CORO_INITIAL
// Send the request to the server
BOOST_REDIS_YIELD(resume_point_, 1, exec_one_action_type::write)
// Errors and cancellations
if (is_terminal_cancel(cancel_state))
return system::error_code{asio::error::operation_aborted};
if (ec)
return ec;
// If the request didn't expect any response, we're done
if (remaining_responses_ == 0u)
return system::error_code{};
// Read responses until we're done
buffer.clear();
while (true) {
// Prepare the buffer to read some data
ec = buffer.prepare();
if (ec)
return ec;
// Read data
BOOST_REDIS_YIELD(resume_point_, 2, exec_one_action_type::read_some)
// Errors and cancellations
if (is_terminal_cancel(cancel_state))
return system::error_code{asio::error::operation_aborted};
if (ec)
return ec;
// Commit the data into the buffer
buffer.commit(bytes_transferred);
// Consume the data until we run out or all the responses have been read
while (resp3::parse(parser_, buffer.get_commited(), adapter_, ec)) {
// Check for errors
if (ec)
return ec;
// We've finished parsing a response
buffer.consume(parser_.get_consumed());
parser_.reset();
// When no more responses remain, we're done.
// Don't read ahead, even if more data is available
if (--remaining_responses_ == 0u)
return system::error_code{};
}
}
}
BOOST_ASSERT(false);
return system::error_code();
}
} // namespace boost::redis::detail
#endif

View File

@@ -0,0 +1,248 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Nikolai Vladimirov (nvladimirov.work@gmail.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 <boost/redis/resp3/flat_tree.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/tree.hpp>
#include <boost/assert.hpp>
#include <boost/throw_exception.hpp>
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <stdexcept>
#include <string_view>
namespace boost::redis::resp3 {
namespace detail {
// Updates string views by performing pointer arithmetic
inline void rebase_strings(view_tree& nodes, const char* old_base, const char* new_base)
{
for (auto& nd : nodes) {
if (!nd.value.empty()) {
const auto offset = nd.value.data() - old_base;
BOOST_ASSERT(offset >= 0);
nd.value = {new_base + offset, nd.value.size()};
}
}
}
// --- Operations in flat_buffer ---
// Compute the new capacity upon reallocation. We always use powers of 2,
// starting in 512, to prevent many small allocations
inline std::size_t compute_capacity(std::size_t current, std::size_t requested)
{
std::size_t res = (std::max)(current, static_cast<std::size_t>(512u));
while (res < requested)
res *= 2u;
return res;
}
// Copy construction
inline flat_buffer copy_construct(const flat_buffer& other)
{
flat_buffer res{{}, other.size, 0u, 0u};
if (other.size > 0u) {
const std::size_t capacity = compute_capacity(0u, other.size);
res.data.reset(new char[capacity]);
res.capacity = capacity;
res.reallocs = 1u;
std::copy(other.data.get(), other.data.get() + other.size, res.data.get());
}
return res;
}
// Copy assignment
inline void copy_assign(flat_buffer& buff, const flat_buffer& other)
{
// Make space if required
if (buff.capacity < other.size) {
const std::size_t capacity = compute_capacity(buff.capacity, other.size);
buff.data.reset(new char[capacity]);
buff.capacity = capacity;
++buff.reallocs;
}
// Copy the contents
std::copy(other.data.get(), other.data.get() + other.size, buff.data.get());
buff.size = other.size;
}
// Grows the buffer until reaching a target size.
// Might rebase the strings in nodes
inline void grow(flat_buffer& buff, std::size_t new_capacity, view_tree& nodes)
{
if (new_capacity <= buff.capacity)
return;
// Compute the actual capacity that we will be using
new_capacity = compute_capacity(buff.capacity, new_capacity);
// Allocate space
std::unique_ptr<char[]> new_buffer{new char[new_capacity]};
// Copy any data into the newly allocated space
const char* data_before = buff.data.get();
char* data_after = new_buffer.get();
std::copy(data_before, data_before + buff.size, data_after);
// Update the string views so they don't dangle
rebase_strings(nodes, data_before, data_after);
// Replace the buffer. Note that size hasn't changed here
buff.data = std::move(new_buffer);
buff.capacity = new_capacity;
++buff.reallocs;
}
// Erases the first num_bytes bytes from the buffer by moving
// the remaining bytes forward. Rebases the strings in nodes as required.
inline void erase_first(flat_buffer& buff, std::size_t num_bytes, view_tree& nodes)
{
BOOST_ASSERT(num_bytes <= buff.size);
if (num_bytes > 0u) {
// If we have any data to move, we should always have a buffer
BOOST_ASSERT(buff.data.get() != nullptr);
// Record the old base
const char* old_base = buff.data.get() + num_bytes;
// Move all that we're gonna keep to the start of the buffer
auto bytes_left = buff.size - num_bytes;
std::memmove(buff.data.get(), old_base, bytes_left);
// Rebase strings
rebase_strings(nodes, old_base, buff.data.get());
}
}
// Appends a string to the buffer.
// Might rebase the string in nodes, but doesn't append any new node.
inline std::string_view append(flat_buffer& buff, std::string_view value, view_tree& nodes)
{
// If there is nothing to copy, do nothing
if (value.empty())
return value;
// Make space for the new string
const std::size_t new_size = buff.size + value.size();
grow(buff, new_size, nodes);
// Copy the new value
const std::size_t offset = buff.size;
std::copy(value.data(), value.data() + value.size(), buff.data.get() + offset);
buff.size = new_size;
return {buff.data.get() + offset, value.size()};
}
} // namespace detail
flat_tree::flat_tree(flat_tree const& other)
: data_{detail::copy_construct(other.data_)}
, view_tree_{other.view_tree_}
, total_msgs_{other.total_msgs_}
, node_tmp_offset_{other.node_tmp_offset_}
, data_tmp_offset_{other.data_tmp_offset_}
{
detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get());
}
flat_tree& flat_tree::operator=(const flat_tree& other)
{
if (this != &other) {
// Copy the data
detail::copy_assign(data_, other.data_);
// Copy the nodes
view_tree_ = other.view_tree_;
detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get());
// Copy the other fields
total_msgs_ = other.total_msgs_;
node_tmp_offset_ = other.node_tmp_offset_;
data_tmp_offset_ = other.data_tmp_offset_;
}
return *this;
}
void flat_tree::reserve(std::size_t bytes, std::size_t nodes)
{
// Space for the strings
detail::grow(data_, bytes, view_tree_);
// Space for the nodes
view_tree_.reserve(nodes);
}
void flat_tree::clear() noexcept
{
// Discard everything except for the tmp area
view_tree_.erase(view_tree_.begin(), view_tree_.begin() + node_tmp_offset_);
node_tmp_offset_ = 0u;
// Do the same for the data area
detail::erase_first(data_, data_tmp_offset_, view_tree_);
data_tmp_offset_ = 0u;
// We now have no messages
total_msgs_ = 0u;
}
void flat_tree::push(node_view const& nd)
{
// Add the string
const std::string_view str = detail::append(data_, nd.value, view_tree_);
// Add the node
view_tree_.push_back({
nd.data_type,
nd.aggregate_size,
nd.depth,
str,
});
}
void flat_tree::notify_init()
{
// Discard any data in the tmp area, as it belongs to an operation that never finished
BOOST_ASSERT(node_tmp_offset_ <= view_tree_.size());
BOOST_ASSERT(data_tmp_offset_ <= data_.size);
view_tree_.resize(node_tmp_offset_);
data_.size = data_tmp_offset_;
}
void flat_tree::notify_done()
{
++total_msgs_;
node_tmp_offset_ = view_tree_.size();
data_tmp_offset_ = data_.size;
}
const node_view& flat_tree::at(std::size_t i) const
{
if (i >= size())
BOOST_THROW_EXCEPTION(std::out_of_range("flat_tree::at"));
return view_tree_[i];
}
bool operator==(flat_tree const& a, flat_tree const& b)
{
// data is already taken into account by comparing the nodes.
// Only committed nodes should be taken into account.
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin()) &&
a.get_total_msgs() == b.get_total_msgs();
}
} // namespace boost::redis::resp3

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -6,7 +6,6 @@
#include <boost/redis/ignore.hpp>
namespace boost::redis
{
namespace boost::redis {
ignore_t ignore;
}

View File

@@ -0,0 +1,23 @@
//
// 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_IS_TERMINAL_CANCEL_HPP
#define BOOST_REDIS_IS_TERMINAL_CANCEL_HPP
#include <boost/asio/cancellation_type.hpp>
namespace boost::redis::detail {
constexpr bool is_terminal_cancel(asio::cancellation_type_t cancel_state)
{
return (cancel_state & asio::cancellation_type_t::terminal) != asio::cancellation_type_t::none;
}
} // namespace boost::redis::detail
#endif

View File

@@ -0,0 +1,37 @@
//
// 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_LOG_TO_STDERR_HPP
#define BOOST_REDIS_LOG_TO_STDERR_HPP
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <string_view>
namespace boost::redis::detail {
// Shared by several ipp files
inline void log_to_file(FILE* f, std::string_view msg, const char* prefix = "(Boost.Redis) ")
{
// If the message is empty, data() might return a null pointer
const char* msg_ptr = msg.empty() ? "" : msg.data();
// Precision should be an int when passed to fprintf. Technically,
// message could be larger than INT_MAX. Impose a sane limit on message sizes
// to prevent memory problems
auto precision = static_cast<int>((std::min)(msg.size(), static_cast<std::size_t>(0xffffu)));
// Log the message. None of our messages should contain NULL bytes, so this should be OK.
// We choose fprintf over std::clog because it's safe in multi-threaded environments.
std::fprintf(f, "%s%.*s\n", prefix, precision, msg_ptr);
}
} // namespace boost::redis::detail
#endif

View File

@@ -0,0 +1,110 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_LOG_UTILS_HPP
#define BOOST_REDIS_LOG_UTILS_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/logger.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <string>
#include <string_view>
#include <type_traits>
namespace boost::redis::detail {
// Internal trait that defines how to log different types.
// The base template applies to types convertible to string_view
template <class T>
struct log_traits {
// log should convert the input value to string and append it to the supplied buffer
static inline void log(std::string& to, std::string_view value) { to += value; }
};
// Formatting size_t and error codes is shared between almost all FSMs, so it's defined here.
// Support for types used only in one FSM should be added in the relevant FSM file.
template <>
struct log_traits<std::size_t> {
static inline void log(std::string& to, std::size_t value) { to += std::to_string(value); }
};
template <>
struct log_traits<system::error_code> {
static inline void log(std::string& to, system::error_code value)
{
// Using error_code::what() includes any source code info
// that the error may contain, making the messages too long.
// This implementation was taken from error_code::what()
to += value.message();
to += " [";
to += value.to_string();
to += ']';
}
};
template <>
struct log_traits<address> {
static inline void log(std::string& to, const address& value)
{
to += value.host;
to += ':';
to += value.port;
}
};
template <class... Args>
void format_log_args(std::string& to, const Args&... args)
{
auto dummy = {(log_traits<Args>::log(to, args), 0)...};
ignore_unused(dummy);
}
// Logs a message with the specified severity to the logger.
// Formatting won't be performed if the logger's level is inferior to lvl.
// args are stringized using log_traits, and concatenated.
template <class Arg0, class... Rest>
void log(buffered_logger& to, logger::level lvl, const Arg0& arg0, const Rest&... arg_rest)
{
// Severity check
if (to.lgr.lvl < lvl)
return;
// Optimization: if we get passed a single string, don't copy it to the buffer
if constexpr (sizeof...(Rest) == 0u && std::is_convertible_v<Arg0, std::string_view>) {
to.lgr.fn(lvl, arg0);
} else {
to.buffer.clear();
format_log_args(to.buffer, arg0, arg_rest...);
to.lgr.fn(lvl, to.buffer);
}
}
// Shorthand for each log level we use
template <class... Args>
void log_debug(buffered_logger& to, const Args&... args)
{
log(to, logger::level::debug, args...);
}
template <class... Args>
void log_info(buffered_logger& to, const Args&... args)
{
log(to, logger::level::info, args...);
}
template <class... Args>
void log_err(buffered_logger& to, const Args&... args)
{
log(to, logger::level::err, args...);
}
} // namespace boost::redis::detail
#endif // BOOST_REDIS_LOGGER_HPP

Some files were not shown because too many files have changed in this diff Show More