@workglow/job-queue 0.2.26 → 0.2.28
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/dist/browser.js +650 -73
- package/dist/browser.js.map +20 -15
- package/dist/bun.js +650 -73
- package/dist/bun.js.map +20 -15
- package/dist/common.d.ts +5 -0
- package/dist/common.d.ts.map +1 -1
- package/dist/job/Job.d.ts +1 -1
- package/dist/job/Job.d.ts.map +1 -1
- package/dist/job/JobQueueClient.d.ts +2 -1
- package/dist/job/JobQueueClient.d.ts.map +1 -1
- package/dist/job/JobQueueServer.d.ts +1 -1
- package/dist/job/JobQueueServer.d.ts.map +1 -1
- package/dist/job/JobQueueWorker.d.ts +8 -3
- package/dist/job/JobQueueWorker.d.ts.map +1 -1
- package/dist/job/JobStorageConverters.d.ts +1 -1
- package/dist/job/JobStorageConverters.d.ts.map +1 -1
- package/dist/limiter/CompositeLimiter.d.ts +17 -1
- package/dist/limiter/CompositeLimiter.d.ts.map +1 -1
- package/dist/limiter/ConcurrencyLimiter.d.ts +11 -1
- package/dist/limiter/ConcurrencyLimiter.d.ts.map +1 -1
- package/dist/limiter/DelayLimiter.d.ts +12 -1
- package/dist/limiter/DelayLimiter.d.ts.map +1 -1
- package/dist/limiter/EvenlySpacedRateLimiter.d.ts +18 -1
- package/dist/limiter/EvenlySpacedRateLimiter.d.ts.map +1 -1
- package/dist/limiter/ILimiter.d.ts +56 -0
- package/dist/limiter/ILimiter.d.ts.map +1 -1
- package/dist/limiter/NullLimiter.d.ts +6 -1
- package/dist/limiter/NullLimiter.d.ts.map +1 -1
- package/dist/limiter/RateLimiter.d.ts +43 -4
- package/dist/limiter/RateLimiter.d.ts.map +1 -1
- package/dist/node.js +650 -73
- package/dist/node.js.map +20 -15
- package/dist/queue-storage/IQueueStorage.d.ts +229 -0
- package/dist/queue-storage/IQueueStorage.d.ts.map +1 -0
- package/dist/queue-storage/InMemoryQueueStorage.d.ts +149 -0
- package/dist/queue-storage/InMemoryQueueStorage.d.ts.map +1 -0
- package/dist/queue-storage/TelemetryQueueStorage.d.ts +33 -0
- package/dist/queue-storage/TelemetryQueueStorage.d.ts.map +1 -0
- package/dist/rate-limiter-storage/IRateLimiterStorage.d.ts +127 -0
- package/dist/rate-limiter-storage/IRateLimiterStorage.d.ts.map +1 -0
- package/dist/rate-limiter-storage/InMemoryRateLimiterStorage.d.ts +43 -0
- package/dist/rate-limiter-storage/InMemoryRateLimiterStorage.d.ts.map +1 -0
- package/package.json +3 -8
package/dist/bun.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/
|
|
3
|
-
import {
|
|
2
|
+
// src/queue-storage/IQueueStorage.ts
|
|
3
|
+
import { createServiceToken } from "@workglow/util";
|
|
4
|
+
var QUEUE_STORAGE = createServiceToken("jobqueue.storage");
|
|
5
|
+
var JobStatus = {
|
|
6
|
+
PENDING: "PENDING",
|
|
7
|
+
PROCESSING: "PROCESSING",
|
|
8
|
+
COMPLETED: "COMPLETED",
|
|
9
|
+
ABORTING: "ABORTING",
|
|
10
|
+
FAILED: "FAILED",
|
|
11
|
+
DISABLED: "DISABLED"
|
|
12
|
+
};
|
|
4
13
|
|
|
5
14
|
// src/job/JobError.ts
|
|
6
15
|
import { BaseError } from "@workglow/util";
|
|
@@ -176,7 +185,6 @@ ${fullMessage}`;
|
|
|
176
185
|
}
|
|
177
186
|
|
|
178
187
|
// src/job/JobQueueClient.ts
|
|
179
|
-
import { JobStatus as JobStatus2 } from "@workglow/storage";
|
|
180
188
|
import { EventEmitter } from "@workglow/util";
|
|
181
189
|
|
|
182
190
|
// src/job/JobStorageConverters.ts
|
|
@@ -301,7 +309,7 @@ class JobQueueClient {
|
|
|
301
309
|
run_after: options?.runAfter?.toISOString() ?? new Date().toISOString(),
|
|
302
310
|
deadline_at: options?.deadlineAt?.toISOString() ?? null,
|
|
303
311
|
completed_at: null,
|
|
304
|
-
status:
|
|
312
|
+
status: JobStatus.PENDING
|
|
305
313
|
};
|
|
306
314
|
const id = await this.storage.add(job);
|
|
307
315
|
this.server?.handleJobAdded(id);
|
|
@@ -354,15 +362,15 @@ class JobQueueClient {
|
|
|
354
362
|
this.removePromise(jobId, resolve, reject);
|
|
355
363
|
throw new JobNotFoundError(`Job ${jobId} not found`);
|
|
356
364
|
}
|
|
357
|
-
if (job.status ===
|
|
365
|
+
if (job.status === JobStatus.COMPLETED) {
|
|
358
366
|
this.removePromise(jobId, resolve, reject);
|
|
359
367
|
return job.output;
|
|
360
368
|
}
|
|
361
|
-
if (job.status ===
|
|
369
|
+
if (job.status === JobStatus.DISABLED) {
|
|
362
370
|
this.removePromise(jobId, resolve, reject);
|
|
363
371
|
throw new JobDisabledError(`Job ${jobId} was disabled`);
|
|
364
372
|
}
|
|
365
|
-
if (job.status ===
|
|
373
|
+
if (job.status === JobStatus.FAILED) {
|
|
366
374
|
this.removePromise(jobId, resolve, reject);
|
|
367
375
|
throw this.buildErrorFromJob(job);
|
|
368
376
|
}
|
|
@@ -397,7 +405,7 @@ class JobQueueClient {
|
|
|
397
405
|
throw new JobNotFoundError("Cannot abort job run with undefined jobRunId");
|
|
398
406
|
const jobs = await this.getJobsByRunId(jobRunId);
|
|
399
407
|
await Promise.allSettled(jobs.map((job) => {
|
|
400
|
-
if (job.status ===
|
|
408
|
+
if (job.status === JobStatus.PROCESSING || job.status === JobStatus.PENDING) {
|
|
401
409
|
return this.abort(job.id);
|
|
402
410
|
}
|
|
403
411
|
}));
|
|
@@ -504,15 +512,15 @@ class JobQueueClient {
|
|
|
504
512
|
if (change.type === "UPDATE" && change.new) {
|
|
505
513
|
const newStatus = change.new.status;
|
|
506
514
|
const oldStatus = change.old?.status;
|
|
507
|
-
if (newStatus ===
|
|
515
|
+
if (newStatus === JobStatus.PROCESSING && oldStatus === JobStatus.PENDING) {
|
|
508
516
|
this.handleJobStart(jobId);
|
|
509
|
-
} else if (newStatus ===
|
|
517
|
+
} else if (newStatus === JobStatus.COMPLETED) {
|
|
510
518
|
this.handleJobComplete(jobId, change.new.output);
|
|
511
|
-
} else if (newStatus ===
|
|
519
|
+
} else if (newStatus === JobStatus.FAILED) {
|
|
512
520
|
this.handleJobError(jobId, change.new.error ?? "Job failed", change.new.error_code ?? undefined);
|
|
513
|
-
} else if (newStatus ===
|
|
521
|
+
} else if (newStatus === JobStatus.DISABLED) {
|
|
514
522
|
this.handleJobDisabled(jobId);
|
|
515
|
-
} else if (newStatus ===
|
|
523
|
+
} else if (newStatus === JobStatus.PENDING && oldStatus === JobStatus.PROCESSING) {
|
|
516
524
|
const runAfter = change.new.run_after ? new Date(change.new.run_after) : new Date;
|
|
517
525
|
this.handleJobRetry(jobId, runAfter);
|
|
518
526
|
}
|
|
@@ -555,14 +563,19 @@ class JobQueueClient {
|
|
|
555
563
|
}
|
|
556
564
|
|
|
557
565
|
// src/job/JobQueueServer.ts
|
|
558
|
-
import { JobStatus as JobStatus4 } from "@workglow/storage";
|
|
559
566
|
import { EventEmitter as EventEmitter3, getLogger as getLogger2 } from "@workglow/util";
|
|
560
567
|
|
|
561
568
|
// src/limiter/NullLimiter.ts
|
|
562
|
-
import { createServiceToken } from "@workglow/util";
|
|
563
|
-
var NULL_JOB_LIMITER =
|
|
569
|
+
import { createServiceToken as createServiceToken2 } from "@workglow/util";
|
|
570
|
+
var NULL_JOB_LIMITER = createServiceToken2("jobqueue.limiter.null");
|
|
564
571
|
|
|
565
572
|
class NullLimiter {
|
|
573
|
+
scope = "process";
|
|
574
|
+
static SENTINEL = Symbol("NullLimiter.acquired");
|
|
575
|
+
async tryAcquire() {
|
|
576
|
+
return NullLimiter.SENTINEL;
|
|
577
|
+
}
|
|
578
|
+
async release(_token) {}
|
|
566
579
|
async canProceed() {
|
|
567
580
|
return true;
|
|
568
581
|
}
|
|
@@ -576,7 +589,6 @@ class NullLimiter {
|
|
|
576
589
|
}
|
|
577
590
|
|
|
578
591
|
// src/job/JobQueueWorker.ts
|
|
579
|
-
import { JobStatus as JobStatus3 } from "@workglow/storage";
|
|
580
592
|
import {
|
|
581
593
|
EventEmitter as EventEmitter2,
|
|
582
594
|
getLogger,
|
|
@@ -585,6 +597,8 @@ import {
|
|
|
585
597
|
SpanStatusCode,
|
|
586
598
|
uuid4
|
|
587
599
|
} from "@workglow/util";
|
|
600
|
+
var MAX_LIMITER_WAKE_MS = 30000;
|
|
601
|
+
|
|
588
602
|
class JobQueueWorker {
|
|
589
603
|
queueName;
|
|
590
604
|
workerId;
|
|
@@ -668,15 +682,16 @@ class JobQueueWorker {
|
|
|
668
682
|
return this;
|
|
669
683
|
}
|
|
670
684
|
async processNext() {
|
|
671
|
-
const canProceed = await this.limiter.canProceed();
|
|
672
|
-
if (!canProceed) {
|
|
673
|
-
return false;
|
|
674
|
-
}
|
|
675
685
|
const job = await this.next();
|
|
676
686
|
if (!job) {
|
|
677
687
|
return false;
|
|
678
688
|
}
|
|
679
|
-
await this.
|
|
689
|
+
const limiterToken = await this.limiter.tryAcquire();
|
|
690
|
+
if (limiterToken === null || limiterToken === undefined) {
|
|
691
|
+
await this.releaseClaimedJob(job);
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
await this.processSingleJob(job, limiterToken);
|
|
680
695
|
return true;
|
|
681
696
|
}
|
|
682
697
|
isRunning() {
|
|
@@ -707,32 +722,49 @@ class JobQueueWorker {
|
|
|
707
722
|
while (this.running) {
|
|
708
723
|
try {
|
|
709
724
|
await this.checkForAbortingJobs();
|
|
710
|
-
const
|
|
711
|
-
if (
|
|
712
|
-
const job = await this.next();
|
|
713
|
-
if (job) {
|
|
714
|
-
if (!this.running) {
|
|
715
|
-
await this.releaseClaimedJob(job);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
this.processSingleJob(job);
|
|
719
|
-
continue;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
if (canProceed) {
|
|
725
|
+
const job = await this.next();
|
|
726
|
+
if (!job) {
|
|
723
727
|
const delay = await this.getIdleDelay();
|
|
724
728
|
await this.waitForWakeOrTimeout(delay);
|
|
725
|
-
|
|
726
|
-
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (!this.running) {
|
|
732
|
+
await this.releaseClaimedJob(job);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const limiterToken = await this.limiter.tryAcquire();
|
|
736
|
+
if (limiterToken === null || limiterToken === undefined) {
|
|
737
|
+
await this.releaseClaimedJob(job);
|
|
738
|
+
await this.waitForWakeOrTimeout(await this.getLimiterWakeDelay());
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (!this.running) {
|
|
742
|
+
try {
|
|
743
|
+
await this.limiter.release(limiterToken);
|
|
744
|
+
} catch {}
|
|
745
|
+
await this.releaseClaimedJob(job);
|
|
746
|
+
return;
|
|
727
747
|
}
|
|
748
|
+
this.processSingleJob(job, limiterToken);
|
|
728
749
|
} catch {
|
|
729
750
|
await sleep(this.pollIntervalMs);
|
|
730
751
|
}
|
|
731
752
|
}
|
|
732
753
|
}
|
|
754
|
+
async getLimiterWakeDelay() {
|
|
755
|
+
try {
|
|
756
|
+
const next = await this.limiter.getNextAvailableTime();
|
|
757
|
+
const delay = next.getTime() - Date.now();
|
|
758
|
+
if (delay <= 0)
|
|
759
|
+
return this.pollIntervalMs;
|
|
760
|
+
return Math.min(Math.max(delay, this.pollIntervalMs), MAX_LIMITER_WAKE_MS);
|
|
761
|
+
} catch {
|
|
762
|
+
return this.pollIntervalMs;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
733
765
|
async getIdleDelay() {
|
|
734
766
|
try {
|
|
735
|
-
const pending = await this.storage.peek(
|
|
767
|
+
const pending = await this.storage.peek(JobStatus.PENDING, 1);
|
|
736
768
|
if (pending.length > 0 && pending[0].run_after) {
|
|
737
769
|
const delay = new Date(pending[0].run_after).getTime() - Date.now();
|
|
738
770
|
if (delay > 0) {
|
|
@@ -764,7 +796,7 @@ class JobQueueWorker {
|
|
|
764
796
|
if (this.activeJobAbortControllers.size === 0) {
|
|
765
797
|
return;
|
|
766
798
|
}
|
|
767
|
-
const abortingJobs = await this.storage.peek(
|
|
799
|
+
const abortingJobs = await this.storage.peek(JobStatus.ABORTING);
|
|
768
800
|
for (const jobData of abortingJobs) {
|
|
769
801
|
const controller = this.activeJobAbortControllers.get(jobData.id);
|
|
770
802
|
if (controller && !controller.signal.aborted) {
|
|
@@ -772,7 +804,7 @@ class JobQueueWorker {
|
|
|
772
804
|
}
|
|
773
805
|
}
|
|
774
806
|
}
|
|
775
|
-
async processSingleJob(job) {
|
|
807
|
+
async processSingleJob(job, limiterToken) {
|
|
776
808
|
if (!job || !job.id) {
|
|
777
809
|
throw new JobNotFoundError("Invalid job provided for processing");
|
|
778
810
|
}
|
|
@@ -789,9 +821,17 @@ class JobQueueWorker {
|
|
|
789
821
|
"workglow.job.max_retries": job.maxRetries
|
|
790
822
|
}
|
|
791
823
|
}) : undefined;
|
|
824
|
+
let slotReleased = false;
|
|
792
825
|
try {
|
|
793
|
-
|
|
794
|
-
|
|
826
|
+
try {
|
|
827
|
+
await this.validateJobState(job);
|
|
828
|
+
} catch (validationErr) {
|
|
829
|
+
try {
|
|
830
|
+
await this.limiter.release(limiterToken);
|
|
831
|
+
slotReleased = true;
|
|
832
|
+
} catch {}
|
|
833
|
+
throw validationErr;
|
|
834
|
+
}
|
|
795
835
|
const abortController = this.createAbortController(job.id);
|
|
796
836
|
this.events.emit("job_start", job.id);
|
|
797
837
|
const output = await this.executeJob(job, abortController.signal);
|
|
@@ -829,7 +869,9 @@ class JobQueueWorker {
|
|
|
829
869
|
} finally {
|
|
830
870
|
span?.end();
|
|
831
871
|
try {
|
|
832
|
-
|
|
872
|
+
if (!slotReleased) {
|
|
873
|
+
await this.limiter.recordJobCompletion();
|
|
874
|
+
}
|
|
833
875
|
} finally {
|
|
834
876
|
this.inFlight.delete(job.id);
|
|
835
877
|
resolveInFlight();
|
|
@@ -850,7 +892,7 @@ class JobQueueWorker {
|
|
|
850
892
|
}
|
|
851
893
|
async completeJob(job, output) {
|
|
852
894
|
try {
|
|
853
|
-
job.status =
|
|
895
|
+
job.status = JobStatus.COMPLETED;
|
|
854
896
|
job.progress = 100;
|
|
855
897
|
job.progressMessage = "";
|
|
856
898
|
job.progressDetails = null;
|
|
@@ -868,7 +910,7 @@ class JobQueueWorker {
|
|
|
868
910
|
}
|
|
869
911
|
async failJob(job, error) {
|
|
870
912
|
try {
|
|
871
|
-
job.status =
|
|
913
|
+
job.status = JobStatus.FAILED;
|
|
872
914
|
job.progress = 100;
|
|
873
915
|
job.completedAt = new Date;
|
|
874
916
|
job.progressMessage = "";
|
|
@@ -885,7 +927,7 @@ class JobQueueWorker {
|
|
|
885
927
|
}
|
|
886
928
|
async disableJob(job) {
|
|
887
929
|
try {
|
|
888
|
-
job.status =
|
|
930
|
+
job.status = JobStatus.DISABLED;
|
|
889
931
|
job.progress = 100;
|
|
890
932
|
job.completedAt = new Date;
|
|
891
933
|
job.progressMessage = "";
|
|
@@ -907,7 +949,7 @@ class JobQueueWorker {
|
|
|
907
949
|
}
|
|
908
950
|
async rescheduleJob(job, retryDate) {
|
|
909
951
|
try {
|
|
910
|
-
job.status =
|
|
952
|
+
job.status = JobStatus.PENDING;
|
|
911
953
|
const nextAvailableTime = await this.limiter.getNextAvailableTime();
|
|
912
954
|
job.runAfter = retryDate instanceof Date ? retryDate : nextAvailableTime;
|
|
913
955
|
job.progress = 0;
|
|
@@ -943,7 +985,7 @@ class JobQueueWorker {
|
|
|
943
985
|
getLogger().error("handleAbort: job not found", { jobId });
|
|
944
986
|
return;
|
|
945
987
|
}
|
|
946
|
-
if (job.status ===
|
|
988
|
+
if (job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED || job.status === JobStatus.DISABLED) {
|
|
947
989
|
return;
|
|
948
990
|
}
|
|
949
991
|
await this.failJob(job, new AbortSignalJobError("Job Aborted"));
|
|
@@ -955,19 +997,19 @@ class JobQueueWorker {
|
|
|
955
997
|
return this.storageToClass(job);
|
|
956
998
|
}
|
|
957
999
|
async validateJobState(job) {
|
|
958
|
-
if (job.status ===
|
|
1000
|
+
if (job.status === JobStatus.COMPLETED) {
|
|
959
1001
|
throw new PermanentJobError(`Job ${job.id} is already completed`);
|
|
960
1002
|
}
|
|
961
|
-
if (job.status ===
|
|
1003
|
+
if (job.status === JobStatus.FAILED) {
|
|
962
1004
|
throw new PermanentJobError(`Job ${job.id} has failed`);
|
|
963
1005
|
}
|
|
964
|
-
if (job.status ===
|
|
1006
|
+
if (job.status === JobStatus.ABORTING || this.activeJobAbortControllers.get(job.id)?.signal.aborted) {
|
|
965
1007
|
throw new AbortSignalJobError(`Job ${job.id} is being aborted`);
|
|
966
1008
|
}
|
|
967
1009
|
if (job.deadlineAt && job.deadlineAt < new Date) {
|
|
968
1010
|
throw new PermanentJobError(`Job ${job.id} has exceeded its deadline`);
|
|
969
1011
|
}
|
|
970
|
-
if (job.status ===
|
|
1012
|
+
if (job.status === JobStatus.DISABLED) {
|
|
971
1013
|
throw new JobDisabledError(`Job ${job.id} has been disabled`);
|
|
972
1014
|
}
|
|
973
1015
|
}
|
|
@@ -1039,10 +1081,17 @@ class JobQueueServer {
|
|
|
1039
1081
|
}
|
|
1040
1082
|
this.running = true;
|
|
1041
1083
|
this.events.emit("server_start", this.queueName);
|
|
1084
|
+
if (this.limiter.scope === "process" && this.storage.scope === "cluster" && !(this.limiter instanceof NullLimiter)) {
|
|
1085
|
+
getLogger2().warn("Process-scoped limiter on cluster-scoped queue storage \u2014 limit is enforced per-process, not cluster-wide. Use a cluster-scoped rate limiter storage (Postgres/Supabase) for global enforcement.", {
|
|
1086
|
+
queueName: this.queueName,
|
|
1087
|
+
limiterScope: this.limiter.scope,
|
|
1088
|
+
storage: this.storage.constructor.name
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1042
1091
|
await this.fixupJobs();
|
|
1043
1092
|
try {
|
|
1044
1093
|
this.storageUnsubscribe = this.storage.subscribeToChanges((change) => {
|
|
1045
|
-
if (change.type === "INSERT" || change.type === "UPDATE" && change.new?.status ===
|
|
1094
|
+
if (change.type === "INSERT" || change.type === "RESYNC" || change.type === "UPDATE" && change.new?.status === JobStatus.PENDING) {
|
|
1046
1095
|
this.notifyWorkers();
|
|
1047
1096
|
}
|
|
1048
1097
|
});
|
|
@@ -1237,13 +1286,13 @@ class JobQueueServer {
|
|
|
1237
1286
|
async cleanupJobs() {
|
|
1238
1287
|
try {
|
|
1239
1288
|
if (this.deleteAfterCompletionMs !== undefined && this.deleteAfterCompletionMs > 0) {
|
|
1240
|
-
await this.storage.deleteJobsByStatusAndAge(
|
|
1289
|
+
await this.storage.deleteJobsByStatusAndAge(JobStatus.COMPLETED, this.deleteAfterCompletionMs);
|
|
1241
1290
|
}
|
|
1242
1291
|
if (this.deleteAfterFailureMs !== undefined && this.deleteAfterFailureMs > 0) {
|
|
1243
|
-
await this.storage.deleteJobsByStatusAndAge(
|
|
1292
|
+
await this.storage.deleteJobsByStatusAndAge(JobStatus.FAILED, this.deleteAfterFailureMs);
|
|
1244
1293
|
}
|
|
1245
1294
|
if (this.deleteAfterDisabledMs !== undefined && this.deleteAfterDisabledMs > 0) {
|
|
1246
|
-
await this.storage.deleteJobsByStatusAndAge(
|
|
1295
|
+
await this.storage.deleteJobsByStatusAndAge(JobStatus.DISABLED, this.deleteAfterDisabledMs);
|
|
1247
1296
|
}
|
|
1248
1297
|
} catch (error) {
|
|
1249
1298
|
console.error("Error in cleanup:", error);
|
|
@@ -1251,8 +1300,8 @@ class JobQueueServer {
|
|
|
1251
1300
|
}
|
|
1252
1301
|
async fixupJobs() {
|
|
1253
1302
|
try {
|
|
1254
|
-
const stuckProcessingJobs = await this.storage.peek(
|
|
1255
|
-
const stuckAbortingJobs = await this.storage.peek(
|
|
1303
|
+
const stuckProcessingJobs = await this.storage.peek(JobStatus.PROCESSING);
|
|
1304
|
+
const stuckAbortingJobs = await this.storage.peek(JobStatus.ABORTING);
|
|
1256
1305
|
const stuckJobs = [...stuckProcessingJobs, ...stuckAbortingJobs];
|
|
1257
1306
|
const currentWorkerIds = new Set(this.getWorkerIds());
|
|
1258
1307
|
for (const jobData of stuckJobs) {
|
|
@@ -1261,12 +1310,12 @@ class JobQueueServer {
|
|
|
1261
1310
|
}
|
|
1262
1311
|
const job = this.storageToClass(jobData);
|
|
1263
1312
|
if (job.runAttempts >= job.maxRetries) {
|
|
1264
|
-
job.status =
|
|
1313
|
+
job.status = JobStatus.FAILED;
|
|
1265
1314
|
job.error = "Max retries reached";
|
|
1266
1315
|
job.errorCode = "MAX_RETRIES_REACHED";
|
|
1267
1316
|
job.workerId = null;
|
|
1268
1317
|
} else {
|
|
1269
|
-
job.status =
|
|
1318
|
+
job.status = JobStatus.PENDING;
|
|
1270
1319
|
job.runAfter = job.lastRanAt || new Date;
|
|
1271
1320
|
job.progress = 0;
|
|
1272
1321
|
job.progressMessage = "";
|
|
@@ -1297,6 +1346,9 @@ class CompositeLimiter {
|
|
|
1297
1346
|
constructor(limiters = []) {
|
|
1298
1347
|
this.limiters = limiters;
|
|
1299
1348
|
}
|
|
1349
|
+
get scope() {
|
|
1350
|
+
return this.limiters.every((l) => l.scope === "cluster") && this.limiters.length > 0 ? "cluster" : "process";
|
|
1351
|
+
}
|
|
1300
1352
|
addLimiter(limiter) {
|
|
1301
1353
|
this.limiters.push(limiter);
|
|
1302
1354
|
}
|
|
@@ -1308,6 +1360,27 @@ class CompositeLimiter {
|
|
|
1308
1360
|
}
|
|
1309
1361
|
return true;
|
|
1310
1362
|
}
|
|
1363
|
+
async tryAcquire() {
|
|
1364
|
+
const tokens = [];
|
|
1365
|
+
for (const limiter of this.limiters) {
|
|
1366
|
+
const t = await limiter.tryAcquire();
|
|
1367
|
+
if (t === null || t === undefined) {
|
|
1368
|
+
for (let i = tokens.length - 1;i >= 0; i--) {
|
|
1369
|
+
try {
|
|
1370
|
+
await this.limiters[i].release(tokens[i]);
|
|
1371
|
+
} catch {}
|
|
1372
|
+
}
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
tokens.push(t);
|
|
1376
|
+
}
|
|
1377
|
+
return tokens;
|
|
1378
|
+
}
|
|
1379
|
+
async release(token) {
|
|
1380
|
+
if (!Array.isArray(token))
|
|
1381
|
+
return;
|
|
1382
|
+
await Promise.all(this.limiters.map((l, i) => l.release(token[i]).catch(() => {})));
|
|
1383
|
+
}
|
|
1311
1384
|
async recordJobStart() {
|
|
1312
1385
|
await Promise.all(this.limiters.map((limiter) => limiter.recordJobStart()));
|
|
1313
1386
|
}
|
|
@@ -1335,10 +1408,11 @@ class CompositeLimiter {
|
|
|
1335
1408
|
}
|
|
1336
1409
|
|
|
1337
1410
|
// src/limiter/ConcurrencyLimiter.ts
|
|
1338
|
-
import { createServiceToken as
|
|
1339
|
-
var CONCURRENT_JOB_LIMITER =
|
|
1411
|
+
import { createServiceToken as createServiceToken3 } from "@workglow/util";
|
|
1412
|
+
var CONCURRENT_JOB_LIMITER = createServiceToken3("jobqueue.limiter.concurrent");
|
|
1340
1413
|
|
|
1341
1414
|
class ConcurrencyLimiter {
|
|
1415
|
+
scope = "process";
|
|
1342
1416
|
currentRunningJobs = 0;
|
|
1343
1417
|
maxConcurrentJobs;
|
|
1344
1418
|
nextAllowedStartTime = new Date;
|
|
@@ -1348,6 +1422,19 @@ class ConcurrencyLimiter {
|
|
|
1348
1422
|
async canProceed() {
|
|
1349
1423
|
return this.currentRunningJobs < this.maxConcurrentJobs && Date.now() >= this.nextAllowedStartTime.getTime();
|
|
1350
1424
|
}
|
|
1425
|
+
static SENTINEL = Symbol("ConcurrencyLimiter.acquired");
|
|
1426
|
+
async tryAcquire() {
|
|
1427
|
+
if (this.currentRunningJobs >= this.maxConcurrentJobs || Date.now() < this.nextAllowedStartTime.getTime()) {
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
this.currentRunningJobs++;
|
|
1431
|
+
return ConcurrencyLimiter.SENTINEL;
|
|
1432
|
+
}
|
|
1433
|
+
async release(token) {
|
|
1434
|
+
if (token !== ConcurrencyLimiter.SENTINEL)
|
|
1435
|
+
return;
|
|
1436
|
+
this.currentRunningJobs = Math.max(0, this.currentRunningJobs - 1);
|
|
1437
|
+
}
|
|
1351
1438
|
async recordJobStart() {
|
|
1352
1439
|
this.currentRunningJobs++;
|
|
1353
1440
|
}
|
|
@@ -1371,13 +1458,32 @@ class ConcurrencyLimiter {
|
|
|
1371
1458
|
// src/limiter/DelayLimiter.ts
|
|
1372
1459
|
class DelayLimiter {
|
|
1373
1460
|
delayInMilliseconds;
|
|
1461
|
+
scope = "process";
|
|
1374
1462
|
nextAvailableTime = new Date;
|
|
1463
|
+
lastAcquireBaseline = 0;
|
|
1375
1464
|
constructor(delayInMilliseconds = 50) {
|
|
1376
1465
|
this.delayInMilliseconds = delayInMilliseconds;
|
|
1377
1466
|
}
|
|
1378
1467
|
async canProceed() {
|
|
1379
1468
|
return Date.now() >= this.nextAvailableTime.getTime();
|
|
1380
1469
|
}
|
|
1470
|
+
async tryAcquire() {
|
|
1471
|
+
const now = Date.now();
|
|
1472
|
+
if (now < this.nextAvailableTime.getTime()) {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
const previous = this.nextAvailableTime.getTime();
|
|
1476
|
+
this.lastAcquireBaseline = previous;
|
|
1477
|
+
this.nextAvailableTime = new Date(now + this.delayInMilliseconds);
|
|
1478
|
+
return previous;
|
|
1479
|
+
}
|
|
1480
|
+
async release(token) {
|
|
1481
|
+
if (typeof token !== "number")
|
|
1482
|
+
return;
|
|
1483
|
+
if (this.nextAvailableTime.getTime() === this.lastAcquireBaseline + this.delayInMilliseconds) {
|
|
1484
|
+
this.nextAvailableTime = new Date(token);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1381
1487
|
async recordJobStart() {
|
|
1382
1488
|
this.nextAvailableTime = new Date(Date.now() + this.delayInMilliseconds);
|
|
1383
1489
|
}
|
|
@@ -1396,16 +1502,18 @@ class DelayLimiter {
|
|
|
1396
1502
|
}
|
|
1397
1503
|
|
|
1398
1504
|
// src/limiter/EvenlySpacedRateLimiter.ts
|
|
1399
|
-
import { createServiceToken as
|
|
1400
|
-
var EVENLY_SPACED_JOB_RATE_LIMITER =
|
|
1505
|
+
import { createServiceToken as createServiceToken4 } from "@workglow/util";
|
|
1506
|
+
var EVENLY_SPACED_JOB_RATE_LIMITER = createServiceToken4("jobqueue.limiter.rate.evenlyspaced");
|
|
1401
1507
|
|
|
1402
1508
|
class EvenlySpacedRateLimiter {
|
|
1509
|
+
scope = "process";
|
|
1403
1510
|
maxExecutions;
|
|
1404
1511
|
windowSizeMs;
|
|
1405
1512
|
idealInterval;
|
|
1406
1513
|
nextAvailableTime = Date.now();
|
|
1407
1514
|
lastStartTime = 0;
|
|
1408
1515
|
durations = [];
|
|
1516
|
+
acquireChain = Promise.resolve();
|
|
1409
1517
|
constructor({ maxExecutions, windowSizeInSeconds }) {
|
|
1410
1518
|
if (maxExecutions <= 0) {
|
|
1411
1519
|
throw new Error("maxExecutions must be > 0");
|
|
@@ -1421,6 +1529,43 @@ class EvenlySpacedRateLimiter {
|
|
|
1421
1529
|
const now = Date.now();
|
|
1422
1530
|
return now >= this.nextAvailableTime;
|
|
1423
1531
|
}
|
|
1532
|
+
async tryAcquire() {
|
|
1533
|
+
const previous = this.acquireChain;
|
|
1534
|
+
let release;
|
|
1535
|
+
const next = new Promise((r) => {
|
|
1536
|
+
release = r;
|
|
1537
|
+
});
|
|
1538
|
+
this.acquireChain = next;
|
|
1539
|
+
try {
|
|
1540
|
+
await previous;
|
|
1541
|
+
const now = Date.now();
|
|
1542
|
+
if (now < this.nextAvailableTime) {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
const priorNextAvailable = this.nextAvailableTime;
|
|
1546
|
+
this.lastStartTime = now;
|
|
1547
|
+
if (this.durations.length === 0) {
|
|
1548
|
+
this.nextAvailableTime = now + this.idealInterval;
|
|
1549
|
+
} else {
|
|
1550
|
+
const sum = this.durations.reduce((a, b) => a + b, 0);
|
|
1551
|
+
const avgDuration = sum / this.durations.length;
|
|
1552
|
+
const waitMs = Math.max(0, this.idealInterval - avgDuration);
|
|
1553
|
+
this.nextAvailableTime = now + waitMs;
|
|
1554
|
+
}
|
|
1555
|
+
return { priorNextAvailable, advancedTo: this.nextAvailableTime };
|
|
1556
|
+
} finally {
|
|
1557
|
+
release(undefined);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async release(token) {
|
|
1561
|
+
if (!token || typeof token !== "object" || typeof token.advancedTo !== "number") {
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
const t = token;
|
|
1565
|
+
if (this.nextAvailableTime === t.advancedTo) {
|
|
1566
|
+
this.nextAvailableTime = t.priorNextAvailable;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1424
1569
|
async recordJobStart() {
|
|
1425
1570
|
const now = Date.now();
|
|
1426
1571
|
this.lastStartTime = now;
|
|
@@ -1458,8 +1603,8 @@ class EvenlySpacedRateLimiter {
|
|
|
1458
1603
|
}
|
|
1459
1604
|
|
|
1460
1605
|
// src/limiter/ILimiter.ts
|
|
1461
|
-
import { createServiceToken as
|
|
1462
|
-
var JOB_LIMITER =
|
|
1606
|
+
import { createServiceToken as createServiceToken5 } from "@workglow/util";
|
|
1607
|
+
var JOB_LIMITER = createServiceToken5("jobqueue.limiter");
|
|
1463
1608
|
|
|
1464
1609
|
// src/limiter/RateLimiter.ts
|
|
1465
1610
|
class RateLimiter {
|
|
@@ -1471,6 +1616,7 @@ class RateLimiter {
|
|
|
1471
1616
|
initialBackoffDelay;
|
|
1472
1617
|
backoffMultiplier;
|
|
1473
1618
|
maxBackoffDelay;
|
|
1619
|
+
localBackoffUntilMs = 0;
|
|
1474
1620
|
constructor(storage, queueName, {
|
|
1475
1621
|
maxExecutions,
|
|
1476
1622
|
windowSizeInSeconds,
|
|
@@ -1502,6 +1648,27 @@ class RateLimiter {
|
|
|
1502
1648
|
this.maxBackoffDelay = maxBackoffDelay;
|
|
1503
1649
|
this.currentBackoffDelay = initialBackoffDelay;
|
|
1504
1650
|
}
|
|
1651
|
+
get scope() {
|
|
1652
|
+
return this.storage.scope;
|
|
1653
|
+
}
|
|
1654
|
+
async tryAcquire() {
|
|
1655
|
+
const token = await this.storage.tryReserveExecution(this.queueName, this.maxExecutions, this.windowSizeInMilliseconds);
|
|
1656
|
+
if (token !== null && token !== undefined) {
|
|
1657
|
+
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1658
|
+
this.localBackoffUntilMs = 0;
|
|
1659
|
+
return token;
|
|
1660
|
+
}
|
|
1661
|
+
this.localBackoffUntilMs = Date.now() + this.addJitter(this.currentBackoffDelay);
|
|
1662
|
+
this.increaseBackoff();
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
async release(token) {
|
|
1666
|
+
if (token === null || token === undefined)
|
|
1667
|
+
return;
|
|
1668
|
+
await this.storage.releaseExecution(this.queueName, token);
|
|
1669
|
+
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1670
|
+
this.localBackoffUntilMs = 0;
|
|
1671
|
+
}
|
|
1505
1672
|
addJitter(base) {
|
|
1506
1673
|
return base + Math.random() * base;
|
|
1507
1674
|
}
|
|
@@ -1547,17 +1714,18 @@ class RateLimiter {
|
|
|
1547
1714
|
async recordJobCompletion() {}
|
|
1548
1715
|
async getNextAvailableTime() {
|
|
1549
1716
|
const oldestExecution = await this.storage.getOldestExecutionAtOffset(this.queueName, this.maxExecutions - 1);
|
|
1550
|
-
let
|
|
1717
|
+
let latestMs = Date.now();
|
|
1551
1718
|
if (oldestExecution) {
|
|
1552
|
-
|
|
1553
|
-
rateLimitedTime.setSeconds(rateLimitedTime.getSeconds() + this.windowSizeInMilliseconds / 1000);
|
|
1719
|
+
latestMs = Math.max(latestMs, new Date(oldestExecution).getTime() + this.windowSizeInMilliseconds);
|
|
1554
1720
|
}
|
|
1555
1721
|
const nextAvailableStr = await this.storage.getNextAvailableTime(this.queueName);
|
|
1556
|
-
let nextAvailableTime = new Date;
|
|
1557
1722
|
if (nextAvailableStr) {
|
|
1558
|
-
|
|
1723
|
+
latestMs = Math.max(latestMs, new Date(nextAvailableStr).getTime());
|
|
1724
|
+
}
|
|
1725
|
+
if (this.localBackoffUntilMs > latestMs) {
|
|
1726
|
+
latestMs = this.localBackoffUntilMs;
|
|
1559
1727
|
}
|
|
1560
|
-
return
|
|
1728
|
+
return new Date(latestMs);
|
|
1561
1729
|
}
|
|
1562
1730
|
async setNextAvailableTime(date) {
|
|
1563
1731
|
await this.storage.setNextAvailableTime(this.queueName, date.toISOString());
|
|
@@ -1565,6 +1733,408 @@ class RateLimiter {
|
|
|
1565
1733
|
async clear() {
|
|
1566
1734
|
await this.storage.clear(this.queueName);
|
|
1567
1735
|
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1736
|
+
this.localBackoffUntilMs = 0;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/queue-storage/InMemoryQueueStorage.ts
|
|
1741
|
+
import {
|
|
1742
|
+
createServiceToken as createServiceToken6,
|
|
1743
|
+
EventEmitter as EventEmitter4,
|
|
1744
|
+
getLogger as getLogger3,
|
|
1745
|
+
makeFingerprint,
|
|
1746
|
+
sleep as sleep2,
|
|
1747
|
+
uuid4 as uuid42
|
|
1748
|
+
} from "@workglow/util";
|
|
1749
|
+
var IN_MEMORY_QUEUE_STORAGE = createServiceToken6("jobqueue.storage.inMemory");
|
|
1750
|
+
|
|
1751
|
+
class InMemoryQueueStorage {
|
|
1752
|
+
queueName;
|
|
1753
|
+
scope = "process";
|
|
1754
|
+
prefixValues;
|
|
1755
|
+
events = new EventEmitter4;
|
|
1756
|
+
constructor(queueName, options) {
|
|
1757
|
+
this.queueName = queueName;
|
|
1758
|
+
this.jobQueue = [];
|
|
1759
|
+
this.prefixValues = options?.prefixValues ?? {};
|
|
1760
|
+
}
|
|
1761
|
+
jobQueue;
|
|
1762
|
+
matchesPrefixes(job) {
|
|
1763
|
+
for (const [key, value] of Object.entries(this.prefixValues)) {
|
|
1764
|
+
if (job[key] !== value) {
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return true;
|
|
1769
|
+
}
|
|
1770
|
+
pendingQueue() {
|
|
1771
|
+
const now = new Date().toISOString();
|
|
1772
|
+
return this.jobQueue.filter((job) => this.matchesPrefixes(job)).filter((job) => job.status === JobStatus.PENDING).filter((job) => !job.run_after || job.run_after <= now).sort((a, b) => (a.run_after || "").localeCompare(b.run_after || ""));
|
|
1773
|
+
}
|
|
1774
|
+
async add(job) {
|
|
1775
|
+
await sleep2(0);
|
|
1776
|
+
const now = new Date().toISOString();
|
|
1777
|
+
const jobWithPrefixes = job;
|
|
1778
|
+
jobWithPrefixes.id = jobWithPrefixes.id ?? uuid42();
|
|
1779
|
+
jobWithPrefixes.job_run_id = jobWithPrefixes.job_run_id ?? uuid42();
|
|
1780
|
+
jobWithPrefixes.queue = this.queueName;
|
|
1781
|
+
jobWithPrefixes.fingerprint = await makeFingerprint(jobWithPrefixes.input);
|
|
1782
|
+
jobWithPrefixes.status = JobStatus.PENDING;
|
|
1783
|
+
jobWithPrefixes.progress = 0;
|
|
1784
|
+
jobWithPrefixes.progress_message = "";
|
|
1785
|
+
jobWithPrefixes.progress_details = null;
|
|
1786
|
+
jobWithPrefixes.created_at = now;
|
|
1787
|
+
jobWithPrefixes.run_after = now;
|
|
1788
|
+
for (const [key, value] of Object.entries(this.prefixValues)) {
|
|
1789
|
+
jobWithPrefixes[key] = value;
|
|
1790
|
+
}
|
|
1791
|
+
this.jobQueue.push(jobWithPrefixes);
|
|
1792
|
+
this.events.emit("change", { type: "INSERT", new: jobWithPrefixes });
|
|
1793
|
+
return jobWithPrefixes.id;
|
|
1794
|
+
}
|
|
1795
|
+
async get(id) {
|
|
1796
|
+
await sleep2(0);
|
|
1797
|
+
const job = this.jobQueue.find((j) => j.id === id);
|
|
1798
|
+
if (job && this.matchesPrefixes(job)) {
|
|
1799
|
+
return job;
|
|
1800
|
+
}
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
async peek(status = JobStatus.PENDING, num = 100) {
|
|
1804
|
+
await sleep2(0);
|
|
1805
|
+
num = Number(num) || 100;
|
|
1806
|
+
return this.jobQueue.filter((j) => this.matchesPrefixes(j)).sort((a, b) => (a.run_after || "").localeCompare(b.run_after || "")).filter((j) => j.status === status).slice(0, num);
|
|
1807
|
+
}
|
|
1808
|
+
async next(workerId) {
|
|
1809
|
+
await sleep2(0);
|
|
1810
|
+
const top = this.pendingQueue();
|
|
1811
|
+
const job = top[0];
|
|
1812
|
+
if (job) {
|
|
1813
|
+
const oldJob = { ...job };
|
|
1814
|
+
job.status = JobStatus.PROCESSING;
|
|
1815
|
+
job.last_ran_at = new Date().toISOString();
|
|
1816
|
+
job.worker_id = workerId;
|
|
1817
|
+
this.events.emit("change", { type: "UPDATE", old: oldJob, new: job });
|
|
1818
|
+
return job;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
async size(status = JobStatus.PENDING) {
|
|
1822
|
+
await sleep2(0);
|
|
1823
|
+
return this.jobQueue.filter((j) => this.matchesPrefixes(j) && j.status === status).length;
|
|
1824
|
+
}
|
|
1825
|
+
async saveProgress(id, progress, message, details) {
|
|
1826
|
+
await sleep2(0);
|
|
1827
|
+
const job = this.jobQueue.find((j) => j.id === id && this.matchesPrefixes(j));
|
|
1828
|
+
if (!job) {
|
|
1829
|
+
const jobWithAnyPrefix = this.jobQueue.find((j) => j.id === id);
|
|
1830
|
+
getLogger3().warn("Job not found for progress update", {
|
|
1831
|
+
id,
|
|
1832
|
+
reason: jobWithAnyPrefix ? "prefix_mismatch" : "missing",
|
|
1833
|
+
existingStatus: jobWithAnyPrefix?.status,
|
|
1834
|
+
queueName: this.queueName,
|
|
1835
|
+
prefixValues: this.prefixValues
|
|
1836
|
+
});
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
if (job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) {
|
|
1840
|
+
getLogger3().warn("Job already completed or failed for progress update", {
|
|
1841
|
+
id,
|
|
1842
|
+
status: job.status,
|
|
1843
|
+
completedAt: job.completed_at,
|
|
1844
|
+
error: job.error
|
|
1845
|
+
});
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const oldJob = { ...job };
|
|
1849
|
+
job.progress = progress;
|
|
1850
|
+
job.progress_message = message;
|
|
1851
|
+
job.progress_details = details;
|
|
1852
|
+
this.events.emit("change", { type: "UPDATE", old: oldJob, new: job });
|
|
1853
|
+
}
|
|
1854
|
+
async complete(job) {
|
|
1855
|
+
await sleep2(0);
|
|
1856
|
+
const jobWithPrefixes = job;
|
|
1857
|
+
const index = this.jobQueue.findIndex((j) => j.id === job.id && this.matchesPrefixes(j));
|
|
1858
|
+
if (index !== -1) {
|
|
1859
|
+
const existing = this.jobQueue[index];
|
|
1860
|
+
const currentAttempts = existing?.run_attempts ?? 0;
|
|
1861
|
+
jobWithPrefixes.run_attempts = currentAttempts + 1;
|
|
1862
|
+
for (const [key, value] of Object.entries(this.prefixValues)) {
|
|
1863
|
+
jobWithPrefixes[key] = value;
|
|
1864
|
+
}
|
|
1865
|
+
this.jobQueue[index] = jobWithPrefixes;
|
|
1866
|
+
this.events.emit("change", { type: "UPDATE", old: existing, new: jobWithPrefixes });
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
async release(id) {
|
|
1870
|
+
await sleep2(0);
|
|
1871
|
+
const job = this.jobQueue.find((j) => j.id === id && this.matchesPrefixes(j));
|
|
1872
|
+
if (job) {
|
|
1873
|
+
const oldJob = { ...job };
|
|
1874
|
+
job.status = JobStatus.PENDING;
|
|
1875
|
+
job.worker_id = null;
|
|
1876
|
+
job.progress = 0;
|
|
1877
|
+
job.progress_message = "";
|
|
1878
|
+
job.progress_details = null;
|
|
1879
|
+
this.events.emit("change", { type: "UPDATE", old: oldJob, new: job });
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
async abort(id) {
|
|
1883
|
+
await sleep2(0);
|
|
1884
|
+
const job = this.jobQueue.find((j) => j.id === id && this.matchesPrefixes(j));
|
|
1885
|
+
if (job) {
|
|
1886
|
+
const oldJob = { ...job };
|
|
1887
|
+
job.status = JobStatus.ABORTING;
|
|
1888
|
+
this.events.emit("change", { type: "UPDATE", old: oldJob, new: job });
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
async getByRunId(runId) {
|
|
1892
|
+
await sleep2(0);
|
|
1893
|
+
return this.jobQueue.filter((job) => this.matchesPrefixes(job) && job.job_run_id === runId);
|
|
1894
|
+
}
|
|
1895
|
+
async deleteAll() {
|
|
1896
|
+
await sleep2(0);
|
|
1897
|
+
const deletedJobs = this.jobQueue.filter((job) => this.matchesPrefixes(job));
|
|
1898
|
+
this.jobQueue = this.jobQueue.filter((job) => !this.matchesPrefixes(job));
|
|
1899
|
+
for (const job of deletedJobs) {
|
|
1900
|
+
this.events.emit("change", { type: "DELETE", old: job });
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
async outputForInput(input) {
|
|
1904
|
+
await sleep2(0);
|
|
1905
|
+
const fingerprint = await makeFingerprint(input);
|
|
1906
|
+
return this.jobQueue.find((j) => this.matchesPrefixes(j) && j.fingerprint === fingerprint && j.status === JobStatus.COMPLETED)?.output ?? null;
|
|
1907
|
+
}
|
|
1908
|
+
async delete(id) {
|
|
1909
|
+
await sleep2(0);
|
|
1910
|
+
const deletedJob = this.jobQueue.find((job) => job.id === id && this.matchesPrefixes(job));
|
|
1911
|
+
this.jobQueue = this.jobQueue.filter((job) => !(job.id === id && this.matchesPrefixes(job)));
|
|
1912
|
+
if (deletedJob) {
|
|
1913
|
+
this.events.emit("change", { type: "DELETE", old: deletedJob });
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
async deleteJobsByStatusAndAge(status, olderThanMs) {
|
|
1917
|
+
await sleep2(0);
|
|
1918
|
+
const cutoffDate = new Date(Date.now() - olderThanMs).toISOString();
|
|
1919
|
+
const deletedJobs = this.jobQueue.filter((job) => this.matchesPrefixes(job) && job.status === status && job.completed_at && job.completed_at <= cutoffDate);
|
|
1920
|
+
this.jobQueue = this.jobQueue.filter((job) => !this.matchesPrefixes(job) || job.status !== status || !job.completed_at || job.completed_at > cutoffDate);
|
|
1921
|
+
for (const job of deletedJobs) {
|
|
1922
|
+
this.events.emit("change", { type: "DELETE", old: job });
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
async setupDatabase() {}
|
|
1926
|
+
matchesPrefixFilter(job, prefixFilter) {
|
|
1927
|
+
if (prefixFilter && Object.keys(prefixFilter).length === 0) {
|
|
1928
|
+
return true;
|
|
1929
|
+
}
|
|
1930
|
+
const filterValues = prefixFilter ?? this.prefixValues;
|
|
1931
|
+
if (Object.keys(filterValues).length === 0) {
|
|
1932
|
+
return true;
|
|
1933
|
+
}
|
|
1934
|
+
const jobWithPrefixes = job;
|
|
1935
|
+
for (const [key, value] of Object.entries(filterValues)) {
|
|
1936
|
+
if (jobWithPrefixes[key] !== value) {
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return true;
|
|
1941
|
+
}
|
|
1942
|
+
subscribeToChanges(callback, options) {
|
|
1943
|
+
const prefixFilter = options?.prefixFilter;
|
|
1944
|
+
const filteredCallback = (change) => {
|
|
1945
|
+
const newMatches = change.new ? this.matchesPrefixFilter(change.new, prefixFilter) : false;
|
|
1946
|
+
const oldMatches = change.old ? this.matchesPrefixFilter(change.old, prefixFilter) : false;
|
|
1947
|
+
if (!newMatches && !oldMatches) {
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
callback(change);
|
|
1951
|
+
};
|
|
1952
|
+
return this.events.subscribe("change", filteredCallback);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// src/queue-storage/TelemetryQueueStorage.ts
|
|
1957
|
+
import { traced } from "@workglow/util";
|
|
1958
|
+
|
|
1959
|
+
class TelemetryQueueStorage {
|
|
1960
|
+
storageName;
|
|
1961
|
+
inner;
|
|
1962
|
+
constructor(storageName, inner) {
|
|
1963
|
+
this.storageName = storageName;
|
|
1964
|
+
this.inner = inner;
|
|
1965
|
+
}
|
|
1966
|
+
get scope() {
|
|
1967
|
+
return this.inner.scope;
|
|
1968
|
+
}
|
|
1969
|
+
add(job) {
|
|
1970
|
+
return traced("workglow.storage.queue.add", this.storageName, () => this.inner.add(job));
|
|
1971
|
+
}
|
|
1972
|
+
get(id) {
|
|
1973
|
+
return traced("workglow.storage.queue.get", this.storageName, () => this.inner.get(id));
|
|
1974
|
+
}
|
|
1975
|
+
next(workerId) {
|
|
1976
|
+
return traced("workglow.storage.queue.next", this.storageName, () => this.inner.next(workerId));
|
|
1977
|
+
}
|
|
1978
|
+
peek(status, num) {
|
|
1979
|
+
return traced("workglow.storage.queue.peek", this.storageName, () => this.inner.peek(status, num));
|
|
1980
|
+
}
|
|
1981
|
+
size(status) {
|
|
1982
|
+
return traced("workglow.storage.queue.size", this.storageName, () => this.inner.size(status));
|
|
1983
|
+
}
|
|
1984
|
+
complete(job) {
|
|
1985
|
+
return traced("workglow.storage.queue.complete", this.storageName, () => this.inner.complete(job));
|
|
1986
|
+
}
|
|
1987
|
+
release(id) {
|
|
1988
|
+
return traced("workglow.storage.queue.release", this.storageName, () => this.inner.release(id));
|
|
1989
|
+
}
|
|
1990
|
+
deleteAll() {
|
|
1991
|
+
return traced("workglow.storage.queue.deleteAll", this.storageName, () => this.inner.deleteAll());
|
|
1992
|
+
}
|
|
1993
|
+
outputForInput(input) {
|
|
1994
|
+
return traced("workglow.storage.queue.outputForInput", this.storageName, () => this.inner.outputForInput(input));
|
|
1995
|
+
}
|
|
1996
|
+
abort(id) {
|
|
1997
|
+
return traced("workglow.storage.queue.abort", this.storageName, () => this.inner.abort(id));
|
|
1998
|
+
}
|
|
1999
|
+
getByRunId(runId) {
|
|
2000
|
+
return traced("workglow.storage.queue.getByRunId", this.storageName, () => this.inner.getByRunId(runId));
|
|
2001
|
+
}
|
|
2002
|
+
saveProgress(id, progress, message, details) {
|
|
2003
|
+
return traced("workglow.storage.queue.saveProgress", this.storageName, () => this.inner.saveProgress(id, progress, message, details));
|
|
2004
|
+
}
|
|
2005
|
+
delete(id) {
|
|
2006
|
+
return traced("workglow.storage.queue.delete", this.storageName, () => this.inner.delete(id));
|
|
2007
|
+
}
|
|
2008
|
+
deleteJobsByStatusAndAge(status, olderThanMs) {
|
|
2009
|
+
return traced("workglow.storage.queue.deleteJobsByStatusAndAge", this.storageName, () => this.inner.deleteJobsByStatusAndAge(status, olderThanMs));
|
|
2010
|
+
}
|
|
2011
|
+
setupDatabase() {
|
|
2012
|
+
return this.inner.setupDatabase();
|
|
2013
|
+
}
|
|
2014
|
+
subscribeToChanges(callback, options) {
|
|
2015
|
+
return this.inner.subscribeToChanges(callback, options);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/rate-limiter-storage/IRateLimiterStorage.ts
|
|
2020
|
+
import { createServiceToken as createServiceToken7 } from "@workglow/util";
|
|
2021
|
+
var RATE_LIMITER_STORAGE = createServiceToken7("ratelimiter.storage");
|
|
2022
|
+
|
|
2023
|
+
// src/rate-limiter-storage/InMemoryRateLimiterStorage.ts
|
|
2024
|
+
import { createServiceToken as createServiceToken8, sleep as sleep3, uuid4 as uuid43 } from "@workglow/util";
|
|
2025
|
+
var IN_MEMORY_RATE_LIMITER_STORAGE = createServiceToken8("ratelimiter.storage.inMemory");
|
|
2026
|
+
|
|
2027
|
+
class InMemoryRateLimiterStorage {
|
|
2028
|
+
scope = "process";
|
|
2029
|
+
prefixValues;
|
|
2030
|
+
executions = new Map;
|
|
2031
|
+
nextAvailableTimes = new Map;
|
|
2032
|
+
reserveChains = new Map;
|
|
2033
|
+
constructor(options) {
|
|
2034
|
+
this.prefixValues = options?.prefixValues ?? {};
|
|
2035
|
+
}
|
|
2036
|
+
makeKey(queueName) {
|
|
2037
|
+
const prefixPart = Object.entries(this.prefixValues).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join("|");
|
|
2038
|
+
return prefixPart ? `${prefixPart}|${queueName}` : queueName;
|
|
2039
|
+
}
|
|
2040
|
+
async setupDatabase() {}
|
|
2041
|
+
async withKeyLock(key, fn) {
|
|
2042
|
+
const previous = this.reserveChains.get(key) ?? Promise.resolve();
|
|
2043
|
+
let release;
|
|
2044
|
+
const next = new Promise((resolve) => {
|
|
2045
|
+
release = resolve;
|
|
2046
|
+
});
|
|
2047
|
+
this.reserveChains.set(key, next);
|
|
2048
|
+
try {
|
|
2049
|
+
await previous;
|
|
2050
|
+
return await fn();
|
|
2051
|
+
} finally {
|
|
2052
|
+
release(undefined);
|
|
2053
|
+
if (this.reserveChains.get(key) === next) {
|
|
2054
|
+
this.reserveChains.delete(key);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
async tryReserveExecution(queueName, maxExecutions, windowMs) {
|
|
2059
|
+
const key = this.makeKey(queueName);
|
|
2060
|
+
return this.withKeyLock(key, () => {
|
|
2061
|
+
const now = Date.now();
|
|
2062
|
+
const windowStart = new Date(now - windowMs);
|
|
2063
|
+
const executions = this.executions.get(key) ?? [];
|
|
2064
|
+
const live = executions.filter((e) => e.executedAt > windowStart);
|
|
2065
|
+
if (live.length >= maxExecutions) {
|
|
2066
|
+
if (live.length !== executions.length) {
|
|
2067
|
+
this.executions.set(key, live);
|
|
2068
|
+
}
|
|
2069
|
+
return null;
|
|
2070
|
+
}
|
|
2071
|
+
const next = this.nextAvailableTimes.get(key);
|
|
2072
|
+
if (next && next.getTime() > now) {
|
|
2073
|
+
return null;
|
|
2074
|
+
}
|
|
2075
|
+
const id = uuid43();
|
|
2076
|
+
live.push({ id, queueName, executedAt: new Date(now) });
|
|
2077
|
+
this.executions.set(key, live);
|
|
2078
|
+
return id;
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
async releaseExecution(queueName, token) {
|
|
2082
|
+
if (token === null || token === undefined)
|
|
2083
|
+
return;
|
|
2084
|
+
const key = this.makeKey(queueName);
|
|
2085
|
+
await this.withKeyLock(key, () => {
|
|
2086
|
+
const executions = this.executions.get(key);
|
|
2087
|
+
if (!executions || executions.length === 0)
|
|
2088
|
+
return;
|
|
2089
|
+
const idx = executions.findIndex((e) => e.id === token);
|
|
2090
|
+
if (idx === -1)
|
|
2091
|
+
return;
|
|
2092
|
+
executions.splice(idx, 1);
|
|
2093
|
+
this.executions.set(key, executions);
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
async recordExecution(queueName) {
|
|
2097
|
+
await sleep3(0);
|
|
2098
|
+
const key = this.makeKey(queueName);
|
|
2099
|
+
const executions = this.executions.get(key) ?? [];
|
|
2100
|
+
executions.push({
|
|
2101
|
+
id: uuid43(),
|
|
2102
|
+
queueName,
|
|
2103
|
+
executedAt: new Date
|
|
2104
|
+
});
|
|
2105
|
+
this.executions.set(key, executions);
|
|
2106
|
+
}
|
|
2107
|
+
async getExecutionCount(queueName, windowStartTime) {
|
|
2108
|
+
await sleep3(0);
|
|
2109
|
+
const key = this.makeKey(queueName);
|
|
2110
|
+
const executions = this.executions.get(key) ?? [];
|
|
2111
|
+
const windowStart = new Date(windowStartTime);
|
|
2112
|
+
return executions.filter((e) => e.executedAt > windowStart).length;
|
|
2113
|
+
}
|
|
2114
|
+
async getOldestExecutionAtOffset(queueName, offset) {
|
|
2115
|
+
await sleep3(0);
|
|
2116
|
+
const key = this.makeKey(queueName);
|
|
2117
|
+
const executions = this.executions.get(key) ?? [];
|
|
2118
|
+
const sorted = [...executions].sort((a, b) => a.executedAt.getTime() - b.executedAt.getTime());
|
|
2119
|
+
const execution = sorted[offset];
|
|
2120
|
+
return execution?.executedAt.toISOString();
|
|
2121
|
+
}
|
|
2122
|
+
async getNextAvailableTime(queueName) {
|
|
2123
|
+
await sleep3(0);
|
|
2124
|
+
const key = this.makeKey(queueName);
|
|
2125
|
+
const time = this.nextAvailableTimes.get(key);
|
|
2126
|
+
return time?.toISOString();
|
|
2127
|
+
}
|
|
2128
|
+
async setNextAvailableTime(queueName, nextAvailableAt) {
|
|
2129
|
+
await sleep3(0);
|
|
2130
|
+
const key = this.makeKey(queueName);
|
|
2131
|
+
this.nextAvailableTimes.set(key, new Date(nextAvailableAt));
|
|
2132
|
+
}
|
|
2133
|
+
async clear(queueName) {
|
|
2134
|
+
await sleep3(0);
|
|
2135
|
+
const key = this.makeKey(queueName);
|
|
2136
|
+
this.executions.delete(key);
|
|
2137
|
+
this.nextAvailableTimes.delete(key);
|
|
1568
2138
|
}
|
|
1569
2139
|
}
|
|
1570
2140
|
export {
|
|
@@ -1573,8 +2143,11 @@ export {
|
|
|
1573
2143
|
formatErrorChainForDiagnostics,
|
|
1574
2144
|
classToStorage,
|
|
1575
2145
|
applyPersistedDiagnosticsToStack,
|
|
2146
|
+
TelemetryQueueStorage,
|
|
1576
2147
|
RetryableJobError,
|
|
1577
2148
|
RateLimiter,
|
|
2149
|
+
RATE_LIMITER_STORAGE,
|
|
2150
|
+
QUEUE_STORAGE,
|
|
1578
2151
|
PermanentJobError,
|
|
1579
2152
|
NullLimiter,
|
|
1580
2153
|
NULL_JOB_LIMITER,
|
|
@@ -1588,6 +2161,10 @@ export {
|
|
|
1588
2161
|
Job,
|
|
1589
2162
|
JOB_LIMITER,
|
|
1590
2163
|
JOB_ERROR_DIAGNOSTICS_MARKER,
|
|
2164
|
+
InMemoryRateLimiterStorage,
|
|
2165
|
+
InMemoryQueueStorage,
|
|
2166
|
+
IN_MEMORY_RATE_LIMITER_STORAGE,
|
|
2167
|
+
IN_MEMORY_QUEUE_STORAGE,
|
|
1591
2168
|
EvenlySpacedRateLimiter,
|
|
1592
2169
|
EVENLY_SPACED_JOB_RATE_LIMITER,
|
|
1593
2170
|
DelayLimiter,
|
|
@@ -1597,4 +2174,4 @@ export {
|
|
|
1597
2174
|
AbortSignalJobError
|
|
1598
2175
|
};
|
|
1599
2176
|
|
|
1600
|
-
//# debugId=
|
|
2177
|
+
//# debugId=4ACCF006917528EA64756E2164756E21
|