Skip to content

Commit 970f5ee

Browse files
bric3devflow.devflow-routing-intake
andauthored
chore: Add a back-off for Muzzle Version Range (#11433)
chore: Add a back-off for Muzzle Version Range Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent ec89217 commit 970f5ee

2 files changed

Lines changed: 223 additions & 8 deletions

File tree

buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt

Lines changed: 125 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@ import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory
99
import org.eclipse.aether.repository.LocalRepository
1010
import org.eclipse.aether.repository.RemoteRepository
1111
import org.eclipse.aether.resolution.VersionRangeRequest
12+
import org.eclipse.aether.resolution.VersionRangeResolutionException
1213
import org.eclipse.aether.resolution.VersionRangeResult
1314
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory
1415
import org.eclipse.aether.spi.connector.transport.TransporterFactory
1516
import org.eclipse.aether.transport.file.FileTransporterFactory
1617
import org.eclipse.aether.transport.http.HttpTransporterFactory
1718
import org.eclipse.aether.version.Version
1819
import org.gradle.api.GradleException
20+
import org.gradle.api.logging.Logging
1921
import java.nio.file.Files
2022

2123
internal object MuzzleMavenRepoUtils {
24+
private val log = Logging.getLogger(MuzzleMavenRepoUtils::class.java)
25+
private val backoffDelaysSeconds = listOf(5L, 10L, 30L)
26+
2227
/**
2328
* Remote repositories used to query version ranges and fetch dependencies.
2429
*
@@ -122,12 +127,15 @@ internal object MuzzleMavenRepoUtils {
122127
/**
123128
* Resolves the version range for a given MuzzleDirective using the provided RepositorySystem and RepositorySystemSession.
124129
* Equivalent to the Groovy implementation in MuzzlePlugin.
130+
*
131+
* @param enableBackoffRetries if true, waits 5s, 10s, and 30s after the first three immediate retries
125132
*/
126133
fun resolveVersionRange(
127134
muzzleDirective: MuzzleDirective,
128135
system: RepositorySystem,
129136
session: RepositorySystemSession,
130-
defaultRepos: List<RemoteRepository> = defaultMuzzleRepos()
137+
defaultRepos: List<RemoteRepository> = defaultMuzzleRepos(),
138+
enableBackoffRetries: Boolean = true
131139
): VersionRangeResult {
132140
val directiveArtifact: Artifact = DefaultArtifact(
133141
muzzleDirective.group,
@@ -142,16 +150,56 @@ internal object MuzzleMavenRepoUtils {
142150
}
143151

144152
// In rare cases, the version resolution range silently failed with the maven proxy,
145-
// retries 3 times at most then suggest to restart the job later.
146-
var range = system.resolveVersionRange(session, rangeRequest)
147-
for (i in 0..3) {
148-
if (range.lowestVersion != null && range.highestVersion != null) {
153+
// retries 3 times immediately, then backs off before suggesting to restart the job later.
154+
var attemptCount = 0
155+
var range: VersionRangeResult? = null
156+
var failure: VersionRangeResolutionException? = null
157+
fun attemptResolve(): VersionRangeResult? {
158+
attemptCount++
159+
return try {
160+
range = system.resolveVersionRange(session, rangeRequest)
161+
failure = null
162+
range?.takeIf { it.hasBounds() }
163+
} catch (e: VersionRangeResolutionException) {
164+
failure = e
165+
range = e.result ?: range
166+
null
167+
}
168+
}
169+
170+
repeat(4) {
171+
attemptResolve()?.let { range ->
149172
return range
150173
}
151-
range = system.resolveVersionRange(session, rangeRequest)
152174
}
153175

154-
throw IllegalStateException("The version range resolution failed during report, this is not expected. Advised course of action: Restart the job later.")
176+
var waitedSeconds = 0L
177+
if (enableBackoffRetries) {
178+
for (delaySeconds in backoffDelaysSeconds) {
179+
sleepBeforeBackoffRetry(delaySeconds, directiveArtifact)
180+
waitedSeconds += delaySeconds
181+
attemptResolve()?.let { resolvedRange ->
182+
log.warn(
183+
"Muzzle version range resolution for ${artifactCoordinates(directiveArtifact)} " +
184+
"succeeded after waiting ${waitedSeconds}s across $attemptCount attempts"
185+
)
186+
return resolvedRange
187+
}
188+
}
189+
}
190+
191+
throw IllegalStateException(
192+
versionRangeFailureMessage(
193+
directiveArtifact,
194+
rangeRequest.repositories,
195+
range,
196+
failure,
197+
attemptCount,
198+
waitedSeconds,
199+
enableBackoffRetries
200+
),
201+
failure
202+
)
155203
}
156204

157205
/**
@@ -205,6 +253,76 @@ internal object MuzzleMavenRepoUtils {
205253
*/
206254
fun lowest(a: Version, b: Version): Version = if (a < b) a else b
207255

256+
private fun VersionRangeResult.hasBounds(): Boolean =
257+
lowestVersion != null && highestVersion != null
258+
259+
private fun sleepBeforeBackoffRetry(delaySeconds: Long, artifact: Artifact) {
260+
try {
261+
Thread.sleep(delaySeconds * 1000L)
262+
} catch (e: InterruptedException) {
263+
Thread.currentThread().interrupt()
264+
throw IllegalStateException(
265+
"Interrupted while waiting ${delaySeconds}s before retrying version range resolution for " +
266+
artifactCoordinates(artifact),
267+
e
268+
)
269+
}
270+
}
271+
272+
private fun versionRangeFailureMessage(
273+
artifact: Artifact,
274+
repositories: List<RemoteRepository>,
275+
range: VersionRangeResult?,
276+
failure: VersionRangeResolutionException?,
277+
attemptCount: Int,
278+
waitedSeconds: Long,
279+
enableBackoffRetries: Boolean
280+
): String {
281+
val backoffDetails =
282+
if (enableBackoffRetries) {
283+
"enabled; waited ${waitedSeconds}s using delays ${backoffDelaysSeconds.joinToString(", ") { "${it}s" }}"
284+
} else {
285+
"disabled"
286+
}
287+
return buildString {
288+
appendLine("Muzzle version range resolution failed.")
289+
appendLine("Artifact:")
290+
appendLine(" ${artifactCoordinates(artifact)}")
291+
appendLine("Repositories:")
292+
repositories.forEach { appendLine(" - ${it.id}: ${it.url}") }
293+
appendLine("Attempts:")
294+
appendLine(" $attemptCount")
295+
appendLine("Backoff:")
296+
appendLine(" $backoffDetails")
297+
appendLine("Last resolution result:")
298+
if (range == null) {
299+
appendLine(" <none returned>")
300+
} else {
301+
appendLine(" lowestVersion=${range.lowestVersion ?: "<missing>"}")
302+
appendLine(" highestVersion=${range.highestVersion ?: "<missing>"}")
303+
appendLine(" versionCount=${range.versions.size}")
304+
}
305+
if (failure != null) {
306+
appendLine("Last resolution failure:")
307+
appendLine(" ${failure.javaClass.name}: ${failure.message ?: "<no message>"}")
308+
}
309+
appendLine()
310+
appendLine("Maven metadata resolution may have returned an incomplete range, especially through a proxy.")
311+
appendLine("Restart the job later if the repositories above are reachable.")
312+
}.trimEnd()
313+
}
314+
315+
private fun artifactCoordinates(artifact: Artifact): String {
316+
val classifier = artifact.classifier?.takeUnless { it.isEmpty() }
317+
return listOfNotNull(
318+
artifact.groupId,
319+
artifact.artifactId,
320+
classifier,
321+
artifact.extension,
322+
artifact.version
323+
).joinToString(":")
324+
}
325+
208326
/**
209327
* Convert a muzzle directive to a set of artifacts for all filtered versions.
210328
* Throws GradleException if no artifacts are found.

buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package datadog.gradle.plugin.muzzle
22

33
import datadog.gradle.plugin.MavenRepoFixture
4+
import org.eclipse.aether.RepositorySystem
45
import org.eclipse.aether.artifact.DefaultArtifact
56
import org.eclipse.aether.repository.RemoteRepository
67
import org.eclipse.aether.resolution.VersionRangeRequest
8+
import org.eclipse.aether.resolution.VersionRangeResolutionException
79
import org.eclipse.aether.resolution.VersionRangeResult
810
import org.eclipse.aether.util.version.GenericVersionScheme
911
import org.gradle.api.GradleException
@@ -12,6 +14,8 @@ import org.junit.jupiter.api.io.TempDir
1214
import org.junit.jupiter.params.ParameterizedTest
1315
import org.junit.jupiter.params.provider.CsvSource
1416
import java.io.File
17+
import java.lang.reflect.Proxy
18+
import java.util.concurrent.atomic.AtomicInteger
1519
import org.assertj.core.api.Assertions.assertThat
1620
import org.assertj.core.api.Assertions.assertThatThrownBy
1721

@@ -54,6 +58,32 @@ class MuzzleMavenRepoUtilsTest {
5458
assertThat(resolvedVersions).containsExactly("2.0.0", "3.0.0")
5559
}
5660

61+
@Test
62+
fun `resolveVersionRange retries thrown resolution failures`() {
63+
val directive = MuzzleDirective().apply {
64+
group = "com.example"
65+
module = "mylib"
66+
versions = "[1.0,)"
67+
}
68+
val attempts = AtomicInteger()
69+
val retryingSystem = repositorySystemThrowingThenResolving(
70+
failuresBeforeSuccess = 3,
71+
result = createVersionRangeResult("1.0.0"),
72+
attempts = attempts
73+
)
74+
75+
val result = MuzzleMavenRepoUtils.resolveVersionRange(
76+
directive,
77+
retryingSystem,
78+
newSession(),
79+
emptyList(),
80+
enableBackoffRetries = false
81+
)
82+
83+
assertThat(result.versions.map { it.toString() }).containsExactly("1.0.0")
84+
assertThat(attempts).hasValue(4)
85+
}
86+
5787
@Test
5888
fun `resolveVersionRange throws IllegalStateException when resolution consistently fails`() {
5989
val emptyRepo = RemoteRepository.Builder("empty", "default", File(tempDir, "empty").apply { mkdirs() }.toURI().toString()).build()
@@ -64,8 +94,49 @@ class MuzzleMavenRepoUtilsTest {
6494
}
6595

6696
assertThatThrownBy {
67-
MuzzleMavenRepoUtils.resolveVersionRange(directive, system, newSession(), listOf(emptyRepo))
97+
MuzzleMavenRepoUtils.resolveVersionRange(
98+
directive,
99+
system,
100+
newSession(),
101+
listOf(emptyRepo),
102+
enableBackoffRetries = false
103+
)
68104
}.isInstanceOf(IllegalStateException::class.java)
105+
.hasMessageContaining("Muzzle version range resolution failed")
106+
.hasMessageContaining("com.example:nonexistent:jar:[1.0,)")
107+
.hasMessageContaining("empty:")
108+
.hasMessageContaining("Attempts:\n 4")
109+
.hasMessageContaining("Backoff:\n disabled")
110+
}
111+
112+
@Test
113+
fun `resolveVersionRange failure includes thrown resolution failure details`() {
114+
val directive = MuzzleDirective().apply {
115+
group = "com.example"
116+
module = "mylib"
117+
versions = "[1.0,)"
118+
}
119+
val attempts = AtomicInteger()
120+
val throwingSystem = repositorySystemThrowingThenResolving(
121+
failuresBeforeSuccess = 4,
122+
result = createVersionRangeResult("1.0.0"),
123+
attempts = attempts
124+
)
125+
126+
assertThatThrownBy {
127+
MuzzleMavenRepoUtils.resolveVersionRange(
128+
directive,
129+
throwingSystem,
130+
newSession(),
131+
emptyList(),
132+
enableBackoffRetries = false
133+
)
134+
}.isInstanceOf(IllegalStateException::class.java)
135+
.hasCauseInstanceOf(VersionRangeResolutionException::class.java)
136+
.hasMessageContaining("Attempts:\n 4")
137+
.hasMessageContaining("Last resolution failure:")
138+
.hasMessageContaining("transient version range failure 4")
139+
assertThat(attempts).hasValue(4)
69140
}
70141

71142
@Test
@@ -224,4 +295,30 @@ class MuzzleMavenRepoUtilsTest {
224295
// lowestVersion/highestVersion are computed as versions[0] and versions[last]
225296
return VersionRangeResult(request).apply { this.versions = versions }
226297
}
298+
299+
private fun repositorySystemThrowingThenResolving(
300+
failuresBeforeSuccess: Int,
301+
result: VersionRangeResult,
302+
attempts: AtomicInteger
303+
): RepositorySystem =
304+
Proxy.newProxyInstance(
305+
RepositorySystem::class.java.classLoader,
306+
arrayOf(RepositorySystem::class.java)
307+
) { _, method, args ->
308+
when (method.name) {
309+
"resolveVersionRange" -> {
310+
val attempt = attempts.incrementAndGet()
311+
if (attempt <= failuresBeforeSuccess) {
312+
val request = args?.get(1) as VersionRangeRequest
313+
throw VersionRangeResolutionException(
314+
VersionRangeResult(request),
315+
"transient version range failure $attempt"
316+
)
317+
}
318+
result
319+
}
320+
"toString" -> "repositorySystemThrowingThenResolving"
321+
else -> throw UnsupportedOperationException(method.name)
322+
}
323+
} as RepositorySystem
227324
}

0 commit comments

Comments
 (0)