Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -68,7 +68,6 @@
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
Expand All @@ -84,6 +83,7 @@
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.concurrent.locks.Lock;
import java.util.stream.Stream;

import org.janelia.saalfeldlab.n5.readdata.ReadData;
Expand All @@ -103,21 +103,51 @@ public class FileSystemKeyValueAccess implements KeyValueAccess {
* {@link FileSystem} supports that. If the {@link FileSystem} does not
* support locking, it returns immediately.
*/
protected class LockedFileChannel implements LockedChannel {
protected class LockedFileChannel implements LockedChannel, AutoCloseable {

protected final FileChannel channel;

protected Lock lock;

protected LockedFileChannel(final String path, final boolean readOnly) throws IOException {

this(fileSystem.getPath(path), readOnly);
}

protected LockedFileChannel(final Path path, final boolean readOnly) throws IOException {

final String key = path.toAbsolutePath().toString();
boolean haveLock = false;
while( !haveLock ) {

if (readOnly) {
lock = keyLock.tryLockForReading(key);
}
else {
lock = keyLock.tryLockForWriting(key);
}

if (lock != null) {
haveLock = true;
break;
}

try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new N5Exception(e);
}
}

final OpenOption[] options;
if (readOnly) {
options = new OpenOption[]{StandardOpenOption.READ};
channel = FileChannel.open(path, options);
try {
channel = FileChannel.open(path, options);
} catch (final NoSuchFileException e) {
lock.unlock();
throw e;
}
} else {
options = new OpenOption[]{StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.CREATE};
Expand All @@ -132,20 +162,6 @@ protected LockedFileChannel(final Path path, final boolean readOnly) throws IOEx
}
}

for (boolean waiting = true; waiting;) {
waiting = false;
try {
channel.lock(0L, Long.MAX_VALUE, readOnly);
} catch (final OverlappingFileLockException e) {
waiting = true;
try {
Thread.sleep(100);
} catch (final InterruptedException f) {
waiting = false;
Thread.currentThread().interrupt();
}
} catch (final IOException e) {}
}
}

protected FileChannel getFileChannel() {
Expand Down Expand Up @@ -191,11 +207,13 @@ public OutputStream newOutputStream() throws N5IOException {

@Override
public void close() throws IOException {

lock.unlock();
channel.close();
}
}

protected final KeyLock keyLock;

protected final FileSystem fileSystem;

/**
Expand All @@ -206,6 +224,12 @@ public void close() throws IOException {
public FileSystemKeyValueAccess(final FileSystem fileSystem) {

this.fileSystem = fileSystem;
keyLock = new KeyLock();
}

public KeyLock getLocks() {

return keyLock;
}

@Override
Expand Down
180 changes: 180 additions & 0 deletions src/main/java/org/janelia/saalfeldlab/n5/KeyLock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*-
* #%L
* Not HDF5
* %%
* Copyright (C) 2017 - 2025 Stephan Saalfeld
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
package org.janelia.saalfeldlab.n5;

import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.Lock;

/**
* A lock manager that provides thread-safe read/write locking for keys.
*
* This class manages a set of {@link ReentrantReadWriteLock}s, one per key,
* allowing multiple threads to read the same key simultaneously while ensuring
* exclusive access for writes.
*
* Unlike file locks which operate at the process level, this provides
* thread-level locking within a single JVM.
*/
public class KeyLock {

private final ConcurrentHashMap<String, ReentrantReadWriteLock> locks = new ConcurrentHashMap<>();

/**
* Acquires a read lock for the specified key. Multiple threads can hold
* read locks for the same key simultaneously.
*
* @param key
* the key to lock for reading
* @return a {@link Lock} that must be unlocked when done
*/
public Lock lockForReading(String key) {

ReentrantReadWriteLock rwLock = locks.computeIfAbsent(key, k -> new ReentrantReadWriteLock());
Lock readLock = rwLock.readLock();
readLock.lock();
return readLock;
}

/**
* Acquires a write lock for the specified key. Only one thread can hold a
* write lock for a key at a time.
*
* @param key
* the key to lock for writing
* @return a {@link Lock} that must be unlocked when done
*/
public Lock lockForWriting(String key) {

ReentrantReadWriteLock rwLock = locks.computeIfAbsent(key, k -> new ReentrantReadWriteLock());
Lock writeLock = rwLock.writeLock();
writeLock.lock();
return writeLock;
}

/**
* Attempts to acquire a read lock for the specified key without blocking.
*
* @param key
* the key to lock for reading
* @return a {@link Lock} if the lock was acquired, null otherwise
*/
public Lock tryLockForReading(String key) {

ReentrantReadWriteLock rwLock = locks.computeIfAbsent(key, k -> new ReentrantReadWriteLock());
Lock readLock = rwLock.readLock();
if (readLock.tryLock()) {
return readLock;
}
return null;
}

/**
* Attempts to acquire a write lock for the specified key without blocking.
*
* @param key
* the key to lock for writing
* @return a {@link Lock} if the lock was acquired, null otherwise
*/
public Lock tryLockForWriting(String key) {

ReentrantReadWriteLock rwLock = locks.computeIfAbsent(key, k -> new ReentrantReadWriteLock());
Lock writeLock = rwLock.writeLock();
if (writeLock.tryLock()) {
return writeLock;
}
return null;
}

/**
* Returns the number of keys currently being tracked.
*
* @return the number of keys with associated locks
*/
public int size() {

return locks.size();
}

/**
* Removes the lock for a key if it is not currently held. This can be used
* to clean up unused locks to prevent memory leaks.
*
* @param key
* the key whose lock should be removed
* @return true if the lock was removed, false if it's currently in use
*/
public boolean removeLockIfUnused(String key) {

ReentrantReadWriteLock rwLock = locks.get(key);
if (rwLock != null && !rwLock.isWriteLocked() && rwLock.getReadLockCount() == 0) {
return locks.remove(key, rwLock);
}
return false;
}

/**
* Returns an {@link Optional} containing the lock for the given
* key, if it exists.
* <p>
* This method can be useful for monitoring and debugging. For example,
* to check how many threads are currently reading a key:
* <pre>{@code
* keyLock.getKeyLock("myKey").ifPresent(lock -> {
* System.out.println("Key 'myKey' has " + lock.getReadLockCount() + " readers");
* });
* }</pre>
*
* @param key
* the key whose lock will be returned
* @return an {@link Optional} containing the {@link ReentrantReadWriteLock}
* for the key, or empty if no lock exists for the key
*/
public Optional<ReentrantReadWriteLock> getKeyLock(String key) {
return Optional.ofNullable(locks.get(key));
}

/**
* Clears all unused locks from the lock map. Locks that are currently held
* will not be removed.
*
* @return the number of locks that were removed
*/
public int clearUnusedLocks() {

int removed = 0;
for (String key : locks.keySet()) {
if (removeLockIfUnused(key)) {
removed++;
}
}
return removed;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
import org.janelia.saalfeldlab.n5.N5Exception.N5IOException;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
Expand Down
Loading