動作の仕組み
chime は 1 回きりの cron ジョブではなく,常駐プロセスです.メインループは次のとおりです.
tick_interval_secごとに tick します (MissedTickBehavior::Skip— 遅延した tick はまとめて 1 つにされ,再生されません).- 設定されたタイムゾーンでの現在のローカル時刻を計算します.
- 各リマインダーについて,現在の時と分が一致し,かつ今日がそのスケジュール (
days(曜日) のいずれか,またはday_of_month(当月の日付) のいずれか) に合致する場合に発火します. - 分単位の重複排除: 各リマインダーは,tick 間隔が 60 秒より短くても,一致した分ごとに最大 1 回だけ発火します (例えば
tick_interval_sec = 30でも,スケジュールされた分ごとに POST はちょうど 1 回です).重複排除の記録は HTTP リクエストの前に更新されるため,送信に失敗しても同じ分内で再試行されることはありません. - ステータスページ,watch,フィードのうち最大 1 つをポーリングします.
poll_interval_secが経過しているもののうち最も遅延しているものが対象で,未確認のインシデント更新,バージョン,エントリを転送します. - SIGINT と SIGTERM はどちらも clean shutdown を引き起こします.tick の途中でもすぐに停止します.実行中のポーリングや投稿は中断されます.既読の記録はすでに書き込まれているため,再起動後に送られることもありません.
ポーリング
ステータスページのポーリングはリマインダーと同じルールに従います.
- 起動後の最初のポーリングは通知しません. どのドキュメントにも既知のインシデントが含まれています (Statuspage の
incidents.jsonは直近 50 件,Datadog のconfig.jsonは履歴全体,Instatus はアクティブなものと最近解決されたもの).そのため chime はそれらをベースラインとして記録し,何も報告しません.その後に変化したものだけが転送されます.したがって再起動はベースラインを取り直すだけで,履歴がチャンネルに再送されることはありません.これは「見逃した分は見逃した通知」という考え方と同じ趣旨です. - 既読の記録は Discord へのリクエストの前に書き込まれるため,送信に失敗しても次のポーリングで再試行されません.
- 1 回のポーリングで複数のメッセージを投稿することがあります (インシデント更新の連続や,一度に多数のエントリを公開したフィードなど).chime は Discord の rate limit ヘッダを読み,webhook のバケットを使い切ったら,バケットがリセットされるまで (最大 5 秒) その webhook への次の投稿を待たせます.そのため 6 通目以降が
429で失敗せず,すべて届きます.他の webhook への投稿は待たされません. - ステータスページに到達できない場合は
warnでログに残り,そのページ自身の間隔で再試行されます.chime が自身のポーリングの失敗について投稿することはありません. - リダイレクトは追従しません.
3xxを返すページ,watch,フィードは,他の失敗と同じくポーリングのたびにHTTP 301(など) としてwarnでログに残ります.リダイレクト先の最終 URL を設定してください.Locationはポーリング先のサーバが決めるため,追従すると,そのサーバが chime のリクエストを内部ホストを含む chime から届く任意のホストへ向けられてしまいます. - ポーリングは tick 上で行われるため,更新はページが公開してから最大
poll_interval_sec後に転送されます.embed のタイムスタンプには,ドキュメントが公開時刻を持っている場合は実際の公開時刻が表示されます. - tick ごとにポーリングされるページ,watch,フィードは 1 つです.そのため設定数にかかわらず 1 tick のコストはリクエスト 1 回で,到達できないエンドポイントが多数あっても,
chime healthが heartbeat を古いと判定するほどループが止まることはありません.各エントリはその予算のtick_interval_sec / poll_interval_secを占めます.tick_interval_sec = 60なら,300 のページは 5 分の 1,900 のフィードは 15 分の 1,3600 の watch は 60 分の 1 です.すべてのページ,watch,フィードの占有率の合計が 1 以下である限り,それぞれは本来の間隔を保ちます.それを超えると,単にポーリング頻度が下がります.URL を共有する watch は,共有する間隔で 1 回として数えます. - リクエストは条件付き (
If-None-Match) かつ圧縮 (Accept-Encoding: gzip) されるため,更新のないページは通常,本文のない 304 で済みます.ETagを返さないインスタンス (Instatus と incident.io のページ,および他のインフラで提供される一部の Statuspage 互換フィード) は検証できないため,ポーリングのたびにフィード全体をダウンロードします.Chrome の watch と大半の RSS フィードも同様です.gzip によりそれは数 KB に収まるので,他に必要な対策はありません.
ポーラーの生存確認
デーモンは 1 時間に 1 回,info で status poll summary を 1 行ログに出力します.
{"timestamp":"2026-06-05T09:00:00.000000Z","level":"INFO","fields":{"message":"status poll summary","window_sec":3600,"pages":5,"polls":60,"not_modified":55,"updated":4,"failed":1,"forwarded":3,"send_failed":1},"target":"chime::scheduler"}これがないと,正常に動いているポーラーは何も出力しません.更新のないページは 304 を返し,その経路は debug でしかログに残らず,静かなステータスページは何日もインシデントなしで過ぎることがあります.このサマリーにより,コンテナのネットワークカウンタを読まなくても,「何も起きていない」ことと「ポーラーが死んでいる」ことを区別できます.
not_modifiedとupdatedは HTTP の結果 (304 と 200) であり,動いたインシデントの数ではありません.そのためETagを返さないインスタンスは,フィードが変わっていなくてもすべてのポーリングをupdatedと報告します.failedはウィンドウ内の fetch 失敗の数です (それぞれ発生時にwarnでもログに残ります).forwardedは成功した Discord への投稿の数,send_failedは Discord に拒否された投稿の数です (それぞれerrorでもログに残ります).send_failedが 0 でなければ,それは更新のないページではなく,webhook が投稿を受け付けなくなったことを意味します.
ポーリングごとの詳細を見るには,log_level = "debug" を設定してください.
watch には同じウィンドウで独自の watch poll summary 行があり,pages が watches (設定された数) になる点を除いてフィールドは同じです.
{"timestamp":"2026-06-05T09:00:00.000000Z","level":"INFO","fields":{"message":"watch poll summary","window_sec":3600,"watches":2,"polls":2,"not_modified":1,"updated":1,"failed":0,"forwarded":0,"send_failed":0},"target":"chime::scheduler"}ステータスページと同様に,polls,not_modified,updated はリクエストを数えます (URL を共有する watch は 1 リクエストです).また updated は 200 レスポンスの数であり,変化したバージョンの数ではないため,Chrome の watch はすべてのポーリングを updated と報告します.failed には,本文は取得できたものの,その上の watch の少なくとも 1 つが読み取れなかった場合も含まれます.読み取れた watch の投稿は,引き続き forwarded / send_failed に数えられます.
RSS フィードには 3 つ目の行 rss poll summary があり,pages の代わりに feeds (設定された数) を持ちます.
{"timestamp":"2026-06-05T09:00:00.000000Z","level":"INFO","fields":{"message":"rss poll summary","window_sec":3600,"feeds":1,"polls":4,"not_modified":0,"updated":4,"failed":0,"forwarded":1,"send_failed":0},"target":"chime::scheduler"}ETag を返さないフィードはすべてのポーリングを updated と報告し,failed には,取得できたもののフィードではない本文も含まれます.各行は,そのリストが空の場合は出力されません.
Docker を使わない場合の注意
- chime は自分自身を daemonize せず,PID ファイルを書かず,fork もしません.クラッシュや終了時に再起動するスーパーバイザ (
systemd,launchd,runitなど) の下で実行してください. - プロセスはシングルスレッド (
tokioの current-thread ランタイム) です.動かしっぱなしにしても負荷は小さいです. - 見逃した分は見逃した通知であり,catch-up はありません.09:30 にホストがスリープしていた場合,復帰しても 09:30 のリマインダーは発火しません.これは cron 的な考え方と一致します.
- ログは stdout への行区切り JSON です.スーパーバイザが提供する手段 (
journalctl -u chime,コンテナのログドライバなど) で取得してください.