Skip to content

Commit f39515c

Browse files
Fixed GitLab concurrency issue.
1 parent 321e2f2 commit f39515c

2 files changed

Lines changed: 70 additions & 20 deletions

File tree

build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/GradleDistributionCache.kt

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import java.io.RandomAccessFile
88
import java.net.HttpURLConnection
99
import java.net.URI
1010
import java.net.URLConnection
11+
import java.nio.channels.OverlappingFileLockException
1112
import java.security.MessageDigest
12-
import java.util.concurrent.ConcurrentHashMap
1313
import java.util.zip.ZipInputStream
1414

1515
/** Nested distributions, under `wrapper/` so CI restores them from its existing cache. */
@@ -23,9 +23,7 @@ private val LAUNCHER_JAR = Regex("gradle-launcher-.*\\.jar")
2323

2424
private const val CONNECT_TIMEOUT_MS = 30_000
2525
private const val READ_TIMEOUT_MS = 120_000
26-
27-
/** Serializes same-JVM callers before they acquire the cross-process file lock. */
28-
private val installMonitors = ConcurrentHashMap<String, Any>()
26+
private const val LOCK_RETRY_DELAY_MS = 100L
2927

3028
/** Provisions [gradleVersion] once in [cacheDir], trying [distributionUris] in order. */
3129
internal fun provisionGradleDistribution(
@@ -42,22 +40,35 @@ internal fun provisionGradleDistribution(
4240

4341
installedDistributionRoot(installDir, gradleVersion, marker)?.let { return it }
4442

45-
val monitor = installMonitors.computeIfAbsent(installDir.absolutePath) { Any() }
46-
synchronized(monitor) {
47-
installedDistributionRoot(installDir, gradleVersion, marker)?.let { return it }
48-
if (!cacheDir.isDirectory && !cacheDir.mkdirs()) {
49-
throw GradleException("Could not create Gradle distribution cache: ${cacheDir.absolutePath}")
50-
}
51-
RandomAccessFile(File(cacheDir, "gradle-$gradleVersion-bin.lock"), "rw").use { lockFile ->
52-
lockFile.channel.lock().use {
53-
// Recheck after waiting for the file lock.
54-
installedDistributionRoot(installDir, gradleVersion, marker)?.let { return it }
55-
install(installDir, marker, gradleVersion, distributionUris, logger)
56-
return installedDistributionRoot(installDir, gradleVersion, marker)
57-
?: throw GradleException(
58-
"Gradle $gradleVersion was unpacked into ${installDir.absolutePath} but no " +
59-
"distribution root could be found in it",
60-
)
43+
if (!cacheDir.isDirectory && !cacheDir.mkdirs() && !cacheDir.isDirectory) {
44+
throw GradleException("Could not create Gradle distribution cache: ${cacheDir.absolutePath}")
45+
}
46+
return withDistributionFileLock(File(cacheDir, "gradle-$gradleVersion-bin.lock")) {
47+
// Recheck after waiting for the file lock.
48+
installedDistributionRoot(installDir, gradleVersion, marker)
49+
?.let { return@withDistributionFileLock it }
50+
install(installDir, marker, gradleVersion, distributionUris, logger)
51+
installedDistributionRoot(installDir, gradleVersion, marker)
52+
?: throw GradleException(
53+
"Gradle $gradleVersion was unpacked into ${installDir.absolutePath} but no " +
54+
"distribution root could be found in it",
55+
)
56+
}
57+
}
58+
59+
private fun <T> withDistributionFileLock(lockPath: File, action: () -> T): T {
60+
RandomAccessFile(lockPath, "rw").use { lockFile ->
61+
while (true) {
62+
val lock =
63+
try {
64+
lockFile.channel.lock()
65+
} catch (_: OverlappingFileLockException) {
66+
// Another caller in this JVM holds the lock.
67+
Thread.sleep(LOCK_RETRY_DELAY_MS)
68+
continue
69+
}
70+
lock.use {
71+
return action()
6172
}
6273
}
6374
}

build-logic/smoke-test/src/test/kotlin/datadog/buildlogic/smoketest/GradleDistributionCacheTest.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import org.gradle.api.logging.Logging
88
import org.junit.jupiter.api.Test
99
import org.junit.jupiter.api.io.TempDir
1010
import java.io.File
11+
import java.io.RandomAccessFile
1112
import java.net.InetSocketAddress
1213
import java.net.URI
1314
import java.nio.file.Path
1415
import java.security.MessageDigest
16+
import java.util.concurrent.CompletableFuture
1517
import java.util.concurrent.ConcurrentLinkedQueue
18+
import java.util.concurrent.CountDownLatch
1619
import java.util.concurrent.CyclicBarrier
1720
import java.util.concurrent.TimeUnit
1821
import java.util.concurrent.atomic.AtomicInteger
@@ -192,6 +195,42 @@ class GradleDistributionCacheTest {
192195
}
193196
}
194197

198+
@Test
199+
fun `waits when the distribution lock is already held in this JVM`() {
200+
val distribution = writeDistributionArchive("overlapping-lock.zip", VERSION)
201+
assertThat(cacheDir.mkdirs()).isTrue()
202+
val started = CountDownLatch(1)
203+
val result = CompletableFuture<File>()
204+
val worker = Thread {
205+
started.countDown()
206+
try {
207+
result.complete(
208+
provisionGradleDistribution(cacheDir, VERSION, listOf(distribution), logger),
209+
)
210+
} catch (e: Throwable) {
211+
result.completeExceptionally(e)
212+
}
213+
}
214+
215+
val root =
216+
try {
217+
RandomAccessFile(File(cacheDir, "gradle-$VERSION-bin.lock"), "rw").use { lockFile ->
218+
lockFile.channel.lock().use {
219+
worker.start()
220+
assertThat(started.await(10, TimeUnit.SECONDS)).isTrue()
221+
Thread.sleep(200)
222+
assertThat(result.isDone).isFalse()
223+
}
224+
}
225+
result.get(30, TimeUnit.SECONDS)
226+
} finally {
227+
worker.join(TimeUnit.SECONDS.toMillis(30))
228+
}
229+
230+
assertThat(root).isDirectory()
231+
assertThat(File(root, "lib/gradle-launcher-$VERSION.jar")).exists()
232+
}
233+
195234
@Test
196235
fun `requires at least one source`() {
197236
assertThatThrownBy {

0 commit comments

Comments
 (0)