NativeStoreWalConfigTest.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;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.File;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

import org.eclipse.rdf4j.model.IRI;
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.base.SailStore;
import org.eclipse.rdf4j.sail.base.SnapshotSailStore;
import org.eclipse.rdf4j.sail.nativerdf.config.NativeStoreConfig;
import org.eclipse.rdf4j.sail.nativerdf.config.NativeStoreFactory;
import org.eclipse.rdf4j.sail.nativerdf.wal.ValueStoreWAL;
import org.eclipse.rdf4j.sail.nativerdf.wal.ValueStoreWalConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class NativeStoreWalConfigTest {

	@TempDir
	File dataDir;

	@Test
	void respectsWalMaxSegmentBytes() throws Exception {
		// Configure a very small WAL segment size to force rotation
		NativeStoreConfig cfg = new NativeStoreConfig("spoc");
		cfg.setWalMaxSegmentBytes(32 * 1024); // 32 KiB

		NativeStoreFactory factory = new NativeStoreFactory();
		NativeStore sail = (NativeStore) factory.getSail(cfg);
		sail.setDataDir(dataDir);
		Repository repo = new SailRepository(sail);
		repo.init();
		try (RepositoryConnection conn = repo.getConnection()) {
			SimpleValueFactory vf = SimpleValueFactory.getInstance();
			IRI p = vf.createIRI("http://example.com/p");
			// Add enough statements with ~1KB literals to exceed 32 KiB
			for (int i = 0; i < 200; i++) {
				int len = 1024 + ThreadLocalRandom.current().nextInt(512);
				String s = "x".repeat(len);
				conn.add(vf.createIRI("http://example.com/s/" + i), p, vf.createLiteral(s));
			}
		}
		repo.shutDown();

		// Verify multiple WAL segments were created due to small max size
		Path walDir = dataDir.toPath().resolve(ValueStoreWalConfig.DEFAULT_DIRECTORY_NAME);
		assertThat(Files.isDirectory(walDir)).isTrue();
		try (var stream = Files.list(walDir)) {
			List<Path> segments = stream
					.filter(p -> p.getFileName().toString().matches("wal-[1-9]\\d*\\.v1(\\.gz)?"))
					.collect(Collectors.toList());
			assertThat(segments.size()).as("expect >1 wal segments after forced rotation").isGreaterThan(1);
		}
	}

	@Test
	void mapsAllWalConfigOptions() throws Exception {
		NativeStoreConfig cfg = new NativeStoreConfig("spoc");
		cfg.setWalMaxSegmentBytes(1 << 20); // 1 MiB
		cfg.setWalQueueCapacity(1234);
		cfg.setWalBatchBufferBytes(1 << 14); // 16 KiB
		cfg.setWalSyncPolicy("ALWAYS");
		cfg.setWalSyncIntervalMillis(50);
		cfg.setWalIdlePollIntervalMillis(5);
		cfg.setWalDirectoryName("custom-wal-dir");
		cfg.setWalSyncBootstrapOnOpen(true);

		NativeStoreFactory factory = new NativeStoreFactory();
		NativeStore sail = (NativeStore) factory.getSail(cfg);
		sail.setDataDir(dataDir);
		sail.init();

		SailStore sailStore = sail.getSailStore();
		// unwrap SnapshotSailStore to get underlying NativeSailStore
		Field backingField = SnapshotSailStore.class
				.getDeclaredField("backingStore");
		backingField.setAccessible(true);
		NativeSailStore nss = (NativeSailStore) backingField.get(sailStore);

		Field walField = NativeSailStore.class.getDeclaredField("valueStoreWal");
		walField.setAccessible(true);
		ValueStoreWAL wal = (ValueStoreWAL) walField.get(nss);
		ValueStoreWalConfig walCfg = wal.config();

		assertThat(walCfg.maxSegmentBytes()).isEqualTo(1 << 20);
		assertThat(walCfg.queueCapacity()).isEqualTo(1234);
		assertThat(walCfg.batchBufferBytes()).isEqualTo(1 << 14);
		assertThat(walCfg.syncPolicy()).isEqualTo(ValueStoreWalConfig.SyncPolicy.ALWAYS);
		assertThat(walCfg.syncInterval().toMillis()).isEqualTo(50);
		assertThat(walCfg.idlePollInterval().toMillis()).isEqualTo(5);
		Path expectedWalDir = dataDir.toPath().resolve("custom-wal-dir");
		assertThat(walCfg.walDirectory()).isEqualTo(expectedWalDir);
		assertThat(walCfg.snapshotsDirectory()).isEqualTo(expectedWalDir.resolve("snapshots"));
		assertThat(walCfg.syncBootstrapOnOpen()).isTrue();
	}

	@Test
	void disablesWalWhenConfigured() throws Exception {
		NativeStoreConfig cfg = new NativeStoreConfig("spoc");
		cfg.setWalEnabled(false);

		NativeStoreFactory factory = new NativeStoreFactory();
		NativeStore sail = (NativeStore) factory.getSail(cfg);
		sail.setDataDir(dataDir);
		sail.init();
		try {
			SailStore sailStore = sail.getSailStore();
			Field backingField = SnapshotSailStore.class.getDeclaredField("backingStore");
			backingField.setAccessible(true);
			NativeSailStore nss = (NativeSailStore) backingField.get(sailStore);

			Field walField = NativeSailStore.class.getDeclaredField("valueStoreWal");
			walField.setAccessible(true);
			Object wal = walField.get(nss);
			assertThat(wal).as("WAL should be disabled when walEnabled=false").isNull();

			Path walDir = dataDir.toPath().resolve(ValueStoreWalConfig.DEFAULT_DIRECTORY_NAME);
			assertThat(Files.exists(walDir)).isFalse();
		} finally {
			sail.shutDown();
		}
	}
}