Skip to content

Commit e50360a

Browse files
committed
WIP multiple FileKeyLockManager instances
more or less copied from #211
1 parent 93c909e commit e50360a

4 files changed

Lines changed: 137 additions & 66 deletions

File tree

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

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import java.io.Closeable;
44
import java.io.IOException;
5-
import java.io.UncheckedIOException;
65
import java.nio.channels.FileChannel;
76
import java.nio.channels.FileLock;
87
import java.nio.channels.OverlappingFileLockException;
@@ -18,6 +17,18 @@
1817
class ChannelLock implements Closeable {
1918

2019
private final FileChannel channel;
20+
21+
/**
22+
* Hold a hard reference to the {@code FileLock} to make sure it is not
23+
* prematurely released.
24+
* <p>
25+
* NB: We do not call {@code lock.release()} in {@link #close}, because at
26+
* this point the channel might be already closed (by an external writer).
27+
* {@code lock.release()} will throw an exception if the channel is already
28+
* closed. Instead, we just close the channel which will automatically
29+
* release the lock.
30+
*/
31+
@SuppressWarnings({"unused", "FieldCanBeLocal"})
2132
private final FileLock lock;
2233

2334
private ChannelLock(final FileChannel channel, final FileLock lock) {
@@ -54,9 +65,12 @@ FileChannel getChannel() {
5465
* @throws IOException if an error occurs while opening the channel, or if
5566
* the calling thread is interrupted while waiting for the {@code FileLock}.
5667
*/
57-
static ChannelLock lock(final Path path, final boolean forWriting) throws IOException {
68+
static ChannelLock lock(final Path path, final boolean forWriting, final LockingPolicy policy) throws IOException {
5869

5970
final FileChannel channel = openFileChannel(path, forWriting);
71+
if (policy == LockingPolicy.UNSAFE) {
72+
return new ChannelLock(channel, null);
73+
}
6074
try {
6175
while (true) {
6276
try {
@@ -72,34 +86,12 @@ static ChannelLock lock(final Path path, final boolean forWriting) throws IOExce
7286
}
7387
}
7488
} catch (Exception e) {
75-
closeQuietly(channel);
76-
throw e;
77-
}
78-
}
79-
80-
/**
81-
* Create a {@link FileChannel} on the given {@code path} and try to lock it
82-
* with a system-level {@link FileLock}. If the channel cannot be locked,
83-
* {@code null} is returned.
84-
* <p>
85-
* The {@code FileLock} is exclusive if the {@code path} is locked {@code
86-
* forWriting}, and shared otherwise.
87-
* <p>
88-
* If the {@code path} is locked {@code forWriting} non-existing file and
89-
* the parent directories are created as needed.
90-
*
91-
* @throws IOException if an error occurs while opening the channel.
92-
*/
93-
static ChannelLock tryLock(final Path path, final boolean forWriting) throws IOException {
94-
95-
FileChannel channel = null;
96-
try {
97-
channel = openFileChannel(path, forWriting);
98-
final FileLock lock = channel.tryLock(0, Long.MAX_VALUE, !forWriting);
99-
return lock == null ? null : new ChannelLock(channel, lock);
100-
} catch (Exception e) {
101-
closeQuietly(channel);
102-
throw e;
89+
if (policy == LockingPolicy.STRICT) {
90+
closeQuietly(channel);
91+
throw e;
92+
} else {
93+
return new ChannelLock(channel, null);
94+
}
10395
}
10496
}
10597

@@ -127,8 +119,8 @@ private static void closeQuietly(final FileChannel fileChannel) {
127119
if (fileChannel != null) {
128120
try {
129121
fileChannel.close();
130-
} catch (final IOException | UncheckedIOException ignored) {
122+
} catch (final IOException ignored) {
131123
}
132-
}
124+
}
133125
}
134126
}

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

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,44 @@
3131
import java.io.IOException;
3232
import java.lang.ref.ReferenceQueue;
3333
import java.lang.ref.WeakReference;
34+
import java.nio.channels.FileLock;
3435
import java.nio.file.Path;
3536
import java.util.concurrent.ConcurrentHashMap;
3637

38+
import static org.janelia.saalfeldlab.n5.LockingPolicy.STRICT;
39+
3740
/**
3841
* Provides thread-safe and process-safe read/write locking for filesystem paths.
3942
* Uses thread locks for JVM coordination and file locks for inter-process coordination.
4043
*/
4144
class FileKeyLockManager {
4245

43-
static final FileKeyLockManager FILE_LOCK_MANAGER = new FileKeyLockManager();
46+
/**
47+
* @deprecated use {@link FileKeyLockManager} instance per KVA instead of global one.
48+
*/
49+
@Deprecated
50+
static final FileKeyLockManager FILE_LOCK_MANAGER = new FileKeyLockManager(STRICT);
4451

45-
private FileKeyLockManager() {
46-
// singleton
47-
}
52+
private final LockingPolicy policy;
4853

54+
/**
55+
* Create a new {@link FileKeyLockManager} with the specified locking policy.
56+
* <p>
57+
* The given locking {@link LockingPolicy policy} applies to OS-level locking.
58+
* For both the {@code STRICT} and {@code PERMISSIVE} policy, a {@link
59+
* FileLock} is obtained. If this fails, {@code STRICT} will throw an {@code
60+
* IOException}. {@code PERMISSIVE} will proceed without locking. {@code
61+
* UNSAFE} will not attempt OS-level locking, however will still manage
62+
* mutual exclusion of readers and writers in the same JVM. Trying to lock
63+
* the same path with different locking policies will throw an {@code
64+
* IOException}.
65+
*
66+
* @param policy
67+
* the locking policy
68+
*/
69+
FileKeyLockManager(final LockingPolicy policy) {
70+
this.policy = policy;
71+
}
4972

5073
private final ConcurrentHashMap<String, WeakValue> locks = new ConcurrentHashMap<>();
5174

@@ -79,25 +102,26 @@ private void cleanUp()
79102
}
80103
}
81104

82-
private KeyLockState keyLockState(final Path path) {
105+
private KeyLockState keyLockState(final Path path, final LockingPolicy policy) throws IOException {
83106

84107
final String key = path.toAbsolutePath().toString();
85108

86109
cleanUp();
87110

88111
final WeakValue existingRef = locks.get(key);
89112
KeyLockState state = existingRef == null ? null : existingRef.get();
90-
if (state != null) {
91-
return state;
113+
if (state == null) {
114+
final KeyLockState newState = new KeyLockState(path, policy);
115+
while (state == null) {
116+
final WeakValue ref = locks.compute(key,
117+
(k, v) -> (v != null && v.get() != null)
118+
? v
119+
: new WeakValue(k, newState, refQueue));
120+
state = ref.get();
121+
}
92122
}
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();
123+
if (state.policy() != policy) {
124+
throw new IOException("Trying to lock \"" + path + "\" with policy " + policy + ", but it is already used with " + state.policy());
101125
}
102126
return state;
103127
}
@@ -110,29 +134,33 @@ private KeyLockState keyLockState(final Path path) {
110134
* only acquire the thread-level lock.
111135
*
112136
* @param path
113-
* the key (file path) to lock for reading
137+
* the key (file path) to lock for reading
138+
*
114139
* @return a {@link LockedChannel} that must be closed when done
140+
*
115141
* @throws IOException
116-
* if acquiring the file lock fails
142+
* if acquiring the file lock fails
117143
*/
118144
public LockedFileChannel lockForReading(final Path path) throws IOException {
119145

120-
return keyLockState(path).acquireRead();
146+
return keyLockState(path, policy).acquireRead();
121147
}
122148

123149
/**
124150
* Acquires a write lock for the specified key. Only one thread can hold a
125151
* write lock for a key at a time, and no readers can hold locks.
126152
*
127153
* @param path
128-
* the file path to lock for writing
154+
* the file path to lock for writing
155+
*
129156
* @return a {@link LockedChannel} that must be closed when done
157+
*
130158
* @throws IOException
131-
* if acquiring the file lock fails
159+
* if acquiring the file lock fails
132160
*/
133161
public LockedFileChannel lockForWriting(final Path path) throws IOException {
134162

135-
return keyLockState(path).acquireWrite();
163+
return keyLockState(path, policy).acquireWrite();
136164
}
137165

138166
/**
@@ -144,14 +172,4 @@ int size() {
144172

145173
return locks.size();
146174
}
147-
148-
/**
149-
* Removes the lock state for a key if no locks are held.
150-
*
151-
* @param key the key whose lock state should be removed
152-
* @return true if removed, false if currently in use or not found
153-
*/
154-
public boolean removeLockIfUnused(final String key) {
155-
throw new UnsupportedOperationException("TODO. REMOVE");
156-
}
157175
}

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ class KeyLockState {
1313

1414
private final Path path;
1515

16-
public KeyLockState(final Path path) {
16+
private final LockingPolicy policy;
17+
18+
public KeyLockState(final Path path, LockingPolicy policy) {
1719
this.path = path;
20+
this.policy = policy;
1821
}
1922

2023
/**
@@ -52,7 +55,7 @@ LockedFileChannel acquireRead() throws IOException {
5255
channelLockMutex.acquire();
5356

5457
try {
55-
channelLock = ChannelLock.lock(path, false);
58+
channelLock = ChannelLock.lock(path, false, policy);
5659
} catch (IOException e) {
5760
// Something went wrong. Back off.
5861
channelLockMutex.release();
@@ -119,7 +122,7 @@ LockedFileChannel acquireWrite() throws IOException {
119122
channelLockMutex.acquire();
120123

121124
try {
122-
channelLock = ChannelLock.lock(path, true);
125+
channelLock = ChannelLock.lock(path, true, policy);
123126
} catch (IOException e) {
124127
// Something went wrong. Back off.
125128
channelLockMutex.release();
@@ -137,4 +140,8 @@ LockedFileChannel acquireWrite() throws IOException {
137140
void releaseWrite() throws IOException {
138141
releaseChannelLock();
139142
}
143+
144+
LockingPolicy policy() {
145+
return policy;
146+
}
140147
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.janelia.saalfeldlab.n5;
2+
3+
/**
4+
* File locking policy.
5+
* <p>
6+
* Usually, we want to coordinate reads and writs to a container such that
7+
* <ul>
8+
* <li>multiple readers can access a key simultaneously (blocking all writers).</li>
9+
* <li>A writer should have exclusive access to a key (blocking all other readers and writers).</li>
10+
* </ul>
11+
* However, this cannot always be enforced for all backends:
12+
* For SMB on macOS OS-level file locking is broken.
13+
* For AWS S3 and Google Cloud we can detect (but not prevent) concurrent modifications.
14+
* <p>
15+
* Sometimes we know that we can disregard locking, for example in read-only settings.
16+
* <p>
17+
* {@code IoPolicy} can be used to configure locking policy for backends ({@link
18+
* KeyValueAccess}) that support various locking strategies (potentially coming
19+
* with performance trade-offs).
20+
* <p>
21+
* The policy values ({@link #STRICT}, {@link #UNSAFE}, {@link #PERMISSIVE})
22+
* specify intent. Detailed interpretation is up to the backend implementation.
23+
*/
24+
public enum LockingPolicy {
25+
26+
/**
27+
* Protect all reads and writes by locks.
28+
* Fail if locking is not possible.
29+
*/
30+
STRICT,
31+
32+
/**
33+
* Reads and writes are unprotected.
34+
*/
35+
UNSAFE,
36+
37+
/**
38+
* Try to lock for all reads and writes.
39+
* Fall back to unprotected reads and writes if locking is not possible.
40+
* This is the default.
41+
*/
42+
PERMISSIVE;
43+
44+
static LockingPolicy fromString(final String s) {
45+
if ("strict".equalsIgnoreCase(s))
46+
return STRICT;
47+
else if ("unsafe".equalsIgnoreCase(s))
48+
return UNSAFE;
49+
// else if ("permissive".equalsIgnoreCase(s))
50+
// return PERMISSIVE;
51+
else
52+
return PERMISSIVE;
53+
}
54+
}

0 commit comments

Comments
 (0)