bunqueue-client 0.2.0 → 0.2.1
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/CHANGELOG.md +15 -0
- package/dist/canonical-manifest.json +57 -53
- package/dist/embedded.js +1353 -1261
- package/dist/index.js +52 -4
- package/dist/legacy.js +1 -1
- package/dist/types/sdk/typescript/src/index.d.ts +1 -1
- package/dist/types/sdk/typescript/src/legacy.d.ts +1 -1
- package/dist/types/src/application/clientOwnership.d.ts +65 -0
- package/dist/types/src/application/clientTracking.d.ts +7 -11
- package/dist/types/src/application/operations/jobClaim.d.ts +2 -2
- package/dist/types/src/application/operations/jobManagement.d.ts +2 -0
- package/dist/types/src/application/orphanRecovery.d.ts +25 -0
- package/dist/types/src/application/queue-manager/state.d.ts +2 -0
- package/dist/types/src/application/stallDetection.d.ts +16 -0
- package/dist/types/src/application/types/contextFactory.d.ts +2 -0
- package/dist/types/src/application/types/contexts.d.ts +3 -0
- package/dist/types/src/application/types/queueManager.d.ts +2 -0
- package/dist/types/src/client/forwarder.d.ts +4 -3
- package/dist/types/src/client/types/options.d.ts +2 -0
- package/dist/types/src/domain/job/constants.d.ts +2 -0
- package/dist/types/src/domain/job/create.d.ts +7 -0
- package/dist/types/src/domain/job/locks.d.ts +13 -1
- package/dist/types/src/domain/job/state.d.ts +15 -0
- package/dist/types/src/domain/types/commands/core.d.ts +2 -5
- package/dist/types/src/domain/types/cron.d.ts +2 -4
- package/dist/types/src/domain/types/jobs/model.d.ts +1 -4
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/legacy.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -187,6 +187,18 @@ class BatchExecution {
|
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
+
// ../../src/domain/job/constants.ts
|
|
191
|
+
var DEFAULT_MAX_BACKOFF = 3600000;
|
|
192
|
+
var JOB_DEFAULTS = {
|
|
193
|
+
priority: 0,
|
|
194
|
+
maxAttempts: 3,
|
|
195
|
+
backoff: 1000,
|
|
196
|
+
lifo: false,
|
|
197
|
+
removeOnComplete: false,
|
|
198
|
+
removeOnFail: false,
|
|
199
|
+
stackTraceLimit: 10
|
|
200
|
+
};
|
|
201
|
+
var MAX_BACKOFF_DELAY = 86400000;
|
|
190
202
|
// ../../src/domain/job/payload.ts
|
|
191
203
|
var DEFAULT_JOB_NAME = "default";
|
|
192
204
|
var MAX_JOB_NAME_LENGTH = 256;
|
|
@@ -225,6 +237,13 @@ function normalizeLegacyJobPayload(source, fallback = DEFAULT_JOB_NAME) {
|
|
|
225
237
|
return { name: checkedName(legacy.name), data: legacy.data, legacy: true };
|
|
226
238
|
return { name: checkedName(fallback), data: source.data, legacy: false };
|
|
227
239
|
}
|
|
240
|
+
|
|
241
|
+
// ../../src/domain/job/create.ts
|
|
242
|
+
function parseMaxDelay(value) {
|
|
243
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
244
|
+
return;
|
|
245
|
+
return value >= 0 && value <= MAX_BACKOFF_DELAY ? value : undefined;
|
|
246
|
+
}
|
|
228
247
|
// src/canonical-transport/runtime.ts
|
|
229
248
|
import { access, unlink } from "node:fs/promises";
|
|
230
249
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -340,6 +359,17 @@ function jobId(id) {
|
|
|
340
359
|
function generateJobId() {
|
|
341
360
|
return uuid();
|
|
342
361
|
}
|
|
362
|
+
// ../../src/domain/job/state.ts
|
|
363
|
+
function calculateDelayedErrorDelay(job) {
|
|
364
|
+
const base = job.backoffConfig ? job.backoffConfig.delay : job.backoff;
|
|
365
|
+
const maxDelay = job.backoffConfig?.maxDelay;
|
|
366
|
+
const cap = isPositiveFinite(maxDelay) ? maxDelay : DEFAULT_MAX_BACKOFF;
|
|
367
|
+
const wait = typeof base === "number" && base > 0 ? base : JOB_DEFAULTS.backoff;
|
|
368
|
+
return Math.min(wait, cap);
|
|
369
|
+
}
|
|
370
|
+
function isPositiveFinite(value) {
|
|
371
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
372
|
+
}
|
|
343
373
|
// ../../src/client/jobHelpers.ts
|
|
344
374
|
function resolvePublicJobPayload(job) {
|
|
345
375
|
const payload = normalizeLegacyJobPayload(job);
|
|
@@ -387,7 +417,12 @@ function buildParentOpts(job) {
|
|
|
387
417
|
return;
|
|
388
418
|
}
|
|
389
419
|
function buildJobOpts(job) {
|
|
390
|
-
const
|
|
420
|
+
const config = job.backoffConfig;
|
|
421
|
+
const backoff = config ? {
|
|
422
|
+
type: config.type,
|
|
423
|
+
delay: config.delay,
|
|
424
|
+
...typeof config.maxDelay === "number" ? { maxDelay: config.maxDelay } : {}
|
|
425
|
+
} : job.backoff;
|
|
391
426
|
return {
|
|
392
427
|
priority: job.priority,
|
|
393
428
|
delay: job.runAt > job.createdAt ? job.runAt - job.createdAt : 0,
|
|
@@ -1322,13 +1357,14 @@ async function handleDelayedError(internalJob, config, context) {
|
|
|
1322
1357
|
if (config.shouldAbandonOutcome?.())
|
|
1323
1358
|
return;
|
|
1324
1359
|
try {
|
|
1360
|
+
const delay = calculateDelayedErrorDelay(internalJob);
|
|
1325
1361
|
if (embedded) {
|
|
1326
|
-
await getSharedManager().moveToDelayed(internalJob.id,
|
|
1362
|
+
await getSharedManager().moveToDelayed(internalJob.id, delay, context.token ?? undefined);
|
|
1327
1363
|
} else if (tcp) {
|
|
1328
1364
|
await tcp.send({
|
|
1329
1365
|
cmd: "MoveToDelayed",
|
|
1330
1366
|
id: internalJob.id,
|
|
1331
|
-
delay
|
|
1367
|
+
delay,
|
|
1332
1368
|
...context.token ? { token: context.token } : {}
|
|
1333
1369
|
});
|
|
1334
1370
|
}
|
|
@@ -1536,6 +1572,17 @@ var WORKER_CONSTANTS = {
|
|
|
1536
1572
|
};
|
|
1537
1573
|
|
|
1538
1574
|
// ../../src/client/worker/jobParser.ts
|
|
1575
|
+
function parseBackoffConfig(value) {
|
|
1576
|
+
if (typeof value !== "object" || value === null)
|
|
1577
|
+
return null;
|
|
1578
|
+
const { type, delay, maxDelay } = value;
|
|
1579
|
+
if (type !== "fixed" && type !== "exponential")
|
|
1580
|
+
return null;
|
|
1581
|
+
if (typeof delay !== "number" || !Number.isFinite(delay))
|
|
1582
|
+
return null;
|
|
1583
|
+
const cap = parseMaxDelay(maxDelay);
|
|
1584
|
+
return cap === undefined ? { type, delay } : { type, delay, maxDelay: cap };
|
|
1585
|
+
}
|
|
1539
1586
|
function parseJobFromResponse(jobData, queueName) {
|
|
1540
1587
|
const payload = normalizeLegacyJobPayload({ name: jobData.name, data: jobData.data });
|
|
1541
1588
|
return {
|
|
@@ -1551,6 +1598,7 @@ function parseJobFromResponse(jobData, queueName) {
|
|
|
1551
1598
|
attempts: jobData.attempts ?? 0,
|
|
1552
1599
|
maxAttempts: jobData.maxAttempts ?? 3,
|
|
1553
1600
|
backoff: jobData.backoff ?? 1000,
|
|
1601
|
+
backoffConfig: parseBackoffConfig(jobData.backoffConfig),
|
|
1554
1602
|
ttl: jobData.ttl ?? null,
|
|
1555
1603
|
timeout: jobData.timeout ?? null,
|
|
1556
1604
|
uniqueKey: jobData.uniqueKey ?? null,
|
|
@@ -10867,7 +10915,7 @@ class FlowProducer extends EventEmitter7 {
|
|
|
10867
10915
|
}
|
|
10868
10916
|
}
|
|
10869
10917
|
// src/index.ts
|
|
10870
|
-
var __version__ = "0.2.
|
|
10918
|
+
var __version__ = "0.2.1";
|
|
10871
10919
|
export {
|
|
10872
10920
|
AuthError2 as AuthError,
|
|
10873
10921
|
Bunqueue,
|
package/dist/legacy.js
CHANGED
|
@@ -7,4 +7,4 @@ export { AuthError, BunqueueError, CommandError, CommandTimeoutError, Connection
|
|
|
7
7
|
export { consoleLogger, noopLogger } from "./observability.js";
|
|
8
8
|
export type { Logger, LogLevel, Observability, TelemetryEvent, TelemetryHandler, } from "./observability.js";
|
|
9
9
|
export { MAX_FRAME_SIZE, PROTOCOL_VERSION } from "./frame.js";
|
|
10
|
-
export declare const __version__ = "0.2.
|
|
10
|
+
export declare const __version__ = "0.2.1";
|
|
@@ -24,4 +24,4 @@ export type { BatchResponse, CountResponse, DataResponse, JobCountsResponse, Job
|
|
|
24
24
|
export type { BackoffOptions, DeduplicationOptions, JobCounts, JobOptions, JobStateName, RepeatOptions, } from "./types.js";
|
|
25
25
|
export { Worker } from "./worker.js";
|
|
26
26
|
export type { AckBatchOptions, Processor, WorkerEventMap, WorkerOptions } from "./worker-types.js";
|
|
27
|
-
export declare const __version__ = "0.2.
|
|
27
|
+
export declare const __version__ = "0.2.1";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Ownership - which connection owns a job's current delivery.
|
|
3
|
+
*
|
|
4
|
+
* `clientJobs` maps a connection to the jobs it pulled. `clientJobOwners` is
|
|
5
|
+
* its reverse index: one record per owned job naming the owning connection and
|
|
6
|
+
* the delivery it owns. A delivery is identified by the processing
|
|
7
|
+
* `JobLocation` object the pull installed in `jobIndex`. Only a pull installs a
|
|
8
|
+
* processing location, and every pull installs a fresh object, so the record
|
|
9
|
+
* of an earlier delivery never matches a later delivery of the same job id.
|
|
10
|
+
*
|
|
11
|
+
* Every mutation of the two maps goes through this module so they stay in
|
|
12
|
+
* step: a job is in `clientJobs.get(c)` exactly when its owner record names
|
|
13
|
+
* `c`. Every operation is O(1) per job.
|
|
14
|
+
*/
|
|
15
|
+
import type { JobId } from "../domain/types/job.js";
|
|
16
|
+
import type { JobLocation } from "../domain/types/queue.js";
|
|
17
|
+
/** The connection that registered a job's delivery, and that delivery. */
|
|
18
|
+
export interface ClientJobOwner {
|
|
19
|
+
readonly clientId: string;
|
|
20
|
+
/** The `jobIndex` entry current when the connection registered the job. */
|
|
21
|
+
readonly delivery: JobLocation | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** The two ownership maps plus the index that identifies deliveries. */
|
|
24
|
+
export interface ClientOwnershipContext {
|
|
25
|
+
jobIndex: Map<JobId, JobLocation>;
|
|
26
|
+
clientJobs: Map<string, Set<JobId>>;
|
|
27
|
+
clientJobOwners: Map<JobId, ClientJobOwner>;
|
|
28
|
+
}
|
|
29
|
+
/** The maps a detach touches; it never needs the job index. */
|
|
30
|
+
export type ClientOwnershipMaps = Pick<ClientOwnershipContext, 'clientJobs' | 'clientJobOwners'>;
|
|
31
|
+
/**
|
|
32
|
+
* Register `clientId` as the owner of the job's current delivery (called after
|
|
33
|
+
* PULL). A delivery has one owner, so any previous owner is detached first.
|
|
34
|
+
*/
|
|
35
|
+
export declare function registerClientJob(clientId: string, jobId: JobId, ctx: ClientOwnershipContext): void;
|
|
36
|
+
/**
|
|
37
|
+
* Remove one connection's claim on a job (ACK/FAIL handled on that
|
|
38
|
+
* connection). Another connection's record for the same job is left alone: it
|
|
39
|
+
* may already own a newer delivery.
|
|
40
|
+
*/
|
|
41
|
+
export declare function unregisterClientJob(clientId: string | undefined, jobId: JobId, ctx: ClientOwnershipMaps): void;
|
|
42
|
+
/**
|
|
43
|
+
* End ownership of a job's delivery, whoever holds it. Every transition that
|
|
44
|
+
* ends a delivery without the owner's ACK/FAIL calls this (stall recovery,
|
|
45
|
+
* orphan recovery, lock expiry, timeout, management claims, disconnect
|
|
46
|
+
* release), so a silent connection never keeps a job it no longer runs.
|
|
47
|
+
*/
|
|
48
|
+
export declare function detachClientJob(jobId: JobId, ctx: ClientOwnershipMaps): void;
|
|
49
|
+
/**
|
|
50
|
+
* True only when `clientId` registered the job's current delivery: the job is
|
|
51
|
+
* processing and its processing entry is the one the connection registered.
|
|
52
|
+
* Disconnect release acts on nothing else, so a stale registration can never
|
|
53
|
+
* release or expire a later delivery owned by another worker.
|
|
54
|
+
*/
|
|
55
|
+
export declare function ownsCurrentDelivery(clientId: string, jobId: JobId, ctx: ClientOwnershipContext): boolean;
|
|
56
|
+
/** Forget a disconnected client: its set and only its own owner records. */
|
|
57
|
+
export declare function dropClient(clientId: string, ctx: ClientOwnershipMaps): void;
|
|
58
|
+
/**
|
|
59
|
+
* Detach every record whose delivery has ended. An outcome sent on another
|
|
60
|
+
* connection than the pull (pooled clients) unregisters the sender, not the
|
|
61
|
+
* owner, so such records would otherwise live as long as the pulling
|
|
62
|
+
* connection. Runs synchronously from periodic cleanup.
|
|
63
|
+
* @returns the number of records removed.
|
|
64
|
+
*/
|
|
65
|
+
export declare function pruneEndedClientDeliveries(ctx: ClientOwnershipContext): number;
|
|
@@ -2,28 +2,24 @@
|
|
|
2
2
|
* Client Tracking - Client-job relationship management
|
|
3
3
|
* Handles job ownership and release on client disconnect
|
|
4
4
|
*/
|
|
5
|
-
import type { JobId } from "../domain/types/job.js";
|
|
6
5
|
import type { LockContext } from "./types/index.js";
|
|
7
|
-
|
|
8
|
-
* Register a job as owned by a client (called on PULL).
|
|
9
|
-
*/
|
|
10
|
-
export declare function registerClientJob(clientId: string, jobId: JobId, ctx: LockContext): void;
|
|
11
|
-
/**
|
|
12
|
-
* Unregister a job from a client (called on ACK/FAIL).
|
|
13
|
-
*/
|
|
14
|
-
export declare function unregisterClientJob(clientId: string | undefined, jobId: JobId, ctx: LockContext): void;
|
|
6
|
+
export { registerClientJob, unregisterClientJob } from "./clientOwnership.js";
|
|
15
7
|
/**
|
|
16
8
|
* Release all jobs owned by a client back to queue (called on TCP disconnect).
|
|
17
9
|
* Returns the number of jobs released.
|
|
18
10
|
*
|
|
19
|
-
*
|
|
11
|
+
* Only deliveries the client still owns are touched (`ownsCurrentDelivery`): a
|
|
12
|
+
* job recovered from this client and delivered again to another worker is not
|
|
13
|
+
* this client's to release. The rule is checked when collecting and again
|
|
14
|
+
* under the shard and processing write locks.
|
|
20
15
|
*/
|
|
21
16
|
export declare function releaseClientJobs(clientId: string, ctx: LockContext): Promise<number>;
|
|
22
17
|
/**
|
|
23
18
|
* Force-release client tracking without acquiring queue locks.
|
|
24
19
|
* Used as a last-resort fallback when releaseClientJobs has exhausted its
|
|
25
20
|
* retry budget (e.g. persistent lock contention on TCP disconnect). For each
|
|
26
|
-
*
|
|
21
|
+
* job whose current delivery the client still owns (`ownsCurrentDelivery`;
|
|
22
|
+
* a stale registration must not expire another worker's delivery):
|
|
27
23
|
* - clears `jobLocks` so a stale lock token can't survive the disconnect
|
|
28
24
|
* until its TTL expires;
|
|
29
25
|
* - expires both `lastHeartbeat` (so stall detection's heartbeat check
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* job before its worker can ACK or FAIL it.
|
|
4
4
|
*/
|
|
5
5
|
import type { JobId, JobLock } from "../../domain/types/job.js";
|
|
6
|
-
|
|
6
|
+
import { type ClientOwnershipMaps } from "../clientOwnership.js";
|
|
7
|
+
export interface JobClaimContext extends ClientOwnershipMaps {
|
|
7
8
|
jobLocks: Map<JobId, JobLock>;
|
|
8
|
-
clientJobs: Map<string, Set<JobId>>;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* Release the live lease and detach the job from its TCP client owner.
|
|
@@ -10,6 +10,7 @@ import type { WebhookManager } from "../webhookManager.js";
|
|
|
10
10
|
import type { EventsManager } from "../eventsManager.js";
|
|
11
11
|
import { type RWLock } from "../../shared/lock.js";
|
|
12
12
|
import type { DependencyResultTracker } from "../dependencyResultTracker.js";
|
|
13
|
+
import type { ClientJobOwner } from "../clientOwnership.js";
|
|
13
14
|
import { type DependencyCompletionTracker } from "../dependencyCompletions.js";
|
|
14
15
|
export { discardJob, moveJobToDelayed } from "./jobMoveOperations.js";
|
|
15
16
|
/** Context for job management operations */
|
|
@@ -22,6 +23,7 @@ export interface JobManagementContext {
|
|
|
22
23
|
jobIndex: Map<JobId, JobLocation>;
|
|
23
24
|
jobLocks: Map<JobId, JobLock>;
|
|
24
25
|
clientJobs: Map<string, Set<JobId>>;
|
|
26
|
+
clientJobOwners: Map<JobId, ClientJobOwner>;
|
|
25
27
|
webhookManager: WebhookManager;
|
|
26
28
|
eventsManager: EventsManager;
|
|
27
29
|
repeatChain?: Map<JobId, JobId>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orphan Recovery - cleanup's backstop for the stall checker.
|
|
3
|
+
*
|
|
4
|
+
* An orphan is an active job that has shown no liveness for longer than both
|
|
5
|
+
* a 30-minute floor and its own stall window, on a queue whose stall detection
|
|
6
|
+
* is enabled. It is recovered through the stall path, so anything recovered
|
|
7
|
+
* here the stall checker would also consider stalled. One difference: like lock
|
|
8
|
+
* expiration and startup recovery, it treats `maxStalls: 0` as unlimited,
|
|
9
|
+
* while the stall checker sends a job to the DLQ on its first stall.
|
|
10
|
+
*/
|
|
11
|
+
import type { BackgroundContext } from "./types/index.js";
|
|
12
|
+
/** The shortest silence after which an active job can be an orphan. */
|
|
13
|
+
export declare const ORPHAN_WINDOW_FLOOR_MS: number;
|
|
14
|
+
/**
|
|
15
|
+
* An orphan is a stalled job the stall checker did not reclaim (for example
|
|
16
|
+
* its recovery attempt failed on a lock timeout), so it takes the stall
|
|
17
|
+
* recovery path: concurrency, group and unique-key resources are released,
|
|
18
|
+
* the attempt is counted, client ownership is detached, and the job is retried
|
|
19
|
+
* with backoff or moved to the DLQ, persisted and announced like any stalled
|
|
20
|
+
* job. `handleStalledJob` holds `shardLocks` then `processingLocks`, confirms
|
|
21
|
+
* the same job object is still processing, and only then re-runs this whole
|
|
22
|
+
* predicate (configuration included), because a heartbeat, progress update,
|
|
23
|
+
* lock renewal or stall-config change can land while the sweep waits.
|
|
24
|
+
*/
|
|
25
|
+
export declare function recoverOrphanedProcessingEntries(ctx: BackgroundContext, now: number): Promise<void>;
|
|
@@ -19,6 +19,7 @@ import { DependencyCompletionTracker } from "../dependencyCompletions.js";
|
|
|
19
19
|
import { JobTimeoutScheduler } from "../background/timeouts.js";
|
|
20
20
|
import type { RetiredTimeoutGeneration } from "../types/background.js";
|
|
21
21
|
import { QueueTelemetryJournal } from "../queueTelemetryJournal.js";
|
|
22
|
+
import type { ClientJobOwner } from "../clientOwnership.js";
|
|
22
23
|
export type { QueueManagerConfig };
|
|
23
24
|
export declare abstract class QueueManagerState {
|
|
24
25
|
protected readonly config: typeof DEFAULT_CONFIG & {
|
|
@@ -52,6 +53,7 @@ export declare abstract class QueueManagerState {
|
|
|
52
53
|
protected depFlushRunning: boolean;
|
|
53
54
|
protected readonly jobLocks: Map<JobId, JobLock>;
|
|
54
55
|
protected readonly clientJobs: Map<string, Set<JobId>>;
|
|
56
|
+
protected readonly clientJobOwners: Map<JobId, ClientJobOwner>;
|
|
55
57
|
protected readonly repeatChain: Map<JobId, JobId>;
|
|
56
58
|
protected readonly failedChildrenValues: Map<JobId, Record<string, string>>;
|
|
57
59
|
protected readonly ignoredChildrenFailures: Map<JobId, Record<string, string>>;
|
|
@@ -2,9 +2,25 @@
|
|
|
2
2
|
* Stall Detection - Job stall detection and recovery
|
|
3
3
|
* Uses two-phase detection (like BullMQ) to prevent false positives
|
|
4
4
|
*/
|
|
5
|
+
import type { Job } from "../domain/types/job.js";
|
|
6
|
+
import { StallAction } from "../domain/types/stall.js";
|
|
5
7
|
import type { BackgroundContext } from "./types/index.js";
|
|
6
8
|
/**
|
|
7
9
|
* Check for stalled jobs and handle them
|
|
8
10
|
* Uses two-phase detection to prevent false positives
|
|
9
11
|
*/
|
|
10
12
|
export declare function checkStalledJobs(ctx: BackgroundContext): void;
|
|
13
|
+
/**
|
|
14
|
+
* Caller-specific condition evaluated with both write locks held, right after
|
|
15
|
+
* the stall path confirms the same job object is still in processing. Returning
|
|
16
|
+
* false leaves the job untouched, so a caller can re-verify its own trigger
|
|
17
|
+
* (for example cleanup's orphan liveness rule) atomically with the transition.
|
|
18
|
+
*/
|
|
19
|
+
export type StallRecheck = (job: Job, now: number) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Recover a stalled job: retry it with the attempt counted and backoff applied,
|
|
22
|
+
* or move it to the DLQ once attempts are exhausted or `action` says so.
|
|
23
|
+
* Takes `shardLocks[idx]` then `processingLocks[procIdx]` (lock hierarchy).
|
|
24
|
+
* @returns true when this call performed the transition.
|
|
25
|
+
*/
|
|
26
|
+
export declare function handleStalledJob(job: Job, action: StallAction, ctx: BackgroundContext, recheck?: StallRecheck): Promise<boolean>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Shard } from "../../domain/queue/shard.js";
|
|
2
|
+
import type { ClientJobOwner } from "../clientOwnership.js";
|
|
2
3
|
import type { FailureReason } from "../../domain/types/dlq.js";
|
|
3
4
|
import type { Job, JobId, JobLock } from "../../domain/types/job.js";
|
|
4
5
|
import type { JobLocation } from "../../domain/types/queue.js";
|
|
@@ -41,6 +42,7 @@ export interface ContextDependencies {
|
|
|
41
42
|
jobLogQueues: Map<JobId, string>;
|
|
42
43
|
jobLocks: Map<JobId, JobLock>;
|
|
43
44
|
clientJobs: Map<string, Set<JobId>>;
|
|
45
|
+
clientJobOwners: Map<JobId, ClientJobOwner>;
|
|
44
46
|
stalledCandidates: Set<JobId>;
|
|
45
47
|
pendingDepChecks: Set<JobId>;
|
|
46
48
|
pendingQueueAdmissions: Map<string, number>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Shard } from "../../domain/queue/shard.js";
|
|
2
|
+
import type { ClientJobOwner } from "../clientOwnership.js";
|
|
2
3
|
import type { FailureReason } from "../../domain/types/dlq.js";
|
|
3
4
|
import type { Job, JobId, JobLock } from "../../domain/types/job.js";
|
|
4
5
|
import type { JobLocation } from "../../domain/types/queue.js";
|
|
@@ -38,6 +39,7 @@ export interface QueueManagerState {
|
|
|
38
39
|
readonly retiredCronLeaseTokens: MapLike<JobId, string>;
|
|
39
40
|
readonly timeoutScheduler: JobTimeoutScheduler;
|
|
40
41
|
readonly clientJobs: Map<string, Set<JobId>>;
|
|
42
|
+
readonly clientJobOwners: Map<JobId, ClientJobOwner>;
|
|
41
43
|
readonly stalledCandidates: Set<JobId>;
|
|
42
44
|
readonly pendingDepChecks: Set<JobId>;
|
|
43
45
|
readonly pendingQueueAdmissions: Map<string, number>;
|
|
@@ -70,6 +72,7 @@ export interface LockContext {
|
|
|
70
72
|
jobLocks: Map<JobId, JobLock>;
|
|
71
73
|
retiredCronLeaseTokens: MapLike<JobId, string>;
|
|
72
74
|
clientJobs: Map<string, Set<JobId>>;
|
|
75
|
+
clientJobOwners: Map<JobId, ClientJobOwner>;
|
|
73
76
|
processingShards: Map<JobId, Job>[];
|
|
74
77
|
processingLocks: RWLock[];
|
|
75
78
|
shards: Shard[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Shard } from "../../domain/queue/shard.js";
|
|
2
|
+
import type { ClientJobOwner } from "../clientOwnership.js";
|
|
2
3
|
import type { DlqEntry } from "../../domain/types/dlq.js";
|
|
3
4
|
import type { FailureReason } from "../../domain/types/dlq.js";
|
|
4
5
|
import type { Job, JobId, JobInput, JobLock } from "../../domain/types/job.js";
|
|
@@ -48,6 +49,7 @@ export interface QueueManagerStateView {
|
|
|
48
49
|
stalledCandidates: Set<JobId>;
|
|
49
50
|
jobLocks: Map<JobId, JobLock>;
|
|
50
51
|
clientJobs: Map<string, Set<JobId>>;
|
|
52
|
+
clientJobOwners: Map<JobId, ClientJobOwner>;
|
|
51
53
|
repeatChain: Map<JobId, JobId>;
|
|
52
54
|
queueNamesCache: Set<string>;
|
|
53
55
|
eventsManager: EventsManager;
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* outages and process restarts; memory-only sources provide no such guarantee.
|
|
10
10
|
*
|
|
11
11
|
* Idempotency: every forwarded job carries the deterministic remote jobId
|
|
12
|
-
* `fwd:<localQueueKey>:<localJobId>`. The server dedupes custom
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `fwd:<localQueueKey>:<localJobId>`. The server dedupes a custom jobId only
|
|
13
|
+
* while that job is live (waiting, delayed or active); completion or the DLQ
|
|
14
|
+
* releases the id, so a re-forward after that creates a new remote job.
|
|
15
|
+
* Downstream effects must tolerate at-least-once delivery.
|
|
15
16
|
*/
|
|
16
17
|
import { EventEmitter } from 'events';
|
|
17
18
|
import type { ConnectionOptions, QueueOptions } from "./types.js";
|
|
@@ -5,6 +5,8 @@ export interface ParentOpts {
|
|
|
5
5
|
export interface BackoffOptions {
|
|
6
6
|
type: 'fixed' | 'exponential';
|
|
7
7
|
delay: number;
|
|
8
|
+
/** Upper bound for one retry delay in ms (0 to 86,400,000). Defaults to 1 hour. */
|
|
9
|
+
maxDelay?: number;
|
|
8
10
|
}
|
|
9
11
|
export interface KeepJobs {
|
|
10
12
|
age?: number;
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { Job, JobId, JobInput } from "../types/jobs/model.js";
|
|
2
|
+
/**
|
|
3
|
+
* Keep a caller-supplied retry-delay cap only when it is usable. Embedded and
|
|
4
|
+
* cron admission do not pass through the server validator, so a non-numeric,
|
|
5
|
+
* non-finite, negative or over-limit value is dropped and the default cap applies
|
|
6
|
+
* instead of turning the retry delay into NaN or an unbounded wait.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseMaxDelay(value: unknown): number | undefined;
|
|
2
9
|
export declare function createJob(id: JobId, queue: string, input: JobInput, now?: number): Job;
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import type { JobId, JobLock } from "../types/jobs/model.js";
|
|
1
|
+
import type { Job, JobId, JobLock } from "../types/jobs/model.js";
|
|
2
2
|
export declare function createJobLock(jobId: JobId, owner: string, ttl?: number, now?: number): JobLock;
|
|
3
3
|
export declare function isLockExpired(lock: JobLock, now?: number): boolean;
|
|
4
4
|
export declare function renewLock(lock: JobLock, newTtl?: number, now?: number): void;
|
|
5
|
+
/**
|
|
6
|
+
* True when `lock` was created for an earlier processing generation of `job`,
|
|
7
|
+
* that is, the job was pulled again after the lease was granted. Stall retry
|
|
8
|
+
* deliberately keeps the previous lease as a stale-outcome guard, so a lease in
|
|
9
|
+
* `jobLocks` is not necessarily the current one.
|
|
10
|
+
*
|
|
11
|
+
* Pull stamps `startedAt` from a clock read taken before the lease is created
|
|
12
|
+
* in the same delivery, so a lease from the current pull always has
|
|
13
|
+
* `createdAt >= startedAt`. The comparison is strict: a lease created in the
|
|
14
|
+
* same millisecond as the pull belongs to the current generation.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isLeaseFromEarlierGeneration(job: Pick<Job, 'startedAt'>, lock: Pick<JobLock, 'createdAt'>): boolean;
|
|
@@ -5,4 +5,19 @@ export declare function isReady(job: Job, now?: number): boolean;
|
|
|
5
5
|
export declare function isExpired(job: Job, now?: number): boolean;
|
|
6
6
|
export declare function isTimedOut(job: Job, now?: number): boolean;
|
|
7
7
|
export declare function calculateBackoff(job: Job): number;
|
|
8
|
+
/**
|
|
9
|
+
* Wait applied when a processor throws `DelayedError`. It is always a positive
|
|
10
|
+
* finite number of milliseconds: DelayedError never counts an attempt, so a zero
|
|
11
|
+
* wait would let a processor that keeps throwing it re-pull the job in a tight
|
|
12
|
+
* loop with nothing to stop it.
|
|
13
|
+
*
|
|
14
|
+
* The base is the configured delay with no attempt growth and no jitter:
|
|
15
|
+
* `backoff.delay` for the object form, otherwise the numeric `backoff`. A base
|
|
16
|
+
* that is not a positive number (0, missing, negative or NaN) falls back to the
|
|
17
|
+
* 1000 ms default. The base is then capped at `backoff.maxDelay` when that is a
|
|
18
|
+
* positive finite number, otherwise at DEFAULT_MAX_BACKOFF. A `maxDelay` of 0
|
|
19
|
+
* means "retry failures immediately" and does not apply here, because
|
|
20
|
+
* DelayedError is not a failure: such a job waits its capped base delay.
|
|
21
|
+
*/
|
|
22
|
+
export declare function calculateDelayedErrorDelay(job: Pick<Job, 'backoff' | 'backoffConfig'>): number;
|
|
8
23
|
export declare function canRetry(job: Job): boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AtomicFlowJobInput } from "../flow.js";
|
|
2
|
-
import type { JobInput } from "../job.js";
|
|
2
|
+
import type { BackoffConfig, JobInput } from "../job.js";
|
|
3
3
|
import type { GroupPullOptions } from "../group.js";
|
|
4
4
|
import type { BaseCommand } from "./base.js";
|
|
5
5
|
export interface PushCommand extends BaseCommand {
|
|
@@ -10,10 +10,7 @@ export interface PushCommand extends BaseCommand {
|
|
|
10
10
|
readonly priority?: number;
|
|
11
11
|
readonly delay?: number;
|
|
12
12
|
readonly maxAttempts?: number;
|
|
13
|
-
readonly backoff?: number |
|
|
14
|
-
type: 'fixed' | 'exponential';
|
|
15
|
-
delay: number;
|
|
16
|
-
};
|
|
13
|
+
readonly backoff?: number | BackoffConfig;
|
|
17
14
|
readonly ttl?: number;
|
|
18
15
|
readonly timeout?: number;
|
|
19
16
|
readonly uniqueKey?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cron job domain types
|
|
3
3
|
*/
|
|
4
|
+
import type { BackoffConfig } from "./jobs/model.js";
|
|
4
5
|
/** Deduplication config for cron-spawned jobs */
|
|
5
6
|
export interface CronDedup {
|
|
6
7
|
readonly ttl?: number;
|
|
@@ -14,10 +15,7 @@ export interface CronDedup {
|
|
|
14
15
|
*/
|
|
15
16
|
export interface CronJobOptions {
|
|
16
17
|
readonly maxAttempts?: number;
|
|
17
|
-
readonly backoff?: number |
|
|
18
|
-
type: 'fixed' | 'exponential';
|
|
19
|
-
delay: number;
|
|
20
|
-
};
|
|
18
|
+
readonly backoff?: number | BackoffConfig;
|
|
21
19
|
readonly timeout?: number;
|
|
22
20
|
readonly delay?: number;
|
|
23
21
|
readonly stallTimeout?: number;
|
|
@@ -93,10 +93,7 @@ export interface JobInput {
|
|
|
93
93
|
priority?: number;
|
|
94
94
|
delay?: number;
|
|
95
95
|
maxAttempts?: number;
|
|
96
|
-
backoff?: number |
|
|
97
|
-
type: 'fixed' | 'exponential';
|
|
98
|
-
delay: number;
|
|
99
|
-
};
|
|
96
|
+
backoff?: number | BackoffConfig;
|
|
100
97
|
ttl?: number;
|
|
101
98
|
timeout?: number;
|
|
102
99
|
uniqueKey?: string;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/legacy.ts
CHANGED