summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHenning Baldersheim <balder@yahoo-inc.com>2023-01-04 09:21:28 +0100
committerGitHub <noreply@github.com>2023-01-04 09:21:28 +0100
commit7db83f1e63c06b50fd2dadf271deda9b489fe14d (patch)
treeb7b9905c0656b66112153b6e220986736b6cc6e2
parenta47adad9f0fbb6d7e1dc24f600d4ad0e78cf485b (diff)
parenteb0bf8c5bb18ad89ba03d290c6cedc6773b321bc (diff)
Merge pull request #25381 from vespa-engine/jonmv/feed-client-gzip
Add --gzip-requests to compress request bodies in feed client
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java8
-rw-r--r--vespa-feed-client-api/abi-spec.json18
-rw-r--r--vespa-feed-client-api/src/main/java/ai/vespa/feed/client/FeedClientBuilder.java5
-rw-r--r--vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java19
-rw-r--r--vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliClient.java5
-rw-r--r--vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliArgumentsTest.java41
-rw-r--r--vespa-feed-client-cli/src/test/resources/help.txt3
-rw-r--r--vespa-feed-client/pom.xml5
-rw-r--r--vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/ApacheCluster.java29
-rw-r--r--vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/FeedClientBuilderImpl.java8
-rw-r--r--vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/ApacheClusterTest.java74
-rw-r--r--vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/WireMockExtension.java42
12 files changed, 232 insertions, 25 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java
index a93741fd8fb..e23f0205e5a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java
@@ -79,8 +79,7 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase {
@Override
protected Optional<ErrorResponse> filter(DiscFilterRequest request) {
try {
- Principal principal = request.getUserPrincipal();
- if (principal instanceof AthenzPrincipal) {
+ if (request.getUserPrincipal() instanceof AthenzPrincipal principal) {
Optional<DecodedJWT> oktaAt = Optional.ofNullable((String) request.getAttribute("okta.access-token")).map(JWT::decode);
Optional<X509Certificate> cert = request.getClientCertificateChain().stream().findFirst();
Instant issuedAt = cert.map(X509Certificate::getNotBefore)
@@ -89,9 +88,8 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase {
Instant expireAt = cert.map(X509Certificate::getNotAfter)
.or(() -> oktaAt.map(Payload::getExpiresAt))
.map(Date::toInstant).orElse(Instant.MAX);
- request.setAttribute(SecurityContext.ATTRIBUTE_NAME, new SecurityContext(principal,
- roles((AthenzPrincipal) principal, request.getUri()),
- issuedAt, expireAt));
+ request.setAttribute(SecurityContext.ATTRIBUTE_NAME,
+ new SecurityContext(principal, roles(principal, request.getUri()), issuedAt, expireAt));
}
}
catch (Exception e) {
diff --git a/vespa-feed-client-api/abi-spec.json b/vespa-feed-client-api/abi-spec.json
index 137c7f32bfe..64b049dc75d 100644
--- a/vespa-feed-client-api/abi-spec.json
+++ b/vespa-feed-client-api/abi-spec.json
@@ -112,6 +112,23 @@
],
"fields" : [ ]
},
+ "ai.vespa.feed.client.FeedClientBuilder$Compression" : {
+ "superClass" : "java.lang.Enum",
+ "interfaces" : [ ],
+ "attributes" : [
+ "public",
+ "final",
+ "enum"
+ ],
+ "methods" : [
+ "public static ai.vespa.feed.client.FeedClientBuilder$Compression[] values()",
+ "public static ai.vespa.feed.client.FeedClientBuilder$Compression valueOf(java.lang.String)"
+ ],
+ "fields" : [
+ "public static final enum ai.vespa.feed.client.FeedClientBuilder$Compression none",
+ "public static final enum ai.vespa.feed.client.FeedClientBuilder$Compression gzip"
+ ]
+ },
"ai.vespa.feed.client.FeedClientBuilder" : {
"superClass" : "java.lang.Object",
"interfaces" : [ ],
@@ -142,6 +159,7 @@
"public abstract ai.vespa.feed.client.FeedClientBuilder setCaCertificates(java.util.Collection)",
"public abstract ai.vespa.feed.client.FeedClientBuilder setEndpointUris(java.util.List)",
"public abstract ai.vespa.feed.client.FeedClientBuilder setProxy(java.net.URI)",
+ "public abstract ai.vespa.feed.client.FeedClientBuilder setCompression(ai.vespa.feed.client.FeedClientBuilder$Compression)",
"public abstract ai.vespa.feed.client.FeedClient build()"
],
"fields" : [
diff --git a/vespa-feed-client-api/src/main/java/ai/vespa/feed/client/FeedClientBuilder.java b/vespa-feed-client-api/src/main/java/ai/vespa/feed/client/FeedClientBuilder.java
index e8cb5344aff..d48c3c31348 100644
--- a/vespa-feed-client-api/src/main/java/ai/vespa/feed/client/FeedClientBuilder.java
+++ b/vespa-feed-client-api/src/main/java/ai/vespa/feed/client/FeedClientBuilder.java
@@ -123,6 +123,11 @@ public interface FeedClientBuilder {
/** Specify HTTP(S) proxy for all endpoints */
FeedClientBuilder setProxy(URI uri);
+ /** What compression to use for request bodies; default {@code NONE}. */
+ FeedClientBuilder setCompression(Compression compression);
+
+ enum Compression { none, gzip }
+
/** Constructs instance of {@link FeedClient} from builder configuration */
FeedClient build();
diff --git a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java
index fd36749b109..42f9713c54e 100644
--- a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java
+++ b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package ai.vespa.feed.client.impl;
+import ai.vespa.feed.client.FeedClientBuilder.Compression;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
@@ -24,6 +25,8 @@ import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
+import static ai.vespa.feed.client.FeedClientBuilder.Compression.none;
+
/**
* Parses command line arguments
*
@@ -58,6 +61,7 @@ class CliArguments {
private static final String STDIN_OPTION = "stdin";
private static final String DOOM_OPTION = "max-failure-seconds";
private static final String PROXY_OPTION = "proxy";
+ private static final String COMPRESSION = "compression";
private final CommandLine arguments;
@@ -181,6 +185,15 @@ class CliArguments {
boolean speedTest() { return has(SPEED_TEST_OPTION); }
+ Compression compression() throws CliArgumentsException {
+ try {
+ return stringValue(COMPRESSION).map(Compression::valueOf).orElse(none);
+ }
+ catch (IllegalArgumentException e) {
+ throw new CliArgumentsException("Invalid " + COMPRESSION + " argument: " + e.getMessage(), e);
+ }
+ }
+
OptionalInt testPayloadSize() throws CliArgumentsException { return intValue(TEST_PAYLOAD_SIZE_OPTION); }
Optional<URI> proxy() throws CliArgumentsException {
@@ -354,6 +367,12 @@ class CliArguments {
.desc("URI to proxy endpoint")
.hasArg()
.type(URL.class)
+ .build())
+ .addOption(Option.builder()
+ .longOpt(COMPRESSION)
+ .desc("Compression mode for feed requests: 'none' (default), 'gzip'")
+ .hasArg()
+ .type(Compression.class)
.build());
}
diff --git a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliClient.java b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliClient.java
index 43a16c2abf0..39462d8ba68 100644
--- a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliClient.java
+++ b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliClient.java
@@ -1,7 +1,6 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package ai.vespa.feed.client.impl;
-import ai.vespa.feed.client.DocumentId;
import ai.vespa.feed.client.FeedClient;
import ai.vespa.feed.client.FeedClientBuilder;
import ai.vespa.feed.client.FeedException;
@@ -10,7 +9,6 @@ import ai.vespa.feed.client.JsonFeeder.ResultCallback;
import ai.vespa.feed.client.OperationStats;
import ai.vespa.feed.client.Result;
import ai.vespa.feed.client.ResultException;
-import ai.vespa.feed.client.impl.CliArguments.CliArgumentsException;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
@@ -29,14 +27,12 @@ import java.time.Instant;
import java.util.Enumeration;
import java.util.Map;
import java.util.Random;
-import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
-import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static java.util.stream.Collectors.joining;
@@ -163,6 +159,7 @@ public class CliClient {
cliArgs.headers().forEach(builder::addRequestHeader);
builder.setDryrun(cliArgs.dryrunEnabled());
builder.setSpeedTest(cliArgs.speedTest());
+ builder.setCompression(cliArgs.compression());
cliArgs.doomSeconds().ifPresent(doom -> builder.setCircuitBreaker(new GracePeriodCircuitBreaker(Duration.ofSeconds(10),
Duration.ofSeconds(doom))));
cliArgs.proxy().ifPresent(builder::setProxy);
diff --git a/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliArgumentsTest.java b/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliArgumentsTest.java
index 073ea4a58db..21e279b0584 100644
--- a/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliArgumentsTest.java
+++ b/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliArgumentsTest.java
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package ai.vespa.feed.client.impl;
+import ai.vespa.feed.client.FeedClientBuilder.Compression;
import ai.vespa.feed.client.impl.CliArguments.CliArgumentsException;
import org.junit.jupiter.api.Test;
@@ -24,12 +25,27 @@ class CliArgumentsTest {
@Test
void parses_parameters_correctly() throws CliArguments.CliArgumentsException {
CliArguments args = CliArguments.fromRawArgs(new String[]{
- "--endpoint=https://vespa.ai:4443/", "--file=feed.json", "--connections=10",
- "--max-streams-per-connection=128", "--certificate=cert.pem", "--private-key=key.pem",
- "--ca-certificates=ca-certs.pem", "--disable-ssl-hostname-verification",
- "--header=\"My-Header: my-value\"", "--header", "Another-Header: another-value", "--benchmark",
- "--route=myroute", "--timeout=0.125", "--trace=9", "--verbose", "--silent",
- "--show-errors", "--show-all", "--max-failure-seconds=30", "--proxy", "https://myproxy:1234"});
+ "--endpoint", "https://vespa.ai:4443/",
+ "--file", "feed.json",
+ "--connections", "10",
+ "--max-streams-per-connection", "128",
+ "--certificate", "cert.pem",
+ "--private-key", "key.pem",
+ "--ca-certificates", "ca-certs.pem",
+ "--disable-ssl-hostname-verification",
+ "--header", "\"My-Header: my-value\"",
+ "--header", "Another-Header: another-value",
+ "--benchmark",
+ "--route", "myroute",
+ "--timeout", "0.125",
+ "--trace", "9",
+ "--verbose",
+ "--silent",
+ "--compression", "gzip",
+ "--show-errors",
+ "--show-all",
+ "--max-failure-seconds", "30",
+ "--proxy", "https://myproxy:1234"});
assertEquals(URI.create("https://vespa.ai:4443/"), args.endpoint());
assertEquals(Paths.get("feed.json"), args.inputFile().get());
assertEquals(10, args.connections().getAsInt());
@@ -52,14 +68,15 @@ class CliArgumentsTest {
assertTrue(args.showErrors());
assertTrue(args.showSuccesses());
assertFalse(args.showProgress());
+ assertEquals(Compression.gzip, args.compression());
assertEquals(URI.create("https://myproxy:1234"), args.proxy().orElse(null));
}
@Test
void fails_on_missing_parameters() {
- CliArguments.CliArgumentsException exception = assertThrows(
+ CliArguments.CliArgumentsException exception = assertThrows(
CliArguments.CliArgumentsException.class,
- () -> CliArguments.fromRawArgs(new String[] {"--file=/path/to/file", "--stdin"}));
+ () -> CliArguments.fromRawArgs(new String[] {"--file", "/path/to/file", "--stdin"}));
assertEquals("Endpoint must be specified", exception.getMessage());
}
@@ -67,20 +84,20 @@ class CliArgumentsTest {
void fails_on_conflicting_parameters() throws CliArgumentsException {
assertEquals("Exactly one of 'file' and 'stdin' must be specified",
assertThrows(CliArgumentsException.class,
- () -> CliArguments.fromRawArgs(new String[] {"--endpoint=https://endpoint", "--file=/path/to/file", "--stdin"}))
+ () -> CliArguments.fromRawArgs(new String[] {"--endpoint", "https://endpoint", "--file", "/path/to/file", "--stdin"}))
.getMessage());
assertEquals("Exactly one of 'file' and 'stdin' must be specified",
assertThrows(CliArgumentsException.class,
- () -> CliArguments.fromRawArgs(new String[] {"--endpoint=https://endpoint"}))
+ () -> CliArguments.fromRawArgs(new String[] {"--endpoint", "https://endpoint"}))
.getMessage());
assertEquals("At most one of 'file', 'stdin' and 'test-payload-size' may be specified",
assertThrows(CliArgumentsException.class,
- () -> CliArguments.fromRawArgs(new String[] {"--endpoint=https://endpoint", "--speed-test", "--test-payload-size=123", "--file=file"}))
+ () -> CliArguments.fromRawArgs(new String[] {"--endpoint", "https://endpoint", "--speed-test", "--test-payload-size", "123", "--file", "file"}))
.getMessage());
- CliArguments.fromRawArgs(new String[] {"--endpoint=foo", "--speed-test"});
+ CliArguments.fromRawArgs(new String[] {"--endpoint", "foo", "--speed-test"});
}
@Test
diff --git a/vespa-feed-client-cli/src/test/resources/help.txt b/vespa-feed-client-cli/src/test/resources/help.txt
index e41a78bc932..f33dde82f7b 100644
--- a/vespa-feed-client-cli/src/test/resources/help.txt
+++ b/vespa-feed-client-cli/src/test/resources/help.txt
@@ -6,6 +6,9 @@ Vespa feed client
certificates encoded as PEM
--certificate <arg> Path to PEM encoded X.509
certificate file
+ --compression <arg> Compression mode for feed
+ requests: 'none' (default),
+ 'gzip'
--connections <arg> Number of concurrent HTTP/2
connections
--disable-ssl-hostname-verification Disable SSL hostname
diff --git a/vespa-feed-client/pom.xml b/vespa-feed-client/pom.xml
index 1cc2f2adee1..01b9b00b8a0 100644
--- a/vespa-feed-client/pom.xml
+++ b/vespa-feed-client/pom.xml
@@ -46,6 +46,11 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.github.tomakehurst</groupId>
+ <artifactId>wiremock-jre8-standalone</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/ApacheCluster.java b/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/ApacheCluster.java
index 3ffbaf136f2..1dda8912046 100644
--- a/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/ApacheCluster.java
+++ b/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/ApacheCluster.java
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package ai.vespa.feed.client.impl;
+import ai.vespa.feed.client.FeedClientBuilder.Compression;
import ai.vespa.feed.client.HttpResponse;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
@@ -10,6 +11,8 @@ import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http2.config.H2Config;
@@ -18,6 +21,7 @@ import org.apache.hc.core5.reactor.IOReactorConfig;
import org.apache.hc.core5.util.Timeout;
import javax.net.ssl.SSLContext;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
@@ -29,8 +33,8 @@ import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.GZIPOutputStream;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.hc.core5.http.ssl.TlsCiphers.excludeH2Blacklisted;
import static org.apache.hc.core5.http.ssl.TlsCiphers.excludeWeak;
@@ -40,9 +44,11 @@ import static org.apache.hc.core5.http.ssl.TlsCiphers.excludeWeak;
class ApacheCluster implements Cluster {
private final List<Endpoint> endpoints = new ArrayList<>();
- private final List<BasicHeader> defaultHeaders = Arrays.asList(new BasicHeader("User-Agent", String.format("vespa-feed-client/%s", Vespa.VERSION)),
+ private final List<BasicHeader> defaultHeaders = Arrays.asList(new BasicHeader(HttpHeaders.USER_AGENT, String.format("vespa-feed-client/%s", Vespa.VERSION)),
new BasicHeader("Vespa-Client-Version", Vespa.VERSION));
+ private final Header gzipEncodingHeader = new BasicHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
private final RequestConfig requestConfig;
+ private final boolean gzip;
private int someNumber = 0;
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(t -> new Thread(t, "request-timeout-thread"));
@@ -52,6 +58,7 @@ class ApacheCluster implements Cluster {
for (URI endpoint : builder.endpoints)
endpoints.add(new Endpoint(createHttpClient(builder), endpoint));
this.requestConfig = createRequestConfig(builder);
+ this.gzip = builder.compression == Compression.gzip;
}
@Override
@@ -77,8 +84,14 @@ class ApacheCluster implements Cluster {
request.setConfig(requestConfig);
defaultHeaders.forEach(request::setHeader);
wrapped.headers().forEach((name, value) -> request.setHeader(name, value.get()));
- if (wrapped.body() != null)
- request.setBody(wrapped.body(), ContentType.APPLICATION_JSON);
+ if (wrapped.body() != null) {
+ byte[] body = wrapped.body();
+ if (gzip) {
+ request.setHeader(gzipEncodingHeader);
+ body = gzipped(body);
+ }
+ request.setBody(body, ContentType.APPLICATION_JSON);
+ }
Future<?> future = endpoint.client.execute(request,
new FutureCallback<SimpleHttpResponse>() {
@@ -96,6 +109,14 @@ class ApacheCluster implements Cluster {
vessel.whenComplete((__, ___) -> endpoint.inflight.decrementAndGet());
}
+ private byte[] gzipped(byte[] content) throws IOException{
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream(1 << 10);
+ try (GZIPOutputStream zip = new GZIPOutputStream(buffer)) {
+ zip.write(content);
+ }
+ return buffer.toByteArray();
+ }
+
@Override
public void close() {
Throwable thrown = null;
diff --git a/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/FeedClientBuilderImpl.java b/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/FeedClientBuilderImpl.java
index 1960991792f..6886dc3d2b9 100644
--- a/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/FeedClientBuilderImpl.java
+++ b/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/FeedClientBuilderImpl.java
@@ -21,6 +21,7 @@ import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
+import static ai.vespa.feed.client.FeedClientBuilder.Compression.none;
import static java.util.Objects.requireNonNull;
/**
@@ -50,6 +51,7 @@ public class FeedClientBuilderImpl implements FeedClientBuilder {
boolean benchmark = true;
boolean dryrun = false;
boolean speedTest = false;
+ Compression compression = none;
URI proxy;
@@ -200,6 +202,12 @@ public class FeedClientBuilderImpl implements FeedClientBuilder {
return this;
}
+ @Override
+ public FeedClientBuilderImpl setCompression(Compression compression) {
+ this.compression = compression;
+ return this;
+ }
+
/** Constructs instance of {@link ai.vespa.feed.client.FeedClient} from builder configuration */
@Override
public FeedClient build() {
diff --git a/vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/ApacheClusterTest.java b/vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/ApacheClusterTest.java
new file mode 100644
index 00000000000..33c043ea271
--- /dev/null
+++ b/vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/ApacheClusterTest.java
@@ -0,0 +1,74 @@
+package ai.vespa.feed.client.impl;
+
+import ai.vespa.feed.client.FeedClientBuilder.Compression;
+import ai.vespa.feed.client.HttpResponse;
+import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.zip.GZIPOutputStream;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.any;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ApacheClusterTest {
+
+ @RegisterExtension
+ final WireMockExtension server = new WireMockExtension();
+
+ @Test
+ void testClient() throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ for (Compression compression : Compression.values()) {
+ try (ApacheCluster cluster = new ApacheCluster(new FeedClientBuilderImpl(List.of(URI.create("http://localhost:" + server.port())))
+ .setCompression(compression))) {
+ server.stubFor(any(anyUrl()))
+ .setResponse(okJson("{}").build());
+
+ CompletableFuture<HttpResponse> vessel = new CompletableFuture<>();
+ cluster.dispatch(new HttpRequest("POST",
+ "/path",
+ Map.of("name1", () -> "value1",
+ "name2", () -> "value2"),
+ "content".getBytes(UTF_8),
+ Duration.ofSeconds(1)),
+ vessel);
+ HttpResponse response = vessel.get(5, TimeUnit.SECONDS);
+ assertEquals("{}", new String(response.body(), UTF_8));
+ assertEquals(200, response.code());
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ try (OutputStream zip = new GZIPOutputStream(buffer)) { zip.write("content".getBytes(UTF_8)); }
+ server.verify(1, anyRequestedFor(anyUrl()));
+ RequestPatternBuilder expected = postRequestedFor(urlEqualTo("/path")).withHeader("name1", equalTo("value1"))
+ .withHeader("name2", equalTo("value2"))
+ .withHeader("Content-Type", equalTo("application/json; charset=UTF-8"))
+ .withRequestBody(equalTo("content"));
+ expected = switch (compression) {
+ case none -> expected.withoutHeader("Content-Encoding");
+ case gzip -> expected.withHeader("Content-Encoding", equalTo("gzip"));
+ };
+ server.verify(1, expected);
+ server.resetRequests();
+ }
+ }
+ }
+
+}
diff --git a/vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/WireMockExtension.java b/vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/WireMockExtension.java
new file mode 100644
index 00000000000..ef61213889b
--- /dev/null
+++ b/vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/WireMockExtension.java
@@ -0,0 +1,42 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.feed.client.impl;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.Options;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+/**
+ * Allows wiremock to be used as a JUnit 5 extension, like
+ * <pre>
+ *
+ * &#64RegisterExtension
+ * WireMockExtension mockServer1 = new WireMockExtension();
+ * </pre>
+ */
+public class WireMockExtension extends WireMockServer implements BeforeEachCallback, AfterEachCallback {
+
+ public WireMockExtension() {
+ this(WireMockConfiguration.options()
+ .dynamicPort()
+ .dynamicHttpsPort());
+ }
+
+ public WireMockExtension(Options options) {
+ super(options);
+ }
+
+ @Override
+ public void beforeEach(ExtensionContext extensionContext) {
+ start();
+ }
+
+ @Override
+ public void afterEach(ExtensionContext extensionContext) {
+ stop();
+ resetAll();
+ }
+
+}