Skip to content

Repository files navigation

bundler-resolver-logger

A local experiment for tracing Bundler 4.0's PubGrub resolver. It logs version choices, derived constraints, incompatibilities, conflicts, backtracks, and Bundler's full resolver retries as JSONL.

It patches Bundler internals and is expected to break when those internals change.

Install

bundle plugin install bundler-resolver-logger --path /path/to/bundler-logger

Then wrap any Bundler command:

bundle resolve-log --output tmp/resolve.jsonl -- lock --update

--include-versions adds every matching version at each choice point. This can make the log much larger.

The plugin command starts a child Bundler process with the tracer loaded through RUBYOPT. This leaves Bundler in charge of parsing and running lock, install, or update.

You can skip the plugin and load the patch directly:

RUBYLIB=/path/to/bundler-logger/lib \
BUNDLER_RESOLVER_LOG=tmp/resolve.jsonl \
RUBYOPT=-rbundler/resolver_logger/bootstrap \
bundle lock --update

What gets patched

The patch is in lib/bundler/resolver_logger/monkey_patches.rb. It prepends modules to:

  • Bundler::Resolver
  • Bundler::PubGrub::VersionSolver
  • Bundler::PubGrub::PartialSolution
  • Bundler::Resolver::Strategy
  • Bundler::PubGrub::Strategy

The main patched methods are:

  • resolver start and retry relaxation
  • solver initialization and completion
  • package and version selection
  • incompatibility creation and propagation
  • decisions and derivations
  • conflict resolution and backtracking

The patches publish raw Bundler and PubGrub objects through Bundler::ResolverLogger::Emitter. Recorder subscribes to those events and writes JSONL. Another subscriber can use the same events for a live visualization without going through JSON.

Bundler retries and PubGrub backtracks are separate events. A Bundler retry creates a new solver after unlocking a dependency, allowing prereleases, or loading remote specs. A backtrack rewinds the current partial solution.

Log format

Each line is one JSON object with:

  • schema_version
  • session_id
  • seq
  • event
  • timestamp
  • elapsed_ms

Solver, resolution, and incompatibility IDs are stable within a session.

Events:

  • session_start, session_end
  • resolution_start, resolution_relaxation, resolution_success, resolution_failure
  • solver_start, solver_initialized, solver_success, solver_failure
  • incompatibility
  • candidate, candidate_rejected
  • decision, derivation
  • propagation_conflict, conflict, backtrack

Terms, ranges, assignments, incompatibility causes, and removed decisions are stored as objects rather than Bundler's debug strings. Candidate events also include the strategy score for every currently unsatisfied package.

The schema is currently version 1. Bump SCHEMA_VERSION for changes that would make existing logs hard to read.

Selected fields from the backtracking example:

{"schema_version":1,"session_id":"example","seq":20,"event":"candidate","package":"database","version":{"display":"2.0.0","class":"Gem::Version","gem_version":"2.0.0"},"candidate_count":2,"decision_level":3,"solver_id":"s1"}
{"schema_version":1,"session_id":"example","seq":22,"event":"decision","package":"database","version":{"display":"2.0.0","class":"Gem::Version","gem_version":"2.0.0"},"decision_level":4,"assignment_index":8,"attempted_solutions":1,"solver_id":"s1"}
{"schema_version":1,"session_id":"example","seq":28,"event":"conflict","incompatibility":{"id":"i7","description":"shared < 3.0.0 depends on leaf = 2.0.0"},"decision_level":4,"solver_id":"s1"}
{"schema_version":1,"session_id":"example","seq":29,"event":"backtrack","from_level":4,"to_level":2,"removed_decisions":[{"package":"leaf","version":{"display":"1.0.0","class":"Gem::Version","gem_version":"1.0.0"}},{"package":"database","version":{"display":"2.0.0","class":"Gem::Version","gem_version":"2.0.0"}}],"solver_id":"s1"}
{"schema_version":1,"session_id":"example","seq":44,"event":"solver_success","attempted_solutions":2,"decisions":[{"package":"database","version":{"display":"1.0.0","class":"Gem::Version","gem_version":"1.0.0"}},{"package":"shared","version":{"display":"3.0.0","class":"Gem::Version","gem_version":"3.0.0"}},{"package":"app","version":{"display":"2.0.0","class":"Gem::Version","gem_version":"2.0.0"}}],"solver_id":"s1"}

Full rows also include timestamps, ranges, choice scores, terms, and removed assignments.

Example

ruby examples/pubgrub_backtrack.rb

This builds a small package graph whose first database choice conflicts with a guard package. PubGrub backtracks from decision level 4 to 2 and succeeds on its second attempt. The log is written to tmp/pubgrub-backtrack.jsonl.

Notes

  • Choice logging asks the source for cached version lists again so it can record the strategy score. That adds some overhead.
  • Logs are not redacted. They may contain private gem names, paths, source descriptions, requirements, and error backtraces.
  • Recorder errors are swallowed so a tracing bug does not change resolution. Set BUNDLER_RESOLVER_LOG_DEBUG=1 to print them.

Run the tests with:

bundle exec rake

About

Experimental structured tracing for Bundler's PubGrub resolver

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages