Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid crashing when installing from a corrupted lockfile #6355

Merged

Conversation

composerinteralia
Copy link
Contributor

@composerinteralia composerinteralia commented Feb 7, 2023

What was the end-user or developer problem that led to this PR?

I did a bad thing (script that edits the Gemfile.lock directly) and ended up with a Gemfile.lock that was completely missing some indirect dependencies. While this is my fault and an error is reasonable, I noticed that the error got progressively less friendly in recent versions of bundler.

Something similar came up in #6210, and this commit would have helped with that case as well (although we've already handled that a different way with #6219).

Previous behavior

Back on Bundler 2.2.23, a corrupt lockfile like this would cause a helpful error:

Unable to find a spec satisfying minitest (>= 5.1) in the set. Perhaps the lockfile is corrupted?

Bundler 2.2.24 to 2.3.26 gave a helpful (if inaccurate, since an old Bundler was not necessarily at fault) warning:

Warning:
Your lockfile was created by an old Bundler that left some things out.
Because of the missing DEPENDENCIES, we can only install gems one at a time,
instead of installing 16 at a time.
You can fix this by adding the missing gems to your Gemfile, running bundle
install, and then removing the gems from your Gemfile.
The missing gems are:
* minitest depended upon by activesupport

But then continued on and crashed while trying to report the unmet dependency:

--- ERROR REPORT TEMPLATE -------------------------------------------------------

NoMethodError: undefined method `full_name' for nil:NilClass
lib/bundler/installer/parallel_installer.rb:127:in `block (2 levels) in check_for_unmet_dependencies'

...

Bundler 2.4.0 and up crash as above when jobs=1, but crash even harder when run in parallel:

--- ERROR REPORT TEMPLATE -------------------------------------------------------

fatal: No live threads left. Deadlock?
3 threads, 3 sleeps current:0x00007fa6b6704660 main thread:0x00007fa6b6704660
* #<Thread:0x000000010833b130 sleep_forever>
   rb_thread_t:0x00007fa6b6704660 native:0x0000000108985600 int:0

* #<Thread:0x0000000108dea630@Parallel Installer Worker #0 tmp/1/gems/system/gems/bundler-2.5.0.dev/lib/bundler/worker.rb:90 sleep_forever>
   rb_thread_t:0x00007fa6b67f67c0 native:0x0000700009a62000 int:0

* #<Thread:0x0000000108dea4a0@Parallel Installer Worker #1 tmp/1/gems/system/gems/bundler-2.5.0.dev/lib/bundler/worker.rb:90 sleep_forever>
   rb_thread_t:0x00007fa6b67f63c0 native:0x0000700009c65000 int:0

<internal:thread_sync>:18:in `pop'
tmp/1/gems/system/gems/bundler-2.5.0.dev/lib/bundler/worker.rb:42:in `deq'

...

Note that these crashes only happen when installing local. When installing from a remote we end up getting an APIResponseMismatchError via

SharedHelpers.ensure_same_dependencies(self, dependencies, spec.dependencies)

What is your fix for the problem, implemented in this PR?

This commit fixes the confusing thread deadlock crash by detecting if dependencies are missing such that we'll never be able to enqueue. When that happens we treat it as a failure so the install can finish.

That gets us back to the NoMethodError, which this commit fixes by using a different warning in the case where no spec is found.

Make sure the following tasks are checked

@composerinteralia composerinteralia changed the title Avoid crashing with a corrupted lockfile Avoid crashing when installing from a corrupted lockfile Feb 7, 2023
@composerinteralia composerinteralia force-pushed the corrupt-lockfile-crash branch 2 times, most recently from f8e5007 to 5dd28a9 Compare February 7, 2023 21:44
@composerinteralia composerinteralia force-pushed the corrupt-lockfile-crash branch 2 times, most recently from b40b853 to 6de2b8a Compare February 8, 2023 14:43
@deivid-rodriguez
Copy link
Member

Thank you, looking good. Normally I try to automatically re-resolve and fix the lockfile if we detect it's corrupted. But it's tricky sometimes, so changing a bad error message with a good error message is great!

I did a bad thing (script that edits the Gemfile.lock directly) and
ended up with a Gemfile.lock that was completely missing some indirect
dependencies. While this is my fault and an error is reasonable, I
noticed that the error got progressively less friendly in recent
versions of bundler.

Something similar came up in rubygems#6210,
and this commit would have helped with that case as well
(although we've already handled this a different way with rubygems#6219).

Details:
---

Back on Bundler 2.2.23, a corrupt lockfile like this would cause a helpful error:

```
Unable to find a spec satisfying minitest (>= 5.1) in the set. Perhaps the lockfile is corrupted?
```

Bundler 2.3.26 gave a helpful warning:

```
Warning:
Your lockfile was created by an old Bundler that left some things out.
Because of the missing DEPENDENCIES, we can only install gems one at a time,
instead of installing 16 at a time.
You can fix this by adding the missing gems to your Gemfile, running bundle
install, and then removing the gems from your Gemfile.
The missing gems are:
* minitest depended upon by activesupport
```

But then continued on and crashed while trying to report the unmet
dependency:

```
--- ERROR REPORT TEMPLATE -------------------------------------------------------

NoMethodError: undefined method `full_name' for nil:NilClass
lib/bundler/installer/parallel_installer.rb:127:in `block (2 levels) in check_for_unmet_dependencies'

...
```

Bundler 2.4.0 and up crash as above when jobs=1, but crash
even harder when run in parallel:

```
--- ERROR REPORT TEMPLATE -------------------------------------------------------

fatal: No live threads left. Deadlock?
3 threads, 3 sleeps current:0x00007fa6b6704660 main thread:0x00007fa6b6704660
* #<Thread:0x000000010833b130 sleep_forever>
   rb_thread_t:0x00007fa6b6704660 native:0x0000000108985600 int:0

* #<Thread:0x0000000108dea630@Parallel Installer Worker #0 tmp/1/gems/system/gems/bundler-2.5.0.dev/lib/bundler/worker.rb:90 sleep_forever>
   rb_thread_t:0x00007fa6b67f67c0 native:0x0000700009a62000 int:0

* #<Thread:0x0000000108dea4a0@Parallel Installer Worker rubygems#1 tmp/1/gems/system/gems/bundler-2.5.0.dev/lib/bundler/worker.rb:90 sleep_forever>
   rb_thread_t:0x00007fa6b67f63c0 native:0x0000700009c65000 int:0

<internal:thread_sync>:18:in `pop'
tmp/1/gems/system/gems/bundler-2.5.0.dev/lib/bundler/worker.rb:42:in `deq'

...
```

Changes
---

This commit fixes the confusing thread deadlock crash by detecting if
dependencies are missing such that we'll never be able to enqueue. When
that happens we treat it as a failure so the install can finish.

That gets us back to the `NoMethodError`, which this commit fixes by
using a different warning in the case where no spec is found.
@composerinteralia
Copy link
Contributor Author

Thank you, looking good. Normally I try to automatically re-resolve and fix the lockfile if we detect it's corrupted. But it's tricky sometimes, so changing a bad error message with a good error message is great!

Played around with that a bit and it does seems possible. I can do that in a couple weeks when I get back from a vacation.

@deivid-rodriguez
Copy link
Member

That'd be awesome! For now enjoy your vacation ❤️

@deivid-rodriguez
Copy link
Member

I'm merging this for now regardless since this seems like a strict improvement!

@deivid-rodriguez deivid-rodriguez merged commit e8c2ed0 into rubygems:master Feb 9, 2023
deivid-rodriguez added a commit that referenced this pull request Feb 15, 2023
Avoid crashing when installing from a corrupted lockfile

(cherry picked from commit e8c2ed0)
composerinteralia added a commit to composerinteralia/rubygems that referenced this pull request Feb 21, 2023
Following up on rubygems#6355, which
turned a crash into a nicer error message, this commit auto-heals the
corrupt lockfile instead.

Since in this particular case (a corrupt Gemfile.lock with missing
dependencies) the LazySpecification will not have accurate dependency
information, we have to materialize the SpecSet to determine there are
missing dependencies. Since we've already got some
incomplete-spec-auto-healing here, I've treated this as another brand of
incomplete spec.
composerinteralia added a commit to composerinteralia/rubygems that referenced this pull request Feb 21, 2023
Following up on rubygems#6355, which
turned a crash into a nicer error message, this commit auto-heals the
corrupt lockfile instead.

Since in this particular case (a corrupt Gemfile.lock with missing
dependencies) the LazySpecification will not have accurate dependency
information, we have to materialize the SpecSet to determine there are
missing dependencies. Since we've already got some
incomplete-spec-auto-healing here, I've treated this as another brand of
incomplete spec.
composerinteralia added a commit to composerinteralia/rubygems that referenced this pull request Feb 22, 2023
Following up on rubygems#6355, which
turned a crash into a nicer error message, this commit auto-heals the
corrupt lockfile instead.

Since in this particular case (a corrupt Gemfile.lock with missing
dependencies) the LazySpecification will not have accurate dependency
information, we have to materialize the SpecSet to determine there are
missing dependencies. We've already got a way to handle this, via
`SpecSet#incomplete_specs`, but it wasn't quite working for this case
because we'd get to `@incomplete_specs += lookup[name]` and
`lookup[name]` would be empty for the dependency.

With this commit we catch it a bit earlier, marking the spec containing
the missing dependency as incomplete.
composerinteralia added a commit to composerinteralia/rubygems that referenced this pull request Feb 22, 2023
Following up on rubygems#6355, which
turned a crash into a nicer error message, this commit auto-heals the
corrupt lockfile instead.

In this particular case (a corrupt Gemfile.lock with missing
dependencies) the LazySpecification will not have accurate dependency
information, we have to materialize the SpecSet to determine there are
missing dependencies. We've already got a way to handle this, via
`SpecSet#incomplete_specs`, but it wasn't quite working for this case
because we'd get to `@incomplete_specs += lookup[name]` and
`lookup[name]` would be empty for the dependency.

With this commit we catch it a bit earlier, marking the spec containing
the missing dependency as incomplete.
composerinteralia added a commit to composerinteralia/rubygems that referenced this pull request Feb 22, 2023
Following up on rubygems#6355, which
turned a crash into a nicer error message, this commit auto-heals the
corrupt lockfile instead.

In this particular case (a corrupt Gemfile.lock with missing
dependencies) the LazySpecification will not have accurate dependency
information, we have to materialize the SpecSet to determine there are
missing dependencies. We've already got a way to handle this, via
`SpecSet#incomplete_specs`, but it wasn't quite working for this case
because we'd get to `@incomplete_specs += lookup[name]` and
`lookup[name]` would be empty for the dependency.

With this commit we catch it a bit earlier, marking the spec containing
the missing dependency as incomplete.
composerinteralia added a commit to composerinteralia/rubygems that referenced this pull request Feb 22, 2023
Following up on rubygems#6355, which
turned a crash into a nicer error message, this commit auto-heals the
corrupt lockfile instead.

In this particular case (a corrupt Gemfile.lock with missing
dependencies) the LazySpecification will not have accurate dependency
information, we have to materialize the SpecSet to determine there are
missing dependencies. We've already got a way to handle this, via
`SpecSet#incomplete_specs`, but it wasn't quite working for this case
because we'd get to `@incomplete_specs += lookup[name]` and
`lookup[name]` would be empty for the dependency.

With this commit we catch it a bit earlier, marking the spec containing
the missing dependency as incomplete.
composerinteralia added a commit to composerinteralia/rubygems that referenced this pull request Feb 22, 2023
Following up on rubygems#6355, which
turned a crash into a nicer error message, this commit auto-heals the
corrupt lockfile instead.

In this particular case (a corrupt Gemfile.lock with missing
dependencies) the LazySpecification will not have accurate dependency
information, we have to materialize the SpecSet to determine there are
missing dependencies. We've already got a way to handle this, via
`SpecSet#incomplete_specs`, but it wasn't quite working for this case
because we'd get to `@incomplete_specs += lookup[name]` and
`lookup[name]` would be empty for the dependency.

With this commit we catch it a bit earlier, marking the parent spec
containing the missing dependency as incomplete.
composerinteralia added a commit to composerinteralia/rubygems that referenced this pull request Feb 22, 2023
Following up on rubygems#6355, which
turned a crash into a nicer error message, this commit auto-heals the
corrupt lockfile instead.

In this particular case (a corrupt Gemfile.lock with missing
dependencies) the LazySpecification will not have accurate dependency
information, we have to materialize the SpecSet to determine there are
missing dependencies. We've already got a way to handle this, via
`SpecSet#incomplete_specs`, but it wasn't quite working for this case
because we'd get to `@incomplete_specs += lookup[name]` and
`lookup[name]` would be empty for the dependency.

With this commit we catch it a bit earlier, marking the parent spec
containing the missing dependency as incomplete.
matzbot pushed a commit to ruby/ruby that referenced this pull request Mar 1, 2023
Following up on rubygems/rubygems#6355, which
turned a crash into a nicer error message, this commit auto-heals the
corrupt lockfile instead.

In this particular case (a corrupt Gemfile.lock with missing
dependencies) the LazySpecification will not have accurate dependency
information, we have to materialize the SpecSet to determine there are
missing dependencies. We've already got a way to handle this, via
`SpecSet#incomplete_specs`, but it wasn't quite working for this case
because we'd get to `@incomplete_specs += lookup[name]` and
`lookup[name]` would be empty for the dependency.

With this commit we catch it a bit earlier, marking the parent spec
containing the missing dependency as incomplete.

rubygems/rubygems@486ecb8f20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants