What is the @Destroys annotation for, and how does it affect task scheduling and parallel execution?
answer
- declares paths the task deletes
- clean / Delete task prototype
- mutual exclusion vs producers/consumers
- parallel-build safety
- complements mustRunAfter, not replaces
basics
~20 s@Destroys marks a path a task removes (e.g. a clean task deleting build/). Gradle uses it to schedule destroyer tasks so they never overlap with tasks that produce or consume those same paths, preventing data races in parallel builds.
solid answer
~40 s`@Destroys` declares files/directories a task **deletes** rather than produces — the classic case being a `clean`-style task that removes `build/`. Because such a task is destructive, Gradle must guarantee it never runs **concurrently** with, or in the wrong order relative to, tasks that **create or read** those same paths. With `@Destroys`, Gradle's scheduler treats the destroyed path like a shared resource: a destroyer and a producer/consumer of overlapping paths are mutually exclusive and are ordered so the destroyer doesn't wipe outputs another running task just produced. Without it, a parallel build could interleave `clean` with `compile` and delete freshly-written class files. It complements (doesn't replace) explicit `mustRunAfter`/`shouldRunAfter` ordering by giving the scheduler the actual destroyed-path set to reason about.
code
kotlin · 13 linesabstract class CleanGenerated : DefaultTask() {
@get:Destroys
abstract val generatedDir: DirectoryProperty
@TaskAction
fun clean() {
project.delete(generatedDir)
}
}
tasks.register<CleanGenerated>("cleanGenerated") {
generatedDir.set(layout.buildDirectory.dir("generated"))
}go deeper
Recognize @Destroys marks paths a task deletes, like a clean task removing build/.
Explain that Gradle uses it to avoid running the destroyer concurrently with tasks that produce or read the same paths.
Detail the mutual-exclusion + safe-ordering scheduling behavior under --parallel and contrast with mustRunAfter/shouldRunAfter.
Reason about build-correctness governance: mandating @Destroys on all custom destructive tasks to keep large parallel builds race-free without per-task hand-wiring.
## The problem @Destroys solves Most task properties describe **inputs** (consumed) and **outputs** (produced). But some tasks are **destructive**: their whole job is to *remove* files. The built-in `clean` task (a `Delete`) is the prototype — it deletes `build/`. In a **parallel** or aggressively-scheduled build, Gradle runs independent tasks concurrently and may reorder tasks that have no declared dependency between them. A destructive task that isn't described creates a hazard: - `clean` deletes `build/classes` **while** or **just after** `compileJava` writes into it → corrupted/missing outputs, flaky builds. ## What @Destroys declares ``` @get:Destroys abstract val targetDir: DirectoryProperty ``` It tells Gradle: *this task removes the paths held by this property.* Gradle's execution planner then enforces, for any overlap between a destroyer's destroyed paths and another task's input/output paths: 1. **Mutual exclusion** — the destroyer and the producer/consumer never run at the same time. 2. **Safe ordering** — when both are scheduled, the destroyer is ordered so it cannot wipe outputs that a producer is about to need or has just created. In practice a destroyer is sequenced relative to producers/consumers of the same paths. This is reasoned about from the **declared paths**, similar to how Gradle infers task dependencies from output→input wiring, except here it's a *negative* relationship (destroy vs. produce/consume). ## Relationship to ordering rules `@Destroys` doesn't replace `mustRunAfter`/`shouldRunAfter`. Those express *task→task* ordering intent. `@Destroys` gives the scheduler the **path facts** so it can avoid hazards even between tasks you didn't explicitly order. The canonical guidance — e.g., `clean` should generally run before `build` — is still expressed with ordering rules; `@Destroys` ensures parallel safety underneath. ## When you'd author it Mostly when you write a **custom destructive task type** (a custom cleaner, a cache-wiper, a teardown task) that deletes a path other tasks touch. Declaring `@Destroys` lets Gradle keep the build correct under `--parallel` without you hand-wiring exclusions to every producer. ``` abstract class WipeReports : DefaultTask() { @get:Destroys abstract val reportsDir: DirectoryProperty @TaskAction fun wipe() = project.delete(reportsDir) } ``` ## Caveat `@Destroys` is about scheduling safety. It is **not** an input/output for up-to-date checks — a destroyer typically has no meaningful up-to-date state and runs each time.
- Why isn't a plain mustRunAfter enough to make a destroyer safe under --parallel?mustRunAfter only orders the two specific tasks you name. @Destroys gives the scheduler the destroyed-path set so it can avoid concurrency hazards with ANY producer/consumer of those paths, including ones you didn't explicitly wire.
- Does @Destroys participate in up-to-date checking?No. It's purely a scheduling/concurrency-safety declaration; the destroyed path isn't an input or output fingerprint, so destroyer tasks generally run every invocation.
@Destroys is like telling a shared-kitchen scheduler 'I'm the cleaner who empties this counter' — they make sure no cook is plating food on that counter while you wipe it.
saying these in an interview costs you the question
- Saying @Destroys makes the path an output — it's the opposite, a declared deletion.
- Claiming it caches results or affects up-to-date checks.
- Thinking it replaces explicit ordering rules entirely.