S2Chronosを使ってみた

S2Chronosは,Seasar2に対応したJavaオブジェクトスケジューリングフレームワークです.

というわけで、S2Chronosを利用してみました。
http://s2chronos.sandbox.seasar.org/ja/index.html

特徴

S2ChronosSeasar2の機能がそのまま利用できるのでs2daoとかがDIされ、Webアプリを利用しているのと同じ感覚で組むことができます。

導入

オフィシャルサイト(http://s2chronos.sandbox.seasar.org/ja/install.html)をみて導入します。導入はおそらくそれほど問題ないと思います。

利用方法

@Task
@CronTrigger(expression = "0 */1 * * * ?")  // 1分ごとに実行
public class BasicTask {
}

個人的には一番利用するのがクーロン型のタスクだと思います。LinuxなどのCronとの一番の違いは秒単位での実行が可能になっています。*/15などの指定で15秒間隔での実行が可能です。ただし0秒、15秒、30秒と実行されていくのではなく、前回のタスクが完了してから15秒後となりますので、インターバルでの実行と考えた方がよいと思います。

設定ファイルの外だし

現状のままですとアノテーションで指定していますので、本番サーバーは1日一回実行するが、テストサーバーは毎時間実行するなどの制御ができません。

@Task
public class BasicTask {
    private String cronStr = ResourceUtil.getProperties("s2Chronos.properties").getProperty("BasicTask");
    private CCronTrigger trigger = new CCronTrigger(cronStr);

    public TaskTrigger getTrigger(){
    	return trigger;
    }

    public void doExecute() {
        // 実際の処理
    }
}

こんな感じでプロパティファイルから設定をロードしています。

利用しているダイナミックトリガーの実装について(http://s2chronos.sandbox.seasar.org/ja/userguide.html)を参考にして上記のような設定を行います。

BasicTask=*/15 * * * * ?

プロパティファイルには上記のように記述します。あとはmvnなどの機能を利用して、パッケージを作る際に組み込まれるリソースファイルを変更することで環境別の設定を行うことが可能です。

余談

毎時ぐらいの実行であればTeedaなどで実装したページをwgetで取得して実行していたりします。Taskでタイミングを管理するとぼーっと0分になるまで待っていたり、ブラウザで動かした場合と違う挙動をしたりすることがあるからです。

    public String prerender() {
        try {
            // 実際の処理
        } catch (Exception e) {
            //500(INTERNAL_SERVER_ERROR)発生
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            facesContext.responseComplete();
        }

        return null;
    }

wgetでも上記のようにエラー時には500とかのステータスを返せば、呼び出し側で制御をすることができます。JP1など外から制御するパターンは上記の組み合わせの方が相性が良い気がします。

HOT reloading利用時の注意点

Taskは便利なのですが、HOT reloadingで利用はお勧めできません。なぜならTeedaでページを開いている瞬間に同じクラスを利用しているTaskが動くとHOT reloadingが動き、Teedaで利用していたクラスが破棄されたりします。
これは防ぐことができません。また混在環境の場合にはTaskはログを頻繁に出力したりするので邪魔になります。。。
COOL Deplyにするか、クラスの明示的リロード(http://www.seasar.org/wiki/index.php?SeasarUpdateOperationLog#j39e75d1)を使うかのどちらかがいいと思います。

私は。。。プロジェクトを分離しました!

Taskの同時実行について

原因は特定できていませんが、複数のTaskが同時に動くとTaskが止まる場合があります。エラーは出なかったと思うのですが、以後一切Cronのタスクが実行されませんでした。

ポーリング系の常に短いタームで動いているタスクと、バッチ処理などで高負荷で長時間動くTaskがある場合には、同時に実行して大丈夫かを検証する必要があります。

S2Chronosが原因ではなくて、スレッドセーフじゃない処理がどこかに入っていたのかもしれません。

DBのセッションについて

S2Chronosではなく、S2Dao側の特徴になりますが、トランザクションを利用していない場合にはDBのコネクションが変わる可能性があります。

    dao.insert( data );
    // ここで他のアクセスがあると。。。
    int lastId = dao.lastInsertId();

上記のようにシーケンスを利用したInsertで、最後に登録したIDを取得しようとした場合、InsertとlastInsertIdの間で他の処理が走ると別のDBコネクションが利用される場合があります。

この辺はDBのコネクションプーリングの機能なので仕方ないのですが、意図しない番号やInsertしていないので番号無いなどのエラーが発生する可能性があります。

上記のような作業はServiceクラスを利用して、トランザクションがかかった状態で実行しましょう!
通常のWebアプリでも起こる問題ですが、DBへのポーリング処理などを行っていると非常に発生しやすく原因がわかりにくいバグになります。

感想

非常に便利です!
でもはまりました。。。

開発環境の場合にはTaskのログを出さなくする、taskScanIntervalTimeの値を大きくしてあまり実行されなくする、必要なファイルを環境にいれないようにして実行させない。。。など通常の開発に影響がでないようにしたほうが良いと思います。

お勧めはWebアプリとTaskを別プロジェクトに分離するってのですが、そうすると共通クラスとかが気軽に使えないってことですね。
ただし最初は普通に同居してみて問題があれば分離という方針がいい気がします。Taskのログがどんどん出力されて、見たいログが流されていくのが不便に感じるとは思いますが(苦笑)

ドキュメントを読む限り、もっと高度なことができると思いますが、どう組み合わせて使えばよいのかがわからないのと、タスク管理はJP1を利用していたので、今回は単純なCron設定のみで利用しました。もう少し細かい機能の使い方事例があると有効に使えそうですね!