Skip to content

Avoid churning unrelated dependency edges#17148

Open
konstin wants to merge 6 commits into
rust-lang:masterfrom
konstin:konsti/fix-unrelated-multi-version-updates
Open

Avoid churning unrelated dependency edges#17148
konstin wants to merge 6 commits into
rust-lang:masterfrom
konstin:konsti/fix-unrelated-multi-version-updates

Conversation

@konstin

@konstin konstin commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

View all comments

When updating a dependency, its dependencies get recursively unlocked, while preferring lockfile versions. The edges however were not preserved, causing unrelated dependency edges to toggle between multiple compatible versions available in the lockfile (#5529).

foo
├── shared =0.1.0
└── c 1.0.0
    └── b 1.0.0
        └── shared >=0.1,<0.3 # Locked to 0.2.0

In the example above, cargo update -p c --precise 1.0.1 would change the version of shared that b uses to 0.1.0. This causes churn when updating dependency as packages with multiple versions in the lockfile and wide ranges, such as the windows-* crates, toggle back and forth through unrelated updates.

This PR adds tracking of preferences for each package id, so b 1.0.0 tracks a preference for shared 0.2.0 specifically, which is set as version to try first. We introduce a third mode between locked and skipped dependencies in register_previous_locks: Packages such as b 1.0.0 remain unlocked, but aren't skipped so that their dependency edges remain tracked.

Fixes #5529

@rustbot rustbot added A-crate-dependencies Area: [dependencies] of any kind A-dependency-resolution Area: dependency resolution and the resolver A-registries Area: registries S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 29, 2026
@rustbot

rustbot commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

r? @weihanglo

rustbot has assigned @weihanglo.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: @ehuss, @epage, @weihanglo
  • @ehuss, @epage, @weihanglo expanded to ehuss, epage, weihanglo
  • Random selection from ehuss, epage, weihanglo

Comment thread tests/testsuite/update.rs
@konstin konstin force-pushed the konsti/fix-unrelated-multi-version-updates branch from e81e44d to 136fae1 Compare June 29, 2026 20:39
Comment thread tests/testsuite/update.rs
@konstin konstin force-pushed the konsti/fix-unrelated-multi-version-updates branch 2 times, most recently from 7a9732e to a6e096a Compare June 30, 2026 10:14

@epage epage left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've generally found that isolating commits makes reviewing easier and faster. In a PR touching tricky areas, like the resolver, it becomes even more important to make the actual change easier to see, now and in the future.

As for the change itself, I would definitely want @Eh2406 to look over it. I called out some vague thoughts but I'm not familiar enough to be more concrete and worry I'm missing some subtle interaction.

View changes since this review

Comment thread src/cargo/core/registry.rs
Comment thread src/cargo/core/registry.rs
Comment thread src/cargo/core/resolver/dep_cache.rs Outdated
Comment thread src/cargo/ops/resolve.rs Outdated
Comment thread src/cargo/ops/resolve.rs Outdated
/// dependency resolution. See [`LockedMap`] for more.
pub fn register_lock(&mut self, id: PackageId, deps: Vec<PackageId>) {
trace!("register_lock: {}", id);
pub fn register_lock(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This signature feels hard to understand and get right but unsure if there is a better way.

@konstin konstin Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could rename deps: Vec<PackageId> into locked_deps: Vec<PackageId> here and in LockedPackage, would that help?

Comment thread src/cargo/core/resolver/dep_cache.rs Outdated
@konstin konstin force-pushed the konsti/fix-unrelated-multi-version-updates branch from a6e096a to 0a636a9 Compare June 30, 2026 12:39
@konstin konstin force-pushed the konsti/fix-unrelated-multi-version-updates branch from 0a636a9 to 1b38c7f Compare June 30, 2026 12:49
@Eh2406

Eh2406 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

As for the change itself, I would definitely want @Eh2406 to look over it. I called out some vague thoughts but I'm not familiar enough to be more concrete and worry I'm missing some subtle interaction.

If it works I'm not going to argue with it too much. But this definitely makes me nervous. I worry that there is some scenario in which this ends up picking the existing version before it evaluates the update, and then the update cannot succeed because it's incompatible with that selected version, and instead of backtracking on our lock/preferred we end up backtracking on update. But having articulated that, the order of Package selection is not dependent on this change and the existing packages are already preferred. So I don't see how this problematic case could happen. So 🤷

@konstin konstin force-pushed the konsti/fix-unrelated-multi-version-updates branch from 1b38c7f to 2d1d49f Compare July 2, 2026 10:07
konstin added 2 commits July 2, 2026 12:38
When updating a dependency, its dependencies get recursively unlocked, while preferring lockfile versions. The edges however were not preserved, causing unrelated dependency edges to toggle between multiple compatible versions available in the lockfile (rust-lang#5529).

```
foo
├── shared =0.1.0
└── c 1.0.0
    └── b 1.0.0
        └── shared >=0.1,<0.3 # Locked to 0.2.0
```

In the example above, `cargo update -p c --precise 1.0.1` would change the version of `shared` that `b` uses to 0.1.0. This causes churn when updating dependency as packages with multiple versions in the lockfile and wide ranges, such as the `windows-*` crates, toggle back and forth through unrelated updates.

This PR adds tracking of preferences for each package id, so `b 1.0.0` tracks a preference for `shared 0.2.0` specifically, which is set as version to try first. We introduce a third mode between locked and skipped dependencies in `register_previous_locks`: Packages such as `b 1.0.0` remain unlocked, but aren't skipped so that their dependency edges remain tracked.

Fixes rust-lang#5529

Note: The preference can also misdirect backtracking: when the new version of the package being updated conflicts with a preferred dependency edge, `cargo update <spec>` keeps the old version instead of backtracking on the preference, see `update_with_conflicting_preferred_dependency`.
@konstin konstin force-pushed the konsti/fix-unrelated-multi-version-updates branch from 235b8c8 to de84a36 Compare July 2, 2026 10:40
@konstin konstin force-pushed the konsti/fix-unrelated-multi-version-updates branch from de84a36 to 0f27591 Compare July 2, 2026 10:43
@konstin

konstin commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

I found a case this PR regresses based on @Eh2406's comment: If we have root -> A, root -> B, and an A -> S 1.0 edge that's resolved before the dependency update, an update of B may do nothing if it requires S 1.1 as S was then already locked to 1.1. This is a tradeoff in the way update are currently done (packages are either locked or unlocked, with this PR adding preferences, but there's no backtracking if we discover that something blocks an update, and we want that update), we trade a more stable lockfile for imprecise updates doing nothing. This doesn't block precise updates however, as the preference edges don't block backtracking. A hacky solution would be restarting the resolution with less preferences if detect this scenario, but that sounds way worse for general resolver understandability.

The current state the update resolver "over-changes", it causes churn in precise updates, while this version slightly "under-changes": The resolver sometimes doesn't do updates when they'd be possible, unless you force them with --precise.

@Eh2406

Eh2406 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

If we can build a regression test for that case, I would love to have it added to our code base.

I wonder if there's some way to change https://github.com/rust-lang/cargo/blob/master/src/cargo/core/resolver/types.rs#L299 so that the package we are trying to update is picked earlier in the decision tree. That might help mitigate this problem.

@konstin

konstin commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

I added a test in 824662c. In uv, changing the order of packages could change the whole resolution due to forks and such, but if cargo is resilient to order change with a preexisting lockfile, we could try that.

@Eh2406

Eh2406 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

I mean, it could theoretically have cascading failures that significantly change resolution. But between locking and preferences and this new kind of preferences, I don't think it's very likely in practice. And unexpected unrelated changes is the status quo. So it seems worth a try.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-crate-dependencies Area: [dependencies] of any kind A-dependency-resolution Area: dependency resolution and the resolver A-registries Area: registries S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cargo update -p SPEC can silently downgrade dependencies of other packages when multi-major ranges are used

5 participants