Group batch-sync mappings by EC ID before KV updates - #902
Group batch-sync mappings by EC ID before KV updates#902ChristianPavilonis wants to merge 2 commits into
Conversation
5c71516 to
3a1e5de
Compare
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
Grouping S2S batch-sync mappings by normalized EC ID before touching KV is correct as implemented, and the accounting invariant holds by construction: every input index lands in exactly one bucket (a validation error, or a single group that is accepted or fanned out as an error), and the abort slice groups[group_index..] includes the failing group, so accepted + errors.len() == mappings.len(). I traced the duplicate-UID orderings by hand and the final KV state is identical to the pre-grouping code — fewer writes, same result. No blocking findings.
3 of the inline comments below carry a one-click GitHub
suggestion— use Commit suggestion (or Add suggestion to batch for several at once) to apply them as commits on the PR branch. The remaining comment describes the fix in prose because splitting the bundled test spans two ranges and adds a new test function, which can't be auto-applied.
Non-blocking
♻️ refactor
- Conflicting UIDs inside one group are discarded with no signal — see inline at
crates/trusted-server-core/src/ec/batch_sync.rs:231 handle_batch_sync_reports_grouped_success_and_rejection_countsbundles two tests — see inline atcrates/trusted-server-core/src/ec/batch_sync.rs:883
⛏ nitpick
expectmessage says "serialize" on a deserialize — see inline atcrates/trusted-server-core/src/ec/batch_sync.rs:425- "Therefore" is a non-sequitur in the docs contract — see inline at
docs/guide/api-reference.md:182
Cross-cutting / body-level findings
-
⛏ Stale third mock result in
process_mappings_aborts_on_kv_unavailable(crates/trusted-server-core/src/ec/batch_sync.rs:544-572) — three distinct EC IDs now produce two writer calls, so the queued thirdOk(UpsertResult::Written)is dead. The test still catches a missingbreak(a third call would pop it and pushacceptedto 2, failing theaccepted == 1assertion), but the leftover misrepresents the new call shape. Drop it and addassert_eq!(writer.calls().len(), 2, "should stop after the failing group")— this PR added call recording precisely for that. These lines are unchanged by the diff, so there is no hunk to anchor an inline comment to. -
♻️ No test at the batch size the issue is about — issue #882's acceptance criterion is "work bounded by distinct valid normalized EC IDs", but the largest duplicate case covered is 3 mappings. A
MAX_BATCH_SIZE-sized batch of one repeated EC ID assertingwriter.calls().len() == 1would pin the actual performance contract rather than inferring it from the 2-3 mapping cases. The existing grouping tests prove the mechanism; this would prove the bound. -
🤔 No CHANGELOG entry for a partner-visible contract change —
CHANGELOG.mdis actively maintained (59 bullets under[Unreleased], including endpoint-level behavior entries such as the admin Basic-auth coverage change) and follows Keep a Changelog's "all notable changes". This PR changes the documented/_ts/api/v1/batch-synccontract in two partner-observable ways:acceptednow counts group members rather than individual writes, and the abort boundary moved from positional to groupwise, sokv_unavailableand accepted indexes can interleave (a partner retrying "from the firstkv_unavailableindex onward" will now re-send already-accepted mappings — harmless because the write is idempotent, but a behavior change to reason about). Neither this PR nor #901 touches the file. Worth a### Changedbullet, or an explicit decision that batch-sync internals stay out of the changelog. -
📝 Three-deep stack — 902 → 901 (
fix/idempotent-ec-withdrawal-tombstones, open) → 900 (fix/no-op-kv-reads) →main. The diff reviewed here is the top delta only; merge order matters. -
👍 Load-bearing details that were easy to get wrong — the explicit
errors.sort_by_keyplus thedebug_assert_eq!accounting invariant: the sort is genuinely required now that abort fan-out appends group indexes out of order, and the assertion documents the one-outcome-per-input property at the point it could break. The newupsert_partner_id_if_exists_retries_cas_conflicttest inkv.rspins the CAS boundary the grouping now leans on and was previously uncovered by anyupsert_partner_id_if_existstest. And documenting theA, B, Aboundary change as intentional in both the API reference and the plan, rather than leaving it implicit, is the right call for a contract partners integrate against.
CI Status
All 14 reported checks pass. gh pr checks --required reports no required checks on this branch, so none of these is merge-blocking under branch protection.
- cargo fmt: PASS
- cargo test: PASS
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- vitest: PASS
- format-typescript: PASS
- format-docs: PASS
- prepare integration artifacts: PASS
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- browser integration tests: PASS
Locally, in a worktree at 3a1e5de3: cargo test -p trusted-server-core --lib ec:: (349 pass), cargo clippy -p trusted-server-core --target wasm32-wasip1 --all-targets --all-features -- -D warnings (clean), cargo fmt --all -- --check (clean), and the pinned docs prettier 3.8.1 --check (clean).
prk-Jr
left a comment
There was a problem hiding this comment.
Approving. Nothing in the earlier review was blocking: the grouping logic is correct, every input index resolves to exactly one outcome by construction, and the final KV state matches the pre-grouping code for every duplicate-UID ordering — with one writer call per distinct normalized EC ID instead of one per mapping.
The findings in #902 (review) stand as non-blocking follow-ups, at the author's discretion: three one-click suggestions (a debug log when a batch supplies conflicting UIDs for one EC, an expect message that says "serialize" on a deserialize, and a docs sentence whose "Therefore" points the implication backwards), plus test-hygiene and CHANGELOG notes in the review body.
CI is green across all 14 reported checks.
|
@ChristianPavilonis please assign issue to this PR |
3a1e5de to
fe13e1d
Compare
fe13e1d to
be328a2
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary
The grouping rework is well-executed: validation-before-write, the vector-plus-map split for deterministic first-occurrence ordering, group outcome fan-out, and the per-input accounting invariant are all correct, and the 22 tests cover the contract thoroughly. The docs and the recorded design plan match the implementation exactly.
The blocking issue is CI state rather than the algorithm: the required cargo test and cargo fmt checks are both red, and as a consequence none of this PR's new tests have actually executed in CI.
1 of the inline comments below carries a one-click GitHub
suggestion— use Commit suggestion to apply it as a commit on the PR branch. The remaining findings are prose because they concern CI state or span more than the diff.
Blocking
🔧 wrench
- CI is red: duplicate
InMemoryEcKvimport breaks the test build — see "Cross-cutting / body-level findings" below
Non-blocking
⛏ nitpick
debug_assert_eq!missing the descriptive message CLAUDE.md requires — see inline atcrates/trusted-server-core/src/ec/batch_sync.rs:275
Cross-cutting / body-level findings
-
🔧 CI is red: duplicate
InMemoryEcKvimport breaks the test build —cargo testandcargo fmtare both FAIL, and both are required checks under branch protection. Both trace to a single compile error:error[E0252]: the name `InMemoryEcKv` is defined multiple times --> crates/trusted-server-core/src/ec/kv.rs:1591:9 | 1401 | use crate::ec::kv_backend::test_support::InMemoryEcKv; | ------------------------------------------------- previous import of the type `InMemoryEcKv` here ... 1591 | use crate::ec::kv_backend::test_support::InMemoryEcKv; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `InMemoryEcKv` reimported hereTwo things worth separating:
- This is inherited from the base branch, not introduced here. This PR's diff touches zero
InMemoryEcKvlines, and PR #901 shows the identical two failing checks. The one-line fix (deletekv.rs:1591) belongs in #901. - It means this PR's new tests have never run in CI. The whole
trusted-server-corelib-test target fails to compile, so all 22 newbatch_synctests plus the newupsert_partner_id_if_exists_retries_cas_conflicttest were skipped. Verified locally: with line 1591 deleted, all 22batch_synctests pass.
Also inherited from #901 and surfacing only once compilation is unblocked:
ec::finalize::tests::finalize_withdrawal_keeps_cookie_deletion_on_kv_failurefails on the base branch as well. Both need to clear in #901 before this PR can merge green.Finally, the PR description checks off
cargo test-fastly && cargo test-axumandcargo fmt --all -- --checkas passing. Those gates are currently red, so the test plan should be corrected. - This is inherited from the base branch, not introduced here. This PR's diff touches zero
-
👍 Infrastructure-abort accounting is precisely handled —
crates/trusted-server-core/src/ec/batch_sync.rs:263-269.groups[group_index..]includes the failing group and then breaks, so no input is counted twice, and pre-validated inputs keep their specific error reasons instead of being flattened tokv_unavailablethe way the old positional abort did. The interaction with the trailingerrors.sort_by_keyis subtle, andprocess_mappings_aborts_by_group_and_preserves_sorted_accountingpins it exactly.
CI Status
- cargo test: FAIL (required)
- cargo fmt: FAIL (required)
- format-typescript: PASS (required)
- format-docs: PASS (required)
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- vitest: PASS
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- browser integration tests: PASS
- prepare integration artifacts: PASS
- CodeQL: PASS
- Analyze (rust): PASS
- Analyze (javascript-typescript): PASS
- Analyze (actions): PASS
0683d79 to
e9557c8
Compare
| accepted + errors.len(), | ||
| mappings.len(), | ||
| "should report exactly one outcome per submitted mapping" | ||
| ); |
There was a problem hiding this comment.
Resolved — the bare debug_assert_eq! flagged in the previous pass now carries a descriptive message, matching CLAUDE.md and the other debug_assert_eq! sites in the codebase (publisher.rs:3021, openrtb.rs:480, registry.rs:823).
Verified at e9557c86: cargo fmt --all -- --check clean, cargo clippy-fastly clean, and cargo test -p trusted-server-core --lib passes 2729 tests with 0 failures.
| assert_eq!( | ||
| writer.calls().len(), | ||
| 2, | ||
| "should stop after the failing group" |
There was a problem hiding this comment.
Resolved — this stale third queued result (Ok(UpsertResult::Written)) could never be consumed, so the test would still have passed if the abort had failed to stop group processing. Removing it plus the new writer.calls().len() == 2 assertion now pins the abort boundary directly.
This was a latent gap in the previous revision that the prior review pass did not catch.
Summary
This PR is stacked on #901.
Changes
crates/trusted-server-core/src/ec/batch_sync.rscrates/trusted-server-core/src/ec/kv.rsdocs/guide/api-reference.mddocs/guide/ec-setup-guide.md,docs/guide/edge-cookies.mddocs/superpowers/plans/2026-07-13-issue-882-group-batch-sync-by-ec-id.mdCloses
Closes #882
Test plan
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest runcd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serve— not runcargo test-cloudflare,cargo test-spin, cross-adapter parity, and all Cloudflare/Spin native + WASM clippy targetsChecklist
unwrap()in production code — useexpect("should ...")logmacros (notprintln!)