Skip to content

fix: Use BTreeMap for deterministic lockfile dependency ordering#12254

Merged
anthonyshew merged 1 commit into
mainfrom
shew/issue-12252
Mar 12, 2026
Merged

fix: Use BTreeMap for deterministic lockfile dependency ordering#12254
anthonyshew merged 1 commit into
mainfrom
shew/issue-12252

Conversation

@anthonyshew

@anthonyshew anthonyshew commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Why

HashMap uses per-process random hash seeds (RandomState), so each turbo invocation iterates lockfile dependency entries in a different order. This propagated through the transitive closure computation and parallel DashMap caching in all_transitive_closures, producing different external dependency hashes for the same unchanged lockfile.

The PackageSnapshotV7::dependencies() method was particularly ironic: the internal data was already stored as BTreeMap (via type Map<K, V> = BTreeMap<K, V>), but the method converted it into a HashMap before returning, needlessly discarding the deterministic ordering.

Testing

The test_dependency_index_is_deterministically_ordered test parses the same lockfile 50 times and asserts that all_dependencies() always returns entries in the same order. Before this change, it found 50 distinct iteration orders from 50 parses. This tests goes out of its way to force non-determinism into these codepaths.

How did we not have coverage over this before?

We have many places where we ensure our hashes are deterministic, but this slipped through for a tricky reason. Tests are single-process, and HashMap happens to use seeds from the same thread-local RNG. The DashMaps there were introduced for performance interleave non-determinstically, but the inputs were always deterministic because of the thread scheduling.

Outside of the testing environment, there are, of course, more threads that vary across process invocations, so this bug shows up. We were testing that the inputs into hashes were deterministic, but not the ordering of those inputs.

Closes #12252

Replace HashMap with BTreeMap throughout the lockfile dependency
resolution pipeline to ensure hashOfExternalDependencies is
deterministic across turbo invocations.

HashMap uses per-process random hash seeds (RandomState), so each turbo
invocation iterates lockfile dependency entries in a different order.
This non-determinism propagated through the transitive closure
computation and parallel DashMap caching, producing different hashes
for the same unchanged lockfile.

The fix changes the Lockfile trait's all_dependencies return type from
HashMap to BTreeMap, and updates all 5 lockfile implementations (pnpm,
npm, yarn1, berry, bun) plus callers. For pnpm specifically, the
internal data was already stored as BTreeMap — the dependencies()
method was needlessly converting it to HashMap.

Closes #12252
@anthonyshew anthonyshew requested a review from a team as a code owner March 12, 2026 13:03
@anthonyshew anthonyshew requested review from tknickman and removed request for a team March 12, 2026 13:03
@vercel

vercel Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
examples-basic-web Ready Ready Preview, Comment, Open in v0 Mar 12, 2026 1:04pm
examples-designsystem-docs Ready Ready Preview, Comment, Open in v0 Mar 12, 2026 1:04pm
examples-gatsby-web Ready Ready Preview, Comment, Open in v0 Mar 12, 2026 1:04pm
examples-kitchensink-blog Ready Ready Preview, Comment, Open in v0 Mar 12, 2026 1:04pm
examples-nonmonorepo Building Building Preview, Open in v0 Mar 12, 2026 1:04pm
examples-svelte-web Ready Ready Preview, Comment, Open in v0 Mar 12, 2026 1:04pm
examples-tailwind-web Building Building Preview, Open in v0 Mar 12, 2026 1:04pm
examples-vite-web Ready Ready Preview, Comment, Open in v0 Mar 12, 2026 1:04pm
turbo-site Ready Ready Preview, Comment, Open in v0 Mar 12, 2026 1:04pm
turborepo-agents Ready Ready Preview, Comment, Open in v0 Mar 12, 2026 1:04pm

@anthonyshew anthonyshew enabled auto-merge (squash) March 12, 2026 13:12
@anthonyshew anthonyshew merged commit 3fc7d49 into main Mar 12, 2026
58 checks passed
@anthonyshew anthonyshew deleted the shew/issue-12252 branch March 12, 2026 13:14
github-actions Bot added a commit that referenced this pull request Mar 12, 2026
## Release v2.8.17-canary.5

Versioned docs: https://v2-8-17-canary-5.turborepo.dev

### Changes

- release(turborepo): 2.8.17-canary.4 (#12249) (`c0ddebe`)
- fix: Use BTreeMap for deterministic lockfile dependency ordering
(#12254) (`3fc7d49`)

Co-authored-by: Turbobot <turbobot@vercel.com>
anthonyshew added a commit that referenced this pull request Mar 12, 2026
## Summary

- Switches `bundled_deps` in `PackageIndex` from `HashMap` to `BTreeMap`
for deterministic iteration order in `find_package()`

## Context

Follow-up to #12254. `find_package()` iterates `bundled_deps` and
returns the first match when a package isn't found via workspace-scoped
or top-level lookups. With `HashMap`, the per-process random seed
produces different iteration orders across `turbo` invocations, making
the result non-deterministic when multiple parents bundle the same
dependency name.

This is unlikely to trigger in practice (bundled deps are rare, and
having the same dep bundled by multiple parents is rarer), but it closes
the last remaining `HashMap` iteration on a hash-affecting code path
across all five lockfile implementations.

No performance impact — `bundled_deps` is almost always empty.

Closes #12252
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.

Non-deterministic hashOfExternalDependencies with pnpm lockfile v9

1 participant