Part of #393 (sticky routing). Status: implemented in #419 (commit 575d249), pending review/merge.
One Redis hash per (merchant, customer) holds the customer's NET habit counts — successes add one, gateway failures subtract one (floored at zero; failures never create a key or field, and only positive counts qualify as pin candidates):
Key: sticky_gw_{merchant_id}_{customer_id} (ids trimmed, case preserved)
Field: {PM}:{PMT}:{connector} (PM/PMT case-folded, connector verbatim)
Value: integer net count (+1 success, -1 failure, floor 0) (HINCRBY — atomic under concurrent feedback)
Example: sticky_gw_m1_cust42 → { "INTERAC:RTP:GIGADAT": 3, "INTERAC:RTP:LOONIO": 2 }. Listpack-encoded: 136 B for a typical 2-combo customer, 952 B at the 30-combo cap (measured on Redis 7.2.7) — ~150–250 MB per 1M active customers.
Eviction safety (sticky keys must never crowd out SR/elimination state):
- Sliding TTL on every hash, re-armed in the same MULTI as the write (
STICKY_ROUTING_KEY_TTL, default 90 days). Idle customers self-evict; every key stays volatile.
- Per-customer combo cap (
STICKY_ROUTING_MAX_COMBOS_PER_CUSTOMER, default 30): a new field over the cap prunes the lowest-count fields (full excess drains), keeping the hash listpack-encoded.
- Per-merchant admission budget (
STICKY_ROUTING_MAX_CUSTOMERS_{mid}, default 1M new hashes per TTL window, tracked by two-window counters sticky_gw_adm_{mid}_{window} with TTL 2×window): once spent, existing customers keep updating but no new keys are created — growth bounded up front, no reliance on Redis eviction.
New wrapper commands on RedisConnectionWrapper: hincrby_with_expire (MULTI: HINCRBY+EXPIRE so a hash can never exist without a TTL), hgetall_map, hdel_field. Module: src/sticky_routing.rs.
Part of #393 (sticky routing). Status: implemented in #419 (commit
575d249), pending review/merge.One Redis hash per
(merchant, customer)holds the customer's NET habit counts — successes add one, gateway failures subtract one (floored at zero; failures never create a key or field, and only positive counts qualify as pin candidates):Example:
sticky_gw_m1_cust42→{ "INTERAC:RTP:GIGADAT": 3, "INTERAC:RTP:LOONIO": 2 }. Listpack-encoded: 136 B for a typical 2-combo customer, 952 B at the 30-combo cap (measured on Redis 7.2.7) — ~150–250 MB per 1M active customers.Eviction safety (sticky keys must never crowd out SR/elimination state):
STICKY_ROUTING_KEY_TTL, default 90 days). Idle customers self-evict; every key stays volatile.STICKY_ROUTING_MAX_COMBOS_PER_CUSTOMER, default 30): a new field over the cap prunes the lowest-count fields (full excess drains), keeping the hash listpack-encoded.STICKY_ROUTING_MAX_CUSTOMERS_{mid}, default 1M new hashes per TTL window, tracked by two-window counterssticky_gw_adm_{mid}_{window}with TTL 2×window): once spent, existing customers keep updating but no new keys are created — growth bounded up front, no reliance on Redis eviction.New wrapper commands on
RedisConnectionWrapper:hincrby_with_expire(MULTI: HINCRBY+EXPIRE so a hash can never exist without a TTL),hgetall_map,hdel_field. Module:src/sticky_routing.rs.