From 0fff6496fb97152b4c5c20b1da789b6e7858f58d Mon Sep 17 00:00:00 2001 From: Marcelo Zimbres Date: Fri, 8 Apr 2022 11:41:18 +0200 Subject: [PATCH] Improvements in the documentation. --- aedis/adapter/adapt.hpp | 60 ++--- aedis/adapter/response_traits.hpp | 21 +- aedis/aedis.hpp | 61 +++-- aedis/generic/client.hpp | 354 ++++++++++++++++++------------ aedis/generic/serializer.hpp | 12 +- aedis/resp3/node.hpp | 10 + doc/Doxyfile.in | 2 +- 7 files changed, 289 insertions(+), 231 deletions(-) diff --git a/aedis/adapter/adapt.hpp b/aedis/adapter/adapt.hpp index 2485bf84..c29bf68c 100644 --- a/aedis/adapter/adapt.hpp +++ b/aedis/adapter/adapt.hpp @@ -16,8 +16,8 @@ namespace adapter { \ingroup any The adapter returned by this function ignores responses and is - useful to avoid wasting time with responses which the user is - insterested in. + useful to avoid wasting time with responses on which the user is + not insterested in. Example usage: @@ -32,34 +32,8 @@ auto adapt() noexcept /** \brief Adapts user data to read operations. * \ingroup any * - * For example - * The following types are supported. - * - * - Integer data types e.g. `int`, `unsigned`, etc. - * - * - `std::string` - * - * We also support the following C++ containers - * - * - `std::vector`. Can be used with any RESP3 aggregate type. - * - * - `std::deque`. Can be used with any RESP3 aggregate type. - * - * - `std::list`. Can be used with any RESP3 aggregate type. - * - * - `std::set`. Can be used with RESP3 set type. - * - * - `std::unordered_set`. Can be used with RESP3 set type. - * - * - `std::map`. Can be used with RESP3 hash type. - * - * - `std::unordered_map`. Can be used with RESP3 hash type. - * - * All these types can be wrapped in an `boost::optional`. This - * function also support \c std::tuple to read the response to - * tuples. At the moment this feature supports only transactions that - * contain simple types or aggregates that don't contain aggregates - * themselves (as in most cases). + * All STL containers, \c std::tuple and built-in types are supported and + * can be used in conjunction with \c boost::optional. * * Example usage: * @@ -71,19 +45,19 @@ auto adapt() noexcept * 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, int> execs; - co_await resp3::async_read(socket, dynamic_buffer(buffer), adapt(execs)); + * 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, int> execs; + * co_await resp3::async_read(socket, dynamic_buffer(buffer), adapt(execs)); * @endcode */ template diff --git a/aedis/adapter/response_traits.hpp b/aedis/adapter/response_traits.hpp index 7d7554c0..27c70d63 100644 --- a/aedis/adapter/response_traits.hpp +++ b/aedis/adapter/response_traits.hpp @@ -21,20 +21,25 @@ namespace aedis { namespace adapter { -/** \brief Traits class for response objects. - * \ingroup any +/** @brief Traits class for response objects. + * @ingroup any + * + * Provides traits for all supported response types i.e. all STL containers + * and C++ buil-in types. */ template struct response_traits { - /// The response type. - using response_type = ResponseType; - /// The adapter type. - using adapter_type = adapter::detail::wrapper; + using adapter_type = adapter::detail::wrapper; - /// Returns an adapter for the reponse r - static auto adapt(response_type& r) noexcept { return adapter_type{&r}; } + /** @brief Returns an adapter for the reponse r + * + * @param r The response object e.g a C++ container. + * @return An adapter suitable for use in resp3::read or resp3::async_read. + * @remark Users can also use the free adapt function for type deduction. + */ + static auto adapt(ResponseType& r) noexcept { return adapter_type{&r}; } }; /// Template typedef for response_traits. diff --git a/aedis/aedis.hpp b/aedis/aedis.hpp index cd8dd0d2..5c07ecf9 100644 --- a/aedis/aedis.hpp +++ b/aedis/aedis.hpp @@ -20,20 +20,21 @@ \section Overview - Aedis is a redis client library built on top of Asio that provides - simple and efficient communication with a Redis server. Some of - its distinctive features are + Aedis is a [Redis](https://redis.io/) client library built on top + of [Asio](https://www.boost.org/doc/libs/release/doc/html/boost_asio.html) + that provides simple and efficient communication with a Redis + server. Some of its distinctive features are - @li Support for the latest version of the Redis communication protocol RESP3. + @li Support for the latest version of the Redis communication protocol [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md). @li First class support for STL containers and C++ built-in types. @li Serialization and deserialization of your own data types that avoid unnecessary copies. - @li Sentinel support (https://redis.io/docs/manual/sentinel). + @li Support for Redis [sentinel](https://redis.io/docs/manual/sentinel). @li Sync and async API. In addition to that, Aedis provides a high level client that offers the following functionality @li Management of message queues. - @li Simplified handling of server pushs. + @li Simplified handling of server pushes. @li Zero asymptotic allocations by means of memory reuse. If you never heard about Redis the best place to start is on @@ -41,7 +42,7 @@ \section low-level-api Low-level API - The low-level API is very usefull for simple tasks, for example, + The low-level API is very useful for simple tasks, for example, assume we want to perform the following steps @li Set the value of a Redis key. @@ -76,7 +77,7 @@ } @endcode - The simplicity of the code above makes it self expanatory + The simplicity of the code above makes it self explanatory @li Connect to the Redis server. @li Declare a \c std::string to hold the request and add some commands in it with a serializer. @@ -91,8 +92,8 @@ As stated above, request are created by defining a storage object and a serializer that knowns how to convert user data into valid RESP3 wire-format. Redis request are composed of one or more - commands (in Redis documentation they are called pipelines, see - https://redis.io/topics/pipelining), which means users can add + commands (in Redis documentation they are called [pipelines](https://redis.io/topics/pipelining)), + which means users can add as many commands to the request as they like, a feature that aids performance. @@ -183,7 +184,7 @@ 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. Fourtunately, + proper C++ data structure to receive it in. Fortunately, this is a simple task for most types, for example RESP3 type | C++ | Type @@ -200,7 +201,7 @@ Push | \c std::vector, \c std::map, \c std::unordered_map | Aggregate Exceptions to this rule are responses that contain nested - aggregates or heterogeneuos data types, those will be treated + aggregates or heterogeneous data types, those will be treated later. 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. Now let us see some @@ -304,10 +305,10 @@ co_await resp3::async_read(socket, dynamic_buffer(buffer), adapt(trans)); @endcode - Note that we are not ignoring the response to the comands + Note that we are not ignoring the response to the commands themselves above but whether they have been successfully queued. Only after @c exec is received Redis will execute them in - sequence. The response will then be sent in a single chunck to the + sequence. The response will then be sent in a single chunk to the client. \subsubsection Serialization @@ -432,7 +433,7 @@ @li \b Performance: Keep opening and closing connections impact performance. @li \b Pipeline: Code such as shown in \ref low-level-api don't support pipelines well since it can only send a fixed number of - commands at time. It misses important optimization oportunities + commands at time. It misses important optimization opportunities (https://redis.io/topics/pipelining). To avoid these drawbacks users will address the points above @@ -466,25 +467,17 @@ @code class receiver { public: - void on_resp3(command cmd, node const& nd, boost::system::error_code& ec) - { - // Called when a new chunck of user data becomes available. - } - - void on_read(command cmd) - { - // Called when a response becomes available. - } - - void on_write(std::size_t n) - { - // Called when a request has been writen to the socket. - } - - void on_push() - { - // Called when a server push is received. - } + // Called when a new chunck of user data becomes available. + void on_resp3(command cmd, node const& nd, boost::system::error_code& ec); + + // Called when a response becomes available. + void on_read(command cmd); + + // Called when a request has been writen to the socket. + void on_write(std::size_t n); + + // Called when a server push is received. + void on_push(); }; @endcode diff --git a/aedis/generic/client.hpp b/aedis/generic/client.hpp index 80de8553..f46dd8a9 100644 --- a/aedis/generic/client.hpp +++ b/aedis/generic/client.hpp @@ -23,16 +23,18 @@ namespace aedis { namespace generic { -/** \brief A high level resp3 client. - * \ingroup any +/** \brief A high level Redis client. + * \ingroup any * - * This class represents a connection to the Redis server. It keeps - * an internal queue of user commands that is managed automatically. - * It also achieves asymptotic zero allocation by reusing memory. + * This class represents a connection to the Redis server. Some of + * its most important features are * - * It also reuses its internal buffers for requests and - * for reading Redis responses. With time it will allocate less and - * less. + * 1. Automatic management of commands. The implementation will send + * commands and read responses automatically for the user. + * 2. Memory reuse. Dynamic memory allocations will decrease with time. + * + * For more details, please see the documentation of each individual + * function. */ template class client { @@ -41,6 +43,209 @@ public: using executor_type = typename stream_type::executor_type; using default_completion_token_type = boost::asio::default_completion_token_t; + /** \brief Constructor. + * + * \param ex The executor. + */ + client(boost::asio::any_io_executor ex) + : socket_{ex} + , timer_{ex} + { + timer_.expires_at(std::chrono::steady_clock::time_point::max()); + send(Command::hello, 3); + } + + /// Returns the executor. + auto get_executor() {return socket_.get_executor();} + + /** @brief Adds a command to the output command queue. + * + * Adds a command to the output command queue and signals the write + * operation there are new messages awaiting to be sent to Redis. + * + * @sa serializer.hpp + * + * @param cmd The command to send. + * @param args Arguments to commands. + */ + template + void send(Command cmd, Ts const&... args) + { + auto const can_write = prepare_next(); + + serializer sr(requests_); + auto const before = requests_.size(); + sr.push(cmd, args...); + auto const after = requests_.size(); + assert(after - before != 0); + req_info_.front().size += after - before;; + + if (!has_push_response(cmd)) { + commands_.push_back(cmd); + ++req_info_.front().cmds; + } + + if (can_write) + timer_.cancel_one(); + } + + /** @brief Adds a command to the output command queue. + * + * Adds a command to the output command queue and signals the write + * operation there are new messages awaiting to be sent to Redis. + * + * @sa serializer.hpp + * + * @param cmd The command. + * @param key The key the commands refers to + * @param begin Begin of the range. + * @param end End of the range. + */ + template + void send_range2(Command cmd, Key const& key, ForwardIterator begin, ForwardIterator end) + { + if (begin == end) + return; + + auto const can_write = prepare_next(); + + serializer sr(requests_); + auto const before = requests_.size(); + sr.push_range2(cmd, key, begin, end); + auto const after = requests_.size(); + assert(after - before != 0); + req_info_.front().size += after - before;; + + if (!has_push_response(cmd)) { + commands_.push_back(cmd); + ++req_info_.front().cmds; + } + + if (can_write) + timer_.cancel_one(); + } + + /** @brief Adds a command to the output command queue. + * + * Adds a command to the output command queue and signals the write + * operation there are new messages awaiting to be sent to Redis. + * + * @sa serializer.hpp + * + * @param cmd The command. + * @param begin Begin of the range. + * @param end End of the range. + */ + template + void send_range2(Command cmd, ForwardIterator begin, ForwardIterator end) + { + if (begin == end) + return; + + auto const can_write = prepare_next(); + + serializer sr(requests_); + auto const before = requests_.size(); + sr.push_range2(cmd, begin, end); + auto const after = requests_.size(); + assert(after - before != 0); + req_info_.front().size += after - before;; + + if (!has_push_response(cmd)) { + commands_.push_back(cmd); + ++req_info_.front().cmds; + } + + if (can_write) + timer_.cancel_one(); + } + + /** @brief Adds a command to the output command queue. + * + * Adds a command to the output command queue and signals the write + * operation there are new messages awaiting to be sent to Redis. + * + * @sa serializer.hpp + * + * @param cmd The command. + * @param key The key the commands refers to. + * @param range Range of elements to send. + */ + template + void send_range(Command cmd, Key const& key, Range const& range) + { + using std::begin; + using std::end; + send_range2(cmd, key, begin(range), end(range)); + } + + /** @brief Adds a command to the output command queue. + * + * Adds a command to the output command queue and signals the write + * operation there are new messages awaiting to be sent to Redis. + * + * @sa serializer.hpp + * + * @param cmd The command. + * @param range End of the range. + */ + template + void send_range(Command cmd, Range const& range) + { + using std::begin; + using std::end; + send_range2(cmd, begin(range), end(range)); + } + + /** @brief Starts communication with the Redis server asynchronously. + * + * This class performs the following steps + * + * @li Connect to the endpoint passed in the function parameter. + * @li Start the async read operation that keeps reading responses to commands and server pushes. + * @li Start the async write operation that keeps sending commands to Redis. + * + * \param recv The receiver (see below) + * \param ep The address of the Redis server. + * \param token The completion token (ASIO jargon) + * + * The receiver is a class that privides the following member functions + * + * @code + * class receiver { + * public: + * // Called when a new chunck of user data becomes available. + * void on_resp3(command cmd, node const& nd, boost::system::error_code& ec); + * + * // Called when a response becomes available. + * void on_read(command cmd); + * + * // Called when a request has been writen to the socket. + * void on_write(std::size_t n); + * + * // Called when a server push is received. + * void on_push(); + * }; + * @endcode + * + */ + template < + class Receiver, + class CompletionToken = default_completion_token_type + > + auto + async_run( + Receiver& recv, + boost::asio::ip::tcp::endpoint ep = {boost::asio::ip::make_address("127.0.0.1"), 6379}, + CompletionToken token = CompletionToken{}) + { + endpoint_ = ep; + return boost::asio::async_compose + < CompletionToken + , void(boost::system::error_code) + >(run_op{this, &recv}, token, socket_, timer_); + } + private: template friend struct read_op; template friend struct writer_op; @@ -163,139 +368,6 @@ private: , void(boost::system::error_code) >(read_write_op{this, recv}, token, socket_, timer_); } -public: - /** \brief Client constructor. - * - * Constructos the client from an executor. - * - * \param ex The executor. - */ - client(boost::asio::any_io_executor ex) - : socket_{ex} - , timer_{ex} - { - timer_.expires_at(std::chrono::steady_clock::time_point::max()); - send(Command::hello, 3); - } - - /// Returns the executor used for I/O with Redis. - auto get_executor() {return socket_.get_executor();} - - /** \brief Adds a command to the command queue. - * - * \sa serializer.hpp - */ - template - void send(Command cmd, Ts const&... args) - { - auto const can_write = prepare_next(); - - serializer sr(requests_); - auto const before = requests_.size(); - sr.push(cmd, args...); - auto const after = requests_.size(); - assert(after - before != 0); - req_info_.front().size += after - before;; - - if (!has_push_response(cmd)) { - commands_.push_back(cmd); - ++req_info_.front().cmds; - } - - if (can_write) - timer_.cancel_one(); - } - - /** \brief Sends and iterator range (overload with key). - */ - template - void send_range2(Command cmd, Key const& key, ForwardIterator begin, ForwardIterator end) - { - if (begin == end) - return; - - auto const can_write = prepare_next(); - - serializer sr(requests_); - auto const before = requests_.size(); - sr.push_range2(cmd, key, begin, end); - auto const after = requests_.size(); - assert(after - before != 0); - req_info_.front().size += after - before;; - - if (!has_push_response(cmd)) { - commands_.push_back(cmd); - ++req_info_.front().cmds; - } - - if (can_write) - timer_.cancel_one(); - } - - /** \brief Sends and iterator range (overload without key). - */ - template - void send_range2(Command cmd, ForwardIterator begin, ForwardIterator end) - { - if (begin == end) - return; - - auto const can_write = prepare_next(); - - serializer sr(requests_); - auto const before = requests_.size(); - sr.push_range2(cmd, begin, end); - auto const after = requests_.size(); - assert(after - before != 0); - req_info_.front().size += after - before;; - - if (!has_push_response(cmd)) { - commands_.push_back(cmd); - ++req_info_.front().cmds; - } - - if (can_write) - timer_.cancel_one(); - } - - /** \brief Sends a range. - */ - template - void send_range(Command cmd, Key const& key, Range const& range) - { - using std::begin; - using std::end; - send_range2(cmd, key, begin(range), end(range)); - } - - /** \brief Sends a range. - */ - template - void send_range(Command cmd, Range const& range) - { - using std::begin; - using std::end; - send_range2(cmd, begin(range), end(range)); - } - - /** \brief Starts communication with the Redis server asynchronously. - */ - template < - class Receiver, - class CompletionToken = default_completion_token_type - > - auto - async_run( - Receiver& recv, - boost::asio::ip::tcp::endpoint ep = {boost::asio::ip::make_address("127.0.0.1"), 6379}, - CompletionToken token = CompletionToken{}) - { - endpoint_ = ep; - return boost::asio::async_compose - < CompletionToken - , void(boost::system::error_code) - >(run_op{this, &recv}, token, socket_, timer_); - } }; } // generic diff --git a/aedis/generic/serializer.hpp b/aedis/generic/serializer.hpp index d56b67ba..c6c3239c 100644 --- a/aedis/generic/serializer.hpp +++ b/aedis/generic/serializer.hpp @@ -33,7 +33,7 @@ namespace generic { * co_await async_write(socket, buffer(request)); * @endcode * - * \tparam Storage The storage type. This is currently a \c std::string. + * \tparam Storage The storage type e.g \c std::string. * \tparam Command The command to serialize. * * \remarks Non-string types will be converted to string by using \c @@ -160,7 +160,9 @@ public: resp3::add_bulk(*request_, *begin); } - /** \brief Sends a range. + /** @brief Appends a new command to the end of the request. + * + * Similar to the range version. */ template void push_range(Command cmd, Key const& key, Range const& range) @@ -170,7 +172,9 @@ public: push_range2(cmd, key, begin(range), end(range)); } - /** \brief Sends a range. + /** @brief Appends a new command to the end of the request. + * + * Similar to the range version. */ template void push_range(Command cmd, Range const& range) @@ -181,7 +185,7 @@ public: } }; -/** \brief Creates a serializer for Sentinel commands. +/** \brief Creates a serializer. * \ingroup any * \param storage The string. */ diff --git a/aedis/resp3/node.hpp b/aedis/resp3/node.hpp index 135d5157..ff35f514 100644 --- a/aedis/resp3/node.hpp +++ b/aedis/resp3/node.hpp @@ -20,6 +20,16 @@ namespace resp3 { * * Redis responses are the pre-order view of the response tree (see * https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR). + * + * The node class represent one element in the response tree. The string type + * is a template give more flexibility, for example + * + * @li @c boost::string_view + * @li @c std::string + * @li @c boost::static_string + * + * \remark Any Redis response can be received in an array of nodes, for + * example \c std::vector>. */ template struct node { diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index 9d3928ad..dad59e85 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -179,7 +179,7 @@ STRIP_FROM_PATH = # specify the list of include paths that are normally passed to the compiler # using the -I flag. -STRIP_FROM_INC_PATH = +STRIP_FROM_INC_PATH = . # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but # less readable) file names. This can be useful is your file systems doesn't