2929package org .janelia .saalfeldlab .n5 ;
3030
3131import java .io .IOException ;
32+ import java .lang .ref .PhantomReference ;
33+ import java .lang .ref .Reference ;
34+ import java .lang .ref .ReferenceQueue ;
3235import java .nio .channels .FileChannel ;
33- import java .nio .channels .FileLock ;
3436import java .nio .file .Path ;
3537import java .nio .file .StandardOpenOption ;
38+ import java .util .Set ;
3639import 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}
0 commit comments