qdone 2.0.29-alpha → 2.0.31-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.
@@ -3,60 +3,27 @@
3
3
  * Component to manage all the currently executing jobs, including extending
4
4
  * their visibility timeouts and deleting them when they are successful.
5
5
  */
6
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
7
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
8
- return new (P || (P = Promise))(function (resolve, reject) {
9
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
10
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
11
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
12
- step((generator = generator.apply(thisArg, _arguments || [])).next());
13
- });
14
- };
15
- var __generator = (this && this.__generator) || function (thisArg, body) {
16
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
17
- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
18
- function verb(n) { return function (v) { return step([n, v]); }; }
19
- function step(op) {
20
- if (f) throw new TypeError("Generator is already executing.");
21
- while (g && (g = 0, op[0] && (_ = 0)), _) try {
22
- if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
23
- if (y = 0, t) op = [op[0] & 2, t.value];
24
- switch (op[0]) {
25
- case 0: case 1: t = op; break;
26
- case 4: _.label++; return { value: op[1], done: false };
27
- case 5: _.label++; y = op[1]; op = [0]; continue;
28
- case 7: op = _.ops.pop(); _.trys.pop(); continue;
29
- default:
30
- if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
31
- if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
32
- if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
33
- if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
34
- if (t[2]) _.ops.pop();
35
- _.trys.pop(); continue;
36
- }
37
- op = body.call(thisArg, _);
38
- } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
39
- if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
40
- }
41
- };
42
6
  var __importDefault = (this && this.__importDefault) || function (mod) {
43
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
44
8
  };
45
9
  Object.defineProperty(exports, "__esModule", { value: true });
46
10
  exports.JobExecutor = void 0;
47
- var client_sqs_1 = require("@aws-sdk/client-sqs");
48
- var chalk_1 = __importDefault(require("chalk"));
49
- var debug_1 = __importDefault(require("debug"));
50
- var sqs_js_1 = require("../sqs.js");
51
- var debug = (0, debug_1.default)('qdone:jobExecutor');
52
- var maxJobSeconds = 12 * 60 * 60;
53
- var JobExecutor = /** @class */ (function () {
54
- function JobExecutor(opt) {
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 sqs_js_1 = require("../sqs.js");
15
+ const debug = (0, debug_1.default)('qdone:jobExecutor');
16
+ const maxJobSeconds = 12 * 60 * 60;
17
+ class JobExecutor {
18
+ constructor(opt) {
55
19
  this.opt = opt;
56
20
  this.jobs = [];
57
21
  this.jobsByMessageId = {};
22
+ this.jobsByQueue = new Map();
58
23
  this.stats = {
59
24
  activeJobs: 0,
25
+ waitingJobs: 0,
26
+ runningJobs: 0,
60
27
  sqsCalls: 0,
61
28
  timeoutsExtended: 0,
62
29
  jobsSucceeded: 0,
@@ -66,330 +33,324 @@ var JobExecutor = /** @class */ (function () {
66
33
  this.maintainPromise = this.maintainVisibility();
67
34
  debug({ this: this });
68
35
  }
69
- JobExecutor.prototype.shutdown = function () {
70
- return __awaiter(this, void 0, void 0, function () {
71
- return __generator(this, function (_a) {
72
- switch (_a.label) {
73
- case 0:
74
- this.shutdownRequested = true;
75
- // Trigger a maintenance run right away in case it speeds us up
76
- clearTimeout(this.maintainVisibilityTimeout);
77
- if (this.opt.verbose) {
78
- console.error(chalk_1.default.blue('Shutting down jobExecutor'));
79
- }
80
- return [4 /*yield*/, this.maintainPromise];
81
- case 1:
82
- _a.sent();
83
- return [4 /*yield*/, this.maintainVisibility()];
84
- case 2:
85
- _a.sent();
86
- return [2 /*return*/];
87
- }
88
- });
89
- });
90
- };
91
- JobExecutor.prototype.activeJobCount = function () {
36
+ async shutdown() {
37
+ this.shutdownRequested = true;
38
+ // Trigger a maintenance run right away in case it speeds us up
39
+ clearTimeout(this.maintainVisibilityTimeout);
40
+ if (this.opt.verbose) {
41
+ console.error(chalk_1.default.blue('Shutting down jobExecutor'));
42
+ }
43
+ await this.maintainPromise;
44
+ await this.maintainVisibility();
45
+ }
46
+ activeJobCount() {
92
47
  return this.stats.activeJobs;
93
- };
48
+ }
49
+ runningJobCount() {
50
+ return this.stats.runningJobs;
51
+ }
52
+ /**
53
+ * Returns the number of jobs running in a queue.
54
+ */
55
+ runningJobCountForQueue(qname) {
56
+ const jobs = this.jobsByQueue.get(qname) || new Set();
57
+ let runningCount = 0;
58
+ for (const job of jobs.values())
59
+ runningCount += job.status === 'running';
60
+ return runningCount;
61
+ }
94
62
  /**
95
63
  * Changes message visibility on all running jobs using as few calls as possible.
96
64
  */
97
- JobExecutor.prototype.maintainVisibility = function () {
98
- return __awaiter(this, void 0, void 0, function () {
99
- var nextCheckInMs, start, jobsToExtendByQrl, jobsToDeleteByQrl, jobsToCleanup, i, job, jobRunTime, jobsToDelete, jobsToExtend, doubled, secondsUntilMax, _a, _b, _c, _i, qrl, jobsToExtend, entries, messageId, job, entry, input, result, _d, _e, failed, count, _f, _g, _h, _j, qrl, jobsToDelete, entries, messageId, job, entry, input, result, _k, _l, failed, count;
100
- var _this = this;
101
- return __generator(this, function (_m) {
102
- switch (_m.label) {
103
- case 0:
104
- // Bail if we are shutting down
105
- if (this.shutdownRequested && this.stats.activeJobs === 0 && this.jobs.length === 0) {
106
- if (this.opt.verbose) {
107
- console.error(chalk_1.default.blue('All workers done, finishing shutdown of jobExecutor'));
108
- }
109
- return [2 /*return*/];
110
- }
111
- // Reset our timeout
112
- clearTimeout(this.maintainVisibilityTimeout);
113
- nextCheckInMs = this.shutdownRequested ? 1000 : 10 * 1000;
114
- this.maintainVisibilityTimeout = setTimeout(function () {
115
- _this.maintainPromise = _this.maintainVisibility();
116
- }, nextCheckInMs);
117
- start = new Date();
118
- jobsToExtendByQrl = {};
119
- jobsToDeleteByQrl = {};
120
- jobsToCleanup = new Set();
121
- if (this.opt.verbose) {
122
- console.error(chalk_1.default.blue('Stats: '), this.stats);
123
- console.error(chalk_1.default.blue('Running: '), this.jobs.filter(function (j) { return j.status === 'processing'; }).map(function (_a) {
124
- var qname = _a.qname, message = _a.message;
125
- return ({ qname: qname, payload: message.Body });
126
- }));
127
- }
128
- // Build list of jobs we need to deal with
129
- for (i = 0; i < this.jobs.length; i++) {
130
- job = this.jobs[i];
131
- jobRunTime = Math.round((start - job.start) / 1000);
132
- // debug('considering job', job)
133
- if (job.status === 'complete') {
134
- jobsToDelete = jobsToDeleteByQrl[job.qrl] || [];
135
- job.status = 'deleting';
136
- jobsToDelete.push(job);
137
- jobsToDeleteByQrl[job.qrl] = jobsToDelete;
138
- }
139
- else if (job.status === 'failed') {
140
- jobsToCleanup.add(job);
141
- }
142
- else if (job.status === 'processing') {
143
- debug('processing', { job: job, jobRunTime: jobRunTime });
144
- if (jobRunTime >= job.extendAtSecond) {
145
- jobsToExtend = jobsToExtendByQrl[job.qrl] || [];
146
- jobsToExtend.push(job);
147
- jobsToExtendByQrl[job.qrl] = jobsToExtend;
148
- doubled = job.visibilityTimeout * 2;
149
- secondsUntilMax = Math.max(1, maxJobSeconds - jobRunTime);
150
- // const secondsUntilKill = Math.max(1, this.opt.killAfter - jobRunTime)
151
- job.visibilityTimeout = Math.min(doubled, secondsUntilMax); //, secondsUntilKill)
152
- job.extendAtSecond = Math.round(jobRunTime + job.visibilityTimeout / 2); // this is what we use next time
153
- debug({ doubled: doubled, secondsUntilMax: secondsUntilMax, job: job });
154
- }
155
- }
156
- }
157
- _a = jobsToExtendByQrl;
158
- _b = [];
159
- for (_c in _a)
160
- _b.push(_c);
161
- _i = 0;
162
- _m.label = 1;
163
- case 1:
164
- if (!(_i < _b.length)) return [3 /*break*/, 5];
165
- _c = _b[_i];
166
- if (!(_c in _a)) return [3 /*break*/, 4];
167
- qrl = _c;
168
- jobsToExtend = jobsToExtendByQrl[qrl];
169
- debug({ qrl: qrl, jobsToExtend: jobsToExtend });
170
- _m.label = 2;
171
- case 2:
172
- if (!jobsToExtend.length) return [3 /*break*/, 4];
173
- entries = [];
174
- messageId = 0;
175
- while (messageId++ < 10 && jobsToExtend.length) {
176
- job = jobsToExtend.shift();
177
- entry = {
178
- Id: job.message.MessageId,
179
- ReceiptHandle: job.message.ReceiptHandle,
180
- VisibilityTimeout: job.visibilityTimeout
181
- };
182
- entries.push(entry);
183
- }
184
- debug({ entries: entries });
185
- input = { QueueUrl: qrl, Entries: entries };
186
- debug({ ChangeMessageVisibilityBatch: input });
187
- return [4 /*yield*/, (0, sqs_js_1.getSQSClient)().send(new client_sqs_1.ChangeMessageVisibilityBatchCommand(input))];
188
- case 3:
189
- result = _m.sent();
190
- debug('ChangeMessageVisibilityBatch returned', result);
191
- this.stats.sqsCalls++;
192
- if (result.Failed) {
193
- console.error('FAILED_MESSAGES', result.Failed);
194
- for (_d = 0, _e = result.Failed; _d < _e.length; _d++) {
195
- failed = _e[_d];
196
- console.error('FAILED_TO_EXTEND_JOB', this.jobsByMessageId[failed.Id]);
197
- }
198
- }
199
- if (result.Successful) {
200
- count = result.Successful.length || 0;
201
- this.stats.timeoutsExtended += count;
202
- if (this.opt.verbose) {
203
- console.error(chalk_1.default.blue('Extended'), count, chalk_1.default.blue('jobs'));
204
- }
205
- else if (!this.opt.disableLog) {
206
- console.log(JSON.stringify({ event: 'EXTEND_VISIBILITY_TIMEOUTS', timestamp: start, count: count, qrl: qrl }));
207
- }
208
- }
209
- return [3 /*break*/, 2];
210
- case 4:
211
- _i++;
212
- return [3 /*break*/, 1];
213
- case 5:
214
- _f = jobsToDeleteByQrl;
215
- _g = [];
216
- for (_h in _f)
217
- _g.push(_h);
218
- _j = 0;
219
- _m.label = 6;
220
- case 6:
221
- if (!(_j < _g.length)) return [3 /*break*/, 10];
222
- _h = _g[_j];
223
- if (!(_h in _f)) return [3 /*break*/, 9];
224
- qrl = _h;
225
- jobsToDelete = jobsToDeleteByQrl[qrl];
226
- _m.label = 7;
227
- case 7:
228
- if (!jobsToDelete.length) return [3 /*break*/, 9];
229
- entries = [];
230
- messageId = 0;
231
- while (messageId++ < 10 && jobsToDelete.length) {
232
- job = jobsToDelete.shift();
233
- entry = {
234
- Id: job.message.MessageId,
235
- ReceiptHandle: job.message.ReceiptHandle,
236
- VisibilityTimeout: job.visibilityTimeout
237
- };
238
- entries.push(entry);
239
- }
240
- debug({ entries: entries });
241
- input = { QueueUrl: qrl, Entries: entries };
242
- debug({ DeleteMessageBatch: input });
243
- return [4 /*yield*/, (0, sqs_js_1.getSQSClient)().send(new client_sqs_1.DeleteMessageBatchCommand(input))];
244
- case 8:
245
- result = _m.sent();
246
- this.stats.sqsCalls++;
247
- if (result.Failed) {
248
- console.error('FAILED_MESSAGES', result.Failed);
249
- for (_k = 0, _l = result.Failed; _k < _l.length; _k++) {
250
- failed = _l[_k];
251
- console.error('FAILED_TO_DELETE_JOB', this.jobsByMessageId[failed.Id]);
252
- }
253
- }
254
- if (result.Successful) {
255
- count = result.Successful.length || 0;
256
- this.stats.jobsDeleted += count;
257
- if (this.opt.verbose) {
258
- console.error(chalk_1.default.blue('Deleted'), count, chalk_1.default.blue('jobs'));
259
- }
260
- else if (!this.opt.disableLog) {
261
- console.log(JSON.stringify({ event: 'DELETE_MESSAGES', timestamp: start, count: count, qrl: qrl }));
262
- }
263
- }
264
- debug('DeleteMessageBatch returned', result);
265
- return [3 /*break*/, 7];
266
- case 9:
267
- _j++;
268
- return [3 /*break*/, 6];
269
- case 10:
270
- // Get rid of deleted and failed jobs
271
- this.jobs = this.jobs.filter(function (job) {
272
- if (job.status === 'deleting' || job.status === 'failed') {
273
- debug('removed', job.message.MessageId);
274
- delete _this.jobsByMessageId[job.message.MessageId];
275
- return false;
276
- }
277
- else {
278
- return true;
279
- }
280
- });
281
- return [2 /*return*/];
65
+ async maintainVisibility() {
66
+ // Bail if we are shutting down
67
+ if (this.shutdownRequested && this.stats.activeJobs === 0 && this.jobs.length === 0) {
68
+ if (this.opt.verbose) {
69
+ console.error(chalk_1.default.blue('All workers done, finishing shutdown of jobExecutor'));
70
+ }
71
+ return;
72
+ }
73
+ // Reset our timeout
74
+ clearTimeout(this.maintainVisibilityTimeout);
75
+ const nextCheckInMs = this.shutdownRequested ? 1000 : 10 * 1000;
76
+ this.maintainVisibilityTimeout = setTimeout(() => {
77
+ this.maintainPromise = this.maintainVisibility();
78
+ }, nextCheckInMs);
79
+ // debug('maintainVisibility', this.jobs)
80
+ const start = new Date();
81
+ const jobsToExtendByQrl = {};
82
+ const jobsToDeleteByQrl = {};
83
+ const jobsToCleanup = new Set();
84
+ // Build list of jobs we need to deal with
85
+ const jobStatuses = {};
86
+ for (let i = 0; i < this.jobs.length; i++) {
87
+ const job = this.jobs[i];
88
+ const jobRunTime = Math.round((start - job.start) / 1000);
89
+ jobStatuses[job.status] = (jobStatuses[job.status] || 0) + 1;
90
+ // debug('considering job', job)
91
+ if (job.status === 'complete') {
92
+ const jobsToDelete = jobsToDeleteByQrl[job.qrl] || [];
93
+ job.status = 'deleting';
94
+ jobsToDelete.push(job);
95
+ jobsToDeleteByQrl[job.qrl] = jobsToDelete;
96
+ }
97
+ else if (job.status === 'failed') {
98
+ jobsToCleanup.add(job);
99
+ }
100
+ else if (job.status !== 'deleting') {
101
+ // Any other job state gets visibility accounting
102
+ debug('processing', { job, jobRunTime });
103
+ if (jobRunTime >= job.extendAtSecond) {
104
+ // Add it to our organized list of jobs
105
+ const jobsToExtend = jobsToExtendByQrl[job.qrl] || [];
106
+ jobsToExtend.push(job);
107
+ jobsToExtendByQrl[job.qrl] = jobsToExtend;
108
+ // Update the visibility timeout, double every time, up to max
109
+ const doubled = job.visibilityTimeout * 2;
110
+ const secondsUntilMax = Math.max(1, maxJobSeconds - jobRunTime);
111
+ // const secondsUntilKill = Math.max(1, this.opt.killAfter - jobRunTime)
112
+ job.visibilityTimeout = Math.min(doubled, secondsUntilMax); //, secondsUntilKill)
113
+ job.extendAtSecond = Math.round(jobRunTime + job.visibilityTimeout / 2); // this is what we use next time
114
+ debug({ doubled, secondsUntilMax, job });
282
115
  }
283
- });
284
- });
285
- };
286
- JobExecutor.prototype.executeJob = function (message, callback, qname, qrl, failedCallback) {
287
- return __awaiter(this, void 0, void 0, function () {
288
- var payload, visibilityTimeout, job, oldJob, e, queue, result, err_1;
289
- return __generator(this, function (_a) {
290
- switch (_a.label) {
291
- case 0:
292
- if (this.shutdownRequested)
293
- throw new Error('jobExecutor is shutting down so cannot execute new job');
294
- payload = this.opt.json ? JSON.parse(message.Body) : message.Body;
295
- visibilityTimeout = 60;
296
- job = {
297
- status: 'processing',
298
- start: new Date(),
299
- visibilityTimeout: visibilityTimeout,
300
- extendAtSecond: visibilityTimeout / 2,
301
- payload: this.opt.json ? JSON.parse(message.Body) : message.Body,
302
- message: message,
303
- callback: callback,
304
- qname: qname,
305
- qrl: qrl
306
- };
307
- oldJob = this.jobsByMessageId[job.message.MessageId];
308
- if (oldJob) {
309
- // If we actually see the same job again, we fucked up, probably due to
310
- // the system being overloaded and us missing our extension call. So
311
- // we'll celebrate this occasion by throwing a big fat error.
312
- debug({ oldJob: oldJob });
313
- e = new Error("Saw job ".concat(oldJob.message.MessageId, " twice"));
314
- e.job = oldJob;
315
- // TODO: sentry breadcrumb
316
- throw e;
317
- }
318
- // debug('executeJob', job)
319
- this.jobs.push(job);
320
- this.jobsByMessageId[job.message.MessageId] = job;
321
- this.stats.activeJobs++;
322
- if (this.opt.verbose) {
323
- console.error(chalk_1.default.blue('Executing:'), qname, chalk_1.default.blue('-->'), job.payload);
324
- }
325
- else if (!this.opt.disableLog) {
326
- console.log(JSON.stringify({
327
- event: 'MESSAGE_PROCESSING_START',
328
- timestamp: new Date(),
329
- qrl: qrl,
330
- messageId: message.MessageId,
331
- payload: job.payload
332
- }));
333
- }
334
- _a.label = 1;
335
- case 1:
336
- _a.trys.push([1, 3, , 4]);
337
- queue = qname.slice(this.opt.prefix.length);
338
- return [4 /*yield*/, callback(queue, payload)];
339
- case 2:
340
- result = _a.sent();
341
- debug('executeJob callback finished', { payload: payload, result: result });
342
- if (this.opt.verbose) {
343
- console.error(chalk_1.default.green('SUCCESS'), message.Body);
344
- }
345
- job.status = 'complete';
346
- if (this.opt.verbose) {
347
- console.error(chalk_1.default.blue(' done'));
348
- console.error();
349
- }
350
- else if (!this.opt.disableLog) {
351
- console.log(JSON.stringify({
352
- event: 'MESSAGE_PROCESSING_COMPLETE',
353
- timestamp: new Date(),
354
- messageId: message.MessageId,
355
- payload: payload
356
- }));
357
- }
358
- this.stats.jobsSucceeded++;
359
- return [3 /*break*/, 4];
360
- case 3:
361
- err_1 = _a.sent();
362
- // Notify caller that we failed
363
- if (failedCallback)
364
- failedCallback(message, qname, qrl);
365
- // Fail path for job execution
366
- if (this.opt.verbose) {
367
- console.error(chalk_1.default.red('FAILED'), message.Body);
368
- console.error(chalk_1.default.blue(' error : ') + err_1);
369
- }
370
- else if (!this.opt.disableLog) {
371
- // Production error logging
372
- console.log(JSON.stringify({
373
- event: 'MESSAGE_PROCESSING_FAILED',
374
- reason: 'exception thrown',
375
- qrl: qrl,
376
- timestamp: new Date(),
377
- messageId: message.MessageId,
378
- payload: payload,
379
- errorMessage: err_1.toString().split('\n').slice(1).join('\n').trim() || undefined,
380
- err: err_1
381
- }));
382
- }
383
- job.status = 'failed';
384
- this.stats.jobsFailed++;
385
- return [3 /*break*/, 4];
386
- case 4:
387
- this.stats.activeJobs--;
388
- return [2 /*return*/];
116
+ }
117
+ }
118
+ if (this.opt.verbose) {
119
+ console.error(chalk_1.default.blue('Stats: '), { stats: this.stats, jobStatuses });
120
+ console.error(chalk_1.default.blue('Running: '), this.jobs.filter(j => j.status === 'processing').map(({ qname, message }) => ({ qname, payload: message.Body })));
121
+ }
122
+ // Extend in batches for each queue
123
+ for (const qrl in jobsToExtendByQrl) {
124
+ const jobsToExtend = jobsToExtendByQrl[qrl];
125
+ debug({ qrl, jobsToExtend });
126
+ while (jobsToExtend.length) {
127
+ // Build list of messages to go in this batch
128
+ const entries = [];
129
+ let messageId = 0;
130
+ while (messageId++ < 10 && jobsToExtend.length) {
131
+ const job = jobsToExtend.shift();
132
+ const entry = {
133
+ Id: job.message.MessageId,
134
+ ReceiptHandle: job.message.ReceiptHandle,
135
+ VisibilityTimeout: job.visibilityTimeout
136
+ };
137
+ entries.push(entry);
138
+ }
139
+ debug({ entries });
140
+ // Change batch
141
+ const input = { QueueUrl: qrl, Entries: entries };
142
+ debug({ ChangeMessageVisibilityBatch: input });
143
+ const result = await (0, sqs_js_1.getSQSClient)().send(new client_sqs_1.ChangeMessageVisibilityBatchCommand(input));
144
+ debug('ChangeMessageVisibilityBatch returned', result);
145
+ this.stats.sqsCalls++;
146
+ if (result.Failed) {
147
+ console.error('FAILED_MESSAGES', result.Failed);
148
+ for (const failed of result.Failed) {
149
+ console.error('FAILED_TO_EXTEND_JOB', this.jobsByMessageId[failed.Id]);
150
+ }
151
+ }
152
+ if (result.Successful) {
153
+ const count = result.Successful.length || 0;
154
+ this.stats.timeoutsExtended += count;
155
+ if (this.opt.verbose) {
156
+ console.error(chalk_1.default.blue('Extended'), count, chalk_1.default.blue('jobs'));
157
+ }
158
+ else if (!this.opt.disableLog) {
159
+ console.log(JSON.stringify({ event: 'EXTEND_VISIBILITY_TIMEOUTS', timestamp: start, count, qrl }));
160
+ }
389
161
  }
390
- });
162
+ // TODO Sentry
163
+ }
164
+ }
165
+ // Delete in batches for each queue
166
+ for (const qrl in jobsToDeleteByQrl) {
167
+ const jobsToDelete = jobsToDeleteByQrl[qrl];
168
+ while (jobsToDelete.length) {
169
+ // Build list of messages to go in this batch
170
+ const entries = [];
171
+ let messageId = 0;
172
+ while (messageId++ < 10 && jobsToDelete.length) {
173
+ const job = jobsToDelete.shift();
174
+ const entry = {
175
+ Id: job.message.MessageId,
176
+ ReceiptHandle: job.message.ReceiptHandle,
177
+ VisibilityTimeout: job.visibilityTimeout
178
+ };
179
+ entries.push(entry);
180
+ }
181
+ debug({ entries });
182
+ // Delete batch
183
+ const input = { QueueUrl: qrl, Entries: entries };
184
+ debug({ DeleteMessageBatch: input });
185
+ const result = await (0, sqs_js_1.getSQSClient)().send(new client_sqs_1.DeleteMessageBatchCommand(input));
186
+ this.stats.sqsCalls++;
187
+ if (result.Failed) {
188
+ console.error('FAILED_MESSAGES', result.Failed);
189
+ for (const failed of result.Failed) {
190
+ console.error('FAILED_TO_DELETE_JOB', this.jobsByMessageId[failed.Id]);
191
+ }
192
+ }
193
+ if (result.Successful) {
194
+ const count = result.Successful.length || 0;
195
+ this.stats.jobsDeleted += count;
196
+ if (this.opt.verbose) {
197
+ console.error(chalk_1.default.blue('Deleted'), count, chalk_1.default.blue('jobs'));
198
+ }
199
+ else if (!this.opt.disableLog) {
200
+ console.log(JSON.stringify({ event: 'DELETE_MESSAGES', timestamp: start, count, qrl }));
201
+ }
202
+ }
203
+ debug('DeleteMessageBatch returned', result);
204
+ // TODO Sentry
205
+ }
206
+ }
207
+ // Get rid of deleted and failed jobs
208
+ this.jobs = this.jobs.filter(job => {
209
+ if (job.status === 'deleting' || job.status === 'failed') {
210
+ debug('removed', job.message.MessageId);
211
+ // Accounting
212
+ delete this.jobsByMessageId[job.message.MessageId];
213
+ this.jobsByQueue.get(job.qname).delete(job);
214
+ return false;
215
+ }
216
+ else {
217
+ return true;
218
+ }
391
219
  });
392
- };
393
- return JobExecutor;
394
- }());
220
+ }
221
+ addJob(message, callback, qname, qrl) {
222
+ // Create job entry and track it
223
+ const defaultVisibilityTimeout = 60;
224
+ const job = {
225
+ status: 'waiting',
226
+ start: new Date(),
227
+ visibilityTimeout: defaultVisibilityTimeout,
228
+ extendAtSecond: defaultVisibilityTimeout / 2,
229
+ payload: this.opt.json ? JSON.parse(message.Body) : message.Body,
230
+ message,
231
+ callback,
232
+ qname,
233
+ prettyQname: qname.slice(this.opt.prefix.length),
234
+ qrl
235
+ };
236
+ // See if we are already executing this job
237
+ const oldJob = this.jobsByMessageId[job.message.MessageId];
238
+ if (oldJob) {
239
+ // If we actually see the same job again, we fucked up, probably due to
240
+ // the system being overloaded and us missing our extension call. So
241
+ // we'll celebrate this occasion by throwing a big fat error.
242
+ debug({ oldJob });
243
+ const e = new Error(`Saw job ${oldJob.message.MessageId} twice`);
244
+ e.job = oldJob;
245
+ // TODO: sentry breadcrumb
246
+ throw e;
247
+ }
248
+ // Accounting
249
+ this.jobs.push(job);
250
+ this.jobsByMessageId[job.message.MessageId] = job;
251
+ // Track all jobs for each queue
252
+ if (!this.jobsByQueue.has(job.qname))
253
+ this.jobsByQueue.set(job.qname, new Set());
254
+ this.jobsByQueue.get(job.qname).add(job);
255
+ this.stats.activeJobs++;
256
+ this.stats.waitingJobs++;
257
+ if (this.opt.verbose) {
258
+ console.error(chalk_1.default.blue('Got message:'), job.prettyQname, chalk_1.default.blue('-->'), job.payload, job.message.MessageId);
259
+ }
260
+ else if (!this.opt.disableLog) {
261
+ console.log(JSON.stringify({
262
+ event: 'MESSAGE_RECEIVED',
263
+ timestamp: new Date(),
264
+ queue: job.qname,
265
+ messageId: message.MessageId,
266
+ payload: job.payload
267
+ }));
268
+ }
269
+ return job;
270
+ }
271
+ async runJob(job) {
272
+ try {
273
+ if (this.opt.verbose) {
274
+ console.error(chalk_1.default.blue('Running:'), job.prettyQname, chalk_1.default.blue('-->'), job.payload, job.message.MessageId);
275
+ }
276
+ else if (!this.opt.disableLog) {
277
+ console.log(JSON.stringify({
278
+ event: 'MESSAGE_PROCESSING_START',
279
+ timestamp: new Date(),
280
+ queue: job.qname,
281
+ messageId: job.message.MessageId,
282
+ payload: job.payload
283
+ }));
284
+ }
285
+ job.status = 'running';
286
+ this.stats.runningJobs++;
287
+ this.stats.waitingJobs--;
288
+ const queue = job.qname.slice(this.opt.prefix.length);
289
+ const result = await job.callback(queue, job.payload);
290
+ debug('executeJob callback finished', { payload: job.payload, result });
291
+ if (this.opt.verbose) {
292
+ console.error(chalk_1.default.green('SUCCESS'), job.payload);
293
+ }
294
+ job.status = 'complete';
295
+ if (this.opt.verbose) {
296
+ console.error(chalk_1.default.blue(' done'));
297
+ console.error();
298
+ }
299
+ else if (!this.opt.disableLog) {
300
+ console.log(JSON.stringify({
301
+ event: 'MESSAGE_PROCESSING_COMPLETE',
302
+ queue: job.qname,
303
+ timestamp: new Date(),
304
+ messageId: job.message.MessageId,
305
+ payload: job.payload
306
+ }));
307
+ }
308
+ this.stats.jobsSucceeded++;
309
+ }
310
+ catch (err) {
311
+ job.status = 'failed';
312
+ this.stats.jobsFailed++;
313
+ // Fail path for job execution
314
+ if (this.opt.verbose) {
315
+ console.error(chalk_1.default.red('FAILED'), job.payload);
316
+ console.error(chalk_1.default.blue(' error : ') + err);
317
+ }
318
+ else if (!this.opt.disableLog) {
319
+ // Production error logging
320
+ console.log(JSON.stringify({
321
+ event: 'MESSAGE_PROCESSING_FAILED',
322
+ reason: 'exception thrown',
323
+ queue: job.qname,
324
+ timestamp: new Date(),
325
+ messageId: job.message.MessageId,
326
+ payload: job.payload,
327
+ errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
328
+ err
329
+ }));
330
+ }
331
+ }
332
+ this.stats.activeJobs--;
333
+ this.stats.runningJobs--;
334
+ }
335
+ async executeJobs(messages, callback, qname, qrl) {
336
+ if (this.shutdownRequested)
337
+ throw new Error('jobExecutor is shutting down so cannot execute new jobs');
338
+ // Begin tracking jobs
339
+ const jobs = messages.map(message => this.addJob(message, callback, qname, qrl));
340
+ const isFifo = qrl.endsWith('.fifo');
341
+ // console.log(jobs)
342
+ // Begin executing
343
+ for (const [job, i] of jobs.map((job, i) => [job, i])) {
344
+ // Figure out if the next job needs to happen in serial, otherwise we can parallel execute
345
+ const nextJob = jobs[i + 1];
346
+ const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.GroupId === nextJob.message?.Attributes?.GroupId;
347
+ // console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
348
+ // Execute serial or parallel
349
+ if (nextJobIsSerial)
350
+ await this.runJob(job);
351
+ else
352
+ this.runJob(job);
353
+ }
354
+ }
355
+ }
395
356
  exports.JobExecutor = JobExecutor;