gateway-api icon indicating copy to clipboard operation
gateway-api copied to clipboard

Clarify sessionPersistence scoping and conflict resolution across HTTPRoutes and Backends

Open salonichf5 opened this issue 1 month ago • 7 comments

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:

  1. 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.

  2. (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 /cart and /checkout with shop-session will consistently land on the same pod of svc-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:

  1. 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.

  2. Configuration conflict - Treat this as a conflict because two different sessionPersistence definitions are trying to control the same logical session. Surface a Conflicted condition 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?

salonichf5 avatar Nov 24 '25 05:11 salonichf5

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).

youngnick avatar Nov 25 '25 04:11 youngnick

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

mikemorris avatar Nov 25 '25 14:11 mikemorris

@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

salonichf5 avatar Nov 26 '25 01:11 salonichf5

  1. 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.

  1. 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
  1. I don't see the point of sessionPersistence.name existence 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.

  2. Having a field sessionPersistence.name embedded inside HTTPRoute is 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 name it),
    • 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.)

DamianSawicki avatar Nov 26 '25 14:11 DamianSawicki

@gcs278 I was wondering if you had any updates on this that could be help answer my questions.

salonichf5 avatar Dec 08 '25 18:12 salonichf5

@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.

gcs278 avatar Dec 09 '25 17:12 gcs278

@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 avatar Dec 16 '25 21:12 gcs278

@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.

salonichf5 avatar Dec 17 '25 21:12 salonichf5

After thinking this through more, I’m leaning toward not sharing sessions across route rules:

  1. 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.
  2. 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.name or targetRef.name.
    • Shared behavior from adjacent rules without a clear reference feels too implicit.
  3. Revisiting Shared Sessions for BackendTrafficPolicy (previously BackendLBPolicy)
    • When BackendTrafficPolicy.spec.sessionPersistence.name is configured, having a shared cookie name implies that sharing is the intended behavior.
    • One option would be to treat an empty name as a signal for separate sessions, while an explicit name would indicate shared sessions for BackendTrafficPolicy.
    • For context, there was related discussion here:
      https://github.com/kubernetes-sigs/gateway-api/pull/2909#discussion_r1547075531

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.

gcs278 avatar Dec 19 '25 02:12 gcs278