Oder: Wie wir durch parallele Tests mehr über das Laden von Gems lernten.
Es war einmal, vor nicht allzu langer Zeit, dass wir in einem Projekt in einem Docker-Container mit ParallelTests RSpec-Tests parallelisiert laufen lassen wollten und in unerwartete Probleme beim Setup in Docker liefen.
➜ docker-compose run --rm app rails parallel:spec 8 processes for 156 specs, ~ 19 specs per process /usr/local/lib/ruby/gems/2.3.0/gems/bundler-1.12.5/lib/bundler/rubygems_integration.rb:373:in `block in replace_bin_path': can't find executable bundle (Gem::Exception) from /usr/local/lib/ruby/site_ruby/2.3.0/rubygems.rb:298:in `activate_bin_path' from /usr/local/bin/bundle:22:in `<main>' ...
Spoiler: Die Ursache war die Kombination aus Bundler-Version und RubyGems-Version.
RubyGems ist der offizielle Paket-Manager für Ruby und ermöglicht es, Bibliotheken in Form von Gems nach Bedarf zu verwalten. Bundler dient der Verwaltung der Gem-Abhängigkeiten und erlaubt es, in einem Gemfile
Gem-Versionen für ein Projekt zu spezifizieren, so dass man über verschiedene Systme und Benutzer hinweg eine konsistente Ruby-Umgebung hat.
Um externen Code in Ruby einzubinden, wird typischerweise Kernel.require
oder Kernel.load
verwendet. Beide Methoden durchsuchen die Verzeichnisse in $LOAD_PATH
nach einer passenden Datei. RubyGems ersetzt die gem
- und die require
-Methode im Kernel, um rechtzeitig den passenden Pfad für das gewünschte gem
an den Anfang von $LOAD_PATH
zu setzen, so dass es gefunden werden kann. Bundler stellt die korrekte Version der gem
s im Gemfile
sicher, indem es $LOAD_PATH
komplett leert und dann – mit Hilfe von RubyGems Gem-Pfad-Methoden – mit den passenden Pfaden befüllt. Dadurch wird außerdem gewährleistet, dass keine weiteren Gems als die in Gemfile
genannten Gems (und ihre Abhängigkeiten) geladen sind.
Das bedeutet, dass Bundler und RubyGems eng miteinander verwoben sind, und dass Bundler auf Änderungen in neuen RubyGems-Versionen reagieren muss. Was uns zu unserem ursprünglichen Problem, den parallelen Tests in Docker, zurück führt.
Unser Docker-Image baut auf dem offiziellen Ruby 2.3 Docker-Image auf. Erstellt wurde es erstmals Ende August, so dass die damals aktuelle Version von Bundler und Rubygems installiert wurde, d.h. bundler
Version 1.12.5 und rubygems
Version 2.6.6. Mit RubyGems Version 2.6.2 kam eine neue Pfad-Methode, activate_bin_path
, dazu, die sich auf den Bundler.setup
-Prozess ausgewirkt hat. Anpassungen in Bundler führten zunächst dazu, dass die Variable GEM_PATH
bei verschachtelten bundle exec
Aufrufen nicht korrekt gesetzt war, wenn Bundler systemweit installierte Gems ignorieren sollte. Das ist sinnvoll, wenn man das Gem-Set komplett lokal gebündelt haben möchte – und wir hatten die Option disable_shared_gems
im Docker-Container aktiviert.
Ein Aufruf mit bundle exec
stellt sicher, dass der Code tatsächlich im Kontext der gebündelten Gems ausgeführt wird. In unserem Docker-Container wird daher bundle exec
verwendet, um rails parallel:spec
auszuführen, und das wiederum ruft intern erneut bundle exec
auf, um RSpec in den einzelnen Prozessen auszuführen. Also haben wir die Auswirkungen der GEM_PATH
-Regression zu spüren bekommen. Für mehr Informationen zu diesem Problem: siehe Bundler Pull Request #4992.
Die GEM_PATH
-Regression wurde in Bundler Version 1.13.2 gelöst. Mit Bundler Version 1.13 traten aber neue Probleme auf: Bei Bundler.require
wird der $LOAD_PATH
jetzt so zurückgesetzt, dass nur noch die Gems der gegebenen Gemfile-Gruppe geladen werden – und die default
-Gruppe wird offensichtlich bei verschachtelten bundle exec
-Aufrufen nicht weitergereicht. Dieses Problem ist derzeit ein offenes Bundler Issue (für mehr Informationen: siehe Bundler Issue #5005), es gibt aber einen Workaround. Dazu deaktiviert man eine Optimierung in bundle exec
, die statt Kernel.require
das etwas schlankere Kernel.load
verwendet – das einige RubyGems-Mechanismen umgeht.
Folgende Anpassungen haben geholfen:
Dockerfile
:
FROM ruby:2.3.1-slim # We need bundler version >= 1.13.2 and a matching rubygems version. ENV RUBYGEMS_VERSION 2.6.7 RUN gem update --system "$RUBYGEMS_VERSION" ENV BUNDLER_VERSION 1.13.7 RUN gem install bundler --version "$BUNDLER_VERSION"
docker-compose.yml
:
services: app: environment: - BUNDLE_DISABLE_EXEC_LOAD=true