`prune_bundler` 再実行時にユーザー設定の `BUNDLE_*` 環境変数が消失する問題を修正

puma/puma

prune_bundler が有効な環境でPumaを再実行する際、BUNDLE_WITHOUTBUNDLE_DEPLOYMENT といったユーザー設定の BUNDLE_* 環境変数が失われ、ワーカーの起動クラッシュを引き起こしていた問題が修正されました。

背景

Puma 7以降、prune_bundlerWEB_CONCURRENCY を組み合わせた構成で、ワーカーが Bundler::GemNotFound エラーを出して起動に失敗する不具合が報告されていました(#3744)。Dockerデプロイメントなど、development / test グループのGemをインストールしないケースで特に顕在化し、BUNDLE_WITHOUT の消失によりBundlerがインストールされていないGemグループを含む全グループのアクティベートを試みるため、クラッシュが発生していました。

この問題は、#3297 による修正が引き金となって露わになりました。それ以前のPumaでは、prune_bundler と一緒に WEB_CONCURRENCY を設定すると preload_app が誤って有効化される別のバグが存在していました。アプリがmasterプロセスでプリロードされるため、pruneの実行後もワーカー側で require 'bundler/setup' が呼ばれず、結果として環境変数の欠落が問題として現れなかったのです。#3297prune_bundler 使用時に preload_app false を正しく設定するようになったことで、ワーカー起動時に rack_builder 経由で require 'bundler/setup' が呼ばれるパスが確実に実行されるようになり、環境変数の欠落が致命的なエラーとして顕在化しました。

つまり、環境変数の消失という根本的なバグは以前から存在していたが、別のバグが偶然それをマスクしていたという二重のバグが絡み合った構造です。

技術的な変更

lib/puma/launcher/bundle_pruner.rbprune メソッドで、個別の環境変数を明示的に復元していた処理を、Bundler.original_env から BUNDLE_* プレフィックスを持つ全変数を一括復元する処理に置き換えました。

変更前:

home = ENV['GEM_HOME']
bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
bundle_app_config = Bundler.original_env['BUNDLE_APP_CONFIG']

with_unbundled_env do
  ENV['GEM_HOME'] = home
  ENV['BUNDLE_GEMFILE'] = bundle_gemfile
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
  ENV["BUNDLE_APP_CONFIG"] = bundle_app_config
  args = [Gem.ruby, puma_wild_path, '-I', dirs.join(':')] + @original_argv

変更後:

home = ENV['GEM_HOME']
original_bundle_env = Bundler.original_env.select { |k, v| k.start_with?('BUNDLE_') && v }

with_unbundled_env do
  ENV['GEM_HOME'] = home
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
  original_bundle_env.each { |k, v| ENV[k] = v }
  args = [Gem.ruby, puma_wild_path, '-I', dirs.join(':')] + @original_argv

Bundler.original_envbundle exec が環境を変更する前の状態を保持しています。この変更では BUNDLE_* プレフィックスを持ち、かつ値が存在する(nil や空文字でない)変数のみを選択し、Bundler.with_unbundled_env がすべての BUNDLE_* 変数を除去した後に一括で復元します。従来は BUNDLE_GEMFILEBUNDLE_APP_CONFIG の2変数のみがハードコードで復元されていましたが、この変更によりユーザーが設定したあらゆる BUNDLE_* 変数が漏れなく引き継がれます。

テストとして test/rackup/bundle_without.ru が追加され、リクエスト時に BUNDLE_WITHOUT 環境変数の値をレスポンスとして返すRackアプリが用意されました。また test/test_integration_cluster.rbtest_prune_bundler_preserves_bundle_env_vars が追加され、BUNDLE_WITHOUT=development:test を設定した状態でclusterモードのPumaを起動し、ワーカーが正しくその値を受け取れることを検証しています。

設計判断

「既知の変数を列挙する」アプローチではなく「プレフィックスで一括選択する」アプローチが採用されました。

変更前のコードは BUNDLE_GEMFILEBUNDLE_APP_CONFIG という2つの変数名をハードコードしていました。このアプローチは、Bundlerが新たな BUNDLE_* 変数を導入するたびに対応が必要になるという脆弱性を持ちます。一方、select { |k, v| k.start_with?('BUNDLE_') && v } による一括選択は、変数名を列挙する必要がなく、ユーザーが独自に設定したカスタム変数も含めて網羅的に復元できます。

&& v の条件で値がfalsyな変数を除外しているのも注目すべき点です。Bundler.with_unbundled_env が変数を削除する際、環境変数をnilに設定する実装になっているため、「もともと存在しない変数」と「意図的にunsetされた変数」を区別し、後者を誤って復元しないための配慮です。

まとめ

2変数のハードコードを BUNDLE_* プレフィックスの一括復元に置き換えるという最小限の変更で、prune_bundler の再実行環境における環境変数の完全性が保証されるようになりました。列挙方式から宣言的なパターンマッチ方式への移行により、今後Bundlerが新たな設定変数を追加した場合でも、Puma側の修正なしに正しく動作する堅牢な実装となっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
16e42bb4

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

総論(リード文)→各論(背景、技術的な変更、設計判断)→結論(まとめ)の3部構成が明確に適用されています。特に、任意項目である「設計判断」セクションが設けられており、変更の意図を深く理解できる構成になっています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:ファイルパス)や、GitHubのPR・Issue番号のリンク記法([#123](URL))がガイドラインに沿って正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「prune_bundler」「Bundler.original_env」などの専門用語が前提知識として扱われており、専門のエンジニアという対象読者に合わせた適切な技術レベルで記述されています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフが「総論→各論」の構造で書かれており、トピックセンテンスが段落の冒頭に配置されています。1段落1トピックの原則も守られており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事に引用されている変更前後のコードブロックは、提供されたDiff情報と完全に一致しています。ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「prune_bundler」「Bundler.with_unbundled_env」「Bundler::GemNotFound」など、PumaとBundlerに関連する技術用語が正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

環境変数が失われるメカニズムや、過去の別のバグ(#3297)がこの問題をマスクしていたという複雑な経緯が、PR情報を基に正確かつ論理的に説明されています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのタイトル、Description、Diffの内容によって裏付けられており、ハルシネーション(創作)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#3929)、関連Issue番号(#3744)、言及されている過去のPR番号(#3297)など、すべての数値・固有名詞が正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトル「Fix `prune_bundler` stripping user-configured `BUNDLE_*` env vars on re-exec」の内容を的確に要約しており、主題に相違はありません。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容は提供されたPR情報に完全に準拠しており、サポート期間やリリース予定といったPR外の知識を持ち込んでいる箇所はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「Puma 7以降」「#3297以前」など、問題の経緯に関する時間表現がPR Descriptionの記述と一致しており、正確です。