Skip to content

Commit 7b68e47

Browse files
authored
Merge pull request #195 from saalfeldlab/fix/threadLocking
Fix/thread locking
2 parents 38a1743 + 1724dc5 commit 7b68e47

8 files changed

Lines changed: 1164 additions & 175 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package org.janelia.saalfeldlab.n5;
2+
3+
import java.io.Closeable;
4+
import java.io.IOException;
5+
import java.nio.channels.FileChannel;
6+
import java.nio.channels.FileLock;
7+
import java.nio.channels.OverlappingFileLockException;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.nio.file.StandardOpenOption;
11+
12+
/**
13+
* Holds a channel and system-level file lock (shared for writing, non-shared
14+
* for reading) and keeps it open until this {@code ChannelLock} is {@link
15+
* #close() closed}.
16+
*/
17+
class ChannelLock implements Closeable {
18+
19+
private final FileChannel channel;
20+
private final FileLock lock;
21+
22+
private ChannelLock(final FileChannel channel, final FileLock lock) {
23+
this.channel = channel;
24+
this.lock = lock;
25+
}
26+
27+
public void close() throws IOException {
28+
29+
// NB: We do not call lock.release() here, because it may throw an
30+
// exception if the channel is already closed. Instead, we just close
31+
// the channel. This will automatically release the lock. (And it is ok
32+
// to close an already closed channel.)
33+
34+
channel.close();
35+
}
36+
37+
FileChannel getChannel() {
38+
return channel;
39+
}
40+
41+
/**
42+
* Create a {@link FileChannel} on the given {@code path} and lock it with a
43+
* system-level {@link FileLock}. If there is an existing overlapping file
44+
* lock, this method will block until the existing lock is released and the
45+
* channel could be locked (by us).
46+
* <p>
47+
* The {@code FileLock} is exclusive if the {@code path} is locked {@code
48+
* forWriting}, and shared otherwise.
49+
* <p>
50+
* If the {@code path} is locked {@code forWriting} non-existing file and
51+
* the parent directories are created as needed.
52+
*
53+
* @throws IOException if an error occurs while opening the channel, or if
54+
* the calling thread is interrupted while waiting for the {@code FileLock}.
55+
*/
56+
static ChannelLock lock(final Path path, final boolean forWriting) throws IOException {
57+
58+
final FileChannel channel = openFileChannel(path, forWriting);
59+
try {
60+
while (true) {
61+
try {
62+
final FileLock lock = channel.lock(0, Long.MAX_VALUE, !forWriting);
63+
return new ChannelLock(channel, lock);
64+
} catch (final OverlappingFileLockException e) {
65+
try {
66+
Thread.sleep(100);
67+
} catch (final InterruptedException ie) {
68+
Thread.currentThread().interrupt();
69+
throw new IOException("Interrupted while waiting for file lock", ie);
70+
}
71+
}
72+
}
73+
} catch (Exception e) {
74+
closeQuietly(channel);
75+
throw e;
76+
}
77+
}
78+
79+
/**
80+
* Create a {@link FileChannel} on the given {@code path} and try to lock it
81+
* with a system-level {@link FileLock}. If the channel cannot be locked,
82+
* {@code null} is returned.
83+
* <p>
84+
* The {@code FileLock} is exclusive if the {@code path} is locked {@code
85+
* forWriting}, and shared otherwise.
86+
* <p>
87+
* If the {@code path} is locked {@code forWriting} non-existing file and
88+
* the parent directories are created as needed.
89+
*
90+
* @throws IOException if an error occurs while opening the channel.
91+
*/
92+
static ChannelLock tryLock(final Path path, final boolean forWriting) throws IOException {
93+
94+
FileChannel channel = null;
95+
try {
96+
channel = openFileChannel(path, forWriting);
97+
final FileLock lock = channel.tryLock(0, Long.MAX_VALUE, !forWriting);
98+
return lock == null ? null : new ChannelLock(channel, lock);
99+
} catch (Exception e) {
100+
closeQuietly(channel);
101+
throw e;
102+
}
103+
}
104+
105+
/**
106+
* Opens a file channel. If the channel is opened {@code forWriting},
107+
* then this may create the file and the parent directories as needed.
108+
*
109+
* @throws IOException
110+
* if the channel cannot be opened
111+
*/
112+
private static FileChannel openFileChannel(final Path path, final boolean forWriting) throws IOException {
113+
114+
if (forWriting) {
115+
final Path parent = path.getParent();
116+
if (parent != null) {
117+
Files.createDirectories(parent);
118+
}
119+
return FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
120+
} else {
121+
return FileChannel.open(path, StandardOpenOption.READ);
122+
}
123+
}
124+
125+
private static void closeQuietly(final FileChannel fileChannel) {
126+
if (fileChannel != null) {
127+
try {
128+
fileChannel.close();
129+
} catch (final IOException ignored) {
130+
}
131+
}
132+
}
133+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/*-
2+
* #%L
3+
* Not HDF5
4+
* %%
5+
* Copyright (C) 2017 - 2025 Stephan Saalfeld
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
package org.janelia.saalfeldlab.n5;
30+
31+
import java.io.IOException;
32+
import java.lang.ref.ReferenceQueue;
33+
import java.lang.ref.WeakReference;
34+
import java.nio.file.Path;
35+
import java.util.concurrent.ConcurrentHashMap;
36+
37+
/**
38+
* Provides thread-safe and process-safe read/write locking for filesystem paths.
39+
* Uses thread locks for JVM coordination and file locks for inter-process coordination.
40+
*/
41+
class FileKeyLockManager {
42+
43+
static final FileKeyLockManager FILE_LOCK_MANAGER = new FileKeyLockManager();
44+
45+
private FileKeyLockManager() {
46+
// singleton
47+
}
48+
49+
50+
private final ConcurrentHashMap<String, WeakValue> locks = new ConcurrentHashMap<>();
51+
52+
private final ReferenceQueue<KeyLockState> refQueue = new ReferenceQueue<>();
53+
54+
private static class WeakValue extends WeakReference<KeyLockState> {
55+
56+
final String key;
57+
58+
WeakValue(
59+
final String key,
60+
final KeyLockState value,
61+
final ReferenceQueue<KeyLockState> queue) {
62+
63+
super(value, queue);
64+
this.key = key;
65+
}
66+
}
67+
68+
/**
69+
* Remove entries from the cache whose references have been
70+
* garbage-collected.
71+
*/
72+
private void cleanUp()
73+
{
74+
while (true) {
75+
final WeakValue ref = (WeakValue) refQueue.poll();
76+
if (ref == null)
77+
break;
78+
locks.remove(ref.key, ref);
79+
}
80+
}
81+
82+
private KeyLockState keyLockState(final Path path) {
83+
84+
final String key = path.toAbsolutePath().toString();
85+
86+
cleanUp();
87+
88+
final WeakValue existingRef = locks.get(key);
89+
KeyLockState state = existingRef == null ? null : existingRef.get();
90+
if (state != null) {
91+
return state;
92+
}
93+
94+
final KeyLockState newState = new KeyLockState(path);
95+
while (state == null) {
96+
final WeakValue ref = locks.compute(key,
97+
(k, v) -> (v != null && v.get() != null)
98+
? v
99+
: new WeakValue(k, newState, refQueue));
100+
state = ref.get();
101+
}
102+
return state;
103+
}
104+
105+
/**
106+
* Acquires a read lock for the specified key. Multiple threads can hold
107+
* read locks for the same key simultaneously.
108+
* <p>
109+
* The first reader will acquire a shared file lock. Subsequent readers
110+
* only acquire the thread-level lock.
111+
*
112+
* @param path
113+
* the key (file path) to lock for reading
114+
* @return a {@link LockedChannel} that must be closed when done
115+
* @throws IOException
116+
* if acquiring the file lock fails
117+
*/
118+
public LockedFileChannel lockForReading(final Path path) throws IOException {
119+
120+
return keyLockState(path).acquireRead();
121+
}
122+
123+
/**
124+
* Acquires a write lock for the specified key. Only one thread can hold a
125+
* write lock for a key at a time, and no readers can hold locks.
126+
*
127+
* @param path
128+
* the file path to lock for writing
129+
* @return a {@link LockedChannel} that must be closed when done
130+
* @throws IOException
131+
* if acquiring the file lock fails
132+
*/
133+
public LockedFileChannel lockForWriting(final Path path) throws IOException {
134+
135+
return keyLockState(path).acquireWrite();
136+
}
137+
138+
/**
139+
* Attempts to acquire a read lock for the specified key without blocking.
140+
*
141+
* @param path
142+
* the file path to lock for reading
143+
* @return a {@link LockedChannel} if the lock was acquired, null otherwise
144+
*/
145+
public LockedFileChannel tryLockForReading(final Path path) {
146+
147+
return keyLockState(path).tryAcquireRead();
148+
}
149+
150+
/**
151+
* Attempts to acquire a write lock for the specified key without blocking.
152+
*
153+
* @param path
154+
* the file path to lock for writing
155+
* @return a {@link LockedChannel} if the lock was acquired, null otherwise
156+
*/
157+
public LockedFileChannel tryLockForWriting(final Path path) {
158+
159+
return keyLockState(path).tryAcquireWrite();
160+
}
161+
162+
/**
163+
* Returns the number of keys currently being tracked.
164+
*
165+
* @return the number of keys with associated locks
166+
*/
167+
int size() {
168+
169+
return locks.size();
170+
}
171+
172+
/**
173+
* Removes the lock state for a key if no locks are held.
174+
*
175+
* @param key the key whose lock state should be removed
176+
* @return true if removed, false if currently in use or not found
177+
*/
178+
public boolean removeLockIfUnused(final String key) {
179+
throw new UnsupportedOperationException("TODO. REMOVE");
180+
}
181+
}

0 commit comments

Comments
 (0)