fix(discord): honor retry_after in auto-thread creation instead of dropping messages on 429 - #76060
fix(discord): honor retry_after in auto-thread creation instead of dropping messages on 429#76060mytrashcan wants to merge 2 commits into
Conversation
When Discord rate-limits thread creation (HTTP 429), discord.py raises RateLimited only when the server-requested retry_after exceeds its internal max_ratelimit_timeout. The old _auto_create_thread loop slept a fixed 0.75s and retried — guaranteed to fail against a long Retry-After (e.g. 260s) — so the user's message was dropped with a generic 'please retry' notice. Honor Discord's retry_after: - Within _AUTO_THREAD_MAX_RATE_LIMIT_WAIT_SECONDS (30s): wait out the bucket, then retry the direct create_thread path. - Beyond the bound: give up immediately (do not block the handler for minutes), skip the seed-message fallback (it would hit the same bucket), and record the delay on _last_auto_thread_rate_limit so the caller can surface a 'Discord is rate-limiting (retry in ~Ns)' notice. Adds tests for short/long rate limits, fallback behavior, and the rate-limit-aware user notice.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for targeting a live failure path: current main still uses the fixed 0.75-second retry after direct and seed-message failures (plugins/platforms/discord/adapter.py:6574-6603), so the 429-specific direction is warranted.
Problems
- The new
self._last_auto_thread_rate_limitis shared adapter state (plugins/platforms/discord/adapter.py:6590,:6605) but is consumed later by_handle_message(:7592-7595)._auto_create_threadyields during retry waits (:6619,:6660), so another message can reset or overwrite this value before the first failure chooses its user notice. That can mislabel a non-429 failure or lose a 429 retry hint. - The new tests cover serial outcomes but not this interleaving (
tests/gateway/test_discord_auto_thread_rate_limit.py:106-294).
Suggested changes
- Carry retry-after metadata in the individual
_auto_create_threadresult and consume that local result in_handle_message, rather than storing it onself. - Add a two-message interleaving regression test that proves each failure receives its own notice.
This is an automated hermes-sweeper review.
…dapter state Addresses hermes-sweeper review: self._last_auto_thread_rate_limit was shared adapter state consumed later by _handle_message. Because _auto_create_thread awaits during retry sleeps, a second message could interleave and overwrite the attribute before the first message chose its user notice — mislabeling a non-429 failure or losing the 429 hint. - Replace the shared attribute with _AutoThreadRateLimited, a sentinel returned by _auto_create_thread that carries retry_after per call. - _handle_message checks isinstance(...) before the generic success test and consumes the per-call value locally; no adapter state is read. - Add interleaving regression test: two concurrent rate-limited messages each surface their own retry hint (45s / 90s) with no cross-talk.
|
Addressed in Changes1. Shared adapter state removed → per-call sentinel
2. Interleaving regression test added
Why this fixes the race
Verification
|
BrinkhaT
left a comment
There was a problem hiding this comment.
Verified against a current production reproduction and current main (a90d5369f).
The observed failure was a Discord auto-thread 429 with a server retry delay of about 243 seconds. Current main immediately entered the seed-message fallback, emitted misleading Thread created by Hermes notices, retried after the fixed 0.75-second delay, and then failed—matching this PR’s diagnosis.
I rebased both commits locally onto current main without conflicts and ran the full Discord-focused suite:
49 files, 337 tests passed, 0 failed
The per-call sentinel also avoids cross-message state races, and the bounded wait is a reasonable upstream safety policy for long Discord rate limits. This PR is still relevant on current main and cleanly addresses the production bug class.
What does this PR do?
Fixes auto-thread creation silently dropping user messages when Discord rate-limits thread creation (HTTP 429).
Root cause: When Discord returns 429 on⚠️ could not create a Discord thread … please retry" notice.
message.create_thread(), discord.py raisesRateLimitedonly when the server-requestedretry_afterexceeds its internalmax_ratelimit_timeout. The old retry loop in_auto_create_threadslept a fixed 0.75s and retried — guaranteed to fail against a long Retry-After (observed in production logs: 260s) — then dropped the message with a generic "Fix (in
plugins/platforms/discord/adapter.py):retry_after ≤ 30s, new_AUTO_THREAD_MAX_RATE_LIMIT_WAIT_SECONDS): wait out the bucket, then retry the directcreate_threadpath. The retry now actually succeeds instead of re-429ing.🧵 Thread created by Hermesmessage into the channel), and return a_AutoThreadRateLimitedsentinel carrying the delay._handle_messageis now rate-limit-aware: "_is_discord_rate_limit/_extract_discord_retry_afterhelpers (already used for slash-command sync).Related Issue
No issue filed; discovered live in production logs (auto-thread creation 429'd with
Retry in 260.56 secondswhile the retry loop only slept 0.75s). Related work that motivated the direction:retry_after(same pattern, different call path)Type of Change
Changes Made
plugins/platforms/discord/adapter.py_AUTO_THREAD_MAX_RATE_LIMIT_WAIT_SECONDS = 30.0module constant_AutoThreadRateLimitedsentinel class (carriesretry_afterper call)_auto_create_threadto honor Discord'sretry_after(short: wait + retry; long: give up fast + return sentinel; fallback skipped on 429 to avoid channel spam)_handle_messageconsumes the sentinel locally for a rate-limit-aware user notice (no shared adapter state — race-free across concurrent messages)tests/gateway/test_discord_auto_thread_rate_limit.py(new, 7 tests)retry_afterHow to Test
scripts/run_tests.sh tests/gateway/test_discord_auto_thread_rate_limit.py→ 7 passedscripts/run_tests.sh tests/gateway/test_discord_channel_controls.py tests/gateway/test_discord_double_dispatch.py tests/gateway/test_discord_sync_limit.py→ 12 passedscripts/run_tests.sh -j 4(CI parity) — see note belowruff check plugins/platforms/discord/adapter.py tests/gateway/test_discord_auto_thread_rate_limit.py→ cleanManual reproduction (needs a Discord server with
discord.auto_thread: true): send a burst of @mention messages to exhaust the thread-creation bucket; the bot should either create the thread after the short wait or show "Discord is rate-limiting requests (retry in ~Ns)" instead of silently dropping the request.Checklist
fix/description)scripts/run_tests.sh(CI parity)fix(discord): ...)Platforms Tested
Note on full-suite failures (pre-existing, unrelated)
scripts/run_tests.sh -j 4reports 46 failures across 18 files — none touch the Discord plugin or this change. Verified pre-existing: the same failures reproduce on cleanorigin/main(stash of this branch) — e.g.test_daytona_environment(needs Daytona),test_voice_mode/test_wake_word/test_transcription_tools(needs audio/OS APIs),test_systemd_notify/test_gateway_service/test_service_manager(Linux/systemd-only, run on macOS),test_web_tools_config(network), andtest_update_eol_churn(line-ending churn). All Discord gateway tests pass.