Skip to content

Commit 5fac740

Browse files
committed
Revise FileKeyLockManager.
Locks are not tied to the locking thread anymore. (A LockedChannel can be created by one thread and then be released by another thread). This required to replace ReentrantReadWriteLock with manual reader/writer counting guarded by Semaphores. LockedFileChannel is now created by KeyLockState (instead of FileKeyLockManager) because for releasing we need to distinguish between read and write locks. I revised clean-up of stale KeyLockStates by putting WeakReferences in the ConcurrentHashMap and adding a ReferenceQueue to remove entries whose KeyLockState has been GCed. I put no special logic to handle leaked LockedFileChannels (that are abandoned without being properly closed). Surprisingly, this still works. When the KeyLockState is GCed, its associated ChannelLock along with the existing FileChannel and FileLock is also GCed. At least on MacOS, this causes the JVM to release the system-level lock, and everything works out fine.
1 parent 32ea434 commit 5fac740

6 files changed

Lines changed: 479 additions & 357 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
lock.release();
29+
channel.close();
30+
}
31+
32+
/**
33+
* Create a {@link FileChannel} on the given {@code path} and lock it with a
34+
* system-level {@link FileLock}. If there is an existing overlapping file
35+
* lock, this method will block until the existing lock is released and the
36+
* channel could be locked (by us).
37+
* <p>
38+
* The {@code FileLock} is exclusive if the {@code path} is locked {@code
39+
* forWriting}, and shared otherwise.
40+
* <p>
41+
* If the {@code path} is locked {@code forWriting} non-existing file and
42+
* the parent directories are created as needed.
43+
*
44+
* @throws IOException if an error occurs while opening the channel, or if
45+
* the calling thread is interrupted while waiting for the {@code FileLock}.
46+
*/
47+
static ChannelLock lock(final Path path, final boolean forWriting) throws IOException {
48+
49+
final FileChannel channel = openFileChannel(path, forWriting);
50+
try {
51+
while (true) {
52+
try {
53+
final FileLock lock = channel.lock(0, Long.MAX_VALUE, !forWriting);
54+
return new ChannelLock(channel, lock);
55+
} catch (final OverlappingFileLockException e) {
56+
try {
57+
Thread.sleep(100);
58+
} catch (final InterruptedException ie) {
59+
Thread.currentThread().interrupt();
60+
throw new IOException("Interrupted while waiting for file lock", ie);
61+
}
62+
}
63+
}
64+
} catch (Exception e) {
65+
closeQuietly(channel);
66+
throw e;
67+
}
68+
}
69+
70+
/**
71+
* Create a {@link FileChannel} on the given {@code path} and try to lock it
72+
* with a system-level {@link FileLock}. If the channel cannot be locked,
73+
* {@code null} is returned.
74+
* <p>
75+
* The {@code FileLock} is exclusive if the {@code path} is locked {@code
76+
* forWriting}, and shared otherwise.
77+
* <p>
78+
* If the {@code path} is locked {@code forWriting} non-existing file and
79+
* the parent directories are created as needed.
80+
*
81+
* @throws IOException if an error occurs while opening the channel.
82+
*/
83+
static ChannelLock tryLock(final Path path, final boolean forWriting) throws IOException {
84+
85+
FileChannel channel = null;
86+
try {
87+
channel = openFileChannel(path, forWriting);
88+
final FileLock lock = channel.tryLock(0, Long.MAX_VALUE, !forWriting);
89+
return lock == null ? null : new ChannelLock(channel, lock);
90+
} catch (Exception e) {
91+
closeQuietly(channel);
92+
throw e;
93+
}
94+
}
95+
96+
/**
97+
* Opens a file channel. If the channel is opened {@code forWriting},
98+
* then this may create the file and the parent directories as needed.
99+
*
100+
* @throws IOException
101+
* if the channel cannot be opened
102+
*/
103+
private static FileChannel openFileChannel(final Path path, final boolean forWriting) throws IOException {
104+
105+
if (forWriting) {
106+
final Path parent = path.getParent();
107+
if (parent != null) {
108+
Files.createDirectories(parent);
109+
}
110+
return FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
111+
} else {
112+
return FileChannel.open(path, StandardOpenOption.READ);
113+
}
114+
}
115+
116+
private static void closeQuietly(final FileChannel fileChannel) {
117+
if (fileChannel != null) {
118+
try {
119+
fileChannel.close();
120+
} catch (final IOException ignored) {
121+
}
122+
}
123+
}
124+
}

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

Lines changed: 50 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,9 @@
2929
package org.janelia.saalfeldlab.n5;
3030

3131
import java.io.IOException;
32-
import java.lang.ref.PhantomReference;
33-
import java.lang.ref.Reference;
3432
import java.lang.ref.ReferenceQueue;
35-
import java.nio.channels.FileChannel;
33+
import java.lang.ref.WeakReference;
3634
import java.nio.file.Path;
37-
import java.nio.file.StandardOpenOption;
38-
import java.util.Set;
3935
import java.util.concurrent.ConcurrentHashMap;
4036

4137
/**
@@ -46,70 +42,64 @@ class FileKeyLockManager {
4642

4743
static final FileKeyLockManager FILE_LOCK_MANAGER = new FileKeyLockManager();
4844

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<>();
45+
private FileKeyLockManager() {
46+
// singleton
47+
}
5248

53-
/**
54-
* PhantomReference for LockedFileChannel that releases the lock if the
55-
* channel is garbage collected without being closed.
56-
*/
57-
private static class LockedChannelReference extends PhantomReference<LockedFileChannel> {
5849

59-
private final String key;
60-
private final KeyLockState state;
50+
private final ConcurrentHashMap<String, WeakValue> locks = new ConcurrentHashMap<>();
6151

62-
LockedChannelReference(final LockedFileChannel referent, final String key, final KeyLockState state) {
63-
super(referent, queue);
64-
this.key = key;
65-
this.state = state;
66-
}
52+
private final ReferenceQueue<KeyLockState> refQueue = new ReferenceQueue<>();
6753

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);
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;
7365
}
7466
}
7567

76-
private void clearQueue() {
77-
Reference<? extends LockedFileChannel> ref;
78-
while ((ref = queue.poll()) != null) {
79-
((LockedChannelReference)ref).cleanup();
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);
8079
}
8180
}
8281

83-
private class ManagedLockedFileChannel extends LockedFileChannel {
82+
private KeyLockState keyLockState(final Path path) {
8483

85-
private final String key;
86-
private final KeyLockState state;
84+
final String key = path.toAbsolutePath().toString();
8785

88-
ManagedLockedFileChannel(final KeyLockState state, final FileChannel channel, final String key) {
89-
super(state, channel);
90-
this.key = key;
91-
this.state = state;
92-
}
86+
cleanUp();
9387

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();
88+
final WeakValue existingRef = locks.get(key);
89+
KeyLockState state = existingRef == null ? null : existingRef.get();
90+
if (state != null) {
91+
return state;
10392
}
104-
}
10593

106-
private FileKeyLockManager() {
107-
}
108-
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;
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;
113103
}
114104

115105
/**
@@ -126,22 +116,8 @@ private LockedFileChannel registerLockedFileChannel(final FileChannel channel, f
126116
* if acquiring the file lock fails
127117
*/
128118
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();
131-
132-
final String key = path.toAbsolutePath().toString();
133-
final KeyLockState state = locks.computeIfAbsent(key, k -> new KeyLockState(path));
134-
135-
state.lockForReading();
136119

137-
final FileChannel channel;
138-
try {
139-
channel = FileChannel.open(path, StandardOpenOption.READ);
140-
} catch (final IOException e) {
141-
state.releaseLock();
142-
throw e;
143-
}
144-
return registerLockedFileChannel(channel, key, state);
120+
return keyLockState(path).acquireRead();
145121
}
146122

147123
/**
@@ -155,23 +131,8 @@ public LockedFileChannel lockForReading(final Path path) throws IOException {
155131
* if acquiring the file lock fails
156132
*/
157133
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();
160-
161-
final String key = path.toAbsolutePath().toString();
162-
final KeyLockState state = locks.computeIfAbsent(key, k -> new KeyLockState(path));
163-
164-
state.lockForWriting();
165134

166-
final FileChannel channel;
167-
try {
168-
channel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
169-
} catch (final IOException e) {
170-
state.releaseLock();
171-
throw e;
172-
}
173-
174-
return registerLockedFileChannel(channel, key, state);
135+
return keyLockState(path).acquireWrite();
175136
}
176137

177138
/**
@@ -182,24 +143,8 @@ public LockedFileChannel lockForWriting(final Path path) throws IOException {
182143
* @return a {@link LockedChannel} if the lock was acquired, null otherwise
183144
*/
184145
public LockedFileChannel tryLockForReading(final Path path) {
185-
/* if there are stale entries in the queue, clean them before we try to lock */
186-
clearQueue();
187-
188-
final String key = path.toAbsolutePath().toString();
189-
final KeyLockState state = locks.computeIfAbsent(key, k -> new KeyLockState(path));
190-
191-
if (!state.tryLockForReading())
192-
return null;
193146

194-
final FileChannel channel;
195-
try {
196-
channel = FileChannel.open(path, StandardOpenOption.READ);
197-
} catch (final IOException e) {
198-
state.releaseLock();
199-
return null;
200-
}
201-
202-
return registerLockedFileChannel(channel, key, state);
147+
return keyLockState(path).tryAcquireRead();
203148
}
204149

205150
/**
@@ -210,32 +155,16 @@ public LockedFileChannel tryLockForReading(final Path path) {
210155
* @return a {@link LockedChannel} if the lock was acquired, null otherwise
211156
*/
212157
public LockedFileChannel tryLockForWriting(final Path path) {
213-
/* if there are stale entries in the queue, clean them before we try to lock */
214-
clearQueue();
215-
216-
final String key = path.toAbsolutePath().toString();
217-
final KeyLockState state = locks.computeIfAbsent(key, k -> new KeyLockState(path));
218-
219-
if (!state.tryLockForWriting())
220-
return null;
221158

222-
final FileChannel channel;
223-
try {
224-
channel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
225-
} catch (final IOException e) {
226-
state.releaseLock();
227-
return null;
228-
}
229-
230-
return registerLockedFileChannel(channel, key, state);
159+
return keyLockState(path).tryAcquireWrite();
231160
}
232161

233162
/**
234163
* Returns the number of keys currently being tracked.
235164
*
236165
* @return the number of keys with associated locks
237166
*/
238-
public int size() {
167+
int size() {
239168

240169
return locks.size();
241170
}
@@ -247,15 +176,6 @@ public int size() {
247176
* @return true if removed, false if currently in use or not found
248177
*/
249178
public boolean removeLockIfUnused(final String key) {
250-
251-
final KeyLockState state = locks.get(key);
252-
if (state == null)
253-
return false;
254-
255-
synchronized (state) {
256-
if (!state.isLocked())
257-
return locks.remove(key, state);
258-
}
259-
return false;
179+
throw new UnsupportedOperationException("TODO. REMOVE");
260180
}
261181
}

0 commit comments

Comments
 (0)