sqs-consumer 6.0.2 → 6.2.0
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/.github/CONTRIBUTING.md +10 -0
- package/.github/ISSUE_TEMPLATE/bug-report.yml +115 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/workflows/cla.yml +25 -0
- package/.github/workflows/codeql.yml +76 -0
- package/.github/workflows/coverage.yml +1 -1
- package/.github/workflows/docs.yml +55 -0
- package/.prettierignore +2 -1
- package/README.md +22 -42
- package/dist/bind.d.ts +4 -0
- package/dist/bind.js +9 -0
- package/dist/consumer.d.ts +90 -51
- package/dist/consumer.js +247 -213
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +32 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +15 -0
- package/dist/types.d.ts +163 -0
- package/dist/types.js +28 -0
- package/dist/validation.d.ts +13 -0
- package/dist/validation.js +36 -0
- package/package.json +14 -14
- package/src/bind.ts +9 -0
- package/src/consumer.ts +291 -301
- package/src/errors.ts +36 -1
- package/src/index.ts +2 -1
- package/src/types.ts +178 -0
- package/src/validation.ts +45 -0
- package/typedoc.json +13 -0
- package/.github/ISSUE_TEMPLATE/bug-report.md +0 -27
- package/.github/ISSUE_TEMPLATE/feature-request.md +0 -19
- package/.github/ISSUE_TEMPLATE/technical-question.md +0 -16
package/src/consumer.ts
CHANGED
|
@@ -16,132 +16,42 @@ import {
|
|
|
16
16
|
ReceiveMessageCommandOutput
|
|
17
17
|
} from '@aws-sdk/client-sqs';
|
|
18
18
|
import Debug from 'debug';
|
|
19
|
-
import { EventEmitter } from 'events';
|
|
20
19
|
|
|
21
|
-
import {
|
|
20
|
+
import { ConsumerOptions, TypedEventEmitter } from './types';
|
|
22
21
|
import { autoBind } from './bind';
|
|
23
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
SQSError,
|
|
24
|
+
TimeoutError,
|
|
25
|
+
toSQSError,
|
|
26
|
+
isConnectionError
|
|
27
|
+
} from './errors';
|
|
28
|
+
import { assertOptions, hasMessages } from './validation';
|
|
24
29
|
|
|
25
30
|
const debug = Debug('sqs-consumer');
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
pending: Promise<void>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function createTimeout(duration: number): TimeoutResponse[] {
|
|
39
|
-
let timeout;
|
|
40
|
-
const pending = new Promise((_, reject) => {
|
|
41
|
-
timeout = setTimeout((): void => {
|
|
42
|
-
reject(new TimeoutError());
|
|
43
|
-
}, duration);
|
|
44
|
-
});
|
|
45
|
-
return [timeout, pending];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function assertOptions(options: ConsumerOptions): void {
|
|
49
|
-
requiredOptions.forEach((option) => {
|
|
50
|
-
const possibilities = option.split('|');
|
|
51
|
-
if (!possibilities.find((p) => options[p])) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
`Missing SQS consumer option [ ${possibilities.join(' or ')} ].`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
if (options.batchSize > 10 || options.batchSize < 1) {
|
|
59
|
-
throw new Error('SQS batchSize option must be between 1 and 10.');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
options.heartbeatInterval &&
|
|
64
|
-
!(options.heartbeatInterval < options.visibilityTimeout)
|
|
65
|
-
) {
|
|
66
|
-
throw new Error('heartbeatInterval must be less than visibilityTimeout.');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function isConnectionError(err: Error): boolean {
|
|
71
|
-
if (err instanceof SQSError) {
|
|
72
|
-
return (
|
|
73
|
-
err.statusCode === 403 ||
|
|
74
|
-
err.code === 'CredentialsError' ||
|
|
75
|
-
err.code === 'UnknownEndpoint'
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function toSQSError(err: AWSError, message: string): SQSError {
|
|
82
|
-
const sqsError = new SQSError(message);
|
|
83
|
-
sqsError.code = err.name;
|
|
84
|
-
sqsError.statusCode = err.$metadata?.httpStatusCode;
|
|
85
|
-
sqsError.retryable = err.$retryable?.throttling;
|
|
86
|
-
sqsError.service = err.$service;
|
|
87
|
-
sqsError.fault = err.$fault;
|
|
88
|
-
sqsError.time = new Date();
|
|
89
|
-
|
|
90
|
-
return sqsError;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function hasMessages(response: ReceiveMessageCommandOutput): boolean {
|
|
94
|
-
return response.Messages && response.Messages.length > 0;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface ConsumerOptions {
|
|
98
|
-
queueUrl?: string;
|
|
99
|
-
attributeNames?: string[];
|
|
100
|
-
messageAttributeNames?: string[];
|
|
101
|
-
stopped?: boolean;
|
|
102
|
-
batchSize?: number;
|
|
103
|
-
visibilityTimeout?: number;
|
|
104
|
-
waitTimeSeconds?: number;
|
|
105
|
-
authenticationErrorTimeout?: number;
|
|
106
|
-
pollingWaitTimeMs?: number;
|
|
107
|
-
terminateVisibilityTimeout?: boolean;
|
|
108
|
-
heartbeatInterval?: number;
|
|
109
|
-
sqs?: SQSClient;
|
|
110
|
-
region?: string;
|
|
111
|
-
handleMessageTimeout?: number;
|
|
112
|
-
shouldDeleteMessages?: boolean;
|
|
113
|
-
handleMessage?(message: Message): Promise<void>;
|
|
114
|
-
handleMessageBatch?(messages: Message[]): Promise<void>;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
interface Events {
|
|
118
|
-
response_processed: [];
|
|
119
|
-
empty: [];
|
|
120
|
-
message_received: [Message];
|
|
121
|
-
message_processed: [Message];
|
|
122
|
-
error: [Error, void | Message | Message[]];
|
|
123
|
-
timeout_error: [Error, Message];
|
|
124
|
-
processing_error: [Error, Message];
|
|
125
|
-
stopped: [];
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export class Consumer extends EventEmitter {
|
|
32
|
+
/**
|
|
33
|
+
* [Usage](https://bbc.github.io/sqs-consumer/index.html#usage)
|
|
34
|
+
*/
|
|
35
|
+
export class Consumer extends TypedEventEmitter {
|
|
36
|
+
private pollingTimeoutId: NodeJS.Timeout | undefined = undefined;
|
|
37
|
+
private heartbeatTimeoutId: NodeJS.Timeout | undefined = undefined;
|
|
38
|
+
private handleMessageTimeoutId: NodeJS.Timeout | undefined = undefined;
|
|
39
|
+
private stopped = true;
|
|
129
40
|
private queueUrl: string;
|
|
130
|
-
private handleMessage: (message: Message) => Promise<void>;
|
|
131
|
-
private handleMessageBatch: (message: Message[]) => Promise<void>;
|
|
41
|
+
private handleMessage: (message: Message) => Promise<Message | void>;
|
|
42
|
+
private handleMessageBatch: (message: Message[]) => Promise<Message[] | void>;
|
|
43
|
+
private sqs: SQSClient;
|
|
132
44
|
private handleMessageTimeout: number;
|
|
133
45
|
private attributeNames: string[];
|
|
134
46
|
private messageAttributeNames: string[];
|
|
135
|
-
private
|
|
47
|
+
private shouldDeleteMessages: boolean;
|
|
136
48
|
private batchSize: number;
|
|
137
49
|
private visibilityTimeout: number;
|
|
50
|
+
private terminateVisibilityTimeout: boolean;
|
|
138
51
|
private waitTimeSeconds: number;
|
|
139
52
|
private authenticationErrorTimeout: number;
|
|
140
53
|
private pollingWaitTimeMs: number;
|
|
141
|
-
private terminateVisibilityTimeout: boolean;
|
|
142
54
|
private heartbeatInterval: number;
|
|
143
|
-
private sqs: SQSClient;
|
|
144
|
-
private shouldDeleteMessages: boolean;
|
|
145
55
|
|
|
146
56
|
constructor(options: ConsumerOptions) {
|
|
147
57
|
super();
|
|
@@ -152,7 +62,6 @@ export class Consumer extends EventEmitter {
|
|
|
152
62
|
this.handleMessageTimeout = options.handleMessageTimeout;
|
|
153
63
|
this.attributeNames = options.attributeNames || [];
|
|
154
64
|
this.messageAttributeNames = options.messageAttributeNames || [];
|
|
155
|
-
this.stopped = true;
|
|
156
65
|
this.batchSize = options.batchSize || 1;
|
|
157
66
|
this.visibilityTimeout = options.visibilityTimeout;
|
|
158
67
|
this.terminateVisibilityTimeout =
|
|
@@ -163,89 +72,171 @@ export class Consumer extends EventEmitter {
|
|
|
163
72
|
options.authenticationErrorTimeout ?? 10000;
|
|
164
73
|
this.pollingWaitTimeMs = options.pollingWaitTimeMs ?? 0;
|
|
165
74
|
this.shouldDeleteMessages = options.shouldDeleteMessages ?? true;
|
|
166
|
-
|
|
167
75
|
this.sqs =
|
|
168
76
|
options.sqs ||
|
|
169
77
|
new SQSClient({
|
|
170
78
|
region: options.region || process.env.AWS_REGION || 'eu-west-1'
|
|
171
79
|
});
|
|
172
|
-
|
|
173
80
|
autoBind(this);
|
|
174
81
|
}
|
|
175
82
|
|
|
176
|
-
|
|
177
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Creates a new SQS consumer.
|
|
85
|
+
*/
|
|
86
|
+
public static create(options: ConsumerOptions): Consumer {
|
|
87
|
+
return new Consumer(options);
|
|
178
88
|
}
|
|
179
89
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
):
|
|
184
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Start polling the queue for messages.
|
|
92
|
+
*/
|
|
93
|
+
public start(): void {
|
|
94
|
+
if (this.stopped) {
|
|
95
|
+
debug('Starting consumer');
|
|
96
|
+
this.stopped = false;
|
|
97
|
+
this.poll();
|
|
98
|
+
}
|
|
185
99
|
}
|
|
186
100
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
):
|
|
191
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Stop polling the queue for messages (pre existing requests will still be made until concluded).
|
|
103
|
+
*/
|
|
104
|
+
public stop(): void {
|
|
105
|
+
if (this.stopped) {
|
|
106
|
+
debug('Consumer was already stopped');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
debug('Stopping consumer');
|
|
111
|
+
this.stopped = true;
|
|
112
|
+
|
|
113
|
+
if (this.pollingTimeoutId) {
|
|
114
|
+
clearTimeout(this.pollingTimeoutId);
|
|
115
|
+
this.pollingTimeoutId = undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.emit('stopped');
|
|
192
119
|
}
|
|
193
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Returns the current polling state of the consumer: `true` if it is actively polling, `false` if it is not.
|
|
123
|
+
*/
|
|
194
124
|
public get isRunning(): boolean {
|
|
195
125
|
return !this.stopped;
|
|
196
126
|
}
|
|
197
127
|
|
|
198
|
-
|
|
199
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Emit one of the consumer's error events depending on the error received.
|
|
130
|
+
* @param err The error object to forward on
|
|
131
|
+
* @param message The message that the error occurred on
|
|
132
|
+
*/
|
|
133
|
+
private emitError(err: Error, message?: Message): void {
|
|
134
|
+
if (!message) {
|
|
135
|
+
this.emit('error', err);
|
|
136
|
+
} else if (err.name === SQSError.name) {
|
|
137
|
+
this.emit('error', err, message);
|
|
138
|
+
} else if (err instanceof TimeoutError) {
|
|
139
|
+
this.emit('timeout_error', err, message);
|
|
140
|
+
} else {
|
|
141
|
+
this.emit('processing_error', err, message);
|
|
142
|
+
}
|
|
200
143
|
}
|
|
201
144
|
|
|
202
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Poll for new messages from SQS
|
|
147
|
+
*/
|
|
148
|
+
private poll(): void {
|
|
203
149
|
if (this.stopped) {
|
|
204
|
-
debug('
|
|
205
|
-
|
|
206
|
-
this.poll();
|
|
150
|
+
debug('Poll was called while consumer was stopped, cancelling poll...');
|
|
151
|
+
return;
|
|
207
152
|
}
|
|
153
|
+
|
|
154
|
+
debug('Polling for messages');
|
|
155
|
+
|
|
156
|
+
let currentPollingTimeout = this.pollingWaitTimeMs;
|
|
157
|
+
this.receiveMessage({
|
|
158
|
+
QueueUrl: this.queueUrl,
|
|
159
|
+
AttributeNames: this.attributeNames,
|
|
160
|
+
MessageAttributeNames: this.messageAttributeNames,
|
|
161
|
+
MaxNumberOfMessages: this.batchSize,
|
|
162
|
+
WaitTimeSeconds: this.waitTimeSeconds,
|
|
163
|
+
VisibilityTimeout: this.visibilityTimeout
|
|
164
|
+
})
|
|
165
|
+
.then(this.handleSqsResponse)
|
|
166
|
+
.catch((err) => {
|
|
167
|
+
this.emitError(err);
|
|
168
|
+
if (isConnectionError(err)) {
|
|
169
|
+
debug('There was an authentication error. Pausing before retrying.');
|
|
170
|
+
currentPollingTimeout = this.authenticationErrorTimeout;
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
})
|
|
174
|
+
.then(() => {
|
|
175
|
+
if (this.pollingTimeoutId) {
|
|
176
|
+
clearTimeout(this.pollingTimeoutId);
|
|
177
|
+
}
|
|
178
|
+
this.pollingTimeoutId = setTimeout(this.poll, currentPollingTimeout);
|
|
179
|
+
})
|
|
180
|
+
.catch((err) => {
|
|
181
|
+
this.emitError(err);
|
|
182
|
+
});
|
|
208
183
|
}
|
|
209
184
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Send a request to SQS to retrieve messages
|
|
187
|
+
* @param params The required params to receive messages from SQS
|
|
188
|
+
*/
|
|
189
|
+
private async receiveMessage(
|
|
190
|
+
params: ReceiveMessageCommandInput
|
|
191
|
+
): Promise<ReceiveMessageCommandOutput> {
|
|
192
|
+
try {
|
|
193
|
+
return await this.sqs.send(new ReceiveMessageCommand(params));
|
|
194
|
+
} catch (err) {
|
|
195
|
+
throw toSQSError(err, `SQS receive message failed: ${err.message}`);
|
|
196
|
+
}
|
|
213
197
|
}
|
|
214
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Handles the response from AWS SQS, determining if we should proceed to
|
|
201
|
+
* the message handler.
|
|
202
|
+
* @param response The output from AWS SQS
|
|
203
|
+
*/
|
|
215
204
|
private async handleSqsResponse(
|
|
216
205
|
response: ReceiveMessageCommandOutput
|
|
217
206
|
): Promise<void> {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (response) {
|
|
222
|
-
if (hasMessages(response)) {
|
|
223
|
-
if (this.handleMessageBatch) {
|
|
224
|
-
// prefer handling messages in batch when available
|
|
225
|
-
await this.processMessageBatch(response.Messages);
|
|
226
|
-
} else {
|
|
227
|
-
await Promise.all(response.Messages.map(this.processMessage));
|
|
228
|
-
}
|
|
229
|
-
this.emit('response_processed');
|
|
207
|
+
if (hasMessages(response)) {
|
|
208
|
+
if (this.handleMessageBatch) {
|
|
209
|
+
await this.processMessageBatch(response.Messages);
|
|
230
210
|
} else {
|
|
231
|
-
this.
|
|
211
|
+
await Promise.all(response.Messages.map(this.processMessage));
|
|
232
212
|
}
|
|
213
|
+
|
|
214
|
+
this.emit('response_processed');
|
|
215
|
+
} else if (response) {
|
|
216
|
+
this.emit('empty');
|
|
233
217
|
}
|
|
234
218
|
}
|
|
235
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Process a message that has been received from SQS. This will execute the message
|
|
222
|
+
* handler and delete the message once complete.
|
|
223
|
+
* @param message The message that was delivered from SQS
|
|
224
|
+
*/
|
|
236
225
|
private async processMessage(message: Message): Promise<void> {
|
|
237
|
-
this.emit('message_received', message);
|
|
238
|
-
|
|
239
|
-
let heartbeat;
|
|
240
226
|
try {
|
|
227
|
+
this.emit('message_received', message);
|
|
228
|
+
|
|
241
229
|
if (this.heartbeatInterval) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
230
|
+
this.heartbeatTimeoutId = this.startHeartbeat(message);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const ackedMessage = await this.executeHandler(message);
|
|
234
|
+
|
|
235
|
+
if (ackedMessage?.MessageId === message.MessageId) {
|
|
236
|
+
await this.deleteMessage(message);
|
|
237
|
+
|
|
238
|
+
this.emit('message_processed', message);
|
|
245
239
|
}
|
|
246
|
-
await this.executeHandler(message);
|
|
247
|
-
await this.deleteMessage(message);
|
|
248
|
-
this.emit('message_processed', message);
|
|
249
240
|
} catch (err) {
|
|
250
241
|
this.emitError(err, message);
|
|
251
242
|
|
|
@@ -253,63 +244,71 @@ export class Consumer extends EventEmitter {
|
|
|
253
244
|
await this.changeVisibilityTimeout(message, 0);
|
|
254
245
|
}
|
|
255
246
|
} finally {
|
|
256
|
-
clearInterval(
|
|
247
|
+
clearInterval(this.heartbeatTimeoutId);
|
|
248
|
+
this.heartbeatTimeoutId = undefined;
|
|
257
249
|
}
|
|
258
250
|
}
|
|
259
251
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Process a batch of messages from the SQS queue.
|
|
254
|
+
* @param messages The messages that were delivered from SQS
|
|
255
|
+
*/
|
|
256
|
+
private async processMessageBatch(messages: Message[]): Promise<void> {
|
|
263
257
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
}
|
|
258
|
+
messages.forEach((message) => {
|
|
259
|
+
this.emit('message_received', message);
|
|
260
|
+
});
|
|
269
261
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
'Skipping message delete since shouldDeleteMessages is set to false'
|
|
274
|
-
);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
debug('Deleting message %s', message.MessageId);
|
|
262
|
+
if (this.heartbeatInterval) {
|
|
263
|
+
this.heartbeatTimeoutId = this.startHeartbeat(null, messages);
|
|
264
|
+
}
|
|
278
265
|
|
|
279
|
-
|
|
280
|
-
QueueUrl: this.queueUrl,
|
|
281
|
-
ReceiptHandle: message.ReceiptHandle
|
|
282
|
-
};
|
|
266
|
+
const ackedMessages = await this.executeBatchHandler(messages);
|
|
283
267
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
} catch (err) {
|
|
287
|
-
throw toSQSError(err, `SQS delete message failed: ${err.message}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
268
|
+
if (ackedMessages?.length > 0) {
|
|
269
|
+
await this.deleteMessageBatch(ackedMessages);
|
|
290
270
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
if (this.handleMessageTimeout) {
|
|
296
|
-
[timeout, pending] = createTimeout(this.handleMessageTimeout);
|
|
297
|
-
await Promise.race([this.handleMessage(message), pending]);
|
|
298
|
-
} else {
|
|
299
|
-
await this.handleMessage(message);
|
|
271
|
+
ackedMessages.forEach((message) => {
|
|
272
|
+
this.emit('message_processed', message);
|
|
273
|
+
});
|
|
300
274
|
}
|
|
301
275
|
} catch (err) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
276
|
+
this.emit('error', err, messages);
|
|
277
|
+
|
|
278
|
+
if (this.terminateVisibilityTimeout) {
|
|
279
|
+
await this.changeVisibilityTimeoutBatch(messages, 0);
|
|
306
280
|
}
|
|
307
|
-
throw err;
|
|
308
281
|
} finally {
|
|
309
|
-
|
|
282
|
+
clearInterval(this.heartbeatTimeoutId);
|
|
283
|
+
this.heartbeatTimeoutId = undefined;
|
|
310
284
|
}
|
|
311
285
|
}
|
|
312
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Trigger a function on a set interval
|
|
289
|
+
* @param heartbeatFn The function that should be triggered
|
|
290
|
+
*/
|
|
291
|
+
private startHeartbeat(
|
|
292
|
+
message?: Message,
|
|
293
|
+
messages?: Message[]
|
|
294
|
+
): NodeJS.Timeout {
|
|
295
|
+
return setInterval(() => {
|
|
296
|
+
if (this.handleMessageBatch) {
|
|
297
|
+
return this.changeVisibilityTimeoutBatch(
|
|
298
|
+
messages,
|
|
299
|
+
this.visibilityTimeout
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
return this.changeVisibilityTimeout(message, this.visibilityTimeout);
|
|
303
|
+
}
|
|
304
|
+
}, this.heartbeatInterval * 1000);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Change the visibility timeout on a message
|
|
309
|
+
* @param message The message to change the value of
|
|
310
|
+
* @param timeout The new timeout that should be set
|
|
311
|
+
*/
|
|
313
312
|
private async changeVisibilityTimeout(
|
|
314
313
|
message: Message,
|
|
315
314
|
timeout: number
|
|
@@ -330,82 +329,113 @@ export class Consumer extends EventEmitter {
|
|
|
330
329
|
}
|
|
331
330
|
}
|
|
332
331
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
332
|
+
/**
|
|
333
|
+
* Change the visibility timeout on a batch of messages
|
|
334
|
+
* @param messages The messages to change the value of
|
|
335
|
+
* @param timeout The new timeout that should be set
|
|
336
|
+
*/
|
|
337
|
+
private async changeVisibilityTimeoutBatch(
|
|
338
|
+
messages: Message[],
|
|
339
|
+
timeout: number
|
|
340
|
+
): Promise<ChangeMessageVisibilityBatchCommandOutput> {
|
|
341
|
+
const params: ChangeMessageVisibilityBatchCommandInput = {
|
|
342
|
+
QueueUrl: this.queueUrl,
|
|
343
|
+
Entries: messages.map((message) => ({
|
|
344
|
+
Id: message.MessageId,
|
|
345
|
+
ReceiptHandle: message.ReceiptHandle,
|
|
346
|
+
VisibilityTimeout: timeout
|
|
347
|
+
}))
|
|
348
|
+
};
|
|
349
|
+
try {
|
|
350
|
+
return await this.sqs.send(
|
|
351
|
+
new ChangeMessageVisibilityBatchCommand(params)
|
|
352
|
+
);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
this.emit(
|
|
355
|
+
'error',
|
|
356
|
+
toSQSError(err, `Error changing visibility timeout: ${err.message}`),
|
|
357
|
+
messages
|
|
358
|
+
);
|
|
340
359
|
}
|
|
341
360
|
}
|
|
342
361
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Trigger the applications handleMessage function
|
|
364
|
+
* @param message The message that was received from SQS
|
|
365
|
+
*/
|
|
366
|
+
private async executeHandler(message: Message): Promise<Message> {
|
|
367
|
+
try {
|
|
368
|
+
let result;
|
|
369
|
+
|
|
370
|
+
if (this.handleMessageTimeout) {
|
|
371
|
+
const pending = new Promise((_, reject) => {
|
|
372
|
+
this.handleMessageTimeoutId = setTimeout((): void => {
|
|
373
|
+
reject(new TimeoutError());
|
|
374
|
+
}, this.handleMessageTimeout);
|
|
375
|
+
});
|
|
376
|
+
result = await Promise.race([this.handleMessage(message), pending]);
|
|
377
|
+
} else {
|
|
378
|
+
result = await this.handleMessage(message);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return result instanceof Object ? result : message;
|
|
382
|
+
} catch (err) {
|
|
383
|
+
err.message =
|
|
384
|
+
err instanceof TimeoutError
|
|
385
|
+
? `Message handler timed out after ${this.handleMessageTimeout}ms: Operation timed out.`
|
|
386
|
+
: `Unexpected message handler failure: ${err.message}`;
|
|
387
|
+
throw err;
|
|
388
|
+
} finally {
|
|
389
|
+
if (this.handleMessageTimeoutId) {
|
|
390
|
+
clearTimeout(this.handleMessageTimeoutId);
|
|
391
|
+
}
|
|
347
392
|
}
|
|
393
|
+
}
|
|
348
394
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
VisibilityTimeout: this.visibilityTimeout
|
|
357
|
-
};
|
|
395
|
+
/**
|
|
396
|
+
* Execute the application's message batch handler
|
|
397
|
+
* @param messages The messages that should be forwarded from the SQS queue
|
|
398
|
+
*/
|
|
399
|
+
private async executeBatchHandler(messages: Message[]): Promise<Message[]> {
|
|
400
|
+
try {
|
|
401
|
+
const result = await this.handleMessageBatch(messages);
|
|
358
402
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (isConnectionError(err)) {
|
|
365
|
-
debug('There was an authentication error. Pausing before retrying.');
|
|
366
|
-
currentPollingTimeout = this.authenticationErrorTimeout;
|
|
367
|
-
}
|
|
368
|
-
return;
|
|
369
|
-
})
|
|
370
|
-
.then(() => {
|
|
371
|
-
setTimeout(this.poll, currentPollingTimeout);
|
|
372
|
-
})
|
|
373
|
-
.catch((err) => {
|
|
374
|
-
this.emit('error', err);
|
|
375
|
-
});
|
|
403
|
+
return result instanceof Object ? result : messages;
|
|
404
|
+
} catch (err) {
|
|
405
|
+
err.message = `Unexpected message handler failure: ${err.message}`;
|
|
406
|
+
throw err;
|
|
407
|
+
}
|
|
376
408
|
}
|
|
377
409
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
410
|
+
/**
|
|
411
|
+
* Delete a single message from SQS
|
|
412
|
+
* @param message The message to delete from the SQS queue
|
|
413
|
+
*/
|
|
414
|
+
private async deleteMessage(message: Message): Promise<void> {
|
|
415
|
+
if (!this.shouldDeleteMessages) {
|
|
416
|
+
debug(
|
|
417
|
+
'Skipping message delete since shouldDeleteMessages is set to false'
|
|
418
|
+
);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
debug('Deleting message %s', message.MessageId);
|
|
422
|
+
|
|
423
|
+
const deleteParams: DeleteMessageCommandInput = {
|
|
424
|
+
QueueUrl: this.queueUrl,
|
|
425
|
+
ReceiptHandle: message.ReceiptHandle
|
|
426
|
+
};
|
|
382
427
|
|
|
383
|
-
let heartbeat;
|
|
384
428
|
try {
|
|
385
|
-
|
|
386
|
-
heartbeat = this.startHeartbeat(async () => {
|
|
387
|
-
return this.changeVisibilityTimeoutBatch(
|
|
388
|
-
messages,
|
|
389
|
-
this.visibilityTimeout
|
|
390
|
-
);
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
await this.executeBatchHandler(messages);
|
|
394
|
-
await this.deleteMessageBatch(messages);
|
|
395
|
-
messages.forEach((message) => {
|
|
396
|
-
this.emit('message_processed', message);
|
|
397
|
-
});
|
|
429
|
+
await this.sqs.send(new DeleteMessageCommand(deleteParams));
|
|
398
430
|
} catch (err) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (this.terminateVisibilityTimeout) {
|
|
402
|
-
await this.changeVisibilityTimeoutBatch(messages, 0);
|
|
403
|
-
}
|
|
404
|
-
} finally {
|
|
405
|
-
clearInterval(heartbeat);
|
|
431
|
+
throw toSQSError(err, `SQS delete message failed: ${err.message}`);
|
|
406
432
|
}
|
|
407
433
|
}
|
|
408
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Delete a batch of messages from the SQS queue.
|
|
437
|
+
* @param messages The messages that should be deleted from SQS
|
|
438
|
+
*/
|
|
409
439
|
private async deleteMessageBatch(messages: Message[]): Promise<void> {
|
|
410
440
|
if (!this.shouldDeleteMessages) {
|
|
411
441
|
debug(
|
|
@@ -432,44 +462,4 @@ export class Consumer extends EventEmitter {
|
|
|
432
462
|
throw toSQSError(err, `SQS delete message failed: ${err.message}`);
|
|
433
463
|
}
|
|
434
464
|
}
|
|
435
|
-
|
|
436
|
-
private async executeBatchHandler(messages: Message[]): Promise<void> {
|
|
437
|
-
try {
|
|
438
|
-
await this.handleMessageBatch(messages);
|
|
439
|
-
} catch (err) {
|
|
440
|
-
err.message = `Unexpected message handler failure: ${err.message}`;
|
|
441
|
-
throw err;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
private async changeVisibilityTimeoutBatch(
|
|
446
|
-
messages: Message[],
|
|
447
|
-
timeout: number
|
|
448
|
-
): Promise<ChangeMessageVisibilityBatchCommandOutput> {
|
|
449
|
-
const params: ChangeMessageVisibilityBatchCommandInput = {
|
|
450
|
-
QueueUrl: this.queueUrl,
|
|
451
|
-
Entries: messages.map((message) => ({
|
|
452
|
-
Id: message.MessageId,
|
|
453
|
-
ReceiptHandle: message.ReceiptHandle,
|
|
454
|
-
VisibilityTimeout: timeout
|
|
455
|
-
}))
|
|
456
|
-
};
|
|
457
|
-
try {
|
|
458
|
-
return await this.sqs.send(
|
|
459
|
-
new ChangeMessageVisibilityBatchCommand(params)
|
|
460
|
-
);
|
|
461
|
-
} catch (err) {
|
|
462
|
-
this.emit(
|
|
463
|
-
'error',
|
|
464
|
-
toSQSError(err, `Error changing visibility timeout: ${err.message}`),
|
|
465
|
-
messages
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
private startHeartbeat(heartbeatFn: () => void): NodeJS.Timeout {
|
|
471
|
-
return setInterval(() => {
|
|
472
|
-
heartbeatFn();
|
|
473
|
-
}, this.heartbeatInterval * 1000);
|
|
474
|
-
}
|
|
475
465
|
}
|