月末最終営業日の自動実行まで通す——定義 1 ファイル・crontab 1 行・シェル 6 行

Kairos

はじめに——「別の回で書く」と言った回

引っ越しガイドの最後で、Kairos と実行系をつなぐ型を 2 つ示して、「この 20〜40 行の型は別の回でまとめて書く」と書いた。この記事がその回である。

題材は 1 本に絞る。月次の締め処理を、月末最終営業日の 23:55 に動かす。cron には「月末」も「営業日」も無い(第 1 弾がこの連載の出発点だった)。Kairos で「いつ」を書き、cron か OS のタイマーで「動かす」ところまで、手元で実際に叩いた出力だけで通す。用意するのは定義 1 ファイル・crontab 1 行・シェルスクリプト6 行である。

1. 「いつ」を書く——定義 1 ファイル

month-end.kairos という名前で保存する。前文(premise)は引っ越しガイドと同じもので、祝日 8 個を直書きしている:

premise JP {
  calendar-system: Gregorian; tz: "Asia/Tokyo"; wkst: Mon
  national = [2026-09-21, 2026-09-22, 2026-09-23, 2026-10-12, 2026-11-03, 2026-11-23, 2027-01-01, 2027-01-11] covering: 2026-08-01..2027-01-31
  satSun = everyDay |> filter(d => weekday(d) == Sat or weekday(d) == Sun)
  bizDay = everyDay \ (satSun | national)
}

@JP
bizDay |> within(month) |> last

「営業日の列を作り、月ごとに最後の点を取る」。まず、次の 4 回を聞く:

$ kairos next -n 4 --from 2026-09-24 month-end.kairos
2026-09-30
2026-10-30
2026-11-30
2026-12-31
# 被覆サマリ
#   JP.national covering 2026-08-01..2027-01-31 残走路 31 日

10 月末(10/31 土)が金曜の 10/30 へ退いている。末尾の # 行は「祝日表がいつまで確認済みか」の残りを教えている——4 回目の 12/31 から表の末尾(1/31)まで 31 日。表を伸ばさずにいると、この数字が減って、やがて結果に注記が付く

CLI は npm i -g kairos-lang で入る。以降、kairos は 1.0.1 である。

2. 「今日は動く日か」を聞く——判定は JSON で数える

動かす側が知りたいのは、点の列ではなく 今日 1 日の中に点があるか である。窓を 今日, 明日) にして list を聞く。--to の日は含まない([半開区間)ので、これで「今日」がちょうど 1 日になる:

$ kairos list --from 2026-09-30 --to 2026-10-01 --json month-end.kairos
{
  "command": "list",
  "version": "1.0.1",
  "tz": "Asia/Tokyo",
  "from": "2026-09-30",
  "to": "2026-10-01",
  "results": [
    {
      "source": "",
      "dates": [
        "2026-09-30"
      ],
      "points": [
        1790694000000
      ],
      "annotations": []
    }
  ],
  "coverage": [
    {
      "source": "JP.national",
      "covering": "2026-08-01..2027-01-31",
      "concluded": false,
      "runwayDays": 123
    }
  ],
  "warnings": []
}

dates が壁時計、points が同じ点の epoch ミリ秒である。人間向けの出力には上の # 行が混ざるので、機械が判定するときは --json を付けて dates の長さを数える。前日の 9/29 で聞くと"dates": [] になる。この「1 か 0 か」が、以下すべての判定である。

3. 型 1——cron を時計として残し、判定だけ Kairos に移す

crontab の行は 1 本だけ残す。曜日も日付も書かない。時刻だけ cron が持つ:

55 23 * * *  cd /srv/batch && ./run-if-today.sh month-end.kairos ./close-books.sh

毎晩 23:55 に「今日は点があるか」を聞き、あればジョブを起動する。判定する run-if-today.sh は引数 2 つ(定義ファイルとジョブ)を取る 6 行のスクリプトである:

#!/bin/sh
export TZ=Asia/Tokyo                              # 定義の premise tz と揃える(§4)
today=${TODAY:-$(date +%F)}; tomorrow=$(date -d "$today + 1 day" +%F)   # GNU date。macOS は date -v+1d +%F
n=$(kairos list --from "$today" --to "$tomorrow" --json "$1" | jq '.results[0].dates | length')
[ "$n" -gt 0 ] || exit 0
exec "$2"

TODAY を外から与えられるようにしてあるのは、本番の日を待たずにリハーサルするためである。ジョブの側は close-books.sh(中身は何でもよい。ここでは 1 行 echo)として、月末の前後で4 日分を手で叩く:

$ TODAY=2026-09-29 ./run-if-today.sh month-end.kairos ./close-books.sh
$ TODAY=2026-09-30 ./run-if-today.sh month-end.kairos ./close-books.sh
close-books: 2026-09-18 10:37:14 月次締め処理を開始
$ TODAY=2026-10-30 ./run-if-today.sh month-end.kairos ./close-books.sh
close-books: 2026-09-18 10:37:15 月次締め処理を開始
$ TODAY=2026-10-31 ./run-if-today.sh month-end.kairos ./close-books.sh

9/29 と 10/31(土)は黙って終わり、9/30 と 10/30(金)だけジョブが走った。祝日を足すのも表を伸ばすのも定義ファイルの側で、crontab とスクリプトには触らない。cron は時計、Kairos は暦、ジョブは仕事——三つが分かれていて、それぞれ 1 か所にある。

4. 落とし穴——サーバが UTC だったら

上のスクリプトの 2 行目、export TZ=Asia/Tokyo を消して、UTC のサーバで同じリハーサルをしてみる:

$ TZ=UTC TODAY=2026-09-29 ./run-if-today.sh month-end.kairos ./close-books.sh
close-books: 2026-09-18 01:42:00 月次締め処理を開始
$ TZ=UTC TODAY=2026-09-30 ./run-if-today.sh month-end.kairos ./close-books.sh

9/29 に走って、9/30 に走らない。何が起きたかは next を UTC で聞くと見える:

$ TZ=UTC kairos next --from 2026-09-24 month-end.kairos
2026-09-29T15:00
# 被覆サマリ
#   JP.national covering 2026-08-01..2027-01-31 残走路 124 日

東京の「9 月 30 日」は、UTC の時計では 9/29 の 15:00 に始まる。CLI は機械の tz でラベルを書くのでそう印字し(日付だけの点に時刻が付いているのがその印である)、UTC の「9 月 29 日」の窓に入る。cron は UTC の 9/29 23:55——東京では 9/30 の朝 8:55 ——に判定 1 を得て、締め処理をその日の取引が始まる前に走らせる。エラーは出ない。15 時間早いだけである。音を立てずにずれる——黙りの既定の典型である。

直し方は --tz Asia/Tokyo を付けることでも、TZ を揃えることでもよい。スクリプトではdate +%F の「今日」も同じ tz で読ませたいので、先頭で TZ を揃えるのが簡単である(上の 6 行はそうしてある)。定義に tz: "Asia/Tokyo" と書いてあるのは、まさにこの「どこの9 月 30 日か」をコードに覚えさせるためだった。動かす側も、同じ答えを持っていなければならない。

5. 型 2——次の 1 点を予約する

cron を残さない形もある。定義の最後の行を bizDay |> within(month) |> last |> at(T23:55) にして時刻も定義側に置き、next次の 1 点 を取り、OS の一回限りのタイマーに渡す:

$ kairos next --json month-end.kairos | jq -c '.results[0] | {dates, points}'
{"dates":["2026-09-30T23:55"],"points":[1790780100000]}

壁時計(機械の tz)と epoch ミリ秒の両方が返る。systemd なら --on-calendar に渡す形に直すだけである:

t=$(kairos next --json month-end.kairos | jq -r '.results[0].dates[0]')   # 2026-09-30T23:55
systemd-run --user --on-calendar="$(echo "$t" | tr T ' '):00" --unit=close-books-next /srv/batch/close-books.sh

渡す前に書式を確かめるなら systemd-analyze calendar が読み下してくれる:

$ systemd-analyze calendar "2026-09-30 23:55:00"
Normalized form: 2026-09-30 23:55:00
    Next elapse: Wed 2026-09-30 23:55:00 JST
       (in UTC): Wed 2026-09-30 14:55:00 UTC

Linux の at(1) なら at -t 202609302355、Windows のタスク スケジューラならschtasks /create /tn close-books-next /tr C:\batch\close-books.cmd /sc once /sd 2026/09/30 /st 23:55——同じ 1 点を渡すだけで、/sd の日付書式は OS の地域設定(短い日付形式)に従う。

この型では、ジョブの最後で次の 1 点を登録し直すclose-books.sh の末尾に上の 2 行を置けば、9/30 に走ったジョブが 10/30 を予約して終わる。仕様が「点の列を定期的に取り直す」と言っている運用(spec §7.8)は、これのことである。

6. 止まっていたあいだに何が飛んだか

どちらの型でも、サーバが止まれば点は飛ぶ。型 1 なら cron が起きなかった日、型 2 なら予約が消えた時刻。復旧したら、止まっていた窓で list を聞く:

$ kairos list --from 2026-10-20 --to 2026-11-05 month-end.kairos
2026-10-30T23:55
# 被覆サマリ
#   JP.national covering 2026-08-01..2027-01-31 残走路 88 日

10/30 の締め処理を撃ち漏らしていたことが、列挙で分かる。cron にこの口は無い(第 8 弾)。判定用のスクリプトが --from/--to を受け取る形になっているのは、この復旧の窓と同じ口を使うためでもある。

7. どちらの型を選ぶか

  • 型 1(cron を時計に残す): 既存の crontab に 1 行足すだけ・毎日 1 回 CLI が走る・時刻は cron 側。 まず試すならこちら。
  • 型 2(次の 1 点を予約): 時刻まで定義側に置ける・CLI が走るのはジョブの直後だけ・予約の消失に 備えて §6 の窓を復旧手順に入れる。定義の at(…) を実行時刻の正本にしたいときはこちら。

どちらも Kairos が答えるのは「いつ動くべきか」の集合までで、起動・リトライ・記録は実行系の仕事である(仕様に明文がある。spec §7.8)。分業の線を動かさないことが、この 6 行を 6 行のままにしている。

まとめ

  • 定義 1 ファイル: bizDay |> within(month) |> last——祝日も tz もここにある
  • crontab 1 行: 55 23 * * * ./run-if-today.sh month-end.kairos ./close-books.sh——時刻だけ
  • シェル 6 行: [今日, 明日) の窓を --json で数え、1 なら exec——TODAY= でリハーサル
  • サーバが UTC なら TZ を定義と揃える。揃えないと月末の締めが東京の朝 8:55 に走る
  • 止まったら、止まっていた窓を list で聞く

同じ 2 つの型は、言語サイトの cron レシピの「動かす」節にもある(日本語英語)。

▶ Playground で定義を実行する——時刻つきの形(型 2)で、2027 年 1 月末(1/29 金)まで 5 点が出る。祝日を足したり窓を動かしたりして試せる。ブラウザ内で完結し、何も送信しない。


Kairos はスケジュール定義言語です。仕様と参照実装はGitHub で公開しています。

コメント

タイトルとURLをコピーしました