fix(session): lock session header before command persist - #146
cursor[bot] wants to merge 43 commits into
Conversation
Store participant and published-release identity for SessionState::Created with exact replay and fail-closed rebinding. Command-replay persistence stays outside this first slice.
Assert the Database error message and source, and fail the replay SELECT after ON CONFLICT by redirecting search_path so classify runs instead of the insert.
Linux llvm-cov leaves the isolated query_one ? tail uncovered unless the Err arm is an explicit match. Keep the search_path redirect test.
Linux branch coverage missed the later AND operands of exact-replay classification. Rebind each stored field independently, and prove a domain-legal u64::MAX creation time fails closed as ValueOutOfRange.
The replay SELECT failure constructed Database evidence without checking its safe display text or source, leaving those two production lines uncovered on Linux.
SHOW transaction_isolation can fail after the caller transaction is already aborted. Persist must surface that as a typed database error instead of leaving the probe Result uncovered.
Satisfy clippy::manual_let_else in the library test that instantiates AssessmentSessionPersistenceError::Database.
Name instrument_version_ref in the public persist contract and assert the committed version column. Keep TRACEABILITY, changelog, and as-built schema at Active PR #61 rather than promoting the slice to protected-main truth. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Restore persisted created-session identity from PostgreSQL without asking whether the original release still accepts new sessions, so later suspend or retire cannot rewrite provenance. Missing rows return none; later stored states and malformed lookup references fail closed. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Name the opened successor so TRACEABILITY, as-built schema, and ERD point at the persist-and-load head instead of the persist-only #106 slice. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Review of #146 (
e66e08a)The header-row lock is in the right place.
require_existing_created_identitytakesSELECT … FOR UPDATEbefore insert, count, andsession_stateupdate, so a concurrent Activate-only persist cannot count a prefix underREAD COMMITTEDand then overwrite Pause/Resume.command_persist_locks_session_header_until_caller_commitsproves the waiter hitslock_timeoutwhile the first transaction still holds the row, and the sequential stale-prefix test still provesConflictingReplayafter commit.Do not merge #129, #125, #121, #109, #106, or #61 in parallel. Prefer this lock head over those predecessors. Do not land #100. Do not merge #138 in parallel; it still lacks this lock.
Remaining buyer gap (not a lock defect):
persist_assessment_sessionstill accepts anySessionState::Createdaggregate, includingfrom_persisted_created. A start after suspend/retire can bypassAssessmentSession::newif a caller persists a reconstituted identity. HTTPPOST /v1/sessionsmust not call persist directly. The start boundary belongs on a successor of this head (created_session_for_start/start_created_assessment_session).Independent last-push review is still required. This comment is not approval.
Sent by Cursor Automation: Fix Issues
Name the start-boundary landing vehicle so TRACEABILITY, ERD, UML, and as-built schema point at this head instead of the #146 lock predecessor. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Review
This lock successor is the right repair class, but do not land it in parallel with #154.
command_persist_locks_session_header_until_caller_commitspersists created identity in the same uncommitted transaction that later holdsFOR UPDATE. A second writer underREAD COMMITTEDcannot see that row, so it failsMissingCreatedIdentityinstead of waiting on the header lock. The postgres persist suite was unchecked on this head.#154 keeps the same
SELECT … FOR UPDATE, commits created identity first, then proves:
- a second writer hits lock timeout on the existing header row;
- two concurrent workers cannot rewind a paused projection (
InvalidStoredIdentitywas the RED failure before the lock).Prefer #154 over this head, #129, #125, #121, #109, persist-only #106, and #61. Do not open a fourth persist-lock PR. HTTP session-creation remains #149; do not mix it onto this persist slice.
Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Queue note
This head's sequential stale-prefix reject is real. The remaining concurrent rewind (SELECT without FOR UPDATE) is now on #161, which also maps POST /v1/sessions and the as-built OpenAPI 3.2.0 document.
Prefer #161 over this head, #129, #125, #121, #109, persist-only #106, and #61. Do not merge this persist-only lock in parallel.
Sent by Cursor Automation: Fix Issues
Processing consumption rows now require claim_deadline_at after #81. Seed the wall-clock deadline and prove binary restore preserves it.
|
Pushed a recovery-fixture repair on this successor to closed #129: seed Do not merge until exact-head rust/coverage and independent last-push approval succeed. Rebase onto #138 after that landing. Never self-approve. |
Uncommitted created-session inserts are invisible to a second READ COMMITTED writer, so FOR UPDATE returned MissingCreatedIdentity instead of waiting. Commit the header first, then assert SQLSTATE 55P03.
|
Pushed
The test now commits the created row first, holds Do not merge until exact-head rust/coverage and independent last-push approval succeed. Rebase onto #138 after that landing. Never self-approve. |
… success The library Display wrap test cannot use expect_err because postgres::Client is not Debug, and the let-else success arm is unreachable coverage.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Production coverage missed isolated query ? arms after the header lock when the command or session relation is gone. Each case uses a fresh transaction so an aborted persist cannot hide the next probe.
Production coverage still missed isolated execute ? arms: empty-history UPDATE of the session projection, and INSERT of the first command row. Sinks raise inside BEFORE triggers so the rust ? mapping is executed.
Linux branch coverage missed the later && arms on created-identity compare and command replay classify. Each stored field is rebound alone so every conjunct executes false.
|
Closing as superseded by protected-main behavior rather than reconciling this 43-commit Draft. Current protected main |


Why
PR #129 rejects a stale shorter command history before rewriting
session_state. That check still runs after the insert loop and without locking theassessment_sessionheader row. UnderREAD COMMITTED, a concurrent Activate-only worker can count a prefix, wait for Activate+Pause to commit, then writesession_state = activeoverpaused. Load then fails closed and the buyer cannot continue.What this PR does
SELECT … FOR UPDATEbefore inserting or counting commands.lock_timeoutwhile the first transaction still holds the row, and that the paused session remains loadable after commit.Out of scope
Test plan
cargo test --lib stale_shorter_command_history -- --nocapturecargo test --test session_persisted_identity --test session_aggregate_lifecycle --test session_state --test session_release_binding --test session_command_replay_contract --test documentation_architecture_contract --test traceability_active_pr_contractcargo clippy --all-targets -- -D warningscargo test --test postgres_assessment_session_persistence(needsTEST_DATABASE_URL)This is the successor to #129. Prefer this head over #129, #125, #121, #109, persist-only #106, and #61. Do not land #100. Do not merge until exact-head checks and independent last-push approval are satisfied.