Finding
A transient download failure schedules a retry via tokio::spawn: the task sleeps for the backoff duration, then inserts the retry item back into the queue. The dispatch call that should pick the item up (try_dispatch_next) runs at line 298 — synchronously, before the spawned task's sleep completes — so the queue is empty at dispatch time. When the sleep wakes and the item is inserted at line 290, nothing calls try_dispatch_next. The retry item sits in the queue indefinitely until some unrelated download completion or failure happens to fire an event.
Evidence
crates/syntaxis/src/lib.rs:290:
tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_secs(backoff)).await;
inner.lock().await.queue.insert(queue_item); // no dispatch_next here
});
// ...
self.try_dispatch_next().await; // line 298 — runs before sleep wakes
The dispatch at line 298 observes the pre-insert (empty) queue; the post-sleep insert at line 290 has no following dispatch.
Why this matters
In a quiescent acquisition pipeline — the common steady state on a phone where no other download is active — a retried item is never re-dispatched. The retry path is silently dead: items that report as pending make no progress, so any content fetch that hit one transient error stalls until the device happens to trigger another unrelated event. On a counter-surveillance handset, content the operator is relying on (updates, payloads, mirrors) silently fails to arrive with no error surfaced, and the failure is invisible until manually inspected.
Desired correction
Drive a dispatch after the re-enqueue: call try_dispatch_next inside the spawned task immediately after queue.insert, or replace the sleep-then-insert pattern with a Notify/channel signal that wakes the dispatcher when a backoff expires. The dispatch must observe the queue in its post-insert state.
Done when: a unit test confirms a retried item is dispatched after its backoff elapses with no other downloads active.
Finding
A transient download failure schedules a retry via
tokio::spawn: the task sleeps for the backoff duration, then inserts the retry item back into the queue. The dispatch call that should pick the item up (try_dispatch_next) runs at line 298 — synchronously, before the spawned task's sleep completes — so the queue is empty at dispatch time. When the sleep wakes and the item is inserted at line 290, nothing callstry_dispatch_next. The retry item sits in the queue indefinitely until some unrelated download completion or failure happens to fire an event.Evidence
crates/syntaxis/src/lib.rs:290:The dispatch at line 298 observes the pre-insert (empty) queue; the post-sleep insert at line 290 has no following dispatch.
Why this matters
In a quiescent acquisition pipeline — the common steady state on a phone where no other download is active — a retried item is never re-dispatched. The retry path is silently dead: items that report as pending make no progress, so any content fetch that hit one transient error stalls until the device happens to trigger another unrelated event. On a counter-surveillance handset, content the operator is relying on (updates, payloads, mirrors) silently fails to arrive with no error surfaced, and the failure is invisible until manually inspected.
Desired correction
Drive a dispatch after the re-enqueue: call
try_dispatch_nextinside the spawned task immediately afterqueue.insert, or replace the sleep-then-insert pattern with aNotify/channel signal that wakes the dispatcher when a backoff expires. The dispatch must observe the queue in its post-insert state.Done when: a unit test confirms a retried item is dispatched after its backoff elapses with no other downloads active.