How do mustRunAfter and shouldRunAfter interact with parallel execution and determinism in a large Gradle build, and how would you use them to govern ordering across many subprojects?
answer
- unconnected tasks may run in parallel
- ordering edge serializes a specific pair
- shared resource/port contention use case
- must for correctness, should for cosmetics
- not a substitute for inputs/outputs
basics
~20 sOrdering rules only constrain relative order between the two named tasks; tasks with no ordering or dependency edge between them may run in parallel and in non-deterministic order. Use mustRunAfter/shouldRunAfter to force a specific sequence where it matters without making one task depend on the other.
solid answer
~50 sUnder `--parallel` (and within a build, across project boundaries), Gradle schedules any tasks whose constraints are satisfied, so tasks lacking a connecting edge can execute concurrently or in arbitrary order. `mustRunAfter`/`shouldRunAfter` add explicit *ordering* edges that serialize specific pairs: a `mustRunAfter` guarantees the predecessor finishes before the successor starts, even across subprojects, without coupling their existence. This is the right tool when two otherwise-independent tasks must not overlap or must run in a stable order — e.g. a shared resource (a port, a database, a filesystem location) that two tasks both touch. Rather than inventing a fake dependency, you declare `taskB.mustRunAfter(taskA)` so they're serialized when both run, while each can still run alone. For purely cosmetic ordering (nicer logs, deterministic report order) prefer `shouldRunAfter`, since it won't risk an unsatisfiable graph. Note that ordering rules don't *replace* proper inputs/outputs for parallel-safety — they're a coarse sequencing lever.
code
kotlin · 10 lines// Serialize two otherwise-independent integration suites that share a fixed port,
// without forcing one to run when the other does.
tasks.named("integTest") {
mustRunAfter(":serviceA:integTest") // hard order when both are scheduled
}
// Cosmetic-only: prefer unit reports before integration reports, never risk a broken graph
tasks.named("aggregateReports") {
shouldRunAfter("integTest")
}go deeper
Know that tasks without a connecting edge may run in any order / in parallel.
Use mustRunAfter to serialize a contended pair without a dependency, and pick should for cosmetic ordering.
Reason about parallel scheduling, resource contention, and the must-vs-should tradeoff; know ordering isn't a substitute for inputs/outputs.
Define build-wide ordering governance via convention plugins, balancing determinism against parallel throughput across many modules.
## Parallelism and the task graph Gradle executes the task graph respecting all edges: dependency edges *and* ordering edges. A task becomes eligible to run once every task it must come after has finished. Two tasks with **no** edge between them — and no shared dependency forcing order — are free to run in any order, and under `--parallel` (or parallel project execution) potentially **at the same time**. This non-determinism is usually fine and desirable for throughput. It becomes a problem when two independent tasks contend for a **shared resource** or when stable ordering matters for reproducibility. ## Using ordering rules as a serialization lever Suppose two integration suites in different subprojects both bind to the same fixed port. They have no real dependency on each other, so Gradle may run them concurrently and they collide. ```kotlin // project :serviceB build script tasks.named("integTest") { mustRunAfter(":serviceA:integTest") } ``` Now they are serialized whenever both run, but `:serviceB:integTest` can still run alone. You avoided a bogus dependency (which would have forced `:serviceA:integTest` to run every time) while eliminating the race. ### must vs should under parallelism - `mustRunAfter` — hard serialization; reliable for correctness (resource contention). - `shouldRunAfter` — best-effort; good for *preferred* ordering (deterministic report/log order) where you don't want to risk an unsatisfiable graph and can tolerate the rule being dropped. ## Governing ordering across many subprojects When ordering conventions must hold build-wide, declare them in a **convention/precompiled plugin** or an aggregating root configuration, using lazy references so configuration stays cheap: ```kotlin subprojects { tasks.matching { it.name == "integTest" }.configureEach { // serialize all integTests after a global setup task mustRunAfter(rootProject.tasks.named("provisionSharedEnv")) } } ``` (In practice prefer precompiled convention plugins over `subprojects { }`, but the ordering principle is the same.) ## Important caveats 1. **Ordering rules are not a substitute for correct inputs/outputs.** For cache-correctness and reliable parallel safety, model real producer/consumer relationships via task inputs/outputs where one exists. Ordering rules are for cases where there is *no* data dependency, only a sequencing or contention concern. 2. **They don't reduce parallelism beyond the constrained pair.** Other independent tasks still run concurrently. 3. **`shouldRunAfter` may silently not hold**, so never depend on it for race-freedom — use `mustRunAfter` for correctness. ## Summary Ordering rules let you impose *just enough* sequence to tame non-deterministic parallel scheduling — serializing contended tasks or stabilizing order — without the heavy semantics of a dependency.
- Two integration tasks in different modules race on a shared port. Would you use a dependency or an ordering rule, and which one?An ordering rule — specifically `mustRunAfter` — so they serialize when both run without forcing one to run whenever the other does. A dependency would over-couple their execution.
- Why is `mustRunAfter` for resource contention not a full substitute for modeling inputs/outputs?Ordering rules only sequence; they don't capture data dependencies for incremental builds or cache correctness. Where a real producer/consumer relationship exists, model inputs/outputs so up-to-date and caching work properly.
- Does adding one ordering edge reduce overall build parallelism much?Only between the two constrained tasks. All other independent tasks remain free to run concurrently, so the throughput impact is localized.
Think of a kitchen where cooks work in parallel. Two cooks who never share a station work simultaneously. mustRunAfter is telling cook B "don't use the one oven until cook A is done" — it serializes only that contention, leaving everyone else parallel.
saying these in an interview costs you the question
- Using shouldRunAfter to fix a real resource race (it can be silently dropped).
- Adding a fake dependsOn to serialize tasks, over-coupling execution.
- Believing ordering rules replace proper input/output modeling for parallel safety.