Skip to content

feat(storage): carry ConsistentRead to the data engine as ReadOptions - #559

Open
LeeroyHannigan wants to merge 8 commits into
mainfrom
feat/read-options
Open

LeeroyHannigan wants to merge 8 commits into
mainfrom
feat/read-options

Conversation

@LeeroyHannigan

Copy link
Copy Markdown
Collaborator

What

extenddb_storage::ReadOptions { consistent_read } is passed as the last parameter of DataEngine::get_item, query and scan. Before this change the engine read the client's ConsistentRead for capacity accounting and dropped it; no backend could see it.

The engine passes the client's value on GetItem, Query and Scan (false when the member is absent), each table's own KeysAndAttributes.ConsistentRead on BatchGetItem, and true on the export scan, because an export reflects every write acknowledged before the export time. A read through a global secondary index reaches storage with false: the engine refuses ConsistentRead: true on a GSI before any storage call, on Query and on Scan, as the service does. A local secondary index read carries the client's value. transact_get_items is unchanged; the service has no consistency flag there and the reads reflect every acknowledged write.

The struct is #[non_exhaustive] with Default and const fn new(bool), following the VectorSearch parameter-struct precedent: a later per-read option is added as a field, the three signatures do not change again, and a Copy field that owns its data breaks no backend outside the workspace. The doc comment states the contract by what the read returns: with true the read reflects every write acknowledged before it started; with false it may miss recently acknowledged writes. A backend that serves every read from the copy that took the write meets both by ignoring the value; a backend with followers can serve false from a follower; a quorum backend needs read and write levels that intersect for true.

The SQLite, PostgreSQL, MongoDB and Cassandra backends accept the value and do not route on it. Each get_item carries a doc comment stating what that backend does with the flag today and what a routing change would need: SQLite holds one copy; PostgreSQL serves item reads and writes from the same data_pool; MongoDB refuses a non-primary read preference in the connection string, so every read goes to the primary; Cassandra reads at the driver default ONE and commits its base-row writes at ONE, so true is not honoured above replication factor 1 until both move to LOCAL_QUORUM. That Cassandra gap predates this change and is filed as its own issue.

Why

A backend on a database with more than one copy of the data (a MongoDB replica set, PostgreSQL with a streaming replica, a Cassandra keyspace) can serve ConsistentRead: false from a copy that lags and must serve ConsistentRead: true from one that does not. Without the flag at the storage layer the backend cannot tell the two reads apart, and an application that depends on ConsistentRead: true cannot be shown the difference against ExtendDB. ADR-0024 records the decision and the options considered.

Stacking

This branch is on main and is independent of the other open changes. It touches batch_get_item.rs, which #536 also edits; whichever lands second rebases, and the overlap is the call that passes the per-table value.

Testing done

Eleven engine tests run the handlers over a storage double that records every read with the options it received: GetItem with true, false and absent; Query and Scan with the three shapes on the table and on a local secondary index; BatchGetItem with one table asking for a consistent read and another sending false in one round and nothing in the other; a GSI Query and a GSI Scan reaching storage eventually consistent, and each refused before storage with the service's message when the client asks for true; the export handler, into a temp root removed by a Drop guard, scanning every page consistent. Negative controls, each on a clean committed tree and restored after: the batch handler treating a present false as consistent, every index Query reaching storage eventually consistent, the GSI Scan refusal disabled, and the export passing the default each fail the one test named for them; passing the default on GetItem, Query or Scan, passing true for every batch table, and the double dropping the index name each fail their test too.

The four backends' integration test call sites pass ReadOptions::default(). The PostgreSQL storage-level suite (the five test binaries CI runs: 90 tests) passed against a local PostgreSQL 16. The MongoDB and Cassandra suites run in CI; their changes are the added parameter at each call site and compile under clippy with all targets.

cargo +1.97.0 fmt --all -- --check
cargo +1.97.0 clippy --all-targets -- -D warnings
cargo +1.97.0 test -p extenddb-storage -p extenddb-engine -p extenddb-storage-sqlite -p extenddb-server -p extenddb-app   # 685 passed
EXTENDDB_TEST_PG_CONNECTION_STRING=... cargo +1.97.0 test -p extenddb-storage-postgres --test vector_control_plane --test put_create_race --test twi_conflict --test stream_shard_retention --test backup_restore   # 90 passed

Checklist

  • I have read CONTRIBUTING.md
  • All tests pass (cargo test --workspace --lib; the integration suites run in CI)
  • Code is formatted (cargo fmt --check)
  • Clippy is clean (cargo clippy --all-targets -- -D warnings)
  • I have added or updated tests for new functionality
  • I have updated documentation if behavior changed
  • Breaking changes are noted below (if any)
  • If this changes the wire protocol, Storage trait, auth model, on-disk
    format, or public CLI surface, an RFC has been accepted or is linked
    below. Otherwise, an ADR captures the decision (link below).

ADR / RFC: ADR-0024, read consistency reaches storage as a ReadOptions parameter. RFC-0002 records that the team maintains all backends and that Storage trait changes land across all of them at once, which this change does. The storage design (04-component-storage.md) describes the type under DataEngine and marks its PostgreSQL read replica section as a proposal; the high availability design (11-high-availability.md) gains status notes on section 8 and Stage 1, which this change completes in a different shape; the storage extension manual gains a Read options bullet under DataEngine.

Breaking changes

DataEngine::get_item, query and scan gain a required last parameter, opts: ReadOptions. A backend outside this workspace stops compiling until it adds the parameter; a backend that holds one copy of the data adds it and ignores it. No wire body, status, header or capacity number changes for any client.


By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache License 2.0 and I agree to the Developer Certificate of
Origin (DCO). See CONTRIBUTING.md for details.

Add `ReadOptions { consistent_read }` to extenddb-storage beside
`VectorSearch` and pass it as the last parameter of `DataEngine::get_item`,
`query` and `scan`. The struct is `#[non_exhaustive]` with `new` and
`Default`, so a `Copy` field that owns its data, added later, widens no
signature and breaks no caller.

GetItem, Query and Scan pass the request's ConsistentRead (false when
absent); BatchGetItem passes each table's KeysAndAttributes.ConsistentRead
on its per-key get_item calls; the export scan passes true, because an
export reflects every write acknowledged before the export time. A global
secondary index read reaches storage with consistent_read false because the
handler refuses ConsistentRead true on a GSI before storage is called, on
Query and on Scan; a local secondary index read carries the client's value.

The SQLite, PostgreSQL, MongoDB and Cassandra backends accept the value and
do not route on it; each parameter's doc comment states what that backend
does with the flag today and what a routing change would need. Integration
test call sites pass ReadOptions::default().

Engine tests with a recording storage double pin the flag on GetItem, Query,
Scan and BatchGetItem (per table, with false and absent told apart), the
LSI read carrying the client's value on Query and Scan, the GSI read as
eventually consistent on Query and Scan, the GSI refusal before any storage
call on both, and the export's consistent scan.
…d the backend author manual

ADR-0024 records the decision: a non_exhaustive ReadOptions struct in
place of the bare bool and the ConsistencyLevel enum the high availability
design proposed, the export scan read consistent, the GSI and LSI rules,
and the breaking change for a backend outside the workspace. The storage
design describes the type under DataEngine and marks the PostgreSQL read
replica routing in its section 5.3 as a proposal rather than code. The high
availability design gains a status note on section 8 and Stage 1. The
storage extension manual gains a Read options bullet under DataEngine.
…that does not

The storage design and the backend author manual said every in-tree backend
serves both ConsistentRead values from the copy that took the write. The
Cassandra backend reads at ONE against a keyspace whose replication factor
defaults to 3, which ADR-0024 records under Consequences and issue #560
tracks. Both sentences now name the three backends the statement holds for
and state what the Cassandra backend does.
…test

#401 added crates/storage-postgres/tests/gsi_online_build.rs on main. Its one
DataEngine::get_item call passes two arguments, and this branch gives
get_item a third parameter, ReadOptions, so the test did not compile once
main was merged in (E0061). The call now passes ReadOptions::default(). The
PostgreSQL backend ignores ReadOptions, so the test reads as it did before.
The Deciders line named @lhnng, which is not a GitHub account. ADR-0004, ADR-0005 and ADR-0023 name the same author as @LeeroyHannigan.
crates/storage-cassandra/tests/direct/backup_engine.rs: main removed
the catalog V005 tests (assert_catalog_lacks_v005 and the test that ran
table and item operations on a catalog without V005) when the Cassandra
catalog moved to the migration runner. This branch had added the
ReadOptions argument to the get_item calls inside that block. The merged
file keeps main's removal; the ReadOptions argument stays on every
remaining get_item, query and scan call in the file.

crates/storage-cassandra/tests/direct/index.rs: main added setup_engine
to the helpers import. This branch added the ReadOptions import. The
merged file has both.

docs/adr/README.md: main added rows for ADR-0023, ADR-0026 and ADR-0027.
This branch added the row for ADR-0024. The merged table lists all four
in number order.

Call sites main added since the branch point take the new parameter:
get_item in crates/storage-sqlite/src/data/update_item_tests.rs (five
calls), in crates/storage-postgres/tests/table_snapshot.rs (one call),
and in the Cassandra direct tests
transact_write_rollback_after_commit.rs,
transact_write_stream_records.rs, transact_write_stream_size_sweep.rs
and transaction_ledger.rs; scan in transact_write_stream_tables.rs (two
calls). Each passes ReadOptions::default().

crates/engine/src/read_options_tests/recording.rs: the storage double
follows the trait changes main made. StreamEngine::get_stream_records
takes the stream ARN, BackupEngine no longer has the continuous backups
methods, and StorageEngine requires SnapshotEngine, so the double gains
an open_table_snapshot that is not reached by the handlers under test.
crates/storage-cassandra/tests/direct/backup_engine.rs: main replaced
the `BackupEngine` import with `BackupCatalog`, `JobPermit` and
`LegacyBackupReader` and rewrote the backup tests for the catalog
index. This branch added `ReadOptions` to the same import and passed
`ReadOptions::default()` at the six `get_item`, `query` and `scan`
call sites. The result keeps main's import list and tests with
`ReadOptions` added to the list and the six call sites carrying the
parameter.

crates/storage-postgres/tests/backup_restore.rs: the same change on
the PostgreSQL side. The result keeps main's import list and tests
with `ReadOptions` added and the one `get_item` call site carrying
`ReadOptions::default()`.

crates/engine/src/read_options_tests/recording.rs: the recording
storage double implemented `BackupEngine`, which main removed from the
`StorageEngine` supertraits in favour of `BackupCatalog` and
`LegacyBackupReader`. The double now implements those two traits, with
every method unimplemented as the old one was, since no read handler
reaches them.

crates/engine/src/read_options_tests.rs: `OperationContext` gained a
`backup: Arc<BackupRuntime>` field on main. The test context fills it
with a one-slot runtime and no configured store.

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