bunqueue 2.8.28 → 2.8.29
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/dist/application/dlqManager.d.ts +1 -1
- package/dist/application/dlqManager.js +25 -1
- package/dist/application/queueManager.d.ts +1 -1
- package/dist/application/queueManager.js +2 -2
- package/dist/client/bunqueue.d.ts +2 -0
- package/dist/client/bunqueue.js +2 -2
- package/dist/client/flowJobFactory.js +4 -2
- package/dist/client/queue/failWire.d.ts +17 -0
- package/dist/client/queue/failWire.js +32 -0
- package/dist/client/queue/jobMove.d.ts +1 -1
- package/dist/client/queue/jobMove.js +8 -3
- package/dist/client/queue/jobProxy.js +6 -3
- package/dist/client/queue/operations/management.js +2 -1
- package/dist/client/queue/scheduler.d.ts +2 -0
- package/dist/client/queue/scheduler.js +10 -0
- package/dist/client/sandboxed/worker.js +5 -3
- package/dist/client/worker/processorHandlers.js +7 -1
- package/dist/client/worker/worker.js +9 -1
- package/dist/domain/types/command.d.ts +2 -0
- package/dist/infrastructure/server/handlers/dlq.js +2 -1
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@ export declare function getDlqStats(queue: string, ctx: DlqContext): DlqStats;
|
|
|
22
22
|
/** Retry a single job from DLQ */
|
|
23
23
|
export declare function retryDlqJob(queue: string, jobId: JobId, ctx: DlqContext): Job | null;
|
|
24
24
|
/** Retry jobs from DLQ (backward compatible) */
|
|
25
|
-
export declare function retryDlqJobs(queue: string, ctx: DlqContext, jobId?: JobId): number;
|
|
25
|
+
export declare function retryDlqJobs(queue: string, ctx: DlqContext, jobId?: JobId, limit?: number): number;
|
|
26
26
|
/** Retry jobs by filter */
|
|
27
27
|
export declare function retryDlqByFilter(queue: string, ctx: DlqContext, filter: DlqFilter): number;
|
|
28
28
|
/** Process auto-retry for a queue */
|
|
@@ -86,10 +86,34 @@ export function retryDlqJob(queue, jobId, ctx) {
|
|
|
86
86
|
return job;
|
|
87
87
|
}
|
|
88
88
|
/** Retry jobs from DLQ (backward compatible) */
|
|
89
|
-
export function retryDlqJobs(queue, ctx, jobId) {
|
|
89
|
+
export function retryDlqJobs(queue, ctx, jobId, limit) {
|
|
90
90
|
if (jobId) {
|
|
91
91
|
return retryDlqJob(queue, jobId, ctx) ? 1 : 0;
|
|
92
92
|
}
|
|
93
|
+
// Bounded retry (#111-class: `retryJobs({ state:'failed', count })`). Retry
|
|
94
|
+
// only the first `limit` DLQ entries, reusing the tested single-entry path so
|
|
95
|
+
// the remaining entries stay in the DLQ (memory + SQLite) instead of the
|
|
96
|
+
// clear-all fast path below wrongly draining the whole queue. Any provided
|
|
97
|
+
// `limit` engages the bounded path; a negative/NaN/fractional cap is clamped
|
|
98
|
+
// to a safe non-negative integer so it can never fall through to clear-all
|
|
99
|
+
// (skeptic: `limit >= 0` let `-1`/`NaN` drain the entire DLQ). `undefined`
|
|
100
|
+
// (no cap requested) still means "retry all" via the fast path below.
|
|
101
|
+
if (limit !== undefined) {
|
|
102
|
+
const cap = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 0;
|
|
103
|
+
const idx = shardIndex(queue);
|
|
104
|
+
const shard = ctx.shards[idx];
|
|
105
|
+
// Snapshot ids first: retryDlqJob mutates the DLQ as it goes.
|
|
106
|
+
const ids = shard
|
|
107
|
+
.getDlqEntries(queue)
|
|
108
|
+
.slice(0, cap)
|
|
109
|
+
.map((e) => e.job.id);
|
|
110
|
+
let retried = 0;
|
|
111
|
+
for (const id of ids) {
|
|
112
|
+
if (retryDlqJob(queue, id, ctx))
|
|
113
|
+
retried++;
|
|
114
|
+
}
|
|
115
|
+
return retried;
|
|
116
|
+
}
|
|
93
117
|
const idx = shardIndex(queue);
|
|
94
118
|
const shard = ctx.shards[idx];
|
|
95
119
|
const entries = shard.getDlqEntries(queue);
|
|
@@ -193,7 +193,7 @@ export declare class QueueManager {
|
|
|
193
193
|
getDlqEntries(queue: string, filter?: DlqFilter): DlqEntry[];
|
|
194
194
|
getDlqCount(queue: string): number;
|
|
195
195
|
getDlqStats(queue: string): DlqStats;
|
|
196
|
-
retryDlq(queue: string, jobId?: JobId): number;
|
|
196
|
+
retryDlq(queue: string, jobId?: JobId, limit?: number): number;
|
|
197
197
|
purgeDlq(queue: string): number;
|
|
198
198
|
retryCompleted(queue: string, jobId?: JobId): number;
|
|
199
199
|
setRateLimit(queue: string, limit: number): void;
|
|
@@ -849,8 +849,8 @@ export class QueueManager {
|
|
|
849
849
|
getDlqStats(queue) {
|
|
850
850
|
return dlqOps.getDlqStats(queue, this.contextFactory.getDlqContext());
|
|
851
851
|
}
|
|
852
|
-
retryDlq(queue, jobId) {
|
|
853
|
-
return dlqOps.retryDlqJobs(queue, this.contextFactory.getDlqContext(), jobId);
|
|
852
|
+
retryDlq(queue, jobId, limit) {
|
|
853
|
+
return dlqOps.retryDlqJobs(queue, this.contextFactory.getDlqContext(), jobId, limit);
|
|
854
854
|
}
|
|
855
855
|
purgeDlq(queue) {
|
|
856
856
|
return dlqOps.purgeDlqJobs(queue, this.contextFactory.getDlqContext());
|
|
@@ -43,9 +43,11 @@ export declare class Bunqueue<T = unknown, R = unknown> {
|
|
|
43
43
|
countAsync(): Promise<number>;
|
|
44
44
|
cron(id: string, pattern: string, data?: T, opts?: {
|
|
45
45
|
timezone?: string;
|
|
46
|
+
limit?: number;
|
|
46
47
|
jobOpts?: JobOptions;
|
|
47
48
|
}): Promise<SchedulerInfo | null>;
|
|
48
49
|
every(id: string, intervalMs: number, data?: T, opts?: {
|
|
50
|
+
limit?: number;
|
|
49
51
|
jobOpts?: JobOptions;
|
|
50
52
|
}): Promise<SchedulerInfo | null>;
|
|
51
53
|
removeCron(id: string): Promise<boolean>;
|
package/dist/client/bunqueue.js
CHANGED
|
@@ -171,10 +171,10 @@ export class Bunqueue {
|
|
|
171
171
|
}
|
|
172
172
|
// ============ Cron ============
|
|
173
173
|
cron(id, pattern, data, opts) {
|
|
174
|
-
return this.queue.upsertJobScheduler(id, { pattern, timezone: opts?.timezone }, { name: id, data, opts: opts?.jobOpts });
|
|
174
|
+
return this.queue.upsertJobScheduler(id, { pattern, timezone: opts?.timezone, limit: opts?.limit }, { name: id, data, opts: opts?.jobOpts });
|
|
175
175
|
}
|
|
176
176
|
every(id, intervalMs, data, opts) {
|
|
177
|
-
return this.queue.upsertJobScheduler(id, { every: intervalMs }, { name: id, data, opts: opts?.jobOpts });
|
|
177
|
+
return this.queue.upsertJobScheduler(id, { every: intervalMs, limit: opts?.limit }, { name: id, data, opts: opts?.jobOpts });
|
|
178
178
|
}
|
|
179
179
|
removeCron(id) {
|
|
180
180
|
return this.queue.removeJobScheduler(id);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Creates simple Job objects for FlowProducer results
|
|
4
4
|
*/
|
|
5
5
|
import { getSharedManager } from './manager';
|
|
6
|
+
import { buildFailCommand, failEmbeddedArgs } from './queue/failWire';
|
|
6
7
|
import { jobId } from '../domain/types/job';
|
|
7
8
|
/** Extract user data (remove internal fields like __parentId, __childrenIds, name) */
|
|
8
9
|
export function extractUserDataFromInternal(data) {
|
|
@@ -254,12 +255,13 @@ export function createFlowJobObject(id, name, data, queueName, callbacks) {
|
|
|
254
255
|
return null;
|
|
255
256
|
},
|
|
256
257
|
moveToFailed: async (error) => {
|
|
258
|
+
// Forward stacktrace + UnrecoverableError (was dropped; #74/#111-class).
|
|
257
259
|
if (embedded) {
|
|
258
|
-
await getSharedManager().fail(jobId(id), error
|
|
260
|
+
await getSharedManager().fail(jobId(id), ...failEmbeddedArgs(error));
|
|
259
261
|
return;
|
|
260
262
|
}
|
|
261
263
|
if (tcp)
|
|
262
|
-
await tcp.send(
|
|
264
|
+
await tcp.send(buildFailCommand(id, error));
|
|
263
265
|
},
|
|
264
266
|
moveToWait: async () => {
|
|
265
267
|
if (embedded)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared FAIL payload builder.
|
|
3
|
+
*
|
|
4
|
+
* Several client entry points move a job to `failed` (the Queue reflection API
|
|
5
|
+
* `queue.getJob(id).moveToFailed()`, the job proxies, and `moveJobToFailed`).
|
|
6
|
+
* They must all persist the stacktrace (#74) and honour `UnrecoverableError`,
|
|
7
|
+
* exactly like the worker failure path — otherwise the stack and the
|
|
8
|
+
* "do not retry" intent are silently dropped (the #111 silent-loss class).
|
|
9
|
+
* Centralising the mapping here keeps every site consistent.
|
|
10
|
+
*/
|
|
11
|
+
/** Build the TCP `FAIL` command for an explicit job failure. */
|
|
12
|
+
export declare function buildFailCommand(id: string, error: Error, token?: string): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Decompose an error into the trailing args of the embedded
|
|
15
|
+
* `manager.fail(jobId, error?, token?, unrecoverable?, stack?)` signature.
|
|
16
|
+
*/
|
|
17
|
+
export declare function failEmbeddedArgs(error: Error, token?: string): [string, string | undefined, boolean, string[] | undefined];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared FAIL payload builder.
|
|
3
|
+
*
|
|
4
|
+
* Several client entry points move a job to `failed` (the Queue reflection API
|
|
5
|
+
* `queue.getJob(id).moveToFailed()`, the job proxies, and `moveJobToFailed`).
|
|
6
|
+
* They must all persist the stacktrace (#74) and honour `UnrecoverableError`,
|
|
7
|
+
* exactly like the worker failure path — otherwise the stack and the
|
|
8
|
+
* "do not retry" intent are silently dropped (the #111 silent-loss class).
|
|
9
|
+
* Centralising the mapping here keeps every site consistent.
|
|
10
|
+
*/
|
|
11
|
+
import { UnrecoverableError } from '../errors';
|
|
12
|
+
import { computeStackLines } from '../worker/processorHandlers';
|
|
13
|
+
/** Build the TCP `FAIL` command for an explicit job failure. */
|
|
14
|
+
export function buildFailCommand(id, error, token) {
|
|
15
|
+
const { wireStack } = computeStackLines(error);
|
|
16
|
+
return {
|
|
17
|
+
cmd: 'FAIL',
|
|
18
|
+
id,
|
|
19
|
+
error: error.message,
|
|
20
|
+
...(wireStack ? { stack: wireStack } : {}),
|
|
21
|
+
...(token ? { token } : {}),
|
|
22
|
+
...(error instanceof UnrecoverableError ? { unrecoverable: true } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Decompose an error into the trailing args of the embedded
|
|
27
|
+
* `manager.fail(jobId, error?, token?, unrecoverable?, stack?)` signature.
|
|
28
|
+
*/
|
|
29
|
+
export function failEmbeddedArgs(error, token) {
|
|
30
|
+
const { wireStack } = computeStackLines(error);
|
|
31
|
+
return [error.message, token, error instanceof UnrecoverableError, wireStack];
|
|
32
|
+
}
|
|
@@ -15,7 +15,7 @@ interface JobMoveContext {
|
|
|
15
15
|
/** Move job to completed state */
|
|
16
16
|
export declare function moveJobToCompleted(ctx: JobMoveContext, id: string, returnValue: unknown, _token?: string): Promise<unknown>;
|
|
17
17
|
/** Move job to failed state */
|
|
18
|
-
export declare function moveJobToFailed(ctx: JobMoveContext, id: string, error: Error,
|
|
18
|
+
export declare function moveJobToFailed(ctx: JobMoveContext, id: string, error: Error, token?: string): Promise<void>;
|
|
19
19
|
/** Move job back to waiting state */
|
|
20
20
|
export declare function moveJobToWait(ctx: JobMoveContext, id: string, _token?: string): Promise<boolean>;
|
|
21
21
|
/** Move job to delayed state */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getSharedManager } from '../manager';
|
|
6
6
|
import { jobId } from '../../domain/types/job';
|
|
7
|
+
import { buildFailCommand, failEmbeddedArgs } from './failWire';
|
|
7
8
|
/** Move job to completed state */
|
|
8
9
|
export async function moveJobToCompleted(ctx, id, returnValue, _token) {
|
|
9
10
|
if (ctx.embedded) {
|
|
@@ -14,12 +15,16 @@ export async function moveJobToCompleted(ctx, id, returnValue, _token) {
|
|
|
14
15
|
return null;
|
|
15
16
|
}
|
|
16
17
|
/** Move job to failed state */
|
|
17
|
-
export async function moveJobToFailed(ctx, id, error,
|
|
18
|
+
export async function moveJobToFailed(ctx, id, error, token) {
|
|
19
|
+
// Mirror the worker failure path: persist the stacktrace (#74) and honour
|
|
20
|
+
// UnrecoverableError so a job explicitly failed via the Queue reflection API
|
|
21
|
+
// is not silently retried and does not lose its stack. Previously both were
|
|
22
|
+
// dropped on this path (same silent-loss class as #111).
|
|
18
23
|
if (ctx.embedded) {
|
|
19
|
-
await getSharedManager().fail(jobId(id), error
|
|
24
|
+
await getSharedManager().fail(jobId(id), ...failEmbeddedArgs(error, token));
|
|
20
25
|
}
|
|
21
26
|
else {
|
|
22
|
-
await ctx.tcp.send(
|
|
27
|
+
await ctx.tcp.send(buildFailCommand(id, error, token));
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
30
|
/** Move job back to waiting state */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getSharedManager } from '../manager';
|
|
6
6
|
import { jobId } from '../../domain/types/job';
|
|
7
|
+
import { buildFailCommand, failEmbeddedArgs } from './failWire';
|
|
7
8
|
function reflectFields(id, queueName, meta) {
|
|
8
9
|
const opts = meta?.opts ?? {};
|
|
9
10
|
const p = opts.parent;
|
|
@@ -130,7 +131,8 @@ export function createJobProxy(id, name, data, ctx, meta) {
|
|
|
130
131
|
return null;
|
|
131
132
|
},
|
|
132
133
|
moveToFailed: async (error) => {
|
|
133
|
-
|
|
134
|
+
// Forward stacktrace + UnrecoverableError (was dropped; #74/#111-class).
|
|
135
|
+
await tcp.send(buildFailCommand(id, error));
|
|
134
136
|
},
|
|
135
137
|
moveToWait: async () => {
|
|
136
138
|
const res = await tcp.send({ cmd: 'MoveToWait', id });
|
|
@@ -352,12 +354,13 @@ export function createSimpleJob(id, name, data, timestamp, ctx) {
|
|
|
352
354
|
return null;
|
|
353
355
|
},
|
|
354
356
|
moveToFailed: async (error) => {
|
|
357
|
+
// Forward stacktrace + UnrecoverableError (was dropped; #74/#111-class).
|
|
355
358
|
if (embedded) {
|
|
356
|
-
await getSharedManager().fail(jobId(id), error
|
|
359
|
+
await getSharedManager().fail(jobId(id), ...failEmbeddedArgs(error));
|
|
357
360
|
return;
|
|
358
361
|
}
|
|
359
362
|
if (tcp)
|
|
360
|
-
await tcp.send(
|
|
363
|
+
await tcp.send(buildFailCommand(id, error));
|
|
361
364
|
},
|
|
362
365
|
moveToWait: async () => {
|
|
363
366
|
if (embedded) {
|
|
@@ -56,7 +56,8 @@ export async function retryJob(ctx, id) {
|
|
|
56
56
|
export async function retryJobs(ctx, opts) {
|
|
57
57
|
if (ctx.embedded) {
|
|
58
58
|
if (opts?.state === 'failed') {
|
|
59
|
-
|
|
59
|
+
// #111-class: forward the count cap (was ignored → retried the whole DLQ).
|
|
60
|
+
getSharedManager().retryDlq(ctx.name, undefined, opts?.count);
|
|
60
61
|
}
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
@@ -45,6 +45,8 @@ export interface SchedulerInfo {
|
|
|
45
45
|
next: number;
|
|
46
46
|
pattern?: string;
|
|
47
47
|
every?: number;
|
|
48
|
+
/** Max number of times the scheduler will fire (RepeatOpts.limit). #111 */
|
|
49
|
+
limit?: number;
|
|
48
50
|
}
|
|
49
51
|
/** Create or update a job scheduler */
|
|
50
52
|
export declare function upsertJobScheduler(ctx: SchedulerContext, schedulerId: string, repeatOpts: RepeatOpts, jobTemplate?: JobTemplate): Promise<SchedulerInfo | null>;
|
|
@@ -78,6 +78,8 @@ export async function upsertJobScheduler(ctx, schedulerId, repeatOpts, jobTempla
|
|
|
78
78
|
repeatEvery,
|
|
79
79
|
priority,
|
|
80
80
|
timezone: repeatOpts.timezone ?? 'UTC',
|
|
81
|
+
// #111: forward the run cap; addCron ignores <=0/undefined (stored NULL).
|
|
82
|
+
maxLimit: repeatOpts.limit,
|
|
81
83
|
skipMissedOnRestart: repeatOpts.skipMissedOnRestart,
|
|
82
84
|
immediately: repeatOpts.immediately,
|
|
83
85
|
skipIfNoWorker: repeatOpts.skipIfNoWorker,
|
|
@@ -89,6 +91,7 @@ export async function upsertJobScheduler(ctx, schedulerId, repeatOpts, jobTempla
|
|
|
89
91
|
id: schedulerId,
|
|
90
92
|
name: jobTemplate?.name ?? 'default',
|
|
91
93
|
next: Date.now() + (repeatEvery ?? 60000),
|
|
94
|
+
limit: repeatOpts.limit && repeatOpts.limit > 0 ? repeatOpts.limit : undefined,
|
|
92
95
|
};
|
|
93
96
|
}
|
|
94
97
|
const response = await ctx.tcp.send({
|
|
@@ -100,6 +103,8 @@ export async function upsertJobScheduler(ctx, schedulerId, repeatOpts, jobTempla
|
|
|
100
103
|
repeatEvery,
|
|
101
104
|
priority,
|
|
102
105
|
timezone: repeatOpts.timezone,
|
|
106
|
+
// #111: forward the run cap on the TCP path too.
|
|
107
|
+
maxLimit: repeatOpts.limit,
|
|
103
108
|
skipMissedOnRestart: repeatOpts.skipMissedOnRestart,
|
|
104
109
|
immediately: repeatOpts.immediately,
|
|
105
110
|
skipIfNoWorker: repeatOpts.skipIfNoWorker,
|
|
@@ -113,6 +118,7 @@ export async function upsertJobScheduler(ctx, schedulerId, repeatOpts, jobTempla
|
|
|
113
118
|
id: schedulerId,
|
|
114
119
|
name: jobTemplate?.name ?? 'default',
|
|
115
120
|
next: (response.nextRun ?? Date.now()),
|
|
121
|
+
limit: repeatOpts.limit && repeatOpts.limit > 0 ? repeatOpts.limit : undefined,
|
|
116
122
|
};
|
|
117
123
|
}
|
|
118
124
|
/** Remove a job scheduler */
|
|
@@ -139,6 +145,7 @@ export async function getJobScheduler(ctx, schedulerId) {
|
|
|
139
145
|
next: cron.nextRun,
|
|
140
146
|
pattern: cron.schedule ?? undefined,
|
|
141
147
|
every: cron.repeatEvery ?? undefined,
|
|
148
|
+
limit: cron.maxLimit ?? undefined,
|
|
142
149
|
};
|
|
143
150
|
}
|
|
144
151
|
const response = await ctx.tcp.send({ cmd: 'CronList' });
|
|
@@ -154,6 +161,7 @@ export async function getJobScheduler(ctx, schedulerId) {
|
|
|
154
161
|
next: cron.nextRun,
|
|
155
162
|
pattern: cron.schedule ?? undefined,
|
|
156
163
|
every: cron.repeatEvery ?? undefined,
|
|
164
|
+
limit: cron.maxLimit ?? undefined,
|
|
157
165
|
};
|
|
158
166
|
}
|
|
159
167
|
/** Get all job schedulers for this queue */
|
|
@@ -168,6 +176,7 @@ export async function getJobSchedulers(ctx, _start = 0, _end = -1, _asc = true)
|
|
|
168
176
|
next: c.nextRun,
|
|
169
177
|
pattern: c.schedule ?? undefined,
|
|
170
178
|
every: c.repeatEvery ?? undefined,
|
|
179
|
+
limit: c.maxLimit ?? undefined,
|
|
171
180
|
}));
|
|
172
181
|
}
|
|
173
182
|
const response = await ctx.tcp.send({ cmd: 'CronList' });
|
|
@@ -182,6 +191,7 @@ export async function getJobSchedulers(ctx, _start = 0, _end = -1, _asc = true)
|
|
|
182
191
|
next: c.nextRun,
|
|
183
192
|
pattern: c.schedule ?? undefined,
|
|
184
193
|
every: c.repeatEvery ?? undefined,
|
|
194
|
+
limit: c.maxLimit ?? undefined,
|
|
185
195
|
}));
|
|
186
196
|
}
|
|
187
197
|
/** Get count of job schedulers */
|
|
@@ -7,6 +7,7 @@ import { getSharedManager } from '../manager';
|
|
|
7
7
|
import { getSharedPool, releaseSharedPool } from '../tcpPool';
|
|
8
8
|
import { createPublicJob } from '../jobConversion';
|
|
9
9
|
import { jobId } from '../../domain/types/job';
|
|
10
|
+
import { buildFailCommand, failEmbeddedArgs } from '../queue/failWire';
|
|
10
11
|
import { createProgressHandler, createLogHandler, createGetStateHandler, createGetChildrenValuesHandler, createGetFailedChildrenValuesHandler, createGetIgnoredChildrenFailuresHandler, createRemoveChildDependencyHandler, createRemoveUnprocessedChildrenHandler, createRemoveHandler, createRetryHandler, createUpdateDataHandler, createPromoteHandler, createChangeDelayHandler, createChangePriorityHandler, createExtendLockHandler, createClearLogsHandler, createMoveToWaitHandler, createMoveToDelayedHandler, createMoveToWaitingChildrenHandler, createWaitUntilFinishedHandler, createDiscardHandler, createGetDependenciesHandler, createGetDependenciesCountHandler, createRemoveDeduplicationKeyHandler, } from '../worker/processorHandlers';
|
|
11
12
|
import { createWrapperScript, cleanupWrapperScript } from './wrapper';
|
|
12
13
|
import { createEmbeddedOps, createTcpOps } from './queueOps';
|
|
@@ -514,12 +515,13 @@ export class SandboxedWorker extends EventEmitter {
|
|
|
514
515
|
}
|
|
515
516
|
return null;
|
|
516
517
|
};
|
|
517
|
-
const moveToFailed = async (id, error,
|
|
518
|
+
const moveToFailed = async (id, error, token) => {
|
|
519
|
+
// Forward stacktrace + UnrecoverableError (was dropped; #74/#111-class).
|
|
518
520
|
if (embedded) {
|
|
519
|
-
await getSharedManager().fail(jobId(id), error
|
|
521
|
+
await getSharedManager().fail(jobId(id), ...failEmbeddedArgs(error, token));
|
|
520
522
|
}
|
|
521
523
|
else if (tcp) {
|
|
522
|
-
await tcp.send(
|
|
524
|
+
await tcp.send(buildFailCommand(id, error, token));
|
|
523
525
|
}
|
|
524
526
|
};
|
|
525
527
|
return createPublicJob({
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { jobId } from '../../domain/types/job';
|
|
6
6
|
import { getSharedManager } from '../manager';
|
|
7
|
+
import { UnrecoverableError } from '../errors';
|
|
7
8
|
export function createProgressHandler(embedded, tcp, emitter, jobHolder) {
|
|
8
9
|
return async (id, progress, message) => {
|
|
9
10
|
if (embedded) {
|
|
@@ -126,9 +127,13 @@ export function createMoveToFailedHandler(embedded, tcp, internalJob, token, onC
|
|
|
126
127
|
// natural-throw path already does; @arthurvanl's repro showed the manual
|
|
127
128
|
// path lost it. Compute before the send so the server can persist it.
|
|
128
129
|
const { wireStack } = computeStackLines(error);
|
|
130
|
+
// Honour UnrecoverableError on the in-processor moveToFailed() path too, so
|
|
131
|
+
// it matches the natural-throw and Queue-reflection paths (skeptic: this was
|
|
132
|
+
// the one site still retrying an UnrecoverableError). #74/#82/#111-class.
|
|
133
|
+
const unrecoverable = error instanceof UnrecoverableError;
|
|
129
134
|
if (embedded) {
|
|
130
135
|
const manager = getSharedManager();
|
|
131
|
-
await manager.fail(internalJob.id, error.message, token ?? undefined,
|
|
136
|
+
await manager.fail(internalJob.id, error.message, token ?? undefined, unrecoverable, wireStack);
|
|
132
137
|
}
|
|
133
138
|
else if (tcp) {
|
|
134
139
|
await tcp.send({
|
|
@@ -137,6 +142,7 @@ export function createMoveToFailedHandler(embedded, tcp, internalJob, token, onC
|
|
|
137
142
|
error: error.message,
|
|
138
143
|
...(wireStack ? { stack: wireStack } : {}),
|
|
139
144
|
...(token ? { token } : {}),
|
|
145
|
+
...(unrecoverable ? { unrecoverable: true } : {}),
|
|
140
146
|
});
|
|
141
147
|
}
|
|
142
148
|
onCalled(error);
|
|
@@ -322,7 +322,11 @@ export class Worker extends EventEmitter {
|
|
|
322
322
|
if (this.embedded) {
|
|
323
323
|
const manager = getSharedManager();
|
|
324
324
|
if (this.opts.useLocks) {
|
|
325
|
-
|
|
325
|
+
// Forward the configured lock duration; without it pullWithLock falls
|
|
326
|
+
// back to the server default and a custom `lockDuration` is silently
|
|
327
|
+
// ignored on this manual-acquire path (same class as #111). The main
|
|
328
|
+
// run-loop path (workerPull.ts) already forwards it.
|
|
329
|
+
const { job, token: lockToken } = await manager.pullWithLock(this.queueKey, this.workerId, 0, this.opts.lockDuration);
|
|
326
330
|
if (job && lockToken) {
|
|
327
331
|
const jobIdStr = String(job.id);
|
|
328
332
|
this.pulledJobIds.add(jobIdStr);
|
|
@@ -346,6 +350,10 @@ export class Worker extends EventEmitter {
|
|
|
346
350
|
};
|
|
347
351
|
if (this.opts.useLocks) {
|
|
348
352
|
cmd.owner = this.workerId;
|
|
353
|
+
// Forward the configured lock TTL (mirrors workerPull.ts); otherwise the
|
|
354
|
+
// server applies its 30s default and a custom lockDuration is dropped.
|
|
355
|
+
if (this.opts.lockDuration !== undefined)
|
|
356
|
+
cmd.lockTtl = this.opts.lockDuration;
|
|
349
357
|
if (token)
|
|
350
358
|
cmd.token = token;
|
|
351
359
|
}
|
|
@@ -237,6 +237,8 @@ export interface RetryDlqCommand extends BaseCommand {
|
|
|
237
237
|
readonly cmd: 'RetryDlq';
|
|
238
238
|
readonly queue: string;
|
|
239
239
|
readonly jobId?: string;
|
|
240
|
+
/** Cap the number of DLQ entries retried (omit = retry all). #111-class. */
|
|
241
|
+
readonly count?: number;
|
|
240
242
|
}
|
|
241
243
|
export interface PurgeDlqCommand extends BaseCommand {
|
|
242
244
|
readonly cmd: 'PurgeDlq';
|
|
@@ -12,7 +12,8 @@ export function handleDlq(cmd, ctx, reqId) {
|
|
|
12
12
|
/** Handle RetryDlq command - retry DLQ jobs */
|
|
13
13
|
export function handleRetryDlq(cmd, ctx, reqId) {
|
|
14
14
|
const jid = cmd.jobId ? jobId(cmd.jobId) : undefined;
|
|
15
|
-
|
|
15
|
+
// #111-class: honour the caller's `count` cap instead of retrying the whole DLQ.
|
|
16
|
+
const count = ctx.queueManager.retryDlq(cmd.queue, jid, cmd.count);
|
|
16
17
|
if (count > 0) {
|
|
17
18
|
const event = jid ? 'dlq:retried' : 'dlq:retry-all';
|
|
18
19
|
const data = { queue: cmd.queue, count };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunqueue",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.29",
|
|
4
4
|
"description": "High-performance job queue for Bun & AI agents. SQLite persistence, cron scheduling, priorities, retries, DLQ, webhooks, native MCP server. Zero external dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|