pg-boss 12.5.3 → 12.6.0
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/attorney.d.ts.map +1 -1
- package/dist/attorney.js +57 -0
- package/dist/bam.d.ts +14 -0
- package/dist/bam.d.ts.map +1 -0
- package/dist/bam.js +114 -0
- package/dist/boss.d.ts.map +1 -1
- package/dist/boss.js +18 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/manager.d.ts +3 -1
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +230 -50
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +302 -3
- package/dist/plans.d.ts +21 -2
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +376 -63
- package/dist/types.d.ts +73 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -9
package/dist/manager.js
CHANGED
|
@@ -26,6 +26,8 @@ class Manager extends EventEmitter {
|
|
|
26
26
|
queues;
|
|
27
27
|
pendingOffWorkCleanups;
|
|
28
28
|
#spies;
|
|
29
|
+
#localGroupActive;
|
|
30
|
+
#localGroupConfig;
|
|
29
31
|
constructor(db, config) {
|
|
30
32
|
super();
|
|
31
33
|
this.config = config;
|
|
@@ -35,6 +37,8 @@ class Manager extends EventEmitter {
|
|
|
35
37
|
this.queues = null;
|
|
36
38
|
this.pendingOffWorkCleanups = new Set();
|
|
37
39
|
this.#spies = new Map();
|
|
40
|
+
this.#localGroupActive = new Map();
|
|
41
|
+
this.#localGroupConfig = new Map();
|
|
38
42
|
}
|
|
39
43
|
getSpy(name) {
|
|
40
44
|
if (!this.config.__test__enableSpies) {
|
|
@@ -53,6 +57,138 @@ class Manager extends EventEmitter {
|
|
|
53
57
|
}
|
|
54
58
|
this.#spies.clear();
|
|
55
59
|
}
|
|
60
|
+
#getLocalGroupLimit(queueName, groupTier) {
|
|
61
|
+
const config = this.#localGroupConfig.get(queueName);
|
|
62
|
+
if (!config)
|
|
63
|
+
return Infinity;
|
|
64
|
+
if (groupTier && config.tiers && groupTier in config.tiers) {
|
|
65
|
+
return config.tiers[groupTier];
|
|
66
|
+
}
|
|
67
|
+
return config.default;
|
|
68
|
+
}
|
|
69
|
+
#getGroupsAtLocalCapacity(queueName) {
|
|
70
|
+
const config = this.#localGroupConfig.get(queueName);
|
|
71
|
+
if (!config)
|
|
72
|
+
return [];
|
|
73
|
+
const queueGroups = this.#localGroupActive.get(queueName);
|
|
74
|
+
if (!queueGroups)
|
|
75
|
+
return [];
|
|
76
|
+
const atCapacity = [];
|
|
77
|
+
for (const [groupId, activeCount] of queueGroups.entries()) {
|
|
78
|
+
// We don't have tier info here, so use default limit
|
|
79
|
+
// Jobs with tiers will be checked individually after fetch
|
|
80
|
+
const limit = config.default;
|
|
81
|
+
if (activeCount >= limit) {
|
|
82
|
+
atCapacity.push(groupId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return atCapacity;
|
|
86
|
+
}
|
|
87
|
+
#incrementLocalGroupCount(queueName, groupId) {
|
|
88
|
+
let queueGroups = this.#localGroupActive.get(queueName);
|
|
89
|
+
if (!queueGroups) {
|
|
90
|
+
queueGroups = new Map();
|
|
91
|
+
this.#localGroupActive.set(queueName, queueGroups);
|
|
92
|
+
}
|
|
93
|
+
const current = queueGroups.get(groupId) || 0;
|
|
94
|
+
queueGroups.set(groupId, current + 1);
|
|
95
|
+
}
|
|
96
|
+
#decrementLocalGroupCount(queueName, groupId) {
|
|
97
|
+
const queueGroups = this.#localGroupActive.get(queueName);
|
|
98
|
+
if (!queueGroups)
|
|
99
|
+
return;
|
|
100
|
+
const current = queueGroups.get(groupId) || 0;
|
|
101
|
+
if (current <= 1) {
|
|
102
|
+
queueGroups.delete(groupId);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
queueGroups.set(groupId, current - 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
#trackJobsActive(name, jobs) {
|
|
109
|
+
const spy = this.config.__test__enableSpies ? this.#spies.get(name) : undefined;
|
|
110
|
+
if (spy) {
|
|
111
|
+
for (const job of jobs) {
|
|
112
|
+
spy.addJob(job.id, name, job.data, 'active');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
#trackJobsCompleted(name, jobs, result) {
|
|
117
|
+
const spy = this.config.__test__enableSpies ? this.#spies.get(name) : undefined;
|
|
118
|
+
if (spy) {
|
|
119
|
+
const output = jobs.length === 1 ? result : undefined;
|
|
120
|
+
for (const job of jobs) {
|
|
121
|
+
spy.addJob(job.id, name, job.data, 'completed', output);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
#trackJobsFailed(name, jobs, err) {
|
|
126
|
+
const spy = this.config.__test__enableSpies ? this.#spies.get(name) : undefined;
|
|
127
|
+
if (spy) {
|
|
128
|
+
for (const job of jobs) {
|
|
129
|
+
spy.addJob(job.id, name, job.data, 'failed', { message: err?.message, stack: err?.stack });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
#storeLocalGroupConfig(name, localGroupConcurrency) {
|
|
134
|
+
const config = typeof localGroupConcurrency === 'number'
|
|
135
|
+
? { default: localGroupConcurrency }
|
|
136
|
+
: localGroupConcurrency;
|
|
137
|
+
this.#localGroupConfig.set(name, config);
|
|
138
|
+
}
|
|
139
|
+
#cleanupLocalGroupTracking(name) {
|
|
140
|
+
// Only cleanup if no more workers exist for this queue
|
|
141
|
+
const hasWorkersForQueue = this.getWorkers().some(w => w.name === name && !w.stopping && !w.stopped);
|
|
142
|
+
if (!hasWorkersForQueue) {
|
|
143
|
+
this.#localGroupConfig.delete(name);
|
|
144
|
+
this.#localGroupActive.delete(name);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
#trackLocalGroupStart(name, jobs) {
|
|
148
|
+
const allowed = [];
|
|
149
|
+
const excess = [];
|
|
150
|
+
const groupedJobs = [];
|
|
151
|
+
for (const job of jobs) {
|
|
152
|
+
if (!job.groupId) {
|
|
153
|
+
// Jobs without group bypass local group limits
|
|
154
|
+
allowed.push(job);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const currentCount = this.#localGroupActive.get(name)?.get(job.groupId) || 0;
|
|
158
|
+
const limit = this.#getLocalGroupLimit(name, job.groupTier);
|
|
159
|
+
if (currentCount < limit) {
|
|
160
|
+
this.#incrementLocalGroupCount(name, job.groupId);
|
|
161
|
+
allowed.push(job);
|
|
162
|
+
groupedJobs.push(job);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
excess.push(job);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { allowed, excess, groupedJobs };
|
|
169
|
+
}
|
|
170
|
+
#trackLocalGroupEnd(name, groupedJobs) {
|
|
171
|
+
for (const job of groupedJobs) {
|
|
172
|
+
if (job.groupId) {
|
|
173
|
+
this.#decrementLocalGroupCount(name, job.groupId);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async #processJobs(name, jobs, callback) {
|
|
178
|
+
const jobIds = jobs.map(job => job.id);
|
|
179
|
+
const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expireInSeconds), 0);
|
|
180
|
+
const ac = new AbortController();
|
|
181
|
+
jobs.forEach(job => { job.signal = ac.signal; });
|
|
182
|
+
try {
|
|
183
|
+
const result = await resolveWithinSeconds(callback(jobs), maxExpiration, `handler execution exceeded ${maxExpiration}s`, ac);
|
|
184
|
+
await this.complete(name, jobIds, jobIds.length === 1 ? result : undefined);
|
|
185
|
+
this.#trackJobsCompleted(name, jobs, result);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
await this.fail(name, jobIds, err);
|
|
189
|
+
this.#trackJobsFailed(name, jobs, err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
56
192
|
async start() {
|
|
57
193
|
this.stopped = false;
|
|
58
194
|
this.queueCacheInterval = setInterval(() => this.onCacheQueues({ emit: true }), this.config.queueCacheIntervalSeconds * 1000);
|
|
@@ -87,6 +223,9 @@ class Manager extends EventEmitter {
|
|
|
87
223
|
await Promise.allSettled([...this.workers.values()]
|
|
88
224
|
.filter(worker => !INTERNAL_QUEUES[worker.name])
|
|
89
225
|
.map(async (worker) => await this.offWork(worker.name, { wait: false })));
|
|
226
|
+
// Clean up all local group tracking on full stop
|
|
227
|
+
this.#localGroupConfig.clear();
|
|
228
|
+
this.#localGroupActive.clear();
|
|
90
229
|
}
|
|
91
230
|
async failWip() {
|
|
92
231
|
for (const worker of this.workers.values()) {
|
|
@@ -101,53 +240,59 @@ class Manager extends EventEmitter {
|
|
|
101
240
|
if (this.stopped) {
|
|
102
241
|
throw new Error('Workers are disabled. pg-boss is stopped');
|
|
103
242
|
}
|
|
104
|
-
const { pollingInterval: interval, batchSize = 1, includeMetadata = false, priority = true } = options;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
243
|
+
const { pollingInterval: interval, batchSize = 1, includeMetadata = false, priority = true, localConcurrency = 1, localGroupConcurrency, groupConcurrency } = options;
|
|
244
|
+
if (localGroupConcurrency != null) {
|
|
245
|
+
this.#storeLocalGroupConfig(name, localGroupConcurrency);
|
|
246
|
+
}
|
|
247
|
+
const firstWorkerId = randomUUID({ disableEntropyCache: true });
|
|
248
|
+
const createWorker = (workerId) => {
|
|
249
|
+
const fetch = () => {
|
|
250
|
+
const ignoreGroups = localGroupConcurrency != null
|
|
251
|
+
? this.#getGroupsAtLocalCapacity(name)
|
|
252
|
+
: undefined;
|
|
253
|
+
return this.fetch(name, { batchSize, includeMetadata, priority, groupConcurrency, ignoreGroups });
|
|
254
|
+
};
|
|
255
|
+
const onFetch = async (jobs) => {
|
|
256
|
+
if (!jobs.length)
|
|
257
|
+
return;
|
|
258
|
+
if (this.config.__test__throw_worker)
|
|
259
|
+
throw new Error('__test__throw_worker');
|
|
260
|
+
this.emitWip(name);
|
|
261
|
+
this.#trackJobsActive(name, jobs);
|
|
262
|
+
// Skip all in-memory group tracking when localGroupConcurrency is not enabled
|
|
263
|
+
if (localGroupConcurrency == null) {
|
|
264
|
+
await this.#processJobs(name, jobs, callback);
|
|
119
265
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const result = await resolveWithinSeconds(callback(jobs), maxExpiration, `handler execution exceeded ${maxExpiration}s`, ac);
|
|
127
|
-
await this.complete(name, jobIds, jobIds.length === 1 ? result : undefined);
|
|
128
|
-
if (spy) {
|
|
129
|
-
for (const job of jobs) {
|
|
130
|
-
spy.addJob(job.id, name, job.data, 'completed', jobIds.length === 1 ? result : undefined);
|
|
266
|
+
else {
|
|
267
|
+
const { allowed, excess, groupedJobs } = this.#trackLocalGroupStart(name, jobs);
|
|
268
|
+
if (excess.length > 0) {
|
|
269
|
+
const excessIds = excess.map(job => job.id);
|
|
270
|
+
await this.restore(name, excessIds);
|
|
131
271
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
272
|
+
if (allowed.length > 0) {
|
|
273
|
+
try {
|
|
274
|
+
await this.#processJobs(name, allowed, callback);
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
this.#trackLocalGroupEnd(name, groupedJobs);
|
|
278
|
+
}
|
|
139
279
|
}
|
|
140
280
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
281
|
+
this.emitWip(name);
|
|
282
|
+
};
|
|
283
|
+
const onError = (error) => {
|
|
284
|
+
this.emit(events.error, { ...error, message: error.message, stack: error.stack, queue: name, worker: workerId });
|
|
285
|
+
};
|
|
286
|
+
return new Worker({ id: workerId, name, options, interval, fetch, onFetch, onError });
|
|
146
287
|
};
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
288
|
+
// Spawn workers based on localConcurrency setting
|
|
289
|
+
for (let i = 0; i < localConcurrency; i++) {
|
|
290
|
+
const workerId = i === 0 ? firstWorkerId : randomUUID({ disableEntropyCache: true });
|
|
291
|
+
const worker = createWorker(workerId);
|
|
292
|
+
this.addWorker(worker);
|
|
293
|
+
worker.start();
|
|
294
|
+
}
|
|
295
|
+
return firstWorkerId;
|
|
151
296
|
}
|
|
152
297
|
addWorker(worker) {
|
|
153
298
|
this.workers.set(worker.id, worker);
|
|
@@ -191,10 +336,14 @@ class Manager extends EventEmitter {
|
|
|
191
336
|
}));
|
|
192
337
|
if (options.wait) {
|
|
193
338
|
await cleanupPromise;
|
|
339
|
+
this.#cleanupLocalGroupTracking(name);
|
|
194
340
|
}
|
|
195
341
|
else {
|
|
196
342
|
this.pendingOffWorkCleanups.add(cleanupPromise);
|
|
197
|
-
cleanupPromise.finally(() =>
|
|
343
|
+
cleanupPromise.finally(() => {
|
|
344
|
+
this.pendingOffWorkCleanups.delete(cleanupPromise);
|
|
345
|
+
this.#cleanupLocalGroupTracking(name);
|
|
346
|
+
});
|
|
198
347
|
}
|
|
199
348
|
}
|
|
200
349
|
notifyWorker(workerId) {
|
|
@@ -246,7 +395,7 @@ class Manager extends EventEmitter {
|
|
|
246
395
|
}
|
|
247
396
|
async createJob(request) {
|
|
248
397
|
const { name, data = null, options = {} } = request;
|
|
249
|
-
const { id = null, db: wrapper, priority, startAfter, singletonKey = null, singletonSeconds, singletonNextSlot, expireInSeconds, deleteAfterSeconds, retentionSeconds, keepUntil, retryLimit, retryDelay, retryBackoff, retryDelayMax } = options;
|
|
398
|
+
const { id = null, db: wrapper, priority, startAfter, singletonKey = null, singletonSeconds, singletonNextSlot, expireInSeconds, deleteAfterSeconds, retentionSeconds, keepUntil, retryLimit, retryDelay, retryBackoff, retryDelayMax, group } = options;
|
|
250
399
|
const job = {
|
|
251
400
|
id,
|
|
252
401
|
name,
|
|
@@ -256,6 +405,8 @@ class Manager extends EventEmitter {
|
|
|
256
405
|
singletonKey,
|
|
257
406
|
singletonSeconds,
|
|
258
407
|
singletonOffset: 0,
|
|
408
|
+
groupId: group?.id ?? null,
|
|
409
|
+
groupTier: group?.tier ?? null,
|
|
259
410
|
expireInSeconds,
|
|
260
411
|
deleteAfterSeconds,
|
|
261
412
|
retentionSeconds,
|
|
@@ -391,21 +542,21 @@ class Manager extends EventEmitter {
|
|
|
391
542
|
const result = await db.executeSql(sql, [name, ids, this.mapCompletionDataArg(data)]);
|
|
392
543
|
return this.mapCommandResponse(ids, result);
|
|
393
544
|
}
|
|
394
|
-
async
|
|
545
|
+
async deleteJob(name, id, options = {}) {
|
|
395
546
|
Attorney.assertQueueName(name);
|
|
396
547
|
const db = this.assertDb(options);
|
|
397
|
-
const ids = this.mapCompletionIdArg(id, '
|
|
548
|
+
const ids = this.mapCompletionIdArg(id, 'deleteJob');
|
|
398
549
|
const { table } = await this.getQueueCache(name);
|
|
399
|
-
const sql = plans.
|
|
550
|
+
const sql = plans.deleteJobsById(this.config.schema, table);
|
|
400
551
|
const result = await db.executeSql(sql, [name, ids]);
|
|
401
552
|
return this.mapCommandResponse(ids, result);
|
|
402
553
|
}
|
|
403
|
-
async
|
|
554
|
+
async cancel(name, id, options = {}) {
|
|
404
555
|
Attorney.assertQueueName(name);
|
|
405
556
|
const db = this.assertDb(options);
|
|
406
|
-
const ids = this.mapCompletionIdArg(id, '
|
|
557
|
+
const ids = this.mapCompletionIdArg(id, 'cancel');
|
|
407
558
|
const { table } = await this.getQueueCache(name);
|
|
408
|
-
const sql = plans.
|
|
559
|
+
const sql = plans.cancelJobs(this.config.schema, table);
|
|
409
560
|
const result = await db.executeSql(sql, [name, ids]);
|
|
410
561
|
return this.mapCommandResponse(ids, result);
|
|
411
562
|
}
|
|
@@ -418,6 +569,14 @@ class Manager extends EventEmitter {
|
|
|
418
569
|
const result = await db.executeSql(sql, [name, ids]);
|
|
419
570
|
return this.mapCommandResponse(ids, result);
|
|
420
571
|
}
|
|
572
|
+
async restore(name, id, options = {}) {
|
|
573
|
+
Attorney.assertQueueName(name);
|
|
574
|
+
const db = this.assertDb(options);
|
|
575
|
+
const ids = this.mapCompletionIdArg(id, 'restore');
|
|
576
|
+
const { table } = await this.getQueueCache(name);
|
|
577
|
+
const sql = plans.restoreJobs(this.config.schema, table);
|
|
578
|
+
await db.executeSql(sql, [name, ids]);
|
|
579
|
+
}
|
|
421
580
|
async retry(name, id, options = {}) {
|
|
422
581
|
Attorney.assertQueueName(name);
|
|
423
582
|
const db = options.db || this.db;
|
|
@@ -540,6 +699,27 @@ class Manager extends EventEmitter {
|
|
|
540
699
|
return null;
|
|
541
700
|
}
|
|
542
701
|
}
|
|
702
|
+
async findJobs(name, options = {}) {
|
|
703
|
+
Attorney.assertQueueName(name);
|
|
704
|
+
const db = this.assertDb(options);
|
|
705
|
+
const { table } = await this.getQueueCache(name);
|
|
706
|
+
const { id, key, data, queued = false } = options;
|
|
707
|
+
const sql = plans.findJobs(this.config.schema, table, {
|
|
708
|
+
byId: id !== undefined,
|
|
709
|
+
byKey: key !== undefined,
|
|
710
|
+
byData: data !== undefined,
|
|
711
|
+
queued
|
|
712
|
+
});
|
|
713
|
+
const values = [name];
|
|
714
|
+
if (id !== undefined)
|
|
715
|
+
values.push(id);
|
|
716
|
+
if (key !== undefined)
|
|
717
|
+
values.push(key);
|
|
718
|
+
if (data !== undefined)
|
|
719
|
+
values.push(JSON.stringify(data));
|
|
720
|
+
const result = await db.executeSql(sql, values);
|
|
721
|
+
return result?.rows || [];
|
|
722
|
+
}
|
|
543
723
|
assertDb(options) {
|
|
544
724
|
if (options.db) {
|
|
545
725
|
return options.db;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrationStore.d.ts","sourceRoot":"","sources":["../src/migrationStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AASnC,iBAAS,QAAQ,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,UAQjF;AAED,iBAAS,IAAI,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,SAAS,EAAE,GAAG,SAAS,UAQxF;AAED,iBAAS,OAAO,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"migrationStore.d.ts","sourceRoot":"","sources":["../src/migrationStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AASnC,iBAAS,QAAQ,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,UAQjF;AAED,iBAAS,IAAI,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,SAAS,EAAE,GAAG,SAAS,UAQxF;AAED,iBAAS,OAAO,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,UAuBhF;AAED,iBAAS,MAAM,CAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,SAAS,EAAE,CAkYlD;AAED,OAAO,EACL,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,MAAM,GACP,CAAA"}
|