Repository navigation
fix(bot): drain webhooks and close the Discord session on SIGTERM - #202
Merged
Merged
Conversation
Hypercorn installed its own SIGTERM/SIGINT handlers when the webhook server started, replacing the ones py-cord sets in Client.run. On SIGTERM only the webhook server stopped and the bot kept its gateway session until SIGKILL. Passing a shutdown trigger keeps py-cord's handlers, so the bot closes and the process exits right away. Also call bot.run directly instead of wrapping its result in create_task.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #202 +/- ##
==========================================
- Coverage 67.20% 67.12% -0.08%
==========================================
Files 74 76 +2
Lines 3436 3501 +65
==========================================
+ Hits 2309 2350 +41
- Misses 1127 1151 +24 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ToxicBiohazard
requested changes
Oct 6, 2026
ToxicBiohazard
left a comment
Member
There was a problem hiding this comment.
Thanks, this is a clear write-up and the diagnosis holds up. I checked it against the versions we pin:
- Client.run in py-cord 2.7.1 sets SIGINT/SIGTERM to call loop.stop. When the loop stops, its cleanup cancels the remaining tasks, and the runner's finally calls close().
- worker_serve in hypercorn 0.17 calls
loop.add_signal_handlerfor SIGINT/SIGTERM/SIGBREAK whenshutdown_triggeris None. Its task first runs afterrun_forever()starts, so these handlers replace py-cord's. - The old
bot.loop.create_task(bot.run(...))evaluatedbot.run(...)first, which blocks until the loop is closed. It then passed None tocreate_taskon a closed loop, which explains the Event loop is closed error and exit code 1.
The problem is what this fix does to webhooks during shutdown. Hypercorn never gets to drain, so on SIGTERM any in-flight webhook request is cancelled along with everything else. The webhook source doesn't retry, so those events are lost, or applied only partway. Details and a suggested approach are inline. Everything else is minor.
SIGTERM and SIGINT now set one shutdown event. hypercorn uses it as its shutdown trigger: it stops accepting requests and lets in-flight webhooks finish (up to 50s), then the bot closes its Discord session. Before, py-cord's handler stopped the loop and cancelled in-flight webhooks. The 50s timeout is an upper bound: an idle server stops at once, so the bot stays connected only as long as the slowest in-flight webhook. Also stops logging the bot token at DEBUG, with a test that fails if a log call includes a token.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Types of changes
Proposed changes
On SIGTERM the bot kept its Discord session open until SIGKILL. hypercorn's
worker_serveinstalls its own SIGTERM/SIGINT handlers whenshutdown_triggeris None, and they replace the ones py-cord'sClient.runsets. So SIGTERM only stopped the webhook server. With one pod per bot token, an eviction meant two sessions on the same token for the whole grace period.Now there is one shutdown sequence:
src/webhooks/server.py).shutdown_trigger. It stops accepting requests and lets in-flight webhooks finish, up tograceful_timeout = 50. The bot stays connected meanwhile, because the handlers need it.bot.runreturns, and the process exits.The 50s is an upper bound. An idle server stops at once, so the bot stays connected only as long as the slowest in-flight webhook. The pod needs a grace period above 50s: hackthebox/kubernetes-deploy#5511 sets 60s.
Also:
bot.run(...)is called directly. The oldbot.loop.create_task(bot.run(...))ranbot.runfirst and then calledcreate_task(None)on a closed loop, so the process exited 1 with "Event loop is closed".Checklist
doc.
Additional context
Tests:
tests/src/webhooks/test_server_serve.py: SIGTERM and SIGINT each make the server drain and then close the bot, in that order. The drain timeout is 50s.tests/src/test_no_token_in_logs.py: fails if any log call insrc/reads aTOKENattribute.End-to-end check with py-cord 2.7.1's
Client.run, real hypercorn, a stubbed gateway and a real SIGTERM sent during a 2s webhook request:hypercorn drain time, measured with the 50s timeout: 0.00s with an idle keep-alive connection, 1.8s with a 2s request in flight.
In the container, SIGTERM reaches
python -m srcthroughuv(PID 1),taskandsh.