Skip to content

feat(routing): apply sticky routing in the decide flow - #412

Open
prajjwalkumar17 wants to merge 3 commits into
mainfrom
feat/sticky-routing
Open

feat(routing): apply sticky routing in the decide flow#412
prajjwalkumar17 wants to merge 3 commits into
mainfrom
feat/sticky-routing

Conversation

@prajjwalkumar17

@prajjwalkumar17 prajjwalkumar17 commented Sep 4, 2026

Copy link
Copy Markdown
Member

Phase 3 of 3 for sticky routing (#393). Merge order: #419#420 → this.

Important

This branch necessarily contains phases 1–2 (the code builds on them), so "Files changed" shows all three. Review only this phase's commit: 0274dfc (Commits tab → last commit) — the earlier phases are reviewed in #419 and #420. The branch gets rebased as predecessors merge, collapsing the diff to the one commit.

Closes #416.

What this adds

A post-scoring override in run_decider_flow, running last — after SR scoring, outage/elimination penalties, and the cost/volume post-steps. When the merchant's sticky_routing_enabled flag is on and paymentInfo.customerId is present, it pins the customer's highest-success-count connector for the exact PM:PMT combo (one HGETALL).

Precedence — PL/euclid first, then sticky > SR > default: explicit orders (priority logic, merchant preference) bound the candidate set and are never overridden; within the dynamic decision a pin wins over the SR pick, and over the Default ordering when SR is off. Downtime relabels and hedging exploration stay untouched. Sticky is independent of the euclid rule store — it is gated only by the merchant-level sticky_routing_enabled feature flag, a customerId on the request, and a new optional per-request stickyRouting override (mirroring enableMultiObjective).

Health veto: the pinned connector must be in the caller's eligibleGatewayList and its post-elimination score must clear STICKY_ROUTING_MIN_SCORE_RATIO (default 0.5) × top score — a pin can never resurrect a connector the outage/elimination passes just buried. Fails open to the plain SR pick on any miss.

Label semantics: every applied pin reports routing_approach: STICKY_ROUTING — a returning customer's repeat combo reading as SR would confuse callers. The label is admitted by the SRv3 producer-isolation and explore/exploit gates (mirroring the SR_SELECTION_MULTI_OBJECTIVE precedent), so pinned outcomes keep feeding the SR windows and the veto stays live. A divergent pin additionally clears the superseded multi-objective/volume-steer claims (so cost analytics can't credit a pick that never reached the customer); agree-vs-diverge stays measurable via the sticky decision metrics.

Unit tests cover the scoring-gate classifiers; builds and rustfmt green on both feature sets.

🤖 Generated with Claude Code

@prajjwalkumar17 prajjwalkumar17 changed the title feat(routing): sticky routing — pin customers to their proven connector feat(routing): apply sticky routing in the decide flow Sep 4, 2026
@prajjwalkumar17
prajjwalkumar17 changed the base branch from main to feat/sticky-routing-feedback September 4, 2026 09:10
@prajjwalkumar17
prajjwalkumar17 changed the base branch from feat/sticky-routing-feedback to main September 4, 2026 09:15
…r scores

Storage layer only; feedback writes and the decide-side read come in the
stacked follow-ups.

- One Redis hash per (merchant, customer): fields pm:pmt:connector hold a
  NET count — successes increment, gateway failures decrement (floored at
  zero; merchant feedback is the source of truth in both directions).
  Failures never create a key or field, so key-creation stays tied to
  successful payments and the footprint stays bounded. Only positive
  counts qualify as pin candidates. Read is a single HGETALL returning
  counts sorted per exact combo; deliberately no cross-combo fallback
  (transaction-level fallbacks already cover a missing combo).
- Eviction safety: every key volatile with a sliding TTL re-armed only on
  success; per-customer combo cap with lowest-count pruning that drains
  full excess; per-merchant two-window admission budget so new-key growth
  is bounded up front. Defaults overridable via service config.
- Sticky on/off is a merchant-level feature flag (sticky_routing_enabled
  FeatureConf, dashboard plumbing in a follow-up) — deliberately NOT part
  of the euclid rule store, so toggling never touches the algorithm
  lifecycle.
- New wrapper commands: hincrby_with_expire, hincrby, hgetall_map,
  hdel_field.

Measured: 2-combo customer = 136B, 30-combo = 952B, listpack-encoded
(Redis 7.2.7). Refs #393.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prajjwalkumar17 and others added 2 commits September 9, 2026 14:12
Write side of sticky routing; the decide-side read comes next in the
stack. Merchant feedback is the source of truth in both directions:
successes (CHARGED/AUTHORIZED/PARTIAL_CHARGED) increment the habit,
failure statuses (AUTHENTICATION_FAILED/AUTHORIZATION_FAILED/
JUSPAY_DECLINED/FAILURE) decrement it, floored at zero. Wider lifecycle
statuses (VOIDED, AUTO_REFUNDED, ...) touch neither direction. Failures
never create a key or field.

- update-gateway-score gains optional customerId/paymentMethod/
  paymentMethodType (backward compatible). Payload wins; the decide-time
  GatewayScoringData snapshot (now also carrying customerId) is the
  fallback, so callers need the new fields only when webhooks can
  outlive the snapshot's 30-min TTL.
- Two-pass hook in check_and_update_gateway_score_: payload-only pass
  before the snapshot fetch (survives snapshot expiry, same rationale as
  the ab-test emit), snapshot pass after.
- Deliberately NO dedupe: every feedback event counts, N times if sent
  N times — matching the engine's default SR scoring behavior (its
  feedback locks are opt-in per merchant), and avoiding a lock key per
  payment on the hot path.
- GSM healthy-failure early return is now failure-only: a success with
  errorInfo previously skipped both the SR reward and any snapshot-based
  sticky write.
- Sticky key pm/pmt are case-folded at the key boundary: the snapshot
  stores them uppercased while payloads arrive verbatim, and the two
  write sources must land on one hash field. Connector stays verbatim
  for the eligible-list comparison at decide time.
- openapi + api-ref updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read side of sticky routing, completing the loop over the two prior
commits (storage, feedback writes). Sticky is independent of the euclid
rule store: it is gated only by the merchant-level
sticky_routing_enabled feature flag, a customerId on the request, and
an optional per-request stickyRouting override (mirroring
enableMultiObjective).

- New post-scoring override in run_decider_flow, after the cost and
  volume nudges: pin the customer's highest-success-count connector for
  the pm:pmt combo. Precedence is PL/euclid explicit orders first (they
  bound the candidate set and are never overridden), then sticky, then
  SR, then the default ordering — sticky applies over the SR-selection
  family AND the Default approach, so a pin still wins when SR is off.
  Downtime relabels and hedging exploration stay untouched. The pinned
  connector must clear a configurable fraction of the top score
  (STICKY_ROUTING_MIN_SCORE_RATIO, default 0.5), so a pin can never
  resurrect a connector the outage/elimination passes just buried.
  Fails open on any miss.
- Every applied pin reports the new STICKY_ROUTING approach — a
  returning customer's repeat combo reading as SR would confuse
  callers. The label is admitted by the SRv3 producer-isolation and
  explore/exploit gates (mirroring the SR_SELECTION_MULTI_OBJECTIVE
  precedent), so pinned outcomes keep feeding the SR windows and the
  health veto stays live. A divergent pin additionally clears the
  superseded multi-objective/volume-steer claims so cost analytics
  can't credit a pick that didn't reach the customer.
- Sticky keys trim merchant/customer ids; customerId is documented as
  byte-exact, and the UPI doc guidance matches the V2 flow's actual key
  material.

Refs #393.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prajjwalkumar17 prajjwalkumar17 self-assigned this Sep 9, 2026
@prajjwalkumar17
prajjwalkumar17 marked this pull request as ready for review September 9, 2026 10:31
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.

Sticky routing: decide-time override with health veto

1 participant