@@ -136,7 +136,18 @@ async Task ConsumeMessages(CancellationToken token)
136
136
return ;
137
137
}
138
138
139
- _ = ProcessMessage ( receivedMessage , maxConcurrencySemaphore , token ) ;
139
+ try
140
+ {
141
+ _ = ProcessMessage ( receivedMessage , maxConcurrencySemaphore , token ) ;
142
+ }
143
+ catch ( Exception ex )
144
+ {
145
+ Logger . Debug ( "Returning message to queue..." , ex ) ;
146
+
147
+ await ReturnMessageToQueue ( receivedMessage ) . ConfigureAwait ( false ) ;
148
+
149
+ throw ;
150
+ }
140
151
}
141
152
}
142
153
catch ( OperationCanceledException )
@@ -161,6 +172,12 @@ internal async Task ProcessMessage(Message receivedMessage, SemaphoreSlim proces
161
172
string messageId = null ;
162
173
var isPoisonMessage = false ;
163
174
175
+ if ( messagesToBeDeletedStorage . TryGetFailureInfoForMessage ( nativeMessageId , out _ ) )
176
+ {
177
+ await DeleteMessage ( receivedMessage , null ) . ConfigureAwait ( false ) ;
178
+ return ;
179
+ }
180
+
164
181
try
165
182
{
166
183
if ( receivedMessage . MessageAttributes . TryGetValue ( Headers . MessageId , out var messageIdAttribute ) )
@@ -230,81 +247,126 @@ internal async Task ProcessMessage(Message receivedMessage, SemaphoreSlim proces
230
247
return ;
231
248
}
232
249
233
- if ( ! IsMessageExpired ( receivedMessage , transportMessage . Headers , messageId , CorrectClockSkew . GetClockCorrectionForEndpoint ( awsEndpointUrl ) ) )
250
+ if ( IsMessageExpired ( receivedMessage , transportMessage . Headers , messageId , CorrectClockSkew . GetClockCorrectionForEndpoint ( awsEndpointUrl ) ) )
234
251
{
235
- // here we also want to use the native message id because the core demands it like that
236
- await ProcessMessageWithInMemoryRetries ( transportMessage . Headers , nativeMessageId , messageBody , receivedMessage , token ) . ConfigureAwait ( false ) ;
252
+ await DeleteMessage ( receivedMessage , transportMessage . S3BodyKey ) . ConfigureAwait ( false ) ;
237
253
}
254
+ else
255
+ {
256
+ // here we also want to use the native message id because the core demands it like that
257
+ var messageProcessed = await InnerProcessMessage ( transportMessage . Headers , nativeMessageId , messageBody , receivedMessage , token ) . ConfigureAwait ( false ) ;
238
258
239
- // Always delete the message from the queue.
240
- // If processing failed, the onError handler will have moved the message
241
- // to a retry queue.
242
- await DeleteMessage ( receivedMessage , transportMessage . S3BodyKey ) . ConfigureAwait ( false ) ;
259
+ if ( messageProcessed )
260
+ {
261
+ await DeleteMessage ( receivedMessage , transportMessage . S3BodyKey ) . ConfigureAwait ( false ) ;
262
+ }
263
+ }
243
264
}
244
265
finally
245
266
{
246
267
processingSemaphoreSlim . Release ( ) ;
247
268
}
248
269
}
249
270
250
- async Task ProcessMessageWithInMemoryRetries ( Dictionary < string , string > headers , string nativeMessageId , byte [ ] body , Message nativeMessage , CancellationToken token )
271
+ async Task < bool > InnerProcessMessage ( Dictionary < string , string > headers , string nativeMessageId , byte [ ] body , Message nativeMessage , CancellationToken token )
251
272
{
252
- var immediateProcessingAttempts = 0 ;
253
- var messageProcessedOk = false ;
254
- var errorHandled = false ;
255
-
256
- while ( ! errorHandled && ! messageProcessedOk )
273
+ // set the native message on the context for advanced usage scenario's
274
+ var context = new ContextBag ( ) ;
275
+ context . Set ( nativeMessage ) ;
276
+ // We add it to the transport transaction to make it available in dispatching scenario's so we copy over message attributes when moving messages to the error/audit queue
277
+ var transportTransaction = new TransportTransaction ( ) ;
278
+ transportTransaction . Set ( nativeMessage ) ;
279
+ transportTransaction . Set ( "IncomingMessageId" , headers [ Headers . MessageId ] ) ;
280
+
281
+ using ( var messageContextCancellationTokenSource = new CancellationTokenSource ( ) )
257
282
{
258
- // set the native message on the context for advanced usage scenario's
259
- var context = new ContextBag ( ) ;
260
- context . Set ( nativeMessage ) ;
261
- // We add it to the transport transaction to make it available in dispatching scenario's so we copy over message attributes when moving messages to the error/audit queue
262
- var transportTransaction = new TransportTransaction ( ) ;
263
- transportTransaction . Set ( nativeMessage ) ;
264
- transportTransaction . Set ( "IncomingMessageId" , headers [ Headers . MessageId ] ) ;
265
-
266
283
try
267
284
{
268
- using ( var messageContextCancellationTokenSource = new CancellationTokenSource ( ) )
269
- {
270
- var messageContext = new MessageContext (
271
- nativeMessageId ,
272
- new Dictionary < string , string > ( headers ) ,
273
- body ,
274
- transportTransaction ,
275
- messageContextCancellationTokenSource ,
276
- context ) ;
277
285
278
- await onMessage ( messageContext ) . ConfigureAwait ( false ) ;
286
+ var messageContext = new MessageContext (
287
+ nativeMessageId ,
288
+ new Dictionary < string , string > ( headers ) ,
289
+ body ,
290
+ transportTransaction ,
291
+ messageContextCancellationTokenSource ,
292
+ context ) ;
293
+
294
+ await onMessage ( messageContext ) . ConfigureAwait ( false ) ;
279
295
280
- messageProcessedOk = ! messageContextCancellationTokenSource . IsCancellationRequested ;
281
- }
282
296
}
283
- catch ( Exception ex )
284
- when ( ! ( ex is OperationCanceledException && token . IsCancellationRequested ) )
297
+ catch ( Exception ex ) when ( ! ( ex is OperationCanceledException && token . IsCancellationRequested ) )
285
298
{
286
- immediateProcessingAttempts ++ ;
287
- var errorHandlerResult = ErrorHandleResult . RetryRequired ;
299
+ var deliveryAttempts = GetDeliveryAttempts ( nativeMessageId ) ;
288
300
289
301
try
290
302
{
291
- errorHandlerResult = await onError ( new ErrorContext ( ex ,
303
+ var errorHandlerResult = await onError ( new ErrorContext ( ex ,
292
304
new Dictionary < string , string > ( headers ) ,
293
305
nativeMessageId ,
294
306
body ,
295
307
transportTransaction ,
296
- immediateProcessingAttempts ) ) . ConfigureAwait ( false ) ;
308
+ deliveryAttempts ) ) . ConfigureAwait ( false ) ;
309
+
310
+ if ( errorHandlerResult == ErrorHandleResult . RetryRequired )
311
+ {
312
+ await ReturnMessageToQueue ( nativeMessage ) . ConfigureAwait ( false ) ;
313
+
314
+ return false ;
315
+ }
297
316
}
298
317
catch ( Exception onErrorEx )
299
318
{
300
319
criticalError . Raise ( $ "Failed to execute recoverability policy for message with native ID: `{ nativeMessageId } `", onErrorEx ) ;
320
+
321
+ await ReturnMessageToQueue ( nativeMessage ) . ConfigureAwait ( false ) ;
322
+
323
+ return false ;
301
324
}
325
+ }
326
+
327
+ if ( messageContextCancellationTokenSource . IsCancellationRequested )
328
+ {
329
+ await ReturnMessageToQueue ( nativeMessage ) . ConfigureAwait ( false ) ;
302
330
303
- errorHandled = errorHandlerResult == ErrorHandleResult . Handled ;
331
+ return false ;
304
332
}
333
+
334
+ return true ;
335
+ }
336
+ }
337
+
338
+ async Task ReturnMessageToQueue ( Message message )
339
+ {
340
+ try
341
+ {
342
+ await sqsClient . ChangeMessageVisibilityAsync (
343
+ new ChangeMessageVisibilityRequest ( )
344
+ {
345
+ QueueUrl = inputQueueUrl ,
346
+ ReceiptHandle = message . ReceiptHandle ,
347
+ VisibilityTimeout = 0
348
+ } ,
349
+ CancellationToken . None ) . ConfigureAwait ( false ) ;
350
+ }
351
+ catch ( ReceiptHandleIsInvalidException ex )
352
+ {
353
+ Logger . Warn ( $ "Failed to return message with native ID '{ message . MessageId } ' to the queue because the visibility timeout has expired. The message has already been returned to the queue.", ex ) ;
354
+ }
355
+ catch ( Exception ex )
356
+ {
357
+ Logger . Warn ( $ "Failed to return message with native ID '{ message . MessageId } ' to the queue. The message will return to the queue after the visibility timeout expires.", ex ) ;
305
358
}
306
359
}
307
360
361
+ int GetDeliveryAttempts ( string nativeMessageId )
362
+ {
363
+ deliveryAttemptsStorage . RecordFailureInfoForMessage ( nativeMessageId ) ;
364
+
365
+ deliveryAttemptsStorage . TryGetFailureInfoForMessage ( nativeMessageId , out var attempts ) ;
366
+
367
+ return attempts ;
368
+ }
369
+
308
370
static bool IsMessageExpired ( Message receivedMessage , Dictionary < string , string > headers , string messageId , TimeSpan clockOffset )
309
371
{
310
372
if ( ! headers . TryGetValue ( TimeToBeReceived , out var rawTtbr ) )
@@ -338,16 +400,23 @@ async Task DeleteMessage(Message message, string s3BodyKey)
338
400
{
339
401
// should not be cancelled
340
402
await sqsClient . DeleteMessageAsync ( inputQueueUrl , message . ReceiptHandle , CancellationToken . None ) . ConfigureAwait ( false ) ;
403
+
404
+ if ( ! string . IsNullOrEmpty ( s3BodyKey ) )
405
+ {
406
+ Logger . Info ( $ "Message body data with key '{ s3BodyKey } ' will be aged out by the S3 lifecycle policy when the TTL expires.") ;
407
+ }
341
408
}
342
409
catch ( ReceiptHandleIsInvalidException ex )
343
410
{
344
- Logger . Info ( $ "Message receipt handle { message . ReceiptHandle } no longer valid.", ex ) ;
345
- return ; // if another receiver fetches the data from S3
346
- }
411
+ Logger . Error ( $ "Failed to delete message with native ID '{ message . MessageId } ' because the handler execution time exceeded the visibility timeout. Increase the length of the timeout on the queue. The message was returned to the queue.", ex ) ;
347
412
348
- if ( ! string . IsNullOrEmpty ( s3BodyKey ) )
413
+ messagesToBeDeletedStorage . RecordFailureInfoForMessage ( message . MessageId ) ;
414
+ }
415
+ catch ( Exception ex )
349
416
{
350
- Logger . Info ( $ "Message body data with key '{ s3BodyKey } ' will be aged out by the S3 lifecycle policy when the TTL expires.") ;
417
+ Logger . Warn ( $ "Failed to delete message with native ID '{ message . MessageId } '. The message will be returned to the queue when the visibility timeout expires.", ex ) ;
418
+
419
+ messagesToBeDeletedStorage . RecordFailureInfoForMessage ( message . MessageId ) ;
351
420
}
352
421
}
353
422
@@ -405,6 +474,8 @@ await sqsClient.DeleteMessageAsync(new DeleteMessageRequest
405
474
// If there is a message body in S3, simply leave it there
406
475
}
407
476
477
+ readonly FailureInfoStorage deliveryAttemptsStorage = new FailureInfoStorage ( 1_000 ) ;
478
+ readonly FailureInfoStorage messagesToBeDeletedStorage = new FailureInfoStorage ( 1_000 ) ;
408
479
List < Task > pumpTasks ;
409
480
Func < ErrorContext , Task < ErrorHandleResult > > onError ;
410
481
Func < MessageContext , Task > onMessage ;
0 commit comments