fix: preserve consumer config on update, warn not abort; adr accuracy

This commit is contained in:
2026-10-26 01:43:22 +01:00
parent 0908b7046a
commit 56f3d1ac1b
3 changed files with 41 additions and 18 deletions

View File

@@ -10,8 +10,10 @@ event ran its handlers twice.
**The outbox is `event_queue`.** The same SQLite table the DB-backed bus already used is now the
outbox for every backend. Migration `0101` adds `event_queue.event_id` (UUID, unique when set) and
`claimed_at`, and creates `processed_events` (`event_id`, `handler`, `status`, `started_at`, owner
token). Legacy rows have a NULL `event_id` and are still delivered; they just get no dedup.
`claimed_at`, and creates `processed_events` (`event_id`, `handler`, `status`, `started_at`,
`processed_at`). The migration backfills every existing row with a random event id, so legacy rows get dedup too.
The column stays nullable, but the only insert path (`enqueue_events`) always sets it, and the
consumer still tolerates a NULL id (no dedup) for rows from foreign writers.
**Writes are atomic with their events.** Repository and command-port methods that change state take
the events they cause and write them in the same transaction as the data. `shared::enqueue_events`
@@ -33,8 +35,9 @@ backends dead-letter after the same number of attempts.
`claimed_at` is older than 600 s, so a crash never strands an event in `processing`.
3. *Processed-event ledger.* Before a handler runs, `(event_id, handler)` is claimed in
`processed_events` (INSERT `in_progress`, or take over a claim older than the 1800 s lease).
Success marks it `done`; failure releases it; both are conditioned on an owner token so a
stale worker cannot clobber a newer claim. A delivery that sees a fresh claim held elsewhere is
Success marks it `done`; failure releases it; both are conditioned on the claim's millisecond
`started_at` value (`WHERE ... AND started_at = ?`), which acts as the owner token, so a stale
worker cannot clobber a newer claim. A delivery that sees a fresh claim held elsewhere is
deferred (`AckHandle::defer`: rescheduled to the lease expiry without consuming an attempt;
JetStream `nak` with delay, which does count toward `max_deliver`). A `done` row skips the handler.
Ledger rows are kept 30 days. A ledger read error skips the handler and nacks.
@@ -60,8 +63,8 @@ backends dead-letter after the same number of attempts.
Fixing it needs transaction hooks in k-ap.
- **k-ap 0.5.0 mints random ids for some activities.** Create, Announce and Like ids are
deterministic (`broadcast.rs:22-35`, `:283-288`; backfill `backfill.rs:208-212`). Delete
(`broadcast.rs:157`), Add/Undo (`:210`), Update (`:321`), Move (`:377`) and the actor update
(`:339-344`) use `Uuid::new_v4()` via `url_scheme.rs:52-53` with no override; only
(`broadcast.rs:157`), Add/Undo (`:210`), Update (`:321`), Move (`:377`), the Undo wrappers for
Announce (`:81`) and Like (`:129`) and the actor update (`:339-344`) use `Uuid::new_v4()` via `url_scheme.rs:52-53` with no override; only
`broadcast_raw_to_followers` (`:395`) takes a caller id. A duplicate of those after a crash is a
fresh-id activity. They are semantically idempotent for remote servers (deleting twice, updating
to the same state), but it is a new activity, not a replay. The ledger makes this rare; closing it
@@ -77,8 +80,11 @@ backends dead-letter after the same number of attempts.
- Existing deployments keep their JetStream stream. `get_or_create_stream` applies config only on
creation, so the consumer now compares an existing stream and consumer with what this build needs
and updates `duplicate_window` (1 h) and the consumer's `max_deliver`/`ack_wait` in place, with a
warning in the log. Both are mutable on a live stream/consumer, so no recreate is needed;
immutable consumer fields (name, filter, deliver policy) are never touched.
warning in the log. Both are mutable on a live stream/consumer, so no recreate is needed. Each update is a
clone of the existing config with only those fields changed, so operator-set fields and the
immutable ones (name, filter, deliver policy) are preserved. If an update is rejected the worker
logs a warning with the manual `nats stream edit` / `nats consumer edit` command and keeps
starting.
- Server and worker both migrate at startup (docker compose starts them together). `sqlite::migrate`
retries with a bounded backoff (100 ms to 1 s, 5 retries), so the loser of a migration race
re-reads `_sqlx_migrations`, skips what the winner applied and succeeds; only a persistent error