aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjonmv <venstad@gmail.com>2022-08-11 13:44:37 +0200
committerjonmv <venstad@gmail.com>2022-08-11 13:44:37 +0200
commit5fec3608e404237ac006b59a4acf41bcfb1353dc (patch)
tree35591f766954719296d124d8db9d9fee9e389d05
parent8be6039101a0c4006199c2946d8381c86428ec11 (diff)
Add --speedTest to feed client CLI, and dryRun to /doc/v1
-rw-r--r--vespa-feed-client-api/src/main/java/ai/vespa/feed/client/FeedClientBuilder.java4
-rw-r--r--vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java33
-rw-r--r--vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliClient.java79
-rw-r--r--vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliArgumentsTest.java26
-rw-r--r--vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliClientTest.java31
-rw-r--r--vespa-feed-client-cli/src/test/resources/help.txt14
-rw-r--r--vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/FeedClientBuilderImpl.java7
-rw-r--r--vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/HttpFeedClient.java7
-rw-r--r--vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java31
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java83
10 files changed, 259 insertions, 56 deletions
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 7ec5fbb02b7..b7c51a0c5c9 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
@@ -95,8 +95,12 @@ public interface FeedClientBuilder {
/** Sets client SSL certificate/key */
FeedClientBuilder setCertificate(X509Certificate certificate, PrivateKey privateKey);
+ /** Turns on dryrun mode, where each operation succeeds after a given delay, rather than being sent across the network. */
FeedClientBuilder setDryrun(boolean enabled);
+ /** Turns on speed test mode, where all feed operations are immediately acknowledged by the server. */
+ FeedClientBuilder setSpeedTest(boolean enabled);
+
/**
* Overrides JVM default SSL truststore
* @param caCertificatesFile Path to PEM encoded file containing trusted certificates
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 e024f961e26..0eb829c954f 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
@@ -39,6 +39,8 @@ class CliArguments {
private static final String CONNECTIONS_OPTION = "connections";
private static final String DISABLE_SSL_HOSTNAME_VERIFICATION_OPTION = "disable-ssl-hostname-verification";
private static final String DRYRUN_OPTION = "dryrun";
+ private static final String SPEED_TEST_OPTION = "speed-test";
+ private static final String TEST_PAYLOAD_SIZE_OPTION = "test-payload-size";
private static final String ENDPOINT_OPTION = "endpoint";
private static final String FILE_OPTION = "file";
private static final String HEADER_OPTION = "header";
@@ -78,8 +80,19 @@ class CliArguments {
if (!args.hasOption(ENDPOINT_OPTION)) {
throw new CliArgumentsException("Endpoint must be specified");
}
- if (args.hasOption(FILE_OPTION) == args.hasOption(STDIN_OPTION)) {
- throw new CliArgumentsException(String.format("Either option '%s' or '%s' must be specified", FILE_OPTION, STDIN_OPTION));
+ if (args.hasOption(SPEED_TEST_OPTION)) {
+ if ( args.hasOption(FILE_OPTION) && (args.hasOption(STDIN_OPTION) || args.hasOption(TEST_PAYLOAD_SIZE_OPTION))
+ || args.hasOption(STDIN_OPTION) && args.hasOption(TEST_PAYLOAD_SIZE_OPTION)) {
+ throw new CliArgumentsException(String.format("At most one of '%s', '%s' and '%s' may be specified", FILE_OPTION, STDIN_OPTION, TEST_PAYLOAD_SIZE_OPTION));
+ }
+ }
+ else {
+ if (args.hasOption(FILE_OPTION) == args.hasOption(STDIN_OPTION)) {
+ throw new CliArgumentsException(String.format("Exactly one of '%s' and '%s' must be specified", FILE_OPTION, STDIN_OPTION));
+ }
+ if (args.hasOption(TEST_PAYLOAD_SIZE_OPTION)) {
+ throw new CliArgumentsException(String.format("Option '%s' can only be specified together with '%s'", TEST_PAYLOAD_SIZE_OPTION, SPEED_TEST_OPTION));
+ }
}
if (args.hasOption(CERTIFICATE_OPTION) != args.hasOption(PRIVATE_KEY_OPTION)) {
throw new CliArgumentsException(
@@ -166,6 +179,10 @@ class CliArguments {
boolean dryrunEnabled() { return has(DRYRUN_OPTION); }
+ boolean speedTest() { return has(SPEED_TEST_OPTION); }
+
+ OptionalInt testPayloadSize() throws CliArgumentsException { return intValue(TEST_PAYLOAD_SIZE_OPTION); }
+
Optional<URI> proxy() throws CliArgumentsException {
try {
URL url = (URL) arguments.getParsedOptionValue(PROXY_OPTION);
@@ -304,7 +321,17 @@ class CliArguments {
.build())
.addOption(Option.builder()
.longOpt(DRYRUN_OPTION)
- .desc("Enable dryrun mode where each operation succeeds after " + DryrunCluster.DELAY.toMillis() + "ms")
+ .desc("Let each operation succeed after " + DryrunCluster.DELAY.toMillis() + "ms, instead of sending it across the network ")
+ .build())
+ .addOption(Option.builder()
+ .longOpt(SPEED_TEST_OPTION)
+ .desc("Perform a network speed test, where the server immediately responds to each feed operation with a successful response")
+ .build())
+ .addOption(Option.builder()
+ .longOpt(TEST_PAYLOAD_SIZE_OPTION)
+ .desc("Document JSON test payload size in bytes, for use with --speed-test; requires --file and -stdin to not be set; default is 1024")
+ .hasArg()
+ .type(Number.class)
.build())
.addOption(Option.builder()
.longOpt(VERBOSE_OPTION)
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 68b9cf6af0e..e4fe07bedb2 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,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.DocumentId;
import ai.vespa.feed.client.FeedClient;
import ai.vespa.feed.client.FeedClientBuilder;
import ai.vespa.feed.client.FeedException;
@@ -9,22 +10,35 @@ 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;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
+import java.io.SequenceInputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Duration;
+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.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;
/**
* Main method for CLI interface
@@ -76,7 +90,7 @@ public class CliClient {
if (cliArgs.showProgress()) {
Thread progressPrinter = new Thread(() -> {
try {
- while ( ! latch.await(10, TimeUnit.SECONDS)) {
+ while (!latch.await(10, TimeUnit.SECONDS)) {
synchronized (printMonitor) {
printBenchmarkResult(System.nanoTime() - startNanos, successes.get(), failures.get(), feedClient.stats(), systemError);
}
@@ -89,9 +103,17 @@ public class CliClient {
}
feeder.feedMany(in, new ResultCallback() {
- @Override public void onNextResult(Result result, FeedException error) { handleResult(result, error, successes, failures, cliArgs); }
- @Override public void onError(FeedException error) { fatal.set(error); latch.countDown(); }
- @Override public void onComplete() { latch.countDown(); }
+ @Override
+ public void onNextResult(Result result, FeedException error) { handleResult(result, error, successes, failures, cliArgs); }
+
+ @Override
+ public void onError(FeedException error) {
+ fatal.set(error);
+ latch.countDown();
+ }
+
+ @Override
+ public void onComplete() { latch.countDown(); }
});
latch.await();
@@ -99,9 +121,11 @@ public class CliClient {
if (fatal.get() != null) throw fatal.get();
}
return 0;
- } catch (CliArguments.CliArgumentsException | IOException | FeedException e) {
+ }
+ catch (CliArguments.CliArgumentsException | IOException | FeedException e) {
return handleException(verbose, e);
- } catch (Exception e) {
+ }
+ catch (Exception e) {
return handleException(verbose, "Unknown failure: " + e.getMessage(), e);
}
}
@@ -111,7 +135,8 @@ public class CliClient {
failures.incrementAndGet();
if (args.showErrors()) synchronized (printMonitor) {
systemError.println(error.getMessage());
- if (error instanceof ResultException) ((ResultException) error).getTrace().ifPresent(systemError::println);
+ if (error instanceof ResultException)
+ ((ResultException) error).getTrace().ifPresent(systemError::println);
if (args.verboseSpecified()) error.printStackTrace(systemError);
}
}
@@ -136,6 +161,7 @@ public class CliClient {
cliArgs.caCertificates().ifPresent(builder::setCaCertificatesFile);
cliArgs.headers().forEach(builder::addRequestHeader);
builder.setDryrun(cliArgs.dryrunEnabled());
+ builder.setSpeedTest(cliArgs.speedTest());
cliArgs.doomSeconds().ifPresent(doom -> builder.setCircuitBreaker(new GracePeriodCircuitBreaker(Duration.ofSeconds(10),
Duration.ofSeconds(doom))));
cliArgs.proxy().ifPresent(builder::setProxy);
@@ -151,7 +177,9 @@ public class CliClient {
}
private InputStream createFeedInputStream(CliArguments cliArgs) throws CliArguments.CliArgumentsException, IOException {
- return cliArgs.readFeedFromStandardInput() ? systemIn : Files.newInputStream(cliArgs.inputFile().get());
+ return cliArgs.readFeedFromStandardInput() ? systemIn
+ : cliArgs.inputFile().isPresent() ? Files.newInputStream(cliArgs.inputFile().get())
+ : createDummyInputStream(cliArgs.testPayloadSize().orElse(1024));
}
private int handleException(boolean verbose, Exception e) { return handleException(verbose, e.getMessage(), e); }
@@ -165,8 +193,12 @@ public class CliClient {
}
private static class AcceptAllHostnameVerifier implements HostnameVerifier {
+
static final AcceptAllHostnameVerifier INSTANCE = new AcceptAllHostnameVerifier();
- @Override public boolean verify(String hostname, SSLSession session) { return true; }
+
+ @Override
+ public boolean verify(String hostname, SSLSession session) { return true; }
+
}
static void printBenchmarkResult(long durationNanos, long successes, long failures,
@@ -206,4 +238,31 @@ public class CliClient {
generator.writeNumber(String.format("%." + precision + "f", value));
}
-}
+ /** Creates an input stream that spits out random documents (id and data) for one minute. */
+ static InputStream createDummyInputStream(int payloadSize) {
+ Instant end = Instant.now().plusSeconds(60);
+ return createDummyInputStream(payloadSize, new Random(), () -> Instant.now().isBefore(end));
+ }
+
+ static InputStream createDummyInputStream(int payloadSize, Random random, BooleanSupplier hasNext) {
+ int idSize = 8;
+ String template = String.format("{ \"put\": \"id:test:test::%s\", \"fields\": { \"test\": \"%s\" } }\n",
+ IntStream.range(0, idSize).mapToObj(__ -> "*").collect(joining()),
+ IntStream.range(0, payloadSize).mapToObj(__ -> "#").collect(joining()));
+ byte[] buffer = template.getBytes(StandardCharsets.UTF_8);
+ int idIndex = template.indexOf('*');
+ int dataIndex = template.indexOf('#');
+
+ return new SequenceInputStream(new Enumeration<InputStream>() {
+ @Override public boolean hasMoreElements() {
+ return hasNext.getAsBoolean();
+ }
+ @Override public InputStream nextElement() {
+ for (int i = 0; i < idSize; i++) buffer[ idIndex + i] = (byte) ('a' + (random.nextInt(26)));
+ for (int i = 0; i < payloadSize; i++) buffer[dataIndex + i] = (byte) ('a' + (random.nextInt(26)));
+ return new ByteArrayInputStream(buffer);
+ }
+ });
+ }
+
+} \ No newline at end of file
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 201a85ed09d..073ea4a58db 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.impl.CliArguments.CliArgumentsException;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
@@ -63,16 +64,23 @@ class CliArgumentsTest {
}
@Test
- void fails_on_conflicting_parameters() {
- CliArguments.CliArgumentsException exception = assertThrows(
- CliArguments.CliArgumentsException.class,
- () -> CliArguments.fromRawArgs(new String[] {"--endpoint=https://endpoint", "--file=/path/to/file", "--stdin"}));
- assertEquals("Either option 'file' or 'stdin' must be specified", exception.getMessage());
+ 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"}))
+ .getMessage());
- exception = assertThrows(
- CliArguments.CliArgumentsException.class,
- () -> CliArguments.fromRawArgs(new String[] {"--endpoint=https://endpoint"}));
- assertEquals("Either option 'file' or 'stdin' must be specified", exception.getMessage());
+ assertEquals("Exactly one of 'file' and 'stdin' must be specified",
+ assertThrows(CliArgumentsException.class,
+ () -> 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"}))
+ .getMessage());
+
+ CliArguments.fromRawArgs(new String[] {"--endpoint=foo", "--speed-test"});
}
@Test
diff --git a/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliClientTest.java b/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliClientTest.java
new file mode 100644
index 00000000000..44dbf62966e
--- /dev/null
+++ b/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliClientTest.java
@@ -0,0 +1,31 @@
+package ai.vespa.feed.client.impl;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author jonmv
+ */
+class CliClientTest {
+
+ @Test
+ void testDummyStream() throws IOException {
+ AtomicInteger count = new AtomicInteger(3);
+ InputStream in = CliClient.createDummyInputStream(4, new Random(0), () -> count.decrementAndGet() >= 0);
+ byte[] buffer = new byte[1 << 20];
+ int offset = 0, read;
+ while ((read = in.read(buffer, offset, buffer.length - offset)) >= 0) offset += read;
+ assertEquals("{ \"put\": \"id:test:test::ssxvnjhp\", \"fields\": { \"test\": \"dqdx\" } }\n" +
+ "{ \"put\": \"id:test:test::vcrastvy\", \"fields\": { \"test\": \"bcwv\" } }\n" +
+ "{ \"put\": \"id:test:test::mgnykrxv\", \"fields\": { \"test\": \"zxkg\" } }\n",
+ new String(buffer, 0, offset, StandardCharsets.UTF_8));
+ }
+
+}
diff --git a/vespa-feed-client-cli/src/test/resources/help.txt b/vespa-feed-client-cli/src/test/resources/help.txt
index 66d7c3521c2..40fba062f9b 100644
--- a/vespa-feed-client-cli/src/test/resources/help.txt
+++ b/vespa-feed-client-cli/src/test/resources/help.txt
@@ -10,8 +10,9 @@ Vespa feed client
connections
--disable-ssl-hostname-verification Disable SSL hostname
verification
- --dryrun Enable dryrun mode where each
- operation succeeds after 1ms
+ --dryrun Let each operation succeed after
+ 1ms, instead of sending it
+ across the network
--endpoint <arg> URI to feed endpoint
--file <arg> Path to feed file in JSON format
--header <arg> HTTP header on the form 'Name:
@@ -34,8 +35,17 @@ Vespa feed client
failure
--silent Disable periodic status printing
to stderr
+ --speed-test Perform a network speed test,
+ where the server immediately
+ responds to each feed operation
+ with a successful response
--stdin Read JSON input from standard
input
+ --test-payload-size <arg> Document JSON test payload size
+ in bytes, for use with
+ --speed-test; requires --file
+ and -stdin to not be set;
+ default is 1024
--timeout <arg> Feed operation timeout (in
seconds)
--trace <arg> The trace level of network
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 134ad464618..c70cb7cd850 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
@@ -49,6 +49,7 @@ public class FeedClientBuilderImpl implements FeedClientBuilder {
Collection<X509Certificate> caCertificates;
boolean benchmark = true;
boolean dryrun = false;
+ boolean speedTest = false;
URI proxy;
@@ -170,6 +171,12 @@ public class FeedClientBuilderImpl implements FeedClientBuilder {
return this;
}
+ @Override
+ public FeedClientBuilder setSpeedTest(boolean enabled) {
+ this.speedTest = enabled;
+ return this;
+ }
+
/**
* Overrides JVM default SSL truststore
* @param caCertificatesFile Path to PEM encoded file containing trusted certificates
diff --git a/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/HttpFeedClient.java b/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/HttpFeedClient.java
index c136d697a0b..9037c2377bb 100644
--- a/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/HttpFeedClient.java
+++ b/vespa-feed-client/src/main/java/ai/vespa/feed/client/impl/HttpFeedClient.java
@@ -41,6 +41,7 @@ class HttpFeedClient implements FeedClient {
private final Map<String, Supplier<String>> requestHeaders;
private final RequestStrategy requestStrategy;
private final AtomicBoolean closed = new AtomicBoolean();
+ private final boolean speedTest;
HttpFeedClient(FeedClientBuilderImpl builder) throws IOException {
this(builder, new HttpRequestStrategy(builder));
@@ -49,6 +50,7 @@ class HttpFeedClient implements FeedClient {
HttpFeedClient(FeedClientBuilderImpl builder, RequestStrategy requestStrategy) {
this.requestHeaders = new HashMap<>(builder.requestHeaders);
this.requestStrategy = requestStrategy;
+ this.speedTest = builder.speedTest;
}
@Override
@@ -90,7 +92,7 @@ class HttpFeedClient implements FeedClient {
throw new IllegalStateException("Client is closed");
HttpRequest request = new HttpRequest(method,
- getPath(documentId) + getQuery(params),
+ getPath(documentId) + getQuery(params, speedTest),
requestHeaders,
operationJson == null ? null : operationJson.getBytes(UTF_8), // TODO: make it bytes all the way?
params.timeout().orElse(null));
@@ -217,13 +219,14 @@ class HttpFeedClient implements FeedClient {
}
}
- static String getQuery(OperationParameters params) {
+ static String getQuery(OperationParameters params, boolean speedTest) {
StringJoiner query = new StringJoiner("&", "?", "").setEmptyValue("");
if (params.createIfNonExistent()) query.add("create=true");
params.testAndSetCondition().ifPresent(condition -> query.add("condition=" + encode(condition)));
params.timeout().ifPresent(timeout -> query.add("timeout=" + timeout.toMillis() + "ms"));
params.route().ifPresent(route -> query.add("route=" + encode(route)));
params.tracelevel().ifPresent(tracelevel -> query.add("tracelevel=" + tracelevel));
+ if (speedTest) query.add("speedTest=true");
return query.toString();
}
diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java
index 7f940e5695a..26ef157c5ae 100644
--- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java
+++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java
@@ -20,7 +20,6 @@ import com.yahoo.document.DocumentUpdate;
import com.yahoo.document.FixedBucketSpaces;
import com.yahoo.document.TestAndSetCondition;
import com.yahoo.document.config.DocumentmanagerConfig;
-import com.yahoo.document.fieldset.AllFields;
import com.yahoo.document.fieldset.DocIdOnly;
import com.yahoo.document.fieldset.DocumentOnly;
import com.yahoo.document.idstring.IdIdString;
@@ -169,6 +168,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler {
private static final String STREAM = "stream";
private static final String SLICES = "slices";
private static final String SLICE_ID = "sliceId";
+ private static final String DRY_RUN = "dryRun";
private final Clock clock;
private final Duration handlerTimeout;
@@ -442,6 +442,11 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler {
private ContentChannel postDocument(HttpRequest request, DocumentPath path, ResponseHandler rawHandler) {
ResponseHandler handler = new MeasuringResponseHandler(rawHandler, com.yahoo.documentapi.metrics.DocumentOperationType.PUT, clock.instant());
+ if (getProperty(request, DRY_RUN, booleanParser).orElse(false)) {
+ handleFeedOperation(path, handler, new com.yahoo.documentapi.Response(-1));
+ return ignoredContent;
+ }
+
return new ForwardingContentChannel(in -> {
enqueueAndDispatch(request, handler, () -> {
DocumentPut put = parser.parsePut(in, path.id().toString());
@@ -459,6 +464,11 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler {
private ContentChannel putDocument(HttpRequest request, DocumentPath path, ResponseHandler rawHandler) {
ResponseHandler handler = new MeasuringResponseHandler(rawHandler, com.yahoo.documentapi.metrics.DocumentOperationType.UPDATE, clock.instant());
+ if (getProperty(request, DRY_RUN, booleanParser).orElse(false)) {
+ handleFeedOperation(path, handler, new com.yahoo.documentapi.Response(-1));
+ return ignoredContent;
+ }
+
return new ForwardingContentChannel(in -> {
enqueueAndDispatch(request, handler, () -> {
DocumentUpdate update = parser.parseUpdate(in, path.id().toString());
@@ -477,6 +487,11 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler {
private ContentChannel deleteDocument(HttpRequest request, DocumentPath path, ResponseHandler rawHandler) {
ResponseHandler handler = new MeasuringResponseHandler(rawHandler, com.yahoo.documentapi.metrics.DocumentOperationType.REMOVE, clock.instant());
+ if (getProperty(request, DRY_RUN, booleanParser).orElse(false)) {
+ handleFeedOperation(path, handler, new com.yahoo.documentapi.Response(-1));
+ return ignoredContent;
+ }
+
enqueueAndDispatch(request, handler, () -> {
DocumentRemove remove = new DocumentRemove(path.id());
getProperty(request, CONDITION).map(TestAndSetCondition::new).ifPresent(remove::setCondition);
@@ -1062,19 +1077,19 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler {
private void updatePutMetrics(Outcome outcome) {
switch (outcome) {
- case SUCCESS: metric.add(MetricNames.SUCCEEDED, 1, null); break;
- case CONDITION_FAILED: metric.add(MetricNames.CONDITION_NOT_MET, 1, null); break;
- default: metric.add(MetricNames.FAILED, 1, null); break;
+ case SUCCESS -> metric.add(MetricNames.SUCCEEDED, 1, null);
+ case CONDITION_FAILED -> metric.add(MetricNames.CONDITION_NOT_MET, 1, null);
+ default -> metric.add(MetricNames.FAILED, 1, null);
}
}
private void updateUpdateMetrics(Outcome outcome, boolean create) {
if (create && outcome == Outcome.NOT_FOUND) outcome = Outcome.SUCCESS; // >_<
switch (outcome) {
- case SUCCESS: metric.add(MetricNames.SUCCEEDED, 1, null); break;
- case NOT_FOUND: metric.add(MetricNames.NOT_FOUND, 1, null); break;
- case CONDITION_FAILED: metric.add(MetricNames.CONDITION_NOT_MET, 1, null); break;
- default: metric.add(MetricNames.FAILED, 1, null); break;
+ case SUCCESS -> metric.add(MetricNames.SUCCEEDED, 1, null);
+ case NOT_FOUND -> metric.add(MetricNames.NOT_FOUND, 1, null);
+ case CONDITION_FAILED -> metric.add(MetricNames.CONDITION_NOT_MET, 1, null);
+ default -> metric.add(MetricNames.FAILED, 1, null);
}
}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java
index 74a86b6a7b7..baedae5f580 100644
--- a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java
@@ -524,6 +524,67 @@ public class DocumentV1ApiTest {
"}", response.readAll());
assertEquals(404, response.getStatus());
+ // POST with speedTest=true returns an immediate OK response
+ access.session.expect((__, ___) -> {
+ fail("Should not cause an actual feed operation");
+ return null;
+ });
+ response = driver.sendRequest("http://localhost/document/v1/space/music/number/1/two?dryRun=true", POST,
+ "NOT JSON, NOT PARSED");
+ assertSameJson("{" +
+ " \"pathId\": \"/document/v1/space/music/number/1/two\"," +
+ " \"id\": \"id:space:music:n=1:two\"" +
+ "}", response.readAll());
+ assertEquals(200, response.getStatus());
+
+ // PUT with speedTest=true returns an immediate OK response
+ access.session.expect((__, ___) -> {
+ fail("Should not cause an actual feed operation");
+ return null;
+ });
+ response = driver.sendRequest("http://localhost/document/v1/space/music/number/1/two?dryRun=true", PUT,
+ "NOT JSON, NOT PARSED");
+ assertSameJson("{" +
+ " \"pathId\": \"/document/v1/space/music/number/1/two\"," +
+ " \"id\": \"id:space:music:n=1:two\"" +
+ "}", response.readAll());
+ assertEquals(200, response.getStatus());
+
+ // DELETE with speedTest=true returns an immediate OK response
+ access.session.expect((__, ___) -> {
+ fail("Should not cause an actual feed operation");
+ return null;
+ });
+ response = driver.sendRequest("http://localhost/document/v1/space/music/number/1/two?dryRun=true", DELETE,
+ "NOT JSON, NOT PARSED");
+ assertSameJson("{" +
+ " \"pathId\": \"/document/v1/space/music/number/1/two\"," +
+ " \"id\": \"id:space:music:n=1:two\"" +
+ "}", response.readAll());
+ assertEquals(200, response.getStatus());
+
+ // PUT with a document update payload is a document update operation.
+ access.session.expect((update, parameters) -> {
+ DocumentUpdate expectedUpdate = new DocumentUpdate(doc3.getDataType(), doc3.getId());
+ expectedUpdate.addFieldUpdate(FieldUpdate.createAssign(doc3.getField("artist"), new StringFieldValue("Lisa Ekdahl")));
+ expectedUpdate.setCreateIfNonExistent(true);
+ assertEquals(expectedUpdate, update);
+ assertEquals(parameters(), parameters);
+ parameters.responseHandler().get().handleResponse(new UpdateResponse(0, true));
+ return new Result();
+ });
+ response = driver.sendRequest("http://localhost/document/v1/space/music/group/a/three?create=true&timeout=1e1s&dryRun=false", PUT,
+ "{" +
+ " \"fields\": {" +
+ " \"artist\": { \"assign\": \"Lisa Ekdahl\" }" +
+ " }" +
+ "}");
+ assertSameJson("{" +
+ " \"pathId\": \"/document/v1/space/music/group/a/three\"," +
+ " \"id\": \"id:space:music:g=a:three\"" +
+ "}", response.readAll());
+ assertEquals(200, response.getStatus());
+
// POST with a document payload is a document put operation.
access.session.expect((put, parameters) -> {
DocumentPut expectedPut = new DocumentPut(doc2);
@@ -566,28 +627,6 @@ public class DocumentV1ApiTest {
"}", response.readAll());
assertEquals(200, response.getStatus());
- // PUT with a document update payload is a document update operation.
- access.session.expect((update, parameters) -> {
- DocumentUpdate expectedUpdate = new DocumentUpdate(doc3.getDataType(), doc3.getId());
- expectedUpdate.addFieldUpdate(FieldUpdate.createAssign(doc3.getField("artist"), new StringFieldValue("Lisa Ekdahl")));
- expectedUpdate.setCreateIfNonExistent(true);
- assertEquals(expectedUpdate, update);
- assertEquals(parameters(), parameters);
- parameters.responseHandler().get().handleResponse(new UpdateResponse(0, true));
- return new Result();
- });
- response = driver.sendRequest("http://localhost/document/v1/space/music/group/a/three?create=true&timeout=1e1s", PUT,
- "{" +
- " \"fields\": {" +
- " \"artist\": { \"assign\": \"Lisa Ekdahl\" }" +
- " }" +
- "}");
- assertSameJson("{" +
- " \"pathId\": \"/document/v1/space/music/group/a/three\"," +
- " \"id\": \"id:space:music:g=a:three\"" +
- "}", response.readAll());
- assertEquals(200, response.getStatus());
-
// POST with no payload is a 400
access.session.expect((__, ___) -> { throw new AssertionError("Not supposed to happen"); });
response = driver.sendRequest("http://localhost/document/v1/space/music/number/1/two?condition=test%20it", POST, "");