Skip to content

Commit

Permalink
Acquire a lock on the out dir in order to run tasks / commands (#3599)
Browse files Browse the repository at this point in the history
First cut at #3519

There's no tests yet, but it works fine in manual tests.
  • Loading branch information
alexarchambault authored Oct 8, 2024
1 parent 8b063c4 commit fe73de8
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 6 deletions.
12 changes: 12 additions & 0 deletions integration/feature/output-directory/resources/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,16 @@ import mill.scalalib._

object `package` extends RootModule with ScalaModule {
def scalaVersion = scala.util.Properties.versionNumberString

def hello = Task {
"Hello from hello task"
}

def blockWhileExists(path: os.Path) = Task.Command[String] {
if (!os.exists(path))
os.write(path, Array.emptyByteArray)
while (os.exists(path))
Thread.sleep(100L)
"Blocking command done"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package mill.integration

import mill.testkit.UtestIntegrationTestSuite
import utest._

import java.io.ByteArrayOutputStream
import java.util.concurrent.{CountDownLatch, Executors}

import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext, Future}

object OutputDirectoryLockTests extends UtestIntegrationTestSuite {

private val pool = Executors.newCachedThreadPool()
private val ec = ExecutionContext.fromExecutorService(pool)

override def utestAfterAll(): Unit = {
pool.shutdown()
}

def tests: Tests = Tests {
test("basic") - integrationTest { tester =>
import tester._
val signalFile = workspacePath / "do-wait"
System.err.println("Spawning blocking task")
val blocksFuture =
Future(eval(("show", "blockWhileExists", "--path", signalFile), check = true))(ec)
while (!os.exists(signalFile) && !blocksFuture.isCompleted)
Thread.sleep(100L)
if (os.exists(signalFile))
System.err.println("Blocking task is running")
else {
System.err.println("Failed to run blocking task")
Predef.assert(blocksFuture.isCompleted)
blocksFuture.value.get.get
}

val testCommand: os.Shellable = ("show", "hello")
val testMessage = "Hello from hello task"

System.err.println("Evaluating task without lock")
val noLockRes = eval(("--no-build-lock", testCommand), check = true)
assert(noLockRes.out.contains(testMessage))

System.err.println("Evaluating task without waiting for lock (should fail)")
val noWaitRes = eval(("--no-wait-for-build-lock", testCommand))
assert(noWaitRes.err.contains("Cannot proceed, another Mill process is running tasks"))

System.err.println("Evaluating task waiting for the lock")

val lock = new CountDownLatch(1)
val stderr = new ByteArrayOutputStream
var success = false
val futureWaitingRes = Future {
eval(
testCommand,
stderr = os.ProcessOutput {
val expectedMessage =
"Another Mill process is running tasks, waiting for it to be done..."

(bytes, len) =>
stderr.write(bytes, 0, len)
val output = new String(stderr.toByteArray)
if (output.contains(expectedMessage))
lock.countDown()
},
check = true
)
}(ec)
try {
lock.await()
success = true
} finally {
if (!success) {
System.err.println("Waiting task output:")
System.err.write(stderr.toByteArray)
}
}

System.err.println("Task is waiting for the lock, unblocking it")
os.remove(signalFile)

System.err.println("Blocking task should exit")
val blockingRes = Await.result(blocksFuture, Duration.Inf)
assert(blockingRes.out.contains("Blocking command done"))

System.err.println("Waiting task should be free to proceed")
val waitingRes = Await.result(futureWaitingRes, Duration.Inf)
assert(waitingRes.out.contains(testMessage))
}
}
}
17 changes: 16 additions & 1 deletion main/api/src/mill/api/Logger.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package mill.api

import java.io.{InputStream, PrintStream}

import mill.main.client.lock.{Lock, Locked}

/**
* The standard logging interface of the Mill build tool.
*
Expand All @@ -24,7 +26,7 @@ import java.io.{InputStream, PrintStream}
* but when `show` is used both are forwarded to stderr and stdout is only
* used to display the final `show` output for easy piping.
*/
trait Logger {
trait Logger extends AutoCloseable {
def colored: Boolean

def systemStreams: SystemStreams
Expand Down Expand Up @@ -79,4 +81,17 @@ trait Logger {
try t
finally removePromptLine()
}

def waitForLock(lock: Lock, waitingAllowed: Boolean): Locked = {
val tryLocked = lock.tryLock()
if (tryLocked.isLocked())
tryLocked
else if (waitingAllowed) {
info("Another Mill process is running tasks, waiting for it to be done...")
lock.lock()
} else {
error("Cannot proceed, another Mill process is running tasks")
throw new Exception("Cannot acquire lock on Mill output directory")
}
}
}
4 changes: 4 additions & 0 deletions main/client/src/mill/main/client/OutFiles.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,9 @@ public class OutFiles {
*/
final public static String millNoServer = "mill-no-server";

/**
* Lock file used for exclusive access to the Mill output directory
*/
final public static String millLock = "mill-lock";

}
22 changes: 22 additions & 0 deletions main/client/src/mill/main/client/lock/DummyLock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package mill.main.client.lock;

import java.util.concurrent.locks.ReentrantLock;

class DummyLock extends Lock {

public boolean probe() {
return true;
}

public Locked lock() {
return new DummyTryLocked();
}

public TryLocked tryLock() {
return new DummyTryLocked();
}

@Override
public void close() throws Exception {
}
}
11 changes: 11 additions & 0 deletions main/client/src/mill/main/client/lock/DummyTryLocked.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package mill.main.client.lock;

class DummyTryLocked implements TryLocked {
public DummyTryLocked() {
}

public boolean isLocked(){ return true; }

public void release() throws Exception {
}
}
13 changes: 13 additions & 0 deletions main/client/src/mill/main/client/lock/Lock.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,17 @@ public void await() throws Exception {
*/
public abstract boolean probe() throws Exception;
public void delete() throws Exception {}

public static Lock file(String path) throws Exception {
return new FileLock(path);
}

public static Lock memory() {
return new MemoryLock();
}

public static Lock dummy() {
return new DummyLock();
}

}
14 changes: 13 additions & 1 deletion runner/src/mill/runner/MillCliConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,19 @@ case class MillCliConfig(
status at the command line and falls back to the legacy ticker
"""
)
disablePrompt: Flag = Flag()
disablePrompt: Flag = Flag(),
@arg(
hidden = true,
doc =
"""Evaluate tasks / commands without acquiring an exclusive lock on the Mill output directory"""
)
noBuildLock: Flag = Flag(),
@arg(
hidden = true,
doc =
"""Do not wait for an exclusive lock on the Mill output directory to evaluate tasks / commands. Fail if waiting for a lock is needed."""
)
noWaitForBuildLock: Flag = Flag()
)

import mainargs.ParserForClass
Expand Down
19 changes: 15 additions & 4 deletions runner/src/mill/runner/MillMain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import mill.api.{MillException, SystemStreams, WorkspaceRoot, internal}
import mill.bsp.{BspContext, BspServerResult}
import mill.main.BuildInfo
import mill.main.client.{OutFiles, ServerFiles}
import mill.main.client.lock.Lock
import mill.util.{PromptLogger, PrintLogger, Colors}

import java.lang.reflect.InvocationTargetException
import scala.util.control.NonFatal
import scala.util.Using

@internal
object MillMain {
Expand Down Expand Up @@ -209,6 +211,10 @@ object MillMain {
.map(_ => Seq(bspCmd))
.getOrElse(config.leftoverArgs.value.toList)

val out = os.Path(OutFiles.out, WorkspaceRoot.workspaceRoot)
val outLock =
if (config.noBuildLock.value || bspContext.isDefined) Lock.dummy()
else Lock.file((out / OutFiles.millLock).toString)
var repeatForBsp = true
var loopRes: (Boolean, RunnerState) = (false, RunnerState.empty)
while (repeatForBsp) {
Expand All @@ -235,9 +241,16 @@ object MillMain {
colored = colored,
colors = colors
)
try new MillBuildBootstrap(
Using.resources(
logger,
logger.waitForLock(
outLock,
waitingAllowed = !config.noWaitForBuildLock.value
)
) { (_, _) =>
new MillBuildBootstrap(
projectRoot = WorkspaceRoot.workspaceRoot,
output = os.Path(OutFiles.out, WorkspaceRoot.workspaceRoot),
output = out,
home = config.home,
keepGoing = config.keepGoing.value,
imports = config.imports,
Expand All @@ -252,8 +265,6 @@ object MillMain {
config.allowPositional.value,
systemExit = systemExit
).evaluate()
finally {
logger.close()
}
},
colors = colors
Expand Down

0 comments on commit fe73de8

Please sign in to comment.