Slide 1

Slide 1 text

0 分散型タスクスケジューリングシステム 2023-11-17 第68回NearMe技術勉強会 @yujiosaka

Slide 2

Slide 2 text

1

Slide 3

Slide 3 text

2 タスクスケジューリングの悩み 今決まった時間に実行している バッチ処理を、データベースに 登録された値に応じて、動的に タスクの実行間隔を制御したい タスクをスケジューリング するためだけにプロセスを 常駐させるのは面倒だし、 乗り換え作業も大掛かりだわ

Slide 4

Slide 4 text

3 やりたいことのイメージ const jobInterval = await dokkaKaraMottekuru(); // > “0 * * * *” — cron expression in string // > 1000 * 60 * 60 — milliseconds in number // > { “hours”: 1 } — date-fns Duration object // > etc. await schedule({ jobInterval }, async () => { }); await nannkaOmoiShori();

Slide 5

Slide 5 text

4 気をつけないといけないこと • どのタイミングでタスクの実⾏間隔(jobInterval)を更新するのか • どこにタスクの実⾏履歴を保存するのか • タスクをスケジューリングするためのプロセスを常駐させるのか → 最初に登録された実⾏間隔でタスクが実⾏され続けないようにしたい → プロセスを再起動した時に、実⾏間隔がリセットされないようにしたい → プロセスを常駐させるなら、そのプロセスをモニタリングしないといけない

Slide 6

Slide 6 text

5 node-cron https://github.com/kelektiv/node-cron 5

Slide 7

Slide 7 text

6 API cron.schedule(“0 * * * *”, () => { nannkaOmoiShori(); })

Slide 8

Slide 8 text

7 やってること (概念的には)Cron Expressionをミリ秒に置き換えてsetIntervalを実⾏しているだけ (実際にはsetIntervalではなくてsetTimeoutを再起的に実⾏している) cron.schedule(“0 * * * *”, () => { nannkaOmoiShori(); }) setInterval(() => { nannkaOmoiShori(); }, 1000 * 60 * 60);

Slide 9

Slide 9 text

8 嬉しいこと • 直感的に使えてシンプルなAPI • 依存関係がなく、インストールするだけですぐに使える 困ったこと • プロセスが再起動すると実⾏間隔がリセットされてしまう • タスクの実⾏間隔(jobInterval)をいつ更新するべきかが悩ましい • プロセスを常駐させるため、タスクが実⾏されていない間のリソースが無駄 • プロセスの⽣存を常にモニタリングしなければならない • Cron型からプロセス常駐型への移⾏に⼿間が少しかかる

Slide 10

Slide 10 text

9 Agenda https://github.com/agenda/agenda 9

Slide 11

Slide 11 text

10 API const agenda = new Agenda({ db: { address: process.env.MONGO_URI } }); await agenda.start(); // worker agenda.define(“nannka omoi shori”, async job => { await nannkaOmoiShori(); }); // scheduler await agenda.every(“0 * * * *”, “nannka omoi shori”);

Slide 12

Slide 12 text

11 やってること MongoDBをジョブキューとしてタスク処理を分散化 MongoDB Task Task Task Worker Worker Worker Scheduler Task

Slide 13

Slide 13 text

12 嬉しいこと • 複数のプロセスでタスク処理を分散することができる • 並列度やタスクの優先度を細かく設定することができる • タスクスケジューリング以外にもキューとして使⽤できる 困ったこと • スケジューラーが再起動するたびにスケジュールがリセットされてしまう • タスクの実⾏間隔(jobInterval)をいつ更新するべきかが悩ましい • スケジューラーは冗⻑化させることができず、モニタリングは相変わらず必要 • Cron型からキュー型への移⾏はとても⼤変 • MongoDB縛り(同様にbullはRedis縛り)

Slide 14

Slide 14 text

13 Cronのシンプルさとモダンなタスクスケジューラーの 柔軟性を持ち合わせたライブラリを作れないだろうか

Slide 15

Slide 15 text

14 Cronyx https://github.com/yujiosaka/Cronyx

Slide 16

Slide 16 text

15 API await cronyx.requestJobExec( { jobName: "hourly-job", jobInterval: "0 * * * *", }, async (job) => { await nannkaOmoiShori(); }, );

Slide 17

Slide 17 text

16 やってること • 実際はタスクスケジューラーというよりタスクガードとして振る舞う • 直前のタスクの実⾏時間を保存し、条件を満たした時にだけタスクが実⾏される • 条件を満たしていない場合タスクを実⾏せず、すぐにPromiseをResolveする Database Lock Lock Lock ロック要求 ロック要求

Slide 18

Slide 18 text

17 嬉しいこと • プロセスを常駐させる必要がないため、リソースが無駄にならずモニタリングも簡単 • タスクの実⾏間隔(jobInterval)の更新に強く、常に最新の設定が反映される • プロセスに障害が発⽣しても実⾏間隔がリセットされることがない • Cronを使い続けることができるので、乗り換えが楽 困ったこと • 「タスクスケジューラー」ではなく「タスクガード」なので、少し混乱するかも

Slide 19

Slide 19 text

18 その他の機能 • 依存関係の解決 • タスクの実⾏に遅延や障害が発⽣しても、⾃動で空⽩期間を埋めて復旧する • MongoDB、Redis、MySQL、Postgresの4つをデータソースとしてサポート • トランザクションやアトミックな処理等を使って安全なタスク分散を実現 • それ以外のデータソースも⾃分でプラグインを作成して利⽤できる

Slide 20

Slide 20 text

19 依存関係の解決 await cronyx.requestJobExec( { jobName: "child-job", jobInterval: "*/30 * * * *", }, async (job) => { console.log(job.intervalStartedAt); console.log(job.intervalEndedAt); }, ); await cronyx.requestJobExec( { jobName: "parent-job", jobInterval: "0 * * * *", }, async (job) => { console.log(job.intervalStartedAt); console.log(job.intervalEndedAt); }, ); child-job.ts parent-job.ts requiredJobNames: ["child-job"],

Slide 21

Slide 21 text

20 その他の機能 • 依存関係の解決 • タスクの実⾏に遅延や障害が発⽣しても、⾃動で空⽩期間を埋めて復旧する • MongoDB、Redis、MySQL、Postgresの4つをデータソースとしてサポート • トランザクションやアトミックな処理等を使って安全なタスク分散を実現 • それ以外のデータソースも⾃分でプラグインを作成して利⽤できる

Slide 22

Slide 22 text

21 MongoDB export const mongodbJobLockSchema = new Schema({ jobName: { type: String, required: true }, jobInterval: { type: Number, required: true, default: 0 }, jobIntervalEndedAt: { type: Date, required: true }, isActive: { type: Boolean, required: true, default: true }, createdAt: { type: Date, required: true, default: Date.now }, updatedAt: { type: Date, required: true, default: Date.now }, }).index({ jobName: 1, jobIntervalEndedAt: 1 }, { unique: true })

Slide 23

Slide 23 text

22 MongoDB try { return await this.#model.findOneAndUpdate( { jobName, jobIntervalEndedAt, isActive: true }, { jobInterval, updatedAt: new Date() }, { setDefaultsOnInsert: true, new: true }, ); } catch (error) { throw error; } if (error instanceof MongoError && error.code === 11000) { return null; } , upsert: true

Slide 24

Slide 24 text

23 Cronからの移行手順 23

Slide 25

Slide 25 text

24 ステップ① cronyx.requestJobExecで元の処理を囲む await nannkaOmoiShori(); await cronyx.requestJobExec({ jobName: "job", jobInterval: "0 * * * *", }, nannkaOmoisShori);

Slide 26

Slide 26 text

25 ステップ② Cronの実行時間をjobIntervalよりも短い間隔で設定する。 → こうすることで、遅延が発生しても自動で復旧できるようになる 0 * * * * ./nannka-omoi-shori.ts */10 * * * * ./nannka-omoi-shori.ts

Slide 27

Slide 27 text

26 これからやること • あらゆるサービスから利⽤できるように、HTTPサーバーを提供する • 様々な⾔語から利⽤できるように、クライアントを提供する やったこと

Slide 28

Slide 28 text

27 CronyxServer https://github.com/yujiosaka/CronyxServer

Slide 29

Slide 29 text

28 CronyxClient.js https://github.com/yujiosaka/CronyxClient.js

Slide 30

Slide 30 text

29 CronyxClient.py https://github.com/yujiosaka/CronyxClient.py

Slide 31

Slide 31 text

30 おまけ • CronyxServerはBun + Elysiaで実装 • その他のプロジェクトもNodeで動作するがBunファーストで開発(今度発表します)

Slide 32

Slide 32 text

31 解説記事 https://medium.com/@yujiisobe/cronyx-bridging-the-gap-between-cron-jobs-and-task-scheduling-790b9f709224

Slide 33

Slide 33 text

32 Thank you