jimble

バッチ#

書く#

public class PostCleanupBatch extends AbstractBatch {

	@Override
	public String batchName () { return "記事の掃除"; }

	@Override
	public boolean isScheduler () { return true; }

	@Override
	public String cron () {

		// 毎日 3:15
		return "15 3 * * *";

	}

	@Override
	public void execute (BatchArgs args) {

		int days = settings().getInt("days");

		while (!isCancelOrder()) {
			// 長い処理は中断指示を見る(要件 F-B-06)
		}

	}

}
  • batchName() は管理画面に出る名前です
  • isScheduler()true のものだけ、スケジューラが拾います
  • cron() は5フィールドの cron 式です
  • settings() は DB に入れた設定(defaultBatchSettings() が初期値)です

登録する#

走査はしないので、自分で登録します。

BatchRegistry.add(PostCleanupBatch::new);

// DB の一覧を、いま登録されているものに合わせる
BatchRegistry.sync(DBUtil.getMainDB());

sync は、コードから消えたバッチの行を status = nothing にします(行は残すので履歴からたどれます)。

[!WARNING] sync は登録を済ませてから呼んでください。 「1つも登録されていない」は「全部消えた」として扱うので、 登録し忘れたまま呼ぶと全部の行が nothing になります(スケジューラが何も回さなくなります)。 バッチを持たないアプリは呼ばないでください。

batch_master を別のアプリと共有しないでください。 消えた判定はクラス名で行うので、同じテーブルを見るアプリが2つあると お互いの行を nothing にし合います。

中断できるようにする#

while (!isCancelOrder()) {
	// 1件ずつ処理する
}

管理画面から中断を押すと isCancelOrder()true になります。 長く回る処理では必ず見てください。 見ていないバッチは止められません。

一定件数ずつ読んでまとめて書く#

100万件を1つのトランザクションで抱えたくないときは AbstractChunkBatch を継承します。 読む → 加工する → まとめて書くを繰り返し、chunkSize() 件ごとに確定します。

public class RequestArchiveBatch extends AbstractChunkBatch<Data> {

	@Override public String batchName ()   { return "古い申請の書庫入れ"; }
	@Override public boolean isScheduler () { return true; }
	@Override public String cron ()        { return "0 4 * * *"; }
	@Override public int chunkSize ()      { return 500; }

	@Override
	protected Iterator<Data> reader (BatchArgs args, DB db) {

		// キー順に少しずつ読む
		return KeyPagingReader.of("id", 0L, chunkSize(), (lastKey, limit) -> db.selectList("""
				SELECT id, amount FROM request
				WHERE status = ? AND id > ?
				ORDER BY id ASC
				LIMIT ?
			""", "approved", lastKey, limit));

	}

	@Override
	protected Data process (Data item) {

		// 書かないものは null を返す(既定は素通し)
		return item.getLong("amount") == 0 ? null : item;

	}

	@Override
	protected void write (List<Data> items, DB db) {

		for (Data item : items) {
			db.update("UPDATE request SET status = 'archived' WHERE id = ?", item.getLong("id"));
		}

	}

}

登録の仕方も、管理画面での見え方も、ふつうのバッチと同じです。

  • 1回の write() が1トランザクションです。抜けた時点で確定します
  • 中断はかたまりの切れ目で見ます。isCancelOrder() を自分で呼ぶ必要はありません
  • execute()final です。手を入れるのは reader() / process() / write() の3つだけです

途中で落ちたとき#

**そのかたまりだけ戻して、バッチ全体を失敗にします。**黙って次へ進むことはしません。

どこまで確定したかは batch_history.execute_info に残ります。

キー 中身
chunk_read 読んだ件数
chunk_filtered process()null を返してよけた件数
chunk_written 確定した件数
chunk_committed 確定したかたまりの数
chunk_failed_at 何かたまり目で落ちたか
chunk_canceled 中断で抜けたか

chunk_writtenbatch.progress_seconds(既定5秒)ごとに書き換わるので、 走っている最中でも管理画面から「いま何件目か」が見えます。

読む DB と書く DB は別です#

reader() に渡る dbwrite() に渡る db別のインスタンスです。 渡されたものをそのまま使ってください。

DBTransaction を閉じるとコネクションがプールへ返るので、 同じ DB で読んでいると最初のかたまりを確定した時点で読みかけが死にます。

こつ

カーソル(selectListWithFetcher)ではなく KeyPagingReader を勧めます。 カーソルは開いているあいだ、ずっとコネクションを1本押さえます。 数時間かかるバッチではそれが効いてきます。 ページングはページとページの間はコネクションを持ちません。

KeyPagingReader に渡すキーは、一意で、ORDER BY と揃っていて、走っているあいだ書き換わらない列にしてください。 キーが進まないまま満杯のページが返ってきたときは例外になります(黙って無限に回るより落ちたほうがよいためです)。

[!WARNING] db.insert() などは失敗しても例外を投げません。 db.isError()その直前の1文の結果が入るだけです。 write() の中で複数の文を流すなら、1文ごとに見るか、自分で例外を投げてください。

二重起動#

同じバッチが同時に走る本数は allowConcurrentExecutionCount() で決まります。 既定は1本です。前の実行がまだ終わっていなければ、次はスキップされます。

サーバーが複数台でも、DB のテーブルで排他するので1本しか走りません。

単体で走らせる#

バッチの入口は自分で書きます。起動の順番がそのファイルに全部出ます。

public class BlogBatch {

	public static void main (String[] args) {

		DBUtil.load(Conf.conf().config(), BlogBatch.class);   // 1. DB
		BatchTables.install(DBUtil.getMainDB());              // 2. バッチのテーブル

		BatchRegistry.add(PostCleanupBatch::new);             // 3. 登録
		BatchRegistry.sync(DBUtil.getMainDB());

		BatchResult result = BatchExecutor.start(args);       // 4. 実行

		DBUtil.stop();
		System.exit(result.isExecuted() ? 0 : 1);

	}

}
java -cp app.jar blog.BlogBatch env=local class=blog.batch.PostCleanupBatch days=30

key=value の形で渡したものが args.cliArgs に入ります。

cron で回すなら DbScheduler を入口にした常駐プロセスを別に立てます。

管理画面#

jimble-batch-manager を入れると、Web からバッチの一覧・実行・中断・履歴が見られます。

implementation("io.jimble:jimble-batch-manager:<版>")
install(BatchManagerController::new);

認証は自分で付けてください。 before に置くだけです。 何も付けないと誰でもバッチを起動できます。