bunqueue 2.9.0 → 2.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/application/background/recovery/active.d.ts +1 -0
- package/dist/application/background/recovery/active.js +12 -0
- package/dist/application/background/recovery/index.js +3 -1
- package/dist/application/background/recovery/pending.js +1 -0
- package/dist/application/cleanupTasks.js +4 -16
- package/dist/application/contextFactory.js +3 -0
- package/dist/application/dlqManager.js +1 -0
- package/dist/application/dlqRetry.js +1 -0
- package/dist/application/eventsManager.d.ts +8 -1
- package/dist/application/eventsManager.js +76 -29
- package/dist/application/operations/ackHelpers.d.ts +1 -4
- package/dist/application/operations/ackHelpers.js +39 -14
- package/dist/application/operations/flowPush.js +1 -0
- package/dist/application/operations/flowValidation.js +4 -4
- package/dist/application/operations/pull.d.ts +7 -2
- package/dist/application/operations/pull.js +15 -57
- package/dist/application/operations/pullFinalization.d.ts +6 -0
- package/dist/application/operations/pullFinalization.js +69 -0
- package/dist/application/operations/pullStateTransition.d.ts +8 -7
- package/dist/application/operations/pullStateTransition.js +25 -18
- package/dist/application/operations/push.js +3 -0
- package/dist/application/operations/pushBatch.js +14 -7
- package/dist/application/operations/pushContext.d.ts +2 -1
- package/dist/application/postgres-queue-manager/control.d.ts +4 -0
- package/dist/application/postgres-queue-manager/control.js +24 -0
- package/dist/application/postgres-queue-manager/delivery.d.ts +5 -4
- package/dist/application/postgres-queue-manager/delivery.js +13 -10
- package/dist/application/postgres-queue-manager/groupQueries.d.ts +10 -0
- package/dist/application/postgres-queue-manager/groupQueries.js +40 -0
- package/dist/application/postgres-queue-manager/queries.d.ts +2 -2
- package/dist/application/postgres-queue-manager/queries.js +2 -2
- package/dist/application/queue-manager/control.js +1 -0
- package/dist/application/queue-manager/delivery.d.ts +5 -4
- package/dist/application/queue-manager/delivery.js +15 -8
- package/dist/application/queue-manager/limits.d.ts +11 -0
- package/dist/application/queue-manager/limits.js +61 -0
- package/dist/application/queue-manager/state.js +1 -1
- package/dist/application/queueTelemetryJournal.d.ts +2 -0
- package/dist/application/queueTelemetryJournal.js +23 -0
- package/dist/application/types/ack.d.ts +3 -1
- package/dist/cli/commandRouter.d.ts +1 -1
- package/dist/client/flowPush.js +6 -0
- package/dist/client/groupId.d.ts +1 -0
- package/dist/client/groupId.js +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/jobHelpers.js +1 -0
- package/dist/client/queue/operations/add/bulk.js +4 -4
- package/dist/client/queue/operations/add/payload.d.ts +1 -0
- package/dist/client/queue/operations/add/payload.js +7 -1
- package/dist/client/queue/operations/add/single.js +3 -3
- package/dist/client/queue/operations/groups.d.ts +21 -0
- package/dist/client/queue/operations/groups.js +88 -0
- package/dist/client/queue/runtime/queries.d.ts +13 -0
- package/dist/client/queue/runtime/queries.js +31 -0
- package/dist/client/queue/runtime/state.d.ts +4 -0
- package/dist/client/queue/runtime/state.js +1 -0
- package/dist/client/types/index.d.ts +2 -2
- package/dist/client/types/options.d.ts +4 -0
- package/dist/client/types/worker.d.ts +8 -0
- package/dist/client/worker/runtime/execution.js +1 -0
- package/dist/client/worker/runtime/options.js +3 -0
- package/dist/client/worker/types/options.d.ts +2 -1
- package/dist/client/worker/workerPull.d.ts +2 -0
- package/dist/client/worker/workerPull.js +6 -4
- package/dist/domain/job/groupFifoOrder.d.ts +9 -0
- package/dist/domain/job/groupFifoOrder.js +26 -0
- package/dist/domain/queue/groupLimiterManager.d.ts +23 -0
- package/dist/domain/queue/groupLimiterManager.js +107 -0
- package/dist/domain/queue/groupScheduler.d.ts +31 -0
- package/dist/domain/queue/groupScheduler.js +256 -0
- package/dist/domain/queue/groupSchedulerOrder.d.ts +6 -0
- package/dist/domain/queue/groupSchedulerOrder.js +30 -0
- package/dist/domain/queue/priorityQueue.d.ts +13 -23
- package/dist/domain/queue/priorityQueue.js +53 -42
- package/dist/domain/queue/shard/keys.d.ts +1 -0
- package/dist/domain/queue/shard/keys.js +25 -2
- package/dist/domain/queue/shard/lifecycle.js +4 -0
- package/dist/domain/queue/shard/limits.d.ts +20 -1
- package/dist/domain/queue/shard/limits.js +56 -0
- package/dist/domain/queue/shard/state.d.ts +5 -0
- package/dist/domain/queue/shard/state.js +9 -1
- package/dist/domain/types/commands/core.d.ts +3 -0
- package/dist/domain/types/commands/limits.d.ts +41 -0
- package/dist/domain/types/commands/union.d.ts +1 -1
- package/dist/domain/types/group.d.ts +21 -0
- package/dist/domain/types/group.js +78 -0
- package/dist/domain/types/priorityQueue.d.ts +1 -0
- package/dist/infrastructure/persistence/jobOptionsBlob.d.ts +1 -0
- package/dist/infrastructure/persistence/jobOptionsBlob.js +10 -2
- package/dist/infrastructure/persistence/migrations.js +11 -0
- package/dist/infrastructure/persistence/postgres/admission.js +5 -2
- package/dist/infrastructure/persistence/postgres/batchAdmission.js +22 -4
- package/dist/infrastructure/persistence/postgres/claimBatch.d.ts +3 -0
- package/dist/infrastructure/persistence/postgres/claimBatch.js +16 -3
- package/dist/infrastructure/persistence/postgres/claimSelection.d.ts +8 -1
- package/dist/infrastructure/persistence/postgres/claimSelection.js +73 -131
- package/dist/infrastructure/persistence/postgres/claims.d.ts +8 -1
- package/dist/infrastructure/persistence/postgres/claims.js +19 -3
- package/dist/infrastructure/persistence/postgres/eventCatchupCursors.d.ts +32 -0
- package/dist/infrastructure/persistence/postgres/eventCatchupCursors.js +66 -0
- package/dist/infrastructure/persistence/postgres/eventJournalSchema.d.ts +2 -2
- package/dist/infrastructure/persistence/postgres/eventJournalSchema.js +16 -2
- package/dist/infrastructure/persistence/postgres/eventPruneWatermarks.d.ts +2 -0
- package/dist/infrastructure/persistence/postgres/eventPruneWatermarks.js +21 -3
- package/dist/infrastructure/persistence/postgres/events.d.ts +3 -3
- package/dist/infrastructure/persistence/postgres/events.js +44 -17
- package/dist/infrastructure/persistence/postgres/groupClaims.d.ts +15 -0
- package/dist/infrastructure/persistence/postgres/groupClaims.js +95 -0
- package/dist/infrastructure/persistence/postgres/groupSchema.d.ts +2 -0
- package/dist/infrastructure/persistence/postgres/groupSchema.js +155 -0
- package/dist/infrastructure/persistence/postgres/groupSchemaFingerprint.d.ts +3 -0
- package/dist/infrastructure/persistence/postgres/groupSchemaFingerprint.js +127 -0
- package/dist/infrastructure/persistence/postgres/groupStateRetention.d.ts +3 -0
- package/dist/infrastructure/persistence/postgres/groupStateRetention.js +93 -0
- package/dist/infrastructure/persistence/postgres/groups.d.ts +11 -0
- package/dist/infrastructure/persistence/postgres/groups.js +117 -0
- package/dist/infrastructure/persistence/postgres/queueDestruction.js +4 -0
- package/dist/infrastructure/persistence/postgres/runtime.d.ts +0 -1
- package/dist/infrastructure/persistence/postgres/runtime.js +3 -1
- package/dist/infrastructure/persistence/postgres/schema.d.ts +2 -2
- package/dist/infrastructure/persistence/postgres/schema.js +8 -4
- package/dist/infrastructure/persistence/postgres/schemaInitialization.js +6 -1
- package/dist/infrastructure/persistence/postgres/store.d.ts +11 -1
- package/dist/infrastructure/persistence/postgres/store.js +47 -3
- package/dist/infrastructure/persistence/postgres/telemetry.d.ts +2 -0
- package/dist/infrastructure/persistence/postgres/telemetry.js +33 -21
- package/dist/infrastructure/persistence/postgres/types.d.ts +1 -0
- package/dist/infrastructure/persistence/schema.d.ts +2 -2
- package/dist/infrastructure/persistence/schema.js +12 -1
- package/dist/infrastructure/persistence/sqlite/control.d.ts +14 -0
- package/dist/infrastructure/persistence/sqlite/control.js +69 -0
- package/dist/infrastructure/persistence/sqlite/jobs.d.ts +12 -0
- package/dist/infrastructure/persistence/sqlite/jobs.js +36 -0
- package/dist/infrastructure/persistence/sqlite/telemetry.d.ts +2 -1
- package/dist/infrastructure/persistence/sqlite/telemetry.js +5 -64
- package/dist/infrastructure/persistence/sqlite/telemetryWrites.d.ts +4 -0
- package/dist/infrastructure/persistence/sqlite/telemetryWrites.js +123 -0
- package/dist/infrastructure/persistence/sqliteSerializer.js +2 -0
- package/dist/infrastructure/server/handler-routes/control.js +13 -0
- package/dist/infrastructure/server/handlers/core.d.ts +0 -5
- package/dist/infrastructure/server/handlers/core.js +15 -18
- package/dist/infrastructure/server/handlers/groups.d.ts +11 -0
- package/dist/infrastructure/server/handlers/groups.js +55 -0
- package/dist/infrastructure/server/handlers/pushBatchValidation.js +4 -1
- package/dist/infrastructure/server/protocol/validation.d.ts +1 -0
- package/dist/infrastructure/server/protocol/validation.js +1 -0
- package/dist/infrastructure/server/protocol.d.ts +1 -1
- package/dist/infrastructure/server/protocol.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -82,9 +82,12 @@ is not supported. CI validates PostgreSQL 15, 16, 17, and the pinned/recommended
|
|
|
82
82
|
18.6 release. See the
|
|
83
83
|
[storage guide](https://bunqueue.dev/guide/databases/).
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
Every completed release publishes `ghcr.io/egeominotti/bunqueue` with the exact
|
|
86
|
+
version tag alongside `latest`, the commit SHA and a build timestamp, so pin the
|
|
87
|
+
version tag for reproducible deployments. Confirm the tag exists before pinning
|
|
88
|
+
it, because a release whose pipeline does not complete pushes no image;
|
|
89
|
+
`docker buildx imagetools inspect ghcr.io/egeominotti/bunqueue:<tag>` resolves
|
|
90
|
+
the digest.
|
|
88
91
|
|
|
89
92
|
Then produce and process from the language you already use:
|
|
90
93
|
|
|
@@ -123,6 +126,7 @@ Python, PHP, Go, Rust and Elixir clients speak the same protocol — see
|
|
|
123
126
|
- **PostgreSQL 15–18 multi-broker mode** — PostgreSQL 18.6 is recommended;
|
|
124
127
|
database-authoritative claims, fenced
|
|
125
128
|
leases, shared limits, cron, workers, job-state/lifecycle metrics, and failover state
|
|
129
|
+
([tested three-broker Docker example](https://bunqueue.dev/examples/postgres-multibroker/))
|
|
126
130
|
- **BullMQ-compatible API** — same `Queue`, `Worker`, `QueueEvents`; [migrating takes minutes](https://bunqueue.dev/guide/migration/)
|
|
127
131
|
- **MCP server included** — 73 tools; AI agents get full queue control out of the box
|
|
128
132
|
- **Everything server-side** — retries with backoff, priorities, cron, rate limits, dead letter queue
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type JobId } from '../../../domain/types/job';
|
|
2
2
|
import type { BackgroundContext } from '../../types';
|
|
3
|
+
export declare function restoreGroupPolicies(ctx: BackgroundContext, groupStates: ReturnType<NonNullable<BackgroundContext['storage']>['loadGroupState']>): void;
|
|
3
4
|
export declare function restoreRecoveryPolicies(ctx: BackgroundContext, queueStates: ReturnType<NonNullable<BackgroundContext['storage']>['loadQueueState']>): void;
|
|
4
5
|
export declare function recoverActiveJobs(ctx: BackgroundContext, dlqJobIds: Set<JobId>, now: number): void;
|
|
@@ -3,6 +3,18 @@ import { calculateBackoff } from '../../../domain/types/job';
|
|
|
3
3
|
import { isCorruptDependsOn } from '../../../infrastructure/persistence/sqliteSerializer';
|
|
4
4
|
import { shardIndex } from '../../../shared/hash';
|
|
5
5
|
import { quarantineCorruptDependsOn, RECOVERY_BATCH_SIZE } from './shared';
|
|
6
|
+
export function restoreGroupPolicies(ctx, groupStates) {
|
|
7
|
+
for (const state of groupStates) {
|
|
8
|
+
const shard = ctx.shards[shardIndex(state.queue)];
|
|
9
|
+
if (state.rateLimit !== null && state.rateDuration !== null) {
|
|
10
|
+
shard.setGroupRateLimit(state.queue, state.groupId, state.rateLimit, state.rateDuration);
|
|
11
|
+
}
|
|
12
|
+
if (state.concurrencyLimit !== null) {
|
|
13
|
+
shard.setGroupConcurrency(state.queue, state.groupId, state.concurrencyLimit);
|
|
14
|
+
}
|
|
15
|
+
ctx.registerQueueName(state.queue);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
6
18
|
export function restoreRecoveryPolicies(ctx, queueStates) {
|
|
7
19
|
for (const queueState of queueStates) {
|
|
8
20
|
if (queueState.stallConfig) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { reconcileDependencyCompletionPins } from '../../dependencyCompletions';
|
|
2
|
-
import { recoverActiveJobs, restoreRecoveryPolicies } from './active';
|
|
2
|
+
import { recoverActiveJobs, restoreGroupPolicies, restoreRecoveryPolicies } from './active';
|
|
3
3
|
import { recoverPendingJobs } from './pending';
|
|
4
4
|
import { recoverCompletedJobs, restoreDlq, restoreQueueState } from './restore';
|
|
5
5
|
export function recover(ctx) {
|
|
@@ -11,7 +11,9 @@ export function recover(ctx) {
|
|
|
11
11
|
completedInDatabase.add(record.jobId);
|
|
12
12
|
const dlqJobIds = ctx.storage.loadDlqJobIds();
|
|
13
13
|
const queueStates = ctx.storage.loadQueueState();
|
|
14
|
+
const groupStates = ctx.storage.loadGroupState();
|
|
14
15
|
restoreRecoveryPolicies(ctx, queueStates);
|
|
16
|
+
restoreGroupPolicies(ctx, groupStates);
|
|
15
17
|
const now = Date.now();
|
|
16
18
|
recoverActiveJobs(ctx, dlqJobIds, now);
|
|
17
19
|
recoverPendingJobs(ctx, completedInDatabase, now);
|
|
@@ -18,6 +18,7 @@ export function recoverPendingJobs(ctx, completedInDatabase, now) {
|
|
|
18
18
|
corruptPendingJobs.push(job);
|
|
19
19
|
continue;
|
|
20
20
|
}
|
|
21
|
+
shard.assignGroupFifoOrder(job);
|
|
21
22
|
const hasDependencies = job.dependsOn && job.dependsOn.length > 0;
|
|
22
23
|
const recoveredState = persistedJobState(job);
|
|
23
24
|
const wasAlreadyPromoted = recoveredState === 'waiting' ||
|
|
@@ -26,7 +26,7 @@ export async function cleanup(ctx) {
|
|
|
26
26
|
}
|
|
27
27
|
await cleanOrphanedProcessingEntries(ctx, now, stallTimeout);
|
|
28
28
|
await cleanStaleWaitingDependencies(ctx, now);
|
|
29
|
-
cleanUniqueKeysAndGroups(ctx);
|
|
29
|
+
cleanUniqueKeysAndGroups(ctx, now);
|
|
30
30
|
cleanStalledCandidates(ctx);
|
|
31
31
|
await cleanOrphanedJobIndex(ctx);
|
|
32
32
|
cleanOrphanedJobLocks(ctx);
|
|
@@ -103,9 +103,10 @@ async function cleanStaleWaitingDependencies(ctx, now) {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
-
function cleanUniqueKeysAndGroups(ctx) {
|
|
106
|
+
function cleanUniqueKeysAndGroups(ctx, now) {
|
|
107
107
|
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
108
108
|
const shard = ctx.shards[i];
|
|
109
|
+
shard.cleanExpiredGroupWindows(now);
|
|
109
110
|
// Clean expired unique keys
|
|
110
111
|
shard.cleanExpiredUniqueKeys();
|
|
111
112
|
// Trim if too many keys remain
|
|
@@ -121,19 +122,6 @@ function cleanUniqueKeysAndGroups(ctx) {
|
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
|
-
// Clean orphaned active groups
|
|
125
|
-
for (const [_queueName, groups] of shard.activeGroups) {
|
|
126
|
-
if (groups.size > 1000) {
|
|
127
|
-
const toRemove = Math.floor(groups.size / 2);
|
|
128
|
-
const iter = groups.values();
|
|
129
|
-
for (let j = 0; j < toRemove; j++) {
|
|
130
|
-
const { value, done } = iter.next();
|
|
131
|
-
if (done)
|
|
132
|
-
break;
|
|
133
|
-
groups.delete(value);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
125
|
}
|
|
138
126
|
}
|
|
139
127
|
function cleanStalledCandidates(ctx) {
|
|
@@ -244,7 +232,7 @@ function cleanEmptyQueues(ctx) {
|
|
|
244
232
|
shard.dlq.delete(queueName);
|
|
245
233
|
shard.uniqueKeys.delete(queueName);
|
|
246
234
|
shard.queueState.delete(queueName);
|
|
247
|
-
shard.
|
|
235
|
+
shard.clearEmptyGroupRuntime(queueName);
|
|
248
236
|
shard.clearQueueLimiters(queueName);
|
|
249
237
|
shard.stallConfig.delete(queueName);
|
|
250
238
|
shard.dlqConfig.delete(queueName);
|
|
@@ -102,6 +102,7 @@ export class ContextFactory {
|
|
|
102
102
|
jobIndex: this.deps.jobIndex,
|
|
103
103
|
totalPushed: this.deps.metrics.totalPushed,
|
|
104
104
|
broadcast: this.deps.eventsManager.broadcast.bind(this.deps.eventsManager),
|
|
105
|
+
broadcastBatch: this.deps.eventsManager.broadcastBatch.bind(this.deps.eventsManager),
|
|
105
106
|
dashboardEmit: this.callbacks.emitDashboardEvent,
|
|
106
107
|
registerQueueName: this.callbacks.registerQueueName,
|
|
107
108
|
};
|
|
@@ -116,6 +117,7 @@ export class ContextFactory {
|
|
|
116
117
|
jobIndex: this.deps.jobIndex,
|
|
117
118
|
totalPulled: this.deps.metrics.totalPulled,
|
|
118
119
|
broadcast: this.deps.eventsManager.broadcast.bind(this.deps.eventsManager),
|
|
120
|
+
broadcastBatch: this.deps.eventsManager.broadcastBatch.bind(this.deps.eventsManager),
|
|
119
121
|
dashboardEmit: this.callbacks.emitDashboardEvent,
|
|
120
122
|
};
|
|
121
123
|
}
|
|
@@ -139,6 +141,7 @@ export class ContextFactory {
|
|
|
139
141
|
totalFailed: this.deps.metrics.totalFailed,
|
|
140
142
|
perQueueMetrics: this.deps.perQueueMetrics,
|
|
141
143
|
broadcast: this.deps.eventsManager.broadcast.bind(this.deps.eventsManager),
|
|
144
|
+
broadcastBatch: this.deps.eventsManager.broadcastBatch.bind(this.deps.eventsManager),
|
|
142
145
|
onJobCompleted: this.callbacks.onJobCompleted,
|
|
143
146
|
onJobFailed: this.callbacks.onJobFailed,
|
|
144
147
|
onJobsCompleted: this.callbacks.onJobsCompleted,
|
|
@@ -196,6 +196,7 @@ function requeueCompletedJob(job, ctx) {
|
|
|
196
196
|
timeline,
|
|
197
197
|
};
|
|
198
198
|
setDlqRetryState(requeued, null);
|
|
199
|
+
shard.assignGroupFifoOrder(requeued);
|
|
199
200
|
// Commit the durable transition before publishing the new in-memory state.
|
|
200
201
|
// A SQLite failure therefore leaves the completed generation authoritative.
|
|
201
202
|
ctx.storage?.requeueCompletedJob(requeued);
|
|
@@ -44,6 +44,7 @@ function retryEntry(queue, entry, ctx, now, prepareState) {
|
|
|
44
44
|
if (!canRetry(queue, entry.job, ctx))
|
|
45
45
|
return null;
|
|
46
46
|
const job = prepareRetry(entry.job, now);
|
|
47
|
+
ctx.shards[shardIndex(queue)].assignGroupFifoOrder(job);
|
|
47
48
|
prepareState(job);
|
|
48
49
|
ctx.storage?.requeueDlqJob(job);
|
|
49
50
|
const removed = ctx.shards[shardIndex(queue)].removeFromDlq(queue, entry.job.id);
|
|
@@ -7,11 +7,14 @@ import { EventType, type JobEvent } from '../domain/types/queue';
|
|
|
7
7
|
import type { WebhookManager } from './webhookManager';
|
|
8
8
|
/** Event subscriber callback */
|
|
9
9
|
export type EventSubscriber = (event: JobEvent) => void;
|
|
10
|
+
export type EventBatchSubscriber = (events: readonly JobEvent[]) => void;
|
|
10
11
|
/** Events manager class */
|
|
11
12
|
export declare class EventsManager {
|
|
12
13
|
private readonly webhookManager;
|
|
13
14
|
/** Use Set for O(1) subscribe/unsubscribe instead of indexOf+splice */
|
|
14
15
|
private readonly subscribers;
|
|
16
|
+
/** Optional batch path for internal subscribers such as durable telemetry. */
|
|
17
|
+
private readonly batchSubscribers;
|
|
15
18
|
/** Waiters for specific job completions - for efficient WaitJob implementation */
|
|
16
19
|
private readonly completionWaiters;
|
|
17
20
|
constructor(webhookManager: WebhookManager);
|
|
@@ -20,7 +23,7 @@ export declare class EventsManager {
|
|
|
20
23
|
/** Number of jobs with active completion waiters */
|
|
21
24
|
get completionWaiterCount(): number;
|
|
22
25
|
/** Subscribe to job events - O(1) add and remove */
|
|
23
|
-
subscribe(callback: EventSubscriber): () => void;
|
|
26
|
+
subscribe(callback: EventSubscriber, batchCallback?: EventBatchSubscriber): () => void;
|
|
24
27
|
/** Clear all subscribers (for shutdown) */
|
|
25
28
|
clear(): void;
|
|
26
29
|
/**
|
|
@@ -38,6 +41,10 @@ export declare class EventsManager {
|
|
|
38
41
|
timestamp: number;
|
|
39
42
|
error?: string;
|
|
40
43
|
}): void;
|
|
44
|
+
/** Broadcast an ordered batch while allowing internal subscribers to persist once. */
|
|
45
|
+
broadcastBatch(events: readonly JobEvent[]): void;
|
|
46
|
+
private notifyCompletionWaiters;
|
|
47
|
+
private triggerWebhook;
|
|
41
48
|
/** Map internal event type to webhook event */
|
|
42
49
|
private mapEventToWebhook;
|
|
43
50
|
}
|
|
@@ -8,6 +8,8 @@ export class EventsManager {
|
|
|
8
8
|
webhookManager;
|
|
9
9
|
/** Use Set for O(1) subscribe/unsubscribe instead of indexOf+splice */
|
|
10
10
|
subscribers = new Set();
|
|
11
|
+
/** Optional batch path for internal subscribers such as durable telemetry. */
|
|
12
|
+
batchSubscribers = new Map();
|
|
11
13
|
/** Waiters for specific job completions - for efficient WaitJob implementation */
|
|
12
14
|
completionWaiters = new Map();
|
|
13
15
|
constructor(webhookManager) {
|
|
@@ -22,15 +24,19 @@ export class EventsManager {
|
|
|
22
24
|
return this.completionWaiters.size;
|
|
23
25
|
}
|
|
24
26
|
/** Subscribe to job events - O(1) add and remove */
|
|
25
|
-
subscribe(callback) {
|
|
27
|
+
subscribe(callback, batchCallback) {
|
|
26
28
|
this.subscribers.add(callback);
|
|
29
|
+
if (batchCallback)
|
|
30
|
+
this.batchSubscribers.set(callback, batchCallback);
|
|
27
31
|
return () => {
|
|
28
32
|
this.subscribers.delete(callback); // O(1) instead of O(n)
|
|
33
|
+
this.batchSubscribers.delete(callback);
|
|
29
34
|
};
|
|
30
35
|
}
|
|
31
36
|
/** Clear all subscribers (for shutdown) */
|
|
32
37
|
clear() {
|
|
33
38
|
this.subscribers.clear();
|
|
39
|
+
this.batchSubscribers.clear();
|
|
34
40
|
// Clear all waiters
|
|
35
41
|
for (const waiters of this.completionWaiters.values()) {
|
|
36
42
|
for (const waiter of waiters) {
|
|
@@ -112,40 +118,81 @@ export class EventsManager {
|
|
|
112
118
|
}
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
if (hasWaiters)
|
|
122
|
+
this.notifyCompletionWaiters(event);
|
|
123
|
+
if (hasWebhooks)
|
|
124
|
+
this.triggerWebhook(event);
|
|
125
|
+
}
|
|
126
|
+
/** Broadcast an ordered batch while allowing internal subscribers to persist once. */
|
|
127
|
+
broadcastBatch(events) {
|
|
128
|
+
if (events.length === 0)
|
|
129
|
+
return;
|
|
130
|
+
const hasSubscribers = this.subscribers.size > 0;
|
|
131
|
+
const hasWebhooks = this.webhookManager.hasEnabledWebhooks();
|
|
132
|
+
const hasWaiters = this.completionWaiters.size > 0;
|
|
133
|
+
if (!hasSubscribers && !hasWebhooks && !hasWaiters)
|
|
134
|
+
return;
|
|
135
|
+
if (hasSubscribers) {
|
|
136
|
+
for (const [subscriber, batchSubscriber] of this.batchSubscribers) {
|
|
137
|
+
if (!this.subscribers.has(subscriber))
|
|
138
|
+
continue;
|
|
139
|
+
try {
|
|
140
|
+
batchSubscriber(events);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Ignore subscriber errors.
|
|
126
144
|
}
|
|
127
145
|
}
|
|
128
146
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
jobId: String(event.jobId),
|
|
142
|
-
queue: event.queue,
|
|
143
|
-
error: String(err),
|
|
144
|
-
});
|
|
145
|
-
});
|
|
147
|
+
for (const event of events) {
|
|
148
|
+
if (hasSubscribers) {
|
|
149
|
+
for (const subscriber of this.subscribers) {
|
|
150
|
+
if (this.batchSubscribers.has(subscriber))
|
|
151
|
+
continue;
|
|
152
|
+
try {
|
|
153
|
+
subscriber(event);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Ignore subscriber errors and continue with the ordered batch.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
146
159
|
}
|
|
160
|
+
if (hasWaiters && event.eventType === "completed" /* EventType.Completed */) {
|
|
161
|
+
this.notifyCompletionWaiters(event);
|
|
162
|
+
}
|
|
163
|
+
if (hasWebhooks)
|
|
164
|
+
this.triggerWebhook(event);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
notifyCompletionWaiters(event) {
|
|
168
|
+
const jobKey = String(event.jobId);
|
|
169
|
+
const waiters = this.completionWaiters.get(jobKey);
|
|
170
|
+
if (!waiters)
|
|
171
|
+
return;
|
|
172
|
+
this.completionWaiters.delete(jobKey);
|
|
173
|
+
for (const waiter of waiters) {
|
|
174
|
+
if (!waiter.cancelled)
|
|
175
|
+
waiter.resolve();
|
|
147
176
|
}
|
|
148
177
|
}
|
|
178
|
+
triggerWebhook(event) {
|
|
179
|
+
const webhookEvent = this.mapEventToWebhook(event.eventType);
|
|
180
|
+
if (!webhookEvent)
|
|
181
|
+
return;
|
|
182
|
+
this.webhookManager
|
|
183
|
+
.trigger(webhookEvent, String(event.jobId), event.queue, {
|
|
184
|
+
data: event.data,
|
|
185
|
+
error: event.error,
|
|
186
|
+
})
|
|
187
|
+
.catch((err) => {
|
|
188
|
+
webhookLog.error('Webhook trigger failed', {
|
|
189
|
+
event: webhookEvent,
|
|
190
|
+
jobId: String(event.jobId),
|
|
191
|
+
queue: event.queue,
|
|
192
|
+
error: String(err),
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
149
196
|
/** Map internal event type to webhook event */
|
|
150
197
|
mapEventToWebhook(eventType) {
|
|
151
198
|
switch (eventType) {
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
import type { Job, JobId } from '../../domain/types/job';
|
|
3
3
|
import type { BatchContext, ExtractedJob, FinalizeContext } from '../types/ack';
|
|
4
4
|
export type { BatchContext, ExtractedJob, FinalizeContext } from '../types/ack';
|
|
5
|
-
/**
|
|
6
|
-
* Group job IDs by processing shard
|
|
7
|
-
* Returns Map<shardIndex, jobIds[]>
|
|
8
|
-
*/
|
|
5
|
+
/** Group job IDs by processing shard. */
|
|
9
6
|
export declare function groupByProcShard(jobIds: JobId[]): Map<number, JobId[]>;
|
|
10
7
|
/**
|
|
11
8
|
* Group items by processing shard (with results)
|
|
@@ -4,10 +4,30 @@ import { withWriteLock } from '../../shared/lock';
|
|
|
4
4
|
import { shardIndex, processingShardIndex } from '../../shared/hash';
|
|
5
5
|
import { throughputTracker } from '../throughputTracker';
|
|
6
6
|
import { commitRemovedCompletion } from '../dependencyCompletions';
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
/** Batch only when no per-job durable result/removal ordering must be preserved. */
|
|
8
|
+
function persistCompletedBatch(extractedJobs, ctx, includeResults, now) {
|
|
9
|
+
const storage = ctx.storage;
|
|
10
|
+
if (!storage || extractedJobs.length === 0)
|
|
11
|
+
return false;
|
|
12
|
+
if (extractedJobs.some(({ job, removeOnComplete }) => job.removeOnComplete || removeOnComplete === true) ||
|
|
13
|
+
(includeResults && extractedJobs.some(({ result }) => result !== undefined))) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
storage.markCompletedBatch(extractedJobs.map(({ id: jobId, job }) => ({
|
|
18
|
+
jobId,
|
|
19
|
+
completedAt: now,
|
|
20
|
+
timeline: job.timeline.length < MAX_TIMELINE_ENTRIES
|
|
21
|
+
? [...job.timeline, { state: 'completed', timestamp: now }]
|
|
22
|
+
: job.timeline,
|
|
23
|
+
})));
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Group job IDs by processing shard. */
|
|
11
31
|
export function groupByProcShard(jobIds) {
|
|
12
32
|
const byProcShard = new Map();
|
|
13
33
|
for (const jobId of jobIds) {
|
|
@@ -119,6 +139,7 @@ export function finalizeBatchAck(extractedJobs, ctx, includeResults) {
|
|
|
119
139
|
const jobCount = extractedJobs.length;
|
|
120
140
|
const needsBroadcast = ctx.needsBroadcast?.() ?? true;
|
|
121
141
|
const hasPendingDeps = ctx.hasPendingDeps?.() ?? true;
|
|
142
|
+
const completionBatchPersisted = persistCompletedBatch(extractedJobs, ctx, includeResults, now);
|
|
122
143
|
// Batch counter update
|
|
123
144
|
ctx.totalCompleted.value += BigInt(jobCount);
|
|
124
145
|
// Per-queue completed counter update
|
|
@@ -160,8 +181,9 @@ export function finalizeBatchAck(extractedJobs, ctx, includeResults) {
|
|
|
160
181
|
// 2. Update job index and store job data for completed listing
|
|
161
182
|
ctx.jobIndex.set(jobId, { type: 'completed', queueName: job.queue });
|
|
162
183
|
ctx.completedJobsData.set(jobId, job);
|
|
163
|
-
if (hasStorage)
|
|
184
|
+
if (hasStorage && !completionBatchPersisted) {
|
|
164
185
|
storage.markCompleted(jobId, now, job.timeline);
|
|
186
|
+
}
|
|
165
187
|
// 3. Mark as completed LAST - this is the signal other threads wait for
|
|
166
188
|
ctx.completedJobs.add(jobId);
|
|
167
189
|
}
|
|
@@ -180,15 +202,18 @@ export function finalizeBatchAck(extractedJobs, ctx, includeResults) {
|
|
|
180
202
|
}
|
|
181
203
|
// Broadcast events
|
|
182
204
|
if (needsBroadcast) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
205
|
+
const events = extractedJobs.map(({ id: jobId, job, result }) => ({
|
|
206
|
+
eventType: 'completed',
|
|
207
|
+
queue: job.queue,
|
|
208
|
+
jobId,
|
|
209
|
+
timestamp: now,
|
|
210
|
+
data: includeResults ? result : undefined,
|
|
211
|
+
}));
|
|
212
|
+
if (ctx.broadcastBatch)
|
|
213
|
+
ctx.broadcastBatch(events);
|
|
214
|
+
else {
|
|
215
|
+
for (const event of events)
|
|
216
|
+
ctx.broadcast(event);
|
|
192
217
|
}
|
|
193
218
|
}
|
|
194
219
|
// Notify completions
|
|
@@ -29,6 +29,7 @@ function assertIdsAvailable(batch, ctx) {
|
|
|
29
29
|
function prepareJobs(batch, ctx, now) {
|
|
30
30
|
return batch.jobs.map((planned) => {
|
|
31
31
|
const job = createJob(planned.id, planned.queue, planned.input, now);
|
|
32
|
+
ctx.shards[shardIndex(planned.queue)].assignGroupFifoOrder(job);
|
|
32
33
|
job.timeline.push({ state: initialJobState(job, ctx, now), timestamp: now });
|
|
33
34
|
return job;
|
|
34
35
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateGroupId } from '../../domain/types/group';
|
|
1
2
|
import { normalizeJobPayload } from '../../domain/types/job';
|
|
2
3
|
import { validateFlowTopology } from './flowTopologyValidation';
|
|
3
4
|
const MAX_FLOW_JOBS = 10_000;
|
|
@@ -52,10 +53,9 @@ function validateOptions(input) {
|
|
|
52
53
|
throw new Error(`${name} must be a boolean`);
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new Error(
|
|
58
|
-
}
|
|
56
|
+
const groupError = validateGroupId(input.groupId);
|
|
57
|
+
if (groupError)
|
|
58
|
+
throw new Error(groupError);
|
|
59
59
|
if (input.tags !== undefined &&
|
|
60
60
|
(!Array.isArray(input.tags) ||
|
|
61
61
|
input.tags.some((tag) => typeof tag !== 'string' || tag.length > 256))) {
|
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
* Job pull logic with timeout support
|
|
4
4
|
*/
|
|
5
5
|
import type { Job } from '../../domain/types/job';
|
|
6
|
+
import { type GroupPullOptions } from '../../domain/types/group';
|
|
6
7
|
import { type PullContext } from './pullStateTransition';
|
|
7
8
|
export type { PullContext } from './pullStateTransition';
|
|
9
|
+
interface PullRequestOptions {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
group?: GroupPullOptions;
|
|
12
|
+
}
|
|
8
13
|
/**
|
|
9
14
|
* Pull next job from queue
|
|
10
15
|
*/
|
|
11
|
-
export declare function pullJob(queue: string, timeoutMs: number, ctx: PullContext, signal?: AbortSignal): Promise<Job | null>;
|
|
16
|
+
export declare function pullJob(queue: string, timeoutMs: number, ctx: PullContext, signal?: AbortSignal, groupOptions?: GroupPullOptions): Promise<Job | null>;
|
|
12
17
|
/**
|
|
13
18
|
* Pull multiple jobs from queue
|
|
14
19
|
*/
|
|
15
|
-
export declare function pullJobBatch(queue: string, count: number, timeoutMs: number, ctx: PullContext,
|
|
20
|
+
export declare function pullJobBatch(queue: string, count: number, timeoutMs: number, ctx: PullContext, options?: PullRequestOptions): Promise<Job[]>;
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Pull Operations
|
|
3
3
|
* Job pull logic with timeout support
|
|
4
4
|
*/
|
|
5
|
+
import { assertGroupPullOptions } from '../../domain/types/group';
|
|
5
6
|
import { withWriteLock } from '../../shared/lock';
|
|
6
|
-
import { shardIndex
|
|
7
|
+
import { shardIndex } from '../../shared/hash';
|
|
7
8
|
import { latencyTracker } from '../latencyTracker';
|
|
8
|
-
import {
|
|
9
|
+
import { finalizeProcessing, finalizeProcessingBatch } from './pullFinalization';
|
|
9
10
|
import { createDequeueScan, requeueJob, restoreParkedJobs, tryDequeueNextJob, } from './pullStateTransition';
|
|
10
11
|
/** Wait until either a notification arrives, the next delay matures, or the pull deadline. */
|
|
11
12
|
async function waitForNextCandidate(options) {
|
|
@@ -14,64 +15,18 @@ async function waitForNextCandidate(options) {
|
|
|
14
15
|
const untilNextRun = nextRunAt === null ? remaining : Math.max(1, nextRunAt - now);
|
|
15
16
|
await shard.waitForJob(queue, Math.min(remaining, untilNextRun), signal);
|
|
16
17
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Finish the handoff of a dequeued job: persist active state and broadcast.
|
|
19
|
-
*
|
|
20
|
-
* The job was ALREADY inserted into processingShards and jobIndex was flipped
|
|
21
|
-
* to 'processing' inside the dequeue critical section (tryDequeueNextJob),
|
|
22
|
-
* atomically with the queue pop, so readers never see a stale 'queue'
|
|
23
|
-
* location. Only the bookkeeping that may follow an await happens here.
|
|
24
|
-
*
|
|
25
|
-
* Returns false when the job is no longer in processingShards: a management
|
|
26
|
-
* op (discardJob, moveJobToDelayed, obliterate) claimed it between the
|
|
27
|
-
* dequeue and this call. The claimer owns the job now; the pull must NOT
|
|
28
|
-
* deliver it to a worker nor mark it active.
|
|
29
|
-
*/
|
|
30
|
-
function finalizeProcessing(job, queue, ctx) {
|
|
31
|
-
const procIdx = processingShardIndex(job.id);
|
|
32
|
-
if (!ctx.processingShards[procIdx].has(job.id))
|
|
33
|
-
return false;
|
|
34
|
-
const now = Date.now();
|
|
35
|
-
try {
|
|
36
|
-
ctx.storage?.markActive(job.id, job.startedAt ?? now, job.timeline);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
// Non-fatal: job is already in processingShards (in-memory source of truth).
|
|
40
|
-
// On crash, SQLite recovery will handle the stale state.
|
|
41
|
-
}
|
|
42
|
-
ctx.totalPulled.value++;
|
|
43
|
-
throughputTracker.pullRate.increment();
|
|
44
|
-
ctx.broadcast({
|
|
45
|
-
eventType: 'pulled',
|
|
46
|
-
queue,
|
|
47
|
-
jobId: job.id,
|
|
48
|
-
timestamp: now,
|
|
49
|
-
});
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Finish the handoff for a batch of dequeued jobs.
|
|
54
|
-
* Returns the jobs actually delivered (claimed jobs are skipped, see above).
|
|
55
|
-
*/
|
|
56
|
-
function finalizeProcessingBatch(jobs, queue, ctx) {
|
|
57
|
-
const delivered = [];
|
|
58
|
-
for (const job of jobs) {
|
|
59
|
-
if (finalizeProcessing(job, queue, ctx))
|
|
60
|
-
delivered.push(job);
|
|
61
|
-
}
|
|
62
|
-
return delivered;
|
|
63
|
-
}
|
|
64
18
|
/**
|
|
65
19
|
* Pull next job from queue
|
|
66
20
|
*/
|
|
67
|
-
export async function pullJob(queue, timeoutMs, ctx, signal) {
|
|
21
|
+
export async function pullJob(queue, timeoutMs, ctx, signal, groupOptions) {
|
|
22
|
+
assertGroupPullOptions(groupOptions);
|
|
68
23
|
const startNs = Bun.nanoseconds();
|
|
69
24
|
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
|
|
70
25
|
const idx = shardIndex(queue);
|
|
71
26
|
while (true) {
|
|
72
27
|
if (signal?.aborted)
|
|
73
28
|
return null;
|
|
74
|
-
const attempt = await tryPullFromShard(queue, idx, ctx, signal);
|
|
29
|
+
const attempt = await tryPullFromShard(queue, idx, ctx, signal, groupOptions);
|
|
75
30
|
const job = attempt.job;
|
|
76
31
|
if (job) {
|
|
77
32
|
if (signal?.aborted) {
|
|
@@ -112,7 +67,7 @@ export async function pullJob(queue, timeoutMs, ctx, signal) {
|
|
|
112
67
|
/**
|
|
113
68
|
* Try to pull a job from a specific shard
|
|
114
69
|
*/
|
|
115
|
-
async function tryPullFromShard(queue, idx, ctx, signal) {
|
|
70
|
+
async function tryPullFromShard(queue, idx, ctx, signal, groupOptions) {
|
|
116
71
|
return await withWriteLock(ctx.shardLocks[idx], () => {
|
|
117
72
|
if (signal?.aborted)
|
|
118
73
|
return { job: null, nextRunAt: null };
|
|
@@ -122,7 +77,7 @@ async function tryPullFromShard(queue, idx, ctx, signal) {
|
|
|
122
77
|
return { job: null, nextRunAt: null };
|
|
123
78
|
}
|
|
124
79
|
const now = Date.now();
|
|
125
|
-
const scan = createDequeueScan();
|
|
80
|
+
const scan = createDequeueScan(groupOptions);
|
|
126
81
|
try {
|
|
127
82
|
const result = tryDequeueNextJob(shard, queue, now, ctx, scan);
|
|
128
83
|
return result.status === 'job'
|
|
@@ -137,14 +92,16 @@ async function tryPullFromShard(queue, idx, ctx, signal) {
|
|
|
137
92
|
/**
|
|
138
93
|
* Pull multiple jobs from queue
|
|
139
94
|
*/
|
|
140
|
-
export async function pullJobBatch(queue, count, timeoutMs, ctx,
|
|
95
|
+
export async function pullJobBatch(queue, count, timeoutMs, ctx, options = {}) {
|
|
96
|
+
assertGroupPullOptions(options.group);
|
|
97
|
+
const { signal } = options;
|
|
141
98
|
const startNs = Bun.nanoseconds();
|
|
142
99
|
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
|
|
143
100
|
const idx = shardIndex(queue);
|
|
144
101
|
while (true) {
|
|
145
102
|
if (signal?.aborted)
|
|
146
103
|
return [];
|
|
147
|
-
const attempt = await tryPullBatchFromShard(queue, idx, count, ctx,
|
|
104
|
+
const attempt = await tryPullBatchFromShard(queue, idx, count, ctx, options);
|
|
148
105
|
const jobs = attempt.jobs;
|
|
149
106
|
if (jobs.length > 0) {
|
|
150
107
|
if (signal?.aborted) {
|
|
@@ -194,7 +151,8 @@ export async function pullJobBatch(queue, count, timeoutMs, ctx, signal) {
|
|
|
194
151
|
/**
|
|
195
152
|
* Try to pull multiple jobs from a shard
|
|
196
153
|
*/
|
|
197
|
-
async function tryPullBatchFromShard(queue, idx, count, ctx,
|
|
154
|
+
async function tryPullBatchFromShard(queue, idx, count, ctx, options) {
|
|
155
|
+
const { signal, group: groupOptions } = options;
|
|
198
156
|
return await withWriteLock(ctx.shardLocks[idx], () => {
|
|
199
157
|
if (signal?.aborted)
|
|
200
158
|
return { jobs: [], nextRunAt: null };
|
|
@@ -204,7 +162,7 @@ async function tryPullBatchFromShard(queue, idx, count, ctx, signal) {
|
|
|
204
162
|
if (state.paused)
|
|
205
163
|
return { jobs, nextRunAt: null };
|
|
206
164
|
const now = Date.now();
|
|
207
|
-
const scan = createDequeueScan();
|
|
165
|
+
const scan = createDequeueScan(groupOptions);
|
|
208
166
|
try {
|
|
209
167
|
while (jobs.length < count) {
|
|
210
168
|
const result = tryDequeueNextJob(shard, queue, now, ctx, scan);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Job } from '../../domain/types/job';
|
|
2
|
+
import type { PullContext } from './pullStateTransition';
|
|
3
|
+
/** Persist and publish a single processing handoff. */
|
|
4
|
+
export declare function finalizeProcessing(job: Job, queue: string, ctx: PullContext): boolean;
|
|
5
|
+
/** Persist and publish all processing handoffs with one durable state transaction. */
|
|
6
|
+
export declare function finalizeProcessingBatch(jobs: Job[], queue: string, ctx: PullContext): Job[];
|