Skip to content

Document cache behavior with --only - #2

Open
solsson wants to merge 6 commits into
mainfrom
cache-behavior-with-dashdashonly
Open

solsson wants to merge 6 commits into
mainfrom
cache-behavior-with-dashdashonly

Conversation

@solsson

@solsson solsson commented May 8, 2026

Copy link
Copy Markdown
Owner

Sibling to #1, same level of surprise. The definition of "correctness" for build cache is unclear. Turbo has flexibility in config and CLI flags that's easy to use but hard to grasp the cache effects of.

Leksat and others added 6 commits March 30, 2025 17:35
Builds turbo binaries for 4 architectures (linux-amd64, linux-arm64,
darwin-amd64, darwin-arm64) and publishes them as GitHub release
assets with a sha256 checksums file.

Triggered by tag push. No npm publish.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add config-only detection that identifies tasks whose inputs include
files produced by their dependencies' outputs. This pattern causes
unstable cache hashes because turbo computes file hashes before
dependencies execute.

Detection uses exact string matching, glob matching (via wax), and
directory containment (bare "dir" input matches "dir/**" output),
including through transitive dependency chains. Only same-package
dependencies are checked since cross-package tasks write to different
directories.

Results are deduplicated by task name and shown as info messages in
the run prelude, informing the config author regardless of --filter.

15 unit tests cover pattern matching (7), engine-level graph
traversal (5), and config-level deduplication (3).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents a cache correctness issue where --only drops ^dependsOn
edges even for direct package.json dependencies. This causes stale
cache hits when upstream packages change.

The test asserts that `turbo bundle --only --filter=runtime` should
keep types#bundle in the graph (since runtime depends on types and
bundle declares ^bundle). Currently it doesn't — the edge is dropped
and changes in types are invisible to the cache key.

Marked #[should_panic] because the assertion currently fails.
When the engine is fixed to preserve these edges, remove should_panic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When --only filters out a task from the dependency graph, emit a
tracing::warn with the dropped edge, the affected task, and a
suggestion to add --filter for the missing package.

The warnings fire at the exact point where edges are dropped in
the engine builder, so they cannot drift from actual behavior.

Example output:
  --only: dropping topological dependency types#bundle from
  runtime#bundle. Changes in types won't affect the cache key
  for runtime#bundle. To include it, add --filter=types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@solsson

solsson commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Research by Opus 5:

PR #2 — Document cache behavior with --only

Recommendation: do not close — re-scope. Unlike #1, upstream has made no progress on this one, so there is no upstream feature that closes it. What upstream has shipped only supersedes the parts of this PR that overlap with #1.

Upstream status: unchanged

I diffed crates/turborepo-engine/src/builder.rs between our fork base (4b3e7cc96d, release 2.9.10) and v2.10.6. The --only edge-dropping logic is byte-for-byte identical:

if let Some(allowed_tasks) = &allowed_tasks
    && !allowed_tasks.contains(&from_task_id)
{
    return;      // topological (^) dependency edge, silently dropped
}
if let Some(allowed_tasks) = &allowed_tasks
    && !allowed_tasks.contains(&from_task_id)
{
    continue;    // direct task dependency edge, silently dropped
}

Both sites still drop the edge with no warning and no contribution to the cache key. Nothing in the v2.10.x deferred-hashing work touches this: jit and dependencyOutputs change when inputs are hashed, not which graph edges survive --only. A task whose ^bundle edge was dropped has no dependency to defer on in the first place, so the new modes cannot help.

Searching upstream commits between the fork base and v2.10.6 for --only-related changes turns up only unrelated matches (Cargo-only repos, errors-only, etc.). This remains entirely ours to carry.

What to strip from this PR

Of the 6 commits here, the ones that duplicate #1 are now obsolete and should come out:

That leaves the genuinely unaddressed material:

  1. The #[should_panic] test asserting turbo bundle --only --filter=runtime should keep types#bundle in the graph. Worth keeping as executable documentation of the divergence.
  2. The warning when --only drops dependency edges, suggesting which --filter flags to add.

A design note before we rebase

The #[should_panic] test frames edge-dropping as a bug to be fixed. I'd argue we should not try to fix it by preserving edges. Changing which nodes --only keeps in the graph is a semantic change to a documented flag, and it is the single most likely thing to conflict on every future rebase. Folding the dropped dependency's source hashes into the cache key (the approach in #3) gets us correctness without diverging on graph shape.

If we agree, this PR's test should be reworded from "this is broken" to "this documents that --only intentionally drops edges, which is why we compensate in the cache key" — and stay #[should_panic] as a tripwire that tells us if upstream ever changes its mind.

Suggested action

  1. Rebase onto v2.10.6, dropping the two superseded commits.
  2. Keep the warning and the tripwire test; reword the test's doc comment per above.
  3. Fold this into Fewer false cache positives: hash --only inputs and dependsOn cache=false tasks #3 rather than shipping it separately — the warning and the hash compensation are the same feature, and reviewing them apart is what led to the overlap in the first place.
  4. File an upstream issue. This is a real cache-correctness bug that produces silently stale artifacts, and upstream appears simply not to know about it. Their receptiveness to the Tasks should run dependsOn before hashing inputs vercel/turborepo#8051 work suggests it is worth reporting rather than carrying forever. A minimal repro from the #[should_panic] test would make the case well.

solsson added a commit that referenced this pull request Jul 27, 2026
`--only` removes dependency edges pointing outside the filter set. Those
tasks never run, never produce a task hash, and so contribute nothing to
the downstream task's cache key. A change in an excluded package then
produces a cache *hit* and turbo restores a stale artifact.

Upstream treats the edge dropping as intended behavior for `--only`, and
this change does not argue otherwise: the task graph is left exactly as
upstream builds it. Instead the engine records each dropped edge, and the
excluded package's source hashes are folded in where the missing task
hash would have gone.

Fresh start from upstream v2.10.6, squashing what remains relevant from
#3 (previously released as v2.9.10-hashdepends.2).

The dependsOn-output half of that PR is deliberately dropped: upstream
solved it in v2.9.17-v2.10.6 via structured `inputs` modes (vercel#13043,
vercel#13045, vercel#13125, vercel#13127, vercel#13129), closing vercel#8051. Express
those cases with `mode: "jit"` or `mode: "dependencyOutputs"` instead.
Notably vercel#13273 removed locking from task hash precomputation, which is
the opposite of the `RwLock<HashMap>` the old branch carried.

Changes from the previous implementation:

- Fold the stand-in hashes into the task's dependency hash list rather
  than re-hashing the finished task hash. The tracker's copy is then
  compensated too, so dependents of an affected task also invalidate.
  This also covers all three hashing paths at once, including
  `calculate_task_hash_with_deferred_inputs`, which the old post-hoc
  approach would have missed for tasks that use `jit`/`dependencyOutputs`
  and have an edge dropped -- the exact combination in our bundle task.
- Retain `dropped_dependencies` in `prune_to_reachable` so the
  compensation survives watch and subgraph runs.
- Memoize per-package hashing; several tasks commonly drop the same
  package, and hashing walks the whole package tree.
- Deduplicate dropped edges; a package can be both a topological and a
  direct dependency.
- Pass `repo_index` so hashing uses the repo index fast path (vercel#13213).
- Drop the `hash_string` helper, unnecessary under the new approach;
  turborepo-hash is now untouched by the fork.
- Add positive tests that dropped edges are recorded on both the
  topological and direct-dependency paths, plus one asserting nothing is
  recorded without `--only`. The old branch tested none of this.
- Reword the `#[should_panic]` test as a deliberate tripwire rather than
  a bug report, and pin its expected panic message.

Refs: #1, #2, #3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants