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