(1)はこちら、(2)はこちらから。
App::RunCron
(3)では、前回(2)に出てきたcron実行結果の通知処理やエラーハンドリングを統一的に行うことができる拙作のフレームワークApp::RunCronを紹介します。
App::RunCronとは?
cronにおけるログとエラーハンドリングの問題点
cronで実行したコマンドのエラー処理は悩ましいところです。パイプで出力を後続のコマンドに渡したところで、コマンドの成否自体は後続のコマンドからは知るすべはなく、出力結果から推測するしかありません。ログ処理とも共通することですが、コマンド終了コードがわからないまま、成功時も失敗時の別なく出力が流れてくることから、ログが埋もれてしまうというジレンマをcronは抱えています。
かといって、ジョブごとにエラー処理を書くのは、正しく書くのも難しく、書けたところで各ジョブ内に同じようなコードが書かれてしまいます。
App::RunCronによる解決
こういった背景を踏まえ、コマンドの終了コードを見て処理を切り分けるためのしくみが外部的に必要ではないかと思い、筆者が作ったのがApp::RunCronです。App::RunCronが持っている機能は次のとおりです。
- ラッパスクリプトによって指定したコマンドの終了コードを判別する
- 実行コマンドの終了コードに応じて処理の分岐や出力先の切り替えを行う
- 複数の出力先を指定できる
- メールやIRC(Internet Relay Chat)などの独自の出力先を指定できる
たとえば、コマンドの成功時にはIRCの通知にとどめ、失敗した際にはそれに加えてアラートメールを投げたり、それに加えて成功時も失敗時も出力を一律Fluentdに投げるといったことが実現できます。
cronの実行結果に応じて処理を分岐すると言えば、奥一穂さんのcronlogが知られています。実際、App::RunCronのコア部分はcronlogの実装をかなり参考にしています。cronlogは監視用途に寄った安定した一枚岩のPerlスクリプトですが、App::RunCronは独自の処理をプラガブルに記述できる点が異なります。
App::RunCronの一般的な使い方
インストール
App::RunCronはCPANに上がっているので、cpanmコマンドでインストールできます。
runcronコマンド
App::RunCronをインストールすると、runcron
コマンドがインストールされます。次のように引数として実行するコマンドを受け取ります。
runcron
は標準ではcronlogと同じ動きをします。つまり、コマンドにエラーがなかった場合は何も出力せず、エラーがあったときのみ出力を行います。先ほどの例では何も出力されなかったと思いますが、次のようにすれば出力されるはずです。
正常終了した場合も出力したい場合は、runcron
に--reporter
オプションを指定します。
runcron.yml
runcron
にはさまざまなオプションを指定できますが、指定が長くなってしまうため、直接オプションを指定するよりYAML(YAML Ain't Markup Language)形式のconfigファイルを渡すほうがよいでしょう。configは-c
オプションで渡すこともできますが、実行ディレクトリにruncron.ymlがある場合にはそれを自動的に読み出してくれます。以下にruncron.ymlのサンプルを記載します。
timestamp
を付けると、ミリ秒単位で実行時間をログに書き出してくれます。
common_reporter
、error_reporter
、それと上記には記述されていませんがreporter
オプションにそれぞれ、共通の通知先、エラー時の通知先、正常終了時の通知先を設定できます。先頭に+
があるものは独自レポーターモジュールで、そうではないものはApp::RunCron::Reporter::*以下のモジュールが参照されます。
上記のruncron.ymlは次の設定となります。
- 出力にタイムスタンプを付ける
- Fluentd、ファイル、IRCに常時出力する
- エラー終了時はそれに加えてメールを飛ばす
独自レポーターの書き方
App::RunCronには標準でいくつかのレポーターが同梱されていますが、基本的な機能しかありません。プロジェクト固有の要件に合わせて独自のレポーターを書くことで、App::RunCronは本来の力を発揮します。
レポーターはPerlモジュール形式で記述します。レポーターモジュールには次の2つを定義してください。
- コンストラクタメソッドnew
- オブジェクトメソッドrun
以下がレポーターのひな型です。
new
メソッドに設定項目が渡されてReporterオブジェクトが作られます。引数として必須の属性を持ち、それが足りない場合には例外を投げるようにしておいたほうが、runcron.ymlのテストを行った場合にエラーを検知できるのでよいでしょう。
run
メソッドにはcronの実行情報が格納されたApp::RunCron のオブジェクトが渡ってきます。App::RunCronの各種アクセサに関してはApp::RunCronのドキュメントを参照してください。
独自レポーターやruncron.ymlをテストする方法
runcron.ymlや独自レポーターに不備があってcronが動かなかったら大変です。runcronはその特性上、runcron.ymlや独自レポーターに不備があった場合も指定したコマンドは極力実行されるようになっています。しかしそれらは本来誤りなく動いてほしいものなので、テストなどで検知できるに越したことはありません。ですので、App::RunCronにはruncron.ymlと独自レポーターをテストするためのテストフレームワークTest::App::RunCronを同梱しています。
Test::App::RunCronをuse
すると、runcron.ymlのテストを行うruncron_yml_ok
関数と、カスタムレポーターをテストするためにrun
メソッドに渡されるApp::RunCronオブジェクトのモックを生成するmock_runcron
関数がインポートされます。Test::App::RunCronを利用すると次のようにテストを書くことができます。
App::RunCronはまだユーザも少なく、発展途上のモジュールですが、Perl以外のユーザでも汎用的に使える便利なモジュールですので、ぜひ使ってみてフィードバックをいただけると幸いです。単にcronで実行するジョブに限らず、そのほかのジョブの終了の通知処理などをラップする場合にも便利です。
cron実行に寄与する小さなツール群
crontabで指定するジョブはUNIXコマンドなので、UNIXの哲学に従って小さなツールを組み合わせてやりたいことを実現していくのがよいでしょう。これまで取り上げられませんでしたが、cronを活用するうえで重宝する小さなツールを最後に2点紹介します。
timeout──タイムアウトの時間を設定する
実行コマンドのタイムアウト時間を設定したい場合に便利なのがtimeoutコマンドです。GNU Coreutilsに含まれています。
上記のように指定すると、45秒で処理をタイムアウトさせることができます。タイムアウト時にはデフォルトではTERMシグナルが送られる(オプションで変更も可能)ので、コマンド側で必要に応じてシグナルハンドリングを行いましょう。
setlock──排他的に処理を実行する
たとえば毎分実行するようなジョブの実行時間が1分を超えてしまったような場合であっても、cronは次分のジョブを愚直に実行します。そのようなときにジョブの排他制御を行うのに重宝するのがsetlockコマンドです。setlockはdaemontoolsに含まれています。
setlockは次のようにロックファイルを指定して、後続にコマンドを指定します。
これはcommand1 を毎分実行するcronですが、command1が終わっていなかった場合にはcommand1が実行されないようになっています。
setlockに-n
オプションを指定すると、ロックがかかっている場合に指定のコマンドの実行を行わず即座に終了するようになっています。逆に-n
を指定しない場合は、ロックの解除を待ってからコマンドを実行するようになっています。
ほかのジョブと同じロックファイルを指定することで、ジョブ間の排他制御を行うこともできます。
setlockの-x
オプションは、コマンド実行が失敗した場合(ロックされていた場合も含まれます)でもエラー終了しないオプションです。
この場合、command1は毎分起動され、ロックされていれば実行が行われません。command2は毎時30分に起動され、ロックされている場合はロックしているジョブの実行を待ってからジョブが実行されるようになっています。
応用例としてリスト3のようにsetlockを重ねることで、複数のロックファイルを作ることもできます。これはあまりメリットを感じられる例ではありませんが、複数のジョブ間で複雑な排他制御をしたい場合に効果を発揮します。
daemontoolsにはsetlockのほかにsoftlimitというコマンドも含まれています。これは、メモリなどのリソースを制限して指定コマンドを実行してくれるもので、こちらもcronでも有用です。
まとめ
多くの人が何気なく利用してきたcronですが、しっかり使おうとすると気をつけないといけない点、運用するにあたって知っておいたほうがよい知識、より活用するうえで知っておいたほうがよいTipsなど、いろいろ奥が深い点があることを、本稿を通じて知ってもらえれば幸いです。特に、cronにまつわるもろもろの項目をテストするといった視点は、これまであまり語られてこなかったように感じます。
ただ、使いづらいものを「奥が深い」と言って使い続けることは「奥が深い症候群」に陥りかけているとも言えるでしょう。より良いcronの代替については長らく待ち望まれている領域でもあるので、腕に覚えのある方は実装してみてはいかがでしょうか。
さて、次回の執筆者は石垣憲一さんで、テーマは「Perlで困ったときの調べ方」です。