diff --git a/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/CONFIG.java b/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/CONFIG.java
index bc9096e4386..52b3fac5744 100644
--- a/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/CONFIG.java
+++ b/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/CONFIG.java
@@ -277,6 +277,12 @@ public static final class Native {
*/
public final static IRI namespaceIDCacheSize = createIRI(NAMESPACE, "native.namespaceIDCacheSize");
+ /**
+ * tag:rdf4j.org,2025:config/native.memoryMappedTxnStatusFile
+ */
+ public final static IRI memoryMappedTxnStatusFile = createIRI(NAMESPACE,
+ "native.memoryMappedTxnStatusFile");
+
// ValueStore WAL configuration properties
/** tag:rdf4j.org,2023:config/native.walMaxSegmentBytes */
public final static IRI walMaxSegmentBytes = createIRI(NAMESPACE, "native.walMaxSegmentBytes");
diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java
index cc84e1a08bb..2bf1954a954 100644
--- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java
+++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java
@@ -80,6 +80,7 @@ class NativeSailStore implements SailStore {
private final ContextStore contextStore;
private final boolean walEnabled;
+ private final Boolean memoryMappedTxnStatusFileEnabled;
/**
* A lock to control concurrent access by {@link NativeSailSink} to the TripleStore, ValueStore, and NamespaceStore.
@@ -99,7 +100,7 @@ class NativeSailStore implements SailStore {
public NativeSailStore(File dataDir, String tripleIndexes) throws IOException, SailException {
this(dataDir, tripleIndexes, false, ValueStore.VALUE_CACHE_SIZE, ValueStore.VALUE_ID_CACHE_SIZE,
ValueStore.NAMESPACE_CACHE_SIZE, ValueStore.NAMESPACE_ID_CACHE_SIZE,
- -1L, -1, -1, null, -1L, -1L, null, false, false, true);
+ -1L, -1, -1, null, -1L, -1L, null, false, false, true, null);
}
/**
@@ -114,7 +115,7 @@ public NativeSailStore(File dataDir, String tripleIndexes, boolean forceSync, in
throws IOException, SailException {
this(dataDir, tripleIndexes, forceSync, valueCacheSize, valueIDCacheSize, namespaceCacheSize,
namespaceIDCacheSize, walMaxSegmentBytes, walQueueCapacity, walBatchBufferBytes, walSyncPolicy,
- walSyncIntervalMillis, walIdlePollIntervalMillis, walDirectoryName, false, false, true);
+ walSyncIntervalMillis, walIdlePollIntervalMillis, walDirectoryName, false, false, true, null);
}
public NativeSailStore(File dataDir, String tripleIndexes, boolean forceSync, int valueCacheSize,
@@ -122,9 +123,11 @@ public NativeSailStore(File dataDir, String tripleIndexes, boolean forceSync, in
int walQueueCapacity, int walBatchBufferBytes,
ValueStoreWalConfig.SyncPolicy walSyncPolicy,
long walSyncIntervalMillis, long walIdlePollIntervalMillis, String walDirectoryName,
- boolean walSyncBootstrapOnOpen, boolean walAutoRecoverOnOpen, boolean walEnabled)
+ boolean walSyncBootstrapOnOpen, boolean walAutoRecoverOnOpen, boolean walEnabled,
+ Boolean memoryMappedTxnStatusFileEnabled)
throws IOException, SailException {
this.walEnabled = walEnabled;
+ this.memoryMappedTxnStatusFileEnabled = memoryMappedTxnStatusFileEnabled;
NamespaceStore createdNamespaceStore = null;
ValueStoreWAL createdWal = null;
ValueStore createdValueStore = null;
@@ -171,7 +174,7 @@ public NativeSailStore(File dataDir, String tripleIndexes, boolean forceSync, in
}
createdValueStore = new ValueStore(dataDir, forceSync, valueCacheSize, valueIDCacheSize,
namespaceCacheSize, namespaceIDCacheSize, createdWal);
- createdTripleStore = new TripleStore(dataDir, tripleIndexes, forceSync);
+ createdTripleStore = new TripleStore(dataDir, tripleIndexes, forceSync, memoryMappedTxnStatusFileEnabled);
// Assign fields required by ContextStore before constructing it
namespaceStore = createdNamespaceStore;
diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStore.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStore.java
index 6b3c77d94fa..e5f31939c84 100644
--- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStore.java
+++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStore.java
@@ -144,6 +144,8 @@ protected SailStore createSailStore(File dataDir) throws IOException, SailExcept
private volatile int namespaceIDCacheSize = ValueStore.NAMESPACE_ID_CACHE_SIZE;
+ private volatile Boolean memoryMappedTxnStatusFileEnabled;
+
private SailStore store;
// used to decide if store is writable, is true if the store was writable during initialization
@@ -276,6 +278,14 @@ public void setNamespaceIDCacheSize(int namespaceIDCacheSize) {
this.namespaceIDCacheSize = namespaceIDCacheSize;
}
+ public Boolean getMemoryMappedTxnStatusFileEnabled() {
+ return memoryMappedTxnStatusFileEnabled;
+ }
+
+ public void setMemoryMappedTxnStatusFileEnabled(Boolean memoryMappedTxnStatusFileEnabled) {
+ this.memoryMappedTxnStatusFileEnabled = memoryMappedTxnStatusFileEnabled;
+ }
+
@Experimental
public void setWalMaxSegmentBytes(long walMaxSegmentBytes) {
this.walMaxSegmentBytes = walMaxSegmentBytes;
@@ -492,7 +502,8 @@ protected void initializeInternal() throws SailException {
walDirectoryName,
walSyncBootstrapOnOpen,
walAutoRecoverOnOpen,
- walEnabled);
+ walEnabled,
+ memoryMappedTxnStatusFileEnabled);
this.store = new SnapshotSailStore(mainStore, MemoryOverflowIntoNativeStore::new) {
@Override
diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/TripleStore.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/TripleStore.java
index 88e60a3eaa4..ccb1b4c4943 100644
--- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/TripleStore.java
+++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/TripleStore.java
@@ -80,12 +80,6 @@ class TripleStore implements Closeable {
*/
private static final String INDEXES_KEY = "triple-indexes";
- /**
- * System property that enables the experimental {@link MemoryMappedTxnStatusFile} implementation instead of the
- * default {@link TxnStatusFile}.
- */
- private static final String MEMORY_MAPPED_TXN_STATUS_FILE_ENABLED_PROP = "org.eclipse.rdf4j.sail.nativerdf.MemoryMappedTxnStatusFile.enabled";
-
/**
* The version number for the current triple store.
*
@@ -168,13 +162,18 @@ class TripleStore implements Closeable {
*--------------*/
public TripleStore(File dir, String indexSpecStr) throws IOException, SailException {
- this(dir, indexSpecStr, false);
+ this(dir, indexSpecStr, false, null);
}
public TripleStore(File dir, String indexSpecStr, boolean forceSync) throws IOException, SailException {
+ this(dir, indexSpecStr, forceSync, null);
+ }
+
+ public TripleStore(File dir, String indexSpecStr, boolean forceSync, Boolean memoryMappedTxnStatusFileEnabled)
+ throws IOException, SailException {
this.dir = dir;
this.forceSync = forceSync;
- this.txnStatusFile = createTxnStatusFile(dir);
+ this.txnStatusFile = createTxnStatusFile(dir, memoryMappedTxnStatusFileEnabled);
File propFile = new File(dir, PROPERTIES_FILE);
@@ -229,8 +228,10 @@ public TripleStore(File dir, String indexSpecStr, boolean forceSync) throws IOEx
}
}
- private static TxnStatusFile createTxnStatusFile(File dir) throws IOException {
- if (Boolean.getBoolean(MEMORY_MAPPED_TXN_STATUS_FILE_ENABLED_PROP)) {
+ private static TxnStatusFile createTxnStatusFile(File dir, Boolean memoryMappedTxnStatusFileEnabled)
+ throws IOException {
+ boolean enabled = Boolean.TRUE.equals(memoryMappedTxnStatusFileEnabled);
+ if (enabled) {
return new MemoryMappedTxnStatusFile(dir);
}
return new TxnStatusFile(dir);
diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/config/NativeStoreConfig.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/config/NativeStoreConfig.java
index b757cf0e3f8..2e9cac53097 100644
--- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/config/NativeStoreConfig.java
+++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/config/NativeStoreConfig.java
@@ -37,6 +37,7 @@ public class NativeStoreConfig extends BaseSailConfig {
private int valueIDCacheSize = -1;
private int namespaceCacheSize = -1;
private int namespaceIDCacheSize = -1;
+ private Boolean memoryMappedTxnStatusFileEnabled;
// WAL: expose max segment bytes via config (optional)
private long walMaxSegmentBytes = -1L;
@@ -124,6 +125,14 @@ public void setNamespaceIDCacheSize(int namespaceIDCacheSize) {
this.namespaceIDCacheSize = namespaceIDCacheSize;
}
+ public Boolean getMemoryMappedTxnStatusFileEnabled() {
+ return memoryMappedTxnStatusFileEnabled;
+ }
+
+ public void setMemoryMappedTxnStatusFileEnabled(Boolean memoryMappedTxnStatusFileEnabled) {
+ this.memoryMappedTxnStatusFileEnabled = memoryMappedTxnStatusFileEnabled;
+ }
+
public long getWalMaxSegmentBytes() {
return walMaxSegmentBytes;
}
@@ -231,6 +240,10 @@ public Resource export(Model m) {
if (namespaceIDCacheSize >= 0) {
m.add(implNode, CONFIG.Native.namespaceIDCacheSize, literal(namespaceIDCacheSize));
}
+ if (memoryMappedTxnStatusFileEnabled != null) {
+ m.add(implNode, CONFIG.Native.memoryMappedTxnStatusFile,
+ literal(memoryMappedTxnStatusFileEnabled));
+ }
// WAL configuration properties
if (walMaxSegmentBytes >= 0) {
m.add(implNode, CONFIG.Native.walMaxSegmentBytes, literal(walMaxSegmentBytes));
@@ -347,8 +360,8 @@ public void parse(Model m, Resource implNode) throws SailConfigException {
}
});
- Configurations.getLiteralValue(m, implNode, CONFIG.Native.namespaceIDCacheSize, NAMESPACE_ID_CACHE_SIZE)
- .ifPresent(lit -> {
+ Configurations.getLiteralValue(m, implNode, CONFIG.Native.namespaceIDCacheSize,
+ NAMESPACE_ID_CACHE_SIZE).ifPresent(lit -> {
try {
setNamespaceIDCacheSize(lit.intValue());
} catch (NumberFormatException e) {
@@ -358,6 +371,16 @@ public void parse(Model m, Resource implNode) throws SailConfigException {
}
});
+ Configurations.getLiteralValue(m, implNode, CONFIG.Native.memoryMappedTxnStatusFile)
+ .ifPresent(lit -> {
+ try {
+ setMemoryMappedTxnStatusFileEnabled(lit.booleanValue());
+ } catch (IllegalArgumentException e) {
+ throw new SailConfigException("Boolean value required for "
+ + CONFIG.Native.memoryMappedTxnStatusFile + " property, found " + lit);
+ }
+ });
+
// WAL configuration properties
Configurations.getLiteralValue(m, implNode, CONFIG.Native.walMaxSegmentBytes)
.ifPresent(lit -> {
diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/config/NativeStoreFactory.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/config/NativeStoreFactory.java
index 26c858df305..1a494fb9c18 100644
--- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/config/NativeStoreFactory.java
+++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/config/NativeStoreFactory.java
@@ -72,6 +72,9 @@ public Sail getSail(SailImplConfig config) throws SailConfigException {
if (nativeConfig.getNamespaceIDCacheSize() >= 0) {
nativeStore.setNamespaceIDCacheSize(nativeConfig.getNamespaceIDCacheSize());
}
+ if (nativeConfig.getMemoryMappedTxnStatusFileEnabled() != null) {
+ nativeStore.setMemoryMappedTxnStatusFileEnabled(nativeConfig.getMemoryMappedTxnStatusFileEnabled());
+ }
if (nativeConfig.getIterationCacheSyncThreshold() > 0) {
nativeStore.setIterationCacheSyncThreshold(nativeConfig.getIterationCacheSyncThreshold());
}
diff --git a/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/MemoryMappedTxnStatusFileConfigTest.java b/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/MemoryMappedTxnStatusFileConfigTest.java
index edc9441c130..8370aec0f70 100644
--- a/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/MemoryMappedTxnStatusFileConfigTest.java
+++ b/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/MemoryMappedTxnStatusFileConfigTest.java
@@ -20,7 +20,8 @@
import org.junit.jupiter.api.io.TempDir;
/**
- * Verifies that the implementation used for the transaction status file can be controlled via a system property.
+ * Verifies that the implementation used for the transaction status file is controlled through configuration rather than
+ * a JVM system property.
*/
public class MemoryMappedTxnStatusFileConfigTest {
@@ -52,7 +53,7 @@ public void defaultUsesNioTxnStatusFile() throws Exception {
}
@Test
- public void memoryMappedEnabledUsesFixedSizeFile() throws Exception {
+ public void systemPropertyIsIgnored() throws Exception {
System.setProperty(MEMORY_MAPPED_ENABLED_PROP, "true");
TripleStore tripleStore = new TripleStore(dataDir, "spoc");
@@ -66,7 +67,7 @@ public void memoryMappedEnabledUsesFixedSizeFile() throws Exception {
File txnStatusFile = new File(dataDir, TxnStatusFile.FILE_NAME);
assertTrue(txnStatusFile.exists(), "Transaction status file should exist");
- assertEquals(1L, txnStatusFile.length(),
- "Memory-mapped TxnStatusFile keeps a single status byte on disk for NONE status");
+ assertEquals(0L, txnStatusFile.length(),
+ "System property does not switch to memory-mapped TxnStatusFile");
}
}
diff --git a/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreTxnStatusConfigTest.java b/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreTxnStatusConfigTest.java
new file mode 100644
index 00000000000..c3f8641721e
--- /dev/null
+++ b/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreTxnStatusConfigTest.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Copyright (c) 2025 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
+ *******************************************************************************/
+package org.eclipse.rdf4j.sail.nativerdf;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.lang.reflect.Field;
+
+import org.eclipse.rdf4j.model.IRI;
+import org.eclipse.rdf4j.model.ValueFactory;
+import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
+import org.eclipse.rdf4j.repository.Repository;
+import org.eclipse.rdf4j.repository.RepositoryConnection;
+import org.eclipse.rdf4j.repository.sail.SailRepository;
+import org.eclipse.rdf4j.sail.nativerdf.config.NativeStoreConfig;
+import org.eclipse.rdf4j.sail.nativerdf.config.NativeStoreFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class NativeStoreTxnStatusConfigTest {
+
+ @TempDir
+ File dataDir;
+
+ @Test
+ void configEnablesMemoryMappedTxnStatusFile() throws Exception {
+ NativeStoreConfig cfg = new NativeStoreConfig("spoc");
+ cfg.setMemoryMappedTxnStatusFileEnabled(true);
+
+ NativeStoreFactory factory = new NativeStoreFactory();
+ NativeStore sail = (NativeStore) factory.getSail(cfg);
+ sail.setDataDir(dataDir);
+
+ Repository repo = new SailRepository(sail);
+ repo.init();
+ assertThat(extractTxnStatusFile(sail)).isInstanceOf(MemoryMappedTxnStatusFile.class);
+ try (RepositoryConnection conn = repo.getConnection()) {
+ ValueFactory vf = SimpleValueFactory.getInstance();
+ IRI p = vf.createIRI("http://example.com/p");
+ conn.add(vf.createIRI("http://example.com/s"), p, vf.createLiteral("o"));
+ }
+ repo.shutDown();
+
+ File txnStatusFile = new File(dataDir, TxnStatusFile.FILE_NAME);
+ assertThat(txnStatusFile).exists();
+ assertThat(txnStatusFile.length()).isEqualTo(1L);
+ }
+
+ private TxnStatusFile extractTxnStatusFile(NativeStore sail) throws Exception {
+ NativeSailStore store = (NativeSailStore) sail.getSailStore();
+
+ Field tripleStoreField = NativeSailStore.class.getDeclaredField("tripleStore");
+ tripleStoreField.setAccessible(true);
+ TripleStore tripleStore = (TripleStore) tripleStoreField.get(store);
+
+ Field txnStatusFileField = TripleStore.class.getDeclaredField("txnStatusFile");
+ txnStatusFileField.setAccessible(true);
+ return (TxnStatusFile) txnStatusFileField.get(tripleStore);
+ }
+}
diff --git a/site/content/documentation/reference/configuration.md b/site/content/documentation/reference/configuration.md
index 93e0e121508..687b345051d 100644
--- a/site/content/documentation/reference/configuration.md
+++ b/site/content/documentation/reference/configuration.md
@@ -251,6 +251,8 @@ Creating more indexes potentially speeds up querying (a lot), but also adds over
The native store automatically creates/drops indexes upon (re)initialization, so the parameter can be adjusted and upon the first refresh of the configuration the native store will change its indexing strategy, without loss of data.
+Set `config:native.memoryMappedTxnStatusFile` to `true` to enable the experimental memory-mapped transaction status file. When unset, the store falls back to the legacy file implementation.
+
##### Example configuration
```turtle