Skip to content

Commit c7923c0

Browse files
committed
add queue.snapshot() for live in-flight tasks and group breakdowns
1 parent 348fdfa commit c7923c0

2 files changed

Lines changed: 89 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@prsm/queue",
3-
"version": "3.2.2",
3+
"version": "3.3.0",
44
"description": "Redis-backed distributed task queue with grouped concurrency, retries, and rate limiting",
55
"type": "module",
66
"exports": {

src/queue.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,10 @@ export default class Queue extends EventEmitter {
9494
this._workerClients = []
9595
this._cleanupTimer = null
9696
this._inFlight = 0
97+
this._defaultInFlight = 0
9798
this._pushed = 0
9899
this._totalSettled = 0
100+
this._inFlightTasks = new Map()
99101
this._closed = false
100102
this._localSemaphore = new LocalSemaphore(this._options.concurrency)
101103
this._activeLeases = new Set()
@@ -126,6 +128,86 @@ export default class Queue extends EventEmitter {
126128
return this._inFlight
127129
}
128130

131+
/**
132+
* Point-in-time snapshot of this instance's state and Redis-backed depth counts.
133+
* Safe to call from observability/devtools - hits Redis with LLEN per known group.
134+
* @returns {Promise<{
135+
* options: object,
136+
* inFlight: number,
137+
* defaultInFlight: number,
138+
* pushed: number,
139+
* settled: number,
140+
* defaultDepth: number,
141+
* workers: { default: number, group: number },
142+
* groups: Array<{ name: string, inFlight: number, depth: number, workers: number }>,
143+
* inFlightTasks: Array<{ uuid: string, group: string|null, attempts: number, startedAt: number, workerId: string, payload: any }>,
144+
* }>}
145+
*/
146+
async snapshot({ includePayload = false } = {}) {
147+
const opts = this._options
148+
const groupNames = new Set([
149+
...this._groupWorkers.keys(),
150+
...this._groupInFlight.keys(),
151+
])
152+
153+
let defaultDepth = 0
154+
const groupDepths = new Map()
155+
if (this._redis?.isOpen) {
156+
try {
157+
defaultDepth = await this._redis.lLen('queue:tasks')
158+
} catch {}
159+
for (const name of groupNames) {
160+
try {
161+
groupDepths.set(name, await this._redis.lLen(`queue:groups:${name}`))
162+
} catch {
163+
groupDepths.set(name, 0)
164+
}
165+
}
166+
}
167+
168+
const groups = Array.from(groupNames).map((name) => ({
169+
name,
170+
inFlight: this._groupInFlight.get(name) || 0,
171+
depth: groupDepths.get(name) || 0,
172+
workers: this._groupWorkers.get(name)?.size || 0,
173+
})).sort((a, b) => a.name.localeCompare(b.name))
174+
175+
const inFlightTasks = []
176+
for (const [uuid, entry] of this._inFlightTasks) {
177+
inFlightTasks.push({
178+
uuid,
179+
group: entry.group,
180+
attempts: entry.task.attempts,
181+
startedAt: entry.startedAt,
182+
workerId: entry.workerId,
183+
payload: includePayload ? entry.task.payload : undefined,
184+
})
185+
}
186+
inFlightTasks.sort((a, b) => a.startedAt - b.startedAt)
187+
188+
let groupWorkerTotal = 0
189+
for (const m of this._groupWorkers.values()) groupWorkerTotal += m.size
190+
191+
return {
192+
options: {
193+
concurrency: opts.concurrency,
194+
globalConcurrency: opts.globalConcurrency,
195+
delay: opts.delay,
196+
timeout: opts.timeout,
197+
maxRetries: opts.maxRetries,
198+
groups: { ...opts.groups },
199+
},
200+
inFlight: this._inFlight,
201+
defaultInFlight: this._defaultInFlight,
202+
pushed: this._pushed,
203+
settled: this._totalSettled,
204+
defaultDepth,
205+
workers: { default: this._workers.size, group: groupWorkerTotal },
206+
groups,
207+
inFlightTasks,
208+
}
209+
}
210+
129211
/** @param {TaskHandler} handler */
130212
process(handler) {
131213
this._handler = handler
@@ -430,15 +512,21 @@ export default class Queue extends EventEmitter {
430512
this._inFlight++
431513
if (opts.group) {
432514
this._groupInFlight.set(opts.group, (this._groupInFlight.get(opts.group) || 0) + 1)
515+
} else {
516+
this._defaultInFlight++
433517
}
518+
this._inFlightTasks.set(task.uuid, { task, startedAt: Date.now(), workerId, group: opts.group ?? null })
434519

435520
try {
436521
await this._processTask(task, opts)
437522
} finally {
523+
this._inFlightTasks.delete(task.uuid)
438524
if (opts.group) {
439525
const count = (this._groupInFlight.get(opts.group) || 1) - 1
440526
if (count <= 0) this._groupInFlight.delete(opts.group)
441527
else this._groupInFlight.set(opts.group, count)
528+
} else {
529+
this._defaultInFlight = Math.max(0, this._defaultInFlight - 1)
442530
}
443531
if (leaseId) await this._releaseGlobal(leaseId).catch(() => {})
444532
}

0 commit comments

Comments
 (0)