Skip to content

feat(dashboard): sticky routing toggle in the Multi Objective feature flags - #424

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

prajjwalkumar17 wants to merge 7 commits into
mainfrom
feat/sticky-routing-dashboard

Conversation

@prajjwalkumar17

@prajjwalkumar17 prajjwalkumar17 commented Sep 4, 2026

Copy link
Copy Markdown
Member

Phase 6 for sticky routing (#393): the merchant-facing switch. Merge order: after #423. One commit.

Sticky routing is configured purely by the merchant-level sticky_routing_enabled feature flag (independent of the euclid rule store), so its control lives in the Multi Objective tab → Feature Flags list, beside Cost savings and Volume contracts, wired to the standard /merchant-account/{id}/features API — no new backend surface.

Closes #421.

Verified live: clicking Enable on the card flipped sticky-routing => true on the features API (the exact FeatureConf the decide and feedback paths gate on), the card re-rendered with the Enabled badge + Disable button, and the six sticky e2e tests pass against the same stack.

Important

Builds on phases 1–5; review the single commit 268d283.

🤖 Generated with Claude Code

@prajjwalkumar17
prajjwalkumar17 force-pushed the feat/sticky-routing-dashboard branch from b3fb524 to 9ff32fa Compare September 6, 2026 08:48
@prajjwalkumar17 prajjwalkumar17 changed the title feat(routing): dashboard toggle for sticky routing feat(dashboard): sticky routing toggle in the Multi Objective feature flags Sep 6, 2026
@prajjwalkumar17
prajjwalkumar17 force-pushed the feat/sticky-routing-dashboard branch 2 times, most recently from 3aeb870 to d34be50 Compare September 8, 2026 10:51
@prajjwalkumar17
prajjwalkumar17 force-pushed the feat/sticky-routing-dashboard branch from d34be50 to c6ecabb Compare September 9, 2026 08:45
@prajjwalkumar17 prajjwalkumar17 self-assigned this Sep 9, 2026
@prajjwalkumar17
prajjwalkumar17 marked this pull request as ready for review September 9, 2026 10:32
@prajjwalkumar17
prajjwalkumar17 force-pushed the feat/sticky-routing-dashboard branch from c6ecabb to 268d283 Compare September 15, 2026 07:52
prajjwalkumar17 and others added 7 commits September 15, 2026 15:37
…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>
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>
KnownFeature::StickyRouting (slug "sticky-routing") maps to the
sticky_routing_enabled FeatureConf — the same key the feedback write and
decide read already check — so the ops kill switch gets the standard
features API/dashboard treatment with rollout-percentage support.
Partially addresses #417.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six Playwright API tests against the live stack covering decide ->
update-gateway-score -> decide: a recorded success pins the customer, a
retried payment sticks to the finally-succeeding connector, the
stickyRouting:false opt-out and a disabled merchant flag suppress the
pin, sticky state is scoped to the payment-method combo, and a late
webhook carrying inline customer/pm fields records without the
decide-time snapshot. Score writes are async server-side, so pin
assertions poll. Closes #418's live-stack scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two IntCounterVecs: sticky_routing_decisions_total{outcome} on the
decide side (overridden / pinned_agreeing / vetoed / no_state /
read_error, counted only when the feature engaged) and
sticky_routing_writes_total{outcome} on the feedback side (recorded /
over_budget / duplicate / write_error). over_budget spiking is the
admission-guard alarm; read_error/write_error surface Redis trouble
(the guards fail open by design, so errors are otherwise invisible).
Partially addresses #417 — Grafana panel and
alerting ride the existing metrics endpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… flags

Sticky routing is configured purely by the merchant-level
sticky_routing_enabled feature flag (independent of the euclid rule
store), so its switch lives in the Multi Objective tab's Feature Flags
list beside cost savings and volume contracts, wired to the standard
/merchant-account/{id}/features API. Verified live: enabling the card
flips the sticky-routing flag the decide and feedback paths gate on.
Closes #421.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prajjwalkumar17
prajjwalkumar17 force-pushed the feat/sticky-routing-dashboard branch from 268d283 to 078710f Compare September 15, 2026 10:08
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: dashboard toggle in the Multi Objective feature flags

1 participant