crontabにGoを動かす処理を登録してみたが動かず
普段業務ではタスクスケジューラにcrontabを使用している。そのためGoでバッチ処理を管理するときも当然のようにcrontabで管理するものだと思っていたが、なぜかcrontabが動作せず、エラーログも残らないという現象に直面。
調べてみても、Goをcrontabで実行しようとする人がおらず、代わりにGoのタスクスケジュールライブラリの紹介記事をいくつか発見したため、こちらで実装してみることにした。
gocronを使用してタスクスケジューリングを行う
Goのタスクスケジュールライブラリはいくつか存在するようだが、
こちらで無限ループなどにして待ち状態を実装する必要がなさそうだったため、今回はgocronを採用。
Goのバージョンは1.17.6
package main
import (
"fmt"
"path/filepath"
//"github.com/jasonlvhit/gocron"
"github.com/go-co-op/gocron"
"github.com/joho/godotenv"
)
func funcA() {
fmt.Println("A")
}
func funcB() {
fmt.Println("B")
}
func funcC() {
fmt.Println("C")
}
func main() {
err := godotenv.Load(filepath.Join("path", "to", "dotenv", ".env"))
if err != nil {
panic("can't get .env")
}
// gocron.Every(1).Day().At("00:40").Do(funcA)
// gocron.Every(1).Saturday().At("11:15").Do(funcB)
// gocron.Every(30).Minutes().Do(funcC)
// <-gocron.Start()
scheduler := gocron.NewScheduler(time.Local)
scheduler.Every(1).Day().At("00:40").Do(funcA) // 1日1回0時40分に
scheduler.Every(2).Saturday().At("11:15").Do(funcB)// 2週間に1回土曜日の11:15に
scheduler.Every(30).Minutes().Do(funcC) // 30分に1回
scheduler.StartBlocking()
}
冒頭のgodotenvについては後述。このサンプルコードの時点では不要。
gocronで始まる箇所でタスクを登録する。
Everyの中の数字はintervalとして管理されており、Dayであればinterval * Hour * 24と計算される。
つまりgocron.Every(2).Days().~ と記述すれば2日に1回処理を実行できる。
ただし、後ろに続く関数では内部でintervalが1であることが前提の
チェックが行われているため
intervalに2以上を使用する場合は、それぞれ対応している複数形の関数を使用する必要がある
(例)
Day()-> Days()
Minute() -> Minutes()
github.com/jasonlvhit/gocronパッケージはforkされ
github.com/go-co-op/gocronパッケージとしてメンテナンスされているらしい。
こちらは、以前の名残でDay()とDays()などは両方ありますが、どちらも同じ動きをする。
最後のgocron.Start()をchannel送信することで内部でboolの値を送信し続けてくれるため
これだけでタスクスケジューラの実装が完了してしまう。
github.com/go-co-op/gocronパッケージでは、タスクの登録は全てScheduler型が担っていて、channnel送信も関数で内部化された模様。
これから使うならこちらを推奨。github.com/go-co-op/gocron
作成したタスクスケジューラをデーモン化
サンプルコードを実行するとわかると思うが、この実装は常にプログラムを実行しておかなければならない。
そこで、バイナリにコンパイルしたものを使いデーモン化を試みる。
まずはバイナリを作成する。
go build -o go_cron main.go
次にデーモン化だが、今回はツールを使用せずCentOSのsystemdにserviceを追加する。
cd /etc/systemd/system
vi go_cron.service
[Unit]
Description=golang cron tool
ConditionPathExists=/path/to/go_cron
[Service]
User=root
Type=simple
ExecStart=/path/to/go_cron/go_cron
Restart=on-failure
TimeoutStartSec=300
[Install]
WantedBy=/path/to/wantedby
ExecStartに先ほど作成したバイナリを設定する。
個人的にハマった箇所としては以下
- Restart=on-failureをRestart=noに設定してしまうとpanicなどでgoがクラッシュしたときにサービスごと死んでしまった。on-failureにすることでサービスは立ち上がったままでいてくれる。
- WantedByを設定しないとstaticなサービスとなる。これを設定することでsystemctl enableをすることができるようになる。
.serviceファイルを書いたら、systemdに変更を反映させる
systemctl daemon-reload
サービス立ち上げ
systemctl start go_cron.service
# ステータス確認
systemctl status go_cron.service
go_cron.service - golang cron tool
Loaded: loaded (/etc/systemd/system/go_cron.service; disabled; vendor preset: disabled)
Active: active (running) since 火 2022-03-01 13:15:56 JST; 2h 27min ago
Goのソースコードで環境変数を使う箇所がある場合
いざサービスが立ち上がった!とジョブが開始されるのを待っていたが動かない。
こういう時はjournalctlというコマンドでログが見れるらしい。
sudo journalctl | grep go_cron
ログを見るとGoの処理が内部でpanicを起こしていた。
もしやと思い環境変数を使う箇所を出力してみると、案の定空欄が出力された。
そこでgodotenvパッケージを利用して.envファイルの環境変数を事前に取り込んだところ、無事に動作した。冒頭のmain.goのソースの最初の3行はそういった経緯である。]
まとめ
普段Dockerで曖昧にgoを動かしているので、buildしてバイナリを作って運用するといつもと違うケースで悩むことがあって勉強になった。
次はデーモン化ツールを使ってみようと思う。
あと、Saturday()などの曜日指定は週に1回のジョブ呼び出しにしか対応していないようなのでissue提案して2週間に1回土曜日とかもできるようにとかも考えてみようかな・・・
issue投げようとしたらリポジトリごと移行していたことが発覚したのでその旨を記事の前半に書き加えました。