summaryrefslogtreecommitdiffstats
path: root/jaxrs_client_utils
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /jaxrs_client_utils
Publish
Diffstat (limited to 'jaxrs_client_utils')
-rw-r--r--jaxrs_client_utils/OWNERS2
-rw-r--r--jaxrs_client_utils/README2
-rw-r--r--jaxrs_client_utils/README.md2
-rw-r--r--jaxrs_client_utils/pom.xml79
-rw-r--r--jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsClientFactory.java13
-rw-r--r--jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsStrategy.java14
-rw-r--r--jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JaxRsStrategyFactory.java90
-rw-r--r--jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/JerseyJaxRsClientFactory.java51
-rw-r--r--jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/LocalPassThroughJaxRsStrategy.java23
-rw-r--r--jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/NoRetryJaxRsStrategy.java49
-rw-r--r--jaxrs_client_utils/src/main/java/com/yahoo/vespa/jaxrs/client/RetryingJaxRsStrategy.java73
-rw-r--r--jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/HttpPatchTest.java109
-rw-r--r--jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/NoRetryJaxRsStrategyTest.java74
-rw-r--r--jaxrs_client_utils/src/test/java/com/yahoo/vespa/jaxrs/client/RetryingJaxRsStrategyTest.java142
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));
+ }
+}