Skip to content

Commit ee933f2

Browse files
committed
fix(asgi): quick shutdown on SIGINT/SIGQUIT, graceful on SIGTERM
- SIGINT/SIGQUIT triggers immediate shutdown, skipping connection waits - SIGTERM triggers graceful shutdown, waiting for connections - Arbiter forwards SIGQUIT to workers if received during graceful shutdown - Workers have 2s to exit cleanly after quick shutdown before SIGKILL
1 parent 98ef198 commit ee933f2

File tree

2 files changed

+52
-15
lines changed

2 files changed

+52
-15
lines changed

gunicorn/arbiter.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,22 @@ def stop(self, graceful=True):
414414
# instruct the workers to exit
415415
self.kill_workers(sig)
416416
# wait until the graceful timeout
417+
quick_shutdown = not graceful
417418
while (self.WORKERS or self.dirty_arbiter_pid) and time.time() < limit:
419+
# Check for SIGINT/SIGQUIT to trigger quick shutdown
420+
if not quick_shutdown:
421+
try:
422+
pending_sig = self.SIG_QUEUE.get_nowait()
423+
if pending_sig in (signal.SIGINT, signal.SIGQUIT):
424+
self.log.info("Quick shutdown requested")
425+
quick_shutdown = True
426+
self.kill_workers(signal.SIGQUIT)
427+
if self.dirty_arbiter_pid:
428+
self.kill_dirty_arbiter(signal.SIGQUIT)
429+
# Give workers a short time to exit cleanly
430+
limit = time.time() + 2.0
431+
except Exception:
432+
pass
418433
self.reap_workers()
419434
self.reap_dirty_arbiter()
420435
time.sleep(0.1)

gunicorn/workers/gasgi.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def __init__(self, *args, **kwargs):
3636
self.nr_conns = 0
3737
self.lifespan = None
3838
self.state = {} # Shared state for lifespan
39+
self._quick_shutdown = False # True for SIGINT/SIGQUIT (immediate), False for SIGTERM (graceful)
3940

4041
@classmethod
4142
def check_config(cls, cfg, log):
@@ -122,7 +123,11 @@ def init_signals(self):
122123
self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort_signal)
123124

124125
def handle_quit_signal(self):
125-
"""Handle SIGQUIT - immediate shutdown."""
126+
"""Handle SIGQUIT/SIGINT - immediate shutdown."""
127+
self._quick_shutdown = True
128+
if not self.alive:
129+
# Already shutting down (SIGTERM was sent) - wake up the loop
130+
return
126131
self.alive = False
127132
self.cfg.worker_int(self)
128133

@@ -221,23 +226,32 @@ async def _shutdown(self):
221226
for server in self.servers:
222227
server.close()
223228

224-
# Wait for servers to close
225-
for server in self.servers:
226-
await server.wait_closed()
227-
228-
# Wait for in-flight connections (with timeout)
229-
graceful_timeout = self.cfg.graceful_timeout
230-
if self.nr_conns > 0:
229+
# Wait for servers to close (skip on quick shutdown)
230+
if not self._quick_shutdown:
231+
for server in self.servers:
232+
if self._quick_shutdown:
233+
break
234+
try:
235+
await asyncio.wait_for(server.wait_closed(), timeout=0.5)
236+
except asyncio.TimeoutError:
237+
pass # Check _quick_shutdown on next iteration
238+
239+
# Wait for in-flight connections (skip on quick shutdown)
240+
if self.nr_conns > 0 and not self._quick_shutdown:
241+
graceful_timeout = self.cfg.graceful_timeout
231242
self.log.info("Waiting for %d connections to finish...", self.nr_conns)
232243
deadline = self.loop.time() + graceful_timeout
233244
while self.nr_conns > 0 and self.loop.time() < deadline:
245+
if self._quick_shutdown:
246+
self.log.info("Quick shutdown requested")
247+
break
234248
await asyncio.sleep(0.1)
235249

236250
if self.nr_conns > 0:
237-
self.log.warning("Closing %d connections after timeout", self.nr_conns)
251+
self.log.warning("Forcing close of %d connections", self.nr_conns)
238252

239-
# Run lifespan shutdown
240-
if self.lifespan:
253+
# Run lifespan shutdown (skip on quick shutdown)
254+
if self.lifespan and not self._quick_shutdown:
241255
try:
242256
await self.lifespan.shutdown()
243257
except Exception as e:
@@ -263,11 +277,19 @@ def _cleanup(self):
263277
for task in pending:
264278
task.cancel()
265279

266-
# Run loop until all tasks are cancelled
280+
# Run loop until all tasks are cancelled (with timeout on quick exit)
267281
if pending:
268-
self.loop.run_until_complete(
269-
asyncio.gather(*pending, return_exceptions=True)
270-
)
282+
gather = asyncio.gather(*pending, return_exceptions=True)
283+
if self._quick_shutdown:
284+
# Quick exit - don't wait long for tasks to cancel
285+
try:
286+
self.loop.run_until_complete(
287+
asyncio.wait_for(gather, timeout=1.0)
288+
)
289+
except asyncio.TimeoutError:
290+
self.log.debug("Timeout waiting for tasks to cancel")
291+
else:
292+
self.loop.run_until_complete(gather)
271293

272294
self.loop.close()
273295
except Exception as e:

0 commit comments

Comments
 (0)