1818import static org .assertj .core .api .Assertions .*;
1919import static org .junit .Assume .*;
2020
21+ import io .lettuce .core .codec .StringCodec ;
22+ import io .lettuce .core .output .NestedMultiOutput ;
2123import lombok .AllArgsConstructor ;
2224import lombok .Data ;
2325
2426import java .time .Duration ;
2527import java .util .Collections ;
28+ import java .util .List ;
2629import java .util .concurrent .BlockingQueue ;
2730import java .util .concurrent .LinkedBlockingQueue ;
2831import java .util .concurrent .TimeUnit ;
3841import org .springframework .data .redis .connection .RedisConnectionFactory ;
3942import org .springframework .data .redis .connection .RedisStandaloneConfiguration ;
4043import org .springframework .data .redis .connection .lettuce .LettuceClientConfiguration ;
44+ import org .springframework .data .redis .connection .lettuce .LettuceConnection ;
4145import org .springframework .data .redis .connection .lettuce .LettuceConnectionFactory ;
4246import org .springframework .data .redis .connection .lettuce .LettuceTestClientResources ;
4347import org .springframework .data .redis .connection .stream .Consumer ;
5054import org .springframework .data .redis .core .StringRedisTemplate ;
5155import org .springframework .data .redis .stream .StreamMessageListenerContainer .StreamMessageListenerContainerOptions ;
5256import org .springframework .data .redis .stream .StreamMessageListenerContainer .StreamReadRequest ;
57+ import org .springframework .util .NumberUtils ;
5358
5459/**
5560 * Integration tests for {@link StreamMessageListenerContainer}.
5661 *
5762 * @author Mark Paluch
63+ * @author Christoph Strobl
5864 */
5965public class StreamMessageListenerContainerIntegrationTests {
6066
@@ -103,8 +109,7 @@ public void before() {
103109public void shouldReceiveMapMessages () throws InterruptedException {
104110
105111StreamMessageListenerContainer <String , MapRecord <String , String , String >> container = StreamMessageListenerContainer
106- .create (connectionFactory ,
107- containerOptions );
112+ .create (connectionFactory , containerOptions );
108113BlockingQueue <MapRecord <String , String , String >> queue = new LinkedBlockingQueue <>();
109114
110115container .start ();
@@ -174,12 +179,11 @@ public void shouldReceiveObjectHashRecords() throws InterruptedException {
174179assertThat (subscription .isActive ()).isFalse ();
175180}
176181
177- @ Test // DATAREDIS-864
182+ @ Test // DATAREDIS-864, DATAREDIS-1079
178183public void shouldReceiveMessagesInConsumerGroup () throws InterruptedException {
179184
180185StreamMessageListenerContainer <String , MapRecord <String , String , String >> container = StreamMessageListenerContainer
181- .create (connectionFactory ,
182- containerOptions );
186+ .create (connectionFactory , containerOptions );
183187BlockingQueue <MapRecord <String , String , String >> queue = new LinkedBlockingQueue <>();
184188RecordId messageId = redisTemplate .opsForStream ().add ("my-stream" , Collections .singletonMap ("key" , "value1" ));
185189redisTemplate .opsForStream ().createGroup ("my-stream" , ReadOffset .from (messageId ), "my-group" );
@@ -196,6 +200,34 @@ public void shouldReceiveMessagesInConsumerGroup() throws InterruptedException {
196200assertThat (message ).isNotNull ();
197201assertThat (message .getValue ()).containsEntry ("key" , "value2" );
198202
203+ assertThat (getNumberOfPending ("my-stream" , "my-group" )).isOne ();
204+
205+ cancelAwait (subscription );
206+ }
207+
208+ @ Test // DATAREDIS-1079
209+ public void shouldReceiveAndAckMessagesInConsumerGroup () throws InterruptedException {
210+
211+ StreamMessageListenerContainer <String , MapRecord <String , String , String >> container = StreamMessageListenerContainer
212+ .create (connectionFactory , containerOptions );
213+ BlockingQueue <MapRecord <String , String , String >> queue = new LinkedBlockingQueue <>();
214+ RecordId messageId = redisTemplate .opsForStream ().add ("my-stream" , Collections .singletonMap ("key" , "value1" ));
215+ redisTemplate .opsForStream ().createGroup ("my-stream" , ReadOffset .from (messageId ), "my-group" );
216+
217+ container .start ();
218+ Subscription subscription = container .receiveAutoAck (Consumer .from ("my-group" , "my-consumer" ),
219+ StreamOffset .create ("my-stream" , ReadOffset .lastConsumed ()), queue ::add );
220+
221+ subscription .await (Duration .ofSeconds (2 ));
222+
223+ redisTemplate .opsForStream ().add ("my-stream" , Collections .singletonMap ("key" , "value2" ));
224+
225+ MapRecord <String , String , String > message = queue .poll (1 , TimeUnit .SECONDS );
226+ assertThat (message ).isNotNull ();
227+ assertThat (message .getValue ()).containsEntry ("key" , "value2" );
228+
229+ assertThat (getNumberOfPending ("my-stream" , "my-group" )).isZero ();
230+
199231cancelAwait (subscription );
200232}
201233
@@ -207,8 +239,7 @@ public void shouldUseCustomErrorHandler() throws InterruptedException {
207239StreamMessageListenerContainerOptions <String , MapRecord <String , String , String >> containerOptions = StreamMessageListenerContainerOptions
208240.builder ().errorHandler (failures ::add ).pollTimeout (Duration .ofMillis (100 )).build ();
209241StreamMessageListenerContainer <String , MapRecord <String , String , String >> container = StreamMessageListenerContainer
210- .create (connectionFactory ,
211- containerOptions );
242+ .create (connectionFactory , containerOptions );
212243
213244container .start ();
214245Subscription subscription = container .receive (Consumer .from ("my-group" , "my-consumer" ),
@@ -229,8 +260,7 @@ public void errorShouldStopListening() throws InterruptedException {
229260BlockingQueue <Throwable > failures = new LinkedBlockingQueue <>();
230261
231262StreamMessageListenerContainer <String , MapRecord <String , String , String >> container = StreamMessageListenerContainer
232- .create (connectionFactory ,
233- containerOptions );
263+ .create (connectionFactory , containerOptions );
234264
235265StreamReadRequest <String > readRequest = StreamReadRequest
236266.builder (StreamOffset .create ("my-stream" , ReadOffset .lastConsumed ())).errorHandler (failures ::add )
@@ -260,8 +290,7 @@ public void customizedCancelPredicateShouldNotStopListening() throws Interrupted
260290BlockingQueue <Throwable > failures = new LinkedBlockingQueue <>();
261291
262292StreamMessageListenerContainer <String , MapRecord <String , String , String >> container = StreamMessageListenerContainer
263- .create (connectionFactory ,
264- containerOptions );
293+ .create (connectionFactory , containerOptions );
265294
266295StreamReadRequest <String > readRequest = StreamReadRequest
267296.builder (StreamOffset .create ("my-stream" , ReadOffset .lastConsumed ())) //
@@ -291,8 +320,7 @@ public void customizedCancelPredicateShouldNotStopListening() throws Interrupted
291320public void cancelledStreamShouldNotReceiveMessages () throws InterruptedException {
292321
293322StreamMessageListenerContainer <String , MapRecord <String , String , String >> container = StreamMessageListenerContainer
294- .create (connectionFactory ,
295- containerOptions );
323+ .create (connectionFactory , containerOptions );
296324BlockingQueue <MapRecord <String , String , String >> queue = new LinkedBlockingQueue <>();
297325
298326container .start ();
@@ -310,8 +338,7 @@ public void cancelledStreamShouldNotReceiveMessages() throws InterruptedExceptio
310338public void containerRestartShouldRestartSubscription () throws InterruptedException {
311339
312340StreamMessageListenerContainer <String , MapRecord <String , String , String >> container = StreamMessageListenerContainer
313- .create (connectionFactory ,
314- containerOptions );
341+ .create (connectionFactory , containerOptions );
315342BlockingQueue <MapRecord <String , String , String >> queue = new LinkedBlockingQueue <>();
316343
317344container .start ();
@@ -345,6 +372,14 @@ private static void cancelAwait(Subscription subscription) throws InterruptedExc
345372}
346373}
347374
375+ private Integer getNumberOfPending (String stream , String group ) {
376+
377+ String value = ((List ) ((LettuceConnection ) connectionFactory .getConnection ()).execute ("XPENDING" ,
378+ new NestedMultiOutput (StringCodec .UTF8 ), new byte [][] { stream .getBytes (), group .getBytes () })).iterator ()
379+ .next ().toString ();
380+ return NumberUtils .parseNumber (value , Integer .class );
381+ }
382+
348383@ Data
349384@ AllArgsConstructor
350385static class LoginEvent {
0 commit comments