Summary
A queued mutation that the broker rejects permanently with a non-409 status is misclassified as transient and retried forever by the offline queue, never draining.
Mechanism
check_response (sync_engine.rs) maps broker status codes to errors: 401→SessionInvalid, 403→Ownership, 404→NotFound, 409→Conflict, and everything else → Error::Mqtt. The broker (mqdb-core transport.rs) returns:
Validation / SchemaViolation → 400 (BadRequest)
- internal errors → 500
Both land in the catch-all → Error::Mqtt, and Error::is_transient() is true for Mqtt. In flush_consolidated, Err(err) if err.is_transient() => FlushOutcome::Keep, so a permanently-invalid queued mutation (e.g. a schema-invalid write) is retained and re-sent on every flush cycle indefinitely.
Why is_permanent_mutation does not catch it
Error::is_permanent_mutation matches only Error::Mqdb{ … } variants — errors from the local backend. The offline queue flushes to the remote, which never yields Error::Mqdb (its constraint violations come back as 409 → Conflict; its validation errors as 400 → Mqtt). So the classifier is unreachable on the flush path, and the intended "drop permanent failures" behavior never fires for remote-permanent rejections.
Options
- Broker: use a distinct status code for permanent-non-conflict rejections so the client can classify them as permanent and drop.
- Client: cap retry attempts per queued mutation, or treat a 400 (BadRequest) as permanent (drop after logging) rather than transient.
Pre-existing (not introduced by #22); surfaced while reviewing the conflict-rollback fix.
Summary
A queued mutation that the broker rejects permanently with a non-409 status is misclassified as transient and retried forever by the offline queue, never draining.
Mechanism
check_response(sync_engine.rs) maps broker status codes to errors:401→SessionInvalid,403→Ownership,404→NotFound,409→Conflict, and everything else →Error::Mqtt. The broker (mqdb-coretransport.rs) returns:Validation/SchemaViolation→ 400 (BadRequest)Both land in the catch-all →
Error::Mqtt, andError::is_transient()istrueforMqtt. Inflush_consolidated,Err(err) if err.is_transient() => FlushOutcome::Keep, so a permanently-invalid queued mutation (e.g. a schema-invalid write) is retained and re-sent on every flush cycle indefinitely.Why
is_permanent_mutationdoes not catch itError::is_permanent_mutationmatches onlyError::Mqdb{ … }variants — errors from the local backend. The offline queue flushes to the remote, which never yieldsError::Mqdb(its constraint violations come back as409 → Conflict; its validation errors as400 → Mqtt). So the classifier is unreachable on the flush path, and the intended "drop permanent failures" behavior never fires for remote-permanent rejections.Options
Pre-existing (not introduced by #22); surfaced while reviewing the conflict-rollback fix.