|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +nav-class: dark |
| 4 | +categories: ruben |
| 5 | +title: "A postgres library for Boost" |
| 6 | +author-id: ruben |
| 7 | +author-name: Rubén Pérez Hidalgo |
| 8 | +--- |
| 9 | + |
| 10 | +## The road to `import boost`: a talk in a wonderful conference |
| 11 | + |
| 12 | +- C++20 modules have been in the standard for 6 years already, but we're not seeing widespread adoption. |
| 13 | +- At the moment, the ecosystem is still getting ready. As a quick example, `import std`, an absolute blessing |
| 14 | + for compile times, requires build system support, and this is still experimental as of CMake 4.3.1. |
| 15 | + Just an instance of what it is. |
| 16 | + |
| 17 | +- Something I've realized is that writing module-native applications is really enjoyable. The system is well-thought and allows for better encapsulation, |
| 18 | + just as you'd write in a modern programming language. (TODO: example). |
| 19 | +- I've been using my Servertech Chat project (a webserver that uses Boost.Asio and companion libraries) to have a taste of what modules really look like in real code. |
| 20 | + In case you're curious: https://github.com/anarthal/servertech-chat/tree/feature/cxx20-modules |
| 21 | +- When writing this, I saw clearly that having big dependencies that can't be consumed via import is a big problem. |
| 22 | + With the scheme I used, compile times got 66% worse instead of improving. This is because when writing modules, you tend to have |
| 23 | + a bigger number of translation units. These are supposed to be much more lightweight, but if you're relying on includes |
| 24 | + for third-party libraries, they're not! |
| 25 | + |
| 26 | +For example: |
| 27 | + |
| 28 | +```cpp |
| 29 | +// |
| 30 | +// File: redis_client.cppm. Contains only the interface declaration (somehow like headers do) |
| 31 | +// |
| 32 | +module; |
| 33 | + |
| 34 | +// No import boost yet - must be in the global module fragment |
| 35 | +#include <boost/asio/awaitable.hpp> |
| 36 | +#include <boost/system/result.hpp> |
| 37 | + |
| 38 | +module servertech_chat:redis_client; |
| 39 | +import std; |
| 40 | + |
| 41 | +namespace chat { |
| 42 | + |
| 43 | +class redis_client |
| 44 | +{ |
| 45 | +public: |
| 46 | + virtual ~redis_client() {} |
| 47 | + virtual boost::asio::awaitable<boost::system::result<std::int64_t>> get_int_key(std::string_view key) = 0; |
| 48 | + // ... |
| 49 | +}; |
| 50 | + |
| 51 | +} |
| 52 | + |
| 53 | +// |
| 54 | +// File: redis_client.cpp. Contains the implementation |
| 55 | +// |
| 56 | +module; |
| 57 | + |
| 58 | +#include <boost/redis/connection.hpp> |
| 59 | + |
| 60 | +module servertech_chat; |
| 61 | +import :redis_client; |
| 62 | +import std; |
| 63 | + |
| 64 | +namespace { |
| 65 | + |
| 66 | +class redis_client_impl final : public redis_client { /* ... */ }; |
| 67 | + |
| 68 | +} |
| 69 | + |
| 70 | +``` |
| 71 | +
|
| 72 | +I analyze this in much more depth in the talk I've had the pleasure to give at using std::cpp this March in Madrid. |
| 73 | +https://youtu.be/hD9JHkt7e2Y for the entire talk. |
| 74 | +
|
| 75 | +The TL;DR is that supporting `import boost` natively is very important for any serious usage of Boost in the modules world. |
| 76 | +
|
| 77 | +## `import boost` is upon us |
| 78 | +
|
| 79 | +As you may know, I prefer doing to saying, and I've been writing a prototype to support |
| 80 | +`import boost` natively while keeping today's header code as is. This prototype has |
| 81 | +seen substantial advancements during these months. |
| 82 | +
|
| 83 | +I've developed a systematic approach for modularization, which I summarize here: https://github.com/anarthal/boost-cmake/blob/feature/cxx20-modules/modules.md. |
| 84 | +We've settled for the ABI-breaking style, with compatibility headers. |
| 85 | +I've added support for the remaining compiler, gcc, to the core libraries that we used to support (Config, Mp11, Core, Assert, ThrowException, Charconv). |
| 86 | +And I've added modular bindings for (variant2, compat, endian, system, type_traits, optional, container_hash, io and asio). |
| 87 | +These are only tested under clang yet - it's part of a discovery process. Idea is modularizing the flagship libraries |
| 88 | +to verify that the approach works, and to measure compile time improvements. |
| 89 | +
|
| 90 | +Still lots to do before things become functional. I've received helpful feedback from many community |
| 91 | +members. |
| 92 | +
|
| 93 | +## Redis meets Capy |
| 94 | +
|
| 95 | +If you're a user of Boost.Asio and coroutines, you probably know that there's a new player |
| 96 | +in town - Capy and Corosio. They're a coroutines-native Asio replacement which promise |
| 97 | +a range of benefits, from improved expressiveness, saner compile times, without performance loss. |
| 98 | +
|
| 99 | +Since I maintain Boost.MySQL and co-maintain Boost.Redis, I know the pain of writing |
| 100 | +operations using the universal Asio model. Lifetime management is difficult to follow, |
| 101 | +testing is complex, things must be remained header-only (and usually heavily templatized). |
| 102 | +Coroutine code is much simpler to write and understand, and it's what I use whenever I can. |
| 103 | +So obviously I'm interested in this project. |
| 104 | +
|
| 105 | +My long-term idea is creating a v2 version of MySQL and Redis that exposes a Capy/Corosio |
| 106 | +interface. As a proof-of-concept, I migrated Boost.Redis and some of its tests. |
| 107 | +Still some polishment needed, but - it works! |
| 108 | +You can see the full report: https://lists.boost.org/archives/list/boost@lists.boost.org/thread/FSX5H3MDQSLO3VZFEOUINUZPYQFCIASB/ |
| 109 | +
|
| 110 | +Some sample code as an appetizer: |
| 111 | +
|
| 112 | +```cpp |
| 113 | +
|
| 114 | +capy::task<void> run_request(connection& conn) |
| 115 | +{ |
| 116 | + // A request containing only a ping command. |
| 117 | + request req; |
| 118 | + req.push("PING", "Hello world"); |
| 119 | +
|
| 120 | + // Response where the PONG response will be stored. |
| 121 | + response<std::string> resp; |
| 122 | +
|
| 123 | + // Executes the request. |
| 124 | + auto [ec] = co_await conn.exec(req, resp); |
| 125 | + if (ec) |
| 126 | + co_return; |
| 127 | + std::cout << "PING value: " << std::get<0>(resp).value() << std::endl; |
| 128 | +} |
| 129 | +
|
| 130 | +capy::task<void> co_main() |
| 131 | +{ |
| 132 | + connection conn{(co_await capy::this_coro::executor).context()}; |
| 133 | + co_await capy::when_any( |
| 134 | + // Sends the request |
| 135 | + run_request(conn), |
| 136 | +
|
| 137 | + // Performs connection establishment, re-connection, pings... |
| 138 | + conn.run(config{}) |
| 139 | + ); |
| 140 | +} |
| 141 | +
|
| 142 | +``` |
| 143 | + |
| 144 | +## Redis PubSub |
| 145 | + |
| 146 | +Working with PubSub messages in Boost.Redis has always been more involved than in other libraries. |
| 147 | +For example, we support transparent reconnection, but (before 1.91), the user had to explicitly |
| 148 | +re-establish subscriptions: |
| 149 | + |
| 150 | +```cpp |
| 151 | +request req; |
| 152 | +req.push("SUBSCRIBE", "channel"); |
| 153 | +while (conn->will_reconnect()) { |
| 154 | + // Reconnect to the channels. |
| 155 | + co_await conn->async_exec(req, ignore); |
| 156 | + |
| 157 | + // ... |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +Boost 1.91 has added pubsub state restoration. A fancy name but an easy feature. |
| 162 | +Established subscriptions are recorded. When a reconnection happens, |
| 163 | +the subscription is re-established: |
| 164 | + |
| 165 | +```cpp |
| 166 | +// Subscribe to the channel 'mychannel'. If a re-connection happens, |
| 167 | +// an appropriate SUBSCRIBE command is issued to re-establish the subscription. |
| 168 | +request req; |
| 169 | +req.subscribe({"mychannel"}); |
| 170 | +co_await conn->async_exec(req); |
| 171 | +``` |
| 172 | +
|
| 173 | +Boost 1.91 also adds `flat_tree`, a specialized container for Redis messages |
| 174 | +with an emphasis in memory-reuse, performance and usability. |
| 175 | +This container is specially appropriate for using when dealing with PubSub. |
| 176 | +We've also added `connection::async_receive2()`, a higher-performance |
| 177 | +replacement for `connection::async_receive()` that consumes messages in batches, |
| 178 | +rather than one-by-one, eliminating re-scheduling overhead. |
| 179 | +And `push_parser`, a view to transform raw RESP3 nodes into user-friendly structures. |
| 180 | +
|
| 181 | +With these improvements, code goes from |
| 182 | +
|
| 183 | +```cpp |
| 184 | +// Loop while reconnection is enabled |
| 185 | +while (conn->will_reconnect()) { |
| 186 | +
|
| 187 | + // Reconnect to channels. |
| 188 | + co_await conn->async_exec(req, ignore); |
| 189 | +
|
| 190 | + // Loop reading Redis pushs messages. |
| 191 | + for (error_code ec;;) { |
| 192 | + // First try to read any buffered pushes. |
| 193 | + conn->receive(ec); |
| 194 | + if (ec == error::sync_receive_push_failed) { |
| 195 | + ec = {}; |
| 196 | +
|
| 197 | + // Wait for pushes |
| 198 | + co_await conn->async_receive(asio::redirect_error(asio::use_awaitable, ec)); |
| 199 | + } |
| 200 | +
|
| 201 | + if (ec) |
| 202 | + break; // Connection lost, break so we can reconnect to channels. |
| 203 | +
|
| 204 | + // Left to the user: resp contains raw RESP3 nodes, which need to be parsed manually! |
| 205 | +
|
| 206 | + // Remove the nodes corresponding to one message |
| 207 | + consume_one(resp); |
| 208 | + } |
| 209 | +} |
| 210 | +
|
| 211 | +``` |
| 212 | + |
| 213 | +To: |
| 214 | + |
| 215 | +```cpp |
| 216 | +// Loop to read Redis push messages. |
| 217 | +while (conn->will_reconnect()) { |
| 218 | + // No need to reconnect, we now have PubSub state restoration |
| 219 | + // Wait for pushes |
| 220 | + auto [ec] = co_await conn->async_receive2(asio::as_tuple); |
| 221 | + if (ec) |
| 222 | + break; // Cancelled |
| 223 | + |
| 224 | + // Consume the messages |
| 225 | + for (push_view elem : push_parser(resp.value())) |
| 226 | + std::cout << "Received message from channel " << elem.channel << ": " << elem.payload << "\n"; |
| 227 | + |
| 228 | + // Clear all the batch |
| 229 | + resp.value().clear(); |
| 230 | +} |
| 231 | +``` |
0 commit comments