Skip to content

Commit 63bd1a6

Browse files
Fix race condition in DoubleLocked#release (#5153)
This PR makes some uses of `DoubleLock` safer, and fixes a race condition with it. `DoubleLock` is basically used as a wrapper around a file lock (second lock), using it alongside a memory lock (first lock), that acts as a mutex for it. That way, the same JVM process doesn't try to acquire the file lock twice at the same time in a given JVM, which `java.nio.channels.FileChannel` doesn't allow (it throws an `OverlappingFileLockException` in that case). This PR makes sure `MillMain` and `MillDaemonMain` use the same `Lock.memory()` instance as a guard when trying to acquire a lock for the output directory. This doesn't solve the issue I was running into (see below), but seems to be safer nonetheless. It also tries to dispose of locks in `DoubleLock` in case calls to `Lock#{lock,tryLock}` throw. Lastly, it releases the two locks in `DoubleLocked` in the reverse order that they were acquired. This fixes the test case below. Doing so fixes a race condition, where - a first thread acquires the lock - a second one tries to acquire it, and waits for it - the first thread releases the lock During the third step, releasing the memory lock before the file lock makes the second thread acquire the memory lock, and then possibly try to acquire the file lock, before the first thread is done releasing the file lock. The JVM doesn't allow this, and the second thread gets an `OverlappingFileLockException`. See the discussion in #5152 too This fixes ```text $ ./mill 'integration.feature[output-directory].packaged.daemon.testForked' "mill.integration.OutputDirectoryLockTests.basic" ``` for me, locally.
1 parent aa7125f commit 63bd1a6

File tree

4 files changed

+44
-15
lines changed

4 files changed

+44
-15
lines changed

runner/client/src/mill/client/lock/DoubleLock.java

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,44 @@ public DoubleLock(Lock lock1, Lock lock2) throws Exception {
1212

1313
@Override
1414
public Locked lock() throws Exception {
15-
return new DoubleLocked(lock1.lock(), lock2.lock());
15+
Locked l1 = null;
16+
Locked l2 = null;
17+
Locked result = null;
18+
try {
19+
l1 = lock1.lock();
20+
l2 = lock2.lock();
21+
result = new DoubleLocked(l1, l2);
22+
} finally {
23+
if (result == null) {
24+
if (l2 != null) l2.release();
25+
if (l1 != null) l1.release();
26+
}
27+
}
28+
29+
return result;
1630
}
1731

1832
@Override
1933
public TryLocked tryLock() throws Exception {
20-
TryLocked l1 = lock1.tryLock();
21-
TryLocked l2 = lock2.tryLock();
22-
if (l1.isLocked() && l2.isLocked()) {
23-
return new DoubleTryLocked(l1, l2);
24-
} else {
25-
// Unlock the locks in the opposite order in which we originally took them
26-
l2.release();
27-
l1.release();
28-
29-
return new DoubleTryLocked(null, null);
34+
TryLocked l1 = null;
35+
TryLocked l2 = null;
36+
TryLocked result = null;
37+
try {
38+
l1 = lock1.tryLock();
39+
if (l1.isLocked()) {
40+
l2 = lock2.tryLock();
41+
if (l2.isLocked()) result = new DoubleTryLocked(l1, l2);
42+
}
43+
} finally {
44+
if (result == null) {
45+
if (l2 != null) l2.release();
46+
if (l1 != null) l1.release();
47+
}
3048
}
49+
50+
if (result == null) result = new DoubleTryLocked(null, null);
51+
52+
return result;
3153
}
3254

3355
@Override

runner/client/src/mill/client/lock/DoubleLocked.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public DoubleLocked(Locked lock1, Locked lock2) {
1212

1313
@Override
1414
public void release() throws Exception {
15-
this.lock1.release();
1615
this.lock2.release();
16+
this.lock1.release();
1717
}
1818
}

runner/daemon/src/mill/daemon/MillDaemonMain.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class MillDaemonMain(
5252
val out = os.Path(OutFiles.out, mill.define.BuildCtx.workspaceRoot)
5353

5454
val outLock = new DoubleLock(
55-
Lock.memory(),
55+
MillMain.outMemoryLock,
5656
Lock.file((out / OutFiles.millOutLock).toString)
5757
)
5858

runner/daemon/src/mill/daemon/MillMain.scala

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package mill.daemon
33
import mill.api.internal.{BspServerResult, internal}
44
import mill.api.{Logger, MillException, Result, SystemStreams}
55
import mill.bsp.BSP
6-
import mill.client.lock.Lock
6+
import mill.client.lock.{DoubleLock, Lock}
77
import mill.constants.{DaemonFiles, OutFiles, Util}
88
import mill.define.BuildCtx
99
import mill.internal.{Colors, MultiStream, PromptLogger}
@@ -58,6 +58,11 @@ object MillMain {
5858
}
5959
)
6060

61+
val outLock = new DoubleLock(
62+
outMemoryLock,
63+
Lock.file((out / OutFiles.millOutLock).toString)
64+
)
65+
6166
val daemonDir = os.Path(args.head)
6267
val (result, _) =
6368
try main0(
@@ -71,13 +76,15 @@ object MillMain {
7176
initialSystemProperties = sys.props.toMap,
7277
systemExit = i => sys.exit(i),
7378
daemonDir = daemonDir,
74-
outLock = Lock.file((out / OutFiles.millOutLock).toString)
79+
outLock = outLock
7580
)
7681
catch handleMillException(initialSystemStreams.err, ())
7782

7883
System.exit(if (result) 0 else 1)
7984
}
8085

86+
val outMemoryLock = Lock.memory()
87+
8188
private def withStreams[T](
8289
bspMode: Boolean,
8390
streams: SystemStreams

0 commit comments

Comments
 (0)