Graceful Shutdown Executor Context Manager in Python
A context manager that starts a background thread and ensures it stops gracefully on exit, handling timeouts and exceptions.
Python code
42 linesimport signal
import threading
import time
from contextlib import contextmanager
@contextmanager
def graceful_shutdown_executor(timeout=5.0):
"""Context manager that runs a task and gracefully stops it on timeout or exception."""
stop_event = threading.Event()
def task():
print("Task started")
while not stop_event.is_set():
time.sleep(0.1)
print("Task stopped gracefully")
thread = threading.Thread(target=task, daemon=True)
thread.start()
try:
yield stop_event
except BaseException:
print("Exception caught, initiating shutdown...")
stop_event.set()
thread.join(timeout=timeout)
if thread.is_alive():
print("Task still running after timeout")
raise
else:
print("Context exited normally, shutting down...")
stop_event.set()
thread.join(timeout=timeout)
if thread.is_alive():
print("Task still running after timeout")
if __name__ == "__main__":
with graceful_shutdown_executor(timeout=2.0) as stop:
print("Inside context, doing work...")
time.sleep(1.0)
stop.set() # Early stop example
Output
Task started
Inside context, doing work...
Context exited normally, shutting down...
Task stopped gracefully
How it works
This context manager uses threading.Event to coordinate shutdown. The yield exposes the event to the caller, allowing manual early termination. On normal exit or exception, stop_event.set() signals the thread to break its loop, and join(timeout) blocks until the thread finishes or the timeout expires. The finally-like structure ensures cleanup even if the context body raises.
Common mistakes
- Forgetting to set the stop event before joining, causing deadlock
- Not handling `thread.join(timeout)` return value to detect timeout
- Making the thread non-daemon, which can block program exit if not joined
Variations
- Use `concurrent.futures.ThreadPoolExecutor` with `shutdown(wait=True)` for simpler pooling
- Use `asyncio` with `asyncio.Event` and async context manager for non-blocking tasks
Real-world use cases
- Shutting down a background polling loop when a service receives a termination signal.
- Cleaning up worker threads in a batch job after processing is complete or on error.
- Managing a long-running background task in a CLI tool that must exit gracefully on Ctrl+C.
Sponsored
More from Concurrency & performance
- Benchmark list.append vs deque.append in Python medium
- Build a Python Performance Profiler That Generates Readable Reports medium
- How to Build a Producer-Consumer Pattern with asyncio.Queue in Python medium
- How to Cancel an asyncio Task with Graceful Cleanup in Python medium
- How to Convert Data in Parallel with ThreadPoolExecutor in Python easy
- How to Demonstrate the GIL with Python Threads vs Processes medium
Keep learning
Related tutorials and quizzes for this topic.