/*
* 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;
import javax.ws.rs.core.HttpHeaders;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.shared.Applications;
import com.netflix.discovery.shared.resolver.ClosableResolver;
import com.netflix.discovery.shared.resolver.ClusterResolver;
import com.netflix.discovery.shared.resolver.DefaultEndpoint;
import com.netflix.discovery.shared.resolver.EurekaEndpoint;
import com.netflix.discovery.shared.resolver.StaticClusterResolver;
import com.netflix.discovery.shared.resolver.aws.ApplicationsResolver;
import com.netflix.discovery.shared.resolver.aws.AwsEndpoint;
import com.netflix.discovery.shared.resolver.aws.EurekaHttpResolver;
import com.netflix.discovery.shared.resolver.aws.TestEurekaHttpResolver;
import com.netflix.discovery.shared.transport.jersey.Jersey1TransportClientFactories;
import com.netflix.discovery.shared.transport.jersey.TransportClientFactories;
import com.netflix.discovery.util.EurekaEntityComparators;
import com.netflix.discovery.util.InstanceInfoGenerator;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientRequest;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.filter.ClientFilter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static com.netflix.discovery.shared.transport.EurekaHttpResponse.anEurekaHttpResponse;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Tomasz Bak
*/
public class EurekaHttpClientsTest {
private static final InstanceInfo MY_INSTANCE = InstanceInfoGenerator.newBuilder(1, "myApp").build().first();
private final EurekaInstanceConfig instanceConfig = mock(EurekaInstanceConfig.class);
private final ApplicationInfoManager applicationInfoManager = new ApplicationInfoManager(instanceConfig, MY_INSTANCE);
private final EurekaHttpClient writeRequestHandler = mock(EurekaHttpClient.class);
private final EurekaHttpClient readRequestHandler = mock(EurekaHttpClient.class);
private EurekaClientConfig clientConfig;
private EurekaTransportConfig transportConfig;
private SimpleEurekaHttpServer writeServer;
private SimpleEurekaHttpServer readServer;
private ClusterResolver<EurekaEndpoint> clusterResolver;
private EurekaHttpClientFactory clientFactory;
private String readServerURI;
private final InstanceInfoGenerator instanceGen = InstanceInfoGenerator.newBuilder(2, 1).build();
@Before
public void setUp() throws IOException {
clientConfig = mock(EurekaClientConfig.class);
transportConfig = mock(EurekaTransportConfig.class);
when(clientConfig.getEurekaServerTotalConnectionsPerHost()).thenReturn(10);
when(clientConfig.getEurekaServerTotalConnections()).thenReturn(10);
when(transportConfig.getSessionedClientReconnectIntervalSeconds()).thenReturn(10);
writeServer = new SimpleEurekaHttpServer(writeRequestHandler);
clusterResolver = new StaticClusterResolver<EurekaEndpoint>("regionA", new DefaultEndpoint("localhost", writeServer.getServerPort(), false, "/v2/"));
readServer = new SimpleEurekaHttpServer(readRequestHandler);
readServerURI = "http://localhost:" + readServer.getServerPort();
clientFactory = EurekaHttpClients.canonicalClientFactory(
"test",
transportConfig,
clusterResolver,
new Jersey1TransportClientFactories().newTransportClientFactory(
clientConfig,
Collections.<ClientFilter>emptyList(),
applicationInfoManager.getInfo()
));
}
@After
public void tearDown() throws Exception {
if (writeServer != null) {
writeServer.shutdown();
}
if (readServer != null) {
readServer.shutdown();
}
if (clientFactory != null) {
clientFactory.shutdown();
}
}
@Test
public void testCanonicalClient() throws Exception {
Applications apps = instanceGen.toApplications();
when(writeRequestHandler.getApplications()).thenReturn(
anEurekaHttpResponse(302, Applications.class).headers("Location", readServerURI + "/v2/apps").build()
);
when(readRequestHandler.getApplications()).thenReturn(
anEurekaHttpResponse(200, apps).headers(HttpHeaders.CONTENT_TYPE, "application/json").build()
);
EurekaHttpClient eurekaHttpClient = clientFactory.newClient();
EurekaHttpResponse<Applications> result = eurekaHttpClient.getApplications();
assertThat(result.getStatusCode(), is(equalTo(200)));
assertThat(EurekaEntityComparators.equal(result.getEntity(), apps), is(true));
}
@Test
public void testCompositeBootstrapResolver() throws Exception {
Applications applications = InstanceInfoGenerator.newBuilder(5, "eurekaWrite", "someOther").build().toApplications();
Applications applications2 = InstanceInfoGenerator.newBuilder(2, "eurekaWrite", "someOther").build().toApplications();
String vipAddress = applications.getRegisteredApplications("eurekaWrite").getInstances().get(0).getVIPAddress();
// setup client config to use fixed root ips for testing
when(clientConfig.shouldUseDnsForFetchingServiceUrls()).thenReturn(false);
when(clientConfig.getEurekaServerServiceUrls(anyString())).thenReturn(Arrays.asList("http://foo:0")); // can use anything here
when(clientConfig.getRegion()).thenReturn("us-east-1");
when(transportConfig.getWriteClusterVip()).thenReturn(vipAddress);
when(transportConfig.getAsyncExecutorThreadPoolSize()).thenReturn(4);
when(transportConfig.getAsyncResolverRefreshIntervalMs()).thenReturn(400);
when(transportConfig.getAsyncResolverWarmUpTimeoutMs()).thenReturn(400);
ApplicationsResolver.ApplicationsSource applicationsSource = mock(ApplicationsResolver.ApplicationsSource.class);
when(applicationsSource.getApplications(anyInt(), eq(TimeUnit.SECONDS)))
.thenReturn(null) // first time
.thenReturn(applications) // second time
.thenReturn(null); // subsequent times
EurekaHttpClient mockHttpClient = mock(EurekaHttpClient.class);
when(mockHttpClient.getVip(eq(vipAddress)))
.thenReturn(anEurekaHttpResponse(200, applications).build())
.thenReturn(anEurekaHttpResponse(200, applications2).build()); // contains diff number of servers
TransportClientFactory transportClientFactory = mock(TransportClientFactory.class);
when(transportClientFactory.newClient(any(EurekaEndpoint.class))).thenReturn(mockHttpClient);
ClosableResolver<AwsEndpoint> resolver = null;
try {
resolver = EurekaHttpClients.compositeBootstrapResolver(
clientConfig,
transportConfig,
transportClientFactory,
applicationInfoManager.getInfo(),
applicationsSource
);
List endpoints = resolver.getClusterEndpoints();
assertThat(endpoints.size(), equalTo(applications.getInstancesByVirtualHostName(vipAddress).size()));
// wait for the second cycle that hits the app source
verify(applicationsSource, timeout(3000).times(2)).getApplications(anyInt(), eq(TimeUnit.SECONDS));
endpoints = resolver.getClusterEndpoints();
assertThat(endpoints.size(), equalTo(applications.getInstancesByVirtualHostName(vipAddress).size()));
// wait for the third cycle that triggers the mock http client (which is the third resolver cycle)
// for the third cycle we have mocked the application resolver to return null data so should fall back
// to calling the remote resolver again (which should return applications2)
verify(mockHttpClient, timeout(3000).times(3)).getVip(anyString());
endpoints = resolver.getClusterEndpoints();
assertThat(endpoints.size(), equalTo(applications2.getInstancesByVirtualHostName(vipAddress).size()));
} finally {
if (resolver != null) {
resolver.shutdown();
}
}
}
@Test
public void testCanonicalResolver() throws Exception {
when(clientConfig.getEurekaServerURLContext()).thenReturn("context");
when(clientConfig.getRegion()).thenReturn("region");
when(transportConfig.getAsyncExecutorThreadPoolSize()).thenReturn(3);
when(transportConfig.getAsyncResolverRefreshIntervalMs()).thenReturn(400);
when(transportConfig.getAsyncResolverWarmUpTimeoutMs()).thenReturn(400);
Applications applications = InstanceInfoGenerator.newBuilder(5, "eurekaRead", "someOther").build().toApplications();
String vipAddress = applications.getRegisteredApplications("eurekaRead").getInstances().get(0).getVIPAddress();
ApplicationsResolver.ApplicationsSource applicationsSource = mock(ApplicationsResolver.ApplicationsSource.class);
when(applicationsSource.getApplications(anyInt(), eq(TimeUnit.SECONDS)))
.thenReturn(null) // first time
.thenReturn(applications); // subsequent times
EurekaHttpClientFactory remoteResolverClientFactory = mock(EurekaHttpClientFactory.class);
EurekaHttpClient httpClient = mock(EurekaHttpClient.class);
when(remoteResolverClientFactory.newClient()).thenReturn(httpClient);
when(httpClient.getVip(vipAddress)).thenReturn(EurekaHttpResponse.anEurekaHttpResponse(200, applications).build());
EurekaHttpResolver remoteResolver = spy(new TestEurekaHttpResolver(clientConfig, transportConfig, remoteResolverClientFactory, vipAddress));
when(transportConfig.getReadClusterVip()).thenReturn(vipAddress);
ApplicationsResolver localResolver = spy(new ApplicationsResolver(
clientConfig, transportConfig, applicationsSource, transportConfig.getReadClusterVip()));
ClosableResolver resolver = null;
try {
resolver = EurekaHttpClients.compositeQueryResolver(
remoteResolver,
localResolver,
clientConfig,
transportConfig,
applicationInfoManager.getInfo()
);
List endpoints = resolver.getClusterEndpoints();
assertThat(endpoints.size(), equalTo(applications.getInstancesByVirtualHostName(vipAddress).size()));
verify(remoteResolver, times(1)).getClusterEndpoints();
verify(localResolver, times(1)).getClusterEndpoints();
// wait for the second cycle that hits the app source
verify(applicationsSource, timeout(3000).times(2)).getApplications(anyInt(), eq(TimeUnit.SECONDS));
endpoints = resolver.getClusterEndpoints();
assertThat(endpoints.size(), equalTo(applications.getInstancesByVirtualHostName(vipAddress).size()));
verify(remoteResolver, times(1)).getClusterEndpoints();
verify(localResolver, times(2)).getClusterEndpoints();
} finally {
if (resolver != null) {
resolver.shutdown();
}
}
}
@Test
public void testAddingAdditionalFilters() throws Exception {
TestFilter testFilter = new TestFilter();
Collection<ClientFilter> additionalFilters = Arrays.<ClientFilter>asList(testFilter);
TransportClientFactory transportClientFactory = new Jersey1TransportClientFactories().newTransportClientFactory(
clientConfig,
additionalFilters,
MY_INSTANCE
);
EurekaHttpClient client = transportClientFactory.newClient(clusterResolver.getClusterEndpoints().get(0));
client.getApplication("foo");
assertThat(testFilter.await(30, TimeUnit.SECONDS), is(true));
}
private static class TestFilter extends ClientFilter {
private final CountDownLatch latch = new CountDownLatch(1);
@Override
public ClientResponse handle(ClientRequest cr) throws ClientHandlerException {
latch.countDown();
return mock(ClientResponse.class);
}
public boolean await(long timeout, TimeUnit unit) throws Exception {
return latch.await(timeout, unit);
}
}
}