diff --git a/docs/design/01-requirements.md b/docs/design/01-requirements.md index 394412bd2..b35a683f5 100755 --- a/docs/design/01-requirements.md +++ b/docs/design/01-requirements.md @@ -164,7 +164,7 @@ These are the core CRUD operations. All must be fully implemented. - REQ-DATA-012: TransactGetItems must be atomic — consistent snapshot of up to 100 items - REQ-DATA-013: All data operations must support legacy `Expected` and `ConditionalOperator` parameters (converted internally to expressions) - REQ-DATA-014: All write operations must trigger stream event capture when streams are enabled on the table -- REQ-DATA-015: `ConsistentRead` parameter must be accepted on GetItem, Query, Scan, and BatchGetItem. When a read replica is configured, `ConsistentRead=false` reads must route to the replica; `ConsistentRead=true` reads must route to the primary. When no replica is configured, all reads are strongly consistent. Capacity calculations must reflect the consistency mode regardless of replica configuration (0.5 RCU for eventually consistent, 1.0 RCU for strongly consistent). +- REQ-DATA-015: `ConsistentRead` parameter must be accepted on GetItem, Query, Scan, and BatchGetItem. Capacity calculations must reflect the consistency mode (0.5 RCU for eventually consistent, 1.0 RCU for strongly consistent). No in-tree backend routes a read on the parameter. Routing `ConsistentRead=false` reads to a follower is proposed for the PostgreSQL backend (REQ-PG-008) and not implemented. ### 3.2 Control Plane Operations (In Scope) @@ -449,7 +449,7 @@ The catalog database stores extenddb metadata: table definitions, indexes, tags, - REQ-PG-005: Use parameterized queries for all read operations to prevent SQL injection - REQ-PG-006: Schema migrations managed via embedded migration files - REQ-PG-007: Support PostgreSQL 14+ -- REQ-PG-008: Support optional read replica connection for eventually consistent reads. When `read_replica_url` is configured, `ConsistentRead=false` reads (GetItem, Query, Scan, BatchGetItem) route to the replica pool. All writes and `ConsistentRead=true` reads always use the primary pool. +- REQ-PG-008 (proposed, not implemented): Support an optional read replica connection for eventually consistent reads. When a `read_replica_url` is configured, `ConsistentRead=false` reads (GetItem, Query, Scan, BatchGetItem) would route to the replica pool, and all writes and `ConsistentRead=true` reads would use the primary pool. The backend has no such setting today: `PostgresStorageConfig` (`crates/storage-postgres/src/config.rs`) refuses unknown fields, so a configuration carrying `read_replica_url` stops the server at startup. Every item read and write runs on the data pool (`data_pool`). Design document 04, section 5.3, sketches the proposal with one replica URL; design document 11, Stage 2, plans the same routing with a `replicas` list. ## 9. Expression Engine Requirements diff --git a/docs/design/02-high-level-design.md b/docs/design/02-high-level-design.md index 3a680e615..02472aecd 100755 --- a/docs/design/02-high-level-design.md +++ b/docs/design/02-high-level-design.md @@ -265,18 +265,18 @@ sequenceDiagram - sqlx provides native async PostgreSQL support - The storage trait being async allows backends that use network I/O (e.g., a future DynamoDB-backed backend for testing, or a distributed storage backend) -### 4.7 Read Consistency via Optional Read Replica +### 4.7 Read Consistency and the Proposed PostgreSQL Read Replica -**Decision:** All reads are strongly consistent by default. Eventually consistent reads (`ConsistentRead=false`) are supported by routing to an optional PostgreSQL streaming replica. +**Decision:** The PostgreSQL backend serves every item read from `data_pool`, the pool that takes the writes, so its reads are strongly consistent whatever `ConsistentRead` says. The flag is accepted and charged (0.5 RCU for eventually consistent, 1.0 RCU for strongly consistent). Routing `ConsistentRead=false` reads to a PostgreSQL streaming replica is a proposal (design document 04, section 5.3; REQ-PG-008; design document 11, Stage 2) and not code that exists. **Rationale:** - DynamoDB's eventual consistency is a consequence of reading from storage replicas that haven't received the latest write yet — it's not artificial delays or queues - Strongly consistent is strictly stronger than eventually consistent — any application that works against eventually consistent DynamoDB also works against a strongly consistent backend -- For migration testing (relational → DynamoDB), teams need to surface bugs where application code assumes strong consistency on default reads. A real read replica provides genuine staleness without synthetic delays -- The read replica is opt-in via `storage.postgres.read_replica_url`. When unset, all reads hit the primary — simple, correct, and matching what DynamoDB Local and other emulators do -- Writes always go to the primary. `ConsistentRead=true` always reads from the primary. Only `ConsistentRead=false` reads route to the replica -- Capacity calculations always reflect the consistency mode (0.5 RCU for eventually consistent, 1.0 RCU for strongly consistent) regardless of whether a replica is configured -- This requires zero changes to the storage trait, engine, or server — it's entirely contained in the PostgreSQL backend's pool selection logic +- For migration testing (relational → DynamoDB), teams need to surface bugs where application code assumes strong consistency on default reads. A read replica would return values that lag the primary without synthetic delays +- In the proposal the replica is opt-in; when it is unset, all reads go to the primary, which is what the backend does today +- Writes always go to the primary. `ConsistentRead=true` always reads from the primary. In the proposal only `ConsistentRead=false` reads route to the replica +- Capacity calculations always reflect the consistency mode (0.5 RCU for eventually consistent, 1.0 RCU for strongly consistent) whether or not a replica exists +- The proposal routes on the flag inside the PostgreSQL backend, so it needs the storage trait to carry the flag (design document 11, Stage 1); with that in place it needs no change to the engine or server and is contained in the backend's pool selection ## 5. Data Flow diff --git a/docs/design/04-component-storage.md b/docs/design/04-component-storage.md index d3591a5bd..e3e217741 100755 --- a/docs/design/04-component-storage.md +++ b/docs/design/04-component-storage.md @@ -419,6 +419,11 @@ The PostgreSQL backend uses two categories of tables: ### 5.2 Connection Pooling +The sketch below is a proposal for a read replica mode, not code in this repository; section 5.3 states what the +backend does today. It predates the split into a catalog pool and a data pool: its `pool` stands for today's +`data_pool`. `PostgresStorageConfig` refuses unknown fields, so a configuration carrying `read_replica_url` or +`read_replica_pool_size` stops the server at startup. + ```rust use sqlx::postgres::PgPoolOptions; diff --git a/docs/design/08-component-config.md b/docs/design/08-component-config.md index 8e7e59f15..a95678b32 100755 --- a/docs/design/08-component-config.md +++ b/docs/design/08-component-config.md @@ -49,7 +49,6 @@ Environment variables use double-underscore (`__`) as a nesting separator, prefi | `server.port` | `EXTENDDB__SERVER__PORT` | | `server.bind_addr` | `EXTENDDB__SERVER__BIND_ADDR` | | `storage.postgres.connection_string` | `EXTENDDB__STORAGE__POSTGRES__CONNECTION_STRING` | -| `storage.postgres.read_replica_url` | `EXTENDDB__STORAGE__POSTGRES__READ_REPLICA_URL` | | `storage.sqlite.path` | `EXTENDDB__STORAGE__SQLITE__PATH` | | `auth.provider` | `EXTENDDB__AUTH__PROVIDER` | | `auth.encryption_key` | `EXTENDDB__AUTH__ENCRYPTION_KEY` | @@ -111,10 +110,6 @@ backend = "postgres" # "postgres" | future: "sqlite", "mysql" [storage.postgres] connection_string = "postgresql://localhost:5432/extenddb" # Set credentials via env var in production pool_size = 20 -read_replica_url = "" # Optional: PostgreSQL streaming replica for eventually consistent reads. - # When set, ConsistentRead=false reads route here. When empty, all reads - # use the primary (strongly consistent). Recommended for migration testing. -read_replica_pool_size = 20 # Pool size for the read replica (defaults to pool_size if unset) connection_timeout_secs = 5 statement_timeout_secs = 30 diff --git a/docs/technical-debt.md b/docs/technical-debt.md index 59bd8819a..7b67ac387 100755 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -14,7 +14,7 @@ Last updated: 2026-09-16 | # | Item | Location | Priority | Origin | |---|------|----------|----------|--------| | F-1 | `AttributesToGet` (legacy API) not supported | `core/types/batch.rs:27` | Low | P6 | -| F-2 | `ConsistentRead=false` not routed to read replica | `core/types/item.rs:166` | Low | P5 | +| F-2 | `ConsistentRead=false` is served the same way as `ConsistentRead=true` on every backend; the PostgreSQL read replica mode that would route it is proposed, not built (REQ-PG-008) | `core/types/item.rs:176` | Low | P5 | | F-3 | Import/export: full Ion parser not implemented (JSON subset only) | `engine/import_export.rs:339` | Medium | P24 | | F-4 | Import/export: full Ion writer not implemented (JSON subset only) | `engine/import_export.rs:433` | Medium | P24 | | F-5 | `PutItem` returns `None` for `Item` field instead of omitting it | `server/lib.rs:235` | Low | P2 |