Proxy::Run に値比較を実装しコンフリクト検出を正確化
Proxy::Run が持つ設定ハッシュの等価性を比較できるようにし、複数ロールが同一ホストを共有する際の誤検出を解消しました。
背景
同一ホスト上に複数ロールが存在すると、グローバル proxy.run 設定が各ロールに deep_merge され、run_config が同一の Run オブジェクトが複数生成されます。この状態で ensure_no_conflicting_proxy_runs が Array#uniq に依存して重複を除去しようとしますが、Run クラスは ==/eql?/hash を実装していないため、オブジェクトの同一性(ID)で比較され、実際には同一設定でも衝突と判定されていました。
この問題は、設定例のように web と worker が同じ IP を使用しつつ、グローバル proxy.run がマージされるシナリオで顕在化し、Kamal::ConfigurationError が発生してデプロイが失敗していました。
技術的な変更
Proxy::Run クラスに等価比較メソッドを追加し、run_config の内容に基づいてオブジェクトを比較できるようにしました。具体的には == がクラスと run_config の同値性をチェックし、eql? を == のエイリアスとして定義、hash が run_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 の内容でオブジェクトを比較可能にしたことで、同一ホスト上の複数ロールで発生していた偽陽性のコンフリクト検出が解消されました。この変更は既存コードへの侵入を最小限に抑えつつ、設定マージと衝突チェックの信頼性を向上させます。