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.
- package/LICENSE +2 -2
- package/README.md +1 -4
- package/commonjs/index.js +11 -0
- package/commonjs/src/cache.js +72 -0
- package/commonjs/src/cloudWatch.js +102 -0
- package/commonjs/src/consumer.js +173 -0
- package/commonjs/src/dedup.js +266 -0
- package/commonjs/src/defaults.js +182 -0
- package/commonjs/src/enqueue.js +521 -0
- package/commonjs/src/exponentialBackoff.js +101 -0
- package/commonjs/src/idleQueues.js +333 -0
- package/commonjs/src/monitor.js +80 -0
- package/commonjs/src/qrlCache.js +172 -0
- package/commonjs/src/scheduler/jobExecutor.js +383 -0
- package/commonjs/src/scheduler/queueManager.js +161 -0
- package/commonjs/src/scheduler/systemMonitor.js +94 -0
- package/commonjs/src/sqs.js +97 -0
- package/package.json +7 -3
|
@@ -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;
|