bunqueue 2.8.49 → 2.8.51
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 +62 -9
- package/dist/application/backgroundTasks.js +21 -4
- package/dist/application/cleanupTasks.js +7 -3
- package/dist/application/contextFactory.d.ts +6 -3
- package/dist/application/contextFactory.js +8 -0
- package/dist/application/dependencyCompletions.d.ts +53 -0
- package/dist/application/dependencyCompletions.js +123 -0
- package/dist/application/dependencyProcessor.d.ts +5 -0
- package/dist/application/dependencyProcessor.js +25 -13
- package/dist/application/flowFailureRecovery.d.ts +20 -0
- package/dist/application/flowFailureRecovery.js +94 -0
- package/dist/application/flowParentBackpatch.d.ts +27 -0
- package/dist/application/flowParentBackpatch.js +120 -0
- package/dist/application/operations/ack.d.ts +6 -3
- package/dist/application/operations/ack.js +36 -10
- package/dist/application/operations/ackHelpers.d.ts +5 -4
- package/dist/application/operations/ackHelpers.js +4 -10
- package/dist/application/operations/customId.d.ts +3 -0
- package/dist/application/operations/customId.js +10 -0
- package/dist/application/operations/flowPush.d.ts +9 -0
- package/dist/application/operations/flowPush.js +112 -0
- package/dist/application/operations/flowTopologyValidation.d.ts +3 -0
- package/dist/application/operations/flowTopologyValidation.js +101 -0
- package/dist/application/operations/flowValidation.d.ts +3 -0
- package/dist/application/operations/flowValidation.js +166 -0
- package/dist/application/operations/jobManagement.d.ts +3 -0
- package/dist/application/operations/jobManagement.js +10 -4
- package/dist/application/operations/push.d.ts +6 -1
- package/dist/application/operations/push.js +2 -2
- package/dist/application/operations/pushInsert.d.ts +11 -2
- package/dist/application/operations/pushInsert.js +20 -8
- package/dist/application/operations/pushLocks.d.ts +1 -0
- package/dist/application/operations/pushLocks.js +5 -1
- package/dist/application/operations/queryOperations.js +7 -6
- package/dist/application/queueManager.d.ts +7 -0
- package/dist/application/queueManager.js +261 -67
- package/dist/application/types.d.ts +3 -1
- package/dist/cli/commandRouter.d.ts +1 -1
- package/dist/client/flow.d.ts +3 -6
- package/dist/client/flow.js +71 -210
- package/dist/client/flowAtomic.d.ts +9 -0
- package/dist/client/flowAtomic.js +24 -0
- package/dist/client/flowJobCoreMethods.d.ts +28 -0
- package/dist/client/flowJobCoreMethods.js +151 -0
- package/dist/client/flowJobDependencies.d.ts +7 -0
- package/dist/client/flowJobDependencies.js +64 -0
- package/dist/client/flowJobFactory.d.ts +11 -24
- package/dist/client/flowJobFactory.js +91 -360
- package/dist/client/flowJobMoveMethods.d.ts +16 -0
- package/dist/client/flowJobMoveMethods.js +112 -0
- package/dist/client/flowJobTypes.d.ts +29 -0
- package/dist/client/flowJobTypes.js +6 -0
- package/dist/client/flowLegacyPlan.d.ts +16 -0
- package/dist/client/flowLegacyPlan.js +118 -0
- package/dist/client/flowOptions.d.ts +10 -0
- package/dist/client/flowOptions.js +44 -0
- package/dist/client/flowPlan.d.ts +16 -0
- package/dist/client/flowPlan.js +101 -0
- package/dist/client/flowPush.js +4 -1
- package/dist/client/flowReader.d.ts +10 -0
- package/dist/client/flowReader.js +147 -0
- package/dist/client/flowTypes.d.ts +2 -2
- package/dist/client/jobHelpers.js +2 -0
- package/dist/client/workflow/clock.d.ts +3 -1
- package/dist/client/workflow/clock.js +18 -1
- package/dist/client/workflow/compensationChild.d.ts +30 -0
- package/dist/client/workflow/compensationChild.js +65 -0
- package/dist/client/workflow/compensationPass.d.ts +16 -0
- package/dist/client/workflow/compensationPass.js +96 -0
- package/dist/client/workflow/compensationSupport.d.ts +24 -0
- package/dist/client/workflow/compensationSupport.js +57 -0
- package/dist/client/workflow/compensator.d.ts +11 -50
- package/dist/client/workflow/compensator.js +22 -374
- package/dist/client/workflow/definitionGuard.d.ts +11 -0
- package/dist/client/workflow/definitionGuard.js +22 -0
- package/dist/client/workflow/engine.d.ts +2 -2
- package/dist/client/workflow/engine.js +8 -8
- package/dist/client/workflow/eventTypes.d.ts +44 -0
- package/dist/client/workflow/eventTypes.js +2 -0
- package/dist/client/workflow/executionTypes.d.ts +133 -0
- package/dist/client/workflow/executionTypes.js +2 -0
- package/dist/client/workflow/executor.d.ts +4 -17
- package/dist/client/workflow/executor.js +50 -208
- package/dist/client/workflow/executorLifecycle.d.ts +18 -0
- package/dist/client/workflow/executorLifecycle.js +69 -0
- package/dist/client/workflow/executorNodes.d.ts +17 -0
- package/dist/client/workflow/executorNodes.js +138 -0
- package/dist/client/workflow/identity.d.ts +2 -0
- package/dist/client/workflow/identity.js +8 -0
- package/dist/client/workflow/index.d.ts +1 -1
- package/dist/client/workflow/loops.d.ts +3 -5
- package/dist/client/workflow/loops.js +25 -23
- package/dist/client/workflow/mapRunner.d.ts +4 -0
- package/dist/client/workflow/mapRunner.js +45 -0
- package/dist/client/workflow/recovery.js +10 -3
- package/dist/client/workflow/runner.d.ts +2 -21
- package/dist/client/workflow/runner.js +39 -104
- package/dist/client/workflow/runnerTiming.d.ts +14 -0
- package/dist/client/workflow/runnerTiming.js +66 -0
- package/dist/client/workflow/stepTypes.d.ts +171 -0
- package/dist/client/workflow/stepTypes.js +3 -0
- package/dist/client/workflow/store.d.ts +17 -17
- package/dist/client/workflow/store.js +65 -100
- package/dist/client/workflow/storeExecutionCodec.d.ts +11 -0
- package/dist/client/workflow/storeExecutionCodec.js +34 -0
- package/dist/client/workflow/storeListing.d.ts +11 -0
- package/dist/client/workflow/storeListing.js +45 -0
- package/dist/client/workflow/storeMaintenance.d.ts +4 -0
- package/dist/client/workflow/storeMaintenance.js +40 -0
- package/dist/client/workflow/storeSignals.d.ts +9 -0
- package/dist/client/workflow/storeSignals.js +38 -2
- package/dist/client/workflow/subWorkflowRunner.d.ts +13 -0
- package/dist/client/workflow/subWorkflowRunner.js +40 -0
- package/dist/client/workflow/types.d.ts +4 -356
- package/dist/client/workflow/types.js +1 -3
- package/dist/client/workflow/waitFor.js +39 -26
- package/dist/client/workflow/workflow.d.ts +16 -59
- package/dist/client/workflow/workflow.js +53 -179
- package/dist/client/workflow/workflowDecisions.d.ts +11 -0
- package/dist/client/workflow/workflowDecisions.js +27 -0
- package/dist/client/workflow/workflowDefinition.d.ts +16 -0
- package/dist/client/workflow/workflowDefinition.js +123 -0
- package/dist/client/workflow/workflowIntrospection.d.ts +5 -0
- package/dist/client/workflow/workflowIntrospection.js +46 -0
- package/dist/client/workflow/workflowValidation.d.ts +44 -0
- package/dist/client/workflow/workflowValidation.js +143 -0
- package/dist/domain/types/command.d.ts +7 -1
- package/dist/domain/types/flow.d.ts +25 -0
- package/dist/domain/types/flow.js +1 -0
- package/dist/infrastructure/persistence/dependencyCompletionSchema.d.ts +6 -0
- package/dist/infrastructure/persistence/dependencyCompletionSchema.js +16 -0
- package/dist/infrastructure/persistence/dependencyCompletionStore.d.ts +38 -0
- package/dist/infrastructure/persistence/dependencyCompletionStore.js +105 -0
- package/dist/infrastructure/persistence/schema.d.ts +2 -5
- package/dist/infrastructure/persistence/schema.js +57 -3
- package/dist/infrastructure/persistence/sqlite.d.ts +35 -0
- package/dist/infrastructure/persistence/sqlite.js +238 -12
- package/dist/infrastructure/persistence/sqliteBatch.js +10 -4
- package/dist/infrastructure/persistence/sqliteSerializer.d.ts +5 -0
- package/dist/infrastructure/persistence/sqliteSerializer.js +28 -5
- package/dist/infrastructure/persistence/statements.d.ts +4 -0
- package/dist/infrastructure/persistence/statements.js +8 -2
- package/dist/infrastructure/server/handlerRoutes.js +3 -0
- package/dist/infrastructure/server/handlers/advanced.js +2 -2
- package/dist/infrastructure/server/handlers/flow.d.ts +7 -0
- package/dist/infrastructure/server/handlers/flow.js +11 -0
- package/dist/infrastructure/server/handlers/index.d.ts +1 -0
- package/dist/infrastructure/server/handlers/index.js +1 -0
- package/package.json +4 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SQLite schema and migrations
|
|
3
3
|
*/
|
|
4
|
+
import { DEPENDENCY_COMPLETION_SCHEMA } from './dependencyCompletionSchema';
|
|
4
5
|
/** SQLite PRAGMA settings for optimal performance */
|
|
5
6
|
export const PRAGMA_SETTINGS = `
|
|
6
7
|
PRAGMA journal_mode = WAL;
|
|
@@ -42,6 +43,10 @@ CREATE TABLE IF NOT EXISTS jobs (
|
|
|
42
43
|
progress_msg TEXT,
|
|
43
44
|
remove_on_complete INTEGER DEFAULT 0,
|
|
44
45
|
remove_on_fail INTEGER DEFAULT 0,
|
|
46
|
+
fail_parent_on_failure INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
remove_dependency_on_failure INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
continue_parent_on_failure INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
ignore_dependency_on_failure INTEGER NOT NULL DEFAULT 0,
|
|
45
50
|
stall_timeout INTEGER,
|
|
46
51
|
last_heartbeat INTEGER,
|
|
47
52
|
stall_count INTEGER NOT NULL DEFAULT 0,
|
|
@@ -58,7 +63,7 @@ CREATE INDEX IF NOT EXISTS idx_jobs_queue_created
|
|
|
58
63
|
CREATE INDEX IF NOT EXISTS idx_jobs_queue_state_created
|
|
59
64
|
ON jobs(queue, state, created_at, id);
|
|
60
65
|
CREATE INDEX IF NOT EXISTS idx_jobs_run_at
|
|
61
|
-
ON jobs(run_at) WHERE state IN ('waiting', 'delayed');
|
|
66
|
+
ON jobs(run_at) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
|
|
62
67
|
CREATE INDEX IF NOT EXISTS idx_jobs_unique
|
|
63
68
|
ON jobs(queue, unique_key) WHERE unique_key IS NOT NULL;
|
|
64
69
|
CREATE INDEX IF NOT EXISTS idx_jobs_custom_id
|
|
@@ -66,6 +71,20 @@ CREATE INDEX IF NOT EXISTS idx_jobs_custom_id
|
|
|
66
71
|
CREATE INDEX IF NOT EXISTS idx_jobs_parent
|
|
67
72
|
ON jobs(parent_id) WHERE parent_id IS NOT NULL;
|
|
68
73
|
|
|
74
|
+
-- Durable outbox/state for terminal child failure propagation.
|
|
75
|
+
CREATE TABLE IF NOT EXISTS flow_failures (
|
|
76
|
+
parent_id TEXT NOT NULL,
|
|
77
|
+
child_id TEXT NOT NULL,
|
|
78
|
+
child_queue TEXT NOT NULL,
|
|
79
|
+
mode TEXT NOT NULL,
|
|
80
|
+
error TEXT NOT NULL,
|
|
81
|
+
created_at INTEGER NOT NULL,
|
|
82
|
+
PRIMARY KEY (parent_id, child_id)
|
|
83
|
+
);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_flow_failures_parent ON flow_failures(parent_id);
|
|
85
|
+
|
|
86
|
+
${DEPENDENCY_COMPLETION_SCHEMA}
|
|
87
|
+
|
|
69
88
|
-- Job results storage (BLOB for MessagePack)
|
|
70
89
|
CREATE TABLE IF NOT EXISTS job_results (
|
|
71
90
|
job_id TEXT PRIMARY KEY,
|
|
@@ -97,7 +116,7 @@ CREATE INDEX IF NOT EXISTS idx_jobs_group_id
|
|
|
97
116
|
|
|
98
117
|
-- Pending jobs: compound index for priority-ordered retrieval
|
|
99
118
|
CREATE INDEX IF NOT EXISTS idx_jobs_pending_priority
|
|
100
|
-
ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'delayed');
|
|
119
|
+
ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
|
|
101
120
|
|
|
102
121
|
-- Completed jobs: index for recovery ordering (issue #84)
|
|
103
122
|
CREATE INDEX IF NOT EXISTS idx_jobs_completed_order
|
|
@@ -146,7 +165,7 @@ CREATE TABLE IF NOT EXISTS migrations (
|
|
|
146
165
|
);
|
|
147
166
|
`;
|
|
148
167
|
/** Current schema version */
|
|
149
|
-
export const SCHEMA_VERSION =
|
|
168
|
+
export const SCHEMA_VERSION = 29;
|
|
150
169
|
/** All migrations in order */
|
|
151
170
|
export const MIGRATIONS = {
|
|
152
171
|
1: SCHEMA,
|
|
@@ -241,4 +260,39 @@ ALTER TABLE queue_state ADD COLUMN stall_grace_period INTEGER;
|
|
|
241
260
|
22: `
|
|
242
261
|
ALTER TABLE queue_state ADD COLUMN dlq_config BLOB;
|
|
243
262
|
`,
|
|
263
|
+
// Migrations 23-26: flow failure policy must survive active-job recovery.
|
|
264
|
+
23: `
|
|
265
|
+
ALTER TABLE jobs ADD COLUMN fail_parent_on_failure INTEGER NOT NULL DEFAULT 0;
|
|
266
|
+
`,
|
|
267
|
+
24: `
|
|
268
|
+
ALTER TABLE jobs ADD COLUMN remove_dependency_on_failure INTEGER NOT NULL DEFAULT 0;
|
|
269
|
+
`,
|
|
270
|
+
25: `
|
|
271
|
+
ALTER TABLE jobs ADD COLUMN continue_parent_on_failure INTEGER NOT NULL DEFAULT 0;
|
|
272
|
+
`,
|
|
273
|
+
26: `
|
|
274
|
+
ALTER TABLE jobs ADD COLUMN ignore_dependency_on_failure INTEGER NOT NULL DEFAULT 0;
|
|
275
|
+
`,
|
|
276
|
+
27: `
|
|
277
|
+
CREATE TABLE IF NOT EXISTS flow_failures (
|
|
278
|
+
parent_id TEXT NOT NULL,
|
|
279
|
+
child_id TEXT NOT NULL,
|
|
280
|
+
child_queue TEXT NOT NULL,
|
|
281
|
+
mode TEXT NOT NULL,
|
|
282
|
+
error TEXT NOT NULL,
|
|
283
|
+
created_at INTEGER NOT NULL,
|
|
284
|
+
PRIMARY KEY (parent_id, child_id)
|
|
285
|
+
);
|
|
286
|
+
CREATE INDEX IF NOT EXISTS idx_flow_failures_parent ON flow_failures(parent_id);
|
|
287
|
+
|
|
288
|
+
DROP INDEX IF EXISTS idx_jobs_run_at;
|
|
289
|
+
CREATE INDEX idx_jobs_run_at
|
|
290
|
+
ON jobs(run_at) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
|
|
291
|
+
|
|
292
|
+
DROP INDEX IF EXISTS idx_jobs_pending_priority;
|
|
293
|
+
CREATE INDEX idx_jobs_pending_priority
|
|
294
|
+
ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
|
|
295
|
+
`,
|
|
296
|
+
28: DEPENDENCY_COMPLETION_SCHEMA,
|
|
297
|
+
29: 'ALTER TABLE dependency_completions ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;',
|
|
244
298
|
};
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* Uses MessagePack for ~2-3x faster serialization than JSON
|
|
5
5
|
*/
|
|
6
6
|
import type { Job, JobId, JobTimelineEntry } from '../../domain/types/job';
|
|
7
|
+
import type { FlowFailureRecord } from '../../domain/types/flow';
|
|
7
8
|
import type { StallConfig } from '../../domain/types/stall';
|
|
8
9
|
import type { CronJob } from '../../domain/types/cron';
|
|
9
10
|
import { type DlqConfig, type DlqEntry } from '../../domain/types/dlq';
|
|
11
|
+
import { type DependencyCompletionRecord } from './dependencyCompletionStore';
|
|
10
12
|
/** Critical-loss callback: invoked when WriteBuffer drops jobs after exhausting retries. */
|
|
11
13
|
export type SqliteCriticalLossCallback = (jobs: Job[], lastError: Error, attempts: number) => void;
|
|
12
14
|
/** Record of a critical job loss event (jobs dropped after max retries). */
|
|
@@ -37,6 +39,7 @@ export declare class SqliteStorage {
|
|
|
37
39
|
private readonly statements;
|
|
38
40
|
private readonly batchManager;
|
|
39
41
|
private readonly writeBuffer;
|
|
42
|
+
private readonly dependencyCompletionStore;
|
|
40
43
|
private _diskFull;
|
|
41
44
|
private _lastDiskFullError;
|
|
42
45
|
private _lastDiskFullAt;
|
|
@@ -98,6 +101,11 @@ export declare class SqliteStorage {
|
|
|
98
101
|
markFailed(job: Job, error: string | null): void;
|
|
99
102
|
/** Save DLQ entry with full metadata */
|
|
100
103
|
saveDlqEntry(entry: DlqEntry): void;
|
|
104
|
+
/**
|
|
105
|
+
* Commit a terminal child outcome, its optional DLQ row, and its flow outbox
|
|
106
|
+
* record together. Recovery therefore never observes only half the transition.
|
|
107
|
+
*/
|
|
108
|
+
commitFailedJob(jobId: JobId, entry: DlqEntry | null, flowFailure: FlowFailureRecord | null): void;
|
|
101
109
|
/** Delete DLQ entry by job ID */
|
|
102
110
|
deleteDlqEntry(jobId: JobId): void;
|
|
103
111
|
/** Clear all DLQ entries for a queue */
|
|
@@ -129,6 +137,25 @@ export declare class SqliteStorage {
|
|
|
129
137
|
updateRunAt(jobId: JobId, runAt: number): void;
|
|
130
138
|
/** Update a job's children_ids blob and parent_id */
|
|
131
139
|
updateJobChildrenIds(jobId: JobId, childrenIds: JobId[]): void;
|
|
140
|
+
/** Persist both sides of a legacy parent link in one SQLite transaction. */
|
|
141
|
+
updateFlowLink(child: Pick<Job, 'id' | 'parentId' | 'data'>, parent: Pick<Job, 'id' | 'childrenIds' | 'dependsOn' | 'data'>, parentState: 'waiting-children' | 'waiting' | 'prioritized' | 'delayed'): void;
|
|
142
|
+
/**
|
|
143
|
+
* Persist a legacy SDK parent-id backpatch without changing parent state.
|
|
144
|
+
*
|
|
145
|
+
* A fast child can finish before the SDK sends UpdateParent. Completed jobs
|
|
146
|
+
* remain in `jobs`, while failed jobs live only in the serialized DLQ entry.
|
|
147
|
+
* The failure outbox is re-keyed in the same transaction so a crash between
|
|
148
|
+
* this write and in-memory propagation still targets the real parent.
|
|
149
|
+
*/
|
|
150
|
+
backpatchFlowChild(child: Pick<Job, 'id' | 'parentId' | 'data'>, previousParentId: JobId | null): void;
|
|
151
|
+
/** Persist both sides of a removed parent/child relationship atomically. */
|
|
152
|
+
removeFlowLink(child: Pick<Job, 'id' | 'parentId' | 'data'>, parent: Pick<Job, 'id' | 'childrenIds' | 'dependsOn' | 'data' | 'runAt'>, parentState: 'waiting-children' | 'waiting' | 'prioritized'): void;
|
|
153
|
+
/** Record terminal child propagation before applying its parent-side effect. */
|
|
154
|
+
saveFlowFailure(record: FlowFailureRecord): void;
|
|
155
|
+
loadFlowFailures(): FlowFailureRecord[];
|
|
156
|
+
deleteFlowFailure(parentId: JobId, childId?: JobId): void;
|
|
157
|
+
/** Persist dependency removal/promotion as one parent-side recovery checkpoint. */
|
|
158
|
+
updateFlowParentResolution(job: Pick<Job, 'id' | 'dependsOn' | 'runAt' | 'priority' | 'timeline'>, stateOverride?: 'waiting-children' | 'waiting' | 'prioritized' | 'delayed'): void;
|
|
132
159
|
getJob(id: JobId): Job | null;
|
|
133
160
|
storeResult(jobId: JobId, result: unknown): void;
|
|
134
161
|
getResult(jobId: JobId): unknown;
|
|
@@ -144,6 +171,14 @@ export declare class SqliteStorage {
|
|
|
144
171
|
getJobStateRaw(jobId: JobId): string | null;
|
|
145
172
|
/** Load all completed job IDs (for dependency recovery) */
|
|
146
173
|
loadCompletedJobIds(): Set<JobId>;
|
|
174
|
+
/** Atomically replace a removed completed job with payload-free dependency evidence. */
|
|
175
|
+
commitRemovedCompletion(job: Pick<Job, 'id' | 'queue'>, retentionLimit: number, pinned: boolean, completedAt?: number): void;
|
|
176
|
+
loadDependencyCompletions(): DependencyCompletionRecord[];
|
|
177
|
+
pinDependencyCompletions(jobIds: Iterable<JobId>): void;
|
|
178
|
+
unpinDependencyCompletions(jobIds: Iterable<JobId>, retentionLimit: number): DependencyCompletionRecord[];
|
|
179
|
+
reconcileDependencyCompletionPins(referenced: ReadonlySet<JobId>, retentionLimit: number): DependencyCompletionRecord[];
|
|
180
|
+
deleteDependencyCompletion(jobId: JobId): boolean;
|
|
181
|
+
deleteDependencyCompletionsForQueue(queue: string): JobId[];
|
|
147
182
|
/**
|
|
148
183
|
* Insert batch of jobs. By default the jobs go through the write buffer.
|
|
149
184
|
* When `durable` is true they bypass the buffer and are written to disk
|
|
@@ -7,9 +7,10 @@ import { Database } from 'bun:sqlite';
|
|
|
7
7
|
import { createDlqEntry, } from '../../domain/types/dlq';
|
|
8
8
|
import { PRAGMA_SETTINGS, SCHEMA, MIGRATION_TABLE, SCHEMA_VERSION, MIGRATIONS } from './schema';
|
|
9
9
|
import { prepareStatements, } from './statements';
|
|
10
|
-
import { pack, persistedStallCount, reconstructDlqEntry, rowToJob, unpack, } from './sqliteSerializer';
|
|
10
|
+
import { pack, persistedInitialState, persistedStallCount, reconstructDlqEntry, rowToJob, unpack, } from './sqliteSerializer';
|
|
11
11
|
import { BatchInsertManager, WriteBuffer } from './sqliteBatch';
|
|
12
12
|
import { storageLog } from '../../shared/logger';
|
|
13
|
+
import { DependencyCompletionStore, } from './dependencyCompletionStore';
|
|
13
14
|
/** Check if an error is a SQLITE_FULL (disk full) error */
|
|
14
15
|
function isSqliteFullError(err) {
|
|
15
16
|
if (!(err instanceof Error))
|
|
@@ -25,6 +26,7 @@ export class SqliteStorage {
|
|
|
25
26
|
statements;
|
|
26
27
|
batchManager;
|
|
27
28
|
writeBuffer;
|
|
29
|
+
dependencyCompletionStore;
|
|
28
30
|
_diskFull = false;
|
|
29
31
|
_lastDiskFullError = null;
|
|
30
32
|
_lastDiskFullAt = null;
|
|
@@ -48,6 +50,7 @@ export class SqliteStorage {
|
|
|
48
50
|
});
|
|
49
51
|
}
|
|
50
52
|
this.migrate();
|
|
53
|
+
this.dependencyCompletionStore = new DependencyCompletionStore(this.db);
|
|
51
54
|
this.statements = prepareStatements(this.db);
|
|
52
55
|
this._onCriticalLoss = config.onCriticalLoss;
|
|
53
56
|
// Initialize batch manager and write buffer
|
|
@@ -256,7 +259,7 @@ export class SqliteStorage {
|
|
|
256
259
|
runInsertJobStmt(job) {
|
|
257
260
|
this.statements
|
|
258
261
|
.get('insertJob')
|
|
259
|
-
.run(job.id, job.queue, pack(job.data), job.priority, job.createdAt, job.runAt, job.attempts, job.maxAttempts, job.backoff, job.ttl, job.timeout, job.uniqueKey, job.customId, job.dependsOn.length > 0 ? pack(job.dependsOn) : null, job.parentId, job.childrenIds.length > 0 ? pack(job.childrenIds) : null, job.tags.length > 0 ? pack(job.tags) : null, job.
|
|
262
|
+
.run(job.id, job.queue, pack(job.data), job.priority, job.createdAt, job.runAt, job.attempts, job.maxAttempts, job.backoff, job.ttl, job.timeout, job.uniqueKey, job.customId, job.dependsOn.length > 0 ? pack(job.dependsOn) : null, job.parentId, job.childrenIds.length > 0 ? pack(job.childrenIds) : null, job.tags.length > 0 ? pack(job.tags) : null, persistedInitialState(job), job.lifo ? 1 : 0, job.groupId, job.removeOnComplete ? 1 : 0, job.removeOnFail ? 1 : 0, job.failParentOnFailure ? 1 : 0, job.removeDependencyOnFailure ? 1 : 0, job.continueParentOnFailure ? 1 : 0, job.ignoreDependencyOnFailure ? 1 : 0, job.stallTimeout, persistedStallCount(job), job.timeline.length > 0 ? pack(job.timeline) : null);
|
|
260
263
|
}
|
|
261
264
|
/**
|
|
262
265
|
* Ensure a job's buffered INSERT has been written to disk before issuing a
|
|
@@ -308,6 +311,35 @@ export class SqliteStorage {
|
|
|
308
311
|
.run(entry.job.id, entry.job.queue, pack(entry), entry.enteredAt);
|
|
309
312
|
});
|
|
310
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Commit a terminal child outcome, its optional DLQ row, and its flow outbox
|
|
316
|
+
* record together. Recovery therefore never observes only half the transition.
|
|
317
|
+
*/
|
|
318
|
+
commitFailedJob(jobId, entry, flowFailure) {
|
|
319
|
+
this.writeBuffer.removePending(jobId);
|
|
320
|
+
this.safeWrite(() => {
|
|
321
|
+
const tx = this.db.transaction(() => {
|
|
322
|
+
if (entry) {
|
|
323
|
+
this.statements
|
|
324
|
+
.get('insertDlq')
|
|
325
|
+
.run(entry.job.id, entry.job.queue, pack(entry), entry.enteredAt);
|
|
326
|
+
}
|
|
327
|
+
if (flowFailure) {
|
|
328
|
+
this.db
|
|
329
|
+
.prepare(`INSERT INTO flow_failures
|
|
330
|
+
(parent_id, child_id, child_queue, mode, error, created_at)
|
|
331
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
332
|
+
ON CONFLICT(parent_id, child_id) DO UPDATE SET
|
|
333
|
+
child_queue=excluded.child_queue, mode=excluded.mode,
|
|
334
|
+
error=excluded.error, created_at=excluded.created_at`)
|
|
335
|
+
.run(flowFailure.parentId, flowFailure.childId, flowFailure.childQueue, flowFailure.mode, flowFailure.error, flowFailure.createdAt);
|
|
336
|
+
}
|
|
337
|
+
this.statements.get('deleteJob').run(jobId);
|
|
338
|
+
this.statements.get('deleteJobResult').run(jobId);
|
|
339
|
+
});
|
|
340
|
+
tx();
|
|
341
|
+
});
|
|
342
|
+
}
|
|
311
343
|
/** Delete DLQ entry by job ID */
|
|
312
344
|
deleteDlqEntry(jobId) {
|
|
313
345
|
this.safeWrite(() => {
|
|
@@ -388,6 +420,7 @@ export class SqliteStorage {
|
|
|
388
420
|
const tx = this.db.transaction((id) => {
|
|
389
421
|
deleteStmt.run(id);
|
|
390
422
|
deleteResultStmt.run(id);
|
|
423
|
+
this.db.prepare('DELETE FROM flow_failures WHERE parent_id = ?').run(id);
|
|
391
424
|
});
|
|
392
425
|
tx(jobId);
|
|
393
426
|
});
|
|
@@ -438,12 +471,161 @@ export class SqliteStorage {
|
|
|
438
471
|
}
|
|
439
472
|
/** Update a job's children_ids blob and parent_id */
|
|
440
473
|
updateJobChildrenIds(jobId, childrenIds) {
|
|
474
|
+
this.flushIfBuffered(jobId);
|
|
441
475
|
this.safeWrite(() => {
|
|
442
476
|
this.db
|
|
443
477
|
.prepare('UPDATE jobs SET children_ids = ? WHERE id = ?')
|
|
444
478
|
.run(childrenIds.length > 0 ? pack(childrenIds) : null, jobId);
|
|
445
479
|
});
|
|
446
480
|
}
|
|
481
|
+
/** Persist both sides of a legacy parent link in one SQLite transaction. */
|
|
482
|
+
updateFlowLink(child, parent, parentState) {
|
|
483
|
+
this.flushIfBuffered(child.id);
|
|
484
|
+
this.flushIfBuffered(parent.id);
|
|
485
|
+
this.safeWrite(() => {
|
|
486
|
+
const tx = this.db.transaction(() => {
|
|
487
|
+
this.db
|
|
488
|
+
.prepare('UPDATE jobs SET parent_id = ?, data = ? WHERE id = ?')
|
|
489
|
+
.run(child.parentId, pack(child.data), child.id);
|
|
490
|
+
this.db
|
|
491
|
+
.prepare('UPDATE jobs SET children_ids = ?, depends_on = ?, data = ?, state = ? WHERE id = ?')
|
|
492
|
+
.run(parent.childrenIds.length > 0 ? pack(parent.childrenIds) : null, parent.dependsOn.length > 0 ? pack(parent.dependsOn) : null, pack(parent.data), parentState, parent.id);
|
|
493
|
+
});
|
|
494
|
+
tx();
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Persist a legacy SDK parent-id backpatch without changing parent state.
|
|
499
|
+
*
|
|
500
|
+
* A fast child can finish before the SDK sends UpdateParent. Completed jobs
|
|
501
|
+
* remain in `jobs`, while failed jobs live only in the serialized DLQ entry.
|
|
502
|
+
* The failure outbox is re-keyed in the same transaction so a crash between
|
|
503
|
+
* this write and in-memory propagation still targets the real parent.
|
|
504
|
+
*/
|
|
505
|
+
backpatchFlowChild(child, previousParentId) {
|
|
506
|
+
this.flushIfBuffered(child.id);
|
|
507
|
+
this.safeWrite(() => {
|
|
508
|
+
const tx = this.db.transaction(() => {
|
|
509
|
+
let persisted = this.db
|
|
510
|
+
.prepare('UPDATE jobs SET parent_id = ?, data = ? WHERE id = ?')
|
|
511
|
+
.run(child.parentId, pack(child.data), child.id).changes > 0;
|
|
512
|
+
const dlqRows = this.db
|
|
513
|
+
.query('SELECT id, entry FROM dlq WHERE job_id = ?')
|
|
514
|
+
.all(String(child.id));
|
|
515
|
+
for (const row of dlqRows) {
|
|
516
|
+
const entry = unpack(row.entry, null, `backpatchFlowChild:${String(child.id)}`);
|
|
517
|
+
if (!entry?.job)
|
|
518
|
+
continue;
|
|
519
|
+
const updated = {
|
|
520
|
+
...entry,
|
|
521
|
+
job: { ...entry.job, parentId: child.parentId, data: child.data },
|
|
522
|
+
};
|
|
523
|
+
this.db.prepare('UPDATE dlq SET entry = ? WHERE id = ?').run(pack(updated), row.id);
|
|
524
|
+
persisted = true;
|
|
525
|
+
}
|
|
526
|
+
if (!persisted) {
|
|
527
|
+
throw new Error(`Flow child is no longer persisted: ${String(child.id)}`);
|
|
528
|
+
}
|
|
529
|
+
if (previousParentId && child.parentId && previousParentId !== child.parentId) {
|
|
530
|
+
const failure = this.db
|
|
531
|
+
.query(`SELECT child_queue, mode, error, created_at
|
|
532
|
+
FROM flow_failures
|
|
533
|
+
WHERE parent_id = ? AND child_id = ?`)
|
|
534
|
+
.get(String(previousParentId), String(child.id));
|
|
535
|
+
if (failure) {
|
|
536
|
+
this.db
|
|
537
|
+
.prepare(`INSERT INTO flow_failures
|
|
538
|
+
(parent_id, child_id, child_queue, mode, error, created_at)
|
|
539
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
540
|
+
ON CONFLICT(parent_id, child_id) DO UPDATE SET
|
|
541
|
+
child_queue=excluded.child_queue, mode=excluded.mode,
|
|
542
|
+
error=excluded.error, created_at=excluded.created_at`)
|
|
543
|
+
.run(child.parentId, child.id, failure.child_queue, failure.mode, failure.error, failure.created_at);
|
|
544
|
+
this.db
|
|
545
|
+
.prepare('DELETE FROM flow_failures WHERE parent_id = ? AND child_id = ?')
|
|
546
|
+
.run(previousParentId, child.id);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
tx();
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
/** Persist both sides of a removed parent/child relationship atomically. */
|
|
554
|
+
removeFlowLink(child, parent, parentState) {
|
|
555
|
+
this.flushIfBuffered(child.id);
|
|
556
|
+
this.flushIfBuffered(parent.id);
|
|
557
|
+
this.safeWrite(() => {
|
|
558
|
+
const tx = this.db.transaction(() => {
|
|
559
|
+
this.db
|
|
560
|
+
.prepare('UPDATE jobs SET parent_id = ?, data = ? WHERE id = ?')
|
|
561
|
+
.run(child.parentId, pack(child.data), child.id);
|
|
562
|
+
this.db
|
|
563
|
+
.prepare(`UPDATE jobs
|
|
564
|
+
SET children_ids = ?, depends_on = ?, data = ?, run_at = ?, state = ?
|
|
565
|
+
WHERE id = ?`)
|
|
566
|
+
.run(parent.childrenIds.length > 0 ? pack(parent.childrenIds) : null, parent.dependsOn.length > 0 ? pack(parent.dependsOn) : null, pack(parent.data), parent.runAt, parentState, parent.id);
|
|
567
|
+
this.db
|
|
568
|
+
.prepare('DELETE FROM flow_failures WHERE parent_id = ? AND child_id = ?')
|
|
569
|
+
.run(parent.id, child.id);
|
|
570
|
+
});
|
|
571
|
+
tx();
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
/** Record terminal child propagation before applying its parent-side effect. */
|
|
575
|
+
saveFlowFailure(record) {
|
|
576
|
+
this.safeWrite(() => {
|
|
577
|
+
this.db
|
|
578
|
+
.prepare(`INSERT INTO flow_failures
|
|
579
|
+
(parent_id, child_id, child_queue, mode, error, created_at)
|
|
580
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
581
|
+
ON CONFLICT(parent_id, child_id) DO UPDATE SET
|
|
582
|
+
child_queue=excluded.child_queue, mode=excluded.mode,
|
|
583
|
+
error=excluded.error, created_at=excluded.created_at`)
|
|
584
|
+
.run(record.parentId, record.childId, record.childQueue, record.mode, record.error, record.createdAt);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
loadFlowFailures() {
|
|
588
|
+
const rows = this.db
|
|
589
|
+
.query('SELECT * FROM flow_failures ORDER BY created_at, parent_id, child_id')
|
|
590
|
+
.all();
|
|
591
|
+
return rows.map((row) => ({
|
|
592
|
+
parentId: row.parent_id,
|
|
593
|
+
childId: row.child_id,
|
|
594
|
+
childQueue: row.child_queue,
|
|
595
|
+
mode: row.mode,
|
|
596
|
+
error: row.error,
|
|
597
|
+
createdAt: row.created_at,
|
|
598
|
+
}));
|
|
599
|
+
}
|
|
600
|
+
deleteFlowFailure(parentId, childId) {
|
|
601
|
+
this.safeWrite(() => {
|
|
602
|
+
if (childId) {
|
|
603
|
+
this.db
|
|
604
|
+
.prepare('DELETE FROM flow_failures WHERE parent_id = ? AND child_id = ?')
|
|
605
|
+
.run(parentId, childId);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
this.db.prepare('DELETE FROM flow_failures WHERE parent_id = ?').run(parentId);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/** Persist dependency removal/promotion as one parent-side recovery checkpoint. */
|
|
613
|
+
updateFlowParentResolution(job, stateOverride) {
|
|
614
|
+
this.flushIfBuffered(job.id);
|
|
615
|
+
const state = stateOverride ??
|
|
616
|
+
(job.dependsOn.length > 0
|
|
617
|
+
? 'waiting-children'
|
|
618
|
+
: job.runAt > Date.now()
|
|
619
|
+
? 'delayed'
|
|
620
|
+
: job.priority > 0
|
|
621
|
+
? 'prioritized'
|
|
622
|
+
: 'waiting');
|
|
623
|
+
this.safeWrite(() => {
|
|
624
|
+
this.db
|
|
625
|
+
.prepare('UPDATE jobs SET depends_on = ?, run_at = ?, state = ?, timeline = ? WHERE id = ?')
|
|
626
|
+
.run(job.dependsOn.length > 0 ? pack(job.dependsOn) : null, job.runAt, state, job.timeline.length > 0 ? pack(job.timeline) : null, job.id);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
447
629
|
getJob(id) {
|
|
448
630
|
const row = this.statements.get('getJob').get(id);
|
|
449
631
|
return row ? rowToJob(row) : null;
|
|
@@ -495,18 +677,62 @@ export class SqliteStorage {
|
|
|
495
677
|
}
|
|
496
678
|
/** Load all completed job IDs (for dependency recovery) */
|
|
497
679
|
loadCompletedJobIds() {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
// done and unblocks dependents (instead of parking them forever).
|
|
680
|
+
// `jobs.state` is the completion authority. A result row can exist while its
|
|
681
|
+
// job is still active if a process dies between the two legacy writes, so it
|
|
682
|
+
// must never release a dependency by itself.
|
|
683
|
+
const ids = new Set();
|
|
503
684
|
const stateRows = this.db
|
|
504
685
|
.query("SELECT id FROM jobs WHERE state = 'completed'")
|
|
505
686
|
.all();
|
|
506
687
|
for (const r of stateRows)
|
|
507
688
|
ids.add(r.id);
|
|
689
|
+
for (const record of this.dependencyCompletionStore.load())
|
|
690
|
+
ids.add(record.jobId);
|
|
508
691
|
return ids;
|
|
509
692
|
}
|
|
693
|
+
/** Atomically replace a removed completed job with payload-free dependency evidence. */
|
|
694
|
+
commitRemovedCompletion(job, retentionLimit, pinned, completedAt = Date.now()) {
|
|
695
|
+
this.writeBuffer.removePending(job.id);
|
|
696
|
+
this.safeWrite(() => {
|
|
697
|
+
this.dependencyCompletionStore.commit({ jobId: job.id, queue: job.queue, completedAt, pinned }, retentionLimit);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
loadDependencyCompletions() {
|
|
701
|
+
return this.dependencyCompletionStore.load();
|
|
702
|
+
}
|
|
703
|
+
pinDependencyCompletions(jobIds) {
|
|
704
|
+
this.safeWrite(() => {
|
|
705
|
+
this.dependencyCompletionStore.pin(jobIds);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
unpinDependencyCompletions(jobIds, retentionLimit) {
|
|
709
|
+
let records = [];
|
|
710
|
+
this.safeWrite(() => {
|
|
711
|
+
records = this.dependencyCompletionStore.unpin(jobIds, retentionLimit);
|
|
712
|
+
});
|
|
713
|
+
return records;
|
|
714
|
+
}
|
|
715
|
+
reconcileDependencyCompletionPins(referenced, retentionLimit) {
|
|
716
|
+
let records = [];
|
|
717
|
+
this.safeWrite(() => {
|
|
718
|
+
records = this.dependencyCompletionStore.reconcilePins(referenced, retentionLimit);
|
|
719
|
+
});
|
|
720
|
+
return records;
|
|
721
|
+
}
|
|
722
|
+
deleteDependencyCompletion(jobId) {
|
|
723
|
+
let deleted = false;
|
|
724
|
+
this.safeWrite(() => {
|
|
725
|
+
deleted = this.dependencyCompletionStore.delete(jobId);
|
|
726
|
+
});
|
|
727
|
+
return deleted;
|
|
728
|
+
}
|
|
729
|
+
deleteDependencyCompletionsForQueue(queue) {
|
|
730
|
+
let deleted = [];
|
|
731
|
+
this.safeWrite(() => {
|
|
732
|
+
deleted = this.dependencyCompletionStore.deleteForQueue(queue);
|
|
733
|
+
});
|
|
734
|
+
return deleted;
|
|
735
|
+
}
|
|
510
736
|
// ============ Bulk Operations ============
|
|
511
737
|
/**
|
|
512
738
|
* Insert batch of jobs. By default the jobs go through the write buffer.
|
|
@@ -573,15 +799,15 @@ export class SqliteStorage {
|
|
|
573
799
|
const predicateParams = [];
|
|
574
800
|
const requested = new Set(uniqueStates);
|
|
575
801
|
if (requested.has('waiting')) {
|
|
576
|
-
predicates.push("(state IN ('waiting', 'delayed') AND run_at <= ? AND priority <= 0)");
|
|
802
|
+
predicates.push("(state IN ('waiting', 'prioritized', 'delayed') AND run_at <= ? AND priority <= 0)");
|
|
577
803
|
predicateParams.push(options.now);
|
|
578
804
|
}
|
|
579
805
|
if (requested.has('prioritized')) {
|
|
580
|
-
predicates.push("(state IN ('waiting', 'delayed') AND run_at <= ? AND priority > 0)");
|
|
806
|
+
predicates.push("(state IN ('waiting', 'prioritized', 'delayed') AND run_at <= ? AND priority > 0)");
|
|
581
807
|
predicateParams.push(options.now);
|
|
582
808
|
}
|
|
583
809
|
if (requested.has('delayed')) {
|
|
584
|
-
predicates.push("(state IN ('waiting', 'delayed') AND run_at > ?)");
|
|
810
|
+
predicates.push("(state IN ('waiting', 'prioritized', 'delayed') AND run_at > ?)");
|
|
585
811
|
predicateParams.push(options.now);
|
|
586
812
|
}
|
|
587
813
|
const persistedStates = uniqueStates.filter((state) => state !== 'waiting' && state !== 'prioritized' && state !== 'delayed');
|
|
@@ -607,7 +833,7 @@ export class SqliteStorage {
|
|
|
607
833
|
*/
|
|
608
834
|
loadPendingJobs(limit = 10000, offset = 0) {
|
|
609
835
|
const rows = this.db
|
|
610
|
-
.query("SELECT * FROM jobs WHERE state IN ('waiting', 'delayed') ORDER BY priority DESC, run_at ASC, id ASC LIMIT ? OFFSET ?")
|
|
836
|
+
.query("SELECT * FROM jobs WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed') ORDER BY priority DESC, run_at ASC, id ASC LIMIT ? OFFSET ?")
|
|
611
837
|
.all(limit, offset);
|
|
612
838
|
return rows.map((row) => rowToJob(row));
|
|
613
839
|
}
|
|
@@ -639,7 +865,7 @@ export class SqliteStorage {
|
|
|
639
865
|
*/
|
|
640
866
|
countPendingJobs() {
|
|
641
867
|
const result = this.db
|
|
642
|
-
.query("SELECT COUNT(*) as count FROM jobs WHERE state IN ('waiting', 'delayed')")
|
|
868
|
+
.query("SELECT COUNT(*) as count FROM jobs WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed')")
|
|
643
869
|
.get();
|
|
644
870
|
return result?.count ?? 0;
|
|
645
871
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* SQLite Batch Operations
|
|
3
3
|
* High-performance batch insert with prepared statement caching
|
|
4
4
|
*/
|
|
5
|
-
import { pack, persistedStallCount } from './sqliteSerializer';
|
|
6
|
-
const COLS_PER_ROW =
|
|
5
|
+
import { pack, persistedInitialState, persistedStallCount } from './sqliteSerializer';
|
|
6
|
+
const COLS_PER_ROW = 29;
|
|
7
7
|
// SQLite has a limit of ~999 variables, so batch in chunks
|
|
8
8
|
const MAX_ROWS_PER_INSERT = Math.floor(999 / COLS_PER_ROW);
|
|
9
9
|
/**
|
|
@@ -75,7 +75,7 @@ export class BatchInsertManager {
|
|
|
75
75
|
getBatchInsertStmt(size) {
|
|
76
76
|
let stmt = this.cache.get(size);
|
|
77
77
|
if (!stmt) {
|
|
78
|
-
const rowPlaceholder = '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
|
78
|
+
const rowPlaceholder = '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
|
79
79
|
const placeholders = Array(size).fill(rowPlaceholder).join(', ');
|
|
80
80
|
// ON CONFLICT(id) DO UPDATE (upsert): a colliding id overwrites in place instead
|
|
81
81
|
// of failing the whole flush. Without it, one stale ORPHAN row (left when
|
|
@@ -87,6 +87,8 @@ export class BatchInsertManager {
|
|
|
87
87
|
id, queue, data, priority, created_at, run_at, attempts, max_attempts,
|
|
88
88
|
backoff, ttl, timeout, unique_key, custom_id, depends_on, parent_id,
|
|
89
89
|
children_ids, tags, state, lifo, group_id, remove_on_complete, remove_on_fail,
|
|
90
|
+
fail_parent_on_failure, remove_dependency_on_failure,
|
|
91
|
+
continue_parent_on_failure, ignore_dependency_on_failure,
|
|
90
92
|
stall_timeout, stall_count, timeline
|
|
91
93
|
) VALUES ${placeholders}
|
|
92
94
|
ON CONFLICT(id) DO UPDATE SET
|
|
@@ -97,6 +99,10 @@ export class BatchInsertManager {
|
|
|
97
99
|
depends_on=excluded.depends_on, parent_id=excluded.parent_id, children_ids=excluded.children_ids,
|
|
98
100
|
tags=excluded.tags, state=excluded.state, lifo=excluded.lifo, group_id=excluded.group_id,
|
|
99
101
|
remove_on_complete=excluded.remove_on_complete, remove_on_fail=excluded.remove_on_fail,
|
|
102
|
+
fail_parent_on_failure=excluded.fail_parent_on_failure,
|
|
103
|
+
remove_dependency_on_failure=excluded.remove_dependency_on_failure,
|
|
104
|
+
continue_parent_on_failure=excluded.continue_parent_on_failure,
|
|
105
|
+
ignore_dependency_on_failure=excluded.ignore_dependency_on_failure,
|
|
100
106
|
stall_timeout=excluded.stall_timeout, stall_count=excluded.stall_count,
|
|
101
107
|
timeline=excluded.timeline,
|
|
102
108
|
started_at=excluded.started_at, completed_at=excluded.completed_at,
|
|
@@ -116,7 +122,7 @@ export class BatchInsertManager {
|
|
|
116
122
|
// Flatten all values
|
|
117
123
|
const values = [];
|
|
118
124
|
for (const job of jobs) {
|
|
119
|
-
values.push(job.id, job.queue, pack(job.data), job.priority, job.createdAt, job.runAt, job.attempts, job.maxAttempts, job.backoff, job.ttl, job.timeout, job.uniqueKey, job.customId, job.dependsOn.length > 0 ? pack(job.dependsOn) : null, job.parentId, job.childrenIds.length > 0 ? pack(job.childrenIds) : null, job.tags.length > 0 ? pack(job.tags) : null, job.
|
|
125
|
+
values.push(job.id, job.queue, pack(job.data), job.priority, job.createdAt, job.runAt, job.attempts, job.maxAttempts, job.backoff, job.ttl, job.timeout, job.uniqueKey, job.customId, job.dependsOn.length > 0 ? pack(job.dependsOn) : null, job.parentId, job.childrenIds.length > 0 ? pack(job.childrenIds) : null, job.tags.length > 0 ? pack(job.tags) : null, persistedInitialState(job, now), job.lifo ? 1 : 0, job.groupId, job.removeOnComplete ? 1 : 0, job.removeOnFail ? 1 : 0, job.failParentOnFailure ? 1 : 0, job.removeDependencyOnFailure ? 1 : 0, job.continueParentOnFailure ? 1 : 0, job.ignoreDependencyOnFailure ? 1 : 0, job.stallTimeout, persistedStallCount(job), job.timeline.length > 0 ? pack(job.timeline) : null);
|
|
120
126
|
}
|
|
121
127
|
stmt.run(...values);
|
|
122
128
|
}
|
|
@@ -16,6 +16,8 @@ export declare function unpack<T>(buffer: Uint8Array | null, fallback: T, contex
|
|
|
16
16
|
* field receives the same zero default as a freshly created Job.
|
|
17
17
|
*/
|
|
18
18
|
export declare function persistedStallCount(job: Pick<Job, 'stallCount'>): number;
|
|
19
|
+
/** Derive the durable initial state from the authoritative timeline. */
|
|
20
|
+
export declare function persistedInitialState(job: Job, now?: number): string;
|
|
19
21
|
/**
|
|
20
22
|
* Symbol marker stamped on a Job whose `depends_on` blob failed to decode.
|
|
21
23
|
*
|
|
@@ -30,6 +32,9 @@ export declare function persistedStallCount(job: Pick<Job, 'stallCount'>): numbe
|
|
|
30
32
|
* so it only exists on the in-memory Job for the duration of recovery.
|
|
31
33
|
*/
|
|
32
34
|
export declare const CORRUPT_DEPENDS_ON: unique symbol;
|
|
35
|
+
export type PersistedJobState = 'active' | 'completed' | 'delayed' | 'prioritized' | 'waiting' | 'waiting-children';
|
|
36
|
+
/** Read the authoritative SQLite state carried by a recovered job. */
|
|
37
|
+
export declare function persistedJobState(job: Job): PersistedJobState | undefined;
|
|
33
38
|
/** True if a Job was recovered with a corrupt `depends_on` blob. */
|
|
34
39
|
export declare function isCorruptDependsOn(job: Job): boolean;
|
|
35
40
|
/** Convert database row to Job object */
|
|
@@ -30,6 +30,19 @@ export function unpack(buffer, fallback, context) {
|
|
|
30
30
|
export function persistedStallCount(job) {
|
|
31
31
|
return job.stallCount ?? 0;
|
|
32
32
|
}
|
|
33
|
+
/** Derive the durable initial state from the authoritative timeline. */
|
|
34
|
+
export function persistedInitialState(job, now = Date.now()) {
|
|
35
|
+
const latest = job.timeline[job.timeline.length - 1]?.state;
|
|
36
|
+
if (latest === 'waiting-children' ||
|
|
37
|
+
latest === 'waiting' ||
|
|
38
|
+
latest === 'prioritized' ||
|
|
39
|
+
latest === 'delayed') {
|
|
40
|
+
return latest;
|
|
41
|
+
}
|
|
42
|
+
if (job.runAt > now)
|
|
43
|
+
return 'delayed';
|
|
44
|
+
return job.priority > 0 ? 'prioritized' : 'waiting';
|
|
45
|
+
}
|
|
33
46
|
/**
|
|
34
47
|
* Symbol marker stamped on a Job whose `depends_on` blob failed to decode.
|
|
35
48
|
*
|
|
@@ -44,6 +57,11 @@ export function persistedStallCount(job) {
|
|
|
44
57
|
* so it only exists on the in-memory Job for the duration of recovery.
|
|
45
58
|
*/
|
|
46
59
|
export const CORRUPT_DEPENDS_ON = Symbol('bunqueue.corruptDependsOn');
|
|
60
|
+
const PERSISTED_JOB_STATE = Symbol('bunqueue.persistedJobState');
|
|
61
|
+
/** Read the authoritative SQLite state carried by a recovered job. */
|
|
62
|
+
export function persistedJobState(job) {
|
|
63
|
+
return job[PERSISTED_JOB_STATE];
|
|
64
|
+
}
|
|
47
65
|
/** True if a Job was recovered with a corrupt `depends_on` blob. */
|
|
48
66
|
export function isCorruptDependsOn(job) {
|
|
49
67
|
return job[CORRUPT_DEPENDS_ON] === true;
|
|
@@ -113,14 +131,14 @@ export function rowToJob(row) {
|
|
|
113
131
|
lastHeartbeat: row.last_heartbeat ?? row.created_at,
|
|
114
132
|
stallTimeout: row.stall_timeout,
|
|
115
133
|
stallCount: row.stall_count ?? 0,
|
|
116
|
-
// BullMQ v5 additional fields
|
|
134
|
+
// BullMQ v5 additional fields
|
|
117
135
|
stackTraceLimit: 10,
|
|
118
136
|
keepLogs: null,
|
|
119
137
|
sizeLimit: null,
|
|
120
|
-
failParentOnFailure:
|
|
121
|
-
removeDependencyOnFailure:
|
|
122
|
-
continueParentOnFailure:
|
|
123
|
-
ignoreDependencyOnFailure:
|
|
138
|
+
failParentOnFailure: row.fail_parent_on_failure === 1,
|
|
139
|
+
removeDependencyOnFailure: row.remove_dependency_on_failure === 1,
|
|
140
|
+
continueParentOnFailure: row.continue_parent_on_failure === 1,
|
|
141
|
+
ignoreDependencyOnFailure: row.ignore_dependency_on_failure === 1,
|
|
124
142
|
deduplicationTtl: null,
|
|
125
143
|
deduplicationExtend: false,
|
|
126
144
|
deduplicationReplace: false,
|
|
@@ -133,6 +151,11 @@ export function rowToJob(row) {
|
|
|
133
151
|
? unpack(row.stacktrace, null, `${jobContext}:stacktrace`)
|
|
134
152
|
: null,
|
|
135
153
|
};
|
|
154
|
+
Object.defineProperty(job, PERSISTED_JOB_STATE, {
|
|
155
|
+
value: row.state,
|
|
156
|
+
enumerable: false,
|
|
157
|
+
configurable: false,
|
|
158
|
+
});
|
|
136
159
|
// Stamp a collision-proof corruption marker (non-enumerable Symbol, never
|
|
137
160
|
// persisted) so the recovery path routes this job to the DLQ rather than
|
|
138
161
|
// enqueuing it as ready. We keep dependsOn: [] here — the real deps are
|
|
@@ -37,6 +37,10 @@ export interface DbJob {
|
|
|
37
37
|
progress_msg: string | null;
|
|
38
38
|
remove_on_complete: number;
|
|
39
39
|
remove_on_fail: number;
|
|
40
|
+
fail_parent_on_failure: number;
|
|
41
|
+
remove_dependency_on_failure: number;
|
|
42
|
+
continue_parent_on_failure: number;
|
|
43
|
+
ignore_dependency_on_failure: number;
|
|
40
44
|
stall_timeout: number | null;
|
|
41
45
|
last_heartbeat: number | null;
|
|
42
46
|
stall_count: number;
|