Make signal handlers async-signal-safe

This commit is contained in:
Julien Letessier 2026-01-15 12:33:33 +01:00
parent d4b10d5065
commit 33601ea2f0
3 changed files with 106 additions and 15 deletions

View File

@ -20,26 +20,23 @@
#include "RequestParser.h" #include "RequestParser.h"
#include "Backtrace.h" #include "Backtrace.h"
static bool Running = false; static volatile sig_atomic_t Running = 0;
static bool Abort = false; static volatile sig_atomic_t AbortSignal = 0;
static bool Stop = false; static volatile sig_atomic_t StopSignal = 0;
static void abortHandler(int sig) static void abortHandler(int sig)
{ {
if (!Abort) { // Signal handlers must be async-signal-safe: only set flags here.
traceInfo(sig); if (!AbortSignal) {
} AbortSignal = sig;
Abort = true;
if (!Running) {
abort();
} }
} }
static void stopHandler(int sig) static void stopHandler(int sig)
{ {
Stop = true; // Signal handlers must be async-signal-safe: only set flags here.
if (!Running) { if (!StopSignal) {
abort(); StopSignal = sig;
} }
} }
@ -141,18 +138,21 @@ int Proxy::run()
logNotice("predixy running with Name:%s Workers:%d", logNotice("predixy running with Name:%s Workers:%d",
mConf->name(), mConf->name(),
(int)mHandlers.size()); (int)mHandlers.size());
Running = 1;
std::vector<std::shared_ptr<std::thread>> tasks; std::vector<std::shared_ptr<std::thread>> tasks;
for (auto h : mHandlers) { for (auto h : mHandlers) {
std::shared_ptr<std::thread> t(new std::thread([=](){h->run();})); std::shared_ptr<std::thread> t(new std::thread([=](){h->run();}));
tasks.push_back(t); tasks.push_back(t);
} }
Running = true;
bool stop = false; bool stop = false;
while (!stop) { while (!stop) {
if (Abort) { if (AbortSignal) {
int sig = AbortSignal;
AbortSignal = 0;
traceInfo(sig);
stop = true; stop = true;
abort(); abort();
} else if (Stop) { } else if (StopSignal) {
fprintf(stderr, "predixy will quit ASAP Bye!\n"); fprintf(stderr, "predixy will quit ASAP Bye!\n");
stop = true; stop = true;
} }

View File

@ -144,6 +144,7 @@ TESTS=(
"test/request_parser_error_log.py" "test/request_parser_error_log.py"
"test/response_parser_error_log.py" "test/response_parser_error_log.py"
"test/request_parser_long_command.py" "test/request_parser_long_command.py"
"test/signal_handling.py"
"test/pubsub_long_name.py" "test/pubsub_long_name.py"
"test/pubsub_large_message.py" "test/pubsub_large_message.py"
"test/transaction_forbid.py" "test/transaction_forbid.py"

90
test/signal_handling.py Normal file
View File

@ -0,0 +1,90 @@
#!/usr/bin/env python3
#
# Start a temporary predixy instance and verify SIGTERM shuts it down cleanly.
#
import os
import signal
import socket
import subprocess
import tempfile
import time
from test_util import parse_args, exit_with_result
def wait_for_port(host, port, timeout=5.0):
deadline = time.time() + timeout
while time.time() < deadline:
try:
with socket.create_connection((host, port), timeout=0.5):
return True
except Exception:
time.sleep(0.05)
return False
def start_predixy(root, redis_port):
predixy_bin = os.path.join(root, "src", "predixy")
if not os.path.exists(predixy_bin):
raise RuntimeError("predixy binary not found")
listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_sock.bind(("127.0.0.1", 0))
listen_port = listen_sock.getsockname()[1]
listen_sock.close()
tmp_dir = tempfile.TemporaryDirectory()
conf_path = os.path.join(tmp_dir.name, "predixy_test.conf")
with open(conf_path, "w") as f:
f.write(
"Name PredixySignalTest\n"
f"Bind 127.0.0.1:{listen_port}\n"
"WorkerThreads 1\n"
"ClientTimeout 3\n"
"LogVerbSample 0\n"
"LogDebugSample 0\n"
"LogInfoSample 10000\n"
"LogNoticeSample 1\n"
"LogWarnSample 1\n"
"LogErrorSample 1\n"
"\n"
"StandaloneServerPool {\n"
" RefreshMethod fixed\n"
" Group test {\n"
f" + 127.0.0.1:{redis_port}\n"
" }\n"
"}\n"
)
proc = subprocess.Popen([predixy_bin, conf_path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
if not wait_for_port("127.0.0.1", listen_port, timeout=5.0):
proc.terminate()
tmp_dir.cleanup()
raise RuntimeError("predixy did not start")
return proc, tmp_dir
def run_test(project_root, redis_port):
proc, tmp_dir = start_predixy(project_root, redis_port)
try:
proc.send_signal(signal.SIGTERM)
try:
proc.wait(timeout=5.0)
except subprocess.TimeoutExpired:
print("FAIL: predixy did not exit after SIGTERM")
proc.kill()
return False
finally:
tmp_dir.cleanup()
return True
if __name__ == "__main__":
args = parse_args("Signal handling test", default_port=6380)
root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
success = run_test(root, args.port)
exit_with_result(success, "signal handling", "signal handling")