qdone 2.2.5 → 2.2.6

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.
@@ -71,6 +71,9 @@ class JobExecutor {
71
71
  getExecutionTimeMs(job, start = new Date()) {
72
72
  return start - job.executionStart;
73
73
  }
74
+ shouldEnforceKillAfter(job) {
75
+ return !!(this.opt.killAfter && job.executionMode !== 'inline');
76
+ }
74
77
  scheduleKillAfter(job) {
75
78
  if (!this.opt.killAfter)
76
79
  return;
@@ -86,6 +89,8 @@ class JobExecutor {
86
89
  return;
87
90
  if (job.killed)
88
91
  return;
92
+ if (!this.shouldEnforceKillAfter(job))
93
+ return;
89
94
  const executionTimeMs = this.getExecutionTimeMs(job, start);
90
95
  if (executionTimeMs < this.opt.killAfter * 1000)
91
96
  return;
@@ -134,14 +139,10 @@ class JobExecutor {
134
139
  }, SIGKILL_DELAY_MS);
135
140
  job.killSignalTimer.unref?.();
136
141
  }
137
- async setRunningVisibilityTimeout(job) {
138
- if (!this.opt.killAfter)
139
- return;
140
- const visibilityTimeout = Math.max(1, Math.min(job.visibilityTimeout, this.opt.killAfter));
141
- if (visibilityTimeout >= job.visibilityTimeout)
142
- return;
142
+ async setJobVisibilityTimeout(job, visibilityTimeout, start = new Date()) {
143
143
  job.visibilityTimeout = visibilityTimeout;
144
- job.extendAtSecond = Math.round(job.visibilityTimeout / 2);
144
+ const jobRunTime = Math.round((start - job.start) / 1000);
145
+ job.extendAtSecond = Math.round(jobRunTime + job.visibilityTimeout / 2);
145
146
  const input = {
146
147
  QueueUrl: job.qrl,
147
148
  ReceiptHandle: job.message.ReceiptHandle,
@@ -161,6 +162,51 @@ class JobExecutor {
161
162
  }
162
163
  }
163
164
  }
165
+ async setRunningVisibilityTimeout(job) {
166
+ if (!this.shouldEnforceKillAfter(job))
167
+ return;
168
+ const visibilityTimeout = Math.max(1, Math.min(job.visibilityTimeout, this.opt.killAfter));
169
+ if (visibilityTimeout >= job.visibilityTimeout)
170
+ return;
171
+ await this.setJobVisibilityTimeout(job, visibilityTimeout);
172
+ }
173
+ async registerInlineExecution(job) {
174
+ if (job.executionMode === 'inline')
175
+ return;
176
+ if (job.executionMode === 'child_process') {
177
+ debug('registerInlineExecution ignored after registerPid', { messageId: job.message?.MessageId });
178
+ return;
179
+ }
180
+ job.executionMode = 'inline';
181
+ job.killDue = false;
182
+ this.clearJobTimers(job);
183
+ if (job.status === 'running' && job.visibilityTimeout < defaultVisibilityTimeout) {
184
+ await this.setJobVisibilityTimeout(job, defaultVisibilityTimeout);
185
+ }
186
+ }
187
+ logInlineKillAfterOverrun(job, start = new Date()) {
188
+ if (!this.opt.killAfter || !job.executionStart || job.inlineKillAfterLogged)
189
+ return;
190
+ const executionTimeMs = this.getExecutionTimeMs(job, start);
191
+ if (executionTimeMs < this.opt.killAfter * 1000)
192
+ return;
193
+ job.inlineKillAfterLogged = true;
194
+ const executionTime = Math.floor(executionTimeMs / 1000);
195
+ if (this.opt.verbose) {
196
+ console.error(chalk_1.default.yellow('INLINE_JOB_EXCEEDED_KILL_AFTER'), job.prettyQname, chalk_1.default.yellow('after'), executionTime, chalk_1.default.yellow('seconds (limit:'), this.opt.killAfter + ')');
197
+ }
198
+ else if (!this.opt.disableLog) {
199
+ console.log(JSON.stringify({
200
+ event: 'INLINE_JOB_EXCEEDED_KILL_AFTER',
201
+ timestamp: start,
202
+ queue: job.qname,
203
+ messageId: job.message.MessageId,
204
+ executionTime,
205
+ killAfter: this.opt.killAfter,
206
+ payload: job.payload
207
+ }));
208
+ }
209
+ }
164
210
  /**
165
211
  * Changes message visibility on all running jobs using as few calls as possible.
166
212
  */
@@ -178,7 +224,6 @@ class JobExecutor {
178
224
  this.maintainVisibilityTimeout = setTimeout(() => {
179
225
  this.maintainPromise = this.maintainVisibility();
180
226
  }, nextCheckInMs);
181
- // debug('maintainVisibility', this.jobs)
182
227
  const start = new Date();
183
228
  const jobsToExtendByQrl = {};
184
229
  const jobsToDeleteByQrl = {};
@@ -189,7 +234,6 @@ class JobExecutor {
189
234
  const job = this.jobs[i];
190
235
  const jobRunTime = Math.round((start - job.start) / 1000);
191
236
  jobStatuses[job.status] = (jobStatuses[job.status] || 0) + 1;
192
- // debug('considering job', job)
193
237
  if (job.status === 'complete') {
194
238
  const jobsToDelete = jobsToDeleteByQrl[job.qrl] || [];
195
239
  job.status = 'deleting';
@@ -205,13 +249,16 @@ class JobExecutor {
205
249
  // Kill-after enforcement: terminate child process if it exceeds the deadline.
206
250
  // Uses executionStart (when runJob began) so FIFO serial jobs aren't
207
251
  // penalized for queue wait time.
208
- if (this.opt.killAfter && job.executionStart && !job.killed) {
252
+ if (this.shouldEnforceKillAfter(job) && job.executionStart && !job.killed) {
209
253
  const executionTimeMs = this.getExecutionTimeMs(job, start);
210
254
  if (executionTimeMs >= this.opt.killAfter * 1000) {
211
255
  job.killDue = true;
212
256
  this.killJob(job, start);
213
257
  }
214
258
  }
259
+ else if (job.executionMode === 'inline') {
260
+ this.logInlineKillAfterOverrun(job, start);
261
+ }
215
262
  if (jobRunTime >= job.extendAtSecond) {
216
263
  // Add it to our organized list of jobs
217
264
  const jobsToExtend = jobsToExtendByQrl[job.qrl] || [];
@@ -223,7 +270,7 @@ class JobExecutor {
223
270
  const doubled = job.visibilityTimeout * 2;
224
271
  const secondsUntilMax = Math.max(1, maxJobSeconds - jobRunTime);
225
272
  const executionTimeMs = job.executionStart ? this.getExecutionTimeMs(job, start) : 0;
226
- const secondsUntilKill = (this.opt.killAfter && job.executionStart)
273
+ const secondsUntilKill = (this.shouldEnforceKillAfter(job) && job.executionStart)
227
274
  ? Math.max(1, Math.ceil((this.opt.killAfter * 1000 - executionTimeMs) / 1000))
228
275
  : Infinity;
229
276
  job.visibilityTimeout = Math.min(doubled, secondsUntilMax, secondsUntilKill);
@@ -438,13 +485,22 @@ class JobExecutor {
438
485
  messageGroupId: job.message.Attributes?.MessageGroupId || '',
439
486
  /** Call with a child process PID to enable kill-after process termination. */
440
487
  registerPid: (pid) => {
488
+ if (job.executionMode === 'inline') {
489
+ debug('registerPid ignored after registerInlineExecution', { messageId: job.message?.MessageId });
490
+ return;
491
+ }
441
492
  if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 1 || pid === process.pid) {
442
493
  debug('registerPid: rejected invalid PID', pid);
443
494
  return;
444
495
  }
496
+ job.executionMode = 'child_process';
445
497
  job.pid = pid;
446
498
  if (job.killDue && !job.killed)
447
499
  this.killJob(job, new Date());
500
+ },
501
+ /** Call before inline work starts to opt out of kill-after visibility expiry. */
502
+ registerInlineExecution: async () => {
503
+ await this.registerInlineExecution(job);
448
504
  }
449
505
  };
450
506
  const result = await job.callback(queue, job.payload, attributes);
@@ -503,13 +559,11 @@ class JobExecutor {
503
559
  const jobs = messages.map(message => this.addJob(message, callback, qname, qrl));
504
560
  const isFifo = qrl.endsWith('.fifo');
505
561
  const runningJobs = [];
506
- // console.log(jobs)
507
562
  // Begin executing
508
563
  for (const [job, i] of jobs.map((job, i) => [job, i])) {
509
564
  // Figure out if the next job needs to happen in serial, otherwise we can parallel execute
510
565
  const nextJob = jobs[i + 1];
511
566
  const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.MessageGroupId === nextJob.message?.Attributes?.MessageGroupId;
512
- // console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
513
567
  // Execute serial or parallel
514
568
  if (nextJobIsSerial)
515
569
  await this.runJob(job);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdone",
3
- "version": "2.2.5",
3
+ "version": "2.2.6",
4
4
  "description": "A distributed scheduler for SQS",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -81,6 +81,10 @@ export class JobExecutor {
81
81
  return start - job.executionStart
82
82
  }
83
83
 
84
+ shouldEnforceKillAfter (job) {
85
+ return !!(this.opt.killAfter && job.executionMode !== 'inline')
86
+ }
87
+
84
88
  scheduleKillAfter (job) {
85
89
  if (!this.opt.killAfter) return
86
90
  clearTimeout(job.killTimer)
@@ -94,6 +98,7 @@ export class JobExecutor {
94
98
  killJob (job, start = new Date()) {
95
99
  if (!job.executionStart || job.status !== 'running') return
96
100
  if (job.killed) return
101
+ if (!this.shouldEnforceKillAfter(job)) return
97
102
 
98
103
  const executionTimeMs = this.getExecutionTimeMs(job, start)
99
104
  if (executionTimeMs < this.opt.killAfter * 1000) return
@@ -140,14 +145,10 @@ export class JobExecutor {
140
145
  job.killSignalTimer.unref?.()
141
146
  }
142
147
 
143
- async setRunningVisibilityTimeout (job) {
144
- if (!this.opt.killAfter) return
145
-
146
- const visibilityTimeout = Math.max(1, Math.min(job.visibilityTimeout, this.opt.killAfter))
147
- if (visibilityTimeout >= job.visibilityTimeout) return
148
-
148
+ async setJobVisibilityTimeout (job, visibilityTimeout, start = new Date()) {
149
149
  job.visibilityTimeout = visibilityTimeout
150
- job.extendAtSecond = Math.round(job.visibilityTimeout / 2)
150
+ const jobRunTime = Math.round((start - job.start) / 1000)
151
+ job.extendAtSecond = Math.round(jobRunTime + job.visibilityTimeout / 2)
151
152
 
152
153
  const input = {
153
154
  QueueUrl: job.qrl,
@@ -169,6 +170,55 @@ export class JobExecutor {
169
170
  }
170
171
  }
171
172
 
173
+ async setRunningVisibilityTimeout (job) {
174
+ if (!this.shouldEnforceKillAfter(job)) return
175
+
176
+ const visibilityTimeout = Math.max(1, Math.min(job.visibilityTimeout, this.opt.killAfter))
177
+ if (visibilityTimeout >= job.visibilityTimeout) return
178
+
179
+ await this.setJobVisibilityTimeout(job, visibilityTimeout)
180
+ }
181
+
182
+ async registerInlineExecution (job) {
183
+ if (job.executionMode === 'inline') return
184
+ if (job.executionMode === 'child_process') {
185
+ debug('registerInlineExecution ignored after registerPid', { messageId: job.message?.MessageId })
186
+ return
187
+ }
188
+
189
+ job.executionMode = 'inline'
190
+ job.killDue = false
191
+ this.clearJobTimers(job)
192
+
193
+ if (job.status === 'running' && job.visibilityTimeout < defaultVisibilityTimeout) {
194
+ await this.setJobVisibilityTimeout(job, defaultVisibilityTimeout)
195
+ }
196
+ }
197
+
198
+ logInlineKillAfterOverrun (job, start = new Date()) {
199
+ if (!this.opt.killAfter || !job.executionStart || job.inlineKillAfterLogged) return
200
+
201
+ const executionTimeMs = this.getExecutionTimeMs(job, start)
202
+ if (executionTimeMs < this.opt.killAfter * 1000) return
203
+
204
+ job.inlineKillAfterLogged = true
205
+ const executionTime = Math.floor(executionTimeMs / 1000)
206
+ if (this.opt.verbose) {
207
+ console.error(chalk.yellow('INLINE_JOB_EXCEEDED_KILL_AFTER'), job.prettyQname,
208
+ chalk.yellow('after'), executionTime, chalk.yellow('seconds (limit:'), this.opt.killAfter + ')')
209
+ } else if (!this.opt.disableLog) {
210
+ console.log(JSON.stringify({
211
+ event: 'INLINE_JOB_EXCEEDED_KILL_AFTER',
212
+ timestamp: start,
213
+ queue: job.qname,
214
+ messageId: job.message.MessageId,
215
+ executionTime,
216
+ killAfter: this.opt.killAfter,
217
+ payload: job.payload
218
+ }))
219
+ }
220
+ }
221
+
172
222
  /**
173
223
  * Changes message visibility on all running jobs using as few calls as possible.
174
224
  */
@@ -188,7 +238,6 @@ export class JobExecutor {
188
238
  this.maintainPromise = this.maintainVisibility()
189
239
  }, nextCheckInMs)
190
240
 
191
- // debug('maintainVisibility', this.jobs)
192
241
  const start = new Date()
193
242
  const jobsToExtendByQrl = {}
194
243
  const jobsToDeleteByQrl = {}
@@ -200,7 +249,6 @@ export class JobExecutor {
200
249
  const job = this.jobs[i]
201
250
  const jobRunTime = Math.round((start - job.start) / 1000)
202
251
  jobStatuses[job.status] = (jobStatuses[job.status] || 0) + 1
203
- // debug('considering job', job)
204
252
  if (job.status === 'complete') {
205
253
  const jobsToDelete = jobsToDeleteByQrl[job.qrl] || []
206
254
  job.status = 'deleting'
@@ -215,12 +263,14 @@ export class JobExecutor {
215
263
  // Kill-after enforcement: terminate child process if it exceeds the deadline.
216
264
  // Uses executionStart (when runJob began) so FIFO serial jobs aren't
217
265
  // penalized for queue wait time.
218
- if (this.opt.killAfter && job.executionStart && !job.killed) {
266
+ if (this.shouldEnforceKillAfter(job) && job.executionStart && !job.killed) {
219
267
  const executionTimeMs = this.getExecutionTimeMs(job, start)
220
268
  if (executionTimeMs >= this.opt.killAfter * 1000) {
221
269
  job.killDue = true
222
270
  this.killJob(job, start)
223
271
  }
272
+ } else if (job.executionMode === 'inline') {
273
+ this.logInlineKillAfterOverrun(job, start)
224
274
  }
225
275
 
226
276
  if (jobRunTime >= job.extendAtSecond) {
@@ -235,7 +285,7 @@ export class JobExecutor {
235
285
  const doubled = job.visibilityTimeout * 2
236
286
  const secondsUntilMax = Math.max(1, maxJobSeconds - jobRunTime)
237
287
  const executionTimeMs = job.executionStart ? this.getExecutionTimeMs(job, start) : 0
238
- const secondsUntilKill = (this.opt.killAfter && job.executionStart)
288
+ const secondsUntilKill = (this.shouldEnforceKillAfter(job) && job.executionStart)
239
289
  ? Math.max(1, Math.ceil((this.opt.killAfter * 1000 - executionTimeMs) / 1000))
240
290
  : Infinity
241
291
  job.visibilityTimeout = Math.min(doubled, secondsUntilMax, secondsUntilKill)
@@ -455,12 +505,21 @@ export class JobExecutor {
455
505
  messageGroupId: job.message.Attributes?.MessageGroupId || '',
456
506
  /** Call with a child process PID to enable kill-after process termination. */
457
507
  registerPid: (pid) => {
508
+ if (job.executionMode === 'inline') {
509
+ debug('registerPid ignored after registerInlineExecution', { messageId: job.message?.MessageId })
510
+ return
511
+ }
458
512
  if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 1 || pid === process.pid) {
459
513
  debug('registerPid: rejected invalid PID', pid)
460
514
  return
461
515
  }
516
+ job.executionMode = 'child_process'
462
517
  job.pid = pid
463
518
  if (job.killDue && !job.killed) this.killJob(job, new Date())
519
+ },
520
+ /** Call before inline work starts to opt out of kill-after visibility expiry. */
521
+ registerInlineExecution: async () => {
522
+ await this.registerInlineExecution(job)
464
523
  }
465
524
  }
466
525
  const result = await job.callback(queue, job.payload, attributes)
@@ -518,15 +577,12 @@ export class JobExecutor {
518
577
  const isFifo = qrl.endsWith('.fifo')
519
578
  const runningJobs = []
520
579
 
521
- // console.log(jobs)
522
-
523
580
  // Begin executing
524
581
  for (const [job, i] of jobs.map((job, i) => [job, i])) {
525
582
  // Figure out if the next job needs to happen in serial, otherwise we can parallel execute
526
583
  const nextJob = jobs[i + 1]
527
584
  const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.MessageGroupId === nextJob.message?.Attributes?.MessageGroupId
528
585
 
529
- // console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
530
586
  // Execute serial or parallel
531
587
  if (nextJobIsSerial) await this.runJob(job)
532
588
  else runningJobs.push(this.runJob(job))