Skip to content

NA-0642: durable store-and-forward queue — SQLite, retention TTL, delete-on-ACKNOWLEDGED-pull (D578) - #61

Merged
Tebbens4832 merged 1 commit into
mainfrom
na-0642-durable-queue
Jul 13, 2026
Merged

NA-0642: durable store-and-forward queue — SQLite, retention TTL, delete-on-ACKNOWLEDGED-pull (D578)#61
Tebbens4832 merged 1 commit into
mainfrom
na-0642-durable-queue

Conversation

@Tebbens4832

Copy link
Copy Markdown
Member

qsl-protocol-governed lane NA-0642 (QSL-DIR-2026-07-13-578, operator-approved design-lock: ack model B, rusqlite+bundled, all five sub-choices as proposed). Governance closeout lands in qsl-protocol at D-1265; this PR is the CODE half. Records D-0011 in this repo's DECISIONS.md (the pull-contract decision, per the D-0009/D-0010 surface).

What changes

  • Durable store (src/store.rs): embedded SQLite (rusqlite bundled), journal_mode=WAL + synchronous=FULL (a 200 on push = fsynced), schema v1 with fail-closed version check. Route tokens are persisted only as SHA-256 digests; payloads stay opaque verbatim BLOBs; msg_id is deliberately NOT unique (NA-0275 duplicate-push contract).
  • Acknowledged-pull (option B, opt-in): GET /v1/pull?ack=lease returns without deleting and leases for PULL_LEASE_SECS (default 60 s); POST /v1/pull/ack {"ids":[...]} deletes only leased copies, idempotent, route-scoped; un-acked leases expire and messages reappear. Legacy GET /v1/pull is byte-identical — delete-on-deliver, same JSON shape (guarded by an exact-field-set test) — the current non-acking qsc client and the NA-0640 e2e are unaffected.
  • Retention TTL replaces idle-discard: RETENTION_TTL_SECS (default 604800 = 7 d, ceiling 30 d) governs undelivered-message lifetime, swept lazily on push/pull + a 60 s background task. ROUTE_IDLE_TTL_MS is retired warn-and-ignore (a stale relay.env cannot brick a restart). Delivered+acked messages are still forgotten — reliable, not an archive.
  • Config, fail-closed: STORE_PATH REQUIRED (no default; :memory: accepted as a deliberate ephemeral choice; ERR_INVALID_CONFIG_STORE_PATH on missing/unopenable). Rate buckets stay in-memory (abuse control, resets on restart by design); drain-release parity for route slots and buckets preserved.
  • Packaging (minimal, config-required): relay.env.example gains the three envs and the MAX_QUEUE_DEPTH 256→257 fix (NA-0598 exact-4-MiB); StateDirectory=qsl-server added to the unit — flagged scope judgment: required so the packaged ProtectSystem=strict unit can write /var/lib/qsl-server; runbook §3 updated.

Test evidence (cargo test: 100 passed, 0 failed, 25 binaries; fmt + clippy -D warnings clean; both CI guard scripts pass)

  • Hard-kill restart durability (na0642_durability_restart.rs, real binary, Child::kill = SIGKILL, no graceful shutdown): pushed_message_survives_hard_kill_and_restart (32 KiB payload byte-identical after SIGKILL between push-200 and restart — proves the synchronous=FULL fsync, not a graceful flush); leased_message_survives_hard_kill_and_reappears_after_lease_expiry (SIGKILL mid-lease; lease survives restart, message invisible while live, reappears byte-identical after expiry, ack then deletes); legacy_delivery_does_not_resurrect_after_restart (delivered messages stay forgotten).
  • Ack contract (5 tests): no-delete-until-ack; redelivery after lease expiry; leased-only deletion incl. the duplicate-msg_id case; fail-closed inputs (ERR_BAD_ACK_MODE/_BODY/_IDS); route-scoped ack.
  • Retention (7 + logging): expiry + TWO non-vacuity controls (default-TTL survival; post-expiry delivery works); sweep entry point stats; slot release; no resurrection; redaction (event=retention_expired, no token/payload leak).
  • Backward compat: exact top-level/item field sets on the legacy path.
  • Concurrency (new coverage): 8 pushers × 25 + 4 legacy pullers = exactly-once, no loss; 4 racing lease-pullers never double-lease.
  • At-rest privacy: raw route token absent from db+wal+shm bytes (payload-presence control proves the right file).
  • Contract updates, called out: config_semantics zero-env start → now fail-closed (STORE_PATH required); route_lifecycle_ttl* idle-discard tests retired → drain-release contracts carried into na0642_retention_lifecycle.rs; na0347 retention-purge block moved to the retention mechanism.

Not in this PR (recorded as owed in qsl-protocol)

qsc ack-client lane; qsc dev-dep pin bump + NA-0640 e2e re-run; the filed hardening debt (a)–(f) incl. README/DOC-SRV-003 idle-TTL text now behind the code; ENG-0014 bearer compare untouched.

🤖 Generated with Claude Code

… TTL, delete-on-ACKNOWLEDGED-pull (D578, D-0011)

Store: rusqlite(bundled), WAL + synchronous=FULL, schema v1
(routes keyed by SHA-256 of the route token — raw tokens never persisted;
messages: seq FIFO, msg_id non-unique per NA-0275, opaque BLOB body,
enqueued_at, leased_until). STORE_PATH is required, fail-closed.

Contract (D-0011, per the qsl-protocol D578 design-lock, option B):
- legacy GET /v1/pull unchanged byte-for-byte (delete-on-deliver) — the
  current non-acking qsc client is not stranded
- GET /v1/pull?ack=lease leases without deleting (PULL_LEASE_SECS, 60s)
- POST /v1/pull/ack deletes only leased copies; idempotent; route-scoped
- RETENTION_TTL_SECS (7d default) replaces the 5-min idle-route discard
  (ROUTE_IDLE_TTL_MS now warn-and-ignore); delivered+acked still forgotten

Tests: hard-kill (SIGKILL) restart durability + crash-between-pull-and-ack
lease survival; retention expiry + non-vacuity controls; ack semantics;
legacy-shape guard; concurrency no-loss/no-dup; token-at-rest privacy.
Packaging: relay.env.example (new envs + MAX_QUEUE_DEPTH 256->257 fix),
StateDirectory=qsl-server, runbook config section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Tebbens4832
Tebbens4832 merged commit 8e4ea27 into main Jul 13, 2026
1 check passed
@Tebbens4832
Tebbens4832 deleted the na-0642-durable-queue branch July 13, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant