From ecd8b33a62843a77b0c922891b6c270f184ab3c2 Mon Sep 17 00:00:00 2001 From: Stan Brubaker <120737309+stanbrub@users.noreply.github.com> Date: Tue, 26 Dec 2023 22:09:59 -0700 Subject: [PATCH] Handle per operation logging (#239) --- .../tests/compare/CompareTestRunner.java | 18 +- .../experimental/ExperimentalTestRunner.java | 20 +-- .../mergescale/ScaleTestRunner.java | 5 +- .../tests/standard/StandardTestRunner.java | 50 ++++-- .../tests/standard/file/FileTestRunner.java | 20 ++- .../tests/standard/kafka/KafkaTestRunner.java | 12 +- .../io/deephaven/benchmark/api/Bench.java | 13 ++ .../io/deephaven/benchmark/api/BenchLog.java | 81 +++++++++ .../deephaven/benchmark/api/BenchQuery.java | 1 - .../benchmark/controller/Controller.java | 50 ++++++ .../controller/DeephavenDockerController.java | 168 ++++++++++++++++++ .../io/deephaven/benchmark/util/Exec.java | 66 ++----- .../DeephavenDockerControllerTest.java | 43 +++++ .../io/deephaven/benchmark/util/ExecTest.java | 3 +- 14 files changed, 453 insertions(+), 97 deletions(-) create mode 100644 src/main/java/io/deephaven/benchmark/api/BenchLog.java create mode 100644 src/main/java/io/deephaven/benchmark/controller/Controller.java create mode 100644 src/main/java/io/deephaven/benchmark/controller/DeephavenDockerController.java create mode 100644 src/test/java/io/deephaven/benchmark/controller/DeephavenDockerControllerTest.java 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"); } }