HashFileSyncBehaviorTest.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.datastore;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import org.eclipse.rdf4j.common.io.NioFile;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
* Verifies that HashFile.sync(boolean) honors the caller's request to force metadata to disk by passing {@code true} to
* the underlying FileChannel#force(boolean) call. This must also hold when HashFile is constructed with
* {@code forceSync=true}.
*/
public class HashFileSyncBehaviorTest {
@TempDir
File tempDir;
private File hashFilePath;
@BeforeEach
public void setup() {
hashFilePath = new File(tempDir, "values.hash");
}
@AfterEach
public void tearDown() {
// nothing to close because individual tests close files explicitly when needed
}
@Test
public void syncFalseForcesFileContent_whenForceSyncDisabled() throws Exception {
try (HashFile hf = new HashFile(hashFilePath, /* forceSync= */ false, /* initialSize= */ 16)) {
TrackingFileChannel tracker = injectTrackingChannel(hf);
// Act
hf.sync(false);
// Assert: even without metadata, content must be flushed
assertThat(tracker.forceFalseCount)
.as("HashFile.sync(false) should call force(false) on the underlying channel when forceSync=false")
.isGreaterThan(0);
}
}
@Test
public void syncTrueHonorsMetadataFlag_whenForceSyncDisabled() throws Exception {
try (HashFile hf = new HashFile(hashFilePath, /* forceSync= */ false, /* initialSize= */ 16)) {
TrackingFileChannel tracker = injectTrackingChannel(hf);
// Act
hf.sync(true);
// Assert: must have at least one force(true) call
assertThat(tracker.forceTrueCount)
.as("HashFile.sync(true) should call force(true) on the underlying channel when forceSync=false")
.isGreaterThan(0);
}
}
@Test
public void syncTrueHonorsMetadataFlag_whenForceSyncEnabled() throws Exception {
try (HashFile hf = new HashFile(hashFilePath, /* forceSync= */ true, /* initialSize= */ 16)) {
TrackingFileChannel tracker = injectTrackingChannel(hf);
// Act
hf.sync(true);
// Assert: must have at least one force(true) call
assertThat(tracker.forceTrueCount)
.as("HashFile.sync(true) should call force(true) on the underlying channel when forceSync=true")
.isGreaterThan(0);
}
}
private static TrackingFileChannel injectTrackingChannel(HashFile hf) throws Exception {
// Access private final NioFile field on HashFile
Field nioFileField = HashFile.class.getDeclaredField("nioFile");
nioFileField.setAccessible(true);
NioFile nio = (NioFile) nioFileField.get(hf);
// Access private volatile FileChannel field on NioFile
Field fcField = NioFile.class.getDeclaredField("fc");
fcField.setAccessible(true);
FileChannel delegate = (FileChannel) fcField.get(nio);
TrackingFileChannel tracking = new TrackingFileChannel(delegate);
fcField.set(nio, tracking);
return tracking;
}
/**
* Delegating channel that tracks calls to force(boolean).
*/
static class TrackingFileChannel extends FileChannel {
final FileChannel delegate;
volatile int forceTrueCount = 0;
volatile int forceFalseCount = 0;
TrackingFileChannel(FileChannel delegate) {
this.delegate = delegate;
}
@Override
public void force(boolean metaData) throws IOException {
if (metaData) {
forceTrueCount++;
} else {
forceFalseCount++;
}
delegate.force(metaData);
}
// Delegations for abstract methods / other operations
@Override
public int read(ByteBuffer dst) throws IOException {
return delegate.read(dst);
}
@Override
public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
return delegate.read(dsts, offset, length);
}
@Override
public int write(ByteBuffer src) throws IOException {
return delegate.write(src);
}
@Override
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
return delegate.write(srcs, offset, length);
}
@Override
public long position() throws IOException {
return delegate.position();
}
@Override
public FileChannel position(long newPosition) throws IOException {
delegate.position(newPosition);
return this;
}
@Override
public long size() throws IOException {
return delegate.size();
}
@Override
public FileChannel truncate(long size) throws IOException {
delegate.truncate(size);
return this;
}
@Override
public void implCloseChannel() throws IOException {
delegate.close();
}
@Override
public int read(ByteBuffer dst, long position) throws IOException {
return delegate.read(dst, position);
}
@Override
public int write(ByteBuffer src, long position) throws IOException {
return delegate.write(src, position);
}
@Override
public long transferTo(long position, long count, java.nio.channels.WritableByteChannel target)
throws IOException {
return delegate.transferTo(position, count, target);
}
@Override
public long transferFrom(java.nio.channels.ReadableByteChannel src, long position, long count)
throws IOException {
return delegate.transferFrom(src, position, count);
}
@Override
public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
return delegate.map(mode, position, size);
}
@Override
public FileLock lock(long position, long size, boolean shared) throws IOException {
return delegate.lock(position, size, shared);
}
@Override
public FileLock tryLock(long position, long size, boolean shared) throws IOException {
return delegate.tryLock(position, size, shared);
}
}
}