Skip to content

ADR-005: Async is scoped to I/O concurrency, not a scheduled execution plan

Status: accepted

Context

Applying an overlay touches many independent resources: files to symlink, git repositories to clone, packages to install. A scheduler that built a plan of independent actions and ran them with bounded parallelism was one option (src/exec/plan.rs exists as a named module for exactly this). A simpler sequential walk, parallelizing only where ordering genuinely doesn't matter, was the other.

Decision

Overlay::apply_inner is a sequential, recursively-async function: uses entries apply before the overlay's own files, and each one is awaited before moving on. visited/stack sets make diamond dependencies safe and cycles an error — logic that is easier to reason about single-threaded than across a concurrent scheduler.

The one place that genuinely runs in parallel is actions::git:: clone_repositories, which join_alls a tokio::spawned task per declared git repository, because independent clones have no ordering constraint between them. Elsewhere, async/#[async_trait] Action exists mainly so that blocking filesystem syscalls and interactive dialoguer prompts can run inside tokio::task::spawn_blocking without stalling the runtime — not because those operations benefit from concurrency.

src/exec/plan.rs remains an empty, unused file: scaffolding for a real execution plan that was never built.

Consequences

Ordering-sensitive behavior (conflict prompts, uses applying fully before its dependents write on top) stays simple to follow. The cost: applying an overlay with many files or many uses entries is not parallelized beyond the one git-clone hot path — large trees still apply file-by-file, sequentially. The dead plan.rs file is also a standing source of confusion about where execution-planning logic is meant to live; it should either be implemented or removed.