WalSyncBootstrapOnOpenTest.java
/*******************************************************************************
* 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.wal;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import org.eclipse.rdf4j.common.io.ByteArrayUtil;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.sail.nativerdf.ValueStore;
import org.eclipse.rdf4j.sail.nativerdf.datastore.DataStore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
* Verifies that when configured with syncBootstrapOnOpen=true, the ValueStore rebuilds the WAL synchronously during
* open before returning, so the WAL already contains entries for existing values.
*/
class WalSyncBootstrapOnOpenTest {
@TempDir
Path tempDir;
@Test
void bootstrapSynchronousOnOpen() throws Exception {
// Arrange: create a ValueStore dictionary without WAL
Path dataDir = tempDir.resolve("store");
Files.createDirectories(dataDir);
try (DataStore ds = new DataStore(new File(dataDir.toString()), "values", false)) {
// Store a namespace and an IRI value
int nsId = ds.storeData("http://example.org/".getBytes(StandardCharsets.UTF_8));
IRI iri = SimpleValueFactory.getInstance().createIRI("http://example.org/x");
byte[] local = iri.getLocalName().getBytes(StandardCharsets.UTF_8);
byte[] iriBytes = new byte[1 + 4 + local.length];
iriBytes[0] = 0x1;
ByteArrayUtil.putInt(nsId, iriBytes, 1);
ByteArrayUtil.put(local, iriBytes, 5);
ds.storeData(iriBytes);
ds.sync();
}
// Act: open ValueStore with WAL configured to synchronous bootstrap
Path walDir = dataDir.resolve(ValueStoreWalConfig.DEFAULT_DIRECTORY_NAME);
ValueStoreWalConfig cfg = ValueStoreWalConfig.builder()
.walDirectory(walDir)
.storeUuid(UUID.randomUUID().toString())
.syncBootstrapOnOpen(true)
.build();
try (ValueStoreWAL wal = ValueStoreWAL.open(cfg);
ValueStore vs = new ValueStore(new File(dataDir.toString()), false,
ValueStore.VALUE_CACHE_SIZE, ValueStore.VALUE_ID_CACHE_SIZE,
ValueStore.NAMESPACE_CACHE_SIZE, ValueStore.NAMESPACE_ID_CACHE_SIZE, wal)) {
// Upon return, bootstrap should be complete and WAL should contain records for existing values
}
// Assert: WAL contains at least the namespace and the IRI records
Map<Integer, ValueStoreWalRecord> dictionary;
try (ValueStoreWalReader reader = ValueStoreWalReader.open(cfg)) {
ValueStoreWalRecovery recovery = new ValueStoreWalRecovery();
dictionary = new LinkedHashMap<>(recovery.replay(reader));
}
assertThat(dictionary).isNotEmpty();
assertThat(dictionary.values().stream().anyMatch(r -> r.valueKind() == ValueStoreWalValueKind.NAMESPACE))
.isTrue();
assertThat(dictionary.values()
.stream()
.anyMatch(r -> r.valueKind() == ValueStoreWalValueKind.IRI && r.lexical().endsWith("/x"))).isTrue();
}
}