`--lock-wait` オプションでデプロイロック取得をリトライ可能に
--lock-wait を導入することで、CI ワークフローが同一のデプロイロックを争う際に即座に失敗せず、指定した時間までポーリングでロック取得を待機できるようになりました。これにより共有 CI 環境でのデプロイ安定性が向上します。
背景
CI 環境で複数のジョブが同時にデプロイを実行しようとすると、デプロイロック が競合し、最初に取得したジョブ以外は LockError で即座に中止されます。(総論)
現在の実装はロック取得の mkdir を単一の原子操作として行い、ロックが既に存在すれば例外をスローするだけです。この挙動は単体のデプロイでは問題ありませんが、同時走行が前提のパイプラインではジョブ全体が失敗する原因となります。(各論)
そのため、ロックが解放されるまで待機し、タイムアウトまでに取得できれば処理を続行できるメカニズムが求められました。(結論)
技術的な変更
Kamal::Cli::Base に 3 つの新しい class_option が追加され、--lock-wait 系のフラグとタイムアウト・ポーリング間隔を設定できるようになりました。(総論)
+ class_option :lock_wait, type: :boolean, default: false, desc: "Wait for the deploy lock if it's already held instead of failing immediately"
+ class_option :lock_wait_timeout, type: :numeric, default: 900, desc: "Maximum seconds to wait for the deploy lock when --lock-wait is set"
+ class_option :lock_wait_interval, type: :numeric, default: 15, desc: "Seconds between deploy lock polls when --lock-wait is set"
これにより CLI の呼び出し側は kamal deploy --lock-wait のようにシンプルに指定でき、デフォルトで 15 分間、15 秒ごとに再試行します。(各論)
Kamal::Commander でも同様の属性が追加され、reset メソッドでデフォルト値が初期化されます。(各論)
- attr_accessor :verbosity, :holding_lock, :connected, :logging
+ attr_accessor :verbosity, :holding_lock, :connected, :logging, :lock_wait, :lock_wait_timeout, :lock_wait_interval
@@
- self.logging = false
+ self.logging = false
+ self.lock_wait = false
+ self.lock_wait_timeout = 900
+ self.lock_wait_interval = 15
Kamal::Cli::Base#acquire_lock では、KAMAL.lock_wait が真の場合に新設した acquire_lock_with_wait を呼び出す分岐が追加されました。(総論)
@@
- raise_if_locked do
- say "Acquiring the deploy lock...", :magenta
- on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
- end
+ if KAMAL.lock_wait
+ acquire_lock_with_wait
+ else
+ raise_if_locked do
+ say "Acquiring the deploy lock...", :magenta
+ execute_lock_acquire(AUTOMATIC_DEPLOY_LOCK_MESSAGE)
+ end
+ end
acquire_lock_with_wait はロック取得が失敗した際に capture_lock_status でロック情報を出力し、設定された間隔で再度 mkdir を試行します。タイムアウトか手動でロック保持されている場合は Kamal::Cli::LockError が送出されます。(各論)
ロック取得ロジック自体は既存のコードパスを変更せず、--lock-wait が無効な場合は従来通り即座に例外をスローします。これにより既存ユーザーへの後方互換性が保たれます。(結論)
設計判断
ロック待機機構は新規コマンドではなく、既存のロック取得フローに組み込む方針が採用されました。(総論)
同じ mkdir の原子操作を再利用することで、待機中に別プロセスがロックを取得するレースコンディションを回避し、追加のチェック段階を不要にしています。(各論)
CLI オプションは Kamal::Cli::Base に直接追加し、Kamal::Commander に属性を持たせることで、全コマンドで一貫したフラグ扱いが可能になりました。デフォルトは false のままで、従来の動作を変えることはありません。(各論)
新たに LockHeldError と LockMissingError が定義され、lib/kamal/cli/lock.rb 内でロックが存在しないケースを明示的に捕捉しています。これによりエラーメッセージが統一され、テストでも容易にシミュレートできます。(各論)
以上の設計は、CI 環境でのデプロイ競合を緩和しつつ、既存フローへの侵入を最小限に抑えるトレードオフを選択した結果と言えます。(結論)
まとめ
--lock-wait オプションは、デプロイロック取得失敗時に待機リトライを行うことで、共有 CI パイプラインにおけるデプロイ失敗を減少させます。オプションはデフォルトで無効なため既存利用者への影響はなく、必要に応じてタイムアウトとポーリング間隔を調整可能です。