summaryrefslogtreecommitdiffstats
path: root/vespa-feed-client-cli
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 /vespa-feed-client-cli
parent8be6039101a0c4006199c2946d8381c86428ec11 (diff)
Add --speedTest to feed client CLI, and dryRun to /doc/v1
Diffstat (limited to 'vespa-feed-client-cli')
-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
5 files changed, 159 insertions, 24 deletions
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