`CurrentAttributes#clear_all` でのHash反復中追加エラーを修正
ActiveSupport::CurrentAttributes.clear_all の実行中に別の CurrentAttributes サブクラスが初期化されると RuntimeError: can't add a new key into hash during iteration が発生していたバグが修正されました。修正は1行の変更で、each_value から values.each への置き換えにより、反復前にHashのスナップショットを取ることで解決しています。
背景
clear_all はテスト間のステート漏洩を防ぐため、すべての CurrentAttributes インスタンスをリセットする内部メソッドです。しかし resets コールバックの中で別の CurrentAttributes サブクラスのアトリビュートに初めてアクセスすると、その場でインスタンスが current_instances ハッシュに追加され、反復処理中のHashへの新規キー追加として例外が発生していました。
この問題は、コミット d43dc9a で導入された push / pop ロジックによる ExecutionContext の改善の一部として修正されていましたが、本PRはその修正を現行ブランチにバックポートしたものです。
技術的な変更
current_instances ハッシュを反復しながらその中身が変更されないよう、反復開始前に値のスナップショットを取る方式に変更しました。
変更前:
def reset_all # :nodoc:
current_instances.each_value(&:reset)
end
変更後:
def reset_all # :nodoc:
current_instances.values.each(&:reset)
end
each_value はHashを直接参照しながら反復するため、コールバック内で新しいサブクラスインスタンスが current_instances に追加されると即座に例外が発生します。一方 values はHash全体の値を配列としてコピーした後に each で反復するため、コールバック実行中に current_instances が変更されても安全です。
テストでは、resets ブロック内で別の CurrentAttributes サブクラスのアトリビュート(default: -> { SecureRandom.uuid } を持つ auxiliary.uuid)に初回アクセスするシナリオが追加され、assert_nothing_raised により例外が発生しないことを確認しています。
設計判断
values.each による防御的コピー という最小限のアプローチが選択されました。
コールバックが current_instances を変更しうるという前提のもと、反復対象を事前に確定させることで問題を解消しています。コールバック実行中に新たに追加されたインスタンスは今回の reset_all サイクルではリセットされませんが、次のサイクルで処理されます。これは resets コールバック内での遅延初期化という用途では許容できる挙動であり、余分なロックや複雑な制御フローを導入せずに済む判断です。
まとめ
each_value から values.each への1行の変更が、resets コールバック内での遅延初期化という実際のユースケースで発生していたクラッシュを防ぎます。Hashの反復中に要素が追加されうる場面ではスナップショットを取るという、Rubyでの定番の防御的パターンを CurrentAttributes のライフサイクル管理に適用した修正といえます。