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?
answer
- must run on failure path → completion hook
- buildFinished (legacy) vs FlowScope.always
- BuildService + AutoCloseable.close() auto-teardown
- completion hook is build-global, not per-task
- config-cache safety pushes you off buildFinished
basics
~10 sRegister 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 sThe 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 linesabstract 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
Know that buildFinished runs on failure too, so teardown goes there.
Compare buildFinished vs FlowAction vs BuildService+AutoCloseable and pick per scenario.
Default to BuildService AutoCloseable for owned resources; justify cache-safety and per-resource lifetime over build-global hooks.
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.