Skip to content

Commit dd5cfcf

Browse files
committed
fix: Exit 0 on SIGTERM while serving
1 parent 5391577 commit dd5cfcf

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

‎cloudquery/sdk/serve/plugin.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import logging
55
import os
66
import shutil
7+
import signal
78
import subprocess
89
import tarfile
10+
import threading
911
from concurrent import futures
1012
from pathlib import Path
1113

@@ -401,7 +403,17 @@ def _serve(self, args):
401403
self._server.add_insecure_port(args.address)
402404
logger.info("Starting server", address=args.address)
403405
self._server.start()
406+
self._stop_on_termination_signals()
404407
self._server.wait_for_termination()
405408

409+
def _stop_on_termination_signals(self):
410+
if threading.current_thread() is not threading.main_thread():
411+
return
412+
for signum in (signal.SIGTERM, signal.SIGINT):
413+
signal.signal(signum, self._stop_in_background)
414+
415+
def _stop_in_background(self, signum, frame):
416+
threading.Thread(target=self.stop, daemon=True).start()
417+
406418
def stop(self):
407419
self._server.stop(5)

‎tests/serve/signals.py‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import signal
2+
import subprocess
3+
import sys
4+
import textwrap
5+
import time
6+
7+
import pytest
8+
9+
SERVE_PLUGIN = """
10+
from cloudquery.sdk import serve
11+
from cloudquery.sdk.plugin import Plugin
12+
13+
serve.PluginCommand(Plugin("test", "v0.0.1")).run(["serve", "--address", "127.0.0.1:0"])
14+
"""
15+
16+
SIGTERM_DURING_LOCK_RESTORE = """
17+
import os, signal, sys, threading
18+
from cloudquery.sdk import serve
19+
from cloudquery.sdk.plugin import Plugin
20+
21+
def signal_handler(sig, frame):
22+
sys.exit(0)
23+
24+
signal.signal(signal.SIGTERM, signal_handler)
25+
26+
_orig = threading.Condition._acquire_restore
27+
_fired = threading.Event()
28+
29+
def _acquire_restore(self, x):
30+
if threading.current_thread() is threading.main_thread() and not _fired.is_set():
31+
_fired.set()
32+
os.kill(os.getpid(), signal.SIGTERM)
33+
return _orig(self, x)
34+
35+
threading.Timer(1.0, lambda: setattr(threading.Condition, "_acquire_restore", _acquire_restore)).start()
36+
37+
serve.PluginCommand(Plugin("test", "v0.0.1")).run(["serve", "--address", "127.0.0.1:0"])
38+
"""
39+
40+
41+
def start_plugin(tmp_path, source):
42+
script = tmp_path / "plugin_main.py"
43+
script.write_text(textwrap.dedent(source))
44+
return subprocess.Popen(
45+
[sys.executable, str(script)],
46+
stdout=subprocess.PIPE,
47+
stderr=subprocess.STDOUT,
48+
text=True,
49+
)
50+
51+
52+
def wait_until_serving(process):
53+
for line in process.stdout:
54+
if "Starting server" in line:
55+
time.sleep(0.5)
56+
return
57+
pytest.fail("plugin exited before it started serving")
58+
59+
60+
def test_sigterm_during_lock_restore_exits_zero(tmp_path):
61+
process = start_plugin(tmp_path, SIGTERM_DURING_LOCK_RESTORE)
62+
output, _ = process.communicate(timeout=30)
63+
assert process.returncode == 0, output
64+
65+
66+
@pytest.mark.parametrize("signum", [signal.SIGTERM, signal.SIGINT])
67+
def test_signal_while_serving_exits_zero(tmp_path, signum):
68+
process = start_plugin(tmp_path, SERVE_PLUGIN)
69+
wait_until_serving(process)
70+
process.send_signal(signum)
71+
output, _ = process.communicate(timeout=30)
72+
assert process.returncode == 0, output

0 commit comments

Comments
 (0)