Skip to content

Commit 40a6b13

Browse files
anarthalsdarwin
authored andcommitted
Initial report
1 parent c213096 commit 40a6b13

1 file changed

Lines changed: 231 additions & 0 deletions

File tree

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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

Comments
 (0)