1919import static java .util .stream .Collectors .toList ;
2020import static org .assertj .core .api .Assertions .assertThat ;
2121import static org .awaitility .Awaitility .await ;
22+ import static org .mockito .ArgumentMatchers .any ;
23+ import static org .mockito .Mockito .doAnswer ;
24+ import static org .mockito .Mockito .spy ;
2225
2326import com .fasterxml .jackson .databind .ObjectMapper ;
2427import io .awspring .cloud .sqs .CompletableFutures ;
8184import org .springframework .util .Assert ;
8285import org .springframework .util .StopWatch ;
8386import software .amazon .awssdk .services .sqs .SqsAsyncClient ;
87+ import software .amazon .awssdk .services .sqs .model .ChangeMessageVisibilityBatchRequest ;
88+ import software .amazon .awssdk .services .sqs .model .ChangeMessageVisibilityBatchRequestEntry ;
8489import software .amazon .awssdk .services .sqs .model .QueueAttributeName ;
8590
8691/**
@@ -116,7 +121,10 @@ class SqsFifoIntegrationTests extends BaseSqsIntegrationTest {
116121
117122 static final String OBSERVES_MESSAGE_FIFO_QUEUE_NAME = "observes_fifo_message_test_queue.fifo" ;
118123
124+ static final String FIFO_VISIBILITY_TIMEOUT_EXTENSION_QUEUE_NAME = "fifo_visibility_timeout_extension_test_queue.fifo" ;
125+
119126 private static final String ERROR_ON_ACK_FACTORY = "errorOnAckFactory" ;
127+ private static final String VISIBILITY_TIMEOUT_EXTENSION_FACTORY = "visibilityTimeoutExtensionFactory" ;
120128
121129 @ Autowired
122130 LatchContainer latchContainer ;
@@ -165,6 +173,7 @@ static void beforeTests() {
165173 createFifoQueue (client , FIFO_MANUALLY_CREATE_FACTORY_QUEUE_NAME ),
166174 createFifoQueue (client , FIFO_MANUALLY_CREATE_BATCH_CONTAINER_QUEUE_NAME ),
167175 createFifoQueue (client , OBSERVES_MESSAGE_FIFO_QUEUE_NAME ),
176+ createFifoQueue (client , FIFO_VISIBILITY_TIMEOUT_EXTENSION_QUEUE_NAME , getVisibilityAttribute ("5" )),
168177 createFifoQueue (client , FIFO_MANUALLY_CREATE_BATCH_FACTORY_QUEUE_NAME )).join ();
169178 }
170179
@@ -460,6 +469,26 @@ public void onMessage(Collection<Message<String>> messages) {
460469
461470 }
462471
472+ @ Test
473+ void visibilityTimeoutExtensionWorksForFifoBatch () throws Exception {
474+ final int messageCount = this .settings .messagesPerMessageGroup ;
475+ // There will be messageCount - 1 requests to change visibility, before each message except the first one
476+ latchContainer .visibilityTimeoutExtensionLatch = new CountDownLatch (messageCount - 1 );
477+
478+ List <String > values = IntStream .range (0 , messageCount ).mapToObj (String ::valueOf ).toList ();
479+ String messageGroupId = UUID .randomUUID ().toString ();
480+ sqsTemplate .sendMany (FIFO_VISIBILITY_TIMEOUT_EXTENSION_QUEUE_NAME ,
481+ createMessagesFromValues (messageGroupId , values ));
482+
483+ assertThat (latchContainer .visibilityTimeoutExtensionLatch .await (settings .latchTimeoutSeconds , TimeUnit .SECONDS ))
484+ .isTrue ();
485+ List <Integer > expectedRequestEntryCounts = IntStream .range (1 , messageCount ).map (i -> messageCount - i ).boxed ()
486+ .toList ();
487+ assertThat (messagesContainer .visibilityTimeoutExtensionBatchRequests ).as (
488+ "Number of entries in each ChangeMessageVisibilityBatchRequest should decrease by 1 on each message" )
489+ .extracting (List ::size ).containsExactlyElementsOf (expectedRequestEntryCounts );
490+ }
491+
463492 @ Test
464493 void manuallyCreatesContainer () throws Exception {
465494 List <String > values = IntStream .range (0 , this .settings .messagesPerTest ).mapToObj (String ::valueOf )
@@ -657,6 +686,13 @@ void listen(List<Message<String>> messages) {
657686
658687 }
659688
689+ static class VisibilityTimeoutExtensionListener {
690+ @ SqsListener (queueNames = FIFO_VISIBILITY_TIMEOUT_EXTENSION_QUEUE_NAME , messageVisibilitySeconds = "5" , factory = VISIBILITY_TIMEOUT_EXTENSION_FACTORY )
691+ void listen (String message ) {
692+ logger .debug ("Processing message: {}" , message );
693+ }
694+ }
695+
660696 static class LatchContainer {
661697
662698 Settings settings ;
@@ -678,6 +714,7 @@ static class LatchContainer {
678714 CountDownLatch stopsProcessingOnAckErrorHasThrown ;
679715 CountDownLatch receivesBatchManyGroupsLatch ;
680716 CountDownLatch receivesFifoBatchGroupingStrategyMultipleGroupsInSameBatchLatch ;
717+ CountDownLatch visibilityTimeoutExtensionLatch ;
681718
682719 LatchContainer (Settings settings ) {
683720 this .settings = settings ;
@@ -698,6 +735,7 @@ static class LatchContainer {
698735 this .receivesFifoBatchGroupingStrategyMultipleGroupsInSameBatchLatch = new CountDownLatch (1 );
699736 this .stopsProcessingOnAckErrorHasThrown = new CountDownLatch (1 );
700737 this .observesFifoMessageLatch = new CountDownLatch (1 );
738+ this .visibilityTimeoutExtensionLatch = new CountDownLatch (1 );
701739 }
702740
703741 }
@@ -711,6 +749,7 @@ static class MessagesContainer {
711749 List <String > manuallyCreatedBatchFactoryMessages = Collections .synchronizedList (new ArrayList <>());
712750 List <String > stopsProcessingOnAckErrorBeforeThrown = Collections .synchronizedList (new ArrayList <>());
713751 List <String > stopsProcessingOnAckErrorAfterThrown = Collections .synchronizedList (new ArrayList <>());
752+ List <List <String >> visibilityTimeoutExtensionBatchRequests = Collections .synchronizedList (new ArrayList <>());
714753
715754 }
716755
@@ -820,6 +859,28 @@ private void handleResult(Message<String> message) {
820859 return factory ;
821860 }
822861
862+ @ Bean (VISIBILITY_TIMEOUT_EXTENSION_FACTORY )
863+ SqsMessageListenerContainerFactory <String > visibilityTrackingSqsListenerContainerFactory () {
864+ SqsAsyncClient spyAsyncClient = spy (createAsyncClient ());
865+
866+ doAnswer (invocation -> {
867+ ChangeMessageVisibilityBatchRequest request = invocation .getArgument (0 );
868+ messagesContainer .visibilityTimeoutExtensionBatchRequests .add (request .entries ().stream ().map (ChangeMessageVisibilityBatchRequestEntry ::receiptHandle ).toList ());
869+ latchContainer .visibilityTimeoutExtensionLatch .countDown ();
870+
871+ return invocation .callRealMethod ();
872+ }).when (spyAsyncClient ).changeMessageVisibilityBatch (any (ChangeMessageVisibilityBatchRequest .class ));
873+
874+ SqsMessageListenerContainerFactory <String > factory = new SqsMessageListenerContainerFactory <>();
875+ factory .configure (options -> options
876+ .maxConcurrentMessages (10 )
877+ .acknowledgementThreshold (10 )
878+ .acknowledgementOrdering (AcknowledgementOrdering .ORDERED_BY_GROUP )
879+ .messageVisibility (Duration .ofSeconds (5 )));
880+ factory .setSqsAsyncClientSupplier (() -> spyAsyncClient );
881+ return factory ;
882+ }
883+
823884 @ Bean
824885 public MessageListenerContainer <String > manuallyCreatedContainer () {
825886 SqsMessageListenerContainer <String > container = new SqsMessageListenerContainer <>(createAsyncClient ());
@@ -931,6 +992,11 @@ ReceivesBatchesFromManyGroupsListener receiveBatchesFromManyGroupsListener() {
931992 return new ReceivesBatchesFromManyGroupsListener ();
932993 }
933994
995+ @ Bean
996+ VisibilityTimeoutExtensionListener visibilityTimeoutExtensionListener () {
997+ return new VisibilityTimeoutExtensionListener ();
998+ }
999+
9341000 @ Bean
9351001 ObservesFifoMessageListener observesFifoMessageListener () {
9361002 return new ObservesFifoMessageListener ();
0 commit comments