Add hint-based instance filtering (#923)

This commit is contained in:
Olga Maciaszek-Sharma
2021-03-12 12:55:44 +01:00
committed by GitHub
parent 6b6f6dbbd5
commit 272365490a
7 changed files with 300 additions and 6 deletions

View File

@@ -35,6 +35,7 @@
|spring.cloud.loadbalancer.health-check.refetch-instances-interval | `25s` | Interval for refetching available service instances.
|spring.cloud.loadbalancer.health-check.repeat-health-check | `true` | Indicates whether health checks should keep repeating. It might be useful to set it to <code>false</code> if periodically refetching the instances, as every refetch will also trigger a healthcheck.
|spring.cloud.loadbalancer.hint | | Allows setting the value of <code>hint</code> that is passed on to the LoadBalancer request and can subsequently be used in {@link ReactiveLoadBalancer} implementations.
|spring.cloud.loadbalancer.hint-header-name | `X-SC-LB-Hint` | Allows setting the name of the header used for passing the hint for hint-based service instance filtering.
|spring.cloud.loadbalancer.retry.avoid-previous-instance | `true` | Enables wrapping ServiceInstanceListSupplier beans with `RetryAwareServiceInstanceListSupplier` if Spring-Retry is in the classpath.
|spring.cloud.loadbalancer.retry.backoff.enabled | `false` | Indicates whether Reactor Retry backoffs should be applied.
|spring.cloud.loadbalancer.retry.backoff.jitter | `0.5` | Used to set {@link RetryBackoffSpec#jitter}.

View File

@@ -932,7 +932,7 @@ public class CustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSuppliers.builder()
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withZonePreference()
.withCaching()
@@ -1053,6 +1053,38 @@ Spring Cloud LoadBalancer lets you set `String` hints that are passed to the Loa
You can set a default hint for all services by setting the value of the `spring.cloud.loadbalancer.hint.default` property. You can also set a specific value
for any given service by setting the value of the `spring.cloud.loadbalancer.hint.[SERVICE_ID]` property, substituting `[SERVICE_ID]` with the correct ID of your service. If the hint is not set by the user, `default` is used.
[[hints-based-loadbalancing]]
=== Hint-Based Load-Balancing
We also provide a `HintBasedServiceInstanceListSupplier`, which is a `ServiceInstanceListSupplier` implementation for hint-based instance selection.
`HintBasedServiceInstanceListSupplier` checks for a hint request header (the default header-name is `X-SC-LB-Hint`, but you can modify it by changing the value of the `spring.cloud.loadbalancer.hint-header-name` property) and, if it finds a hint request header, uses the hint value passed in the header to filter service instances.
If no hint header has been added, `HintBasedServiceInstanceListSupplier` uses <<spring-cloud-loadbalancer-hints,hint values from properties>> to filter service instances.
If no hint is set, either by the header or by properties, all service instances provided by the delegate are returned.
While filtering, `HintBasedServiceInstanceListSupplier` looks for service instances that have a matching value set under the `hint` key in their `metadataMap`. If no matching instances are found, all instances provided by the delegate are returned.
You could use the following sample configuration to set it up:
[[hints-based-custom-loadbalancer-configuration]]
[source,java,indent=0]
----
public class CustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withHints()
.withCaching()
.build(context);
}
}
----
[[spring-cloud-loadbalancer-starter]]
=== Spring Cloud LoadBalancer Starter

View File

@@ -49,6 +49,12 @@ public class LoadBalancerProperties {
*/
private Map<String, String> hint = new LinkedCaseInsensitiveMap<>();
/**
* Allows setting the name of the header used for passing the hint for hint-based
* service instance filtering.
*/
private String hintHeaderName = "X-SC-LB-Hint";
/**
* Properties for Spring-Retry and Reactor Retry support in Spring Cloud LoadBalancer.
*/
@@ -91,6 +97,14 @@ public class LoadBalancerProperties {
this.stickySession = stickySession;
}
public String getHintHeaderName() {
return hintHeaderName;
}
public void setHintHeaderName(String hintHeaderName) {
this.hintHeaderName = hintHeaderName;
}
public static class StickySession {
/**

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2012-2021 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.ArrayList;
import java.util.List;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.HintRequestContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
/**
* A {@link ServiceInstanceListSupplier} implementation that uses hints to filter service
* instances provided by the delegate.
*
* @author Olga Maciaszek-Sharma
* @since 3.0.2
*/
public class HintBasedServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
private final LoadBalancerProperties properties;
public HintBasedServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
LoadBalancerProperties properties) {
super(delegate);
this.properties = properties;
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get();
}
@Override
public Flux<List<ServiceInstance>> get(Request request) {
return get().map(instances -> filteredByHint(instances, getHint(request.getContext())));
}
private String getHint(Object requestContext) {
if (requestContext == null) {
return null;
}
String hint = null;
if (requestContext instanceof RequestDataContext) {
hint = getHintFromHeader((RequestDataContext) requestContext);
}
if (!StringUtils.hasText(hint) && requestContext instanceof HintRequestContext) {
hint = ((HintRequestContext) requestContext).getHint();
}
return hint;
}
private String getHintFromHeader(RequestDataContext context) {
if (context.getClientRequest() != null) {
HttpHeaders headers = context.getClientRequest().getHeaders();
if (headers != null) {
return headers.getFirst(properties.getHintHeaderName());
}
}
return null;
}
private List<ServiceInstance> filteredByHint(List<ServiceInstance> instances, String hint) {
if (!StringUtils.hasText(hint)) {
return instances;
}
List<ServiceInstance> filteredInstances = new ArrayList<>();
for (ServiceInstance serviceInstance : instances) {
if (serviceInstance.getMetadata().getOrDefault("hint", "").equals(hint)) {
filteredInstances.add(serviceInstance);
}
}
if (filteredInstances.size() > 0) {
return filteredInstances;
}
// If instances cannot be found based on hint,
// we return all instances retrieved for given service id.
return instances;
}
}

View File

@@ -48,11 +48,6 @@ public class RequestBasedStickySessionServiceInstanceListSupplier extends Delega
this.properties = properties;
}
@Override
public String getServiceId() {
return delegate.getServiceId();
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get();

View File

@@ -239,6 +239,15 @@ public final class ServiceInstanceListSupplierBuilder {
return this;
}
public ServiceInstanceListSupplierBuilder withHints() {
DelegateCreator creator = (context, delegate) -> {
LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class);
return new HintBasedServiceInstanceListSupplier(delegate, properties);
};
creators.add(creator);
return this;
}
/**
* Builds the {@link ServiceInstanceListSupplier} hierarchy.
* @param context application context

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2012-2021 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.HashMap;
import java.util.List;
import java.util.Map;
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.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RequestData;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
import org.springframework.mock.http.client.MockClientHttpRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Tests for {@link HintBasedServiceInstanceListSupplier}.
*
* @author Olga Maciaszek-Sharma
*/
class HintBasedServiceInstanceListSupplierTests {
private final DiscoveryClientServiceInstanceListSupplier delegate = mock(
DiscoveryClientServiceInstanceListSupplier.class);
private final LoadBalancerProperties properties = new LoadBalancerProperties();
private final RequestDataContext requestContext = new RequestDataContext(
new RequestData(new MockClientHttpRequest()));
private final HintBasedServiceInstanceListSupplier supplier = new HintBasedServiceInstanceListSupplier(delegate,
properties);
private final ServiceInstance first = serviceInstance("test-1", buildHintMetadata("test1"));
private final ServiceInstance second = serviceInstance("test-2", buildHintMetadata("test2"));
private final ServiceInstance third = serviceInstance("test-3", new HashMap<>());
@BeforeEach
void setUp() {
properties.setHintHeaderName("X-Test");
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(first, second, third)));
}
@Test
void shouldReturnInstancesForHintFromHeaderWhenAvailable() {
requestContext.setHint("test1");
requestContext.getClientRequest().getHeaders().add("X-Test", "test2");
Request<RequestDataContext> request = new DefaultRequest<>(requestContext);
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
assertThat(filtered).hasSize(1);
assertThat(filtered.get(0).getInstanceId()).isEqualTo("test-2");
}
@Test
void shouldReturnInstancesForHintFromPropertiesWhenNoHintHeader() {
requestContext.setHint("test1");
Request<RequestDataContext> request = new DefaultRequest<>(requestContext);
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
assertThat(filtered).hasSize(1);
assertThat(filtered.get(0).getInstanceId()).isEqualTo("test-1");
}
@Test
void shouldReturnAllInstancesWhenNoHint() {
Request<RequestDataContext> request = new DefaultRequest<>(requestContext);
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
assertThat(filtered).hasSize(3);
}
@Test
void shouldReturnAllInstancesWhenHintNotMatched() {
requestContext.getClientRequest().getHeaders().add("X-Test", "testX");
Request<RequestDataContext> request = new DefaultRequest<>(requestContext);
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
assertThat(filtered).hasSize(3);
}
@Test
void shouldReturnAllInstancesWhenRequestContextNull() {
Request<RequestDataContext> request = new DefaultRequest<>(null);
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
assertThat(filtered).hasSize(3);
}
@Test
void shouldReturnAllInstancesWhenClientRequestNull() {
Request<RequestDataContext> request = new DefaultRequest<>(new RequestDataContext(null));
List<ServiceInstance> filtered = supplier.get(request).blockFirst();
assertThat(filtered).hasSize(3);
}
private DefaultServiceInstance serviceInstance(String instanceId, Map<String, String> metadata) {
return new DefaultServiceInstance(instanceId, "test", "http://test.test", 9080, false, metadata);
}
private Map<String, String> buildHintMetadata(String zone) {
Map<String, String> metadata = new HashMap<>();
metadata.put("hint", zone);
return metadata;
}
}