@workglow/job-queue 0.2.25 → 0.2.27
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 +195 -32
- package/dist/browser.js.map +11 -11
- package/dist/bun.js +195 -32
- package/dist/bun.js.map +11 -11
- package/dist/job/JobQueueServer.d.ts.map +1 -1
- package/dist/job/JobQueueWorker.d.ts +7 -2
- package/dist/job/JobQueueWorker.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 +42 -3
- package/dist/limiter/RateLimiter.d.ts.map +1 -1
- package/dist/node.js +195 -32
- package/dist/node.js.map +11 -11
- package/package.json +5 -5
package/dist/bun.js
CHANGED
|
@@ -563,6 +563,12 @@ import { createServiceToken } from "@workglow/util";
|
|
|
563
563
|
var NULL_JOB_LIMITER = createServiceToken("jobqueue.limiter.null");
|
|
564
564
|
|
|
565
565
|
class NullLimiter {
|
|
566
|
+
scope = "process";
|
|
567
|
+
static SENTINEL = Symbol("NullLimiter.acquired");
|
|
568
|
+
async tryAcquire() {
|
|
569
|
+
return NullLimiter.SENTINEL;
|
|
570
|
+
}
|
|
571
|
+
async release(_token) {}
|
|
566
572
|
async canProceed() {
|
|
567
573
|
return true;
|
|
568
574
|
}
|
|
@@ -585,6 +591,8 @@ import {
|
|
|
585
591
|
SpanStatusCode,
|
|
586
592
|
uuid4
|
|
587
593
|
} from "@workglow/util";
|
|
594
|
+
var MAX_LIMITER_WAKE_MS = 30000;
|
|
595
|
+
|
|
588
596
|
class JobQueueWorker {
|
|
589
597
|
queueName;
|
|
590
598
|
workerId;
|
|
@@ -668,15 +676,16 @@ class JobQueueWorker {
|
|
|
668
676
|
return this;
|
|
669
677
|
}
|
|
670
678
|
async processNext() {
|
|
671
|
-
const canProceed = await this.limiter.canProceed();
|
|
672
|
-
if (!canProceed) {
|
|
673
|
-
return false;
|
|
674
|
-
}
|
|
675
679
|
const job = await this.next();
|
|
676
680
|
if (!job) {
|
|
677
681
|
return false;
|
|
678
682
|
}
|
|
679
|
-
await this.
|
|
683
|
+
const limiterToken = await this.limiter.tryAcquire();
|
|
684
|
+
if (limiterToken === null || limiterToken === undefined) {
|
|
685
|
+
await this.releaseClaimedJob(job);
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
await this.processSingleJob(job, limiterToken);
|
|
680
689
|
return true;
|
|
681
690
|
}
|
|
682
691
|
isRunning() {
|
|
@@ -707,29 +716,46 @@ class JobQueueWorker {
|
|
|
707
716
|
while (this.running) {
|
|
708
717
|
try {
|
|
709
718
|
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) {
|
|
719
|
+
const job = await this.next();
|
|
720
|
+
if (!job) {
|
|
723
721
|
const delay = await this.getIdleDelay();
|
|
724
722
|
await this.waitForWakeOrTimeout(delay);
|
|
725
|
-
|
|
726
|
-
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (!this.running) {
|
|
726
|
+
await this.releaseClaimedJob(job);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const limiterToken = await this.limiter.tryAcquire();
|
|
730
|
+
if (limiterToken === null || limiterToken === undefined) {
|
|
731
|
+
await this.releaseClaimedJob(job);
|
|
732
|
+
await this.waitForWakeOrTimeout(await this.getLimiterWakeDelay());
|
|
733
|
+
continue;
|
|
727
734
|
}
|
|
735
|
+
if (!this.running) {
|
|
736
|
+
try {
|
|
737
|
+
await this.limiter.release(limiterToken);
|
|
738
|
+
} catch {}
|
|
739
|
+
await this.releaseClaimedJob(job);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
this.processSingleJob(job, limiterToken);
|
|
728
743
|
} catch {
|
|
729
744
|
await sleep(this.pollIntervalMs);
|
|
730
745
|
}
|
|
731
746
|
}
|
|
732
747
|
}
|
|
748
|
+
async getLimiterWakeDelay() {
|
|
749
|
+
try {
|
|
750
|
+
const next = await this.limiter.getNextAvailableTime();
|
|
751
|
+
const delay = next.getTime() - Date.now();
|
|
752
|
+
if (delay <= 0)
|
|
753
|
+
return this.pollIntervalMs;
|
|
754
|
+
return Math.min(Math.max(delay, this.pollIntervalMs), MAX_LIMITER_WAKE_MS);
|
|
755
|
+
} catch {
|
|
756
|
+
return this.pollIntervalMs;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
733
759
|
async getIdleDelay() {
|
|
734
760
|
try {
|
|
735
761
|
const pending = await this.storage.peek(JobStatus3.PENDING, 1);
|
|
@@ -772,7 +798,7 @@ class JobQueueWorker {
|
|
|
772
798
|
}
|
|
773
799
|
}
|
|
774
800
|
}
|
|
775
|
-
async processSingleJob(job) {
|
|
801
|
+
async processSingleJob(job, limiterToken) {
|
|
776
802
|
if (!job || !job.id) {
|
|
777
803
|
throw new JobNotFoundError("Invalid job provided for processing");
|
|
778
804
|
}
|
|
@@ -789,9 +815,17 @@ class JobQueueWorker {
|
|
|
789
815
|
"workglow.job.max_retries": job.maxRetries
|
|
790
816
|
}
|
|
791
817
|
}) : undefined;
|
|
818
|
+
let slotReleased = false;
|
|
792
819
|
try {
|
|
793
|
-
|
|
794
|
-
|
|
820
|
+
try {
|
|
821
|
+
await this.validateJobState(job);
|
|
822
|
+
} catch (validationErr) {
|
|
823
|
+
try {
|
|
824
|
+
await this.limiter.release(limiterToken);
|
|
825
|
+
slotReleased = true;
|
|
826
|
+
} catch {}
|
|
827
|
+
throw validationErr;
|
|
828
|
+
}
|
|
795
829
|
const abortController = this.createAbortController(job.id);
|
|
796
830
|
this.events.emit("job_start", job.id);
|
|
797
831
|
const output = await this.executeJob(job, abortController.signal);
|
|
@@ -829,7 +863,9 @@ class JobQueueWorker {
|
|
|
829
863
|
} finally {
|
|
830
864
|
span?.end();
|
|
831
865
|
try {
|
|
832
|
-
|
|
866
|
+
if (!slotReleased) {
|
|
867
|
+
await this.limiter.recordJobCompletion();
|
|
868
|
+
}
|
|
833
869
|
} finally {
|
|
834
870
|
this.inFlight.delete(job.id);
|
|
835
871
|
resolveInFlight();
|
|
@@ -1039,10 +1075,17 @@ class JobQueueServer {
|
|
|
1039
1075
|
}
|
|
1040
1076
|
this.running = true;
|
|
1041
1077
|
this.events.emit("server_start", this.queueName);
|
|
1078
|
+
if (this.limiter.scope === "process" && this.storage.scope === "cluster" && !(this.limiter instanceof NullLimiter)) {
|
|
1079
|
+
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.", {
|
|
1080
|
+
queueName: this.queueName,
|
|
1081
|
+
limiterScope: this.limiter.scope,
|
|
1082
|
+
storage: this.storage.constructor.name
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1042
1085
|
await this.fixupJobs();
|
|
1043
1086
|
try {
|
|
1044
1087
|
this.storageUnsubscribe = this.storage.subscribeToChanges((change) => {
|
|
1045
|
-
if (change.type === "INSERT" || change.type === "UPDATE" && change.new?.status === JobStatus4.PENDING) {
|
|
1088
|
+
if (change.type === "INSERT" || change.type === "RESYNC" || change.type === "UPDATE" && change.new?.status === JobStatus4.PENDING) {
|
|
1046
1089
|
this.notifyWorkers();
|
|
1047
1090
|
}
|
|
1048
1091
|
});
|
|
@@ -1297,6 +1340,9 @@ class CompositeLimiter {
|
|
|
1297
1340
|
constructor(limiters = []) {
|
|
1298
1341
|
this.limiters = limiters;
|
|
1299
1342
|
}
|
|
1343
|
+
get scope() {
|
|
1344
|
+
return this.limiters.every((l) => l.scope === "cluster") && this.limiters.length > 0 ? "cluster" : "process";
|
|
1345
|
+
}
|
|
1300
1346
|
addLimiter(limiter) {
|
|
1301
1347
|
this.limiters.push(limiter);
|
|
1302
1348
|
}
|
|
@@ -1308,6 +1354,27 @@ class CompositeLimiter {
|
|
|
1308
1354
|
}
|
|
1309
1355
|
return true;
|
|
1310
1356
|
}
|
|
1357
|
+
async tryAcquire() {
|
|
1358
|
+
const tokens = [];
|
|
1359
|
+
for (const limiter of this.limiters) {
|
|
1360
|
+
const t = await limiter.tryAcquire();
|
|
1361
|
+
if (t === null || t === undefined) {
|
|
1362
|
+
for (let i = tokens.length - 1;i >= 0; i--) {
|
|
1363
|
+
try {
|
|
1364
|
+
await this.limiters[i].release(tokens[i]);
|
|
1365
|
+
} catch {}
|
|
1366
|
+
}
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
tokens.push(t);
|
|
1370
|
+
}
|
|
1371
|
+
return tokens;
|
|
1372
|
+
}
|
|
1373
|
+
async release(token) {
|
|
1374
|
+
if (!Array.isArray(token))
|
|
1375
|
+
return;
|
|
1376
|
+
await Promise.all(this.limiters.map((l, i) => l.release(token[i]).catch(() => {})));
|
|
1377
|
+
}
|
|
1311
1378
|
async recordJobStart() {
|
|
1312
1379
|
await Promise.all(this.limiters.map((limiter) => limiter.recordJobStart()));
|
|
1313
1380
|
}
|
|
@@ -1339,6 +1406,7 @@ import { createServiceToken as createServiceToken2 } from "@workglow/util";
|
|
|
1339
1406
|
var CONCURRENT_JOB_LIMITER = createServiceToken2("jobqueue.limiter.concurrent");
|
|
1340
1407
|
|
|
1341
1408
|
class ConcurrencyLimiter {
|
|
1409
|
+
scope = "process";
|
|
1342
1410
|
currentRunningJobs = 0;
|
|
1343
1411
|
maxConcurrentJobs;
|
|
1344
1412
|
nextAllowedStartTime = new Date;
|
|
@@ -1348,6 +1416,19 @@ class ConcurrencyLimiter {
|
|
|
1348
1416
|
async canProceed() {
|
|
1349
1417
|
return this.currentRunningJobs < this.maxConcurrentJobs && Date.now() >= this.nextAllowedStartTime.getTime();
|
|
1350
1418
|
}
|
|
1419
|
+
static SENTINEL = Symbol("ConcurrencyLimiter.acquired");
|
|
1420
|
+
async tryAcquire() {
|
|
1421
|
+
if (this.currentRunningJobs >= this.maxConcurrentJobs || Date.now() < this.nextAllowedStartTime.getTime()) {
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
this.currentRunningJobs++;
|
|
1425
|
+
return ConcurrencyLimiter.SENTINEL;
|
|
1426
|
+
}
|
|
1427
|
+
async release(token) {
|
|
1428
|
+
if (token !== ConcurrencyLimiter.SENTINEL)
|
|
1429
|
+
return;
|
|
1430
|
+
this.currentRunningJobs = Math.max(0, this.currentRunningJobs - 1);
|
|
1431
|
+
}
|
|
1351
1432
|
async recordJobStart() {
|
|
1352
1433
|
this.currentRunningJobs++;
|
|
1353
1434
|
}
|
|
@@ -1371,13 +1452,32 @@ class ConcurrencyLimiter {
|
|
|
1371
1452
|
// src/limiter/DelayLimiter.ts
|
|
1372
1453
|
class DelayLimiter {
|
|
1373
1454
|
delayInMilliseconds;
|
|
1455
|
+
scope = "process";
|
|
1374
1456
|
nextAvailableTime = new Date;
|
|
1457
|
+
lastAcquireBaseline = 0;
|
|
1375
1458
|
constructor(delayInMilliseconds = 50) {
|
|
1376
1459
|
this.delayInMilliseconds = delayInMilliseconds;
|
|
1377
1460
|
}
|
|
1378
1461
|
async canProceed() {
|
|
1379
1462
|
return Date.now() >= this.nextAvailableTime.getTime();
|
|
1380
1463
|
}
|
|
1464
|
+
async tryAcquire() {
|
|
1465
|
+
const now = Date.now();
|
|
1466
|
+
if (now < this.nextAvailableTime.getTime()) {
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
const previous = this.nextAvailableTime.getTime();
|
|
1470
|
+
this.lastAcquireBaseline = previous;
|
|
1471
|
+
this.nextAvailableTime = new Date(now + this.delayInMilliseconds);
|
|
1472
|
+
return previous;
|
|
1473
|
+
}
|
|
1474
|
+
async release(token) {
|
|
1475
|
+
if (typeof token !== "number")
|
|
1476
|
+
return;
|
|
1477
|
+
if (this.nextAvailableTime.getTime() === this.lastAcquireBaseline + this.delayInMilliseconds) {
|
|
1478
|
+
this.nextAvailableTime = new Date(token);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1381
1481
|
async recordJobStart() {
|
|
1382
1482
|
this.nextAvailableTime = new Date(Date.now() + this.delayInMilliseconds);
|
|
1383
1483
|
}
|
|
@@ -1400,12 +1500,14 @@ import { createServiceToken as createServiceToken3 } from "@workglow/util";
|
|
|
1400
1500
|
var EVENLY_SPACED_JOB_RATE_LIMITER = createServiceToken3("jobqueue.limiter.rate.evenlyspaced");
|
|
1401
1501
|
|
|
1402
1502
|
class EvenlySpacedRateLimiter {
|
|
1503
|
+
scope = "process";
|
|
1403
1504
|
maxExecutions;
|
|
1404
1505
|
windowSizeMs;
|
|
1405
1506
|
idealInterval;
|
|
1406
1507
|
nextAvailableTime = Date.now();
|
|
1407
1508
|
lastStartTime = 0;
|
|
1408
1509
|
durations = [];
|
|
1510
|
+
acquireChain = Promise.resolve();
|
|
1409
1511
|
constructor({ maxExecutions, windowSizeInSeconds }) {
|
|
1410
1512
|
if (maxExecutions <= 0) {
|
|
1411
1513
|
throw new Error("maxExecutions must be > 0");
|
|
@@ -1421,6 +1523,43 @@ class EvenlySpacedRateLimiter {
|
|
|
1421
1523
|
const now = Date.now();
|
|
1422
1524
|
return now >= this.nextAvailableTime;
|
|
1423
1525
|
}
|
|
1526
|
+
async tryAcquire() {
|
|
1527
|
+
const previous = this.acquireChain;
|
|
1528
|
+
let release;
|
|
1529
|
+
const next = new Promise((r) => {
|
|
1530
|
+
release = r;
|
|
1531
|
+
});
|
|
1532
|
+
this.acquireChain = next;
|
|
1533
|
+
try {
|
|
1534
|
+
await previous;
|
|
1535
|
+
const now = Date.now();
|
|
1536
|
+
if (now < this.nextAvailableTime) {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
const priorNextAvailable = this.nextAvailableTime;
|
|
1540
|
+
this.lastStartTime = now;
|
|
1541
|
+
if (this.durations.length === 0) {
|
|
1542
|
+
this.nextAvailableTime = now + this.idealInterval;
|
|
1543
|
+
} else {
|
|
1544
|
+
const sum = this.durations.reduce((a, b) => a + b, 0);
|
|
1545
|
+
const avgDuration = sum / this.durations.length;
|
|
1546
|
+
const waitMs = Math.max(0, this.idealInterval - avgDuration);
|
|
1547
|
+
this.nextAvailableTime = now + waitMs;
|
|
1548
|
+
}
|
|
1549
|
+
return { priorNextAvailable, advancedTo: this.nextAvailableTime };
|
|
1550
|
+
} finally {
|
|
1551
|
+
release(undefined);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
async release(token) {
|
|
1555
|
+
if (!token || typeof token !== "object" || typeof token.advancedTo !== "number") {
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
const t = token;
|
|
1559
|
+
if (this.nextAvailableTime === t.advancedTo) {
|
|
1560
|
+
this.nextAvailableTime = t.priorNextAvailable;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1424
1563
|
async recordJobStart() {
|
|
1425
1564
|
const now = Date.now();
|
|
1426
1565
|
this.lastStartTime = now;
|
|
@@ -1471,6 +1610,7 @@ class RateLimiter {
|
|
|
1471
1610
|
initialBackoffDelay;
|
|
1472
1611
|
backoffMultiplier;
|
|
1473
1612
|
maxBackoffDelay;
|
|
1613
|
+
localBackoffUntilMs = 0;
|
|
1474
1614
|
constructor(storage, queueName, {
|
|
1475
1615
|
maxExecutions,
|
|
1476
1616
|
windowSizeInSeconds,
|
|
@@ -1502,6 +1642,27 @@ class RateLimiter {
|
|
|
1502
1642
|
this.maxBackoffDelay = maxBackoffDelay;
|
|
1503
1643
|
this.currentBackoffDelay = initialBackoffDelay;
|
|
1504
1644
|
}
|
|
1645
|
+
get scope() {
|
|
1646
|
+
return this.storage.scope;
|
|
1647
|
+
}
|
|
1648
|
+
async tryAcquire() {
|
|
1649
|
+
const token = await this.storage.tryReserveExecution(this.queueName, this.maxExecutions, this.windowSizeInMilliseconds);
|
|
1650
|
+
if (token !== null && token !== undefined) {
|
|
1651
|
+
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1652
|
+
this.localBackoffUntilMs = 0;
|
|
1653
|
+
return token;
|
|
1654
|
+
}
|
|
1655
|
+
this.localBackoffUntilMs = Date.now() + this.addJitter(this.currentBackoffDelay);
|
|
1656
|
+
this.increaseBackoff();
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
async release(token) {
|
|
1660
|
+
if (token === null || token === undefined)
|
|
1661
|
+
return;
|
|
1662
|
+
await this.storage.releaseExecution(this.queueName, token);
|
|
1663
|
+
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1664
|
+
this.localBackoffUntilMs = 0;
|
|
1665
|
+
}
|
|
1505
1666
|
addJitter(base) {
|
|
1506
1667
|
return base + Math.random() * base;
|
|
1507
1668
|
}
|
|
@@ -1547,17 +1708,18 @@ class RateLimiter {
|
|
|
1547
1708
|
async recordJobCompletion() {}
|
|
1548
1709
|
async getNextAvailableTime() {
|
|
1549
1710
|
const oldestExecution = await this.storage.getOldestExecutionAtOffset(this.queueName, this.maxExecutions - 1);
|
|
1550
|
-
let
|
|
1711
|
+
let latestMs = Date.now();
|
|
1551
1712
|
if (oldestExecution) {
|
|
1552
|
-
|
|
1553
|
-
rateLimitedTime.setSeconds(rateLimitedTime.getSeconds() + this.windowSizeInMilliseconds / 1000);
|
|
1713
|
+
latestMs = Math.max(latestMs, new Date(oldestExecution).getTime() + this.windowSizeInMilliseconds);
|
|
1554
1714
|
}
|
|
1555
1715
|
const nextAvailableStr = await this.storage.getNextAvailableTime(this.queueName);
|
|
1556
|
-
let nextAvailableTime = new Date;
|
|
1557
1716
|
if (nextAvailableStr) {
|
|
1558
|
-
|
|
1717
|
+
latestMs = Math.max(latestMs, new Date(nextAvailableStr).getTime());
|
|
1718
|
+
}
|
|
1719
|
+
if (this.localBackoffUntilMs > latestMs) {
|
|
1720
|
+
latestMs = this.localBackoffUntilMs;
|
|
1559
1721
|
}
|
|
1560
|
-
return
|
|
1722
|
+
return new Date(latestMs);
|
|
1561
1723
|
}
|
|
1562
1724
|
async setNextAvailableTime(date) {
|
|
1563
1725
|
await this.storage.setNextAvailableTime(this.queueName, date.toISOString());
|
|
@@ -1565,6 +1727,7 @@ class RateLimiter {
|
|
|
1565
1727
|
async clear() {
|
|
1566
1728
|
await this.storage.clear(this.queueName);
|
|
1567
1729
|
this.currentBackoffDelay = this.initialBackoffDelay;
|
|
1730
|
+
this.localBackoffUntilMs = 0;
|
|
1568
1731
|
}
|
|
1569
1732
|
}
|
|
1570
1733
|
export {
|
|
@@ -1597,4 +1760,4 @@ export {
|
|
|
1597
1760
|
AbortSignalJobError
|
|
1598
1761
|
};
|
|
1599
1762
|
|
|
1600
|
-
//# debugId=
|
|
1763
|
+
//# debugId=81121BC83C83DF5F64756E2164756E21
|