Skip to content

Add WebSession-based sticky load balancing filter - #4234

Open
Gebreyesus wants to merge 1 commit into
spring-cloud:mainfrom
Gebreyesus:feature/session-based-sticky-loadbalancer
Open

Add WebSession-based sticky load balancing filter#4234
Gebreyesus wants to merge 1 commit into
spring-cloud:mainfrom
Gebreyesus:feature/session-based-sticky-loadbalancer

Conversation

@Gebreyesus

@Gebreyesus Gebreyesus commented Jul 8, 2026

Copy link
Copy Markdown
## Add WebSession-based sticky load balancing (server-side, per-route)

This PR adds per-route, server-side session affinity to Spring Cloud Gateway using the gateway's own `WebSession` as the affinity store.

### Problem

`ReactiveLoadBalancerClientFilter` passes a `RequestDataContext` to the load balancer. That context carries HTTP request metadata but no reference to the `ServerWebExchange` or `WebSession`. A custom `ReactorServiceInstanceLoadBalancer` therefore cannot implement server-side session affinity without fully replacing the default filter.

The existing `RequestBasedStickySessionServiceInstanceListSupplier` solves a related but different problem: it keys stickiness off a client-supplied cookie and applies uniformly to a service, with no per-route opt-in.

Our production case involves a legacy monolith that cannot achieve distributed sessions. The gateway itself must track which instance a client is pinned to, stored server-side in `WebSession`, with per-route granularity.

### What this PR adds

**`WebSessionStickyGatewayFilterFactory`** (the recommended opt-in mechanism)

A per-route `GatewayFilter` that sets `IS_STICKY_ATTRIBUTE = true` on the exchange. Works with the standard `lb://` URI scheme and composes with other filters:

```yaml
spring:
  cloud:
    gateway:
      server:
        webflux:
          global-filter:
            web-session-sticky-load-balancer:
              enabled: true
          routes:
            - id: legacy-route
              uri: lb://legacy-service
              predicates:
                - Path=/legacy/**
              filters:
                - WebSessionSticky
            - id: stateless-route
              uri: lb://legacy-service
              predicates:
                - Path=/api/**

/legacy/** requests are pinned to one instance per client session. /api/** requests use normal round-robin. Same service, different behavior per route.

WebSessionStickyLoadBalancer

A ReactorServiceInstanceLoadBalancer that:

  • Reads/writes a serviceId -> instanceId affinity map from the gateway WebSession
  • Reuses the pinned instance while it remains registered
  • Transparently re-pins via round-robin if the instance disappears
  • Falls through to the delegate for non-sticky exchanges or non-exchange contexts (e.g. internal WebClient calls)

WebSessionStickyLoadBalancerFilter (alternative opt-in)

A GlobalFilter that handles routes using the sticky:// URI scheme as an alternative to the GatewayFilter approach. Disabled by default.

Key design decisions

  • Affinity stored server-side in WebSession, not in a client cookie
  • Per-route opt-in via - WebSessionSticky filter (or sticky:// scheme)
  • Disabled by default; zero impact on existing deployments
  • Uses 2-arg getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class) so a WebSessionStickyLoadBalancer wired via @LoadBalancerClient is resolved correctly per service
  • Transparent re-pinning on instance loss (no client-visible error)

Files changed

File Purpose
WebSessionStickyGatewayFilterFactory.java Per-route filter that marks exchange as sticky
WebSessionStickyLoadBalancer.java Load balancer with session affinity logic
WebSessionStickyLoadBalancerFilter.java GlobalFilter that performs instance selection and URI rewrite for sticky routes
GatewayReactiveLoadBalancerClientAutoConfiguration.java Wires both beans (disabled by default)
WebSessionStickyLoadBalancerTests.java 6 unit tests for the load balancer
WebSessionStickyLoadBalancerFilterTests.java 10 unit tests for the global filter
WebSessionStickyGatewayFilterFactoryTests.java 4 unit tests for the factory
global-filters.adoc Documents the global filter
websessionsticky-factory.adoc Documents the GatewayFilter factory
nav.adoc Registers the factory doc page

Testing

  • 20 new unit tests, all passing
  • All 22 pre-existing ReactiveLoadBalancerClientFilterTests and LoadBalancerServiceInstanceCookieFilterTests pass without modification

Checklist

  • ASF license header on all new .java files
  • @author tags on new and modified files
  • Unit tests for all new classes
  • Documentation added
  • Disabled by default
  • DCO Signed-off-by on commit

Related: #1176, spring-cloud/spring-cloud-commons#764

@Gebreyesus
Gebreyesus force-pushed the feature/session-based-sticky-loadbalancer branch from cff3f91 to fb61cc5 Compare July 8, 2026 22:01
@spencergibb

Copy link
Copy Markdown
Member

Without having reviewed this at all, I'm not sure a new scheme is the way to configure this

…eLoadBalancerClientFilter passes a RequestDataContext to the load balancer, carrying no reference to the ServerWebExchange or WebSession. This makes it impossible for a custom ReactorServiceInstanceLoadBalancer to implement per-route, server-side session affinity without fully replacing the default filter. This commit adds: WebSessionStickyLoadBalancerFilter: a GlobalFilter that handles routes with the sticky:// URI scheme. Sets IS_STICKY_ATTRIBUTE on the exchange, passes the raw ServerWebExchange as load-balancer context, and rewrites the request URI to the chosen instance. Runs at the same order as ReactiveLoadBalancerClientFilter (10150). lb:// routes are not affected. WebSessionStickyLoadBalancer: a ReactorServiceInstanceLoadBalancer that stores a serviceId->instanceId affinity map in the gateway WebSession. Reuses the pinned instance while registered; transparently re-pins via round-robin if the instance is deregistered or fails a health check. Falls through to the delegate for non-sticky exchanges and for any context that is not a ServerWebExchange (e.g. internal WebClient calls). WebSessionStickyGatewayFilterFactory: a per-route GatewayFilter that enables the same sticky behavior while keeping lb:// as the URI scheme. Routes add - WebSessionSticky to their filter list. This is the composable alternative to the sticky:// scheme approach. choose() uses the 2-arg getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class) overload, matching ReactiveLoadBalancerClientFilter, so a WebSessionStickyLoadBalancer configured via @LoadBalancerClient is resolved correctly per service. GatewayReactiveLoadBalancerClientAutoConfiguration wires both beans, disabled by default: spring.cloud.gateway.global-filter.web-session-sticky-load-balancer.enabled=true Routes opt in per-route via either: uri: sticky://my-service or: filters: - WebSessionSticky Documentation added to global-filters.adoc and gatewayfilter-factories/websessionsticky-factory.adoc. Related upstream gap: spring-cloud/spring-cloud-commons#764 and spring-cloud#1176.

Signed-off-by: Beteab G. Gebreyesus <beteab@live.com>
@Gebreyesus
Gebreyesus force-pushed the feature/session-based-sticky-loadbalancer branch from c82577d to 8a4bba3 Compare July 12, 2026 22:22
@Gebreyesus

Gebreyesus commented Jul 12, 2026

Copy link
Copy Markdown
Author

Without having reviewed this at all, I'm not sure a new scheme is the way to configure this

@spencergibb Agreed.
I've updated the PR to provide a WebSessionSticky GatewayFilterFactory as the primary opt-in mechanism. It uses the standard lb:// scheme and applies sticky load balancing per route through configuration:

routes:
  - id: legacy-route
    uri: lb://legacy-service
    predicates:
      - Path=/legacy/**
    filters:
      - WebSessionSticky 

When this filter is applied to a route, every client session is pinned to a specific backend instance for the duration of that session. Affinity is maintained server-side in the gateway WebSession. No new URI scheme required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants