From 97199dc8edad1e5aac75016ace30176538b645c4 Mon Sep 17 00:00:00 2001 From: unlsycn Date: Fri, 29 Aug 2025 14:59:58 +0800 Subject: [PATCH 1/2] Fix completely broken lock during setuping local repo on Linux On Solaris/Linux if DELETE_ON_CLOSE is specified, the appropriate file is unlinked from the directory when opened and therefore appears inaccessible via path. In this case, each Scala-CLI process considers itself holding the lock, leading to a race condition. Ref: https://bugs.openjdk.org/browse/JDK-7095452 Signed-off-by: unlsycn --- .../main/scala/scala/build/LocalRepo.scala | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/LocalRepo.scala b/modules/build/src/main/scala/scala/build/LocalRepo.scala index 307d7870a0..30fd55e5a7 100644 --- a/modules/build/src/main/scala/scala/build/LocalRepo.scala +++ b/modules/build/src/main/scala/scala/build/LocalRepo.scala @@ -77,11 +77,7 @@ object LocalRepo { // potential race conditions between initial check and lock acquisition if !os.exists(repoDir) then val tmpRepoDir = repoDir / os.up / s".$version.tmp" - try os.remove.all(tmpRepoDir) - catch { - case t: Throwable => - logger.message(s"Error removing $tmpRepoDir: ${t.getMessage}") - } + os.remove.all(tmpRepoDir) using(archiveUrl.openStream()) { is => using(WrappedZipInputStream.create(new BufferedInputStream(is))) { zis => extractZip(zis, tmpRepoDir) @@ -101,30 +97,21 @@ object LocalRepo { val lockFile = dir.resolve(s".lock-$id"); Util.createDirectories(lockFile.getParent) var channel: FileChannel = null + var lock: FileLock = null try { channel = FileChannel.open( lockFile, StandardOpenOption.CREATE, - StandardOpenOption.WRITE, - StandardOpenOption.DELETE_ON_CLOSE + StandardOpenOption.WRITE ) - - var lock: FileLock = null - try { - lock = channel.lock() - - try f - finally { - lock.release() - lock = null - channel.close() - channel = null - } - } - finally if (lock != null) lock.release() + lock = channel.lock() + f + } + finally { + if (lock != null) lock.release() + if (channel != null) channel.close() } - finally if (channel != null) channel.close() } } From d7d77f780dfe486104775a5509471ec9c56e71fb Mon Sep 17 00:00:00 2001 From: unlsycn Date: Tue, 2 Sep 2025 00:24:30 +0800 Subject: [PATCH 2/2] Add test for parallel local repo setup Signed-off-by: unlsycn --- .../cli/integration/RunTestDefinitions.scala | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index ad43142b14..ab84b78478 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -2413,4 +2413,29 @@ abstract class RunTestDefinitions expect(!res.err.trim().contains(legacyRunnerWarning)) } } + + for (parallelInstancesCount <- Seq(2, 5, 10)) + test( + s"run $parallelInstancesCount instances in parallel without local repo" + ) { + TestInputs.empty.fromRoot { root => + val localRepoPath = + os.Path(os.proc( + TestUtil.cli, + "directories", + "--power" + ).call().out.text().linesIterator.find(_.startsWith("Local repository:")).getOrElse( + throw new RuntimeException("Local repository line not found in directories output") + ).split(":", 2).last.trim()) + os.remove.all(localRepoPath) + + val processes = + (0 until parallelInstancesCount).map { _ => + os.proc(TestUtil.cli, "--version") + .spawn(cwd = root) + }.zipWithIndex + processes.foreach { case (p, _) => p.waitFor() } + processes.foreach { case (p, _) => expect(p.exitCode == 0) } + } + } }