Skip to content

Commit 7304d99

Browse files
authored
Adds support for UNIX sockets (#272)
close #246
1 parent 89a42db commit 7304d99

File tree

20 files changed

+639
-140
lines changed

20 files changed

+639
-140
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,13 @@ jobs:
196196
build-type: 'Debug'
197197
cxxflags: '-fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all'
198198
ldflags: '-fsanitize=address -fsanitize=undefined'
199+
200+
- toolset: gcc-14
201+
install: 'g++-14'
202+
container: ubuntu:24.04
203+
cxxstd: '23'
204+
build-type: 'Debug'
205+
cxxflags: '-DBOOST_ASIO_DISABLE_LOCAL_SOCKETS=1' # If a system had no UNIX socket support, we build correctly
199206

200207
- toolset: gcc-14
201208
install: 'g++-14'

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ The examples below show how to use the features discussed so far
414414
415415
* cpp20_intro.cpp: Does not use awaitable operators.
416416
* cpp20_intro_tls.cpp: Communicates over TLS.
417+
* cpp20_unix_sockets.cpp: Communicates over UNIX domain sockets.
417418
* cpp20_containers.cpp: Shows how to send and receive STL containers and how to use transactions.
418419
* cpp20_json.cpp: Shows how to serialize types using Boost.Json.
419420
* cpp20_protobuf.cpp: Shows how to serialize types using protobuf.

example/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ make_testable_example(cpp20_intro 20)
2929
make_testable_example(cpp20_containers 20)
3030
make_testable_example(cpp20_json 20)
3131
make_testable_example(cpp20_intro_tls 20)
32+
make_testable_example(cpp20_unix_sockets 20)
3233

3334
make_example(cpp20_subscriber 20)
3435
make_example(cpp20_streams 20)

example/cpp20_unix_sockets.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// Copyright (c) 2025 Marcelo Zimbres Silva ([email protected]),
3+
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
4+
//
5+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
6+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7+
//
8+
#include <boost/asio/awaitable.hpp>
9+
10+
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
11+
12+
#include <boost/redis/connection.hpp>
13+
14+
#include <boost/asio/consign.hpp>
15+
#include <boost/asio/detached.hpp>
16+
#include <boost/asio/this_coro.hpp>
17+
18+
#include <iostream>
19+
20+
namespace asio = boost::asio;
21+
using boost::redis::request;
22+
using boost::redis::response;
23+
using boost::redis::config;
24+
using boost::redis::logger;
25+
using boost::redis::connection;
26+
27+
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
28+
29+
auto co_main(config cfg) -> asio::awaitable<void>
30+
{
31+
// If unix_socket is set to a non-empty string, UNIX domain sockets will be used
32+
// instead of TCP. Set this value to the path where your server is listening.
33+
// UNIX domain socket connections work in the same way as TCP connections.
34+
cfg.unix_socket = "/tmp/redis-socks/redis.sock";
35+
36+
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
37+
conn->async_run(cfg, {}, asio::consign(asio::detached, conn));
38+
39+
request req;
40+
req.push("PING");
41+
42+
response<std::string> resp;
43+
44+
co_await conn->async_exec(req, resp);
45+
conn->cancel();
46+
47+
std::cout << "Response: " << std::get<0>(resp).value() << std::endl;
48+
}
49+
50+
#else
51+
52+
auto co_main(config) -> asio::awaitable<void>
53+
{
54+
std::cout << "Sorry, your system does not support UNIX domain sockets\n";
55+
co_return;
56+
}
57+
58+
#endif
59+
60+
#endif

include/boost/redis/config.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ struct config {
3434
/// Address of the Redis server.
3535
address addr = address{"127.0.0.1", "6379"};
3636

37+
/// The UNIX domain socket path where the server is listening. If non-empty,
38+
/// communication with the server will happen using UNIX domain sockets, and addr will be ignored.
39+
/// UNIX domain sockets can't be used with SSL: if `unix_socket` is non-empty, `use_ssl` must be false.
40+
std::string unix_socket;
41+
3742
/** @brief Username passed to the
3843
* [HELLO](https://redis.io/commands/hello/) command. If left
3944
* empty `HELLO` will be sent without authentication parameters.

include/boost/redis/connection.hpp

Lines changed: 83 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,25 @@ struct reader_op {
290290
}
291291
};
292292

293+
inline system::error_code check_config(const config& cfg)
294+
{
295+
if (!cfg.unix_socket.empty()) {
296+
#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS
297+
return error::unix_sockets_unsupported;
298+
#endif
299+
if (cfg.use_ssl)
300+
return error::unix_sockets_ssl_unsupported;
301+
}
302+
return system::error_code{};
303+
}
304+
293305
template <class Conn, class Logger>
294306
class run_op {
295307
private:
296308
Conn* conn_ = nullptr;
297309
Logger logger_;
298310
asio::coroutine coro_{};
311+
system::error_code stored_ec_;
299312

300313
using order_t = std::array<std::size_t, 5>;
301314

@@ -321,75 +334,87 @@ class run_op {
321334
system::error_code ec3 = {},
322335
system::error_code = {})
323336
{
324-
BOOST_ASIO_CORO_REENTER(coro_) for (;;)
337+
BOOST_ASIO_CORO_REENTER(coro_)
325338
{
326-
// Try to connect
327-
BOOST_ASIO_CORO_YIELD
328-
conn_->stream_.async_connect(&conn_->cfg_, logger_, std::move(self));
329-
330-
// If we failed, try again
339+
// Check config
340+
ec0 = check_config(conn_->cfg_);
331341
if (ec0) {
332-
self.complete(ec0);
342+
logger_.log_error("Invalid configuration", ec0);
343+
stored_ec_ = ec0;
344+
BOOST_ASIO_CORO_YIELD asio::async_immediate(self.get_io_executor(), std::move(self));
345+
self.complete(stored_ec_);
333346
return;
334347
}
335348

336-
conn_->mpx_.reset();
349+
for (;;) {
350+
// Try to connect
351+
BOOST_ASIO_CORO_YIELD
352+
conn_->stream_.async_connect(&conn_->cfg_, logger_, std::move(self));
337353

338-
// Note: Order is important here because the writer might
339-
// trigger an async_write before the async_hello thereby
340-
// causing an authentication problem.
341-
BOOST_ASIO_CORO_YIELD
342-
asio::experimental::make_parallel_group(
343-
[this](auto token) {
344-
return conn_->handshaker_.async_hello(*conn_, logger_, token);
345-
},
346-
[this](auto token) {
347-
return conn_->health_checker_.async_ping(*conn_, logger_, token);
348-
},
349-
[this](auto token) {
350-
return conn_->health_checker_.async_check_timeout(*conn_, logger_, token);
351-
},
352-
[this](auto token) {
353-
return conn_->reader(logger_, token);
354-
},
355-
[this](auto token) {
356-
return conn_->writer(logger_, token);
357-
})
358-
.async_wait(asio::experimental::wait_for_one_error(), std::move(self));
359-
360-
if (order[0] == 0 && !!ec0) {
361-
self.complete(ec0);
362-
return;
363-
}
354+
// If we failed, try again
355+
if (ec0) {
356+
self.complete(ec0);
357+
return;
358+
}
364359

365-
if (order[0] == 2 && ec2 == error::pong_timeout) {
366-
self.complete(ec1);
367-
return;
368-
}
360+
conn_->mpx_.reset();
369361

370-
// The receive operation must be cancelled because channel
371-
// subscription does not survive a reconnection but requires
372-
// re-subscription.
373-
conn_->cancel(operation::receive);
362+
// Note: Order is important here because the writer might
363+
// trigger an async_write before the async_hello thereby
364+
// causing an authentication problem.
365+
BOOST_ASIO_CORO_YIELD
366+
asio::experimental::make_parallel_group(
367+
[this](auto token) {
368+
return conn_->handshaker_.async_hello(*conn_, logger_, token);
369+
},
370+
[this](auto token) {
371+
return conn_->health_checker_.async_ping(*conn_, logger_, token);
372+
},
373+
[this](auto token) {
374+
return conn_->health_checker_.async_check_timeout(*conn_, logger_, token);
375+
},
376+
[this](auto token) {
377+
return conn_->reader(logger_, token);
378+
},
379+
[this](auto token) {
380+
return conn_->writer(logger_, token);
381+
})
382+
.async_wait(asio::experimental::wait_for_one_error(), std::move(self));
383+
384+
if (order[0] == 0 && !!ec0) {
385+
self.complete(ec0);
386+
return;
387+
}
374388

375-
if (!conn_->will_reconnect()) {
376-
conn_->cancel(operation::reconnection);
377-
self.complete(ec3);
378-
return;
379-
}
389+
if (order[0] == 2 && ec2 == error::pong_timeout) {
390+
self.complete(ec1);
391+
return;
392+
}
380393

381-
conn_->reconnect_timer_.expires_after(conn_->cfg_.reconnect_wait_interval);
394+
// The receive operation must be cancelled because channel
395+
// subscription does not survive a reconnection but requires
396+
// re-subscription.
397+
conn_->cancel(operation::receive);
382398

383-
BOOST_ASIO_CORO_YIELD
384-
conn_->reconnect_timer_.async_wait(asio::prepend(std::move(self), order_t{}));
385-
if (ec0) {
386-
self.complete(ec0);
387-
return;
388-
}
399+
if (!conn_->will_reconnect()) {
400+
conn_->cancel(operation::reconnection);
401+
self.complete(ec3);
402+
return;
403+
}
389404

390-
if (!conn_->will_reconnect()) {
391-
self.complete(asio::error::operation_aborted);
392-
return;
405+
conn_->reconnect_timer_.expires_after(conn_->cfg_.reconnect_wait_interval);
406+
407+
BOOST_ASIO_CORO_YIELD
408+
conn_->reconnect_timer_.async_wait(asio::prepend(std::move(self), order_t{}));
409+
if (ec0) {
410+
self.complete(ec0);
411+
return;
412+
}
413+
414+
if (!conn_->will_reconnect()) {
415+
self.complete(asio::error::operation_aborted);
416+
return;
417+
}
393418
}
394419
}
395420
}
@@ -753,9 +778,7 @@ class basic_connection {
753778
writer_timer_);
754779
}
755780

756-
auto is_open() const noexcept { return stream_.is_open(); }
757-
758-
[[nodiscard]] bool trigger_write() const noexcept { return is_open() && !mpx_.is_writing(); }
781+
bool is_open() const noexcept { return stream_.is_open(); }
759782

760783
detail::redis_stream<Executor> stream_;
761784

0 commit comments

Comments
 (0)