Add signal handling for graceful Docker shutdown#2
Conversation
Co-authored-by: rgregg <1435771+rgregg@users.noreply.github.com>
Co-authored-by: rgregg <1435771+rgregg@users.noreply.github.com>
Co-authored-by: rgregg <1435771+rgregg@users.noreply.github.com>
Co-authored-by: rgregg <1435771+rgregg@users.noreply.github.com>
|
@copilot can you look at why the pull_request validation failed. |
There was a problem hiding this comment.
Pull request overview
This pull request adds graceful signal handling for Docker container shutdown to resolve slow container termination issues. Previously, the Docker container took 10+ seconds to stop because the application didn't handle SIGTERM signals, forcing Docker to wait for the timeout before sending SIGKILL.
Changes:
- Added
run_with_shutdown()function incli.pythat registers SIGTERM/SIGINT signal handlers using asyncio's event loop - Added comprehensive test coverage for signal handling in
test_cli.py - Replaced direct
asyncio.run(proxy.run())call withasyncio.run(run_with_shutdown(proxy))and removed obsolete KeyboardInterrupt handling
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/meshcore_proxy/cli.py | Implements graceful shutdown mechanism with SIGTERM/SIGINT signal handlers and updates main() to use the new wrapper |
| tests/test_cli.py | Adds new test file with tests for SIGTERM and SIGINT signal handling with 5-second shutdown timeout verification |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for sig in (signal.SIGTERM, signal.SIGINT): | ||
| loop.add_signal_handler(sig, functools.partial(signal_handler, sig)) | ||
|
|
||
| # Create the proxy task | ||
| proxy_task = asyncio.create_task(proxy.run()) | ||
|
|
||
| # Wait for either the proxy to complete or a shutdown signal | ||
| shutdown_task = asyncio.create_task(shutdown_event.wait()) | ||
| done, pending = await asyncio.wait( | ||
| [proxy_task, shutdown_task], | ||
| return_when=asyncio.FIRST_COMPLETED, | ||
| ) | ||
|
|
||
| # If shutdown was signaled, cancel the proxy task | ||
| if shutdown_task in done: | ||
| proxy_task.cancel() | ||
| try: | ||
| await proxy_task | ||
| except asyncio.CancelledError: | ||
| pass | ||
|
|
||
| # Cancel any remaining tasks | ||
| for task in pending: | ||
| task.cancel() | ||
| try: | ||
| await task | ||
| except asyncio.CancelledError: | ||
| pass |
There was a problem hiding this comment.
Signal handlers are registered but never removed. Consider adding a finally block to remove the signal handlers using loop.remove_signal_handler() to ensure proper cleanup, especially if this function is called multiple times or if the function exits early due to an exception.
| for sig in (signal.SIGTERM, signal.SIGINT): | |
| loop.add_signal_handler(sig, functools.partial(signal_handler, sig)) | |
| # Create the proxy task | |
| proxy_task = asyncio.create_task(proxy.run()) | |
| # Wait for either the proxy to complete or a shutdown signal | |
| shutdown_task = asyncio.create_task(shutdown_event.wait()) | |
| done, pending = await asyncio.wait( | |
| [proxy_task, shutdown_task], | |
| return_when=asyncio.FIRST_COMPLETED, | |
| ) | |
| # If shutdown was signaled, cancel the proxy task | |
| if shutdown_task in done: | |
| proxy_task.cancel() | |
| try: | |
| await proxy_task | |
| except asyncio.CancelledError: | |
| pass | |
| # Cancel any remaining tasks | |
| for task in pending: | |
| task.cancel() | |
| try: | |
| await task | |
| except asyncio.CancelledError: | |
| pass | |
| signals = (signal.SIGTERM, signal.SIGINT) | |
| for sig in signals: | |
| loop.add_signal_handler(sig, functools.partial(signal_handler, sig)) | |
| try: | |
| # Create the proxy task | |
| proxy_task = asyncio.create_task(proxy.run()) | |
| # Wait for either the proxy to complete or a shutdown signal | |
| shutdown_task = asyncio.create_task(shutdown_event.wait()) | |
| done, pending = await asyncio.wait( | |
| [proxy_task, shutdown_task], | |
| return_when=asyncio.FIRST_COMPLETED, | |
| ) | |
| # If shutdown was signaled, cancel the proxy task | |
| if shutdown_task in done: | |
| proxy_task.cancel() | |
| try: | |
| await proxy_task | |
| except asyncio.CancelledError: | |
| pass | |
| # Cancel any remaining tasks | |
| for task in pending: | |
| task.cancel() | |
| try: | |
| await task | |
| except asyncio.CancelledError: | |
| pass | |
| finally: | |
| # Ensure signal handlers are removed even if an error occurs | |
| for sig in signals: | |
| try: | |
| loop.remove_signal_handler(sig) | |
| except RuntimeError: | |
| # Event loop may be closing; ignore cleanup errors | |
| pass |
| # If shutdown was signaled, cancel the proxy task | ||
| if shutdown_task in done: | ||
| proxy_task.cancel() | ||
| try: | ||
| await proxy_task | ||
| except asyncio.CancelledError: | ||
| pass | ||
|
|
||
| # Cancel any remaining tasks | ||
| for task in pending: | ||
| task.cancel() | ||
| try: | ||
| await task | ||
| except asyncio.CancelledError: | ||
| pass |
There was a problem hiding this comment.
The code at lines 142-148 that cancels pending tasks is unnecessary because when shutdown_task is done, the only pending task is shutdown_task itself (since proxy_task is already being awaited), and when proxy_task completes naturally, shutdown_task would be in pending and will never complete. This creates confusing logic. The pending task cleanup can be removed or simplified.
| signal_task = asyncio.create_task(run_and_signal()) | ||
| shutdown_task = asyncio.create_task(run_with_shutdown(proxy)) |
There was a problem hiding this comment.
The signal_task is created but the test doesn't verify that it actually completes before sending the signal. If run_with_shutdown() exits before the signal is sent (due to other issues), the test might pass incorrectly. Consider using asyncio.gather() or ensure both tasks complete as expected.
| signal_task = asyncio.create_task(run_and_signal()) | ||
| shutdown_task = asyncio.create_task(run_with_shutdown(proxy)) |
There was a problem hiding this comment.
Same issue as in the SIGTERM test - the signal_task should be properly awaited or gathered to ensure both tasks complete as expected.
| proxy_task.cancel() | ||
| try: | ||
| await proxy_task | ||
| except asyncio.CancelledError: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| task.cancel() | ||
| try: | ||
| await task | ||
| except asyncio.CancelledError: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
Replace github.ref_name with github.sha to avoid forward slashes in artifact names for PR builds Co-authored-by: rgregg <1435771+rgregg@users.noreply.github.com>
The PR validation failed because the artifact name contained a forward slash ( Fixed in commit 1c93bc0 by replacing |
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.