Skip to content

Backport retryable lb exchange filter function #902

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Extract the files into the JDK/jre/lib/security folder for whichever version of

== Building

:jdkversion: 1.7
:jdkversion: 1.8

=== Basic Compile and Test

Expand Down
4 changes: 4 additions & 0 deletions docs/src/main/asciidoc/_configprops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
|spring.cloud.loadbalancer.health-check.refetch-instances | false | Indicates whether the instances should be refetched by the <code>HealthCheckServiceInstanceListSupplier</code>. This can be used if the instances can be updated and the underlying delegate does not provide an ongoing flux.
|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.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}.
|spring.cloud.loadbalancer.retry.backoff.max-backoff | | Used to set {@link RetryBackoffSpec#maxBackoff}.
|spring.cloud.loadbalancer.retry.backoff.min-backoff | 5ms | Used to set {@link RetryBackoffSpec#minBackoff}.
|spring.cloud.loadbalancer.retry.enabled | true |
|spring.cloud.loadbalancer.retry.max-retries-on-next-service-instance | 1 | Number of retries to be executed on the next <code>ServiceInstance</code>. A <code>ServiceInstance</code> is chosen before each retry call.
|spring.cloud.loadbalancer.retry.max-retries-on-same-service-instance | 0 | Number of retries to be executed on the same <code>ServiceInstance</code>.
Expand Down
77 changes: 58 additions & 19 deletions docs/src/main/asciidoc/spring-cloud-commons.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -452,11 +452,67 @@ set the `spring.cloud.loadbalancer.ribbon.enabled` property to `false`.

A load-balanced `RestTemplate` can be configured to retry failed requests.
By default, this logic is disabled.
You can enable it by adding link:https://github.com/spring-projects/spring-retry[Spring Retry] to your application's classpath.
For the non-reactive version (with `RestTemplate`), you can enable it by adding link:https://github.com/spring-projects/spring-retry[Spring Retry] to your application's classpath.

To use the reactive version of load-balanced retries in the Hoxton release train, you will need to instantiate your own `RetryableLoadBalancerExchangeFilterFunction` bean:

[source,java,indent=0]
----
@Configuration
public class MyConfiguration {

@Bean
RetryableLoadBalancerExchangeFilterFunction retryableLoadBalancerExchangeFilterFunction(
LoadBalancerRetryProperties properties,
ReactiveLoadBalancer.Factory<ServiceInstance> factory) {
return new RetryableLoadBalancerExchangeFilterFunction(
new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(
properties),
factory, properties);
}
}
----

Then you can use it as a filter while building `webClient` instances:

[source,java,indent=0]
----
public class MyClass {
@Autowired
private RetryableLoadBalancerExchangeFilterFunction retryableLbFunction;

public Mono<String> doOtherStuff() {
return WebClient.builder().baseUrl("http://stores")
.filter(retryableLbFunction)
.build()
.get()
.uri("/stores")
.retrieve()
.bodyToMono(String.class);
}
}
----

If you would like to disable the retry logic with Spring Retry on the classpath, you can set `spring.cloud.loadbalancer.retry.enabled=false`.

If you would like to implement a `BackOffPolicy` in your retries, you need to create a bean of type `LoadBalancedRetryFactory` and override the `createBackOffPolicy()` method.
For the non-reactive implementation, if you would like to implement a `BackOffPolicy` in your retries, you need to create a bean of type `LoadBalancedRetryFactory` and override the `createBackOffPolicy()` method.

For the reactive implementation, you just need to enable it by setting `spring.cloud.loadbalancer.retry.backoff.enabled` to `false`.

You can set:

- `spring.cloud.loadbalancer.retry.maxRetriesOnSameServiceInstance` - indicates how many times a request should be retried on the same `ServiceInstance` (counted separately for every selected instance)
- `spring.cloud.loadbalancer.retry.maxRetriesOnNextServiceInstance` - indicates how many times a request should be retried a newly selected `ServiceInstance`
- `spring.cloud.loadbalancer.retry.retryableStatusCodes` - the status codes on which to always retry a failed request.

For the reactive implementation, you can additionally set:
- `spring.cloud.loadbalancer.retry.backoff.minBackoff` - Sets the minimum backoff duration (by default, 5 milliseconds)
- `spring.cloud.loadbalancer.retry.backoff.maxBackoff` - Sets the maximum backoff duration (by default, max long value of milliseconds)
- `spring.cloud.loadbalancer.retry.backoff.jitter` - Sets the jitter used for calculationg the actual backoff duration for each call (by default, 0.5).

For the reactive implementation, you can also implement your own `LoadBalancerRetryPolicy` to have more detailed control over the load-balanced call retries.

WARN:: For the non-reactive version, if you chose to override the `LoadBalancedRetryFactory` while using the Spring Cloud LoadBalancer-backed approach, make sure you annotate your bean with `@Order` and set it to a higher precedence than `1000`, which is the order set on the `BlockingLoadBalancedRetryFactory`.

===== Ribbon-based retries

Expand All @@ -475,23 +531,6 @@ For the Spring Cloud LoadBalancer-backed implementation, you can set:

WARN:: If you chose to override the `LoadBalancedRetryFactory` while using the Spring Cloud LoadBalancer-backed approach, make sure you annotate your bean with `@Order` and set it to a higher precedence than `1000`, which is the order set on the `BlockingLoadBalancedRetryFactory`.

====
[source,java,indent=0]
----
@Configuration
public class MyConfiguration {
@Bean
LoadBalancedRetryFactory retryFactory() {
return new LoadBalancedRetryFactory() {
@Override
public BackOffPolicy createBackOffPolicy(String service) {
return new ExponentialBackOffPolicy();
}
};
}
}
----
====

NOTE: `client` in the preceding examples should be replaced with your Ribbon client's name.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

package org.springframework.cloud.client.loadbalancer;

import java.time.Duration;
import java.util.HashSet;
import java.util.Set;

import reactor.util.retry.RetryBackoffSpec;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpMethod;

Expand Down Expand Up @@ -54,6 +57,11 @@ public class LoadBalancerRetryProperties {
*/
private Set<Integer> retryableStatusCodes = new HashSet<>();

/**
* Properties for Reactor Retry backoffs in Spring Cloud LoadBalancer.
*/
private Backoff backoff = new Backoff();

/**
* Returns true if the load balancer should retry failed requests.
* @return True if the load balancer should retry failed requests; false otherwise.
Expand Down Expand Up @@ -102,4 +110,68 @@ public void setRetryableStatusCodes(Set<Integer> retryableStatusCodes) {
this.retryableStatusCodes = retryableStatusCodes;
}

public Backoff getBackoff() {
return backoff;
}

public void setBackoff(Backoff backoff) {
this.backoff = backoff;
}

public static class Backoff {

/**
* Indicates whether Reactor Retry backoffs should be applied.
*/
private boolean enabled = false;

/**
* Used to set {@link RetryBackoffSpec#minBackoff}.
*/
private Duration minBackoff = Duration.ofMillis(5);

/**
* Used to set {@link RetryBackoffSpec#maxBackoff}.
*/
private Duration maxBackoff = Duration.ofMillis(Long.MAX_VALUE);

/**
* Used to set {@link RetryBackoffSpec#jitter}.
*/
private double jitter = 0.5d;

public Duration getMinBackoff() {
return minBackoff;
}

public void setMinBackoff(Duration minBackoff) {
this.minBackoff = minBackoff;
}

public Duration getMaxBackoff() {
return maxBackoff;
}

public void setMaxBackoff(Duration maxBackoff) {
this.maxBackoff = maxBackoff;
}

public double getJitter() {
return jitter;
}

public void setJitter(double jitter) {
this.jitter = jitter;
}

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.client.loadbalancer;

import java.util.Objects;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.core.style.ToStringCreator;

/**
* A request context object that allows storing information on previously used service
* instances.
*
* @author Olga Maciaszek-Sharma
* @since 2.2.7
*/
public class RetryableRequestContext extends DefaultRequestContext {

private ServiceInstance previousServiceInstance;

public RetryableRequestContext(ServiceInstance previousServiceInstance) {
this.previousServiceInstance = previousServiceInstance;
}

public RetryableRequestContext(ServiceInstance previousServiceInstance, String hint) {
super(hint);
this.previousServiceInstance = previousServiceInstance;
}

public ServiceInstance getPreviousServiceInstance() {
return previousServiceInstance;
}

public void setPreviousServiceInstance(ServiceInstance previousServiceInstance) {
this.previousServiceInstance = previousServiceInstance;
}

@Override
public String toString() {
ToStringCreator to = new ToStringCreator(this);
to.append("previousServiceInstance", previousServiceInstance);
return to.toString();
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RetryableRequestContext)) {
return false;
}
if (!super.equals(o)) {
return false;
}
RetryableRequestContext context = (RetryableRequestContext) o;
return Objects.equals(previousServiceInstance, context.previousServiceInstance);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), previousServiceInstance);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.client.loadbalancer.reactive;

import java.net.URI;

import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;

/**
* A utility class for load-balanced {@link ExchangeFilterFunction} instances.
*
* @author Olga Maciaszek-Sharma
* @since 2.2.7
*/
public final class ExchangeFilterFunctionUtils {

private ExchangeFilterFunctionUtils() {
throw new IllegalStateException("Can't instantiate a utility class.");
}

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();
}

}
Loading