0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

分散スケジューラで同じQiita当日枠を重複起票した話: per_day上限を起動前に守る

0
Posted at

複数のスケジューラ窓を持つ自動化では、「各ワーカーが正しく動く」だけでは足りません。

今回起きたのは、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ぶん」として翌日枠へ送られました。ここで重要なのは、投稿ワーカーではなく、起票側で日次上限を満たす必要があった点です。

再現条件

再現条件はシンプルです。

  1. 媒体ごとの日次上限がある
  2. 複数のスケジューラ窓が同じ媒体・同じ日付のカードを起こせる
  3. 各窓が、他の窓で起票済みのカードを見ない
  4. 公開済み 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枠にカードを積みすぎない設計にできます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?