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();
	}
}