Skip to content

fix: attempt to read without locking when locking fails; Some file sy… - #205

Merged
bogovicj merged 11 commits into
batchingfrom
fix/fsLockUnsupported
Feb 11, 2026
Merged

fix: attempt to read without locking when locking fails; Some file sy…#205
bogovicj merged 11 commits into
batchingfrom
fix/fsLockUnsupported

Conversation

@cmhulbert

Copy link
Copy Markdown
Contributor

See #204 for more details.

@cmhulbert

cmhulbert commented Feb 5, 2026

Copy link
Copy Markdown
Contributor Author

@tpietzsch currently this handles the reading case in a way I'm happy with.

However, I think the batching branch is missing an atomic write method that uses VolatileReadData. perhaps a void write(String path, ReadData readData); method? If we have that, than we can apply similar logic there to attempt writing without locks. That may be trickier, in that we don't want to write if someone already has the lock, just if the operation is not supported.

another this I did is remove the try / catch / return null for createReadData. It can't return null if we use it in try-with-resource blocks or you get a NPE. And we DO handle already N5NoSuchKeyException and return null in places that call createReadData so we should just the N5NoSuchKeyException bubble up to the ultimate caller.

@cmhulbert
cmhulbert requested a review from tpietzsch February 5, 2026 16:51
@cmhulbert cmhulbert added this to the 4.0.0 Release milestone Feb 5, 2026
@cmhulbert cmhulbert added the bug label Feb 5, 2026
@tpietzsch

Copy link
Copy Markdown
Collaborator

another this I did is remove the try / catch / return null for createReadData. It can't return null if we use it in try-with-resource blocks or you get a NPE. And we DO handle already N5NoSuchKeyException and return null in places that call createReadData so we should just the N5NoSuchKeyException bubble up to the ultimate caller.

That isn't true. There will be no NPE in try-with-resources if the Closable is null.
At least this works for me:

public class ClosableNPETest {
	static class MyClosable implements AutoCloseable {
		@Override
		public void close() {
			System.out.println("MyClosable.close");
		}
	}

	static MyClosable create() {
//		return new MyClosable();
		return null; // this works fine
	}

	public static void main(String[] args) {
		try (MyClosable c = create()) {
			System.out.println("doing something with " + c);
		}
		System.out.println("done");
	}
}

this(path, true);
}

FileLazyRead(final Path path, final Boolean requireLock ) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the boxing? This should be boolean instead of Boolean.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No good reason, just didn't notice. I agree

@cmhulbert

Copy link
Copy Markdown
Contributor Author

another this I did is remove the try / catch / return null for createReadData. It can't return null if we use it in try-with-resource blocks or you get a NPE. And we DO handle already N5NoSuchKeyException and return null in places that call createReadData so we should just the N5NoSuchKeyException bubble up to the ultimate caller.

That isn't true. There will be no NPE in try-with-resources if the Closable is null. At least this works for me:

public class ClosableNPETest {
	static class MyClosable implements AutoCloseable {
		@Override
		public void close() {
			System.out.println("MyClosable.close");
		}
	}

	static MyClosable create() {
//		return new MyClosable();
		return null; // this works fine
	}

	public static void main(String[] args) {
		try (MyClosable c = create()) {
			System.out.println("doing something with " + c);
		}
		System.out.println("done");
	}
}

Your example is too trivial. The NPE arrises because the block is still executed. in your example, you are only using c as String + null which is fine. but if you try to call a method on c in the block, you will get an NPE, and that is always the case that we want to use methods on the Closable returned from the createReadData method.

modifying your example, and the output:

public static void main(String[] args) {
	try (MyClosable c = create()) {
		System.out.println("doing something with " + c);
		c.toString();
	}
	System.out.println("done");
}
doing something with null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Object.toString()" because "c" is null
	at org.janelia.saalfeldlab.n5.N5Writer$ClosableNPETest.main(N5Writer.java:485)

@tpietzsch

Copy link
Copy Markdown
Collaborator

Oh, then I misunderstood.

It can't return null if we use it in try-with-resource blocks or you get a NPE.

I thought that you mean the try-with-resources will try to close() the null. Instead you just meant

It can't return null if we use it in try-with-resource blocks or you get a NPE.

But then there is no problem.
The null behaviour is documented

* If the requested key does not exist, either {@code null} is returned or a
* lazy {@code VolatileReadData} that will throw {@code N5NoSuchKeyException}
* when trying to materialize.

and I explicitly decided to do it this way ("this way" meaning that N5NoSuchKeyException will not be thrown by createReadData.) I considered the alternative, and code like this:
final ReadData modifiedData;
try (final VolatileReadData existingData = pva.get(key)) {
modifiedData = writeBlockRecursive(existingData, dataBlock, position, grid.numLevels() - 1);
// Here, we are about to write the shard data, but with the new block modified.
// Need to make sure that the read operations happen now before pva.set acquires a write lock
modifiedData.materialize();
}
pva.set(key, modifiedData);

would become a lot more convoluted. (The code in the try block needs to run, even if there is no existingReadData.)

If we decide to let pva.get(key) throw the N5NoSuchKeyException, then the easiest solution would be to just wrap it in a static method that turns it into null, replicating the current behaviour for ease of use in DefaultDatasetAccess. That seems pointless.

@bogovicj

bogovicj commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

We (@cmhulbert , @tpietzsch ) discussed having PositionValueAccess catching the NoSuchKeyException from the wrapped KeyValueAccess and returning null

…stem mount circumstances can lead to files being read/writeable, but not lockable. In these cases we should attempt to read, without locking, and immediately materialize as a best-effort attempt at ensuring valid data

Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
…ead/write operations

Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
…, so we handle it as `null` in PositionValueAccess

Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
…ReadData` and `write`

Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
… regex

Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
@cmhulbert
cmhulbert force-pushed the fix/fsLockUnsupported branch from ff03d33 to 760e5b6 Compare February 10, 2026 21:14
Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
…Access. In practice, it was not consistently used. Most `Paths` and `Files` static methods internally get a file system from the FileSystemProvider

Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
@cmhulbert
cmhulbert requested a review from tpietzsch February 10, 2026 21:26
@Override
public VolatileReadData get(final long[] key) throws N5IOException {
return kva.createReadData(absolutePath(key));
try {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KVA throws N5NoSuchFileException, so we catch/return null for PositionKVA

public FileSystemKeyValueAccess(final FileSystem fileSystem) {

this.fileSystem = fileSystem;
private static final IoPolicy ioPolicy = getIoPolicy();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relevant code for configuring what I'm calling IoPolicy. currently support strict atomic, no locking attempt unsafe and fallback which tries with atomic, but tries again with unsafe if it fails.

*/
public class FileSystemKeyValueAccess implements KeyValueAccess {

protected final FileSystem fileSystem;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove fileSystem field. It was used inconsistently. Most file calls used Files or Paths which get the file system internally

}
}
}
} catch (NoSuchFileException ignore) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is new; previously we threw an exception on NoSuchFile. My opinion is we should just return without complaining, since the file does not exist, which is the intention of delete.

* if a locked channel could not be created
* @deprecated migrate to {@link KeyValueAccess#createReadData(String)}
*/
@Deprecated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deprecated lockForReading, lockForWriting

Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
private static final IoPolicy ioPolicy = getIoPolicy();

private static IoPolicy getIoPolicy() {
String property = System.getProperty("n5.ioPolicy");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Longer term, I'd prefer that this property be configurable at a per-KVA instance (or maybe per KVA-class [1]) level rather than at a per-JVM level.

That being said, it won't bother me too much if this is how we do it for this release.

Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
@bogovicj
bogovicj merged commit 8ab32fc into batching Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants