skip to content

You start an external resource (a Docker container or temp DB) during a build and must guarantee it is torn down even if tasks fail. How would you implement that with a completion hook?

level: middleimportance: should knowfreq 30%

answer

  1. must run on failure path → completion hook
  2. buildFinished (legacy) vs FlowScope.always
  3. BuildService + AutoCloseable.close() auto-teardown
  4. completion hook is build-global, not per-task
  5. config-cache safety pushes you off buildFinished

basics

~10 s

Register a completion hook that runs on the failure path too — legacy gradle.buildFinished { ... } or a config-cache-safe FlowScope.always FlowAction. Put the teardown there so it executes whether tasks pass or fail.

solid answer

~40 s

The requirement is 'runs even on failure', which is precisely what build-completion hooks give you. Two correct options: 1. **Legacy:** `gradle.buildFinished { result -> stopContainer() }`. Simple, but not configuration-cache compatible. 2. **Modern:** a `FlowAction` registered with `flowScope.always(...)`, reading `flowProviders.buildWorkResult` if you want to branch on success/failure. Cache-safe. Key caveat: build-completion hooks run once for the **whole build**, not per task — don't use them for per-task teardown (that's task `finalizedBy` or a `BuildService` with `close()`). For shared external resources, a `BuildService` implementing `AutoCloseable` is often the cleanest: Gradle calls `close()` at the end of the build automatically, which works under the configuration cache and scopes lifetime to the service. Choose the completion hook when teardown is genuinely build-global and not tied to a service.

code

kotlin · 6 lines
kotlin
abstract class ContainerService :
    BuildService<BuildServiceParameters.None>, AutoCloseable {
    private val id = startContainer()
    fun containerId() = id
    override fun close() { stopContainer(id) } // runs at build end, even on failure
}

go deeper

for a junior

Know that buildFinished runs on failure too, so teardown goes there.

for a middle

Compare buildFinished vs FlowAction vs BuildService+AutoCloseable and pick per scenario.

for a senior

Default to BuildService AutoCloseable for owned resources; justify cache-safety and per-resource lifetime over build-global hooks.

for a principal

Standardize teardown patterns across the build platform and codify which mechanism teams should use for shared infra.

## The guarantee you need 'Tear down even on failure' rules out doing cleanup at the end of a task's action — a failing task never reaches its own cleanup code. You need a hook that Gradle invokes on the failure path. Build-completion hooks qualify. ## Option A — legacy buildFinished ```kotlin gradle.buildFinished { result -> dockerClient.stop(containerId) // runs on success AND failure } ``` Works, but reported as a configuration-cache problem because the closure captures live state (e.g. `dockerClient`, `containerId`). ## Option B — Flow API ```kotlin abstract class StopContainerAction : FlowAction<StopContainerAction.Params> { interface Params : FlowParameters { @get:Input val id: Property<String> } override fun execute(p: Params) { /* stop container p.id.get() */ } } flowScope.always(StopContainerAction::class.java) { parameters.id.set(containerId) } ``` Cache-safe because the container id is a serializable `Property`. ## Option C — BuildService with AutoCloseable (often best) A `BuildService` is a shared, lazily-created object scoped to the build. If it implements `AutoCloseable`, Gradle invokes `close()` when the build ends — including on failure — and it is fully configuration-cache compatible: ```kotlin abstract class DbService : BuildService<BuildServiceParameters.None>, AutoCloseable { val db = startEmbeddedDb() override fun close() { db.stop() } // called at build end } ``` This ties the resource's lifetime to the service that owns it, avoids global state, and parallel-safe by construction. It is the idiomatic modern answer when the resource is owned by tasks via `usesService`. ## Choosing - Resource owned/used by specific tasks, want auto-cleanup, cache-safe → **BuildService + AutoCloseable**. - Genuinely build-global completion logic (reporting, notifications) → **FlowAction via flowScope.always**. - Quick script-level hook, cache not a concern → **buildFinished**. ## Common mistake Using `buildFinished` for *per-task* teardown. It fires once per build, so if you start N containers across N tasks you'd need to track them all and tear them down together — a `BuildService` (or `finalizedBy`) models per-resource lifetime far better.

  • Why is a BuildService often better than buildFinished for resource teardown?
    It is configuration-cache compatible, scopes the resource's lifetime to the service (not the whole build), is parallel-safe, and Gradle calls close() automatically at build end.
  • Does finalizedBy guarantee teardown if the build is cancelled mid-task?
    finalizedBy runs the finalizer when its finalized task runs; on hard cancellation neither may complete, so a BuildService.close() or completion hook is more robust for true build-end teardown.

saying these in an interview costs you the question

  • Putting teardown at the end of a task action — it won't run if the task fails.
  • Using buildFinished for per-task resource cleanup (it's build-global, fires once).
  • Ignoring the configuration cache when choosing the mechanism.

context