Capistranoでのエラー処理(ロールバック処理)

通常、Capistranoではリモートホスト上でのコマンド実行に失敗する(終了値が0以外になる)とそこでタスクを中断します。トランザクションの中でのコマンド実行であり、ロールバック処理が与えられていればそれを実行します。

ここではタスク実行の中断によりコマンド実行されないホストが生じる場合にロールバック処理がどのように行われるかを見てみたいと思います。

ロールバックの基本的な動き

まず、シンプルな例です。

role :foo, "mike", "tora"
task :foo do
  run "hostname"
end

これを実行すると次のようになります(以下、動作確認はCapistrano 2.5.19/Ruby 1.8.7で行っています)。

  * executing `foo'
  * executing "hostname"
    servers: ["mike", "tama"]
    [tamper] executing command
 ** [out :: tama] tama
    [rice] executing command
 ** [out :: mike] mike
    command finished

タスクの内容を変更して、必ずコマンド実行エラーが起きるようにしてみます。

task :foo do
  run "hostname; exit 1"
end

これを実行した結果は以下です。

  * executing `foo'
  * executing "hostname; exit 1"
    servers: ["mike", "tama"]
    [tama] executing command
 ** [out :: tama] tama
    [mike] executing command
 ** [out :: mike] mike
    command finished
failed: "sh -c 'hostname; exit 1'" on mike,tama

hostnameコマンド自体は問題なく(最初の例と変わりなく)動作しますが、その後の「exit 1」によりコマンドライン全体としては異常終了(終了値が1)となります。Capistranoはこの結果を受けてタスクの実行を中断します(この例ではタスク内に一つのrunしかありませんが、続けて別のrunがあったとしてもそれは実行されません)。

ロールバック処理があった場合にそれがどのように実行されるかを確認するために、タスクの内容をさらに変更します。

task :foo do
  transaction do
    on_rollback do
      run "hostname"
    end
    run "hostname; exit 1"
  end
end

runの中身は一つ前の例と同じで必ず失敗します。これを実行してみます。

  * executing `foo'
 ** transaction: start
  * executing "hostname; exit 1"
    servers: ["mike", "tama"]
    [mike] executing command
    [tama] executing command
 ** [out :: tama] tama
 ** [out :: mike] mike
    command finished
*** [foo] rolling back
  * executing "hostname"
    servers: ["mike", "tama"]
    [mike] executing command
 ** [out :: mike] mike
    [tama] executing command
 ** [out :: tama] tama
    command finished
failed: "sh -c 'hostname; exit 1'" on mike,tama

「rolling back」より後がロールバック処理です。

一部のホストでコマンド実行が失敗したときのロールバック

リモートホストでのコマンド実行は、すべてが成功するか、すべてが失敗するかのどちらかだというわけではありません。一部のホストで成功、他のホストで失敗ということもあります。

タスクの対象となるリモートホスト群のうちのどれか一つででもコマンド実行が失敗すれば、そのタスクはその時点で中断されます。コマンド実行に成功しているホストでも続きが行われることはありません(ただし、この動作は:on_error指定により変更できます)。

そのような場合のロールバック処理はどのように行われるのかを見てみます。

task :foo do
  run "hostname; test mike = `hostname`"
end 
ホストmike上ではhostnameが「mike」を出力します。ホストtama上では「tama」を出力します。したがって「test mike = `hostname`」はmikeにおいては成功し、tamaにおいては失敗します。これを実行すると次のようになります。
  * executing `foo'
  * executing "hostname; test mike = `hostname`"
    servers: ["mike", "tama"]
    [tama] executing command
 ** [out :: tama] tama
    [mike] executing command
 ** [out :: mike] mike
    command finished
failed: "sh -c 'hostname; test mike = `hostname`'" on tama

これにロールバック処理を加えてみます。

task :foo do
  transaction do
    on_rollback do
      run "hostname"
    end
    run "hostname; test mike = `hostname`"
  end
end

実行してみます。

  * executing `foo'
 ** transaction: start
  * executing "hostname; test mike = `hostname`"
    servers: ["mike", "tama"]
    [mike] executing command
    [tama] executing command
 ** [out :: tama] tama
 ** [out :: mike] mike
    command finished
*** [foo] rolling back
  * executing "hostname"
    servers: ["mike", "tama"]
    [mike] executing command
 ** [out :: mike] mike
    [tama] executing command
 ** [out :: tama] tama
    command finished
failed: "sh -c 'hostname; test mike = `hostname`'" on tama

先の場合と同じく「rolling back」より後がロールバック処理です。これを見ると、コマンド実行に成功したmikeにおいてもロールバック処理が行われていることがわかります。

まだコマンド実行していないホストでのロールバック

最後に、まだコマンド実行されていないホストがあったとき、ロールバック処理がどのように行われるかを見てみます。

Capistranoでは特に指定しない限り対象全ホストに同時にコマンドを送信します。ただし、コマンド実行の内容や環境によっては全ホスト同時というのが難しいこともあります(たとえば特定ホストに負荷をかけるようなコマンド実行が必要な場合などです)。

このようなときには:max_hostsというオプションで同時にコマンド送信するホストの数を指定することができます。タスク定義またはrunメソッドのオプションとして与えることができ、たとえば次のようにすると同時に一ホストにだけコマンド送信します。

role :foo, "mike", "tama", "tora"
task :foo, :max_hosts => 1 do
  run "date; sleep 1"
end

実行結果は次の通りです(sleep 1によりdateコマンドの実行が約一秒おきになっています。

  * executing `foo'
  * executing "date; sleep 1"
    servers: ["mike", "tama", "tora"]
    [mike] executing command
 ** [out :: mike] 2010年 10月 21日 木曜日 20:07:07 JST
    command finished
    [tama] executing command
 ** [out :: tama] 2010年 10月 21日 木曜日 20:07:09 JST
    command finished
    [tora] executing command
 ** [out :: tora] 2010年 10月 21日 木曜日 20:07:10 JST
    command finished

これをもとに、先と同じくコマンド実行に失敗するようにしてみます。

task :foo, :max_hosts => 1 do
  run "date; sleep 1; test mike = `hostname`"
end

実行すると次のような結果となり、コマンド実行されないホストが生じることがわかります(toraではコマンド実行されていません)。

  * executing `foo'
  * executing "date; sleep 1; test mike = `hostname`"
    servers: ["mike", "tama", "tora"]
    [mike] executing command
 ** [out :: mike] 2010年 10月 21日 木曜日 20:09:22 JST
    command finished
    [tama] executing command
 ** [out :: tama] 2010年 10月 21日 木曜日 20:09:23 JST
    command finished
failed: "sh -c 'date; sleep 1; test mike = `hostname`'" on tama

ここにロールバック処理が与えられるとどうなるでしょうか。

task :foo, :max_hosts => 1 do
  transaction do
    on_rollback do
      run "hostname"
    end
    run "date; sleep 1; test mike = `hostname`"
  end
end

実行してみます。

  * executing `foo'
 ** transaction: start
  * executing "date; sleep 1; test mike = `hostname`"
    servers: ["mike", "tama", "tora"]
    [mike] executing command
 ** [out :: mike] 2010年 10月 21日 木曜日 20:13:15 JST
    command finished
    [tama] executing command
 ** [out :: tama] 2010年 10月 21日 木曜日 20:13:17 JST
    command finished
*** [foo] rolling back
  * executing "hostname"
    servers: ["mike", "tama", "tora"]
    [mike] executing command
 ** [out :: mike] mike
    command finished
    [tama] executing command
 ** [out :: tama] tama
    command finished
    [tora] executing command
 ** [out :: tora] tora
    command finished
failed: "sh -c 'date; sleep 1; test mike = `hostname`'" on tama

タスクで指定されたコマンドは(この例では)mike、tama、そしてtoraの順に実行されるはずでした(:max_hosts => 1により一ホストずつ実行されます)。ところがtamaにおいてコマンド実行に失敗してしまいます。タスクはそこで中断されてしまい、結果としてtoraではコマンドが実行されませんでした。

そしてロールバック処理が開始されます。上の実行結果からは、コマンド実行のなかったtoraを含めた全ホストにおいてロールバック処理が実行されたのを見てとれます。

まとめ

Capistranoによるリモートコマンド実行は、基本的には対象全ホストで同時に行われますが、:max_hostsの指定により同時実行ホスト数を制御することが可能です。このような指定は特定ホストに負荷かけてしまう処理の実行や、非常に多数のホストを制御しなければならないときに有用です。

ただし、これを用いた場合、本来のタスクの処理が行われていないにも関わらずロールバック処理だけが実行されるという状況が起こり得ることに注意が必要です。

おまけ

タスク定義での:max_hosts指定はロールバック処理にも有効です。

task :foo, :max_hosts => 1 do
  transaction do
    on_rollback do
      run "date; sleep 1"
    end
    run "/bin/false"
  end
end

これを実行すると次のようになります。

  * executing `foo'
 ** transaction: start
  * executing "/bin/false"
    servers: ["mike", "tama", "tora"]
    [mike] executing command
    command finished
*** [foo] rolling back
  * executing "date; sleep 1"
    servers: ["mike", "tama", "tora"]
    [mike] executing command
 ** [out :: mike] 2010年 10月 21日 木曜日 20:23:56 JST
    command finished
    [tama] executing command
 ** [out :: tama] 2010年 10月 21日 木曜日 20:23:58 JST
    command finished
    [tora] executing command
 ** [out :: tora] 2010年 10月 21日 木曜日 20:23:59 JST
    command finished
failed: "sh -c '/bin/false'" on mike

:max_hostsの指定はrunに指定することも可能です。この場合、そのコマンド実行にだけ影響します。

task :foo do
  transaction do
    on_rollback do
      run "date; sleep 1"
    end
    run "/bin/false", :max_hosts => 1
  end
end

実行結果は次のようになります。

  * executing `foo'
 ** transaction: start
  * executing "/bin/false"
    servers: ["mike", "tama", "tora"]
    [mike] executing command
    command finished
*** [foo] rolling back
  * executing "date; sleep 1"
    servers: ["mike", "tama", "tora"]
    [mike] executing command
    [tama] executing command
    [tora] executing command
 ** [out :: tama] 2010年 10月 21日 木曜日 20:25:13 JST
 ** [out :: tora] 2010年 10月 21日 木曜日 20:25:13 JST
 ** [out :: mike] 2010年 10月 21日 木曜日 20:25:13 JST
    command finished
failed: "sh -c '/bin/false'" on mike