@workglow/job-queue 0.0.57 → 0.0.59
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 +432 -306
- package/dist/browser.js +808 -509
- package/dist/browser.js.map +9 -8
- package/dist/bun.js +823 -823
- package/dist/bun.js.map +9 -10
- package/dist/common-server.d.ts +0 -2
- package/dist/common-server.d.ts.map +1 -1
- package/dist/common.d.ts +4 -3
- package/dist/common.d.ts.map +1 -1
- package/dist/job/Job.d.ts +3 -4
- package/dist/job/Job.d.ts.map +1 -1
- package/dist/job/JobQueueClient.d.ts +171 -0
- package/dist/job/JobQueueClient.d.ts.map +1 -0
- package/dist/job/JobQueueEventListeners.d.ts +1 -1
- package/dist/job/JobQueueEventListeners.d.ts.map +1 -1
- package/dist/job/JobQueueServer.d.ts +160 -0
- package/dist/job/JobQueueServer.d.ts.map +1 -0
- package/dist/job/JobQueueWorker.d.ts +157 -0
- package/dist/job/JobQueueWorker.d.ts.map +1 -0
- package/dist/limiter/RateLimiter.d.ts +49 -0
- package/dist/limiter/RateLimiter.d.ts.map +1 -0
- package/dist/node.js +823 -823
- package/dist/node.js.map +9 -10
- package/package.json +5 -13
- package/dist/job/IJobQueue.d.ts +0 -160
- package/dist/job/IJobQueue.d.ts.map +0 -1
- package/dist/job/JobQueue.d.ts +0 -272
- package/dist/job/JobQueue.d.ts.map +0 -1
- package/dist/limiter/InMemoryRateLimiter.d.ts +0 -32
- package/dist/limiter/InMemoryRateLimiter.d.ts.map +0 -1
- package/dist/limiter/PostgresRateLimiter.d.ts +0 -53
- package/dist/limiter/PostgresRateLimiter.d.ts.map +0 -1
- package/dist/limiter/SqliteRateLimiter.d.ts +0 -44
- package/dist/limiter/SqliteRateLimiter.d.ts.map +0 -1
package/dist/bun.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/job/
|
|
2
|
+
// src/job/Job.ts
|
|
3
3
|
import { JobStatus } from "@workglow/storage";
|
|
4
|
-
|
|
5
|
-
((QueueMode2) => {
|
|
6
|
-
QueueMode2["CLIENT"] = "CLIENT";
|
|
7
|
-
QueueMode2["SERVER"] = "SERVER";
|
|
8
|
-
QueueMode2["BOTH"] = "BOTH";
|
|
9
|
-
})(QueueMode ||= {});
|
|
4
|
+
|
|
10
5
|
// src/job/JobError.ts
|
|
11
6
|
import { BaseError } from "@workglow/util";
|
|
12
7
|
|
|
@@ -76,7 +71,6 @@ class Job {
|
|
|
76
71
|
progress = 0;
|
|
77
72
|
progressMessage = "";
|
|
78
73
|
progressDetails = null;
|
|
79
|
-
queue;
|
|
80
74
|
constructor({
|
|
81
75
|
queueName,
|
|
82
76
|
input,
|
|
@@ -129,7 +123,6 @@ class Job {
|
|
|
129
123
|
for (const listener of this.progressListeners) {
|
|
130
124
|
listener(progress, message, details);
|
|
131
125
|
}
|
|
132
|
-
await this.queue?.updateProgress(this.id, progress, message, details);
|
|
133
126
|
}
|
|
134
127
|
onJobProgress(listener) {
|
|
135
128
|
this.progressListeners.add(listener);
|
|
@@ -138,78 +131,81 @@ class Job {
|
|
|
138
131
|
};
|
|
139
132
|
}
|
|
140
133
|
}
|
|
141
|
-
// src/job/
|
|
142
|
-
import {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
class NullLimiter {
|
|
157
|
-
async canProceed() {
|
|
158
|
-
return true;
|
|
134
|
+
// src/job/JobQueueClient.ts
|
|
135
|
+
import { JobStatus as JobStatus2 } from "@workglow/storage";
|
|
136
|
+
import { EventEmitter } from "@workglow/util";
|
|
137
|
+
class JobQueueClient {
|
|
138
|
+
queueName;
|
|
139
|
+
storage;
|
|
140
|
+
events = new EventEmitter;
|
|
141
|
+
server = null;
|
|
142
|
+
storageUnsubscribe = null;
|
|
143
|
+
activeJobPromises = new Map;
|
|
144
|
+
jobProgressListeners = new Map;
|
|
145
|
+
lastKnownProgress = new Map;
|
|
146
|
+
constructor(options) {
|
|
147
|
+
this.queueName = options.queueName;
|
|
148
|
+
this.storage = options.storage;
|
|
159
149
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
attach(server) {
|
|
151
|
+
if (this.server) {
|
|
152
|
+
this.detach();
|
|
153
|
+
}
|
|
154
|
+
this.server = server;
|
|
155
|
+
server.addClient(this);
|
|
156
|
+
if (this.storageUnsubscribe) {
|
|
157
|
+
this.storageUnsubscribe();
|
|
158
|
+
this.storageUnsubscribe = null;
|
|
159
|
+
}
|
|
164
160
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// src/job/JobQueue.ts
|
|
170
|
-
class JobQueue {
|
|
171
|
-
queueName;
|
|
172
|
-
jobClass;
|
|
173
|
-
options;
|
|
174
|
-
constructor(queueName, jobClass, options) {
|
|
175
|
-
this.queueName = queueName;
|
|
176
|
-
this.jobClass = jobClass;
|
|
177
|
-
const { limiter, storage, ...rest } = options;
|
|
178
|
-
this.options = {
|
|
179
|
-
waitDurationInMilliseconds: 100,
|
|
180
|
-
...rest
|
|
181
|
-
};
|
|
182
|
-
if (limiter) {
|
|
183
|
-
this.limiter = limiter;
|
|
184
|
-
} else {
|
|
185
|
-
try {
|
|
186
|
-
this.limiter = globalServiceRegistry.get(JOB_LIMITER);
|
|
187
|
-
} catch (err) {
|
|
188
|
-
console.warn("Warning: did not find job limiter in global DI", err);
|
|
189
|
-
this.limiter = new NullLimiter;
|
|
190
|
-
}
|
|
161
|
+
detach() {
|
|
162
|
+
if (this.server) {
|
|
163
|
+
this.server.removeClient(this);
|
|
164
|
+
this.server = null;
|
|
191
165
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this.storage = new InMemoryQueueStorage(queueName);
|
|
200
|
-
}
|
|
166
|
+
}
|
|
167
|
+
connect() {
|
|
168
|
+
if (this.server) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (this.storageUnsubscribe) {
|
|
172
|
+
return;
|
|
201
173
|
}
|
|
202
|
-
this.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
174
|
+
this.storageUnsubscribe = this.storage.subscribeToChanges((change) => {
|
|
175
|
+
this.handleStorageChange(change);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
disconnect() {
|
|
179
|
+
if (this.storageUnsubscribe) {
|
|
180
|
+
this.storageUnsubscribe();
|
|
181
|
+
this.storageUnsubscribe = null;
|
|
182
|
+
}
|
|
183
|
+
this.detach();
|
|
184
|
+
}
|
|
185
|
+
async submit(input, options) {
|
|
186
|
+
const job = {
|
|
187
|
+
queue: this.queueName,
|
|
188
|
+
input,
|
|
189
|
+
job_run_id: options?.jobRunId,
|
|
190
|
+
fingerprint: options?.fingerprint,
|
|
191
|
+
max_retries: options?.maxRetries ?? 10,
|
|
192
|
+
run_after: options?.runAfter?.toISOString() ?? new Date().toISOString(),
|
|
193
|
+
deadline_at: options?.deadlineAt?.toISOString() ?? null,
|
|
194
|
+
completed_at: null,
|
|
195
|
+
status: JobStatus2.PENDING
|
|
210
196
|
};
|
|
197
|
+
const id = await this.storage.add(job);
|
|
198
|
+
return this.createJobHandle(id);
|
|
199
|
+
}
|
|
200
|
+
async submitBatch(inputs, options) {
|
|
201
|
+
const handles = [];
|
|
202
|
+
for (const input of inputs) {
|
|
203
|
+
const handle = await this.submit(input, options);
|
|
204
|
+
handles.push(handle);
|
|
205
|
+
}
|
|
206
|
+
return handles;
|
|
211
207
|
}
|
|
212
|
-
async
|
|
208
|
+
async getJob(id) {
|
|
213
209
|
if (!id)
|
|
214
210
|
throw new JobNotFoundError("Cannot get undefined job");
|
|
215
211
|
const job = await this.storage.get(id);
|
|
@@ -217,15 +213,11 @@ class JobQueue {
|
|
|
217
213
|
return;
|
|
218
214
|
return this.storageToClass(job);
|
|
219
215
|
}
|
|
220
|
-
async
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const job = await this.storage.next();
|
|
226
|
-
if (!job)
|
|
227
|
-
return;
|
|
228
|
-
return this.storageToClass(job);
|
|
216
|
+
async getJobsByRunId(runId) {
|
|
217
|
+
if (!runId)
|
|
218
|
+
throw new JobNotFoundError("Cannot get jobs by undefined runId");
|
|
219
|
+
const jobs = await this.storage.getByRunId(runId);
|
|
220
|
+
return jobs.map((job) => this.storageToClass(job));
|
|
229
221
|
}
|
|
230
222
|
async peek(status, num) {
|
|
231
223
|
const jobs = await this.storage.peek(status, num);
|
|
@@ -234,86 +226,48 @@ class JobQueue {
|
|
|
234
226
|
async size(status) {
|
|
235
227
|
return this.storage.size(status);
|
|
236
228
|
}
|
|
237
|
-
async delete(id) {
|
|
238
|
-
if (!id)
|
|
239
|
-
throw new JobNotFoundError("Cannot delete undefined job");
|
|
240
|
-
await this.storage.delete(id);
|
|
241
|
-
}
|
|
242
|
-
async disable(id) {
|
|
243
|
-
if (!id)
|
|
244
|
-
throw new JobNotFoundError("Cannot disable undefined job");
|
|
245
|
-
const job = await this.get(id);
|
|
246
|
-
if (!job)
|
|
247
|
-
throw new JobNotFoundError(`Job ${id} not found`);
|
|
248
|
-
await this.disableJob(job);
|
|
249
|
-
}
|
|
250
|
-
getStats() {
|
|
251
|
-
return { ...this.stats };
|
|
252
|
-
}
|
|
253
229
|
async outputForInput(input) {
|
|
254
230
|
if (!input)
|
|
255
231
|
throw new JobNotFoundError("Cannot get output for undefined input");
|
|
256
232
|
return this.storage.outputForInput(input);
|
|
257
233
|
}
|
|
258
|
-
async executeJob(job, signal) {
|
|
259
|
-
if (!job)
|
|
260
|
-
throw new JobNotFoundError("Cannot execute null or undefined job");
|
|
261
|
-
return await job.execute(job.input, {
|
|
262
|
-
signal,
|
|
263
|
-
updateProgress: this.updateProgress.bind(this, job.id)
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
234
|
async waitFor(jobId) {
|
|
267
235
|
if (!jobId)
|
|
268
236
|
throw new JobNotFoundError("Cannot wait for undefined job");
|
|
269
|
-
const
|
|
270
|
-
promise.catch(() => {});
|
|
271
|
-
const promises = this.activeJobPromises.get(jobId) || [];
|
|
272
|
-
promises.push({ resolve, reject });
|
|
273
|
-
this.activeJobPromises.set(jobId, promises);
|
|
274
|
-
const job = await this.get(jobId);
|
|
237
|
+
const job = await this.getJob(jobId);
|
|
275
238
|
if (!job)
|
|
276
239
|
throw new JobNotFoundError(`Job ${jobId} not found`);
|
|
277
|
-
if (job.status ===
|
|
240
|
+
if (job.status === JobStatus2.COMPLETED) {
|
|
278
241
|
return job.output;
|
|
279
242
|
}
|
|
280
|
-
if (job.status ===
|
|
281
|
-
|
|
243
|
+
if (job.status === JobStatus2.DISABLED) {
|
|
244
|
+
throw new JobDisabledError(`Job ${jobId} was disabled`);
|
|
282
245
|
}
|
|
283
|
-
if (job.status ===
|
|
246
|
+
if (job.status === JobStatus2.FAILED) {
|
|
284
247
|
throw this.buildErrorFromJob(job);
|
|
285
248
|
}
|
|
249
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
250
|
+
promise.catch(() => {});
|
|
251
|
+
const promises = this.activeJobPromises.get(jobId) || [];
|
|
252
|
+
promises.push({ resolve, reject });
|
|
253
|
+
this.activeJobPromises.set(jobId, promises);
|
|
286
254
|
return promise;
|
|
287
255
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (job.errorCode === "RetryableJobError") {
|
|
294
|
-
return new RetryableJobError(errorMessage);
|
|
295
|
-
}
|
|
296
|
-
if (job.errorCode === "AbortSignalJobError") {
|
|
297
|
-
return new AbortSignalJobError(errorMessage);
|
|
298
|
-
}
|
|
299
|
-
if (job.errorCode === "JobDisabledError") {
|
|
300
|
-
return new JobDisabledError(errorMessage);
|
|
301
|
-
}
|
|
302
|
-
return new JobError(errorMessage);
|
|
256
|
+
async abort(jobId) {
|
|
257
|
+
if (!jobId)
|
|
258
|
+
throw new JobNotFoundError("Cannot abort undefined job");
|
|
259
|
+
await this.storage.abort(jobId);
|
|
260
|
+
this.events.emit("job_aborting", this.queueName, jobId);
|
|
303
261
|
}
|
|
304
|
-
async
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
job.progressMessage = message;
|
|
314
|
-
job.progressDetails = details;
|
|
315
|
-
await this.saveProgress(jobId, progress, message, details ?? null);
|
|
316
|
-
this.announceProgress(jobId, progress, message, details ?? null);
|
|
262
|
+
async abortJobRun(jobRunId) {
|
|
263
|
+
if (!jobRunId)
|
|
264
|
+
throw new JobNotFoundError("Cannot abort job run with undefined jobRunId");
|
|
265
|
+
const jobs = await this.getJobsByRunId(jobRunId);
|
|
266
|
+
await Promise.allSettled(jobs.map((job) => {
|
|
267
|
+
if ([JobStatus2.PROCESSING, JobStatus2.PENDING].includes(job.status)) {
|
|
268
|
+
return this.abort(job.id);
|
|
269
|
+
}
|
|
270
|
+
}));
|
|
317
271
|
}
|
|
318
272
|
onJobProgress(jobId, listener) {
|
|
319
273
|
if (!this.jobProgressListeners.has(jobId)) {
|
|
@@ -331,78 +285,6 @@ class JobQueue {
|
|
|
331
285
|
}
|
|
332
286
|
};
|
|
333
287
|
}
|
|
334
|
-
async start(mode = "BOTH" /* BOTH */) {
|
|
335
|
-
if (this.running) {
|
|
336
|
-
return this;
|
|
337
|
-
}
|
|
338
|
-
this.mode = mode;
|
|
339
|
-
this.running = true;
|
|
340
|
-
this.events.emit("queue_start", this.queueName);
|
|
341
|
-
if (this.mode === "SERVER" /* SERVER */ || this.mode === "BOTH" /* BOTH */) {
|
|
342
|
-
await this.fixupJobs();
|
|
343
|
-
await this.processJobs();
|
|
344
|
-
}
|
|
345
|
-
if (this.mode === "CLIENT" /* CLIENT */ || this.mode === "BOTH" /* BOTH */) {
|
|
346
|
-
await this.monitorJobs();
|
|
347
|
-
}
|
|
348
|
-
return this;
|
|
349
|
-
}
|
|
350
|
-
async stop() {
|
|
351
|
-
if (!this.running)
|
|
352
|
-
return this;
|
|
353
|
-
this.running = false;
|
|
354
|
-
const size = await this.size(JobStatus.PROCESSING);
|
|
355
|
-
const sleepTime = Math.max(100, size * 2);
|
|
356
|
-
await sleep(sleepTime);
|
|
357
|
-
for (const [jobId] of this.activeJobAbortControllers.entries()) {
|
|
358
|
-
this.abort(jobId);
|
|
359
|
-
}
|
|
360
|
-
this.activeJobPromises.forEach((promises) => promises.forEach(({ reject }) => reject(new PermanentJobError("Queue Stopped"))));
|
|
361
|
-
await sleep(sleepTime);
|
|
362
|
-
this.events.emit("queue_stop", this.queueName);
|
|
363
|
-
return this;
|
|
364
|
-
}
|
|
365
|
-
async clear() {
|
|
366
|
-
await this.storage.deleteAll();
|
|
367
|
-
this.activeJobAbortControllers.clear();
|
|
368
|
-
this.activeJobPromises.clear();
|
|
369
|
-
this.processingTimes.clear();
|
|
370
|
-
this.lastKnownProgress.clear();
|
|
371
|
-
this.jobProgressListeners.clear();
|
|
372
|
-
this.stats = {
|
|
373
|
-
totalJobs: 0,
|
|
374
|
-
completedJobs: 0,
|
|
375
|
-
failedJobs: 0,
|
|
376
|
-
abortedJobs: 0,
|
|
377
|
-
retriedJobs: 0,
|
|
378
|
-
disabledJobs: 0,
|
|
379
|
-
lastUpdateTime: new Date
|
|
380
|
-
};
|
|
381
|
-
this.emitStatsUpdate();
|
|
382
|
-
return this;
|
|
383
|
-
}
|
|
384
|
-
async restart() {
|
|
385
|
-
await this.stop();
|
|
386
|
-
await this.clear();
|
|
387
|
-
await this.start();
|
|
388
|
-
return this;
|
|
389
|
-
}
|
|
390
|
-
async abortJobRun(jobRunId) {
|
|
391
|
-
if (!jobRunId)
|
|
392
|
-
throw new JobNotFoundError("Cannot abort job run with undefined jobRunId");
|
|
393
|
-
const jobs = await this.getJobsByRunId(jobRunId);
|
|
394
|
-
await Promise.allSettled(jobs.map((job) => {
|
|
395
|
-
if ([JobStatus.PROCESSING, JobStatus.PENDING].includes(job.status)) {
|
|
396
|
-
this.abort(job.id);
|
|
397
|
-
}
|
|
398
|
-
}));
|
|
399
|
-
}
|
|
400
|
-
async getJobsByRunId(runId) {
|
|
401
|
-
if (!runId)
|
|
402
|
-
throw new JobNotFoundError("Cannot get jobs by undefined runId");
|
|
403
|
-
const jobs = await this.storage.getByRunId(runId);
|
|
404
|
-
return jobs.map((job) => this.storageToClass(job));
|
|
405
|
-
}
|
|
406
288
|
on(event, listener) {
|
|
407
289
|
this.events.on(event, listener);
|
|
408
290
|
}
|
|
@@ -415,83 +297,44 @@ class JobQueue {
|
|
|
415
297
|
waitOn(event) {
|
|
416
298
|
return this.events.waitOn(event);
|
|
417
299
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
abortedJobs: 0,
|
|
426
|
-
retriedJobs: 0,
|
|
427
|
-
disabledJobs: 0,
|
|
428
|
-
lastUpdateTime: new Date
|
|
429
|
-
};
|
|
430
|
-
events = new EventEmitter;
|
|
431
|
-
activeJobAbortControllers = new Map;
|
|
432
|
-
activeJobPromises = new Map;
|
|
433
|
-
processingTimes = new Map;
|
|
434
|
-
mode = "BOTH" /* BOTH */;
|
|
435
|
-
jobProgressListeners = new Map;
|
|
436
|
-
lastKnownProgress = new Map;
|
|
437
|
-
async saveProgress(id, progress, message, details) {
|
|
438
|
-
if (!id)
|
|
439
|
-
throw new JobNotFoundError("Cannot save progress for undefined job");
|
|
440
|
-
await this.storage.saveProgress(id, progress, message, details);
|
|
441
|
-
}
|
|
442
|
-
createAbortController(jobId) {
|
|
443
|
-
if (!jobId)
|
|
444
|
-
throw new JobNotFoundError("Cannot create abort controller for undefined job");
|
|
445
|
-
if (this.activeJobAbortControllers.has(jobId)) {
|
|
446
|
-
return this.activeJobAbortControllers.get(jobId);
|
|
447
|
-
}
|
|
448
|
-
const abortController = new AbortController;
|
|
449
|
-
abortController.signal.addEventListener("abort", () => this.handleAbort(jobId));
|
|
450
|
-
this.activeJobAbortControllers.set(jobId, abortController);
|
|
451
|
-
return abortController;
|
|
300
|
+
handleJobStart(jobId) {
|
|
301
|
+
this.lastKnownProgress.set(jobId, {
|
|
302
|
+
progress: 0,
|
|
303
|
+
message: "",
|
|
304
|
+
details: null
|
|
305
|
+
});
|
|
306
|
+
this.events.emit("job_start", this.queueName, jobId);
|
|
452
307
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
throw new PermanentJobError(`Job ${job.id} has failed`);
|
|
459
|
-
}
|
|
460
|
-
if (job.status === JobStatus.ABORTING || this.activeJobAbortControllers.get(job.id)?.signal.aborted) {
|
|
461
|
-
throw new AbortSignalJobError(`Job ${job.id} is being aborted`);
|
|
462
|
-
}
|
|
463
|
-
if (job.deadlineAt && job.deadlineAt < new Date) {
|
|
464
|
-
throw new PermanentJobError(`Job ${job.id} has exceeded its deadline`);
|
|
465
|
-
}
|
|
466
|
-
if (job.status === JobStatus.DISABLED) {
|
|
467
|
-
throw new JobDisabledError(`Job ${job.id} has been disabled`);
|
|
308
|
+
handleJobComplete(jobId, output) {
|
|
309
|
+
this.events.emit("job_complete", this.queueName, jobId, output);
|
|
310
|
+
const promises = this.activeJobPromises.get(jobId);
|
|
311
|
+
if (promises) {
|
|
312
|
+
promises.forEach(({ resolve }) => resolve(output));
|
|
468
313
|
}
|
|
314
|
+
this.cleanupJob(jobId);
|
|
469
315
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
316
|
+
handleJobError(jobId, error, errorCode) {
|
|
317
|
+
this.events.emit("job_error", this.queueName, jobId, error);
|
|
318
|
+
const promises = this.activeJobPromises.get(jobId);
|
|
319
|
+
if (promises) {
|
|
320
|
+
const jobError = this.buildErrorFromCode(error, errorCode);
|
|
321
|
+
promises.forEach(({ reject }) => reject(jobError));
|
|
476
322
|
}
|
|
477
|
-
|
|
323
|
+
this.cleanupJob(jobId);
|
|
478
324
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
325
|
+
handleJobDisabled(jobId) {
|
|
326
|
+
this.events.emit("job_disabled", this.queueName, jobId);
|
|
327
|
+
const promises = this.activeJobPromises.get(jobId);
|
|
328
|
+
if (promises) {
|
|
329
|
+
promises.forEach(({ reject }) => reject(new JobDisabledError("Job was disabled")));
|
|
483
330
|
}
|
|
331
|
+
this.cleanupJob(jobId);
|
|
484
332
|
}
|
|
485
|
-
|
|
486
|
-
this.
|
|
487
|
-
this.events.emit("queue_stats_update", this.queueName, { ...this.stats });
|
|
333
|
+
handleJobRetry(jobId, runAfter) {
|
|
334
|
+
this.events.emit("job_retry", this.queueName, jobId, runAfter);
|
|
488
335
|
}
|
|
489
|
-
|
|
490
|
-
this.lastKnownProgress.set(jobId, {
|
|
491
|
-
progress,
|
|
492
|
-
message,
|
|
493
|
-
details
|
|
494
|
-
});
|
|
336
|
+
handleJobProgress(jobId, progress, message, details) {
|
|
337
|
+
this.lastKnownProgress.set(jobId, { progress, message, details });
|
|
495
338
|
this.events.emit("job_progress", this.queueName, jobId, progress, message, details);
|
|
496
339
|
const listeners = this.jobProgressListeners.get(jobId);
|
|
497
340
|
if (listeners) {
|
|
@@ -500,6 +343,48 @@ class JobQueue {
|
|
|
500
343
|
}
|
|
501
344
|
}
|
|
502
345
|
}
|
|
346
|
+
createJobHandle(id) {
|
|
347
|
+
return {
|
|
348
|
+
id,
|
|
349
|
+
waitFor: () => this.waitFor(id),
|
|
350
|
+
abort: () => this.abort(id),
|
|
351
|
+
onProgress: (callback) => this.onJobProgress(id, callback)
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
cleanupJob(jobId) {
|
|
355
|
+
this.activeJobPromises.delete(jobId);
|
|
356
|
+
this.lastKnownProgress.delete(jobId);
|
|
357
|
+
this.jobProgressListeners.delete(jobId);
|
|
358
|
+
}
|
|
359
|
+
handleStorageChange(change) {
|
|
360
|
+
if (!change.new && !change.old)
|
|
361
|
+
return;
|
|
362
|
+
const jobId = change.new?.id ?? change.old?.id;
|
|
363
|
+
if (!jobId)
|
|
364
|
+
return;
|
|
365
|
+
const queueName = change.new?.queue ?? change.old?.queue;
|
|
366
|
+
if (queueName !== this.queueName)
|
|
367
|
+
return;
|
|
368
|
+
if (change.type === "UPDATE" && change.new) {
|
|
369
|
+
const newStatus = change.new.status;
|
|
370
|
+
const oldStatus = change.old?.status;
|
|
371
|
+
if (newStatus === JobStatus2.PROCESSING && oldStatus === JobStatus2.PENDING) {
|
|
372
|
+
this.handleJobStart(jobId);
|
|
373
|
+
} else if (newStatus === JobStatus2.COMPLETED) {
|
|
374
|
+
this.handleJobComplete(jobId, change.new.output);
|
|
375
|
+
} else if (newStatus === JobStatus2.FAILED) {
|
|
376
|
+
this.handleJobError(jobId, change.new.error ?? "Job failed", change.new.error_code ?? undefined);
|
|
377
|
+
} else if (newStatus === JobStatus2.DISABLED) {
|
|
378
|
+
this.handleJobDisabled(jobId);
|
|
379
|
+
} else if (newStatus === JobStatus2.PENDING && oldStatus === JobStatus2.PROCESSING) {
|
|
380
|
+
const runAfter = change.new.run_after ? new Date(change.new.run_after) : new Date;
|
|
381
|
+
this.handleJobRetry(jobId, runAfter);
|
|
382
|
+
}
|
|
383
|
+
if (change.new.progress !== change.old?.progress || change.new.progress_message !== change.old?.progress_message) {
|
|
384
|
+
this.handleJobProgress(jobId, change.new.progress ?? 0, change.new.progress_message ?? "", change.new.progress_details ?? null);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
503
388
|
storageToClass(details) {
|
|
504
389
|
const toDate = (date) => {
|
|
505
390
|
if (!date)
|
|
@@ -507,7 +392,7 @@ class JobQueue {
|
|
|
507
392
|
const d = new Date(date);
|
|
508
393
|
return isNaN(d.getTime()) ? null : d;
|
|
509
394
|
};
|
|
510
|
-
|
|
395
|
+
return new Job({
|
|
511
396
|
id: details.id,
|
|
512
397
|
jobRunId: details.job_run_id,
|
|
513
398
|
queueName: details.queue,
|
|
@@ -528,173 +413,172 @@ class JobQueue {
|
|
|
528
413
|
runAttempts: details.run_attempts ?? 0,
|
|
529
414
|
maxRetries: details.max_retries ?? 10
|
|
530
415
|
});
|
|
531
|
-
job.queue = this;
|
|
532
|
-
return job;
|
|
533
416
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (!date)
|
|
537
|
-
return null;
|
|
538
|
-
return isNaN(date.getTime()) ? null : date.toISOString();
|
|
539
|
-
};
|
|
540
|
-
const now = new Date().toISOString();
|
|
541
|
-
return {
|
|
542
|
-
id: job.id,
|
|
543
|
-
job_run_id: job.jobRunId,
|
|
544
|
-
queue: job.queueName || this.queueName,
|
|
545
|
-
fingerprint: job.fingerprint,
|
|
546
|
-
input: job.input,
|
|
547
|
-
status: job.status,
|
|
548
|
-
output: job.output ?? null,
|
|
549
|
-
error: job.error === null ? null : String(job.error),
|
|
550
|
-
error_code: job.errorCode || null,
|
|
551
|
-
run_attempts: job.runAttempts ?? 0,
|
|
552
|
-
max_retries: job.maxRetries ?? 10,
|
|
553
|
-
run_after: dateToISOString(job.runAfter) ?? now,
|
|
554
|
-
created_at: dateToISOString(job.createdAt) ?? now,
|
|
555
|
-
deadline_at: dateToISOString(job.deadlineAt),
|
|
556
|
-
last_ran_at: dateToISOString(job.lastRanAt),
|
|
557
|
-
completed_at: dateToISOString(job.completedAt),
|
|
558
|
-
progress: job.progress ?? 0,
|
|
559
|
-
progress_message: job.progressMessage ?? "",
|
|
560
|
-
progress_details: job.progressDetails ?? null
|
|
561
|
-
};
|
|
417
|
+
buildErrorFromJob(job) {
|
|
418
|
+
return this.buildErrorFromCode(job.error || "Job failed", job.errorCode ?? undefined);
|
|
562
419
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const nextAvailableTime = await this.limiter.getNextAvailableTime();
|
|
567
|
-
job.runAfter = retryDate instanceof Date ? retryDate : nextAvailableTime;
|
|
568
|
-
job.progress = 0;
|
|
569
|
-
job.progressMessage = "";
|
|
570
|
-
job.progressDetails = null;
|
|
571
|
-
await this.storage.complete(this.classToStorage(job));
|
|
572
|
-
this.stats.retriedJobs++;
|
|
573
|
-
this.events.emit("job_retry", this.queueName, job.id, job.runAfter);
|
|
574
|
-
} catch (err) {
|
|
575
|
-
console.error("rescheduleJob", err);
|
|
420
|
+
buildErrorFromCode(message, errorCode) {
|
|
421
|
+
if (errorCode === "PermanentJobError") {
|
|
422
|
+
return new PermanentJobError(message);
|
|
576
423
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
job.completedAt = new Date;
|
|
583
|
-
job.progressMessage = "";
|
|
584
|
-
job.progressDetails = null;
|
|
585
|
-
await this.storage.complete(this.classToStorage(job));
|
|
586
|
-
if (this.options.deleteAfterDisabledMs === 0) {
|
|
587
|
-
await this.delete(job.id);
|
|
588
|
-
}
|
|
589
|
-
this.stats.disabledJobs++;
|
|
590
|
-
this.events.emit("job_disabled", this.queueName, job.id);
|
|
591
|
-
const promises = this.activeJobPromises.get(job.id) || [];
|
|
592
|
-
promises.forEach(({ resolve }) => resolve(undefined));
|
|
593
|
-
this.activeJobPromises.delete(job.id);
|
|
594
|
-
} catch (err) {
|
|
595
|
-
console.error("disableJob", err);
|
|
424
|
+
if (errorCode === "RetryableJobError") {
|
|
425
|
+
return new RetryableJobError(message);
|
|
426
|
+
}
|
|
427
|
+
if (errorCode === "AbortSignalJobError") {
|
|
428
|
+
return new AbortSignalJobError(message);
|
|
596
429
|
}
|
|
430
|
+
if (errorCode === "JobDisabledError") {
|
|
431
|
+
return new JobDisabledError(message);
|
|
432
|
+
}
|
|
433
|
+
return new JobError(message);
|
|
597
434
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
435
|
+
}
|
|
436
|
+
// src/job/JobQueueServer.ts
|
|
437
|
+
import { JobStatus as JobStatus4 } from "@workglow/storage";
|
|
438
|
+
import { EventEmitter as EventEmitter3 } from "@workglow/util";
|
|
439
|
+
|
|
440
|
+
// src/limiter/NullLimiter.ts
|
|
441
|
+
import { createServiceToken } from "@workglow/util";
|
|
442
|
+
var NULL_JOB_LIMITER = createServiceToken("jobqueue.limiter.null");
|
|
443
|
+
|
|
444
|
+
class NullLimiter {
|
|
445
|
+
async canProceed() {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
async recordJobStart() {}
|
|
449
|
+
async recordJobCompletion() {}
|
|
450
|
+
async getNextAvailableTime() {
|
|
451
|
+
return new Date;
|
|
452
|
+
}
|
|
453
|
+
async setNextAvailableTime(date) {}
|
|
454
|
+
async clear() {}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/job/JobQueueWorker.ts
|
|
458
|
+
import { JobStatus as JobStatus3 } from "@workglow/storage";
|
|
459
|
+
import { EventEmitter as EventEmitter2, sleep, uuid4 } from "@workglow/util";
|
|
460
|
+
class JobQueueWorker {
|
|
461
|
+
queueName;
|
|
462
|
+
workerId;
|
|
463
|
+
storage;
|
|
464
|
+
jobClass;
|
|
465
|
+
limiter;
|
|
466
|
+
pollIntervalMs;
|
|
467
|
+
events = new EventEmitter2;
|
|
468
|
+
running = false;
|
|
469
|
+
activeJobAbortControllers = new Map;
|
|
470
|
+
processingTimes = new Map;
|
|
471
|
+
constructor(jobClass, options) {
|
|
472
|
+
this.queueName = options.queueName;
|
|
473
|
+
this.workerId = uuid4();
|
|
474
|
+
this.storage = options.storage;
|
|
475
|
+
this.jobClass = jobClass;
|
|
476
|
+
this.limiter = options.limiter ?? new NullLimiter;
|
|
477
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 100;
|
|
478
|
+
}
|
|
479
|
+
async start() {
|
|
480
|
+
if (this.running) {
|
|
481
|
+
return this;
|
|
618
482
|
}
|
|
619
|
-
this.
|
|
620
|
-
this.
|
|
621
|
-
this.
|
|
622
|
-
this
|
|
483
|
+
this.running = true;
|
|
484
|
+
this.events.emit("worker_start");
|
|
485
|
+
this.processJobs();
|
|
486
|
+
return this;
|
|
623
487
|
}
|
|
624
|
-
async
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (job && this.options.deleteAfterCompletionMs === 0) {
|
|
636
|
-
await this.delete(job.id);
|
|
637
|
-
}
|
|
638
|
-
this.stats.completedJobs++;
|
|
639
|
-
this.events.emit("job_complete", this.queueName, job.id, output);
|
|
640
|
-
const promises = this.activeJobPromises.get(job.id);
|
|
641
|
-
if (promises) {
|
|
642
|
-
promises.forEach(({ resolve }) => resolve(output));
|
|
488
|
+
async stop() {
|
|
489
|
+
if (!this.running) {
|
|
490
|
+
return this;
|
|
491
|
+
}
|
|
492
|
+
this.running = false;
|
|
493
|
+
const size = await this.storage.size(JobStatus3.PROCESSING);
|
|
494
|
+
const sleepTime = Math.max(100, size * 2);
|
|
495
|
+
await sleep(sleepTime);
|
|
496
|
+
for (const controller of this.activeJobAbortControllers.values()) {
|
|
497
|
+
if (!controller.signal.aborted) {
|
|
498
|
+
controller.abort();
|
|
643
499
|
}
|
|
644
|
-
this.activeJobPromises.delete(job.id);
|
|
645
|
-
} catch (err) {
|
|
646
|
-
console.error("completeJob errored out?", err);
|
|
647
500
|
}
|
|
648
|
-
|
|
649
|
-
this.
|
|
650
|
-
this
|
|
651
|
-
this.activeJobPromises.delete(job.id);
|
|
501
|
+
await sleep(sleepTime);
|
|
502
|
+
this.events.emit("worker_stop");
|
|
503
|
+
return this;
|
|
652
504
|
}
|
|
653
|
-
async
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
let controller = this.activeJobAbortControllers.get(jobId);
|
|
658
|
-
if (!controller) {
|
|
659
|
-
controller = this.createAbortController(jobId);
|
|
505
|
+
async processNext() {
|
|
506
|
+
const canProceed = await this.limiter.canProceed();
|
|
507
|
+
if (!canProceed) {
|
|
508
|
+
return false;
|
|
660
509
|
}
|
|
661
|
-
|
|
662
|
-
|
|
510
|
+
const job = await this.next();
|
|
511
|
+
if (!job) {
|
|
512
|
+
return false;
|
|
663
513
|
}
|
|
664
|
-
this.
|
|
514
|
+
await this.processSingleJob(job);
|
|
515
|
+
return true;
|
|
665
516
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
517
|
+
isRunning() {
|
|
518
|
+
return this.running;
|
|
519
|
+
}
|
|
520
|
+
getActiveJobCount() {
|
|
521
|
+
return this.activeJobAbortControllers.size;
|
|
522
|
+
}
|
|
523
|
+
getAverageProcessingTime() {
|
|
524
|
+
const times = Array.from(this.processingTimes.values());
|
|
525
|
+
if (times.length === 0)
|
|
526
|
+
return;
|
|
527
|
+
return times.reduce((a, b) => a + b, 0) / times.length;
|
|
528
|
+
}
|
|
529
|
+
on(event, listener) {
|
|
530
|
+
this.events.on(event, listener);
|
|
531
|
+
}
|
|
532
|
+
off(event, listener) {
|
|
533
|
+
this.events.off(event, listener);
|
|
534
|
+
}
|
|
535
|
+
async next() {
|
|
536
|
+
const job = await this.storage.next(this.workerId);
|
|
537
|
+
if (!job)
|
|
538
|
+
return;
|
|
539
|
+
return this.storageToClass(job);
|
|
540
|
+
}
|
|
541
|
+
async processJobs() {
|
|
542
|
+
if (!this.running) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
await this.checkForAbortingJobs();
|
|
547
|
+
const canProceed = await this.limiter.canProceed();
|
|
548
|
+
if (canProceed) {
|
|
549
|
+
const job = await this.next();
|
|
550
|
+
if (job) {
|
|
551
|
+
this.processSingleJob(job);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} finally {
|
|
555
|
+
if (this.running) {
|
|
556
|
+
setTimeout(() => this.processJobs(), this.pollIntervalMs);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async checkForAbortingJobs() {
|
|
561
|
+
const abortingJobs = await this.storage.peek(JobStatus3.ABORTING);
|
|
562
|
+
for (const jobData of abortingJobs) {
|
|
563
|
+
const controller = this.activeJobAbortControllers.get(jobData.id);
|
|
564
|
+
if (controller && !controller.signal.aborted) {
|
|
565
|
+
controller.abort();
|
|
673
566
|
}
|
|
674
|
-
const error = new AbortSignalJobError("Job Aborted");
|
|
675
|
-
this.failJob(job, error);
|
|
676
567
|
}
|
|
677
|
-
this.stats.abortedJobs++;
|
|
678
568
|
}
|
|
679
569
|
async processSingleJob(job) {
|
|
680
|
-
if (!job || !job.id)
|
|
570
|
+
if (!job || !job.id) {
|
|
681
571
|
throw new JobNotFoundError("Invalid job provided for processing");
|
|
572
|
+
}
|
|
682
573
|
const startTime = Date.now();
|
|
683
574
|
try {
|
|
684
575
|
await this.validateJobState(job);
|
|
685
576
|
await this.limiter.recordJobStart();
|
|
686
|
-
this.emitStatsUpdate();
|
|
687
577
|
const abortController = this.createAbortController(job.id);
|
|
688
|
-
this.
|
|
689
|
-
progress: 0,
|
|
690
|
-
message: "",
|
|
691
|
-
details: null
|
|
692
|
-
});
|
|
693
|
-
this.events.emit("job_start", this.queueName, job.id);
|
|
578
|
+
this.events.emit("job_start", job.id);
|
|
694
579
|
const output = await this.executeJob(job, abortController.signal);
|
|
695
580
|
await this.completeJob(job, output);
|
|
696
581
|
this.processingTimes.set(job.id, Date.now() - startTime);
|
|
697
|
-
this.updateAverageProcessingTime();
|
|
698
582
|
} catch (err) {
|
|
699
583
|
const error = this.normalizeError(err);
|
|
700
584
|
if (error instanceof RetryableJobError) {
|
|
@@ -708,85 +592,485 @@ class JobQueue {
|
|
|
708
592
|
}
|
|
709
593
|
} finally {
|
|
710
594
|
await this.limiter.recordJobCompletion();
|
|
711
|
-
this.emitStatsUpdate();
|
|
712
595
|
}
|
|
713
596
|
}
|
|
714
|
-
async
|
|
715
|
-
if (!
|
|
716
|
-
|
|
597
|
+
async executeJob(job, signal) {
|
|
598
|
+
if (!job)
|
|
599
|
+
throw new JobNotFoundError("Cannot execute null or undefined job");
|
|
600
|
+
return await job.execute(job.input, {
|
|
601
|
+
signal,
|
|
602
|
+
updateProgress: this.updateProgress.bind(this, job.id)
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
async updateProgress(jobId, progress, message = "", details = null) {
|
|
606
|
+
progress = Math.max(0, Math.min(100, progress));
|
|
607
|
+
await this.storage.saveProgress(jobId, progress, message, details);
|
|
608
|
+
this.events.emit("job_progress", jobId, progress, message, details);
|
|
609
|
+
}
|
|
610
|
+
async completeJob(job, output) {
|
|
611
|
+
try {
|
|
612
|
+
job.status = JobStatus3.COMPLETED;
|
|
613
|
+
job.progress = 100;
|
|
614
|
+
job.progressMessage = "";
|
|
615
|
+
job.progressDetails = null;
|
|
616
|
+
job.completedAt = new Date;
|
|
617
|
+
job.output = output ?? null;
|
|
618
|
+
job.error = null;
|
|
619
|
+
job.errorCode = null;
|
|
620
|
+
await this.storage.complete(this.classToStorage(job));
|
|
621
|
+
this.events.emit("job_complete", job.id, output);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.error("completeJob errored:", err);
|
|
624
|
+
} finally {
|
|
625
|
+
this.cleanupJob(job.id);
|
|
717
626
|
}
|
|
627
|
+
}
|
|
628
|
+
async failJob(job, error) {
|
|
718
629
|
try {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
630
|
+
job.status = JobStatus3.FAILED;
|
|
631
|
+
job.progress = 100;
|
|
632
|
+
job.completedAt = new Date;
|
|
633
|
+
job.progressMessage = "";
|
|
634
|
+
job.progressDetails = null;
|
|
635
|
+
job.error = error.message;
|
|
636
|
+
job.errorCode = error?.constructor?.name ?? null;
|
|
637
|
+
await this.storage.complete(this.classToStorage(job));
|
|
638
|
+
this.events.emit("job_error", job.id, error.message, error.constructor.name);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.error("failJob errored:", err);
|
|
641
|
+
} finally {
|
|
642
|
+
this.cleanupJob(job.id);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async disableJob(job) {
|
|
646
|
+
try {
|
|
647
|
+
job.status = JobStatus3.DISABLED;
|
|
648
|
+
job.progress = 100;
|
|
649
|
+
job.completedAt = new Date;
|
|
650
|
+
job.progressMessage = "";
|
|
651
|
+
job.progressDetails = null;
|
|
652
|
+
await this.storage.complete(this.classToStorage(job));
|
|
653
|
+
this.events.emit("job_disabled", job.id);
|
|
654
|
+
} catch (err) {
|
|
655
|
+
console.error("disableJob errored:", err);
|
|
656
|
+
} finally {
|
|
657
|
+
this.cleanupJob(job.id);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
async rescheduleJob(job, retryDate) {
|
|
661
|
+
try {
|
|
662
|
+
job.status = JobStatus3.PENDING;
|
|
663
|
+
const nextAvailableTime = await this.limiter.getNextAvailableTime();
|
|
664
|
+
job.runAfter = retryDate instanceof Date ? retryDate : nextAvailableTime;
|
|
665
|
+
job.progress = 0;
|
|
666
|
+
job.progressMessage = "";
|
|
667
|
+
job.progressDetails = null;
|
|
668
|
+
await this.storage.complete(this.classToStorage(job));
|
|
669
|
+
this.events.emit("job_retry", job.id, job.runAfter);
|
|
670
|
+
} catch (err) {
|
|
671
|
+
console.error("rescheduleJob errored:", err);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
createAbortController(jobId) {
|
|
675
|
+
if (!jobId)
|
|
676
|
+
throw new JobNotFoundError("Cannot create abort controller for undefined job");
|
|
677
|
+
if (this.activeJobAbortControllers.has(jobId)) {
|
|
678
|
+
return this.activeJobAbortControllers.get(jobId);
|
|
679
|
+
}
|
|
680
|
+
const abortController = new AbortController;
|
|
681
|
+
abortController.signal.addEventListener("abort", () => this.handleAbort(jobId));
|
|
682
|
+
this.activeJobAbortControllers.set(jobId, abortController);
|
|
683
|
+
return abortController;
|
|
684
|
+
}
|
|
685
|
+
async handleAbort(jobId) {
|
|
686
|
+
const job = await this.getJob(jobId);
|
|
687
|
+
if (!job) {
|
|
688
|
+
console.error("handleAbort: job not found", jobId);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const error = new AbortSignalJobError("Job Aborted");
|
|
692
|
+
await this.failJob(job, error);
|
|
693
|
+
}
|
|
694
|
+
async getJob(id) {
|
|
695
|
+
const job = await this.storage.get(id);
|
|
696
|
+
if (!job)
|
|
697
|
+
return;
|
|
698
|
+
return this.storageToClass(job);
|
|
699
|
+
}
|
|
700
|
+
async validateJobState(job) {
|
|
701
|
+
if (job.status === JobStatus3.COMPLETED) {
|
|
702
|
+
throw new PermanentJobError(`Job ${job.id} is already completed`);
|
|
703
|
+
}
|
|
704
|
+
if (job.status === JobStatus3.FAILED) {
|
|
705
|
+
throw new PermanentJobError(`Job ${job.id} has failed`);
|
|
706
|
+
}
|
|
707
|
+
if (job.status === JobStatus3.ABORTING || this.activeJobAbortControllers.get(job.id)?.signal.aborted) {
|
|
708
|
+
throw new AbortSignalJobError(`Job ${job.id} is being aborted`);
|
|
709
|
+
}
|
|
710
|
+
if (job.deadlineAt && job.deadlineAt < new Date) {
|
|
711
|
+
throw new PermanentJobError(`Job ${job.id} has exceeded its deadline`);
|
|
712
|
+
}
|
|
713
|
+
if (job.status === JobStatus3.DISABLED) {
|
|
714
|
+
throw new JobDisabledError(`Job ${job.id} has been disabled`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
normalizeError(err) {
|
|
718
|
+
if (err instanceof JobError) {
|
|
719
|
+
return err;
|
|
720
|
+
}
|
|
721
|
+
if (err instanceof Error) {
|
|
722
|
+
return new PermanentJobError(err.message);
|
|
723
|
+
}
|
|
724
|
+
return new PermanentJobError(String(err));
|
|
725
|
+
}
|
|
726
|
+
cleanupJob(jobId) {
|
|
727
|
+
this.activeJobAbortControllers.delete(jobId);
|
|
728
|
+
}
|
|
729
|
+
storageToClass(details) {
|
|
730
|
+
const toDate = (date) => {
|
|
731
|
+
if (!date)
|
|
732
|
+
return null;
|
|
733
|
+
const d = new Date(date);
|
|
734
|
+
return isNaN(d.getTime()) ? null : d;
|
|
735
|
+
};
|
|
736
|
+
return new this.jobClass({
|
|
737
|
+
id: details.id,
|
|
738
|
+
jobRunId: details.job_run_id,
|
|
739
|
+
queueName: details.queue,
|
|
740
|
+
fingerprint: details.fingerprint,
|
|
741
|
+
input: details.input,
|
|
742
|
+
output: details.output,
|
|
743
|
+
runAfter: toDate(details.run_after),
|
|
744
|
+
createdAt: toDate(details.created_at),
|
|
745
|
+
deadlineAt: toDate(details.deadline_at),
|
|
746
|
+
lastRanAt: toDate(details.last_ran_at),
|
|
747
|
+
completedAt: toDate(details.completed_at),
|
|
748
|
+
progress: details.progress || 0,
|
|
749
|
+
progressMessage: details.progress_message || "",
|
|
750
|
+
progressDetails: details.progress_details ?? null,
|
|
751
|
+
status: details.status,
|
|
752
|
+
error: details.error ?? null,
|
|
753
|
+
errorCode: details.error_code ?? null,
|
|
754
|
+
runAttempts: details.run_attempts ?? 0,
|
|
755
|
+
maxRetries: details.max_retries ?? 10
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
classToStorage(job) {
|
|
759
|
+
const dateToISOString = (date) => {
|
|
760
|
+
if (!date)
|
|
761
|
+
return null;
|
|
762
|
+
return isNaN(date.getTime()) ? null : date.toISOString();
|
|
763
|
+
};
|
|
764
|
+
const now = new Date().toISOString();
|
|
765
|
+
return {
|
|
766
|
+
id: job.id,
|
|
767
|
+
job_run_id: job.jobRunId,
|
|
768
|
+
queue: job.queueName || this.queueName,
|
|
769
|
+
fingerprint: job.fingerprint,
|
|
770
|
+
input: job.input,
|
|
771
|
+
status: job.status,
|
|
772
|
+
output: job.output ?? null,
|
|
773
|
+
error: job.error === null ? null : String(job.error),
|
|
774
|
+
error_code: job.errorCode || null,
|
|
775
|
+
run_attempts: job.runAttempts ?? 0,
|
|
776
|
+
max_retries: job.maxRetries ?? 10,
|
|
777
|
+
run_after: dateToISOString(job.runAfter) ?? now,
|
|
778
|
+
created_at: dateToISOString(job.createdAt) ?? now,
|
|
779
|
+
deadline_at: dateToISOString(job.deadlineAt),
|
|
780
|
+
last_ran_at: dateToISOString(job.lastRanAt),
|
|
781
|
+
completed_at: dateToISOString(job.completedAt),
|
|
782
|
+
progress: job.progress ?? 0,
|
|
783
|
+
progress_message: job.progressMessage ?? "",
|
|
784
|
+
progress_details: job.progressDetails ?? null
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/job/JobQueueServer.ts
|
|
790
|
+
class JobQueueServer {
|
|
791
|
+
queueName;
|
|
792
|
+
storage;
|
|
793
|
+
jobClass;
|
|
794
|
+
limiter;
|
|
795
|
+
workerCount;
|
|
796
|
+
pollIntervalMs;
|
|
797
|
+
deleteAfterCompletionMs;
|
|
798
|
+
deleteAfterFailureMs;
|
|
799
|
+
deleteAfterDisabledMs;
|
|
800
|
+
cleanupIntervalMs;
|
|
801
|
+
events = new EventEmitter3;
|
|
802
|
+
workers = [];
|
|
803
|
+
clients = new Set;
|
|
804
|
+
running = false;
|
|
805
|
+
cleanupTimer = null;
|
|
806
|
+
stats = {
|
|
807
|
+
totalJobs: 0,
|
|
808
|
+
completedJobs: 0,
|
|
809
|
+
failedJobs: 0,
|
|
810
|
+
abortedJobs: 0,
|
|
811
|
+
retriedJobs: 0,
|
|
812
|
+
disabledJobs: 0,
|
|
813
|
+
lastUpdateTime: new Date
|
|
814
|
+
};
|
|
815
|
+
constructor(jobClass, options) {
|
|
816
|
+
this.queueName = options.queueName;
|
|
817
|
+
this.storage = options.storage;
|
|
818
|
+
this.jobClass = jobClass;
|
|
819
|
+
this.limiter = options.limiter ?? new NullLimiter;
|
|
820
|
+
this.workerCount = options.workerCount ?? 1;
|
|
821
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 100;
|
|
822
|
+
this.deleteAfterCompletionMs = options.deleteAfterCompletionMs;
|
|
823
|
+
this.deleteAfterFailureMs = options.deleteAfterFailureMs;
|
|
824
|
+
this.deleteAfterDisabledMs = options.deleteAfterDisabledMs;
|
|
825
|
+
this.cleanupIntervalMs = options.cleanupIntervalMs ?? 1e4;
|
|
826
|
+
this.initializeWorkers();
|
|
827
|
+
}
|
|
828
|
+
async start() {
|
|
829
|
+
if (this.running) {
|
|
830
|
+
return this;
|
|
831
|
+
}
|
|
832
|
+
this.running = true;
|
|
833
|
+
this.events.emit("server_start", this.queueName);
|
|
834
|
+
await this.fixupJobs();
|
|
835
|
+
await Promise.all(this.workers.map((worker) => worker.start()));
|
|
836
|
+
this.startCleanupLoop();
|
|
837
|
+
return this;
|
|
838
|
+
}
|
|
839
|
+
async stop() {
|
|
840
|
+
if (!this.running) {
|
|
841
|
+
return this;
|
|
842
|
+
}
|
|
843
|
+
this.running = false;
|
|
844
|
+
if (this.cleanupTimer) {
|
|
845
|
+
clearTimeout(this.cleanupTimer);
|
|
846
|
+
this.cleanupTimer = null;
|
|
847
|
+
}
|
|
848
|
+
await Promise.all(this.workers.map((worker) => worker.stop()));
|
|
849
|
+
this.events.emit("server_stop", this.queueName);
|
|
850
|
+
return this;
|
|
851
|
+
}
|
|
852
|
+
getStats() {
|
|
853
|
+
return { ...this.stats };
|
|
854
|
+
}
|
|
855
|
+
getStorage() {
|
|
856
|
+
return this.storage;
|
|
857
|
+
}
|
|
858
|
+
async scaleWorkers(count) {
|
|
859
|
+
if (count < 1) {
|
|
860
|
+
throw new Error("Worker count must be at least 1");
|
|
861
|
+
}
|
|
862
|
+
const currentCount = this.workers.length;
|
|
863
|
+
if (count > currentCount) {
|
|
864
|
+
for (let i = currentCount;i < count; i++) {
|
|
865
|
+
const worker = this.createWorker();
|
|
866
|
+
this.workers.push(worker);
|
|
867
|
+
if (this.running) {
|
|
868
|
+
await worker.start();
|
|
725
869
|
}
|
|
726
870
|
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
|
|
871
|
+
} else if (count < currentCount) {
|
|
872
|
+
const toRemove = this.workers.splice(count);
|
|
873
|
+
await Promise.all(toRemove.map((worker) => worker.stop()));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
isRunning() {
|
|
877
|
+
return this.running;
|
|
878
|
+
}
|
|
879
|
+
getWorkerCount() {
|
|
880
|
+
return this.workers.length;
|
|
881
|
+
}
|
|
882
|
+
addClient(client) {
|
|
883
|
+
this.clients.add(client);
|
|
884
|
+
}
|
|
885
|
+
removeClient(client) {
|
|
886
|
+
this.clients.delete(client);
|
|
887
|
+
}
|
|
888
|
+
on(event, listener) {
|
|
889
|
+
this.events.on(event, listener);
|
|
890
|
+
}
|
|
891
|
+
off(event, listener) {
|
|
892
|
+
this.events.off(event, listener);
|
|
893
|
+
}
|
|
894
|
+
initializeWorkers() {
|
|
895
|
+
for (let i = 0;i < this.workerCount; i++) {
|
|
896
|
+
const worker = this.createWorker();
|
|
897
|
+
this.workers.push(worker);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
createWorker() {
|
|
901
|
+
const worker = new JobQueueWorker(this.jobClass, {
|
|
902
|
+
storage: this.storage,
|
|
903
|
+
queueName: this.queueName,
|
|
904
|
+
limiter: this.limiter,
|
|
905
|
+
pollIntervalMs: this.pollIntervalMs
|
|
906
|
+
});
|
|
907
|
+
worker.on("job_start", (jobId) => {
|
|
908
|
+
this.stats = { ...this.stats, totalJobs: this.stats.totalJobs + 1 };
|
|
909
|
+
this.events.emit("job_start", this.queueName, jobId);
|
|
910
|
+
this.forwardToClients("handleJobStart", jobId);
|
|
911
|
+
});
|
|
912
|
+
worker.on("job_complete", async (jobId, output) => {
|
|
913
|
+
this.stats = { ...this.stats, completedJobs: this.stats.completedJobs + 1 };
|
|
914
|
+
this.updateAverageProcessingTime();
|
|
915
|
+
this.events.emit("job_complete", this.queueName, jobId, output);
|
|
916
|
+
this.forwardToClients("handleJobComplete", jobId, output);
|
|
917
|
+
if (this.deleteAfterCompletionMs === 0) {
|
|
918
|
+
await this.storage.delete(jobId);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
worker.on("job_error", async (jobId, error, errorCode) => {
|
|
922
|
+
this.stats = { ...this.stats, failedJobs: this.stats.failedJobs + 1 };
|
|
923
|
+
this.events.emit("job_error", this.queueName, jobId, error);
|
|
924
|
+
this.forwardToClients("handleJobError", jobId, error, errorCode);
|
|
925
|
+
if (this.deleteAfterFailureMs === 0) {
|
|
926
|
+
await this.storage.delete(jobId);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
worker.on("job_disabled", async (jobId) => {
|
|
930
|
+
this.stats = { ...this.stats, disabledJobs: this.stats.disabledJobs + 1 };
|
|
931
|
+
this.events.emit("job_disabled", this.queueName, jobId);
|
|
932
|
+
this.forwardToClients("handleJobDisabled", jobId);
|
|
933
|
+
if (this.deleteAfterDisabledMs === 0) {
|
|
934
|
+
await this.storage.delete(jobId);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
worker.on("job_retry", (jobId, runAfter) => {
|
|
938
|
+
this.stats = { ...this.stats, retriedJobs: this.stats.retriedJobs + 1 };
|
|
939
|
+
this.events.emit("job_retry", this.queueName, jobId, runAfter);
|
|
940
|
+
this.forwardToClients("handleJobRetry", jobId, runAfter);
|
|
941
|
+
});
|
|
942
|
+
worker.on("job_progress", (jobId, progress, message, details) => {
|
|
943
|
+
this.events.emit("job_progress", this.queueName, jobId, progress, message, details);
|
|
944
|
+
this.forwardToClients("handleJobProgress", jobId, progress, message, details);
|
|
945
|
+
});
|
|
946
|
+
return worker;
|
|
730
947
|
}
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
948
|
+
forwardToClients(method, ...args) {
|
|
949
|
+
for (const client of this.clients) {
|
|
950
|
+
const fn = client[method];
|
|
951
|
+
if (typeof fn === "function") {
|
|
952
|
+
fn.apply(client, args);
|
|
953
|
+
}
|
|
735
954
|
}
|
|
736
|
-
|
|
737
|
-
|
|
955
|
+
}
|
|
956
|
+
updateAverageProcessingTime() {
|
|
957
|
+
const times = [];
|
|
958
|
+
for (const worker of this.workers) {
|
|
959
|
+
const avgTime = worker.getAverageProcessingTime();
|
|
960
|
+
if (avgTime !== undefined) {
|
|
961
|
+
times.push(avgTime);
|
|
962
|
+
}
|
|
738
963
|
}
|
|
739
|
-
if (
|
|
740
|
-
|
|
964
|
+
if (times.length > 0) {
|
|
965
|
+
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
966
|
+
this.stats = {
|
|
967
|
+
...this.stats,
|
|
968
|
+
averageProcessingTime: avg,
|
|
969
|
+
lastUpdateTime: new Date
|
|
970
|
+
};
|
|
741
971
|
}
|
|
742
972
|
}
|
|
743
|
-
|
|
744
|
-
if (!this.running)
|
|
973
|
+
startCleanupLoop() {
|
|
974
|
+
if (!this.running)
|
|
745
975
|
return;
|
|
746
|
-
|
|
976
|
+
this.cleanupJobs().finally(() => {
|
|
977
|
+
if (this.running) {
|
|
978
|
+
this.cleanupTimer = setTimeout(() => this.startCleanupLoop(), this.cleanupIntervalMs);
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
async cleanupJobs() {
|
|
747
983
|
try {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const job = await this.get(jobId);
|
|
751
|
-
if (job) {
|
|
752
|
-
const currentProgress = {
|
|
753
|
-
progress: job.progress,
|
|
754
|
-
message: job.progressMessage,
|
|
755
|
-
details: job.progressDetails || null
|
|
756
|
-
};
|
|
757
|
-
const lastProgress = this.lastKnownProgress.get(jobId);
|
|
758
|
-
const hasChanged = !lastProgress || lastProgress.progress !== currentProgress.progress || lastProgress.message !== currentProgress.message;
|
|
759
|
-
if (hasChanged && currentProgress.progress !== 0 && currentProgress.message !== "") {
|
|
760
|
-
this.announceProgress(jobId, currentProgress.progress, currentProgress.message, currentProgress.details);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
984
|
+
if (this.deleteAfterCompletionMs !== undefined && this.deleteAfterCompletionMs > 0) {
|
|
985
|
+
await this.storage.deleteJobsByStatusAndAge(JobStatus4.COMPLETED, this.deleteAfterCompletionMs);
|
|
763
986
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
987
|
+
if (this.deleteAfterFailureMs !== undefined && this.deleteAfterFailureMs > 0) {
|
|
988
|
+
await this.storage.deleteJobsByStatusAndAge(JobStatus4.FAILED, this.deleteAfterFailureMs);
|
|
989
|
+
}
|
|
990
|
+
if (this.deleteAfterDisabledMs !== undefined && this.deleteAfterDisabledMs > 0) {
|
|
991
|
+
await this.storage.deleteJobsByStatusAndAge(JobStatus4.DISABLED, this.deleteAfterDisabledMs);
|
|
769
992
|
}
|
|
770
993
|
} catch (error) {
|
|
771
|
-
console.error(
|
|
994
|
+
console.error("Error in cleanup:", error);
|
|
772
995
|
}
|
|
773
|
-
setTimeout(() => this.monitorJobs(), this.options.waitDurationInMilliseconds);
|
|
774
996
|
}
|
|
775
997
|
async fixupJobs() {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
998
|
+
try {
|
|
999
|
+
const stuckProcessingJobs = await this.storage.peek(JobStatus4.PROCESSING);
|
|
1000
|
+
const stuckAbortingJobs = await this.storage.peek(JobStatus4.ABORTING);
|
|
1001
|
+
const stuckJobs = [...stuckProcessingJobs, ...stuckAbortingJobs];
|
|
1002
|
+
for (const jobData of stuckJobs) {
|
|
1003
|
+
const job = this.storageToClass(jobData);
|
|
1004
|
+
job.status = JobStatus4.PENDING;
|
|
1005
|
+
job.runAfter = job.lastRanAt || new Date;
|
|
1006
|
+
job.progress = 0;
|
|
1007
|
+
job.progressMessage = "";
|
|
1008
|
+
job.progressDetails = null;
|
|
1009
|
+
job.error = "Server restarted";
|
|
1010
|
+
await this.storage.complete(this.classToStorage(job));
|
|
1011
|
+
}
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
console.error("Error in fixupJobs:", error);
|
|
788
1014
|
}
|
|
789
1015
|
}
|
|
1016
|
+
storageToClass(details) {
|
|
1017
|
+
const toDate = (date) => {
|
|
1018
|
+
if (!date)
|
|
1019
|
+
return null;
|
|
1020
|
+
const d = new Date(date);
|
|
1021
|
+
return isNaN(d.getTime()) ? null : d;
|
|
1022
|
+
};
|
|
1023
|
+
return new this.jobClass({
|
|
1024
|
+
id: details.id,
|
|
1025
|
+
jobRunId: details.job_run_id,
|
|
1026
|
+
queueName: details.queue,
|
|
1027
|
+
fingerprint: details.fingerprint,
|
|
1028
|
+
input: details.input,
|
|
1029
|
+
output: details.output,
|
|
1030
|
+
runAfter: toDate(details.run_after),
|
|
1031
|
+
createdAt: toDate(details.created_at),
|
|
1032
|
+
deadlineAt: toDate(details.deadline_at),
|
|
1033
|
+
lastRanAt: toDate(details.last_ran_at),
|
|
1034
|
+
completedAt: toDate(details.completed_at),
|
|
1035
|
+
progress: details.progress || 0,
|
|
1036
|
+
progressMessage: details.progress_message || "",
|
|
1037
|
+
progressDetails: details.progress_details ?? null,
|
|
1038
|
+
status: details.status,
|
|
1039
|
+
error: details.error ?? null,
|
|
1040
|
+
errorCode: details.error_code ?? null,
|
|
1041
|
+
runAttempts: details.run_attempts ?? 0,
|
|
1042
|
+
maxRetries: details.max_retries ?? 10
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
classToStorage(job) {
|
|
1046
|
+
const dateToISOString = (date) => {
|
|
1047
|
+
if (!date)
|
|
1048
|
+
return null;
|
|
1049
|
+
return isNaN(date.getTime()) ? null : date.toISOString();
|
|
1050
|
+
};
|
|
1051
|
+
const now = new Date().toISOString();
|
|
1052
|
+
return {
|
|
1053
|
+
id: job.id,
|
|
1054
|
+
job_run_id: job.jobRunId,
|
|
1055
|
+
queue: job.queueName || this.queueName,
|
|
1056
|
+
fingerprint: job.fingerprint,
|
|
1057
|
+
input: job.input,
|
|
1058
|
+
status: job.status,
|
|
1059
|
+
output: job.output ?? null,
|
|
1060
|
+
error: job.error === null ? null : String(job.error),
|
|
1061
|
+
error_code: job.errorCode || null,
|
|
1062
|
+
run_attempts: job.runAttempts ?? 0,
|
|
1063
|
+
max_retries: job.maxRetries ?? 10,
|
|
1064
|
+
run_after: dateToISOString(job.runAfter) ?? now,
|
|
1065
|
+
created_at: dateToISOString(job.createdAt) ?? now,
|
|
1066
|
+
deadline_at: dateToISOString(job.deadlineAt),
|
|
1067
|
+
last_ran_at: dateToISOString(job.lastRanAt),
|
|
1068
|
+
completed_at: dateToISOString(job.completedAt),
|
|
1069
|
+
progress: job.progress ?? 0,
|
|
1070
|
+
progress_message: job.progressMessage ?? "",
|
|
1071
|
+
progress_details: job.progressDetails ?? null
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
790
1074
|
}
|
|
791
1075
|
// src/limiter/CompositeLimiter.ts
|
|
792
1076
|
class CompositeLimiter {
|
|
@@ -831,8 +1115,8 @@ class CompositeLimiter {
|
|
|
831
1115
|
}
|
|
832
1116
|
}
|
|
833
1117
|
// src/limiter/ConcurrencyLimiter.ts
|
|
834
|
-
import { createServiceToken as
|
|
835
|
-
var CONCURRENT_JOB_LIMITER =
|
|
1118
|
+
import { createServiceToken as createServiceToken2 } from "@workglow/util";
|
|
1119
|
+
var CONCURRENT_JOB_LIMITER = createServiceToken2("jobqueue.limiter.concurrent");
|
|
836
1120
|
|
|
837
1121
|
class ConcurrencyLimiter {
|
|
838
1122
|
currentRunningJobs = 0;
|
|
@@ -895,8 +1179,8 @@ class DelayLimiter {
|
|
|
895
1179
|
}
|
|
896
1180
|
}
|
|
897
1181
|
// src/limiter/EvenlySpacedRateLimiter.ts
|
|
898
|
-
import { createServiceToken as
|
|
899
|
-
var EVENLY_SPACED_JOB_RATE_LIMITER =
|
|
1182
|
+
import { createServiceToken as createServiceToken3 } from "@workglow/util";
|
|
1183
|
+
var EVENLY_SPACED_JOB_RATE_LIMITER = createServiceToken3("jobqueue.limiter.rate.evenlyspaced");
|
|
900
1184
|
|
|
901
1185
|
class EvenlySpacedRateLimiter {
|
|
902
1186
|
maxExecutions;
|
|
@@ -955,117 +1239,27 @@ class EvenlySpacedRateLimiter {
|
|
|
955
1239
|
this.lastStartTime = 0;
|
|
956
1240
|
}
|
|
957
1241
|
}
|
|
958
|
-
// src/limiter/
|
|
959
|
-
import { createServiceToken as
|
|
960
|
-
var
|
|
961
|
-
|
|
962
|
-
class
|
|
963
|
-
|
|
964
|
-
|
|
1242
|
+
// src/limiter/ILimiter.ts
|
|
1243
|
+
import { createServiceToken as createServiceToken4 } from "@workglow/util";
|
|
1244
|
+
var JOB_LIMITER = createServiceToken4("jobqueue.limiter");
|
|
1245
|
+
// src/limiter/RateLimiter.ts
|
|
1246
|
+
class RateLimiter {
|
|
1247
|
+
storage;
|
|
1248
|
+
queueName;
|
|
1249
|
+
windowSizeInMilliseconds;
|
|
965
1250
|
currentBackoffDelay;
|
|
966
1251
|
maxExecutions;
|
|
967
|
-
windowSizeInMilliseconds;
|
|
968
1252
|
initialBackoffDelay;
|
|
969
1253
|
backoffMultiplier;
|
|
970
1254
|
maxBackoffDelay;
|
|
971
|
-
constructor({
|
|
972
|
-
maxExecutions,
|
|
973
|
-
windowSizeInSeconds,
|
|
974
|
-
initialBackoffDelay = 1000,
|
|
975
|
-
backoffMultiplier = 2,
|
|
976
|
-
maxBackoffDelay = 600000
|
|
977
|
-
}) {
|
|
978
|
-
if (maxExecutions <= 0) {
|
|
979
|
-
throw new Error("maxExecutions must be greater than 0");
|
|
980
|
-
}
|
|
981
|
-
if (windowSizeInSeconds <= 0) {
|
|
982
|
-
throw new Error("windowSizeInSeconds must be greater than 0");
|
|
983
|
-
}
|
|
984
|
-
if (initialBackoffDelay <= 0) {
|
|
985
|
-
throw new Error("initialBackoffDelay must be greater than 0");
|
|
986
|
-
}
|
|
987
|
-
if (backoffMultiplier <= 1) {
|
|
988
|
-
throw new Error("backoffMultiplier must be greater than 1");
|
|
989
|
-
}
|
|
990
|
-
if (maxBackoffDelay <= initialBackoffDelay) {
|
|
991
|
-
throw new Error("maxBackoffDelay must be greater than initialBackoffDelay");
|
|
992
|
-
}
|
|
993
|
-
this.maxExecutions = maxExecutions;
|
|
994
|
-
this.windowSizeInMilliseconds = windowSizeInSeconds * 1000;
|
|
995
|
-
this.initialBackoffDelay = initialBackoffDelay;
|
|
996
|
-
this.backoffMultiplier = backoffMultiplier;
|
|
997
|
-
this.maxBackoffDelay = maxBackoffDelay;
|
|
998
|
-
this.currentBackoffDelay = initialBackoffDelay;
|
|
999
|
-
}
|
|
1000
|
-
removeOldRequests() {
|
|
1001
|
-
const now = Date.now();
|
|
1002
|
-
const cutoff = now - this.windowSizeInMilliseconds;
|
|
1003
|
-
this.requests = this.requests.filter((d) => d.getTime() > cutoff);
|
|
1004
|
-
if (this.nextAvailableTime.getTime() < now) {
|
|
1005
|
-
this.nextAvailableTime = new Date(now);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
increaseBackoff() {
|
|
1009
|
-
this.currentBackoffDelay = Math.min(this.currentBackoffDelay * this.backoffMultiplier, this.maxBackoffDelay);
|
|
1010
|
-
}
|
|
1011
|
-
addJitter(base) {
|
|
1012
|
-
return base + Math.random() * base;
|
|
1013
|
-
}
|
|
1014
|
-
async canProceed() {
|
|
1015
|
-
this.removeOldRequests();
|
|
1016
|
-
const now = Date.now();
|
|
1017
|
-
const okRequestCount = this.requests.length < this.maxExecutions;
|
|
1018
|
-
const okTime = now >= this.nextAvailableTime.getTime();
|
|
1019
|
-
const canProceedNow = okRequestCount && okTime;
|
|
1020
|
-
if (!canProceedNow) {
|
|
1021
|
-
this.increaseBackoff();
|
|
1022
|
-
} else {
|
|
1023
|
-
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1024
|
-
}
|
|
1025
|
-
return canProceedNow;
|
|
1026
|
-
}
|
|
1027
|
-
async recordJobStart() {
|
|
1028
|
-
this.requests.push(new Date);
|
|
1029
|
-
if (this.requests.length >= this.maxExecutions) {
|
|
1030
|
-
const earliest = this.requests[0].getTime();
|
|
1031
|
-
const windowExpires = earliest + this.windowSizeInMilliseconds;
|
|
1032
|
-
const backoffExpires = Date.now() + this.addJitter(this.currentBackoffDelay);
|
|
1033
|
-
this.nextAvailableTime = new Date(Math.max(windowExpires, backoffExpires));
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
async recordJobCompletion() {}
|
|
1037
|
-
async getNextAvailableTime() {
|
|
1038
|
-
this.removeOldRequests();
|
|
1039
|
-
return new Date(Math.max(Date.now(), this.nextAvailableTime.getTime()));
|
|
1040
|
-
}
|
|
1041
|
-
async setNextAvailableTime(date) {
|
|
1042
|
-
if (date.getTime() > this.nextAvailableTime.getTime()) {
|
|
1043
|
-
this.nextAvailableTime = date;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
async clear() {
|
|
1047
|
-
this.requests = [];
|
|
1048
|
-
this.nextAvailableTime = new Date;
|
|
1049
|
-
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
// src/limiter/PostgresRateLimiter.ts
|
|
1053
|
-
import { createServiceToken as createServiceToken6 } from "@workglow/util";
|
|
1054
|
-
var POSTGRES_JOB_RATE_LIMITER = createServiceToken6("jobqueue.limiter.rate.postgres");
|
|
1055
|
-
|
|
1056
|
-
class PostgresRateLimiter {
|
|
1057
|
-
db;
|
|
1058
|
-
queueName;
|
|
1059
|
-
windowSizeInMilliseconds;
|
|
1060
|
-
currentBackoffDelay;
|
|
1061
|
-
constructor(db, queueName, {
|
|
1255
|
+
constructor(storage, queueName, {
|
|
1062
1256
|
maxExecutions,
|
|
1063
1257
|
windowSizeInSeconds,
|
|
1064
1258
|
initialBackoffDelay = 1000,
|
|
1065
1259
|
backoffMultiplier = 2,
|
|
1066
1260
|
maxBackoffDelay = 600000
|
|
1067
1261
|
}) {
|
|
1068
|
-
this.
|
|
1262
|
+
this.storage = storage;
|
|
1069
1263
|
this.queueName = queueName;
|
|
1070
1264
|
if (maxExecutions <= 0) {
|
|
1071
1265
|
throw new Error("maxExecutions must be greater than 0");
|
|
@@ -1088,281 +1282,87 @@ class PostgresRateLimiter {
|
|
|
1088
1282
|
this.backoffMultiplier = backoffMultiplier;
|
|
1089
1283
|
this.maxBackoffDelay = maxBackoffDelay;
|
|
1090
1284
|
this.currentBackoffDelay = initialBackoffDelay;
|
|
1091
|
-
this.dbPromise = this.ensureTableExists();
|
|
1092
1285
|
}
|
|
1093
|
-
maxExecutions;
|
|
1094
|
-
initialBackoffDelay;
|
|
1095
|
-
backoffMultiplier;
|
|
1096
|
-
maxBackoffDelay;
|
|
1097
|
-
dbPromise;
|
|
1098
1286
|
addJitter(base) {
|
|
1099
1287
|
return base + Math.random() * base;
|
|
1100
1288
|
}
|
|
1101
1289
|
increaseBackoff() {
|
|
1102
1290
|
this.currentBackoffDelay = Math.min(this.currentBackoffDelay * this.backoffMultiplier, this.maxBackoffDelay);
|
|
1103
1291
|
}
|
|
1104
|
-
async ensureTableExists() {
|
|
1105
|
-
await this.db.query(`
|
|
1106
|
-
CREATE TABLE IF NOT EXISTS job_queue_execution_tracking (
|
|
1107
|
-
id SERIAL PRIMARY KEY,
|
|
1108
|
-
queue_name TEXT NOT NULL,
|
|
1109
|
-
executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
1110
|
-
)
|
|
1111
|
-
`);
|
|
1112
|
-
await this.db.query(`
|
|
1113
|
-
CREATE TABLE IF NOT EXISTS job_queue_next_available (
|
|
1114
|
-
queue_name TEXT PRIMARY KEY,
|
|
1115
|
-
next_available_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
1116
|
-
)
|
|
1117
|
-
`);
|
|
1118
|
-
}
|
|
1119
|
-
async clear() {
|
|
1120
|
-
await this.dbPromise;
|
|
1121
|
-
await this.db.query(`DELETE FROM job_queue_execution_tracking WHERE queue_name = $1`, [
|
|
1122
|
-
this.queueName
|
|
1123
|
-
]);
|
|
1124
|
-
await this.db.query(`DELETE FROM job_queue_next_available WHERE queue_name = $1`, [
|
|
1125
|
-
this.queueName
|
|
1126
|
-
]);
|
|
1127
|
-
}
|
|
1128
1292
|
async canProceed() {
|
|
1129
|
-
|
|
1130
|
-
const
|
|
1131
|
-
SELECT next_available_at
|
|
1132
|
-
FROM job_queue_next_available
|
|
1133
|
-
WHERE queue_name = $1
|
|
1134
|
-
`, [this.queueName]);
|
|
1135
|
-
const nextAvailableTime = nextAvailableResult.rows[0]?.next_available_at;
|
|
1136
|
-
if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {
|
|
1137
|
-
this.increaseBackoff();
|
|
1138
|
-
return false;
|
|
1139
|
-
}
|
|
1140
|
-
const result = await this.db.query(`
|
|
1141
|
-
SELECT
|
|
1142
|
-
COUNT(*) AS attempt_count
|
|
1143
|
-
FROM job_queue_execution_tracking
|
|
1144
|
-
WHERE
|
|
1145
|
-
queue_name = $1
|
|
1146
|
-
AND executed_at > $2
|
|
1147
|
-
`, [this.queueName, new Date(Date.now() - this.windowSizeInMilliseconds).toISOString()]);
|
|
1148
|
-
const attemptCount = result.rows[0]?.attempt_count;
|
|
1293
|
+
const windowStartTime = new Date(Date.now() - this.windowSizeInMilliseconds).toISOString();
|
|
1294
|
+
const attemptCount = await this.storage.getExecutionCount(this.queueName, windowStartTime);
|
|
1149
1295
|
const canProceedNow = attemptCount < this.maxExecutions;
|
|
1150
|
-
if (
|
|
1151
|
-
this.
|
|
1152
|
-
|
|
1296
|
+
if (canProceedNow) {
|
|
1297
|
+
const nextAvailableTime2 = await this.storage.getNextAvailableTime(this.queueName);
|
|
1298
|
+
if (nextAvailableTime2 && new Date(nextAvailableTime2).getTime() > Date.now()) {
|
|
1299
|
+
const pastTime = new Date(Date.now() - 1000);
|
|
1300
|
+
await this.storage.setNextAvailableTime(this.queueName, pastTime.toISOString());
|
|
1301
|
+
}
|
|
1153
1302
|
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1303
|
+
return true;
|
|
1154
1304
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
async recordJobStart() {
|
|
1158
|
-
await this.dbPromise;
|
|
1159
|
-
await this.db.query(`
|
|
1160
|
-
INSERT INTO job_queue_execution_tracking (queue_name)
|
|
1161
|
-
VALUES ($1)
|
|
1162
|
-
`, [this.queueName]);
|
|
1163
|
-
const result = await this.db.query(`
|
|
1164
|
-
SELECT COUNT(*) AS attempt_count
|
|
1165
|
-
FROM job_queue_execution_tracking
|
|
1166
|
-
WHERE queue_name = $1
|
|
1167
|
-
`, [this.queueName]);
|
|
1168
|
-
if (result.rows[0].attempt_count >= this.maxExecutions) {
|
|
1169
|
-
const backoffExpires = new Date(Date.now() + this.addJitter(this.currentBackoffDelay));
|
|
1170
|
-
await this.setNextAvailableTime(backoffExpires);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
async recordJobCompletion() {}
|
|
1174
|
-
async getNextAvailableTime() {
|
|
1175
|
-
await this.dbPromise;
|
|
1176
|
-
const result = await this.db.query(`
|
|
1177
|
-
SELECT executed_at
|
|
1178
|
-
FROM job_queue_execution_tracking
|
|
1179
|
-
WHERE queue_name = $1
|
|
1180
|
-
ORDER BY executed_at ASC
|
|
1181
|
-
LIMIT 1 OFFSET $2
|
|
1182
|
-
`, [this.queueName, this.maxExecutions - 1]);
|
|
1183
|
-
const oldestExecution = result.rows[0];
|
|
1184
|
-
let rateLimitedTime = new Date;
|
|
1185
|
-
if (oldestExecution) {
|
|
1186
|
-
rateLimitedTime = new Date(oldestExecution.executed_at);
|
|
1187
|
-
rateLimitedTime.setSeconds(rateLimitedTime.getSeconds() + this.windowSizeInMilliseconds / 1000);
|
|
1188
|
-
}
|
|
1189
|
-
const nextAvailableResult = await this.db.query(`
|
|
1190
|
-
SELECT next_available_at
|
|
1191
|
-
FROM job_queue_next_available
|
|
1192
|
-
WHERE queue_name = $1
|
|
1193
|
-
`, [this.queueName]);
|
|
1194
|
-
let nextAvailableTime = new Date;
|
|
1195
|
-
if (nextAvailableResult?.rows[0]?.next_available_at) {
|
|
1196
|
-
nextAvailableTime = new Date(nextAvailableResult.rows[0].next_available_at);
|
|
1197
|
-
}
|
|
1198
|
-
return nextAvailableTime > rateLimitedTime ? nextAvailableTime : rateLimitedTime;
|
|
1199
|
-
}
|
|
1200
|
-
async setNextAvailableTime(date) {
|
|
1201
|
-
await this.dbPromise;
|
|
1202
|
-
await this.db.query(`
|
|
1203
|
-
INSERT INTO job_queue_next_available (queue_name, next_available_at)
|
|
1204
|
-
VALUES ($1, $2)
|
|
1205
|
-
ON CONFLICT (queue_name)
|
|
1206
|
-
DO UPDATE SET next_available_at = EXCLUDED.next_available_at;
|
|
1207
|
-
`, [this.queueName, date.toISOString()]);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
// src/limiter/SqliteRateLimiter.ts
|
|
1211
|
-
import { createServiceToken as createServiceToken7, toSQLiteTimestamp } from "@workglow/util";
|
|
1212
|
-
var SQLITE_JOB_RATE_LIMITER = createServiceToken7("jobqueue.limiter.rate.sqlite");
|
|
1213
|
-
|
|
1214
|
-
class SqliteRateLimiter {
|
|
1215
|
-
db;
|
|
1216
|
-
queueName;
|
|
1217
|
-
windowSizeInMilliseconds;
|
|
1218
|
-
currentBackoffDelay;
|
|
1219
|
-
constructor(db, queueName, {
|
|
1220
|
-
maxExecutions,
|
|
1221
|
-
windowSizeInSeconds,
|
|
1222
|
-
initialBackoffDelay = 1000,
|
|
1223
|
-
backoffMultiplier = 2,
|
|
1224
|
-
maxBackoffDelay = 600000
|
|
1225
|
-
}) {
|
|
1226
|
-
if (maxExecutions <= 0) {
|
|
1227
|
-
throw new Error("maxExecutions must be greater than 0");
|
|
1228
|
-
}
|
|
1229
|
-
if (windowSizeInSeconds <= 0) {
|
|
1230
|
-
throw new Error("windowSizeInSeconds must be greater than 0");
|
|
1231
|
-
}
|
|
1232
|
-
if (initialBackoffDelay <= 0) {
|
|
1233
|
-
throw new Error("initialBackoffDelay must be greater than 0");
|
|
1234
|
-
}
|
|
1235
|
-
if (backoffMultiplier <= 1) {
|
|
1236
|
-
throw new Error("backoffMultiplier must be greater than 1");
|
|
1237
|
-
}
|
|
1238
|
-
if (maxBackoffDelay <= initialBackoffDelay) {
|
|
1239
|
-
throw new Error("maxBackoffDelay must be greater than initialBackoffDelay");
|
|
1240
|
-
}
|
|
1241
|
-
this.db = db;
|
|
1242
|
-
this.queueName = queueName;
|
|
1243
|
-
this.windowSizeInMilliseconds = windowSizeInSeconds * 1000;
|
|
1244
|
-
this.maxExecutions = maxExecutions;
|
|
1245
|
-
this.initialBackoffDelay = initialBackoffDelay;
|
|
1246
|
-
this.backoffMultiplier = backoffMultiplier;
|
|
1247
|
-
this.maxBackoffDelay = maxBackoffDelay;
|
|
1248
|
-
this.currentBackoffDelay = initialBackoffDelay;
|
|
1249
|
-
this.ensureTableExists();
|
|
1250
|
-
}
|
|
1251
|
-
maxExecutions;
|
|
1252
|
-
initialBackoffDelay;
|
|
1253
|
-
backoffMultiplier;
|
|
1254
|
-
maxBackoffDelay;
|
|
1255
|
-
addJitter(base) {
|
|
1256
|
-
return base + Math.random() * base;
|
|
1257
|
-
}
|
|
1258
|
-
increaseBackoff() {
|
|
1259
|
-
this.currentBackoffDelay = Math.min(this.currentBackoffDelay * this.backoffMultiplier, this.maxBackoffDelay);
|
|
1260
|
-
}
|
|
1261
|
-
ensureTableExists() {
|
|
1262
|
-
this.db.exec(`
|
|
1263
|
-
CREATE TABLE IF NOT EXISTS job_queue_execution_tracking (
|
|
1264
|
-
id INTEGER PRIMARY KEY,
|
|
1265
|
-
queue_name TEXT NOT NULL,
|
|
1266
|
-
executed_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
1267
|
-
)
|
|
1268
|
-
`);
|
|
1269
|
-
this.db.exec(`
|
|
1270
|
-
CREATE TABLE IF NOT EXISTS job_queue_next_available (
|
|
1271
|
-
queue_name TEXT PRIMARY KEY,
|
|
1272
|
-
next_available_at TEXT
|
|
1273
|
-
)
|
|
1274
|
-
`);
|
|
1275
|
-
return this;
|
|
1276
|
-
}
|
|
1277
|
-
async clear() {
|
|
1278
|
-
this.db.prepare("DELETE FROM job_queue_execution_tracking WHERE queue_name = ?").run(this.queueName);
|
|
1279
|
-
this.db.prepare("DELETE FROM job_queue_next_available WHERE queue_name = ?").run(this.queueName);
|
|
1280
|
-
}
|
|
1281
|
-
async canProceed() {
|
|
1282
|
-
const nextAvailableTimeStmt = this.db.prepare(`
|
|
1283
|
-
SELECT next_available_at
|
|
1284
|
-
FROM job_queue_next_available
|
|
1285
|
-
WHERE queue_name = ?`);
|
|
1286
|
-
const nextAvailableResult = nextAvailableTimeStmt.get(this.queueName);
|
|
1287
|
-
if (nextAvailableResult && new Date(nextAvailableResult.next_available_at + "Z").getTime() > Date.now()) {
|
|
1305
|
+
const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);
|
|
1306
|
+
if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {
|
|
1288
1307
|
this.increaseBackoff();
|
|
1289
1308
|
return false;
|
|
1290
1309
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
FROM job_queue_execution_tracking
|
|
1294
|
-
WHERE queue_name = ? AND executed_at > ?`).get(this.queueName, thresholdTime);
|
|
1295
|
-
const canProceedNow = result.attempt_count < this.maxExecutions;
|
|
1296
|
-
if (!canProceedNow) {
|
|
1297
|
-
this.increaseBackoff();
|
|
1298
|
-
} else {
|
|
1299
|
-
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1300
|
-
}
|
|
1301
|
-
return canProceedNow;
|
|
1310
|
+
this.increaseBackoff();
|
|
1311
|
+
return false;
|
|
1302
1312
|
}
|
|
1303
1313
|
async recordJobStart() {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
WHERE queue_name = ?`).get(this.queueName);
|
|
1309
|
-
if (result.attempt_count >= this.maxExecutions) {
|
|
1314
|
+
await this.storage.recordExecution(this.queueName);
|
|
1315
|
+
const windowStartTime = new Date(Date.now() - this.windowSizeInMilliseconds).toISOString();
|
|
1316
|
+
const attemptCount = await this.storage.getExecutionCount(this.queueName, windowStartTime);
|
|
1317
|
+
if (attemptCount >= this.maxExecutions) {
|
|
1310
1318
|
const backoffExpires = new Date(Date.now() + this.addJitter(this.currentBackoffDelay));
|
|
1311
1319
|
await this.setNextAvailableTime(backoffExpires);
|
|
1320
|
+
} else {
|
|
1321
|
+
const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);
|
|
1322
|
+
if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {
|
|
1323
|
+
const pastTime = new Date(Date.now() - 1000);
|
|
1324
|
+
await this.storage.setNextAvailableTime(this.queueName, pastTime.toISOString());
|
|
1325
|
+
}
|
|
1312
1326
|
}
|
|
1313
1327
|
}
|
|
1314
1328
|
async recordJobCompletion() {}
|
|
1315
1329
|
async getNextAvailableTime() {
|
|
1316
|
-
const
|
|
1317
|
-
SELECT executed_at
|
|
1318
|
-
FROM job_queue_execution_tracking
|
|
1319
|
-
WHERE queue_name = ?
|
|
1320
|
-
ORDER BY executed_at ASC
|
|
1321
|
-
LIMIT 1 OFFSET ?`);
|
|
1322
|
-
const oldestExecution = rateLimitedTimeStmt.get(this.queueName, this.maxExecutions - 1);
|
|
1330
|
+
const oldestExecution = await this.storage.getOldestExecutionAtOffset(this.queueName, this.maxExecutions - 1);
|
|
1323
1331
|
let rateLimitedTime = new Date;
|
|
1324
1332
|
if (oldestExecution) {
|
|
1325
|
-
rateLimitedTime = new Date(oldestExecution
|
|
1333
|
+
rateLimitedTime = new Date(oldestExecution);
|
|
1326
1334
|
rateLimitedTime.setSeconds(rateLimitedTime.getSeconds() + this.windowSizeInMilliseconds / 1000);
|
|
1327
1335
|
}
|
|
1328
|
-
const
|
|
1329
|
-
SELECT next_available_at
|
|
1330
|
-
FROM job_queue_next_available
|
|
1331
|
-
WHERE queue_name = ?`);
|
|
1332
|
-
const nextAvailableResult = nextAvailableStmt.get(this.queueName);
|
|
1336
|
+
const nextAvailableStr = await this.storage.getNextAvailableTime(this.queueName);
|
|
1333
1337
|
let nextAvailableTime = new Date;
|
|
1334
|
-
if (
|
|
1335
|
-
nextAvailableTime = new Date(
|
|
1338
|
+
if (nextAvailableStr) {
|
|
1339
|
+
nextAvailableTime = new Date(nextAvailableStr);
|
|
1336
1340
|
}
|
|
1337
1341
|
return nextAvailableTime > rateLimitedTime ? nextAvailableTime : rateLimitedTime;
|
|
1338
1342
|
}
|
|
1339
1343
|
async setNextAvailableTime(date) {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1344
|
+
await this.storage.setNextAvailableTime(this.queueName, date.toISOString());
|
|
1345
|
+
}
|
|
1346
|
+
async clear() {
|
|
1347
|
+
await this.storage.clear(this.queueName);
|
|
1348
|
+
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1345
1349
|
}
|
|
1346
1350
|
}
|
|
1347
1351
|
export {
|
|
1348
|
-
SqliteRateLimiter,
|
|
1349
|
-
SQLITE_JOB_RATE_LIMITER,
|
|
1350
1352
|
RetryableJobError,
|
|
1351
|
-
|
|
1352
|
-
PostgresRateLimiter,
|
|
1353
|
+
RateLimiter,
|
|
1353
1354
|
PermanentJobError,
|
|
1354
|
-
POSTGRES_JOB_RATE_LIMITER,
|
|
1355
1355
|
NullLimiter,
|
|
1356
1356
|
NULL_JOB_LIMITER,
|
|
1357
|
-
MEMORY_JOB_RATE_LIMITER,
|
|
1358
1357
|
JobStatus,
|
|
1359
|
-
|
|
1358
|
+
JobQueueWorker,
|
|
1359
|
+
JobQueueServer,
|
|
1360
|
+
JobQueueClient,
|
|
1360
1361
|
JobNotFoundError,
|
|
1361
1362
|
JobError,
|
|
1362
1363
|
JobDisabledError,
|
|
1363
1364
|
Job,
|
|
1364
1365
|
JOB_LIMITER,
|
|
1365
|
-
InMemoryRateLimiter,
|
|
1366
1366
|
EvenlySpacedRateLimiter,
|
|
1367
1367
|
EVENLY_SPACED_JOB_RATE_LIMITER,
|
|
1368
1368
|
DelayLimiter,
|
|
@@ -1372,4 +1372,4 @@ export {
|
|
|
1372
1372
|
AbortSignalJobError
|
|
1373
1373
|
};
|
|
1374
1374
|
|
|
1375
|
-
//# debugId=
|
|
1375
|
+
//# debugId=FFD59B0DF6C7A03464756E2164756E21
|