@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/browser.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
|
-
this.storage = globalServiceRegistry.get(QUEUE_STORAGE);
|
|
196
|
-
} catch (err) {
|
|
197
|
-
console.warn("Warning: did not find queue storage in global DI", err);
|
|
198
|
-
this.storage = new InMemoryQueueStorage(queueName);
|
|
199
|
-
}
|
|
165
|
+
}
|
|
166
|
+
connect() {
|
|
167
|
+
if (this.server) {
|
|
168
|
+
return;
|
|
200
169
|
}
|
|
201
|
-
this.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
170
|
+
if (this.storageUnsubscribe) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
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,77 +284,246 @@ class JobQueue {
|
|
|
330
284
|
}
|
|
331
285
|
};
|
|
332
286
|
}
|
|
333
|
-
|
|
287
|
+
on(event, listener) {
|
|
288
|
+
this.events.on(event, listener);
|
|
289
|
+
}
|
|
290
|
+
off(event, listener) {
|
|
291
|
+
this.events.off(event, listener);
|
|
292
|
+
}
|
|
293
|
+
once(event, listener) {
|
|
294
|
+
this.events.once(event, listener);
|
|
295
|
+
}
|
|
296
|
+
waitOn(event) {
|
|
297
|
+
return this.events.waitOn(event);
|
|
298
|
+
}
|
|
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);
|
|
306
|
+
}
|
|
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));
|
|
312
|
+
}
|
|
313
|
+
this.cleanupJob(jobId);
|
|
314
|
+
}
|
|
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));
|
|
321
|
+
}
|
|
322
|
+
this.cleanupJob(jobId);
|
|
323
|
+
}
|
|
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")));
|
|
329
|
+
}
|
|
330
|
+
this.cleanupJob(jobId);
|
|
331
|
+
}
|
|
332
|
+
handleJobRetry(jobId, runAfter) {
|
|
333
|
+
this.events.emit("job_retry", this.queueName, jobId, runAfter);
|
|
334
|
+
}
|
|
335
|
+
handleJobProgress(jobId, progress, message, details) {
|
|
336
|
+
this.lastKnownProgress.set(jobId, { progress, message, details });
|
|
337
|
+
this.events.emit("job_progress", this.queueName, jobId, progress, message, details);
|
|
338
|
+
const listeners = this.jobProgressListeners.get(jobId);
|
|
339
|
+
if (listeners) {
|
|
340
|
+
for (const listener of listeners) {
|
|
341
|
+
listener(progress, message, details);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
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
|
+
}
|
|
387
|
+
storageToClass(details) {
|
|
388
|
+
const toDate = (date) => {
|
|
389
|
+
if (!date)
|
|
390
|
+
return null;
|
|
391
|
+
const d = new Date(date);
|
|
392
|
+
return isNaN(d.getTime()) ? null : d;
|
|
393
|
+
};
|
|
394
|
+
return new Job({
|
|
395
|
+
id: details.id,
|
|
396
|
+
jobRunId: details.job_run_id,
|
|
397
|
+
queueName: details.queue,
|
|
398
|
+
fingerprint: details.fingerprint,
|
|
399
|
+
input: details.input,
|
|
400
|
+
output: details.output,
|
|
401
|
+
runAfter: toDate(details.run_after),
|
|
402
|
+
createdAt: toDate(details.created_at),
|
|
403
|
+
deadlineAt: toDate(details.deadline_at),
|
|
404
|
+
lastRanAt: toDate(details.last_ran_at),
|
|
405
|
+
completedAt: toDate(details.completed_at),
|
|
406
|
+
progress: details.progress || 0,
|
|
407
|
+
progressMessage: details.progress_message || "",
|
|
408
|
+
progressDetails: details.progress_details ?? null,
|
|
409
|
+
status: details.status,
|
|
410
|
+
error: details.error ?? null,
|
|
411
|
+
errorCode: details.error_code ?? null,
|
|
412
|
+
runAttempts: details.run_attempts ?? 0,
|
|
413
|
+
maxRetries: details.max_retries ?? 10
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
buildErrorFromJob(job) {
|
|
417
|
+
return this.buildErrorFromCode(job.error || "Job failed", job.errorCode ?? undefined);
|
|
418
|
+
}
|
|
419
|
+
buildErrorFromCode(message, errorCode) {
|
|
420
|
+
if (errorCode === "PermanentJobError") {
|
|
421
|
+
return new PermanentJobError(message);
|
|
422
|
+
}
|
|
423
|
+
if (errorCode === "RetryableJobError") {
|
|
424
|
+
return new RetryableJobError(message);
|
|
425
|
+
}
|
|
426
|
+
if (errorCode === "AbortSignalJobError") {
|
|
427
|
+
return new AbortSignalJobError(message);
|
|
428
|
+
}
|
|
429
|
+
if (errorCode === "JobDisabledError") {
|
|
430
|
+
return new JobDisabledError(message);
|
|
431
|
+
}
|
|
432
|
+
return new JobError(message);
|
|
433
|
+
}
|
|
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() {
|
|
334
479
|
if (this.running) {
|
|
335
480
|
return this;
|
|
336
481
|
}
|
|
337
|
-
this.mode = mode;
|
|
338
482
|
this.running = true;
|
|
339
|
-
this.events.emit("
|
|
340
|
-
|
|
341
|
-
await this.fixupJobs();
|
|
342
|
-
await this.processJobs();
|
|
343
|
-
}
|
|
344
|
-
if (this.mode === "CLIENT" /* CLIENT */ || this.mode === "BOTH" /* BOTH */) {
|
|
345
|
-
await this.monitorJobs();
|
|
346
|
-
}
|
|
483
|
+
this.events.emit("worker_start");
|
|
484
|
+
this.processJobs();
|
|
347
485
|
return this;
|
|
348
486
|
}
|
|
349
487
|
async stop() {
|
|
350
|
-
if (!this.running)
|
|
488
|
+
if (!this.running) {
|
|
351
489
|
return this;
|
|
490
|
+
}
|
|
352
491
|
this.running = false;
|
|
353
|
-
const size = await this.size(
|
|
492
|
+
const size = await this.storage.size(JobStatus3.PROCESSING);
|
|
354
493
|
const sleepTime = Math.max(100, size * 2);
|
|
355
494
|
await sleep(sleepTime);
|
|
356
|
-
for (const
|
|
357
|
-
|
|
495
|
+
for (const controller of this.activeJobAbortControllers.values()) {
|
|
496
|
+
if (!controller.signal.aborted) {
|
|
497
|
+
controller.abort();
|
|
498
|
+
}
|
|
358
499
|
}
|
|
359
|
-
this.activeJobPromises.forEach((promises) => promises.forEach(({ reject }) => reject(new PermanentJobError("Queue Stopped"))));
|
|
360
500
|
await sleep(sleepTime);
|
|
361
|
-
this.events.emit("
|
|
501
|
+
this.events.emit("worker_stop");
|
|
362
502
|
return this;
|
|
363
503
|
}
|
|
364
|
-
async
|
|
365
|
-
await this.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
abortedJobs: 0,
|
|
376
|
-
retriedJobs: 0,
|
|
377
|
-
disabledJobs: 0,
|
|
378
|
-
lastUpdateTime: new Date
|
|
379
|
-
};
|
|
380
|
-
this.emitStatsUpdate();
|
|
381
|
-
return this;
|
|
504
|
+
async processNext() {
|
|
505
|
+
const canProceed = await this.limiter.canProceed();
|
|
506
|
+
if (!canProceed) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
const job = await this.next();
|
|
510
|
+
if (!job) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
await this.processSingleJob(job);
|
|
514
|
+
return true;
|
|
382
515
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
await this.clear();
|
|
386
|
-
await this.start();
|
|
387
|
-
return this;
|
|
516
|
+
isRunning() {
|
|
517
|
+
return this.running;
|
|
388
518
|
}
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
}));
|
|
519
|
+
getActiveJobCount() {
|
|
520
|
+
return this.activeJobAbortControllers.size;
|
|
398
521
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return
|
|
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;
|
|
404
527
|
}
|
|
405
528
|
on(event, listener) {
|
|
406
529
|
this.events.on(event, listener);
|
|
@@ -408,35 +531,144 @@ class JobQueue {
|
|
|
408
531
|
off(event, listener) {
|
|
409
532
|
this.events.off(event, listener);
|
|
410
533
|
}
|
|
411
|
-
|
|
412
|
-
this.
|
|
534
|
+
async next() {
|
|
535
|
+
const job = await this.storage.next(this.workerId);
|
|
536
|
+
if (!job)
|
|
537
|
+
return;
|
|
538
|
+
return this.storageToClass(job);
|
|
413
539
|
}
|
|
414
|
-
|
|
415
|
-
|
|
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
|
+
}
|
|
416
558
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async processSingleJob(job) {
|
|
569
|
+
if (!job || !job.id) {
|
|
570
|
+
throw new JobNotFoundError("Invalid job provided for processing");
|
|
571
|
+
}
|
|
572
|
+
const startTime = Date.now();
|
|
573
|
+
try {
|
|
574
|
+
await this.validateJobState(job);
|
|
575
|
+
await this.limiter.recordJobStart();
|
|
576
|
+
const abortController = this.createAbortController(job.id);
|
|
577
|
+
this.events.emit("job_start", job.id);
|
|
578
|
+
const output = await this.executeJob(job, abortController.signal);
|
|
579
|
+
await this.completeJob(job, output);
|
|
580
|
+
this.processingTimes.set(job.id, Date.now() - startTime);
|
|
581
|
+
} catch (err) {
|
|
582
|
+
const error = this.normalizeError(err);
|
|
583
|
+
if (error instanceof RetryableJobError) {
|
|
584
|
+
if (job.runAttempts > job.maxRetries) {
|
|
585
|
+
await this.failJob(job, error);
|
|
586
|
+
} else {
|
|
587
|
+
await this.rescheduleJob(job, error.retryDate);
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
await this.failJob(job, error);
|
|
591
|
+
}
|
|
592
|
+
} finally {
|
|
593
|
+
await this.limiter.recordJobCompletion();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
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);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
async failJob(job, error) {
|
|
628
|
+
try {
|
|
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
|
+
}
|
|
440
672
|
}
|
|
441
673
|
createAbortController(jobId) {
|
|
442
674
|
if (!jobId)
|
|
@@ -449,20 +681,35 @@ class JobQueue {
|
|
|
449
681
|
this.activeJobAbortControllers.set(jobId, abortController);
|
|
450
682
|
return abortController;
|
|
451
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
|
+
}
|
|
452
699
|
async validateJobState(job) {
|
|
453
|
-
if (job.status ===
|
|
700
|
+
if (job.status === JobStatus3.COMPLETED) {
|
|
454
701
|
throw new PermanentJobError(`Job ${job.id} is already completed`);
|
|
455
702
|
}
|
|
456
|
-
if (job.status ===
|
|
703
|
+
if (job.status === JobStatus3.FAILED) {
|
|
457
704
|
throw new PermanentJobError(`Job ${job.id} has failed`);
|
|
458
705
|
}
|
|
459
|
-
if (job.status ===
|
|
706
|
+
if (job.status === JobStatus3.ABORTING || this.activeJobAbortControllers.get(job.id)?.signal.aborted) {
|
|
460
707
|
throw new AbortSignalJobError(`Job ${job.id} is being aborted`);
|
|
461
708
|
}
|
|
462
709
|
if (job.deadlineAt && job.deadlineAt < new Date) {
|
|
463
710
|
throw new PermanentJobError(`Job ${job.id} has exceeded its deadline`);
|
|
464
711
|
}
|
|
465
|
-
if (job.status ===
|
|
712
|
+
if (job.status === JobStatus3.DISABLED) {
|
|
466
713
|
throw new JobDisabledError(`Job ${job.id} has been disabled`);
|
|
467
714
|
}
|
|
468
715
|
}
|
|
@@ -470,34 +717,13 @@ class JobQueue {
|
|
|
470
717
|
if (err instanceof JobError) {
|
|
471
718
|
return err;
|
|
472
719
|
}
|
|
473
|
-
if (err instanceof Error) {
|
|
474
|
-
return err;
|
|
475
|
-
}
|
|
476
|
-
return new PermanentJobError(String(err));
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (times.length > 0) {
|
|
481
|
-
this.stats.averageProcessingTime = times.reduce((a, b) => a + b, 0) / times.length;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
emitStatsUpdate() {
|
|
485
|
-
this.stats.lastUpdateTime = new Date;
|
|
486
|
-
this.events.emit("queue_stats_update", this.queueName, { ...this.stats });
|
|
487
|
-
}
|
|
488
|
-
announceProgress(jobId, progress, message, details) {
|
|
489
|
-
this.lastKnownProgress.set(jobId, {
|
|
490
|
-
progress,
|
|
491
|
-
message,
|
|
492
|
-
details
|
|
493
|
-
});
|
|
494
|
-
this.events.emit("job_progress", this.queueName, jobId, progress, message, details);
|
|
495
|
-
const listeners = this.jobProgressListeners.get(jobId);
|
|
496
|
-
if (listeners) {
|
|
497
|
-
for (const listener of listeners) {
|
|
498
|
-
listener(progress, message, details);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
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);
|
|
501
727
|
}
|
|
502
728
|
storageToClass(details) {
|
|
503
729
|
const toDate = (date) => {
|
|
@@ -506,7 +732,7 @@ class JobQueue {
|
|
|
506
732
|
const d = new Date(date);
|
|
507
733
|
return isNaN(d.getTime()) ? null : d;
|
|
508
734
|
};
|
|
509
|
-
|
|
735
|
+
return new this.jobClass({
|
|
510
736
|
id: details.id,
|
|
511
737
|
jobRunId: details.job_run_id,
|
|
512
738
|
queueName: details.queue,
|
|
@@ -527,8 +753,6 @@ class JobQueue {
|
|
|
527
753
|
runAttempts: details.run_attempts ?? 0,
|
|
528
754
|
maxRetries: details.max_retries ?? 10
|
|
529
755
|
});
|
|
530
|
-
job.queue = this;
|
|
531
|
-
return job;
|
|
532
756
|
}
|
|
533
757
|
classToStorage(job) {
|
|
534
758
|
const dateToISOString = (date) => {
|
|
@@ -559,233 +783,293 @@ class JobQueue {
|
|
|
559
783
|
progress_details: job.progressDetails ?? null
|
|
560
784
|
};
|
|
561
785
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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;
|
|
575
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;
|
|
576
837
|
}
|
|
577
|
-
async
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
job.progress = 100;
|
|
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);
|
|
838
|
+
async stop() {
|
|
839
|
+
if (!this.running) {
|
|
840
|
+
return this;
|
|
595
841
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
job.progress = 100;
|
|
601
|
-
job.completedAt = new Date;
|
|
602
|
-
job.progressMessage = "";
|
|
603
|
-
job.progressDetails = null;
|
|
604
|
-
job.error = error.message;
|
|
605
|
-
job.errorCode = error?.constructor?.name ?? null;
|
|
606
|
-
await this.storage.complete(this.classToStorage(job));
|
|
607
|
-
if (this.options.deleteAfterFailureMs === 0) {
|
|
608
|
-
await this.delete(job.id);
|
|
609
|
-
}
|
|
610
|
-
this.stats.failedJobs++;
|
|
611
|
-
this.events.emit("job_error", this.queueName, job.id, `${error.name}: ${error.message}`);
|
|
612
|
-
const promises = this.activeJobPromises.get(job.id) || [];
|
|
613
|
-
promises.forEach(({ reject }) => reject(error));
|
|
614
|
-
this.activeJobPromises.delete(job.id);
|
|
615
|
-
} catch (err) {
|
|
616
|
-
console.error("failJob errored out?", err);
|
|
842
|
+
this.running = false;
|
|
843
|
+
if (this.cleanupTimer) {
|
|
844
|
+
clearTimeout(this.cleanupTimer);
|
|
845
|
+
this.cleanupTimer = null;
|
|
617
846
|
}
|
|
618
|
-
this.
|
|
619
|
-
this.
|
|
620
|
-
this
|
|
621
|
-
this.activeJobPromises.delete(job.id);
|
|
847
|
+
await Promise.all(this.workers.map((worker) => worker.stop()));
|
|
848
|
+
this.events.emit("server_stop", this.queueName);
|
|
849
|
+
return this;
|
|
622
850
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
job.status = JobStatus.COMPLETED;
|
|
626
|
-
job.progress = 100;
|
|
627
|
-
job.progressMessage = "";
|
|
628
|
-
job.progressDetails = null;
|
|
629
|
-
job.completedAt = new Date;
|
|
630
|
-
job.output = output ?? null;
|
|
631
|
-
job.error = null;
|
|
632
|
-
job.errorCode = null;
|
|
633
|
-
await this.storage.complete(this.classToStorage(job));
|
|
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));
|
|
642
|
-
}
|
|
643
|
-
this.activeJobPromises.delete(job.id);
|
|
644
|
-
} catch (err) {
|
|
645
|
-
console.error("completeJob errored out?", err);
|
|
646
|
-
}
|
|
647
|
-
this.activeJobAbortControllers.delete(job.id);
|
|
648
|
-
this.lastKnownProgress.delete(job.id);
|
|
649
|
-
this.jobProgressListeners.delete(job.id);
|
|
650
|
-
this.activeJobPromises.delete(job.id);
|
|
851
|
+
getStats() {
|
|
852
|
+
return { ...this.stats };
|
|
651
853
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
controller = this.createAbortController(jobId);
|
|
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");
|
|
659
860
|
}
|
|
660
|
-
|
|
661
|
-
|
|
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();
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} else if (count < currentCount) {
|
|
871
|
+
const toRemove = this.workers.splice(count);
|
|
872
|
+
await Promise.all(toRemove.map((worker) => worker.stop()));
|
|
662
873
|
}
|
|
663
|
-
this.events.emit("job_aborting", this.queueName, jobId);
|
|
664
874
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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);
|
|
675
897
|
}
|
|
676
|
-
this.stats.abortedJobs++;
|
|
677
898
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
this.
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
this.events.emit("job_start", this.queueName, job.id);
|
|
693
|
-
const output = await this.executeJob(job, abortController.signal);
|
|
694
|
-
await this.completeJob(job, output);
|
|
695
|
-
this.processingTimes.set(job.id, Date.now() - startTime);
|
|
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 };
|
|
696
913
|
this.updateAverageProcessingTime();
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (
|
|
700
|
-
|
|
701
|
-
await this.failJob(job, error);
|
|
702
|
-
} else {
|
|
703
|
-
await this.rescheduleJob(job, error.retryDate);
|
|
704
|
-
}
|
|
705
|
-
} else {
|
|
706
|
-
await this.failJob(job, error);
|
|
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);
|
|
707
918
|
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
this.
|
|
711
|
-
|
|
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;
|
|
712
946
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
await this.cleanUpJobs();
|
|
719
|
-
const canProceed = await this.limiter.canProceed();
|
|
720
|
-
if (canProceed) {
|
|
721
|
-
const job = await this.next();
|
|
722
|
-
if (job) {
|
|
723
|
-
this.processSingleJob(job);
|
|
724
|
-
}
|
|
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);
|
|
725
952
|
}
|
|
726
|
-
} finally {
|
|
727
|
-
setTimeout(() => this.processJobs(), this.options.waitDurationInMilliseconds);
|
|
728
953
|
}
|
|
729
954
|
}
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
for (const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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,26 +1238,28 @@ 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({
|
|
1254
|
+
constructor(storage, queueName, {
|
|
971
1255
|
maxExecutions,
|
|
972
1256
|
windowSizeInSeconds,
|
|
973
1257
|
initialBackoffDelay = 1000,
|
|
974
1258
|
backoffMultiplier = 2,
|
|
975
1259
|
maxBackoffDelay = 600000
|
|
976
1260
|
}) {
|
|
1261
|
+
this.storage = storage;
|
|
1262
|
+
this.queueName = queueName;
|
|
977
1263
|
if (maxExecutions <= 0) {
|
|
978
1264
|
throw new Error("maxExecutions must be greater than 0");
|
|
979
1265
|
}
|
|
@@ -989,80 +1275,93 @@ class InMemoryRateLimiter {
|
|
|
989
1275
|
if (maxBackoffDelay <= initialBackoffDelay) {
|
|
990
1276
|
throw new Error("maxBackoffDelay must be greater than initialBackoffDelay");
|
|
991
1277
|
}
|
|
992
|
-
this.maxExecutions = maxExecutions;
|
|
993
1278
|
this.windowSizeInMilliseconds = windowSizeInSeconds * 1000;
|
|
1279
|
+
this.maxExecutions = maxExecutions;
|
|
994
1280
|
this.initialBackoffDelay = initialBackoffDelay;
|
|
995
1281
|
this.backoffMultiplier = backoffMultiplier;
|
|
996
1282
|
this.maxBackoffDelay = maxBackoffDelay;
|
|
997
1283
|
this.currentBackoffDelay = initialBackoffDelay;
|
|
998
1284
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
-
}
|
|
1285
|
+
addJitter(base) {
|
|
1286
|
+
return base + Math.random() * base;
|
|
1006
1287
|
}
|
|
1007
1288
|
increaseBackoff() {
|
|
1008
1289
|
this.currentBackoffDelay = Math.min(this.currentBackoffDelay * this.backoffMultiplier, this.maxBackoffDelay);
|
|
1009
1290
|
}
|
|
1010
|
-
addJitter(base) {
|
|
1011
|
-
return base + Math.random() * base;
|
|
1012
|
-
}
|
|
1013
1291
|
async canProceed() {
|
|
1014
|
-
this.
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1292
|
+
const windowStartTime = new Date(Date.now() - this.windowSizeInMilliseconds).toISOString();
|
|
1293
|
+
const attemptCount = await this.storage.getExecutionCount(this.queueName, windowStartTime);
|
|
1294
|
+
const canProceedNow = attemptCount < this.maxExecutions;
|
|
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
|
+
}
|
|
1022
1301
|
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);
|
|
1305
|
+
if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {
|
|
1306
|
+
this.increaseBackoff();
|
|
1307
|
+
return false;
|
|
1023
1308
|
}
|
|
1024
|
-
|
|
1309
|
+
this.increaseBackoff();
|
|
1310
|
+
return false;
|
|
1025
1311
|
}
|
|
1026
1312
|
async recordJobStart() {
|
|
1027
|
-
this.
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
const backoffExpires = Date.now() + this.addJitter(this.currentBackoffDelay);
|
|
1032
|
-
this.
|
|
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) {
|
|
1317
|
+
const backoffExpires = new Date(Date.now() + this.addJitter(this.currentBackoffDelay));
|
|
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
|
+
}
|
|
1033
1325
|
}
|
|
1034
1326
|
}
|
|
1035
1327
|
async recordJobCompletion() {}
|
|
1036
1328
|
async getNextAvailableTime() {
|
|
1037
|
-
this.
|
|
1038
|
-
|
|
1329
|
+
const oldestExecution = await this.storage.getOldestExecutionAtOffset(this.queueName, this.maxExecutions - 1);
|
|
1330
|
+
let rateLimitedTime = new Date;
|
|
1331
|
+
if (oldestExecution) {
|
|
1332
|
+
rateLimitedTime = new Date(oldestExecution);
|
|
1333
|
+
rateLimitedTime.setSeconds(rateLimitedTime.getSeconds() + this.windowSizeInMilliseconds / 1000);
|
|
1334
|
+
}
|
|
1335
|
+
const nextAvailableStr = await this.storage.getNextAvailableTime(this.queueName);
|
|
1336
|
+
let nextAvailableTime = new Date;
|
|
1337
|
+
if (nextAvailableStr) {
|
|
1338
|
+
nextAvailableTime = new Date(nextAvailableStr);
|
|
1339
|
+
}
|
|
1340
|
+
return nextAvailableTime > rateLimitedTime ? nextAvailableTime : rateLimitedTime;
|
|
1039
1341
|
}
|
|
1040
1342
|
async setNextAvailableTime(date) {
|
|
1041
|
-
|
|
1042
|
-
this.nextAvailableTime = date;
|
|
1043
|
-
}
|
|
1343
|
+
await this.storage.setNextAvailableTime(this.queueName, date.toISOString());
|
|
1044
1344
|
}
|
|
1045
1345
|
async clear() {
|
|
1046
|
-
this.
|
|
1047
|
-
this.nextAvailableTime = new Date;
|
|
1346
|
+
await this.storage.clear(this.queueName);
|
|
1048
1347
|
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1049
1348
|
}
|
|
1050
1349
|
}
|
|
1051
1350
|
export {
|
|
1052
1351
|
RetryableJobError,
|
|
1053
|
-
|
|
1352
|
+
RateLimiter,
|
|
1054
1353
|
PermanentJobError,
|
|
1055
1354
|
NullLimiter,
|
|
1056
1355
|
NULL_JOB_LIMITER,
|
|
1057
|
-
MEMORY_JOB_RATE_LIMITER,
|
|
1058
1356
|
JobStatus,
|
|
1059
|
-
|
|
1357
|
+
JobQueueWorker,
|
|
1358
|
+
JobQueueServer,
|
|
1359
|
+
JobQueueClient,
|
|
1060
1360
|
JobNotFoundError,
|
|
1061
1361
|
JobError,
|
|
1062
1362
|
JobDisabledError,
|
|
1063
1363
|
Job,
|
|
1064
1364
|
JOB_LIMITER,
|
|
1065
|
-
InMemoryRateLimiter,
|
|
1066
1365
|
EvenlySpacedRateLimiter,
|
|
1067
1366
|
EVENLY_SPACED_JOB_RATE_LIMITER,
|
|
1068
1367
|
DelayLimiter,
|
|
@@ -1072,4 +1371,4 @@ export {
|
|
|
1072
1371
|
AbortSignalJobError
|
|
1073
1372
|
};
|
|
1074
1373
|
|
|
1075
|
-
//# debugId=
|
|
1374
|
+
//# debugId=98D28D3ECAF6920F64756E2164756E21
|