2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-19 04:42:09 +00:00

Improvements in the docs.

This commit is contained in:
Marcelo Zimbres
2022-12-31 12:23:14 +01:00
parent 73ad66eb93
commit 56bcdb7914
4 changed files with 123 additions and 94 deletions

179
README.md
View File

@@ -32,7 +32,7 @@ auto async_main() -> net::awaitable<void>
req.push("QUIT"); req.push("QUIT");
// Responses as tuple elements. // Responses as tuple elements.
std::tuple<aedis::ignore, std::map<std::string, std::string>, aedis::ignore> resp; std::tuple<ignore, std::map<std::string, std::string>, ignore> resp;
// Executes the request and reads the response. // Executes the request and reads the response.
co_await (conn->async_run() || conn->async_exec(req, adapt(resp))); co_await (conn->async_run() || conn->async_exec(req, adapt(resp)));
@@ -41,129 +41,135 @@ auto async_main() -> net::awaitable<void>
} }
``` ```
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.cpp: Does not use awaitable operators.
* cpp20_intro_awaitable_ops.cpp: The version above. * cpp20_intro_awaitable_ops.cpp: The version from above.
* cpp17_intro.cpp: Requires C++17 only. * cpp17_intro.cpp: Uses callbacks and requires C++17.
* cpp20_intro_tls.cpp: Communicates over TLS. * cpp20_intro_tls.cpp: Communicates over TLS.
The execution of `connection::async_exec` above is composed with The execution of `connection::async_exec` above is composed with
`connection::async_run` with the aid of the Asio awaitable `operator ||` `connection::async_run` with the aid of the Asio awaitable `operator ||`
that ensures that one operation is cancelled as soon as the other 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 * `connection::async_exec`: Execute commands by queuing the request
for writing. It will wait for the response sent back by Redis and for writing. It will wait for the response sent back by Redis and
can be called from multiple places in your code concurrently. can be called from multiple places in your code concurrently.
* `connection::async_run`: Coordinate low-level read and write * `connection::async_run`: Coordinate low-level read and write
operations. More specifically, it will hand IO control to operations. More specifically, it will hand IO control to
`async_exec` when a response arrives, to `async_exec` when a response arrives and to `async_receive` when a
`async_receive` when a server-push is received server-push is received. It will also trigger writes of pending
and will trigger writes of pending requests when a reconnection requests when a reconnection occurs.
occurs.
The role played by `async_run` can be better understood in the context 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. 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.
<a name="connection"></a> <a name="connection"></a>
## Connection ## Connection
For performance reasons we will usually want to perform multiple For performance reasons we will usually want to perform multiple
requests on the same connection. We can do this with the example above requests in the same connection. We can do this in the example above
by decoupling the `HELLO` command and the call to `async_run` in a by letting `async_run` run detached in a separate coroutine, for
separate coroutine example (see cpp20_intro.cpp)
```cpp ```cpp
auto run(std::shared_ptr<connection> conn) -> net::awaitable<void> auto run(std::shared_ptr<connection> conn) -> net::awaitable<void>
{ {
co_await connect(conn, "127.0.0.1", "6379"); co_await connect(conn, "127.0.0.1", "6379");
co_await conn->async_run();
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));
} }
```
We can now let `run` run detached in the background while other
coroutines perform requests on the connection, for example
```cpp auto hello(std::shared_ptr<connection> conn) -> net::awaitable<void>
auto async_main() -> net::awaitable<void>
{ {
auto conn = std::make_shared<connection>(co_await net::this_coro::executor); resp3::request req;
req.push("HELLO", 3);
// Run detached. co_await conn->async_exec(req);
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.
auto ping(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
resp3::request req; resp3::request req;
req.push("PING", "Hello world"); req.push("PING", "Hello world");
co_await conn->async_exec(req); req.push("QUIT");
... std::tuple<std::string, aedis::ignore> resp;
co_await conn->async_exec(req, adapt(resp));
// Use the response ...
}
// Cancels the run operation so we can exit. auto async_main() -> net::awaitable<void>
conn->cancel(operation::run); {
auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(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 With this separation, it is now easy to incorporate other long-running
in our application, for example, to cancel the connection on `SIGINT` operations in our application, for example, the run coroutine below
and `SIGTERM` we can extend `run` as follows adds signal handling and a healthy checker
```cpp ```cpp
auto run(std::shared_ptr<connection> conn) -> net::awaitable<void> auto run(std::shared_ptr<connection> conn) -> net::awaitable<void>
{ {
co_await connect(conn, "127.0.0.1", "6379");
signal_set sig{ex, SIGINT, SIGTERM}; signal_set sig{ex, SIGINT, SIGTERM};
co_await connect(conn, "127.0.0.1", "6379");
resp3::request req; co_await (conn->async_run() || sig.async_wait() || healthy_checker(conn));
req.push("HELLO", 3);
co_await ((conn->async_run() || sig.async_wait()) && conn->async_exec(req));
} }
``` ```
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<connection> conn) -> net::awaitable<void>
{
using resp_type = std::vector<resp3::node<std::string>>;
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 ```cpp
auto run(std::shared_ptr<connection> conn) -> net::awaitable<void> auto run(std::shared_ptr<connection> conn) -> net::awaitable<void>
{ {
co_await connect(conn, "127.0.0.1", "6379");
signal_set sig{ex, SIGINT, SIGTERM}; signal_set sig{ex, SIGINT, SIGTERM};
co_await connect(conn, "127.0.0.1", "6379");
resp3::request req; co_await (conn->async_run() || sig.async_wait() || healthy_checker(conn) || receiver(conn));
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));
} }
``` ```
The definition of `receiver` and `healthy_checker` above can be found ### Reconnecting
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_, 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 for example, to reconnect to the same address
```cpp ```cpp
@@ -172,15 +178,11 @@ auto run(std::shared_ptr<connection> conn) -> net::awaitable<void>
auto ex = co_await net::this_coro::executor; auto ex = co_await net::this_coro::executor;
steady_timer timer{ex}; steady_timer timer{ex};
resp3::request req;
req.push("HELLO", 3);
req.push("SUBSCRIBE", "channel1", "channel2");
for (;;) { for (;;) {
co_await connect(conn, "127.0.0.1", "6379"); 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(); conn->reset_stream();
// Waits one second before trying to reconnect. // 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 * Provides a way to limit how long the execution of a single request
should last. 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 * NOTE: It is usually a better idea to have a healthy checker than adding
per request timeout, see cpp20_subscriber.cpp for an example. 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 * This works but is unnecessary. Unless the user has set
`aedis::resp3::request::config::coalesce` to `false`, and he `aedis::resp3::request::config::coalesce` to `false`, and he
shouldn't, the connection will automatically merge the individual usually shouldn't, the connection will automatically merge the
requests into a single payload anyway. individual requests into a single payload anyway.
<a name="requests"></a> <a name="requests"></a>
## Requests ## 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` containers. The same reasoning also applies to sets e.g. `SMEMBERS`
and other data structures in general. 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 ## Echo server benchmark
This document benchmarks the performance of TCP echo servers I This document benchmarks the performance of TCP echo servers I

View File

@@ -12,6 +12,7 @@
namespace net = boost::asio; namespace net = boost::asio;
namespace resp3 = aedis::resp3; namespace resp3 = aedis::resp3;
using aedis::adapt; using aedis::adapt;
using aedis::operation;
auto run(std::shared_ptr<connection> conn) -> net::awaitable<void> auto run(std::shared_ptr<connection> conn) -> net::awaitable<void>
{ {
@@ -19,22 +20,35 @@ auto run(std::shared_ptr<connection> conn) -> net::awaitable<void>
co_await conn->async_run(); co_await conn->async_run();
} }
// Called from the main function (see main.cpp) auto hello(std::shared_ptr<connection> conn) -> net::awaitable<void>
auto async_main() -> net::awaitable<void>
{ {
resp3::request req; resp3::request req;
req.push("HELLO", 3); req.push("HELLO", 3);
co_await conn->async_exec(req);
}
auto ping(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
resp3::request req;
req.push("PING", "Hello world"); req.push("PING", "Hello world");
req.push("QUIT"); req.push("QUIT");
std::tuple<aedis::ignore, std::string, aedis::ignore> resp; std::tuple<std::string, aedis::ignore> 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<void>
{
auto ex = co_await net::this_coro::executor; auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex); auto conn = std::make_shared<connection>(ex);
net::co_spawn(ex, run(conn), net::detached); net::co_spawn(ex, run(conn), net::detached);
co_await conn->async_exec(req, adapt(resp)); co_await hello(conn);
co_await ping(conn);
std::cout << "PING: " << std::get<1>(resp) << std::endl;
} }
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) #endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -14,7 +14,6 @@
namespace net = boost::asio; namespace net = boost::asio;
namespace resp3 = aedis::resp3; namespace resp3 = aedis::resp3;
using namespace net::experimental::awaitable_operators; using namespace net::experimental::awaitable_operators;
using signal_set = net::use_awaitable_t<>::as_default_on_t<net::signal_set>;
using steady_timer = net::use_awaitable_t<>::as_default_on_t<net::steady_timer>; using steady_timer = net::use_awaitable_t<>::as_default_on_t<net::steady_timer>;
using aedis::adapt; using aedis::adapt;
@@ -37,7 +36,8 @@ using aedis::adapt;
// Receives pushes. // Receives pushes.
auto receiver(std::shared_ptr<connection> conn) -> net::awaitable<void> auto receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
{ {
for (std::vector<resp3::node<std::string>> resp;;) { using resp_type = std::vector<resp3::node<std::string>>;
for (resp_type resp;;) {
co_await conn->async_receive(adapt(resp)); co_await conn->async_receive(adapt(resp));
std::cout << resp.at(1).value << " " << resp.at(2).value << " " << resp.at(3).value << std::endl; std::cout << resp.at(1).value << " " << resp.at(2).value << " " << resp.at(3).value << std::endl;
resp.clear(); resp.clear();
@@ -48,7 +48,6 @@ auto async_main() -> net::awaitable<void>
{ {
auto ex = co_await net::this_coro::executor; auto ex = co_await net::this_coro::executor;
auto conn = std::make_shared<connection>(ex); auto conn = std::make_shared<connection>(ex);
signal_set sig{ex, SIGINT, SIGTERM};
steady_timer timer{ex}; steady_timer timer{ex};
resp3::request req; resp3::request req;
@@ -58,8 +57,7 @@ auto async_main() -> net::awaitable<void>
// The loop will reconnect on connection lost. To exit type Ctrl-C twice. // The loop will reconnect on connection lost. To exit type Ctrl-C twice.
for (;;) { for (;;) {
co_await connect(conn, "127.0.0.1", "6379"); co_await connect(conn, "127.0.0.1", "6379");
co_await ((conn->async_run() || healthy_checker(conn) || sig.async_wait() || co_await ((conn->async_run() || healthy_checker(conn) || receiver(conn)) && conn->async_exec(req));
receiver(conn)) && conn->async_exec(req));
conn->reset_stream(); conn->reset_stream();
timer.expires_after(std::chrono::seconds{1}); timer.expires_after(std::chrono::seconds{1});

View File

@@ -120,7 +120,7 @@ public:
* @param adapter Response adapter. * @param adapter Response adapter.
* @param token Asio completion token. * @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 * have the following signature
* *
* @code * @code
@@ -150,7 +150,7 @@ public:
* @param adapter The response adapter. * @param adapter The response adapter.
* @param token The Asio completion token. * @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 * have the following signature
* *
* @code * @code