From 33601ea2f057b6209de268755fb89c013170ab75 Mon Sep 17 00:00:00 2001 From: Julien Letessier Date: Thu, 15 Jan 2026 12:33:33 +0100 Subject: [PATCH] Make signal handlers async-signal-safe --- src/Proxy.cpp | 30 +++++++------- test/run.sh | 1 + test/signal_handling.py | 90 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 test/signal_handling.py diff --git a/src/Proxy.cpp b/src/Proxy.cpp index 3ee0842..ba1968e 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -20,26 +20,23 @@ #include "RequestParser.h" #include "Backtrace.h" -static bool Running = false; -static bool Abort = false; -static bool Stop = false; +static volatile sig_atomic_t Running = 0; +static volatile sig_atomic_t AbortSignal = 0; +static volatile sig_atomic_t StopSignal = 0; static void abortHandler(int sig) { - if (!Abort) { - traceInfo(sig); - } - Abort = true; - if (!Running) { - abort(); + // Signal handlers must be async-signal-safe: only set flags here. + if (!AbortSignal) { + AbortSignal = sig; } } static void stopHandler(int sig) { - Stop = true; - if (!Running) { - abort(); + // Signal handlers must be async-signal-safe: only set flags here. + if (!StopSignal) { + StopSignal = sig; } } @@ -141,18 +138,21 @@ int Proxy::run() logNotice("predixy running with Name:%s Workers:%d", mConf->name(), (int)mHandlers.size()); + Running = 1; std::vector> tasks; for (auto h : mHandlers) { std::shared_ptr t(new std::thread([=](){h->run();})); tasks.push_back(t); } - Running = true; bool stop = false; while (!stop) { - if (Abort) { + if (AbortSignal) { + int sig = AbortSignal; + AbortSignal = 0; + traceInfo(sig); stop = true; abort(); - } else if (Stop) { + } else if (StopSignal) { fprintf(stderr, "predixy will quit ASAP Bye!\n"); stop = true; } diff --git a/test/run.sh b/test/run.sh index b63a727..4525dd1 100755 --- a/test/run.sh +++ b/test/run.sh @@ -144,6 +144,7 @@ TESTS=( "test/request_parser_error_log.py" "test/response_parser_error_log.py" "test/request_parser_long_command.py" + "test/signal_handling.py" "test/pubsub_long_name.py" "test/pubsub_large_message.py" "test/transaction_forbid.py" diff --git a/test/signal_handling.py b/test/signal_handling.py new file mode 100644 index 0000000..b6c6e6c --- /dev/null +++ b/test/signal_handling.py @@ -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")