Stop all-clear delivery retrying forever (#148) #151
No reviewers
Labels
No labels
area:ai
area:ci-cd
area:notifications
area:observability
area:public-pages
backlog
bug
duplicate
enhancement
help wanted
invalid
question
type:decision
type:feature
type:infra
type:maintenance
type:security
v1.0.1
v1.1.0
v1.2.0
v1.3.0
v2.0.0
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/WeatherBot!151
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/webex-permanent-failure"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #148.
Problem
A Webex all-clear returning HTTP 400 was retried once a minute, indefinitely in production.
The classification was already correct —
classify_http_errormaps a 4xx other than 408/429 toPermanentDeliveryError, andwebex.send_alert_liftedraises it. The loss happened one layer up:_dispatch_alert_liftedcaught every exception and returned a bareFalse, throwing the distinction away. The caller only stampedlifted_notified_aton success, so the row stayed pending andsend_pending_lifted_notificationsre-selected it every cycle — for a request that could never succeed.Not Webex-specific.
_dispatch_alert_liftedis the shared dispatch for every channel type, so the same unbounded loop applied to all of them. That covers the issue's "check the other notifiers for the same pattern" task: there was one pattern, in one place.Change
_dispatch_alert_liftedreturns(sent, permanent), preserving what the notifier already worked out.record_delivery_resultacceptspermanent=Trueand marks the ledger row dead immediately, rather than burning throughdelivery_max_attemptson a request that cannot succeed.lifted_notified_atonce the delivery row reachesdead— whether from a permanent rejection or from transient failures that exhausteddelivery_max_attempts/delivery_max_age_hours.Transient failures still retry. They just stop eventually. No new configuration: this reuses the bounds the outbox already had, which the all-clear path was simply not consulting.
Diagnosability
The Webex response body is now logged on rejection, with the
trackingIdheader. Webex explains a rejection in the body'smessagefield; the status code alone is not actionable, which is exactly why the production 400 could not be diagnosed from logs. Only the response is logged — the bot token lives in the request headers and is never echoed back.That is the piece that will tell you why channel
e70842aais 400ing, which I could not determine from the code alone. Likely candidates are a deleted room or a revoked token, but the body will say.Tests
tests/test_all_clear_retry_bounds.py:deadafter exactly 1 attempt, record stampeddelivery_max_attempts(3 sends across 6 cycles) and endsdeadsentVerification
ruff check .python -m compileall appalembic upgrade headon fresh Postgres 16Note
This stops the loop; it does not retroactively clear the stuck row in production. The existing
sa-*record for channele70842aastill haslifted_notified_atNULL, so on deploy it will make one final attempt, fail, and be marked dead — after which the log goes quiet and the delivery row carries the reason.🤖 Generated with Claude Code