Proxy::Run に値比較を実装しコンフリクト検出を正確化

basecamp/kamal

Proxy::Run が持つ設定ハッシュの等価性を比較できるようにし、複数ロールが同一ホストを共有する際の誤検出を解消しました。

背景

同一ホスト上に複数ロールが存在すると、グローバル proxy.run 設定が各ロールに deep_merge され、run_config が同一の Run オブジェクトが複数生成されます。この状態で ensure_no_conflicting_proxy_runsArray#uniq に依存して重複を除去しようとしますが、Run クラスは ==/eql?/hash を実装していないため、オブジェクトの同一性(ID)で比較され、実際には同一設定でも衝突と判定されていました。

この問題は、設定例のように webworker が同じ IP を使用しつつ、グローバル proxy.run がマージされるシナリオで顕在化し、Kamal::ConfigurationError が発生してデプロイが失敗していました。

技術的な変更

Proxy::Run クラスに等価比較メソッドを追加し、run_config の内容に基づいてオブジェクトを比較できるようにしました。具体的には == がクラスと run_config の同値性をチェックし、eql?== のエイリアスとして定義、hashrun_config.hash を返す実装です。

@@
   def app_container_directory
     File.join apps_container_directory, config.service_and_destination
   end

+  def ==(other)
+    other.is_a?(self.class) && run_config == other.run_config
+  end
+  alias_method :eql?, :==
+
+  def hash
+    run_config.hash
+  end
+
   private
     def format_bind_ip(ip)
       # Ensure IPv6 address inside square brackets - e.g. [::1]

併せて、同様の振る舞いを検証するテストが追加されました。テストは同一設定の Run オブジェクトが assert_equal になること、ハッシュ値が一致し uniq 後の要素数が 1 になること、そして設定が異なる場合は別オブジェクトとして扱われることを確認します。また、グローバル設定がロールに継承されたケースでも衝突エラーが出ないことをシナリオとして検証しています。

require "test_helper"

class ConfigurationProxyRunTest < ActiveSupport::TestCase
  setup do
    ENV["RAILS_MASTER_KEY"] = "456"
    ENV["VERSION"] = "missing"
  end

  test "run objects with identical config are equal" do
    deploy = base_deploy.deep_merge(proxy: { "run" => { "log_max_size" => "50m" } })
    config = Kamal::Configuration.new(deploy)

    run_a = Kamal::Configuration::Proxy::Run.new(config, run_config: { "log_max_size" => "50m" })
    run_b = Kamal::Configuration::Proxy::Run.new(config, run_config: { "log_max_size" => "50m" })

    assert_equal run_a, run_b
    assert_equal run_a.hash, run_b.hash
    assert_equal 1, [ run_a, run_b ].uniq.size
  end

  test "run objects with different config are not equal" do
    deploy = base_deploy.deep_merge(proxy: { "run" => { "log_max_size" => "50m" } })
    config = Kamal::Configuration.new(deploy)

    run_a = Kamal::Configuration::Proxy::Run.new(config, run_config: { "log_max_size" => "50m" })
    run_b = Kamal::Configuration::Proxy::Run.new(config, run_config: { "log_max_size" => "100m" })

    assert_not_equal run_a, run_b
    assert_equal 2, [ run_a, run_b ].uniq.size
  end

  test "no conflict when global proxy run config is inherited by role proxy" do
    deploy = base_deploy.deep_merge(
      servers: {
        "web" => { "hosts" => [ "1.1.1.1" ] },
        "worker" => {
          "hosts" => [ "1.1.1.1" ],
          "cmd" => "bin/jobs",
          "proxy" => {
            "hosts" => [ "worker.example.com" ],
            "app_port" => 8080
          }
        }
      },
      proxy: {
        "hosts" => [ "example.com" ],
        "run" => {
          "log_max_size" => "",
          "options" => { "log-driver" => "journald" }
        }
      }
    )
    # Configuration generation succeeds without raising a conflict error.
    assert_nothing_raised { Kamal::Configuration.new(deploy) }
  end
end

設計判断

値ベースの等価性を実装する という選択は、Proxy::Run が保持する唯一の状態は run_config であるという前提に基づきます。代替案としては別キー(例: proxy.run_options)を導入して比較ロジックを外部化することが考えられましたが、既存コードへの侵入度を最小化し、後方互換性を保つために 同クラス内で run_config の比較に委譲 する実装が採用されました。hash の実装は Array#uniq がハッシュテーブルを利用する前提に合わせ、run_config.hash を直接返すことで一貫した振る舞いを保証します。

この設計は、オブジェクトの同一性に依存しない 正確な重複判定を提供し、設定マージロジックを変更せずに既存の衝突検出機構を活かすことができます。結果として、ユーザーは同一ホスト上でロールを組み合わせても設定エラーに直面しなくなります。

まとめ

Proxy::Run==eql?hash を実装し、run_config の内容でオブジェクトを比較可能にしたことで、同一ホスト上の複数ロールで発生していた偽陽性のコンフリクト検出が解消されました。この変更は既存コードへの侵入を最小限に抑えつつ、設定マージと衝突チェックの信頼性を向上させます。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
395771bb

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的変更、設計判断(任意)、まとめの5部構成が揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックのファイル名付きシンタックスハイライトは正しい形式です。ただし PR リンクが `[PR #1818](URL)` となっており、要求された `[#1818](URL)` 形式と異なります。

対象読者への適合性 ✓ PASS

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

内容はエンジニア向けで、専門用語の説明が中心。初心者向けの余計な説明はありません。

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

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

各セクションは総論・各論・結論の段落構成が取られ、段落はトピックセンテンスで始まり 1 段落 1 トピック、適切な長さで空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは Diff に提示された追加内容と完全に一致しており、抜けや改変はありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`deep_merge`, `run_config`, `hash` などの用語は PR 内容と合致し、誤用は見られません。

説明の技術的正確性 ✓ PASS

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

技術的な説明は Diff と PR の記述に基づき、因果関係も妥当です。

事実の突合 ✓ PASS

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

すべての主張は PR のタイトル・説明・Diff に裏付けられており、根拠のない推測はありません。

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

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

PR 番号やファイルパスなどの固有名詞は正しく記載されています。

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

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

記事タイトルは PR の「Add value equality to Proxy::Run」を日本語で適切に要約しています。

外部知識の正確性 ✓ PASS

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

バージョンサポートやリリース日程等の外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現に関する記述はなく、矛盾はありません。