@@ -291,6 +291,106 @@ void testSyncRelativeSeeks() throws InterruptedException {
291
291
container .stop ();
292
292
}
293
293
294
+ @ SuppressWarnings ({ "rawtypes" , "unchecked" })
295
+ @ Test
296
+ void seekOffsetFromComputeFnOnInitAssignmentAndIdleContainer () throws InterruptedException {
297
+ ConsumerFactory consumerFactory = mock (ConsumerFactory .class );
298
+ final Consumer consumer = mock (Consumer .class );
299
+ TestMessageListener3 listener = new TestMessageListener3 ();
300
+ ConsumerRecords empty = new ConsumerRecords <>(Collections .emptyMap ());
301
+ willAnswer (invocation -> {
302
+ Thread .sleep (10 );
303
+ return empty ;
304
+ }).given (consumer ).poll (any ());
305
+ TopicPartition tp0 = new TopicPartition ("test-topic" , 0 );
306
+ TopicPartition tp1 = new TopicPartition ("test-topic" , 1 );
307
+ TopicPartition tp2 = new TopicPartition ("test-topic" , 2 );
308
+ TopicPartition tp3 = new TopicPartition ("test-topic" , 3 );
309
+ List <TopicPartition > assignments = List .of (tp0 , tp1 , tp2 , tp3 );
310
+ willAnswer (invocation -> {
311
+ ((ConsumerRebalanceListener ) invocation .getArgument (1 ))
312
+ .onPartitionsAssigned (assignments );
313
+ return null ;
314
+ }).given (consumer ).subscribe (any (Collection .class ), any ());
315
+ given (consumer .position (any ())).willReturn (30L ); // current offset position is always 30
316
+ given (consumerFactory .createConsumer ("grp" , "" , "-0" , KafkaTestUtils .defaultPropertyOverrides ()))
317
+ .willReturn (consumer );
318
+ ContainerProperties containerProperties = new ContainerProperties ("test-topic" );
319
+ containerProperties .setGroupId ("grp" );
320
+ containerProperties .setMessageListener (listener );
321
+ containerProperties .setIdleEventInterval (10L );
322
+ containerProperties .setMissingTopicsFatal (false );
323
+ ConcurrentMessageListenerContainer container = new ConcurrentMessageListenerContainer (consumerFactory ,
324
+ containerProperties );
325
+ container .start ();
326
+ assertThat (listener .latch .await (10 , TimeUnit .SECONDS )).isTrue ();
327
+ verify (consumer ).seek (tp0 , 20L );
328
+ verify (consumer ).seek (tp1 , 21L );
329
+ verify (consumer ).seek (tp2 , 22L );
330
+ verify (consumer ).seek (tp3 , 23L );
331
+
332
+ verify (consumer ).seek (tp0 , 30L );
333
+ verify (consumer ).seek (tp1 , 30L );
334
+ verify (consumer ).seek (tp2 , 30L );
335
+ verify (consumer ).seek (tp3 , 30L );
336
+ container .stop ();
337
+ }
338
+
339
+ @ SuppressWarnings ({ "rawtypes" , "unchecked" })
340
+ @ Test
341
+ void seekOffsetFromComputeFnFromActiveListener () throws InterruptedException {
342
+ ConsumerFactory consumerFactory = mock (ConsumerFactory .class );
343
+ final Consumer consumer = mock (Consumer .class );
344
+ TestMessageListener4 listener = new TestMessageListener4 ();
345
+ CountDownLatch latch = new CountDownLatch (2 );
346
+ TopicPartition tp0 = new TopicPartition ("test-topic" , 0 );
347
+ TopicPartition tp1 = new TopicPartition ("test-topic" , 1 );
348
+ TopicPartition tp2 = new TopicPartition ("test-topic" , 2 );
349
+ TopicPartition tp3 = new TopicPartition ("test-topic" , 3 );
350
+ List <TopicPartition > assignments = List .of (tp0 , tp1 , tp2 , tp3 );
351
+ Map <TopicPartition , List <ConsumerRecord <String , String >>> recordMap = new HashMap <>();
352
+ recordMap .put (tp0 , Collections .singletonList (new ConsumerRecord ("test-topic" , 0 , 0 , null , "test-data" )));
353
+ recordMap .put (tp1 , Collections .singletonList (new ConsumerRecord ("test-topic" , 1 , 0 , null , "test-data" )));
354
+ recordMap .put (tp2 , Collections .singletonList (new ConsumerRecord ("test-topic" , 2 , 0 , null , "test-data" )));
355
+ recordMap .put (tp3 , Collections .singletonList (new ConsumerRecord ("test-topic" , 3 , 0 , null , "test-data" )));
356
+ ConsumerRecords records = new ConsumerRecords <>(recordMap );
357
+ willAnswer (invocation -> {
358
+ Thread .sleep (10 );
359
+ if (listener .latch .getCount () <= 0 ) {
360
+ latch .countDown ();
361
+ }
362
+ return records ;
363
+ }).given (consumer ).poll (any ());
364
+ willAnswer (invocation -> {
365
+ ((ConsumerRebalanceListener ) invocation .getArgument (1 ))
366
+ .onPartitionsAssigned (assignments );
367
+ return null ;
368
+ }).given (consumer ).subscribe (any (Collection .class ), any ());
369
+ given (consumer .position (tp0 )).willReturn (30L ); // current offset 30, target 20 (see hard-coded in onMessage)
370
+ given (consumer .position (tp1 )).willReturn (10L ); // current 10, target 21
371
+ given (consumer .position (tp2 )).willReturn (22L ); // current 22, target 22
372
+ given (consumer .position (tp3 )).willReturn (22L ); // current 22, target 23
373
+ given (consumer .beginningOffsets (any ())).willReturn (assignments .stream ()
374
+ .collect (Collectors .toMap (tp -> tp , tp -> 0L )));
375
+ given (consumer .endOffsets (any ())).willReturn (assignments .stream ()
376
+ .collect (Collectors .toMap (tp -> tp , tp -> 100L )));
377
+ given (consumerFactory .createConsumer ("grp" , "" , "-0" , KafkaTestUtils .defaultPropertyOverrides ()))
378
+ .willReturn (consumer );
379
+ ContainerProperties containerProperties = new ContainerProperties ("test-topic" );
380
+ containerProperties .setGroupId ("grp" );
381
+ containerProperties .setMessageListener (listener );
382
+ containerProperties .setMissingTopicsFatal (false );
383
+ ConcurrentMessageListenerContainer container = new ConcurrentMessageListenerContainer (consumerFactory ,
384
+ containerProperties );
385
+ container .start ();
386
+ assertThat (latch .await (10 , TimeUnit .SECONDS )).isTrue ();
387
+ verify (consumer ).seek (tp0 , 20L );
388
+ verify (consumer ).seek (tp1 , 10L );
389
+ verify (consumer ).seek (tp2 , 22L );
390
+ verify (consumer ).seek (tp3 , 22L );
391
+ container .stop ();
392
+ }
393
+
294
394
@ SuppressWarnings ({ "rawtypes" , "unchecked" })
295
395
@ Test
296
396
@ DisplayName ("Seek from activeListener" )
@@ -1282,4 +1382,81 @@ public void onIdleContainer(Map<TopicPartition, Long> assignments, ConsumerSeekC
1282
1382
1283
1383
}
1284
1384
1385
+ public static class TestMessageListener3 implements MessageListener <String , String >, ConsumerSeekAware {
1386
+
1387
+ private static final ThreadLocal <ConsumerSeekCallback > callbacks = new ThreadLocal <>();
1388
+
1389
+ CountDownLatch latch = new CountDownLatch (2 );
1390
+
1391
+ @ Override
1392
+ public void onMessage (ConsumerRecord <String , String > data ) {
1393
+
1394
+ }
1395
+
1396
+ @ Override
1397
+ public void registerSeekCallback (ConsumerSeekCallback callback ) {
1398
+ callbacks .set (callback );
1399
+ }
1400
+
1401
+ @ Override
1402
+ public void onPartitionsAssigned (Map <TopicPartition , Long > assignments , ConsumerSeekCallback callback ) {
1403
+ if (latch .getCount () > 0 ) {
1404
+ int absoluteTarget1 = 20 ;
1405
+ int absoluteTarget2 = 21 ;
1406
+ int absoluteTarget3 = 22 ;
1407
+ int absoluteTarget4 = 23 ;
1408
+ callback .seek ("test-topic" , 0 , current -> current > absoluteTarget1 ? absoluteTarget1 : current );
1409
+ callback .seek ("test-topic" , 1 , current -> current > absoluteTarget2 ? absoluteTarget2 : current );
1410
+ callback .seek ("test-topic" , 2 , current -> current > absoluteTarget3 ? absoluteTarget3 : current );
1411
+ callback .seek ("test-topic" , 3 , current -> current > absoluteTarget4 ? absoluteTarget4 : current );
1412
+ }
1413
+ this .latch .countDown ();
1414
+ }
1415
+
1416
+
1417
+ @ Override
1418
+ public void onIdleContainer (Map <TopicPartition , Long > assignments , ConsumerSeekCallback callback ) {
1419
+ if (latch .getCount () > 0 ) {
1420
+ int absoluteTarget = 31 ;
1421
+ callback .seek ("test-topic" , 0 , current -> current > absoluteTarget ? absoluteTarget : current );
1422
+ callback .seek ("test-topic" , 1 , current -> current > absoluteTarget ? absoluteTarget : current );
1423
+ callback .seek ("test-topic" , 2 , current -> current > absoluteTarget ? absoluteTarget : current );
1424
+ callback .seek ("test-topic" , 3 , current -> current > absoluteTarget ? absoluteTarget : current );
1425
+ }
1426
+ this .latch .countDown ();
1427
+ }
1428
+
1429
+ }
1430
+
1431
+ public static class TestMessageListener4 implements MessageListener <String , String >, ConsumerSeekAware {
1432
+
1433
+ private static final ThreadLocal <ConsumerSeekCallback > callbacks = new ThreadLocal <>();
1434
+
1435
+ CountDownLatch latch = new CountDownLatch (1 );
1436
+
1437
+ @ Override
1438
+ public void onMessage (ConsumerRecord <String , String > data ) {
1439
+ ConsumerSeekCallback callback = callbacks .get ();
1440
+ if (latch .getCount () > 0 ) {
1441
+
1442
+ int absoluteTarget1 = 20 ;
1443
+ int absoluteTarget2 = 21 ;
1444
+ int absoluteTarget3 = 22 ;
1445
+ int absoluteTarget4 = 23 ;
1446
+
1447
+ callback .seek ("test-topic" , 0 , current -> current > absoluteTarget1 ? absoluteTarget1 : current );
1448
+ callback .seek ("test-topic" , 1 , current -> current > absoluteTarget2 ? absoluteTarget2 : current );
1449
+ callback .seek ("test-topic" , 2 , current -> current > absoluteTarget3 ? absoluteTarget3 : current );
1450
+ callback .seek ("test-topic" , 3 , current -> current > absoluteTarget4 ? absoluteTarget4 : current );
1451
+ }
1452
+ this .latch .countDown ();
1453
+ }
1454
+
1455
+ @ Override
1456
+ public void registerSeekCallback (ConsumerSeekCallback callback ) {
1457
+ callbacks .set (callback );
1458
+ }
1459
+
1460
+ }
1461
+
1285
1462
}
0 commit comments