Skip to content

fix: db() reflects owned-state commits (read-your-writes)#1385

Open
bplatz wants to merge 1 commit into
mainfrom
fix/owned-state-transact-cache-staleness
Open

fix: db() reflects owned-state commits (read-your-writes)#1385
bplatz wants to merge 1 commit into
mainfrom
fix/owned-state-transact-cache-staleness

Conversation

@bplatz

@bplatz bplatz commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Problem

Closes #1330

Calling fluree.db(ledger_id) after committing through the owned-state API could return a previously cached GraphDb view that predates those commits — a read-your-writes violation.

The owned-state API (insert / update / transact(ledger, ...) and the transact(...).execute() builder) consumes a caller-supplied LedgerState, commits, and publishes to the nameservice, then returns a new LedgerState — but it never wrote that state back into the LedgerManager cache. A subsequent db() / ledger_cached() on a warm cache therefore served the pre-commit view.

The guarded builder path (fluree.stage(&handle).execute()) was never affected: it holds the ledger write lock across stage + commit and replaces the cached state via finalize_commit. The server transaction path (LocalCommitter) uses that guarded path, so server-side read-your-writes was already correct — this only affected the embedded owned-state API.

Fix

  • Add Fluree::finalize_owned_commit, a shared post-commit tail for the owned-state methods that writes the committed state back into the cache (via sync_owned_commit_to_cache) before computing the indexing status and triggering background indexing. This also consolidates six duplicated post-commit tails into one.
  • sync_owned_commit_to_cache is monotonic (skips when the cache has advanced past the new state) and only touches an already-cached handle — an uncached ledger loads fresh from the nameservice head on next access. On a refresh_index failure it evicts the entry so the next access reloads rather than serving stale state.
  • Add LedgerManager::get_loaded_handle to fetch a cached handle without forcing a load.

Tests

  • Drop the db_at_t workarounds in it_cyclic_bgp_probe and it_join_batched_overlay (the sites the issue called out).
  • Add it_read_your_writes as a direct regression test.
  • it_notify_incremental: in-process commits no longer leave the writer's own cache stale, so these now create a genuine cross-writer gap via a peer LedgerManager (the realistic scenario notify catch-up exists for).
  • it_query_sparql_indexed: head-with-novelty views now take the production overlay fast path, which renders a decimal AVG as a precision-preserving string.

Verification

  • Full fluree-db-api suite (2253 tests) + fluree-db-server / fluree-db-consensus tests pass.
  • cargo fmt --all --check and cargo clippy --all --all-features --all-targets -- -D warnings clean.

Docs

Updated docs/getting-started/rust-api.md and docs/concepts/time-travel.md to state that a writer reads its own writes in-process, with eventual consistency remaining a cross-process concern.

The owned-state API (`insert`/`update`/`transact(ledger, ...)` and the
`transact(...).execute()` builder) committed and published to the
nameservice but returned a new `LedgerState` without writing it back to
the `LedgerManager` cache. A subsequent `fluree.db()` / `ledger_cached()`
on a warm cache then served the pre-commit view — a read-your-writes
violation (issue #1330).

Add `Fluree::finalize_owned_commit`, a shared tail for the owned-state
commit methods that writes the committed state back into the cache
(monotonic, only when already cached) before computing indexing status
and triggering background indexing. This also consolidates six
duplicated post-commit tails. The guarded builder path is unaffected —
it already updates the cache via `finalize_commit` under the write lock,
which is why the server transaction path was never affected.

- `LedgerManager::get_loaded_handle`: fetch a cached handle without
  forcing a load from the nameservice.
- Drop the `db_at_t` workarounds in `it_cyclic_bgp_probe` and
  `it_join_batched_overlay`; add `it_read_your_writes` regression test.
- `it_notify_incremental` now creates a genuine cross-writer gap via a
  peer `LedgerManager` (in-process commits no longer leave the writer's
  own cache stale — that's the fix).
- `it_query_sparql_indexed`: head-with-novelty views now take the
  production overlay fast path, which renders a decimal AVG as a string.

Closes #1330
@bplatz bplatz requested review from aaj3f and zonotope June 27, 2026 01:51
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.

fluree.db() can serve a stale cached view after subsequent commits (read-your-writes violation)

1 participant