Reaper startup scan misses stalled jobs once the worker's registration expires
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- node.js, postgresql, redis, typescript
調査の方向性
Start with src/consumer.ts around the registerWorker call at line 110, then read refreshWorker implementations and the Reaper's #checkStalledJobs startup scan. Use the supplied reproduction as the initial failing case. Done means live consumers remain visible while running and a restarted Reaper can recover processing jobs even after their worker registration expires.
索引モデルが issue の本文から書いたものです。
説明
A Reaper's startup scan only looks at workers returned by storage.getWorkers(), but consumers register their worker once, with a TTL of visibilityTimeout * 2, and never refresh it. Once that TTL passes, a still-running worker drops out of the list. If a Reaper then starts (e.g. in a process restarted after a crash), it never finds that worker's in-flight job, which stays processing forever.
A Reaper that was already running when the job started isn't affected: it has a timer for the job from the processing event and recovers it. The problem is the startup scan, which is exactly what recovers jobs after a restart.
Reproduction
@platformatic/[email protected] (also on main), Node 26. A handler that never settles keeps the job in the worker's processing list, standing in for a worker process that died mid-job:
import { MemoryStorage, Queue, Reaper } from '@platformatic/job-queue'
import { setTimeout as sleep } from 'node:timers/promises'
const visibilityTimeout = 1000
const storage = new MemoryStorage()
// The handler never settles: the job stays in the worker's processing list,
// as it would if the worker process had died mid-job.
let jobStarted
const started = new Promise(resolve => { jobStarted = resolve })
const worker = new Queue({ storage, visibilityTimeout, workerId: 'worker-1' })
worker.execute(() => { jobStarted(); return new Promise(() => {}) })
await worker.start()
console.log('workers right after start:', await storage.getWorkers())
await sleep(visibilityTimeout * 2 + 500)
console.log('workers 2.5s later (worker still running):', await storage.getWorkers())
await worker.enqueue('job-1', {})
await started
// A Reaper that starts now (as it would in a restarted process) relies on its
// startup scan to find the in-flight job.
const reaper = new Reaper({ storage, visibilityTimeout })
reaper.on('stalled', id => console.log('recovered', id))
await reaper.start()
await sleep(visibilityTimeout * 3)
console.log('job-1 state:', (await worker.getStatus('job-1'))?.state)
await reaper.stop()
await worker.stop()
await storage.disconnect()
Output:
workers right after start: [ 'worker-1' ]
workers 2.5s later (worker still running): []
job-1 state: processing
Without the sleep(visibilityTimeout * 2 + 500), the same script prints recovered job-1.
I saw the same expiry with a running consumer on RedisStorage and PgStorage (listed right after start(), gone 2.5s later with visibilityTimeout: 1000). On Redis the TTL is on the whole workers hash, so another worker registering later extends it.
Cause
Consumer.start() calls registerWorker(workerId, visibilityTimeout * 2) once (consumer.ts#L110). Nothing calls refreshWorker(), which every storage implements. The Reaper's startup scan (#checkStalledJobs) only walks getWorkers().
Fix
Two parts are needed:
- Consumers refresh their registration while running, e.g. every
visibilityTimeout / 2, and stop instop(). This keeps live workers visible. - The startup scan shouldn't depend on live registrations at all. A worker that crashed stops refreshing, so its registration still expires, and if the restart happens after that, its jobs are still missed. The scan needs a way to enumerate processing lists regardless of registration (and treat unregistered owners as dead).
I'm happy to send a PR for this.
- 主要言語
- TypeScript
- スター
- 371
- フォーク
- 11
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
- Dockerfile または Docker Compose ファイルあり
- プルリクエストのテンプレートなし
- コントリビューションガイドなし
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
platformatic/job-queue のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
platformatic/job-queue#54 ·
-
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
platformatic/job-queue#53 ·
-
難易度 3/5 1〜2日 初心者へのやさしさ 78/100
platformatic/job-queue#52 ·
platformatic/job-queue の issue をすべて見る
似ている issue
-
calcite-ui-icons enhancement impact - p3 - not time sensitive needs triage
難易度 2/5 1〜3時間 初心者へのやさしさ 67/100
Esri/calcite-design-system#15467 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
diegosouzapw/OmniRoute#18390 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
-
難易度 1/5 1〜3時間 初心者へのやさしさ 86/100
guilhermerodz/input-otp#157 · コメント 1 件 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
davidjerleke/embla-carousel#1414 ·
メンテナーはふだん 4 日以内に返信