RetryLoadBalancerInterceptorTests.java

/*
 * Copyright 2012-present 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.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentMatchers;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.quality.Strictness;

import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.TerminatedRetryException;
import org.springframework.retry.backoff.BackOffContext;
import org.springframework.retry.backoff.BackOffInterruptedException;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.NoBackOffPolicy;
import org.springframework.retry.listener.RetryListenerSupport;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIOException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.BDDAssertions.then;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

/**
 * @author Ryan Baxter
 * @author Gang Li
 * @author Olga Maciaszek-Sharma
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
@ExtendWith(MockitoExtension.class)
public class RetryLoadBalancerInterceptorTests {

	private LoadBalancerClient client;

	private LoadBalancerRequestFactory lbRequestFactory;

	private final LoadBalancedRetryFactory loadBalancedRetryFactory = new LoadBalancedRetryFactory() {
	};

	private LoadBalancerProperties properties;

	private ReactiveLoadBalancer.Factory<ServiceInstance> lbFactory;

	@BeforeEach
	public void setUp() {
		client = mock(LoadBalancerClient.class);
		lbRequestFactory = mock(LoadBalancerRequestFactory.class);
		properties = new LoadBalancerProperties();
		properties.getRetry().setRetryOnAllExceptions(true);
		lbFactory = mock(ReactiveLoadBalancer.Factory.class, withSettings().strictness(Strictness.LENIENT));
		when(lbFactory.getProperties(any())).thenReturn(properties);
	}

	@AfterEach
	public void tearDown() {
		client = null;
	}

	@Test
	public void interceptDisableRetry() throws Throwable {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://foo"));
		ServiceInstance serviceInstance = mock(ServiceInstance.class);
		when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
		when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
			.thenThrow(new IOException());
		properties.getRetry().setEnabled(false);
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				loadBalancedRetryFactory, lbFactory);
		byte[] body = new byte[] {};
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);

		when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));

		assertThatIOException().isThrownBy(() -> interceptor.intercept(request, body, execution));
		verify(lbRequestFactory).createRequest(request, body, execution);
	}

	@Test
	public void interceptInvalidHost() throws Throwable {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://foo_underscore"));
		properties.getRetry().setEnabled(true);
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				loadBalancedRetryFactory, lbFactory);
		byte[] body = new byte[] {};
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
		assertThatIllegalStateException().isThrownBy(() -> interceptor.intercept(request, body, execution));
	}

	@Test
	public void interceptNeverRetry() throws Throwable {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://foo"));
		ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
		ServiceInstance serviceInstance = mock(ServiceInstance.class);
		when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
		when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
			.thenReturn(clientHttpResponse);
		when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
		properties.getRetry().setEnabled(true);
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				loadBalancedRetryFactory, lbFactory);
		byte[] body = new byte[] {};
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
		interceptor.intercept(request, body, execution);
		verify(lbRequestFactory).createRequest(request, body, execution);
	}

	@Test
	public void interceptSuccess() throws Throwable {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://foo"));
		ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
		LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
		when(policy.retryableException(any())).thenReturn(true);
		ServiceInstance serviceInstance = mock(ServiceInstance.class);
		when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
		when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
			.thenReturn(clientHttpResponse);
		when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
		properties.getRetry().setEnabled(true);
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				new MyLoadBalancedRetryFactory(policy), lbFactory);
		byte[] body = new byte[] {};
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
		ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
		then(rsp).isEqualTo(clientHttpResponse);
		verify(lbRequestFactory).createRequest(request, body, execution);
	}

	@Test
	public void interceptRetryOnStatusCode() throws Throwable {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://foo"));
		InputStream notFoundStream = mock(InputStream.class);
		ClientHttpResponse clientHttpResponseNotFound = new MockClientHttpResponse(notFoundStream,
				HttpStatus.NOT_FOUND);
		ClientHttpResponse clientHttpResponseOk = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
		LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
		when(policy.retryableStatusCode(eq(HttpStatus.NOT_FOUND.value()))).thenReturn(true);
		when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
		when(policy.retryableException(any())).thenReturn(true);
		ServiceInstance serviceInstance = mock(ServiceInstance.class);
		when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
		when(client.execute(eq("foo"), eq(serviceInstance), nullable(LoadBalancerRequest.class)))
			.thenReturn(clientHttpResponseNotFound)
			.thenReturn(clientHttpResponseOk);
		properties.getRetry().setEnabled(true);
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				new MyLoadBalancedRetryFactory(policy), lbFactory);
		byte[] body = new byte[] {};
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
		ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
		verify(client, times(2)).execute(eq("foo"), eq(serviceInstance), nullable(LoadBalancerRequest.class));
		verify(notFoundStream, times(1)).close();
		then(rsp).isEqualTo(clientHttpResponseOk);
		verify(lbRequestFactory, times(2)).createRequest(request, body, execution);
	}

	@Test
	public void interceptRetryFailOnStatusCode() throws Throwable {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://foo"));

		InputStream notFoundStream = new ByteArrayInputStream("foo".getBytes());
		ClientHttpResponse clientHttpResponseNotFound = new MockClientHttpResponse(notFoundStream,
				HttpStatus.NOT_FOUND);

		LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
		when(policy.retryableStatusCode(eq(HttpStatus.NOT_FOUND.value()))).thenReturn(true);
		when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(false);
		when(policy.retryableException(any())).thenReturn(true);

		ServiceInstance serviceInstance = mock(ServiceInstance.class);
		when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
		when(client.execute(eq("foo"), eq(serviceInstance),
				ArgumentMatchers.<LoadBalancerRequest<ClientHttpResponse>>any()))
			.thenReturn(clientHttpResponseNotFound);

		properties.getRetry().setEnabled(true);
		byte[] body = new byte[] {};
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				new MyLoadBalancedRetryFactory(policy), lbFactory);
		ClientHttpResponse rsp = interceptor.intercept(request, body, execution);

		verify(client, times(1)).execute(eq("foo"), eq(serviceInstance),
				ArgumentMatchers.<LoadBalancerRequest<ClientHttpResponse>>any());
		verify(lbRequestFactory, times(1)).createRequest(request, body, execution);
		verify(policy, times(2)).canRetryNextServer(any(LoadBalancedRetryContext.class));

		// call twice in a retry attempt
		byte[] content = new byte[1024];
		int length = rsp.getBody().read(content);
		then(length).isEqualTo("foo".getBytes().length);
		then(new String(content, 0, length)).isEqualTo("foo");
	}

	@Test
	public void interceptRetry() throws Throwable {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://foo"));
		ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
		LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
		when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
		when(policy.retryableException(any())).thenReturn(true);
		MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
		ServiceInstance serviceInstance = mock(ServiceInstance.class);
		when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
		when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
			.thenThrow(new IOException())
			.thenReturn(clientHttpResponse);
		when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
		properties.getRetry().setEnabled(true);
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory);
		byte[] body = new byte[] {};
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
		ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
		verify(client, times(2)).execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class));
		then(rsp).isEqualTo(clientHttpResponse);
		verify(lbRequestFactory, times(2)).createRequest(request, body, execution);
		then(backOffPolicy.getBackoffAttempts()).isEqualTo(1);
	}

	@Test
	public void interceptFailedRetry() throws Exception {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://foo"));
		ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
		LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
		when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(false);
		when(policy.retryableException(any())).thenReturn(true);
		ServiceInstance serviceInstance = mock(ServiceInstance.class);
		when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
		when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
			.thenThrow(new IOException())
			.thenReturn(clientHttpResponse);
		when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
		properties.getRetry().setEnabled(true);
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				new MyLoadBalancedRetryFactory(policy), lbFactory);
		byte[] body = new byte[] {};
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
		assertThatIOException().isThrownBy(() -> interceptor.intercept(request, body, execution));
		verify(lbRequestFactory).createRequest(request, body, execution);
	}

	private static ServiceInstance defaultServiceInstance() {
		return new DefaultServiceInstance("testInstance", "test", "testHost", 80, false);
	}

	@Test
	public void retryListenerTest() throws Throwable {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://listener"));
		ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
		LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
		when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
		when(policy.retryableException(any())).thenReturn(true);
		MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
		ServiceInstance serviceInstance = mock(ServiceInstance.class);
		when(client.choose(eq("listener"), any())).thenReturn(serviceInstance);
		when(client.execute(eq("listener"), eq(serviceInstance), any(LoadBalancerRequest.class)))
			.thenThrow(new IOException())
			.thenReturn(clientHttpResponse);
		properties.getRetry().setEnabled(true);
		MyRetryListener retryListener = new MyRetryListener();
		when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				new MyLoadBalancedRetryFactory(policy, backOffPolicy, new RetryListener[] { retryListener }),
				lbFactory);
		byte[] body = new byte[] {};
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
		ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
		verify(client, times(2)).execute(eq("listener"), eq(serviceInstance), any(LoadBalancerRequest.class));
		then(rsp).isEqualTo(clientHttpResponse);
		verify(lbRequestFactory, times(2)).createRequest(request, body, execution);
		then(backOffPolicy.getBackoffAttempts()).isEqualTo(1);
		then(retryListener.getOnError()).isEqualTo(1);
	}

	@Test
	public void retryWithDefaultConstructorTest() throws Throwable {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://default"));
		ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
		LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
		when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
		MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
		ServiceInstance serviceInstance = mock(ServiceInstance.class);
		when(client.choose(eq("default"), any())).thenReturn(serviceInstance);
		when(client.execute(eq("default"), eq(serviceInstance), any(LoadBalancerRequest.class)))
			.thenThrow(new IOException())
			.thenReturn(clientHttpResponse);
		properties.getRetry().setEnabled(true);
		when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
		when(policy.retryableException(any())).thenReturn(true);
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory);
		byte[] body = new byte[] {};
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
		ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
		verify(client, times(2)).execute(eq("default"), eq(serviceInstance), any(LoadBalancerRequest.class));
		then(rsp).isEqualTo(clientHttpResponse);
		verify(lbRequestFactory, times(2)).createRequest(request, body, execution);
		then(backOffPolicy.getBackoffAttempts()).isEqualTo(1);
	}

	@Test
	public void retryListenerTestNoRetry() throws Throwable {
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://noRetry"));
		LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
		MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
		properties.getRetry().setEnabled(true);
		RetryListener myRetryListener = new RetryListenerSupport() {
			@Override
			public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
				return false;
			}
		};
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				new MyLoadBalancedRetryFactory(policy, backOffPolicy, new RetryListener[] { myRetryListener }),
				lbFactory);
		ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
		assertThatExceptionOfType(TerminatedRetryException.class)
			.isThrownBy(() -> interceptor.intercept(request, new byte[] {}, execution));
	}

	@Test
	public void shouldNotDuplicateLifecycleCalls() throws IOException, URISyntaxException {
		Map<String, LoadBalancerLifecycle> lifecycleProcessors = new HashMap<>();
		lifecycleProcessors.put("testLifecycle", new TestLoadBalancerLifecycle());
		lifecycleProcessors.put("anotherLifecycle", new AnotherLoadBalancerLifecycle());
		when(lbFactory.getInstances("test", LoadBalancerLifecycle.class)).thenReturn(lifecycleProcessors);
		HttpRequest request = mock(HttpRequest.class);
		when(request.getURI()).thenReturn(new URI("http://test"));
		TestLoadBalancerClient client = new TestLoadBalancerClient();
		RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
				loadBalancedRetryFactory, lbFactory);

		interceptor.intercept(request, new byte[] {}, mock(ClientHttpRequestExecution.class));

		assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("testLifecycle")).getStartLog()).hasSize(1);
		assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("testLifecycle")).getStartRequestLog())
			.hasSize(0);
		assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("testLifecycle")).getCompleteLog()).hasSize(0);
		assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("anotherLifecycle")).getStartLog()).hasSize(1);
		assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("anotherLifecycle")).getStartRequestLog())
			.hasSize(0);
		assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("anotherLifecycle")).getCompleteLog())
			.hasSize(0);
		assertThat(((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("testLifecycle")).getStartLog())
			.hasSize(0);
		assertThat(
				((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("testLifecycle")).getStartRequestLog())
			.hasSize(1);
		assertThat(((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("testLifecycle")).getCompleteLog())
			.hasSize(1);
		assertThat(((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("anotherLifecycle")).getStartLog())
			.hasSize(0);
		assertThat(
				((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("testLifecycle")).getStartRequestLog())
			.hasSize(1);
		assertThat(
				((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("anotherLifecycle")).getCompleteLog())
			.hasSize(1);
	}

	static class MyLoadBalancedRetryFactory implements LoadBalancedRetryFactory {

		private final LoadBalancedRetryPolicy loadBalancedRetryPolicy;

		private BackOffPolicy backOffPolicy;

		private RetryListener[] retryListeners;

		MyLoadBalancedRetryFactory(LoadBalancedRetryPolicy loadBalancedRetryPolicy) {
			this.loadBalancedRetryPolicy = loadBalancedRetryPolicy;
		}

		MyLoadBalancedRetryFactory(LoadBalancedRetryPolicy loadBalancedRetryPolicy, BackOffPolicy backOffPolicy) {
			this(loadBalancedRetryPolicy);
			this.backOffPolicy = backOffPolicy;
		}

		MyLoadBalancedRetryFactory(LoadBalancedRetryPolicy loadBalancedRetryPolicy, BackOffPolicy backOffPolicy,
				RetryListener[] retryListeners) {
			this(loadBalancedRetryPolicy, backOffPolicy);
			this.retryListeners = retryListeners;
		}

		@Override
		public LoadBalancedRetryPolicy createRetryPolicy(String service,
				ServiceInstanceChooser serviceInstanceChooser) {
			return loadBalancedRetryPolicy;
		}

		@Override
		public BackOffPolicy createBackOffPolicy(String service) {
			return Objects.requireNonNullElseGet(backOffPolicy, NoBackOffPolicy::new);
		}

		@Override
		public RetryListener[] createRetryListeners(String service) {
			return Objects.requireNonNullElseGet(retryListeners, () -> new RetryListener[0]);
		}

	}

	static class MyBackOffPolicy implements BackOffPolicy {

		private int backoffAttempts = 0;

		@Override
		public BackOffContext start(RetryContext retryContext) {
			return new BackOffContext() {
				@Override
				protected Object clone() throws CloneNotSupportedException {
					return super.clone();
				}
			};
		}

		@Override
		public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
			backoffAttempts++;
		}

		int getBackoffAttempts() {
			return backoffAttempts;
		}

	}

	static class MyRetryListener extends RetryListenerSupport {

		private int onError = 0;

		@Override
		public <T, E extends Throwable> void onError(RetryContext retryContext, RetryCallback<T, E> retryCallback,
				Throwable throwable) {
			onError++;
		}

		int getOnError() {
			return onError;
		}

	}

	protected static class TestLoadBalancerClient implements LoadBalancerClient {

		private final Map<String, LoadBalancerLifecycle> lifecycleProcessors = new HashMap<>();

		TestLoadBalancerClient() {
			lifecycleProcessors.put("testLifecycle", new TestLoadBalancerLifecycle());
			lifecycleProcessors.put("anotherLifecycle", new AnotherLoadBalancerLifecycle());
		}

		@Override
		public <T> T execute(String serviceId, LoadBalancerRequest<T> request) {
			throw new UnsupportedOperationException("Not implemented");
		}

		@Override
		public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) {
			Set<LoadBalancerLifecycle> supportedLoadBalancerProcessors = LoadBalancerLifecycleValidator
				.getSupportedLifecycleProcessors(lifecycleProcessors, DefaultRequestContext.class, Object.class,
						ServiceInstance.class);
			supportedLoadBalancerProcessors.forEach(lifecycle -> lifecycle.onStartRequest(new DefaultRequest<>(),
					new DefaultResponse(serviceInstance)));
			T response = (T) new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
			supportedLoadBalancerProcessors
				.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext(CompletionContext.Status.SUCCESS,
						new DefaultRequest<>(), new DefaultResponse(defaultServiceInstance()))));
			return response;
		}

		@Override
		public URI reconstructURI(ServiceInstance instance, URI original) {
			throw new UnsupportedOperationException("Please, implement me.");
		}

		@Override
		public ServiceInstance choose(String serviceId) {
			return defaultServiceInstance();
		}

		@Override
		public <T> ServiceInstance choose(String serviceId, Request<T> request) {
			return defaultServiceInstance();
		}

		Map<String, LoadBalancerLifecycle> getLifecycleProcessors() {
			return lifecycleProcessors;
		}

	}

	protected static class TestLoadBalancerLifecycle implements LoadBalancerLifecycle<Object, Object, ServiceInstance> {

		final ConcurrentHashMap<String, Request<Object>> startLog = new ConcurrentHashMap<>();

		final ConcurrentHashMap<String, Request<Object>> startRequestLog = new ConcurrentHashMap<>();

		final ConcurrentHashMap<String, CompletionContext<Object, ServiceInstance, Object>> completeLog = new ConcurrentHashMap<>();

		@Override
		public boolean supports(Class requestContextClass, Class responseClass, Class serverTypeClass) {
			return DefaultRequestContext.class.isAssignableFrom(requestContextClass)
					&& Object.class.isAssignableFrom(responseClass)
					&& ServiceInstance.class.isAssignableFrom(serverTypeClass);
		}

		@Override
		public void onStart(Request<Object> request) {
			startLog.put(getName() + UUID.randomUUID(), request);
		}

		@Override
		public void onStartRequest(Request<Object> request, Response<ServiceInstance> lbResponse) {
			startRequestLog.put(getName() + UUID.randomUUID(), request);
		}

		@Override
		public void onComplete(CompletionContext<Object, ServiceInstance, Object> completionContext) {
			completeLog.put(getName() + UUID.randomUUID(), completionContext);
		}

		ConcurrentHashMap<String, Request<Object>> getStartLog() {
			return startLog;
		}

		ConcurrentHashMap<String, CompletionContext<Object, ServiceInstance, Object>> getCompleteLog() {
			return completeLog;
		}

		ConcurrentHashMap<String, Request<Object>> getStartRequestLog() {
			return startRequestLog;
		}

		protected String getName() {
			return getClass().getSimpleName();
		}

	}

	protected static class AnotherLoadBalancerLifecycle extends TestLoadBalancerLifecycle {

		@Override
		protected String getName() {
			return getClass().getSimpleName();
		}

	}

}