qdone 2.0.52-alpha → 2.0.54-alpha

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.
@@ -92,7 +92,7 @@ async function processMessages(queues, callback, options) {
92
92
  const messages = await getMessages(qrl, opt, maxMessages);
93
93
  if (!shutdownRequested) {
94
94
  if (messages.length) {
95
- jobExecutor.executeJobs(messages, callback, qname, qrl);
95
+ await jobExecutor.executeJobs(messages, callback, qname, qrl);
96
96
  queueManager.updateIcehouse(qrl, false);
97
97
  }
98
98
  else {
@@ -130,7 +130,7 @@ async function processMessages(queues, callback, options) {
130
130
  const remainingMemory = Math.max(0, freeMemory - freememThreshold);
131
131
  const freememFactor = Math.min(1, Math.max(0, remainingMemory / memoryThreshold));
132
132
  // Load
133
- const oneMinuteLoad = (0, os_1.loadavg)()[0];
133
+ const oneMinuteLoad = systemMonitor.getLoad();
134
134
  const loadPerCore = oneMinuteLoad / cores;
135
135
  const loadFactor = 1 - Math.min(1, Math.max(0, loadPerCore / 3));
136
136
  const overallFactor = Math.min(latencyFactor, freememFactor, loadFactor);
@@ -137,7 +137,7 @@ async function checkIdle(qname, qrl, opt) {
137
137
  if (cheapResult.idle === false || cheapResult.exists === false) {
138
138
  return {
139
139
  queue: qname.slice(opt.prefix.length),
140
- cheap: cheapResult,
140
+ cheap: { SQS, result: cheapResult },
141
141
  idle: cheapResult.idle,
142
142
  exists: cheapResult.exists,
143
143
  apiCalls: { SQS, CloudWatch: 0 }
@@ -70,8 +70,10 @@ async function qrlCacheGet(qname) {
70
70
  // debug({ cmd })
71
71
  const result = await client.send(cmd);
72
72
  // debug('result', result)
73
- if (!result)
73
+ if (!result) {
74
+ qrlCacheInvalidate(qname);
74
75
  throw new client_sqs_1.QueueDoesNotExist(qname);
76
+ }
75
77
  const { QueueUrl: qrl } = result;
76
78
  // debug('getQueueUrl returned', data)
77
79
  qcache.set(qname, qrl);
@@ -84,7 +86,8 @@ exports.qrlCacheGet = qrlCacheGet;
84
86
  // Immediately updates the cache
85
87
  //
86
88
  function qrlCacheSet(qname, qrl) {
87
- qcache.set(qname, qrl);
89
+ if (qrl)
90
+ qcache.set(qname, qrl);
88
91
  // debug('qcache', Object.keys(qcache), 'set', qname, ' => ', qcache[qname])
89
92
  }
90
93
  exports.qrlCacheSet = qrlCacheSet;
@@ -363,6 +363,7 @@ class JobExecutor {
363
363
  // Begin tracking jobs
364
364
  const jobs = messages.map(message => this.addJob(message, callback, qname, qrl));
365
365
  const isFifo = qrl.endsWith('.fifo');
366
+ const runningJobs = [];
366
367
  // console.log(jobs)
367
368
  // Begin executing
368
369
  for (const [job, i] of jobs.map((job, i) => [job, i])) {
@@ -374,8 +375,9 @@ class JobExecutor {
374
375
  if (nextJobIsSerial)
375
376
  await this.runJob(job);
376
377
  else
377
- this.runJob(job);
378
+ runningJobs.push(this.runJob(job));
378
379
  }
380
+ await Promise.all(runningJobs);
379
381
  }
380
382
  }
381
383
  exports.JobExecutor = JobExecutor;
@@ -3,13 +3,19 @@
3
3
  * Component to track event loop latency, which can be used as a metric for
4
4
  * backpressure.
5
5
  */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
6
9
  Object.defineProperty(exports, "__esModule", { value: true });
7
10
  exports.SystemMonitor = void 0;
11
+ const os_1 = __importDefault(require("os"));
8
12
  class SystemMonitor {
9
13
  constructor(reportCallback, reportSeconds = 1) {
10
14
  this.reportCallback = reportCallback || console.log;
11
15
  this.reportSeconds = reportSeconds;
12
- this.measurements = [];
16
+ this.latencies = [];
17
+ this.oneMinuteLoad = os_1.default.loadavg()[0];
18
+ this.instantaneousLoad = this.oneMinuteLoad;
13
19
  this.measure();
14
20
  this.reportLatency();
15
21
  }
@@ -17,15 +23,19 @@ class SystemMonitor {
17
23
  clearTimeout(this.measureTimeout);
18
24
  const start = new Date();
19
25
  this.measureTimeout = setTimeout(() => {
20
- const latency = new Date() - start;
21
- this.measurements.push(latency);
22
- if (this.measurements.length > 1000)
23
- this.measurements.shift();
26
+ this.measureLatency(start);
27
+ this.measureLoad();
24
28
  this.measure();
25
29
  });
26
30
  }
31
+ measureLatency(start) {
32
+ const latency = new Date() - start;
33
+ this.latencies.push(latency);
34
+ if (this.latencies.length > 1000)
35
+ this.latencies.shift();
36
+ }
27
37
  getLatency() {
28
- return this.measurements.length ? this.measurements.reduce((a, b) => a + b, 0) / this.measurements.length : 0;
38
+ return this.latencies.length ? this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length : 0;
29
39
  }
30
40
  reportLatency() {
31
41
  clearTimeout(this.reportTimeout);
@@ -37,6 +47,45 @@ class SystemMonitor {
37
47
  this.reportLatency();
38
48
  }, this.reportSeconds * 1000);
39
49
  }
50
+ /**
51
+ * Measures load over the last five seconds instead of being averaged over one
52
+ * minute. This lets the scheduler respond much faster to dips in load.
53
+ *
54
+ * Theory:
55
+ *
56
+ * The Linux kernel calculates the moving average something like:
57
+ * A_1 = A_0 * e + A_now (1 - e)
58
+ * Where:
59
+ * - A_now is the number of processes active/waiting
60
+ * - A_1 is the new one-minute load average after the measurement of A_now
61
+ * - A_0 is the previous one-minute average
62
+ * - e is 1884/2048.
63
+ *
64
+ * Solving this for A_now, which we want to access, we get:
65
+ * A_now = (A_1 - A_0 * e) / (1 - e)
66
+ *
67
+ * We use this formula below to extract A_now when we detect a change in A_1.
68
+ *
69
+ * Note: this code assums that we are observing the average often enough to
70
+ * detect each change. So you have to call it at least every 5 seconds. 1
71
+ * second is better to reduce latency of detecting the change.
72
+ */
73
+ measureLoad() {
74
+ const [newLoad,] = os_1.default.loadavg();
75
+ const previousLoad = this.oneMinuteLoad;
76
+ if (previousLoad !== newLoad) {
77
+ const e = 1884 / 2048; // see include/linux/sched/loadavg.h
78
+ const active = (newLoad - previousLoad * e) / (1 - e);
79
+ // We take the min here so that spikes up in load are averaged out. We
80
+ // care about detecting spikes downward so we can allow more jobs to run.
81
+ this.instantaneousLoad = Math.min(active, newLoad);
82
+ this.oneMinuteLoad = newLoad;
83
+ console.log({ newLoad, previousLoad, active, instantaneousLoad: this.instantaneousLoad, oneMinuteLoad: this.oneMinuteLoad });
84
+ }
85
+ }
86
+ getLoad() {
87
+ return this.instantaneousLoad;
88
+ }
40
89
  shutdown() {
41
90
  clearTimeout(this.measureTimeout);
42
91
  clearTimeout(this.reportTimeout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdone",
3
- "version": "2.0.52-alpha",
3
+ "version": "2.0.54-alpha",
4
4
  "description": "A distributed scheduler for SQS",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/src/cli.js CHANGED
@@ -451,19 +451,14 @@ export async function idleQueues (argv, testHook) {
451
451
  {
452
452
  content: [
453
453
  { count: '1 + q + i', desc: 'q: number of queues in pattern\ni: number of idle queues' },
454
- { context: 'with --delete options', count: '1 + q + 3i', desc: 'q: number of queues in pattern\ni: number of idle queues' },
455
- { context: 'with --unpair option', count: '1 + q', desc: 'q: number of queues in pattern' },
456
- { context: 'with --unpair and --delete options', count: '1 + q + i', desc: 'q: number of queues in pattern\ni: number of idle queues' },
457
- { desc: 'NOTE: the --unpair option not cheaper if you include fail queues, because it doubles q.' }
454
+ { context: 'with --delete options', count: '1 + q + 3i', desc: 'q: number of queues in pattern\ni: number of idle queues' }
458
455
  ],
459
456
  long: true
460
457
  },
461
458
  { content: 'CloudWatch API Call Complexity', raw: true, long: true },
462
459
  {
463
460
  content: [
464
- { count: 'min: 0 (if queue and fail queue have waiting messages)\nmax: 12q\nexpected (approximate observed): 0.5q + 12i', desc: 'q: number of queues in pattern\ni: number of idle queues' },
465
- { context: 'with --unpair option', count: 'min: 0 (if queue has waiting messages)\nmax: 6q\nexpected (approximate observed): q + 6i', desc: 'q: number of queues in pattern\ni: number of idle queues' },
466
- { desc: 'NOTE: the --unpair option not cheaper if you include fail queues, because it doubles q.' }
461
+ { count: 'min: 0 (if queue and fail queue have waiting messages)\nmax: 12q\nexpected (approximate observed): 0.5q + 12i', desc: 'q: number of queues in pattern\ni: number of idle queues' }
467
462
  ],
468
463
  long: true
469
464
  },
@@ -479,7 +474,6 @@ export async function idleQueues (argv, testHook) {
479
474
  debug('idleQueues options', options)
480
475
  if (options.help) return Promise.resolve(console.log(getUsage(usageSections)))
481
476
  if (!options._unknown || options._unknown.length === 0) throw new UsageError('idle-queues requres one or more <queue> arguments')
482
- if (options['include-failed'] && !options.unpair) throw new UsageError('--include-failed should be used with --unpair')
483
477
  if (options['idle-for'] < 5) throw new UsageError('--idle-for must be at least 5 minutes (CloudWatch limitation)')
484
478
  queues = options._unknown
485
479
  debug('queues', queues)
package/src/consumer.js CHANGED
@@ -96,7 +96,7 @@ export async function processMessages (queues, callback, options) {
96
96
 
97
97
  if (!shutdownRequested) {
98
98
  if (messages.length) {
99
- jobExecutor.executeJobs(messages, callback, qname, qrl)
99
+ await jobExecutor.executeJobs(messages, callback, qname, qrl)
100
100
  queueManager.updateIcehouse(qrl, false)
101
101
  } else {
102
102
  // If we didn't get any, update the icehouse so we can back off
@@ -136,7 +136,7 @@ export async function processMessages (queues, callback, options) {
136
136
  const freememFactor = Math.min(1, Math.max(0, remainingMemory / memoryThreshold))
137
137
 
138
138
  // Load
139
- const oneMinuteLoad = loadavg()[0]
139
+ const oneMinuteLoad = systemMonitor.getLoad()
140
140
  const loadPerCore = oneMinuteLoad / cores
141
141
  const loadFactor = 1 - Math.min(1, Math.max(0, loadPerCore / 3))
142
142
 
package/src/idleQueues.js CHANGED
@@ -7,7 +7,7 @@ import { getCloudWatchClient } from './cloudWatch.js'
7
7
  import { getOptionsWithDefaults } from './defaults.js'
8
8
  import { GetQueueAttributesCommand, DeleteQueueCommand, QueueDoesNotExist } from '@aws-sdk/client-sqs'
9
9
  import { GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch'
10
- import { normalizeFailQueueName, normalizeDLQName, getQnameUrlPairs, fifoSuffix } from './qrlCache.js'
10
+ import { normalizeFailQueueName, normalizeDLQName, getQnameUrlPairs, fifoSuffix, qrlCacheSet } from './qrlCache.js'
11
11
  import { getCache, setCache } from './cache.js'
12
12
  // const AWS = require('aws-sdk')
13
13
 
@@ -133,7 +133,7 @@ export async function checkIdle (qname, qrl, opt) {
133
133
  if (cheapResult.idle === false || cheapResult.exists === false) {
134
134
  return {
135
135
  queue: qname.slice(opt.prefix.length),
136
- cheap: cheapResult,
136
+ cheap: { SQS, result: cheapResult },
137
137
  idle: cheapResult.idle,
138
138
  exists: cheapResult.exists,
139
139
  apiCalls: { SQS, CloudWatch: 0 }
package/src/qrlCache.js CHANGED
@@ -66,7 +66,10 @@ export async function qrlCacheGet (qname) {
66
66
  // debug({ cmd })
67
67
  const result = await client.send(cmd)
68
68
  // debug('result', result)
69
- if (!result) throw new QueueDoesNotExist(qname)
69
+ if (!result) {
70
+ qrlCacheInvalidate(qname)
71
+ throw new QueueDoesNotExist(qname)
72
+ }
70
73
  const { QueueUrl: qrl } = result
71
74
  // debug('getQueueUrl returned', data)
72
75
  qcache.set(qname, qrl)
@@ -79,7 +82,7 @@ export async function qrlCacheGet (qname) {
79
82
  // Immediately updates the cache
80
83
  //
81
84
  export function qrlCacheSet (qname, qrl) {
82
- qcache.set(qname, qrl)
85
+ if (qrl) qcache.set(qname, qrl)
83
86
  // debug('qcache', Object.keys(qcache), 'set', qname, ' => ', qcache[qname])
84
87
  }
85
88
 
@@ -373,6 +373,7 @@ export class JobExecutor {
373
373
  // Begin tracking jobs
374
374
  const jobs = messages.map(message => this.addJob(message, callback, qname, qrl))
375
375
  const isFifo = qrl.endsWith('.fifo')
376
+ const runningJobs = []
376
377
 
377
378
  // console.log(jobs)
378
379
 
@@ -385,7 +386,8 @@ export class JobExecutor {
385
386
  // console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
386
387
  // Execute serial or parallel
387
388
  if (nextJobIsSerial) await this.runJob(job)
388
- else this.runJob(job)
389
+ else runningJobs.push(this.runJob(job))
389
390
  }
391
+ await Promise.all(runningJobs)
390
392
  }
391
393
  }
@@ -3,11 +3,15 @@
3
3
  * backpressure.
4
4
  */
5
5
 
6
+ import os from 'os'
7
+
6
8
  export class SystemMonitor {
7
9
  constructor (reportCallback, reportSeconds = 1) {
8
10
  this.reportCallback = reportCallback || console.log
9
11
  this.reportSeconds = reportSeconds
10
- this.measurements = []
12
+ this.latencies = []
13
+ this.oneMinuteLoad = os.loadavg()[0]
14
+ this.instantaneousLoad = this.oneMinuteLoad
11
15
  this.measure()
12
16
  this.reportLatency()
13
17
  }
@@ -16,15 +20,20 @@ export class SystemMonitor {
16
20
  clearTimeout(this.measureTimeout)
17
21
  const start = new Date()
18
22
  this.measureTimeout = setTimeout(() => {
19
- const latency = new Date() - start
20
- this.measurements.push(latency)
21
- if (this.measurements.length > 1000) this.measurements.shift()
23
+ this.measureLatency(start)
24
+ this.measureLoad()
22
25
  this.measure()
23
26
  })
24
27
  }
25
28
 
29
+ measureLatency (start) {
30
+ const latency = new Date() - start
31
+ this.latencies.push(latency)
32
+ if (this.latencies.length > 1000) this.latencies.shift()
33
+ }
34
+
26
35
  getLatency () {
27
- return this.measurements.length ? this.measurements.reduce((a, b) => a + b, 0) / this.measurements.length : 0
36
+ return this.latencies.length ? this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length : 0
28
37
  }
29
38
 
30
39
  reportLatency () {
@@ -37,6 +46,48 @@ export class SystemMonitor {
37
46
  }, this.reportSeconds * 1000)
38
47
  }
39
48
 
49
+ /**
50
+ * Measures load over the last five seconds instead of being averaged over one
51
+ * minute. This lets the scheduler respond much faster to dips in load.
52
+ *
53
+ * Theory:
54
+ *
55
+ * The Linux kernel calculates the moving average something like:
56
+ * A_1 = A_0 * e + A_now (1 - e)
57
+ * Where:
58
+ * - A_now is the number of processes active/waiting
59
+ * - A_1 is the new one-minute load average after the measurement of A_now
60
+ * - A_0 is the previous one-minute average
61
+ * - e is 1884/2048.
62
+ *
63
+ * Solving this for A_now, which we want to access, we get:
64
+ * A_now = (A_1 - A_0 * e) / (1 - e)
65
+ *
66
+ * We use this formula below to extract A_now when we detect a change in A_1.
67
+ *
68
+ * Note: this code assums that we are observing the average often enough to
69
+ * detect each change. So you have to call it at least every 5 seconds. 1
70
+ * second is better to reduce latency of detecting the change.
71
+ */
72
+
73
+ measureLoad () {
74
+ const [newLoad, ] = os.loadavg()
75
+ const previousLoad = this.oneMinuteLoad
76
+ if (previousLoad !== newLoad) {
77
+ const e = 1884 / 2048 // see include/linux/sched/loadavg.h
78
+ const active = (newLoad - previousLoad * e) / (1 - e)
79
+ // We take the min here so that spikes up in load are averaged out. We
80
+ // care about detecting spikes downward so we can allow more jobs to run.
81
+ this.instantaneousLoad = Math.min(active, newLoad)
82
+ this.oneMinuteLoad = newLoad
83
+ console.log({ newLoad, previousLoad, active, instantaneousLoad: this.instantaneousLoad, oneMinuteLoad: this.oneMinuteLoad })
84
+ }
85
+ }
86
+
87
+ getLoad() {
88
+ return this.instantaneousLoad
89
+ }
90
+
40
91
  shutdown () {
41
92
  clearTimeout(this.measureTimeout)
42
93
  clearTimeout(this.reportTimeout)