Clarify sessionPersistence scoping and conflict resolution across HTTPRoutes and Backends
There is ambiguity in the Gateway API sessionPersistence GEP around what constitutes the scope of a sticky session and how conflicting configurations should be handled. In particular, it’s unclear whether the unit of stickiness should be per (HTTPRouteRule, backendRef, sessionName) or per (backendRef, sessionName) across rules and routes, and what should happen when multiple rules use the same (backendRef, sessionName) but define different sessionPersistence settings (e.g. different cookie lifetimes).
Initial conversation link
Goal : Clarifying these semantics and codifying them in conformance tests would help ensure consistent behavior across implementations.
More details
Ambiguity 1: Scope of sessionName and Unit of Stickiness
The first question is how strictly the spec wants to bind stickiness to a particular Route rule. Is it valid for two different HTTPRoute rules to configure sessionPersistence using the same sessionName?
kind: HTTPRoute
spec:
rules:
- matches:
- path:
type: PathPrefix
value: /cart
backendRefs:
- name: svc-a
port: 80
sessionPersistence:
name: shop-session
- matches:
- path:
type: PathPrefix
value: /checkout
backendRefs:
- name: svc-a
port: 80
sessionPersistence:
name: shop-session
Two possible interpretations:
-
Per-rule scope: The unit of stickiness is (rule, backendRef, sessionPersistence). Even though both rules share the same backend and sessionName, each rule has its own sticky “upstream” and can route the same client to different pods.
-
(BackendRef, sessionName) scope (as suggested in Slack) The unit of stickiness is (backendRef, sessionName). If multiple rules share that combination, they must share sticky sessions. In the example above, a client that hits
/cartand/checkoutwithshop-sessionwill consistently land on the same pod ofsvc-a:80.
Ambiguity 2: Conflicts When sessionPersistence Configs Differ
If we assume (backendRef, sessionName) is the intended scope, we then have a configuration question
# HTTPRoute A
rules:
- backendRefs:
- name: svc-a
port: 80
sessionPersistence:
name: shop-session
cookie:
lifetime: 2h
---
# HTTPRoute B
rules:
- backendRefs:
- name: svc-a
port: 80
sessionPersistence:
name: shop-session
cookie:
lifetime: 3h
Both rules share (backendRef = svc-a:80, sessionName = shop-session). But sessionPersistence differs (lifetime: 2h vs 3h, and in general other fields could differ too).
Some possible behaviors:
-
Deterministic winner - Choose one config (e.g., based on Route name, creation timestamp, lexical rule order, etc.), and apply it to the session for that (backendRef, sessionName) tuple. The other config is effectively ignored. This follows the general conflict guidance, but we should spell out the tie-breaker strategy for this specific case.
-
Configuration conflict - Treat this as a conflict because two different sessionPersistence definitions are trying to control the same logical session. Surface a
Conflictedcondition on the affected Routes and/or rules, indicating that (backendRef, sessionName) is defined inconsistently.
Clarifications:
- Can the spec explicitly define the unit of stickiness?
- What’s the intended behavior when the same (backendRef, sessionName) is used with different sessionPersistence settings?
Thanks for this issue @salonichf5, this is a great description. I agree that we should make decisions about this, although I haven't spent enough time thinking about sessions to really have an opinion on what the right unit of stickiness is.
For resolving conflicts though, we should definitely stick to the usual rules (deterministic, oldest object by creation time wins, followed by lexically-sorted name on ties).
Thanks for writing up all the context here @salonichf5!
/cc @gcs278 in case you had thought about some of these cases while writing the initial GEP
@youngnick @gcs278 @mikemorris I know there's been discussions around wanting to modify the spec a bit since its outdated. What can I do to possibly contribute to that? I would love to drive this forward with some more config additions that could be helpful for implementations
- With cookie-based session persistence, is it even possible to share stickiness across paths? See https://gateway-api.sigs.k8s.io/geps/gep-1619/#path, and the following quote:
It is also important to note that this design makes persistent session unique per route path. For instance, if two distinct routes, one with path prefix /foo and the other with /bar, both target the same service, the persistent session won't be shared between these two paths.
- If a user intends to have shared stickiness, wouldn't it suffice to write something like the following?
kind: HTTPRoute
spec:
rules:
- matches:
- path:
type: PathPrefix
value: /cart
- path:
type: PathPrefix
value: /checkout
backendRefs:
- name: svc-a
port: 80
sessionPersistence:
name: shop-session
-
I don't see the point of
sessionPersistence.nameexistence with per-rule scope of stickiness. So the original GEP author's intention must have been to allow sharing stickiness across rules. The section https://gateway-api.sigs.k8s.io/geps/gep-1619/#session-naming-collision confirms this line of thought. -
Having a field
sessionPersistence.nameembedded insideHTTPRouteis a problematic design (e.g. we end up with conflicts as described in @salonichf5's Ambiguity 2). We should make up our minds:- either inline session persistence config inside a rule and scope it per rule (then we don't need to
nameit), - or have session persistence configured on a separate resource and allow referring to this resource from multiple rules to allow stickiness sharing. (A similar config is described in https://gateway-api.sigs.k8s.io/geps/gep-1619/#route-rules-referencing-to-a-session-persistent-enabled-service-must-not-share-sessions, but the GEP explicitly says that there should be no sharing with that config, so we'd need a slightly different config.)
- either inline session persistence config inside a rule and scope it per rule (then we don't need to
@gcs278 I was wondering if you had any updates on this that could be help answer my questions.
@salonichf5 hello!
Thanks for asking these clarifications. I agree there are some ambiguities we need to work through. Sorry, I’ve been occupied for the last couple of months, but I plan to help with this again, starting next week. So let me get back to you after some review, and until then feel free to pursue any updates you feel necessary.
@salonichf5 Hi, sorry for the long delay. It's been a long time since I've look at this, which is great because everything feels new again.
I agree, the GEP fails to explicitly address shared sessions between when specified inline to route, though as @DamianSawicki mentioned, it does mention that sessions will NOT be shared when using BackendLBPolicy.
For context, I believe my original (undocumented) position was that no sessions would be shared between xRoutes paths, but that was more of a default position rather than a firm conviction. The original reasons that sessionPersistence.name was added is mentioned here:
I think "clients should not expect a specific name" could be highly contentious. Note that the client (whether gRPC or regular HTTP one) needs to copy the cookie value from the first response to subsequent requests so they need to know the cookie name. Either each implementation has a pre-baked name or this spec allows specifying a name and I prefer the latter option.
And here:
As shown in the 'prior art' - many load balancers have hardcoded names for the cookie - google, cloudfare, aws - and would not be practical for a client to guess.
So I agree adding the name field complicates thing, but we had some use cases that required it, so I think it needs to stay.
Now, it sounds like the consensus is leaning towards allowing session persistence across route paths. Here's what I think the easiest path forward is:
- Share session persistence for xRoute rules where
name==name - Apply the usual deconflicting rules
- For cases with differing sessionPersistence configurations on a single HTTPRoute, possibly mark as Conflicted
But I'm open to differing opinions, as I am not a huge fan of having to deconflict like this. We could explore using BackendLBPolicy as a way to enable shared sessions, though I think I prefer the granularity of allowing it on xRoute rules.
And as @DamianSawicki mentioned, https://gateway-api.sigs.k8s.io/geps/gep-1619/#path needs to get an update as it assumes no session persistence sharing among route paths is happening. When sharing, the cookie path needs to include all shared routes paths, which could be a common parent path, or just simply /. Otherwise, I think you'd get some less-than-desirable behavior when a client switches between the two paths.
@gcs278 thanks for the detailed reply — sharing session persistence across paths when name == name makes sense.
A couple clarifications to make sure I’m implementing this correctly:
Is sessionPersistence.name effectively required?
Right now, if sessionPersistence.name is omitted, our implementation generates one. But based on this discussion, that seems risky because it could unintentionally create sharing/collisions. So should name be a required field in sessionPersistence spec?
Scope of sharing My understanding is that if multiple HTTPRoute rules use the same (backendRef, sessionPersistence.name), they share the same session scope.
Conflicts when configs differ If two rules (or routes) define the same (backendRef, name) but with different sessionPersistence settings (e.g. different cookie lifetime), my initial plan was to surface a warning/error and ignore sessionPersistence for the conflicting cases, while still keeping the Route otherwise valid.
If we instead use an explicit Conflicted condition:
does that imply the entire Route becomes effectively invalid / not programmed, or
is it acceptable to keep the Route attached and only mark the conflicting rules as invalid (i.e. a partially-valid route)?
Sharing across different HTTPRoutes + conflict expectations Does the “name==name shares” behavior apply across different HTTPRoutes as well (not just multiple rules within a single HTTPRoute), assuming the same (backendRef, name)?
Related: if two HTTPRoutes target the same backendRef and use the same sessionPersistence name, should they always be treated as sharing the same effective config, or should both routes/rules be marked Conflicted when their sessionPersistence configs differ? I’m trying to avoid a situation where two routes effectively create two different upstream/session behaviors for the same (backendRef, name) tuple.
After thinking this through more, I’m leaning toward not sharing sessions across route rules:
- Session Sharing Is Already Possible
- After re-reading @DamianSawicki’s comment, I realized I missed an important detail: sessions can already be shared across different paths within the same HTTPRoute rule.
- While this forces shared filters, that feels like an acceptable tradeoff, especially considering point 3 below.
- Explicit vs. Implicit Semantics
- Using matching values to implicitly create linking or sharing behavior feels inconsistent with existing API semantics.
- Today, all forms of linking rely on explicit references, such as
parentRef.nameortargetRef.name. - Shared behavior from adjacent rules without a clear reference feels too implicit.
- Revisiting Shared Sessions for BackendTrafficPolicy (previously BackendLBPolicy)
- When
BackendTrafficPolicy.spec.sessionPersistence.nameis configured, having a shared cookie name implies that sharing is the intended behavior. - One option would be to treat an empty
nameas a signal for separate sessions, while an explicitnamewould indicate shared sessions forBackendTrafficPolicy. - For context, there was related discussion here:
https://github.com/kubernetes-sigs/gateway-api/pull/2909#discussion_r1547075531
- When
I think this makes it a bit less complicated to implement (hopefully?).
Though a follow up question is: Do we still need to reject route rules with the same session name? According to https://gateway-api.sigs.k8s.io/geps/gep-1619/#path, if we set a unique cookie path attribute for each route path, then that effectively makes the cookie unique so they can indeed have separate configurations. Though I think there's some edge cases with regexes that might need to be documented.
Thoughts? Agree? Disagree? @mikemorris maybe you can chime in too if you have time.
Given this change in position, my answers below:
Is sessionPersistence.name effectively required?
I do see your point about it being easier to avoid collisions if we make it required, but I don't think that's possible per some of our other use cases in https://gateway-api.sigs.k8s.io/geps/gep-1619/#name:
Additionally, SessionName is not universally supported as some implementations, such as
ones supporting global load balancers, don't have the capability to configure the cookie name.
This feels like the responsibility should be on the implementation to avoid naming collisions. How does your implementation handle naming the cookies? Is this easy for you to avoid?
is it acceptable to keep the Route attached and only mark the conflicting rules as invalid (i.e. a partially-valid route)?
TBD on whether we need to reject, but to answer this: we do have the PartiallyInvalid route condition which I think would be appropriate (if needed).
Does the “name==name shares” behavior apply across different HTTPRoutes as well (not just multiple rules within a single HTTPRoute), assuming the same (backendRef, name)?
See above, I don't think so anymore. But may still need to be rejected, TBD on my question above.