Skip to content

Commit 9c1a661

Browse files
committed
GH-5691 CLI for running and storing query explanations
1 parent ae9024f commit 9c1a661

92 files changed

Lines changed: 5061 additions & 10 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

testsuites/benchmark/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ Every CLI run also prints:
169169
- `softLimitMillis` (currently `60000`)
170170
- whether stopping hit the soft-limit projection or max repeat-run cap
171171

172+
### 8) Configure query timeout
173+
174+
Set a per-query timeout in seconds (`0` disables timeout):
175+
176+
```bash
177+
... -Dexec.args="--store memory --theme MEDICAL_RECORDS --query-index 0 --query-timeout-seconds 30"
178+
```
179+
172180
### 7) Run all themed queries across all themes for one store
173181

174182
Memory store:

testsuites/benchmark/src/main/java/org/eclipse/rdf4j/benchmark/plan/QueryPlanSnapshotCli.java

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.eclipse.rdf4j.benchmark.common.plan.QueryPlanSnapshot;
3636
import org.eclipse.rdf4j.benchmark.rio.util.ThemeDataSetGenerator.Theme;
3737
import org.eclipse.rdf4j.common.annotation.Experimental;
38+
import org.eclipse.rdf4j.query.TupleQuery;
3839
import org.eclipse.rdf4j.queryrender.sparql.TupleExprIRRenderer;
3940
import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection;
4041

@@ -171,14 +172,16 @@ private void runSingleQueryCapture(QueryPlanSnapshotCliOptions options,
171172
QueryExecutionVerification executionVerification;
172173
try (SailRepositoryConnection connection = storeRuntime.repository.getConnection()) {
173174
if (options.persist) {
174-
snapshotPath = capture.captureAndWrite(context, () -> connection.prepareTupleQuery(queryText));
175+
snapshotPath = capture.captureAndWrite(context,
176+
() -> prepareTupleQuery(connection, queryText, options.queryTimeoutSeconds));
175177
currentSnapshot = capture.readSnapshot(snapshotPath);
176178
output.println("Snapshot written: " + snapshotPath.toAbsolutePath());
177179
} else {
178-
currentSnapshot = capture.capture(context, () -> connection.prepareTupleQuery(queryText));
180+
currentSnapshot = capture.capture(context,
181+
() -> prepareTupleQuery(connection, queryText, options.queryTimeoutSeconds));
179182
output.println("Snapshot captured in-memory only (--persist=false).");
180183
}
181-
executionVerification = verifyRepeatedExecution(connection, queryText);
184+
executionVerification = verifyRepeatedExecution(connection, queryText, options.queryTimeoutSeconds);
182185
}
183186

184187
printResultsSection(options, queryId, queryText);
@@ -222,14 +225,17 @@ private void runAllThemeQueriesCapture(QueryPlanSnapshotCliOptions options,
222225
QueryExecutionVerification executionVerification;
223226
try (SailRepositoryConnection connection = storeRuntime.repository.getConnection()) {
224227
if (options.persist) {
225-
snapshotPath = capture.captureAndWrite(context, () -> connection.prepareTupleQuery(queryText));
228+
snapshotPath = capture.captureAndWrite(context,
229+
() -> prepareTupleQuery(connection, queryText, perQueryOptions.queryTimeoutSeconds));
226230
currentSnapshot = capture.readSnapshot(snapshotPath);
227231
output.println("Snapshot written: " + snapshotPath.toAbsolutePath());
228232
} else {
229-
currentSnapshot = capture.capture(context, () -> connection.prepareTupleQuery(queryText));
233+
currentSnapshot = capture.capture(context,
234+
() -> prepareTupleQuery(connection, queryText, perQueryOptions.queryTimeoutSeconds));
230235
output.println("Snapshot captured in-memory only (--persist=false).");
231236
}
232-
executionVerification = verifyRepeatedExecution(connection, queryText);
237+
executionVerification = verifyRepeatedExecution(connection, queryText,
238+
perQueryOptions.queryTimeoutSeconds);
233239
}
234240

235241
output.println();
@@ -589,6 +595,9 @@ private void promptAllRunOptions(QueryPlanSnapshotCliOptions options) throws IOE
589595
options.diffMode = promptDiffMode(options.diffMode);
590596
}
591597
options.queryId = promptOptionalValue("Query id (blank keeps default/auto)", options.queryId);
598+
options.queryTimeoutSeconds = promptOptionalNonNegativeInteger(
599+
"Query timeout seconds (blank keeps current, 0 disables timeout)",
600+
options.queryTimeoutSeconds);
592601
if (options.outputDirectory == null) {
593602
options.outputDirectory = promptOptionalPath("Output directory (blank uses default)");
594603
}
@@ -641,6 +650,20 @@ private Path promptOptionalPath(String message) throws IOException {
641650
return Path.of(raw.trim());
642651
}
643652

653+
private Integer promptOptionalNonNegativeInteger(String message, Integer currentValue) throws IOException {
654+
while (true) {
655+
String raw = prompt(message);
656+
if (raw.isBlank()) {
657+
return currentValue;
658+
}
659+
try {
660+
return QueryPlanSnapshotCliOptions.parseNonNegativeInteger(raw, message);
661+
} catch (IllegalArgumentException e) {
662+
output.println(e.getMessage());
663+
}
664+
}
665+
}
666+
644667
private void promptForAssignments(String heading, Map<String, String> target) throws IOException {
645668
output.println(heading);
646669
while (true) {
@@ -807,6 +830,9 @@ private static QueryPlanCaptureContext createContext(QueryPlanSnapshotCliOptions
807830
.addMetadata(QueryPlanCapture.metadataFromSystemProperties())
808831
.featureFlagCollector(featureFlags)
809832
.tupleExprRenderer(QueryPlanSnapshotCli::renderTupleExprWithIr);
833+
if (options.queryTimeoutSeconds != null) {
834+
contextBuilder.addMetadata("queryTimeoutSeconds", options.queryTimeoutSeconds.toString());
835+
}
810836

811837
if (options.queryIndex != null && benchmarkQuery != null) {
812838
contextBuilder.addMetadata("queryIndex", Integer.toString(options.queryIndex))
@@ -823,7 +849,8 @@ private static FeatureFlagCollector createFeatureFlagCollector(QueryPlanSnapshot
823849
.addValue("cli.store", options.store.id)
824850
.addValue("cli.theme", options.theme.name())
825851
.addValue("cli.querySource", querySource)
826-
.addValue("cli.persist", Boolean.toString(options.persist));
852+
.addValue("cli.persist", Boolean.toString(options.persist))
853+
.addValue("cli.queryTimeoutSeconds", formatQueryTimeoutSeconds(options.queryTimeoutSeconds));
827854
if (options.queryIndex != null) {
828855
featureFlags.addValue("cli.queryIndex", options.queryIndex.toString());
829856
}
@@ -903,6 +930,22 @@ private static String normalizedOrNull(String value) {
903930
return normalized.isEmpty() ? null : normalized;
904931
}
905932

933+
private static String formatQueryTimeoutSeconds(Integer queryTimeoutSeconds) {
934+
if (queryTimeoutSeconds == null || queryTimeoutSeconds == 0) {
935+
return "<none>";
936+
}
937+
return queryTimeoutSeconds.toString();
938+
}
939+
940+
private static TupleQuery prepareTupleQuery(SailRepositoryConnection connection, String queryText,
941+
Integer queryTimeoutSeconds) {
942+
TupleQuery tupleQuery = connection.prepareTupleQuery(queryText);
943+
if (queryTimeoutSeconds != null && queryTimeoutSeconds > 0) {
944+
tupleQuery.setMaxExecutionTime(queryTimeoutSeconds);
945+
}
946+
return tupleQuery;
947+
}
948+
906949
private void printPrettyExplanations(QueryPlanSnapshot snapshot) {
907950
Map<String, QueryPlanExplanation> explanations = snapshot.getExplanations();
908951
if (explanations == null || explanations.isEmpty()) {
@@ -925,7 +968,8 @@ private void printResultsSection(QueryPlanSnapshotCliOptions options, String que
925968
output.println("=== Results ===");
926969
output.println("Original query:");
927970
output.println(queryText.trim());
928-
output.println("Store=" + options.store.id + ", Theme=" + options.theme + ", QueryId=" + queryId);
971+
output.println("Store=" + options.store.id + ", Theme=" + options.theme + ", QueryId=" + queryId
972+
+ ", QueryTimeoutSeconds=" + formatQueryTimeoutSeconds(options.queryTimeoutSeconds));
929973
}
930974

931975
private void printExplanation(String levelKey, QueryPlanExplanation explanation) {
@@ -952,7 +996,8 @@ private void printExplanation(String levelKey, QueryPlanExplanation explanation)
952996
}
953997
}
954998

955-
private QueryExecutionVerification verifyRepeatedExecution(SailRepositoryConnection connection, String queryText) {
999+
private QueryExecutionVerification verifyRepeatedExecution(SailRepositoryConnection connection, String queryText,
1000+
Integer queryTimeoutSeconds) {
9561001
long elapsedNanos = 0;
9571002
long stableResultCount = Long.MIN_VALUE;
9581003
int runs = 0;
@@ -971,7 +1016,10 @@ private QueryExecutionVerification verifyRepeatedExecution(SailRepositoryConnect
9711016
}
9721017

9731018
long startedAt = System.nanoTime();
974-
long currentResultCount = connection.prepareTupleQuery(queryText).evaluate().stream().count();
1019+
long currentResultCount = prepareTupleQuery(connection, queryText, queryTimeoutSeconds)
1020+
.evaluate()
1021+
.stream()
1022+
.count();
9751023
long runNanos = Math.max(1L, System.nanoTime() - startedAt);
9761024
elapsedNanos += runNanos;
9771025
runs++;

testsuites/benchmark/src/main/java/org/eclipse/rdf4j/benchmark/plan/QueryPlanSnapshotCliOptions.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ final class QueryPlanSnapshotCliOptions {
4747
Path outputDirectory;
4848
String queryId;
4949
Path lmdbDataDirectory;
50+
Integer queryTimeoutSeconds;
5051
final LinkedHashMap<String, String> systemProperties = new LinkedHashMap<>();
5152
final LinkedHashMap<String, String> metadata = new LinkedHashMap<>();
5253

@@ -75,6 +76,7 @@ QueryPlanSnapshotCliOptions copy() {
7576
copy.outputDirectory = outputDirectory;
7677
copy.queryId = queryId;
7778
copy.lmdbDataDirectory = lmdbDataDirectory;
79+
copy.queryTimeoutSeconds = queryTimeoutSeconds;
7880
copy.systemProperties.putAll(systemProperties);
7981
copy.metadata.putAll(metadata);
8082
return copy;
@@ -172,6 +174,9 @@ static QueryPlanSnapshotCliOptions parseArgs(String[] args) {
172174
case "--lmdb-data-dir":
173175
options.lmdbDataDirectory = Path.of(requireValue(args, ++i, arg));
174176
break;
177+
case "--query-timeout-seconds":
178+
options.queryTimeoutSeconds = parseNonNegativeInteger(requireValue(args, ++i, arg), arg);
179+
break;
175180
case "--property": {
176181
Assignment assignment = parseAssignment(requireValue(args, ++i, arg), arg);
177182
options.systemProperties.put(assignment.key, assignment.value);
@@ -336,6 +341,20 @@ private static boolean parseBoolean(String value, String optionName) {
336341
throw new IllegalArgumentException("Invalid " + optionName + " value '" + value + "'. Use true or false.");
337342
}
338343

344+
static int parseNonNegativeInteger(String value, String optionName) {
345+
int parsed;
346+
try {
347+
parsed = Integer.parseInt(value.trim());
348+
} catch (NumberFormatException e) {
349+
throw new IllegalArgumentException("Invalid " + optionName + " value '" + value + "'. Use a whole number.",
350+
e);
351+
}
352+
if (parsed < 0) {
353+
throw new IllegalArgumentException("Invalid " + optionName + " value '" + value + "'. Must be >= 0.");
354+
}
355+
return parsed;
356+
}
357+
339358
private static DiffMode parseDiffMode(String value, String optionName) {
340359
String normalized = value.trim().toLowerCase(Locale.ROOT);
341360
for (DiffMode mode : DiffMode.values()) {
@@ -421,6 +440,7 @@ static void printUsage(PrintStream output) {
421440
output.println(" --all-theme-queries run all themed queries across all themes");
422441
output.println(" --query <SPARQL> direct query text");
423442
output.println(" --query-file <path> load query text from file");
443+
output.println(" --query-timeout-seconds <int>=0 per-query max execution time (0 disables timeout)");
424444
output.println(" --persist <true|false> | --no-persist");
425445
output.println(" --compare-latest compare current run with latest prior run");
426446
output.println();

testsuites/benchmark/src/main/resources/plan/cli/lmdb/lmdb-electrical_grid-q0-8608b9dfc996a3a316478c1deb02c8910fe964ce708b8c9b74fda608fa544870-20260217-163819234-b35ac940.json

Lines changed: 56 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)