Expand description
Service-level traffic splitting (DW-040, feature analysis section 5 “Traffic”): canary releases, blue-green switches, and the sticky cookie that pins a session through them.
§The pick
A split service dispatches each request to ONE of its target
upstreams by a deterministic weighted pick over the SAME FNV-1a
hash the balancer’s consistent-hash ring uses
(balance::key_hash — the stability argument for that choice,
toolchain-stable hashing, applies identically here):
- key = the sticky cookie’s VALUE when the service configures
crate::config::StickyAffinityand the request carries the cookie — a session lands on the same branch for every request while the weights are unchanged; - key = the request id otherwise — per-request distribution whose realized ratios converge on the configured weights statistically (the issue’s acceptance shape: “split ratios verified statistically”).
The slot is hash % total_weight, walked over the targets’
cumulative weights. DISPLACEMENT IS BOUNDED BY AN INVARIANT TOTAL:
when a weight change KEEPS the total constant, only the changed
share of hash space moves — 95/5 -> 90/10 moves exactly the 5% that
became canary, and the blue-green flip 100/0 -> 0/100 (total still
100) moves exactly the stable side. A change that ALTERS the total
changes the modulus and therefore reshuffles EVERY key — ramp a
canary by RE-BALANCING the pair (95/5 -> 90/10), never by growing
the canary alone (95/5 -> 95/10 reshuffles all sessions). The pick
is STATELESS — no counters, no locks, no per-service runtime state
— which is what makes the blue-green switch instant: a re-publish
rebuilds the compiled split, and the very next request dispatches
by the new generation.
§Stickiness is layered
The cookie guarantees BRANCH affinity (which upstream). Endpoint
affinity within a branch comes from the branch upstream’s own
balancer: when it runs ip_hash, the sticky value becomes the
ring key (the proxy passes it as the dispatch hash key), so the
session pins one endpoint through the same ketama machinery that
pins client IPs. With other balancers the branch pick is the
affinity guarantee and the endpoint is free to float — documented,
not hidden: ring-based endpoint pinning and round-robin spreading
are different tools and the operator picks per branch.
The cookie value is an opaque affinity handle generated by the gateway (time + a process counter, hex): NOT a secret, carrying no identity, never trusted as one. Reusing a presented cookie is safe precisely because its only power is choosing among upstreams the operator already configured — a client “forging” one can only pick a branch it could have landed on anyway.
One documented edge: a session whose FIRST request is a response cache HIT is served without a dispatch, so no cookie is minted until its first cache MISS — while hits last there is nothing to pin (no upstream was contacted), and the miss mints exactly as the uncached path would.
Structs§
- Service
Split - The compiled weighted split of one service (DW-040): built per config generation by the upstream registry, held by the dataplane dispatch path. Immutable once built; the pick is stateless.
- Split
Entry - One compiled target of a service split (DW-040).
Functions§
- mint_
affinity_ id - Mint a fresh affinity handle:
hex(unix ms) hex(counter)— 24 printable hex characters. Opaque, unique per process, NOT a secret and never treated as one (its only power is choosing among upstreams the operator already configured). - read_
cookie - Read one cookie’s value from a request’s
Cookieheaders (the first match across all header lines wins;Cookieis a semicolon-separatedname=valuelist per RFC 6265). Returns the trimmed value; a malformed pair cannot match a validated cookie name and is skipped silently (the gateway then mints a fresh handle — the safe direction: a bad cookie re-sessions rather than mis-routing).