Skip to content

fix(sqlite): preserve POSIX locks for verified readers - #4803

Open
ScriptedAlchemy wants to merge 3 commits into
masterfrom
fix/4513-verified-reader-posix-locks-20261108
Open

ScriptedAlchemy wants to merge 3 commits into
masterfrom
fix/4513-verified-reader-posix-locks-20261108

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Scope — Refs #4513 (partial; do not close issue)

Extract the SQLite-only verified-reader lock fix from the held draft integration PR #4776 as an independently reviewable three-file change based on master 91c72aba7750a398dd28e83d64bf0c6d6fef4172. No Kev, model scoring, decision-model, release, or infrastructure changes. Do not merge #4776 as a consequence.

A verified foreign reader used to independently File::open-pin the database on Unix; closing that raw descriptor can release other SQLite connections' process-wide POSIX record locks, permitting an external writer or journal-mode change while a live daemon transaction remains active.

  • Unix verified snapshot and immutable readers now rely on SQLite's own descriptor; verify dev/inode identity before and after SQLITE_FCNTL_HAS_MOVED and refuse when the VFS cannot prove it.
  • Windows retains its original pinned-handle behavior. The ordinary attachment worker's shared descriptor lifetime and existing identity checks are retained.
  • New real cross-process Rust regression iterates rollback/WAL × verified snapshot/immutable, checking external writer/journal-change exclusion while the original writer holds a transaction, then committed data and integrity. The sessions consumer test confirms the Unix reader retains only the SQLite descriptor.

Verification / strict limits

  • The commit is a single-parent, 3-file patch against master; verified no unrelated files via GitHub compare. The source fix and tests were copied byte-for-byte from the issue-specific hunks already staged on held fix: consolidate follow-up application issue repairs #4776, without merging that PR.
  • Previous separate-process native SQLite/Linux probes documented in #4513 reproduced the lock-loss mechanism; they are not this Rust/Bazel test run.
  • This environment has no repository checkout or Bazel binary. No Rust tests, Bazel, hosted CI, or native macOS tests are claimed passed. CI must run the nonzero //crates/tracedecay-rusqlite-runtime:unit_test and affected sessions test targets on this exact head. Native macOS cross-process regression must also be executed before declaring the platform qualified; do not weaken tests/CI.

Remaining issue acceptance

This patch does not fix new-store sidecar namespace ownership: pre-existing -wal, -shm, and -journal can be overwritten even during successful initialization; failed-initialization discard_created also unlinks unproven sidecars and has a pathname replacement TOCTOU. Those require separately justified race-safe admission/teardown work and real tests, not a preflight exists() band-aid. Exact daemon/external sqlite3 reader endurance, fail/restart recovery, native macOS, and final proven state on master remain open.

Please review and run hosted Bazel gates before the ordinary TraceDecay PR-maintenance task considers a merge. Refs #4513 only.

Refs #4513

Keep Unix verified readers on SQLite-owned handles, retain pathname
identity checks around HAS_MOVED, and fail closed without VFS proof.
Carry the cross-process rollback/WAL lock regressions separately from
the held application/decision-model integration PR.
@changeset-bot

changeset-bot Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f647c8b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown
Owner Author

Native macOS acceptance is not covered by ordinary PR CI: current .github/workflows/ci.yml gates Test macOS on workflow_dispatch + run_os=true + native_platform=macos|all (the new PR check is correctly skipped, not passed). Run the existing CI workflow on branch fix/4513-verified-reader-posix-locks-20261108, inputs run_os=true, native_platform=macos, native_targets=//crates/tracedecay-rusqlite-runtime:unit_test //crates/tracedecay-sessions:unit_test. Confirm nonzero Rust regressions in the hosted Bazel output and link the job to #4513. Linux PR gates and this separate macOS run must be inspected on the exact tested head before any platform-qualified merge/closure claim. No workflow or infrastructure changes requested.

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refs #4513 — review of exact head 83253b18e2d73ba72e6ac005fc71b242feeb608e.

The Unix change removes the extra raw descriptor from verified readers and preserves the NOTFOUND refusal for their HAS_MOVED check. I have not established an additional production defect in this narrow patch in this review; the remaining actionable request is to strengthen behavioral qualification of the new lifecycle regression (inline), not to conflate this PR with #4805's separate initialization bugs.

Required evidence before treating this as qualified:

  • Execute nonzero bazel test //crates/tracedecay-rusqlite-runtime:unit_test //crates/tracedecay-sessions:unit_test --test_output=errors on the exact resulting head. The PR-triggered run https://github.com/ScriptedAlchemy/tracedecay/actions/runs/42434926345 was still in progress when checked; its existence is not a pass.
  • Run the existing native macOS workflow with run_os=true, native_platform=macos, and those same native_targets. Record the tested SHA and actual nonzero results. No native macOS result is established by this review, and a skipped platform job is not evidence.
  • For full #4513 acceptance, retain a separate isolated-daemon test using an ordinary external read-only sqlite3 client while writes/checkpoints and verified-reader teardown occur, followed by restart, committed-row verification and integrity checks. The subprocess Rust helper is useful lock evidence, but it is not itself the reported daemon/CLI journey. Do not make that client immutable=1 to bypass normal locking.

No Rust/Bazel execution is claimed here. Keep this independently reviewable, use Refs #4513, and leave the issue open until full acceptance is on master. Ordinary merging remains with the existing maintenance owner; held draft #4776 and release #4763 remain excluded.

assert_eq!(
reader
.connection()
.query_row("SELECT 1", [], |row| row.get::<_, i64>(0)),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Also assert committed-row visibility across verified-reader teardown.

SELECT 1 proves the returned connection executes a constant, but not that this newly exercised reader returns the correct database contents. The snapshot constructor already reads sqlite_schema, so this is not a claim that it takes no read lock. The missing assertion is data visibility through this particular verified-reader/drop lifecycle while the independent writer is active.

For the snapshot arm, read sample.value and require the old committed value (1), then use the existing separate-process read:1 helper before and after dropping the verified reader; after the writer commits require a new reader to see 2 and pass integrity. The existing external_read_only_wal_reader_survives_live_writer_identity_checks regression covers identity checks, not this verified-reader destruction path.

Keep immutable semantics separate: the WAL fixture creates sample after entering WAL mode, so its schema/data need not yet exist in the main file. Do not mechanically replace the constant with a current-WAL-data expectation in the immutable arm, or claim that arm qualifies live external reading. Preserve the lock-exclusion test and add the appropriate real-row/lifecycle assertions with nonzero Linux and native macOS Bazel results.

Exercise real WAL and rollback-mode snapshots through the verified
reader path, with separate-process read-only checks before and after
reader teardown and writer commit. Keep immutable semantics distinct.

Refs #4513

Copy link
Copy Markdown
Owner Author

Refs #4513 — pushed e9cf7d5 as an expected-old-head, nonforce fast-forward on this issue-only branch. The cross-process verified-reader regression now asserts the old committed value through a live read snapshot, separate-process mode=ro reads and quick_check before/after verified-reader teardown, and the newly committed value after writer commit. Immutable reader remains a distinct lock-lifetime case (no assumption it sees uncheckpointed WAL). A disposable Linux SQLite 3.46.1 equivalent read/commit sequence passed in both rollback and WAL mode, but this is not the repository Rust/Bazel run. Exact-head CI is queued. Please retain the open qualification hold for native macOS and the full daemon/external sqlite3/restart journey. The previous hosted Linux fleet did pass the rusqlite and sessions unit targets, but was not a green workflow due to unrelated automation-scheduler failures and a global-db timeout. No approval/merge or issue closure claimed.

Verify real committed-row visibility through both snapshot and immutable
readers while a separate-process contender confirms SQLite writer locks.
Checkpoint only the immutable WAL fixture's seed; leave ordinary WAL
snapshot coverage uncheckpointed. Preserve teardown/commit assertions.

Refs #4513

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +617 to 618
#[cfg(not(unix))]
_opened_file: OpenedDatabaseFile,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Immutable reader close releases writer locks

When an immutable VerifiedReader closes during a same-process writer transaction, its descriptor releases the writer's POSIX locks. Immutable SQLite handles bypass inode-lock bookkeeping, so external writers can enter before the transaction ends.

Learn more

A POSIX record lock belongs to the process, not to the file descriptor that acquired it. Closing any descriptor for the database inode releases locks held by other SQLite connections in that process. Ordinary SQLite Unix connections can defer a close while their shared inode record has locks; an immutable=1 connection uses a no-lock path that does not participate in that bookkeeping. Removing the retained pin eliminates one unsafe close, but closing the immutable SQLite handle itself still releases the writer's lock. The immutable reader is opened by open_verified_immutable_reader, while open_immutable_reader specifies immutable=1.

Example: A daemon holds BEGIN IMMEDIATE on a database in DELETE mode. It opens and drops an immutable verified reader of that database. The reader closes its own database descriptor; a separate process can now acquire a write lock while the daemon transaction is still active.

Recommended fix: Ensure immutable SQLite descriptors cannot close while a same-inode SQLite writer holds POSIX locks. Use a VFS/reader lifecycle with effective shared-inode deferred-close coordination, then run the new cross-process DELETE and WAL probes against the exact implementation.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
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