Skip to content

Commit 659ee1c

Browse files
authored
Add fault tolerance for builds which hang upon retrieval (#52)
Also: - Adds info-level logging of what builds are in flight to help identity hanging scans - Simplifies the `DevelocityService.build` function to eliminate the vestigial transform argument
1 parent a5d5ffd commit 659ee1c

7 files changed

+53
-43
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ extensions.configure<MetricsForDevelocityExtension> {
6464
// when querying build data. Defaults to 24.
6565
develocityMaxConcurrency.set(10)
6666

67+
// Optional: Set the maximum number of seconds to wait for an individual build scan to be
68+
// processed before skipping it and proceeding with data retrieval. This provides some
69+
// mitigation if Develocity is slow to return a scan. Defaults to 120 seconds.
70+
buildScanRetrievalTimeout.set(60)
71+
6772
// Optional: Add custom summarizers to the plugin. These summarizers will be used to
6873
// build
6974
summarizers.add(MyCustomSummarizer())

src/main/kotlin/com/ebay/plugins/metrics/develocity/GatherHourlyTask.kt

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import kotlinx.coroutines.Dispatchers
1010
import kotlinx.coroutines.Job
1111
import kotlinx.coroutines.channels.Channel
1212
import kotlinx.coroutines.channels.ReceiveChannel
13-
import kotlinx.coroutines.channels.consumeEach
1413
import kotlinx.coroutines.delay
1514
import kotlinx.coroutines.isActive
1615
import kotlinx.coroutines.launch
1716
import kotlinx.coroutines.runBlocking
17+
import kotlinx.coroutines.withTimeoutOrNull
1818
import org.gradle.api.DefaultTask
1919
import org.gradle.api.file.DirectoryProperty
2020
import org.gradle.api.model.ObjectFactory
@@ -31,6 +31,7 @@ import java.time.OffsetDateTime
3131
import java.time.ZoneId
3232
import java.time.ZoneOffset
3333
import java.time.format.DateTimeFormatter
34+
import java.util.concurrent.CopyOnWriteArrayList
3435
import java.util.concurrent.atomic.AtomicInteger
3536
import javax.inject.Inject
3637
import kotlin.math.roundToInt
@@ -85,11 +86,15 @@ open class GatherHourlyTask @Inject constructor(
8586
@get:Internal
8687
override val summarizersProperty: ListProperty<MetricSummarizer<*>> = objectFactory.listProperty(MetricSummarizer::class.java)
8788

89+
@get:Internal
90+
val buildScanRetrievalTimeoutProperty: Property<Int> = objectFactory.property(Int::class.java)
91+
8892
@get:OutputDirectory
8993
override val outputDirectoryProperty: DirectoryProperty = objectFactory.directoryProperty()
9094

9195
private val refCount = AtomicInteger()
9296
private val processedCount = AtomicInteger()
97+
private val processingBuilds = CopyOnWriteArrayList<String>()
9398

9499
@TaskAction
95100
fun gather() {
@@ -179,13 +184,21 @@ open class GatherHourlyTask @Inject constructor(
179184
val detailsQuery = BuildQuery(
180185
models = modelsNeeded
181186
)
182-
channel.consumeEach { buildRef ->
183-
develocityServiceProperty.get().build(buildRef.id, detailsQuery) { build ->
187+
val timeout = buildScanRetrievalTimeoutProperty.get().seconds
188+
for (buildRef in channel) {
189+
processingBuilds.add(buildRef.id)
190+
val build = withTimeoutOrNull(timeout) {
191+
develocityServiceProperty.get().build(buildRef.id, detailsQuery)
192+
}
193+
if (build == null) {
194+
logger.warn("Unable to process build ${buildRef.id}! Skipping!")
195+
} else {
184196
summarizerStates.forEach { state ->
185197
state.ingestBuild(build)
186198
}
187-
processedCount.incrementAndGet()
188199
}
200+
processingBuilds.remove(buildRef.id)
201+
processedCount.incrementAndGet()
189202
}
190203
}
191204
}
@@ -213,6 +226,18 @@ open class GatherHourlyTask @Inject constructor(
213226
} else {
214227
"%.2f / second".format(processedSoFar / elapsedSeconds)
215228
}
216-
logger.info("Processed $processedSoFar build(s) in ${elapsedSeconds.roundToInt()} second(s): $rate")
229+
val processingBuilds = processingBuilds.toArray().let { processing ->
230+
if (processing.isEmpty()) {
231+
""
232+
} else {
233+
processing.joinToString(
234+
prefix = " (Processing ${processing.size} build(s): ",
235+
separator = ", ",
236+
postfix = ")"
237+
)
238+
}
239+
240+
}
241+
logger.info("Processed $processedSoFar build(s) in ${elapsedSeconds.roundToInt()} second(s): $rate $processingBuilds")
217242
}
218243
}

src/main/kotlin/com/ebay/plugins/metrics/develocity/MetricsForDevelocityExtension.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ abstract class MetricsForDevelocityExtension : ExtensionAware {
4242
*/
4343
abstract val develocityMaxConcurrency: Property<Int>
4444

45+
/**
46+
* The maximum amount of time, in seconds, to wait for an individual build scan to be processed.
47+
* If a build scan takes longer than this amount of time, it will be discarded / skipped,
48+
* mainly to prevent hanging the entire data collection process. By default, the timeout
49+
* is set to 2 minutes.
50+
*/
51+
abstract val buildScanRetrievalTimeout: Property<Int>
52+
4553
/**
4654
* Custom build data summarizers to apply to the build data. These capture the details
4755
* of the build that are important to report upon and summarize them in a way that can

src/main/kotlin/com/ebay/plugins/metrics/develocity/MetricsForDevelocityInternalExtension.kt

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/main/kotlin/com/ebay/plugins/metrics/develocity/MetricsForDevelocityProjectPlugin.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ internal class MetricsForDevelocityProjectPlugin @Inject constructor(
6060
develocityQueryFilter.convention(
6161
providerFactory.gradleProperty(QUERY_FILTER_PROPERTY)
6262
.orElse("project:${project.name}"))
63+
64+
buildScanRetrievalTimeout.convention(120)
6365
}
6466

6567
// Register the build service used to query Develocity for build data
@@ -278,6 +280,7 @@ internal class MetricsForDevelocityProjectPlugin @Inject constructor(
278280
queryProperty.set(ext.develocityQueryFilter)
279281
summarizersProperty.set(ext.summarizers)
280282
develocityServiceProperty.set(buildServiceProvider)
283+
buildScanRetrievalTimeoutProperty.set(ext.buildScanRetrievalTimeout)
281284
outputDirectoryProperty.set(PathUtil.hourlyOutputDir(project.layout, timeSpec))
282285
maxConcurrencyProperty.set(ext.develocityMaxConcurrency)
283286
usesService(buildServiceProvider)

src/main/kotlin/com/ebay/plugins/metrics/develocity/service/DevelocityBuildService.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,15 @@ abstract class DevelocityBuildService @Inject constructor(
8080
} ?: flow
8181
}
8282

83-
override suspend fun <T> build(
83+
override suspend fun build(
8484
buildId: String,
8585
params: BuildQuery,
86-
transform: (Build) -> T,
87-
): T? = withContext(dispatcher) {
88-
transform.invoke(api.buildsApi.getBuild(
86+
): Build? = withContext(dispatcher) {
87+
api.buildsApi.getBuild(
8988
id = buildId,
9089
models = params.models,
9190
allModels = params.allModels,
92-
))
91+
)
9392
}
9493

9594
private fun resolveServer(): String {

src/main/kotlin/com/ebay/plugins/metrics/develocity/service/DevelocityService.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@ interface DevelocityService {
1919
/**
2020
* Get build details for the build with the given ID.
2121
*/
22-
suspend fun <T> build(
22+
suspend fun build(
2323
buildId: String,
2424
params: BuildQuery,
25-
transform: (Build) -> T,
26-
): T?
27-
28-
}
25+
): Build?
26+
}

0 commit comments

Comments
 (0)