feat(routing): record sticky-routing outcomes from the feedback path - #420
prajjwalkumar17 wants to merge 2 commits into
Conversation
9c50f17 to
7bec489
Compare
7bec489 to
dc9f338
Compare
dc9f338 to
23f953b
Compare
23f953b to
5c542ea
Compare
| "enforceDynamicRoutingFailure": null | ||
| "enforceDynamicRoutingFailure": null, | ||
| "customerId": "cust_123", | ||
| "paymentMethod": "INTERAC", |
There was a problem hiding this comment.
we already have the paymentMethod and paymentMethodType in the decide_gateway call right? why are we getting it again?
There was a problem hiding this comment.
The decide-time values survive only in the gateway_scoring_data_{payment_id} Redis snapshot, which lives 30 minutes — DE has no transactions table, so nothing else remembers a payment's pm/pmt. Within that window the feedback fields are indeed redundant and can be omitted (the snapshot fallback covers it). They exist for feedback that arrives later — CHARGED webhooks routinely land hours after the decide — where without them the success can't be attributed to a customer × pm:pmt × connector habit at all. Same reason customerId is on the payload: payload wins, snapshot is the fallback, and only late-webhook callers need to send them.
…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>
5c542ea to
3b887d8
Compare
Phase 2 of 3 for sticky routing (#393). Merge order: #419 → this → #412.
Important
This branch necessarily contains phase 1's commit (the code builds on it), so "Files changed" shows both. Review only this phase's commit:
5c542ea(Commits tab → last commit) — phase 1 is reviewed in #419. After #419 merges, this branch gets rebased so the diff collapses to the one commit.Closes #415.
What this adds
POST /update-gateway-scoregains three optional, backward-compatible fields —customerId,paymentMethod,paymentMethodType. Payload wins; the decide-timegateway_scoring_data_{payment_id}snapshot (which now also stashescustomerIdfrom the order) is the fallback, so existing callers change nothing and only need the fields when success webhooks can outlive the snapshot's 30-minute TTL.check_and_update_gateway_score_: payload-only pass before the snapshot fetch (a CHARGED webhook hours late still counts — same placement rationale as the A/B outcome emit), snapshot pass after.CHARGED/AUTHORIZED/PARTIAL_CHARGEDincrement the habit; failure statuses (AUTHENTICATION_FAILED/AUTHORIZATION_FAILED/JUSPAY_DECLINED/FAILURE) decrement it, floored at zero. Wider lifecycle statuses (VOIDED,AUTO_REFUNDED, …) touch neither. Failures never create a key or field, so footprint stays tied to successful payments. Retries are correct by construction: fail-on-A subtracts from A only if A had a habit; the succeeding attempt's connector gains.errorInfo(e.g. echoed from a 3DS step-up) skipped both the SR reward and the snapshot fetch.sticky_routing_enabledFeatureConf; sticky write failures are logged, never surfaced. openapi + api-refs updated.🤖 Generated with Claude Code