How do you implement a build-listener build service that observes task execution events via OperationCompletionListener, and how is it registered?
answer
- BuildService + OperationCompletionListener
- override onFinish(FinishEvent)
- TaskFinishEvent.result: success/failure/skipped
- BuildEventsListenerRegistry.onTaskCompletion(provider)
- config-cache-safe replacement for TaskExecutionListener
basics
~10 sMake a BuildService also implement OperationCompletionListener, override onFinish(event) to inspect TaskFinishEvent results, then register it with BuildEventsListenerRegistry.onTaskCompletion(provider). This is the configuration-cache-safe replacement for legacy TaskExecutionListener.
solid answer
~40 sLegacy callbacks like `gradle.taskGraph` listeners and `TaskExecutionListener` are incompatible with the configuration cache. The modern, supported way to react to task completion is a **build service that also implements `OperationCompletionListener`**. You override `onFinish(event: FinishEvent)`; for task events the payload is a `TaskFinishEvent` whose `getResult()` is a `TaskSuccessResult` / `TaskFailureResult` / `TaskSkippedResult`. You register it by injecting `BuildEventsListenerRegistry` (via `@Inject` in a plugin or settings) and calling `registry.onTaskCompletion(serviceProvider)`, passing the `Provider` returned from `registerIfAbsent`. Gradle keeps the service alive for the build and feeds it completion events as tasks finish — across parallel execution. Because it's a build service, it can accumulate state safely and is configuration-cache compatible. This pattern underpins build scans, timing collectors, and custom reporting.
code
kotlin · 17 linesabstract class TimingService :
BuildService<BuildServiceParameters.None>, OperationCompletionListener {
override fun onFinish(event: FinishEvent) {
if (event is TaskFinishEvent)
println(event.descriptor.taskPath + " -> " + event.result)
}
}
abstract class TimingPlugin @Inject constructor(
private val registry: BuildEventsListenerRegistry,
) : Plugin<Project> {
override fun apply(p: Project) {
val svc = p.gradle.sharedServices.registerIfAbsent(
"timing", TimingService::class) {}
registry.onTaskCompletion(svc)
}
}go deeper
Know there's a modern listener service that observes task completion instead of the old listeners.
Name OperationCompletionListener.onFinish and BuildEventsListenerRegistry.onTaskCompletion as the registration path.
Explain config-cache motivation, the TaskFinishEvent result types, passing the Provider, and thread-safe accumulation.
Design org-wide build-telemetry/reporting on this pattern and reason about its parallel-event semantics and lifecycle/close flushing.
## Why not the old listeners Classic approaches — `gradle.addListener(...)`, `TaskExecutionListener`, `taskGraph.afterTask {}` — capture live build state and are **not** configuration-cache compatible; Gradle deprecated them for this purpose. The replacement is a build service wired to the **build events** infrastructure. ## The shape of a listener service A build service can additionally implement `OperationCompletionListener` (from the tooling-events API). You override one method: ```kotlin import org.gradle.tooling.events.FinishEvent import org.gradle.tooling.events.OperationCompletionListener import org.gradle.tooling.events.task.TaskFinishEvent abstract class TimingService : BuildService<BuildServiceParameters.None>, OperationCompletionListener, AutoCloseable { override fun onFinish(event: FinishEvent) { if (event is TaskFinishEvent) { val path = event.descriptor.taskPath val ms = event.result.endTime - event.result.startTime // record (path, ms) — safe to accumulate here } } override fun close() { /* flush report */ } } ``` `event.result` is one of `TaskSuccessResult`, `TaskFailureResult`, or `TaskSkippedResult` — pattern-match to know the outcome. ## Registration You do **not** register it through `gradle.addListener`. Instead inject `BuildEventsListenerRegistry`: ```kotlin abstract class TimingPlugin @Inject constructor( private val registry: BuildEventsListenerRegistry ) : Plugin<Project> { override fun apply(project: Project) { val svc = project.gradle.sharedServices.registerIfAbsent( "timing", TimingService::class) {} registry.onTaskCompletion(svc) // pass the Provider, not .get() } } ``` `onTaskCompletion(provider)` subscribes the service to task-finish events. You pass the lazy `Provider`, so the service is only instantiated if needed, preserving laziness and configuration-cache compatibility. ## Relationship to @ServiceReference This is the *other* half of build-service consumption: `@ServiceReference` is how a **task** pulls a service in; `onTaskCompletion` is how a **listener** service subscribes to the build's task-completion stream. A single service could even do both, though usually a listener service is registration-only. ## Behavior under parallelism Events arrive as tasks finish, potentially from many workers, so the service must treat `onFinish` as possibly-concurrent and accumulate state thread-safely (e.g. a concurrent collection).
- Why not use TaskExecutionListener / gradle.addListener for this?Those legacy listeners are not configuration-cache compatible; the build-service + BuildEventsListenerRegistry pattern is the supported, cache-safe replacement.
- What do you pass to onTaskCompletion, and why the Provider rather than the instance?You pass the Provider returned by registerIfAbsent. Passing the lazy Provider keeps the service uninstantiated until needed and preserves configuration-cache compatibility.
- How do you distinguish a failed task from a successful one in onFinish?Inspect event.result: it is a TaskSuccessResult, TaskFailureResult, or TaskSkippedResult — branch on the concrete type.
saying these in an interview costs you the question
- Registering the listener via gradle.addListener instead of BuildEventsListenerRegistry.
- Passing service.get() instead of the Provider to onTaskCompletion, defeating laziness.
- Assuming onFinish is single-threaded — events can arrive concurrently under parallel execution.