diff --git a/cmake/Modules/ValkeySetup.cmake b/cmake/Modules/ValkeySetup.cmake index 4efefefe694..698fcef7560 100644 --- a/cmake/Modules/ValkeySetup.cmake +++ b/cmake/Modules/ValkeySetup.cmake @@ -218,6 +218,30 @@ else () set(USE_RDMA 0) endif () +# Systemd support (auto-detect by default, matching Make build behaviour) +# Pass -DUSE_SYSTEMD=no to disable, -DUSE_SYSTEMD=yes to require. +if (NOT "${USE_SYSTEMD}" STREQUAL "no") + find_package(PkgConfig QUIET) + if (PKG_CONFIG_FOUND) + pkg_check_modules(LIBSYSTEMD libsystemd) + if (LIBSYSTEMD_FOUND) + add_valkey_server_compiler_options("-DHAVE_LIBSYSTEMD") + list(APPEND SERVER_LIBS "${LIBSYSTEMD_LIBRARIES}") + message(STATUS "Building with systemd support: ${LIBSYSTEMD_LIBRARIES}") + elseif ("${USE_SYSTEMD}" STREQUAL "yes") + message(FATAL_ERROR "USE_SYSTEMD=yes but libsystemd was not found") + else () + message(STATUS "libsystemd not found, building without systemd support") + endif () + elseif ("${USE_SYSTEMD}" STREQUAL "yes") + message(FATAL_ERROR "USE_SYSTEMD=yes but pkg-config is not available") + else () + message(STATUS "pkg-config not found, building without systemd support") + endif () +else () + message(STATUS "Systemd support disabled") +endif () + set(BUILDING_ARM64 0) set(BUILDING_ARM32 0) diff --git a/src/cluster_legacy.c b/src/cluster_legacy.c index 72b869bb077..5b460774d10 100644 --- a/src/cluster_legacy.c +++ b/src/cluster_legacy.c @@ -1545,7 +1545,7 @@ void clusterInitLast(void) { listener->bindaddr_count = server.bindaddr_count; listener->port = server.cluster_port ? server.cluster_port : port + CLUSTER_PORT_INCR; listener->ct = connTypeOfCluster(); - if (connListen(listener) == C_ERR) { + if (adoptInheritedFdsForListener(listener, CONN_TYPE_CLUSTER_BUS) == 0 && connListen(listener) == C_ERR) { /* Note: the following log text is matched by the test suite. */ serverLog(LL_WARNING, "Failed listening on port %u (cluster), aborting.", listener->port); exit(1); diff --git a/src/connection.h b/src/connection.h index 44e15e8f1c1..01d04d39b7e 100644 --- a/src/connection.h +++ b/src/connection.h @@ -180,7 +180,8 @@ struct connListener { int bindaddr_count; int port; ConnectionType *ct; - void *priv; /* used by connection type specified data */ + void *priv; /* used by connection type specified data */ + int inherited; /* 1 if fds were passed by systemd socket activation */ }; /* The connection module does not deal with listening and accepting sockets, diff --git a/src/server.c b/src/server.c index 03050bbae6e..253954ebab6 100644 --- a/src/server.c +++ b/src/server.c @@ -80,6 +80,10 @@ #include #endif +#ifdef HAVE_LIBSYSTEMD +#include +#endif + #if defined(HAVE_SYSCTL_KIPC_SOMAXCONN) || defined(HAVE_SYSCTL_KERN_SOMAXCONN) #include #endif @@ -3190,7 +3194,8 @@ void initListeners(void) { listener = &server.listeners[j]; if (listener->ct == NULL) continue; - if (connListen(listener) == C_ERR) { + if (adoptInheritedFdsForListener(listener, listener->ct->get_type()) == 0 && + connListen(listener) == C_ERR) { serverLog(LL_WARNING, "Failed listening on port %u (%s), aborting.", listener->port, getConnectionTypeName(listener->ct->get_type())); exit(1); @@ -4667,7 +4672,8 @@ void closeListeningSockets(int unlink_unix_socket) { if (server.cluster_enabled) for (j = 0; j < server.clistener.count; j++) close(server.clistener.fd[j]); - if (unlink_unix_socket && server.unixsocket) { + if (unlink_unix_socket && server.unixsocket && + !server.listeners[CONN_TYPE_UNIX].inherited) { serverLog(LL_NOTICE, "Removing the unix socket file."); if (unlink(server.unixsocket) != 0) serverLog(LL_WARNING, "Error removing the unix socket file: %s", strerror(errno)); @@ -6946,6 +6952,18 @@ connListener *listenerByType(int type) { /* Close original listener, re-create a new listener from the updated bind address & port */ int changeListener(connListener *listener) { + /* Refuse runtime reconfiguration for listeners inherited from systemd + * socket activation: closing and re-binding here would relinquish the + * fd that systemd holds, and the subsequent bind() can fail or race. + * Adjust the systemd .socket unit instead. */ + if (listener->inherited) { + serverLog(LL_WARNING, + "Cannot reconfigure %s listener at runtime: the listening socket was inherited " + "from systemd socket activation. Adjust the systemd .socket unit instead.", + getConnectionTypeName(listener->ct->get_type())); + return C_ERR; + } + /* Close old servers */ connCloseListener(listener); @@ -7371,6 +7389,118 @@ int serverCommunicateSystemd(const char *sd_notify_msg) { #endif } +/* ============== Systemd socket activation ============== */ + +#ifdef HAVE_LIBSYSTEMD +struct sdFdEntry { + int fd; + int conn_type; +}; + +static struct sdFdEntry sd_inherited_fds[CONFIG_BINDADDR_MAX + 1]; +static int sd_inherited_fd_count = 0; + +/* Return 1 if `inherited_ip` (as returned by anetFdToString) on family + * `inherited_family` is accepted by any entry in server.bindaddr[]. + * Valkey wildcards `*` and `::*` match all IPv4 / IPv6 addresses respectively. + * Other entries must match the inherited address string exactly. */ +static int sdInheritedAddrMatchesBindaddr(int inherited_family, const char *inherited_ip) { + if (server.bindaddr_count == 0) return 0; + for (int i = 0; i < server.bindaddr_count; i++) { + const char *ba = server.bindaddr[i]; + if (!strcmp(ba, "*")) { + if (inherited_family == AF_INET) return 1; + } else if (!strcmp(ba, "::*")) { + if (inherited_family == AF_INET6) return 1; + } else if (!strcmp(ba, inherited_ip)) { + return 1; + } + } + return 0; +} + +void inheritSystemdListenFds(void) { + /* Pass 1 so LISTEN_PID/LISTEN_FDS are unset, keeping children unaffected. */ + int n = sd_listen_fds(1); + if (n <= 0) return; + + for (int i = 0; i < n; i++) { + int fd = SD_LISTEN_FDS_START + i; + int conn_type = -1; + + if (sd_is_socket_unix(fd, SOCK_STREAM, 1, NULL, 0) > 0) { + if (server.unixsocket) { + struct sockaddr_un sa; + socklen_t salen = sizeof(sa); + if (getsockname(fd, (struct sockaddr *)&sa, &salen) == 0 && + strcmp(sa.sun_path, server.unixsocket) == 0) + conn_type = CONN_TYPE_UNIX; + } + } else if (sd_is_socket_inet(fd, AF_UNSPEC, SOCK_STREAM, 1, 0) > 0) { + struct sockaddr_storage sa; + socklen_t salen = sizeof(sa); + int port = 0; + char ip[NET_IP_STR_LEN]; + if (getsockname(fd, (struct sockaddr *)&sa, &salen) == 0 && + anetFdToString(fd, ip, sizeof(ip), &port, 0) == 0 && port > 0) { + int matches_bindaddr = sdInheritedAddrMatchesBindaddr(sa.ss_family, ip); + if (server.cluster_enabled && matches_bindaddr && + (port == server.cluster_port || + (server.cluster_port == 0 && port == server.port + CLUSTER_PORT_INCR))) + conn_type = CONN_TYPE_CLUSTER_BUS; + else if (server.tls_port && port == server.tls_port && matches_bindaddr) + conn_type = CONN_TYPE_TLS; + else if (server.port && port == server.port && matches_bindaddr) + conn_type = CONN_TYPE_SOCKET; + } + } + + if (conn_type >= 0 && sd_inherited_fd_count < (int)(sizeof(sd_inherited_fds) / sizeof(sd_inherited_fds[0]))) { + sd_inherited_fds[sd_inherited_fd_count].fd = fd; + sd_inherited_fds[sd_inherited_fd_count].conn_type = conn_type; + sd_inherited_fd_count++; + } else { + serverLog(LL_WARNING, + "Systemd socket activation: fd %d doesn't match any configured listener, closing.", fd); + close(fd); + } + } +} + +int adoptInheritedFdsForListener(connListener *listener, int sd_conn_type) { + int adopted = 0; + for (int i = 0; i < sd_inherited_fd_count; i++) { + if (sd_inherited_fds[i].conn_type != sd_conn_type) continue; + int fd = sd_inherited_fds[i].fd; + anetNonBlock(NULL, fd); + anetCloexec(fd); + listener->fd[listener->count++] = fd; + adopted++; + } + if (adopted > 0) { + listener->inherited = 1; + if (sd_conn_type == CONN_TYPE_UNIX) + serverLog(LL_NOTICE, "Systemd socket activation: adopted %d fd(s) for unix listener on path %s", adopted, + server.unixsocket ? server.unixsocket : ""); + else if (sd_conn_type == CONN_TYPE_CLUSTER_BUS) + serverLog(LL_NOTICE, "Systemd socket activation: adopted %d fd(s) for cluster bus listener on port %d", + adopted, listener->port); + else + serverLog(LL_NOTICE, "Systemd socket activation: adopted %d fd(s) for %s listener on port %d", adopted, + getConnectionTypeName(sd_conn_type), listener->port); + } + return adopted; +} +#else +void inheritSystemdListenFds(void) { +} +int adoptInheritedFdsForListener(connListener *listener, int sd_conn_type) { + UNUSED(listener); + UNUSED(sd_conn_type); + return 0; +} +#endif /* HAVE_LIBSYSTEMD */ + /* Attempt to set up upstart supervision. Returns 1 if successful. */ static int serverSupervisedUpstart(void) { const char *upstart_job = getenv("UPSTART_JOB"); @@ -7658,6 +7788,8 @@ __attribute__((weak)) int main(int argc, char **argv) { /* Daemonize if needed */ server.supervised = serverIsSupervised(server.supervised_mode); + /* Must be called after config is loaded and before initListeners(). */ + inheritSystemdListenFds(); int background = server.daemonize && !server.supervised; if (background) { /* We need to reset server.pid after daemonize(), otherwise the diff --git a/src/server.h b/src/server.h index d0079f7dae5..652453bba36 100644 --- a/src/server.h +++ b/src/server.h @@ -2836,6 +2836,11 @@ long long serverPopcount(void *s, long count); int serverSetProcTitle(char *title); int validateProcTitleTemplate(const char *templ); int serverCommunicateSystemd(const char *sd_notify_msg); +void inheritSystemdListenFds(void); +/* Sentinel for the cluster bus in adoptInheritedFdsForListener(), since the + * cluster bus has no ConnectionType and isn't in server.listeners[]. */ +#define CONN_TYPE_CLUSTER_BUS CONN_TYPE_MAX +int adoptInheritedFdsForListener(connListener *listener, int sd_conn_type); void serverSetCpuAffinity(const char *cpulist); void dictVanillaFree(void *val); diff --git a/tests/unit/socket-activation.tcl b/tests/unit/socket-activation.tcl new file mode 100644 index 00000000000..a6af1525798 --- /dev/null +++ b/tests/unit/socket-activation.tcl @@ -0,0 +1,239 @@ +# Tests for systemd socket activation. +# +# Simulates socket activation using systemd-socket-activate (part of the +# systemd package). The tests are skipped automatically when either: +# - systemd-socket-activate is not in PATH, or +# - systemd-socket-activate does not support --now (added in systemd 258), or +# - valkey-server was not compiled with libsystemd (HAVE_LIBSYSTEMD). + +if {[auto_execok systemd-socket-activate] eq {}} { + return +} + +# --now is required so systemd-socket-activate execs the service immediately +# rather than waiting for a connection. +if {[catch {exec systemd-socket-activate --help 2>@1} help_out] || + ![string match "*--now*" $help_out]} { + return +} + +if {[catch {exec ldd $::VALKEY_SERVER_BIN} ldd_out] || + ![string match "*libsystemd*" $ldd_out]} { + return +} + +proc sa_write_config {cfgfile port datadir logfile {unixsock {}}} { + set f [open $cfgfile w] + puts $f "port $port" + puts $f "bind 127.0.0.1" + puts $f "daemonize no" + puts $f "loglevel verbose" + puts $f "logfile $logfile" + puts $f "dir $datadir" + puts $f "protected-mode no" + if {$unixsock ne {}} { + puts $f "unixsocket $unixsock" + } + close $f +} + +proc sa_start {cfgfile port {unixsock {}}} { + set args [list systemd-socket-activate --now -l "127.0.0.1:$port"] + if {$unixsock ne {}} { + lappend args -l $unixsock + } + lappend args -- $::VALKEY_SERVER_BIN $cfgfile + set fd [open "|$args" r] + fconfigure $fd -blocking 0 + return $fd +} + +proc sa_ping_tcp {port} { + catch {exec $::VALKEY_CLI_BIN -p $port PING} r + return $r +} + +proc sa_ping_unix {sock} { + catch {exec $::VALKEY_CLI_BIN -s $sock PING} r + return $r +} + +proc sa_find_free_port {} { + set s [socket -server {} 0] + set port [lindex [fconfigure $s -sockname] 2] + close $s + return $port +} + +proc sa_read_file {path} { + if {[catch {open $path r} f]} { return "" } + set data [read $f] + close $f + return $data +} + +# --------------------------------------------------------------------------- +# Test 1: TCP socket activation +# --------------------------------------------------------------------------- +test {Socket activation: TCP listener adopted from systemd} { + set port [sa_find_free_port] + set dir [file normalize [tmpdir socket-activation-tcp]] + set logfile [file join $dir server.log] + set cfgfile [file join $dir valkey.conf] + + sa_write_config $cfgfile $port $dir $logfile + set fd [sa_start $cfgfile $port] + + wait_for_condition 50 100 { + [sa_ping_tcp $port] eq "PONG" + } else { + fail "valkey-server did not respond to PING on port $port" + } + + set result [sa_ping_tcp $port] + + catch {exec $::VALKEY_CLI_BIN -p $port SHUTDOWN NOSAVE} _ + after 500 + catch {close $fd} _ + + set adopted [string match "*Systemd socket activation: adopted*" [sa_read_file $logfile]] + + list $result $adopted +} {PONG 1} + +# --------------------------------------------------------------------------- +# Test 2: Unix socket activation + systemd-owned path must survive shutdown +# --------------------------------------------------------------------------- +test {Socket activation: unix socket adopted and not unlinked on shutdown} { + set port [sa_find_free_port] + set dir [file normalize [tmpdir socket-activation-unix]] + # sockaddr_un.sun_path is limited to 108 bytes; an absolute path under + # tests/tmp/... is typically too long, so use a short fixed-root path. + set sock "/tmp/valkey-sa-[pid]-[clock microseconds].sock" + set logfile [file join $dir server.log] + set cfgfile [file join $dir valkey.conf] + + sa_write_config $cfgfile $port $dir $logfile $sock + + set fd [sa_start $cfgfile $port $sock] + + wait_for_condition 50 100 { + [sa_ping_unix $sock] eq "PONG" + } else { + fail "valkey-server did not respond to PING on unix socket $sock" + } + + set ping_result [sa_ping_unix $sock] + + catch {exec $::VALKEY_CLI_BIN -s $sock SHUTDOWN NOSAVE} _ + after 500 + + # Systemd owns the unix socket; valkey must not unlink it on shutdown. + set sock_exists [file exists $sock] + + catch {close $fd} _ + catch {file delete -force $sock} _ + + list $ping_result $sock_exists +} {PONG 1} + +# --------------------------------------------------------------------------- +# Test 3: Bind-address mismatch — inherited fd bound to all interfaces is +# rejected when the server is configured with bind 127.0.0.1 only. +# --------------------------------------------------------------------------- +test {Socket activation: inherited fd bound to 0.0.0.0 is rejected when bind=127.0.0.1} { + set port [sa_find_free_port] + set dir [file normalize [tmpdir socket-activation-bindmismatch]] + set logfile [file join $dir server.log] + set cfgfile [file join $dir valkey.conf] + + sa_write_config $cfgfile $port $dir $logfile + + # systemd passes 0.0.0.0:$port (wildcard), but valkey config binds only 127.0.0.1. + # Expect valkey to close the mismatched fd and self-bind on 127.0.0.1:$port. + set args [list systemd-socket-activate --now -l "0.0.0.0:$port" -- $::VALKEY_SERVER_BIN $cfgfile] + set fd [open "|$args" r] + fconfigure $fd -blocking 0 + + wait_for_condition 50 100 { + [sa_ping_tcp $port] eq "PONG" + } else { + fail "valkey-server did not respond to PING on port $port" + } + + set result [sa_ping_tcp $port] + + catch {exec $::VALKEY_CLI_BIN -p $port SHUTDOWN NOSAVE} _ + after 500 + catch {close $fd} _ + + set log [sa_read_file $logfile] + set rejected [string match "*doesn't match any configured listener*" $log] + set adopted [string match "*Systemd socket activation: adopted*" $log] + + list $result $rejected $adopted +} {PONG 1 0} + +# --------------------------------------------------------------------------- +# Test 4: Runtime CONFIG SET port is refused on an inherited TCP listener. +# --------------------------------------------------------------------------- +test {Socket activation: CONFIG SET port is refused on inherited listener} { + set port [sa_find_free_port] + set newport [sa_find_free_port] + set dir [file normalize [tmpdir socket-activation-configset]] + set logfile [file join $dir server.log] + set cfgfile [file join $dir valkey.conf] + + sa_write_config $cfgfile $port $dir $logfile + set fd [sa_start $cfgfile $port] + + wait_for_condition 50 100 { + [sa_ping_tcp $port] eq "PONG" + } else { + fail "valkey-server did not respond to PING on port $port" + } + + # valkey-cli prints the server error to stdout and exits 0; capture it. + catch {exec $::VALKEY_CLI_BIN -p $port CONFIG SET port $newport} cli_out + set cli_error [string match "*CONFIG SET failed*" $cli_out] + + catch {exec $::VALKEY_CLI_BIN -p $port SHUTDOWN NOSAVE} _ + after 500 + catch {close $fd} _ + + set log [sa_read_file $logfile] + set log_has_warning [string match "*Cannot reconfigure*inherited from systemd*" $log] + + list $cli_error $log_has_warning +} {1 1} + +# --------------------------------------------------------------------------- +# Test 5: Without socket activation the server binds normally +# --------------------------------------------------------------------------- +test {Socket activation: server self-binds when no inherited fds} { + set port [sa_find_free_port] + set dir [file normalize [tmpdir socket-activation-none]] + set logfile [file join $dir server.log] + set cfgfile [file join $dir valkey.conf] + + sa_write_config $cfgfile $port $dir $logfile + + set fd [open "|[list $::VALKEY_SERVER_BIN $cfgfile]" r] + fconfigure $fd -blocking 0 + + wait_for_condition 50 100 { + [sa_ping_tcp $port] eq "PONG" + } else { + fail "valkey-server did not respond to PING on port $port" + } + + set result [sa_ping_tcp $port] + + catch {exec $::VALKEY_CLI_BIN -p $port SHUTDOWN NOSAVE} _ + after 500 + catch {close $fd} _ + + set activated [string match "*Systemd socket activation*" [sa_read_file $logfile]] + + list $result $activated +} {PONG 0} diff --git a/utils/systemd-valkey_server.service b/utils/systemd-valkey_server.service index 08421051863..cbf50e0cf1a 100644 --- a/utils/systemd-valkey_server.service +++ b/utils/systemd-valkey_server.service @@ -12,6 +12,18 @@ # this example service unit file, but you are highly encouraged to set them to # fit your needs. # +# Socket activation (optional): if you want clients to survive a valkey restart +# without receiving ECONNREFUSED, enable systemd socket activation by also +# enabling the companion valkey-server.socket unit: +# +# systemctl enable --now valkey-server.socket +# systemctl enable valkey-server.service +# +# When socket activation is in use, valkey-server automatically inherits the +# listening sockets from systemd (both TCP and Unix) without any extra +# configuration. The server must be compiled with libsystemd support +# (USE_SYSTEMD=yes, the default when libsystemd-dev is installed). +# # Please refer to systemd.unit(5), systemd.service(5), and systemd.exec(5) for # more information. diff --git a/utils/systemd-valkey_server.socket b/utils/systemd-valkey_server.socket new file mode 100644 index 00000000000..116c402c4fd --- /dev/null +++ b/utils/systemd-valkey_server.socket @@ -0,0 +1,32 @@ +# systemd socket unit for valkey-server socket activation +# +# Socket activation allows valkey-server to be restarted without causing +# ECONNREFUSED for clients: systemd holds the listening sockets open across +# restarts, so in-flight connections simply wait in the kernel backlog while +# the new process starts up. +# +# Usage: +# systemctl enable --now valkey-server.socket +# systemctl enable valkey-server.service +# +# Valkey auto-detects the inherited sockets by matching each fd against the +# configured port / unix socket path. No special configuration in valkey.conf +# is required. The server must be compiled with libsystemd (USE_SYSTEMD=yes, +# the default when libsystemd-dev is installed). +# +# Adapt ListenStream= to match your valkey.conf (port and/or unixsocket). +# Add or remove lines as needed. +# +# Please refer to systemd.socket(5) for more information. + +[Unit] +Description=Valkey data structure server socket + +[Socket] +# TCP port (matches 'port' in valkey.conf, default 6379) +ListenStream=6379 +# Unix domain socket (uncomment and set to match 'unixsocket' in valkey.conf) +#ListenStream=/run/valkey/valkey.sock + +[Install] +WantedBy=sockets.target diff --git a/valkey.conf b/valkey.conf index 6b2d26c3884..efe25f6ce0b 100644 --- a/valkey.conf +++ b/valkey.conf @@ -413,6 +413,13 @@ daemonize no # # supervised auto +# Systemd socket activation: when the server is compiled with libsystemd +# (USE_SYSTEMD=yes, auto-detected by default) and started via a systemd +# .socket unit, it automatically inherits the listening sockets from systemd. +# No configuration is needed here; the server matches each inherited fd to the +# configured port/unixsocket values below and skips bind()/listen() for those. +# See utils/systemd-valkey_server.socket for an example .socket unit. + # If a pid file is specified, the server writes it where specified at startup # and removes it at exit. #