Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0f062cd
fix: don't lock when deleting directories
cmhulbert Mar 6, 2026
1de4a7b
fix: ensure the VolatileReadData closes on InputStream close
cmhulbert Mar 9, 2026
e463dc1
fix: ignore UncheckedIOException too for `closeQuietly`
cmhulbert Mar 13, 2026
957a1ed
fix: don't throw exception after a successful read of data from a cha…
cmhulbert Mar 13, 2026
eab40ee
fix: close order inside-out
cmhulbert Mar 13, 2026
6f78537
refactor!: block -> chunk, shard -> block
bogovicj Mar 16, 2026
ba4778b
feat: add getChunkSize
bogovicj Mar 16, 2026
b03deff
fix: ShardTest uses getChunkSize
bogovicj Mar 16, 2026
4ceb118
doc: writeRegion and DataBlockSupplier
bogovicj Mar 16, 2026
fe52660
feat: add N5Writer.deleteBlock
bogovicj Mar 19, 2026
944a3ea
test: AbstractN5Test changes related to chunk-block refactor
bogovicj Mar 21, 2026
3587f6b
test: AbstractN5Test back to using read/writeBlock
bogovicj Mar 23, 2026
156d92c
clean up and fix typos
tpietzsch Mar 24, 2026
908c42c
Avoid stream creation
tpietzsch Mar 24, 2026
a6ffbc5
fix javadoc and argument names
tpietzsch Mar 24, 2026
59160b5
fix javadoc and argument names
tpietzsch Mar 25, 2026
d5e7510
fix javadoc
tpietzsch Mar 25, 2026
75535e2
clean up redundant modifiers
tpietzsch Mar 25, 2026
d3eaf76
fix javadoc and argument names
tpietzsch Mar 25, 2026
96cb3f5
renaming in Nesting and DefaultDatasetAccess
tpietzsch Mar 25, 2026
2430d03
variable renaming and javadoc fixes
tpietzsch Mar 25, 2026
1b0cb82
rename variables, comments, minor fixes
tpietzsch Mar 26, 2026
a0bca79
Make getDatasetAccess() protected and createDatasetAccess() private
tpietzsch Mar 26, 2026
ede0a37
more renaming in comments
tpietzsch Mar 26, 2026
f9c0ea3
test: flesh out WriteRegionTest
bogovicj Mar 30, 2026
c72e467
perf: Don't use `Files.copy` for Unsafe. It's much slower than writin…
cmhulbert Mar 3, 2026
b39bd0b
perf: don't truncate separately over the file channel when writing, j…
cmhulbert Mar 3, 2026
61631c1
perf: only try to create directory if parent is not already a directo…
cmhulbert Mar 3, 2026
fdf451e
refactor: make explicit the use of the FileLock in ChannelLock
cmhulbert Mar 3, 2026
d175b54
fix: continue-on-close-failure behavior needs to work for successful …
cmhulbert Apr 3, 2026
c3a815d
Add SliceTrackingLazyRead
tpietzsch Feb 2, 2026
639f6cc
feat: add Range.aggregate
bogovicj Feb 2, 2026
861dfa1
simplify
tpietzsch Apr 3, 2026
f702c71
feat: make SliceTrackingLazyRead abstract, add two implementations
bogovicj Feb 2, 2026
d79dec3
wip: toward using prefetching
bogovicj Feb 9, 2026
ede576d
refactor: migrate previous batching/aggregate work to current develop…
cmhulbert Mar 26, 2026
8d96319
clean up
tpietzsch Apr 3, 2026
a7ef2f1
Make SliceTrackingLazyRead non-abstract
tpietzsch Apr 3, 2026
ee8612c
rename prefetching LazyReads to "...PrefetchLazyRead"
tpietzsch Apr 3, 2026
a3e707b
Don't include ranges that are already materialized in the aggregation
tpietzsch Apr 3, 2026
90ddf5d
fix: setDatasetAttributes no long overwrites user attributes
bogovicj Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.janelia.saalfeldlab.n5;

import org.apache.commons.io.input.ProxyInputStream;
import org.janelia.saalfeldlab.n5.readdata.ReadData;
import org.janelia.saalfeldlab.n5.readdata.VolatileReadData;

import java.io.*;

Expand All @@ -23,7 +25,15 @@ public Reader newReader() throws N5Exception.N5IOException {

@Override
public InputStream newInputStream() throws N5Exception.N5IOException {
return kva.createReadData(key).inputStream();

VolatileReadData volatileReadData = kva.createReadData(key);
return new ProxyInputStream(volatileReadData.inputStream()) {
@Override
public void close() throws IOException {
super.close();
volatileReadData.close();
}
};
}

@Override
Expand Down
38 changes: 10 additions & 28 deletions src/main/java/org/janelia/saalfeldlab/n5/ChannelLock.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

/**
* Holds a channel and system-level file lock (shared for writing, non-shared
Expand All @@ -17,11 +16,14 @@
class ChannelLock implements Closeable {

private final FileChannel channel;
private final FileLock lock;

//Used to ensure a hard reference exists until we close
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private final FileLock unusedHardRef;

private ChannelLock(final FileChannel channel, final FileLock lock) {
this.channel = channel;
this.lock = lock;
this.unusedHardRef = lock;
}

public void close() throws IOException {
Expand Down Expand Up @@ -55,7 +57,7 @@ FileChannel getChannel() {
*/
static ChannelLock lock(final Path path, final boolean forWriting) throws IOException {

final FileChannel channel = openFileChannel(path, forWriting);
final FileChannel channel = FsIoPolicy.openFileChannel(path, forWriting);
try {
while (true) {
try {
Expand Down Expand Up @@ -93,7 +95,7 @@ static ChannelLock tryLock(final Path path, final boolean forWriting) throws IOE

FileChannel channel = null;
try {
channel = openFileChannel(path, forWriting);
channel = FsIoPolicy.openFileChannel(path, forWriting);
final FileLock lock = channel.tryLock(0, Long.MAX_VALUE, !forWriting);
return lock == null ? null : new ChannelLock(channel, lock);
} catch (Exception e) {
Expand All @@ -102,32 +104,12 @@ static ChannelLock tryLock(final Path path, final boolean forWriting) throws IOE
}
}

/**
* Opens a file channel. If the channel is opened {@code forWriting},
* then this may create the file and the parent directories as needed.
*
* @throws IOException
* if the channel cannot be opened
*/
private static FileChannel openFileChannel(final Path path, final boolean forWriting) throws IOException {

if (forWriting) {
final Path parent = path.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
return FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
} else {
return FileChannel.open(path, StandardOpenOption.READ);
}
}

private static void closeQuietly(final FileChannel fileChannel) {
if (fileChannel != null) {
try {
fileChannel.close();
} catch (final IOException ignored) {
} catch (final IOException | UncheckedIOException ignored) {
}
}
}
}
}
1 change: 1 addition & 0 deletions src/main/java/org/janelia/saalfeldlab/n5/DataType.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

/**
* Enumerates available data types.
*
Expand Down
34 changes: 19 additions & 15 deletions src/main/java/org/janelia/saalfeldlab/n5/DatasetAttributes.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ public class DatasetAttributes implements Serializable {

private final long[] dimensions;

// number of samples per chunk per dimension
private final int[] chunkSize;

// number of samples per block per dimension
// identical to chunkSize for non-sharded datasets
private final int[] blockSize;

// TODO add a getter?
// the shard size
private final int[] outerBlockSize;

private final DataType dataType;

private final JsonElement defaultValue;
Expand All @@ -108,7 +108,7 @@ public class DatasetAttributes implements Serializable {

public DatasetAttributes(
final long[] dimensions,
final int[] outerBlockSize,
final int[] blockSize,
final DataType dataType,
final JsonElement defaultValue,
final BlockCodecInfo blockCodecInfo,
Expand All @@ -117,7 +117,7 @@ public DatasetAttributes(

this.dimensions = dimensions;
this.dataType = dataType;
this.outerBlockSize = outerBlockSize;
this.blockSize = blockSize;
this.defaultValue = defaultValue == null ? JsonNull.INSTANCE : defaultValue;

this.blockCodecInfo = blockCodecInfo == null ? defaultBlockCodecInfo() : blockCodecInfo;
Expand All @@ -131,7 +131,7 @@ public DatasetAttributes(
.toArray(DataCodecInfo[]::new);

access = createDatasetAccess();
blockSize = access.getGrid().getBlockSize(0);
chunkSize = access.getGrid().getBlockSize(0);
}

public DatasetAttributes(
Expand Down Expand Up @@ -189,7 +189,7 @@ public DatasetAttributes(
this(dimensions, blockSize, dataType, new DataCodecInfo[0]);
}

protected DatasetAccess<?> createDatasetAccess() {
private DatasetAccess<?> createDatasetAccess() {

final int m = nestingDepth(blockCodecInfo);

Expand All @@ -200,7 +200,7 @@ protected DatasetAccess<?> createDatasetAccess() {
// NestedGrid validates block sizes, so instantiate it before creating the blockCodecs
// blockCodecInfo.create below could fail unexpecedly with invalid
// blockSizes so validate first
blockSizes[m - 1] = outerBlockSize;
blockSizes[m - 1] = blockSize;
BlockCodecInfo tmpInfo = blockCodecInfo;
for (int l = m - 1; l > 0; --l) {
final ShardCodecInfo info = (ShardCodecInfo)tmpInfo;
Expand All @@ -210,7 +210,7 @@ protected DatasetAccess<?> createDatasetAccess() {

BlockCodecInfo currentBlockCodecInfo = blockCodecInfo;
DataCodecInfo[] currentDataCodecInfos = dataCodecInfos;

DatasetCodecInfo[] datasetCodecInfos = this.datasetCodecInfos;

final NestedGrid grid = new NestedGrid(blockSizes, dimensions);
Expand Down Expand Up @@ -274,6 +274,11 @@ public int getNumDimensions() {
return dimensions.length;
}

public int[] getChunkSize() {

return chunkSize;
}

public int[] getBlockSize() {

return blockSize;
Expand Down Expand Up @@ -318,9 +323,9 @@ public DataType getDataType() {
*
* @return the {@code DatasetAccess} for this dataset
*/
<T> DatasetAccess<T> getDatasetAccess() {
protected <T> DatasetAccess<T> getDatasetAccess() {

return (DatasetAccess<T>)access;
return (DatasetAccess<T>) access;
}

/**
Expand All @@ -334,7 +339,6 @@ public NestedGrid getNestedBlockGrid() {
return getDatasetAccess().getGrid();
}


public BlockCodecInfo getBlockCodecInfo() {

return blockCodecInfo;
Expand All @@ -359,7 +363,7 @@ public HashMap<String, Object> asMap() {

final HashMap<String, Object> map = new HashMap<>();
map.put(DIMENSIONS_KEY, dimensions);
map.put(BLOCK_SIZE_KEY, blockSize);
map.put(BLOCK_SIZE_KEY, chunkSize);
map.put(DATA_TYPE_KEY, dataType);
map.put(COMPRESSION_KEY, getCompression());
return map;
Expand Down Expand Up @@ -425,7 +429,7 @@ public static class DatasetAttributesAdapter implements JsonSerializer<DatasetAt

final JsonObject obj = new JsonObject();
obj.add(DIMENSIONS_KEY, context.serialize(src.dimensions));
obj.add(BLOCK_SIZE_KEY, context.serialize(src.blockSize));
obj.add(BLOCK_SIZE_KEY, context.serialize(src.chunkSize));
obj.add(DATA_TYPE_KEY, context.serialize(src.dataType));

final DataCodecInfo[] codecs = src.dataCodecInfos;
Expand Down
71 changes: 68 additions & 3 deletions src/main/java/org/janelia/saalfeldlab/n5/FsIoPolicy.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.*;

Expand All @@ -29,11 +31,65 @@ else if (length >= 0 && offset + length > channelSize)
return true;
}

/**
* Opens a file channel. If the channel is opened {@code forWriting},
* then this may create the file and the parent directories as needed.
*
* @throws IOException
* if the channel cannot be opened
*/
static FileChannel openFileChannel(final Path path, final boolean forWriting) throws IOException {

if (forWriting) {
final Path parent = path.getParent();
/* if not null and not directory, it will call `createDirectories` but we expect it to throw an IOException */
if (parent != null && !parent.toFile().isDirectory()) {
Files.createDirectories(parent);
}
return FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} else {
return FileChannel.open(path, StandardOpenOption.READ);
}
}


/**
* This method is necessary to handle the situtation where writing is successful, but `close` fails on the file channel.
* This has been observed to happen fairly consistently on MacOS when writing to a file mounted over SMB.
*
* @param readData to write to the {@code Path}
* @param path to write to
* @throws IOException if writing failed.
*/
private static void writeToPathIgnoreCloseException(ReadData readData, Path path) throws IOException {

FileChannel channel = openFileChannel(path, true);
OutputStream os = Channels.newOutputStream(channel);

try {
readData.writeTo(os);
os.flush();
channel.force(true);
} catch (Throwable e) {
os.close();
channel.close();
throw e;
}

/* if we get here, the write succeeded, and the os/channel may not be closed yet */
try {
os.close();
channel.close();
} catch (IOException | UncheckedIOException ignore) {
/* Ignore; we know the data was written already. */
}
}

public static class Unsafe implements IoPolicy {
@Override
public void write(String key, ReadData readData) throws IOException {
final Path path = Paths.get(key);
Files.copy(readData.inputStream(), path, StandardCopyOption.REPLACE_EXISTING);
writeToPathIgnoreCloseException(readData, path);
}

@Override
Expand Down Expand Up @@ -69,6 +125,8 @@ public VolatileReadData read(String key) throws IOException {
@Override
public void delete(final String key) throws IOException {
final Path path = Paths.get(key);
if (!Files.isRegularFile(path))
Files.delete(path);
try (LockedFileChannel ignore = FILE_LOCK_MANAGER.lockForWriting(path)) {
Files.delete(path);
}
Expand Down Expand Up @@ -110,6 +168,7 @@ public ReadData materialize(final long offset, final long length) {
throw new N5Exception.N5IOException("FileLazyRead is already closed.");
}

ReadData readData = null;
try (final FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {

channel.position(offset);
Expand All @@ -127,13 +186,19 @@ public ReadData materialize(final long offset, final long length) {
final byte[] data = new byte[(int) size];
final ByteBuffer buf = ByteBuffer.wrap(data);
channel.read(buf);
return ReadData.from(data);
readData = ReadData.from(data);

} catch (final NoSuchFileException e) {
throw new N5Exception.N5NoSuchKeyException("No such file", e);
} catch (IOException | UncheckedIOException e) {
throw new N5Exception.N5IOException(e);
/* Occasionally (frequently for some source remote mounted file systems) this can throw exceptions during
* `channel.close()` which is called automatically in the try-with-resources block. In this case, we have
* successfully read the data, and we can return it, and ignore the exception.
* */
if (readData == null)
throw new N5Exception.N5IOException(e);
}
return readData;
}

@Override
Expand Down
Loading
Loading