Cloud Run jobs の安全なキャンセル・タイムアウト処理【検証付き】

2024/07/11に公開

はじめに

こんにちは 3-shake のとあるチームでお仕事しているエンジニアです。
記事執筆時点でCloud Run serviceのGraceful Shutdownについての情報は簡単に見つかるのですが、Cloud Run jobsのキャンセル・タイムアウト時にGraceful Shutdownを行う方法について、公式ドキュメントにはっきりとは記載されていなかったため自分で検証して挙動を確かめました。
(最終的な結論を知りたい方は まとめ のセクションをお読みください)

jobsのキャンセル・タイムアウト

皆さんCloud Run jobs使われていますか?

私は仕事上でもプライベートでも触る機会が多いのですが、Cloud Monitoringと良く統合されておりバッチ処理等のモニタリングが比較的楽に出来たり、1ジョブを10000並列までスケーリング出来る性能だったりとても便利なサービスだと思っています。

ところでCloud Run jobsの実行中に処理を停止したくなった場合はキャンセル機能が利用できます。
(以下のように管理画面からキャンセルできるだけでなく、CLIやAPI経由でもキャンセルできます)

また手動のキャンセル機能を使わなくとも、以下のように予めタイムアウト時間を設定しておくことで、指定時間を超えたらジョブをタイムアウトさせることができます。

# 30秒でタイムアウトさせる場合
gcloud run jobs create JOB_NAME --image IMAGE_URL --task-timeout 30s

Cloud Run 「service」のGraceful Shutdown

Cloud Run jobsに対して、同じCloud Run系サービスにウェブサーバー等の用途に特化したCloud Run 「service」というものがあります(こちらの方がjobsより先にリリースされている)

公式ドキュメントにはCloud Run serviceは以下のような利用を想定しているようです。

ウェブ リクエストまたはイベントに応答するコードの実行に使用します。
https://cloud.google.com/run/docs/overview/what-is-cloud-run

少し話が変わりますが、WebサーバにはGraceful Shutdownという処理があります。
これはWebサーバの文脈では新規リクエスト受付停止して、処理中のリクエストが全て完了してからプログラムを終了させるといった処理をイメージしておけば概ね間違い無いかと思います。

Cloud Run serviceでGraceful Shutdownを行う方法について公式には以下のように記載されています。

Cloud Run は、スケールダウンやリビジョンの削除などのイベントによってコンテナ インスタンスが終了される前に、SIGTERM シグナルをコンテナ インスタンスに送信します。このシグナルを扱うことで、コンテナの急激なシャットダウンを避け、アプリケーションを正常に終了してクリーンアップ タスクを実行できるようになりました。
https://cloud.google.com/run/docs/samples/cloudrun-sigterm-handler

つまり、SIGTERMを処理すれば良いようですね。
またGraceful Shutdownを行う一例として以下のようなサンプルコードが記載されています。

// Clean up resources on shutdown
process.on('SIGTERM', () => {
  logger.info(`${pkg.name}: received SIGTERM`);
  closeConnection();
  logger.end();
  logger.on('finish', () => {
    console.log(`${pkg.name}: logs flushed`);
    process.exit(0);
  });
});

jobsの挙動を確かめる

冒頭でも記載したように、Cloud Run jobsのキャンセル・タイムアウト時にGraceful Shutdownを行う方法について、公式ドキュメントにはっきりとは記載されていなかったため自分で以下のコードをデプロイした上で、実際にキャンセル処理やタイムアウトを発生させて確かめました。

// 任意の X 秒間 Sleep させる処理
const sleepSec = (sec: number) =>
  new Promise((resolve) => {
    setTimeout(resolve, sec * 1000)
  })

// 30秒の間、毎秒1回、経過秒数をカウントする処理
const count30sec = async () => {
  for (let i = 0; i < 30; i++) {
    console.log(i)
    await sleepSec(1)
  }
}

// SIGINTシグナルを受け取った際に30秒間カウントする処理を実行する
process.on('SIGINT', () => {
  console.log('SIGINT')
  count30sec()
})

// SIGTERMシグナルを受け取った際に30秒間カウントする処理を実行する
process.on('SIGTERM', () => {
  console.log('SIGTERM')
  count30sec()
})

const main = async () => {
  console.log('job started')
  await sleepSec(100) // 100秒間Sleepする(jobを終わらせない)
}

main()

検証結果: キャンセル

以下のように、SIGTERMが送信され、そこから10秒程待ってから停止するようです。

Timeout

こちらも以下のように、SIGTERMが送信され、そこから10秒程待ってから停止するようです。
(タイムアウトを30秒に指定して実行しました)

まとめ

Cloud Run jobsはキャンセル時もタイムアウト時もSIGTERMが送信され、そこから10秒程待ってから停止するようです。

Cloud Run jobsでバッチ処理などを実装する際は、この10秒間の間に実行途中の処理を安全に終わらせたり、DBコネクションを切断するなどの終了処理を記載しましょう。

Discussion