/* * Copyright 2013 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 feign.ribbon; import static com.netflix.config.ConfigurationManager.getConfigInstance; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; import java.net.URI; import java.net.URL; import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.IClientConfig; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.SocketPolicy; import okhttp3.mockwebserver.MockWebServer; import feign.Client; import feign.Feign; import feign.Param; import feign.Request; import feign.RequestLine; import feign.RetryableException; import feign.Retryer; import feign.client.TrustingSSLSocketFactory; public class RibbonClientTest { @Rule public final TestName testName = new TestName(); @Rule public final MockWebServer server1 = new MockWebServer(); @Rule public final MockWebServer server2 = new MockWebServer(); private static String oldRetryConfig = null; private static final String SUN_RETRY_PROPERTY = "sun.net.http.retryPost"; @BeforeClass public static void disableSunRetry() throws Exception { // The Sun HTTP Client retries all requests once on an IOException, which makes testing retry code harder than would // be ideal. We can only disable it for post, so lets at least do that. oldRetryConfig = System.setProperty(SUN_RETRY_PROPERTY, "false"); } @AfterClass public static void resetSunRetry() throws Exception { if (oldRetryConfig == null) { System.clearProperty(SUN_RETRY_PROPERTY); } else { System.setProperty(SUN_RETRY_PROPERTY, oldRetryConfig); } } static String hostAndPort(URL url) { // our build slaves have underscores in their hostnames which aren't permitted by ribbon return "localhost:" + url.getPort(); } @Test public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setBody("success!")); server2.enqueue(new MockResponse().setBody("success!")); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.url("").url()) + "," + hostAndPort( server2.url("").url())); TestInterface api = Feign.builder().client(RibbonClient.create()) .target(TestInterface.class, "http://" + client()); api.post(); api.post(); assertEquals(1, server1.getRequestCount()); assertEquals(1, server2.getRequestCount()); // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } @Test public void ioExceptionRetry() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server1.enqueue(new MockResponse().setBody("success!")); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.url("").url())); TestInterface api = Feign.builder().client(RibbonClient.create()) .target(TestInterface.class, "http://" + client()); api.post(); assertEquals(2, server1.getRequestCount()); // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } @Test public void ioExceptionFailsAfterTooManyFailures() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.url("").url())); TestInterface api = Feign.builder().client(RibbonClient.create()).retryer(Retryer.NEVER_RETRY) .target(TestInterface.class, "http://" + client()); try { api.post(); fail("No exception thrown"); } catch (RetryableException ignored) { } //TODO: why are these retrying? assertThat(server1.getRequestCount()).isGreaterThanOrEqualTo(1); // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } @Test public void ribbonRetryConfigurationOnSameServer() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server2.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server2.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.url("").url()) + "," + hostAndPort(server2.url("").url())); getConfigInstance().setProperty(client() + ".ribbon.MaxAutoRetries", 1); TestInterface api = Feign.builder().client(RibbonClient.create()).retryer(Retryer.NEVER_RETRY) .target(TestInterface.class, "http://" + client()); try { api.post(); fail("No exception thrown"); } catch (RetryableException ignored) { } assertTrue(server1.getRequestCount() >= 2 || server2.getRequestCount() >= 2); assertThat(server1.getRequestCount() + server2.getRequestCount()).isGreaterThanOrEqualTo(2); // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } @Test public void ribbonRetryConfigurationOnMultipleServers() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server2.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server2.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.url("").url()) + "," + hostAndPort(server2.url("").url())); getConfigInstance().setProperty(client() + ".ribbon.MaxAutoRetriesNextServer", 1); TestInterface api = Feign.builder().client(RibbonClient.create()).retryer(Retryer.NEVER_RETRY) .target(TestInterface.class, "http://" + client()); try { api.post(); fail("No exception thrown"); } catch (RetryableException ignored) { } assertThat(server1.getRequestCount()).isGreaterThanOrEqualTo(1); assertThat(server1.getRequestCount()).isGreaterThanOrEqualTo(1); // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } /* This test-case replicates a bug that occurs when using RibbonRequest with a query string. The querystrings would not be URL-encoded, leading to invalid HTTP-requests if the query string contained invalid characters (ex. space). */ @Test public void urlEncodeQueryStringParameters() throws IOException, InterruptedException { String queryStringValue = "some string with space"; String expectedQueryStringValue = "some+string+with+space"; String expectedRequestLine = String.format("GET /?a=%s HTTP/1.1", expectedQueryStringValue); server1.enqueue(new MockResponse().setBody("success!")); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.url("").url())); TestInterface api = Feign.builder().client(RibbonClient.create()) .target(TestInterface.class, "http://" + client()); api.getWithQueryParameters(queryStringValue); final String recordedRequestLine = server1.takeRequest().getRequestLine(); assertEquals(recordedRequestLine, expectedRequestLine); } @Test public void testHTTPSViaRibbon() { Client trustSSLSockets = new Client.Default(TrustingSSLSocketFactory.get(), null); server1.useHttps(TrustingSSLSocketFactory.get("localhost"), false); server1.enqueue(new MockResponse().setBody("success!")); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.url("").url())); TestInterface api = Feign.builder().client(RibbonClient.builder().delegate(trustSSLSockets).build()) .target(TestInterface.class, "https://" + client()); api.post(); assertEquals(1, server1.getRequestCount()); } @Test public void ioExceptionRetryWithBuilder() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server1.enqueue(new MockResponse().setBody("success!")); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.url("").url())); TestInterface api = Feign.builder().client(RibbonClient.create()) .target(TestInterface.class, "http://" + client()); api.post(); assertEquals(server1.getRequestCount(), 2); // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } @Test public void ribbonRetryOnStatusCodes() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setResponseCode(502)); server2.enqueue(new MockResponse().setResponseCode(503)); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.url("").url()) + "," + hostAndPort(server2.url("").url())); getConfigInstance().setProperty(client() + ".ribbon.MaxAutoRetriesNextServer", 1); getConfigInstance().setProperty(client() + ".ribbon.RetryableStatusCodes", "503,502"); TestInterface api = Feign.builder().client(RibbonClient.create()).retryer(Retryer.NEVER_RETRY) .target(TestInterface.class, "http://" + client()); try { api.post(); fail("No exception thrown"); } catch (Exception ignored) { } assertEquals(1, server1.getRequestCount()); assertEquals(1, server2.getRequestCount()); } @Test public void testFeignOptionsClientConfig() { Request.Options options = new Request.Options(1111, 22222); IClientConfig config = new RibbonClient.FeignOptionsClientConfig(options); assertThat(config.get(CommonClientConfigKey.ConnectTimeout), equalTo(options.connectTimeoutMillis())); assertThat(config.get(CommonClientConfigKey.ReadTimeout), equalTo(options.readTimeoutMillis())); assertEquals(2, config.getProperties().size()); } @Test public void testCleanUrlWithMatchingHostAndPart() throws IOException { URI uri = RibbonClient.cleanUrl("http://questions/questions/answer/123", "questions"); assertEquals("http:///questions/answer/123", uri.toString()); } @Test public void testCleanUrl() throws IOException { URI uri = RibbonClient.cleanUrl("http://myservice/questions/answer/123", "myservice"); assertEquals("http:///questions/answer/123", uri.toString()); } private String client() { return testName.getMethodName(); } private String serverListKey() { return client() + ".ribbon.listOfServers"; } @After public void clearServerList() { getConfigInstance().clearProperty(serverListKey()); } interface TestInterface { @RequestLine("POST /") void post(); @RequestLine("GET /?a={a}") void getWithQueryParameters(@Param("a") String a); } }