qdone 2.0.29-alpha → 2.0.30-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,26 @@
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 = {};
58
22
  this.stats = {
59
23
  activeJobs: 0,
24
+ waitingJobs: 0,
25
+ runningJobs: 0,
60
26
  sqsCalls: 0,
61
27
  timeoutsExtended: 0,
62
28
  jobsSucceeded: 0,
@@ -66,330 +32,306 @@ var JobExecutor = /** @class */ (function () {
66
32
  this.maintainPromise = this.maintainVisibility();
67
33
  debug({ this: this });
68
34
  }
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 () {
35
+ async shutdown() {
36
+ this.shutdownRequested = true;
37
+ // Trigger a maintenance run right away in case it speeds us up
38
+ clearTimeout(this.maintainVisibilityTimeout);
39
+ if (this.opt.verbose) {
40
+ console.error(chalk_1.default.blue('Shutting down jobExecutor'));
41
+ }
42
+ await this.maintainPromise;
43
+ await this.maintainVisibility();
44
+ }
45
+ activeJobCount() {
92
46
  return this.stats.activeJobs;
93
- };
47
+ }
94
48
  /**
95
49
  * Changes message visibility on all running jobs using as few calls as possible.
96
50
  */
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*/];
51
+ async maintainVisibility() {
52
+ // Bail if we are shutting down
53
+ if (this.shutdownRequested && this.stats.activeJobs === 0 && this.jobs.length === 0) {
54
+ if (this.opt.verbose) {
55
+ console.error(chalk_1.default.blue('All workers done, finishing shutdown of jobExecutor'));
56
+ }
57
+ return;
58
+ }
59
+ // Reset our timeout
60
+ clearTimeout(this.maintainVisibilityTimeout);
61
+ const nextCheckInMs = this.shutdownRequested ? 1000 : 10 * 1000;
62
+ this.maintainVisibilityTimeout = setTimeout(() => {
63
+ this.maintainPromise = this.maintainVisibility();
64
+ }, nextCheckInMs);
65
+ // debug('maintainVisibility', this.jobs)
66
+ const start = new Date();
67
+ const jobsToExtendByQrl = {};
68
+ const jobsToDeleteByQrl = {};
69
+ const jobsToCleanup = new Set();
70
+ // Build list of jobs we need to deal with
71
+ const jobStatuses = {};
72
+ for (let i = 0; i < this.jobs.length; i++) {
73
+ const job = this.jobs[i];
74
+ const jobRunTime = Math.round((start - job.start) / 1000);
75
+ jobStatuses[job.status] = (jobStatuses[job.status] || 0) + 1;
76
+ // debug('considering job', job)
77
+ if (job.status === 'complete') {
78
+ const jobsToDelete = jobsToDeleteByQrl[job.qrl] || [];
79
+ job.status = 'deleting';
80
+ jobsToDelete.push(job);
81
+ jobsToDeleteByQrl[job.qrl] = jobsToDelete;
82
+ }
83
+ else if (job.status === 'failed') {
84
+ jobsToCleanup.add(job);
85
+ }
86
+ else if (job.status !== 'deleting') {
87
+ // Any other job state gets visibility accounting
88
+ debug('processing', { job, jobRunTime });
89
+ if (jobRunTime >= job.extendAtSecond) {
90
+ // Add it to our organized list of jobs
91
+ const jobsToExtend = jobsToExtendByQrl[job.qrl] || [];
92
+ jobsToExtend.push(job);
93
+ jobsToExtendByQrl[job.qrl] = jobsToExtend;
94
+ // Update the visibility timeout, double every time, up to max
95
+ const doubled = job.visibilityTimeout * 2;
96
+ const secondsUntilMax = Math.max(1, maxJobSeconds - jobRunTime);
97
+ // const secondsUntilKill = Math.max(1, this.opt.killAfter - jobRunTime)
98
+ job.visibilityTimeout = Math.min(doubled, secondsUntilMax); //, secondsUntilKill)
99
+ job.extendAtSecond = Math.round(jobRunTime + job.visibilityTimeout / 2); // this is what we use next time
100
+ debug({ doubled, secondsUntilMax, job });
282
101
  }
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*/];
102
+ }
103
+ }
104
+ if (this.opt.verbose) {
105
+ console.error(chalk_1.default.blue('Stats: '), { stats: this.stats, jobStatuses });
106
+ console.error(chalk_1.default.blue('Running: '), this.jobs.filter(j => j.status === 'processing').map(({ qname, message }) => ({ qname, payload: message.Body })));
107
+ }
108
+ // Extend in batches for each queue
109
+ for (const qrl in jobsToExtendByQrl) {
110
+ const jobsToExtend = jobsToExtendByQrl[qrl];
111
+ debug({ qrl, jobsToExtend });
112
+ while (jobsToExtend.length) {
113
+ // Build list of messages to go in this batch
114
+ const entries = [];
115
+ let messageId = 0;
116
+ while (messageId++ < 10 && jobsToExtend.length) {
117
+ const job = jobsToExtend.shift();
118
+ const entry = {
119
+ Id: job.message.MessageId,
120
+ ReceiptHandle: job.message.ReceiptHandle,
121
+ VisibilityTimeout: job.visibilityTimeout
122
+ };
123
+ entries.push(entry);
124
+ }
125
+ debug({ entries });
126
+ // Change batch
127
+ const input = { QueueUrl: qrl, Entries: entries };
128
+ debug({ ChangeMessageVisibilityBatch: input });
129
+ const result = await (0, sqs_js_1.getSQSClient)().send(new client_sqs_1.ChangeMessageVisibilityBatchCommand(input));
130
+ debug('ChangeMessageVisibilityBatch returned', result);
131
+ this.stats.sqsCalls++;
132
+ if (result.Failed) {
133
+ console.error('FAILED_MESSAGES', result.Failed);
134
+ for (const failed of result.Failed) {
135
+ console.error('FAILED_TO_EXTEND_JOB', this.jobsByMessageId[failed.Id]);
136
+ }
137
+ }
138
+ if (result.Successful) {
139
+ const count = result.Successful.length || 0;
140
+ this.stats.timeoutsExtended += count;
141
+ if (this.opt.verbose) {
142
+ console.error(chalk_1.default.blue('Extended'), count, chalk_1.default.blue('jobs'));
143
+ }
144
+ else if (!this.opt.disableLog) {
145
+ console.log(JSON.stringify({ event: 'EXTEND_VISIBILITY_TIMEOUTS', timestamp: start, count, qrl }));
146
+ }
147
+ }
148
+ // TODO Sentry
149
+ }
150
+ }
151
+ // Delete in batches for each queue
152
+ for (const qrl in jobsToDeleteByQrl) {
153
+ const jobsToDelete = jobsToDeleteByQrl[qrl];
154
+ while (jobsToDelete.length) {
155
+ // Build list of messages to go in this batch
156
+ const entries = [];
157
+ let messageId = 0;
158
+ while (messageId++ < 10 && jobsToDelete.length) {
159
+ const job = jobsToDelete.shift();
160
+ const entry = {
161
+ Id: job.message.MessageId,
162
+ ReceiptHandle: job.message.ReceiptHandle,
163
+ VisibilityTimeout: job.visibilityTimeout
164
+ };
165
+ entries.push(entry);
389
166
  }
390
- });
167
+ debug({ entries });
168
+ // Delete batch
169
+ const input = { QueueUrl: qrl, Entries: entries };
170
+ debug({ DeleteMessageBatch: input });
171
+ const result = await (0, sqs_js_1.getSQSClient)().send(new client_sqs_1.DeleteMessageBatchCommand(input));
172
+ this.stats.sqsCalls++;
173
+ if (result.Failed) {
174
+ console.error('FAILED_MESSAGES', result.Failed);
175
+ for (const failed of result.Failed) {
176
+ console.error('FAILED_TO_DELETE_JOB', this.jobsByMessageId[failed.Id]);
177
+ }
178
+ }
179
+ if (result.Successful) {
180
+ const count = result.Successful.length || 0;
181
+ this.stats.jobsDeleted += count;
182
+ if (this.opt.verbose) {
183
+ console.error(chalk_1.default.blue('Deleted'), count, chalk_1.default.blue('jobs'));
184
+ }
185
+ else if (!this.opt.disableLog) {
186
+ console.log(JSON.stringify({ event: 'DELETE_MESSAGES', timestamp: start, count, qrl }));
187
+ }
188
+ }
189
+ debug('DeleteMessageBatch returned', result);
190
+ // TODO Sentry
191
+ }
192
+ }
193
+ // Get rid of deleted and failed jobs
194
+ this.jobs = this.jobs.filter(job => {
195
+ if (job.status === 'deleting' || job.status === 'failed') {
196
+ debug('removed', job.message.MessageId);
197
+ delete this.jobsByMessageId[job.message.MessageId];
198
+ return false;
199
+ }
200
+ else {
201
+ return true;
202
+ }
391
203
  });
392
- };
393
- return JobExecutor;
394
- }());
204
+ }
205
+ addJob(message, callback, qname, qrl) {
206
+ // Create job entry and track it
207
+ const defaultVisibilityTimeout = 60;
208
+ const job = {
209
+ status: 'waiting',
210
+ start: new Date(),
211
+ visibilityTimeout: defaultVisibilityTimeout,
212
+ extendAtSecond: defaultVisibilityTimeout / 2,
213
+ payload: this.opt.json ? JSON.parse(message.Body) : message.Body,
214
+ message,
215
+ callback,
216
+ qname,
217
+ prettyQname: qname.slice(this.opt.prefix.length),
218
+ qrl
219
+ };
220
+ // See if we are already executing this job
221
+ const oldJob = this.jobsByMessageId[job.message.MessageId];
222
+ if (oldJob) {
223
+ // If we actually see the same job again, we fucked up, probably due to
224
+ // the system being overloaded and us missing our extension call. So
225
+ // we'll celebrate this occasion by throwing a big fat error.
226
+ debug({ oldJob });
227
+ const e = new Error(`Saw job ${oldJob.message.MessageId} twice`);
228
+ e.job = oldJob;
229
+ // TODO: sentry breadcrumb
230
+ throw e;
231
+ }
232
+ this.jobs.push(job);
233
+ this.jobsByMessageId[job.message.MessageId] = job;
234
+ this.stats.activeJobs++;
235
+ this.stats.waitingJobs++;
236
+ if (this.opt.verbose) {
237
+ console.error(chalk_1.default.blue('Got message:'), job.prettyQname, chalk_1.default.blue('-->'), job.payload, job.message.MessageId);
238
+ }
239
+ else if (!this.opt.disableLog) {
240
+ console.log(JSON.stringify({
241
+ event: 'MESSAGE_RECEIVED',
242
+ timestamp: new Date(),
243
+ queue: job.qname,
244
+ messageId: message.MessageId,
245
+ payload: job.payload
246
+ }));
247
+ }
248
+ return job;
249
+ }
250
+ async runJob(job) {
251
+ try {
252
+ if (this.opt.verbose) {
253
+ console.error(chalk_1.default.blue('Running:'), job.prettyQname, chalk_1.default.blue('-->'), job.payload, job.message.MessageId);
254
+ }
255
+ else if (!this.opt.disableLog) {
256
+ console.log(JSON.stringify({
257
+ event: 'MESSAGE_PROCESSING_START',
258
+ timestamp: new Date(),
259
+ queue: job.qname,
260
+ messageId: job.message.MessageId,
261
+ payload: job.payload
262
+ }));
263
+ }
264
+ job.status = 'running';
265
+ this.stats.runningJobs++;
266
+ this.stats.waitingJobs--;
267
+ const queue = job.qname.slice(this.opt.prefix.length);
268
+ const result = await job.callback(queue, job.payload);
269
+ debug('executeJob callback finished', { payload: job.payload, result });
270
+ if (this.opt.verbose) {
271
+ console.error(chalk_1.default.green('SUCCESS'), job.payload);
272
+ }
273
+ job.status = 'complete';
274
+ if (this.opt.verbose) {
275
+ console.error(chalk_1.default.blue(' done'));
276
+ console.error();
277
+ }
278
+ else if (!this.opt.disableLog) {
279
+ console.log(JSON.stringify({
280
+ event: 'MESSAGE_PROCESSING_COMPLETE',
281
+ queue: job.qname,
282
+ timestamp: new Date(),
283
+ messageId: job.message.MessageId,
284
+ payload: job.payload
285
+ }));
286
+ }
287
+ this.stats.jobsSucceeded++;
288
+ }
289
+ catch (err) {
290
+ job.status = 'failed';
291
+ this.stats.jobsFailed++;
292
+ // Fail path for job execution
293
+ if (this.opt.verbose) {
294
+ console.error(chalk_1.default.red('FAILED'), job.payload);
295
+ console.error(chalk_1.default.blue(' error : ') + err);
296
+ }
297
+ else if (!this.opt.disableLog) {
298
+ // Production error logging
299
+ console.log(JSON.stringify({
300
+ event: 'MESSAGE_PROCESSING_FAILED',
301
+ reason: 'exception thrown',
302
+ queue: job.qname,
303
+ timestamp: new Date(),
304
+ messageId: job.message.MessageId,
305
+ payload: job.payload,
306
+ errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
307
+ err
308
+ }));
309
+ }
310
+ }
311
+ this.stats.activeJobs--;
312
+ this.stats.runningJobs--;
313
+ }
314
+ async executeJobs(messages, callback, qname, qrl) {
315
+ if (this.shutdownRequested)
316
+ throw new Error('jobExecutor is shutting down so cannot execute new jobs');
317
+ // Begin tracking jobs
318
+ const jobs = messages.map(message => this.addJob(message, callback, qname, qrl));
319
+ const isFifo = qrl.endsWith('.fifo');
320
+ // console.log(jobs)
321
+ // Begin executing
322
+ for (const [job, i] of jobs.map((job, i) => [job, i])) {
323
+ // Figure out if the next job needs to happen in serial, otherwise we can parallel execute
324
+ // const job = jobs[i]
325
+ const nextJob = jobs[i + 1];
326
+ const nextJobIsSerial = isFifo && nextJob &&
327
+ job.message?.Attributes?.GroupId === nextJob.message?.Attributes?.GroupId;
328
+ console.log({ i, nextJobAtt: nextJob.message.Attributes, nextJobIsSerial });
329
+ // Execute serial or parallel
330
+ if (nextJobIsSerial)
331
+ await this.runJob(job);
332
+ else
333
+ this.runJob(job);
334
+ }
335
+ }
336
+ }
395
337
  exports.JobExecutor = JobExecutor;