Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/*******************************************************************************
* Copyright (c) 2026 Eclipse RDF4J contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*******************************************************************************/
// Some portions generated by Codex
package org.eclipse.rdf4j.sail.lmdb;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

import org.eclipse.rdf4j.sail.lmdb.config.LmdbStoreConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

/**
* Exhaustive combinatorics test for {@link TripleStore#getBestIndex(long, long, long, long)}.
*/
public class TripleStoreIndexSelectionCombinatoricsTest {

private static final int MAX_CONFIGURED_INDEXES = 6;

@Test
public void selectsMaxScoreIndexForEveryPatternAndConfiguredIndexCombination(@TempDir File dataDir)
throws Exception {
List<String> allIndexSpecs = allIndexSpecs();
Map<String, TripleStore.TripleIndex> detachedIndexes = createDetachedIndexes(dataDir, allIndexSpecs);
List<long[]> statementPatterns = allStatementPatterns();

File selectorDir = new File(dataDir, "selector");
selectorDir.mkdirs();

try (TripleStore selectorStore = new TripleStore(selectorDir, new LmdbStoreConfig("spoc"), null)) {
List<TripleStore.TripleIndex> selectorIndexes = indexes(selectorStore);
List<TripleStore.TripleIndex> originalSelectorIndexes = new ArrayList<>(selectorIndexes);
long[] configuredCombinationCount = new long[1];
long[] checkedSelections = new long[1];

try {
forEachConfiguredIndexCombination(allIndexSpecs, configuredIndexSpecs -> {
configuredCombinationCount[0]++;
selectorIndexes.clear();
for (String indexSpec : configuredIndexSpecs) {
selectorIndexes.add(detachedIndexes.get(indexSpec));
}

for (long[] pattern : statementPatterns) {
TripleStore.TripleIndex selected = selectorStore.getBestIndex(pattern[0], pattern[1],
pattern[2],
pattern[3]);
String selectedSpec = new String(selected.getFieldSeq());
int selectedScore = patternScore(selectedSpec, pattern);

int bestScore = Integer.MIN_VALUE;
Set<String> bestScoringSpecs = new LinkedHashSet<>();
for (String configuredIndexSpec : configuredIndexSpecs) {
int score = patternScore(configuredIndexSpec, pattern);
if (score > bestScore) {
bestScore = score;
bestScoringSpecs.clear();
bestScoringSpecs.add(configuredIndexSpec);
} else if (score == bestScore) {
bestScoringSpecs.add(configuredIndexSpec);
}
}

if (selectedScore != bestScore || !bestScoringSpecs.contains(selectedSpec)) {
fail("Incorrect index selection for pattern " + patternLabel(pattern)
+ " and configured indexes " + configuredIndexSpecs + ": selected=" + selectedSpec
+ " (score=" + selectedScore + "), best=" + bestScoringSpecs + " (score="
+ bestScore + ")");
}

checkedSelections[0]++;
}
});
} finally {
selectorIndexes.clear();
selectorIndexes.addAll(originalSelectorIndexes);
}

assertEquals(expectedCombinationCount(allIndexSpecs.size(), MAX_CONFIGURED_INDEXES),
configuredCombinationCount[0]);
assertEquals(configuredCombinationCount[0] * statementPatterns.size(), checkedSelections[0]);
}
}

private static Map<String, TripleStore.TripleIndex> createDetachedIndexes(File dataDir, List<String> indexSpecs)
throws Exception {
Map<String, TripleStore.TripleIndex> indexes = new LinkedHashMap<>();
for (String indexSpec : indexSpecs) {
File donorDir = new File(dataDir, "donor-" + indexSpec);
try (TripleStore donorStore = new TripleStore(donorDir, new LmdbStoreConfig(indexSpec), null)) {
indexes.put(indexSpec, donorStore.getBestIndex(-1, -1, -1, -1));
}
}
return indexes;
}

private static List<long[]> allStatementPatterns() {
List<long[]> patterns = new ArrayList<>(16);
for (int mask = 0; mask < 16; mask++) {
long subj = (mask & 0b0001) == 0 ? -1 : 11;
long pred = (mask & 0b0010) == 0 ? -1 : 13;
long obj = (mask & 0b0100) == 0 ? -1 : 17;
long context = (mask & 0b1000) == 0 ? -1 : 0;
patterns.add(new long[] { subj, pred, obj, context });
}
return patterns;
}

private static String patternLabel(long[] pattern) {
return "(" + componentLabel("s", pattern[0]) + ", " + componentLabel("p", pattern[1]) + ", "
+ componentLabel("o", pattern[2]) + ", " + componentLabel("c", pattern[3]) + ")";
}

private static String componentLabel(String name, long value) {
return value >= 0 ? name + "=" + value : name + "=*";
}

private static int patternScore(String indexSpec, long[] pattern) {
int score = 0;
for (int i = 0; i < indexSpec.length(); i++) {
if (isBound(pattern, indexSpec.charAt(i))) {
score++;
} else {
return score;
}
}
return score;
}

private static boolean isBound(long[] pattern, char component) {
switch (component) {
case 's':
return pattern[0] >= 0;
case 'p':
return pattern[1] >= 0;
case 'o':
return pattern[2] >= 0;
case 'c':
return pattern[3] >= 0;
default:
throw new IllegalArgumentException("Unknown component: " + component);
}
}

private static List<String> allIndexSpecs() {
List<String> indexSpecs = new ArrayList<>(24);
char[] components = new char[] { 's', 'p', 'o', 'c' };
permute(components, 0, indexSpecs);
return indexSpecs;
}

private static void permute(char[] components, int index, List<String> indexSpecs) {
if (index == components.length) {
indexSpecs.add(new String(components));
return;
}

for (int i = index; i < components.length; i++) {
swap(components, index, i);
permute(components, index + 1, indexSpecs);
swap(components, index, i);
}
}

private static void swap(char[] values, int left, int right) {
char current = values[left];
values[left] = values[right];
values[right] = current;
}

private static void forEachConfiguredIndexCombination(List<String> allIndexSpecs,
Consumer<List<String>> consumer) {
int maxSize = Math.min(MAX_CONFIGURED_INDEXES, allIndexSpecs.size());
List<String> current = new ArrayList<>(maxSize);
for (int combinationSize = 1; combinationSize <= maxSize; combinationSize++) {
buildCombinations(allIndexSpecs, 0, combinationSize, current, consumer);
}
}

private static void buildCombinations(List<String> values, int start, int remaining, List<String> current,
Consumer<List<String>> consumer) {
if (remaining == 0) {
consumer.accept(new ArrayList<>(current));
return;
}

for (int i = start; i <= values.size() - remaining; i++) {
current.add(values.get(i));
buildCombinations(values, i + 1, remaining - 1, current, consumer);
current.remove(current.size() - 1);
}
}

private static long expectedCombinationCount(int totalIndexes, int maxConfiguredIndexes) {
int maxSize = Math.min(totalIndexes, maxConfiguredIndexes);
long count = 0;
for (int size = 1; size <= maxSize; size++) {
count += nChooseK(totalIndexes, size);
}
return count;
}

private static long nChooseK(int n, int k) {
long value = 1;
for (int i = 1; i <= k; i++) {
value = value * (n - k + i) / i;
}
return value;
}

@SuppressWarnings("unchecked")
private static List<TripleStore.TripleIndex> indexes(TripleStore tripleStore) throws Exception {
Field field = TripleStore.class.getDeclaredField("indexes");
field.setAccessible(true);
return (List<TripleStore.TripleIndex>) field.get(tripleStore);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,26 @@ private void handleSignal(String signalName) {
context.close();
}
logger.info("Application context closed after SIG{}, exit status {}", signalName, exitCode);
System.exit(exitCode);
exitJvm(exitCode, signalName);
} catch (Throwable e) {
logger.warn("Error while shutting down after SIG{}", signalName, e);
}
} else {
logger.warn("SIG{} received before application context became available; shutting down immediately.",
signalName);
exitJvm(0, signalName);
}

}

private static void exitJvm(int exitCode, String signalName) {
try {
System.exit(exitCode);
} catch (SecurityException e) {
logger.error("System.exit({}) blocked by security manager after SIG{}", exitCode, signalName, e);
}
}

private static void startDelayedSystemExitThread(String signalName) {
// Start a thread that will forcibly exit the JVM after a delay, in case spring-boot hangs during shutdown
Thread thread = new Thread(() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.eclipse.rdf4j.model.IRI;
Expand Down Expand Up @@ -95,6 +96,11 @@ void gracefullyStopsOnSigterm() throws Exception {
assertGracefulShutdown("TERM");
}

@Test
void exitsOnSigtermBeforeContextAttachment() throws Exception {
assertShutdownBeforeContextAttachment("TERM");
}

private void assertGracefulShutdownWithSigintFallback() throws Exception {
assertGracefulShutdown("INT", true);
}
Expand All @@ -103,6 +109,52 @@ private void assertGracefulShutdown(String signalName) throws Exception {
assertGracefulShutdown(signalName, false);
}

private void assertShutdownBeforeContextAttachment(String signalName) throws Exception {
Path projectRoot = Path.of("").toAbsolutePath();
String javaBin = Path.of(System.getProperty("java.home"), "bin", "java").toString();
int serverPort = findFreePort();
int managementPort = findFreePort();

Path targetDir = projectRoot.resolve("target");
Path jarPath = Files.list(targetDir)
.sorted(Comparator.comparing(Path::toString))
.filter(p -> p.toString().endsWith(".jar"))
.filter(p -> !p.toString().endsWith("-sources.jar"))
.filter(p -> !p.toString().endsWith("-javadoc.jar"))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Could not find executable JAR in " + targetDir));

ProcessBuilder processBuilder = new ProcessBuilder(javaBin, "-jar", jarPath.toString(),
"--server.port=" + serverPort,
"--management.server.port=" + managementPort);
processBuilder.directory(projectRoot.toFile());
processBuilder.redirectErrorStream(true);

Process process = processBuilder.start();
cleanupActions.add(() -> process.destroyForcibly());

CountDownLatch started = new CountDownLatch(1);
StringBuilder outputBuffer = new StringBuilder();
startStreamGobbler(process, started, outputBuffer);

boolean registeredHandler = waitForOutputContains(outputBuffer,
"Registered SIGTERM handler for graceful shutdown.", 30, SECONDS);
assertThat(registeredHandler)
.as(() -> "Did not observe SIGTERM handler registration before sending signal. Output:\\n"
+ outputBuffer)
.isTrue();

sendSignal(process.pid(), signalName);
boolean exited = process.waitFor(30, SECONDS);
assertThat(exited)
.as(() -> "Process did not exit after SIG" + signalName
+ " sent before context attachment. Output:\\n" + outputBuffer)
.isTrue();
assertThat(process.exitValue())
.as(() -> "Process exit value after startup-phase SIG" + signalName + ". Output:\\n" + outputBuffer)
.isEqualTo(0);
}

private void assertGracefulShutdown(String signalName, boolean allowSigtermFallback) throws Exception {
Path projectRoot = Path.of("").toAbsolutePath();
String javaBin = Path.of(System.getProperty("java.home"), "bin", "java").toString();
Expand Down Expand Up @@ -196,6 +248,22 @@ private void sendSignal(long pid, String signalName) throws IOException, Interru
}
}

private boolean waitForOutputContains(StringBuilder outputBuffer, String marker, long timeout, TimeUnit unit)
throws InterruptedException {
long deadline = System.nanoTime() + unit.toNanos(timeout);
while (System.nanoTime() < deadline) {
synchronized (outputBuffer) {
if (outputBuffer.indexOf(marker) >= 0) {
return true;
}
}
Thread.sleep(100);
}
synchronized (outputBuffer) {
return outputBuffer.indexOf(marker) >= 0;
}
}

private void exerciseRemoteRepository(String serverUrl, StringBuilder outputBuffer)
throws InterruptedException, RepositoryException, RepositoryConfigException {
RemoteRepositoryManager manager = awaitRepositoryManager(serverUrl, outputBuffer);
Expand Down
Loading