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

Compare commits

...

461 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
Marcelo
0380e643ed Merge pull request #157 from boostorg/develop
Latest develop changes
2023-10-08 09:10:07 +02:00
Marcelo
ff734694ab Merge pull request #156 from boostorg/prepare_for_first_boost_release
Prepare for first boost release
2023-10-07 21:51:28 +02:00
Marcelo Zimbres
548e3d4cb6 Updates the copywrite notice. 2023-10-07 16:44:17 +02:00
Marcelo Zimbres
66b632b13d Small fixes in the docs. 2023-10-07 16:40:13 +02:00
Marcelo
11c9c1b787 Merge pull request #155 from anarthal/feature/118-boost-integration
118 boost integration
2023-10-05 22:42:31 +02:00
Ruben Perez
d6f9e435c7 Revert "Fixed libc++ link flags"
This reverts commit 199fb6c261.
2023-10-05 16:55:53 +02:00
Ruben Perez
9a7816dbf4 switched to default installation of openssl 2023-10-05 16:22:59 +02:00
Ruben Perez
199fb6c261 Fixed libc++ link flags 2023-10-05 16:19:24 +02:00
Ruben Perez
4d30d1e0c0 split cmake_test 2023-10-05 16:10:25 +02:00
Ruben Perez
92be6d958f Reduced ci.py verbosity 2023-10-05 16:08:49 +02:00
Ruben Perez
14d3c0232e Removed unnecessary checks fom jamfile 2023-10-05 16:08:32 +02:00
Ruben Perez
7412b37e08 choco => vcpkg 2023-10-05 16:03:32 +02:00
Ruben Perez
60ba5b62af Missing packages in coverage build 2023-10-05 14:13:19 +02:00
Ruben Perez
0303ae0dbc Simplified & documented Jamfile 2023-10-05 13:47:14 +02:00
Ruben Perez
ea6c5536c1 CMAKE_BUILD_PARALLEL_LEVEL for coverage 2023-10-05 13:26:00 +02:00
Ruben Perez
d386b30c3a Simplified ci.py 2023-10-05 13:25:49 +02:00
Ruben Perez
2951acc80f Merge branch 'feature/118-boost-integration' of github.com:anarthal/boost-redis into feature/118-boost-integration 2023-10-05 12:53:52 +02:00
Ruben Perez
faf15fe7e8 Initial coverage workflow 2023-10-05 12:45:35 +02:00
Ruben Perez
7f3f8b0c13 Relaxed cxx17 requirement in Jamfile 2023-10-05 12:33:49 +02:00
Ruben Perez
f37e514961 Link error fix in win b2 2023-10-05 12:02:47 +02:00
Ruben Perez
b7b4f8f449 OpenSSL win fix in CI 2023-10-05 12:02:31 +02:00
Ruben Perez
686cb306ea README now states Boost requirements 2023-10-04 19:13:50 +02:00
Ruben Perez
fcbe2c431c Canonical project name 2023-10-04 19:10:24 +02:00
Ruben Perez
a7b3fbdd9a Protect min/max 2023-10-04 19:06:31 +02:00
Ruben Perez
5ea0d3c467 Fixed OPENSSL_ROOT on win 2023-10-04 19:06:13 +02:00
Ruben Perez
2cd487784b Attempt to solve b2 openssl problem in win 2023-10-04 18:58:07 +02:00
Ruben Perez
b41e2704a1 choco no progress 2023-10-04 18:50:51 +02:00
Ruben Perez
765f0d45e8 Improved CI build names 2023-10-04 18:48:37 +02:00
Ruben Perez
84c8649d66 Bad b2 command 2023-10-04 18:48:29 +02:00
Ruben Perez
0bf4e76981 B2 CI 2023-10-04 18:41:48 +02:00
Ruben Perez
1d329df81b test jamfile 2023-10-04 18:31:47 +02:00
Ruben Perez
56f7d5af69 examples => example 2023-10-04 17:47:03 +02:00
Ruben Perez
d0c3b3f7ee generator fix 2023-10-04 17:38:50 +02:00
Ruben Perez
87ebc6cf4a protobuf fix 2023-10-04 17:38:34 +02:00
Ruben Perez
ffc35e8e3e copytree and cxxstd 2023-10-04 17:23:48 +02:00
Ruben Perez
a02837ab33 Explicit Python & typos 2023-10-04 17:12:11 +02:00
Ruben Perez
4a39a0d20a Toolset 2023-10-04 17:07:26 +02:00
Ruben Perez
56d9a2778f Typo fix 2023-10-04 12:50:41 +02:00
Ruben Perez
c732f33b48 New CI 2023-10-04 12:49:13 +02:00
Ruben Perez
221016f1c9 subdir tests 2023-10-04 12:29:59 +02:00
Ruben Perez
cb9fdba0a4 New cmakes 2023-10-04 11:28:55 +02:00
Ruben Perez
1c96a60709 ci.py first version 2023-10-03 23:09:34 +02:00
Ruben Perez
b66d067af8 tests => test 2023-10-03 23:08:59 +02:00
Ruben Perez
bc08a8d411 Trigger CI 2023-10-03 21:04:43 +02:00
Ruben Perez
53ef947cf3 Doc install and redirection 2023-10-03 18:59:21 +02:00
Ruben Perez
ecfe51c7ae Doc fixes 2023-10-03 17:27:31 +02:00
Ruben Perez
be20c0d48c Docs via b2 2023-10-03 16:51:05 +02:00
Ruben Perez
d5031c3f69 libraries.json 2023-10-02 17:17:44 +02:00
Marcelo
6748f7682a Merge pull request #153 from boostorg/152-enable-reading-server-pushes-in-batches
152 enable reading server pushes in batches
2023-09-10 22:28:28 +02:00
Marcelo Zimbres
2a4936a9e1 Implements batch reads for server pushes. 2023-09-10 12:05:37 +02:00
Marcelo Zimbres
4547e1ac07 First steps with using adapters to process a generic_response. 2023-09-04 14:00:12 +02:00
Marcelo
44a608c0ba Merge pull request #151 from boostorg/150-remove-resp3read-and-resp3async_read
Removes resp3::async_read.
2023-09-02 14:52:36 +02:00
Marcelo Zimbres
1ed8e0182c Removes resp3::async_read. 2023-09-02 13:05:06 +02:00
Marcelo
d8cf431dc2 Merge pull request #149 from boostorg/144-implement-connection-usage-information
Adds connection usage information.
2023-08-30 09:30:30 +02:00
Marcelo Zimbres
401dd24419 Adds connection usage information. 2023-08-29 16:31:23 +02:00
Marcelo
509635f222 Merge pull request #145 from boostorg/138-use-stdfunction-to-type-erase-the-adapter
Uses std::function to type erase the response adapter
2023-08-26 15:39:49 +02:00
Marcelo Zimbres
4fbd0c6853 Progreeses with the adapter type erasure. 2023-08-26 13:09:48 +02:00
Marcelo
b8899ecdc7 Merge pull request #143 from mrichmon/develop
Fix cmake find_package
2023-08-22 08:26:07 +02:00
Michael Richmond
7d09040646 Bump version number 2023-08-21 16:42:41 -07:00
Michael Richmond
0de26fb0ce Fix out of date filename 2023-08-21 16:42:17 -07:00
Marcelo
84ee2f37f1 Merge pull request #141 from boostorg/develop
Pre-Boost release of latest changes in develop
2023-08-18 23:08:05 +02:00
Marcelo
81927deda4 Merge pull request #134 from boostorg/123-automatically-select-database-after-hello
123 automatically select database after hello
2023-08-06 11:00:34 +02:00
Marcelo Zimbres
34ff1cea63 Fixes https://github.com/boostorg/redis/issues/121 2023-08-06 10:07:17 +02:00
Marcelo Zimbres
10603b7d3a Sends SELECT right after HELLO after a connection. 2023-08-06 10:07:17 +02:00
Marcelo
ad3c2914db Merge pull request #133 from boostorg/122-health-check-not-restart-after-connection-lost
Simplifies parse ops and fixes health-check on reconnection.
2023-08-05 23:04:50 +02:00
Marcelo Zimbres
91014b13bf Simplifies parse ops and fixes health-check on reconnection. 2023-08-05 22:20:39 +02:00
Marcelo
4f6f8b454d Merge pull request #132 from cbodley/wip-exec-forward
connection: async_exec forwards completion token
2023-08-02 22:06:27 +02:00
Casey Bodley
9ebcc544ae connection: async_exec forwards completion token
async operations should support move-only completion handlers. forward
the CompletionToken argument to avoid an unnecessary copy

Fixes: #131

Signed-off-by: Casey Bodley <cbodley@redhat.com>
2023-08-02 11:45:57 -04:00
Marcelo
7d16259749 Merge pull request #130 from boostorg/119-simplify-the-parser-op
Simplifications in the parser
2023-07-30 11:44:58 +02:00
Marcelo Zimbres
9dec63515e Simplifications in the parser. 2023-07-30 08:18:17 +02:00
Marcelo
46525371b9 Merge pull request #117 from cthulhu-irl/build/add-cmake-options
build: add cmake options
2023-07-01 13:03:31 +02:00
Cthulhu
b5f8348598 build: add cmake options
cmake options for install, tests, examples, and doc instead of
building them always.

options are enabled by default when building the project directly,
otherwise if add_subdirectory (directly or by FetchContent, etc),
then unnecessary options will be disabled.

issue #115
2023-06-25 19:26:27 +03:30
Marcelo
69d12421e2 Merge pull request #116 from boostorg/113-create-an-experimental-connection-class-that-has-fast-compilation-times
113 create an experimental connection class that has fast compilation times
2023-06-24 09:52:48 +02:00
Marcelo Zimbres
a715c251bf Improvements in the docs. 2023-06-23 22:24:34 +02:00
Marcelo Zimbres
d29a057fa6 Uses composition instead of inheritance in the connection class. 2023-06-20 23:01:17 +02:00
Marcelo Zimbres
82430afc8b Make the connection non-generic on the executor type. 2023-06-18 09:34:40 +02:00
Marcelo
607946f00e Merge pull request #112 from boostorg/111-simplify-the-serialization-examples
111 simplify the serialization examples
2023-06-11 11:12:06 +02:00
Marcelo Zimbres
c99790ab5c Uses choco instead of cinst. 2023-06-11 09:28:12 +02:00
Marcelo Zimbres
635b3608ad Removes unnecessary files. 2023-06-08 22:00:07 +02:00
Marcelo
a8a78c38c6 Merge pull request #109 from boostorg/103-cant-read-response-of-hello-+-ping-+-hello
Removes payload rotation from request.
2023-06-04 16:50:12 +02:00
Marcelo Zimbres
e09a53ff08 Removes payload rotation from request.
The user can simply call HELLO before other commands. Altering the order
of requests makes it impossible to declare responses.
2023-06-04 16:18:35 +02:00
Marcelo
ec8a1c7286 Merge pull request #105 from boostorg/95-improve-the-performance-of-connectionasync_receive
95 improve the performance of connectionasync receive
2023-05-28 11:14:14 +02:00
Marcelo Zimbres
3c02a7662b Replaces connection channel with a timer. 2023-05-27 23:20:15 +02:00
Marcelo Zimbres
538ab8f35f Reduces the number of rescheduling needed to process a server sent push.
Performance improved by close to 10%.
2023-05-21 21:17:13 +02:00
Marcelo Zimbres
f5f57e370b Improvements in the redis-push stress test. 2023-05-21 21:17:13 +02:00
Marcelo
7abfc5fd8d Merge pull request #101 from boostorg/100-runhpp-no-longer-present-but-still-referred-to-from-redishpp
Fixes redis.hpp and slightly improves compilation times.
2023-05-20 14:16:19 +02:00
Marcelo Zimbres
11eebcf771 Fixes redis.hpp and slightly improves compilation times. 2023-05-20 13:19:31 +02:00
Marcelo
c21f70bc07 Merge pull request #99 from boostorg/93-use-cmake-foreach-to-simplify-cmakeliststxt
Simplifies the CMakeLists.txt.
2023-05-14 13:55:46 +02:00
Marcelo Zimbres
22bacbd52c Simplifies the CMakeLists.txt. 2023-05-14 10:42:16 +02:00
Marcelo
2982f831f6 Merge pull request #98 from boostorg/94-unify-redisconnection-and-redissslconnection
94 unify redisconnection and redissslconnection
2023-05-13 18:22:53 +02:00
Marcelo Zimbres
663e9ac671 Simplifications. 2023-05-13 10:22:11 +02:00
Marcelo Zimbres
c0aa4356ea The ssl::context is now owned by the connection. 2023-05-10 23:25:09 +02:00
Marcelo Zimbres
6f9fd5b2fb Unifies ssl and plain connections. 2023-05-09 23:12:16 +02:00
Marcelo
30a6e34e4e Merge pull request #97 from boostorg/85-added-example-cpp20_streams-which-reproduces-an-assertion
85 added example cpp20 streams which reproduces an assertion
2023-05-06 21:26:09 +02:00
Marcelo Zimbres
1f9b3e8008 Rebase the branch on develop. 2023-05-06 20:48:32 +02:00
bram
3808fec0e3 Cleaned up a bit
Removed unused stuff
Using request and response as shared_ptrs.
Removed (unnecessary?) calls to net::post.
2023-05-06 17:14:10 +02:00
bram
607a9e9dd6 Added example cpp20_streams, which reproduces an assertion. 2023-05-06 16:55:15 +02:00
Marcelo
2d53bb748e Merge pull request #92 from boostorg/90-add-support-for-reconnection
90 add support for reconnection
2023-05-06 15:39:29 +02:00
Marcelo Zimbres
a6cb4ca323 Adds high-level functionality to connection::async_run. 2023-05-02 23:15:08 +02:00
Marcelo Zimbres
5ac4f7e8ad Removes dependency on asio::promise as it does not compile on windows. 2023-03-31 04:00:20 +02:00
Marcelo Zimbres
7a08588808 Progresses with the subscriber. 2023-03-31 04:00:19 +02:00
Marcelo
e7ff1cedf3 Merge pull request #89 from boostorg/88-simplify-async_check_health-with-asioconsign
Uses consign to simplify the check-health operation.
2023-03-20 01:53:40 +01:00
Marcelo Zimbres
0bcb7dcf16 Uses consign to simplify the check-health operation. 2023-03-20 01:26:50 +01:00
Marcelo
c28969674b Merge pull request #86 from boostorg/83-fix-reconnect-loop-in-the-subscriber-example
83 fix reconnect loop in the subscriber example
2023-03-16 18:05:40 +01:00
Marcelo Zimbres
c7f49c6677 Adds address struct. 2023-03-16 17:01:49 +01:00
Marcelo Zimbres
90bcd621fb Including only necessary headers. 2023-03-14 20:11:22 +01:00
Marcelo Zimbres
fd967204df Implements non-member async_run for plain connections.
This function will resolve and connect before calling member async_run.
2023-03-13 21:37:25 +01:00
Marcelo
cd00047a49 Merge pull request #82 from boostorg/81-add-support-for-protobuf
First steps with protobuf support.
2023-03-11 13:10:39 +01:00
Marcelo Zimbres
728b35cfe0 Adds protobuf example. 2023-03-11 12:24:35 +01:00
Marcelo
52e62ba78c Merge pull request #80 from boostorg/79-missing-header-in-resulthpp
Adds missing include header.
2023-03-08 12:10:37 +01:00
Marcelo Zimbres
bb18ff4891 Adds missing include header. 2023-03-07 20:24:53 +01:00
Marcelo
6ce793e413 Merge pull request #78 from boostorg/69-move-health-check-functionality-from-examples-to-the-library
Implements a function that checks Redis health.
2023-03-05 21:18:12 +01:00
Marcelo Zimbres
a83c0e7803 Trying to fix build on MSVC by including tuple before asio. 2023-03-05 20:54:02 +01:00
Marcelo Zimbres
64820bd25b Implements a function that checks Redis health. 2023-03-04 15:54:23 +01:00
Marcelo
16b5c8d1ba Merge pull request #77 from boostorg/72-cannot-build-on-macos
Adds support for libc++.
2023-03-02 08:12:36 +01:00
Marcelo Zimbres
8ef4d3cf0b Adds support for libc++. 2023-03-02 07:46:21 +01:00
Marcelo
d01a9acf3b Merge pull request #75 from boostorg/51-more-suggestions-for-documentation-improvements
Addresses issue #45, #47, #51 and #74.
2023-02-26 20:58:14 +01:00
Marcelo Zimbres
ac7e425d47 Addresses issue #45, #47, #51 and #74. 2023-02-26 20:34:43 +01:00
Marcelo
d620cdee59 Merge pull request #71 from boostorg/70-rename-node-to-basic_node-and-add-typedef-for-nodestdstring
70 rename node to basic node and add typedef for nodestdstring
2023-02-25 15:30:04 +01:00
Marcelo Zimbres
5f07b730f7 Upgrades to Boost 1.81. 2023-02-25 10:26:07 +01:00
Marcelo Zimbres
6d3a112f94 Removes json serialization boilerplate. 2023-02-25 10:14:28 +01:00
Marcelo Zimbres
1f3ef6b486 Renames node to basic_node. 2023-02-25 10:14:28 +01:00
Marcelo
a850a6ed63 Merge pull request #68 from boostorg/67-make-the-connection-full-duplex
67 make the connection full duplex
2023-02-18 21:39:13 +01:00
Marcelo Zimbres
c8b73c2fe8 Removes coalesce property of the requests.
It doesn't make any sense after the implementation of full-duplex
communication.
2023-02-18 20:08:55 +01:00
Marcelo Zimbres
8b02268182 Uses boost.describe to simplify json serialization. 2023-02-16 21:44:45 +01:00
Marcelo Zimbres
1b60eeb352 Makes the connection full-duplex. 2023-02-12 19:14:07 +01:00
Marcelo
b93f36163d Merge pull request #66 from boostorg/65-move-redisresp3async_read-to-redis-namespace
Moves read functions from resp3:: to redis::.
2023-02-12 07:44:33 +01:00
Marcelo Zimbres
071f9a93aa Moves read functions from resp3:: to redis::. 2023-02-11 19:17:05 +01:00
Marcelo
5a6ca14a67 Merge pull request #64 from boostorg/40-improve-support-to-redis-error-messages
Uses system::result to implement per request error handling.
2023-02-11 14:56:12 +01:00
Marcelo Zimbres
a5c86107f8 Uses system::result to implement per request error handling. 2023-02-11 11:53:44 +01:00
Marcelo
3a4445022e Merge pull request #63 from boostorg/62-let-the-implementation-call-adaptresp-automatically
async_exec accepts response now instead of adapter.
2023-02-05 10:08:05 +01:00
Marcelo Zimbres
bfb26f2602 async_exec accepts response now instead of adapter. 2023-02-04 20:43:36 +01:00
Marcelo
7e70cb4ad7 Merge pull request #61 from boostorg/60-we-need-response-typedefs
Adds response typedefs.
2023-02-04 10:29:22 +01:00
Marcelo Zimbres
886561409a Adds response typedefs. 2023-02-04 10:03:08 +01:00
Marcelo
0c5ff09685 Merge pull request #59 from boostorg/42-the-names-from_bulk-and-to_bulk-are-too-generic-for-adl-customization-points
Prefix to_ and from_bulk with boost_redis_ (boost review).
2023-02-02 08:33:44 +01:00
Marcelo Zimbres
4b07b6d516 Prefix to_ and from_bulk with boost_redis_ (boost review). 2023-02-01 22:48:33 +01:00
Marcelo
c1ce8358c7 Merge pull request #57 from boostorg/56-remove-memory_resource-usage
Removes memory_resource.
2023-01-29 21:14:37 +01:00
Marcelo Zimbres
13e16b7a60 Removes memory_resource. 2023-01-29 20:21:30 +01:00
Marcelo
e11502e0df Merge pull request #55 from boostorg/54-rename-aedis-to-boost-redis
Renames Aedis to Boost.Redis.
2023-01-28 21:43:57 +01:00
Marcelo Zimbres
b2344384cf Renames Aedis to Boost.Redis. 2023-01-28 17:57:35 +01:00
Marcelo Zimbres
56c0b28003 Fixes issue 50 and 44. 2023-01-28 09:35:36 +01:00
Marcelo Zimbres
c88fcfb9ed Adds more doc to node class. 2023-01-08 21:51:41 +01:00
Marcelo
a56bf982ab Merge pull request #41 from Hailios/remove_duplicate_cmake
remove duplicate line in cmake
2023-01-07 20:11:50 +01:00
Jakob Lövhall
5d0ed0e986 remove duplicate line in cmake 2023-01-07 16:58:50 +01:00
Marcelo Zimbres
15deaa637d Doc improvements. 2023-01-07 00:14:29 +01:00
Marcelo Zimbres
bb8ff90351 Fixes issue 39. 2023-01-06 17:38:10 +01:00
Marcelo Zimbres
7d4902369a Doc improvements and replaces async_main to co_main. 2023-01-05 23:37:55 +01:00
Marcelo Zimbres
607ca17a89 Improvements in the documentation. 2023-01-04 22:51:53 +01:00
Marcelo Zimbres
3849ba42fd Changes:
- Fix include header order.
- Removes default completion token where it is not needed.
- Replaces yield with BOOST_ macros.
2023-01-02 23:51:50 +01:00
Marcelo Zimbres
56bcdb7914 Improvements in the docs. 2022-12-31 15:58:31 +01:00
Marcelo Zimbres
73ad66eb93 Adds example that does not user awaitable ops. 2022-12-30 18:13:09 +01:00
Marcelo Zimbres
9cf00d6a23 Adds cpp17 async example. 2022-12-30 00:04:41 +01:00
Marcelo Zimbres
a00c9e7439 Doc improvements. 2022-12-27 21:21:43 +01:00
Marcelo Zimbres
0520791100 Renames request flag. 2022-12-27 18:46:27 +01:00
Marcelo Zimbres
14b376e36e Fixes cancelation of async_exec (2). 2022-12-26 11:02:13 +01:00
Marcelo Zimbres
4f9dcc7dc5 Fixes async_exec terminal cancellation. 2022-12-25 20:01:35 +01:00
Marcelo Zimbres
ad5dd8c30b Refactors the parser so it is not header-only. 2022-12-22 21:42:41 +01:00
Marcelo Zimbres
842f864689 Using doxygen-awesome css. 2022-12-19 21:40:44 +01:00
Marcelo Zimbres
63f9b74502 Improves the stress test. 2022-12-18 09:20:29 +01:00
Marcelo Zimbres
801f60a026 Readme improvements. 2022-12-17 22:59:44 +01:00
Marcelo Zimbres
c37fcb641c Documentation improvements. 2022-12-17 16:59:06 +01:00
Marcelo Zimbres
48c3f37168 Test improvements and bugfix in send-retry. 2022-12-11 22:19:37 +01:00
Marcelo Zimbres
3c63911802 Removes some boost dependencies. 2022-12-10 19:42:51 +01:00
Marcelo Zimbres
1645881a44 Doc improvements and add guarded_op class. 2022-12-07 22:32:49 +01:00
Marcelo Zimbres
730e06c38d Adds bigobj and other vs flags. 2022-12-04 20:36:17 +01:00
Marcelo Zimbres
cf3a79737d Removes mingw from windows builds. 2022-12-04 16:15:12 +01:00
Marcelo Zimbres
edb384c843 Build fix on windows. 2022-12-04 15:13:14 +01:00
Marcelo Zimbres
f745faddf8 Replaces boost::string_view with std::string_view. 2022-12-04 13:53:44 +01:00
Marcelo Zimbres
927117568e Build fix. 2022-12-04 13:53:44 +01:00
Marcelo Zimbres
1e7c176f92 Removes dependency on Boost.Hana. 2022-12-03 22:29:04 +01:00
Marcelo Zimbres
449b5f7e7c First steps with windows CI. 2022-12-03 18:14:10 +01:00
Marcelo Zimbres
75f91f3b11 v1.3.1 and build fixes. 2022-12-03 14:34:15 +01:00
Marcelo Zimbres
b9a23568e3 Many improvements in the examples. 2022-12-02 22:58:39 +01:00
Marcelo Zimbres
4ac2509afa Improvements in the docs and examples. 2022-11-27 21:59:02 +01:00
Marcelo Zimbres
e9dab97992 v1.3.0 2022-11-26 22:22:56 +01:00
Marcelo Zimbres
2e8cad858d Improvements in the examples. 2022-11-26 19:42:39 +01:00
Marcelo Zimbres
5a6e426028 Build fix and improvements in the examples. 2022-11-22 22:57:33 +01:00
Marcelo Zimbres
c55978a379 CI fix and improvements in the examples. 2022-11-21 23:41:41 +01:00
Marcelo Zimbres
6f51397e49 Build fix. 2022-11-20 14:06:07 +01:00
Marcelo Zimbres
6b9ba6b2d9 Adds connection typedef and improves docs. 2022-11-19 23:53:26 +01:00
Marcelo Zimbres
d29c03cb38 Changes:
* Uses pmr::string for the connection read and write buffer.
* Improvements in the examples.
2022-11-18 23:15:47 +01:00
Marcelo Zimbres
34cfbaa22f Removes healthy checks from the connection class. 2022-11-13 21:22:50 +01:00
Marcelo Zimbres
c9354fe320 Test improvements. 2022-11-13 18:39:28 +01:00
Marcelo Zimbres
bb555cb509 Remove built-in resolve and connect operation in async_run. 2022-11-13 00:10:26 +01:00
Marcelo Zimbres
5b209afa1d Removes endpoint class. 2022-11-09 23:05:52 +01:00
Marcelo Zimbres
3f5491654d Removes built-in HELLO from the connection. 2022-11-08 00:04:52 +01:00
Marcelo Zimbres
2bdc25752f Simplifications in the low-level tests. 2022-11-06 22:40:00 +01:00
Marcelo Zimbres
faafce1c64 Adds tls test. 2022-11-06 19:12:36 +01:00
Marcelo Zimbres
562075230f v1.2.0 2022-11-05 19:06:56 +01:00
Marcelo Zimbres
5dc677c6d8 Changes:
* Adds allocator support for the internal connection queue.
* Support for std::tie in aedis::adapt.
* Docs.
2022-11-05 12:07:22 +01:00
Marcelo Zimbres
395a167d48 Improvements in the coverage. 2022-11-01 14:13:33 +01:00
Marcelo Zimbres
f93f3cab58 Merge branch 'klemens-morgenstern-allocator-nonsense' 2022-10-31 22:20:54 +01:00
Marcelo Zimbres
df68fb0235 Changes:
- Ports from boost::container::pmr to std::pmr.
- Fixes clang-tidy issues.
- Adds resp3::request unit-tests.
2022-10-31 22:17:58 +01:00
Klemens Morgenstern
15e6883bc1 Added boost.container.pmr to request. 2022-10-31 07:37:03 +01:00
Marcelo Zimbres
3816d1d358 Documentation and stress test. 2022-10-30 19:48:04 +01:00
Marcelo
bb15c70723 Merge pull request #36 from klemens-morgenstern/sfinae
Added sfinae to push_range.
2022-10-30 19:47:16 +01:00
Klemens Morgenstern
297b7f15eb Added sfinae to push_range. 2022-10-30 23:11:35 +08:00
Marcelo Zimbres
ec6e99d99a Docs and example improvements. 2022-10-29 22:49:53 +02:00
Marcelo Zimbres
8dc6db069b Docs and examples. 2022-10-27 23:09:59 +02:00
Marcelo Zimbres
bac27c1770 Fixes cancellation. 2022-10-25 20:58:16 +02:00
Marcelo Zimbres
feaaedc6c0 Improvements in the cancellation support. 2022-10-23 22:32:58 +02:00
Marcelo Zimbres
000ebddf44 Fixes bug that caused unwritten request to be closed if write fails. 2022-10-22 22:34:10 +02:00
Marcelo Zimbres
268ea2c10f Improvements in the writer cancellation. 2022-10-22 21:23:33 +02:00
Marcelo Zimbres
d8b67f6e23 Improves async_exec cancellation support. 2022-10-22 20:43:37 +02:00
Marcelo Zimbres
ce1fa6a683 Implements per-op cancelation of async_exec. 2022-10-18 20:52:18 +02:00
Marcelo Zimbres
b8ede6ccb7 Fixes bug in conn.cancel(exec). 2022-10-16 22:44:44 +02:00
Marcelo Zimbres
6dce1a9226 Marks function inline. 2022-10-15 13:08:50 +02:00
Marcelo Zimbres
8566745d83 Changes the behaviour of adapt() with vector<node>. 2022-10-13 21:45:01 +02:00
Marcelo Zimbres
0b4906fcba Test improvements. 2022-10-13 20:51:35 +02:00
Marcelo Zimbres
2c8bb92071 Improvements in the docs. 2022-10-10 23:14:54 +02:00
Marcelo Zimbres
770e224917 Changes:
- CI fix.
- Renames request::fail_* to request::cancel_*.
- Adds a second parameter to async_run.
- Adds request::retry flag.
2022-10-09 22:45:42 +02:00
Marcelo Zimbres
4fb2b20954 Build fixes. 2022-10-08 22:24:20 +02:00
Marcelo Zimbres
c01a57b6cb Adds cancelation test. 2022-10-08 22:07:51 +02:00
Marcelo Zimbres
ea0b333c4d Removes the second async_run overload. 2022-10-08 17:07:28 +02:00
Marcelo Zimbres
ba82c6cd84 Progresses removing the second async_run overload. 2022-10-07 22:43:32 +02:00
Marcelo Zimbres
4c298ddc6b Adds doxygen output to the preset. 2022-10-03 14:32:36 +02:00
Marcelo Zimbres
690002b2f1 Fixes CI. 2022-10-03 13:10:56 +02:00
Marcelo Zimbres
12899da4db Back to Boost 1.79 as CI does not support 1.80. 2022-10-03 11:28:34 +02:00
Marcelo Zimbres
b2c19df113 Documentation improvements. 2022-10-02 20:24:35 +02:00
Marcelo Zimbres
61f9a29ebc Makes connection_base a private base. 2022-10-02 09:04:09 +02:00
Marcelo Zimbres
dc9b333f1e Passes timeouts as parameters to async_run. 2022-10-01 21:20:07 +02:00
Marcelo Zimbres
dac7bea54f Moves coalesce request from the connection the request. 2022-10-01 15:17:26 +02:00
Marcelo Zimbres
da84321378 Adds request::config. 2022-10-01 12:34:46 +02:00
Marcelo Zimbres
3295459700 Moves connection::config::max_read_size to adapt(). 2022-10-01 10:53:54 +02:00
Marcelo Zimbres
dff8833fe3 Removes the sync wrapper. 2022-09-29 23:15:50 +02:00
Marcelo Zimbres
3a03a43c06 Fixes some of the bugs reported in issue #28. 2022-09-25 22:43:24 +02:00
Marcelo Zimbres
b614826bb4 Improvements in the docs. 2022-09-25 20:14:05 +02:00
Marcelo Zimbres
4e30a9d53d Adds reserve member function to aedis::request. 2022-09-24 15:12:31 +02:00
Marcelo Zimbres
85ba41ae5a Adds support to request::close_on_connection_lost. 2022-09-24 15:04:33 +02:00
Marcelo Zimbres
fd82204ba9 Improvements in the documentation. 2022-09-18 23:11:13 +02:00
Marcelo Zimbres
084e95cbc7 Improvements. 2022-09-17 22:20:06 +02:00
Marcelo Zimbres
798f193f14 Progresses with code quality. 2022-09-12 21:57:57 +02:00
Marcelo Zimbres
a23a3db9ac Improvements in the readme. 2022-09-11 21:48:30 +02:00
Marcelo Zimbres
bca6511333 Progresses with TLS support and improvements in the docs. 2022-09-11 18:09:44 +02:00
Marcelo Zimbres
e2d642f34c Server role check and progresses with TLS. 2022-09-10 22:42:39 +02:00
Marcelo Zimbres
d8607af669 Summary:
- Lots of improvements.
- Changes the behaviour of the second async_run overload.
- Moves to std::optional.
2022-09-10 11:25:42 +02:00
Marcelo Zimbres
223a9aa74b Progresses with TLS support. 2022-09-04 22:19:27 +02:00
Marcelo Zimbres
75433cd028 Improves clang-tidy, adds endpoint etc. 2022-09-03 14:41:22 +02:00
Marcelo Zimbres
552c6cf6e4 Applies some clang-tidy suggestions. 2022-08-31 20:56:28 +02:00
Marcelo Zimbres
16b9347c51 Adds a cmake preset file and fixes install target. 2022-08-28 20:03:31 +02:00
Marcelo Zimbres
03558b1466 Adds more tests. 2022-08-27 23:12:44 +02:00
Marcelo Zimbres
f8165bcb6f Error fixes and test coverage improvements. 2022-08-27 19:22:10 +02:00
Marcelo Zimbres
8aad27269c Improvements in the coverage and replaces autotools with cmake. 2022-08-27 12:53:44 +02:00
Marcelo Zimbres
f11622e746 Updates the go benchmark. 2022-08-25 21:24:23 +02:00
Marcelo Zimbres
8249360a52 Progresses with coverage. 2022-08-24 22:41:17 +02:00
Marcelo Zimbres
d00f26d3da Ports code coverage build to cmake. 2022-08-23 21:49:26 +02:00
Marcelo Zimbres
480ec13119 Tries to ignore branch coverage. 2022-08-22 21:21:17 +02:00
Marcelo Zimbres
4cc3fc59a1 Improves CMakeLists.txt with packaging. 2022-08-21 21:28:21 +02:00
240 changed files with 34297 additions and 11475 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
---

172
.clang-tidy Normal file
View File

@@ -0,0 +1,172 @@
---
# Enable ALL the things! Except not really
# misc-non-private-member-variables-in-classes: the options don't do anything
Checks: "*,\
-readability-*,\
-readability-identifier-length,\
-google-readability-todo,\
-google-readability-namespace-comments,\
-google-readability-braces-around-statements,\
-hicpp-braces-around-statements,\
-hicpp-named-parameter,\
-hicpp-avoid-goto,\
-google-build-using-namespace,\
-altera-*,\
-fuchsia-*,\
fuchsia-multiple-inheritance,\
-llvm-namespace-comment,\
-llvm-header-guard,\
-llvm-include-order,\
-llvmlibc-*,\
-misc-non-private-member-variables-in-classes,\
-bugprone-use-after-move,\
-hicpp-invalid-access-moved,\
-misc-no-recursion,\
-cppcoreguidelines-pro-bounds-pointer-arithmetic,\
-cppcoreguidelines-avoid-magic-numbers,\
-cppcoreguidelines-pro-bounds-constant-array-index,\
-cppcoreguidelines-interfaces-global-init,\
-cppcoreguidelines-macro-usage,\
-cppcoreguidelines-avoid-goto,\
-cppcoreguidelines-non-private-member-variables-in-classes"
WarningsAsErrors: ''
CheckOptions:
- key: 'bugprone-argument-comment.StrictMode'
value: 'true'
# Prefer using enum classes with 2 values for parameters instead of bools
- key: 'bugprone-argument-comment.CommentBoolLiterals'
value: 'true'
- key: 'bugprone-misplaced-widening-cast.CheckImplicitCasts'
value: 'true'
- key: 'bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression'
value: 'true'
- key: 'bugprone-suspicious-string-compare.WarnOnLogicalNotComparison'
value: 'true'
- key: 'readability-simplify-boolean-expr.ChainedConditionalReturn'
value: 'true'
- key: 'readability-simplify-boolean-expr.ChainedConditionalAssignment'
value: 'true'
- key: 'readability-uniqueptr-delete-release.PreferResetCall'
value: 'true'
- key: 'cppcoreguidelines-init-variables.MathHeader'
value: '<cmath>'
- key: 'cppcoreguidelines-narrowing-conversions.PedanticMode'
value: 'true'
- key: 'readability-else-after-return.WarnOnUnfixable'
value: 'true'
- key: 'readability-else-after-return.WarnOnConditionVariables'
value: 'true'
- key: 'readability-inconsistent-declaration-parameter-name.Strict'
value: 'true'
- key: 'readability-qualified-auto.AddConstToQualified'
value: 'true'
- key: 'readability-redundant-access-specifiers.CheckFirstDeclaration'
value: 'true'
# These seem to be the most common identifier styles
- key: 'readability-identifier-naming.AbstractClassCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ClassCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ClassConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ClassMemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ClassMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstantMemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstantParameterCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstantPointerParameterCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstexprFunctionCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstexprMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstexprVariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.EnumCase'
value: 'lower_case'
- key: 'readability-identifier-naming.EnumConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.FunctionCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalConstantPointerCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalFunctionCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalPointerCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalVariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.InlineNamespaceCase'
value: 'lower_case'
- key: 'readability-identifier-naming.LocalConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.LocalConstantPointerCase'
value: 'lower_case'
- key: 'readability-identifier-naming.LocalPointerCase'
value: 'lower_case'
- key: 'readability-identifier-naming.LocalVariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.MacroDefinitionCase'
value: 'UPPER_CASE'
- key: 'readability-identifier-naming.MemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.MethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.NamespaceCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ParameterCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ParameterPackCase'
value: 'lower_case'
- key: 'readability-identifier-naming.PointerParameterCase'
value: 'lower_case'
- key: 'readability-identifier-naming.PrivateMemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.PrivateMemberPrefix'
value: 'm_'
- key: 'readability-identifier-naming.PrivateMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ProtectedMemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ProtectedMemberPrefix'
value: 'm_'
- key: 'readability-identifier-naming.ProtectedMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.PublicMemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.PublicMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ScopedEnumConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.StaticConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.StaticVariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.StructCase'
value: 'lower_case'
- key: 'readability-identifier-naming.TemplateParameterCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.TemplateTemplateParameterCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.TypeAliasCase'
value: 'lower_case'
- key: 'readability-identifier-naming.TypedefCase'
value: 'lower_case'
- key: 'readability-identifier-naming.TypeTemplateParameterCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.UnionCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ValueTemplateParameterCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.VariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.VirtualMethodCase'
value: 'lower_case'
...

View File

@@ -7,7 +7,16 @@ codecov:
ignore:
- "benchmarks/cpp/asio/*"
- "examples/*"
- "example/*"
- "tests/*"
- "/usr/*"
- "**/boost/*"
parsers:
gcov:
branch_detection:
conditional: no
loop: no
method: no
macro: no

View File

@@ -1,9 +1,128 @@
# CI script to verify that CMake and B2 builds work.
# B2 builds include only tests that don't require a DB server, to avoid race conditions.
# CMake tests include the actual project tests and all the CMake integration workflows
# recommended by Boost.CI.
# Windows CMake jobs build the code but don't run the tests,
# since we don't have a way to set up a Redis server on Windows (yet).
# Subcommands are implemented by the tools/ci.py script in a platform-independent manner.
name: CI
on: [push, pull_request]
jobs:
posix:
windows-cmake:
name: "CMake ${{matrix.toolset}} ${{matrix.build-type}} C++${{matrix.cxxstd}}"
runs-on: ${{matrix.os}}
defaults:
run:
shell: bash
strategy:
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.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@v4
- name: Setup Boost
run: python3 tools/ci.py setup-boost --source-dir=$(pwd)
- name: Build a Boost distribution using B2
run: |
python3 tools/ci.py build-b2-distro \
--toolset ${{ matrix.toolset }}
- name: Build a Boost distribution using CMake
run: |
python3 tools/ci.py build-cmake-distro \
--build-type ${{ matrix.build-type }} \
--cxxstd ${{ matrix.cxxstd }} \
--toolset ${{ matrix.toolset }} \
--generator "${{ matrix.generator }}" \
--build-shared-libs ${{ matrix.build-shared-libs }}
- name: Build the project tests
run: |
python3 tools/ci.py build-cmake-standalone-tests \
--build-type ${{ matrix.build-type }} \
--cxxstd ${{ matrix.cxxstd }} \
--toolset ${{ matrix.toolset }} \
--generator "${{ matrix.generator }}" \
--build-shared-libs ${{ matrix.build-shared-libs }}
# # TODO: re-enable this when a Redis server is available for this job
# - name: Run the project tests
# run: |
# python3 tools/ci.py run-cmake-standalone-tests \
# --build-type ${{ matrix.build-type }}
- name: Run add_subdirectory tests
run: |
python3 tools/ci.py run-cmake-add-subdirectory-tests \
--build-type ${{ matrix.build-type }} \
--cxxstd ${{ matrix.cxxstd }} \
--toolset ${{ matrix.toolset }} \
--generator "${{ matrix.generator }}" \
--build-shared-libs ${{ matrix.build-shared-libs }}
- name: Run find_package tests with the built cmake distribution
run: |
python3 tools/ci.py run-cmake-find-package-tests \
--build-type ${{ matrix.build-type }} \
--cxxstd ${{ matrix.cxxstd }} \
--toolset ${{ matrix.toolset }} \
--generator "${{ matrix.generator }}" \
--build-shared-libs ${{ matrix.build-shared-libs }}
- name: Run find_package tests with the built b2 distribution
run: |
python3 tools/ci.py run-cmake-b2-find-package-tests \
--build-type ${{ matrix.build-type }} \
--cxxstd ${{ matrix.cxxstd }} \
--toolset ${{ matrix.toolset }} \
--generator "${{ matrix.generator }}" \
--build-shared-libs ${{ matrix.build-shared-libs }}
windows-b2:
name: "B2 ${{matrix.toolset}}"
runs-on: ${{matrix.os}}
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
include:
#- { 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@v4
- name: Setup user-config.jam
run: cp tools/user-config.jam "${HOMEDRIVE}${HOMEPATH}/"
- name: Setup Boost
run: python3 tools/ci.py setup-boost --source-dir=$(pwd)
- name: Build and run project tests using B2
run: |
python3 tools/ci.py run-b2-tests \
--toolset ${{ matrix.toolset }} \
--cxxstd 17,20 \
--variant debug,release
posix-cmake:
name: "CMake ${{ matrix.toolset }} ${{ matrix.cxxstd }} ${{ matrix.build-type }} ${{ matrix.cxxflags }}"
defaults:
run:
shell: bash
@@ -12,37 +131,238 @@ jobs:
fail-fast: false
matrix:
include:
- { toolset: gcc, compiler: g++-10, install: g++-10, os: ubuntu-22.04, cxxstd: 'c++17' }
- { toolset: gcc, compiler: g++-11, install: g++-11, os: ubuntu-22.04, cxxstd: 'c++17' }
- { toolset: gcc, compiler: g++-11, install: g++-11, os: ubuntu-22.04, cxxstd: 'c++20' }
- { toolset: clang, compiler: clang++-11, install: clang-11, os: ubuntu-22.04, cxxstd: 'c++17' }
- { toolset: clang, compiler: clang++-11, install: clang-11, os: ubuntu-22.04, cxxstd: 'c++20' }
- { toolset: clang, compiler: clang++-13, install: clang-13, os: ubuntu-22.04, cxxstd: 'c++17' }
- { toolset: clang, compiler: clang++-13, install: clang-13, os: ubuntu-22.04, cxxstd: 'c++20' }
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:
CXX: ${{ matrix.compiler }}
CXXFLAGS: -std=${{matrix.cxxstd}} -Wall -Wextra
CXXFLAGS: ${{matrix.cxxflags}} -Wall -Wextra
LDFLAGS: ${{matrix.ldflags}}
CMAKE_BUILD_PARALLEL_LEVEL: 4
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Autotools
run: sudo apt install automake
- name: Install compiler
run: sudo apt-get install -y ${{ matrix.install }}
- name: Install Redis
run: sudo apt-get install -y redis-server
- name: Install boost
uses: MarkusJx/install-boost@v2.3.0
id: install-boost
with:
boost_version: 1.79.0
platform_version: 22.04
- name: Configure
uses: actions/checkout@v4
- name: Set up the required containers
run: |
autoreconf -i
./configure --with-boost=${{ steps.install-boost.outputs.BOOST_ROOT }}
- name: Build
run: make
- name: Check
run: make check VERBOSE=1
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: |
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: docker exec builder /boost-redis/tools/ci.py setup-boost --source-dir=/boost-redis
- name: Build a Boost distribution using B2
run: |
docker exec builder /boost-redis/tools/ci.py build-b2-distro \
--toolset ${{ matrix.toolset }}
- name: Build a Boost distribution using CMake
run: |
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: |
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: |
docker exec builder /boost-redis/tools/ci.py run-cmake-standalone-tests \
--build-type ${{ matrix.build-type }}
- name: Run add_subdirectory tests
run: |
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: |
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: |
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 }}
posix-b2:
name: "B2 ${{ matrix.toolset }}"
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
include:
- 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@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 update
sudo apt-get -y install python3 ${{ matrix.install }}
- name: Setup Boost
run: ./tools/ci.py setup-boost --source-dir=$(pwd)
- name: Build and run project tests using B2
run: |
python3 tools/ci.py run-b2-tests \
--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,50 +0,0 @@
name: Coverage
on:
push:
branches:
- master
jobs:
posix:
defaults:
run:
shell: bash
runs-on: ubuntu-22.04
env:
CXX: g++-11
CXXFLAGS: -std=c++20 -Wall -Wextra --coverage -fkeep-inline-functions -fkeep-static-functions -O0
LDFLAGS: --coverage
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Autotools
run: sudo apt install automake
- name: Install compiler
run: sudo apt-get install -y g++-11
- name: Install Redis
run: sudo apt-get install -y redis-server
- name: Install boost
uses: MarkusJx/install-boost@v2.3.0
id: install-boost
with:
boost_version: 1.79.0
platform_version: 22.04
- name: Configure
run: |
autoreconf -i
./configure --with-boost=${{ steps.install-boost.outputs.BOOST_ROOT }}
- name: Build
run: make
- name: Check
run: make check VERBOSE=1
# - name: Generate coverage report
# run: |
# lcov --base-directory . --directory tests/ --output-file aedis.info --capture
# lcov --remove aedis.info '/usr/*' "${{ steps.install-boost.outputs.BOOST_ROOT }}/include/boost/*" --output-file aedis.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
gcov: true
working_directory: ${{ env.GITHUB_WORKSPACE }}

4
BUILD_STATUS.md Normal file
View File

@@ -0,0 +1,4 @@
Branch | GH Actions | codecov.io |
:-------------: | ---------- | ---------- |
[`master`](https://github.com/mzimbres/aedis/tree/master) | [![CI](https://github.com/mzimbres/aedis/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/mzimbres/aedis/actions/workflows/ci.yml) | [![codecov](https://codecov.io/gh/mzimbres/aedis/branch/master/graph/badge.svg)](https://codecov.io/gh/mzimbres/aedis/branch/master)

View File

@@ -1,102 +0,0 @@
# Changelog
## 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 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 theses
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.1
* Fixes a bug that happens on very high load.
## v0.2.0
* 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 asynchrnous 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.2
* Adds reconnect coroutine in the `echo_server` example.
* Corrects `client::async_wait_for_data` with `make_parallel_group` to launch operation.
* Improvements in the documentation.
* Avoids dynamic memory allocation in the client class after reconnection.
## v0.1.1
* Improves the documentation and adds some features to the high-level client.
## v0.1.0
* Improvements in the design and documentation.
## v0.0.1
* First release to collect design feedback.

View File

@@ -1,50 +1,142 @@
# At the moment the official build system is still autotools and this
# file is meant to support Aedis on windows.
cmake_minimum_required(VERSION 3.8...3.20)
cmake_minimum_required(VERSION 3.14)
# determine whether it's main/root project
# or being built under another project.
if (NOT DEFINED BOOST_REDIS_MAIN_PROJECT)
set(BOOST_REDIS_MAIN_PROJECT OFF)
if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
set(BOOST_REDIS_MAIN_PROJECT ON)
endif()
endif()
project(
Aedis
VERSION 1.0.0
DESCRIPTION "An async redis client designed for performance and scalability"
HOMEPAGE_URL "https://mzimbres.github.io/aedis"
LANGUAGES CXX
)
project(boost_redis VERSION "${BOOST_SUPERPROJECT_VERSION}" LANGUAGES CXX)
add_library(aedis INTERFACE)
target_include_directories(aedis INTERFACE include)
# Library
add_library(boost_redis INTERFACE)
add_library(Boost::redis ALIAS boost_redis)
target_include_directories(boost_redis INTERFACE include)
target_compile_features(boost_redis INTERFACE cxx_std_17)
find_package(Boost 1.79 REQUIRED)
include_directories(${Boost_INCLUDE_DIRS})
# Dependencies
if (BOOST_REDIS_MAIN_PROJECT)
# 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)
enable_testing()
include_directories(include)
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
)
add_executable(chat_room examples/chat_room.cpp)
add_executable(containers examples/containers.cpp)
add_executable(echo_server examples/echo_server.cpp)
add_executable(intro examples/intro.cpp)
add_executable(intro_sync examples/intro_sync.cpp)
add_executable(serialization examples/serialization.cpp)
add_executable(subscriber examples/subscriber.cpp)
add_executable(subscriber_sync examples/subscriber_sync.cpp)
add_executable(test_low_level tests/low_level.cpp)
add_executable(test_connection tests/connection.cpp)
add_executable(low_level_sync tests/low_level_sync.cpp)
foreach(dep IN LISTS deps)
add_subdirectory(../${dep} boostorg/${dep})
endforeach()
add_test(containers containers)
add_test(intro intro)
add_test(intro_sync intro_sync)
add_test(serialization serialization)
add_test(test_low_level test_low_level)
add_test(test_connection test_connection)
add_test(low_level_sync low_level_sync)
find_package(Threads REQUIRED)
find_package(OpenSSL REQUIRED)
target_link_libraries(boost_redis
INTERFACE
Boost::system
Boost::asio
Threads::Threads
OpenSSL::Crypto
OpenSSL::SSL
)
else()
# If we're in the superproject or called from add_subdirectory,
# Boost dependencies should be already available.
# If other dependencies are not found, we bail out
find_package(Threads)
if(NOT Threads_FOUND)
message(STATUS "Boost.Redis has been disabled, because the required package Threads hasn't been found")
return()
endif()
find_package(OpenSSL)
if(NOT OpenSSL_FOUND)
message(STATUS "Boost.Redis has been disabled, because the required package OpenSSL hasn't been found")
return()
endif()
include(GNUInstallDirs)
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/boost
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING
PATTERN "*.hpp"
PATTERN "*.ipp"
)
# This is generated by boostdep
target_link_libraries(boost_redis
INTERFACE
Boost::asio
Boost::assert
Boost::core
Boost::mp11
Boost::system
Boost::throw_exception
Threads::Threads
OpenSSL::Crypto
OpenSSL::SSL
)
endif()
# Enable testing. If we're being called from the superproject, this has already been done
if (BOOST_REDIS_MAIN_PROJECT)
include(CTest)
endif()
# Most tests require a running Redis server, so we only run them if we're the main project
if(BOOST_REDIS_MAIN_PROJECT AND BUILD_TESTING)
# Tests and common utilities
add_subdirectory(test)
# Benchmarks. Build them with tests to prevent code rotting
add_subdirectory(benchmarks)
# Examples
add_subdirectory(example)
endif()

View File

@@ -1,96 +0,0 @@
AUTOMAKE_OPTIONS = subdir-objects
ACLOCAL_AMFLAGS = -I m4
AM_COLOR_TESTS = always
DISTCHECK_CONFIGURE_FLAGS = CPPFLAGS="$(BOOST_CPPFLAGS) $(CPPFLAGS)" LDFLAGS="$(BOOST_LDFLAGS)"
AM_CPPFLAGS =
AM_CPPFLAGS += $(BOOST_CPPFLAGS)
AM_CPPFLAGS += -I$(top_srcdir)/include
AM_LDFLAGS =
AM_LDFLAGS += -pthread
SUBDIRS = include
check_PROGRAMS =
check_PROGRAMS += low_level_sync
check_PROGRAMS += intro
check_PROGRAMS += intro_sync
check_PROGRAMS += containers
check_PROGRAMS += serialization
check_PROGRAMS += test_low_level
check_PROGRAMS += test_connection
EXTRA_PROGRAMS =
if HAVE_COROUTINES
EXTRA_PROGRAMS += subscriber
EXTRA_PROGRAMS += subscriber_sync
EXTRA_PROGRAMS += echo_server
EXTRA_PROGRAMS += echo_server_direct
EXTRA_PROGRAMS += chat_room
EXTRA_PROGRAMS += echo_server_client
endif
CLEANFILES =
CLEANFILES += $(EXTRA_PROGRAMS)
.PHONY: all
all: $(check_PROGRAMS) $(EXTRA_PROGRAMS)
low_level_sync_SOURCES = $(top_srcdir)/tests/low_level_sync.cpp
test_low_level_SOURCES = $(top_srcdir)/tests/low_level.cpp
intro_SOURCES = $(top_srcdir)/examples/intro.cpp
intro_sync_SOURCES = $(top_srcdir)/examples/intro_sync.cpp
containers_SOURCES = $(top_srcdir)/examples/containers.cpp
serialization_SOURCES = $(top_srcdir)/examples/serialization.cpp
test_connection_SOURCES = $(top_srcdir)/tests/connection.cpp
subscriber_sync_SOURCES = $(top_srcdir)/examples/subscriber_sync.cpp
if HAVE_COROUTINES
subscriber_SOURCES = $(top_srcdir)/examples/subscriber.cpp
chat_room_SOURCES = $(top_srcdir)/examples/chat_room.cpp
echo_server_SOURCES = $(top_srcdir)/examples/echo_server.cpp
echo_server_direct_SOURCES = $(top_srcdir)/benchmarks/cpp/asio/echo_server_direct.cpp
echo_server_client_SOURCES = $(top_srcdir)/benchmarks/cpp/asio/echo_server_client.cpp
endif
nobase_noinst_HEADERS = $(top_srcdir)/examples/print.hpp
TESTS = $(check_PROGRAMS)
EXTRA_DIST =
EXTRA_DIST += $(top_srcdir)/README.md
EXTRA_DIST += $(top_srcdir)/doc/DoxygenLayout.xml
EXTRA_DIST += $(top_srcdir)/doc/aedis.css
EXTRA_DIST += $(top_srcdir)/doc/htmlfooter.html
EXTRA_DIST += $(top_srcdir)/doc/htmlheader.html
EXTRA_DIST += $(top_srcdir)/benchmarks/benchmarks.md
EXTRA_DIST += $(top_srcdir)/benchmarks/benchmarks.tex
EXTRA_DIST += $(top_srcdir)/benchmarks/c/libuv/echo_server_direct.c
EXTRA_DIST += $(top_srcdir)/benchmarks/c/libuv/README.md
EXTRA_DIST += $(top_srcdir)/benchmarks/go/echo_server_direct.go
EXTRA_DIST += $(top_srcdir)/benchmarks/nodejs/echo_server_direct/echo_server_direct.js
EXTRA_DIST += $(top_srcdir)/benchmarks/nodejs/echo_server_direct/package.json
EXTRA_DIST += $(top_srcdir)/benchmarks/nodejs/echo_server_over_redis/echo_server_over_redis.js
EXTRA_DIST += $(top_srcdir)/benchmarks/nodejs/echo_server_over_redis/package.json
EXTRA_DIST += $(top_srcdir)/benchmarks/rust/echo_server_direct/Cargo.toml
EXTRA_DIST += $(top_srcdir)/benchmarks/rust/echo_server_direct/src/main.rs
EXTRA_DIST += $(top_srcdir)/benchmarks/rust/echo_server_over_redis/Cargo.toml
EXTRA_DIST += $(top_srcdir)/benchmarks/rust/echo_server_over_redis/src/main.rs
EXTRA_DIST += $(top_srcdir)/CMakeLists.txt
.PHONY: doc
doc:
doxygen doc/Doxyfile
.PHONY: coverage
coverage:
lcov --base-directory . --directory tests/ --output-file aedis.info --capture
lcov --remove aedis.info '/usr/*' '/opt/boost_1_79_0/include/boost/*' --output-file aedis.info
genhtml --output-directory html aedis.info
.PHONY: bench
bench:
pdflatex --jobname=echo-f0 benchmarks/benchmarks.tex
pdflatex --jobname=echo-f1 benchmarks/benchmarks.tex

139
README.md
View File

@@ -1,20 +1,137 @@
Branch | GH Actions | codecov.io |
:-------------: | ---------- | ---------- |
[`master`](https://github.com/mzimbres/aedis/tree/master) | [![CI](https://github.com/mzimbres/aedis/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/mzimbres/aedis/actions/workflows/ci.yml) | [![codecov](https://codecov.io/gh/mzimbres/aedis/branch/master/graph/badge.svg)](https://codecov.io/gh/mzimbres/aedis/branch/master)
# Boost.Redis
## Aedis
Boost.Redis is a high-level [Redis](https://redis.io/) client library built on top of
[Boost.Asio](https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html)
that implements the Redis protocol
[RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md).
An async redis client designed for performance and scalability
Full documentation is [here](https://www.boost.org/doc/libs/master/libs/redis/index.html).
### License
## Requirements
Distributed under the [Boost Software License, Version 1.0](http://www.boost.org/LICENSE_1_0.txt).
The requirements for using Boost.Redis are:
### More information
* Boost 1.84 or higher. Boost.Redis is included in Boost installations since Boost 1.84.
* 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 (must support RESP3).
* OpenSSL.
* See the official github-pages for documentation: https://mzimbres.github.io/aedis
The documentation assumes basic-level knowledge about [Redis](https://redis.io/docs/) and [Boost.Asio](https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html).
### Installation
## Building the library
See https://mzimbres.github.io/aedis/#using-aedis
To use the library it is necessary to include the following:
```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
[ping](https://redis.io/commands/ping/) a Redis server:
```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:
* `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.
* `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:
* [Pubsub messages](https://redis.io/docs/manual/pubsub/).
* [Keyspace notifications](https://redis.io/docs/manual/keyspace-notifications/).
* [Client-side caching](https://redis.io/docs/manual/client-side-caching/).
The connection class supports server pushes by means of the
`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
```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
Full documentation is [here](https://www.boost.org/doc/libs/master/libs/redis/index.html).

20
benchmarks/CMakeLists.txt Normal file
View File

@@ -0,0 +1,20 @@
add_library(benchmarks_options INTERFACE)
target_link_libraries(benchmarks_options INTERFACE boost_redis_src)
target_link_libraries(benchmarks_options INTERFACE boost_redis_project_options)
target_compile_features(benchmarks_options INTERFACE cxx_std_20)
add_executable(echo_server_client cpp/asio/echo_server_client.cpp)
target_link_libraries(echo_server_client PRIVATE benchmarks_options)
add_executable(echo_server_direct cpp/asio/echo_server_direct.cpp)
target_link_libraries(echo_server_direct PRIVATE benchmarks_options)
# TODO
#=======================================================================
#.PHONY: bench
#bench:
# pdflatex --jobname=echo-f0 benchmarks/benchmarks.tex
# pdflatex --jobname=echo-f1 benchmarks/benchmarks.tex
# pdftoppm {input.pdf} {output.file} -png

View File

@@ -1,87 +0,0 @@
# TCP echo server performance
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 excelent 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.
## No 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
![](https://mzimbres.github.io/aedis/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.
### Remarks
* 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 performance did not surprise me: decent and not some much far behind nodejs.
The code used in the benchmarks can be found at
* [Asio](https://github.com/mzimbres/aedis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/cpp/asio/echo_server_direct.cpp): A variation of [this](https://github.com/chriskohlhoff/asio/blob/4915cfd8a1653c157a1480162ae5601318553eb8/asio/src/examples/cpp20/coroutines/echo_server.cpp) Asio example.
* [Libuv](https://github.com/mzimbres/aedis/tree/835a1decf477b09317f391eddd0727213cdbe12b/benchmarks/c/libuv): Taken from [here](https://github.com/libuv/libuv/blob/06948c6ee502862524f233af4e2c3e4ca876f5f6/docs/code/tcp-echo-server/main.c) Libuv example .
* [Tokio](https://github.com/mzimbres/aedis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/rust/echo_server_direct): Taken from [here](https://docs.rs/tokio/latest/tokio/).
* [Nodejs](https://github.com/mzimbres/aedis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/nodejs/echo_server_direct)
* [Go](https://github.com/mzimbres/aedis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/go/echo_server_direct.go)
## Echo over 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
![](https://mzimbres.github.io/aedis/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.
### Remarks
As the reader can see, the Libuv and the Rust test are not depicted
in the graph, the reasons are
* [redis-rs](https://github.com/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 proper
[pipelining](https://redis.io/docs/manual/pipelining/) support.
In fact, the more TCP connections I lauch the worst its
performance gets.
* Libuv: I left it out because it would require too much work to
write it and make it have a good performance. More specifically,
I would have to use hiredis and implement support for pipelines
manually.
The code used in the benchmarks can be found at
* [Aedis](https://github.com/mzimbres/aedis): [code](https://github.com/mzimbres/aedis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/examples/echo_server.cpp)
* [node-redis](https://github.com/redis/node-redis): [code](https://github.com/mzimbres/aedis/tree/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/nodejs/echo_server_over_redis)
* [go-redis](https://github.com/go-redis/redis): [code](https://github.com/mzimbres/aedis/blob/3fb018ccc6138d310ac8b73540391cdd8f2fdad6/benchmarks/go/echo_server_over_redis.go)
## Running the benchmarks
Run one of the echo-server programs in one terminal and the [echo-server-client](https://github.com/mzimbres/aedis/blob/42880e788bec6020dd018194075a211ad9f339e8/benchmarks/cpp/asio/echo_server_client.cpp) in another.

View File

@@ -17,27 +17,25 @@
width=15cm, height=6cm, enlarge y limits=0.5,
title={TCP Echo Server Performance},
xlabel={Seconds},
symbolic y coords={Asio,Tokio,Libuv,Nodejs,Go},
symbolic y coords={Asio,Tokio,Go,Libuv,Nodejs},
ytick=data,
%bar width=1cm,
nodes near coords,
nodes near coords align={horizontal},
]
\addplot coordinates {
(31.1,Asio)
(29.5,Asio)
(30.7,Tokio)
(35.6,Go)
(43.6,Libuv)
(74.2,Nodejs)
(81.0,Go)
};
\end{axis}
\end{tikzpicture}
\endpgfgraphicnamed
\beginpgfgraphicnamed{echo-f1}
%debian2[0]$ time ./echo_server_client 1000 1000
%Go (1): 1.000s
%C++ (1): 0.07s
%$ time ./echo_server_client 1000 1000
\begin{tikzpicture}[scale=1.0]
\begin{axis}[
y dir=reverse,

View File

@@ -4,17 +4,18 @@
* accompanying file LICENSE.txt)
*/
#include <iostream>
#include <boost/asio.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace net = boost::asio;
using net::ip::tcp;
using tcp_socket = net::use_awaitable_t<>::as_default_on_t<net::ip::tcp::socket>;
using timer_type = net::use_awaitable_t<>::as_default_on_t<net::steady_timer>;
net::awaitable<void>
example(boost::asio::ip::tcp::endpoint ep, std::string msg, int n)
auto example(boost::asio::ip::tcp::endpoint ep, std::string msg, int n) -> net::awaitable<void>
{
try {
auto ex = co_await net::this_coro::executor;
@@ -26,9 +27,9 @@ example(boost::asio::ip::tcp::endpoint ep, std::string msg, int n)
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,3 +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)

View File

@@ -8,9 +8,12 @@
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <cstdio>
#include <boost/asio.hpp>
#include <cstdio>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace net = boost::asio;
namespace this_coro = net::this_coro;
using net::ip::tcp;
@@ -25,34 +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& e) {
//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)

View File

@@ -1,29 +1,31 @@
package main
import (
"bufio"
"fmt"
"io"
"net"
"os"
"runtime"
)
func echo(conn net.Conn) {
r := bufio.NewReader(conn)
buf := make([]byte, 1024)
for {
line, err := r.ReadBytes(byte('\n'))
switch err {
case nil:
break
case io.EOF:
default:
fmt.Println("ERROR", err)
}
conn.Write(line)
n, err := conn.Read(buf)
if err != nil {
break;
}
conn.Write(buf[:n])
if err != nil {
fmt.Println("ERROR", err)
os.Exit(1)
}
}
}
func main() {
runtime.GOMAXPROCS(1)
l, err := net.Listen("tcp", "0.0.0.0:55555")
if err != nil {
fmt.Println("ERROR", err)

View File

@@ -0,0 +1,4 @@
$ npm install
$ node echo_server_over_redis.js

View File

@@ -1,7 +1,7 @@
import { createClient } from 'redis';
import * as net from 'net';
const client = createClient({url: 'redis://db.occase.de:6379' });
const client = createClient({url: 'redis://aedis.occase.de:63799' });
client.on('error', (err) => console.log('Redis Client Error', err));
await client.connect();

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
;

View File

@@ -1,50 +0,0 @@
AC_PREREQ([2.69])
AC_INIT([Aedis], [1.0.0], [mzimbres@gmail.com])
AC_CONFIG_MACRO_DIR([m4])
AC_CONFIG_HEADERS([config.h])
AC_CONFIG_SRCDIR(include/aedis.hpp)
AM_INIT_AUTOMAKE([-Wall foreign])
AC_LANG(C++)
# Checks for programs.
AC_PROG_CXX
AC_PROG_INSTALL
AC_PROG_LN_S
AC_PROG_RANLIB
AM_PROG_AR
AX_BOOST_BASE([1.78],, AC_MSG_ERROR[Boost not found])
AC_CHECK_HEADER_STDBOOL
AC_TYPE_UINT64_T
AC_CHECK_TYPES([ptrdiff_t])
# This check has been stolen from Asio
AC_MSG_CHECKING([whether coroutines are enabled])
AC_COMPILE_IFELSE(
[AC_LANG_PROGRAM(
[[#if defined(__clang__)]]
[[# if (__cplusplus >= 201703) && (__cpp_coroutines >= 201703)]]
[[# if __has_include(<experimental/coroutine>)]]
[[# define AEDIS_HAS_CO_AWAIT 1]]
[[# endif]]
[[# endif]]
[[#elif defined(__GNUC__)]]
[[# if (__cplusplus >= 201709) && (__cpp_impl_coroutine >= 201902)]]
[[# if __has_include(<coroutine>)]]
[[# define AEDIS_HAS_CO_AWAIT 1]]
[[# endif]]
[[# endif]]
[[#endif]]
[[#ifndef AEDIS_HAS_CO_AWAIT]]
[[# error coroutines not available]]
[[#endif]])],
[AC_MSG_RESULT([yes])
HAVE_COROUTINES=yes;],
[AC_MSG_RESULT([no])
HAVE_COROUTINES=no;])
AM_CONDITIONAL(HAVE_COROUTINES,test x$HAVE_COROUTINES = xyes)
AC_CONFIG_FILES([Makefile include/Makefile doc/Doxyfile])
AC_OUTPUT

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)

File diff suppressed because it is too large Load Diff

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="no" 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>

24
doc/Jamfile Normal file
View File

@@ -0,0 +1,24 @@
#
# 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)
#
# Adapted from Boost.Unordered
make html/index.html : build_antora.sh : @run-script ;
# Runs the Antora script
actions run-script
{
bash -x $(>)
}
# These are used to inform the build system of the
# means to build the integrated and stand-alone docs.
alias boostdoc ;
explicit boostdoc ;
alias boostrelease : html/index.html ;
explicit boostrelease ;

View File

@@ -1,30 +0,0 @@
/* Doxygen HTML_EXTRA_STYLESHEET */
div.contents {
max-width: 100em;
margin-right: 5em;
margin-left: 5em;
}
.ui-resizable-e {
background-image:url("splitbar.png");
background-size:100%;
background-repeat:repeat-y;
background-attachment: scroll;
cursor:ew-resize;
height:100%;
right:0;
top:0;
width:1px;
}
.pyrootbox {
border: 1px solid #879ecb;
background-color: #f9fafc;
padding: 15px;
}
code
{
background-color:#fffbeb;
}

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,19 +0,0 @@
<!-- HTML footer for doxygen 1.8.14-->
<!-- start footer part -->
<!--BEGIN GENERATE_TREEVIEW-->
<div id="nav-path" class="navpath"><!-- id is needed for treeview function! -->
<ul>
$navpath
<li class="footer">
Aedis 1.0.0 - Reference Guide generated on $datetime using Doxygen $doxygenversion &#160;&#160;
<img class="footer" src="rootlogo_s.gif" alt="root"/></li>
</ul>
</div>
<!--END GENERATE_TREEVIEW-->
<!--BEGIN !GENERATE_TREEVIEW-->
<hr class="footer"/><address class="footer">
Author: Marcelo Zimbres Silva.
</address>
<!--END !GENERATE_TREEVIEW-->
</body>
</html>

View File

@@ -1,34 +0,0 @@
<!-- HTML header for doxygen 1.8.14-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=9"/>
<meta name="generator" content="Doxygen $doxygenversion"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!--BEGIN PROJECT_NAME--><title>$projectname: $title</title><!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME--><title>$title</title><!--END !PROJECT_NAME-->
<link href="$relpath^tabs.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="$relpath^jquery.js"></script>
<script type="text/javascript" src="$relpath^dynsections.js"></script>
$search
<link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
$extrastylesheet
</head>
<body>
<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
<!--BEGIN TITLEAREA-->
<div id="titlearea">
<table bgcolor="#346295" cellspacing="0" cellpadding="6">
<tbody>
<tr>
<td valign="middle" style="color: #FFFFFF" nowrap="nowrap"><font size="6">$projectname $projectnumber</font> &#160; <br> $projectbrief </td>
<td style="width:100%"> $searchbox </td>
</tr>
</tbody>
</table>
</div>
<!--END TITLEAREA-->
<!-- end header part -->

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

61
example/CMakeLists.txt Normal file
View File

@@ -0,0 +1,61 @@
add_library(examples_main STATIC main.cpp)
target_compile_features(examples_main PRIVATE cxx_std_20)
target_link_libraries(examples_main PRIVATE boost_redis_project_options)
macro(make_example EXAMPLE_NAME STANDARD)
add_executable(${EXAMPLE_NAME} ${EXAMPLE_NAME}.cpp)
target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_redis_src)
target_link_libraries(${EXAMPLE_NAME} PRIVATE boost_redis_project_options)
target_compile_features(${EXAMPLE_NAME} PRIVATE cxx_std_${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})
add_test(${EXAMPLE_NAME} ${EXAMPLE_NAME} $ENV{BOOST_REDIS_TEST_SERVER} 6379)
endmacro()
make_testable_example(cpp17_intro 17)
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_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_intro_tls 20)
# We test the protobuf example only on gcc.
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
find_package(Protobuf)
if (Protobuf_FOUND)
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS person.proto)
make_testable_example(cpp20_protobuf 20)
target_sources(cpp20_protobuf PUBLIC ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(cpp20_protobuf PRIVATE ${Protobuf_LIBRARIES})
target_include_directories(cpp20_protobuf PUBLIC ${Protobuf_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})
endif()
endif()
if (NOT MSVC)
make_example(cpp20_chat_room 20)
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()

51
example/cpp17_intro.cpp Normal file
View File

@@ -0,0 +1,51 @@
/* 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/detached.hpp>
#include <iostream>
namespace asio = boost::asio;
using boost::redis::connection;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
auto main(int argc, char* argv[]) -> int
{
try {
config cfg;
if (argc == 3) {
cfg.addr.host = argv[1];
cfg.addr.port = argv[2];
}
request req;
req.push("PING", "Hello world");
response<std::string> resp;
asio::io_context ioc;
connection conn{ioc};
conn.async_run(cfg, asio::detached);
conn.async_exec(req, resp, [&](auto ec, auto) {
if (!ec)
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
conn.cancel();
});
ioc.run();
} catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
}

View File

@@ -0,0 +1,43 @@
/* 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 "sync_connection.hpp"
#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
{
try {
config cfg;
if (argc == 3) {
cfg.addr.host = argv[1];
cfg.addr.port = argv[2];
}
sync_connection conn;
conn.run(cfg);
request req;
req.push("PING");
response<std::string> resp;
conn.exec(req, resp);
conn.stop();
std::cout << "Response: " << std::get<0>(resp).value() << std::endl;
} catch (std::exception const& e) {
std::cerr << e.what() << std::endl;
}
}

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;
}
}

123
example/cpp20_chat_room.cpp Normal file
View File

@@ -0,0 +1,123 @@
/* 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/as_tuple.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/posix/stream_descriptor.hpp>
#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 asio::posix::stream_descriptor;
using asio::signal_set;
using boost::asio::async_read_until;
using boost::asio::awaitable;
using boost::asio::co_spawn;
using boost::asio::consign;
using boost::asio::detached;
using boost::asio::dynamic_buffer;
using boost::redis::config;
using boost::redis::connection;
using boost::redis::generic_flat_response;
using boost::redis::request;
using boost::system::error_code;
using namespace std::chrono_literals;
// Chat over Redis pubsub. To test, run this program from multiple
// terminals and type messages to stdin.
namespace {
auto rethrow_on_error = [](std::exception_ptr exc) {
if (exc)
std::rethrow_exception(exc);
};
auto receiver(std::shared_ptr<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);
// 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>
{
for (std::string msg;;) {
auto n = co_await async_read_until(*in, dynamic_buffer(msg, 1024), "\n");
request req;
req.push("PUBLISH", "channel", msg);
co_await conn->async_exec(req);
msg.erase(0, n);
}
}
} // namespace
// Called from the main function (see main.cpp)
auto co_main(config cfg) -> awaitable<void>
{
auto ex = co_await asio::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
auto stream = std::make_shared<stream_descriptor>(ex, ::dup(STDIN_FILENO));
co_spawn(ex, receiver(conn), rethrow_on_error);
co_spawn(ex, publisher(stream, conn), rethrow_on_error);
conn->async_run(cfg, consign(detached, conn));
signal_set sig_set{ex, SIGINT, SIGTERM};
co_await sig_set.async_wait();
conn->cancel();
stream->cancel();
}
#else // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
auto co_main(config const&) -> awaitable<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)

View File

@@ -0,0 +1,146 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
#include <map>
#include <vector>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace asio = boost::asio;
using boost::redis::request;
using boost::redis::response;
using boost::redis::ignore_t;
using boost::redis::ignore;
using boost::redis::config;
using boost::redis::connection;
using boost::asio::awaitable;
using boost::asio::detached;
using boost::asio::consign;
template <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)
std::cout << e.first << ": " << e.second << "\n";
}
template <class T>
void print(std::vector<T> const& cont)
{
for (auto const& e : cont)
std::cout << e << " ";
std::cout << "\n";
}
// Stores the content of some STL containers in Redis.
auto store(std::shared_ptr<connection> conn) -> awaitable<void>
{
std::vector<int> vec{1, 2, 3, 4, 5, 6};
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);
}
auto hgetall(std::shared_ptr<connection> conn) -> awaitable<void>
{
// A request contains multiple commands.
request req;
req.push("HGETALL", "hset-key");
// Responses as tuple elements.
response<std::map<std::string, std::string>> resp;
// Executes the request and reads the response.
co_await conn->async_exec(req, resp);
print(std::get<0>(resp).value());
}
auto mget(std::shared_ptr<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());
}
// Retrieves in a transaction.
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("MGET", "key", "non-existing-key");
req.push("EXEC");
response<
ignore_t, // multi
ignore_t, // lrange
ignore_t, // hgetall
ignore_t, // mget
response<
std::optional<std::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);
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));
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)

View File

@@ -0,0 +1,72 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.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 boost::asio::signal_set;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
using boost::system::error_code;
using boost::redis::connection;
using namespace std::chrono_literals;
auto echo_server_session(asio::ip::tcp::socket socket, std::shared_ptr<connection> conn)
-> asio::awaitable<void>
{
request req;
response<std::string> resp;
for (std::string buffer;;) {
auto n = co_await asio::async_read_until(socket, asio::dynamic_buffer(buffer, 1024), "\n");
req.push("PING", buffer);
co_await conn->async_exec(req, resp);
co_await asio::async_write(socket, asio::buffer(std::get<0>(resp).value()));
std::get<0>(resp).value().clear();
req.clear();
buffer.erase(0, n);
}
}
// Listens for tcp connections.
auto listener(std::shared_ptr<connection> conn) -> asio::awaitable<void>
{
try {
auto ex = co_await asio::this_coro::executor;
asio::ip::tcp::acceptor acc(ex, {asio::ip::tcp::v4(), 55555});
for (;;)
asio::co_spawn(ex, echo_server_session(co_await acc.async_accept(), conn), asio::detached);
} catch (std::exception const& e) {
std::clog << "Listener: " << e.what() << std::endl;
}
}
// Called from the main function (see main.cpp)
auto co_main(config cfg) -> asio::awaitable<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));
signal_set sig_set(ex, SIGINT, SIGTERM);
co_await sig_set.async_wait();
conn->cancel();
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

43
example/cpp20_intro.cpp Normal file
View File

@@ -0,0 +1,43 @@
/* 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/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>
{
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.
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

@@ -0,0 +1,57 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.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)
namespace asio = boost::asio;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
using boost::redis::logger;
using boost::redis::connection;
auto verify_certificate(bool, asio::ssl::verify_context&) -> bool
{
std::cout << "set_verify_callback" << std::endl;
return true;
}
auto co_main(config cfg) -> asio::awaitable<void>
{
cfg.use_ssl = true;
cfg.username = "aedis";
cfg.password = "aedis";
cfg.addr.host = "db.occase.de";
cfg.addr.port = "6380";
asio::ssl::context ctx{asio::ssl::context::tlsv12_client};
ctx.set_verify_mode(asio::ssl::verify_peer);
ctx.set_verify_callback(verify_certificate);
auto conn = std::make_shared<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;
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)

82
example/cpp20_json.cpp Normal file
View File

@@ -0,0 +1,82 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/describe.hpp>
#include <iostream>
#include <string>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
#include <boost/redis/resp3/serialization.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 user {
std::string name;
std::string age;
std::string country;
};
// The type must be described for serialization to work.
BOOST_DESCRIBE_STRUCT(user, (), (name, age, country))
// Boost.Redis customization points (example/json.hpp)
void boost_redis_to_bulk(std::string& to, user const& u)
{
resp3::boost_redis_to_bulk(to, boost::json::serialize(boost::json::value_from(u)));
}
void boost_redis_from_bulk(user& u, node_view const& node, boost::system::error_code&)
{
u = boost::json::value_to<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));
// user object that will be stored in Redis in json format.
user const u{"Joao", "58", "Brazil"};
// Stores and retrieves in the same request.
request req;
req.push("SET", "json-key", u); // Stores in Redis.
req.push("GET", "json-key"); // Retrieves from Redis.
response<ignore_t, user> resp;
co_await conn->async_exec(req, resp);
conn->cancel();
// Prints the first ping
std::cout << "Name: " << std::get<1>(resp).value().name << "\n"
<< "Age: " << std::get<1>(resp).value().age << "\n"
<< "Country: " << std::get<1>(resp).value().country << "\n";
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -0,0 +1,89 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/asio/co_spawn.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
// generated by CMakeLists.txt.
#include "person.pb.h"
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace asio = boost::asio;
namespace resp3 = boost::redis::resp3;
using boost::redis::request;
using boost::redis::response;
using boost::redis::operation;
using boost::redis::ignore_t;
using boost::redis::config;
using boost::redis::connection;
using boost::redis::resp3::node_view;
// The protobuf type described in example/person.proto
using tutorial::person;
// Boost.Redis customization points (example/protobuf.hpp)
namespace tutorial {
// Below I am using a Boost.Redis to indicate a protobuf error, this
// is ok for an example, users however might want to define their own
// error codes.
void boost_redis_to_bulk(std::string& to, person const& u)
{
std::string tmp;
if (!u.SerializeToString(&tmp))
throw boost::system::system_error(boost::redis::error::invalid_data_type);
resp3::boost_redis_to_bulk(to, tmp);
}
void boost_redis_from_bulk(person& u, node_view const& node, boost::system::error_code& ec)
{
std::string const tmp{node.value};
if (!u.ParseFromString(tmp))
ec = boost::redis::error::invalid_data_type;
}
} // namespace tutorial
using tutorial::boost_redis_to_bulk;
using tutorial::boost_redis_from_bulk;
asio::awaitable<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));
person p;
p.set_name("Louis");
p.set_id(3);
p.set_email("No email yet.");
request req;
req.push("SET", "protobuf-key", p);
req.push("GET", "protobuf-key");
response<ignore_t, person> resp;
// Sends the request and receives the response.
co_await conn->async_exec(req, resp);
conn->cancel();
std::cout << "Name: " << std::get<1>(resp).value().name() << "\n"
<< "Age: " << std::get<1>(resp).value().id() << "\n"
<< "Email: " << std::get<1>(resp).value().email() << "\n";
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

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)

97
example/cpp20_streams.cpp Normal file
View File

@@ -0,0 +1,97 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.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)
#include <memory>
#include <string>
#include <thread>
#include <vector>
namespace net = boost::asio;
using boost::redis::config;
using boost::redis::generic_response;
using boost::redis::operation;
using boost::redis::request;
using boost::redis::connection;
using net::signal_set;
auto stream_reader(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
std::string redisStreamKey_;
request req;
generic_response resp;
std::string stream_id{"$"};
std::string const field = "myfield";
for (;;) {
req.push("XREAD", "BLOCK", "0", "STREAMS", "test-topic", stream_id);
co_await conn->async_exec(req, resp);
//std::cout << "Response: ";
//for (auto i = 0UL; i < resp->size(); ++i) {
// std::cout << resp->at(i).value << ", ";
//}
//std::cout << std::endl;
// The following approach was taken in order to be able to
// deal with the responses, as generated by redis in the case
// that there are multiple stream 'records' within a single
// generic_response. The nesting and number of values in
// resp.value() are different, depending on the contents
// of the stream in redis. Uncomment the above commented-out
// code for examples while running the XADD command.
std::size_t item_index = 0;
while (item_index < std::size(resp.value())) {
auto const& val = resp.value().at(item_index).value;
if (field.compare(val) == 0) {
// We've hit a myfield field.
// The streamId is located at item_index - 2
// The payload is located at item_index + 1
stream_id = resp.value().at(item_index - 2).value;
std::cout << "StreamId: " << stream_id << ", "
<< "MyField: " << resp.value().at(item_index + 1).value << std::endl;
++item_index; // We can increase so we don't read this again
}
++item_index;
}
req.clear();
resp.value().clear();
}
}
// Run this in another terminal:
// redis-cli -r 100000 -i 0.0001 XADD "test-topic" "*" "myfield" "myfieldvalue1"
auto co_main(config cfg) -> net::awaitable<void>
{
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex);
net::co_spawn(ex, stream_reader(conn), net::detached);
// Disable health checks.
cfg.health_check_interval = std::chrono::seconds::zero();
conn->async_run(cfg, net::consign(net::detached, conn));
signal_set sig_set(ex, SIGINT, SIGTERM);
co_await sig_set.async_wait();
conn->cancel();
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

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)
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/as_tuple.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)
namespace asio = boost::asio;
using namespace std::chrono_literals;
using boost::redis::request;
using boost::redis::generic_flat_response;
using boost::redis::config;
using boost::system::error_code;
using boost::redis::connection;
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 mychannel some-message
* (integer) 3
* 127.0.0.1:6379>
*
* To test reconnection try, for example, to close all clients currently
* connected to the Redis instance
*
* $ redis-cli
* > CLIENT kill TYPE pubsub
*/
// Receives server pushes.
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();
}
}
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));
signal_set sig_set(ex, SIGINT, SIGTERM);
co_await sig_set.async_wait();
conn->cancel();
}
#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

55
example/main.cpp Normal file
View File

@@ -0,0 +1,55 @@
/* 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/config.hpp>
#include <boost/redis/connection.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <iostream>
namespace asio = boost::asio;
using boost::redis::config;
using boost::redis::logger;
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
extern asio::awaitable<void> co_main(config);
auto main(int argc, char* argv[]) -> int
{
try {
config cfg;
if (argc == 3) {
cfg.addr.host = argv[1];
cfg.addr.port = argv[2];
}
asio::io_context ioc;
asio::co_spawn(ioc, co_main(cfg), [](std::exception_ptr p) {
if (p)
std::rethrow_exception(p);
});
ioc.run();
} catch (std::exception const& e) {
std::cerr << "(main) " << e.what() << std::endl;
return 1;
}
}
#else // defined(BOOST_ASIO_HAS_CO_AWAIT)
auto main() -> int
{
std::cout << "Requires coroutine support." << std::endl;
return 0;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

9
example/person.proto Normal file
View File

@@ -0,0 +1,9 @@
syntax = "proto2";
package tutorial;
message person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
}

View File

@@ -0,0 +1,63 @@
/* 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/connection.hpp>
#include <boost/redis/request.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/use_future.hpp>
#include <chrono>
#include <thread>
using namespace std::chrono_literals;
namespace boost::redis {
class sync_connection {
public:
sync_connection()
: ioc_{1}
, conn_{std::make_shared<connection>(ioc_)}
{ }
~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);
ioc_.run();
}};
}
void stop()
{
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();
}
private:
asio::io_context ioc_{1};
std::shared_ptr<connection> conn_;
std::thread thread_;
};
} // namespace boost::redis

View File

@@ -1,96 +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 <string>
#include <iostream>
#include <boost/asio.hpp>
#include <aedis.hpp>
#include "unistd.h"
#include "print.hpp"
// Include this in no more than one .cpp file.
#include <aedis/src.hpp>
#if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
namespace net = boost::asio;
using aedis::adapt;
using aedis::resp3::request;
using aedis::resp3::node;
using tcp_socket = net::use_awaitable_t<>::as_default_on_t<net::ip::tcp::socket>;
using tcp_acceptor = net::use_awaitable_t<>::as_default_on_t<net::ip::tcp::acceptor>;
using stream_descriptor = net::use_awaitable_t<>::as_default_on_t<net::posix::stream_descriptor>;
using connection = aedis::connection<tcp_socket>;
// Chat over redis pubsub. To test, run this program from different
// terminals and type messages to stdin. Use
//
// $ redis-cli monitor
//
// to monitor the message traffic.
// Receives messages from other users.
net::awaitable<void> push_receiver(std::shared_ptr<connection> db)
{
for (std::vector<node<std::string>> resp;;) {
co_await db->async_receive_push(adapt(resp));
print_push(resp);
resp.clear();
}
}
// Subscribes to the channels when a new connection is stablished.
net::awaitable<void> event_receiver(std::shared_ptr<connection> db)
{
request req;
req.push("SUBSCRIBE", "chat-channel");
for (;;) {
auto ev = co_await db->async_receive_event();
if (ev == connection::event::hello)
co_await db->async_exec(req);
}
}
// Publishes messages to other users.
net::awaitable<void> publisher(stream_descriptor& in, std::shared_ptr<connection> db)
{
for (std::string msg;;) {
auto n = co_await net::async_read_until(in, net::dynamic_buffer(msg, 1024), "\n");
request req;
req.push("PUBLISH", "chat-channel", msg);
co_await db->async_exec(req);
msg.erase(0, n);
}
}
int main()
{
try {
net::io_context ioc{1};
stream_descriptor in{ioc, ::dup(STDIN_FILENO)};
auto db = std::make_shared<connection>(ioc);
db->get_config().enable_events = true;
db->get_config().enable_reconnect = true;
co_spawn(ioc, publisher(in, db), net::detached);
co_spawn(ioc, push_receiver(db), net::detached);
co_spawn(ioc, event_receiver(db), net::detached);
db->async_run(net::detached);
net::signal_set signals(ioc, SIGINT, SIGTERM);
signals.async_wait([&](auto, auto){ ioc.stop(); });
ioc.run();
} catch (std::exception const& e) {
std::cerr << e.what() << std::endl;
}
}
#else // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
int main() {}
#endif // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)

View File

@@ -1,59 +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 <map>
#include <vector>
#include <iostream>
#include <aedis.hpp>
#include "print.hpp"
// Include this in no more than one .cpp file.
#include <aedis/src.hpp>
namespace net = boost::asio;
using boost::optional;
using aedis::adapt;
using aedis::resp3::request;
using connection = aedis::connection<>;
int main()
{
std::vector<int> vec
{1, 2, 3, 4, 5, 6};
std::map<std::string, int> map
{{"key1", 10}, {"key2", 20}, {"key3", 30}};
request req;
req.push_range("RPUSH", "rpush-key", vec); // Sends
req.push_range("HSET", "hset-key", map); // Sends
req.push("MULTI");
req.push("LRANGE", "rpush-key", 0, -1); // Retrieves
req.push("HGETALL", "hset-key"); // Retrieves
req.push("EXEC");
req.push("QUIT");
std::tuple<
aedis::ignore, // rpush
aedis::ignore, // hset
aedis::ignore, // multi
aedis::ignore, // lrange
aedis::ignore, // hgetall
std::tuple<optional<std::vector<int>>, optional<std::map<std::string, int>>>, // exec
aedis::ignore // quit
> resp;
net::io_context ioc;
connection db{ioc};
db.async_run(req, adapt(resp), [](auto ec, auto) {
std::cout << ec.message() << std::endl;
});
ioc.run();
print(std::get<0>(std::get<5>(resp)).value());
print(std::get<1>(std::get<5>(resp)).value());
}

View File

@@ -1,62 +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 <string>
#include <iostream>
#include <boost/asio.hpp>
#include <aedis.hpp>
// Include this in no more than one .cpp file.
#include <aedis/src.hpp>
namespace net = boost::asio;
using aedis::adapt;
using aedis::resp3::request;
using executor_type = net::io_context::executor_type;
using socket_type = net::basic_stream_socket<net::ip::tcp, executor_type>;
using tcp_socket = net::use_awaitable_t<executor_type>::as_default_on_t<socket_type>;
using acceptor_type = net::basic_socket_acceptor<net::ip::tcp, executor_type>;
using tcp_acceptor = net::use_awaitable_t<executor_type>::as_default_on_t<acceptor_type>;
using awaitable_type = net::awaitable<void, executor_type>;
using connection = aedis::connection<tcp_socket>;
awaitable_type echo_loop(tcp_socket socket, std::shared_ptr<connection> db)
{
request req;
std::tuple<std::string> resp;
for (std::string buffer;;) {
auto n = co_await net::async_read_until(socket, net::dynamic_buffer(buffer, 1024), "\n");
req.push("PING", buffer);
co_await db->async_exec(req, adapt(resp));
co_await net::async_write(socket, net::buffer(std::get<0>(resp)));
std::get<0>(resp).clear();
req.clear();
buffer.erase(0, n);
}
}
awaitable_type listener()
{
auto ex = co_await net::this_coro::executor;
auto db = std::make_shared<connection>(ex);
db->async_run(net::detached);
tcp_acceptor acc(ex, {net::ip::tcp::v4(), 55555});
for (;;)
net::co_spawn(ex, echo_loop(co_await acc.async_accept(), db), net::detached);
}
int main()
{
try {
net::io_context ioc{BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO};
co_spawn(ioc, listener(), net::detached);
ioc.run();
} catch (std::exception const& e) {
std::cerr << e.what() << std::endl;
}
}

View File

@@ -1,38 +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 <tuple>
#include <string>
#include <boost/asio.hpp>
#include <aedis.hpp>
// Include this in no more than one .cpp file.
#include <aedis/src.hpp>
namespace net = boost::asio;
using aedis::adapt;
using aedis::resp3::request;
using connection = aedis::connection<>;
int main()
{
net::io_context ioc;
connection db{ioc};
request req;
req.push("PING");
req.push("QUIT");
std::tuple<std::string, aedis::ignore> resp;
db.async_run(req, adapt(resp), [](auto ec, auto) {
std::cout << ec.message() << std::endl;
});
ioc.run();
std::cout << std::get<0>(resp) << std::endl;
}

View File

@@ -1,46 +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 <tuple>
#include <string>
#include <thread>
#include <boost/asio/io_context.hpp>
#include <aedis.hpp>
// Include this in no more than one .cpp file.
#include <aedis/src.hpp>
namespace net = boost::asio;
using aedis::adapt;
using aedis::resp3::request;
using connection = aedis::sync<aedis::connection<>>;
int main()
{
try {
net::io_context ioc{1};
auto work = net::make_work_guard(ioc);
std::thread t1{[&]() { ioc.run(); }};
connection conn{work.get_executor()};
std::thread t2{[&]() { boost::system::error_code ec; conn.run(ec); }};
request req;
req.push("PING");
req.push("QUIT");
std::tuple<std::string, aedis::ignore> resp;
conn.exec(req, adapt(resp));
std::cout << "Response: " << std::get<0>(resp) << std::endl;
work.reset();
t1.join();
t2.join();
} catch (std::exception const& e) {
std::cerr << e.what() << std::endl;
}
}

View File

@@ -1,62 +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 <map>
#include <set>
#include <vector>
#include <string>
#include <iostream>
#include <aedis/resp3/type.hpp>
#include <aedis/resp3/node.hpp>
// Some functions to make the examples less repetitive.
namespace net = boost::asio;
using aedis::resp3::node;
void print_aggr(std::vector<aedis::resp3::node<std::string>>& v)
{
auto const m = aedis::resp3::element_multiplicity(v.front().data_type);
for (auto i = 0lu; i < m * v.front().aggregate_size; ++i)
std::cout << v[i + 1].value << " ";
std::cout << "\n";
v.clear();
}
template <class T>
void print(std::vector<T> const& cont)
{
for (auto const& e: cont) std::cout << e << " ";
std::cout << "\n";
}
template <class T>
void print(std::set<T> const& cont)
{
for (auto const& e: cont) std::cout << e << "\n";
}
template <class T, class U>
void print(std::map<T, U> const& cont)
{
for (auto const& e: cont)
std::cout << e.first << ": " << e.second << "\n";
}
void print(std::string const& e)
{
std::cout << e << std::endl;
}
void print_push(std::vector<aedis::resp3::node<std::string>>& resp)
{
std::cout
<< "Push type: " << resp.at(1).value << "\n"
<< "Channel: " << resp.at(2).value << "\n"
<< "Message: " << resp.at(3).value << "\n"
<< std::endl;
}

View File

@@ -1,109 +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 <algorithm>
#include <cstdint>
#include <iostream>
#include <set>
#include <iterator>
#include <string>
#include <boost/json.hpp>
#include <boost/json/src.hpp>
#include <aedis.hpp>
#include "print.hpp"
// Include this in no more than one .cpp file.
#include <aedis/src.hpp>
namespace net = boost::asio;
using aedis::resp3::request;
using aedis::adapt;
using connection = aedis::connection<>;
using namespace boost::json;
struct user {
std::string name;
std::string age;
std::string country;
};
void tag_invoke(value_from_tag, value& jv, user const& u)
{
jv =
{ {"name", u.name}
, {"age", u.age}
, {"country", u.country}
};
}
template<class T>
void extract(object const& obj, T& t, boost::string_view key)
{
t = value_to<T>(obj.at(key));
}
user tag_invoke(value_to_tag<user>, value const& jv)
{
user u;
object const& obj = jv.as_object();
extract(obj, u.name, "name");
extract(obj, u.age, "age");
extract(obj, u.country, "country");
return u;
}
// Serializes
void to_bulk(std::string& to, user const& u)
{
aedis::resp3::to_bulk(to, serialize(value_from(u)));
}
// Deserializes
void from_bulk(user& u, boost::string_view sv, boost::system::error_code&)
{
value jv = parse(sv);
u = value_to<user>(jv);
}
std::ostream& operator<<(std::ostream& os, user const& u)
{
os << "Name: " << u.name << "\n"
<< "Age: " << u.age << "\n"
<< "Country: " << u.country;
return os;
}
bool operator<(user const& a, user const& b)
{
return std::tie(a.name, a.age, a.country) < std::tie(b.name, b.age, b.country);
}
int main()
{
net::io_context ioc;
connection db{ioc};
std::set<user> users
{{"Joao", "58", "Brazil"} , {"Serge", "60", "France"}};
request req;
req.push("HELLO", 3);
req.push_range("SADD", "sadd-key", users); // Sends
req.push("SMEMBERS", "sadd-key"); // Retrieves
req.push("QUIT");
std::tuple<aedis::ignore, int, std::set<user>, std::string> resp;
db.async_run(req, adapt(resp), [](auto ec, auto) {
std::cout << ec.message() << std::endl;
});
ioc.run();
// Print
print(std::get<2>(resp));
}

View File

@@ -1,84 +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 <string>
#include <vector>
#include <iostream>
#include <tuple>
#include <boost/asio.hpp>
#include <aedis.hpp>
#include "print.hpp"
// Include this in no more than one .cpp file.
#include <aedis/src.hpp>
namespace net = boost::asio;
using aedis::adapt;
using aedis::resp3::request;
using aedis::resp3::node;
using tcp_socket = net::use_awaitable_t<>::as_default_on_t<net::ip::tcp::socket>;
using connection = aedis::connection<tcp_socket>;
/* 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
* (integer) 3
* 127.0.0.1:6379>
*
* To test reconnection try, for example, to close all clients currently
* connected to the Redis instance
*
* $ redis-cli
* > CLIENT kill TYPE pubsub
*/
// Receives pushes.
net::awaitable<void> push_receiver(std::shared_ptr<connection> db)
{
for (std::vector<node<std::string>> resp;;) {
co_await db->async_receive_push(adapt(resp));
print_push(resp);
resp.clear();
}
}
// Receives events.
net::awaitable<void> event_receiver(std::shared_ptr<connection> db)
{
request req;
req.push("SUBSCRIBE", "channel");
for (;;) {
auto ev = co_await db->async_receive_event();
if (ev == connection::event::hello)
co_await db->async_exec(req);
}
}
int main()
{
try {
net::io_context ioc;
auto db = std::make_shared<connection>(ioc);
db->get_config().enable_events = true;
db->get_config().enable_reconnect = true;
net::co_spawn(ioc, push_receiver(db), net::detached);
net::co_spawn(ioc, event_receiver(db), net::detached);
db->async_run(net::detached);
net::signal_set signals(ioc, SIGINT, SIGTERM);
signals.async_wait([&](auto, auto){ ioc.stop(); });
ioc.run();
} catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}

View File

@@ -1,67 +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 <tuple>
#include <string>
#include <thread>
#include <boost/asio.hpp>
#include <aedis.hpp>
#include "print.hpp"
// Include this in no more than one .cpp file.
#include <aedis/src.hpp>
namespace net = boost::asio;
using aedis::adapt;
using aedis::resp3::node;
using aedis::resp3::request;
using connection = aedis::sync<aedis::connection<>>;
using event = connection::event;
// See subscriber.cpp for more info about how to run this example.
// Subscribe again everytime there is a disconnection.
void event_receiver(connection& conn)
{
request req;
req.push("SUBSCRIBE", "channel");
for (;;) {
auto ev = conn.receive_event();
if (ev == connection::event::hello)
conn.exec(req);
}
}
int main()
{
try {
net::io_context ioc{1};
auto work = net::make_work_guard(ioc);
connection::config cfg;
cfg.enable_events = true;
cfg.enable_reconnect = true;
connection conn{work.get_executor(), cfg};
std::thread t1{[&]() { ioc.run(); }};
std::thread t2{[&]() { boost::system::error_code ec; conn.run(ec); }};
std::thread t3{[&]() { event_receiver(conn); }};
for (std::vector<node<std::string>> resp;;) {
conn.receive_push(adapt(resp));
print_push(resp);
resp.clear();
}
t1.join();
t2.join();
t3.join();
} catch (std::exception const& e) {
std::cerr << e.what() << std::endl;
}
}

View File

@@ -1,25 +0,0 @@
nobase_include_HEADERS =\
$(top_srcdir)/include/aedis/src.hpp\
$(top_srcdir)/include/aedis/error.hpp\
$(top_srcdir)/include/aedis/impl/error.ipp\
$(top_srcdir)/include/aedis/detail/net.hpp\
$(top_srcdir)/include/aedis/connection.hpp\
$(top_srcdir)/include/aedis/adapt.hpp\
$(top_srcdir)/include/aedis/sync.hpp\
$(top_srcdir)/include/aedis/detail/connection_ops.hpp\
$(top_srcdir)/include/aedis.hpp\
$(top_srcdir)/include/aedis/adapter/detail/adapters.hpp\
$(top_srcdir)/include/aedis/adapter/adapt.hpp\
$(top_srcdir)/include/aedis/adapter/detail/response_traits.hpp\
$(top_srcdir)/include/aedis/resp3/node.hpp\
$(top_srcdir)/include/aedis/resp3/detail/read_ops.hpp\
$(top_srcdir)/include/aedis/resp3/detail/parser.hpp\
$(top_srcdir)/include/aedis/resp3/detail/exec.hpp\
$(top_srcdir)/include/aedis/resp3/type.hpp\
$(top_srcdir)/include/aedis/resp3/read.hpp\
$(top_srcdir)/include/aedis/resp3/write.hpp\
$(top_srcdir)/include/aedis/resp3/request.hpp\
$(top_srcdir)/include/aedis/resp3/impl/request.ipp\
$(top_srcdir)/include/aedis/resp3/detail/impl/parser.ipp\
$(top_srcdir)/include/aedis/resp3/impl/type.ipp

View File

@@ -1,636 +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)
*/
#ifndef AEDIS_HPP
#define AEDIS_HPP
#include <aedis/error.hpp>
#include <aedis/adapt.hpp>
#include <aedis/connection.hpp>
#include <aedis/sync.hpp>
#include <aedis/resp3/request.hpp>
/** \mainpage Documentation
\tableofcontents
Useful links: \subpage any, [Changelog](CHANGELOG.md) and [Benchmarks](benchmarks/benchmarks.md).
\section Overview
Aedis is a high-level [Redis](https://redis.io/) client library
built on top of
[Asio](https://www.boost.org/doc/libs/release/doc/html/boost_asio.html).
Some of its distinctive features are
\li Support for the latest version of the Redis communication protocol [RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md).
\li First class support for STL containers and C++ built-in types.
\li Serialization and deserialization of your own data types.
\li Healthy checks, back pressure and low latency.
\li Hides most of the low level asynchronous operations away from the user.
Let us have a look a some code snippets
@subsection Async
The code below sends a ping command to Redis and quits (see intro.cpp)
@code
int main()
{
net::io_context ioc;
connection db{ioc};
request req;
req.push("PING");
req.push("QUIT");
std::tuple<std::string, aedis::ignore> resp;
db.async_run(req, adapt(resp), net::detached);
ioc.run();
std::cout << std::get<0>(resp) << std::endl;
}
@endcode
The connection class maintains a healthy connection with Redis
over which users can execute their commands, without any need of
queuing. For example, to execute more than one request
@code
int main()
{
...
net::io_context ioc;
connection db{ioc};
db.async_exec(req1, adapt(resp1), handler1);
db.async_exec(req2, adapt(resp2), handler2);
db.async_exec(req3, adapt(resp3), handler3);
db.async_run(net::detached);
ioc.run();
...
}
@endcode
The `connection::async_exec` functions above can be called from different
places in the code without knowing about each other, see for
example echo_server.cpp. Server-side pushes are supported on the
same connection where commands are executed, a typical subscriber
will look like
(see subscriber.cpp)
@code
net::awaitable<void> reader(std::shared_ptr<connection> db)
{
for (std::vector<node_type> resp;;) {
co_await db->async_receive_event(adapt(resp));
// Use resp and clear it.
resp.clear();
}
}
@endcode
@subsection Sync
The `connection` class offers only an asynchronous API.
Synchronous communications with redis is provided by the `aedis::sync`
wrapper class. (see intro_sync.cpp)
@code
int main()
{
net::io_context ioc{1};
auto work = net::make_work_guard(ioc);
std::thread t1{[&]() { ioc.run(); }};
sync<connection> conn{work.get_executor()};
std::thread t2{[&]() { boost::system::error_code ec; conn.run(ec); }};
request req;
req.push("PING");
req.push("QUIT");
std::tuple<std::string, aedis::ignore> resp;
conn.exec(req, adapt(resp));
std::cout << "Response: " << std::get<0>(resp) << std::endl;
work.reset();
t1.join();
t2.join();
}
@endcode
\subsection using-aedis Installation
To install and use Aedis you will need
- Boost 1.78 or greater.
- C++17. Some examples require C++20 with coroutine support.
- Redis 6 or higher. Optionally also redis-cli and Redis Sentinel.
For a simple installation run
```
$ git clone --branch v1.0.0 https://github.com/mzimbres/aedis.git
$ cd aedis
# Option 1: Direct compilation.
$ g++ -std=c++17 -pthread examples/intro.cpp -I./include -I/path/boost_1_79_0/include/
# Option 2: Use cmake.
$ BOOST_ROOT=/opt/boost_1_79_0/ cmake -DCMAKE_CXX_FLAGS=-std=c++20 .
```
@note CMake support is still experimental.
For a proper full installation on the system run
```
# Download and unpack the latest release
$ wget https://github.com/mzimbres/aedis/releases/download/v1.0.0/aedis-1.0.0.tar.gz
$ tar -xzvf aedis-1.0.0.tar.gz
# Configure, build and install
$ CXXFLAGS="-std=c++17" ./configure --prefix=/opt/aedis-1.0.0 --with-boost=/opt/boost_1_78_0
$ sudo make install
```
To build examples and tests
```
$ make
```
@subsubsection using_aedis Using Aedis
When writing you own applications include the following header
```cpp
#include <aedis/src.hpp>
```
in no more than one source file in your applications.
@subsubsection sup-comp Supported compilers
Aedis has been tested with the following compilers
- Tested with gcc: 12, 11.
- Tested with clang: 14, 13, 11.
\subsubsection Developers
To generate the build system clone the repository and run
```
# git clone https://github.com/mzimbres/aedis.git
$ autoreconf -i
```
After that we get a configure script that can be run as explained
above, for example, to build with a compiler other that the system
compiler with coverage support run
```
$ CXX=clang++-14 \
CXXFLAGS="-g -std=c++20 -Wall -Wextra --coverage -fkeep-inline-functions -fkeep-static-functions" \
LDFLAGS="--coverage" \
./configure --with-boost=/opt/boost_1_79_0
```
To generate release tarballs run
```
$ make distcheck
```
\section requests Requests
Redis requests are composed of one of more Redis commands (in
Redis documentation they are called
[pipelines](https://redis.io/topics/pipelining)). For example
@code
request req;
// Command with variable length of arguments.
req.push("SET", "key", "some value", value, "EX", "2");
// Pushes a list.
std::list<std::string> list
{"channel1", "channel2", "channel3"};
req.push_range("SUBSCRIBE", list);
// Same as above but as an iterator range.
req.push_range2("SUBSCRIBE", std::cbegin(list), std::cend(list));
// Pushes a map.
std::map<std::string, mystruct> map
{ {"key1", "value1"}
, {"key2", "value2"}
, {"key3", "value3"}};
req.push_range("HSET", "key", map);
@endcode
Sending a request to Redis is then peformed with the following function
@code
co_await db->async_exec(req, adapt(resp));
@endcode
\subsection requests-serialization Serialization
The \c push and \c push_range functions above work with integers
e.g. \c int and \c std::string out of the box. To send your own
data type defined a \c to_bulk function like this
@code
struct mystruct {
// Example struct.
};
void to_bulk(std::string& to, mystruct const& obj)
{
std::string dummy = "Dummy serializaiton string.";
aedis::resp3::to_bulk(to, dummy);
}
@endcode
Once \c to_bulk is defined and accessible over ADL \c mystruct can
be passed to the \c request
@code
request req;
std::map<std::string, mystruct> map {...};
req.push_range("HSET", "key", map);
@endcode
Example serialization.cpp shows how store json string in Redis.
\section low-level-responses Responses
To read responses effectively, users must know their RESP3 type,
this can be found in the Redis documentation for each command
(https://redis.io/commands). For example
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
Once the RESP3 type of a given response is known we can choose a
proper C++ data structure to receive it in. Fortunately, this is a
simple task for most types. The table below summarises the options
RESP3 type | Possible C++ type | Type
---------------|--------------------------------------------------------------|------------------
Simple-string | \c std::string | Simple
Simple-error | \c std::string | Simple
Blob-string | \c std::string, \c std::vector | Simple
Blob-error | \c std::string, \c std::vector | Simple
Number | `long long`, `int`, `std::size_t`, \c std::string | Simple
Double | `double`, \c std::string | Simple
Null | `boost::optional<T>` | Simple
Array | \c std::vector, \c std::list, \c std::array, \c std::deque | Aggregate
Map | \c std::vector, \c std::map, \c std::unordered_map | Aggregate
Set | \c std::vector, \c std::set, \c std::unordered_set | Aggregate
Push | \c std::vector, \c std::map, \c std::unordered_map | Aggregate
For example
@code
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");
std::tuple<
aedis::ignore, // hello
int, // rpush
int, // hset
std::vector<T>, // lrange
std::map<U, V>, // hgetall
std::string // quit
> resp;
co_await db->async_exec(req, adapt(resp));
@endcode
The tag @c aedis::ignore can be used to ignore individual
elements in the responses. If the intention is to ignore the
response to all commands in the request use @c adapt()
@code
co_await db->async_exec(req, adapt());
@endcode
Responses that contain nested aggregates or heterogeneous data
types will be given special treatment later in \ref gen-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.
\subsection Optional
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
cases Aedis provides support for \c boost::optional. To use it,
wrap your type around \c boost::optional like this
@code
boost::optional<std::unordered_map<T, U>> resp;
co_await db->async_exec(req, adapt(resp));
@endcode
Everything else stays the same.
\subsection transactions Transactions
To read the response to transactions we have to observe that Redis
queues the commands as they arrive and sends the responses back to
the user as an array, in the response to the @c exec command.
For example, to read the response to the this request
@code
db.send("MULTI");
db.send("GET", "key1");
db.send("LRANGE", "key2", 0, -1);
db.send("HGETALL", "key3");
db.send("EXEC");
@endcode
use the following response type
@code
using aedis::ignore;
using boost::optional;
using exec_resp_type =
std::tuple<
optional<std::string>, // get
optional<std::vector<std::string>>, // lrange
optional<std::map<std::string, std::string>> // hgetall
>;
std::tuple<
ignore, // multi
ignore, // get
ignore, // lrange
ignore, // hgetall
exec_resp_type, // exec
> resp;
co_await db->async_exec(req, adapt(resp));
@endcode
Note that above we are not ignoring the response to the commands
themselves but whether they have been successfully queued. For a
complete example see containers.cpp.
\subsection Deserialization
As mentioned in \ref requests-serialization, it is common to
serialize data before sending it to Redis e.g. to json strings.
For performance and convenience reasons, we may also want to
deserialize it directly in its final data structure. Aedis
supports this use case by calling a user provided \c from_bulk
function while parsing the response. For example
@code
void from_bulk(mystruct& obj, char const* p, std::size_t size, boost::system::error_code& ec)
{
// Deserializes p into obj.
}
@endcode
After that, you can start receiving data efficiently in the desired
types e.g. \c mystruct, \c std::map<std::string, mystruct> etc.
\subsection gen-case The general case
There are cases where responses to Redis
commands won't fit in the model presented above, some examples are
@li Commands (like \c set) whose responses don't have a fixed
RESP3 type. Expecting an \c int and receiving a blob-string
will result in error.
@li RESP3 aggregates that contain nested aggregates can't be read in STL containers.
@li Transactions with a dynamic number of commands can't be read in a \c std::tuple.
To deal with these cases Aedis provides the \c resp3::node
type, that is the most general form of an element in a response,
be it a simple RESP3 type or an aggregate. It is defined like this
@code
template <class String>
struct 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;
};
@endcode
Any response to a Redis command can be received in a \c
std::vector<node<std::string>>. The vector can be seen as a
pre-order view of the response tree
(https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR).
Using it is no different than using other types
@code
// Receives any RESP3 simple data type.
node<std::string> resp;
co_await db->async_exec(req, adapt(resp));
// Receives any RESP3 simple or aggregate data type.
std::vector<node<std::string>> resp;
co_await db->async_exec(req, adapt(resp));
@endcode
For example, suppose we want to retrieve a hash data structure
from Redis with `HGETALL`, some of the options are
@li \c std::vector<node<std::string>: Works always.
@li \c std::vector<std::string>: Efficient and flat, all elements as string.
@li \c std::map<std::string, std::string>: Efficient if you need the data as a \c std::map
@li \c std::map<U, V>: Efficient if you are storing serialized data. Avoids temporaries and requires \c from_bulk for \c U and \c V.
In addition to the above users can also use unordered versions of the containers. The same reasoning also applies to sets e.g. `SMEMBERS`.
\section examples Examples
The examples listed below cover most use cases presented in the documentation above.
@li intro.cpp: Basic steps with Aedis.
@li intro_sync.cpp: Synchronous version of intro.cpp.
@li containers.cpp: Shows how to send and receive stl containers.
@li serialization.cpp: Shows how to serialize your own types.
@li subscriber.cpp: Shows how to use pubsub.
@li subscriber_sync.cpp: Synchronous version of subscriber.cpp.
@li echo_server.cpp: A simple TCP echo server that uses coroutines.
@li chat_room.cpp: A simple chat room that uses coroutines.
\section why-aedis Why Aedis
At the time of this writing there are seventeen Redis clients
listed in the [official](https://redis.io/docs/clients/#cpp) list.
With so many clients available it is not unlikely that users are
asking themselves why yet another one. In this section I will try
to compare Aedis with the most popular clients and why we need
Aedis. Notice however that this is ongoing work as comparing
client objectively is difficult and time consuming.
The most popular client at the moment of this writing ranked by
github stars is
@li https://github.com/sewenew/redis-plus-plus
Before we start it is worth mentioning some of the things it does
not support
@li RESP3. Without RESP3 is impossible to support some important Redis features like client side caching, among other things.
@li Coroutines.
@li Reading responses directly in user data structures avoiding temporaries.
@li Error handling with error-code and exception overloads.
@li Healthy checks.
The remaining points will be addressed individually.
@subsection redis-plus-plus
Let us first have a look at what sending a command a pipeline and a
transaction look like
@code
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);
// ...
@endcode
Some of the problems with this API are
@li Heterogeneous treatment of commands, pipelines and transaction. This makes auto-pipelining impossible.
@li Any Api that sends individual commands has a very restricted scope of usability and should be avoided for performance reasons.
@li The API imposes exceptions on users, no error-code overload is provided.
@li No way to reuse the buffer for new calls to e.g. \c redis.get in order to avoid further dynamic memory allocations.
@li 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 of 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 Aedis there is no difference between sending one command, a
pipeline or a transaction because requests are decoupled
from the IO 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
@code
auto async_redis = AsyncRedis(opts, pool_opts);
Future<string> ping_res = async_redis.ping();
cout << ping_res.get() << endl;
@endcode
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 the design
(if at all).
\section Acknowledgement
Some people that were helpful in the development of Aedis
@li Richard Hodges ([madmongo1](https://github.com/madmongo1)): For helping me with Asio and the design of asynchronous programs in general.
@li Vinícius dos Santos Oliveira ([vinipsmaker](https://github.com/vinipsmaker)): For useful discussion about how Aedis consumes buffers in the read operation (among other things).
@li Petr Dannhofer ([Eddie-cz](https://github.com/Eddie-cz)): For helping me understand how the `AUTH` and `HELLO` command can influence each other.
*/
/** \defgroup any Reference
*
* This page contains the documentation of all user facing code.
*/
// Support sentinel support as described in
//
// - https://redis.io/docs/manual/sentinel.
// - https://redis.io/docs/reference/sentinel-clients.
//
// Avoid conflicts between
//
// - aedis::adapt
// - aedis::resp3::adapt.
#endif // AEDIS_HPP

View File

@@ -1,159 +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)
*/
#ifndef AEDIS_ADAPT_HPP
#define AEDIS_ADAPT_HPP
#include <tuple>
#include <boost/mp11.hpp>
#include <boost/variant2.hpp>
#include <boost/utility/string_view.hpp>
#include <boost/system.hpp>
#include <aedis/resp3/node.hpp>
#include <aedis/adapter/adapt.hpp>
#include <aedis/adapter/detail/response_traits.hpp>
namespace aedis {
/** @brief Tag used to ignore responses.
* @ingroup any
*
* For example
*
* @code
* std::tuple<aedis::ignore, std::string, aedis::ignore> resp;
* @endcode
*
* will cause only the second tuple type to be parsed, the others
* will be ignored.
*/
using ignore = adapter::detail::ignore;
namespace detail {
struct ignore_adapter {
void
operator()(
std::size_t,
resp3::node<boost::string_view> const&,
boost::system::error_code&)
{
}
auto supported_response_size() const noexcept { return std::size_t(-1);}
};
template <class Tuple>
class static_adapter {
private:
static constexpr auto size = std::tuple_size<Tuple>::value;
using adapter_tuple = boost::mp11::mp_transform<adapter::adapter_t, Tuple>;
using variant_type = boost::mp11::mp_rename<adapter_tuple, boost::variant2::variant>;
using adapters_array_type = std::array<variant_type, size>;
adapters_array_type adapters_;
public:
static_adapter(Tuple& r = nullptr)
{
adapter::detail::assigner<size - 1>::assign(adapters_, r);
}
auto supported_response_size() const noexcept { return size;}
void
operator()(
std::size_t i,
resp3::node<boost::string_view> const& nd,
boost::system::error_code& ec)
{
using boost::variant2::visit;
BOOST_ASSERT(i < adapters_.size());
visit([&](auto& arg){arg(nd, ec);}, adapters_.at(i));
}
};
template <class Vector>
class vector_adapter {
private:
using adapter_type = typename adapter::detail::response_traits<Vector>::adapter_type;
adapter_type adapter_;
public:
vector_adapter(Vector& v) : adapter_{adapter::adapt2(v)} { }
auto supported_response_size() const noexcept { return std::size_t(-1);}
void
operator()(
std::size_t,
resp3::node<boost::string_view> const& nd,
boost::system::error_code& ec)
{
adapter_(nd, ec);
}
};
template <class>
struct response_traits;
template <>
struct response_traits<void> {
using response_type = void;
using adapter_type = detail::ignore_adapter;
static auto adapt() noexcept
{ return detail::ignore_adapter{}; }
};
template <class String, class Allocator>
struct response_traits<std::vector<resp3::node<String>, Allocator>> {
using response_type = std::vector<resp3::node<String>, Allocator>;
using adapter_type = vector_adapter<response_type>;
static auto adapt(response_type& v) noexcept
{ return adapter_type{v}; }
};
template <class ...Ts>
struct response_traits<std::tuple<Ts...>> {
using response_type = std::tuple<Ts...>;
using adapter_type = static_adapter<response_type>;
static auto adapt(response_type& r) noexcept
{ return adapter_type{r}; }
};
} // detail
/** @brief Creates an adapter that ignores responses.
* @ingroup any
*
* This function can be used to create adapters that ignores
* responses.
*/
auto adapt() noexcept
{
return detail::response_traits<void>::adapt();
}
/** @brief Adapts a type to be used as a response.
* @ingroup any
*
* The type T can be any STL container, any integer type and
* \c std::string
*/
template<class T>
auto adapt(T& t) noexcept
{
return detail::response_traits<T>::adapt(t);
}
} // aedis
#endif // AEDIS_ADAPT_HPP

View File

@@ -1,84 +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)
*/
#ifndef AEDIS_ADAPTER_ADAPT_HPP
#define AEDIS_ADAPTER_ADAPT_HPP
#include <aedis/adapter/detail/response_traits.hpp>
namespace aedis {
namespace adapter {
template <class T>
using adapter_t = typename detail::adapter_t<T>;
/** \internal
\brief Creates a dummy response adapter.
\ingroup any
The adapter returned by this function ignores responses. It is
useful to avoid wasting time with responses which are not needed.
Example:
@code
// Pushes and writes some commands to the server.
sr.push(command::hello, 3);
sr.push(command::ping);
sr.push(command::quit);
net::write(socket, net::buffer(request));
// Ignores all responses except for the response to ping.
std::string buffer;
resp3::read(socket, dynamic_buffer(buffer), adapt()); // hello
resp3::read(socket, dynamic_buffer(buffer), adapt(resp)); // ping
resp3::read(socket, dynamic_buffer(buffer, adapt())); // quit
@endcode
*/
inline
auto adapt2() noexcept
{ return detail::response_traits<void>::adapt(); }
/** \internal
* \brief Adapts user data to read operations.
* \ingroup any
*
* STL containers, \c std::tuple and built-in types are supported and
* can be used in conjunction with \c boost::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
* std::tuple<std::string, int, int, std::vector<std::string>, int> execs;
* co_await resp3::async_read(socket, dynamic_buffer(buffer), adapt(execs));
* @endcode
*/
template<class T>
auto adapt2(T& t) noexcept
{ return detail::response_traits<T>::adapt(t); }
} // adapter
} // aedis
#endif // AEDIS_ADAPTER_ADAPT_HPP

View File

@@ -1,430 +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)
*/
#ifndef AEDIS_ADAPTER_ADAPTERS_HPP
#define AEDIS_ADAPTER_ADAPTERS_HPP
#include <set>
#include <unordered_set>
#include <forward_list>
#include <system_error>
#include <map>
#include <unordered_map>
#include <list>
#include <deque>
#include <vector>
#include <array>
#include <boost/assert.hpp>
#include <boost/optional.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/home/x3.hpp>
#include <boost/utility/string_view.hpp>
#include <aedis/error.hpp>
#include <aedis/resp3/type.hpp>
#include <aedis/resp3/request.hpp>
#include <aedis/resp3/detail/parser.hpp>
#include <aedis/resp3/node.hpp>
namespace aedis {
namespace adapter {
namespace detail {
double
parse_double(
char const* data,
std::size_t size,
boost::system::error_code& ec)
{
static constexpr boost::spirit::x3::real_parser<double> p{};
double ret;
if (!parse(data, data + size, p, ret))
ec = error::not_a_double;
return ret;
}
// Serialization.
template <class T>
typename std::enable_if<std::is_integral<T>::value, void>::type
from_bulk(
T& i,
boost::string_view sv,
boost::system::error_code& ec)
{
i = resp3::detail::parse_uint(sv.data(), sv.size(), ec);
}
void from_bulk(
bool& t,
boost::string_view sv,
boost::system::error_code&)
{
t = *sv.data() == 't';
}
void from_bulk(
double& d,
boost::string_view sv,
boost::system::error_code& ec)
{
d = parse_double(sv.data(), sv.size(), ec);
}
template <class CharT, class Traits, class Allocator>
void
from_bulk(
std::basic_string<CharT, Traits, Allocator>& s,
boost::string_view sv,
boost::system::error_code&)
{
s.append(sv.data(), sv.size());
}
//================================================
void set_on_resp3_error(resp3::type t, boost::system::error_code& ec)
{
switch (t) {
case resp3::type::simple_error: ec = error::simple_error; return;
case resp3::type::blob_error: ec = error::blob_error; return;
case resp3::type::null: ec = error::null; return;
default: return;
}
}
template <class Result>
class general_aggregate {
private:
Result* result_;
public:
general_aggregate(Result* c = nullptr): result_(c) {}
void operator()(resp3::node<boost::string_view> const& n, boost::system::error_code& ec)
{
result_->push_back({n.data_type, n.aggregate_size, n.depth, std::string{std::cbegin(n.value), std::cend(n.value)}});
set_on_resp3_error(n.data_type, ec);
}
};
template <class Node>
class general_simple {
private:
Node* result_;
public:
general_simple(Node* t = nullptr) : result_(t) {}
void operator()(resp3::node<boost::string_view> const& n, boost::system::error_code& ec)
{
result_->data_type = n.data_type;
result_->aggregate_size = n.aggregate_size;
result_->depth = n.depth;
result_->value.assign(n.value.data(), n.value.size());
set_on_resp3_error(n.data_type, ec);
}
};
template <class Result>
class simple_impl {
public:
void on_value_available(Result&) {}
void
operator()(
Result& result,
resp3::node<boost::string_view> const& n,
boost::system::error_code& ec)
{
set_on_resp3_error(n.data_type, ec);
if (ec)
return;
if (is_aggregate(n.data_type)) {
ec = error::expects_resp3_simple_type;
return;
}
from_bulk(result, n.value, ec);
}
};
template <class Result>
class set_impl {
private:
typename Result::iterator hint_;
public:
void on_value_available(Result& result)
{ hint_ = std::end(result); }
void
operator()(
Result& result,
resp3::node<boost::string_view> const& nd,
boost::system::error_code& ec)
{
set_on_resp3_error(nd.data_type, ec);
if (ec)
return;
if (is_aggregate(nd.data_type)) {
if (nd.data_type != resp3::type::set)
ec = error::expects_resp3_set;
return;
}
BOOST_ASSERT(nd.aggregate_size == 1);
if (nd.depth < 1) {
ec = error::expects_resp3_set;
return;
}
typename Result::key_type obj;
from_bulk(obj, nd.value, ec);
hint_ = result.insert(hint_, std::move(obj));
}
};
template <class Result>
class map_impl {
private:
typename Result::iterator current_;
bool on_key_ = true;
public:
void on_value_available(Result& result)
{ current_ = std::end(result); }
void
operator()(
Result& result,
resp3::node<boost::string_view> const& nd,
boost::system::error_code& ec)
{
set_on_resp3_error(nd.data_type, ec);
if (ec)
return;
if (is_aggregate(nd.data_type)) {
if (element_multiplicity(nd.data_type) != 2)
ec = error::expects_resp3_map;
return;
}
BOOST_ASSERT(nd.aggregate_size == 1);
if (nd.depth < 1) {
ec = error::expects_resp3_map;
return;
}
if (on_key_) {
typename Result::key_type obj;
from_bulk(obj, nd.value, ec);
current_ = result.insert(current_, {std::move(obj), {}});
} else {
typename Result::mapped_type obj;
from_bulk(obj, nd.value, ec);
current_->second = std::move(obj);
}
on_key_ = !on_key_;
}
};
template <class Result>
class vector_impl {
public:
void on_value_available(Result& ) { }
void
operator()(
Result& result,
resp3::node<boost::string_view> const& nd,
boost::system::error_code& ec)
{
set_on_resp3_error(nd.data_type, ec);
if (ec)
return;
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({});
from_bulk(result.back(), nd.value, ec);
}
}
};
template <class Result>
class array_impl {
private:
int i_ = -1;
public:
void on_value_available(Result& ) { }
void
operator()(
Result& result,
resp3::node<boost::string_view> const& nd,
boost::system::error_code& ec)
{
set_on_resp3_error(nd.data_type, ec);
if (ec)
return;
if (is_aggregate(nd.data_type)) {
if (i_ != -1) {
ec = error::nested_aggregate_unsupported;
return;
}
if (result.size() != nd.aggregate_size * element_multiplicity(nd.data_type)) {
ec = error::incompatible_size;
return;
}
} else {
if (i_ == -1) {
ec = error::expects_resp3_aggregate;
return;
}
BOOST_ASSERT(nd.aggregate_size == 1);
from_bulk(result.at(i_), nd.value, ec);
}
++i_;
}
};
template <class Result>
struct list_impl {
void on_value_available(Result& ) { }
void
operator()(
Result& result,
resp3::node<boost::string_view> const& nd,
boost::system::error_code& ec)
{
set_on_resp3_error(nd.data_type, ec);
if (ec)
return;
if (!is_aggregate(nd.data_type)) {
BOOST_ASSERT(nd.aggregate_size == 1);
if (nd.depth < 1) {
ec = error::expects_resp3_aggregate;
return;
}
result.push_back({});
from_bulk(result.back(), nd.value, ec);
}
}
};
//---------------------------------------------------
template <class 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>>; };
template <class Key, class Compare, class 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>>; };
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>>; };
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>>; };
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>>; };
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>>; };
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>>; };
template <class T, class 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>>; };
template <class T, class 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>>; };
//---------------------------------------------------
template <class Result>
class wrapper {
private:
Result* result_;
typename impl_map<Result>::type impl_;
public:
wrapper(Result* t = nullptr) : result_(t)
{ impl_.on_value_available(*result_); }
void
operator()(
resp3::node<boost::string_view> const& nd,
boost::system::error_code& ec)
{
BOOST_ASSERT(result_);
impl_(*result_, nd, ec);
}
};
template <class T>
class wrapper<boost::optional<T>> {
private:
boost::optional<T>* result_;
typename impl_map<T>::type impl_;
public:
wrapper(boost::optional<T>* o = nullptr) : result_(o), impl_{} {}
void
operator()(
resp3::node<boost::string_view> const& nd,
boost::system::error_code& ec)
{
if (nd.data_type == resp3::type::null)
return;
if (!result_->has_value()) {
*result_ = T{};
impl_.on_value_available(result_->value());
}
impl_(result_->value(), nd, ec);
}
};
} // detail
} // adapter
} // aedis
#endif // AEDIS_ADAPTER_ADAPTERS_HPP

View File

@@ -1,161 +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)
*/
#ifndef AEDIS_ADAPTER_RESPONSE_TRAITS_HPP
#define AEDIS_ADAPTER_RESPONSE_TRAITS_HPP
#include <vector>
#include <tuple>
#include <boost/mp11.hpp>
#include <boost/variant2.hpp>
#include <aedis/error.hpp>
#include <aedis/resp3/type.hpp>
#include <aedis/resp3/read.hpp>
#include <aedis/adapter/detail/adapters.hpp>
namespace aedis {
namespace adapter {
namespace detail {
struct ignore {};
/* Traits class for response objects.
*
* Provides traits for all supported response types i.e. all STL
* containers and C++ buil-in types.
*/
template <class ResponseType>
struct response_traits {
using adapter_type = adapter::detail::wrapper<ResponseType>;
static auto adapt(ResponseType& r) noexcept { return adapter_type{&r}; }
};
template <class T>
using adapter_t = typename response_traits<T>::adapter_type;
template <>
struct response_traits<ignore> {
using response_type = ignore;
using adapter_type = resp3::detail::ignore_response;
static auto adapt(response_type&) noexcept { return adapter_type{}; }
};
template <class T>
struct response_traits<resp3::node<T>> {
using response_type = resp3::node<T>;
using adapter_type = adapter::detail::general_simple<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <class String, class Allocator>
struct response_traits<std::vector<resp3::node<String>, Allocator>> {
using response_type = std::vector<resp3::node<String>, Allocator>;
using adapter_type = adapter::detail::general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <>
struct response_traits<void> {
using response_type = void;
using adapter_type = resp3::detail::ignore_response;
static auto adapt() noexcept { return adapter_type{}; }
};
// Duplicated here to avoid circular include dependency.
template<class T>
auto internal_adapt(T& t) noexcept
{ return response_traits<T>::adapt(t); }
template <std::size_t N>
struct assigner {
template <class T1, class T2>
static void assign(T1& dest, T2& from)
{
dest[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] = internal_adapt(std::get<0>(from));
}
};
template <class Tuple>
class static_aggregate_adapter {
private:
using adapters_array_type =
std::array<
boost::mp11::mp_rename<
boost::mp11::mp_transform<
adapter_t, Tuple>,
boost::variant2::variant>,
std::tuple_size<Tuple>::value>;
std::size_t i_ = 0;
std::size_t aggregate_size_ = 0;
adapters_array_type adapters_;
public:
static_aggregate_adapter(Tuple* r = nullptr)
{
detail::assigner<std::tuple_size<Tuple>::value - 1>::assign(adapters_, *r);
}
void count(resp3::node<boost::string_view> const& nd)
{
if (nd.depth == 1) {
if (is_aggregate(nd.data_type))
aggregate_size_ = element_multiplicity(nd.data_type) * nd.aggregate_size;
else
++i_;
return;
}
if (--aggregate_size_ == 0)
++i_;
}
void
operator()(
resp3::node<boost::string_view> const& nd,
boost::system::error_code& ec)
{
using boost::variant2::visit;
if (nd.depth == 0) {
auto const real_aggr_size = nd.aggregate_size * element_multiplicity(nd.data_type);
if (real_aggr_size != std::tuple_size<Tuple>::value)
ec = error::incompatible_size;
return;
}
visit([&](auto& arg){arg(nd, ec);}, adapters_[i_]);
count(nd);
}
};
template <class... Ts>
struct response_traits<std::tuple<Ts...>>
{
using response_type = std::tuple<Ts...>;
using adapter_type = static_aggregate_adapter<response_type>;
static auto adapt(response_type& r) noexcept { return adapter_type{&r}; }
};
} // detail
} // adapter
} // aedis
#endif // AEDIS_ADAPTER_RESPONSE_TRAITS_HPP

View File

@@ -1,616 +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)
*/
#ifndef AEDIS_CONNECTION_HPP
#define AEDIS_CONNECTION_HPP
#include <vector>
#include <queue>
#include <limits>
#include <chrono>
#include <memory>
#include <type_traits>
#include <boost/assert.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/experimental/channel.hpp>
#include <aedis/adapt.hpp>
#include <aedis/resp3/request.hpp>
#include <aedis/detail/connection_ops.hpp>
namespace aedis {
/** @brief A high level connection to Redis.
* @ingroup any
*
* This class keeps a healthy connection to the Redis instance where
* commands can be sent at any time. For more details, please see the
* documentation of each individual function.
*
* @remarks This class exposes only asynchronous member functions,
* synchronous communications with the Redis server is provided by
* the sync class.
*
* @tparam AsyncReadWriteStream A stream that supports
* `async_read_some` and `async_write_some`.
*
*/
template <class AsyncReadWriteStream = boost::asio::ip::tcp::socket>
class connection {
public:
/// Executor type.
using executor_type = typename AsyncReadWriteStream::executor_type;
/// Type of the next layer
using next_layer_type = AsyncReadWriteStream;
/** @brief Connection configuration parameters.
*/
struct config {
/// Redis server address.
std::string host = "127.0.0.1";
/// Redis server port.
std::string port = "6379";
/// Username if authentication is required.
std::string username;
/// Password if authentication is required.
std::string password;
/// Timeout of the resolve operation.
std::chrono::milliseconds resolve_timeout = std::chrono::seconds{10};
/// Timeout of the connect operation.
std::chrono::milliseconds connect_timeout = std::chrono::seconds{10};
/// Time interval of ping operations.
std::chrono::milliseconds ping_interval = std::chrono::seconds{1};
/// Time waited before trying a reconnection (see config::enable_reconnect).
std::chrono::milliseconds reconnect_interval = std::chrono::seconds{1};
/// The maximum size of read operations.
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)();
/// Whether to coalesce requests (see [pipelines](https://redis.io/topics/pipelining)).
bool coalesce_requests = true;
/// Enable internal events, see connection::async_receive_event.
bool enable_events = false;
/// Enable automatic reconnection (see also config::reconnect_interval).
bool enable_reconnect = false;
};
/// Events that are communicated by `connection::async_receive_event`.
enum class event {
/// Resolve operation was successful.
resolve,
/// Connect operation was successful.
connect,
/// Success sending AUTH and HELLO.
hello,
/// Used internally.
invalid
};
/** @brief Async operations exposed by this class.
*
* The operations listed below can be cancelled with the `cancel`
* member function.
*/
enum class operation {
/// `connection::async_exec` operations.
exec,
/// `connection::async_run` operations.
run,
/// `connection::async_receive_event` operations.
receive_event,
/// `connection::async_receive_push` operations.
receive_push,
};
/** \brief Contructor
*
* \param ex The executor.
* \param cfg Configuration parameters.
*/
connection(executor_type ex, config cfg = config{})
: resv_{ex}
, ping_timer_{ex}
, check_idle_timer_{ex}
, writer_timer_{ex}
, read_timer_{ex}
, push_channel_{ex}
, event_channel_{ex}
, cfg_{cfg}
, last_data_{std::chrono::time_point<std::chrono::steady_clock>::min()}
{
writer_timer_.expires_at(std::chrono::steady_clock::time_point::max());
read_timer_.expires_at(std::chrono::steady_clock::time_point::max());
}
/** \brief Constructor
*
* \param ioc The executor.
* \param cfg Configuration parameters.
*/
connection(boost::asio::io_context& ioc, config cfg = config{})
: connection(ioc.get_executor(), cfg)
{ }
/// Returns the executor.
auto get_executor() {return resv_.get_executor();}
/** @brief Cancel operations.
*
* @li `operation::exec`: Cancels operations started with `async_exec`.
*
* @li operation::run: Cancels `async_run`. Notice that the
* preferred way to close a connection is to ensure
* `config::enable_reconnect` is set to `false` and send `QUIT`
* to the server. An unresponsive Redis server will also cause
* the idle-checks to kick in and lead to
* `connection::async_run` completing with
* `error::idle_timeout`. Calling `cancel(operation::run)`
* directly should be seen as the last option.
*
* @li operation::receive_event: Cancels `connection::async_receive_event`.
*
* @param op: The operation to be cancelled.
* @returns The number of operations that have been canceled.
*/
std::size_t cancel(operation op)
{
switch (op) {
case operation::exec:
{
for (auto& e: reqs_) {
e->stop = true;
e->timer.cancel_one();
}
auto const ret = reqs_.size();
reqs_ = {};
return ret;
}
case operation::run:
{
if (socket_)
socket_->close();
read_timer_.cancel();
check_idle_timer_.cancel();
writer_timer_.cancel();
ping_timer_.cancel();
auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
return !ptr->req->close_on_run_completion;
});
// Cancel own pings if there are any waiting.
std::for_each(point, std::end(reqs_), [](auto const& ptr) {
ptr->stop = true;
ptr->timer.cancel();
});
reqs_.erase(point, std::end(reqs_));
return 1U;
}
case operation::receive_event:
{
event_channel_.cancel();
return 1U;
}
case operation::receive_push:
{
push_channel_.cancel();
return 1U;
}
}
return 0;
}
/// Get the config object.
config& get_config() noexcept { return cfg_;}
/// Gets the config object.
config const& get_config() const noexcept { return cfg_;}
/** @name Asynchronous functions
*
* Each of these operations a individually cancellable.
**/
/// @{
/** @brief Starts communication with the Redis server asynchronously.
*
* This function performs the following steps
*
* @li Resolves the Redis host as of `async_resolve` with the
* timeout passed in `config::resolve_timeout`.
*
* @li Connects to one of the endpoints returned by the resolve
* operation with the timeout passed in `config::connect_timeout`.
*
* @li Starts healthy checks with a timeout twice
* the value of `config::ping_interval`. If no data is
* received during that time interval `connection::async_run` completes with
* `error::idle_timeout`.
*
* @li Starts the healthy check operation that sends `PING`s to
* Redis with a frequency equal to `config::ping_interval`.
*
* @li Starts reading from the socket and executes all requests
* that have been started prior to this function call.
*
* @remark When a timeout occur and config::enable_reconnect is
* set, this function will automatically try a reconnection
* without returning control to the user.
*
* For an example see echo_server.cpp.
*
* \param token Completion token.
*
* The completion token must have the following signature
*
* @code
* void f(boost::system::error_code);
* @endcode
*/
template <class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_run(CompletionToken token = CompletionToken{})
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::run_op<connection>{this}, token, resv_);
}
/** @brief Connects and executes a request asynchronously.
*
* Combines the other `async_run` overload with `async_exec` in a
* single function. This function is useful for users that want to
* send a single request to the server and close it.
*
* \param req Request object.
* \param adapter Response adapter.
* \param token Asio completion token.
*
* For an example see intro.cpp. The completion token must have
* the following signature
*
* @code
* void f(boost::system::error_code, std::size_t);
* @endcode
*
* Where the second parameter is the size of the response in bytes.
*/
template <
class Adapter = detail::response_traits<void>::adapter_type,
class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_run(
resp3::request const& req,
Adapter adapter = adapt(),
CompletionToken token = CompletionToken{})
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(detail::runexec_op<connection, Adapter>
{this, &req, adapter}, token, resv_);
}
/** @brief Executes a command on the redis server asynchronously.
*
* There is no need to synchronize multiple calls to this
* function as it keeps an internal queue.
*
* \param req Request object.
* \param adapter Response adapter.
* \param token Asio completion token.
*
* For an example see echo_server.cpp. The completion token must
* have the following signature
*
* @code
* void f(boost::system::error_code, std::size_t);
* @endcode
*
* Where the second parameter is the size of the response in
* bytes.
*/
template <
class Adapter = detail::response_traits<void>::adapter_type,
class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_exec(
resp3::request const& req,
Adapter adapter = adapt(),
CompletionToken token = CompletionToken{})
{
BOOST_ASSERT_MSG(req.size() <= adapter.supported_response_size(), "Request and adapter have incompatible sizes.");
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(detail::exec_op<connection, Adapter>{this, &req, adapter}, token, resv_);
}
/** @brief Receives server side pushes asynchronously.
*
* Users that expect server pushes have to call this function in a
* loop. If an unsolicited event comes in and there is no reader,
* the connection will hang and eventually timeout.
*
* \param adapter The response adapter.
* \param token The Asio completion token.
*
* For an example see subscriber.cpp. The completion token must
* have the following signature
*
* @code
* void f(boost::system::error_code, std::size_t);
* @endcode
*
* Where the second parameter is the size of the push in
* bytes.
*/
template <
class Adapter = detail::response_traits<void>::adapter_type,
class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_receive_push(
Adapter adapter = adapt(),
CompletionToken token = CompletionToken{})
{
auto f =
[adapter]
(resp3::node<boost::string_view> const& node, boost::system::error_code& ec) mutable
{
adapter(std::size_t(-1), node, ec);
};
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(detail::receive_push_op<connection, decltype(f)>{this, f}, token, resv_);
}
/** @brief Receives internal events.
*
* See enum \c events for the list of events.
*
* \param token The Asio completion token.
*
* The completion token must have the following signature
*
* @code
* void f(boost::system::error_code, event);
* @endcode
*/
template <class CompletionToken = boost::asio::default_completion_token_t<executor_type>>
auto async_receive_event(CompletionToken token = CompletionToken{})
{
return event_channel_.async_receive(token);
}
/// @}
private:
using clock_type = std::chrono::steady_clock;
using clock_traits_type = boost::asio::wait_traits<clock_type>;
using timer_type = boost::asio::basic_waitable_timer<clock_type, clock_traits_type, executor_type>;
using resolver_type = boost::asio::ip::basic_resolver<boost::asio::ip::tcp, executor_type>;
using push_channel_type = boost::asio::experimental::channel<executor_type, void(boost::system::error_code, std::size_t)>;
using time_point_type = std::chrono::time_point<std::chrono::steady_clock>;
using event_channel_type = boost::asio::experimental::channel<executor_type, void(boost::system::error_code, event)>;
struct req_info {
req_info(executor_type ex) : timer{ex} {}
timer_type timer;
resp3::request const* req = nullptr;
std::size_t cmds = 0;
bool stop = false;
bool written = false;
};
using reqs_type = std::deque<std::shared_ptr<req_info>>;
template <class T, class U> friend struct detail::receive_push_op;
template <class T> friend struct detail::reader_op;
template <class T> friend struct detail::writer_op;
template <class T> friend struct detail::ping_op;
template <class T> friend struct detail::run_op;
template <class T> friend struct detail::run_one_op;
template <class T, class U> friend struct detail::exec_op;
template <class T, class U> friend struct detail::exec_read_op;
template <class T, class U> friend struct detail::runexec_op;
template <class T> friend struct detail::connect_with_timeout_op;
template <class T> friend struct detail::resolve_with_timeout_op;
template <class T> friend struct detail::check_idle_op;
template <class T> friend struct detail::start_op;
template <class T> friend struct detail::send_receive_op;
template <class CompletionToken>
auto async_run_one(CompletionToken token = CompletionToken{})
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::run_one_op<connection>{this}, token, resv_);
}
void cancel_push_requests()
{
auto point = std::stable_partition(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) {
return !(ptr->written && ptr->req->size() == 0);
});
std::for_each(point, std::end(reqs_), [](auto const& ptr) {
ptr->timer.cancel();
});
reqs_.erase(point, std::end(reqs_));
}
void add_request_info(std::shared_ptr<req_info> const& info)
{
reqs_.push_back(info);
if (socket_ != nullptr && socket_->is_open() && cmds_ == 0 && write_buffer_.empty())
writer_timer_.cancel();
}
auto make_dynamic_buffer()
{ return boost::asio::dynamic_buffer(read_buffer_, cfg_.max_read_size); }
template <class CompletionToken>
auto async_resolve_with_timeout(CompletionToken&& token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::resolve_with_timeout_op<connection>{this},
token, resv_);
}
template <class CompletionToken>
auto async_connect_with_timeout(CompletionToken&& token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::connect_with_timeout_op<connection>{this}, token, resv_);
}
template <class CompletionToken>
auto reader(CompletionToken&& token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::reader_op<connection>{this}, token, resv_.get_executor());
}
template <class CompletionToken>
auto writer(CompletionToken&& token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::writer_op<connection>{this}, token, resv_.get_executor());
}
template <class CompletionToken>
auto
async_start(CompletionToken&& token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::start_op<connection>{this}, token, resv_);
}
template <class CompletionToken>
auto async_ping(CompletionToken&& token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::ping_op<connection>{this}, token, resv_);
}
template <class CompletionToken>
auto async_check_idle(CompletionToken&& token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code)
>(detail::check_idle_op<connection>{this}, token, check_idle_timer_);
}
template <class Adapter, class CompletionToken>
auto async_exec_read(Adapter adapter, std::size_t cmds, CompletionToken token)
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(detail::exec_read_op<connection, Adapter>{this, adapter, cmds}, token, resv_);
}
void coalesce_requests()
{
// Coaleces all requests: Copies the request to the variables
// that won't be touched while async_write is suspended.
BOOST_ASSERT(write_buffer_.empty());
BOOST_ASSERT(!reqs_.empty());
auto const size = cfg_.coalesce_requests ? reqs_.size() : 1;
for (auto i = 0UL; i < size; ++i) {
write_buffer_ += reqs_.at(i)->req->payload();
cmds_ += reqs_.at(i)->req->size();
reqs_.at(i)->written = true;
}
}
// IO objects
resolver_type resv_;
std::shared_ptr<AsyncReadWriteStream> socket_;
timer_type ping_timer_;
timer_type check_idle_timer_;
timer_type writer_timer_;
timer_type read_timer_;
push_channel_type push_channel_;
event_channel_type event_channel_;
config cfg_;
std::string read_buffer_;
std::string write_buffer_;
std::size_t cmds_ = 0;
reqs_type reqs_;
// Last time we received data.
time_point_type last_data_;
// The result of async_resolve.
boost::asio::ip::tcp::resolver::results_type endpoints_;
resp3::request req_;
};
/** @brief Converts a connection event to a string.
* @relates connection
*/
template <class T>
char const* to_string(typename connection<T>::event e)
{
using event_type = typename connection<T>::event;
switch (e) {
case event_type::resolve: return "resolve";
case event_type::connect: return "connect";
case event_type::hello: return "hello";
case event_type::push: return "push";
case event_type::invalid: return "invalid";
default: BOOST_ASSERT_MSG(false, "to_string: unhandled event.");
}
}
/** @brief Writes a connection event to the stream.
* @relates connection
*/
template <class T>
std::ostream& operator<<(std::ostream& os, typename connection<T>::event e)
{
os << to_string(e);
return os;
}
} // aedis
#endif // AEDIS_CONNECTION_HPP

View File

@@ -1,664 +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)
*/
#ifndef AEDIS_CONNECTION_OPS_HPP
#define AEDIS_CONNECTION_OPS_HPP
#include <array>
#include <algorithm>
#include <boost/assert.hpp>
#include <boost/system.hpp>
#include <boost/asio/write.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
#include <aedis/adapt.hpp>
#include <aedis/error.hpp>
#include <aedis/detail/net.hpp>
#include <aedis/resp3/type.hpp>
#include <aedis/resp3/detail/exec.hpp>
#include <aedis/resp3/detail/parser.hpp>
#include <aedis/resp3/read.hpp>
#include <aedis/resp3/write.hpp>
#include <aedis/resp3/request.hpp>
#define HANDLER_LOCATION \
BOOST_ASIO_HANDLER_LOCATION((__FILE__, __LINE__, __func__))
namespace aedis {
namespace detail {
#include <boost/asio/yield.hpp>
template <class Conn>
struct connect_with_timeout_op {
Conn* conn;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, boost::system::error_code ec = {}
, boost::asio::ip::tcp::endpoint const& = {})
{
reenter (coro)
{
BOOST_ASSERT(conn->socket_ != nullptr);
conn->ping_timer_.expires_after(conn->cfg_.connect_timeout);
yield
aedis::detail::async_connect(*conn->socket_, conn->ping_timer_, conn->endpoints_, std::move(self));
self.complete(ec);
}
}
};
template <class Conn>
struct resolve_with_timeout_op {
Conn* conn;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, boost::system::error_code ec = {}
, boost::asio::ip::tcp::resolver::results_type res = {})
{
reenter (coro)
{
conn->ping_timer_.expires_after(conn->cfg_.resolve_timeout);
yield
aedis::detail::async_resolve(
conn->resv_, conn->ping_timer_,
conn->cfg_.host, conn->cfg_.port, std::move(self));
conn->endpoints_ = res;
self.complete(ec);
}
}
};
template <class Conn, class Adapter>
struct receive_push_op {
Conn* conn = nullptr;
Adapter adapter;
std::size_t read_size = 0;
boost::asio::coroutine coro{};
template <class Self>
void
operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
reenter (coro)
{
yield
conn->push_channel_.async_receive(std::move(self));
if (ec) {
self.complete(ec, 0);
return;
}
BOOST_ASSERT(conn->socket_ != nullptr);
yield
resp3::async_read(*conn->socket_, conn->make_dynamic_buffer(), adapter, std::move(self));
if (ec) {
conn->cancel(Conn::operation::run);
self.complete(ec, 0);
return;
}
read_size = n;
yield
conn->push_channel_.async_send({}, 0, std::move(self));
self.complete(ec, read_size);
return;
}
}
};
template <class Conn, class Adapter>
struct exec_read_op {
Conn* conn;
Adapter adapter;
std::size_t cmds = 0;
std::size_t read_size = 0;
std::size_t index = 0;
boost::asio::coroutine coro{};
template <class Self>
void
operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
reenter (coro)
{
// Loop reading the responses to this request.
BOOST_ASSERT(!conn->reqs_.empty());
while (cmds != 0) {
BOOST_ASSERT(conn->cmds_ != 0);
//-----------------------------------
// If we detect a push in the middle of a request we have
// to hand it to the push consumer. To do that we need
// some data in the read bufer.
if (conn->read_buffer_.empty()) {
BOOST_ASSERT(conn->socket_ != nullptr);
yield
boost::asio::async_read_until(*conn->socket_, conn->make_dynamic_buffer(), "\r\n", std::move(self));
if (ec) {
conn->cancel(Conn::operation::run);
self.complete(ec, 0);
return;
}
}
// If the next request is a push we have to handle it to
// the receive_push_op wait for it to be done and continue.
if (resp3::to_type(conn->read_buffer_.front()) == resp3::type::push) {
yield
async_send_receive(conn->push_channel_, std::move(self));
if (ec) {
// Notice we don't call cancel_run() as that is the
// responsability of the receive_push_op.
self.complete(ec, 0);
return;
}
continue;
}
//-----------------------------------
yield
resp3::async_read(*conn->socket_, conn->make_dynamic_buffer(),
[i = index, adpt = adapter] (resp3::node<boost::string_view> const& nd, boost::system::error_code& ec) mutable { adpt(i, nd, ec); },
std::move(self));
++index;
if (ec) {
conn->cancel(Conn::operation::run);
self.complete(ec, 0);
return;
}
read_size += n;
BOOST_ASSERT(cmds != 0);
--cmds;
BOOST_ASSERT(conn->cmds_ != 0);
--conn->cmds_;
}
self.complete({}, read_size);
}
}
};
template <class Conn, class Adapter>
struct exec_op {
using req_info_type = typename Conn::req_info;
Conn* conn = nullptr;
resp3::request const* req = nullptr;
Adapter adapter{};
std::shared_ptr<req_info_type> info = nullptr;
std::size_t read_size = 0;
boost::asio::coroutine coro{};
template <class Self>
void
operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
reenter (coro)
{
info = std::allocate_shared<req_info_type>(boost::asio::get_associated_allocator(self), conn->resv_.get_executor());
info->timer.expires_at(std::chrono::steady_clock::time_point::max());
info->req = req;
info->cmds = req->size();
info->stop = false;
conn->add_request_info(info);
yield
info->timer.async_wait(std::move(self));
BOOST_ASSERT(conn->socket_ != nullptr);
BOOST_ASSERT(!!ec);
if (info->stop) {
self.complete(ec, 0);
return;
}
BOOST_ASSERT(conn->socket_->is_open());
if (req->size() == 0) {
self.complete({}, 0);
return;
}
BOOST_ASSERT(!conn->reqs_.empty());
BOOST_ASSERT(conn->reqs_.front() != nullptr);
BOOST_ASSERT(conn->cmds_ != 0);
yield
conn->async_exec_read(adapter, conn->reqs_.front()->cmds, std::move(self));
if (ec) {
self.complete(ec, 0);
return;
}
read_size = n;
BOOST_ASSERT(!conn->reqs_.empty());
conn->reqs_.pop_front();
if (conn->cmds_ == 0) {
conn->read_timer_.cancel_one();
if (!conn->reqs_.empty())
conn->writer_timer_.cancel_one();
} else {
BOOST_ASSERT(!conn->reqs_.empty());
conn->reqs_.front()->timer.cancel_one();
}
self.complete({}, read_size);
}
}
};
template <class Conn>
struct ping_op {
Conn* conn;
boost::asio::coroutine coro{};
template <class Self>
void
operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t = 0)
{
reenter (coro) for (;;)
{
conn->ping_timer_.expires_after(conn->cfg_.ping_interval);
yield
conn->ping_timer_.async_wait(std::move(self));
BOOST_ASSERT(conn->socket_ != nullptr);
if (ec || !conn->socket_->is_open()) {
self.complete(ec);
return;
}
conn->req_.clear();
conn->req_.push("PING");
conn->req_.close_on_run_completion = true;
yield
conn->async_exec(conn->req_, adapt(), std::move(self));
if (ec) {
// Notice we don't report error but let the idle check
// timeout. It is enough to finish the op.
self.complete({});
return;
}
}
}
};
template <class Conn>
struct check_idle_op {
Conn* conn;
boost::asio::coroutine coro{};
template <class Self>
void operator()(Self& self, boost::system::error_code ec = {})
{
reenter (coro) for (;;)
{
conn->check_idle_timer_.expires_after(2 * conn->cfg_.ping_interval);
yield
conn->check_idle_timer_.async_wait(std::move(self));
BOOST_ASSERT(conn->socket_ != nullptr);
if (ec || !conn->socket_->is_open()) {
// Notice this is not an error, it was requested from an
// external op.
self.complete({});
return;
}
auto const now = std::chrono::steady_clock::now();
if (conn->last_data_ + (2 * conn->cfg_.ping_interval) < now) {
conn->cancel(Conn::operation::run);
self.complete(error::idle_timeout);
return;
}
conn->last_data_ = now;
}
}
};
template <class Conn>
struct start_op {
Conn* conn;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 4> order = {}
, boost::system::error_code ec0 = {}
, boost::system::error_code ec1 = {}
, boost::system::error_code ec2 = {}
, boost::system::error_code ec3 = {})
{
reenter (coro)
{
yield
boost::asio::experimental::make_parallel_group(
[this](auto token) { return conn->reader(token);},
[this](auto token) { return conn->writer(token);},
[this](auto token) { return conn->async_check_idle(token);},
[this](auto token) { return conn->async_ping(token);}
).async_wait(
boost::asio::experimental::wait_for_one_error(),
std::move(self));
switch (order[0]) {
case 0: self.complete(ec0); break;
case 1: self.complete(ec1); break;
case 2: self.complete(ec2); break;
case 3: self.complete(ec3); break;
default: BOOST_ASSERT(false);
}
}
}
};
template <class Conn>
struct run_one_op {
Conn* conn;
boost::asio::coroutine coro{};
template <class Self>
void operator()(
Self& self,
boost::system::error_code ec = {},
std::size_t = 0)
{
reenter (coro)
{
yield
conn->async_resolve_with_timeout(std::move(self));
if (ec) {
conn->cancel(Conn::operation::run);
self.complete(ec);
return;
}
if (conn->cfg_.enable_events) {
yield
conn->event_channel_.async_send({}, Conn::event::resolve, std::move(self));
if (ec) {
self.complete(ec);
return;
}
}
conn->socket_ = std::make_shared<typename Conn::next_layer_type>(conn->resv_.get_executor());
yield
conn->async_connect_with_timeout(std::move(self));
if (ec) {
conn->cancel(Conn::operation::run);
self.complete(ec);
return;
}
if (conn->cfg_.enable_events) {
yield
conn->event_channel_.async_send({}, Conn::event::connect, std::move(self));
if (ec) {
self.complete(ec);
return;
}
}
conn->req_.clear();
if (!std::empty(conn->cfg_.username) && !std::empty(conn->cfg_.password))
conn->req_.push("AUTH", conn->cfg_.username, conn->cfg_.password);
conn->req_.push("HELLO", "3");
conn->ping_timer_.expires_after(conn->cfg_.ping_interval);
yield
async_exec(
*conn->socket_,
conn->ping_timer_,
conn->req_,
adapter::adapt2(),
conn->make_dynamic_buffer(),
std::move(self)
);
if (ec) {
conn->cancel(Conn::operation::run);
self.complete(ec);
return;
}
if (conn->cfg_.enable_events) {
yield
conn->event_channel_.async_send({}, Conn::event::hello, std::move(self));
if (ec) {
self.complete(ec);
return;
}
}
conn->write_buffer_.clear();
conn->cmds_ = 0;
std::for_each(std::begin(conn->reqs_), std::end(conn->reqs_), [](auto const& ptr) {
return ptr->written = false;
});
yield
conn->async_start(std::move(self));
self.complete(ec);
}
}
};
template <class Conn>
struct run_op {
Conn* conn;
boost::asio::coroutine coro{};
template <class Self>
void operator()(
Self& self,
boost::system::error_code ec = {},
std::size_t = 0)
{
reenter (coro) for(;;)
{
yield
conn->async_run_one(std::move(self));
if (!conn->cfg_.enable_reconnect) {
self.complete(ec);
return;
}
// Consider communicating the return of async_run_one as an
// event here.
conn->ping_timer_.expires_after(conn->cfg_.reconnect_interval);
yield
conn->ping_timer_.async_wait(std::move(self));
}
}
};
template <class Conn>
struct writer_op {
Conn* conn;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
boost::ignore_unused(n);
reenter (coro) for (;;)
{
while (!conn->reqs_.empty() && conn->cmds_ == 0 && conn->write_buffer_.empty()) {
conn->coalesce_requests();
yield
boost::asio::async_write(*conn->socket_, boost::asio::buffer(conn->write_buffer_), std::move(self));
if (ec) {
self.complete(ec);
return;
}
// We have to clear the payload right after the read op in
// order to to use it as a flag that informs there is no
// ongoing write.
conn->write_buffer_.clear();
conn->cancel_push_requests();
}
if (conn->socket_->is_open()) {
yield
conn->writer_timer_.async_wait(std::move(self));
// The timer may be canceled either to stop the write op
// or to proceed to the next write, the difference between
// the two is that for the former the socket will be
// closed first. We check for that below.
}
if (!conn->socket_->is_open()) {
// 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>
struct reader_op {
Conn* conn;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
boost::ignore_unused(n);
reenter (coro) for (;;)
{
BOOST_ASSERT(conn->socket_->is_open());
yield
boost::asio::async_read_until(*conn->socket_, conn->make_dynamic_buffer(), "\r\n", std::move(self));
if (ec) {
conn->cancel(Conn::operation::run);
self.complete(ec);
return;
}
conn->last_data_ = std::chrono::steady_clock::now();
// 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.
//
BOOST_ASSERT(!conn->read_buffer_.empty());
if (resp3::to_type(conn->read_buffer_.front()) == resp3::type::push
|| conn->reqs_.empty()
|| (!conn->reqs_.empty() && conn->reqs_.front()->cmds == 0)) {
yield
async_send_receive(conn->push_channel_, std::move(self));
if (ec) {
self.complete(ec);
return;
}
} else {
BOOST_ASSERT(conn->cmds_ != 0);
BOOST_ASSERT(!conn->reqs_.empty());
BOOST_ASSERT(conn->reqs_.front()->cmds != 0);
conn->reqs_.front()->timer.cancel_one();
yield
conn->read_timer_.async_wait(std::move(self));
if (!conn->socket_->is_open()) {
self.complete({});
return;
}
}
}
}
};
template <class Conn, class Adapter>
struct runexec_op {
Conn* conn;
resp3::request const* req = nullptr;
Adapter adapter;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 2> order = {}
, boost::system::error_code ec1 = {}
, boost::system::error_code ec2 = {}
, std::size_t n = 0)
{
reenter (coro)
{
req->close_on_run_completion = true;
yield
boost::asio::experimental::make_parallel_group(
[this](auto token) { return conn->async_run(token);},
[this](auto token) { return conn->async_exec(*req, adapter, token);}
).async_wait(
boost::asio::experimental::wait_for_one_error(),
std::move(self));
switch (order[0]) {
case 0: self.complete(ec1, n); break;
case 1: self.complete(ec2, n); break;
default: BOOST_ASSERT(false);
}
}
}
};
#include <boost/asio/unyield.hpp>
} // detail
} // aedis
#endif // AEDIS_CONNECTION_OPS_HPP

View File

@@ -1,208 +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)
*/
#ifndef AEDIS_NET_HPP
#define AEDIS_NET_HPP
#include <array>
#include <boost/system.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/connect.hpp>
#include <boost/assert.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
namespace aedis {
namespace detail {
template <class Executor>
using conn_timer_t = boost::asio::basic_waitable_timer<std::chrono::steady_clock, boost::asio::wait_traits<std::chrono::steady_clock>, Executor>;
#include <boost/asio/yield.hpp>
template <
class Protocol,
class Executor,
class EndpointSequence
>
struct connect_op {
boost::asio::basic_socket<Protocol, Executor>* socket;
conn_timer_t<Executor>* timer;
EndpointSequence* endpoints;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 2> order = {}
, boost::system::error_code ec1 = {}
, typename Protocol::endpoint const& ep = {}
, boost::system::error_code ec2 = {})
{
reenter (coro)
{
yield
boost::asio::experimental::make_parallel_group(
[this](auto token)
{
auto f = [](boost::system::error_code const&, typename Protocol::endpoint const&) { return true; };
return boost::asio::async_connect(*socket, *endpoints, f, token);
},
[this](auto token) { return timer->async_wait(token);}
).async_wait(
boost::asio::experimental::wait_for_one(),
std::move(self));
switch (order[0]) {
case 0: self.complete(ec1, ep); break;
case 1:
{
if (ec2)
self.complete({}, ep);
else
self.complete(error::connect_timeout, ep);
} break;
default: BOOST_ASSERT(false);
}
}
}
};
template <class Resolver, class Timer>
struct resolve_op {
Resolver* resv;
Timer* timer;
boost::string_view host;
boost::string_view port;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 2> order = {}
, boost::system::error_code ec1 = {}
, boost::asio::ip::tcp::resolver::results_type res = {}
, boost::system::error_code ec2 = {})
{
reenter (coro)
{
yield
boost::asio::experimental::make_parallel_group(
[this](auto token) { return resv->async_resolve(host.data(), port.data(), token);},
[this](auto token) { return timer->async_wait(token);}
).async_wait(
boost::asio::experimental::wait_for_one(),
std::move(self));
switch (order[0]) {
case 0:
{
if (ec1) {
self.complete(ec1, {});
return;
}
} break;
case 1:
{
if (!ec2) {
self.complete(error::resolve_timeout, {});
return;
}
} break;
default: BOOST_ASSERT(false);
}
self.complete({}, res);
}
}
};
template <class Channel>
struct send_receive_op {
Channel* channel;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t = 0)
{
reenter (coro)
{
yield
channel->async_send(boost::system::error_code{}, 0, std::move(self));
if (ec) {
self.complete(ec, 0);
return;
}
yield
channel->async_receive(std::move(self));
self.complete(ec, 0);
}
}
};
#include <boost/asio/unyield.hpp>
template <
class Protocol,
class Executor,
class EndpointSequence,
class CompletionToken = boost::asio::default_completion_token_t<Executor>
>
auto async_connect(
boost::asio::basic_socket<Protocol, Executor>& socket,
conn_timer_t<Executor>& timer,
EndpointSequence ep,
CompletionToken&& token = boost::asio::default_completion_token_t<Executor>{})
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, typename Protocol::endpoint const&)
>(connect_op<Protocol, Executor, EndpointSequence>
{&socket, &timer, &ep}, token, socket, timer);
}
template <
class Resolver,
class Timer,
class CompletionToken =
boost::asio::default_completion_token_t<typename Resolver::executor_type>
>
auto async_resolve(
Resolver& resv,
Timer& timer,
boost::string_view host,
boost::string_view port,
CompletionToken&& token = CompletionToken{})
{
// TODO: Use static_assert to check Resolver::executor_type and
// Timer::executor_type are same.
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, boost::asio::ip::tcp::resolver::results_type)
>(resolve_op<Resolver, Timer>{&resv, &timer, host, port}, token, resv, timer);
}
template <
class Channel,
class CompletionToken =
boost::asio::default_completion_token_t<typename Channel::executor_type>
>
auto async_send_receive(Channel& channel, CompletionToken&& token = CompletionToken{})
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(send_receive_op<Channel>{&channel}, token, channel);
}
} // detail
} // aedis
#endif // AEDIS_NET_HPP

View File

@@ -1,96 +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)
*/
#ifndef AEDIS_ERROR_HPP
#define AEDIS_ERROR_HPP
#include <boost/system/error_code.hpp>
namespace aedis {
/** \brief Generic errors.
* \ingroup any
*/
enum class error
{
/// Resolve timeout.
resolve_timeout = 1,
/// Connect timeout.
connect_timeout,
/// Idle timeout.
idle_timeout,
/// Exec timeout.
exec_timeout,
/// Invalid RESP3 type.
invalid_data_type,
/// Can't parse the string as a number.
not_a_number,
/// Received less bytes than expected.
unexpected_read_size,
/// The maximum depth of a nested response was exceeded.
exceeeds_max_nested_depth,
/// Got non boolean value.
unexpected_bool_value,
/// Expected field value is empty.
empty_field,
/// Expects a simple RESP3 type but got an aggregate.
expects_resp3_simple_type,
/// Expects aggregate.
expects_resp3_aggregate,
/// Expects a map but got other aggregate.
expects_resp3_map,
/// Expects a set aggregate but got something else.
expects_resp3_set,
/// Nested response not supported.
nested_aggregate_unsupported,
/// Got RESP3 simple error.
simple_error,
/// Got RESP3 blob_error.
blob_error,
/// Aggregate container has incompatible size.
incompatible_size,
/// Not a double
not_a_double,
/// Got RESP3 null.
null
};
/** \internal
* \brief Creates a error_code object from an error.
* \param e Error code.
* \ingroup any
*/
boost::system::error_code make_error_code(error e);
} // aedis
namespace std {
template<>
struct is_error_code_enum<::aedis::error> : std::true_type {};
} // std
#endif // AEDIS_ERROR_HPP

View File

@@ -1,62 +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 <aedis/error.hpp>
namespace aedis {
namespace detail {
struct error_category_impl : boost::system::error_category {
char const* name() const noexcept override
{
return "aedis";
}
std::string message(int ev) const override
{
switch(static_cast<error>(ev)) {
case error::resolve_timeout: return "Resolve operation timeout.";
case error::connect_timeout: return "Connect operation timeout.";
case error::idle_timeout: return "Idle timeout.";
case error::exec_timeout: return "Exec timeout.";
case error::invalid_data_type: return "Invalid resp3 type.";
case error::not_a_number: return "Can't convert string to number.";
case error::unexpected_read_size: return "Unexpected read size.";
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_unsupported: return "Nested aggregate unsupported.";
case error::simple_error: return "Got RESP3 simple-error.";
case error::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::null: return "Got RESP3 null.";
default:
BOOST_ASSERT(false);
return "Aedis error.";
}
}
};
boost::system::error_category const& category()
{
static error_category_impl instance;
return instance;
}
} // detail
boost::system::error_code make_error_code(error e)
{
return boost::system::error_code{static_cast<int>(e), detail::category()};
}
} // aedis

View File

@@ -1,176 +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)
*/
#ifndef AEDIS_RESP3_EXEC_HPP
#define AEDIS_RESP3_EXEC_HPP
#include <boost/assert.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/write.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/experimental/parallel_group.hpp>
#include <aedis/error.hpp>
#include <aedis/resp3/read.hpp>
#include <aedis/resp3/request.hpp>
namespace aedis {
namespace resp3 {
namespace detail {
#include <boost/asio/yield.hpp>
template <
class AsyncStream,
class Adapter,
class DynamicBuffer
>
struct exec_op {
AsyncStream* socket = nullptr;
request const* req = nullptr;
Adapter adapter;
DynamicBuffer dbuf{};
std::size_t n_cmds = 0;
std::size_t size = 0;
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
reenter (coro) for (;;)
{
if (req) {
yield
boost::asio::async_write(
*socket,
boost::asio::buffer(req->payload()),
std::move(self));
if (ec || n_cmds == 0) {
self.complete(ec, n);
return;
}
req = nullptr;
}
yield resp3::async_read(*socket, dbuf, adapter, std::move(self));
if (ec) {
self.complete(ec, 0);
return;
}
size += n;
if (--n_cmds == 0) {
self.complete(ec, size);
return;
}
}
}
};
template <
class AsyncStream,
class Adapter,
class DynamicBuffer,
class CompletionToken = boost::asio::default_completion_token_t<typename AsyncStream::executor_type>
>
auto async_exec(
AsyncStream& socket,
request const& req,
Adapter adapter,
DynamicBuffer dbuf,
CompletionToken token = CompletionToken{})
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(detail::exec_op<AsyncStream, Adapter, DynamicBuffer>
{&socket, &req, adapter, dbuf, req.size()}, token, socket);
}
template <
class AsyncStream,
class Timer,
class Adapter,
class DynamicBuffer
>
struct exec_with_timeout_op {
AsyncStream* socket = nullptr;
Timer* timer = nullptr;
request const* req = nullptr;
Adapter adapter;
DynamicBuffer dbuf{};
boost::asio::coroutine coro{};
template <class Self>
void operator()( Self& self
, std::array<std::size_t, 2> order = {}
, boost::system::error_code ec1 = {}
, std::size_t n = 0
, boost::system::error_code ec2 = {})
{
reenter (coro)
{
yield
boost::asio::experimental::make_parallel_group(
[this](auto token) { return detail::async_exec(*socket, *req, adapter, dbuf, token);},
[this](auto token) { return timer->async_wait(token);}
).async_wait(
boost::asio::experimental::wait_for_one(),
std::move(self));
switch (order[0]) {
case 0: self.complete(ec1, n); break;
case 1:
{
if (ec2)
self.complete({}, n);
else
self.complete(aedis::error::exec_timeout, 0);
} break;
default: BOOST_ASSERT(false);
}
}
}
};
#include <boost/asio/unyield.hpp>
template <
class AsyncStream,
class Timer,
class Adapter,
class DynamicBuffer,
class CompletionToken = boost::asio::default_completion_token_t<typename AsyncStream::executor_type>
>
auto async_exec(
AsyncStream& socket,
Timer& timer,
request const& req,
Adapter adapter,
DynamicBuffer dbuf,
CompletionToken token = CompletionToken{})
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(detail::exec_with_timeout_op<AsyncStream, Timer, Adapter, DynamicBuffer>
{&socket, &timer, &req, adapter, dbuf}, token, socket, timer);
}
} // detail
} // resp3
} // aedis
#endif // AEDIS_RESP3_EXEC_HPP

View File

@@ -1,30 +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/spirit/include/qi.hpp>
#include <boost/spirit/home/x3.hpp>
#include <aedis/resp3/detail/parser.hpp>
#include <aedis/resp3/type.hpp>
namespace aedis {
namespace resp3 {
namespace detail {
std::size_t
parse_uint(char const* data, std::size_t size, boost::system::error_code& ec)
{
static constexpr boost::spirit::x3::uint_parser<std::size_t, 10> p{};
std::size_t ret;
if (!parse(data, data + size, p, ret))
ec = error::not_a_number;
return ret;
}
} // detail
} // resp3
} // aedis

View File

@@ -1,226 +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)
*/
#ifndef AEDIS_RESP3_PARSER_HPP
#define AEDIS_RESP3_PARSER_HPP
#include <limits>
#include <system_error>
#include <boost/assert.hpp>
#include <boost/utility/string_view.hpp>
#include <aedis/error.hpp>
#include <aedis/resp3/node.hpp>
namespace aedis {
namespace resp3 {
namespace detail {
std::size_t parse_uint(char const* data, std::size_t size, boost::system::error_code& ec);
template <class ResponseAdapter>
class parser {
private:
using node_type = node<boost::string_view>;
static constexpr std::size_t max_embedded_depth = 5;
ResponseAdapter adapter_;
// The current depth. Simple data types will have depth 0, whereas
// the elements of aggregates will have depth 1. Embedded types
// will have increasing depth.
std::size_t depth_ = 0;
// The parser supports up to 5 levels of nested structures. The
// first element in the sizes stack is a sentinel and must be
// different from 1.
std::size_t sizes_[max_embedded_depth + 1] = {1};
// Contains the length expected in the next bulk read.
std::size_t bulk_length_ = (std::numeric_limits<std::size_t>::max)();
// The type of the next bulk. Contains type::invalid if no bulk is
// expected.
type bulk_ = type::invalid;
public:
parser(ResponseAdapter adapter)
: adapter_{adapter}
{
sizes_[0] = 2; // The sentinel must be more than 1.
}
// Returns the number of bytes that have been consumed.
std::size_t
consume(char const* data, std::size_t n, boost::system::error_code& ec)
{
if (bulk_ != type::invalid) {
n = bulk_length_ + 2;
switch (bulk_) {
case type::streamed_string_part:
{
BOOST_ASSERT(bulk_length_ != 0);
adapter_({bulk_, 1, depth_, {data, bulk_length_}}, ec);
if (ec)
return 0;
} break;
default:
{
adapter_({bulk_, 1, depth_, {data, bulk_length_}}, ec);
if (ec)
return 0;
}
}
bulk_ = type::invalid;
--sizes_[depth_];
} else if (sizes_[depth_] != 0) {
auto const t = to_type(*data);
switch (t) {
case type::streamed_string_part:
{
bulk_length_ = parse_uint(data + 1, n - 2, ec);
if (ec)
return 0;
if (bulk_length_ == 0) {
adapter_({type::streamed_string_part, 1, depth_, {}}, ec);
sizes_[depth_] = 0; // We are done.
} else {
bulk_ = type::streamed_string_part;
}
} break;
case type::blob_error:
case type::verbatim_string:
case type::blob_string:
{
if (*(data + 1) == '?') {
// NOTE: This can only be triggered with blob_string.
// Trick: A streamed string is read as an aggregate
// of infinite lenght. When the streaming is done
// the server is supposed to send a part with length
// 0.
sizes_[++depth_] = (std::numeric_limits<std::size_t>::max)();
} else {
bulk_length_ = parse_uint(data + 1, n - 2, ec);
if (ec)
return 0;
bulk_ = t;
}
} break;
case type::boolean:
{
if (n == 3) {
ec = error::empty_field;
return 0;
}
if (*(data + 1) != 'f' && *(data + 1) != 't') {
ec = error::unexpected_bool_value;
return 0;
}
adapter_({t, 1, depth_, {data + 1, n - 3}}, ec);
if (ec)
return 0;
--sizes_[depth_];
} break;
case type::doublean:
case type::big_number:
case type::number:
{
if (n == 3) {
ec = error::empty_field;
return 0;
}
adapter_({t, 1, depth_, {data + 1, n - 3}}, ec);
if (ec)
return 0;
--sizes_[depth_];
} break;
case type::simple_error:
case type::simple_string:
{
adapter_({t, 1, depth_, {data + 1, n - 3}}, ec);
if (ec)
return 0;
--sizes_[depth_];
} break;
case type::null:
{
adapter_({type::null, 1, depth_, {}}, ec);
if (ec)
return 0;
--sizes_[depth_];
} break;
case type::push:
case type::set:
case type::array:
case type::attribute:
case type::map:
{
auto const l = parse_uint(data + 1, n - 2, ec);
if (ec)
return 0;
adapter_({t, l, depth_, {}}, ec);
if (ec)
return 0;
if (l == 0) {
--sizes_[depth_];
} else {
if (depth_ == max_embedded_depth) {
ec = error::exceeeds_max_nested_depth;
return 0;
}
++depth_;
sizes_[depth_] = l * element_multiplicity(t);
}
} break;
default:
{
ec = error::invalid_data_type;
return 0;
}
}
}
while (sizes_[depth_] == 0) {
--depth_;
--sizes_[depth_];
}
return n;
}
// Returns true when the parser is done with the current message.
auto done() const noexcept
{ return depth_ == 0 && bulk_ == type::invalid; }
// The bulk type expected in the next read. If none is expected returns
// type::invalid.
auto bulk() const noexcept { return bulk_; }
// The length expected in the the next bulk.
auto bulk_length() const noexcept { return bulk_length_; }
};
} // detail
} // resp3
} // aedis
#endif // AEDIS_RESP3_PARSER_HPP

View File

@@ -1,115 +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)
*/
#ifndef AEDIS_RESP3_READ_OPS_HPP
#define AEDIS_RESP3_READ_OPS_HPP
#include <boost/assert.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/utility/string_view.hpp>
#include <aedis/resp3/detail/parser.hpp>
namespace aedis {
namespace resp3 {
namespace detail {
#include <boost/asio/yield.hpp>
struct ignore_response {
void operator()(node<boost::string_view>, boost::system::error_code&) { }
};
template <
class AsyncReadStream,
class DynamicBuffer,
class ResponseAdapter>
class parse_op {
private:
AsyncReadStream& stream_;
DynamicBuffer buf_;
parser<ResponseAdapter> parser_;
std::size_t consumed_;
std::size_t buffer_size_;
boost::asio::coroutine coro_{};
public:
parse_op(AsyncReadStream& stream, DynamicBuffer buf, ResponseAdapter adapter)
: stream_ {stream}
, buf_ {buf}
, parser_ {adapter}
, consumed_{0}
{ }
template <class Self>
void operator()( Self& self
, boost::system::error_code ec = {}
, std::size_t n = 0)
{
reenter (coro_) for (;;) {
if (parser_.bulk() == type::invalid) {
yield
boost::asio::async_read_until(stream_, buf_, "\r\n", std::move(self));
if (ec) {
self.complete(ec, 0);
return;
}
} else {
// On a bulk read we can't read until delimiter since the
// payload may contain the delimiter itself so we have to
// read the whole chunk. However if the bulk blob is small
// enough it may be already on the buffer (from the last
// read), in which case there is no need of initiating
// another async op, otherwise we have to read the missing
// bytes.
if (buf_.size() < (parser_.bulk_length() + 2)) {
buffer_size_ = buf_.size();
buf_.grow(parser_.bulk_length() + 2 - buffer_size_);
yield
boost::asio::async_read(
stream_,
buf_.data(buffer_size_, parser_.bulk_length() + 2 - buffer_size_),
boost::asio::transfer_all(),
std::move(self));
if (ec) {
self.complete(ec, 0);
return;
}
}
n = parser_.bulk_length() + 2;
BOOST_ASSERT(buf_.size() >= n);
}
n = parser_.consume((char const*)buf_.data(0, n).data(), n, ec);
if (ec) {
self.complete(ec, 0);
return;
}
buf_.consume(n);
consumed_ += n;
if (parser_.done()) {
self.complete({}, consumed_);
return;
}
}
}
};
#include <boost/asio/unyield.hpp>
} // detail
} // resp3
} // aedis
#endif // AEDIS_RESP3_READ_OPS_HPP

View File

@@ -1,23 +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 <aedis/resp3/request.hpp>
namespace aedis {
namespace resp3 {
namespace detail {
bool has_push_response(boost::string_view cmd)
{
if (cmd == "SUBSCRIBE") return true;
if (cmd == "PSUBSCRIBE") return true;
if (cmd == "UNSUBSCRIBE") return true;
return false;
}
} // detail
} // resp3
} // aedis

View File

@@ -1,113 +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/assert.hpp>
#include <aedis/resp3/type.hpp>
namespace aedis {
namespace resp3 {
char const* to_string(type t)
{
switch (t) {
case type::array: return "array";
case type::push: return "push";
case type::set: return "set";
case type::map: return "map";
case type::attribute: return "attribute";
case type::simple_string: return "simple_string";
case type::simple_error: return "simple_error";
case type::number: return "number";
case type::doublean: return "doublean";
case type::boolean: return "boolean";
case type::big_number: return "big_number";
case type::null: return "null";
case type::blob_error: return "blob_error";
case type::verbatim_string: return "verbatim_string";
case type::blob_string: return "blob_string";
case type::streamed_string_part: return "streamed_string_part";
default: return "invalid";
}
}
std::ostream& operator<<(std::ostream& os, type t)
{
os << to_string(t);
return os;
}
bool is_aggregate(type t)
{
switch (t) {
case type::array:
case type::push:
case type::set:
case type::map:
case type::attribute: return true;
default: return false;
}
}
std::size_t element_multiplicity(type t)
{
switch (t) {
case type::map:
case type::attribute: return 2ULL;
default: return 1ULL;
}
}
char to_code(type t)
{
switch (t) {
case type::blob_error: return '!';
case type::verbatim_string: return '=';
case type::blob_string: return '$';
case type::streamed_string_part: return ';';
case type::simple_error: return '-';
case type::number: return ':';
case type::doublean: return ',';
case type::boolean: return '#';
case type::big_number: return '(';
case type::simple_string: return '+';
case type::null: return '_';
case type::push: return '>';
case type::set: return '~';
case type::array: return '*';
case type::attribute: return '|';
case type::map: return '%';
default:
BOOST_ASSERT(false);
return ' ';
}
}
type to_type(char c)
{
switch (c) {
case '!': return type::blob_error;
case '=': return type::verbatim_string;
case '$': return type::blob_string;
case ';': return type::streamed_string_part;
case '-': return type::simple_error;
case ':': return type::number;
case ',': return type::doublean;
case '#': return type::boolean;
case '(': return type::big_number;
case '+': return type::simple_string;
case '_': return type::null;
case '>': return type::push;
case '~': return type::set;
case '*': return type::array;
case '|': return type::attribute;
case '%': return type::map;
default: return type::invalid;
}
}
} // resp3
} // aedis

View File

@@ -1,96 +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)
*/
#ifndef AEDIS_RESP3_NODE_HPP
#define AEDIS_RESP3_NODE_HPP
#include <aedis/resp3/type.hpp>
#include <string>
#include <vector>
namespace aedis {
namespace resp3 {
/** \brief A node in the response tree.
* \ingroup any
*
* Redis responses are the pre-order view of the response tree (see
* https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR).
*
* \remark Any Redis response can be received in an array of nodes,
* for example \c std::vector<node<std::string>>.
*/
template <class String>
struct node {
/// The RESP3 type of the data in this node.
resp3::type data_type;
/// The number of elements of an aggregate.
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 usually empty.
String value;
};
/** @brief Converts the node to a string.
* @relates node
*
* @param in The node object.
*/
template <class String>
std::string to_string(node<String> const& in)
{
std::string out;
out += std::to_string(in.depth);
out += '\t';
out += to_string(in.data_type);
out += '\t';
out += std::to_string(in.aggregate_size);
out += '\t';
if (!is_aggregate(in.data_type))
out.append(in.value.data(), in.value.size());
return out;
}
/** @brief Compares a node for equality.
* @relates node
*
* @param a Left hand side node object.
* @param b Right hand side node object.
*/
template <class String>
bool operator==(node<String> const& a, node<String> const& b)
{
return a.aggregate_size == b.aggregate_size
&& a.depth == b.depth
&& a.data_type == b.data_type
&& a.value == b.value;
};
/** @brief Writes the node string to the stream.
* @relates node
*
* @param os Output stream.
* @param node Node object.
*
* \remark Binary data is not converted to text.
*/
template <class String>
std::ostream& operator<<(std::ostream& os, node<String> const& node)
{
os << to_string(node);
return os;
}
} // adapter
} // aedis
#endif // AEDIS_RESP3_NODE_HPP

View File

@@ -1,194 +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)
*/
#ifndef AEDIS_RESP3_READ_HPP
#define AEDIS_RESP3_READ_HPP
#include <aedis/resp3/type.hpp>
#include <aedis/resp3/detail/parser.hpp>
#include <aedis/resp3/detail/read_ops.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/async_result.hpp>
namespace aedis {
namespace resp3 {
/** \internal
* \brief Reads a complete response to a command sychronously.
* \ingroup any
*
* This function reads a complete response to a command or a
* server push synchronously. For example
*
* @code
* int resp;
* std::string buffer;
* resp3::read(socket, dynamic_buffer(buffer), adapt(resp));
* @endcode
*
* For a complete example see examples/intro_sync.cpp. This function
* is implemented in terms of one or more calls to @c
* asio::read_until and @c asio::read functions, and is known as a @a
* composed @a operation. Furthermore, the implementation may read
* additional bytes from the stream that lie past the end of the
* message being read. These additional bytes are stored in the
* dynamic buffer, which must be preserved for subsequent reads.
*
* \param stream The stream from which to read e.g. a tcp socket.
* \param buf Dynamic buffer (version 2).
* \param adapter The response adapter, see more on \ref low-level-responses.
* \param ec If an error occurs, it will be assigned to this paramter.
* \returns The number of bytes that have been consumed from the dynamic buffer.
*
* \remark This function calls buf.consume() in each chunk of data
* after it has been passed to the adapter. Users must not consume
* the bytes after it returns.
*/
template <
class SyncReadStream,
class DynamicBuffer,
class ResponseAdapter
>
std::size_t
read(
SyncReadStream& stream,
DynamicBuffer buf,
ResponseAdapter adapter,
boost::system::error_code& ec)
{
detail::parser<ResponseAdapter> p {adapter};
std::size_t n = 0;
std::size_t consumed = 0;
do {
if (p.bulk() == type::invalid) {
n = boost::asio::read_until(stream, buf, "\r\n", ec);
if (ec)
return 0;
if (n < 3) {
ec = error::unexpected_read_size;
return 0;
}
} else {
auto const s = buf.size();
auto const l = p.bulk_length();
if (s < (l + 2)) {
auto const to_read = l + 2 - s;
buf.grow(to_read);
n = boost::asio::read(stream, buf.data(s, to_read), ec);
if (ec)
return 0;
if (n < to_read) {
ec = error::unexpected_read_size;
return 0;
}
}
}
auto const* data = (char const*) buf.data(0, n).data();
n = p.consume(data, n, ec);
if (ec)
return 0;
buf.consume(n);
consumed += n;
} while (!p.done());
return consumed;
}
/** \internal
* \brief Reads a complete response to a command sychronously.
* \ingroup any
*
* Same as the error_code overload but throws on error.
*/
template<
class SyncReadStream,
class DynamicBuffer,
class ResponseAdapter = detail::ignore_response>
std::size_t
read(
SyncReadStream& stream,
DynamicBuffer buf,
ResponseAdapter adapter = ResponseAdapter{})
{
boost::system::error_code ec;
auto const n = resp3::read(stream, buf, adapter, ec);
if (ec)
BOOST_THROW_EXCEPTION(boost::system::system_error{ec});
return n;
}
/** \internal
* \brief Reads a complete response to a Redis command asynchronously.
* \ingroup any
*
* This function reads a complete response to a command or a
* server push asynchronously. For example
*
* @code
* std::string buffer;
* std::set<std::string> resp;
* co_await resp3::async_read(socket, dynamic_buffer(buffer), adapt(resp));
* @endcode
*
* For a complete example see examples/transaction.cpp. This function
* is implemented in terms of one or more calls to @c
* asio::async_read_until and @c asio::async_read functions, and is
* known as a @a composed @a operation. Furthermore, the
* implementation may read additional bytes from the stream that lie
* past the end of the message being read. These additional bytes are
* stored in the dynamic buffer, which must be preserved for
* subsequent reads.
*
* \param stream The stream from which to read e.g. a tcp socket.
* \param buffer Dynamic buffer (version 2).
* \param adapter The response adapter, see more on \ref low-level-responses.
* \param token The completion token.
*
* The completion handler will receive as a parameter the total
* number of bytes transferred from the stream and must have the
* following signature
*
* @code
* void(boost::system::error_code, std::size_t);
* @endcode
*
* \remark This function calls buf.consume() in each chunk of data
* after it has been passed to the adapter. Users must not consume
* the bytes after it returns.
*/
template <
class AsyncReadStream,
class DynamicBuffer,
class ResponseAdapter = detail::ignore_response,
class CompletionToken = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>
>
auto async_read(
AsyncReadStream& stream,
DynamicBuffer buffer,
ResponseAdapter adapter = ResponseAdapter{},
CompletionToken&& token =
boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>{})
{
return boost::asio::async_compose
< CompletionToken
, void(boost::system::error_code, std::size_t)
>(detail::parse_op<AsyncReadStream, DynamicBuffer, ResponseAdapter> {stream, buffer, adapter},
token,
stream);
}
} // resp3
} // aedis
#endif // AEDIS_RESP3_READ_HPP

View File

@@ -1,340 +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)
*/
#ifndef AEDIS_RESP3_REQUEST_HPP
#define AEDIS_RESP3_REQUEST_HPP
#include <string>
#include <tuple>
#include <boost/hana.hpp>
#include <boost/utility/string_view.hpp>
#include <aedis/resp3/type.hpp>
// NOTE: Consider detecting tuples in the type in the parameter pack
// to calculate the header size correctly.
//
// NOTE: For some commands like hset it would be a good idea to assert
// the value type is a pair.
namespace aedis {
namespace resp3 {
constexpr char separator[] = "\r\n";
/** @brief Adds a bulk to the request.
* @relates request
*
* This function is useful in serialization of your own data
* structures in a request. For example
*
* @code
* void to_bulk(std::string& to, mystruct const& obj)
* {
* auto const str = // Convert obj to a string.
* resp3::to_bulk(to, str);
* }
* @endcode
*
* @param to Storage on which data will be copied into.
* @param data Data that will be serialized and stored in @c to.
*
* See more in \ref requests-serialization.
*/
template <class Request>
void to_bulk(Request& to, boost::string_view data)
{
auto const str = std::to_string(data.size());
to += to_code(type::blob_string);
to.append(std::cbegin(str), std::cend(str));
to += separator;
to.append(std::cbegin(data), std::cend(data));
to += separator;
}
template <class Request, class T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
void to_bulk(Request& to, T n)
{
auto const s = std::to_string(n);
to_bulk(to, boost::string_view{s});
}
namespace detail {
bool has_push_response(boost::string_view cmd);
template <class T>
struct add_bulk_impl {
template <class Request>
static void add(Request& to, T const& from)
{
using namespace aedis::resp3;
to_bulk(to, from);
}
};
template <class U, class V>
struct add_bulk_impl<std::pair<U, V>> {
template <class Request>
static void add(Request& to, std::pair<U, V> const& from)
{
using namespace aedis::resp3;
to_bulk(to, from.first);
to_bulk(to, from.second);
}
};
template <class ...Ts>
struct add_bulk_impl<boost::hana::tuple<Ts...>> {
template <class Request>
static void add(Request& to, boost::hana::tuple<Ts...> const& from)
{
using boost::hana::for_each;
// Fold expressions is C++17 so we use hana.
//(detail::add_bulk(*request_, args), ...);
for_each(from, [&](auto const& e) {
using namespace aedis::resp3;
to_bulk(to, e);
});
}
};
template <class Request>
void add_header(Request& to, type t, std::size_t size)
{
auto const str = std::to_string(size);
to += to_code(t);
to.append(std::cbegin(str), std::cend(str));
to += separator;
}
template <class Request, class T>
void add_bulk(Request& to, T const& data)
{
detail::add_bulk_impl<T>::add(to, data);
}
template <class>
struct bulk_counter;
template <class>
struct bulk_counter {
static constexpr auto size = 1U;
};
template <class T, class U>
struct bulk_counter<std::pair<T, U>> {
static constexpr auto size = 2U;
};
template <class Request>
void add_blob(Request& to, boost::string_view blob)
{
to.append(std::cbegin(blob), std::cend(blob));
to += separator;
}
template <class Request>
void add_separator(Request& to)
{
to += separator;
}
} // detail
/** @brief Creates Redis requests.
* \ingroup any
*
* A request is composed of one or more Redis commands and is
* referred to in the redis documentation as a pipeline, see
* https://redis.io/topics/pipelining. For example
*
* @code
* request r;
* r.push("HELLO", 3);
* r.push("FLUSHALL");
* r.push("PING");
* r.push("PING", "key");
* r.push("QUIT");
* co_await async_write(socket, buffer(r));
* @endcode
*
* \remarks Non-string types will be converted to string by using \c
* to_bulk, which must be made available over ADL.
*/
class request {
public:
//// Returns the number of commands contained in this request.
std::size_t size() const noexcept { return commands_;};
// Returns the request payload.
auto const& payload() const noexcept { return payload_;}
/// Clears the request preserving allocated memory.
void clear()
{
payload_.clear();
commands_ = 0;
}
/** @brief Appends a new command to the end of the request.
*
* For example
*
* \code
* request req;
* req.push("SET", "key", "some string", "EX", "2");
* \endcode
*
* will add the \c set command with value "some string" and an
* expiration of 2 seconds.
*
* \param cmd The command e.g redis or sentinel command.
* \param args Command arguments.
*/
template <class... Ts>
void push(boost::string_view cmd, Ts const&... args)
{
using boost::hana::for_each;
using boost::hana::make_tuple;
using resp3::type;
auto constexpr pack_size = sizeof...(Ts);
detail::add_header(payload_, type::array, 1 + pack_size);
detail::add_bulk(payload_, cmd);
detail::add_bulk(payload_, make_tuple(args...));
if (!detail::has_push_response(cmd))
++commands_;
}
/** @brief Appends a new command to the end of the request.
*
* This overload is useful for commands that have a key and have a
* dynamic range of arguments. For example
*
* @code
* std::map<std::string, std::string> map
* { {"key1", "value1"}
* , {"key2", "value2"}
* , {"key3", "value3"}
* };
*
* request req;
* req.push_range2("HSET", "key", std::cbegin(map), std::cend(map));
* @endcode
*
* \param cmd The command e.g. Redis or Sentinel command.
* \param key The command key.
* \param begin Iterator to the begin of the range.
* \param end Iterator to the end of the range.
*/
template <class Key, class ForwardIterator>
void push_range2(boost::string_view cmd, Key const& key, ForwardIterator begin, ForwardIterator end)
{
using value_type = typename std::iterator_traits<ForwardIterator>::value_type;
using resp3::type;
if (begin == end)
return;
auto constexpr size = detail::bulk_counter<value_type>::size;
auto const distance = std::distance(begin, end);
detail::add_header(payload_, type::array, 2 + size * distance);
detail::add_bulk(payload_, cmd);
detail::add_bulk(payload_, key);
for (; begin != end; ++begin)
detail::add_bulk(payload_, *begin);
if (!detail::has_push_response(cmd))
++commands_;
}
/** @brief Appends a new command to the end of the request.
*
* This overload is useful for commands that have a dynamic number
* of arguments and don't have a key. For example
*
* \code
* std::set<std::string> channels
* { "channel1" , "channel2" , "channel3" }
*
* request req;
* req.push("SUBSCRIBE", std::cbegin(channels), std::cedn(channels));
* \endcode
*
* \param cmd The Redis command
* \param begin Iterator to the begin of the range.
* \param end Iterator to the end of the range.
*/
template <class ForwardIterator>
void push_range2(boost::string_view cmd, ForwardIterator begin, ForwardIterator end)
{
using value_type = typename std::iterator_traits<ForwardIterator>::value_type;
using resp3::type;
if (begin == end)
return;
auto constexpr size = detail::bulk_counter<value_type>::size;
auto const distance = std::distance(begin, end);
detail::add_header(payload_, type::array, 1 + size * distance);
detail::add_bulk(payload_, cmd);
for (; begin != end; ++begin)
detail::add_bulk(payload_, *begin);
if (!detail::has_push_response(cmd))
++commands_;
}
/** @brief Appends a new command to the end of the request.
*
* Equivalent to the overload taking a range (i.e. send_range2).
*
* \param cmd Redis command.
* \param key Redis key.
* \param range Range to send e.g. and \c std::map.
*/
template <class Key, class Range>
void push_range(boost::string_view cmd, Key const& key, Range const& range)
{
using std::begin;
using std::end;
push_range2(cmd, key, begin(range), end(range));
}
/** @brief Appends a new command to the end of the request.
*
* Equivalent to the overload taking a range (i.e. send_range2).
*
* \param cmd Redis command.
* \param range Range to send e.g. and \c std::map.
*/
template <class Range>
void push_range(boost::string_view cmd, Range const& range)
{
using std::begin;
using std::end;
push_range2(cmd, begin(range), end(range));
}
mutable bool close_on_run_completion = false;
private:
std::string payload_;
std::size_t commands_ = 0;
};
} // resp3
} // aedis
#endif // AEDIS_RESP3_SERIALIZER_HPP

View File

@@ -1,89 +0,0 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef AEDIS_RESP3_TYPE_HPP
#define AEDIS_RESP3_TYPE_HPP
#include <ostream>
#include <vector>
#include <string>
namespace aedis {
namespace resp3 {
/** \brief RESP3 data types.
\ingroup any
The RESP3 specification can be found at https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md.
*/
enum class type
{ /// Aggregate
array,
/// Aaggregate
push,
/// Aggregate
set,
/// Aggregate
map,
/// Aggregate
attribute,
/// Simple
simple_string,
/// Simple
simple_error,
/// Simple
number,
/// Simple
doublean,
/// Simple
boolean,
/// Simple
big_number,
/// Simple
null,
/// Simple
blob_error,
/// Simple
verbatim_string,
/// Simple
blob_string,
/// Simple
streamed_string_part,
/// Invalid
invalid
};
/** \brief Converts the data type to a string.
* \ingroup any
* \param t RESP3 type.
*/
char const* to_string(type t);
/** \brief Writes the type to the output stream.
* \ingroup any
* \param os Output stream.
* \param t RESP3 type.
*/
std::ostream& operator<<(std::ostream& os, type t);
/* Checks whether the data type is an aggregate.
*/
bool is_aggregate(type t);
// For map and attribute data types this function returns 2. All
// other types have value 1.
std::size_t element_multiplicity(type t);
// Returns the wire code of a given type.
char to_code(type t);
// Converts a wire-format RESP3 type (char) to a resp3 type.
type to_type(char c);
} // resp3
} // aedis
#endif // AEDIS_RESP3_TYPE_HPP

View File

@@ -1,53 +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)
*/
#ifndef AEDIS_RESP3_WRITE_HPP
#define AEDIS_RESP3_WRITE_HPP
#include <boost/asio/write.hpp>
namespace aedis {
namespace resp3 {
template<
class SyncWriteStream,
class Request
>
std::size_t write(SyncWriteStream& stream, Request const& req)
{
return boost::asio::write(stream, boost::asio::buffer(req.payload()));
}
template<
class SyncWriteStream,
class Request
>
std::size_t write(
SyncWriteStream& stream,
Request const& req,
boost::system::error_code& ec)
{
return boost::asio::write(stream, boost::asio::buffer(req.payload()), ec);
}
template<
class AsyncWriteStream,
class Request,
class CompletionToken = boost::asio::default_completion_token_t<typename AsyncWriteStream::executor_type>
>
auto async_write(
AsyncWriteStream& stream,
Request const& req,
CompletionToken&& token =
boost::asio::default_completion_token_t<typename AsyncWriteStream::executor_type>{})
{
return boost::asio::async_write(stream, boost::asio::buffer(req.payload()), token);
}
} // resp3
} // aedis
#endif // AEDIS_RESP3_WRITE_HPP

View File

@@ -1,245 +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)
*/
#ifndef AEDIS_SYNC_HPP
#define AEDIS_SYNC_HPP
#include <condition_variable>
#include <aedis/resp3/request.hpp>
namespace aedis {
/** @brief A high level synchronous connection to Redis.
* @ingroup any
*
* This class keeps a healthy and thread safe connection to the Redis
* instance where commands can be sent at any time. For more details,
* please see the documentation of each individual function.
*
*/
template <class Connection>
class sync {
public:
using event = typename Connection::event;
using config = typename Connection::config;
/** @brief Constructor
*
* @param ex Executor
* @param cfg Config options.
*/
template <class Executor>
sync(Executor ex, config cfg = config{}) : conn_{ex, cfg} { }
/** @brief Executes a request synchronously.
*
* The functions calls `connection::async_exec` and waits
* for its completion.
*
* @param req The request.
* @param adapter The response adapter.
* @param ec Error code in case of error.
* @returns The number of bytes of the response.
*/
template <class ResponseAdapter>
std::size_t
exec(resp3::request const& req, ResponseAdapter adapter, boost::system::error_code& ec)
{
sync_helper sh;
std::size_t res = 0;
auto f = [this, &ec, &res, &sh, &req, adapter]()
{
conn_.async_exec(req, adapter, [&sh, &res, &ec](auto const& ecp, std::size_t n) {
std::unique_lock ul(sh.mutex);
ec = ecp;
res = n;
sh.ready = true;
ul.unlock();
sh.cv.notify_one();
});
};
boost::asio::dispatch(boost::asio::bind_executor(conn_.get_executor(), f));
std::unique_lock lk(sh.mutex);
sh.cv.wait(lk, [&sh]{return sh.ready;});
return res;
}
/** @brief Executes a command synchronously
*
* The functions calls `connection::async_exec` and waits for its
* completion.
*
* @param req The request.
* @param adapter The response adapter.
* @throws std::system_error in case of error.
* @returns The number of bytes of the response.
*/
template <class ResponseAdapter = detail::response_traits<void>::adapter_type>
std::size_t exec(resp3::request const& req, ResponseAdapter adapter = aedis::adapt())
{
boost::system::error_code ec;
auto const res = exec(req, adapter, ec);
if (ec)
throw std::system_error(ec);
return res;
}
/** @brief Receives server pushes synchronusly.
*
* The functions calls `connection::async_receive_push` and
* waits for its completion.
*
* @param adapter The response adapter.
* @param ec Error code in case of error.
* @returns The number of bytes received.
*/
template <class ResponseAdapter>
auto receive_push(ResponseAdapter adapter, boost::system::error_code& ec)
{
sync_helper sh;
std::size_t res = 0;
auto f = [this, &ec, &res, &sh, adapter]()
{
conn_.async_receive_push(adapter, [&ec, &res, &sh](auto const& e, std::size_t n) {
std::unique_lock ul(sh.mutex);
ec = e;
res = n;
sh.ready = true;
ul.unlock();
sh.cv.notify_one();
});
};
boost::asio::dispatch(boost::asio::bind_executor(conn_.get_executor(), f));
std::unique_lock lk(sh.mutex);
sh.cv.wait(lk, [&sh]{return sh.ready;});
return res;
}
/** @brief Receives server pushes synchronusly.
*
* The functions calls `connection::async_receive_push` and
* waits for its completion.
*
* @param adapter The response adapter.
* @throws std::system_error in case of error.
* @returns The number of bytes received.
*/
template <class ResponseAdapter = aedis::detail::response_traits<void>::adapter_type>
auto receive_push(ResponseAdapter adapter = aedis::adapt())
{
boost::system::error_code ec;
auto const res = receive_push(adapter, ec);
if (ec)
throw std::system_error(ec);
return res;
}
/** @brief Receives events synchronously.
*
* The functions calls `connection::async_receive_event` and
* waits for its completion.
*
* @param ec Error code in case of error.
* @returns The event received.
*/
auto receive_event(boost::system::error_code& ec)
{
sync_helper sh;
auto res = event::invalid;
auto f = [this, &ec, &res, &sh]()
{
conn_.async_receive_event([&ec, &res, &sh](auto const& ecp, event ev) {
std::unique_lock ul(sh.mutex);
ec = ecp;
res = ev;
sh.ready = true;
ul.unlock();
sh.cv.notify_one();
});
};
boost::asio::dispatch(boost::asio::bind_executor(conn_.get_executor(), f));
std::unique_lock lk(sh.mutex);
sh.cv.wait(lk, [&sh]{return sh.ready;});
return res;
}
/** @brief Receives events synchronously
*
* The functions calls `connection::async_receive_event` and
* waits for its completion.
*
* @throws std::system_error in case of error.
* @returns The event received.
*/
auto receive_event()
{
boost::system::error_code ec;
auto const res = receive_event(ec);
if (ec)
throw std::system_error(ec);
return res;
}
/** @brief Calls \c async_run from the underlying connection.
*
* The functions calls `connection::async_run` and waits for its
* completion.
*
* @param ec Error code.
*/
void run(boost::system::error_code& ec)
{
sync_helper sh;
auto f = [this, &ec, &sh]()
{
conn_.async_run([&ec, &sh](auto const& e) {
std::unique_lock ul(sh.mutex);
ec = e;
sh.ready = true;
ul.unlock();
sh.cv.notify_one();
});
};
boost::asio::dispatch(boost::asio::bind_executor(conn_.get_executor(), f));
std::unique_lock lk(sh.mutex);
sh.cv.wait(lk, [&sh]{return sh.ready;});
}
/** @brief Calls \c async_run from the underlying connection.
*
* The functions calls `connection::async_run` and waits for its
* completion.
*
* @throws std::system_error.
*/
void run()
{
boost::system::error_code ec;
run(ec);
if (ec)
throw std::system_error(ec);
}
private:
struct sync_helper {
std::mutex mutex;
std::condition_variable cv;
bool ready = false;
};
Connection conn_;
};
} // aedis
#endif // AEDIS_SYNC_HPP

28
include/boost/redis.hpp Normal file
View File

@@ -0,0 +1,28 @@
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#ifndef BOOST_REDIS_HPP
#define BOOST_REDIS_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/connection.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
*
* This page contains the documentation of the Aedis high-level API.
*/
/** @defgroup low-level-api Reference
*
* This page contains the documentation of the Aedis low-level API.
*/
#endif // BOOST_REDIS_HPP

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