From 799ed7763f433aabeb7d418beec133fec25dbdb3 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Wed, 2 Dec 2020 17:10:42 +0100 Subject: [PATCH 1/7] Implement first draft for sticky-session load-balancing. --- .../reactive/ExchangeFilterFunctionUtils.java | 15 ++- .../reactive/LoadBalancerProperties.java | 10 ++ ...torLoadBalancerExchangeFilterFunction.java | 4 +- ...bleLoadBalancerExchangeFilterFunction.java | 4 +- .../LoadBalancerClientConfiguration.java | 11 ++ ...ckySessionServiceInstanceListSupplier.java | 120 ++++++++++++++++++ .../ServiceInstanceListSupplierBuilder.java | 14 ++ 7 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java index 18e7124ec..a1d1a7ad5 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java @@ -19,6 +19,8 @@ import java.net.URI; import java.util.Map; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; @@ -40,10 +42,15 @@ static String getHint(String serviceId, Map hints) { return hintPropertyValue != null ? hintPropertyValue : defaultHint; } - static ClientRequest buildClientRequest(ClientRequest request, URI uri) { - return ClientRequest.create(request.method(), uri).headers(headers -> headers.addAll(request.headers())) - .cookies(cookies -> cookies.addAll(request.cookies())) - .attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build(); + static ClientRequest buildClientRequest(ClientRequest request, ServiceInstance serviceInstance, + String instanceIdCookieName) { + URI originalUrl = request.url(); + return ClientRequest.create(request.method(), LoadBalancerUriTools.reconstructURI(serviceInstance, originalUrl)) + .headers(headers -> headers.addAll(request.headers())).cookies(cookies -> { + cookies.addAll(request.cookies()); + // TODO: make opt-in + cookies.add(instanceIdCookieName, serviceInstance.getInstanceId()); + }).attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build(); } static String serviceInstanceUnavailableMessage(String serviceId) { diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java index 218bf534a..633cd7cea 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java @@ -53,6 +53,8 @@ public class LoadBalancerProperties { */ private Retry retry = new Retry(); + private String instanceIdCookieName = "sc-lb-instance-id"; + public HealthCheck getHealthCheck() { return healthCheck; } @@ -77,6 +79,14 @@ public void setRetry(Retry retry) { this.retry = retry; } + public String getInstanceIdCookieName() { + return instanceIdCookieName; + } + + public void setInstanceIdCookieName(String instanceIdCookieName) { + this.instanceIdCookieName = instanceIdCookieName; + } + public static class HealthCheck { /** diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java index ec2e150bb..fa91825a7 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java @@ -38,7 +38,6 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFunction; -import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.reconstructURI; import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.buildClientRequest; import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.getHint; import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.serviceInstanceUnavailableMessage; @@ -101,7 +100,8 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction LOG.debug(String.format("LoadBalancer has retrieved the instance for service %s: %s", serviceId, instance.getUri())); } - ClientRequest newRequest = buildClientRequest(clientRequest, reconstructURI(instance, originalUrl)); + ClientRequest newRequest = buildClientRequest(clientRequest, instance, + properties.getInstanceIdCookieName()); return next.exchange(newRequest) .doOnError(throwable -> supportedLifecycleProcessors.forEach( lifecycle -> lifecycle.onComplete(new CompletionContext( diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java index 8864e9733..a4f55cf02 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java @@ -46,7 +46,6 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFunction; -import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.reconstructURI; import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.buildClientRequest; import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.getHint; import static org.springframework.cloud.client.loadbalancer.reactive.ExchangeFilterFunctionUtils.serviceInstanceUnavailableMessage; @@ -123,7 +122,8 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction LOG.debug(String.format("LoadBalancer has retrieved the instance for service %s: %s", serviceId, instance.getUri())); } - ClientRequest newRequest = buildClientRequest(clientRequest, reconstructURI(instance, originalUrl)); + ClientRequest newRequest = buildClientRequest(clientRequest, instance, + properties.getInstanceIdCookieName()); return next.exchange(newRequest) .doOnError(throwable -> supportedLifecycleProcessors.forEach( lifecycle -> lifecycle.onComplete(new CompletionContext( diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java index 9ec31471e..957110c0a 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java @@ -98,6 +98,17 @@ public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceList return ServiceInstanceListSupplier.builder().withDiscoveryClient().withHealthChecks().build(context); } + @Bean + @ConditionalOnBean(ReactiveDiscoveryClient.class) + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", + havingValue = "request-based-sticky-session") + public ServiceInstanceListSupplier requestBasedStickySessionDiscoveryClientServiceInstanceListSupplier( + ConfigurableApplicationContext context) { + return ServiceInstanceListSupplier.builder().withDiscoveryClient().withRequestBasedStickySession() + .build(context); + } + } @Configuration(proxyBeanMethods = false) diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java new file mode 100644 index 000000000..961747e8f --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.loadbalancer.core; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.ClientRequestContext; +import org.springframework.cloud.client.loadbalancer.Request; +import org.springframework.cloud.client.loadbalancer.ServerHttpRequestContext; +import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.http.HttpCookie; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.reactive.function.client.ClientRequest; + +/** + * A session cookie based implementation of {@link ReactorServiceInstanceLoadBalancer} + * that ensures requests from the same client are routed to the same server. + * + * @author Olga Maciaszek-Sharma + */ +public class RequestBasedStickySessionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier { + + private static final Log LOG = LogFactory.getLog(RequestBasedStickySessionServiceInstanceListSupplier.class); + + private final LoadBalancerProperties properties; + + public RequestBasedStickySessionServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, + LoadBalancerProperties properties) { + super(delegate); + this.properties = properties; + } + + @Override + public String getServiceId() { + return delegate.getServiceId(); + } + + @Override + public Flux> get() { + return delegate.get(); + } + + @SuppressWarnings("rawtypes") + @Override + public Flux> get(Request request) { + String instanceIdCookieName = properties.getInstanceIdCookieName(); + Object context = request.getContext(); + if ((context instanceof ClientRequestContext)) { + ClientRequest originalRequest = ((ClientRequestContext) context).getClientRequest(); + // We expect there to be one value in this cookie + String cookie = originalRequest.cookies().getFirst(instanceIdCookieName); + if (cookie != null) { + return get().map(serviceInstances -> selectInstance(serviceInstances, cookie)); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Cookie not found. Returning all instances returned by delegate."); + } + return get(); + } + if ((context instanceof ServerHttpRequestContext)) { + ServerHttpRequest originalRequest = ((ServerHttpRequestContext) context).getClientRequest(); + HttpCookie cookie = originalRequest.getCookies().getFirst(instanceIdCookieName); + if (cookie != null) { + return get().map(serviceInstances -> selectInstance(serviceInstances, cookie.getValue())); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Cookie not found. Returning all instances returned by delegate."); + } + return get(); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Searching for instances based on cookie not supported for ClientRequestContext type." + + " Returning all instances returned by delegate."); + } + // If no cookie is available, we return all the instances provided by the + // delegate. + return get(); + } + + private List selectInstance(List serviceInstances, String cookie) { + for (ServiceInstance serviceInstance : serviceInstances) { + if (cookie.equals(serviceInstance.getInstanceId())) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Returning the service instance: %s. Found for cookie: %s", + serviceInstance.toString(), cookie)); + } + return Collections.singletonList(serviceInstance); + } + } + // If the instances cannot be found based on the cookie, + // we return all the instances provided by the delegate. + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "Service instance for cookie: %s not found. Returning all instances returned by delegate.", + cookie)); + } + return serviceInstances; + } + +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java index 22cd7ac29..40d7d72e4 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java @@ -145,6 +145,20 @@ public ServiceInstanceListSupplierBuilder withZonePreference() { return this; } + /** + * Adds a {@link ZonePreferenceServiceInstanceListSupplier} to the + * {@link ServiceInstanceListSupplier} hierarchy. + * @return the {@link ServiceInstanceListSupplierBuilder} object + */ + public ServiceInstanceListSupplierBuilder withRequestBasedStickySession() { + DelegateCreator creator = (context, delegate) -> { + LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class); + return new RequestBasedStickySessionServiceInstanceListSupplier(delegate, properties); + }; + this.creators.add(creator); + return this; + } + /** * If {@link LoadBalancerCacheManager} is available in the context, wraps created * {@link ServiceInstanceListSupplier} hierarchy with a From 6b01147c886427a5483f9197bb10785b10956217 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 3 Dec 2020 11:17:45 +0100 Subject: [PATCH 2/7] Make setting instance cookie opt-in. --- .../loadbalancer/reactive/ExchangeFilterFunctionUtils.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java index a1d1a7ad5..a12e06fdb 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java @@ -48,8 +48,11 @@ static ClientRequest buildClientRequest(ClientRequest request, ServiceInstance s return ClientRequest.create(request.method(), LoadBalancerUriTools.reconstructURI(serviceInstance, originalUrl)) .headers(headers -> headers.addAll(request.headers())).cookies(cookies -> { cookies.addAll(request.cookies()); - // TODO: make opt-in - cookies.add(instanceIdCookieName, serviceInstance.getInstanceId()); + if (!(instanceIdCookieName == null || instanceIdCookieName + .length() == 0)) { + cookies.add(instanceIdCookieName, serviceInstance + .getInstanceId()); + } }).attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build(); } From a840163901f9ef5326de9df4ca3b1cc2dae81ea0 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 3 Dec 2020 13:10:15 +0100 Subject: [PATCH 3/7] Add tests. --- .../reactive/ExchangeFilterFunctionUtils.java | 6 +- .../LoadBalancerClientConfigurationTests.java | 14 ++ ...ssionServiceInstanceListSupplierTests.java | 162 ++++++++++++++++++ ...renceServiceInstanceListSupplierTests.java | 20 +-- 4 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java index a12e06fdb..356621bf9 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java @@ -48,10 +48,8 @@ static ClientRequest buildClientRequest(ClientRequest request, ServiceInstance s return ClientRequest.create(request.method(), LoadBalancerUriTools.reconstructURI(serviceInstance, originalUrl)) .headers(headers -> headers.addAll(request.headers())).cookies(cookies -> { cookies.addAll(request.cookies()); - if (!(instanceIdCookieName == null || instanceIdCookieName - .length() == 0)) { - cookies.add(instanceIdCookieName, serviceInstance - .getInstanceId()); + if (!(instanceIdCookieName == null || instanceIdCookieName.length() == 0)) { + cookies.add(instanceIdCookieName, serviceInstance.getInstanceId()); } }).attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build(); } diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationTests.java index 1379b62e9..6d8becdc0 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationTests.java @@ -30,6 +30,7 @@ import org.springframework.cloud.loadbalancer.core.DelegatingServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.core.HealthCheckServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.RequestBasedStickySessionServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.core.RetryAwareServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.core.ZonePreferenceServiceInstanceListSupplier; @@ -111,6 +112,19 @@ void shouldInstantiateHealthCheckServiceInstanceListSupplier() { }); } + @Test + void shouldInstantiateRequestBasedStickySessionServiceInstanceListSupplierTests() { + reactiveDiscoveryClientRunner.withUserConfiguration(TestConfig.class) + .withPropertyValues("spring.cloud.loadbalancer.configurations=request-based-sticky-session") + .run(context -> { + ServiceInstanceListSupplier supplier = context.getBean(ServiceInstanceListSupplier.class); + then(supplier).isInstanceOf(RequestBasedStickySessionServiceInstanceListSupplier.class); + ServiceInstanceListSupplier delegate = ((DelegatingServiceInstanceListSupplier) supplier) + .getDelegate(); + then(delegate).isInstanceOf(DiscoveryClientServiceInstanceListSupplier.class); + }); + } + @Test void shouldInstantiateDefaultBlockingServiceInstanceListSupplierWhenConfigurationsPropertyNotSet() { blockingDiscoveryClientRunner.run(context -> { diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java new file mode 100644 index 000000000..c0950ab1c --- /dev/null +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.loadbalancer.core; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.ClientRequestContext; +import org.springframework.cloud.client.loadbalancer.DefaultRequest; +import org.springframework.cloud.client.loadbalancer.DefaultRequestContext; +import org.springframework.cloud.client.loadbalancer.Request; +import org.springframework.cloud.client.loadbalancer.ServerHttpRequestContext; +import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.client.ClientRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Olga Maciaszek-Sharma + */ +class RequestBasedStickySessionServiceInstanceListSupplierTests { + + private final DiscoveryClientServiceInstanceListSupplier delegate = mock( + DiscoveryClientServiceInstanceListSupplier.class); + + private final LoadBalancerProperties properties = new LoadBalancerProperties(); + + private final RequestBasedStickySessionServiceInstanceListSupplier supplier = new RequestBasedStickySessionServiceInstanceListSupplier( + delegate, properties); + + private final ClientRequest clientRequest = mock(ClientRequest.class); + + private final ServerHttpRequest serverHttpRequest = mock(ServerHttpRequest.class); + + private final ServiceInstance first = serviceInstance("test-1"); + + private final ServiceInstance second = serviceInstance("test-2"); + + private final ServiceInstance third = serviceInstance("test-3"); + + @BeforeEach + void setUp() { + when(delegate.get()).thenReturn(Flux.just(Arrays.asList(first, second, third))); + } + + @Test + void shouldReturnInstanceBasedOnCookieFromClientRequest() { + HttpHeaders headers = new HttpHeaders(); + headers.add(properties.getInstanceIdCookieName(), "test-1"); + when(clientRequest.cookies()).thenReturn(headers); + Request request = new DefaultRequest<>(new ClientRequestContext(clientRequest)); + + List serviceInstances = supplier.get(request).blockFirst(); + + assertThat(serviceInstances).hasSize(1); + assertThat(serviceInstances.get(0).getInstanceId()).isEqualTo("test-1"); + } + + @Test + void shouldReturnAllDelegateInstancesIfInstanceBasedOnCookieFromClientRequestNotFound() { + HttpHeaders headers = new HttpHeaders(); + headers.add(properties.getInstanceIdCookieName(), "test-4"); + when(clientRequest.cookies()).thenReturn(headers); + Request request = new DefaultRequest<>(new ClientRequestContext(clientRequest)); + + List serviceInstances = supplier.get(request).blockFirst(); + + assertThat(serviceInstances).hasSize(3); + } + + @Test + void shouldReturnAllInstancesFromDelegateIfClientRequestHasNoCookie() { + when(clientRequest.cookies()).thenReturn(new HttpHeaders()); + Request request = new DefaultRequest<>(new ClientRequestContext(clientRequest)); + + List serviceInstances = supplier.get(request).blockFirst(); + + assertThat(serviceInstances).hasSize(3); + } + + @Test + void shouldReturnInstanceBasedOnCookieFromServerHttpRequest() { + MultiValueMap cookies = new LinkedMultiValueMap<>(); + cookies.add(properties.getInstanceIdCookieName(), + new HttpCookie(properties.getInstanceIdCookieName(), "test-1")); + when(serverHttpRequest.getCookies()).thenReturn(cookies); + Request request = new DefaultRequest<>( + new ServerHttpRequestContext(serverHttpRequest)); + + List serviceInstances = supplier.get(request).blockFirst(); + + assertThat(serviceInstances).hasSize(1); + assertThat(serviceInstances.get(0).getInstanceId()).isEqualTo("test-1"); + } + + @Test + void shouldReturnAllDelegateInstancesIfInstanceBasedOnCookieFromServerHttpRequestNotFound() { + MultiValueMap cookies = new LinkedMultiValueMap<>(); + cookies.add(properties.getInstanceIdCookieName(), + new HttpCookie(properties.getInstanceIdCookieName(), "test-4")); + when(serverHttpRequest.getCookies()).thenReturn(cookies); + Request request = new DefaultRequest<>( + new ServerHttpRequestContext(serverHttpRequest)); + + List serviceInstances = supplier.get(request).blockFirst(); + + assertThat(serviceInstances).hasSize(3); + } + + @Test + void shouldReturnAllInstancesFromDelegateIfServerHttpRequestHasNoCookie() { + MultiValueMap cookies = new LinkedMultiValueMap<>(); + when(serverHttpRequest.getCookies()).thenReturn(cookies); + Request request = new DefaultRequest<>( + new ServerHttpRequestContext(serverHttpRequest)); + + List serviceInstances = supplier.get(request).blockFirst(); + + assertThat(serviceInstances).hasSize(3); + } + + @Test + void shouldReturnAllInstancesFromDelegateIfNotSupportedRequestContext() { + Request request = new DefaultRequest<>(new DefaultRequestContext(clientRequest)); + + List serviceInstances = supplier.get(request).blockFirst(); + + assertThat(serviceInstances).hasSize(3); + } + + private DefaultServiceInstance serviceInstance(String instanceId) { + return new DefaultServiceInstance(instanceId, "test", "http://test.test", 9080, false); + } + +} diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplierTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplierTests.java index c6ff15639..4fe7d39f6 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplierTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplierTests.java @@ -41,23 +41,23 @@ */ class ZonePreferenceServiceInstanceListSupplierTests { - private DiscoveryClientServiceInstanceListSupplier delegate = mock( + private final DiscoveryClientServiceInstanceListSupplier delegate = mock( DiscoveryClientServiceInstanceListSupplier.class); - private LoadBalancerZoneConfig zoneConfig = new LoadBalancerZoneConfig(null); + private final LoadBalancerZoneConfig zoneConfig = new LoadBalancerZoneConfig(null); - private ZonePreferenceServiceInstanceListSupplier supplier = new ZonePreferenceServiceInstanceListSupplier(delegate, - zoneConfig); + private final ZonePreferenceServiceInstanceListSupplier supplier = new ZonePreferenceServiceInstanceListSupplier( + delegate, zoneConfig); - private ServiceInstance first = serviceInstance("test-1", buildZoneMetadata("zone1")); + private final ServiceInstance first = serviceInstance("test-1", buildZoneMetadata("zone1")); - private ServiceInstance second = serviceInstance("test-2", buildZoneMetadata("zone1")); + private final ServiceInstance second = serviceInstance("test-2", buildZoneMetadata("zone1")); - private ServiceInstance third = serviceInstance("test-3", buildZoneMetadata("zone2")); + private final ServiceInstance third = serviceInstance("test-3", buildZoneMetadata("zone2")); - private ServiceInstance fourth = serviceInstance("test-4", buildZoneMetadata("zone3")); + private final ServiceInstance fourth = serviceInstance("test-4", buildZoneMetadata("zone3")); - private ServiceInstance fifth = serviceInstance("test-5", buildZoneMetadata(null)); + private final ServiceInstance fifth = serviceInstance("test-5", buildZoneMetadata(null)); @Test void shouldFilterInstancesByZone() { @@ -103,7 +103,7 @@ void shouldReturnAllInstancesIfNoZone() { } private DefaultServiceInstance serviceInstance(String instanceId, Map metadata) { - return new DefaultServiceInstance("test", instanceId, "http://test.test", 9080, false, metadata); + return new DefaultServiceInstance(instanceId, "test", "http://test.test", 9080, false, metadata); } private Map buildZoneMetadata(String zone) { From 0b4a5f7ab3fcfb91c1a7a3c89d9f0ef6b6025810 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 3 Dec 2020 15:49:56 +0100 Subject: [PATCH 4/7] Make adding request cookie opt-in. --- .../reactive/ExchangeFilterFunctionUtils.java | 8 +++-- .../reactive/LoadBalancerProperties.java | 34 ++++++++++++++++--- ...torLoadBalancerExchangeFilterFunction.java | 4 ++- ...bleLoadBalancerExchangeFilterFunction.java | 8 +++-- ...ckySessionServiceInstanceListSupplier.java | 2 +- ...ssionServiceInstanceListSupplierTests.java | 12 +++---- 6 files changed, 49 insertions(+), 19 deletions(-) diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java index 356621bf9..34a07faf7 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java @@ -43,15 +43,17 @@ static String getHint(String serviceId, Map hints) { } static ClientRequest buildClientRequest(ClientRequest request, ServiceInstance serviceInstance, - String instanceIdCookieName) { + String instanceIdCookieName, boolean addServiceInstanceCookie) { URI originalUrl = request.url(); return ClientRequest.create(request.method(), LoadBalancerUriTools.reconstructURI(serviceInstance, originalUrl)) .headers(headers -> headers.addAll(request.headers())).cookies(cookies -> { cookies.addAll(request.cookies()); - if (!(instanceIdCookieName == null || instanceIdCookieName.length() == 0)) { + if (!(instanceIdCookieName == null || instanceIdCookieName.length() == 0) + && addServiceInstanceCookie) { cookies.add(instanceIdCookieName, serviceInstance.getInstanceId()); } - }).attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build(); + }) + .attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build(); } static String serviceInstanceUnavailableMessage(String serviceId) { diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java index 633cd7cea..52a31c40e 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java @@ -53,7 +53,7 @@ public class LoadBalancerProperties { */ private Retry retry = new Retry(); - private String instanceIdCookieName = "sc-lb-instance-id"; + private StickySession stickySession = new StickySession(); public HealthCheck getHealthCheck() { return healthCheck; @@ -79,12 +79,36 @@ public void setRetry(Retry retry) { this.retry = retry; } - public String getInstanceIdCookieName() { - return instanceIdCookieName; + public StickySession getStickySession() { + return stickySession; } - public void setInstanceIdCookieName(String instanceIdCookieName) { - this.instanceIdCookieName = instanceIdCookieName; + public void setStickySession(StickySession stickySession) { + this.stickySession = stickySession; + } + + public static class StickySession { + + private String instanceIdCookieName = "sc-lb-instance-id"; + + private boolean addServiceInstanceCookie = false; + + public String getInstanceIdCookieName() { + return instanceIdCookieName; + } + + public void setInstanceIdCookieName(String instanceIdCookieName) { + this.instanceIdCookieName = instanceIdCookieName; + } + + public boolean isAddServiceInstanceCookie() { + return addServiceInstanceCookie; + } + + public void setAddServiceInstanceCookie(boolean addServiceInstanceCookie) { + this.addServiceInstanceCookie = addServiceInstanceCookie; + } + } public static class HealthCheck { diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java index fa91825a7..7ce1df5f1 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java @@ -100,8 +100,10 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction LOG.debug(String.format("LoadBalancer has retrieved the instance for service %s: %s", serviceId, instance.getUri())); } + LoadBalancerProperties.StickySession stickySessionProperties = properties.getStickySession(); ClientRequest newRequest = buildClientRequest(clientRequest, instance, - properties.getInstanceIdCookieName()); + stickySessionProperties.getInstanceIdCookieName(), + stickySessionProperties.isAddServiceInstanceCookie()); return next.exchange(newRequest) .doOnError(throwable -> supportedLifecycleProcessors.forEach( lifecycle -> lifecycle.onComplete(new CompletionContext( diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java index a4f55cf02..abb7c23ee 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java @@ -122,15 +122,17 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction LOG.debug(String.format("LoadBalancer has retrieved the instance for service %s: %s", serviceId, instance.getUri())); } + LoadBalancerProperties.StickySession stickySessionProperties = properties.getStickySession(); ClientRequest newRequest = buildClientRequest(clientRequest, instance, - properties.getInstanceIdCookieName()); + stickySessionProperties.getInstanceIdCookieName(), + stickySessionProperties.isAddServiceInstanceCookie()); return next.exchange(newRequest) .doOnError(throwable -> supportedLifecycleProcessors.forEach( lifecycle -> lifecycle.onComplete(new CompletionContext( CompletionContext.Status.FAILED, throwable, lbResponse)))) .doOnSuccess(clientResponse -> supportedLifecycleProcessors.forEach( - lifecycle -> lifecycle.onComplete(new CompletionContext( - CompletionContext.Status.SUCCESS, lbResponse, clientResponse)))) + lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS, + lbResponse, clientResponse)))) .map(clientResponse -> { loadBalancerRetryContext.setClientResponse(clientResponse); if (shouldRetrySameServiceInstance(loadBalancerRetryContext)) { diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java index 961747e8f..67f368fcf 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java @@ -63,7 +63,7 @@ public Flux> get() { @SuppressWarnings("rawtypes") @Override public Flux> get(Request request) { - String instanceIdCookieName = properties.getInstanceIdCookieName(); + String instanceIdCookieName = properties.getStickySession().getInstanceIdCookieName(); Object context = request.getContext(); if ((context instanceof ClientRequestContext)) { ClientRequest originalRequest = ((ClientRequestContext) context).getClientRequest(); diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java index c0950ab1c..7b0a79f7a 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java @@ -73,7 +73,7 @@ void setUp() { @Test void shouldReturnInstanceBasedOnCookieFromClientRequest() { HttpHeaders headers = new HttpHeaders(); - headers.add(properties.getInstanceIdCookieName(), "test-1"); + headers.add(properties.getStickySession().getInstanceIdCookieName(), "test-1"); when(clientRequest.cookies()).thenReturn(headers); Request request = new DefaultRequest<>(new ClientRequestContext(clientRequest)); @@ -86,7 +86,7 @@ void shouldReturnInstanceBasedOnCookieFromClientRequest() { @Test void shouldReturnAllDelegateInstancesIfInstanceBasedOnCookieFromClientRequestNotFound() { HttpHeaders headers = new HttpHeaders(); - headers.add(properties.getInstanceIdCookieName(), "test-4"); + headers.add(properties.getStickySession().getInstanceIdCookieName(), "test-4"); when(clientRequest.cookies()).thenReturn(headers); Request request = new DefaultRequest<>(new ClientRequestContext(clientRequest)); @@ -108,8 +108,8 @@ void shouldReturnAllInstancesFromDelegateIfClientRequestHasNoCookie() { @Test void shouldReturnInstanceBasedOnCookieFromServerHttpRequest() { MultiValueMap cookies = new LinkedMultiValueMap<>(); - cookies.add(properties.getInstanceIdCookieName(), - new HttpCookie(properties.getInstanceIdCookieName(), "test-1")); + cookies.add(properties.getStickySession().getInstanceIdCookieName(), + new HttpCookie(properties.getStickySession().getInstanceIdCookieName(), "test-1")); when(serverHttpRequest.getCookies()).thenReturn(cookies); Request request = new DefaultRequest<>( new ServerHttpRequestContext(serverHttpRequest)); @@ -123,8 +123,8 @@ void shouldReturnInstanceBasedOnCookieFromServerHttpRequest() { @Test void shouldReturnAllDelegateInstancesIfInstanceBasedOnCookieFromServerHttpRequestNotFound() { MultiValueMap cookies = new LinkedMultiValueMap<>(); - cookies.add(properties.getInstanceIdCookieName(), - new HttpCookie(properties.getInstanceIdCookieName(), "test-4")); + cookies.add(properties.getStickySession().getInstanceIdCookieName(), + new HttpCookie(properties.getStickySession().getInstanceIdCookieName(), "test-4")); when(serverHttpRequest.getCookies()).thenReturn(cookies); Request request = new DefaultRequest<>( new ServerHttpRequestContext(serverHttpRequest)); From 693915fd8cad1a5db6945f7e5e04ccbf02db2b65 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 3 Dec 2020 16:55:46 +0100 Subject: [PATCH 5/7] Add docs and javadocs. --- docs/src/main/asciidoc/_configprops.adoc | 2 ++ .../main/asciidoc/spring-cloud-commons.adoc | 26 +++++++++++++++++++ .../reactive/ExchangeFilterFunctionUtils.java | 3 +-- .../reactive/LoadBalancerProperties.java | 10 +++++++ ...ckySessionServiceInstanceListSupplier.java | 5 ++-- .../ServiceInstanceListSupplierBuilder.java | 2 +- ...ssionServiceInstanceListSupplierTests.java | 2 ++ 7 files changed, 45 insertions(+), 5 deletions(-) diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 71794eac7..0c6f1d88a 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -46,6 +46,8 @@ |spring.cloud.loadbalancer.retry.retryable-status-codes | | A {@link Set} of status codes that should trigger a retry. |spring.cloud.loadbalancer.ribbon.enabled | `true` | Causes `RibbonLoadBalancerClient` to be used by default. |spring.cloud.loadbalancer.service-discovery.timeout | | String representation of Duration of the timeout for calls to service discovery. +|spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie | `false` | Indicates whether a cookie with the newly selected instance should be added by SC LoadBalancer. +|spring.cloud.loadbalancer.sticky-session.instance-id-cookie-name | `sc-lb-instance-id` | The name of the cookie holding the preferred instance id. |spring.cloud.loadbalancer.zone | | Spring Cloud LoadBalancer zone. |spring.cloud.refresh.enabled | `true` | Enables autoconfiguration for the refresh scope and associated features. |spring.cloud.refresh.extra-refreshable | `true` | Additional class names for beans to post process into refresh scope. diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index 2fdf15546..127f4e484 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -970,6 +970,32 @@ public class CustomLoadBalancerConfiguration { WARNING: `HealthCheckServiceInstanceListSupplier` has its own caching mechanism based on Reactor Flux `replay()`. Therefore, if it's being used, you may want to skip wrapping that supplier with `CachingServiceInstanceListSupplier`. +=== Request-based Sticky Session for LoadBalancer + +It is possible to set up the LoadBalancer in such a way that it will prefer the instance with `instanceId` provided in a request cookie. We currently support this if the request is being passed to the LoadBalancer either via the `ClientRequestContext` or `ServerHttpRequestContext`, which are being used by the SC LoadBalancer exchange filter functions and filters. + +For that, you will need to use the `RequestBasedStickySessionServiceInstanceListSupplier`. It can be configured either by setting the value of `spring.cloud.loadbalancer.configurations` to `request-based-sticky-session` or by providing your own `ServiceInstanceListSupplier` bean, for example: + +[[health-check-based-custom-loadbalancer-configuration]] +[source,java,indent=0] +---- +public class CustomLoadBalancerConfiguration { + + @Bean + public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( + ConfigurableApplicationContext context) { + return ServiceInstanceListSupplier.builder() + .withDiscoveryClient() + .withRequestBasedStickySession() + .build(context); + } + } +---- + +For that functionality, it will be useful to have the selected service instance (that can be different from the one in the original request cookie in case that one was not available) to be updated before sending the request forward. In order to do that set the value of `spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie` to `true`. + +By default, the name of the cookie is `sc-lb-instance-id`. It can be modified by changing the value of `spring.cloud.loadbalancer.instance-id-cookie-name` property. + [[spring-cloud-loadbalancer-hints]] === Spring Cloud LoadBalancer Hints diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java index 34a07faf7..b5f6134ea 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ExchangeFilterFunctionUtils.java @@ -52,8 +52,7 @@ static ClientRequest buildClientRequest(ClientRequest request, ServiceInstance s && addServiceInstanceCookie) { cookies.add(instanceIdCookieName, serviceInstance.getInstanceId()); } - }) - .attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build(); + }).attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build(); } static String serviceInstanceUnavailableMessage(String serviceId) { diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java index 52a31c40e..1a36cc2e9 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java @@ -53,6 +53,9 @@ public class LoadBalancerProperties { */ private Retry retry = new Retry(); + /** + * Properties for LoadBalancer sticky-session. + */ private StickySession stickySession = new StickySession(); public HealthCheck getHealthCheck() { @@ -89,8 +92,15 @@ public void setStickySession(StickySession stickySession) { public static class StickySession { + /** + * The name of the cookie holding the preferred instance id. + */ private String instanceIdCookieName = "sc-lb-instance-id"; + /** + * Indicates whether a cookie with the newly selected instance should be added by + * SC LoadBalancer. + */ private boolean addServiceInstanceCookie = false; public String getInstanceIdCookieName() { diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java index 67f368fcf..4022b00a6 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplier.java @@ -33,10 +33,11 @@ import org.springframework.web.reactive.function.client.ClientRequest; /** - * A session cookie based implementation of {@link ReactorServiceInstanceLoadBalancer} - * that ensures requests from the same client are routed to the same server. + * A session cookie based implementation of {@link ServiceInstanceListSupplier} that gives + * preference to the instance with an id specified in a request cookie. * * @author Olga Maciaszek-Sharma + * @since 3.0.0 */ public class RequestBasedStickySessionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier { diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java index 40d7d72e4..604b1fb3f 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java @@ -146,7 +146,7 @@ public ServiceInstanceListSupplierBuilder withZonePreference() { } /** - * Adds a {@link ZonePreferenceServiceInstanceListSupplier} to the + * Adds a {@link RequestBasedStickySessionServiceInstanceListSupplier} to the * {@link ServiceInstanceListSupplier} hierarchy. * @return the {@link ServiceInstanceListSupplierBuilder} object */ diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java index 7b0a79f7a..3a4df6634 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java @@ -43,6 +43,8 @@ import static org.mockito.Mockito.when; /** + * Tests for {@link RequestBasedStickySessionServiceInstanceListSupplier}. + * * @author Olga Maciaszek-Sharma */ class RequestBasedStickySessionServiceInstanceListSupplierTests { From 29751890a3369b9f1e1281ab8e244ca5a3c5385d Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Fri, 4 Dec 2020 14:52:37 +0100 Subject: [PATCH 6/7] Add default configuration with blocking discovery client. --- .../annotation/LoadBalancerClientConfiguration.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java index 957110c0a..d78ae14c6 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java @@ -146,6 +146,17 @@ public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceList .build(context); } + @Bean + @ConditionalOnBean(DiscoveryClient.class) + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", + havingValue = "request-based-sticky-session") + public ServiceInstanceListSupplier requestBasedStickySessionDiscoveryClientServiceInstanceListSupplier( + ConfigurableApplicationContext context) { + return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withRequestBasedStickySession() + .build(context); + } + } @Configuration(proxyBeanMethods = false) From e068cf1ecdfdb078c80928fdfa03ceec7e3b3947 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Mon, 7 Dec 2020 12:56:43 +0100 Subject: [PATCH 7/7] Fix docs after review. --- docs/src/main/asciidoc/spring-cloud-commons.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index 127f4e484..c82e50910 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -972,9 +972,9 @@ WARNING: `HealthCheckServiceInstanceListSupplier` has its own caching mechanism === Request-based Sticky Session for LoadBalancer -It is possible to set up the LoadBalancer in such a way that it will prefer the instance with `instanceId` provided in a request cookie. We currently support this if the request is being passed to the LoadBalancer either via the `ClientRequestContext` or `ServerHttpRequestContext`, which are being used by the SC LoadBalancer exchange filter functions and filters. +You can set up the LoadBalancer in such a way that it prefers the instance with `instanceId` provided in a request cookie. We currently support this if the request is being passed to the LoadBalancer through either `ClientRequestContext` or `ServerHttpRequestContext`, which are used by the SC LoadBalancer exchange filter functions and filters. -For that, you will need to use the `RequestBasedStickySessionServiceInstanceListSupplier`. It can be configured either by setting the value of `spring.cloud.loadbalancer.configurations` to `request-based-sticky-session` or by providing your own `ServiceInstanceListSupplier` bean, for example: +For that, you need to use the `RequestBasedStickySessionServiceInstanceListSupplier`. You can configure it either by setting the value of `spring.cloud.loadbalancer.configurations` to `request-based-sticky-session` or by providing your own `ServiceInstanceListSupplier` bean -- for example: [[health-check-based-custom-loadbalancer-configuration]] [source,java,indent=0] @@ -992,9 +992,9 @@ public class CustomLoadBalancerConfiguration { } ---- -For that functionality, it will be useful to have the selected service instance (that can be different from the one in the original request cookie in case that one was not available) to be updated before sending the request forward. In order to do that set the value of `spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie` to `true`. +For that functionality, it is useful to have the selected service instance (which can be different from the one in the original request cookie if that one is not available) to be updated before sending the request forward. To do that, set the value of `spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie` to `true`. -By default, the name of the cookie is `sc-lb-instance-id`. It can be modified by changing the value of `spring.cloud.loadbalancer.instance-id-cookie-name` property. +By default, the name of the cookie is `sc-lb-instance-id`. You can modify it by changing the value of the `spring.cloud.loadbalancer.instance-id-cookie-name` property. [[spring-cloud-loadbalancer-hints]] === Spring Cloud LoadBalancer Hints