fix(store): route the remaining seventeen SQLite writers through _writer_txn - #1162
wshallwshall wants to merge 10 commits into
Conversation
… only on Exception asyncio.CancelledError derives from BaseException, so the store's `except Exception: await self._db.rollback()` handlers never fired on a cancellation. A cancelled writer unwound with its transaction still open. SQLite has ONE writer connection behind one asyncio.Lock, so "no pool" does not mean "no next borrower" -- it makes every later writer the next borrower. Most of the store's short writers issue no BEGIN of their own, so the next one's COMMIT would make the abandoned statements durable too. On a stage handoff that is work loss: a cancelled route handoff would commit the ingress row's guarded DELETE on first deployment while the routed rows it should have produced never existed. Measured against the pre-fix tree at all three cancel points. One `_writer_txn(db, lock)` async context manager now owns the shape. Its handler is BaseException; the rollback runs INSIDE the lock (or another writer takes the connection mid-rollback), under a bounded asyncio.shield (or the rollback is itself cancelled, or a wedged one hangs shutdown), then re-raises. A second cancellation landing in the shielded rollback is swallowed and the wait resumes for what is left of the bound -- the opposite of ADR 0159's `_release_dirty`, which can return early only because its connection is already out of the pool. Routed through it: `_run_grouped`'s inline arm (covering all ten grouped stage-handoff writers), `_GroupCommitter._flush` (covering the same ten with group commit enabled), the inline fused `handoff`, and `dead_letter_now`'s `_standalone` arm -- the one grouped writer that owns a second transaction of its own. `_flush` additionally rejects every member's future on the cancellation path before re-raising; a batch abandoned without rejection parked every awaiting caller forever. ADR 0159's "there is no next-borrower to inherit anything" is corrected at the sentence, keeping its remedy's genuine SQL-Server-only scope.
The correction above it reads as though `_writer_txn` covered every SQLite writer. It covers the four that carry a stage handoff. Seventeen others open their own BEGIN directly under `self._lock` and still unwind on `except Exception`, so a cancellation there would still leave the transaction open and the next writer would still inherit it -- `ingress_handoff` among them, which its own docstring calls a clone of `route_handoff`. Stating the scope is the point. A reader citing this ADR as evidence that a given writer unwinds on cancellation would rest a reliability claim on a premise the code does not support. The remainder is named by subject, not by a number, because none is allocated for it.
…ter_txn
The writer-transaction unwind landed on the four writers that carry a stage
handoff. Every other writer that opens its own BEGIN under self._lock still
unwound on `except Exception`, which asyncio.CancelledError does not match
because it derives from BaseException. A writer cancelled there would leave
its transaction open, and SQLite has one writer connection behind one lock,
so the next writer would inherit it -- and most of the store's short writers
issue no BEGIN of their own, so their COMMIT would make the abandoned
statements durable too.
Re-derived the population from the property the ADR states rather than from
its count: seventeen writers open `self._db.execute("BEGIN")` directly under
`async with self._lock:` and unwind on `except Exception`. The count matches.
All seventeen shared one identical shape -- a single `except Exception:` with
`rollback(); raise`, no else, no finally -- so all seventeen convert to the
existing helper with no new variant.
ingress_handoff is the one on the staged-pipeline path. Its own docstring
calls it a clone of route_handoff, and a cancelled handoff there would
commit the work-row's guarded DELETE while the re-ingressed rows it should
have produced never existed. The rest are auth, retention, purge and
receipt writers.
The diff is mostly the dedent of seventeen bodies losing their `try:` level.
ingress_handoff is the second stage handoff and its own docstring calls it a
clone of route_handoff, but it was left on the `except Exception` shape when
the unwind first landed. These drive it through the same three cancel points
the route_handoff cases use -- after BEGIN, on the guarded DELETE that
consumes the exactly-once token, and just before COMMIT -- plus the
cancel-twice case that proves the shielded rollback survives a second
cancellation.
The load-bearing assertion is that the guarded DELETE did not become durable.
Had it, the reply would be consumed with no child produced: the re-ingress is
gone and nothing re-derives it. Each case also asserts an unrelated writer
can still use the connection afterwards -- that writer issues no BEGIN of its
own, so it is the probe that would carry the abandoned work if a transaction
were still open -- and that the same handoff re-runs to success, which is the
at-least-once contract the unwind exists to keep.
Measured both directions against the writer, with the helper's conversion
reverted for the red arm and the reverted function confirmed AST-identical to
its pre-conversion self (docstrings aside):
unconverted: 6 cancel arms FAIL on "the failed writer left its transaction
open"; all 3 control arms PASS
converted: 9 pass, 29 in the file
The control arms are what make the red attributable. They inject an ordinary
exception at the same await and run the same assertions, so a cancel arm that
merely swallowed the cancellation could not produce this split -- and the
arms that do not move show the rig itself is sound.
…esidual
Three things a review pass found, all of them the invariant going stale the
moment it leaves the code.
The guard. Nothing stopped an eighteenth writer hand-rolling `BEGIN` again,
and it would have broken no existing test -- a new writer with the old shape
just quietly reopens the hole. tests/test_writer_txn_is_the_only_begin.py
AST-scans store.py and reds on any execute("BEGIN") outside two carve-outs
pinned by count: `_writer_txn`, and `_read`'s pooled snapshot, which runs on
a borrowed connection and already unwinds in its own `except BaseException`.
Modelled on tests/test_fixture_outbox_reset.py, with its liveness receipt and
pinned-count carve-outs, plus paired controls: a positive arm that plants the
banned shape and requires it found, and a negative arm that plants the word
in a docstring and a comment and requires it ignored.
The docstring. `_writer_txn` called itself "the store's single writer-
transaction shape", which reads as total and is not: about seventy short
writers still auto-begin under the lock. It now says which writers it covers,
says the short writers are deferred, and states the rule a new caller must
not miss -- a clean exit unwinds nothing, so any path leaving between the
BEGIN and the COMMIT must roll back itself. That rule lived only in the ADR,
which is not what the next writer reads.
The ADR. Two claims in yesterday's amendment were wrong. It cast the short
writers purely as inheriting victims; eight of them carry the same
cancellation-blind `except Exception` handler the seventeen had, so they are
the same defect reached through an implicit begin. And "a short writer does
not carry an early-exit rollback" is over-general -- attachment_incref rolls
back before its raise KeyError. Re-measured by AST: 70 short writers, 8 with
the handler and 62 without, 15 with an early return or raise inside the lock.
That tiering is what makes the deferred work schedulable instead of just
deferred, and it names the enabling step: giving `_writer_txn` the COMMIT.
Also from the review: the connection-clean probe -- the assertion that IS the
invariant -- was written out three times. One `_assert_connection_clean` now,
and the reason `record_connection_event` works as the probe (it issues no
BEGIN of its own) is stated once. The file docstring said every case also
runs under group commit; ingress_handoff has no such arm, since it takes
`_writer_txn` directly rather than going through `_run_grouped`.
|
MERGE HAZARD: PR 1147 AND PR 1162 MUST NOT BOTH LAND. Posted on both by the Manager whose Builder opened 1162. I briefed 1162 to STACK on 1147. It did not, and I verified that rather than trusting my own brief: Both are based on main. Both rewrite the same large region of messagefoundry/store/store.py. GitHub computed file lists confirm the overlap: 1162 IS SELF-CONTAINED: it defines MY READING: 1162 supersedes 1147, and 1147 should be closed rather than merged. I am NOT closing it, because it is not mine and another Manager has already routed it to the Lander. Whoever lands this pair: pick one. Landing both is the content conflict in store.py that cannot be auto-resolved. One honest limit: I tried to confirm per-symbol that 1147 four stage-handoff conversions all appear in 1162 and my grep was too crude to answer (it returned zeros that the 32-vs-14 aggregate contradicts). Treat the aggregate and the independent review as the evidence, not that probe. |
|
ADVERSARIAL REVIEW RESULT on the open question this PR left for the next brief. Commissioned by the Manager; the reviewer read both branches and re-derived every number. Ruling: do NOT give
|
|
WITHDRAWING MY MERGE-HAZARD COMMENT ABOVE. IT WAS WRONG. Posted on both PRs by the Manager who wrote it. I claimed PR 1147 and PR 1162 are independent branches that must not both land. PR 1162 does stack on 1147, and there is no unresolvable conflict. WHAT MISLED ME. I tested ancestry on the branch HEADS: That exit 1 is a true fact about the wrong question. Checking 1147 commits individually: So the only commit 1147 has that 1162 lacks is a merge of main. 1147 acquired it AFTER 1162 took its content. A HEAD-level ancestry check answers "is this exact commit included", not "is this branch work included" -- and I ran the check that was easy rather than the one that matched the claim. I even ran a control (--is-ancestor origin/main <1162>, exit 1) and read it as validating the instrument. It did validate that the command works. It did not validate that the command answers my question, which is a different property and the one that was failing. CORRECTED GUIDANCE. Land 1147 then 1162: clean, git holds the shared history. Land 1162 alone: also fine, it contains 1147 work, and 1147 is then redundant. Either is safe. Nothing needs closing on account of a conflict, because there is not one. WHAT I GOT RIGHT AND STILL STANDS: 1162 is the larger and self-contained change (32 _writer_txn sites against 1147 14, with 0 on main), so it supersedes 1147 in content. That was never the disputed part. An independent adversarial review reached the same wrong conclusion from the same HEAD-level check, and I quoted it as corroboration. Two readings agreeing is ONE measurement when both use the same broken instrument -- which is a rule I had cited to a peer earlier tonight and then failed to apply to myself. |
Correction:
|
| arm | self._db.execute("BEGIN") sites |
result |
|---|---|---|
| HEAD (converted) | 0 | 1 failed, 11 passed in 23.62s |
45ef0491e (unconverted) |
17 | 1 failed, 11 passed in 23.05s |
Identical failure on both sides:
INTAKE AUDIT COULD NOT ANSWER -- fixed_aggregate@N=24: intake audit [post_mortem]
PROBE_UNUSABLE: the store sweep failed: OperationalError: disk I/O error
(... store_rows=0 missing_accepted=0 missing_rejected=0 ...)
The only difference between the arms is sent=36 confirmed=36 versus sent=36 confirmed=35, which is the sampling jitter this smoke already tolerates. store_rows=0 and the disk I/O error are the same in both.
Conclusion: pre-existing on this branch's base, not introduced here. That is now measured, not inferred.
Two things worth someone's attention, neither in this PR's scope
- This test appears to be red on the base lineage, and it is a no-loss coverage check over the store. It deserves its own item; a
disk I/O errorreading a stopped engine's committed store is either a real defect or a test-rig assumption about this box, and the audit is explicitly designed not to tolerate an instrument that cannot answer. - Selecting the single test by node id hangs past 500s, while the whole file finishes in 23s. Something in that module is order- or fixture-dependent, which makes the failing case hard to bisect in isolation.
Unchanged
The other seven failures stand as described in the body. test_asvs_login_deadline still has its serial control arm (31 passed alone), and the four installed-hook parity failures byte-compare a hook under %USERPROFILE% against this worktree's source.
test_tooling_partition.py requires every test file to sit in one of two buckets, and the new guard was in neither, so test_every_non_engine_test_is _classified failed on all three platform legs. It goes in _STAYS_WITHOUT_IMPORTING, not the manifest. The two buckets both satisfy the classification assertion, so passing does not discriminate between them; the gate does. The `tooling=true` predicate in ci.yml matches scripts/, .github/, docs/, .claude/, ide/, pyproject.toml, constraints.lock and three tests/ files, and does NOT name messagefoundry/. What this guard catches is an eighteenth writer hand-rolling BEGIN in messagefoundry/store/store.py, which is an engine diff. In the manifest it would be deselected on the engine legs by `-m 'not tooling'` AND unreached by the tooling job, so it would run on zero legs for the one change it exists to stop -- the false-premise gate that file's docstring describes, and the mistake adversarial review caught for four other entries. Its nearest precedent is already in the list: test_sqlserver_encrypt_pass _tables.py, a guard whose subject is a store module it reads rather than imports.
Retraction: the single-test "hang" in my previous comment was my own instrumentMy comment above listed, as a second finding:
Both sentences are wrong. Withdrawn. Putting the retraction at the claim rather than only in a later summary, because that paragraph would have sent someone hunting a fixture-ordering bug that does not exist. What actually happenspytest completes, and fast. The run I called hung had already written its full summary: I re-ran it with clean plumbing -- a plain pytest finished in 20.75s and wrote everything. The invoking shell still never returned, and was open at a 400s harness timeout. So the lingering thing is a child process outliving the run and holding the shell's stdout handle, which means the parent never sees EOF. That is a teardown problem, not a pytest or ordering one. Why the correction matters for the findingIt reverses the conclusion. The failure reproduces identically whether the test is selected alone or with the whole file, so it is not order- or fixture-dependent -- which makes it easier to bisect, not harder. I had it backwards.
UnchangedThe paired-arm measurement in the comment above stands and is the load-bearing part: the same failure occurs with How I got it wrongI read a harness timeout on a shell command as a timeout of the thing the command ran. The tool reported the state truthfully -- the command had not returned -- and I substituted a cause I have really seen before (a slow or order-dependent test) for the one actually in front of me. The output file was there the whole time and said 22.09s; I did not open it until a second notification made me check. The lingering-child observation is real and worth its own look -- possibly the same family as |
The three engine legs are green; one harness leg is red on a documented pwsh flake
The remaining red is not this PR's subject
Both with the same message, which the test author wrote to pre-empt exactly this misattribution: Their own docstring adds: "The This PR touches I am reporting this from the job log rather than inferring it from the job name -- the same discipline the retraction above was written to enforce. |
|
SUPERSESSION SETTLED BY CONTENT, not by ancestry. Posted on both PRs by the Manager, closing a question that has now been asked three times. I got the ancestry question wrong once tonight by checking branch HEADS, so this check deliberately does NOT use ancestry at all. It parses Seventeen added, which matches 1162 reported count exactly, and the helper definitions themselves are in both. So 1162 is a strict content superset of 1147. That holds whatever the commit graph says, which is the point of measuring it this way: three methods now agree, and only two of them share an instrument. MERGE GUIDANCE, unchanged and now well supported: landing 1147 then 1162 is clean. Landing 1162 alone is also fine and makes 1147 redundant. There is no conflict, and my earlier claim that there was one is withdrawn above. Nobody needs to re-verify this. If a fourth check is ever wanted, it should use a fourth method -- re-running any of these three would be one measurement repeated, which is the error that produced my false warning in the first place. |
Sharpening the teardown mechanism in my retraction aboveMy retraction said the lingering thing was "a child process outliving the run and holding the shell's stdout handle, which means the parent never sees EOF." That specific mechanism is not what the evidence shows. The conclusion it supports is unchanged; the pointer for whoever investigates is not. The command was
If pytest had exited, the shell's wait would have returned and Why it hangs is unmeasured. A non-daemon thread and an un-reaped grandchild both fit; I am not picking between them without evidence, which is the mistake this thread already corrected once. Unchanged, and still the load-bearing parts: the failure is pre-existing on this branch's base (paired arms, |
PR 1147 squash-merged, so this branch carried 1147's original commits while main carried the same content under a new SHA. Three conflicts, not the one forecast: the ADR, plus store.py and the cancel-unwind test, both of which main had moved since. store.py: both conflicts were inside _writer_txn's docstring, no code. Took this branch's text, which corrects main's "single writer-transaction shape" to the narrower, true claim. tests/test_backlog1548_writer_txn_cancel_unwind.py: add/add. Main has not touched it since the 1147 squash, so ours is theirs plus the re-ingress arms. All 19 removed lines are this branch's own refactor into _assert_connection_clean. ADR 0159: resolved as a union, with three figures re-measured rather than carried. Population is 74, not 70 -- a direct-DML scan returns exactly 70 and misses four helper-mediated blocks. The severity split is 55 single-DML against 19 multi-statement or loop-driven, which is blast radius rather than handler presence. The early exits are not an exposure today; all 52 exit sites fire before the first DML, after a commit, or already roll back. The recommended remedy is a no-BEGIN/no-COMMIT guard checking in_transaction, not giving _writer_txn the COMMIT. Verified post-merge: 23 functions use _writer_txn, so nothing was dropped. Relates to BACKLOG #1548.
Conflict with main resolved (merge commit 276cdd7)PR 1147 squash-merged, so this branch carried 1147's original commits while Three conflicts, not the one forecast. The brief predicted only the ADR would conflict and
Nothing was dropped23 functions reference
ADR 0159, resolved as a unionThe false-premise correction sits outside the conflict and survives intact (lines 166-184): one Three figures were re-measured rather than carried, and two of them were wrong:
The recommended remedy is recorded as a second helper that takes the lock, issues no Measured 2026-09-15 on this box: Three places this differs from the brief, flagged rather than silently applied
Checks
|
|
CORRECTION TO MY REVIEW-FINDINGS COMMENT ABOVE. Three of the numbers I published there are wrong. Corrected by this PR author, who measured rather than transcribed, and I am recording it where I published the error. I relayed an adversarial review's census figures into this thread. The review re-derived them and was right about the direction of every correction, but three of its numbers do not survive measurement, and one of them I should have caught before passing on.
One further claim of mine is now FALSE and its correction is instructive. I wrote that The early-exit finding is also now stronger than when I relayed it: verified EXHAUSTIVELY across all 52 return and raise sites in the 74 blocks, not on a sample of six. Every one fires before the first DML, sits after a WHAT STANDS UNCHANGED: the ruling against giving Also worth recording: my brief for this conflict resolution forecast ONE conflict. There were three, because PRs 1152, 1161 and 1167 landed while it ran. The tree won over the brief, which is the instruction that made the difference. |
Full local suite: 6 failed, 17129 passed, 875 skipped (0:20:19). None of the six is this PR's.Completing the check the previous comment left open. Head The runner reported Attribution, by control run rather than by inspectionFour reproduce on
All four compare a hook installed on this machine against the repository's committed source. The The other two did not reproduce. Both pass serially on this PR's merged tree:
They failed only under The structural argument, which does not depend on the re-runsThis PR changes five files against
Unresolved, for whoever reads CIThe four installed-parity failures are a real signal about this box, not about either branch: the Hosted-only legs such as |
STACKS ON PR 1147 and must land after it. Its last two commits (
7cd10fa1d,e025c0d8a) are in this branch's history because_writer_txndoes not exist onmainyet. Land 1147 first; this PR's own three commits are58bbc8528,0d9428b70,e07962a3c.What this finishes
PR 1147 added
_writer_txn, which unwinds the SQLite writer transaction onBaseExceptionso a cancellation stops leaving a half-open transaction for the next writer to inherit. It routed only the four writers carrying a stage handoff. ADR 0159 recorded the remedy as partial and defined what was left by a property.This routes the rest of that property through the same helper, and adds a guard so the rule stops depending on anyone remembering it.
Beta framing, per CLAUDE.md section 0: nothing here is a live exposure. A cancelled writer would leave its transaction open on first deployment; nobody is running this.
The population: re-derived, and it does match seventeen
I did not take the ADR's number. I re-derived the property against the tree --
self._db.execute("BEGIN")directly underasync with self._lock:, unwinding onexcept Exception-- by AST rather than grep. Seventeen matched, so the ADR's figure was right.enqueue_messagedelete_custom_rolerelease_message_attachmentsupsert_search_presetwrite_reference_snapshotset_user_rolesrecord_receivedset_ad_group_role_mapingress_handoffset_ad_group_scope_maprecord_ack_sentpurge_message_bodiesresend_to_apply_document_stripsreingresspurge_dead_lettersdelete_userAll seventeen are converted,
ingress_handoffincluded. All seventeen shared one shape exactly -- a singleexcept Exception:doingrollback(); raise, noelse, nofinally-- so all seventeen took the existing helper and none needed a variant.Instrument notes. One
BEGINspelling exists in the file (noBEGIN IMMEDIATE/TRANSACTION/isolation_levelvariants -- checked). Line 2529'sconn.execute("BEGIN")is_read's pooled read snapshot on a borrowed connection, notself._db, and it already unwinds onBaseException. The diff is large but mostly the dedent of seventeen bodies losing theirtry:level.Proof, measured both directions
ingress_handoffgets its own cases, modelled on the existingroute_handoffones: three cancel points (afterBEGIN, on the guardedDELETEthat consumes the exactly-once token, just beforeCOMMIT), a cancel-twice case for the shielded rollback, and a byte-identical ordinary-exception control arm at each point.To measure red I reverted
ingress_handoffalone and confirmed the reverted function was AST-identical to its pre-conversion self (docstrings aside -- the only diffs were two ruff re-wraps and cp1252 mojibake in my owngit showdecode).The control arms are what make the red attributable. They inject a plain exception at the same await and run the same assertions, so a cancel arm that merely swallowed the cancellation could not produce this split -- and the arms that do not move show the rig itself is sound. File total: 31 pass.
What still is not covered, and why I stopped here
The short writers. A writer that takes the lock, issues DML with no
BEGINof its own and calls_commit()is exposed too, which the ADR's property did not capture. The store opens SQLite withisolation_level='', so sqlite3 auto-begins before DML. Measured: after a bareINSERTunder the lock and before the commit,in_transactionisTrue.Seventy such blocks, by AST census of
self._lock+ DML +_commit()with noBEGIN. They includeclaim_readyandclaim_next_fifo, so this is not auxiliary-only. Two tiers:except Exception: rollback(); raisehandler the seventeen had --put_attachment,attachment_incref,sweep_orphan_attachments,claim_fifo_heads,release_claimed,reschedule_claimed,reset_stale_inflight,replay_dead. Same cancellation-blind handler, reached through the implicit begin.Those 15 are why this is not a scripted edit:
_writer_txnneither commits nor rolls back on a clean exit, so an early exit between theBEGINand the commit needs an explicitrollback()first. Some already have one (attachment_increfrolls back before itsraise KeyError); most do not. The change that would make the seventy scriptable is giving_writer_txntheCOMMITplus an abort sentinel for the no-op exits -- named in the ADR as the enabling step, not attempted here.ADR 0159 is updated to say exactly this. The do-not-cite caveat stays, re-aimed at what is actually open.
A guard, because the invariant was only a convention
Nothing stopped an eighteenth writer hand-rolling
BEGINagain, and it would have broken no existing test.tests/test_writer_txn_is_the_only_begin.pyAST-scansstore.pyand reds on anyexecute("BEGIN")outside two carve-outs pinned by count. Modelled ontests/test_fixture_outbox_reset.py(liveness receipt, pinned-count carve-outs, stated mutation), with paired controls on the scanner itself: a positive arm that plants the banned shape and requires it found, a negative arm that plants the word in a docstring and a comment and requires it ignored.Checks
Ran:
/simplify(4 agents; see below),ruff format --check .,ruff check .,mypy messagefoundry(274 files, clean), and the full suite:pytest -n auto, 16974 passed, 8 failed, 871 skipped in 15m11s.Why CI caught a failure that local run did not
The first CI round went red on all three platform legs with
test_tooling_partition.py::test_every_non_engine_test_is_classified-- the new guard file was in neither classification bucket. Fixed in5f8c3a701.That local run was not wrong, and the meta-test is not skipped locally. It ran against a tree that did not yet contain the file. I launched the full suite before writing
test_writer_txn_is_the_only_begin.pyand read its result afterwards, so collection predated the file. The run's own junitxml says so:test_writer_txn_is_the_only_beginappears 0 times in it, whiletest_every_non_engine_test_is_classifiedappears once and passed -- correctly, for the tree it saw.So this is a stale-subject measurement, not an inert gate: I verified a result against a tree I had since changed. The cheap guard against repeating it is to re-run any whole-tree classification or scanner test after adding a file, or to treat a suite run as measuring the tree at collection time rather than at read time.
Worth knowing for the next person anyway: a new test file is unclassified until listed, and the failure surfaces only in CI if the local suite predates it.
None of the 8 looks attributable to this change. Read the evidence per row, because it is not uniform: only
test_asvs_login_deadlinehas a serial control arm. The rest rest on the failure MECHANISM -- the quoted error -- which is weaker, and I have not re-run them in isolation.test_gate_installed_parity,test_installed_coord_hooksx2,test_selfheal_installed_parity) -- they byte-compare a hook installed under%USERPROFILE%against this worktree's source.test_sandbox.py::test_the_relay_thread_ends_when_the_worker_tree_is_reaped-- process teardown, no store.test_estate_driver-- event-rate calibration, timing.test_connscale_smoke-- fails onOperationalError: disk I/O errorduring the store sweep. CORRECTED, see the comment below: this is NOT a parallel-contention artifact. It reproduces serially, alone, in 23s. Now measured with a paired control arm instead of inferred: the same file fails identically withstore.pyreverted to45ef0491e(the commit before this PR's first, where all seventeen writers still hand-rollBEGIN). Pre-existing on the base, not introduced here -- and it looks like it deserves its own item.test_asvs_login_deadline-- asserts every login branch pads to one deadline within 1 ms; under-n autothe spread was 1.0 s. Re-ran serially on this branch: 31 passed. Contention flake.Not run: the hosted-runner-only legs (for example
windows-service-smokeunder NSSM). Please read those on the PR.From /simplify -- applied, and skipped with reasons
Applied: the shared
_assert_connection_cleanprobe (three reviewers flagged the same verbatim triplication of the assertion that is the invariant); the_writer_txndocstring, which called itself "the store's single writer-transaction shape" when 70 short writers remain, and which did not state the early-exit rollback rule a new caller must follow; the test file docstring's claim that every case also runs under group commit (ingress_handoffhas no such arm); and two factual errors in the ADR paragraph I had written the day before -- it cast the short writers purely as inheriting victims when 8 carry the same blind handler, and said "a short writer does not carry an early-exit rollback" whenattachment_increfdoes.Skipped, with reasons:
purge_dead_letters' trailingreturnout of the transaction block to matchpurge_message_bodies. That would narrow the lock by one statement. The efficiency reviewer verified by AST that all 17 methods currently have byte-identical in-lock and out-of-lock statement sequences versus pre-conversion; preserving that verified property is worth more on the hot write path than shape symmetry._seed_reingressfromtests/test_reingress.pyinstead of the local_prepare_reingress. My helper calls four public store methods and encodes no contract, so the coupling would cost more than the duplication._eventwrites inenqueue_message/record_receivedintoMessageStore._event. Real duplication, including the crypto AAD tuple, but it predates this change and touches AAD construction on the hot path. Out of scope for a PR about cancellation unwinding; worth its own item.trap.stall_rollback's 50 ms sleep with an event handshake. Correct, but the pattern is PR 1147's and lives in two of its tests; changing it here would edit the branch I am stacked on.dead_letter_nowthat explains the refactor's scope rather than the code -- PR 1147's line, not mine. Left alone to avoid a stacked-branch conflict.Unresolved, for whoever picks this up
#N._writer_txnown theCOMMIT? It would delete ~20await self._commit()lines and make "early return leaves a transaction open" structurally impossible, which is the blocker for the remaining 70. It needs an explicit abort sentinel for the no-op exits and has to settle_note_commitaccounting and the_GroupCommitterpath. I did not attempt it; it deserves its own brief.