fix(gateway): recognize Weixin's rate-limit wording in the delivery ledger's flood classifier - #105070
Open
nftpoetrist wants to merge 1 commit into
Open
fix(gateway): recognize Weixin's rate-limit wording in the delivery ledger's flood classifier#105070nftpoetrist wants to merge 1 commit into
nftpoetrist wants to merge 1 commit into
Conversation
…edger's flood classifier
is_flood_error() (gateway/delivery_ledger.py) only recognized Telegram's two flood shapes: the
adapters' fail-closed "flood_control:<seconds>" prefix, and PTB's raw "Flood control exceeded...
Retry in N seconds" text. It gates whether a final-send failure gets a timed redelivery
(_schedule_flood_redelivery) and whether the boot/runtime sweeps treat a failed row as still inside
its penalty window or as an ordinary failure to retry immediately.
Weixin's rate-limit circuit breaker fails a send closed with neither shape ("iLink sendmessage rate
limited; cooldown active for {N}s", gateway/platforms/weixin.py), so a Weixin send throttled by iLink
never got a flood timer armed: the row sat as an ordinary "failed" row and the next boot/reconnect
sweep claimed and resent it immediately, spending a redelivery attempt inside the still-active
cooldown instead of waiting it out.
Widen is_flood_error() to also recognize the platform-neutral rate-limit classification already used
by _send_with_retry's in-attempt backoff (classify_send_error() in gateway/platforms/base.py, via a
local import to avoid a module-load-order dependency) instead of hand-rolling a second Weixin-specific
regex. Also extend the flood-wait extraction to read Weixin's own embedded "cooldown active for Ns"
figure (a new _COOLDOWN_WAIT_RE, tried alongside the existing PTB regex) so the redelivery timer uses
the platform's actual cooldown instead of falling back to the generic 60s default.
Extends tests/gateway/test_delivery_flood_invariants.py (mutation-verified: new tests fail without
the fix, pass with it; existing Telegram-only tests are unaffected) to cover: is_flood_error()/
flood_wait_seconds() on Weixin's error text, the runtime sweep arming a timed redelivery instead of
claiming immediately inside the cooldown, and boot-sweep adoption without spending an attempt.
nftpoetrist
force-pushed
the
fix/weixin-flood-ledger-classification
branch
from
September 7, 2026 17:13
2a9e827 to
9eba9e9
Compare
Contributor
SummaryTeaches the delivery ledger's flood classifier to recognise Weixin's own rate-limit wording ( Findings
Verification
|
This branch has not been deployed
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.
Summary
is_flood_error()(gateway/delivery_ledger.py) only recognized Telegram's two flood shapes: theadapters' fail-closed
flood_control:<seconds>prefix, and python-telegram-bot's raw"Flood control exceeded... Retry in N seconds"text. This function gates whether a final-send failure gets a timedredelivery armed (
_schedule_flood_redelivery) and whether the boot/runtime sweeps(
sweep_recoverable,sweep_failed_for_runtime) treat a failed row as still inside its penalty window(adopt without spending an attempt, wait for the deadline) or as an ordinary failure (claim and resend
immediately).
Weixin's rate-limit circuit breaker fails a send closed with neither shape —
"iLink sendmessage rate limited; cooldown active for {N}s"(gateway/platforms/weixin.py, a bareSendResult(success=False, error=str(exc)), noretry_after/flood_control:framing) — so a Weixinsend throttled by iLink never got a flood timer armed. The row sat as an ordinary
failedrow, and thenext boot sweep or reconnect-triggered sweep claimed and resent it immediately, spending a redelivery
attempt inside the still-active cooldown instead of waiting it out — defeating the point of the
flood-ledger mechanism for this one platform.
Fix
Widened
is_flood_error()to also recognize the platform-neutral rate-limit classification alreadyused by
_send_with_retry's in-attempt backoff —classify_send_error()ingateway/platforms/base.py(imported locally inside the new_classified_rate_limited()helper, best-effort, to avoid a module-load-order dependency between
delivery_ledger.pyand the much largergateway/platforms/base.py) — rather than hand-rolling a second, Weixin-specific regex. This is thesame chokepoint
_is_rate_limited_error()already wraps for the in-attempt backoff decision, so anyfuture adapter whose error text matches "flood"/"too many requests"/"retry after"/"rate limit" gets the
ledger's timed-redelivery treatment too, not just Weixin.
Also extended the wait-seconds extraction (
_raw_flood_wait, used byflood_wait_seconds/flood_not_before) with a second regex,_COOLDOWN_WAIT_RE, matching Weixin's own embedded"cooldown active for Ns"figure — so the redelivery timer honors the platform's actual cooldowninstead of falling back to the generic 60s default every time.
Why this approach over a scoped Weixin-only pattern
Reusing
classify_send_error()was judged safer and more maintainable than adding a narrowWeixin-specific literal match:
flood_control:prefix, closing the same class of gap ahead of time rather than one platform at a time._RETRYABLE_ERROR_PATTERNS(the separate "transient"/network-error bucket) doesn't overlap with therate_limitedclassifier's patterns, andis_flood_error()is only ever called on the row'slast_errorstring — never on"send_path_degraded"or blocked/forbidden/not-found texts, which the classifier correctly leaves unmatched (covered by a new regression assertion).Testing
Extended
tests/gateway/test_delivery_flood_invariants.py(the current, focused successor to theolder/larger flood-retry test file) with four new tests:
test_weixin_rate_limit_text_classifies_as_flood_and_extracts_its_own_wait— unit-level:is_flood_error()recognizes the Weixin text,flood_wait_seconds()reads the embedded12.3figure (not the generic default), and an ordinary (non-rate-limited) Weixin failure is correctly left unmatched.test_weixin_rate_limit_arms_timed_redelivery_not_immediate_retry— end-to-end throughsweep_failed_for_runtime: a Weixin flood row is not claimed while still inside its cooldown, and is claimed (with the rate-limit marker) once the deadline passes.test_weixin_rate_limit_boot_adoption_does_not_spend_attempt— end-to-end throughsweep_recoverable+sweep_failed_for_runtime: a dead-owner Weixin flood row is adopted without spending an attempt while cooling down, then claimed normally past the deadline (mirrors the existing Telegram boot-adoption test).test_telegram_flood_detection_unaffected_by_broader_classifier— confirms Telegram's two established shapes and the "permanent rejection is not a flood" invariant are unchanged.Ran:
tests/gateway/test_delivery_flood_invariants.py,test_delivery_ledger.py,test_delivery.py,test_weixin.py,test_restart_redelivery_dedup.py,test_send_error_classification.py— 109 passed.ruff checkon both changed files: clean.Mutation-verified: reverted
gateway/delivery_ledger.pyvia patch file (tests kept), reran thefour new tests — 3 of 4 failed as expected (
AssertionErrors on the exact behaviors the fix adds); theTelegram-only invariant test correctly still passed since it doesn't exercise the new code path.
Reapplied the fix — all 6 tests in the file pass again.
Competitor check
Fresh search across
weixin flood/is_flood_error/delivery_ledger weixin/weixin rate limit/flood_control weixin/delivery ledger rate limit/flood-capped/delivery_ledger, all states,right before pushing:
gateway/platforms/base.py's_send_with_retry(in-attempt fallback/notice avoidance) andgateway/platforms/weixin.py'ssend()(addserror_kind/retry_aftertoSendResult, via a_retry_after_from_errorextractor using the samecooldown active for Nswording). It does not touchgateway/delivery_ledger.pyat all, and its base commit predates currentmain's_send_with_retry(which already has equivalent, differently-shaped rate-limit handling — likely converged on independently). No overlap with this PR's target file; noted here as complementary, adjacent prior art for the in-attempt/SendResult-enrichment layer, not the post-exhaustion ledger layer this PR fixes.gateway/platforms/weixin.py+ its own tests; also does not touchdelivery_ledger.py.pending_flood_retries, thesweep_recoverable/sweep_failed_for_runtimedeadline enforcement — all read and relied on above) and validated it end-to-end for Telegram, but its ownis_flood_error()stayed Telegram-only; this PR is the direct, non-duplicate follow-up for Weixin.is_flood_error,flood_wait_seconds, or Weixin's rate-limit text indelivery_ledger.py.