Skip to content

feat(routing): make volume-split selection deterministic per payment - #406

Open
prajjwalkumar17 wants to merge 3 commits into
mainfrom
feat/seeded-volume-split
Open

feat(routing): make volume-split selection deterministic per payment#406
prajjwalkumar17 wants to merge 3 commits into
mainfrom
feat/seeded-volume-split

Conversation

@prajjwalkumar17

@prajjwalkumar17 prajjwalkumar17 commented Sep 1, 2026

Copy link
Copy Markdown
Member

Pairs with hyperswitch PR: juspay/hyperswitch#13956

Why

Volume splits roll rand::thread_rng() on every evaluation. One checkout mount fires
three concurrent evaluating calls (payment-methods list, sdk-config, session tokens),
so the same payment could get connector A from one call and connector B from
another — skewing the configured split and letting a wallet's session token disagree
with the connector the payment later confirms on.

What

  • sample_split_winner_first (and everything above it: perform_volume_split,
    perform_volume_split_priority, evaluate_output, eval_program) takes an
    optional seed; the evaluate handlers pass the request's payment_id (single and
    batch entries alike, plus the A/B preview arms).
  • Seeded selection = djb2 slot over the cumulative weights, walked in declaration
    order — the exact scheme ab_test::assign_arm / the A/B static-arm evaluator
    already use, and byte-identical with Hyperswitch's seeded split (cross-repo
    test vector djb2("pay_nZwbogscgFwIanlGnUxw") = 10501740297535541692 asserted in
    both repos).
  • Requests without a payment_id keep the random roll. Winner-first ordering and
    the tail's declaration order are unchanged.

Notes for reviewers

  • A retried payment re-rolls the same winner; winner-first ordering keeps the rest
    of the list so failure-retries still fall through to the next connector.
  • Once the paired Hyperswitch PR lands, HS-local and DE evaluations of the same
    payment agree exactly, so volume-split rules no longer need diff-suppression in
    shadow comparisons (HS-side follow-up).
  • Tests: determinism, winner-first ordering, 80/20 distribution over 10k payment
    ids, zero-weight guard, unseeded fallback. 423 lib tests + clippy clean.

🤖 Generated with Claude Code

Volume splits rolled rand::thread_rng() on every evaluation, so the SDK's
concurrent init calls (payment-methods list, sdk-config, session tokens) could
each pick a different winner for the same payment - skewing the configured
split and letting a wallet's session token disagree with the connector the
payment later confirms on.

Seed the split with the request's payment_id when present: djb2 slot over the
cumulative weights, walked in declaration order (the same scheme the A/B arm
assignment already uses, and byte-identical with Hyperswitch's seeded split so
both engines pick the same winner). Requests without a payment_id keep the
random roll. Winner-first ordering and the remaining declaration order are
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prajjwalkumar17 prajjwalkumar17 self-assigned this Sep 4, 2026
# Conflicts:
#	src/decider/gatewaydecider/ab_test/preview.rs
#	src/euclid/interpreter.rs
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