From 72231250ed81e10d66bfe70701e64fa5fe50f712 Mon Sep 17 00:00:00 2001 From: Jon Bratseth Date: Wed, 15 Jun 2016 23:09:44 +0200 Subject: Publish --- jaxrs_client_utils/OWNERS | 2 + jaxrs_client_utils/README | 2 + jaxrs_client_utils/README.md | 2 + jaxrs_client_utils/pom.xml | 79 ++++++++++++ .../vespa/jaxrs/client/JaxRsClientFactory.java | 13 ++ .../yahoo/vespa/jaxrs/client/JaxRsStrategy.java | 14 ++ .../vespa/jaxrs/client/JaxRsStrategyFactory.java | 90 +++++++++++++ .../jaxrs/client/JerseyJaxRsClientFactory.java | 51 ++++++++ .../client/LocalPassThroughJaxRsStrategy.java | 23 ++++ .../vespa/jaxrs/client/NoRetryJaxRsStrategy.java | 49 +++++++ .../vespa/jaxrs/client/RetryingJaxRsStrategy.java | 73 +++++++++++ .../yahoo/vespa/jaxrs/client/HttpPatchTest.java | 109 ++++++++++++++++ .../jaxrs/client/NoRetryJaxRsStrategyTest.java | 74 +++++++++++ .../jaxrs/client/RetryingJaxRsStrategyTest.java | 142 +++++++++++++++++++++ 14 files changed, 723 insertions(+) create mode 100644 jaxrs_client_utils/OWNERS create mode 100644 jaxrs_client_utils/README create mode 100644 jaxrs_client_utils/README.md create mode 100644 jaxrs_client_utils/pom.xml create mode 100644 jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsClientFactory.java create mode 100644 jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsStrategy.java create mode 100644 jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsStrategyFactory.java create mode 100644 jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JerseyJaxRsClientFactory.java create mode 100644 jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/LocalPassThroughJaxRsStrategy.java create mode 100644 jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/NoRetryJaxRsStrategy.java create mode 100644 jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/RetryingJaxRsStrategy.java create mode 100644 jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/HttpPatchTest.java create mode 100644 jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/NoRetryJaxRsStrategyTest.java create mode 100644 jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/RetryingJaxRsStrategyTest.java (limited to 'jaxrs_client_utils') 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 @@ + + + + 4.0.0 + + com.yahoo.vespa + parent + 6-SNAPSHOT + ../parent/pom.xml + + jaxrs_client_utils + 6-SNAPSHOT + container-plugin + ${project.artifactId} + + + com.yahoo.vespa + application-model + ${project.version} + provided + + + javax.ws.rs + javax.ws.rs-api + 2.0 + provided + + + org.glassfish.jersey.core + jersey-client + provided + + + org.glassfish.jersey.ext + jersey-proxy-client + provided + + + com.yahoo.vespa + jaxrs_utils + ${project.version} + test + + + com.yahoo.vespa + defaults + ${project.version} + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + ${jersey2.version} + test + + + junit + junit + test + + + org.mockito + mockito-core + test + + + + + + com.yahoo.vespa + bundle-plugin + true + + + + 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 createClient(Class 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 Oyvind Bakksjo + */ +public interface JaxRsStrategy { + R apply(final Function 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: + * + *
    + *
  1. + * It can provide alternative strategies for communicating with a JAX-RS-based server API. + *
  2. + *
  3. + * It can make it simpler to work with hosts that serve multiple APIs. Example: + *
    {@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);
    + *         }
    + *
  4. + *
+ * + * @author Oyvind Bakksjo + */ +public class JaxRsStrategyFactory { + private final Set hostNames; + private int port; + private final JaxRsClientFactory jaxRsClientFactory; + + // TODO: We might need to support per-host port specification. + public JaxRsStrategyFactory( + final Set 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 JaxRsStrategy apiWithRetries(final Class 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(hostNames, port, jaxRsClientFactory, apiClass, pathPrefix); + } + + public JaxRsStrategy apiNoRetries(final Class 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(hostName, port, jaxRsClientFactory, apiClass, pathPrefix); + } + + private static final Random random = new Random(); + + private static T getRandom(final Collection collection) { + int index = random.nextInt(collection.size()); + return getIndex(collection, index); + } + + private static T getIndex(final Collection 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 list = (List) collection; + return list.get(index); + } + + final Iterator 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 createClient(final Class 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 Oyvind Bakksjo + */ +public class LocalPassThroughJaxRsStrategy implements JaxRsStrategy { + private final T api; + + public LocalPassThroughJaxRsStrategy(final T api) { + this.api = api; + } + + @Override + public R apply(final Function 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 Oyvind Bakksjo + */ +public class NoRetryJaxRsStrategy implements JaxRsStrategy { + private final HostName hostName; + private final int port; + private final JaxRsClientFactory jaxRsClientFactory; + private final Class apiClass; + private String pathPrefix; + + public NoRetryJaxRsStrategy( + final HostName hostName, + final int port, + final JaxRsClientFactory jaxRsClientFactory, + final Class 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 apply(final Function 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 Oyvind Bakksjo + */ +public class RetryingJaxRsStrategy implements JaxRsStrategy { + private static final Logger logger = Logger.getLogger(RetryingJaxRsStrategy.class.getName()); + private static final int NUM_LOOP_ATTEMPTS = 2; + + private final Set hostNames; + private final int port; + private final JaxRsClientFactory jaxRsClientFactory; + private final Class apiClass; + private String pathPrefix; + + public RetryingJaxRsStrategy( + final Set hostNames, + final int port, + final JaxRsClientFactory jaxRsClientFactory, + final Class 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 apply(final Function 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> getClasses() { + return Collections.emptySet(); + } + + @Override + public Set 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 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 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 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 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 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 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 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 actualServerHostsContacted = new HashSet<>(hostNameCaptor.getAllValues()); + assertThat(actualServerHostsContacted, equalTo(SERVER_HOSTS)); + } +} -- cgit v1.2.3