Repository navigation
Conversation
…ping them The request queue exists to preserve heartbeats when the server is unavailable, but _dispatch_request's error classification defeated it: - HTTP 503 (sent by aw-server when the heartbeat lock times out, e.g. while the server is slow) fell into the 'Unknown error, not retrying' branch, permanently discarding the queued request. - The existing 400/500 branches were dead code: Response.__bool__ returns Response.ok, which is False for any error status, so 'if e.response and ...' never matched and every HTTP error was dropped - including the 500s the code claimed to retry. - A plain ConnectionError mid-dispatch (server died after connect) also hit the drop branch, and since 'connected' stayed True the dispatch loop kept popping and discarding requests one by one - draining the entire on-disk queue during a server outage. Now transient errors (ConnectionError/Timeout, HTTP 429/500/502/503/504) leave the request at the head of the persistqueue for a later retry, exactly like ConnectTimeout already did; only permanent client errors (e.g. HTTP 400 bad payload) are dropped so they can't wedge the queue. Connection errors also mark the queue disconnected so the run loop goes back to reconnecting. Retrying is safe for heartbeats: a duplicate of an already-processed heartbeat merges into the last event as a no-op, and FIFO head-retry preserves the ordering that merging requires (dropping requests from the middle of the stream broke merge chains, fragmenting events).
Greptile SummaryRetry queued heartbeat requests after transient transport and server failures instead of dropping them.
Confidence Score: 5/5The PR appears safe to merge. No blocking failures remain. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Dispatch queued heartbeat] --> B{Request result}
B -->|Success| C[Remove queue head]
B -->|Connection error or timeout| D[Mark disconnected]
D --> E[Keep queue head]
B -->|HTTP 429, 500, 502, 503, or 504| F[Calculate bounded retry delay]
F --> G[Stop-aware wait]
G --> E
B -->|Other request error| H[Drop queue head]
Reviews (2): Last reviewed commit: "fix(queue): honor Retry-After on transie..." | Re-trigger Greptile |
| logger.warning( | ||
| f"Server error {status_code}, will retry: {request.endpoint}" | ||
| ) | ||
| sleep(0.5) |
There was a problem hiding this comment.
Fixed in b31ba5a — retry delay now honors Retry-After (delta-seconds form, floored at 0.5s, capped at 60s; HTTP-date form falls back to default), and uses the stop-aware wait() instead of sleep() so a long delay can't block shutdown.
Use the Retry-After header (delta-seconds form, floored at 0.5s and capped at 60s) for the retry delay on 429/503-style responses, and use the stop-aware wait() instead of sleep() so a long delay can't block shutdown.
|
@greptileai review |
|
@ErikBjare — this branch is currently CONFLICTING against The conflict is small and localized to Your call which is easier:
Either works for me; option 2 is ready to go. |
|
Carry PR opened as #130 (#130) — rebased on current master, resolving the |
Problem
Diagnosed while investigating sustained aw-server CPU load (see ActivityWatch/aw-core#147): the server was 503-rejecting heartbeats (
Heartbeat lock could not be acquired within timeout), and it turned out each rejection permanently lost data, despite the on-disk request queue existing precisely to prevent that.Three bugs in
RequestQueue._dispatch_request's error classification:Unknown error, not retryingbranch, which pops the request from the persistqueue permanently. 503 Service Unavailable is the most retry-worthy status there is.Response.__bool__returnsResponse.ok, which isFalsefor every error status — soif e.response and e.response.status_code == 400never matched, and all HTTP errors (including the 500s the code claimed to retry) were dropped.ConnectionErrormid-dispatch (server died after connect; onlyConnectTimeoutwas caught) hit the drop branch, and sinceconnectedstayedTruethe dispatch loop kept popping and discarding queued requests one by one — draining the entire on-disk queue into the void, the exact scenario the class docstring says it protects against.Fix
No new retry machinery — the persistqueue is the retry mechanism. "Retry" here means returning without
_task_done()so the request stays at the head of the queue, exactly the pathConnectTimeoutalready took. The change is purely classification:ConnectionError/Timeout(also setsconnected = Falseso the run loop reconnects), and HTTP 429/500/502/503/504 (with the existing 0.5 s backoff).e.response is not Noneso the check actually works.Retrying is safe for heartbeats (the only thing in this queue): the 503 lock-rejection happens before any server-side processing, and even a true duplicate (processed but response lost) merges into the last event as a no-op. FIFO head-retry also preserves ordering, which heartbeat merging requires — dropping requests from the middle of the stream broke merge chains and fragmented events.
Tests
test_dispatch_retries_transient_server_errors(parametrized over 429/500/502/503/504): request survives the error, is retried, and is popped once the server recovers. Also guards theResponse.__bool__pitfall.test_dispatch_drops_client_errors: HTTP 400 is dropped and doesn't block the queue.test_dispatch_keeps_queue_on_connection_error: request stays queued and the queue marks itself disconnected.