Relay review fixes: message ordering, prekeys, key log, account delete, PITR drill #251

Closed
extant2000git wants to merge 0 commits from extant2000git/relay-review-fixes-2026-10-14 AGit into main
Owner

From the relay architecture review 2026-10-14. Concurrent sends to one device now commit in id order (row lock), so a poll cursor can no longer skip and delete an unseen envelope. One Delivery.write for send, sealed send and federated deposits, with a bounded push worker; federated deposits now wake and push. Prekey pops use FOR UPDATE SKIP LOCKED plus a used recheck. Key log appends take an advisory lock. Account delete retires each device (revoke leaves). Deleted accounts and DB errors are refused. retryingTransaction retries PG 40P01 and 40001. Monthly PITR restore drill (installed, PASS). 915 tests pass with Postgres. Generated with Claude Code.

From the relay architecture review 2026-10-14. Concurrent sends to one device now commit in id order (row lock), so a poll cursor can no longer skip and delete an unseen envelope. One Delivery.write for send, sealed send and federated deposits, with a bounded push worker; federated deposits now wake and push. Prekey pops use FOR UPDATE SKIP LOCKED plus a used recheck. Key log appends take an advisory lock. Account delete retires each device (revoke leaves). Deleted accounts and DB errors are refused. retryingTransaction retries PG 40P01 and 40001. Monthly PITR restore drill (installed, PASS). 915 tests pass with Postgres. Generated with Claude Code.
extant2000git added 10 commits 2026-10-14 12:04:45 +00:00
Restores the newest base backup plus every WAL segment since into a
throwaway postgres container, compares table count, devices, users and
key_log (md5 over seq and leaf_hash up to the restored seq) with live,
and writes PASS or FAIL to LAST_PITR_DRILL. Installed on the box
2026-10-14: first run PASS, and pointing it at a missing archive
gives FAIL with exit 1. Timer: the 2nd of each month, 06:30 UTC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Postgres test holds one send inside its transaction after its insert
and sends a second to the same device. On main the second commits first,
a poll sees id N+1, the cursor passes N, and delete-on-ack removes N
unread. RelayHarness gains a postgres option for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
popPreKey and popPQPreKey: the candidate subquery takes FOR UPDATE SKIP
LOCKED on Postgres and the outer UPDATE rechecks used = false. Before,
under READ COMMITTED a second concurrent pop waited on the first one's
row lock, rechecked only id = X, and returned the same one-time key.
SQLite keeps the old statement plus the recheck: one writer at a time.

KeyLog.append takes pg_advisory_xact_lock in the same transaction as the
insert, joining the caller's transaction when there is one. Before, two
appends could commit out of seq order, so a head read between the two
commits was not a prefix of the next head.

The pooler guard now allows pg_advisory_xact_, which is released at
COMMIT. docs/POOLING.md says so.

Postgres tests (FN_TEST_POSTGRES): two connections pop one device's pool
and get two different keys, classical and PQ; a head read while one
append is open is a prefix of the final log. All three fail on
gitea/main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deleteAccount now retires each device through DeviceRetirement inside
one retryingTransaction, before the user row goes, so every device gets
a revoke leaf under the account's subject. Before, devices were deleted
directly and the key log never heard about it. Earlier leaves are
untouched; key_log only grows.

noteAliveOrRefuse no longer swallows errors. A database error refuses
the request, so the suspension check cannot be skipped. A missing user
row is refused when this relay issues tokens (FN_ISSUER_PRIVATE_KEY set);
a verify-only relay still accepts accounts it has no row for.

Tests: account delete appends one revoke leaf per device and keeps the
earlier leaves; a token for the deleted account is refused (410 for the
retired device, 401 for a fresh device id); a database error refuses;
a verify-only relay still accepts an unknown account. The first three
fail on gitea/main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sends and federated deposits now write envelopes through one function,
Delivery.write. Inside the transaction it locks each target device row
(sorted, FOR NO KEY UPDATE) before inserting, so on Postgres one
device's envelopes take ids and commit in the same order. A poll cursor
can no longer pass an uncommitted lower id, and delete-on-ack can no
longer remove an envelope the device never saw. The cursor stays the
global envelope id, so installed apps are unaffected. SQLite already
has one writer and skips the lock.

After the commit, Delivery.write wakes the devices and queues the push
on a PushWorker: 8 at a time, 1024 queued, a full queue drops the push
and counts it, and shutdown flushes the queue. This replaces one
Task.detached per send. Federated deposits now wake and push like a
local send. An introduction's first message is a sealed send and
already took this path.

placeDeposits becomes resolveDeposits and returns the rows to write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
retryingTransaction now retries PSQLError with SQLSTATE 40P01
(deadlock_detected) or 40001 (serialization_failure), with the same
5 attempts and backoff as SQLite BUSY. No other code is retried.
Postgres has already rolled the attempt back, so a rerun is clean.

Only the outermost call retries a Postgres error. A nested call shares
an already aborted transaction, so it rethrows and the outer call
starts over. This covers KeyLog.append taking the advisory lock inside
a caller's transaction.

Test (FN_TEST_POSTGRES): two connections lock rows 1 and 2 in opposite
order inside retryingTransaction. One gets 40P01, runs again, and both
commit once: 3 attempts, rows end at [2, 2]. Fails with 40P01 before
this change; passed 5 of 5 runs after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge ops/pitr-monthly-drill
ci / checks (pull_request) Successful in 15s
ci / swift (pull_request) Failing after 8m0s
ci / image (pull_request) Skipped
ci / deploy (pull_request) Skipped
cf1ce36602
extant2000git added 1 commit 2026-10-14 12:24:50 +00:00
Refuse only deleted accounts, not every subject without a row
ci / image (pull_request) Successful in 1m58s
ci / deploy (pull_request) Skipped
ci / checks (pull_request) Successful in 13s
ci / swift (pull_request) Successful in 14m43s
774b59bd10
CI's e2e conformance suite (test/relay.mjs at 310c315) mints tokens
for bare subjects on an issuing relay, and the account check refused
them all as deleted. Account delete now lists the id in a new
deleted_accounts table (id and time only), and a missing row is
refused only when the id is listed there. Production had 0 devices
without a user row. Conformance: 266 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
extant2000git added 1 commit 2026-10-14 12:25:51 +00:00
Source page and site layout link to git.extant2000.com
ci / checks (push) Successful in 12s
ci / swift (push) Successful in 10m20s
ci / image (push) Successful in 1m4s
ci / deploy (push) Successful in 22s
ci / swift (pull_request) Successful in 14m10s
ci / image (pull_request) Successful in 1m48s
ci / deploy (pull_request) Skipped
ci / checks (pull_request) Successful in 33s
5a0331e37a
The four repositories moved from GitLab to Gitea. The hand-written
source page (en, es) and the layout's sameAs list now point at
https://git.extant2000.com/extant2000/<repo>; all four answer 200
and clone without credentials. Generated doc pages still carry
GitLab links until their source documents change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Already in main: this PR head and main are both 5a0331e37a. Wave 1 is live. Closing rather than merging a no-op.

Already in main: this PR head and main are both 5a0331e37a19. Wave 1 is live. Closing rather than merging a no-op.
extant2000git closed this pull request 2026-10-14 14:05:39 +00:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.