@monque/core 1.5.2 → 1.7.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/CHANGELOG.md +44 -0
- package/dist/index.cjs +522 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +83 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +520 -170
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/index.ts +3 -0
- package/src/scheduler/monque.ts +112 -27
- package/src/scheduler/services/change-stream-handler.ts +183 -31
- package/src/scheduler/services/index.ts +1 -1
- package/src/scheduler/services/job-manager.ts +151 -114
- package/src/scheduler/services/job-processor.ts +109 -54
- package/src/scheduler/services/job-scheduler.ts +42 -9
- package/src/scheduler/services/lifecycle-manager.ts +77 -17
- package/src/scheduler/services/types.ts +7 -0
- package/src/scheduler/types.ts +14 -0
- package/src/shared/errors.ts +29 -0
- package/src/shared/index.ts +3 -0
- package/src/shared/utils/index.ts +1 -0
- package/src/shared/utils/job-identifiers.ts +71 -0
|
@@ -39,47 +39,48 @@ export class JobManager {
|
|
|
39
39
|
|
|
40
40
|
const _id = new ObjectId(jobId);
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
try {
|
|
43
|
+
const now = new Date();
|
|
44
|
+
const result = await this.ctx.collection.findOneAndUpdate(
|
|
45
|
+
{ _id, status: JobStatus.PENDING },
|
|
46
|
+
{
|
|
47
|
+
$set: {
|
|
48
|
+
status: JobStatus.CANCELLED,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{ returnDocument: 'after' },
|
|
53
|
+
);
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
if (!result) {
|
|
56
|
+
const jobDoc = await this.ctx.collection.findOne({ _id });
|
|
57
|
+
if (!jobDoc) return null;
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
jobId,
|
|
54
|
-
jobDoc['status'],
|
|
55
|
-
'cancel',
|
|
56
|
-
);
|
|
57
|
-
}
|
|
59
|
+
if (jobDoc['status'] === JobStatus.CANCELLED) {
|
|
60
|
+
return this.ctx.documentToPersistedJob(jobDoc);
|
|
61
|
+
}
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
throw new JobStateError(
|
|
64
|
+
`Cannot cancel job in status '${jobDoc['status']}'`,
|
|
65
|
+
jobId,
|
|
66
|
+
jobDoc['status'],
|
|
67
|
+
'cancel',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const job = this.ctx.documentToPersistedJob(result);
|
|
72
|
+
this.ctx.emit('job:cancelled', { job });
|
|
73
|
+
return job;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof MonqueError) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
const message = error instanceof Error ? error.message : 'Unknown error during cancelJob';
|
|
79
|
+
throw new ConnectionError(
|
|
80
|
+
`Failed to cancel job: ${message}`,
|
|
81
|
+
error instanceof Error ? { cause: error } : undefined,
|
|
77
82
|
);
|
|
78
83
|
}
|
|
79
|
-
|
|
80
|
-
const job = this.ctx.documentToPersistedJob(result);
|
|
81
|
-
this.ctx.emit('job:cancelled', { job });
|
|
82
|
-
return job;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
/**
|
|
@@ -104,51 +105,69 @@ export class JobManager {
|
|
|
104
105
|
if (!ObjectId.isValid(jobId)) return null;
|
|
105
106
|
|
|
106
107
|
const _id = new ObjectId(jobId);
|
|
107
|
-
const currentJob = await this.ctx.collection.findOne({ _id });
|
|
108
|
-
|
|
109
|
-
if (!currentJob) return null;
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
try {
|
|
110
|
+
const now = new Date();
|
|
111
|
+
const result = await this.ctx.collection.findOneAndUpdate(
|
|
112
|
+
{
|
|
113
|
+
_id,
|
|
114
|
+
status: { $in: [JobStatus.FAILED, JobStatus.CANCELLED] },
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
$set: {
|
|
118
|
+
status: JobStatus.PENDING,
|
|
119
|
+
failCount: 0,
|
|
120
|
+
nextRunAt: now,
|
|
121
|
+
updatedAt: now,
|
|
122
|
+
},
|
|
123
|
+
$unset: {
|
|
124
|
+
failReason: '',
|
|
125
|
+
lockedAt: '',
|
|
126
|
+
claimedBy: '',
|
|
127
|
+
lastHeartbeat: '',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{ returnDocument: 'before' },
|
|
117
131
|
);
|
|
118
|
-
}
|
|
119
132
|
|
|
120
|
-
|
|
133
|
+
if (!result) {
|
|
134
|
+
const currentJob = await this.ctx.collection.findOne({ _id });
|
|
135
|
+
if (!currentJob) return null;
|
|
121
136
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
status: JobStatus.PENDING,
|
|
130
|
-
failCount: 0,
|
|
131
|
-
nextRunAt: new Date(),
|
|
132
|
-
updatedAt: new Date(),
|
|
133
|
-
},
|
|
134
|
-
$unset: {
|
|
135
|
-
failReason: '',
|
|
136
|
-
lockedAt: '',
|
|
137
|
-
claimedBy: '',
|
|
138
|
-
lastHeartbeat: '',
|
|
139
|
-
heartbeatInterval: '',
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
{ returnDocument: 'after' },
|
|
143
|
-
);
|
|
137
|
+
throw new JobStateError(
|
|
138
|
+
`Cannot retry job in status '${currentJob['status']}'`,
|
|
139
|
+
jobId,
|
|
140
|
+
currentJob['status'],
|
|
141
|
+
'retry',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
const previousStatus = result['status'] as 'failed' | 'cancelled';
|
|
146
|
+
|
|
147
|
+
const updatedDoc = { ...result };
|
|
148
|
+
updatedDoc['status'] = JobStatus.PENDING;
|
|
149
|
+
updatedDoc['failCount'] = 0;
|
|
150
|
+
updatedDoc['nextRunAt'] = now;
|
|
151
|
+
updatedDoc['updatedAt'] = now;
|
|
152
|
+
delete updatedDoc['failReason'];
|
|
153
|
+
delete updatedDoc['lockedAt'];
|
|
154
|
+
delete updatedDoc['claimedBy'];
|
|
155
|
+
delete updatedDoc['lastHeartbeat'];
|
|
156
|
+
|
|
157
|
+
const job = this.ctx.documentToPersistedJob(updatedDoc);
|
|
158
|
+
this.ctx.notifyPendingJob(job.name, job.nextRunAt);
|
|
159
|
+
this.ctx.emit('job:retried', { job, previousStatus });
|
|
160
|
+
return job;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof MonqueError) {
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
const message = error instanceof Error ? error.message : 'Unknown error during retryJob';
|
|
166
|
+
throw new ConnectionError(
|
|
167
|
+
`Failed to retry job: ${message}`,
|
|
168
|
+
error instanceof Error ? { cause: error } : undefined,
|
|
169
|
+
);
|
|
147
170
|
}
|
|
148
|
-
|
|
149
|
-
const job = this.ctx.documentToPersistedJob(result);
|
|
150
|
-
this.ctx.emit('job:retried', { job, previousStatus });
|
|
151
|
-
return job;
|
|
152
171
|
}
|
|
153
172
|
|
|
154
173
|
/**
|
|
@@ -171,40 +190,45 @@ export class JobManager {
|
|
|
171
190
|
if (!ObjectId.isValid(jobId)) return null;
|
|
172
191
|
|
|
173
192
|
const _id = new ObjectId(jobId);
|
|
174
|
-
const currentJobDoc = await this.ctx.collection.findOne({ _id });
|
|
175
|
-
|
|
176
|
-
if (!currentJobDoc) return null;
|
|
177
193
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
194
|
+
try {
|
|
195
|
+
const now = new Date();
|
|
196
|
+
const result = await this.ctx.collection.findOneAndUpdate(
|
|
197
|
+
{ _id, status: JobStatus.PENDING },
|
|
198
|
+
{
|
|
199
|
+
$set: {
|
|
200
|
+
nextRunAt: runAt,
|
|
201
|
+
updatedAt: now,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{ returnDocument: 'after' },
|
|
184
205
|
);
|
|
185
|
-
}
|
|
186
206
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
207
|
+
if (!result) {
|
|
208
|
+
const currentJobDoc = await this.ctx.collection.findOne({ _id });
|
|
209
|
+
if (!currentJobDoc) return null;
|
|
210
|
+
|
|
211
|
+
throw new JobStateError(
|
|
212
|
+
`Cannot reschedule job in status '${currentJobDoc['status']}'`,
|
|
213
|
+
jobId,
|
|
214
|
+
currentJobDoc['status'],
|
|
215
|
+
'reschedule',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const job = this.ctx.documentToPersistedJob(result);
|
|
220
|
+
this.ctx.notifyPendingJob(job.name, job.nextRunAt);
|
|
221
|
+
return job;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error instanceof MonqueError) {
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
const message = error instanceof Error ? error.message : 'Unknown error during rescheduleJob';
|
|
227
|
+
throw new ConnectionError(
|
|
228
|
+
`Failed to reschedule job: ${message}`,
|
|
229
|
+
error instanceof Error ? { cause: error } : undefined,
|
|
204
230
|
);
|
|
205
231
|
}
|
|
206
|
-
|
|
207
|
-
return this.ctx.documentToPersistedJob(result);
|
|
208
232
|
}
|
|
209
233
|
|
|
210
234
|
/**
|
|
@@ -229,14 +253,25 @@ export class JobManager {
|
|
|
229
253
|
|
|
230
254
|
const _id = new ObjectId(jobId);
|
|
231
255
|
|
|
232
|
-
|
|
256
|
+
try {
|
|
257
|
+
const result = await this.ctx.collection.deleteOne({ _id });
|
|
233
258
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
259
|
+
if (result.deletedCount > 0) {
|
|
260
|
+
this.ctx.emit('job:deleted', { jobId });
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
238
263
|
|
|
239
|
-
|
|
264
|
+
return false;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error instanceof MonqueError) {
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
const message = error instanceof Error ? error.message : 'Unknown error during deleteJob';
|
|
270
|
+
throw new ConnectionError(
|
|
271
|
+
`Failed to delete job: ${message}`,
|
|
272
|
+
error instanceof Error ? { cause: error } : undefined,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
240
275
|
}
|
|
241
276
|
|
|
242
277
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -276,10 +311,11 @@ export class JobManager {
|
|
|
276
311
|
query['status'] = JobStatus.PENDING;
|
|
277
312
|
|
|
278
313
|
try {
|
|
314
|
+
const now = new Date();
|
|
279
315
|
const result = await this.ctx.collection.updateMany(query, {
|
|
280
316
|
$set: {
|
|
281
317
|
status: JobStatus.CANCELLED,
|
|
282
|
-
updatedAt:
|
|
318
|
+
updatedAt: now,
|
|
283
319
|
},
|
|
284
320
|
});
|
|
285
321
|
|
|
@@ -343,19 +379,20 @@ export class JobManager {
|
|
|
343
379
|
const spreadWindowMs = 30_000; // 30s max spread for staggered retry
|
|
344
380
|
|
|
345
381
|
try {
|
|
382
|
+
const now = new Date();
|
|
346
383
|
const result = await this.ctx.collection.updateMany(query, [
|
|
347
384
|
{
|
|
348
385
|
$set: {
|
|
349
386
|
status: JobStatus.PENDING,
|
|
350
387
|
failCount: 0,
|
|
351
388
|
nextRunAt: {
|
|
352
|
-
$add: [
|
|
389
|
+
$add: [now, { $multiply: [{ $rand: {} }, spreadWindowMs] }],
|
|
353
390
|
},
|
|
354
|
-
updatedAt:
|
|
391
|
+
updatedAt: now,
|
|
355
392
|
},
|
|
356
393
|
},
|
|
357
394
|
{
|
|
358
|
-
$unset: ['failReason', 'lockedAt', 'claimedBy', 'lastHeartbeat'
|
|
395
|
+
$unset: ['failReason', 'lockedAt', 'claimedBy', 'lastHeartbeat'],
|
|
359
396
|
},
|
|
360
397
|
]);
|
|
361
398
|
|
|
@@ -16,20 +16,19 @@ export class JobProcessor {
|
|
|
16
16
|
/** Guard flag to prevent concurrent poll() execution */
|
|
17
17
|
private _isPolling = false;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/** Flag to request a re-poll after the current poll finishes */
|
|
20
|
+
private _repollRequested = false;
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
+
* O(1) counter tracking the total number of active jobs across all workers.
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
+
* Incremented when a job is added to `worker.activeJobs` in `_doPoll`,
|
|
26
|
+
* decremented in the `processJob` finally block. Replaces the previous
|
|
27
|
+
* O(workers) loop in `getTotalActiveJobs()` for instance-level throttling.
|
|
25
28
|
*/
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
total += worker.activeJobs.size;
|
|
30
|
-
}
|
|
31
|
-
return total;
|
|
32
|
-
}
|
|
29
|
+
private _totalActiveJobs = 0;
|
|
30
|
+
|
|
31
|
+
constructor(private readonly ctx: SchedulerContext) {}
|
|
33
32
|
|
|
34
33
|
/**
|
|
35
34
|
* Get the number of available slots considering the global instanceConcurrency limit.
|
|
@@ -44,8 +43,7 @@ export class JobProcessor {
|
|
|
44
43
|
return workerAvailableSlots;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
const
|
|
48
|
-
const globalAvailable = instanceConcurrency - totalActive;
|
|
46
|
+
const globalAvailable = instanceConcurrency - this._totalActiveJobs;
|
|
49
47
|
|
|
50
48
|
return Math.min(workerAvailableSlots, globalAvailable);
|
|
51
49
|
}
|
|
@@ -57,16 +55,34 @@ export class JobProcessor {
|
|
|
57
55
|
* attempts to acquire jobs up to the worker's available concurrency slots.
|
|
58
56
|
* Aborts early if the scheduler is stopping (`isRunning` is false) or if
|
|
59
57
|
* the instance-level `instanceConcurrency` limit is reached.
|
|
58
|
+
*
|
|
59
|
+
* If a poll is requested while one is already running, it is queued and
|
|
60
|
+
* executed as a full poll after the current one finishes. This prevents
|
|
61
|
+
* change-stream-triggered polls from being silently dropped.
|
|
62
|
+
*
|
|
63
|
+
* @param targetNames - Optional set of worker names to poll. When provided, only the
|
|
64
|
+
* specified workers are checked. Used by change stream handler for targeted polling.
|
|
60
65
|
*/
|
|
61
|
-
async poll(): Promise<void> {
|
|
62
|
-
if (!this.ctx.isRunning()
|
|
66
|
+
async poll(targetNames?: ReadonlySet<string>): Promise<void> {
|
|
67
|
+
if (!this.ctx.isRunning()) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this._isPolling) {
|
|
72
|
+
// Queue a re-poll so work discovered during this poll isn't missed
|
|
73
|
+
this._repollRequested = true;
|
|
63
74
|
return;
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
this._isPolling = true;
|
|
67
78
|
|
|
68
79
|
try {
|
|
69
|
-
|
|
80
|
+
do {
|
|
81
|
+
this._repollRequested = false;
|
|
82
|
+
await this._doPoll(targetNames);
|
|
83
|
+
// Re-polls are always full polls to catch all pending work
|
|
84
|
+
targetNames = undefined;
|
|
85
|
+
} while (this._repollRequested && this.ctx.isRunning());
|
|
70
86
|
} finally {
|
|
71
87
|
this._isPolling = false;
|
|
72
88
|
}
|
|
@@ -75,15 +91,20 @@ export class JobProcessor {
|
|
|
75
91
|
/**
|
|
76
92
|
* Internal poll implementation.
|
|
77
93
|
*/
|
|
78
|
-
private async _doPoll(): Promise<void> {
|
|
94
|
+
private async _doPoll(targetNames?: ReadonlySet<string>): Promise<void> {
|
|
79
95
|
// Early exit if global instanceConcurrency is reached
|
|
80
96
|
const { instanceConcurrency } = this.ctx.options;
|
|
81
97
|
|
|
82
|
-
if (instanceConcurrency !== undefined && this.
|
|
98
|
+
if (instanceConcurrency !== undefined && this._totalActiveJobs >= instanceConcurrency) {
|
|
83
99
|
return;
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
for (const [name, worker] of this.ctx.workers) {
|
|
103
|
+
// Skip workers not in the target set (if provided)
|
|
104
|
+
if (targetNames && !targetNames.has(name)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
// Check if worker has capacity
|
|
88
109
|
const workerAvailableSlots = worker.concurrency - worker.activeJobs.size;
|
|
89
110
|
|
|
@@ -99,31 +120,57 @@ export class JobProcessor {
|
|
|
99
120
|
return;
|
|
100
121
|
}
|
|
101
122
|
|
|
102
|
-
// Try to acquire jobs up to available slots
|
|
123
|
+
// Try to acquire jobs up to available slots in parallel
|
|
124
|
+
if (!this.ctx.isRunning()) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const acquisitionPromises: Promise<void>[] = [];
|
|
103
129
|
for (let i = 0; i < availableSlots; i++) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
acquisitionPromises.push(
|
|
131
|
+
this.acquireJob(name)
|
|
132
|
+
.then(async (job) => {
|
|
133
|
+
if (!job) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this.ctx.isRunning()) {
|
|
138
|
+
// Add to activeJobs immediately to correctly track concurrency
|
|
139
|
+
worker.activeJobs.set(job._id.toString(), job);
|
|
140
|
+
this._totalActiveJobs++;
|
|
141
|
+
|
|
142
|
+
this.processJob(job, worker).catch((error: unknown) => {
|
|
143
|
+
this.ctx.emit('job:error', { error: toError(error), job });
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
// Revert claim if shut down while acquiring
|
|
147
|
+
try {
|
|
148
|
+
await this.ctx.collection.updateOne(
|
|
149
|
+
{ _id: job._id, status: JobStatus.PROCESSING, claimedBy: this.ctx.instanceId },
|
|
150
|
+
{
|
|
151
|
+
$set: {
|
|
152
|
+
status: JobStatus.PENDING,
|
|
153
|
+
updatedAt: new Date(),
|
|
154
|
+
},
|
|
155
|
+
$unset: {
|
|
156
|
+
lockedAt: '',
|
|
157
|
+
claimedBy: '',
|
|
158
|
+
lastHeartbeat: '',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this.ctx.emit('job:error', { error: toError(error) });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
.catch((error: unknown) => {
|
|
168
|
+
this.ctx.emit('job:error', { error: toError(error) });
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
126
171
|
}
|
|
172
|
+
|
|
173
|
+
await Promise.allSettled(acquisitionPromises);
|
|
127
174
|
}
|
|
128
175
|
}
|
|
129
176
|
|
|
@@ -171,10 +218,6 @@ export class JobProcessor {
|
|
|
171
218
|
},
|
|
172
219
|
);
|
|
173
220
|
|
|
174
|
-
if (!this.ctx.isRunning()) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
221
|
if (!result) {
|
|
179
222
|
return null;
|
|
180
223
|
}
|
|
@@ -222,6 +265,8 @@ export class JobProcessor {
|
|
|
222
265
|
}
|
|
223
266
|
} finally {
|
|
224
267
|
worker.activeJobs.delete(jobId);
|
|
268
|
+
this._totalActiveJobs--;
|
|
269
|
+
this.ctx.notifyJobFinished();
|
|
225
270
|
}
|
|
226
271
|
}
|
|
227
272
|
|
|
@@ -245,6 +290,8 @@ export class JobProcessor {
|
|
|
245
290
|
return null;
|
|
246
291
|
}
|
|
247
292
|
|
|
293
|
+
const now = new Date();
|
|
294
|
+
|
|
248
295
|
if (job.repeatInterval) {
|
|
249
296
|
// Recurring job - schedule next run
|
|
250
297
|
const nextRunAt = getNextCronDate(job.repeatInterval);
|
|
@@ -255,20 +302,25 @@ export class JobProcessor {
|
|
|
255
302
|
status: JobStatus.PENDING,
|
|
256
303
|
nextRunAt,
|
|
257
304
|
failCount: 0,
|
|
258
|
-
updatedAt:
|
|
305
|
+
updatedAt: now,
|
|
259
306
|
},
|
|
260
307
|
$unset: {
|
|
261
308
|
lockedAt: '',
|
|
262
309
|
claimedBy: '',
|
|
263
310
|
lastHeartbeat: '',
|
|
264
|
-
heartbeatInterval: '',
|
|
265
311
|
failReason: '',
|
|
266
312
|
},
|
|
267
313
|
},
|
|
268
314
|
{ returnDocument: 'after' },
|
|
269
315
|
);
|
|
270
316
|
|
|
271
|
-
|
|
317
|
+
if (!result) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const persistedJob = this.ctx.documentToPersistedJob(result);
|
|
322
|
+
this.ctx.notifyPendingJob(persistedJob.name, persistedJob.nextRunAt);
|
|
323
|
+
return persistedJob;
|
|
272
324
|
}
|
|
273
325
|
|
|
274
326
|
// One-time job - mark as completed
|
|
@@ -277,20 +329,24 @@ export class JobProcessor {
|
|
|
277
329
|
{
|
|
278
330
|
$set: {
|
|
279
331
|
status: JobStatus.COMPLETED,
|
|
280
|
-
updatedAt:
|
|
332
|
+
updatedAt: now,
|
|
281
333
|
},
|
|
282
334
|
$unset: {
|
|
283
335
|
lockedAt: '',
|
|
284
336
|
claimedBy: '',
|
|
285
337
|
lastHeartbeat: '',
|
|
286
|
-
heartbeatInterval: '',
|
|
287
338
|
failReason: '',
|
|
288
339
|
},
|
|
289
340
|
},
|
|
290
341
|
{ returnDocument: 'after' },
|
|
291
342
|
);
|
|
292
343
|
|
|
293
|
-
|
|
344
|
+
if (!result) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const persistedJob = this.ctx.documentToPersistedJob(result);
|
|
349
|
+
return persistedJob;
|
|
294
350
|
}
|
|
295
351
|
|
|
296
352
|
/**
|
|
@@ -316,6 +372,7 @@ export class JobProcessor {
|
|
|
316
372
|
return null;
|
|
317
373
|
}
|
|
318
374
|
|
|
375
|
+
const now = new Date();
|
|
319
376
|
const newFailCount = job.failCount + 1;
|
|
320
377
|
|
|
321
378
|
if (newFailCount >= this.ctx.options.maxRetries) {
|
|
@@ -327,13 +384,12 @@ export class JobProcessor {
|
|
|
327
384
|
status: JobStatus.FAILED,
|
|
328
385
|
failCount: newFailCount,
|
|
329
386
|
failReason: error.message,
|
|
330
|
-
updatedAt:
|
|
387
|
+
updatedAt: now,
|
|
331
388
|
},
|
|
332
389
|
$unset: {
|
|
333
390
|
lockedAt: '',
|
|
334
391
|
claimedBy: '',
|
|
335
392
|
lastHeartbeat: '',
|
|
336
|
-
heartbeatInterval: '',
|
|
337
393
|
},
|
|
338
394
|
},
|
|
339
395
|
{ returnDocument: 'after' },
|
|
@@ -357,13 +413,12 @@ export class JobProcessor {
|
|
|
357
413
|
failCount: newFailCount,
|
|
358
414
|
failReason: error.message,
|
|
359
415
|
nextRunAt,
|
|
360
|
-
updatedAt:
|
|
416
|
+
updatedAt: now,
|
|
361
417
|
},
|
|
362
418
|
$unset: {
|
|
363
419
|
lockedAt: '',
|
|
364
420
|
claimedBy: '',
|
|
365
421
|
lastHeartbeat: '',
|
|
366
|
-
heartbeatInterval: '',
|
|
367
422
|
},
|
|
368
423
|
},
|
|
369
424
|
{ returnDocument: 'after' },
|