feat(jepsen): M5a — dynamodb-append-multi-table-workload#916
Conversation
Second slice of Composed-1 M5a per PR #905 design doc §3.3 + PR #911 checklist. New self-contained workload variant of the DynamoDB list-append test that exercises cross-table 2PC, which the single-table elastickv.dynamodb-workload cannot do. Background: kv/shard_key.go's dynamoRouteKey normalises every DynamoDB table-meta / item / GSI key for one table to a single per-table route key (!ddb|route|table|<base64(name)>). Single- table workloads always route to one shard regardless of partition-key value, so dispatchMultiShardTxn / commitSecondaryTxns / the ErrTxnSecondaryRouteShiftedAfterPrimaryCommit sentinel never fire (codex P1 on PR #905 ffb9c73). This workload: * Creates N=4 tables (jepsen_append_t1 .. jepsen_append_t4) in setup!. * Routes Elle's integer keys k via (mod k N)+1 -> table-idx deterministically, so each Elle key always lands at the same (table, pk) storage location (Elle's append checker still sees a single key namespace). * Default Elle txns of up to 4 mops with key-count=12 distribute evenly across all 4 tables on every multi-op txn — a 4-key txn touches all 4 tables and therefore both Raft groups under the launch script's planned --shardRanges layout (t1-t2 -> group 1, t3-t4 -> group 2). * Uses TransactGetItems for atomic cross-table pre-read and TransactWriteItems for atomic cross-table writes (both DynamoDB APIs natively support multi-table txns). * Carries the same OCC condition expressions as the single- table workload so any G1c or G-single anomaly observed is attributable to the multi-shard 2PC path rather than to a workload-shape difference. The file is intentionally self-contained (small helpers duplicated from elastickv.dynamodb-workload rather than extracted into a shared namespace). Refactoring shared helpers into a third namespace would couple the workloads through a maintained API and obscure the side-by-side comparability that is the point of running both — relevant because PR #905 commits to keeping the single-table workload as-is for trend comparison. Tests cover the public API surface plus the load-bearing routing invariants: * builds-test-spec — :name = elastickv-dynamodb-append-multi-table * custom-options-override-defaults — option passthrough * host-override-creates-client — DDB client opens * key-routing-distributes-across-all-tables — 12 keys distribute 3-per-table across 4 tables * key-routing-table-name-matches-prefix — wraparound at k=4 * key-routing-pks-disambiguate_colliding_keys — k=0 and k=4 share table t1 but have distinct pks * multi-op-txn-spans_multiple_groups — default 4-key txn touches BOTH the planned group-1 tables (1,2) and group-2 tables (3,4) — without this, dispatchMultiShardTxn never fires and the whole workload is a no-op The routing helpers are private; the tests access them via var-get on #'workload/key->table-idx etc. so a future change to the routing surfaces in this test rather than as a silent G1c during a Jepsen run. Verification: lein test elastickv.dynamodb-multi-table-workload-test -> Ran 7 tests containing 17 assertions, 0 failures, 0 errors. lein test elastickv.dynamodb-workload-test (existing single-table suite) -> Ran 3 tests containing 5 assertions, 0 failures. Remaining M5a work (separate commits on this branch): * scripts/run-jepsen-m5-local.sh — single-process two-group launch with --raftGroups + --shardRanges using elastickv-route-key to compute boundary keys * Setup-hook verification — Clojure ListRoutes gRPC client asserting multi-group routing is in place * Done-when check — workload exercises dispatchMultiShardTxn (server-side log marker or probe) and Elle finds zero G1c
|
Warning Review limit reached
More reviews will be available in 59 minutes and 48 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a new Jepsen workload, dynamodb-multi-table-workload, along with its corresponding unit tests. This workload is designed to exercise the multi-shard 2PC path in elastickv by distributing keys deterministically across four different DynamoDB tables, ensuring transactions span multiple Raft groups. There are no review comments to address, and I have no feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
Workload draft + tests are stable; gemini's first pass returned zero findings. Could use a proper code review before iterating the remaining M5a slices (launch script + setup-hook verification). @claude review |
|
Claude finished @bootjp's task in 4m 36s —— View job Code ReviewTasks
OverallThe workload is well-structured. The routing design is sound, the OCC condition-expression pattern is faithfully carried over from the single-table workload, and the key tests lock down the two M5a-specific invariants (deterministic routing + multi-group span). Gemini's "no findings" read was broadly correct — there are no blocking bugs. The items below are one medium-severity correctness gap and several lower-priority issues. Medium — Stale table data can produce false Elle anomalies across runsFile: (teardown! [_this _test])
The single-table workload has the same gap, so this is an inherited pattern rather than a new defect. But with four tables instead of one, the blast radius for stale data is larger and consecutive runs in the launch script are more likely to hit it. Suggested fix: delete all four tables in Low —
|
… key-count guard (claude[bot] on PR #916) claude[bot]'s review of dded8b96 surfaced one medium-severity correctness gap and several low-priority items. All addressed: * MEDIUM — Stale table data across runs (teardown! no-op). Tables accumulated appended values across runs; the next TransactGetItems pre-read picked them up as snapshot, :r mops reported them to Jepsen, and Elle saw a history containing writes it never generated — spurious G-single / G1c. Fix: added delete-all-tables! mirroring create-all-tables!'s exception handling (ignore ResourceNotFoundException for idempotency), and called it from the client's teardown!. * LOW — TransactGetItems positional alignment unverified. DynamoDB guarantees :Responses is positionally aligned with :TransactItems, but the workload relied on this without an assertion. A future cognitect SDK that ever normalises the response would silently corrupt the snapshot map. Fix: one-line assert on (count ks-vec) vs (count Responses) so the failure surfaces loudly rather than via mysterious OCC behaviour. * LOW — key-count must be a multiple of num-tables. Default key-count=12 splits evenly across N=4 tables, but --key-count 10 (or any non-multiple) silently gives some tables fewer keys, breaking the multi-group-span invariant that is the entire point of this workload. Fix: construction-time (assert (zero? (mod key-count num-tables))) so an uneven split fails-fast at workload build time rather than producing a confusing test run. Two new tests pin this: rejects-key-count-not-divisible- by-num-tables (10, 7 reject) and accepts-divisible-key- counts (4, 8, 16 accept). * TRIVIAL — deftest naming inconsistency. Two test names had stray underscores instead of hyphens (key-routing-pks-disambiguate_colliding_keys and multi-op-txn-spans_multiple_groups). Renamed both to the project's hyphen-case convention. Deferred (claude[bot] notes): * LOW — ConditionalCheckFailedException dead-code arm. Mirrors the single-table workload; not blocking; kept for parity until a single-table cleanup lands. * DESIGN NOTE — setup! doesn't wait for ACTIVE state. Real DynamoDB's CreateTable is async, but elastickv's implementation may be synchronous. Worth verifying at the end-to-end Jepsen run; if needed, will add a DescribeTable poll loop. Verification: lein test elastickv.dynamodb-multi-table-workload-test -> Ran 9 tests containing 22 assertions, 0 failures, 0 errors.
|
Pushed as c46b2b5 — claude[bot]'s findings on aded8b9 addressed. Medium — stale table data across runs. Fix: added Low — TransactGetItems positional alignment unverified. One-line Low — Trivial — deftest naming. Two test names had stray underscores; renamed to project's hyphen-case convention. Deferred (claude[bot] explicitly tagged as low-priority):
@claude review |
|
Claude finished @bootjp's task in 1m 43s —— View job Code Review — follow-up pass on c46b2b5Tasks
Verdict: all four previous findings correctly addressed — ready to remove draft once launch script landsFix-by-fix verificationMedium (stale table data) → ✅ Fixed correctly
Low (TransactGetItems positional alignment) → ✅ Fixed correctly Lines 224–225 add the assert with a meaningful failure message. One note: Clojure assertions can be elided globally via Low (key-count % num-tables guard) → ✅ Fixed correctly Construction-time Trivial (deftest naming) → ✅ Fixed
Deferred items — status unchanged, dispositions look right
One new observation (no action needed before unmark-draft)
In Jepsen's lifecycle, What looks good in c46b2b5
|
… (claude[bot] design note on c46b2b5) claude[bot]'s follow-up review of c46b2b5 listed two deferred items: * ConditionalCheckFailedException dead-code arm — mirrors the single-table workload, claude OK'd 'A future cleanup can remove both together.' Still deferred. * setup! doesn't wait for ACTIVE state — claude OK'd 'gating this on the end-to-end Jepsen run is correct' but flagged the assumption is unverified. The second one is cheap to lock down with actual code evidence rather than leaving it speculative. Verified against adapter/dynamodb.go:779-783 — elastickv's CreateTable handler returns TableStatus=ACTIVE synchronously in the response body, so the table is queryable as soon as create-one-table! returns. No DescribeTable poll needed for the elastickv target. Real DynamoDB's CreateTable is async; if this workload is ever pointed at a real DynamoDB endpoint (unlikely — Jepsen is wired against elastickv only), a poll loop must be added. Documented at the setup! call site so a future reader doesn't have to re-do the adapter inspection. Doc-comment-only change. lein test elastickv.dynamodb-multi-table-workload-test still 9 tests / 22 assertions, 0 failures, 0 errors.
|
You linked claude[bot]'s initial review of aded8b9 (the 6-finding pass). All 4 actionable findings from that review were addressed in c46b2b5 and confirmed by claude[bot]'s follow-up at 14:54:50 — both deferrals were explicitly OK'd:
The second deferral was the only one with an unverified assumption: claude[bot] flagged it would matter "if elastickv's Doc-only; @claude review |
|
Claude finished @bootjp's task in 1m 8s —— View job Code Review — third pass on cf07ec0Tasks
Verdict: clean — all prior findings resolved, doc-only commit is correct, no new issuescf07ec0 verificationThe synchronous ACTIVE claim is correct and well-evidenced.
The added comment at
No doc content is incorrect or misleading. Status of all findings from prior rounds
What looks good overallThe workload is sound. The routing math ( Ready to unmark draft once the launch script and setup-hook verification PRs land. |
Draft — second slice of Composed-1 M5a per
docs/design/2026_06_02_proposed_composed1_m5_jepsen_route_shuffle.md§3.3. Follows #911 (CLI tools, merged); blocks on launch script + setup-hook verification (remaining M5a slices) before unmark-draft.What this PR ships
jepsen/src/elastickv/dynamodb_multi_table_workload.clj— a new workload variant of the DynamoDB list-append test that exercises cross-table 2PC, which the single-tableelastickv.dynamodb-workloadcannot do.Why a separate workload exists
kv/shard_key.go'sdynamoRouteKeynormalises every DynamoDB table-meta / item / GSI key for one table to a single per-table route key (!ddb|route|table|<base64(name)>). Single-table workloads always route to one shard regardless of partition-key value, sodispatchMultiShardTxn/commitSecondaryTxns/ the newErrTxnSecondaryRouteShiftedAfterPrimaryCommitsentinel never fire (codex P1 on #905 ffb9c73).What this workload does
jepsen_append_t1…jepsen_append_t4) created insetup!.kroute via(mod k N)+1 → table-idx, so each Elle key always lands at the same(table, pk)storage location (Elle's append checker still sees a single key namespace).--shardRangeslayout (t1-t2 → group 1, t3-t4 → group 2).TransactGetItemsfor atomic cross-table pre-read;TransactWriteItemsfor atomic cross-table writes (both DynamoDB APIs natively support multi-table txns).Why self-contained
The file intentionally duplicates small helpers from
elastickv.dynamodb-workloadrather than extracting into a shared namespace. Refactoring would couple the workloads through a maintained API and obscure the side-by-side comparability that is the point of running both — relevant because #905 commits to keeping the single-table workload as-is for trend comparison.Tests (
jepsen/test/elastickv/dynamodb_multi_table_workload_test.clj)7 tests / 17 assertions. The load-bearing ones:
key-routing-distributes-across-all-tables— 12 keys distribute 3-per-table across 4 tableskey-routing-table-name-matches-prefix— wraparound at k=4key-routing-pks-disambiguate_colliding_keys— k=0 and k=4 share table t1 but have distinct pksmulti-op-txn-spans_multiple_groups— default 4-key txn touches BOTH the planned group-1 tables (1,2) and group-2 tables (3,4) — without this,dispatchMultiShardTxnnever fires and the whole workload is a no-opThe routing helpers are private; tests access via
var-geton#'workload/key->table-idxetc. so a future change to the routing surfaces in this test rather than as a silent G1c during a Jepsen run.Remaining M5a work (separate PRs after this lands)
scripts/run-jepsen-m5-local.sh— single-process two-group launch with--raftGroups+--shardRangesusingelastickv-route-keyto compute boundary keysListRoutesgRPC client asserting multi-group routing is in placedispatchMultiShardTxn(server-side log marker or probe) and Elle finds zero G1cTest plan
lein test elastickv.dynamodb-multi-table-workload-test→ Ran 7 tests / 17 assertions, 0 failures, 0 errorslein test elastickv.dynamodb-workload-test(existing single-table suite) → Ran 3 tests / 5 assertions, 0 failures (no regression)