Skip to content

Commit 05b09ca

Browse files
committed
feat: graceful SIGTERM/SIGINT drain of workers (finish in-flight task before exit)
Worker loops ran `while True` and ignored the shutdown flag, and the threads are daemons — so on SIGTERM the process exited and killed them mid-task, orphaning the in-flight task (and losing its result if the task handler uploaded synchronously). Worker loops now run `while not self._shutdown_event.is_set()` and blpop with a 5s timeout so shutdown is noticed promptly. New ModelQ.shutdown() sets the event and joins the worker threads (up to a timeout), and the CLI calls it on SIGTERM/SIGINT so an in-flight task — and its upload — completes before the process exits.
1 parent 5199a36 commit 05b09ca

3 files changed

Lines changed: 26 additions & 4 deletions

File tree

modelq/app/base.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ def __init__(
152152
)
153153

154154
self.worker_threads = []
155+
# Set on SIGTERM/SIGINT so worker loops stop pulling and exit cleanly
156+
# after finishing their current task (see shutdown()).
157+
self._shutdown_event = threading.Event()
155158
if server_id is None:
156159
# Attempt to load the server_id from a local file:
157160
server_id = self._get_or_create_server_id_file()
@@ -662,6 +665,21 @@ def wrapper(*args, **kwargs):
662665
return wrapper
663666
return decorator
664667

668+
def shutdown(self, timeout: float = 300.0):
669+
"""Signal worker loops to stop after their current task, then wait.
670+
671+
Called on SIGTERM/SIGINT (see the CLI) so an in-flight task finishes —
672+
and, for synchronous task handlers, its upload completes — before the
673+
process exits, instead of daemon worker threads being killed mid-task
674+
(which orphans the task and loses its result). Waits up to `timeout`
675+
seconds in total for the workers to drain.
676+
"""
677+
self._shutdown_event.set()
678+
deadline = time.time() + timeout
679+
for thread in self.worker_threads:
680+
remaining = max(0.1, deadline - time.time())
681+
thread.join(timeout=remaining)
682+
665683
def start_workers(self, no_of_workers: int = 1):
666684
"""
667685
Starts worker threads to pop tasks from 'ml_tasks' and process them.
@@ -703,7 +721,7 @@ def start_workers(self, no_of_workers: int = 1):
703721
# 4) Worker threads
704722
def worker_loop(worker_id):
705723
self.check_middleware("after_worker_boot")
706-
while True:
724+
while not self._shutdown_event.is_set():
707725
try:
708726
# Check worker health before picking up tasks
709727
if not self.worker_healthy:
@@ -712,7 +730,7 @@ def worker_loop(worker_id):
712730
continue
713731

714732
self.update_server_status(f"worker_{worker_id}: idle")
715-
task_data = self.redis_client.blpop("ml_tasks") # blocks until a task is available
733+
task_data = self.redis_client.blpop("ml_tasks", timeout=5) # wake every 5s so shutdown is noticed promptly
716734
if not task_data:
717735
continue
718736

modelq/app/cli/main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ def run_workers(
9393
raise typer.Exit(1)
9494
finally:
9595
logger.info("Shutting down workers...")
96-
typer.echo("🛑 Shutting down workers...")
96+
typer.echo("🛑 Draining workers (finishing any in-flight task)...")
97+
try:
98+
app_instance.shutdown()
99+
except Exception as e:
100+
logger.warning(f"Error while draining workers: {e}")
97101
typer.echo("✅ Shutdown complete")
98102

99103
@app.command()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "modelq"
3-
version = "1.0.14"
3+
version = "1.0.15"
44
description = "Celery-like task queue for ML inference."
55
authors = ["Tanmaypatil123 <tanmay@modelslab.com>"]
66
readme = "README.md"

0 commit comments

Comments
 (0)