Skip to content

Commit 32ea434

Browse files
cmhulberttpietzsch
authored andcommitted
feat: add cleanup to FileLockKeyManager so stale references still unlock
refactor: FileKeyLockManager is a static singleton now, and more KeyLockState logic is package-private Signed-off-by: Caleb Hulbert <cmhulbert@gmail.com>
1 parent f05f547 commit 32ea434

6 files changed

Lines changed: 197 additions & 474 deletions

File tree

src/main/java/org/janelia/saalfeldlab/n5/FileKeyLockManager.java

Lines changed: 85 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -29,47 +29,87 @@
2929
package org.janelia.saalfeldlab.n5;
3030

3131
import java.io.IOException;
32+
import java.lang.ref.PhantomReference;
33+
import java.lang.ref.Reference;
34+
import java.lang.ref.ReferenceQueue;
3235
import java.nio.channels.FileChannel;
33-
import java.nio.channels.FileLock;
3436
import java.nio.file.Path;
3537
import java.nio.file.StandardOpenOption;
38+
import java.util.Set;
3639
import java.util.concurrent.ConcurrentHashMap;
37-
import java.util.concurrent.locks.ReentrantReadWriteLock;
3840

3941
/**
40-
* A lock manager that provides both thread-safe and process-safe read/write
41-
* locking for filesystem keys.
42-
* <p>
43-
* This class coordinates two levels of locking:
44-
* <ul>
45-
* <li><b>Thread-level:</b> Uses {@link ReentrantReadWriteLock} to coordinate
46-
* access among threads within the same JVM.</li>
47-
* <li><b>Process-level:</b> Uses {@link FileLock} to coordinate access among
48-
* different processes/JVMs.</li>
49-
* </ul>
50-
* <p>
51-
* For reading:
52-
* <ul>
53-
* <li>The first thread to acquire a read lock also acquires a shared file lock.</li>
54-
* <li>Subsequent threads acquire only the internal read lock (no duplicate file lock).</li>
55-
* <li>When the last reader releases, the file lock is also released.</li>
56-
* </ul>
57-
* <p>
58-
* For writing:
59-
* <ul>
60-
* <li>The writer acquires an exclusive file lock (after all readers have released).</li>
61-
* <li>When the write lock is released, the file lock is also released.</li>
62-
* </ul>
42+
* Provides thread-safe and process-safe read/write locking for filesystem paths.
43+
* Uses thread locks for JVM coordination and file locks for inter-process coordination.
6344
*/
64-
public class FileKeyLockManager {
45+
class FileKeyLockManager {
6546

66-
private final ConcurrentHashMap<String, KeyLockState> locks = new ConcurrentHashMap<>();
47+
static final FileKeyLockManager FILE_LOCK_MANAGER = new FileKeyLockManager();
48+
49+
private static final ReferenceQueue<LockedFileChannel> queue = new ReferenceQueue<>();
50+
private static final Set<LockedChannelReference> phantomRefs = ConcurrentHashMap.newKeySet();
51+
private static final ConcurrentHashMap<String, KeyLockState> locks = new ConcurrentHashMap<>();
6752

6853
/**
69-
* Creates a new FileSystemKeyLockManager.
54+
* PhantomReference for LockedFileChannel that releases the lock if the
55+
* channel is garbage collected without being closed.
7056
*/
71-
public FileKeyLockManager() {
57+
private static class LockedChannelReference extends PhantomReference<LockedFileChannel> {
58+
59+
private final String key;
60+
private final KeyLockState state;
61+
62+
LockedChannelReference(final LockedFileChannel referent, final String key, final KeyLockState state) {
63+
super(referent, queue);
64+
this.key = key;
65+
this.state = state;
66+
}
67+
68+
void cleanup() {
69+
state.releaseFileLockForCleanup();
70+
phantomRefs.remove(this);
71+
/* always remove from map - if the LockedFileChannel was GC'd, the entry is stale */
72+
locks.remove(key, state);
73+
}
74+
}
75+
76+
private void clearQueue() {
77+
Reference<? extends LockedFileChannel> ref;
78+
while ((ref = queue.poll()) != null) {
79+
((LockedChannelReference)ref).cleanup();
80+
}
81+
}
82+
83+
private class ManagedLockedFileChannel extends LockedFileChannel {
84+
85+
private final String key;
86+
private final KeyLockState state;
87+
88+
ManagedLockedFileChannel(final KeyLockState state, final FileChannel channel, final String key) {
89+
super(state, channel);
90+
this.key = key;
91+
this.state = state;
92+
}
93+
94+
@Override
95+
public void close() throws IOException {
96+
super.close();
97+
synchronized (state) {
98+
if (!state.isLocked()) {
99+
locks.remove(key, state);
100+
}
101+
}
102+
clearQueue();
103+
}
104+
}
105+
106+
private FileKeyLockManager() {
107+
}
72108

109+
private LockedFileChannel registerLockedFileChannel(final FileChannel channel, final String key, final KeyLockState state) {
110+
final LockedFileChannel lockedChannel = new ManagedLockedFileChannel(state, channel, key);
111+
phantomRefs.add(new LockedChannelReference(lockedChannel, key, state));
112+
return lockedChannel;
73113
}
74114

75115
/**
@@ -86,6 +126,8 @@ public FileKeyLockManager() {
86126
* if acquiring the file lock fails
87127
*/
88128
public LockedFileChannel lockForReading(final Path path) throws IOException {
129+
/* if there are stale entries in the queue, clean them before we try to lock */
130+
clearQueue();
89131

90132
final String key = path.toAbsolutePath().toString();
91133
final KeyLockState state = locks.computeIfAbsent(key, k -> new KeyLockState(path));
@@ -99,8 +141,7 @@ public LockedFileChannel lockForReading(final Path path) throws IOException {
99141
state.releaseLock();
100142
throw e;
101143
}
102-
103-
return new LockedFileChannel(state, channel);
144+
return registerLockedFileChannel(channel, key, state);
104145
}
105146

106147
/**
@@ -114,6 +155,8 @@ public LockedFileChannel lockForReading(final Path path) throws IOException {
114155
* if acquiring the file lock fails
115156
*/
116157
public LockedFileChannel lockForWriting(final Path path) throws IOException {
158+
/* if there are stale entries in the queue, clean them before we try to lock */
159+
clearQueue();
117160

118161
final String key = path.toAbsolutePath().toString();
119162
final KeyLockState state = locks.computeIfAbsent(key, k -> new KeyLockState(path));
@@ -128,7 +171,7 @@ public LockedFileChannel lockForWriting(final Path path) throws IOException {
128171
throw e;
129172
}
130173

131-
return new LockedFileChannel(state, channel);
174+
return registerLockedFileChannel(channel, key, state);
132175
}
133176

134177
/**
@@ -139,6 +182,8 @@ public LockedFileChannel lockForWriting(final Path path) throws IOException {
139182
* @return a {@link LockedChannel} if the lock was acquired, null otherwise
140183
*/
141184
public LockedFileChannel tryLockForReading(final Path path) {
185+
/* if there are stale entries in the queue, clean them before we try to lock */
186+
clearQueue();
142187

143188
final String key = path.toAbsolutePath().toString();
144189
final KeyLockState state = locks.computeIfAbsent(key, k -> new KeyLockState(path));
@@ -154,7 +199,7 @@ public LockedFileChannel tryLockForReading(final Path path) {
154199
return null;
155200
}
156201

157-
return new LockedFileChannel(state, channel);
202+
return registerLockedFileChannel(channel, key, state);
158203
}
159204

160205
/**
@@ -165,14 +210,15 @@ public LockedFileChannel tryLockForReading(final Path path) {
165210
* @return a {@link LockedChannel} if the lock was acquired, null otherwise
166211
*/
167212
public LockedFileChannel tryLockForWriting(final Path path) {
213+
/* if there are stale entries in the queue, clean them before we try to lock */
214+
clearQueue();
168215

169216
final String key = path.toAbsolutePath().toString();
170217
final KeyLockState state = locks.computeIfAbsent(key, k -> new KeyLockState(path));
171218

172219
if (!state.tryLockForWriting())
173220
return null;
174221

175-
176222
final FileChannel channel;
177223
try {
178224
channel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
@@ -181,7 +227,7 @@ public LockedFileChannel tryLockForWriting(final Path path) {
181227
return null;
182228
}
183229

184-
return new LockedFileChannel(state, channel);
230+
return registerLockedFileChannel(channel, key, state);
185231
}
186232

187233
/**
@@ -195,40 +241,21 @@ public int size() {
195241
}
196242

197243
/**
198-
* Removes the lock for a key if it is not currently held.
244+
* Removes the lock state for a key if no locks are held.
199245
*
200-
* @param key
201-
* the key whose lock should be removed
202-
* @return true if the lock was removed, false if it's currently in use
246+
* @param key the key whose lock state should be removed
247+
* @return true if removed, false if currently in use or not found
203248
*/
204249
public boolean removeLockIfUnused(final String key) {
205250

206251
final KeyLockState state = locks.get(key);
207-
if (state == null) {
252+
if (state == null)
208253
return false;
209-
}
210254

211255
synchronized (state) {
212256
if (!state.isLocked())
213257
return locks.remove(key, state);
214258
}
215259
return false;
216260
}
217-
218-
/**
219-
* Clears all unused locks from the lock map.
220-
*
221-
* @return the number of locks that were removed
222-
*/
223-
public int clearUnusedLocks() {
224-
225-
int removed = 0;
226-
for (final String key : locks.keySet()) {
227-
if (removeLockIfUnused(key)) {
228-
removed++;
229-
}
230-
}
231-
return removed;
232-
}
233-
234261
}

src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454

5555
import org.janelia.saalfeldlab.n5.readdata.ReadData;
5656

57+
import static org.janelia.saalfeldlab.n5.FileKeyLockManager.FILE_LOCK_MANAGER;
58+
5759
/**
5860
* Filesystem {@link KeyValueAccess}.
5961
*
@@ -63,8 +65,6 @@
6365
*/
6466
public class FileSystemKeyValueAccess implements KeyValueAccess {
6567

66-
protected static final FileKeyLockManager KEY_LOCK_MANAGER = new FileKeyLockManager();
67-
6868
protected final FileSystem fileSystem;
6969

7070
/**
@@ -86,7 +86,7 @@ public ReadData createReadData(final String normalPath) {
8686
public LockedFileChannel lockForReading(final String normalPath) throws N5IOException {
8787

8888
try {
89-
return KEY_LOCK_MANAGER.lockForReading(fileSystem.getPath(normalPath));
89+
return FILE_LOCK_MANAGER.lockForReading(fileSystem.getPath(normalPath));
9090
} catch (final NoSuchFileException e) {
9191
throw new N5NoSuchKeyException("No such file", e);
9292
} catch (IOException | UncheckedIOException e) {
@@ -98,7 +98,7 @@ public LockedFileChannel lockForReading(final String normalPath) throws N5IOExce
9898
public LockedChannel lockForWriting(final String normalPath) throws N5IOException {
9999

100100
try {
101-
return KEY_LOCK_MANAGER.lockForWriting(fileSystem.getPath(normalPath));
101+
return FILE_LOCK_MANAGER.lockForWriting(fileSystem.getPath(normalPath));
102102
} catch (final NoSuchFileException e) {
103103
throw new N5NoSuchKeyException("No such file", e);
104104
} catch (IOException | UncheckedIOException e) {
@@ -109,7 +109,7 @@ public LockedChannel lockForWriting(final String normalPath) throws N5IOExceptio
109109
public LockedFileChannel lockForReading(final Path path) throws N5IOException {
110110

111111
try {
112-
return KEY_LOCK_MANAGER.lockForReading(path);
112+
return FILE_LOCK_MANAGER.lockForReading(path);
113113
} catch (final NoSuchFileException e) {
114114
throw new N5NoSuchKeyException("No such file", e);
115115
} catch (IOException | UncheckedIOException e) {
@@ -120,7 +120,7 @@ public LockedFileChannel lockForReading(final Path path) throws N5IOException {
120120
public LockedFileChannel lockForWriting(final Path path) throws N5IOException {
121121

122122
try {
123-
return KEY_LOCK_MANAGER.lockForWriting(path);
123+
return FILE_LOCK_MANAGER.lockForWriting(path);
124124
} catch (final NoSuchFileException e) {
125125
throw new N5NoSuchKeyException("No such file", e);
126126
} catch (IOException | UncheckedIOException e) {

src/main/java/org/janelia/saalfeldlab/n5/KeyLockState.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
/**
1313
* Per-key state that tracks both thread locks and file locks.
1414
*/
15-
public class KeyLockState {
15+
class KeyLockState {
1616

1717
private final ReentrantReadWriteLock threadLock = new ReentrantReadWriteLock();
1818
private final Path path;
@@ -194,4 +194,15 @@ void releaseLock() {
194194
}
195195
}
196196

197+
/**
198+
* Release file lock only, for cleanup when the LockedFileChannel was GC'd
199+
* without being closed. Does not release thread locks since those can only
200+
* be released by the owning thread.
201+
*/
202+
synchronized void releaseFileLockForCleanup() {
203+
writeLockHeld = false;
204+
readLockHolders = 0;
205+
releaseFileLock();
206+
}
207+
197208
}

0 commit comments

Comments
 (0)