qdone 2.1.0 → 2.1.1

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.
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+ /**
3
+ * Component to manage all the currently executing jobs, including extending
4
+ * their visibility timeouts and deleting them when they are successful.
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.JobExecutor = void 0;
11
+ const client_sqs_1 = require("@aws-sdk/client-sqs");
12
+ const chalk_1 = __importDefault(require("chalk"));
13
+ const debug_1 = __importDefault(require("debug"));
14
+ const dedup_js_1 = require("../dedup.js");
15
+ const sqs_js_1 = require("../sqs.js");
16
+ const debug = (0, debug_1.default)('qdone:jobExecutor');
17
+ const maxJobSeconds = 12 * 60 * 60;
18
+ class JobExecutor {
19
+ constructor(opt) {
20
+ this.opt = opt;
21
+ this.jobs = []; // for full traversals
22
+ this.jobsByMessageId = {}; // for looking up via message id
23
+ this.jobsByQueue = new Map(); // for looking up via queue name
24
+ this.stats = {
25
+ activeJobs: 0,
26
+ waitingJobs: 0,
27
+ runningJobs: 0,
28
+ sqsCalls: 0,
29
+ timeoutsExtended: 0,
30
+ jobsSucceeded: 0,
31
+ jobsFailed: 0,
32
+ jobsDeleted: 0
33
+ };
34
+ this.maintainPromise = this.maintainVisibility();
35
+ debug({ this: this });
36
+ }
37
+ async shutdown() {
38
+ this.shutdownRequested = true;
39
+ // Trigger a maintenance run right away in case it speeds us up
40
+ clearTimeout(this.maintainVisibilityTimeout);
41
+ if (this.opt.verbose) {
42
+ console.error(chalk_1.default.blue('Shutting down jobExecutor'));
43
+ }
44
+ await this.maintainPromise;
45
+ await this.maintainVisibility();
46
+ }
47
+ activeJobCount() {
48
+ return this.stats.activeJobs;
49
+ }
50
+ runningJobCount() {
51
+ return this.stats.runningJobs;
52
+ }
53
+ /**
54
+ * Returns the number of jobs running in a queue.
55
+ */
56
+ runningJobCountForQueue(qname) {
57
+ const jobs = this.jobsByQueue.get(qname) || new Set();
58
+ let runningCount = 0;
59
+ for (const job of jobs.values())
60
+ runningCount += job.status === 'running';
61
+ return runningCount;
62
+ }
63
+ /**
64
+ * Changes message visibility on all running jobs using as few calls as possible.
65
+ */
66
+ async maintainVisibility() {
67
+ // Bail if we are shutting down
68
+ if (this.shutdownRequested && this.stats.activeJobs === 0 && this.jobs.length === 0) {
69
+ if (this.opt.verbose) {
70
+ console.error(chalk_1.default.blue('All workers done, finishing shutdown of jobExecutor'));
71
+ }
72
+ return;
73
+ }
74
+ // Reset our timeout
75
+ clearTimeout(this.maintainVisibilityTimeout);
76
+ const nextCheckInMs = this.shutdownRequested ? 1000 : 10 * 1000;
77
+ this.maintainVisibilityTimeout = setTimeout(() => {
78
+ this.maintainPromise = this.maintainVisibility();
79
+ }, nextCheckInMs);
80
+ // debug('maintainVisibility', this.jobs)
81
+ const start = new Date();
82
+ const jobsToExtendByQrl = {};
83
+ const jobsToDeleteByQrl = {};
84
+ const jobsToCleanup = new Set();
85
+ // Build list of jobs we need to deal with
86
+ const jobStatuses = {};
87
+ for (let i = 0; i < this.jobs.length; i++) {
88
+ const job = this.jobs[i];
89
+ const jobRunTime = Math.round((start - job.start) / 1000);
90
+ jobStatuses[job.status] = (jobStatuses[job.status] || 0) + 1;
91
+ // debug('considering job', job)
92
+ if (job.status === 'complete') {
93
+ const jobsToDelete = jobsToDeleteByQrl[job.qrl] || [];
94
+ job.status = 'deleting';
95
+ jobsToDelete.push(job);
96
+ jobsToDeleteByQrl[job.qrl] = jobsToDelete;
97
+ }
98
+ else if (job.status === 'failed') {
99
+ jobsToCleanup.add(job);
100
+ }
101
+ else if (job.status !== 'deleting') {
102
+ // Any other job state gets visibility accounting
103
+ debug('processing', { job, jobRunTime });
104
+ if (jobRunTime >= job.extendAtSecond) {
105
+ // Add it to our organized list of jobs
106
+ const jobsToExtend = jobsToExtendByQrl[job.qrl] || [];
107
+ jobsToExtend.push(job);
108
+ jobsToExtendByQrl[job.qrl] = jobsToExtend;
109
+ // Update the visibility timeout, double every time, up to max
110
+ const doubled = job.visibilityTimeout * 2;
111
+ const secondsUntilMax = Math.max(1, maxJobSeconds - jobRunTime);
112
+ // const secondsUntilKill = Math.max(1, this.opt.killAfter - jobRunTime)
113
+ job.visibilityTimeout = Math.min(doubled, secondsUntilMax); //, secondsUntilKill)
114
+ job.extendAtSecond = Math.round(jobRunTime + job.visibilityTimeout / 2); // this is what we use next time
115
+ debug({ doubled, secondsUntilMax, job });
116
+ }
117
+ }
118
+ }
119
+ if (this.opt.verbose) {
120
+ console.error(chalk_1.default.blue('Stats: '), { stats: this.stats, jobStatuses });
121
+ console.error(chalk_1.default.blue('Jobs: '));
122
+ for (const [qname, jobs] of this.jobsByQueue.entries()) {
123
+ if (jobs.size) {
124
+ const stats = {};
125
+ for (const job of jobs)
126
+ stats[job.status] = (stats[job.status] || 0) + 1;
127
+ console.error(chalk_1.default.blue(' queue:'), qname);
128
+ if (stats.running)
129
+ console.error(chalk_1.default.green(' running: '), stats.running);
130
+ if (stats.waiting)
131
+ console.error(chalk_1.default.yellow(' waiting: '), stats.waiting);
132
+ if (stats.deleting)
133
+ console.error(chalk_1.default.blue(' complete: '), stats.deleting);
134
+ if (stats.failed)
135
+ console.error(chalk_1.default.red(' failed: '), stats.failed);
136
+ }
137
+ }
138
+ }
139
+ // Extend in batches for each queue
140
+ for (const qrl in jobsToExtendByQrl) {
141
+ const jobsToExtend = jobsToExtendByQrl[qrl];
142
+ debug({ qrl, jobsToExtend });
143
+ while (jobsToExtend.length) {
144
+ // Build list of messages to go in this batch
145
+ const entries = [];
146
+ let messageId = 0;
147
+ while (messageId++ < 10 && jobsToExtend.length) {
148
+ const job = jobsToExtend.shift();
149
+ const entry = {
150
+ Id: job.message.MessageId,
151
+ ReceiptHandle: job.message.ReceiptHandle,
152
+ VisibilityTimeout: job.visibilityTimeout
153
+ };
154
+ entries.push(entry);
155
+ }
156
+ debug({ entries });
157
+ // Change batch
158
+ const input = { QueueUrl: qrl, Entries: entries };
159
+ debug({ ChangeMessageVisibilityBatch: input });
160
+ const result = await (0, sqs_js_1.getSQSClient)().send(new client_sqs_1.ChangeMessageVisibilityBatchCommand(input));
161
+ debug('ChangeMessageVisibilityBatch returned', result);
162
+ this.stats.sqsCalls++;
163
+ if (result.Failed) {
164
+ console.error('FAILED_MESSAGES', result.Failed);
165
+ for (const failed of result.Failed) {
166
+ console.error('FAILED_TO_EXTEND_JOB', { failedEntry: failed, job: this.jobsByMessageId[failed.Id] });
167
+ // ensure that we clean this one up so it doesn't generate api calls
168
+ if (this.jobsByMessageId[failed.Id])
169
+ this.jobsByMessageId[failed.Id].status = 'failed';
170
+ }
171
+ }
172
+ if (result.Successful) {
173
+ const count = result.Successful.length || 0;
174
+ this.stats.timeoutsExtended += count;
175
+ if (this.opt.verbose) {
176
+ console.error(chalk_1.default.blue('Extended'), count, chalk_1.default.blue('jobs'));
177
+ }
178
+ else if (!this.opt.disableLog) {
179
+ console.log(JSON.stringify({ event: 'EXTEND_VISIBILITY_TIMEOUTS', timestamp: start, count, qrl }));
180
+ }
181
+ }
182
+ // TODO Sentry
183
+ }
184
+ }
185
+ // Delete in batches for each queue
186
+ for (const qrl in jobsToDeleteByQrl) {
187
+ const jobsToDelete = jobsToDeleteByQrl[qrl];
188
+ while (jobsToDelete.length) {
189
+ // Build list of messages to go in this batch
190
+ const entries = [];
191
+ let messageId = 0;
192
+ while (messageId++ < 10 && jobsToDelete.length) {
193
+ const job = jobsToDelete.shift();
194
+ const entry = {
195
+ Id: job.message.MessageId,
196
+ ReceiptHandle: job.message.ReceiptHandle,
197
+ VisibilityTimeout: job.visibilityTimeout
198
+ };
199
+ entries.push(entry);
200
+ }
201
+ debug({ entries });
202
+ // Delete batch
203
+ const input = { QueueUrl: qrl, Entries: entries };
204
+ debug({ DeleteMessageBatch: input });
205
+ const result = await (0, sqs_js_1.getSQSClient)().send(new client_sqs_1.DeleteMessageBatchCommand(input));
206
+ this.stats.sqsCalls++;
207
+ if (result.Failed) {
208
+ console.error('FAILED_MESSAGES', result.Failed);
209
+ for (const failed of result.Failed) {
210
+ console.error('FAILED_TO_DELETE_JOB', { failedEntry: failed, job: this.jobsByMessageId[failed.Id] });
211
+ // ensure that we clean this one up so it doesn't generate api calls
212
+ if (this.jobsByMessageId[failed.Id])
213
+ this.jobsByMessageId[failed.Id].status = 'failed';
214
+ }
215
+ }
216
+ if (result.Successful) {
217
+ const count = result.Successful.length || 0;
218
+ this.stats.jobsDeleted += count;
219
+ if (this.opt.verbose) {
220
+ console.error(chalk_1.default.blue('Deleted'), count, chalk_1.default.blue('jobs'));
221
+ }
222
+ else if (!this.opt.disableLog) {
223
+ console.log(JSON.stringify({ event: 'DELETE_MESSAGES', timestamp: start, count, qrl }));
224
+ }
225
+ // Mark batch as processed for dedup
226
+ await Promise.all(result.Successful.filter(e => this.jobsByMessageId[e.Id]).map(e => (0, dedup_js_1.dedupSuccessfullyProcessed)(this.jobsByMessageId[e.Id].message, this.opt)));
227
+ }
228
+ debug('DeleteMessageBatch returned', result);
229
+ // TODO Sentry
230
+ }
231
+ }
232
+ // Get rid of deleted and failed jobs
233
+ this.jobs = this.jobs.filter(job => {
234
+ if (job.status === 'deleting' || job.status === 'failed') {
235
+ debug('removed', job.message.MessageId);
236
+ // Accounting
237
+ delete this.jobsByMessageId[job.message.MessageId];
238
+ this.jobsByQueue.get(job.qname).delete(job);
239
+ return false;
240
+ }
241
+ else {
242
+ return true;
243
+ }
244
+ });
245
+ }
246
+ addJob(message, callback, qname, qrl) {
247
+ // Create job entry and track it
248
+ const defaultVisibilityTimeout = 120;
249
+ const job = {
250
+ status: 'waiting',
251
+ start: new Date(),
252
+ visibilityTimeout: defaultVisibilityTimeout,
253
+ extendAtSecond: defaultVisibilityTimeout / 2,
254
+ payload: this.opt.json ? JSON.parse(message.Body) : message.Body,
255
+ message,
256
+ callback,
257
+ qname,
258
+ prettyQname: qname.slice(this.opt.prefix.length),
259
+ qrl
260
+ };
261
+ // See if we are already executing this job
262
+ const oldJob = this.jobsByMessageId[job.message.MessageId];
263
+ if (oldJob) {
264
+ // If we actually see the same job again, we fucked up, probably due to
265
+ // the system being overloaded and us missing our extension call. So
266
+ // we'll celebrate this occasion by throwing a big fat error.
267
+ debug({ oldJob });
268
+ const e = new Error(`Saw job ${oldJob.message.MessageId} twice`);
269
+ e.job = oldJob;
270
+ // TODO: sentry breadcrumb
271
+ throw e;
272
+ }
273
+ // Accounting
274
+ this.jobs.push(job);
275
+ this.jobsByMessageId[job.message.MessageId] = job;
276
+ // Track all jobs for each queue
277
+ if (!this.jobsByQueue.has(job.qname))
278
+ this.jobsByQueue.set(job.qname, new Set());
279
+ this.jobsByQueue.get(job.qname).add(job);
280
+ this.stats.activeJobs++;
281
+ this.stats.waitingJobs++;
282
+ if (this.opt.verbose) {
283
+ console.error(chalk_1.default.blue('Got message:'), job.prettyQname, chalk_1.default.blue('-->'), job.payload, job.message.MessageId);
284
+ }
285
+ else if (!this.opt.disableLog) {
286
+ console.log(JSON.stringify({
287
+ event: 'MESSAGE_RECEIVED',
288
+ timestamp: new Date(),
289
+ queue: job.qname,
290
+ messageId: message.MessageId,
291
+ payload: job.payload
292
+ }));
293
+ }
294
+ return job;
295
+ }
296
+ async runJob(job) {
297
+ try {
298
+ if (this.opt.verbose) {
299
+ console.error(chalk_1.default.blue('Running:'), job.prettyQname, chalk_1.default.blue('-->'), job.payload, job.message.MessageId);
300
+ }
301
+ else if (!this.opt.disableLog) {
302
+ console.log(JSON.stringify({
303
+ event: 'MESSAGE_PROCESSING_START',
304
+ timestamp: new Date(),
305
+ queue: job.qname,
306
+ messageId: job.message.MessageId,
307
+ payload: job.payload
308
+ }));
309
+ }
310
+ job.status = 'running';
311
+ this.stats.runningJobs++;
312
+ this.stats.waitingJobs--;
313
+ const queue = job.qname.slice(this.opt.prefix.length);
314
+ const result = await job.callback(queue, job.payload);
315
+ debug('executeJob callback finished', { payload: job.payload, result });
316
+ if (this.opt.verbose) {
317
+ console.error(chalk_1.default.green('SUCCESS'), job.payload);
318
+ }
319
+ job.status = 'complete';
320
+ if (this.opt.verbose) {
321
+ console.error(chalk_1.default.blue(' done'));
322
+ console.error();
323
+ }
324
+ else if (!this.opt.disableLog) {
325
+ console.log(JSON.stringify({
326
+ event: 'MESSAGE_PROCESSING_COMPLETE',
327
+ queue: job.qname,
328
+ timestamp: new Date(),
329
+ messageId: job.message.MessageId,
330
+ payload: job.payload
331
+ }));
332
+ }
333
+ this.stats.jobsSucceeded++;
334
+ }
335
+ catch (err) {
336
+ job.status = 'failed';
337
+ this.stats.jobsFailed++;
338
+ // Fail path for job execution
339
+ if (this.opt.verbose) {
340
+ console.error(chalk_1.default.red('FAILED'), job.payload);
341
+ console.error(chalk_1.default.blue(' error : ') + err);
342
+ }
343
+ else if (!this.opt.disableLog) {
344
+ // Production error logging
345
+ console.log(JSON.stringify({
346
+ event: 'MESSAGE_PROCESSING_FAILED',
347
+ reason: 'exception thrown',
348
+ queue: job.qname,
349
+ timestamp: new Date(),
350
+ messageId: job.message.MessageId,
351
+ payload: job.payload,
352
+ errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
353
+ err
354
+ }));
355
+ }
356
+ }
357
+ this.stats.activeJobs--;
358
+ this.stats.runningJobs--;
359
+ }
360
+ async executeJobs(messages, callback, qname, qrl) {
361
+ if (this.shutdownRequested)
362
+ throw new Error('jobExecutor is shutting down so cannot execute new jobs');
363
+ // Begin tracking jobs
364
+ const jobs = messages.map(message => this.addJob(message, callback, qname, qrl));
365
+ const isFifo = qrl.endsWith('.fifo');
366
+ const runningJobs = [];
367
+ // console.log(jobs)
368
+ // Begin executing
369
+ for (const [job, i] of jobs.map((job, i) => [job, i])) {
370
+ // Figure out if the next job needs to happen in serial, otherwise we can parallel execute
371
+ const nextJob = jobs[i + 1];
372
+ const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.GroupId === nextJob.message?.Attributes?.GroupId;
373
+ // console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
374
+ // Execute serial or parallel
375
+ if (nextJobIsSerial)
376
+ await this.runJob(job);
377
+ else
378
+ runningJobs.push(this.runJob(job));
379
+ }
380
+ await Promise.all(runningJobs);
381
+ }
382
+ }
383
+ exports.JobExecutor = JobExecutor;
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ /**
3
+ * Component to manange what queues are being listened to and in what order.
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.QueueManager = void 0;
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const debug_1 = __importDefault(require("debug"));
12
+ const qrlCache_js_1 = require("../qrlCache.js");
13
+ const idleQueues_js_1 = require("../idleQueues.js");
14
+ const debug = (0, debug_1.default)('qdone:queueManager');
15
+ class QueueManager {
16
+ constructor(opt, queues, resolveSeconds = 10) {
17
+ this.opt = opt;
18
+ this.queues = queues;
19
+ this.resolveSeconds = resolveSeconds;
20
+ this.selectedPairs = [];
21
+ this.icehouse = {};
22
+ this.resolveTimeout = undefined;
23
+ this.shutdownRequested = false;
24
+ this.resolvePromise = this.resolveQueues();
25
+ }
26
+ // Sends a queue to the icehouse, where it waits for a while before being
27
+ // checked again
28
+ updateIcehouse(qrl, emptyReceive) {
29
+ const foundEntry = !!this.icehouse[qrl];
30
+ const { lastCheck, secondsToWait, numEmptyReceives } = this.icehouse[qrl] || {
31
+ lastCheck: new Date(),
32
+ secondsToWait: Math.round(20 + 10 * Math.random()),
33
+ numEmptyReceives: 0 + emptyReceive
34
+ };
35
+ if (emptyReceive) {
36
+ const now = new Date();
37
+ const secondsElapsed = lastCheck - now;
38
+ const minWait = 10;
39
+ const maxWait = 120;
40
+ const baseSeconds = numEmptyReceives ** 2 * 20;
41
+ const jitterSeconds = Math.round((Math.random() - 0.5) * baseSeconds);
42
+ const newSecondsToWait = Math.max(minWait, Math.min(maxWait, baseSeconds + jitterSeconds));
43
+ const newEntry = { lastCheck: now, secondsToWait: newSecondsToWait, numEmptyReceives: numEmptyReceives + 1 };
44
+ this.icehouse[qrl] = newEntry;
45
+ if (this.opt.verbose) {
46
+ console.error(chalk_1.default.blue('Sending queue to icehouse'), qrl, chalk_1.default.blue('for'), newSecondsToWait, chalk_1.default.blue('seconds'));
47
+ }
48
+ debug({ foundEntry, newEntry, lastCheck, secondsToWait, now, secondsElapsed, maxWait, minWait, baseSeconds, jitterSeconds });
49
+ }
50
+ else {
51
+ delete this.icehouse[qrl];
52
+ }
53
+ }
54
+ // Returns true if the queue should be kept in the icehouse
55
+ keepInIcehouse(qrl, now) {
56
+ if (this.icehouse[qrl]) {
57
+ const { lastCheck, secondsToWait } = this.icehouse[qrl];
58
+ const secondsElapsed = Math.round((now - lastCheck) / 1000);
59
+ debug({ icehouseCheck: { qrl, lastCheck, secondsToWait, secondsElapsed } });
60
+ const letOut = secondsElapsed > secondsToWait;
61
+ if (letOut) {
62
+ delete this.icehouse[qrl];
63
+ if (this.opt.verbose) {
64
+ console.error(chalk_1.default.blue('Coming out of icehouse:'), qrl);
65
+ }
66
+ }
67
+ return !letOut;
68
+ }
69
+ else {
70
+ return false;
71
+ }
72
+ }
73
+ async resolveQueues() {
74
+ clearTimeout(this.resolveTimeout);
75
+ if (this.shutdownRequested)
76
+ return;
77
+ this.resolveTimeout = setTimeout(() => {
78
+ this.resolvePromise = this.resolveQueues();
79
+ }, this.resolveSeconds * 1000);
80
+ if (this.opt.verbose) {
81
+ console.error(chalk_1.default.blue('Will resolve queues again in ' + this.resolveSeconds + ' seconds'));
82
+ }
83
+ // Start processing
84
+ const qnames = this.queues.map(queue => (0, qrlCache_js_1.normalizeQueueName)(queue, this.opt));
85
+ const pairs = await (0, qrlCache_js_1.getQnameUrlPairs)(qnames, this.opt);
86
+ if (this.opt.verbose)
87
+ console.error(chalk_1.default.blue('Resolving queues:'), pairs.map(({ qname }) => qname));
88
+ if (this.shutdownRequested)
89
+ return;
90
+ // Filter out queues
91
+ const now = new Date();
92
+ const filteredPairs = pairs
93
+ // first failed
94
+ .filter(({ qname, qrl }) => {
95
+ const suf = this.opt.failSuffix + (this.opt.fifo ? '.fifo' : '');
96
+ const isFailQueue = qname.slice(-suf.length) === suf;
97
+ return this.opt.includeFailed ? true : !isFailQueue;
98
+ })
99
+ // next fifo
100
+ .filter(({ qname, qrl }) => {
101
+ const isFifo = qname.endsWith('.fifo');
102
+ return this.opt.fifo ? isFifo : true;
103
+ })
104
+ // next dead
105
+ .filter(({ qname, qrl }) => {
106
+ const isFifo = qname.endsWith('.fifo');
107
+ const isDead = isFifo ? qname.endsWith('_dead.fifo') : qname.endsWith('_dead');
108
+ return this.opt.includeDead ? true : !isDead;
109
+ })
110
+ // then icehouse
111
+ .filter(({ qname, qrl }) => !this.keepInIcehouse(qrl, now));
112
+ // Figure out which pairs are active
113
+ const activePairs = [];
114
+ if (this.opt.activeOnly) {
115
+ if (this.opt.verbose) {
116
+ console.error(chalk_1.default.blue(' checking active only'));
117
+ }
118
+ await Promise.all(filteredPairs.map(async ({ qname, qrl }) => {
119
+ const { result: { idle } } = await (0, idleQueues_js_1.cheapIdleCheck)(qname, qrl, this.opt);
120
+ debug({ idle, qname });
121
+ if (!idle)
122
+ activePairs.push({ qname, qrl });
123
+ }));
124
+ }
125
+ if (this.shutdownRequested)
126
+ return;
127
+ // Figure out which queues we want to listen on, choosing between active and
128
+ // all, filtering out failed queues if the user wants that
129
+ this.selectedPairs = (this.opt.activeOnly ? activePairs : filteredPairs);
130
+ // Randomize order
131
+ this.selectedPairs.sort(() => 0.5 - Math.random());
132
+ if (this.opt.verbose)
133
+ console.error(chalk_1.default.blue(' selected:\n ') + this.selectedPairs.map(({ qname }) => qname).join('\n '));
134
+ debug('selectedPairs', this.selectedPairs);
135
+ // Finished resolving
136
+ if (this.opt.verbose) {
137
+ console.error(chalk_1.default.blue('Done resolving'));
138
+ console.error();
139
+ }
140
+ }
141
+ // Return the next queue in the lineup
142
+ nextPair() {
143
+ const pair = this.selectedPairs.shift();
144
+ this.selectedPairs.push(pair);
145
+ return pair;
146
+ }
147
+ getPairs() {
148
+ const now = new Date();
149
+ this.selectedPairs.sort(() => 0.5 - Math.random());
150
+ return this.selectedPairs.filter(({ qname, qrl }) => !this.keepInIcehouse(qrl, now));
151
+ }
152
+ async shutdown() {
153
+ this.shutdownRequested = true;
154
+ clearTimeout(this.resolveTimeout);
155
+ if (this.opt.verbose) {
156
+ console.error(chalk_1.default.blue('Waiting for queues to resolve'));
157
+ }
158
+ await this.resolvePromise;
159
+ }
160
+ }
161
+ exports.QueueManager = QueueManager;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ /**
3
+ * Component to track event loop latency, which can be used as a metric for
4
+ * backpressure.
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SystemMonitor = void 0;
11
+ const os_1 = __importDefault(require("os"));
12
+ class SystemMonitor {
13
+ constructor(reportCallback, reportSeconds = 1) {
14
+ this.reportCallback = reportCallback || console.log;
15
+ this.reportSeconds = reportSeconds;
16
+ this.latencies = [];
17
+ this.oneMinuteLoad = os_1.default.loadavg()[0];
18
+ this.instantaneousLoad = this.oneMinuteLoad;
19
+ this.measure();
20
+ this.reportLatency();
21
+ }
22
+ measure() {
23
+ clearTimeout(this.measureTimeout);
24
+ const start = new Date();
25
+ this.measureTimeout = setTimeout(() => {
26
+ this.measureLatency(start);
27
+ this.measureLoad();
28
+ this.measure();
29
+ });
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
+ }
37
+ getLatency() {
38
+ return this.latencies.length ? this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length : 0;
39
+ }
40
+ reportLatency() {
41
+ clearTimeout(this.reportTimeout);
42
+ this.reportTimeout = setTimeout(() => {
43
+ const latency = this.getLatency();
44
+ // console.log({ latency })
45
+ if (this.reportCallback)
46
+ this.reportCallback(latency);
47
+ this.reportLatency();
48
+ }, this.reportSeconds * 1000);
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
+ }
89
+ shutdown() {
90
+ clearTimeout(this.measureTimeout);
91
+ clearTimeout(this.reportTimeout);
92
+ }
93
+ }
94
+ exports.SystemMonitor = SystemMonitor;