@nicnocquee/dataqueue 1.24.0 → 1.26.0-beta.20260223195940
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 +44 -0
- package/ai/build-docs-content.ts +96 -0
- package/ai/build-llms-full.ts +42 -0
- package/ai/docs-content.json +278 -0
- package/ai/rules/advanced.md +132 -0
- package/ai/rules/basic.md +159 -0
- package/ai/rules/react-dashboard.md +83 -0
- package/ai/skills/dataqueue-advanced/SKILL.md +320 -0
- package/ai/skills/dataqueue-core/SKILL.md +234 -0
- package/ai/skills/dataqueue-react/SKILL.md +189 -0
- package/dist/cli.cjs +1149 -14
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +66 -1
- package/dist/cli.d.ts +66 -1
- package/dist/cli.js +1146 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +4630 -928
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1033 -15
- package/dist/index.d.ts +1033 -15
- package/dist/index.js +4626 -929
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +186 -0
- package/dist/mcp-server.cjs.map +1 -0
- package/dist/mcp-server.d.cts +32 -0
- package/dist/mcp-server.d.ts +32 -0
- package/dist/mcp-server.js +175 -0
- package/dist/mcp-server.js.map +1 -0
- package/migrations/1751131910825_add_timeout_seconds_to_job_queue.sql +2 -2
- package/migrations/1751186053000_add_job_events_table.sql +12 -8
- package/migrations/1751984773000_add_tags_to_job_queue.sql +1 -1
- package/migrations/1765809419000_add_force_kill_on_timeout_to_job_queue.sql +1 -1
- package/migrations/1771100000000_add_idempotency_key_to_job_queue.sql +7 -0
- package/migrations/1781200000000_add_wait_support.sql +12 -0
- package/migrations/1781200000001_create_waitpoints_table.sql +18 -0
- package/migrations/1781200000002_add_performance_indexes.sql +34 -0
- package/migrations/1781200000003_add_progress_to_job_queue.sql +7 -0
- package/migrations/1781200000004_create_cron_schedules_table.sql +33 -0
- package/migrations/1781200000005_add_retry_config_to_job_queue.sql +17 -0
- package/package.json +40 -23
- package/src/backend.ts +328 -0
- package/src/backends/postgres.ts +2040 -0
- package/src/backends/redis-scripts.ts +865 -0
- package/src/backends/redis.test.ts +1906 -0
- package/src/backends/redis.ts +1792 -0
- package/src/cli.test.ts +82 -6
- package/src/cli.ts +73 -10
- package/src/cron.test.ts +126 -0
- package/src/cron.ts +40 -0
- package/src/db-util.ts +4 -2
- package/src/index.test.ts +688 -1
- package/src/index.ts +277 -39
- package/src/init-command.test.ts +449 -0
- package/src/init-command.ts +709 -0
- package/src/install-mcp-command.test.ts +216 -0
- package/src/install-mcp-command.ts +185 -0
- package/src/install-rules-command.test.ts +218 -0
- package/src/install-rules-command.ts +233 -0
- package/src/install-skills-command.test.ts +176 -0
- package/src/install-skills-command.ts +124 -0
- package/src/mcp-server.test.ts +162 -0
- package/src/mcp-server.ts +231 -0
- package/src/processor.test.ts +559 -18
- package/src/processor.ts +456 -49
- package/src/queue.test.ts +682 -6
- package/src/queue.ts +135 -944
- package/src/supervisor.test.ts +340 -0
- package/src/supervisor.ts +162 -0
- package/src/test-util.ts +32 -0
- package/src/types.ts +726 -17
- package/src/wait.test.ts +698 -0
- package/LICENSE +0 -21
package/src/queue.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backward-compatible re-exports.
|
|
3
|
+
* All SQL logic has moved to backends/postgres.ts (PostgresBackend class).
|
|
4
|
+
* These functions delegate to a temporary PostgresBackend instance so that
|
|
5
|
+
* any existing internal callers continue to work.
|
|
6
|
+
*
|
|
7
|
+
* Wait-related functions (waitJob, updateStepData, createWaitpoint, etc.)
|
|
8
|
+
* are PostgreSQL-only and use direct SQL queries.
|
|
9
|
+
*/
|
|
1
10
|
import { Pool } from 'pg';
|
|
2
11
|
import {
|
|
3
12
|
JobOptions,
|
|
@@ -6,146 +15,41 @@ import {
|
|
|
6
15
|
JobEvent,
|
|
7
16
|
JobEventType,
|
|
8
17
|
TagQueryMode,
|
|
18
|
+
WaitpointRecord,
|
|
19
|
+
AddJobOptions,
|
|
9
20
|
} from './types.js';
|
|
10
|
-
import {
|
|
21
|
+
import { PostgresBackend } from './backends/postgres.js';
|
|
22
|
+
|
|
23
|
+
/* Thin wrappers — every function creates a lightweight backend wrapper
|
|
24
|
+
around the given pool and forwards the call. The class itself holds
|
|
25
|
+
no mutable state so this is safe and cheap. */
|
|
11
26
|
|
|
12
|
-
/**
|
|
13
|
-
* Record a job event in the job_events table
|
|
14
|
-
*/
|
|
15
27
|
export const recordJobEvent = async (
|
|
16
28
|
pool: Pool,
|
|
17
29
|
jobId: number,
|
|
18
30
|
eventType: JobEventType,
|
|
19
31
|
metadata?: any,
|
|
20
|
-
): Promise<void> =>
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
await client.query(
|
|
24
|
-
`INSERT INTO job_events (job_id, event_type, metadata) VALUES ($1, $2, $3)`,
|
|
25
|
-
[jobId, eventType, metadata ? JSON.stringify(metadata) : null],
|
|
26
|
-
);
|
|
27
|
-
} catch (error) {
|
|
28
|
-
log(`Error recording job event for job ${jobId}: ${error}`);
|
|
29
|
-
// Do not throw, to avoid interfering with main job logic
|
|
30
|
-
} finally {
|
|
31
|
-
client.release();
|
|
32
|
-
}
|
|
33
|
-
};
|
|
32
|
+
): Promise<void> =>
|
|
33
|
+
new PostgresBackend(pool).recordJobEvent(jobId, eventType, metadata);
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Add a job to the queue
|
|
37
|
-
*/
|
|
38
35
|
export const addJob = async <PayloadMap, T extends keyof PayloadMap & string>(
|
|
39
36
|
pool: Pool,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}: JobOptions<PayloadMap, T>,
|
|
50
|
-
): Promise<number> => {
|
|
51
|
-
const client = await pool.connect();
|
|
52
|
-
try {
|
|
53
|
-
let result;
|
|
54
|
-
if (runAt) {
|
|
55
|
-
result = await client.query(
|
|
56
|
-
`INSERT INTO job_queue
|
|
57
|
-
(job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags)
|
|
58
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
59
|
-
RETURNING id`,
|
|
60
|
-
[
|
|
61
|
-
jobType,
|
|
62
|
-
payload,
|
|
63
|
-
maxAttempts,
|
|
64
|
-
priority,
|
|
65
|
-
runAt,
|
|
66
|
-
timeoutMs ?? null,
|
|
67
|
-
forceKillOnTimeout ?? false,
|
|
68
|
-
tags ?? null,
|
|
69
|
-
],
|
|
70
|
-
);
|
|
71
|
-
log(
|
|
72
|
-
`Added job ${result.rows[0].id}: payload ${JSON.stringify(payload)}, runAt ${runAt.toISOString()}, priority ${priority}, maxAttempts ${maxAttempts} jobType ${jobType}, tags ${JSON.stringify(tags)}`,
|
|
73
|
-
);
|
|
74
|
-
} else {
|
|
75
|
-
result = await client.query(
|
|
76
|
-
`INSERT INTO job_queue
|
|
77
|
-
(job_type, payload, max_attempts, priority, timeout_ms, force_kill_on_timeout, tags)
|
|
78
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
79
|
-
RETURNING id`,
|
|
80
|
-
[
|
|
81
|
-
jobType,
|
|
82
|
-
payload,
|
|
83
|
-
maxAttempts,
|
|
84
|
-
priority,
|
|
85
|
-
timeoutMs ?? null,
|
|
86
|
-
forceKillOnTimeout ?? false,
|
|
87
|
-
tags ?? null,
|
|
88
|
-
],
|
|
89
|
-
);
|
|
90
|
-
log(
|
|
91
|
-
`Added job ${result.rows[0].id}: payload ${JSON.stringify(payload)}, priority ${priority}, maxAttempts ${maxAttempts} jobType ${jobType}, tags ${JSON.stringify(tags)}`,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
await recordJobEvent(pool, result.rows[0].id, JobEventType.Added, {
|
|
95
|
-
jobType,
|
|
96
|
-
payload,
|
|
97
|
-
tags,
|
|
98
|
-
});
|
|
99
|
-
return result.rows[0].id;
|
|
100
|
-
} catch (error) {
|
|
101
|
-
log(`Error adding job: ${error}`);
|
|
102
|
-
throw error;
|
|
103
|
-
} finally {
|
|
104
|
-
client.release();
|
|
105
|
-
}
|
|
106
|
-
};
|
|
37
|
+
job: JobOptions<PayloadMap, T>,
|
|
38
|
+
options?: AddJobOptions,
|
|
39
|
+
): Promise<number> => new PostgresBackend(pool).addJob(job, options);
|
|
40
|
+
|
|
41
|
+
export const addJobs = async <PayloadMap, T extends keyof PayloadMap & string>(
|
|
42
|
+
pool: Pool,
|
|
43
|
+
jobs: JobOptions<PayloadMap, T>[],
|
|
44
|
+
options?: AddJobOptions,
|
|
45
|
+
): Promise<number[]> => new PostgresBackend(pool).addJobs(jobs, options);
|
|
107
46
|
|
|
108
|
-
/**
|
|
109
|
-
* Get a job by ID
|
|
110
|
-
*/
|
|
111
47
|
export const getJob = async <PayloadMap, T extends keyof PayloadMap & string>(
|
|
112
48
|
pool: Pool,
|
|
113
49
|
id: number,
|
|
114
|
-
): Promise<JobRecord<PayloadMap, T> | null> =>
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const result = await client.query(
|
|
118
|
-
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags FROM job_queue WHERE id = $1`,
|
|
119
|
-
[id],
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
if (result.rows.length === 0) {
|
|
123
|
-
log(`Job ${id} not found`);
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
50
|
+
): Promise<JobRecord<PayloadMap, T> | null> =>
|
|
51
|
+
new PostgresBackend(pool).getJob<PayloadMap, T>(id);
|
|
126
52
|
|
|
127
|
-
log(`Found job ${id}`);
|
|
128
|
-
|
|
129
|
-
const job = result.rows[0] as JobRecord<PayloadMap, T>;
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
...job,
|
|
133
|
-
payload: job.payload,
|
|
134
|
-
timeoutMs: job.timeoutMs,
|
|
135
|
-
forceKillOnTimeout: job.forceKillOnTimeout,
|
|
136
|
-
failureReason: job.failureReason,
|
|
137
|
-
};
|
|
138
|
-
} catch (error) {
|
|
139
|
-
log(`Error getting job ${id}: ${error}`);
|
|
140
|
-
throw error;
|
|
141
|
-
} finally {
|
|
142
|
-
client.release();
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get jobs by status
|
|
148
|
-
*/
|
|
149
53
|
export const getJobsByStatus = async <
|
|
150
54
|
PayloadMap,
|
|
151
55
|
T extends keyof PayloadMap & string,
|
|
@@ -154,38 +58,13 @@ export const getJobsByStatus = async <
|
|
|
154
58
|
status: string,
|
|
155
59
|
limit = 100,
|
|
156
60
|
offset = 0,
|
|
157
|
-
): Promise<JobRecord<PayloadMap, T>[]> =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
log(`Found ${result.rows.length} jobs by status ${status}`);
|
|
166
|
-
|
|
167
|
-
return result.rows.map((job) => ({
|
|
168
|
-
...job,
|
|
169
|
-
payload: job.payload,
|
|
170
|
-
timeoutMs: job.timeoutMs,
|
|
171
|
-
forceKillOnTimeout: job.forceKillOnTimeout,
|
|
172
|
-
failureReason: job.failureReason,
|
|
173
|
-
}));
|
|
174
|
-
} catch (error) {
|
|
175
|
-
log(`Error getting jobs by status ${status}: ${error}`);
|
|
176
|
-
throw error;
|
|
177
|
-
} finally {
|
|
178
|
-
client.release();
|
|
179
|
-
}
|
|
180
|
-
};
|
|
61
|
+
): Promise<JobRecord<PayloadMap, T>[]> =>
|
|
62
|
+
new PostgresBackend(pool).getJobsByStatus<PayloadMap, T>(
|
|
63
|
+
status,
|
|
64
|
+
limit,
|
|
65
|
+
offset,
|
|
66
|
+
);
|
|
181
67
|
|
|
182
|
-
/**
|
|
183
|
-
* Get the next batch of jobs to process
|
|
184
|
-
* @param pool - The database pool
|
|
185
|
-
* @param workerId - The worker ID
|
|
186
|
-
* @param batchSize - The batch size
|
|
187
|
-
* @param jobType - Only fetch jobs with this job type (string or array of strings)
|
|
188
|
-
*/
|
|
189
68
|
export const getNextBatch = async <
|
|
190
69
|
PayloadMap,
|
|
191
70
|
T extends keyof PayloadMap & string,
|
|
@@ -194,234 +73,40 @@ export const getNextBatch = async <
|
|
|
194
73
|
workerId: string,
|
|
195
74
|
batchSize = 10,
|
|
196
75
|
jobType?: string | string[],
|
|
197
|
-
): Promise<JobRecord<PayloadMap, T>[]> =>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Build job type filter
|
|
204
|
-
let jobTypeFilter = '';
|
|
205
|
-
let params: any[] = [workerId, batchSize];
|
|
206
|
-
if (jobType) {
|
|
207
|
-
if (Array.isArray(jobType)) {
|
|
208
|
-
jobTypeFilter = ` AND job_type = ANY($3)`;
|
|
209
|
-
params.push(jobType);
|
|
210
|
-
} else {
|
|
211
|
-
jobTypeFilter = ` AND job_type = $3`;
|
|
212
|
-
params.push(jobType);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Get and lock a batch of jobs
|
|
217
|
-
const result = await client.query(
|
|
218
|
-
`
|
|
219
|
-
UPDATE job_queue
|
|
220
|
-
SET status = 'processing',
|
|
221
|
-
locked_at = NOW(),
|
|
222
|
-
locked_by = $1,
|
|
223
|
-
attempts = attempts + 1,
|
|
224
|
-
updated_at = NOW(),
|
|
225
|
-
pending_reason = NULL,
|
|
226
|
-
started_at = COALESCE(started_at, NOW()),
|
|
227
|
-
last_retried_at = CASE WHEN attempts > 0 THEN NOW() ELSE last_retried_at END
|
|
228
|
-
WHERE id IN (
|
|
229
|
-
SELECT id FROM job_queue
|
|
230
|
-
WHERE (status = 'pending' OR (status = 'failed' AND next_attempt_at <= NOW()))
|
|
231
|
-
AND (attempts < max_attempts)
|
|
232
|
-
AND run_at <= NOW()
|
|
233
|
-
${jobTypeFilter}
|
|
234
|
-
ORDER BY priority DESC, created_at ASC
|
|
235
|
-
LIMIT $2
|
|
236
|
-
FOR UPDATE SKIP LOCKED
|
|
237
|
-
)
|
|
238
|
-
RETURNING id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason"
|
|
239
|
-
`,
|
|
240
|
-
params,
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
log(`Found ${result.rows.length} jobs to process`);
|
|
244
|
-
|
|
245
|
-
// Commit transaction
|
|
246
|
-
await client.query('COMMIT');
|
|
76
|
+
): Promise<JobRecord<PayloadMap, T>[]> =>
|
|
77
|
+
new PostgresBackend(pool).getNextBatch<PayloadMap, T>(
|
|
78
|
+
workerId,
|
|
79
|
+
batchSize,
|
|
80
|
+
jobType,
|
|
81
|
+
);
|
|
247
82
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
await recordJobEvent(pool, row.id, JobEventType.Processing);
|
|
251
|
-
}
|
|
83
|
+
export const completeJob = async (pool: Pool, jobId: number): Promise<void> =>
|
|
84
|
+
new PostgresBackend(pool).completeJob(jobId);
|
|
252
85
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
payload: job.payload,
|
|
256
|
-
timeoutMs: job.timeoutMs,
|
|
257
|
-
forceKillOnTimeout: job.forceKillOnTimeout,
|
|
258
|
-
}));
|
|
259
|
-
} catch (error) {
|
|
260
|
-
log(`Error getting next batch: ${error}`);
|
|
261
|
-
await client.query('ROLLBACK');
|
|
262
|
-
throw error;
|
|
263
|
-
} finally {
|
|
264
|
-
client.release();
|
|
265
|
-
}
|
|
266
|
-
};
|
|
86
|
+
export const prolongJob = async (pool: Pool, jobId: number): Promise<void> =>
|
|
87
|
+
new PostgresBackend(pool).prolongJob(jobId);
|
|
267
88
|
|
|
268
|
-
/**
|
|
269
|
-
* Mark a job as completed
|
|
270
|
-
*/
|
|
271
|
-
export const completeJob = async (pool: Pool, jobId: number): Promise<void> => {
|
|
272
|
-
const client = await pool.connect();
|
|
273
|
-
try {
|
|
274
|
-
await client.query(
|
|
275
|
-
`
|
|
276
|
-
UPDATE job_queue
|
|
277
|
-
SET status = 'completed', updated_at = NOW(), completed_at = NOW()
|
|
278
|
-
WHERE id = $1
|
|
279
|
-
`,
|
|
280
|
-
[jobId],
|
|
281
|
-
);
|
|
282
|
-
await recordJobEvent(pool, jobId, JobEventType.Completed);
|
|
283
|
-
} catch (error) {
|
|
284
|
-
log(`Error completing job ${jobId}: ${error}`);
|
|
285
|
-
throw error;
|
|
286
|
-
} finally {
|
|
287
|
-
log(`Completed job ${jobId}`);
|
|
288
|
-
client.release();
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Mark a job as failed
|
|
294
|
-
*/
|
|
295
89
|
export const failJob = async (
|
|
296
90
|
pool: Pool,
|
|
297
91
|
jobId: number,
|
|
298
92
|
error: Error,
|
|
299
93
|
failureReason?: FailureReason,
|
|
300
|
-
): Promise<void> =>
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
/**
|
|
304
|
-
* The next attempt will be scheduled after `2^attempts * 1 minute` from the last attempt.
|
|
305
|
-
*/
|
|
306
|
-
await client.query(
|
|
307
|
-
`
|
|
308
|
-
UPDATE job_queue
|
|
309
|
-
SET status = 'failed',
|
|
310
|
-
updated_at = NOW(),
|
|
311
|
-
next_attempt_at = CASE
|
|
312
|
-
WHEN attempts < max_attempts THEN NOW() + (POWER(2, attempts) * INTERVAL '1 minute')
|
|
313
|
-
ELSE NULL
|
|
314
|
-
END,
|
|
315
|
-
error_history = COALESCE(error_history, '[]'::jsonb) || $2::jsonb,
|
|
316
|
-
failure_reason = $3,
|
|
317
|
-
last_failed_at = NOW()
|
|
318
|
-
WHERE id = $1
|
|
319
|
-
`,
|
|
320
|
-
[
|
|
321
|
-
jobId,
|
|
322
|
-
JSON.stringify([
|
|
323
|
-
{
|
|
324
|
-
message: error.message || String(error),
|
|
325
|
-
timestamp: new Date().toISOString(),
|
|
326
|
-
},
|
|
327
|
-
]),
|
|
328
|
-
failureReason ?? null,
|
|
329
|
-
],
|
|
330
|
-
);
|
|
331
|
-
await recordJobEvent(pool, jobId, JobEventType.Failed, {
|
|
332
|
-
message: error.message || String(error),
|
|
333
|
-
failureReason,
|
|
334
|
-
});
|
|
335
|
-
} catch (error) {
|
|
336
|
-
log(`Error failing job ${jobId}: ${error}`);
|
|
337
|
-
throw error;
|
|
338
|
-
} finally {
|
|
339
|
-
log(`Failed job ${jobId}`);
|
|
340
|
-
client.release();
|
|
341
|
-
}
|
|
342
|
-
};
|
|
94
|
+
): Promise<void> =>
|
|
95
|
+
new PostgresBackend(pool).failJob(jobId, error, failureReason);
|
|
343
96
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
*/
|
|
347
|
-
export const retryJob = async (pool: Pool, jobId: number): Promise<void> => {
|
|
348
|
-
const client = await pool.connect();
|
|
349
|
-
try {
|
|
350
|
-
await client.query(
|
|
351
|
-
`
|
|
352
|
-
UPDATE job_queue
|
|
353
|
-
SET status = 'pending',
|
|
354
|
-
updated_at = NOW(),
|
|
355
|
-
locked_at = NULL,
|
|
356
|
-
locked_by = NULL,
|
|
357
|
-
next_attempt_at = NOW(),
|
|
358
|
-
last_retried_at = NOW()
|
|
359
|
-
WHERE id = $1
|
|
360
|
-
`,
|
|
361
|
-
[jobId],
|
|
362
|
-
);
|
|
363
|
-
await recordJobEvent(pool, jobId, JobEventType.Retried);
|
|
364
|
-
} catch (error) {
|
|
365
|
-
log(`Error retrying job ${jobId}: ${error}`);
|
|
366
|
-
throw error;
|
|
367
|
-
} finally {
|
|
368
|
-
log(`Retried job ${jobId}`);
|
|
369
|
-
client.release();
|
|
370
|
-
}
|
|
371
|
-
};
|
|
97
|
+
export const retryJob = async (pool: Pool, jobId: number): Promise<void> =>
|
|
98
|
+
new PostgresBackend(pool).retryJob(jobId);
|
|
372
99
|
|
|
373
|
-
/**
|
|
374
|
-
* Delete old completed jobs
|
|
375
|
-
*/
|
|
376
100
|
export const cleanupOldJobs = async (
|
|
377
101
|
pool: Pool,
|
|
378
102
|
daysToKeep = 30,
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const result = await client.query(`
|
|
383
|
-
DELETE FROM job_queue
|
|
384
|
-
WHERE status = 'completed'
|
|
385
|
-
AND updated_at < NOW() - INTERVAL '${daysToKeep} days'
|
|
386
|
-
RETURNING id
|
|
387
|
-
`);
|
|
388
|
-
log(`Deleted ${result.rowCount} old jobs`);
|
|
389
|
-
return result.rowCount || 0;
|
|
390
|
-
} catch (error) {
|
|
391
|
-
log(`Error cleaning up old jobs: ${error}`);
|
|
392
|
-
throw error;
|
|
393
|
-
} finally {
|
|
394
|
-
client.release();
|
|
395
|
-
}
|
|
396
|
-
};
|
|
103
|
+
batchSize = 1000,
|
|
104
|
+
): Promise<number> =>
|
|
105
|
+
new PostgresBackend(pool).cleanupOldJobs(daysToKeep, batchSize);
|
|
397
106
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
*/
|
|
401
|
-
export const cancelJob = async (pool: Pool, jobId: number): Promise<void> => {
|
|
402
|
-
const client = await pool.connect();
|
|
403
|
-
try {
|
|
404
|
-
await client.query(
|
|
405
|
-
`
|
|
406
|
-
UPDATE job_queue
|
|
407
|
-
SET status = 'cancelled', updated_at = NOW(), last_cancelled_at = NOW()
|
|
408
|
-
WHERE id = $1 AND status = 'pending'
|
|
409
|
-
`,
|
|
410
|
-
[jobId],
|
|
411
|
-
);
|
|
412
|
-
await recordJobEvent(pool, jobId, JobEventType.Cancelled);
|
|
413
|
-
} catch (error) {
|
|
414
|
-
log(`Error cancelling job ${jobId}: ${error}`);
|
|
415
|
-
throw error;
|
|
416
|
-
} finally {
|
|
417
|
-
log(`Cancelled job ${jobId}`);
|
|
418
|
-
client.release();
|
|
419
|
-
}
|
|
420
|
-
};
|
|
107
|
+
export const cancelJob = async (pool: Pool, jobId: number): Promise<void> =>
|
|
108
|
+
new PostgresBackend(pool).cancelJob(jobId);
|
|
421
109
|
|
|
422
|
-
/**
|
|
423
|
-
* Edit a pending job (only if still pending)
|
|
424
|
-
*/
|
|
425
110
|
export const editJob = async <PayloadMap, T extends keyof PayloadMap & string>(
|
|
426
111
|
pool: Pool,
|
|
427
112
|
jobId: number,
|
|
@@ -432,88 +117,12 @@ export const editJob = async <PayloadMap, T extends keyof PayloadMap & string>(
|
|
|
432
117
|
runAt?: Date | null;
|
|
433
118
|
timeoutMs?: number | null;
|
|
434
119
|
tags?: string[] | null;
|
|
120
|
+
retryDelay?: number | null;
|
|
121
|
+
retryBackoff?: boolean | null;
|
|
122
|
+
retryDelayMax?: number | null;
|
|
435
123
|
},
|
|
436
|
-
): Promise<void> =>
|
|
437
|
-
const client = await pool.connect();
|
|
438
|
-
try {
|
|
439
|
-
const updateFields: string[] = [];
|
|
440
|
-
const params: any[] = [];
|
|
441
|
-
let paramIdx = 1;
|
|
442
|
-
|
|
443
|
-
// Build dynamic UPDATE query based on provided fields
|
|
444
|
-
if (updates.payload !== undefined) {
|
|
445
|
-
updateFields.push(`payload = $${paramIdx++}`);
|
|
446
|
-
params.push(updates.payload);
|
|
447
|
-
}
|
|
448
|
-
if (updates.maxAttempts !== undefined) {
|
|
449
|
-
updateFields.push(`max_attempts = $${paramIdx++}`);
|
|
450
|
-
params.push(updates.maxAttempts);
|
|
451
|
-
}
|
|
452
|
-
if (updates.priority !== undefined) {
|
|
453
|
-
updateFields.push(`priority = $${paramIdx++}`);
|
|
454
|
-
params.push(updates.priority);
|
|
455
|
-
}
|
|
456
|
-
if (updates.runAt !== undefined) {
|
|
457
|
-
if (updates.runAt === null) {
|
|
458
|
-
// null means run now (use current timestamp)
|
|
459
|
-
updateFields.push(`run_at = NOW()`);
|
|
460
|
-
} else {
|
|
461
|
-
updateFields.push(`run_at = $${paramIdx++}`);
|
|
462
|
-
params.push(updates.runAt);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
if (updates.timeoutMs !== undefined) {
|
|
466
|
-
updateFields.push(`timeout_ms = $${paramIdx++}`);
|
|
467
|
-
params.push(updates.timeoutMs ?? null);
|
|
468
|
-
}
|
|
469
|
-
if (updates.tags !== undefined) {
|
|
470
|
-
updateFields.push(`tags = $${paramIdx++}`);
|
|
471
|
-
params.push(updates.tags ?? null);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// If no fields to update, return early
|
|
475
|
-
if (updateFields.length === 0) {
|
|
476
|
-
log(`No fields to update for job ${jobId}`);
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Always update updated_at timestamp
|
|
481
|
-
updateFields.push(`updated_at = NOW()`);
|
|
482
|
-
|
|
483
|
-
// Add jobId as the last parameter for WHERE clause
|
|
484
|
-
params.push(jobId);
|
|
485
|
-
|
|
486
|
-
const query = `
|
|
487
|
-
UPDATE job_queue
|
|
488
|
-
SET ${updateFields.join(', ')}
|
|
489
|
-
WHERE id = $${paramIdx} AND status = 'pending'
|
|
490
|
-
`;
|
|
124
|
+
): Promise<void> => new PostgresBackend(pool).editJob(jobId, updates);
|
|
491
125
|
|
|
492
|
-
await client.query(query, params);
|
|
493
|
-
|
|
494
|
-
// Record edit event with metadata containing updated fields
|
|
495
|
-
const metadata: any = {};
|
|
496
|
-
if (updates.payload !== undefined) metadata.payload = updates.payload;
|
|
497
|
-
if (updates.maxAttempts !== undefined)
|
|
498
|
-
metadata.maxAttempts = updates.maxAttempts;
|
|
499
|
-
if (updates.priority !== undefined) metadata.priority = updates.priority;
|
|
500
|
-
if (updates.runAt !== undefined) metadata.runAt = updates.runAt;
|
|
501
|
-
if (updates.timeoutMs !== undefined) metadata.timeoutMs = updates.timeoutMs;
|
|
502
|
-
if (updates.tags !== undefined) metadata.tags = updates.tags;
|
|
503
|
-
|
|
504
|
-
await recordJobEvent(pool, jobId, JobEventType.Edited, metadata);
|
|
505
|
-
log(`Edited job ${jobId}: ${JSON.stringify(metadata)}`);
|
|
506
|
-
} catch (error) {
|
|
507
|
-
log(`Error editing job ${jobId}: ${error}`);
|
|
508
|
-
throw error;
|
|
509
|
-
} finally {
|
|
510
|
-
client.release();
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Edit all pending jobs matching the filters
|
|
516
|
-
*/
|
|
517
126
|
export const editAllPendingJobs = async <
|
|
518
127
|
PayloadMap,
|
|
519
128
|
T extends keyof PayloadMap & string,
|
|
@@ -528,7 +137,7 @@ export const editAllPendingJobs = async <
|
|
|
528
137
|
| { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
|
|
529
138
|
tags?: { values: string[]; mode?: TagQueryMode };
|
|
530
139
|
}
|
|
531
|
-
| undefined
|
|
140
|
+
| undefined,
|
|
532
141
|
updates: {
|
|
533
142
|
payload?: PayloadMap[T];
|
|
534
143
|
maxAttempts?: number;
|
|
@@ -536,160 +145,13 @@ export const editAllPendingJobs = async <
|
|
|
536
145
|
runAt?: Date | null;
|
|
537
146
|
timeoutMs?: number;
|
|
538
147
|
tags?: string[];
|
|
148
|
+
retryDelay?: number | null;
|
|
149
|
+
retryBackoff?: boolean | null;
|
|
150
|
+
retryDelayMax?: number | null;
|
|
539
151
|
},
|
|
540
|
-
): Promise<number> =>
|
|
541
|
-
|
|
542
|
-
try {
|
|
543
|
-
// Build SET clause from updates
|
|
544
|
-
const updateFields: string[] = [];
|
|
545
|
-
const params: any[] = [];
|
|
546
|
-
let paramIdx = 1;
|
|
547
|
-
|
|
548
|
-
if (updates.payload !== undefined) {
|
|
549
|
-
updateFields.push(`payload = $${paramIdx++}`);
|
|
550
|
-
params.push(updates.payload);
|
|
551
|
-
}
|
|
552
|
-
if (updates.maxAttempts !== undefined) {
|
|
553
|
-
updateFields.push(`max_attempts = $${paramIdx++}`);
|
|
554
|
-
params.push(updates.maxAttempts);
|
|
555
|
-
}
|
|
556
|
-
if (updates.priority !== undefined) {
|
|
557
|
-
updateFields.push(`priority = $${paramIdx++}`);
|
|
558
|
-
params.push(updates.priority);
|
|
559
|
-
}
|
|
560
|
-
if (updates.runAt !== undefined) {
|
|
561
|
-
if (updates.runAt === null) {
|
|
562
|
-
// null means run now (use current timestamp)
|
|
563
|
-
updateFields.push(`run_at = NOW()`);
|
|
564
|
-
} else {
|
|
565
|
-
updateFields.push(`run_at = $${paramIdx++}`);
|
|
566
|
-
params.push(updates.runAt);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
if (updates.timeoutMs !== undefined) {
|
|
570
|
-
updateFields.push(`timeout_ms = $${paramIdx++}`);
|
|
571
|
-
params.push(updates.timeoutMs ?? null);
|
|
572
|
-
}
|
|
573
|
-
if (updates.tags !== undefined) {
|
|
574
|
-
updateFields.push(`tags = $${paramIdx++}`);
|
|
575
|
-
params.push(updates.tags ?? null);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// If no fields to update, return early
|
|
579
|
-
if (updateFields.length === 0) {
|
|
580
|
-
log(`No fields to update for batch edit`);
|
|
581
|
-
return 0;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Always update updated_at timestamp
|
|
585
|
-
updateFields.push(`updated_at = NOW()`);
|
|
586
|
-
|
|
587
|
-
// Build WHERE clause from filters
|
|
588
|
-
let query = `
|
|
589
|
-
UPDATE job_queue
|
|
590
|
-
SET ${updateFields.join(', ')}
|
|
591
|
-
WHERE status = 'pending'`;
|
|
592
|
-
|
|
593
|
-
if (filters) {
|
|
594
|
-
if (filters.jobType) {
|
|
595
|
-
query += ` AND job_type = $${paramIdx++}`;
|
|
596
|
-
params.push(filters.jobType);
|
|
597
|
-
}
|
|
598
|
-
if (filters.priority !== undefined) {
|
|
599
|
-
query += ` AND priority = $${paramIdx++}`;
|
|
600
|
-
params.push(filters.priority);
|
|
601
|
-
}
|
|
602
|
-
if (filters.runAt) {
|
|
603
|
-
if (filters.runAt instanceof Date) {
|
|
604
|
-
query += ` AND run_at = $${paramIdx++}`;
|
|
605
|
-
params.push(filters.runAt);
|
|
606
|
-
} else if (typeof filters.runAt === 'object') {
|
|
607
|
-
const ops = filters.runAt;
|
|
608
|
-
if (ops.gt) {
|
|
609
|
-
query += ` AND run_at > $${paramIdx++}`;
|
|
610
|
-
params.push(ops.gt);
|
|
611
|
-
}
|
|
612
|
-
if (ops.gte) {
|
|
613
|
-
query += ` AND run_at >= $${paramIdx++}`;
|
|
614
|
-
params.push(ops.gte);
|
|
615
|
-
}
|
|
616
|
-
if (ops.lt) {
|
|
617
|
-
query += ` AND run_at < $${paramIdx++}`;
|
|
618
|
-
params.push(ops.lt);
|
|
619
|
-
}
|
|
620
|
-
if (ops.lte) {
|
|
621
|
-
query += ` AND run_at <= $${paramIdx++}`;
|
|
622
|
-
params.push(ops.lte);
|
|
623
|
-
}
|
|
624
|
-
if (ops.eq) {
|
|
625
|
-
query += ` AND run_at = $${paramIdx++}`;
|
|
626
|
-
params.push(ops.eq);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
if (
|
|
631
|
-
filters.tags &&
|
|
632
|
-
filters.tags.values &&
|
|
633
|
-
filters.tags.values.length > 0
|
|
634
|
-
) {
|
|
635
|
-
const mode = filters.tags.mode || 'all';
|
|
636
|
-
const tagValues = filters.tags.values;
|
|
637
|
-
switch (mode) {
|
|
638
|
-
case 'exact':
|
|
639
|
-
query += ` AND tags = $${paramIdx++}`;
|
|
640
|
-
params.push(tagValues);
|
|
641
|
-
break;
|
|
642
|
-
case 'all':
|
|
643
|
-
query += ` AND tags @> $${paramIdx++}`;
|
|
644
|
-
params.push(tagValues);
|
|
645
|
-
break;
|
|
646
|
-
case 'any':
|
|
647
|
-
query += ` AND tags && $${paramIdx++}`;
|
|
648
|
-
params.push(tagValues);
|
|
649
|
-
break;
|
|
650
|
-
case 'none':
|
|
651
|
-
query += ` AND NOT (tags && $${paramIdx++})`;
|
|
652
|
-
params.push(tagValues);
|
|
653
|
-
break;
|
|
654
|
-
default:
|
|
655
|
-
query += ` AND tags @> $${paramIdx++}`;
|
|
656
|
-
params.push(tagValues);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
query += '\nRETURNING id';
|
|
661
|
-
|
|
662
|
-
const result = await client.query(query, params);
|
|
663
|
-
const editedCount = result.rowCount || 0;
|
|
664
|
-
|
|
665
|
-
// Record edit event with metadata containing updated fields for each job
|
|
666
|
-
const metadata: any = {};
|
|
667
|
-
if (updates.payload !== undefined) metadata.payload = updates.payload;
|
|
668
|
-
if (updates.maxAttempts !== undefined)
|
|
669
|
-
metadata.maxAttempts = updates.maxAttempts;
|
|
670
|
-
if (updates.priority !== undefined) metadata.priority = updates.priority;
|
|
671
|
-
if (updates.runAt !== undefined) metadata.runAt = updates.runAt;
|
|
672
|
-
if (updates.timeoutMs !== undefined) metadata.timeoutMs = updates.timeoutMs;
|
|
673
|
-
if (updates.tags !== undefined) metadata.tags = updates.tags;
|
|
152
|
+
): Promise<number> =>
|
|
153
|
+
new PostgresBackend(pool).editAllPendingJobs(filters, updates);
|
|
674
154
|
|
|
675
|
-
// Record events for each affected job
|
|
676
|
-
for (const row of result.rows) {
|
|
677
|
-
await recordJobEvent(pool, row.id, JobEventType.Edited, metadata);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
log(`Edited ${editedCount} pending jobs: ${JSON.stringify(metadata)}`);
|
|
681
|
-
return editedCount;
|
|
682
|
-
} catch (error) {
|
|
683
|
-
log(`Error editing pending jobs: ${error}`);
|
|
684
|
-
throw error;
|
|
685
|
-
} finally {
|
|
686
|
-
client.release();
|
|
687
|
-
}
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Cancel all upcoming jobs (pending and scheduled in the future) with optional filters
|
|
692
|
-
*/
|
|
693
155
|
export const cancelAllUpcomingJobs = async (
|
|
694
156
|
pool: Pool,
|
|
695
157
|
filters?: {
|
|
@@ -698,97 +160,8 @@ export const cancelAllUpcomingJobs = async (
|
|
|
698
160
|
runAt?: Date | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
|
|
699
161
|
tags?: { values: string[]; mode?: TagQueryMode };
|
|
700
162
|
},
|
|
701
|
-
): Promise<number> =>
|
|
702
|
-
const client = await pool.connect();
|
|
703
|
-
try {
|
|
704
|
-
let query = `
|
|
705
|
-
UPDATE job_queue
|
|
706
|
-
SET status = 'cancelled', updated_at = NOW()
|
|
707
|
-
WHERE status = 'pending'`;
|
|
708
|
-
const params: any[] = [];
|
|
709
|
-
let paramIdx = 1;
|
|
710
|
-
if (filters) {
|
|
711
|
-
if (filters.jobType) {
|
|
712
|
-
query += ` AND job_type = $${paramIdx++}`;
|
|
713
|
-
params.push(filters.jobType);
|
|
714
|
-
}
|
|
715
|
-
if (filters.priority !== undefined) {
|
|
716
|
-
query += ` AND priority = $${paramIdx++}`;
|
|
717
|
-
params.push(filters.priority);
|
|
718
|
-
}
|
|
719
|
-
if (filters.runAt) {
|
|
720
|
-
if (filters.runAt instanceof Date) {
|
|
721
|
-
query += ` AND run_at = $${paramIdx++}`;
|
|
722
|
-
params.push(filters.runAt);
|
|
723
|
-
} else if (typeof filters.runAt === 'object') {
|
|
724
|
-
const ops = filters.runAt;
|
|
725
|
-
if (ops.gt) {
|
|
726
|
-
query += ` AND run_at > $${paramIdx++}`;
|
|
727
|
-
params.push(ops.gt);
|
|
728
|
-
}
|
|
729
|
-
if (ops.gte) {
|
|
730
|
-
query += ` AND run_at >= $${paramIdx++}`;
|
|
731
|
-
params.push(ops.gte);
|
|
732
|
-
}
|
|
733
|
-
if (ops.lt) {
|
|
734
|
-
query += ` AND run_at < $${paramIdx++}`;
|
|
735
|
-
params.push(ops.lt);
|
|
736
|
-
}
|
|
737
|
-
if (ops.lte) {
|
|
738
|
-
query += ` AND run_at <= $${paramIdx++}`;
|
|
739
|
-
params.push(ops.lte);
|
|
740
|
-
}
|
|
741
|
-
if (ops.eq) {
|
|
742
|
-
query += ` AND run_at = $${paramIdx++}`;
|
|
743
|
-
params.push(ops.eq);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
if (
|
|
748
|
-
filters.tags &&
|
|
749
|
-
filters.tags.values &&
|
|
750
|
-
filters.tags.values.length > 0
|
|
751
|
-
) {
|
|
752
|
-
const mode = filters.tags.mode || 'all';
|
|
753
|
-
const tagValues = filters.tags.values;
|
|
754
|
-
switch (mode) {
|
|
755
|
-
case 'exact':
|
|
756
|
-
query += ` AND tags = $${paramIdx++}`;
|
|
757
|
-
params.push(tagValues);
|
|
758
|
-
break;
|
|
759
|
-
case 'all':
|
|
760
|
-
query += ` AND tags @> $${paramIdx++}`;
|
|
761
|
-
params.push(tagValues);
|
|
762
|
-
break;
|
|
763
|
-
case 'any':
|
|
764
|
-
query += ` AND tags && $${paramIdx++}`;
|
|
765
|
-
params.push(tagValues);
|
|
766
|
-
break;
|
|
767
|
-
case 'none':
|
|
768
|
-
query += ` AND NOT (tags && $${paramIdx++})`;
|
|
769
|
-
params.push(tagValues);
|
|
770
|
-
break;
|
|
771
|
-
default:
|
|
772
|
-
query += ` AND tags @> $${paramIdx++}`;
|
|
773
|
-
params.push(tagValues);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
query += '\nRETURNING id';
|
|
778
|
-
const result = await client.query(query, params);
|
|
779
|
-
log(`Cancelled ${result.rowCount} jobs`);
|
|
780
|
-
return result.rowCount || 0;
|
|
781
|
-
} catch (error) {
|
|
782
|
-
log(`Error cancelling upcoming jobs: ${error}`);
|
|
783
|
-
throw error;
|
|
784
|
-
} finally {
|
|
785
|
-
client.release();
|
|
786
|
-
}
|
|
787
|
-
};
|
|
163
|
+
): Promise<number> => new PostgresBackend(pool).cancelAllUpcomingJobs(filters);
|
|
788
164
|
|
|
789
|
-
/**
|
|
790
|
-
* Get all jobs with optional pagination
|
|
791
|
-
*/
|
|
792
165
|
export const getAllJobs = async <
|
|
793
166
|
PayloadMap,
|
|
794
167
|
T extends keyof PayloadMap & string,
|
|
@@ -796,113 +169,27 @@ export const getAllJobs = async <
|
|
|
796
169
|
pool: Pool,
|
|
797
170
|
limit = 100,
|
|
798
171
|
offset = 0,
|
|
799
|
-
): Promise<JobRecord<PayloadMap, T>[]> =>
|
|
800
|
-
|
|
801
|
-
try {
|
|
802
|
-
const result = await client.query(
|
|
803
|
-
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason" FROM job_queue ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
|
804
|
-
[limit, offset],
|
|
805
|
-
);
|
|
806
|
-
log(`Found ${result.rows.length} jobs (all)`);
|
|
807
|
-
return result.rows.map((job) => ({
|
|
808
|
-
...job,
|
|
809
|
-
payload: job.payload,
|
|
810
|
-
timeoutMs: job.timeoutMs,
|
|
811
|
-
forceKillOnTimeout: job.forceKillOnTimeout,
|
|
812
|
-
}));
|
|
813
|
-
} catch (error) {
|
|
814
|
-
log(`Error getting all jobs: ${error}`);
|
|
815
|
-
throw error;
|
|
816
|
-
} finally {
|
|
817
|
-
client.release();
|
|
818
|
-
}
|
|
819
|
-
};
|
|
172
|
+
): Promise<JobRecord<PayloadMap, T>[]> =>
|
|
173
|
+
new PostgresBackend(pool).getAllJobs<PayloadMap, T>(limit, offset);
|
|
820
174
|
|
|
821
|
-
/**
|
|
822
|
-
* Set a pending reason for unpicked jobs
|
|
823
|
-
*/
|
|
824
175
|
export const setPendingReasonForUnpickedJobs = async (
|
|
825
176
|
pool: Pool,
|
|
826
177
|
reason: string,
|
|
827
178
|
jobType?: string | string[],
|
|
828
|
-
) =>
|
|
829
|
-
|
|
830
|
-
try {
|
|
831
|
-
let jobTypeFilter = '';
|
|
832
|
-
let params: any[] = [reason];
|
|
833
|
-
if (jobType) {
|
|
834
|
-
if (Array.isArray(jobType)) {
|
|
835
|
-
jobTypeFilter = ` AND job_type = ANY($2)`;
|
|
836
|
-
params.push(jobType);
|
|
837
|
-
} else {
|
|
838
|
-
jobTypeFilter = ` AND job_type = $2`;
|
|
839
|
-
params.push(jobType);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
await client.query(
|
|
843
|
-
`UPDATE job_queue SET pending_reason = $1 WHERE status = 'pending'${jobTypeFilter}`,
|
|
844
|
-
params,
|
|
845
|
-
);
|
|
846
|
-
} finally {
|
|
847
|
-
client.release();
|
|
848
|
-
}
|
|
849
|
-
};
|
|
179
|
+
): Promise<void> =>
|
|
180
|
+
new PostgresBackend(pool).setPendingReasonForUnpickedJobs(reason, jobType);
|
|
850
181
|
|
|
851
|
-
/**
|
|
852
|
-
* Reclaim jobs stuck in 'processing' for too long.
|
|
853
|
-
*
|
|
854
|
-
* If a process (e.g., API route or worker) crashes after marking a job as 'processing' but before completing it, the job can remain stuck in the 'processing' state indefinitely. This can happen if the process is killed or encounters an unhandled error after updating the job status but before marking it as 'completed' or 'failed'.
|
|
855
|
-
* @param pool - The database pool
|
|
856
|
-
* @param maxProcessingTimeMinutes - Max allowed processing time in minutes (default: 10)
|
|
857
|
-
* @returns Number of jobs reclaimed
|
|
858
|
-
*/
|
|
859
182
|
export const reclaimStuckJobs = async (
|
|
860
183
|
pool: Pool,
|
|
861
184
|
maxProcessingTimeMinutes = 10,
|
|
862
|
-
): Promise<number> =>
|
|
863
|
-
|
|
864
|
-
try {
|
|
865
|
-
const result = await client.query(
|
|
866
|
-
`
|
|
867
|
-
UPDATE job_queue
|
|
868
|
-
SET status = 'pending', locked_at = NULL, locked_by = NULL, updated_at = NOW()
|
|
869
|
-
WHERE status = 'processing'
|
|
870
|
-
AND locked_at < NOW() - INTERVAL '${maxProcessingTimeMinutes} minutes'
|
|
871
|
-
RETURNING id
|
|
872
|
-
`,
|
|
873
|
-
);
|
|
874
|
-
log(`Reclaimed ${result.rowCount} stuck jobs`);
|
|
875
|
-
return result.rowCount || 0;
|
|
876
|
-
} catch (error) {
|
|
877
|
-
log(`Error reclaiming stuck jobs: ${error}`);
|
|
878
|
-
throw error;
|
|
879
|
-
} finally {
|
|
880
|
-
client.release();
|
|
881
|
-
}
|
|
882
|
-
};
|
|
185
|
+
): Promise<number> =>
|
|
186
|
+
new PostgresBackend(pool).reclaimStuckJobs(maxProcessingTimeMinutes);
|
|
883
187
|
|
|
884
|
-
/**
|
|
885
|
-
* Get all events for a job, ordered by createdAt ascending
|
|
886
|
-
*/
|
|
887
188
|
export const getJobEvents = async (
|
|
888
189
|
pool: Pool,
|
|
889
190
|
jobId: number,
|
|
890
|
-
): Promise<JobEvent[]> =>
|
|
891
|
-
const client = await pool.connect();
|
|
892
|
-
try {
|
|
893
|
-
const res = await client.query(
|
|
894
|
-
`SELECT id, job_id AS "jobId", event_type AS "eventType", metadata, created_at AS "createdAt" FROM job_events WHERE job_id = $1 ORDER BY created_at ASC`,
|
|
895
|
-
[jobId],
|
|
896
|
-
);
|
|
897
|
-
return res.rows as JobEvent[];
|
|
898
|
-
} finally {
|
|
899
|
-
client.release();
|
|
900
|
-
}
|
|
901
|
-
};
|
|
191
|
+
): Promise<JobEvent[]> => new PostgresBackend(pool).getJobEvents(jobId);
|
|
902
192
|
|
|
903
|
-
/**
|
|
904
|
-
* Get jobs by tags (matches all specified tags)
|
|
905
|
-
*/
|
|
906
193
|
export const getJobsByTags = async <
|
|
907
194
|
PayloadMap,
|
|
908
195
|
T extends keyof PayloadMap & string,
|
|
@@ -912,55 +199,13 @@ export const getJobsByTags = async <
|
|
|
912
199
|
mode: TagQueryMode = 'all',
|
|
913
200
|
limit = 100,
|
|
914
201
|
offset = 0,
|
|
915
|
-
): Promise<JobRecord<PayloadMap, T>[]> =>
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
case 'exact':
|
|
923
|
-
query += ' WHERE tags = $1';
|
|
924
|
-
params = [tags];
|
|
925
|
-
break;
|
|
926
|
-
case 'all':
|
|
927
|
-
query += ' WHERE tags @> $1';
|
|
928
|
-
params = [tags];
|
|
929
|
-
break;
|
|
930
|
-
case 'any':
|
|
931
|
-
query += ' WHERE tags && $1';
|
|
932
|
-
params = [tags];
|
|
933
|
-
break;
|
|
934
|
-
case 'none':
|
|
935
|
-
query += ' WHERE NOT (tags && $1)';
|
|
936
|
-
params = [tags];
|
|
937
|
-
break;
|
|
938
|
-
default:
|
|
939
|
-
query += ' WHERE tags @> $1';
|
|
940
|
-
params = [tags];
|
|
941
|
-
}
|
|
942
|
-
query += ' ORDER BY created_at DESC LIMIT $2 OFFSET $3';
|
|
943
|
-
params.push(limit, offset);
|
|
944
|
-
const result = await client.query(query, params);
|
|
945
|
-
log(
|
|
946
|
-
`Found ${result.rows.length} jobs by tags ${JSON.stringify(tags)} (mode: ${mode})`,
|
|
947
|
-
);
|
|
948
|
-
return result.rows.map((job) => ({
|
|
949
|
-
...job,
|
|
950
|
-
payload: job.payload,
|
|
951
|
-
timeoutMs: job.timeoutMs,
|
|
952
|
-
forceKillOnTimeout: job.forceKillOnTimeout,
|
|
953
|
-
failureReason: job.failureReason,
|
|
954
|
-
}));
|
|
955
|
-
} catch (error) {
|
|
956
|
-
log(
|
|
957
|
-
`Error getting jobs by tags ${JSON.stringify(tags)} (mode: ${mode}): ${error}`,
|
|
958
|
-
);
|
|
959
|
-
throw error;
|
|
960
|
-
} finally {
|
|
961
|
-
client.release();
|
|
962
|
-
}
|
|
963
|
-
};
|
|
202
|
+
): Promise<JobRecord<PayloadMap, T>[]> =>
|
|
203
|
+
new PostgresBackend(pool).getJobsByTags<PayloadMap, T>(
|
|
204
|
+
tags,
|
|
205
|
+
mode,
|
|
206
|
+
limit,
|
|
207
|
+
offset,
|
|
208
|
+
);
|
|
964
209
|
|
|
965
210
|
export const getJobs = async <PayloadMap, T extends keyof PayloadMap & string>(
|
|
966
211
|
pool: Pool,
|
|
@@ -972,113 +217,59 @@ export const getJobs = async <PayloadMap, T extends keyof PayloadMap & string>(
|
|
|
972
217
|
},
|
|
973
218
|
limit = 100,
|
|
974
219
|
offset = 0,
|
|
975
|
-
): Promise<JobRecord<PayloadMap, T>[]> =>
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
}
|
|
1032
|
-
if (
|
|
1033
|
-
filters.tags &&
|
|
1034
|
-
filters.tags.values &&
|
|
1035
|
-
filters.tags.values.length > 0
|
|
1036
|
-
) {
|
|
1037
|
-
const mode = filters.tags.mode || 'all';
|
|
1038
|
-
const tagValues = filters.tags.values;
|
|
1039
|
-
switch (mode) {
|
|
1040
|
-
case 'exact':
|
|
1041
|
-
where.push(`tags = $${paramIdx++}`);
|
|
1042
|
-
params.push(tagValues);
|
|
1043
|
-
break;
|
|
1044
|
-
case 'all':
|
|
1045
|
-
where.push(`tags @> $${paramIdx++}`);
|
|
1046
|
-
params.push(tagValues);
|
|
1047
|
-
break;
|
|
1048
|
-
case 'any':
|
|
1049
|
-
where.push(`tags && $${paramIdx++}`);
|
|
1050
|
-
params.push(tagValues);
|
|
1051
|
-
break;
|
|
1052
|
-
case 'none':
|
|
1053
|
-
where.push(`NOT (tags && $${paramIdx++})`);
|
|
1054
|
-
params.push(tagValues);
|
|
1055
|
-
break;
|
|
1056
|
-
default:
|
|
1057
|
-
where.push(`tags @> $${paramIdx++}`);
|
|
1058
|
-
params.push(tagValues);
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
if (where.length > 0) {
|
|
1063
|
-
query += ` WHERE ${where.join(' AND ')}`;
|
|
1064
|
-
}
|
|
1065
|
-
// Always add LIMIT and OFFSET as the last parameters
|
|
1066
|
-
paramIdx = params.length + 1;
|
|
1067
|
-
query += ` ORDER BY created_at DESC LIMIT $${paramIdx++} OFFSET $${paramIdx}`;
|
|
1068
|
-
params.push(limit, offset);
|
|
1069
|
-
const result = await client.query(query, params);
|
|
1070
|
-
log(`Found ${result.rows.length} jobs`);
|
|
1071
|
-
return result.rows.map((job) => ({
|
|
1072
|
-
...job,
|
|
1073
|
-
payload: job.payload,
|
|
1074
|
-
timeoutMs: job.timeoutMs,
|
|
1075
|
-
forceKillOnTimeout: job.forceKillOnTimeout,
|
|
1076
|
-
failureReason: job.failureReason,
|
|
1077
|
-
}));
|
|
1078
|
-
} catch (error) {
|
|
1079
|
-
log(`Error getting jobs: ${error}`);
|
|
1080
|
-
throw error;
|
|
1081
|
-
} finally {
|
|
1082
|
-
client.release();
|
|
1083
|
-
}
|
|
1084
|
-
};
|
|
220
|
+
): Promise<JobRecord<PayloadMap, T>[]> =>
|
|
221
|
+
new PostgresBackend(pool).getJobs<PayloadMap, T>(filters, limit, offset);
|
|
222
|
+
|
|
223
|
+
// ── Progress ──────────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
export const updateProgress = async (
|
|
226
|
+
pool: Pool,
|
|
227
|
+
jobId: number,
|
|
228
|
+
progress: number,
|
|
229
|
+
): Promise<void> => new PostgresBackend(pool).updateProgress(jobId, progress);
|
|
230
|
+
|
|
231
|
+
// ── Wait support functions (backward-compatible delegates) ────────────────────
|
|
232
|
+
|
|
233
|
+
/** @deprecated Use backend.waitJob() directly. Delegates to PostgresBackend. */
|
|
234
|
+
export const waitJob = async (
|
|
235
|
+
pool: Pool,
|
|
236
|
+
jobId: number,
|
|
237
|
+
options: {
|
|
238
|
+
waitUntil?: Date;
|
|
239
|
+
waitTokenId?: string;
|
|
240
|
+
stepData: Record<string, any>;
|
|
241
|
+
},
|
|
242
|
+
): Promise<void> => new PostgresBackend(pool).waitJob(jobId, options);
|
|
243
|
+
|
|
244
|
+
/** @deprecated Use backend.updateStepData() directly. Delegates to PostgresBackend. */
|
|
245
|
+
export const updateStepData = async (
|
|
246
|
+
pool: Pool,
|
|
247
|
+
jobId: number,
|
|
248
|
+
stepData: Record<string, any>,
|
|
249
|
+
): Promise<void> => new PostgresBackend(pool).updateStepData(jobId, stepData);
|
|
250
|
+
|
|
251
|
+
/** @deprecated Use backend.createWaitpoint() directly. Delegates to PostgresBackend. */
|
|
252
|
+
export const createWaitpoint = async (
|
|
253
|
+
pool: Pool,
|
|
254
|
+
jobId: number | null,
|
|
255
|
+
options?: { timeout?: string; tags?: string[] },
|
|
256
|
+
): Promise<{ id: string }> =>
|
|
257
|
+
new PostgresBackend(pool).createWaitpoint(jobId, options);
|
|
258
|
+
|
|
259
|
+
/** @deprecated Use backend.completeWaitpoint() directly. Delegates to PostgresBackend. */
|
|
260
|
+
export const completeWaitpoint = async (
|
|
261
|
+
pool: Pool,
|
|
262
|
+
tokenId: string,
|
|
263
|
+
data?: any,
|
|
264
|
+
): Promise<void> => new PostgresBackend(pool).completeWaitpoint(tokenId, data);
|
|
265
|
+
|
|
266
|
+
/** @deprecated Use backend.getWaitpoint() directly. Delegates to PostgresBackend. */
|
|
267
|
+
export const getWaitpoint = async (
|
|
268
|
+
pool: Pool,
|
|
269
|
+
tokenId: string,
|
|
270
|
+
): Promise<WaitpointRecord | null> =>
|
|
271
|
+
new PostgresBackend(pool).getWaitpoint(tokenId);
|
|
272
|
+
|
|
273
|
+
/** @deprecated Use backend.expireTimedOutWaitpoints() directly. Delegates to PostgresBackend. */
|
|
274
|
+
export const expireTimedOutWaitpoints = async (pool: Pool): Promise<number> =>
|
|
275
|
+
new PostgresBackend(pool).expireTimedOutWaitpoints();
|