From 56bcdb7914eed367bafc8c45d50742d38a9d7c88 Mon Sep 17 00:00:00 2001 From: Marcelo Zimbres Date: Sat, 31 Dec 2022 12:23:14 +0100 Subject: [PATCH] Improvements in the docs. --- README.md | 179 +++++++++++++++++++--------------- examples/cpp20_intro.cpp | 26 +++-- examples/cpp20_subscriber.cpp | 8 +- include/aedis/connection.hpp | 4 +- 4 files changed, 123 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 4223ef2e..cfed13ca 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ auto async_main() -> net::awaitable req.push("QUIT"); // Responses as tuple elements. - std::tuple, aedis::ignore> resp; + std::tuple, ignore> resp; // Executes the request and reads the response. co_await (conn->async_run() || conn->async_exec(req, adapt(resp))); @@ -41,129 +41,135 @@ auto async_main() -> net::awaitable } ``` -For different versions of this example using different styles see +For other versions of this example that use different styles see -* cpp20_intro.cpp: Does not use awaitable operators -* cpp20_intro_awaitable_ops.cpp: The version above. -* cpp17_intro.cpp: Requires C++17 only. +* cpp20_intro.cpp: Does not use awaitable operators. +* cpp20_intro_awaitable_ops.cpp: The version from above. +* cpp17_intro.cpp: Uses callbacks and requires C++17. * cpp20_intro_tls.cpp: Communicates over TLS. The execution of `connection::async_exec` above is composed with `connection::async_run` with the aid of the Asio awaitable `operator ||` that ensures that one operation is cancelled as soon as the other -completes, these functions play the following roles +completes. These functions play the following roles * `connection::async_exec`: Execute commands by queuing the request for writing. It will wait for the response sent back by Redis and can be called from multiple places in your code concurrently. * `connection::async_run`: Coordinate low-level read and write operations. More specifically, it will hand IO control to - `async_exec` when a response arrives, to - `async_receive` when a server-push is received - and will trigger writes of pending requests when a reconnection - occurs. + `async_exec` when a response arrives and to `async_receive` when a + server-push is received. It will also trigger writes of pending + requests when a reconnection occurs. The role played by `async_run` can be better understood in the context of long-lived connections, which we will cover in the next section. -Before that however, the reader might want to skim over some further examples - -* cpp20_containers.cpp: Shows how to send and receive STL containers and how to use transactions. -* cpp20_serialization.cpp: Shows how to serialize types using Boost.Json. -* cpp20_resolve_with_sentinel.cpp: Shows how to resolve a master address using sentinels. -* cpp20_subscriber.cpp: Shows how to implement pubsub with reconnection re-subscription. -* cpp20_echo_server.cpp: A simple TCP echo server. -* cpp20_chat_room.cpp: A command line chat built on Redis pubsub. -* cpp20_low_level_async.cpp: Sends a ping asynchronously using the low-level API. -* cpp17_low_level_sync.cpp: Sends a ping synchronously using the low-level API. - -To avoid repetition code that is common to some examples has been -grouped in common.hpp. The main function used in some async examples -has been factored out in the main.cpp file. ## Connection For performance reasons we will usually want to perform multiple -requests on the same connection. We can do this with the example above -by decoupling the `HELLO` command and the call to `async_run` in a -separate coroutine +requests in the same connection. We can do this in the example above +by letting `async_run` run detached in a separate coroutine, for +example (see cpp20_intro.cpp) ```cpp auto run(std::shared_ptr conn) -> net::awaitable { co_await connect(conn, "127.0.0.1", "6379"); - - resp3::request req; - req.push("HELLO", 3); // Upgrade to RESP3 - - // Notice we use && instead of || so async_run is not cancelled - // when the HELLO response arrives. We are also ignoring the - // response for simplicity. - co_await (conn->async_run() && conn->async_exec(req)); + co_await conn->async_run(); } -``` -We can now let `run` run detached in the background while other -coroutines perform requests on the connection, for example -```cpp -auto async_main() -> net::awaitable +auto hello(std::shared_ptr conn) -> net::awaitable { - auto conn = std::make_shared(co_await net::this_coro::executor); + resp3::request req; + req.push("HELLO", 3); - // Run detached. - net::co_spawn(ex, run(conn), net::detached); - - // Here we can use the connection to perform requests and pass it - // around to other coroutines so they can make requests. + co_await conn->async_exec(req); +} +auto ping(std::shared_ptr conn) -> net::awaitable +{ resp3::request req; req.push("PING", "Hello world"); - co_await conn->async_exec(req); + req.push("QUIT"); - ... + std::tuple resp; + co_await conn->async_exec(req, adapt(resp)); + // Use the response ... +} - // Cancels the run operation so we can exit. - conn->cancel(operation::run); +auto async_main() -> net::awaitable +{ + auto ex = co_await net::this_coro::executor; + auto conn = std::make_shared(ex); + net::co_spawn(ex, run(conn), net::detached); + co_await hello(conn); + co_await ping(conn); + + // Here we can pass conn to other coroutines that need to + // communicate with Redis. } ``` -With this separation, it is now easy to incorporate other operations -in our application, for example, to cancel the connection on `SIGINT` -and `SIGTERM` we can extend `run` as follows +With this separation, it is now easy to incorporate other long-running +operations in our application, for example, the run coroutine below +adds signal handling and a healthy checker ```cpp auto run(std::shared_ptr conn) -> net::awaitable { - co_await connect(conn, "127.0.0.1", "6379"); signal_set sig{ex, SIGINT, SIGTERM}; - - resp3::request req; - req.push("HELLO", 3); - - co_await ((conn->async_run() || sig.async_wait()) && conn->async_exec(req)); + co_await connect(conn, "127.0.0.1", "6379"); + co_await (conn->async_run() || sig.async_wait() || healthy_checker(conn)); } ``` -Likewise we can incorporate support for server pushes, healthy checks and pubsub +Here we use Asio awaitable operator for simplicity, the same +functionality can be achieved by means of the +`aedis::connection::cancel` function. The definition of the +`healthy_checker` used above can be found in common.cpp. + +### Server pushes + +Redis servers can also send a variety of pushes to the client, some of +them are + +* [Pubsub](https://redis.io/docs/manual/pubsub/) +* [Keyspace notification](https://redis.io/docs/manual/keyspace-notifications/) +* [Client-side caching](https://redis.io/docs/manual/client-side-caching/) + +The connection class supports that by means of the +`aedis::connection::async_receive` function + +```cpp +auto receiver(std::shared_ptr conn) -> net::awaitable +{ + using resp_type = std::vector>; + for (resp_type resp;;) { + co_await conn->async_receive(adapt(resp)); + // Use resp and clear the response for a new push. + resp.clear(); + } +} +``` + +This function can be also easily incorporated in the run function from +above, for example ```cpp auto run(std::shared_ptr conn) -> net::awaitable { - co_await connect(conn, "127.0.0.1", "6379"); signal_set sig{ex, SIGINT, SIGTERM}; - - resp3::request req; - req.push("HELLO", 3); - req.push("SUBSCRIBE", "channel1", "channel2"); - - co_await ((conn->async_run() || sig.async_wait() || receiver(conn) || healthy_checker(conn)) - && conn->async_exec(req)); + co_await connect(conn, "127.0.0.1", "6379"); + co_await (conn->async_run() || sig.async_wait() || healthy_checker(conn) || receiver(conn)); } ``` -The definition of `receiver` and `healthy_checker` above can be found -in cpp20_subscriber.cpp. Adding a loop around `async_run` produces a simple -way to support reconnection _while there are pending operations on the connection_, +### Reconnecting + +Adding a loop around `async_run` produces a simple way to support +reconnection _while there are pending operations on the connection_, for example, to reconnect to the same address ```cpp @@ -172,15 +178,11 @@ auto run(std::shared_ptr conn) -> net::awaitable auto ex = co_await net::this_coro::executor; steady_timer timer{ex}; - resp3::request req; - req.push("HELLO", 3); - req.push("SUBSCRIBE", "channel1", "channel2"); - for (;;) { co_await connect(conn, "127.0.0.1", "6379"); - co_await ((conn->async_run() || healthy_checker(conn) || receiver(conn)) && conn->async_exec(req)); + co_await (conn->async_run() || healthy_checker(conn) || receiver(conn); - // Prepare the stream to a new connection. + // Prepare the stream for a new connection. conn->reset_stream(); // Waits one second before trying to reconnect. @@ -225,8 +227,6 @@ co_await (conn.async_exec(...) || time.async_wait(...)) * Provides a way to limit how long the execution of a single request should last. -* The cancellation will be ignored if the request has already - been written to the socket. * NOTE: It is usually a better idea to have a healthy checker than adding per request timeout, see cpp20_subscriber.cpp for an example. @@ -242,8 +242,8 @@ co_await (conn.async_exec(...) || conn.async_exec(...) || ... || conn.async_exec * This works but is unnecessary. Unless the user has set `aedis::resp3::request::config::coalesce` to `false`, and he - shouldn't, the connection will automatically merge the individual - requests into a single payload anyway. + usually shouldn't, the connection will automatically merge the + individual requests into a single payload anyway. ## Requests @@ -583,6 +583,23 @@ In addition to the above users can also use unordered versions of the containers. The same reasoning also applies to sets e.g. `SMEMBERS` and other data structures in general. +## Examples + +The examples below show how to use the features discussed so far + +* cpp20_containers.cpp: Shows how to send and receive STL containers and how to use transactions. +* cpp20_serialization.cpp: Shows how to serialize types using Boost.Json. +* cpp20_resolve_with_sentinel.cpp: Shows how to resolve a master address using sentinels. +* cpp20_subscriber.cpp: Shows how to implement pubsub with reconnection re-subscription. +* cpp20_echo_server.cpp: A simple TCP echo server. +* cpp20_chat_room.cpp: A command line chat built on Redis pubsub. +* cpp20_low_level_async.cpp: Sends a ping asynchronously using the low-level API. +* cpp17_low_level_sync.cpp: Sends a ping synchronously using the low-level API. + +To avoid repetition code that is common to some examples has been +grouped in common.hpp. The main function used in some async examples +has been factored out in the main.cpp file. + ## Echo server benchmark This document benchmarks the performance of TCP echo servers I diff --git a/examples/cpp20_intro.cpp b/examples/cpp20_intro.cpp index d28df47f..9f045c0a 100644 --- a/examples/cpp20_intro.cpp +++ b/examples/cpp20_intro.cpp @@ -12,6 +12,7 @@ namespace net = boost::asio; namespace resp3 = aedis::resp3; using aedis::adapt; +using aedis::operation; auto run(std::shared_ptr conn) -> net::awaitable { @@ -19,22 +20,35 @@ auto run(std::shared_ptr conn) -> net::awaitable co_await conn->async_run(); } -// Called from the main function (see main.cpp) -auto async_main() -> net::awaitable +auto hello(std::shared_ptr conn) -> net::awaitable { resp3::request req; req.push("HELLO", 3); + + co_await conn->async_exec(req); +} + +auto ping(std::shared_ptr conn) -> net::awaitable +{ + resp3::request req; req.push("PING", "Hello world"); req.push("QUIT"); - std::tuple resp; + std::tuple resp; + co_await conn->async_exec(req, adapt(resp)); + + std::cout << "PING: " << std::get<0>(resp) << std::endl; +} + +// Called from the main function (see main.cpp) +auto async_main() -> net::awaitable +{ auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); net::co_spawn(ex, run(conn), net::detached); - co_await conn->async_exec(req, adapt(resp)); - - std::cout << "PING: " << std::get<1>(resp) << std::endl; + co_await hello(conn); + co_await ping(conn); } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_subscriber.cpp b/examples/cpp20_subscriber.cpp index fcf64dee..181ca712 100644 --- a/examples/cpp20_subscriber.cpp +++ b/examples/cpp20_subscriber.cpp @@ -14,7 +14,6 @@ namespace net = boost::asio; namespace resp3 = aedis::resp3; using namespace net::experimental::awaitable_operators; -using signal_set = net::use_awaitable_t<>::as_default_on_t; using steady_timer = net::use_awaitable_t<>::as_default_on_t; using aedis::adapt; @@ -37,7 +36,8 @@ using aedis::adapt; // Receives pushes. auto receiver(std::shared_ptr conn) -> net::awaitable { - for (std::vector> resp;;) { + using resp_type = std::vector>; + for (resp_type resp;;) { co_await conn->async_receive(adapt(resp)); std::cout << resp.at(1).value << " " << resp.at(2).value << " " << resp.at(3).value << std::endl; resp.clear(); @@ -48,7 +48,6 @@ auto async_main() -> net::awaitable { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); - signal_set sig{ex, SIGINT, SIGTERM}; steady_timer timer{ex}; resp3::request req; @@ -58,8 +57,7 @@ auto async_main() -> net::awaitable // The loop will reconnect on connection lost. To exit type Ctrl-C twice. for (;;) { co_await connect(conn, "127.0.0.1", "6379"); - co_await ((conn->async_run() || healthy_checker(conn) || sig.async_wait() || - receiver(conn)) && conn->async_exec(req)); + co_await ((conn->async_run() || healthy_checker(conn) || receiver(conn)) && conn->async_exec(req)); conn->reset_stream(); timer.expires_after(std::chrono::seconds{1}); diff --git a/include/aedis/connection.hpp b/include/aedis/connection.hpp index f1f07c5a..6fab10b0 100644 --- a/include/aedis/connection.hpp +++ b/include/aedis/connection.hpp @@ -120,7 +120,7 @@ public: * @param adapter Response adapter. * @param token Asio completion token. * - * For an example see echo_server.cpp. The completion token must + * For an example see cpp20_echo_server.cpp. The completion token must * have the following signature * * @code @@ -150,7 +150,7 @@ public: * @param adapter The response adapter. * @param token The Asio completion token. * - * For an example see subscriber.cpp. The completion token must + * For an example see cpp20_subscriber.cpp. The completion token must * have the following signature * * @code