diff --git a/src/it/java/io/deephaven/benchmark/tests/compare/CompareTestRunner.java b/src/it/java/io/deephaven/benchmark/tests/compare/CompareTestRunner.java
index cc08561b..479a5ef7 100644
--- a/src/it/java/io/deephaven/benchmark/tests/compare/CompareTestRunner.java
+++ b/src/it/java/io/deephaven/benchmark/tests/compare/CompareTestRunner.java
@@ -8,7 +8,7 @@
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import io.deephaven.benchmark.api.Bench;
-import io.deephaven.benchmark.util.Exec;
+import io.deephaven.benchmark.controller.DeephavenDockerController;
import io.deephaven.benchmark.util.Filer;
/**
@@ -35,15 +35,6 @@ public CompareTestRunner(Object testInst) {
this.testInst = testInst;
}
- /**
- * Get the Bench API instance for this runner
- *
- * @return the Bench API instance
- */
- public Bench api() {
- return api;
- }
-
/**
* Download and place the given file into the environment Deephaven is running in. If the destination directory is
* specified as a relative path, the download file will be placed relative to the root of the virtual environment
@@ -365,7 +356,9 @@ void restartDocker() {
var api = Bench.create("# Docker Restart");
try {
api.setName("# Docker Restart");
- if (!Exec.restartDocker(api.property("docker.compose.file", ""), api.property("deephaven.addr", "")))
+ var controller = new DeephavenDockerController(api.property("docker.compose.file", ""),
+ api.property("deephaven.addr", ""));
+ if (!controller.restartService())
return;
} finally {
api.close();
@@ -381,7 +374,8 @@ void restartDocker(int heapGigs) {
if (dockerComposeFile.isBlank() || deephavenHostPort.isBlank())
return;
dockerComposeFile = makeHeapAdjustedDockerCompose(dockerComposeFile, heapGigs);
- Exec.restartDocker(dockerComposeFile, deephavenHostPort);
+ var controller = new DeephavenDockerController(dockerComposeFile, deephavenHostPort);
+ controller.restartService();
} finally {
api.close();
}
diff --git a/src/it/java/io/deephaven/benchmark/tests/experimental/ExperimentalTestRunner.java b/src/it/java/io/deephaven/benchmark/tests/experimental/ExperimentalTestRunner.java
index 7a48f545..ff1c6c9a 100644
--- a/src/it/java/io/deephaven/benchmark/tests/experimental/ExperimentalTestRunner.java
+++ b/src/it/java/io/deephaven/benchmark/tests/experimental/ExperimentalTestRunner.java
@@ -6,7 +6,8 @@
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import io.deephaven.benchmark.api.Bench;
-import io.deephaven.benchmark.util.Exec;
+import io.deephaven.benchmark.controller.Controller;
+import io.deephaven.benchmark.controller.DeephavenDockerController;
/**
* A wrapper for the Bench api that allows the running of small (single-operation) tests without requiring the
@@ -21,14 +22,14 @@ public class ExperimentalTestRunner {
final Object testInst;
private long scaleRowCount;
private Bench api;
+ private Controller controller;
private String sourceTable = "source";
private Map supportTables = new LinkedHashMap<>();
private List supportQueries = new ArrayList<>();
public ExperimentalTestRunner(Object testInst) {
this.testInst = testInst;
- this.api = initialize(testInst);
- this.scaleRowCount = api.propertyAsIntegral("scale.row.count", "100000");
+ initialize(testInst);
}
/**
@@ -219,8 +220,11 @@ Bench initialize(Object testInst) {
from deephaven.parquet import read
""";
- Bench api = Bench.create(testInst);
- restartDocker(api);
+ this.api = Bench.create(testInst);
+ this.controller = new DeephavenDockerController(api.property("docker.compose.file", ""),
+ api.property("deephaven.addr", ""));
+ this.scaleRowCount = api.propertyAsIntegral("scale.row.count", "100000");
+ controller.restartService();
api.query(query).execute();
return api;
}
@@ -229,12 +233,6 @@ String listStr(String... values) {
return String.join(", ", Arrays.stream(values).map(c -> "'" + c + "'").toList());
}
- void restartDocker(Bench api) {
- var dockerComposeFile = api.property("docker.compose.file", "");
- var deephavenHostPort = api.property("deephaven.addr", "");
- Exec.restartDocker(dockerComposeFile, deephavenHostPort);
- }
-
void generateQuotesTable(long rowCount) {
api.table("quotes_g")
.add("Date", "string", "2023-01-04")
diff --git a/src/it/java/io/deephaven/benchmark/tests/experimental/mergescale/ScaleTestRunner.java b/src/it/java/io/deephaven/benchmark/tests/experimental/mergescale/ScaleTestRunner.java
index 24797726..e3addc31 100644
--- a/src/it/java/io/deephaven/benchmark/tests/experimental/mergescale/ScaleTestRunner.java
+++ b/src/it/java/io/deephaven/benchmark/tests/experimental/mergescale/ScaleTestRunner.java
@@ -3,8 +3,8 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import io.deephaven.benchmark.api.Bench;
+import io.deephaven.benchmark.controller.DeephavenDockerController;
import io.deephaven.benchmark.metric.Metrics;
-import io.deephaven.benchmark.util.Exec;
import io.deephaven.benchmark.util.Timer;
/**
@@ -82,7 +82,8 @@ void runTest(String testName, String tableName, long baseRowCount, long rowCount
void restartDocker(Bench api) {
var timer = api.timer();
- if (!Exec.restartDocker(api.property("docker.compose.file", ""), api.property("deephaven.addr", "")))
+ var controller = new DeephavenDockerController(api.property("docker.compose.file", ""), api.property("deephaven.addr", ""));
+ if (!controller.restartService())
return;
var metrics = new Metrics(Timer.now(), "test-runner", "setup", "docker");
metrics.set("restart", timer.duration().toMillis(), "standard");
diff --git a/src/it/java/io/deephaven/benchmark/tests/standard/StandardTestRunner.java b/src/it/java/io/deephaven/benchmark/tests/standard/StandardTestRunner.java
index 6f5bcc77..0b73d540 100644
--- a/src/it/java/io/deephaven/benchmark/tests/standard/StandardTestRunner.java
+++ b/src/it/java/io/deephaven/benchmark/tests/standard/StandardTestRunner.java
@@ -7,8 +7,9 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import io.deephaven.benchmark.api.Bench;
+import io.deephaven.benchmark.controller.Controller;
+import io.deephaven.benchmark.controller.DeephavenDockerController;
import io.deephaven.benchmark.metric.Metrics;
-import io.deephaven.benchmark.util.Exec;
import io.deephaven.benchmark.util.Timer;
/**
@@ -19,12 +20,13 @@
* compact and readable, not to cover every possible case. Standard query API code can be used in conjunction as long as
* conventions are followed (ex. main file is "source")
*/
-public class StandardTestRunner {
+final public class StandardTestRunner {
final Object testInst;
final List setupQueries = new ArrayList<>();
final List supportTables = new ArrayList<>();
private String mainTable = "source";
private Bench api;
+ private Controller controller;
private long scaleRowCount;
private int staticFactor = 1;
private int incFactor = 1;
@@ -32,8 +34,7 @@ public class StandardTestRunner {
public StandardTestRunner(Object testInst) {
this.testInst = testInst;
- this.api = initialize(testInst);
- this.scaleRowCount = api.propertyAsIntegral("scale.row.count", "100000");
+ initialize(testInst);
}
/**
@@ -174,9 +175,11 @@ Result runStaticTest(String name, String operation, String read, String... loadC
garbage_collect()
bench_api_metrics_snapshot()
+ print('${logOperationBegin}')
begin_time = time.perf_counter_ns()
result = ${operation}
end_time = time.perf_counter_ns()
+ print('${logOperationEnd}')
bench_api_metrics_snapshot()
standard_metrics = bench_api_metrics_collect()
@@ -202,6 +205,7 @@ Result runIncTest(String name, String operation, String read, String... loadColu
garbage_collect()
bench_api_metrics_snapshot()
+ print('${logOperationBegin}')
begin_time = time.perf_counter_ns()
result = ${operation}
source_filter.start()
@@ -210,6 +214,7 @@ Result runIncTest(String name, String operation, String read, String... loadColu
get_exec_ctx().update_graph.j_update_graph.requestRefresh()
source_filter.waitForCompletion()
end_time = time.perf_counter_ns()
+ print('${logOperationEnd}')
bench_api_metrics_snapshot()
standard_metrics = bench_api_metrics_collect()
@@ -224,14 +229,18 @@ Result runIncTest(String name, String operation, String read, String... loadColu
Result runTest(String name, String query, String operation, String read, String... loadColumns) {
if (api.isClosed())
- api = initialize(testInst);
+ initialize(testInst);
api.setName(name);
+ var logBeginMarker = getLogSnippet("Begin", name);
+ var logEndMarker = getLogSnippet("End", name);
query = query.replace("${readTable}", read);
query = query.replace("${mainTable}", mainTable);
query = query.replace("${loadSupportTables}", loadSupportTables());
query = query.replace("${loadColumns}", listStr(loadColumns));
query = query.replace("${setupQueries}", String.join("\n", setupQueries));
query = query.replace("${operation}", operation);
+ query = query.replace("${logOperationBegin}", logBeginMarker);
+ query = query.replace("${logOperationEnd}", logEndMarker);
try {
var result = new AtomicReference();
@@ -252,6 +261,7 @@ Result runTest(String name, String query, String operation, String read, String.
api.result().test("deephaven-engine", result.get().elapsedTime(), result.get().loadedRowCount());
return result.get();
} finally {
+ addDockerLog(api, logBeginMarker, logEndMarker);
api.close();
}
}
@@ -265,7 +275,12 @@ String loadSupportTables() {
.collect(Collectors.joining(""));
}
- Bench initialize(Object testInst) {
+ String getLogSnippet(String beginEnd, String name) {
+ beginEnd = "BENCH_OPERATION_" + beginEnd.toUpperCase();
+ return String.join(",", "<<<<< " + beginEnd, name, beginEnd + " >>>>>");
+ }
+
+ void initialize(Object testInst) {
var query = """
import time
from deephaven import new_table, empty_table, garbage_collect, merge
@@ -273,15 +288,28 @@ Bench initialize(Object testInst) {
from deephaven.parquet import read
""";
- Bench api = Bench.create(testInst);
- restartDocker(api);
+ this.api = Bench.create(testInst);
+ this.controller = new DeephavenDockerController(api.property("docker.compose.file", ""),
+ api.property("deephaven.addr", ""));
+ this.scaleRowCount = api.propertyAsIntegral("scale.row.count", "100000");
+ restartDocker();
api.query(query).execute();
- return api;
}
- void restartDocker(Bench api) {
+ void addDockerLog(Bench api, String beginMarker, String endMarker) {
+ var timer = api.timer();
+ var logText = controller.getLog();
+ if (logText.isBlank())
+ return;
+ api.log().add("deephaven-engine", logText);
+ var metrics = new Metrics(Timer.now(), "test-runner", "teardown", "docker");
+ metrics.set("log", timer.duration().toMillis(), "standard");
+ api.metrics().add(metrics);
+ }
+
+ void restartDocker() {
var timer = api.timer();
- if (!Exec.restartDocker(api.property("docker.compose.file", ""), api.property("deephaven.addr", "")))
+ if (!controller.restartService())
return;
var metrics = new Metrics(Timer.now(), "test-runner", "setup", "docker");
metrics.set("restart", timer.duration().toMillis(), "standard");
diff --git a/src/it/java/io/deephaven/benchmark/tests/standard/file/FileTestRunner.java b/src/it/java/io/deephaven/benchmark/tests/standard/file/FileTestRunner.java
index a565337a..10b6a199 100644
--- a/src/it/java/io/deephaven/benchmark/tests/standard/file/FileTestRunner.java
+++ b/src/it/java/io/deephaven/benchmark/tests/standard/file/FileTestRunner.java
@@ -5,8 +5,9 @@
import java.util.Arrays;
import java.util.stream.Collectors;
import io.deephaven.benchmark.api.Bench;
+import io.deephaven.benchmark.controller.Controller;
+import io.deephaven.benchmark.controller.DeephavenDockerController;
import io.deephaven.benchmark.metric.Metrics;
-import io.deephaven.benchmark.util.Exec;
import io.deephaven.benchmark.util.Timer;
/**
@@ -15,7 +16,8 @@
class FileTestRunner {
final String parquetCfg = "max_dictionary_keys=1048576, max_dictionary_size=1048576, target_page_size=65536";
final Object testInst;
- final Bench api;
+ private Bench api;
+ private Controller controller;
private double rowCountFactor = 1;
private int scaleFactor = 1;
private long scaleRowCount;
@@ -23,8 +25,7 @@ class FileTestRunner {
FileTestRunner(Object testInst) {
this.testInst = testInst;
- this.api = initialize(testInst);
- this.scaleRowCount = api.propertyAsIntegral("scale.row.count", "100000");
+ initialize(testInst);
}
/**
@@ -248,8 +249,11 @@ private Bench initialize(Object testInst) {
from deephaven import dtypes as dht
""";
- Bench api = Bench.create(testInst);
- restartDocker(api);
+ this.api = Bench.create(testInst);
+ this.controller = new DeephavenDockerController(api.property("docker.compose.file", ""),
+ api.property("deephaven.addr", ""));
+ this.scaleRowCount = api.propertyAsIntegral("scale.row.count", "100000");
+ restartDocker();
api.query(query).execute();
return api;
}
@@ -259,9 +263,9 @@ private Bench initialize(Object testInst) {
*
* @param api the Bench API for this test runner.
*/
- private void restartDocker(Bench api) {
+ private void restartDocker() {
var timer = api.timer();
- if (!Exec.restartDocker(api.property("docker.compose.file", ""), api.property("deephaven.addr", "")))
+ if (!controller.restartService())
return;
var metrics = new Metrics(Timer.now(), "test-runner", "setup", "docker");
metrics.set("restart", timer.duration().toMillis(), "standard");
diff --git a/src/it/java/io/deephaven/benchmark/tests/standard/kafka/KafkaTestRunner.java b/src/it/java/io/deephaven/benchmark/tests/standard/kafka/KafkaTestRunner.java
index 5a99f3a8..e20c9db3 100644
--- a/src/it/java/io/deephaven/benchmark/tests/standard/kafka/KafkaTestRunner.java
+++ b/src/it/java/io/deephaven/benchmark/tests/standard/kafka/KafkaTestRunner.java
@@ -6,8 +6,9 @@
import java.nio.file.Paths;
import java.time.Duration;
import io.deephaven.benchmark.api.Bench;
+import io.deephaven.benchmark.controller.Controller;
+import io.deephaven.benchmark.controller.DeephavenDockerController;
import io.deephaven.benchmark.metric.Metrics;
-import io.deephaven.benchmark.util.Exec;
import io.deephaven.benchmark.util.Filer;
import io.deephaven.benchmark.util.Timer;
@@ -19,6 +20,7 @@
class KafkaTestRunner {
final Object testInst;
final Bench api;
+ final Controller controller;
private long rowCount;
private int colCount;
private String colType;
@@ -32,6 +34,8 @@ class KafkaTestRunner {
KafkaTestRunner(Object testInst) {
this.testInst = testInst;
this.api = Bench.create(testInst);
+ this.controller = new DeephavenDockerController(api.property("docker.compose.file", ""),
+ api.property("deephaven.addr", ""));
}
/**
@@ -41,7 +45,7 @@ class KafkaTestRunner {
* @param deephavenHeapGigs the number of gigabytes to use for Deephave max heap
*/
void restartWithHeap(int deephavenHeapGigs) {
- restartDocker(api, deephavenHeapGigs);
+ restartDocker(deephavenHeapGigs);
}
/**
@@ -179,14 +183,14 @@ private String getResultTableSize(String operation) {
return "result.size";
}
- private void restartDocker(Bench api, int heapGigs) {
+ private void restartDocker(int heapGigs) {
String dockerComposeFile = api.property("docker.compose.file", "");
String deephavenHostPort = api.property("deephaven.addr", "");
if (dockerComposeFile.isBlank() || deephavenHostPort.isBlank())
return;
dockerComposeFile = makeHeapAdjustedDockerCompose(dockerComposeFile, heapGigs);
var timer = api.timer();
- Exec.restartDocker(dockerComposeFile, deephavenHostPort);
+ controller.restartService();
var metrics = new Metrics(Timer.now(), "test-runner", "setup", "docker");
metrics.set("restart", timer.duration().toMillis(), "standard");
api.metrics().add(metrics);
diff --git a/src/main/java/io/deephaven/benchmark/api/Bench.java b/src/main/java/io/deephaven/benchmark/api/Bench.java
index f890de0d..ea1c5877 100644
--- a/src/main/java/io/deephaven/benchmark/api/Bench.java
+++ b/src/main/java/io/deephaven/benchmark/api/Bench.java
@@ -41,6 +41,7 @@ static public Bench create(Object testInst) {
final BenchMetrics metrics;
final BenchPlatform platform;
final QueryLog queryLog;
+ final BenchLog runLog;
final List> futures = new ArrayList<>();
final List closeables = new ArrayList<>();
private boolean isClosed = false;
@@ -51,6 +52,7 @@ static public Bench create(Object testInst) {
this.metrics = new BenchMetrics(outputDir);
this.platform = new BenchPlatform(outputDir);
this.queryLog = new QueryLog(outputDir, testInst);
+ this.runLog = new BenchLog(outputDir, testInst);
}
/**
@@ -64,6 +66,7 @@ public void setName(String name) {
this.result.setName(name);
this.metrics.setName(name);
this.queryLog.setName(name);
+ this.runLog.setName(name);
}
/**
@@ -179,6 +182,15 @@ public BenchPlatform platform() {
return platform;
}
+ /**
+ * Get the metrics for this Benchmark instance (e.g. test) used for collecting metric values
+ *
+ * @return the metrics instance
+ */
+ public BenchLog log() {
+ return runLog;
+ }
+
/**
* Has this Bench api instance been closed along with all connectors and files opened since creating the instance
*
@@ -206,6 +218,7 @@ public void close() {
result.commit();
metrics.commit();
platform.commit();
+ runLog.close();
queryLog.close();
}
diff --git a/src/main/java/io/deephaven/benchmark/api/BenchLog.java b/src/main/java/io/deephaven/benchmark/api/BenchLog.java
new file mode 100644
index 00000000..3b4e2434
--- /dev/null
+++ b/src/main/java/io/deephaven/benchmark/api/BenchLog.java
@@ -0,0 +1,81 @@
+/* Copyright (c) 2022-2023 Deephaven Data Labs and Patent Pending */
+package io.deephaven.benchmark.api;
+
+import static java.nio.file.StandardOpenOption.*;
+import java.io.BufferedWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Contains log data from the target where the benchmarks are run (e.g. Deephaven Engine). There is one log per test
+ * Class, and test name markers are used as beginning and end of the log lines for each benchmark in the log file.
+ */
+final public class BenchLog {
+ final Class> testClass;
+ final Path parent;
+ final Path logFile;
+ private String name = null;
+ private boolean isClosed = false;
+
+ /**
+ * Initialize the log according to the test class it's tracking
+ *
+ * @param parent the parent directory of the log
+ * @param testClass the test class instance being tracked
+ */
+ BenchLog(Path parent, Class> testClass) {
+ this.testClass = testClass;
+ this.parent = parent;
+ this.logFile = getLogFile(parent, testClass);
+ }
+
+ /**
+ * Mark the end of the run log for the currents test class
+ */
+ public void close() {
+ if (isClosed)
+ return;
+ isClosed = true;
+ }
+
+ /**
+ * Add the log info that was collected during the test run
+ *
+ * @param info the log info (i.e. the docker log)
+ */
+ public void add(String origin, String info) {
+ if (name == null)
+ throw new RuntimeException("Set a test name before logging a test run");
+ if (isClosed)
+ throw new RuntimeException("Attempted to log to closed log");
+ write("\n<<<<< BENCH_TEST," + origin + "," + name + ",BENCH_TEST >>>>>\n\n" + info.trim() + '\n');
+ }
+
+ /**
+ * Set the name of the current test. This will be used at the beginning and end of the test's log info.
+ *
+ * @param name the name of the current log section (i.e. test name)
+ */
+ void setName(String name) {
+ this.name = name;
+ }
+
+ private void write(String text) {
+ try (BufferedWriter out = Files.newBufferedWriter(logFile, CREATE, APPEND)) {
+ out.write(text);
+ } catch (Exception ex) {
+ throw new RuntimeException("Failed to write to the run log: " + logFile, ex);
+ }
+ }
+
+ static Path getLogFile(Path parent, Class> testClass) {
+ Path logFile = parent.resolve("test-logs/" + testClass.getName() + ".run.log");
+ try {
+ Files.createDirectories(logFile.getParent());
+ return logFile;
+ } catch (Exception ex) {
+ throw new RuntimeException("Failed to create run log directory" + logFile.getParent(), ex);
+ }
+ }
+
+}
diff --git a/src/main/java/io/deephaven/benchmark/api/BenchQuery.java b/src/main/java/io/deephaven/benchmark/api/BenchQuery.java
index 14f9a5c6..11d3c066 100644
--- a/src/main/java/io/deephaven/benchmark/api/BenchQuery.java
+++ b/src/main/java/io/deephaven/benchmark/api/BenchQuery.java
@@ -86,7 +86,6 @@ public void close() {
if (!session.getUsedVariableNames().isEmpty()) {
String logic = String.join("=None; ", session.getUsedVariableNames()) + "=None\n";
- logic += "from deephaven import garbage_collect; garbage_collect()\n";
executeBarrageQuery(logic);
}
diff --git a/src/main/java/io/deephaven/benchmark/controller/Controller.java b/src/main/java/io/deephaven/benchmark/controller/Controller.java
new file mode 100644
index 00000000..95e263ea
--- /dev/null
+++ b/src/main/java/io/deephaven/benchmark/controller/Controller.java
@@ -0,0 +1,50 @@
+/* Copyright (c) 2022-2023 Deephaven Data Labs and Patent Pending */
+package io.deephaven.benchmark.controller;
+
+/**
+ * Represents a mechanism that can manage the external service (e.g. Deephaven Engine) the benchmarks are running
+ * against. This includes; start, stop, logging, etc.
+ *
+ * Note: For now, this is not part of the Bench API but is used by runners that wrap the Bench API to provide normalized
+ * behavior or generated data reuse.
+ */
+public interface Controller {
+ /**
+ * Start the service according to the following contract:
+ *
+ * - If a service definition (e.g. docker-compose.yml) is not supplied, do nothing
+ * - If a service definition is supplied, stop the existing service and clear state (e.g. logs)
+ * - If a service definition is supplied, wait for the service to be in a usable state
+ *
+ *
+ * @return true if the service is running, otherwise false
+ */
+ public boolean startService();
+
+ /**
+ * Stop the service according to the follow contract:
+ *
+ * - If a service definition (e.g. docker-compose.yml) is not supplied, do nothing
+ * - If a service definition is supplied, stop the service and clear state (e.g. logs)
+ *
+ *
+ * @return true if the service definition is specified, otherwise false
+ */
+ public boolean stopService();
+
+ /**
+ * Stop the service, cleanup state, and start it. Implementors can simply call stopService
followed by
+ * startService
if desired.
+ *
+ * @return true if the service restarted, otherwise false
+ */
+ public boolean restartService();
+
+ /**
+ * Get the available log from the service. Results will vary depending on when the log state was last cleared.
+ *
+ * @return the available log from the service
+ */
+ public String getLog();
+
+}
diff --git a/src/main/java/io/deephaven/benchmark/controller/DeephavenDockerController.java b/src/main/java/io/deephaven/benchmark/controller/DeephavenDockerController.java
new file mode 100644
index 00000000..6e6151d3
--- /dev/null
+++ b/src/main/java/io/deephaven/benchmark/controller/DeephavenDockerController.java
@@ -0,0 +1,168 @@
+/* Copyright (c) 2022-2023 Deephaven Data Labs and Patent Pending */
+package io.deephaven.benchmark.controller;
+
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import io.deephaven.benchmark.util.Exec;
+import io.deephaven.benchmark.util.Threads;
+
+/**
+ * A Controller
implementation that handes docker start, stop and logging through
+ * docker compose
calls. This implementation does not handle remote docker calls and requires that the
+ * service be on the same system as the controller.
+ */
+public class DeephavenDockerController implements Controller {
+ final String composePropPath;
+ final String httpHostPort;
+ final Path workDir;
+
+ /**
+ * Make a Deephaven Controller
instance for starting/stopping a local instance of Deephaven.
+ *
+ * @param composePath the path to the docker-compose.yml
file or null
+ * @param httpHostPort HTTP host and port for checking availability or null (ex deephaven.addr=localhost:10000)
+ */
+ public DeephavenDockerController(String composePath, String httpHostPort) {
+ this.composePropPath = (composePath == null) ? "" : composePath.trim();
+ this.httpHostPort = (httpHostPort == null) ? "" : httpHostPort.trim();
+ this.workDir = composePropPath.isBlank() ? Paths.get(".") : Paths.get(composePropPath).getParent();
+ }
+
+ /**
+ * Start the Deephaven service. If an existing Deephaven service is running, stop it first. If a docker compose file
+ * is not specified, do nothing.
+ *
+ * @return true if the service was started, otherwise false
+ */
+ @Override
+ public boolean startService() {
+ if (composePropPath.isBlank() || httpHostPort.isBlank())
+ return false;
+ var composeRunPath = getRunningComposePath();
+ if (composeRunPath != null)
+ exec("sudo docker compose -f " + composeRunPath + " down");
+ exec("sudo docker compose -f " + composePropPath + " up -d");
+ waitForEngineReady();
+ return true;
+ }
+
+ /**
+ * Stop the Deephaven service and remove the docker container. If no docker compose is specified, do nothing.
+ *
+ * @return true if the service was stopped, otherwise false
+ */
+ @Override
+ public boolean stopService() {
+ if (composePropPath.isBlank())
+ return false;
+ exec("sudo docker compose -f " + composePropPath + " down --timeout 0");
+ return true;
+ }
+
+ /**
+ * Stop the Deephaven service and start it.
+ *
+ * @return true if the service was started, otherwise false
+ */
+ @Override
+ public boolean restartService() {
+ stopService();
+ return startService();
+ }
+
+ /**
+ * Get the docker compose log since starting the Deephaven service.
+ *
+ * @return the text collected from docker compose log
+ */
+ @Override
+ public String getLog() {
+ var composePath = getRunningComposePath();
+ if (composePath != null)
+ return exec("sudo docker compose -f " + composePath + " logs");
+ return "";
+ }
+
+ void waitForEngineReady() {
+ long beginTime = System.currentTimeMillis();
+ while (System.currentTimeMillis() - beginTime < 10000) {
+ if (getUrlStatus("http://" + httpHostPort + "/ide/"))
+ return;
+ Threads.sleep(100);
+ }
+ throw new RuntimeException("Timed out waiting for Deephaven Engine to start");
+ }
+
+ boolean getUrlStatus(String uri) {
+ try {
+ var url = createUrl(uri);
+ var connect = url.openConnection();
+ if (!(connect instanceof HttpURLConnection))
+ return false;
+ var httpConn = (HttpURLConnection) connect;
+ var code = httpConn.getResponseCode();
+ httpConn.disconnect();
+ return (code == 200);
+ } catch (Exception ex) {
+ return false;
+ }
+ }
+
+ URL createUrl(String uri) {
+ try {
+ return new URL(uri);
+ } catch (MalformedURLException e) {
+ throw new RuntimeException("Bad URL: " + uri);
+ }
+ }
+
+ String getRunningComposePath() {
+ var dhContainerIds = getRunningContainerIds();
+ if (dhContainerIds.isEmpty())
+ return null;
+ var containerInfo = getContainerInfo(dhContainerIds.get(0));
+ return containerInfo.composePath;
+ }
+
+ List getRunningContainerIds() {
+ var out = exec("sudo docker ps");
+ return parseContainerIds(out);
+ }
+
+ ContainerInfo getContainerInfo(String containerId) {
+ var out = exec("sudo docker container inspect " + containerId);
+ return parseContainerInfo(out);
+ }
+
+ List parseContainerIds(String dockerPsStr) {
+ return dockerPsStr.lines().filter(s -> s.contains("/deephaven/server"))
+ .map(s -> s.replaceAll("^([^ \t]+)[ \t].*$", "$1")).toList();
+ }
+
+ ContainerInfo parseContainerInfo(String dockerInspectStr) {
+ var name = getPropValue(dockerInspectStr, "Name", "deephaven");
+ var composeUri = getPropValue(dockerInspectStr, "com.docker.compose.project.config_files", "compose");
+ return new ContainerInfo(name, composeUri);
+ }
+
+ String getPropValue(String props, String name, String containsVal) {
+ var matchName = "\"" + name + "\":";
+ var lines = props.lines().map(s -> s.trim()).filter(s -> s.startsWith(matchName) && s.contains(containsVal));
+ var matches = lines.toList();
+ if (matches.size() < 1)
+ throw new RuntimeException("Failed to find docker property: " + name + " containing: " + containsVal);
+ return matches.get(0).replaceAll("\"[,]?", "").split("[:]\s*")[1].trim();
+ }
+
+ String exec(String command) {
+ return Exec.exec(workDir, command);
+ }
+
+ record ContainerInfo(String name, String composePath) {
+ };
+
+}
diff --git a/src/main/java/io/deephaven/benchmark/util/Exec.java b/src/main/java/io/deephaven/benchmark/util/Exec.java
index f5379058..5ec41e0b 100644
--- a/src/main/java/io/deephaven/benchmark/util/Exec.java
+++ b/src/main/java/io/deephaven/benchmark/util/Exec.java
@@ -1,75 +1,47 @@
/* Copyright (c) 2022-2023 Deephaven Data Labs and Patent Pending */
package io.deephaven.benchmark.util;
-import java.net.*;
+import java.io.*;
+import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
/**
- * Utils for executing processes from the command line.
- *
- * Note: No effort has been made to make this secure
+ * A utility for executing a process on the command line. It will run any command, regardless of how dangerous.
*/
public class Exec {
- /**
- * Restart a docker container using docker compose
. If the given compose file property is blank skip.
- *
- * @param dockerComposeFile the path to the relevant docker-compose.yml
- * @param deephavenHostPort the host:port of the Deephaven service
- * @return true if attempted docker restart, otherwise false
- */
- static public boolean restartDocker(String dockerComposeFile, String deephavenHostPort) {
- if (dockerComposeFile.isBlank() || deephavenHostPort.isBlank())
- return false;
- exec("sudo docker compose -f " + dockerComposeFile + " down --timeout 0");
- exec("sudo docker compose -f " + dockerComposeFile + " up -d");
- long beginTime = System.currentTimeMillis();
- while (System.currentTimeMillis() - beginTime < 10000) {
- var status = getUrlStatus("http://" + deephavenHostPort + "/ide/");
- if (status)
- return true;
- Threads.sleep(100);
- }
- return false;
- }
-
/**
* Blindly execute a command in whatever shell Java decides is relevant. Throw exceptions on timeout, non-zero exit
* code, or other general failures.
*
* @param command the shell command to run
- * @return stdout and stderr separated by newlines
+ * @return the standard output of the process
*/
- static public int exec(String command) {
+ static public String exec(Path workingDir, String command) {
try {
- Process process = Runtime.getRuntime().exec(command);
+ Process process = Runtime.getRuntime().exec(command, null, workingDir.toFile());
+ var out = getStdout(process);
if (!process.waitFor(20, TimeUnit.SECONDS))
throw new RuntimeException("Timeout while running command: " + command);
if (process.exitValue() != 0)
throw new RuntimeException("Bad exit code " + process.exitValue() + " for command: " + command);
- return process.exitValue();
+ return out;
} catch (Exception ex) {
throw new RuntimeException("Failed to execute command: " + command, ex);
}
}
- static boolean getUrlStatus(String uri) {
- var url = createUrl(uri);
- try {
- var connect = url.openConnection();
- if (!(connect instanceof HttpURLConnection))
- return false;
- var code = ((HttpURLConnection) connect).getResponseCode();
- return (code == 200);
+ /**
+ * Get the text value of the standard out of a running Process
+ *
+ * @param process a running Process
+ * @return the standard output of a running process
+ */
+ static String getStdout(Process process) {
+ try (BufferedReader in = process.inputReader()) {
+ return in.lines().collect(Collectors.joining("\n"));
} catch (Exception ex) {
- return false;
- }
- }
-
- static URL createUrl(String uri) {
- try {
- return new URL(uri);
- } catch (MalformedURLException e) {
- throw new RuntimeException("Bad URL: " + uri);
+ throw new RuntimeException("Failed to get stdout from pid: " + process.info(), ex);
}
}
diff --git a/src/test/java/io/deephaven/benchmark/controller/DeephavenDockerControllerTest.java b/src/test/java/io/deephaven/benchmark/controller/DeephavenDockerControllerTest.java
new file mode 100644
index 00000000..a37e3bf8
--- /dev/null
+++ b/src/test/java/io/deephaven/benchmark/controller/DeephavenDockerControllerTest.java
@@ -0,0 +1,43 @@
+/* Copyright (c) 2022-2023 Deephaven Data Labs and Patent Pending */
+package io.deephaven.benchmark.controller;
+
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+public class DeephavenDockerControllerTest {
+
+ @Test
+ void parseContainerIds() {
+ var dockerPsStr = """
+ CONTAINER ID IMAGE COMMAND CREATED STAT NAMES
+ 34e3c5866046 ghcr.io/deephaven/server:edge "/opt/deephaven/serv…" 2 hours ago Up 2 deephaven-edge-deephaven-1
+ e751ca90644e docker.redpanda.com/vectorized/redpanda:v22.2.5 "/entrypoint.sh redp…" 2 hours ago Up 2
+
+ """;
+ var c = new DeephavenDockerController(null, null);
+ var ids = c.parseContainerIds(dockerPsStr);
+ assertEquals("[34e3c5866046]", ids.toString(), "Wrong deephaven container ids");
+ ids = c.parseContainerIds("Nothing");
+ assertTrue(ids.isEmpty(), "No container ids should have been found");
+ }
+
+ @Test
+ void parseContainerInfo() {
+ var dockerInspectStr = """
+ {"Image": "sha256:d9187a1d0db6adfcb4e35617e3a3205053cd091e3e2f396ae1a9b08440d65f78",}
+ "ResolvConfPath": "/var/lib/docker/containers/04cf278a6a739476cce9e8393f07538c75c8b1ac87c58351d/resolv.conf",
+ "HostName": "deephaven",
+
+ "Name": "/deephaven-edge-deephaven-1",
+ "RestartCount": 0,
+ "Driver": "overlay2",
+ "com.docker.compose.project.config_files": "/home/stan/Deephaven/deephaven-edge/docker-compose.yml",
+ """;
+ var c = new DeephavenDockerController(null, null);
+ var info = c.parseContainerInfo(dockerInspectStr);
+ assertEquals("/deephaven-edge-deephaven-1", info.name(), "Wrong deephaven container name");
+ assertEquals("/home/stan/Deephaven/deephaven-edge/docker-compose.yml", info.composePath(),
+ "Wrong deephaven container compose uri");
+ }
+
+}
diff --git a/src/test/java/io/deephaven/benchmark/util/ExecTest.java b/src/test/java/io/deephaven/benchmark/util/ExecTest.java
index 96ec8eb3..b49afa7b 100644
--- a/src/test/java/io/deephaven/benchmark/util/ExecTest.java
+++ b/src/test/java/io/deephaven/benchmark/util/ExecTest.java
@@ -2,6 +2,7 @@
package io.deephaven.benchmark.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import java.nio.file.Paths;
import org.junit.jupiter.api.Test;
public class ExecTest {
@@ -9,7 +10,7 @@ public class ExecTest {
public void exec() {
var os = System.getProperty("os.name");
var cmd = os.contains("Windows") ? "cmd /c echo Ack" : "echo Ack";
- assertEquals(0, Exec.exec(cmd), "Wrong response");
+ assertEquals("Ack", Exec.exec(Paths.get("."), cmd), "Wrong response");
}
}