diff --git a/CHANGELOG.md b/CHANGELOG.md index df9b8ff8..f7a27ee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,16 @@ # Changelog -## master +## v1.0.0 * Adds experimental cmake support for windows users. -* Adds new class `sync` that wraps a `connection` and offers a - thread-safe synchronous API. All free functions from the `sync.hpp` - are now member functions of the `sync` class. +* 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 `connection::async_receive_event` in two functions, one to - receive events and another for server side pushes. +* 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`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 6df35e11..76f21eee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.14) project( Aedis - VERSION 0.3.0 + VERSION 1.0.0 DESCRIPTION "An async redis client designed for performance and scalability" HOMEPAGE_URL "https://mzimbres.github.io/aedis" LANGUAGES CXX diff --git a/INSTALL b/INSTALL deleted file mode 100644 index 88ee46b7..00000000 --- a/INSTALL +++ /dev/null @@ -1 +0,0 @@ -See https://mzimbres.github.io/aedis/#using-aedis diff --git a/README.md b/README.md index 4204512a..b62a9f51 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,7 @@ Distributed under the [Boost Software License, Version 1.0](http://www.boost.org * See the official github-pages for documentation: https://mzimbres.github.io/aedis +### Installation + +See https://mzimbres.github.io/aedis/#using-aedis + diff --git a/benchmarks/benchmarks.md b/benchmarks/benchmarks.md index 696df449..c2aa6163 100644 --- a/benchmarks/benchmarks.md +++ b/benchmarks/benchmarks.md @@ -85,9 +85,3 @@ The code used in the benchmarks can be found at ## 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. - -## Contributing - -If your spot any performance improvement in any of the example or -would like to include other clients, please open a PR and I will -gladly merge it. diff --git a/configure.ac b/configure.ac index ac0499fb..eb5e4180 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ AC_PREREQ([2.69]) -AC_INIT([Aedis], [0.3.0], [mzimbres@gmail.com]) +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) diff --git a/include/aedis.hpp b/include/aedis.hpp index 083f8921..5e53779a 100644 --- a/include/aedis.hpp +++ b/include/aedis.hpp @@ -22,8 +22,8 @@ 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 + [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. @@ -31,11 +31,11 @@ \li Healthy checks, back pressure and low latency. \li Hides most of the low level asynchronous operations away from the user. - Let us start with an overview of asynchronous code. + Let us have a look a some code snippets @subsection Async - The code below sends a ping command to Redis (see intro.cpp) + The code below sends a ping command to Redis and quits (see intro.cpp) @code int main() @@ -56,9 +56,9 @@ } @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 command + 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() @@ -78,7 +78,7 @@ } @endcode - The `async_exec` functions above can be called from different + 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 @@ -88,55 +88,42 @@ @code net::awaitable reader(std::shared_ptr db) { - request req; - req.push("SUBSCRIBE", "channel"); - for (std::vector resp;;) { - auto ev = co_await db->async_receive_event(aedis::adapt(resp)); - - switch (ev) { - case connection::event::push: - // Use resp and clear it. - resp.clear(); - break; - - default:; - } + co_await db->async_receive_event(adapt(resp)); + // Use resp and clear it. + resp.clear(); } } @endcode @subsection Sync - The `connection` class is async-only, many users however need to - interact with it synchronously, this is also supported by Aedis as long - as this interaction occurs across threads, for example (see - intro_sync.cpp) + 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() { - try { - net::io_context ioc{1}; - connection conn{ioc}; - - std::thread thread{[&]() { - conn.async_run(net::detached); - ioc.run(); - }}; - - request req; - req.push("PING"); - req.push("QUIT"); - - std::tuple resp; - exec(conn, req, adapt(resp)); - thread.join(); - - std::cout << "Response: " << std::get<0>(resp) << std::endl; - } catch (std::exception const& e) { - std::cerr << e.what() << std::endl; - } + net::io_context ioc{1}; + auto work = net::make_work_guard(ioc); + std::thread t1{[&]() { ioc.run(); }}; + + sync 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 resp; + conn.exec(req, adapt(resp)); + std::cout << "Response: " << std::get<0>(resp) << std::endl; + + work.reset(); + + t1.join(); + t2.join(); } @endcode @@ -151,23 +138,27 @@ For a simple installation run ``` - # Clone the repository and checkout the lastest release tag. - $ git clone --branch v0.3.0 https://github.com/mzimbres/aedis.git + $ git clone --branch v1.0.0 https://github.com/mzimbres/aedis.git $ cd aedis - # Build an example + # 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/v0.3.0/aedis-0.3.0.tar.gz - $ tar -xzvf aedis-0.3.0.tar.gz + $ 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-0.3.0 --with-boost=/opt/boost_1_78_0 + $ CXXFLAGS="-std=c++17" ./configure --prefix=/opt/aedis-1.0.0 --with-boost=/opt/boost_1_78_0 $ sudo make install ``` @@ -177,12 +168,6 @@ $ make ``` - There is also experimental support cmake, for example - - @code - $ BOOST_ROOT=/opt/boost_1_79_0/ cmake -DCMAKE_CXX_FLAGS=-std=c++20 . - @endcode - @subsubsection using_aedis Using Aedis When writing you own applications include the following header @@ -380,7 +365,7 @@ 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 in a single array, in the response to the @c exec command. + the user as an array, in the response to the @c exec command. For example, to read the response to the this request @code @@ -397,7 +382,7 @@ using aedis::ignore; using boost::optional; - using tresp_type = + using exec_resp_type = std::tuple< optional, // get optional>, // lrange @@ -409,7 +394,7 @@ ignore, // get ignore, // lrange ignore, // hgetall - tresp_type, // exec + exec_resp_type, // exec > resp; co_await db->async_exec(req, adapt(resp)); @@ -443,7 +428,7 @@ 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 response don't have a fixed + @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. @@ -487,14 +472,14 @@ @endcode For example, suppose we want to retrieve a hash data structure - from Redis with \c hgetall, some of the options are + from Redis with `HGETALL`, some of the options are @li \c std::vector: Works always. @li \c std::vector: Efficient and flat, all elements as string. @li \c std::map: Efficient if you need the data as a \c std::map @li \c std::map: 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. \c smembers. + 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 @@ -503,8 +488,8 @@ @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 the \c request support to serialization of user types. - @li subscriber.cpp: Shows how to subscribe to a channel and how to reconnect when connection is lost. + @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. diff --git a/include/aedis/adapt.hpp b/include/aedis/adapt.hpp index 681656cd..d40a644d 100644 --- a/include/aedis/adapt.hpp +++ b/include/aedis/adapt.hpp @@ -20,7 +20,7 @@ namespace aedis { -/** @brief Tag used tp ignore responses. +/** @brief Tag used to ignore responses. * @ingroup any * * For example diff --git a/include/aedis/connection.hpp b/include/aedis/connection.hpp index aadd3dcb..85e1a405 100644 --- a/include/aedis/connection.hpp +++ b/include/aedis/connection.hpp @@ -26,13 +26,20 @@ namespace aedis { -/** @brief A high level Redis asynchronous connection to Redis. +/** @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 connection { @@ -43,20 +50,13 @@ class connection { /// Type of the next layer using next_layer_type = AsyncReadWriteStream; - using default_completion_token_type = boost::asio::default_completion_token_t; - using push_channel_type = boost::asio::experimental::channel; - using clock_type = std::chrono::steady_clock; - using clock_traits_type = boost::asio::wait_traits; - using timer_type = boost::asio::basic_waitable_timer; - using resolver_type = boost::asio::ip::basic_resolver; - /** @brief Connection configuration parameters. */ struct config { - /// The Redis server address. + /// Redis server address. std::string host = "127.0.0.1"; - /// The Redis server port. + /// Redis server port. std::string port = "6379"; /// Username if authentication is required. @@ -71,30 +71,30 @@ class connection { /// Timeout of the connect operation. std::chrono::milliseconds connect_timeout = std::chrono::seconds{10}; - /// Time interval ping operations. + /// Time interval of ping operations. std::chrono::milliseconds ping_interval = std::chrono::seconds{1}; - /// Time waited before trying a reconnection (see enable reconnect). + /// Time waited before trying a reconnection (see config::enable_reconnect). std::chrono::milliseconds reconnect_interval = std::chrono::seconds{1}; - /// The maximum size allowed on read operations. + /// The maximum size of read operations. std::size_t max_read_size = (std::numeric_limits::max)(); /// Whether to coalesce requests (see [pipelines](https://redis.io/topics/pipelining)). bool coalesce_requests = true; - /// Enable events + /// Enable internal events, see connection::async_receive_event. bool enable_events = false; - /// Enable automatic reconnection (see also reconnect_interval). + /// Enable automatic reconnection (see also config::reconnect_interval). bool enable_reconnect = false; }; - /// Events communicated through \c async_receive_event. + /// Events that are communicated by `connection::async_receive_event`. enum class event { - /// The address has been successfully resolved. + /// Resolve operation was successful. resolve, - /// Connected to the Redis server. + /// Connect operation was successful. connect, /// Success sending AUTH and HELLO. hello, @@ -102,24 +102,23 @@ class connection { invalid }; - using event_channel_type = boost::asio::experimental::channel; - - /** @brief Async operations that can be cancelled. + /** @brief Async operations exposed by this class. * - * See the \c cancel member function for more information. + * The operations listed below can be cancelled with the `cancel` + * member function. */ enum class operation { - /// Operations started with \c async_exec. + /// `connection::async_exec` operations. exec, - /// Operation started with \c async_run. + /// `connection::async_run` operations. run, - /// Operation started with async_receive_event. + /// `connection::async_receive_event` operations. receive_event, - /// Operation started with async_receive_push. + /// `connection::async_receive_push` operations. receive_push, }; - /** \brief Construct a connection from an executor. + /** \brief Contructor * * \param ex The executor. * \param cfg Configuration parameters. @@ -139,7 +138,7 @@ class connection { read_timer_.expires_at(std::chrono::steady_clock::time_point::max()); } - /** \brief Construct a connection from an io_context. + /** \brief Constructor * * \param ioc The executor. * \param cfg Configuration parameters. @@ -153,15 +152,18 @@ class connection { /** @brief Cancel operations. * - * @li operation::exec: Cancels all operations started with \c async_exec. - * @li operation::run: Cancels @c async_run. The prefered way to - * close a connection is to set config::enable_reconnect to - * false and send a \c quit command. Otherwise an unresponsive Redis server - * will cause the idle-checks to kick in and lead to \c - * async_run returning with idle_timeout. Calling \c - * cancel(operation::run) directly should be seen as the last - * option. - * @li operation::receive_event: Cancels @c async_receive_event. + * @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. @@ -190,11 +192,11 @@ class connection { writer_timer_.cancel(); ping_timer_.cancel(); - // Cancel own pings if there are any waiting. 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(); @@ -225,6 +227,8 @@ class connection { config const& get_config() const noexcept { return cfg_;} /** @name Asynchronous functions + * + * Each of these operations a individually cancellable. **/ /// @{ @@ -232,25 +236,28 @@ class connection { * * This function performs the following steps * - * \li Resolves the Redis host as of \c async_resolve with the - * timeout passed in connection::config::resolve_timeout. + * @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 connection::config::connect_timeout. + * @li Connects to one of the endpoints returned by the resolve + * operation with the timeout passed in `config::connect_timeout`. * - * \li Starts the idle check operation with the timeout of twice - * the value of connection::config::ping_interval. If no data is - * received during that time interval \c async_run completes with - * error::idle_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 command::ping - * to Redis with a frequency equal to - * connection::config::ping_interval. + * @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 delivering events to the - * request started with \c async_exec and \c async_receive_event. + * @li Starts reading from the socket and executes all requests + * that have been started prior to this function call. * - * For an example see echo_server.cpp. + * @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. * @@ -259,10 +266,8 @@ class connection { * @code * void f(boost::system::error_code); * @endcode - * - * \return This function returns only when there is an error. */ - template + template > auto async_run(CompletionToken token = CompletionToken{}) { return boost::asio::async_compose @@ -273,7 +278,7 @@ class connection { /** @brief Connects and executes a request asynchronously. * - * Combines \c async_run and the other \c async_exec overload in a + * 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. * @@ -281,7 +286,8 @@ class connection { * \param adapter Response adapter. * \param token Asio completion token. * - * For an example see intro.cpp. The completion token must have the following signature + * For an example see intro.cpp. The completion token must have + * the following signature * * @code * void f(boost::system::error_code, std::size_t); @@ -291,7 +297,7 @@ class connection { */ template < class Adapter = detail::response_traits::adapter_type, - class CompletionToken = default_completion_token_type> + class CompletionToken = boost::asio::default_completion_token_t> auto async_run( resp3::request const& req, Adapter adapter = adapt(), @@ -305,6 +311,9 @@ class connection { } /** @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. @@ -322,7 +331,7 @@ class connection { */ template < class Adapter = detail::response_traits::adapter_type, - class CompletionToken = default_completion_token_type> + class CompletionToken = boost::asio::default_completion_token_t> auto async_exec( resp3::request const& req, Adapter adapter = adapt(), @@ -336,11 +345,11 @@ class connection { >(detail::exec_op{this, &req, adapter}, token, resv_); } - /** @brief Receives unsolicited events asynchronously. + /** @brief Receives server side pushes asynchronously. * - * Users that expect unsolicited events should call this function - * in a loop. If an unsolicited events comes in and there is no - * reader, the connection will hang and eventually timeout. + * 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. @@ -351,10 +360,13 @@ class connection { * @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::adapter_type, - class CompletionToken = default_completion_token_type> + class CompletionToken = boost::asio::default_completion_token_t> auto async_receive_push( Adapter adapter = adapt(), CompletionToken token = CompletionToken{}) @@ -374,7 +386,7 @@ class connection { /** @brief Receives internal events. * - * See enum \c events for a list of events. + * See enum \c events for the list of events. * * \param token The Asio completion token. * @@ -384,7 +396,7 @@ class connection { * void f(boost::system::error_code, event); * @endcode */ - template + template > auto async_receive_event(CompletionToken token = CompletionToken{}) { return event_channel_.async_receive(token); @@ -392,6 +404,14 @@ class connection { /// @} private: + using clock_type = std::chrono::steady_clock; + using clock_traits_type = boost::asio::wait_traits; + using timer_type = boost::asio::basic_waitable_timer; + using resolver_type = boost::asio::ip::basic_resolver; + using push_channel_type = boost::asio::experimental::channel; + using time_point_type = std::chrono::time_point; + using event_channel_type = boost::asio::experimental::channel; + struct req_info { req_info(executor_type ex) : timer{ex} {} timer_type timer; @@ -401,7 +421,6 @@ class connection { bool written = false; }; - using time_point_type = std::chrono::time_point; using reqs_type = std::deque>; template friend struct detail::receive_push_op; @@ -419,7 +438,7 @@ class connection { template friend struct detail::start_op; template friend struct detail::send_receive_op; - template + template auto async_run_one(CompletionToken token = CompletionToken{}) { return boost::asio::async_compose diff --git a/include/aedis/sync.hpp b/include/aedis/sync.hpp index d8fec402..4090482b 100644 --- a/include/aedis/sync.hpp +++ b/include/aedis/sync.hpp @@ -36,7 +36,7 @@ class sync { /** @brief Executes a request synchronously. * - * The functions calls `connections::async_receive_exec` and waits + * The functions calls `connection::async_exec` and waits * for its completion. * * @param req The request. @@ -71,7 +71,7 @@ class sync { /** @brief Executes a command synchronously * - * The functions calls `connections::async_exec` and waits for its + * The functions calls `connection::async_exec` and waits for its * completion. * * @param req The request. @@ -91,7 +91,7 @@ class sync { /** @brief Receives server pushes synchronusly. * - * The functions calls `connections::async_receive_push` and + * The functions calls `connection::async_receive_push` and * waits for its completion. * * @param adapter The response adapter. @@ -124,7 +124,7 @@ class sync { /** @brief Receives server pushes synchronusly. * - * The functions calls `connections::async_receive_push` and + * The functions calls `connection::async_receive_push` and * waits for its completion. * * @param adapter The response adapter. @@ -143,7 +143,7 @@ class sync { /** @brief Receives events synchronously. * - * The functions calls `connections::async_receive_event` and + * The functions calls `connection::async_receive_event` and * waits for its completion. * * @param ec Error code in case of error. @@ -174,7 +174,7 @@ class sync { /** @brief Receives events synchronously * - * The functions calls `connections::async_receive_event` and + * The functions calls `connection::async_receive_event` and * waits for its completion. * * @throws std::system_error in case of error. @@ -191,7 +191,7 @@ class sync { /** @brief Calls \c async_run from the underlying connection. * - * The functions calls `connections::async_run` and waits for its + * The functions calls `connection::async_run` and waits for its * completion. * * @param ec Error code. @@ -217,7 +217,7 @@ class sync { /** @brief Calls \c async_run from the underlying connection. * - * The functions calls `connections::async_run` and waits for its + * The functions calls `connection::async_run` and waits for its * completion. * * @throws std::system_error.