Skip to content

Commit 320207b

Browse files
committed
fix: Keep output flushing from blocking forced exit
1 parent afba537 commit 320207b

2 files changed

Lines changed: 78 additions & 4 deletions

File tree

‎cloudquery/sdk/serve/plugin.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
_IS_WINDOWS = sys.platform == "win32"
3030
_STOP_GRACE_SECONDS = 5
3131
_FORCED_EXIT_DELAY_SECONDS = 5
32+
_FORCED_EXIT_FLUSH_TIMEOUT_SECONDS = 1
3233

3334
try:
3435
import colorama
@@ -432,6 +433,15 @@ def stop(self):
432433

433434

434435
def _exit_now():
435-
sys.stdout.flush()
436-
sys.stderr.flush()
436+
flusher = threading.Thread(target=_flush_output_streams, daemon=True)
437+
flusher.start()
438+
flusher.join(_FORCED_EXIT_FLUSH_TIMEOUT_SECONDS)
437439
os._exit(0)
440+
441+
442+
def _flush_output_streams():
443+
for stream in (sys.stdout, sys.stderr):
444+
try:
445+
stream.flush()
446+
except Exception:
447+
pass

‎tests/serve/signals.py‎

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,32 @@ def init(self, spec, no_connection=False):
2929
serve.PluginCommand(BlockedInitPlugin("test", "v0.0.1")).run(["serve", "--address", sys.argv[1]])
3030
"""
3131

32+
SERVE_PLUGIN_WITH_INIT_FLOODING_STDOUT = """
33+
import sys
34+
from cloudquery.sdk import serve
35+
from cloudquery.sdk.plugin import Plugin
36+
37+
class FloodingInitPlugin(Plugin):
38+
def init(self, spec, no_connection=False):
39+
while True:
40+
sys.stdout.write("x" * 1024)
41+
42+
serve.PluginCommand(FloodingInitPlugin("test", "v0.0.1")).run(["serve", "--address", sys.argv[1]])
43+
"""
44+
45+
SERVE_PLUGIN_WITH_INIT_BLOCKED_ON_BUFFERED_STDOUT = """
46+
import sys, threading
47+
from cloudquery.sdk import serve
48+
from cloudquery.sdk.plugin import Plugin
49+
50+
class BufferedInitPlugin(Plugin):
51+
def init(self, spec, no_connection=False):
52+
sys.stdout.write("buffered and never flushed")
53+
threading.Event().wait()
54+
55+
serve.PluginCommand(BufferedInitPlugin("test", "v0.0.1")).run(["serve", "--address", sys.argv[1]])
56+
"""
57+
3258
SIGTERM_DURING_LOCK_RESTORE = """
3359
import os, signal, sys, threading
3460
from cloudquery.sdk import serve
@@ -94,9 +120,9 @@ def free_address():
94120
return "127.0.0.1:%d" % sock.getsockname()[1]
95121

96122

97-
def start_plugin_with_blocked_init(tmp_path):
123+
def start_plugin_with_blocked_init(tmp_path, source=SERVE_PLUGIN_WITH_BLOCKED_INIT):
98124
address = free_address()
99-
process = start_plugin(tmp_path, SERVE_PLUGIN_WITH_BLOCKED_INIT, address)
125+
process = start_plugin(tmp_path, source, address)
100126
wait_until_serving(process)
101127
channel = grpc.insecure_channel(address)
102128
init_call = plugin_pb2_grpc.PluginStub(channel).Init.future(
@@ -130,3 +156,41 @@ def test_second_signal_with_blocked_handler_exits_zero_immediately(tmp_path):
130156
init_call.cancel()
131157
channel.close()
132158
process.kill()
159+
160+
161+
def send_termination_signals(process, second_signal):
162+
process.send_signal(signal.SIGTERM)
163+
if second_signal:
164+
time.sleep(0.5)
165+
process.send_signal(signal.SIGINT)
166+
return 3
167+
return 20
168+
169+
170+
@pytest.mark.parametrize("second_signal", [False, True])
171+
def test_forced_exit_with_full_stdout_pipe_exits_zero(tmp_path, second_signal):
172+
process, channel, init_call = start_plugin_with_blocked_init(
173+
tmp_path, SERVE_PLUGIN_WITH_INIT_FLOODING_STDOUT
174+
)
175+
try:
176+
timeout = send_termination_signals(process, second_signal)
177+
assert process.wait(timeout=timeout) == 0
178+
finally:
179+
init_call.cancel()
180+
channel.close()
181+
process.kill()
182+
183+
184+
@pytest.mark.parametrize("second_signal", [False, True])
185+
def test_forced_exit_with_closed_stdout_pipe_exits_zero(tmp_path, second_signal):
186+
process, channel, init_call = start_plugin_with_blocked_init(
187+
tmp_path, SERVE_PLUGIN_WITH_INIT_BLOCKED_ON_BUFFERED_STDOUT
188+
)
189+
try:
190+
process.stdout.close()
191+
timeout = send_termination_signals(process, second_signal)
192+
assert process.wait(timeout=timeout) == 0
193+
finally:
194+
init_call.cancel()
195+
channel.close()
196+
process.kill()

0 commit comments

Comments
 (0)