複数のスケジューラ窓を持つ自動化では、「各ワーカーが正しく動く」だけでは足りません。
今回起きたのは、Qiita向けの記事カードが同じ当日枠に積まれ、媒体側の1日上限と衝突した問題です。カードには次のように記録されていました。
配信2段の(1)content【09-03ぶん・当日枠は既に2枚重複しているので翌日枠に置く】。
対象カードは AUTO-T5-CT-QIITA-D0903-20260902-am です。カード自身の目的にも、Qiita向け技術記事を1本作ること、ただし「このカードでは投稿しない」ことが明記されていました。
"id": "AUTO-T5-CT-QIITA-D0903-20260902-am",
"deliverable": "qiita-posts.md",
"lane": "qiita-jp"
同時に、投稿上限の正本である posting-limits.json では Qiita の上限がこう定義されています。
"qiita": { "per_day": 1 }
つまり、当日枠に複数のQiita投稿ジョブが積まれた時点で、後続ジョブは「投稿処理に入ってから失敗する」のではなく、「起動前にスケジューリング対象から外す」べきでした。
事象
published ledger を見ると、2026-09-02にはすでにQiitaの公開実績があります。
published_at=2026-09-02
title=自動化バグで参照パスが壊れた話: ID生成とregexの境界をそろえる
source=AUTO-W3-PUB-QIITA-20260902-pm
その公開ログには、投稿前に posting-limits.json qiita.per_day=1 を確認したことも残っています。
posting-limits.json qiita.per_day=1・published-ledger実読で本日(2026-09-02 JST)のqiita先行公開0件を確認してから投稿。
この時点では、そのジョブ単体の判断としては成立しています。問題は、別のスケジューラ窓も同じ日付の枠にQiitaカードを積める構造になっていたことです。
AUTO-T5-CT-QIITA-D0903-20260902-am は、その衝突を受けて「09-03ぶん」として翌日枠へ送られました。ここで重要なのは、投稿ワーカーではなく、起票側で日次上限を満たす必要があった点です。
再現条件
再現条件はシンプルです。
- 媒体ごとの日次上限がある
- 複数のスケジューラ窓が同じ媒体・同じ日付のカードを起こせる
- 各窓が、他の窓で起票済みのカードを見ない
- 公開済み ledger だけを見て「まだ投稿されていない」と判断する
Qiitaの場合、上限は per_day=1 です。
"qiita": { "per_day": 1 }
一方で、配信レーンの正本には、Qiitaは稼働中媒体として扱われています。
| **Qiita** | @SciCos | ✅ 稼働 | 技術ストック・信頼蓄積→送客 |
さらに、1日の回し方として「稼働中の媒体は毎日カードを起こす」とあります。
稼働中(✅)の媒体は毎日カードを起こす
この設計自体は自然です。問題は「毎日カードを起こす」処理が複数窓から実行されるとき、日付・媒体・上限の判定が窓ごとに閉じていたことです。
原因
原因は、公開済み ledger と、起票予定のキューを同じ在庫として扱えていなかったことです。
公開ワーカーは公開前に ledger を読み、当日分の実績を確認していました。たとえば直近のQiita公開ログには、qiita.per_day=1 の確認が残っています。
これは公開直前のガードとして有効です。
ただし、スケジューラの問題はもっと手前にあります。まだ公開されていないカードは ledger に出ません。複数の窓が「ledger上は今日のQiitaがまだ空いている」と同時に判断すると、同じ qiita-jp の当日枠へ複数カードを積めます。
この状態では、公開直前の per_day チェックだけでは遅くなります。後続カードは起動前に死ぬか、公開レーンで止まり、contentレーン側には「作ったが流れない在庫」が残ります。
修正
決定論ガードを置く位置は、投稿処理の直前だけではなく、起票時です。
今回のカードでは、当日枠に重複があることを理由に、Qiita記事の制作対象を翌日枠へ移しています。
09-03ぶん・当日枠は既に2枚重複しているので翌日枠に置く
この判断を人間の読み取りやカード文面に閉じ込めず、スケジューラ側で機械的に行うのが修正方針です。
具体的には、起票前に次のキーで既存の公開実績と未実行カードを合算します。
platform = qiita
account = @SciCos
date = 2026-09-02
limit = posting-limits.json.platforms.qiita.per_day
その合計が per_day に達していれば、同じ日付の新規カードは起こさず、次の空き日へ送ります。
擬似コードにするとこうです。
const limit = postingLimits.platforms.qiita.per_day;
const used = countPublished("qiita", account, date);
const reserved = countQueued("qiita", account, date);
if (used + reserved >= limit) {
scheduleForNextOpenDate(card);
return;
}
enqueue(card);
ポイントは、published だけでなく queued も数えることです。公開済みの数だけを見ると、まだ実行前のカード同士が互いに見えません。
また、このガードはワーカー内部の任意チェックではなく、スケジューラの起票境界に置きます。公開ワーカーまで来た時点で止める設計だと、content生成やカード発行はすでに走ってしまうからです。
学び
日次上限は、投稿APIを叩く直前の安全装置だけではありません。分散スケジューラでは、起票時点の排他条件でもあります。
今回のQiitaレーンでは、上限は明確でした。
"qiita": { "per_day": 1 }
そして、対象カードにも「当日枠は既に2枚重複しているので翌日枠に置く」と残っていました。
この2つを合わせると、見るべき不変条件ははっきりします。
同じ platform/account/date に対して、published + queued <= per_day
公開済み ledger は事後の正本として必要です。ただ、スケジューラの冪等性を作るには、未実行の予約カードも同じ枠の消費として扱う必要があります。
「投稿時に止める」だけでは、上限違反そのものは防げても、重複起票は防げません。
「起票時に同じキーで数える」ことで、複数窓が同時に動いても、同じ日付の同じQiita枠にカードを積みすぎない設計にできます。