ストレートなジョブ (LastaJob)

LastaFluteの特徴の一つです。

Java上でスケジューリング

時間が来たら動く

cron 書いたら、その時間に Job が動く

スケジューリングする仕組みが別に用意されている場合は全く問題ありませんが、 Java上で気軽にスケジューリングしてバッチを実装するための仕組みも捨てがたいです。 そんなに大げさなことをしないのであれば、インフラ用意せずにJava上だけで "cron書いたら、その時間に Job 動く" くらいでサクッと実装したいものです。

それが、LastaJob です。

LastaJobのアーキテクチャ

LastaJob Architecture

まずは、Jobの環境準備

pom.xml に lasta-job を定義します。

e.g. Lasta Job dependency @pom.xml
<lasta.job.version>LastaJobの最新バージョンをここに</lasta.job.version>

...

<dependency>
    <groupId>org.lastaflute.job</groupId>
    <artifactId>lasta-job</artifactId>
    <version>${lasta.job.version}</version>
</dependency>

app.xml で lasta_job.xml を include します。

e.g. include Lasta Job @app.xml
<components>
	<include path="convention.xml"/>
	<include path="dbflute.xml"/>
	<include path="lastaflute.xml"/>
	<include path="lasta_job.xml"/>
</components>

Jobのスケジューリング

AllJobSchedulerのパッケージ

app.job.AllJobScheduler クラスを作成して、Jobクラスを cron 登録します。

e.g. AllJobScheduler package @Directory
org.docksidestage
 |-app // application package
 |  |-job
 |  |  |-AllJobScheduler
 |  |  |-SeaJob
 |  |  |-LandJob
 |  |  |-...
 |  |-web // web package
 |     |-AbcAction
 |     |-AbcForm
 |-bizfw
 |-dbflute
 |-mylasta

AllJobSchedulerの実装

AllJobScheduler は、LaJobSchedulerインターフェースを implements します。

e.g. AllJobScheduler implementation @Java
/**
 * @author jflute
 */
public class AllJobScheduler implements LaJobScheduler {

    protected static final String APP_TYPE = "JOB";

    @Resource
    private TimeManager timeManager;
    @Resource
    private DocksideConfig docksideConfig;
    @Resource
    private AccessContextLogic accessContextLogic;

    @Override
    public void schedule(LaCron cron) {
        cron.register("* * * * *", SeaJob.class, waitIfConcurrent(), op -> {});
        cron.register("* * * * *", LandJob.class, quitIfConcurrent(), op -> {});
        ...
    }

    @Override
    public LaJobRunner createRunner() {
        return new LaJobRunner().useAccessContext(resource -> {
            return accessContextLogic.create(resource, () -> OptionalThing.empty(), () -> OptionalThing.empty(), () -> APP_TYPE);
        });
    }
}

ここで、cron(起動時間)をハードコードするのか、configから取るのか、はたまた別のところから取るのかはアプリの自由です。 必要に応じて調整しましょう。

同じJobクラスを複数登録できます。クラスは同じでも、パラメーターを変えたりして、少し違う挙動をする別のJobとして振る舞えるようにしています。 その場合、内部管理的にも別Job扱いとなります。ゆえに、例えば "同じJob" という表現をするときは、明示的にクラスという言葉が付いていなければ、登録単位で同じかどうかを示すます。

二重起動防止のタイプ

register()メソッドでは、必ず二重起動防止のタイプを指定します。

waitIfConcurrent()
同じJobが実行中だったら先Jobが終わるのを待つ
quitIfConcurrent()
同じJobが実行中だったら後Jobが実行を静かに諦める
errorIfConcurrent()
同じJobが実行中だったら後Jobがエラーになる (エラーログに残る)

同じJob同士での二重起動防止ですが、厳密にはJobクラス単位ではなく、cronに登録した単位です。 同じJobクラスを違うパラメーターで複数登録できますが、その場合は別Job扱いとなります。

様々なオプション (タイトルやパラメーターなど)

それぞれのJobに対して、様々なオプションを付与することができます。

op.title(...)
人が見るための、Jobのタイトル (Job管理画面などで使うこと想定) (@since 0.2.6)
op.uniqueBy(...)
Job特定のための、Jobのユニークコード (アプリが付与する不変の文字列を想定)
op.params(...)
Jobに与えるパラメーター (検索範囲の日付などを想定)
op.triggeredBy(...)
起動のきっかけになるJob (後述) (ジョブネットを想定)

ユニークコードは、JobManagerでJobを探すのに使えます。 Jobを特定するためのキーは、デフォルトで JobKey が付与されていますが、再起動のたびに変わる内部管理用の文字列です。 システム外からもJobを制御するために不変のコードを付与したい場合は、ユニークコードを使うと良いでしょう。

タイトルやユニークコードは、それぞれ LaScheduledJob から取得できます。 パラメーターは、Jobクラスの中で LaJobRuntime から取得できます。

CronなしJob (NonCron)

時間起動ではなく、JobManagerによる手動の起動や、NextTriggerによる別Jobからの起動だけで起動するJobを登録したい場合は、registerNonCron()を使います。

e.g. Sea to Land as non-cron in AllJobScheduler @Java
@Override
public void schedule(LaCron cron) {
    cron.registerNonCron(SeaJob.class, waitIfConcurrent(), op -> {});
    ...
}

NextTrigger (ジョブネット)

NextTriggerの設定方法

例えば、SeaJob が終わったら(成功したら)、LandJob を実行したい、という場合...

e.g. Sea to Land in AllJobScheduler @Java
@Override
public void schedule(LaCron cron) {
    RegisteredJob seaJob = cron.register("* * * * *", SeaJob.class, waitIfConcurrent(), op -> {});
    cron.register("* * * * *", LandJob.class, waitIfConcurrent(), op -> {
        op.triggeredBy(seaJob);
    });
}

これにて、SeaJob が成功したら、LandJob が自動的に起動されるようになります。@since 0.2.7

triggeredBy() は複数呼び出せば、起動のきっかけのJobを複数設定することもできます。

NextTriggerの細かい挙動

成功というのは、例外が発生していないこと を表しています。 もし、"例外は発生させたくないけど、とある状況で次のJobを動かしたくない" という場合は、SeaJobの中で分岐させて runtime.suppressNextTrigger() を呼ぶことで次のJobの起動を抑えることができます。

LandJob は cron 設定がされているので、cronで指定された時間が来ても動きます。 連動も独立もしています。両方から同時に起動しても二重起動防止の制御は効きます。もし LandJob は、SeaJob からしか呼ばれず、独立して時間起動しないのであれば、LandJob は NonCron で登録すると良いでしょう。

NextTriggerとJob設計

例えば "SeaJobの後にLandJobが呼ばれる" としたときに、SeaJobからLandJobに対して動的な引数を与えることはできません。

それぞれのJobは、前後のJob (ジョブネット設定) に依存せず、できるだけ独立的に実行できるようにしておいた方がわかりやすくトラブルも少ないと考えます。 どうしても特定の値を受け取るくらいJob間で依存するのであれば、SeaJobに処理をまとめた方が明示的でわかりやすく、引数もコンパイルセーフで扱われます。 SeaJobクラスの複雑化を避けたいのであれば、JobAssistなどに切り出せば良いでしょう。 LandJobを再利用 (別のJobやスケジュールから実行するなど) するであれば、Logicクラスで再利用すれば良いでしょう。

Jobクラスは、簡易な処理はJobクラス自体に実装したりはしますが、あくまでスケジュールや起動タイミングに関するコントローラーであって、それ自体が Logic としてやりくりするものではないと考えています。 (全く別の理由などで、どうしても必要となったときは検討しようかと思います。 あり得るとしたら、Jobの設定をDBに保持するなどしてコード修正無しでアプリの再起動なしで完全に動的なジョブネットを構築そして変更したい場合など...)

Jobの実装の仕方

Jobクラスのパッケージ

app.job の下に Job クラスを作成します。

e.g. Job package @Directory
org.docksidestage
 |-app // application package
 |  |-job
 |  |  |-AllJobScheduler
 |  |  |-SeaJob
 |  |  |-...
 |  |-web // web package
 |     |-AbcAction
 |     |-AbcForm
 |-bizfw
 |-dbflute
 |-mylasta

Jobクラスの実装

Job は、LaJobインターフェースを implements します。

e.g. Job implementation @Java
/**
 * @author jflute
 */
public class SeaJob implements LaJob {

    @Resource
    private TransactionStage stage;
    @Resource
    private MemberBhv memberBhv;

    @Override
    public void run(LaJobRuntime runtime) {
        ... = memberBhv.select...;  // you can select

        stage.required(tx -> { // you can use transaction
            memberBhv.update...;  // you can update
            ...
        });
    }
}

トランザクションはTransactionStage

Actionとは違い、トランザクションはデフォルトではかかっていません。バッチ処理は細かくトランザクションを分ける可能性が非常に高いからです。 なので、TransactionStage を DI して、うまくトランザクションをロジックの中に組み込みましょう

システム例外のcatchは不要

Actionと同じく、システム例外(要はバグ)は自分で catch してログに出す必要はありません。そのまま throw すれば、LastaJob が catch して ERROR レベルでログを出力します。

Jobの実行管理情報を付与して出力してくれるので、アプリでは throw する例外のメッセージをいかにデバッグしやすいものにするかに注力しましょう。 (例外のきっかけになった値をメッセージに載せる)

JobAssistも使える

Action の ActionAssist と同じような感じで、Job も JobAssist が使えます。再利用するようなものではないけど、クラス分けして整理整頓したいという場合にどんどん利用していきましょう。

jobパッケージの下に、Assistで終わる名前のクラスであれば Quick Component として認識されるので、JobクラスでDIして利用することができます。

e.g. job assist @Package
app
 |-job
 |  |-AllJobScheduler
 |  |-sea
 |  |  |-SeaJob.java
 |  |  |-SeaAssist.java
 |  |-land
 |-web

EndTitleRollを活用しよう

例えば、どの会員を処理した?何件処理した?何件中何件は失敗した?などの情報をログとして残すことは、バッチの実装における必須要件とも言えるでしょう。 自前でログに出してもOKではありますが、LastaJobではそういった情報を出力するための領域が用意されています。

e.g. simple EndTitleRoll on Job @Java
runtime.showEndTitleRoll(data -> {
    data.register("targetMember", memberId);
});

Jobの実行通知ログ

開始ログ、終了ログ

バッチで不可欠な、実行通知ログがデフォルトで組み込まれています。 INFOレベルなので、本番でも出力されます。(設定次第ですが、LastaFlute の Startup のデフォルト設定ではINFOも本番出力)

開始ログ
cron(時間)とJobクラスなどの情報
終了ログ
Jobクラスなどの情報に加えて、Jobの結果

Jobの結果ではシステム的な情報が表示されます。

パフォーマンスビュー
そのJobの実行にどれだけ時間がかかったか?
SQL発行回数
そのJobの実行でどれだけDBアクセスしたか? (DBFluteと連携)
メール送信回数
そのJobの実行でどれだけメール送信したか? (MailFluteと連携)
Jobのランタイム情報
LaJobRuntime の toString() (フレームワーク内部情報)
EndTitleRoll
Jobクラスの中で指定された業務上の結果 (アプリで指定、処理件数などを想定)
e.g. Job Notice Log @Log
... INFO  (...) - #flow #job ...Running job: * * * * * SeaJob@run()

...

... INFO  (...) - #flow #job ...Finishing job: SeaJob@run()
[Job Result]
 performanceView: 00m01s328ms
 sqlCount: {total=3, selectCB=1, entityUpdate=2, queryUpdate=0, outsideSql=0, procedure=0}
 runtime: Cron4jRuntime:{* * * * *, SeaJob@run(), params={}}@74469155
 endTitleRoll:
   targetMember: 3

EndTitleRoll

EndTitleRoll は、Jobクラスの中で指定します。 例えば、どの会員を処理した?何件処理した?何件中何件は失敗した?など、仕組みの中では判別できない業務情報を載せると良いでしょう。

e.g. EndTitleRoll on Job @Java
@Override
public void run(LaJobRuntime runtime) {
    int memberId = 3; // #simple_for_example
    stage.required(tx -> {
        Member before = memberBhv.selectByPK(memberId).get();
        updateMember(before.getMemberId());
        restoreMember(before.getMemberId(), before.getMemberName()); // for test
    });
    runtime.showEndTitleRoll(data -> {
        data.register("targetMember", memberId);
    });
}

自前で出さずに、EndTitleRollとして出力するメリットは:

どんな情報をログに出しているかわかりやすい
プログラムの中にあれこれ挟むのではなく、最後に register するのでログ要件のレビューもしやすい。
他のログと紛れない
別々のJobが同時に実行されていても、一回の終了ログでまとめて出すことで他のログと紛れない。
UnitTestでテストしやすくなる
EndTitleRoll自体を、UnitTestの中で参照してアサートすることができる。

例外ログ

例外が発生したら、ERRORレベルで例外メッセージとスタックトレースなどの情報が出力されます。 ゆえに、Jobクラスの中で自前でtry-catchしてエラーログに出力する必要はありません。 (処理を続行するような場合はまたちょっと話は別ですが、処理が中断して良い状態であればthrowしちゃってOK)

e.g. Exception log for Job @Java
2017-02-19 10:38:00,051 [job_7f80fef4] ERROR (LaJobRunner@handleJobException():323) - Failed to run the job process: #flow #job
/= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =: org.docksidestage.app.job.SeaJob
  jobRuntime=Cron4jRuntime:{* * * * *, SeaJob@run(), params={}}@67b2c004
  ; accessContext=AccessContext:{localDateTimeProvider=org.docksidestage.app.logic.context.AccessContextLogic$$Lambda$96/1603633761@3fa058c5, userProvider=org.docksidestage.app.logic.context.AccessContextLogic$$Lambda$98/710322753@73e22d87}
  ; callbackContext=CallbackContext:{behaviorCommandHook=null, sqlFireHook=RomanticTraceableSqlFireHook@4d899cf5, sqlLogHandler=null, sqlResultHandler=RomanticTraceableSqlResultHandler@3c4ac7a0, sqlStringFilter=RomanticTraceableSqlStringFilter@111d69c}
  ; sqlCount={total=0, selectCB=0, entityUpdate=0, queryUpdate=0, outsideSql=0, procedure=0}
= = = = = = = = = =/ [00m00s014ms] #47667fab
java.lang.IllegalStateException: exception test
	at org.docksidestage.app.job.SeaJob.run(SeaJob.java:45)
	at org.lastaflute.job.LaJobRunner.actuallyRun(LaJobRunner.java:169)
	at org.lastaflute.job.LaJobRunner.doRun(LaJobRunner.java:154)
	at org.lastaflute.job.LaJobRunner.run(LaJobRunner.java:118)
	at org.lastaflute.job.cron4j.Cron4jTask.runJob(Cron4jTask.java:153)
	at org.lastaflute.job.cron4j.Cron4jTask.doExecute(Cron4jTask.java:140)
	at org.lastaflute.job.cron4j.Cron4jTask.execute(Cron4jTask.java:104)
	at it.sauronsoftware.cron4j.TaskExecutor$Runner.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)
2017-02-19 10:38:00,061 [job_7f80fef4] INFO  (JobNoticeLog@log():40) - #flow #job ...Finishing job: SeaJob@run()
[Job Result]
 performanceView: 00m00s022ms
 sqlCount: {total=0, selectCB=0, entityUpdate=0, queryUpdate=0, outsideSql=0, procedure=0}
 runtime: Cron4jRuntime:{* * * * *, SeaJob@run(), params={}}@67b2c004
 cause: IllegalStateException *Read the exception message! #47667fab

ERRORレベルの例外情報に加えて、INFOの終了ログも出ます。互いのログの紐付けは、ハッシュコード (Exampleだと #47667fab) を見ることで判断できます。@since 0.2.7

JobクラスのUnitTest

new して inject して run()

普通に UTFlute のやり方で、new して inject して run() メソッドを呼び出せます。

e.g. Job Unit Test on Job @Java
public class SeaJobTest extends UnitOrleansTestCase {

    public void test_run_basic() {
        // ## Arrange ##
        SeaJob job = new SeaJob();
        inject(job);
        MockJobRuntime runtime = MockJobRuntime.of(job.getClass());

        // ## Act ##
        job.run(runtime);

        // ## Assert ##
        // BehaviorをDIして、DBの状態とかのアサートをやると良いでしょう
        ...

        // EndTitleRollのアサートやると良いでしょう
        Map rollMap = runtime.getEndTitleRollMap();
        log(rollMap);
        assertEquals(3, rollMap.get("targetMember"));
    }
}

run() の引数に MockJobRuntime

引数の LaJobRuntime は、MockJobRuntime.of() で Mock を作れます(@since 0.2.7)

大抵の場合は、他の情報を付与する必要はありませんが、Jobクラスの中でパラメーターなどを参照している場合は、必要に応じて設定しましょう。 of() のオーバーロードメソッドで指定できます。

アサートで EndTitleRoll も

EndTitleRollのアサートはしっかりやると良いでしょう。やはりバッチは本番の運用が大事です。 業務は満たしていても、運用で必要なログが出ていなければつらいものです。

内容は、runtime の getEndTitleRollMap() から Map で取れます(@since 0.2.7)

LaJobRunnerのカスタマイズ

用意されているオプション

よくある拡張のために、オプションとして拡張ポイントを用意しています。

useAccessContext()
DBFluteのAccessContextの共通カラムの設定
useCrossVMHook()
別のJavaVM間で排他制御する場合のHook
useErrorLogHook()
エラーログ出力時のHook (エラーメールを飛ばすときなど)
useHistoryHook()
実行履歴保存時のHook (DBに保存するときなど)
useNoticeLogHook()
通知ログ出力時のHook
limitJobHistory()
オンメモリの実行履歴の保存制限を指定

LastaJobでは、Jobを実行する LaJobRunner をカスタマイズすることで、様々な Job に対する共通処理や挙動の変更などを実現できます。LaJobRunner はアプリが new しますので、好きなようにオーバーライドできます。

例えば、すべてのJobでエラーが発生した時にメールを飛ばしたかったら、useErrorLogHook() を利用すると良いでしょう(@since 0.2.7)

e.g. call useErrorLogHook() of job runner for error mail sending @Java
@Override
public LaJobRunner createRunner() {
    return new LaJobRunner().useAccessContext(resource -> {
        return accessContextLogic.create(resource, () -> OptionalThing.empty(), () -> OptionalThing.empty(), () -> APP_TYPE);
    }).useErrorLogHook(resource -> {
        String bigMessage = resource.getBigMessage(); // スタックトレース含みのまるごとエラーメッセージ
        // 例えば、ここでメールを飛ばすとか!
    });
}

共通カラムの設定はExampleデフォルト

大抵の場合、DBFluteの共通カラムの設定は、必ずやる必要があるでしょう。 LastaFlute の Example では、最初から設定されています。Actionクラスのものと再利用するために、Logicクラスに切り出されています(AccessContextLogic)。

JobManagerでマネジメント

DIコンポーネントとして、JobManager が利用できます。

findJobByKey()
JobKeyでJobを検索
findJobByUniqueOf()
アプリ指定のユニークコードでJobを検索
getJobList()
すべてのJobを取得
schedule()
Jobを新たに登録 (システムが動いてる途中で登録できる)
destroy()
登録されたスケジュールを完全に破棄 (完全に初期化、普通やらない)

find した LaScheduledJob で、個々の Job に対して操作することもできます。

getJobKey()
Job特定のための、内部発行したJobキーを取得
getJobTitle()
人が見るための、アプリ指定のタイトルを取得 (あれば) (@since 0.2.6)
getJobUnique()
Job特定のための、アプリ指定のユニークコードを取得 (あれば)
getCronExp()
cron設定を取得
getJobType()
Jobコンポーネントの型を取得
isExecutingNow()
このJobがいま動いているかどうか?
launchNow()
このJobをいま動かす (スケジュールと関係なしに)
stopNow()
このJobが動いていたら停止(停止要求を出す) (要求に応じるかは実装次第)
reschedule()
このJobのcronをリスケする (いま動いているものは最後まで動く)
unschedule()
実行できない、findできて、rescheduleで戻せる (いま動いているものは最後まで動く)
disappear()
完全削除、findできない、もう戻せない (いま動いているものは最後まで動く)
becomeNonCron()
このJobをcronなし状態にする、勝手には動かない (launchNow()だけで動かせる)
isUnscheduled()
このJobがunscheduleされたかどうか?

Jobごとにログファイルを分ける

ログファイルを分けるきっかけ

基本的には、すべての Job のログが、ひとつのアプリログ app_[app].log に出力されます。

  • 本番では、通知ログ(INFO)以上のものしか出力しない (DEBUGは出力しない)
  • Jobの業務的な結果は、showEndTitleRoll()に組み込むのであまりバラバラにならない
  • フレームワークの定型ログがほとんどなので、混ざってもそこまで見づらくはならない
  • そもそも、仕組みやアプリのJobやActionなど様々な出力箇所があるので簡単に分けられない

ということから、Exampleからのスタートアップ環境では、Jobが入っているアプリでも普段のアプリログがデフォルトのログになっています。

ただ、もちろん、実行されるJobの数や頻度によって状況は変わります。 例えば、処理対象データがなければ空振りするだけの数分おきに起動するJobが何個もあるような場合、それらJobの開始と終了のログだけでログファイルが埋め尽くされてしまいます。 そのような場合は、分けたくなるでしょう。

ログファイルを分けるやり方

LastaJobとしては、ログファイルの出力を分けることは(当然のことながら)できません。ログファイルの設定は Logback にあります。どのような設定をすれば分けることができるのでしょうか?

Logbackに、SiftingAppender というログを複数のファイルに振り分けるための Appender が用意されています。 それを使って、Jobごとにログファイルが分かれるようにします。

  1. AllJobSchedulerにて、すべての Job に JobUnique を指定 e.g. op.uniqueBy("sea")
  2. どのようなルールで振り分けるかを決定する Discriminator クラスを作成する
  3. Jobとは関係のないログを除外するような Filter クラスを作成する
  4. logback.xml にて、それらクラスを使って SiftingAppender の設定をする

テストプロジェクトの lastaflute-test-fortress にて、そのExample実装があります。

JobUnique の値が、そのままログファイルの名前で利用されます。

Discriminator と Filter は非常に定型的なクラスですが、厳密には LastaJob は Logback には依存していないので、LastaJob に組み込むことができないため、アプリで用意します。

Jobごとのログファイルを付け足すような設定にしているので、アプリログ app_[app].log 自体は今まで通りです。 起動時のログやActionクラスでのログも入るので、これはこれで大切なログです。 ただ、Jobのログは分割ログで見るから十分でアプリログからは除外したいというのであれば、アプリログの方で Filter すると良いでしょう(同じようにスレッド名のprefixで制御)

バッチ管理画面の作成

バッチが落ちたときにリカバリ実行するためなど、様々な場面でバッチ管理画面があると役に立ちます。 cronを一時的に5分後に設定して再起動してリリースして終わったら元に戻す...なんて運用はしたくないものです。

画面を作ることも想定して、LastaJob は LastaFlute の上で動いています。バッチだから Tomcat じゃなくても良いのでは?と普通は思いますが、バッチ管理画面とセットになっていた方が環境構築の手間も省けるので何かと世話ないのです。 (プロセス分割とかしないシンプルな構成であれば)

例えば、Job一覧画面を作成するのであれば、Actionクラスで JobManager@getJobList() を呼んで画面に表示すればOKです。JobManager が Job のデータベースの入り口のようなものです。

e.g. getJobList() at Action class @Java
@Execute
public HtmlResponse index() {
    List<LaScheduledJob> jobList = jobManager.getJobList();
    List<JobRowBean> beans = mappingToBeans(jobList);
    return asHtml(path_Job_JobListHtml).renderWith(data -> {
        data.register("beans", beans);
    });
}

そこで選択されたJobを実行するなら、Actionクラスで JobManager@findJobByなんとか() を呼んで、LaScheduledJob@launchNow() を呼べばOKです。

e.g. launchNow() at Action class @Java
@Execute
public JsonResponse<Void> launch(String jobKey) { // Ajax想定
    jobManager.findJobByKey(LaJobKey.of(jobKey)).ifPresent(job -> {
        job.launchNow(); // 実行要求 (別スレッドで実行される)
    }).orElse(() -> {
        throw responseManager.new404("Not found the job: " + jobKey);
    });
    return JsonResponse.asEmptyBody();
}

Jobの実行中にアプリを再起動しないように、実行状態を表示しておくのも良いでしょう。再起動する前提で、すべての Job を一気に unschedule するのも良いでしょう。現場の要件に合わせてうまく JobManager を使っていきましょう。

単純に、マスターメンテナンス画面を一つ作るだけ という風に考えてください。

外だしスケジューラー方式

例えば、バッチサーバーをスケールさせたり、Jobごとに実行プロセスを分けてリリースのライフサイクルで互いに影響し合わないようにするなど、 インフラ的な解決が必要な場合は、LastaJobのスケジューリングの機能は利用せず、別途ツールでスケジューリングをする必要があるでしょう。

その場合でも、LastaJob は利用できるでしょう。 JobをCron設定せずに登録 すると、JobManager@launchNow() 経由のみで実行できる状態となります。

e.g. NonCron Job registration @Java
@Override
public void schedule(LaCron cron) {
    //cron.register("* * * * *", SeaJob.class, waitIfConcurrent(), ...);
    cron.registerNonCron(SeaJob.class, waitIfConcurrent(), op -> {});
    ...
}

外部からJobを特定するための業務上の Jobユニークコード を設定しておくと良いでしょう。

e.g. uniqueBy() to specify one Job @Java
@Override
public void schedule(LaCron cron) {
    cron.registerNonCron(SeaJob.class, waitIfConcurrent(), op -> {
        op.uniqueBy("sea"); // seaという名前で Job を特定できる
    });
    ...
}

そして、外部のスケジューラーから信号(リクエスト)を受けたら該当の Job を実行する ような Action クラスを一つ作ってしまえばOKです。

e.g. action to execute Job @Java
@Resource
private JobManager jobManager;

@Override
public JsonResponse<Void> index(String uniqueCode) {
    LaJobUnique jobUnique = LaJobUnique.of(uniqueCode);
    LaScheduledJob job = jobManager.findJobByUniqueOf(jobUnique).orElseTranslatingThrow(cause -> {
        ... // 存在しなかった時の処理: クライアントエラーとか!?
    });
    job.launchNow(); // Jobの実行!
    return JsonResponse.asEmptyBody();
}

でも、これだったら別に Job じゃなくて、普通の Action クラスでバッチ処理を書いても良さそうに一見思えますが、LataJob の Job として実装することにメリットがあります。

Jobの二重起動防止
同じJavaVM内での二重起動防止は効く (Actionにはない)
Jobの状態確認
そのJobがいま実行中か?JobManagerで判別できる (Actionにはない)
Jobの中断要求
Jobに少し実装入れれば、JobManagerで要求できる (Actionにはない)
Jobの実行結果ログ
本番想定の通知ログの仕組みが組み込まれている (Actionにはない)
メール送信回数ログ
メールの送信回数がログに出てきて状況把握に役立つ(Actionにはない)
Jobの一覧管理
Jobの一覧情報をJobManagerから取得できる
Jobだけの共通処理
JobはJobとまとめっていることで、区別した処理を入れやすい

なので、JavaでDBアクセスしてバッチ処理を実装する限りは、スケジューリングを外出しにするか LastaJob に任せるかに関わらず、バッチ処理は Job で実装するのが自然でしょう。

バッチだけ別の言語!?というのも無きにしも非ずですが、DBアクセスポイントを複数にするとDB変更がしづらくなりますので、やはり LastaFlute を使っているのであれば、その選択肢はあまり考えにくいでしょう。

画面起動バッチ方式

この場合、外だしスケジューラー方式とそんなに変わりません。"画面から人がボタンを押して起動する" という外だしスケジューラーだと思えば、サーバーサイドの実装に大きな違いはないからです。

Job を、Jobユニークコード 付きで NonCron として登録します。

e.g. NonCron Job registration for button cron @Java
@Override
public void schedule(LaCron cron) {
    cron.registerNonCron(SeaJob.class, waitIfConcurrent(), op -> {
        op.uniqueBy("sea"); // seaという名前で Job を特定できる
    });
    ...
}

バッチを起動する画面の起動ボタンの Action にて、Jobユニークコードを受け取り、Job を launchNow() します。

e.g. action to execute Job for button cron @Java
@Resource
private JobManager jobManager;

@Override
public JsonResponse<Void> exec(String uniqueCode) {
    LaJobUnique jobUnique = LaJobUnique.of(uniqueCode);
    LaScheduledJob job = jobManager.findJobByUniqueOf(jobUnique).orElseTranslatingThrow(cause -> {
        ... // 存在しなかった時の処理: クライアントエラーとか!?
    });
    job.launchNow(); // Jobの実行!
    return JsonResponse.asEmptyBody();
}

もちろん、画面でも起動するし、時間でも動くようにすることもできます。二重起動防止も、しっかり画面からの起動と時間の起動で制御がかかります。 むしろ、リカバリ実行用の画面というのを作るケースは多いと思うので、形はどうあれらバッチ起動画面を作ることは多いと想定しています。

Example実装は maihama-orleans にて

LastaJobのExample実装は、マルチプロジェクトのExampleであれば、maihamaプロジェクトの maihama-orleans にあります。

https://github.com/lastaflute/lastaflute-example-maihama.git

Thanks, Frameworks

LastaJobは、内部的なスケジュール管理機能としてCron4jを使っています。 Cron4jがなければLastaJobを作ることはできなかったでしょう。とても感謝しています。ありがとうございます。

https://www.sauronsoftware.it/projects/cron4j/