diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /jaxrs_client_utils |
Publish
Diffstat (limited to 'jaxrs_client_utils')
14 files changed, 723 insertions, 0 deletions
diff --git a/jaxrs_client_utils/OWNERS b/jaxrs_client_utils/OWNERS new file mode 100644 index 00000000000..9ecc8472a21 --- /dev/null +++ b/jaxrs_client_utils/OWNERS @@ -0,0 +1,2 @@ +bakksjo +hakon diff --git a/jaxrs_client_utils/README b/jaxrs_client_utils/README new file mode 100644 index 00000000000..038a4830a74 --- /dev/null +++ b/jaxrs_client_utils/README @@ -0,0 +1,2 @@ +Utilities for client side JAX-RS. + diff --git a/jaxrs_client_utils/README.md b/jaxrs_client_utils/README.md new file mode 100644 index 00000000000..7a184e07ddf --- /dev/null +++ b/jaxrs_client_utils/README.md @@ -0,0 +1,2 @@ +# Utilities for client-side JAX-RS +Code to make it simpler to connect to JAX-RS services (aka REST APIs). diff --git a/jaxrs_client_utils/pom.xml b/jaxrs_client_utils/pom.xml new file mode 100644 index 00000000000..1c97bb7fdcc --- /dev/null +++ b/jaxrs_client_utils/pom.xml @@ -0,0 +1,79 @@ +<?xml version="1.0"?> +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.yahoo.vespa</groupId> + <artifactId>parent</artifactId> + <version>6-SNAPSHOT</version> + <relativePath>../parent/pom.xml</relativePath> + </parent> + <artifactId>jaxrs_client_utils</artifactId> + <version>6-SNAPSHOT</version> + <packaging>container-plugin</packaging> + <name>${project.artifactId}</name> + <dependencies> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>application-model</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.ws.rs</groupId> + <artifactId>javax.ws.rs-api</artifactId> + <version>2.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-proxy-client</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>jaxrs_utils</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>defaults</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-grizzly2</artifactId> + <version>${jersey2.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <groupId>com.yahoo.vespa</groupId> + <artifactId>bundle-plugin</artifactId> + <extensions>true</extensions> + </plugin> + </plugins> + </build> +</project> diff --git a/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsClientFactory.java b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsClientFactory.java new file mode 100644 index 00000000000..a9b5790dd86 --- /dev/null +++ b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsClientFactory.java @@ -0,0 +1,13 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.jaxrs.client; + +import com.yahoo.vespa.applicationmodel.HostName; + +/** + * Interface for creating a JAX-RS client API instance for a single server endpoint. + * + * @author bakksjo + */ +public interface JaxRsClientFactory { + <T> T createClient(Class<T> apiClass, HostName hostName, int port, String pathPrefix); +} diff --git a/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsStrategy.java b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsStrategy.java new file mode 100644 index 00000000000..50b81418d00 --- /dev/null +++ b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsStrategy.java @@ -0,0 +1,14 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.jaxrs.client; + +import java.io.IOException; +import java.util.function.Function; + +/** + * This interface allows different strategies for accessing server-side JAX-RS APIs programmatically. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public interface JaxRsStrategy<T> { + <R> R apply(final Function<T, R> function) throws IOException; +} diff --git a/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsStrategyFactory.java b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsStrategyFactory.java new file mode 100644 index 00000000000..0352483dd08 --- /dev/null +++ b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsStrategyFactory.java @@ -0,0 +1,90 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.jaxrs.client; + +import com.yahoo.vespa.applicationmodel.HostName; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; + +/** + * The idea behind this class is twofold: + * + * <ol> + * <li> + * It can provide alternative strategies for communicating with a JAX-RS-based server API. + * </li> + * <li> + * It can make it simpler to work with hosts that serve multiple APIs. Example: + * <pre>{@code + * final JaxRsStrategyFactory apiFactory = new JaxRsStrategyFactory(hostNames, port, clientFactory); + * // No need to repeat the hostNames etc here: + * apiFactory.apiWithRetries(FooApi.class).apply(FooApi::fooMethod); + * apiFactory.apiWithRetries(BarApi.class).apply(BarApi::barMethod); + * apiFactory.apiWithRetries(BazongaApi.class).apply(BazongaApi::bazinga); + * }</pre> + * </li> + * </ol> + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class JaxRsStrategyFactory { + private final Set<HostName> hostNames; + private int port; + private final JaxRsClientFactory jaxRsClientFactory; + + // TODO: We might need to support per-host port specification. + public JaxRsStrategyFactory( + final Set<HostName> hostNames, + final int port, + final JaxRsClientFactory jaxRsClientFactory) { + if (hostNames.isEmpty()) { + throw new IllegalArgumentException("hostNames argument must not be empty"); + } + Objects.requireNonNull(jaxRsClientFactory, "jaxRsClientFactory argument may not be null"); + this.hostNames = hostNames; + this.port = port; + this.jaxRsClientFactory = jaxRsClientFactory; + } + + public <T> JaxRsStrategy<T> apiWithRetries(final Class<T> apiClass, final String pathPrefix) { + Objects.requireNonNull(apiClass, "apiClass argument may not be null"); + Objects.requireNonNull(pathPrefix, "pathPrefix argument may not be null"); + return new RetryingJaxRsStrategy<T>(hostNames, port, jaxRsClientFactory, apiClass, pathPrefix); + } + + public <T> JaxRsStrategy<T> apiNoRetries(final Class<T> apiClass, final String pathPrefix) { + Objects.requireNonNull(apiClass, "apiClass argument may not be null"); + Objects.requireNonNull(pathPrefix, "pathPrefix argument may not be null"); + final HostName hostName = getRandom(hostNames); + return new NoRetryJaxRsStrategy<T>(hostName, port, jaxRsClientFactory, apiClass, pathPrefix); + } + + private static final Random random = new Random(); + + private static <T> T getRandom(final Collection<? extends T> collection) { + int index = random.nextInt(collection.size()); + return getIndex(collection, index); + } + + private static <T> T getIndex(final Collection<? extends T> collection, final int index) { + if (index >= collection.size() || index < 0) { + throw new IndexOutOfBoundsException( + "Attempt to get element #" + index + " from collection with " + collection.size() + " elements"); + } + + if (collection instanceof List) { + final List<? extends T> list = (List<? extends T>) collection; + return list.get(index); + } + + final Iterator<? extends T> iterator = collection.iterator(); + for (int i = 0; i < index; i++) { + iterator.next(); + } + return iterator.next(); + } +} diff --git a/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JerseyJaxRsClientFactory.java b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JerseyJaxRsClientFactory.java new file mode 100644 index 00000000000..66b1bbf6c35 --- /dev/null +++ b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JerseyJaxRsClientFactory.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.jaxrs.client; + +import com.yahoo.vespa.applicationmodel.HostName; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.client.proxy.WebResourceFactory; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.UriBuilder; + +/** + * @author bakksjo + */ +public class JerseyJaxRsClientFactory implements JaxRsClientFactory { + private static final int DEFAULT_CONNECT_TIMEOUT_MS = 30000; + private static final int DEFAULT_READ_TIMEOUT_MS = 30000; + + private final int connectTimeoutMs; + private final int readTimeoutMs; + + public JerseyJaxRsClientFactory() { + this(DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS); + } + + public JerseyJaxRsClientFactory(final int connectTimeoutMs, final int readTimeoutMs) { + this.connectTimeoutMs = connectTimeoutMs; + this.readTimeoutMs = readTimeoutMs; + } + + /** + * Contains some workarounds for HTTP/JAX-RS/Jersey issues. See: + * https://jersey.java.net/apidocs/latest/jersey/org/glassfish/jersey/client/ClientProperties.html#SUPPRESS_HTTP_COMPLIANCE_VALIDATION + * https://jersey.java.net/apidocs/latest/jersey/org/glassfish/jersey/client/HttpUrlConnectorProvider.html#SET_METHOD_WORKAROUND + */ + @Override + public <T> T createClient(final Class<T> apiClass, final HostName hostName, final int port, final String pathPrefix) { + final UriBuilder uriBuilder = UriBuilder.fromPath(pathPrefix).host(hostName.s()).port(port).scheme("http"); + final Client webClient = ClientBuilder.newClient() + .property(ClientProperties.CONNECT_TIMEOUT, connectTimeoutMs) + .property(ClientProperties.READ_TIMEOUT, readTimeoutMs) + .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) // Allow empty PUT. TODO: Fix API. + .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) // Allow e.g. PATCH method. + .property(ClientProperties.FOLLOW_REDIRECTS, true); + final WebTarget target = webClient.target(uriBuilder); + // TODO: Check if this fills up non-heap memory with loaded classes. + return WebResourceFactory.newResource(apiClass, target); + } +} diff --git a/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/LocalPassThroughJaxRsStrategy.java b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/LocalPassThroughJaxRsStrategy.java new file mode 100644 index 00000000000..280842ae4db --- /dev/null +++ b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/LocalPassThroughJaxRsStrategy.java @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.jaxrs.client; + +import java.io.IOException; +import java.util.function.Function; + +/** + * A {@link JaxRsStrategy} that does not use the network, only forwards calls to a local object. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class LocalPassThroughJaxRsStrategy<T> implements JaxRsStrategy<T> { + private final T api; + + public LocalPassThroughJaxRsStrategy(final T api) { + this.api = api; + } + + @Override + public <R> R apply(final Function<T, R> function) throws IOException { + return function.apply(api); + } +} diff --git a/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/NoRetryJaxRsStrategy.java b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/NoRetryJaxRsStrategy.java new file mode 100644 index 00000000000..eab1ff79220 --- /dev/null +++ b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/NoRetryJaxRsStrategy.java @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.jaxrs.client; + +import com.yahoo.vespa.applicationmodel.HostName; + +import javax.ws.rs.ProcessingException; +import java.io.IOException; +import java.util.Objects; +import java.util.function.Function; + +/** + * A {@link JaxRsStrategy} that will try API calls once against a single server, giving up immediately on failure. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class NoRetryJaxRsStrategy<T> implements JaxRsStrategy<T> { + private final HostName hostName; + private final int port; + private final JaxRsClientFactory jaxRsClientFactory; + private final Class<T> apiClass; + private String pathPrefix; + + public NoRetryJaxRsStrategy( + final HostName hostName, + final int port, + final JaxRsClientFactory jaxRsClientFactory, + final Class<T> apiClass, + final String pathPrefix) { + Objects.requireNonNull(hostName, "hostName argument may not be null"); + Objects.requireNonNull(jaxRsClientFactory, "jaxRsClientFactory argument may not be null"); + Objects.requireNonNull(apiClass, "apiClass argument may not be null"); + Objects.requireNonNull(pathPrefix, "pathPrefix argument may not be null"); + this.hostName = hostName; + this.port = port; + this.jaxRsClientFactory = jaxRsClientFactory; + this.apiClass = apiClass; + this.pathPrefix = pathPrefix; + } + + @Override + public <R> R apply(final Function<T, R> function) throws IOException { + final T jaxRsClient = jaxRsClientFactory.createClient(apiClass, hostName, port, pathPrefix); + try { + return function.apply(jaxRsClient); + } catch (ProcessingException e) { + throw new IOException("Communication with REST server failed", e); + } + } +} diff --git a/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/RetryingJaxRsStrategy.java b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/RetryingJaxRsStrategy.java new file mode 100644 index 00000000000..71a9aab36b4 --- /dev/null +++ b/jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/RetryingJaxRsStrategy.java @@ -0,0 +1,73 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.jaxrs.client; + +import com.yahoo.vespa.applicationmodel.HostName; + +import javax.ws.rs.ProcessingException; +import java.io.IOException; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A {@link JaxRsStrategy} that will retry on failures, looping twice over all available server hosts before giving up. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class RetryingJaxRsStrategy<T> implements JaxRsStrategy<T> { + private static final Logger logger = Logger.getLogger(RetryingJaxRsStrategy.class.getName()); + private static final int NUM_LOOP_ATTEMPTS = 2; + + private final Set<HostName> hostNames; + private final int port; + private final JaxRsClientFactory jaxRsClientFactory; + private final Class<T> apiClass; + private String pathPrefix; + + public RetryingJaxRsStrategy( + final Set<HostName> hostNames, + final int port, + final JaxRsClientFactory jaxRsClientFactory, + final Class<T> apiClass, + final String pathPrefix) { + if (hostNames.isEmpty()) { + throw new IllegalArgumentException("hostNames argument must not be empty"); + } + Objects.requireNonNull(jaxRsClientFactory, "jaxRsClientFactory argument may not be null"); + Objects.requireNonNull(apiClass, "apiClass argument may not be null"); + Objects.requireNonNull(pathPrefix, "pathPrefix argument may not be null"); + this.hostNames = hostNames; + this.port = port; + this.jaxRsClientFactory = jaxRsClientFactory; + this.apiClass = apiClass; + this.pathPrefix = pathPrefix; + } + + @Override + public <R> R apply(final Function<T, R> function) throws IOException { + ProcessingException sampleException = null; + + for (int i = 0; i < NUM_LOOP_ATTEMPTS; ++i) { + for (final HostName hostName : hostNames) { + final T jaxRsClient = jaxRsClientFactory.createClient(apiClass, hostName, port, pathPrefix); + try { + return function.apply(jaxRsClient); + } catch (ProcessingException e) { + sampleException = e; + logger.log(Level.WARNING, "Failed REST API call (in retry loop)", e); + } + } + } + + final String message = String.format( + "Giving up invoking REST API after %d tries against hosts %s.%s", + NUM_LOOP_ATTEMPTS, + hostNames, + sampleException == null ? "" : ", sample error: " + sampleException.getMessage()); + + assert sampleException != null; + throw new IOException(message, sampleException); + } +} diff --git a/jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/HttpPatchTest.java b/jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/HttpPatchTest.java new file mode 100644 index 00000000000..ae8f55af551 --- /dev/null +++ b/jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/HttpPatchTest.java @@ -0,0 +1,109 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.jaxrs.client; + +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.jaxrs.annotation.PATCH; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author bakksjo + */ +public class HttpPatchTest extends JerseyTest { + private final TestResource testResourceSingleton = new TestResource(); + + @Override + protected Application configure() { + return new Application() { + @Override + public Set<Class<?>> getClasses() { + return Collections.emptySet(); + } + + @Override + public Set<Object> getSingletons() { + return new HashSet<>(Arrays.asList(testResourceSingleton)); + } + }; + } + + @Override + protected void configureClient(final ClientConfig config) { + config.getConfiguration().property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true); + } + + private static final String REQUEST_BODY = "Hello there"; + + @Test + public void clientPatchRequest() throws Exception { + final Response response = target(TestResourceApi.PATH) + .request() + .method("PATCH", Entity.text(REQUEST_BODY)); + assertThat(testResourceSingleton.invocation.get(60, TimeUnit.SECONDS), is(REQUEST_BODY)); + assertThat(response.readEntity(String.class), is(REQUEST_BODY)); + } + + @Test + public void clientPatchRequestUsingProxyClass() throws Exception { + final URI targetUri = target(TestResourceApi.PATH).getUri(); + final HostName apiHost = new HostName(targetUri.getHost()); + final int apiPort = targetUri.getPort(); + final String apiPath = targetUri.getPath(); + + final JaxRsClientFactory jaxRsClientFactory = new JerseyJaxRsClientFactory(); + final JaxRsStrategyFactory factory = new JaxRsStrategyFactory( + Collections.singleton(apiHost), apiPort, jaxRsClientFactory); + final JaxRsStrategy<TestResourceApi> client = factory.apiNoRetries(TestResourceApi.class, apiPath); + + final String responseBody; + responseBody = client.apply(api -> + api.doPatch(REQUEST_BODY)); + + assertThat(testResourceSingleton.invocation.get(60, TimeUnit.SECONDS), is(REQUEST_BODY)); + assertThat(responseBody, is(REQUEST_BODY)); + } + + public interface TestResourceApi { + String PATH = "test"; + + @GET + String getHello(); + + @PATCH + String doPatch(final String body); + } + + @Path(TestResourceApi.PATH) + public static class TestResource implements TestResourceApi { + public final CompletableFuture<String> invocation = new CompletableFuture<>(); + + @GET + public String getHello() { + return "Hello World!"; + } + + @PATCH + public String doPatch(final String body) { + invocation.complete(body); + return body; + } + } +} diff --git a/jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/NoRetryJaxRsStrategyTest.java b/jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/NoRetryJaxRsStrategyTest.java new file mode 100644 index 00000000000..a03ada20fc5 --- /dev/null +++ b/jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/NoRetryJaxRsStrategyTest.java @@ -0,0 +1,74 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.jaxrs.client; + +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.defaults.Defaults; +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import java.io.IOException; +import static org.junit.Assert.fail; +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.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class NoRetryJaxRsStrategyTest { + private static final String API_PATH = "/foo/bar"; + + @Path(API_PATH) + private interface TestJaxRsApi { + @GET + @Path("/foo/bar") + String doSomething(); + } + + private static final HostName SERVER_HOST = new HostName("host-1"); + private static final int REST_PORT = Defaults.getDefaults().vespaWebServicePort(); + + private final JaxRsClientFactory jaxRsClientFactory = mock(JaxRsClientFactory.class); + private final TestJaxRsApi mockApi = mock(TestJaxRsApi.class); + private final JaxRsStrategy<TestJaxRsApi> jaxRsStrategy = new NoRetryJaxRsStrategy<>( + SERVER_HOST, REST_PORT, jaxRsClientFactory, TestJaxRsApi.class, API_PATH); + + @Before + public void setup() { + when(jaxRsClientFactory.createClient(eq(TestJaxRsApi.class), any(HostName.class), anyInt(), anyString())) + .thenReturn(mockApi); + } + + @Test + public void noRetryIfNoFailure() throws Exception { + jaxRsStrategy.apply(TestJaxRsApi::doSomething); + + verify(mockApi, times(1)).doSomething(); + + verify(jaxRsClientFactory, times(1)) + .createClient(eq(TestJaxRsApi.class), eq(SERVER_HOST), eq(REST_PORT), eq(API_PATH)); + } + + @Test + public void testNoRetryAfterFailure() throws Exception { + // Make the first call fail. + when(mockApi.doSomething()) + .thenThrow(new ProcessingException("Fake timeout induced by test")) + .thenReturn("a response"); + + try { + jaxRsStrategy.apply(TestJaxRsApi::doSomething); + fail("The above statement should throw"); + } catch (IOException e) { + // As expected. + } + + // Check that there was no second attempt. + verify(mockApi, times(1)).doSomething(); + } +} diff --git a/jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/RetryingJaxRsStrategyTest.java b/jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/RetryingJaxRsStrategyTest.java new file mode 100644 index 00000000000..1002ecc996c --- /dev/null +++ b/jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/RetryingJaxRsStrategyTest.java @@ -0,0 +1,142 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.jaxrs.client; + +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.defaults.Defaults; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; +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.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class RetryingJaxRsStrategyTest { + private static final String API_PATH = "/"; + + @Path(API_PATH) + private interface TestJaxRsApi { + @GET + @Path("/foo/bar") + String doSomething(); + } + + private static final Set<HostName> SERVER_HOSTS = new HashSet<>(Arrays.asList( + new HostName("host-1"), + new HostName("host-2"), + new HostName("host-3"))); + private static final int REST_PORT = Defaults.getDefaults().vespaWebServicePort(); + + private final JaxRsClientFactory jaxRsClientFactory = mock(JaxRsClientFactory.class); + private final TestJaxRsApi mockApi = mock(TestJaxRsApi.class); + private final JaxRsStrategy<TestJaxRsApi> jaxRsStrategy = new RetryingJaxRsStrategy<>( + SERVER_HOSTS, REST_PORT, jaxRsClientFactory, TestJaxRsApi.class, API_PATH); + + @Before + public void setup() { + when(jaxRsClientFactory.createClient(eq(TestJaxRsApi.class), any(HostName.class), anyInt(), anyString())) + .thenReturn(mockApi); + } + + @Test + public void noRetryIfNoFailure() throws Exception { + jaxRsStrategy.apply(TestJaxRsApi::doSomething); + + verify(mockApi, times(1)).doSomething(); + + // Check that one of the supplied hosts is contacted. + final ArgumentCaptor<HostName> hostNameCaptor = ArgumentCaptor.forClass(HostName.class); + verify(jaxRsClientFactory, times(1)) + .createClient(eq(TestJaxRsApi.class), hostNameCaptor.capture(), eq(REST_PORT), eq(API_PATH)); + assertThat(SERVER_HOSTS.contains(hostNameCaptor.getValue()), is(true)); + } + + @Test + public void testRetryAfterSingleFailure() throws Exception { + // Make the first attempt fail. + when(mockApi.doSomething()) + .thenThrow(new ProcessingException("Fake timeout induced by test")) + .thenReturn("a response"); + + jaxRsStrategy.apply(TestJaxRsApi::doSomething); + + // Check that there was a second attempt. + verify(mockApi, times(2)).doSomething(); + } + + @Test + public void testRetryUsesAllAvailableServers() throws Exception { + when(mockApi.doSomething()) + .thenThrow(new ProcessingException("Fake timeout 1 induced by test")) + .thenThrow(new ProcessingException("Fake timeout 2 induced by test")) + .thenReturn("a response"); + + jaxRsStrategy.apply(TestJaxRsApi::doSomething); + + verify(mockApi, times(3)).doSomething(); + verifyAllServersContacted(jaxRsClientFactory); + } + + @Test + public void testRetryLoopsOverAvailableServers() throws Exception { + when(mockApi.doSomething()) + .thenThrow(new ProcessingException("Fake timeout 1 induced by test")) + .thenThrow(new ProcessingException("Fake timeout 2 induced by test")) + .thenThrow(new ProcessingException("Fake timeout 3 induced by test")) + .thenThrow(new ProcessingException("Fake timeout 4 induced by test")) + .thenReturn("a response"); + + jaxRsStrategy.apply(TestJaxRsApi::doSomething); + + verify(mockApi, times(5)).doSomething(); + verifyAllServersContacted(jaxRsClientFactory); + } + + @Test + public void testRetryGivesUpAfterTwoLoopsOverAvailableServers() throws Exception { + when(mockApi.doSomething()) + .thenThrow(new ProcessingException("Fake timeout 1 induced by test")) + .thenThrow(new ProcessingException("Fake timeout 2 induced by test")) + .thenThrow(new ProcessingException("Fake timeout 3 induced by test")) + .thenThrow(new ProcessingException("Fake timeout 4 induced by test")) + .thenThrow(new ProcessingException("Fake timeout 5 induced by test")) + .thenThrow(new ProcessingException("Fake timeout 6 induced by test")); + + try { + jaxRsStrategy.apply(TestJaxRsApi::doSomething); + fail("Exception should be thrown from above statement"); + } catch (IOException e) { + // As expected. + } + + verify(mockApi, times(6)).doSomething(); + verifyAllServersContacted(jaxRsClientFactory); + } + + private static void verifyAllServersContacted( + final JaxRsClientFactory jaxRsClientFactory) { + final ArgumentCaptor<HostName> hostNameCaptor = ArgumentCaptor.forClass(HostName.class); + verify(jaxRsClientFactory, atLeast(SERVER_HOSTS.size())) + .createClient(eq(TestJaxRsApi.class), hostNameCaptor.capture(), eq(REST_PORT), eq(API_PATH)); + final Set<HostName> actualServerHostsContacted = new HashSet<>(hostNameCaptor.getAllValues()); + assertThat(actualServerHostsContacted, equalTo(SERVER_HOSTS)); + } +} |