/*
* Copyright 2015 Netflix, Inc.
*
* 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
*
* http://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 com.netflix.discovery.shared.transport.decorator;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import com.netflix.discovery.shared.resolver.ClusterResolver;
import com.netflix.discovery.shared.resolver.EurekaEndpoint;
import com.netflix.discovery.shared.resolver.aws.SampleCluster;
import com.netflix.discovery.shared.resolver.aws.AwsEndpoint;
import com.netflix.discovery.shared.transport.DefaultEurekaTransportConfig;
import com.netflix.discovery.shared.transport.EurekaHttpClient;
import com.netflix.discovery.shared.transport.EurekaHttpResponse;
import com.netflix.discovery.shared.transport.EurekaTransportConfig;
import com.netflix.discovery.shared.transport.TransportClientFactory;
import com.netflix.discovery.shared.transport.TransportException;
import com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.RequestExecutor;
import com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.RequestType;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Tomasz Bak
*/
public class RetryableEurekaHttpClientTest {
private static final int NUMBER_OF_RETRIES = 2;
private static final int CLUSTER_SIZE = 3;
public static final RequestType TEST_REQUEST_TYPE = RequestType.Register;
private static final List<AwsEndpoint> CLUSTER_ENDPOINTS = SampleCluster.UsEast1a.builder().withServerPool(CLUSTER_SIZE).build();
private final EurekaTransportConfig transportConfig = mock(EurekaTransportConfig.class);
private final ClusterResolver clusterResolver = mock(ClusterResolver.class);
private final TransportClientFactory clientFactory = mock(TransportClientFactory.class);
private final ServerStatusEvaluator serverStatusEvaluator = ServerStatusEvaluators.legacyEvaluator();
private final RequestExecutor<Void> requestExecutor = mock(RequestExecutor.class);
private RetryableEurekaHttpClient retryableClient;
private List<EurekaHttpClient> clusterDelegates;
@Before
public void setUp() throws Exception {
when(transportConfig.getRetryableClientQuarantineRefreshPercentage()).thenReturn(0.66);
retryableClient = new RetryableEurekaHttpClient(
"test",
transportConfig,
clusterResolver,
clientFactory,
serverStatusEvaluator,
NUMBER_OF_RETRIES);
clusterDelegates = new ArrayList<>(CLUSTER_SIZE);
for (int i = 0; i < CLUSTER_SIZE; i++) {
clusterDelegates.add(mock(EurekaHttpClient.class));
}
when(clusterResolver.getClusterEndpoints()).thenReturn(CLUSTER_ENDPOINTS);
}
@Test
public void testRequestsReuseSameConnectionIfThereIsNoError() throws Exception {
when(clientFactory.newClient(Matchers.<EurekaEndpoint>anyVararg())).thenReturn(clusterDelegates.get(0));
when(requestExecutor.execute(clusterDelegates.get(0))).thenReturn(EurekaHttpResponse.status(200));
// First request creates delegate, second reuses it
for (int i = 0; i < 3; i++) {
EurekaHttpResponse<Void> httpResponse = retryableClient.execute(requestExecutor);
assertThat(httpResponse.getStatusCode(), is(equalTo(200)));
}
verify(clientFactory, times(1)).newClient(Matchers.<EurekaEndpoint>anyVararg());
verify(requestExecutor, times(3)).execute(clusterDelegates.get(0));
}
@Test
public void testRequestIsRetriedOnConnectionError() throws Exception {
when(clientFactory.newClient(Matchers.<EurekaEndpoint>anyVararg())).thenReturn(clusterDelegates.get(0), clusterDelegates.get(1));
when(requestExecutor.execute(clusterDelegates.get(0))).thenThrow(new TransportException("simulated network error"));
when(requestExecutor.execute(clusterDelegates.get(1))).thenReturn(EurekaHttpResponse.status(200));
EurekaHttpResponse<Void> httpResponse = retryableClient.execute(requestExecutor);
assertThat(httpResponse.getStatusCode(), is(equalTo(200)));
verify(clientFactory, times(2)).newClient(Matchers.<EurekaEndpoint>anyVararg());
verify(requestExecutor, times(1)).execute(clusterDelegates.get(0));
verify(requestExecutor, times(1)).execute(clusterDelegates.get(1));
}
@Test(expected = TransportException.class)
public void testErrorResponseIsReturnedIfRetryLimitIsReached() throws Exception {
simulateTransportError(0, NUMBER_OF_RETRIES + 1);
retryableClient.execute(requestExecutor);
}
@Test
public void testQuarantineListIsResetWhenNoMoreServerAreAvailable() throws Exception {
// First two call fail
simulateTransportError(0, CLUSTER_SIZE);
for (int i = 0; i < 2; i++) {
executeWithTransportErrorExpectation();
}
// Second call, should reset cluster quarantine list, and hit health node 0
when(clientFactory.newClient(Matchers.<EurekaEndpoint>anyVararg())).thenReturn(clusterDelegates.get(0));
reset(requestExecutor);
when(requestExecutor.execute(clusterDelegates.get(0))).thenReturn(EurekaHttpResponse.status(200));
retryableClient.execute(requestExecutor);
}
@Test
public void test5xxStatusCodeResultsInRequestRetry() throws Exception {
when(clientFactory.newClient(Matchers.<EurekaEndpoint>anyVararg())).thenReturn(clusterDelegates.get(0), clusterDelegates.get(1));
when(requestExecutor.execute(clusterDelegates.get(0))).thenReturn(EurekaHttpResponse.status(500));
when(requestExecutor.execute(clusterDelegates.get(1))).thenReturn(EurekaHttpResponse.status(200));
EurekaHttpResponse<Void> httpResponse = retryableClient.execute(requestExecutor);
assertThat(httpResponse.getStatusCode(), is(equalTo(200)));
verify(requestExecutor, times(1)).execute(clusterDelegates.get(0));
verify(requestExecutor, times(1)).execute(clusterDelegates.get(1));
}
@Test(timeout = 10000)
public void testConcurrentRequestsLeaveLastSuccessfulDelegate() throws Exception {
when(clientFactory.newClient(Matchers.<EurekaEndpoint>anyVararg())).thenReturn(clusterDelegates.get(0), clusterDelegates.get(1));
BlockingRequestExecutor executor0 = new BlockingRequestExecutor();
BlockingRequestExecutor executor1 = new BlockingRequestExecutor();
Thread thread0 = new Thread(new RequestExecutorRunner(executor0));
Thread thread1 = new Thread(new RequestExecutorRunner(executor1));
// Run parallel requests
thread0.start();
executor0.awaitReady();
thread1.start();
executor1.awaitReady();
// Complete request, first thread first, second afterwards
executor0.complete();
thread0.join();
executor1.complete();
thread1.join();
// Verify subsequent request done on delegate1
when(requestExecutor.execute(clusterDelegates.get(1))).thenReturn(EurekaHttpResponse.status(200));
EurekaHttpResponse<Void> httpResponse = retryableClient.execute(requestExecutor);
assertThat(httpResponse.getStatusCode(), is(equalTo(200)));
verify(clientFactory, times(2)).newClient(Matchers.<EurekaEndpoint>anyVararg());
verify(requestExecutor, times(0)).execute(clusterDelegates.get(0));
verify(requestExecutor, times(1)).execute(clusterDelegates.get(1));
}
private void simulateTransportError(int delegateFrom, int count) {
for (int i = 0; i < count; i++) {
int delegateId = delegateFrom + i;
when(clientFactory.newClient(Matchers.<EurekaEndpoint>anyVararg())).thenReturn(clusterDelegates.get(delegateId));
when(requestExecutor.execute(clusterDelegates.get(delegateId))).thenThrow(new TransportException("simulated network error"));
}
}
private void executeWithTransportErrorExpectation() {
try {
retryableClient.execute(requestExecutor);
fail("TransportException expected");
} catch (TransportException ignore) {
}
}
class RequestExecutorRunner implements Runnable {
private final RequestExecutor<Void> requestExecutor;
RequestExecutorRunner(RequestExecutor<Void> requestExecutor) {
this.requestExecutor = requestExecutor;
}
@Override
public void run() {
retryableClient.execute(requestExecutor);
}
}
static class BlockingRequestExecutor implements RequestExecutor<Void> {
private final CountDownLatch readyLatch = new CountDownLatch(1);
private final CountDownLatch completeLatch = new CountDownLatch(1);
@Override
public EurekaHttpResponse<Void> execute(EurekaHttpClient delegate) {
readyLatch.countDown();
try {
completeLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException("never released");
}
return EurekaHttpResponse.status(200);
}
@Override
public RequestType getRequestType() {
return TEST_REQUEST_TYPE;
}
void awaitReady() {
try {
readyLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException("never released");
}
}
void complete() {
completeLatch.countDown();
}
}
}