1
1
package software .amazon .lambda .powertools .sqs .internal ;
2
2
3
- import org .slf4j .Logger ;
4
- import org .slf4j .LoggerFactory ;
5
-
6
3
import java .util .ArrayList ;
4
+ import java .util .Arrays ;
5
+ import java .util .HashMap ;
7
6
import java .util .List ;
7
+ import java .util .Map ;
8
+ import java .util .Optional ;
8
9
10
+ import com .fasterxml .jackson .core .JsonProcessingException ;
11
+ import com .fasterxml .jackson .databind .JsonNode ;
12
+ import org .slf4j .Logger ;
13
+ import org .slf4j .LoggerFactory ;
14
+ import software .amazon .awssdk .core .SdkBytes ;
9
15
import software .amazon .awssdk .services .sqs .SqsClient ;
10
16
import software .amazon .awssdk .services .sqs .model .DeleteMessageBatchRequest ;
11
17
import software .amazon .awssdk .services .sqs .model .DeleteMessageBatchRequestEntry ;
12
18
import software .amazon .awssdk .services .sqs .model .DeleteMessageBatchResponse ;
19
+ import software .amazon .awssdk .services .sqs .model .GetQueueAttributesRequest ;
20
+ import software .amazon .awssdk .services .sqs .model .GetQueueAttributesResponse ;
13
21
import software .amazon .awssdk .services .sqs .model .GetQueueUrlRequest ;
22
+ import software .amazon .awssdk .services .sqs .model .MessageAttributeValue ;
23
+ import software .amazon .awssdk .services .sqs .model .QueueAttributeName ;
24
+ import software .amazon .awssdk .services .sqs .model .SendMessageBatchRequestEntry ;
25
+ import software .amazon .awssdk .services .sqs .model .SendMessageBatchResponse ;
14
26
import software .amazon .lambda .powertools .sqs .SQSBatchProcessingException ;
27
+ import software .amazon .lambda .powertools .sqs .SqsUtils ;
15
28
16
29
import static com .amazonaws .services .lambda .runtime .events .SQSEvent .SQSMessage ;
17
30
import static java .lang .String .format ;
18
31
import static java .util .stream .Collectors .toList ;
19
32
20
33
public final class BatchContext {
21
34
private static final Logger LOG = LoggerFactory .getLogger (BatchContext .class );
35
+ private static final Map <String , String > queueArnToQueueUrlMapping = new HashMap <>();
36
+ private static final Map <String , String > queueArnToDlqUrlMapping = new HashMap <>();
22
37
38
+ private final Map <SQSMessage , Exception > messageToException = new HashMap <>();
23
39
private final List <SQSMessage > success = new ArrayList <>();
24
- private final List <SQSMessage > failures = new ArrayList <>();
25
- private final List <Exception > exceptions = new ArrayList <>();
40
+
26
41
private final SqsClient client ;
27
42
28
43
public BatchContext (SqsClient client ) {
@@ -34,37 +49,132 @@ public void addSuccess(SQSMessage event) {
34
49
}
35
50
36
51
public void addFailure (SQSMessage event , Exception e ) {
37
- failures .add (event );
38
- exceptions .add (e );
52
+ messageToException .put (event , e );
39
53
}
40
54
41
- public <T > void processSuccessAndHandleFailed (final List <T > successReturns ,
42
- final boolean suppressException ) {
55
+ @ SafeVarargs
56
+ public final <T > void processSuccessAndHandleFailed (final List <T > successReturns ,
57
+ final boolean suppressException ,
58
+ final boolean deleteNonRetryableMessageFromQueue ,
59
+ final Class <? extends Exception >... nonRetryableExceptions ) {
43
60
if (hasFailures ()) {
44
- deleteSuccessMessage ();
61
+
62
+ List <Exception > exceptions = new ArrayList <>();
63
+ List <SQSMessage > failedMessages = new ArrayList <>();
64
+ Map <SQSMessage , Exception > nonRetryableMessageToException = new HashMap <>();
65
+
66
+ messageToException .forEach ((sqsMessage , exception ) -> {
67
+ boolean nonRetryableMessage = Arrays .stream (nonRetryableExceptions )
68
+ .anyMatch (aClass -> aClass .isInstance (exception ));
69
+
70
+ if (nonRetryableMessage ) {
71
+ nonRetryableMessageToException .put (sqsMessage , exception );
72
+ } else {
73
+ exceptions .add (exception );
74
+ failedMessages .add (sqsMessage );
75
+ }
76
+ });
77
+
78
+ List <SQSMessage > messagesToBeDeleted = new ArrayList <>(success );
79
+
80
+ if (!nonRetryableMessageToException .isEmpty () && deleteNonRetryableMessageFromQueue ) {
81
+ messagesToBeDeleted .addAll (nonRetryableMessageToException .keySet ());
82
+ } else if (!nonRetryableMessageToException .isEmpty ()) {
83
+
84
+ boolean isMovedToDlq = moveNonRetryableMessagesToDlqIfConfigured (nonRetryableMessageToException );
85
+
86
+ if (!isMovedToDlq ) {
87
+ exceptions .addAll (nonRetryableMessageToException .values ());
88
+ failedMessages .addAll (nonRetryableMessageToException .keySet ());
89
+ }
90
+ }
91
+
92
+ deleteMessagesFromQueue (messagesToBeDeleted );
45
93
46
94
if (suppressException ) {
47
- List <String > messageIds = failures .stream ().
95
+ List <String > messageIds = failedMessages .stream ().
48
96
map (SQSMessage ::getMessageId )
49
97
.collect (toList ());
50
98
51
99
LOG .debug (format ("[%s] records failed processing, but exceptions are suppressed. " +
52
- "Failed messages %s" , failures .size (), messageIds ));
100
+ "Failed messages %s" , failedMessages .size (), messageIds ));
53
101
} else {
54
- throw new SQSBatchProcessingException (exceptions , failures , successReturns );
102
+ throw new SQSBatchProcessingException (exceptions , failedMessages , successReturns );
55
103
}
56
104
}
57
105
}
58
106
107
+ private boolean moveNonRetryableMessagesToDlqIfConfigured (Map <SQSMessage , Exception > nonRetryableMessageToException ) {
108
+ Optional <String > dlqUrl = fetchDlqUrl (nonRetryableMessageToException );
109
+
110
+ if (!dlqUrl .isPresent ()) {
111
+ return false ;
112
+ }
113
+
114
+ List <SendMessageBatchRequestEntry > dlqMessages = nonRetryableMessageToException .keySet ().stream ()
115
+ .map (sqsMessage -> {
116
+ Map <String , MessageAttributeValue > messageAttributesMap = new HashMap <>();
117
+
118
+ sqsMessage .getMessageAttributes ().forEach ((s , messageAttribute ) -> {
119
+ MessageAttributeValue .Builder builder = MessageAttributeValue .builder ();
120
+
121
+ builder
122
+ .dataType (messageAttribute .getDataType ())
123
+ .stringValue (messageAttribute .getStringValue ());
124
+
125
+ if (null != messageAttribute .getBinaryValue ()) {
126
+ builder .binaryValue (SdkBytes .fromByteBuffer (messageAttribute .getBinaryValue ()));
127
+ }
128
+
129
+ messageAttributesMap .put (s , builder .build ());
130
+ });
131
+
132
+ return SendMessageBatchRequestEntry .builder ()
133
+ .messageBody (sqsMessage .getBody ())
134
+ .id (sqsMessage .getMessageId ())
135
+ .messageAttributes (messageAttributesMap )
136
+ .build ();
137
+ })
138
+ .collect (toList ());
139
+
140
+ SendMessageBatchResponse sendMessageBatchResponse = client .sendMessageBatch (builder -> builder .queueUrl (dlqUrl .get ())
141
+ .entries (dlqMessages ));
142
+
143
+ LOG .debug (format ("Response from send batch message to DLQ request %s" , sendMessageBatchResponse ));
144
+
145
+ return true ;
146
+ }
147
+
148
+ private Optional <String > fetchDlqUrl (Map <SQSMessage , Exception > nonRetryableMessageToException ) {
149
+ return nonRetryableMessageToException .keySet ().stream ()
150
+ .findFirst ()
151
+ .map (sqsMessage -> queueArnToDlqUrlMapping .computeIfAbsent (sqsMessage .getEventSourceArn (), sourceArn -> {
152
+ String queueUrl = url (sourceArn );
153
+
154
+ GetQueueAttributesResponse queueAttributes = client .getQueueAttributes (GetQueueAttributesRequest .builder ()
155
+ .attributeNames (QueueAttributeName .REDRIVE_POLICY )
156
+ .queueUrl (queueUrl )
157
+ .build ());
158
+
159
+ try {
160
+ JsonNode jsonNode = SqsUtils .objectMapper ().readTree (queueAttributes .attributes ().get (QueueAttributeName .REDRIVE_POLICY ));
161
+ return url (jsonNode .get ("deadLetterTargetArn" ).asText ());
162
+ } catch (JsonProcessingException e ) {
163
+ LOG .debug ("Unable to parse Re drive policy for queue {}. Even if DLQ exists, failed messages will be send back to main queue." , queueUrl , e );
164
+ return null ;
165
+ }
166
+ }));
167
+ }
168
+
59
169
private boolean hasFailures () {
60
- return !failures .isEmpty ();
170
+ return !messageToException .isEmpty ();
61
171
}
62
172
63
- private void deleteSuccessMessage ( ) {
64
- if (!success .isEmpty ()) {
173
+ private void deleteMessagesFromQueue ( final List < SQSMessage > messages ) {
174
+ if (!messages .isEmpty ()) {
65
175
DeleteMessageBatchRequest request = DeleteMessageBatchRequest .builder ()
66
- .queueUrl (url ())
67
- .entries (success .stream ().map (m -> DeleteMessageBatchRequestEntry .builder ()
176
+ .queueUrl (url (messages . get ( 0 ). getEventSourceArn () ))
177
+ .entries (messages .stream ().map (m -> DeleteMessageBatchRequestEntry .builder ()
68
178
.id (m .getMessageId ())
69
179
.receiptHandle (m .getReceiptHandle ())
70
180
.build ()).collect (toList ()))
@@ -75,12 +185,15 @@ private void deleteSuccessMessage() {
75
185
}
76
186
}
77
187
78
- private String url () {
79
- String [] arnArray = success .get (0 ).getEventSourceArn ().split (":" );
80
- return client .getQueueUrl (GetQueueUrlRequest .builder ()
81
- .queueOwnerAWSAccountId (arnArray [4 ])
82
- .queueName (arnArray [5 ])
83
- .build ())
84
- .queueUrl ();
188
+ private String url (String queueArn ) {
189
+ return queueArnToQueueUrlMapping .computeIfAbsent (queueArn , s -> {
190
+ String [] arnArray = queueArn .split (":" );
191
+
192
+ return client .getQueueUrl (GetQueueUrlRequest .builder ()
193
+ .queueOwnerAWSAccountId (arnArray [4 ])
194
+ .queueName (arnArray [5 ])
195
+ .build ())
196
+ .queueUrl ();
197
+ });
85
198
}
86
199
}
0 commit comments