This is an automated email from the ASF dual-hosted git repository.

FrankChen021 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 017c18ab6db fix: Wait for Kafka lookup startup catch-up (#19858)
017c18ab6db is described below

commit 017c18ab6dbf334f1ec58318faef016817e302e0
Author: sakshichitnis27 <[email protected]>
AuthorDate: Sat Sep 26 05:00:53 2026 +0100

    fix: Wait for Kafka lookup startup catch-up (#19858)
    
    * fix: Wait for Kafka lookup startup catch-up
    
    * Wait for Kafka lookup worker before closing cache
    
    * fix: Avoid blocking Kafka lookup startup timeout
    
    * Fix deprecated MockConsumer usage in Kafka lookup tests
    
    * Replace remaining deprecated MockConsumer usages
---
 .../query/lookup/KafkaLookupExtractorFactory.java  |  58 ++++++--
 .../lookup/KafkaLookupExtractorFactoryTest.java    | 163 ++++++++++++++++++---
 2 files changed, 187 insertions(+), 34 deletions(-)

diff --git 
a/extensions-core/kafka-extraction-namespace/src/main/java/org/apache/druid/query/lookup/KafkaLookupExtractorFactory.java
 
b/extensions-core/kafka-extraction-namespace/src/main/java/org/apache/druid/query/lookup/KafkaLookupExtractorFactory.java
index 81c80de1125..2c19e8ece15 100644
--- 
a/extensions-core/kafka-extraction-namespace/src/main/java/org/apache/druid/query/lookup/KafkaLookupExtractorFactory.java
+++ 
b/extensions-core/kafka-extraction-namespace/src/main/java/org/apache/druid/query/lookup/KafkaLookupExtractorFactory.java
@@ -43,6 +43,7 @@ import org.apache.kafka.clients.consumer.ConsumerConfig;
 import org.apache.kafka.clients.consumer.ConsumerRecord;
 import org.apache.kafka.clients.consumer.ConsumerRecords;
 import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.common.TopicPartition;
 import org.apache.kafka.common.serialization.StringDeserializer;
 
 import javax.annotation.Nonnull;
@@ -52,6 +53,7 @@ import java.time.Duration;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.CountDownLatch;
@@ -176,6 +178,7 @@ public class KafkaLookupExtractorFactory implements 
LookupExtractorFactory
       final ListenableFuture<?> future = executorService.submit(() -> {
         final Consumer<String, String> consumer = getConsumer();
         consumer.subscribe(Collections.singletonList(topic));
+        Map<TopicPartition, Long> startupEndOffsets = null;
         try {
           while (!executorService.isShutdown()) {
             try {
@@ -183,7 +186,13 @@ public class KafkaLookupExtractorFactory implements 
LookupExtractorFactory
                 break;
               }
               final ConsumerRecords<String, String> records = 
consumer.poll(Duration.ofMillis(1000));
-              startingReads.countDown();
+              if (startingReads.getCount() > 0) {
+                final Set<TopicPartition> assignment = consumer.assignment();
+                if (!assignment.isEmpty()
+                    && (startupEndOffsets == null || 
!startupEndOffsets.keySet().equals(assignment))) {
+                  startupEndOffsets = consumer.endOffsets(assignment);
+                }
+              }
 
               for (final ConsumerRecord<String, String> record : records) {
                 final String key = record.key();
@@ -204,6 +213,11 @@ public class KafkaLookupExtractorFactory implements 
LookupExtractorFactory
                 doubleEventCount.incrementAndGet();
                 LOG.trace("Placed key[%s] val[%s]", key, message);
               }
+              if (startingReads.getCount() > 0
+                  && startupEndOffsets != null
+                  && hasReachedEndOffsets(consumer, startupEndOffsets)) {
+                startingReads.countDown();
+              }
             }
             catch (Exception e) {
               LOG.error(e, "Error reading stream for topic [%s]", topic);
@@ -251,10 +265,8 @@ public class KafkaLookupExtractorFactory implements 
LookupExtractorFactory
         }
       }
       catch (InterruptedException | ExecutionException | TimeoutException e) {
-        executorService.shutdown();
-        future.cancel(true);
         LOG.error(e, "Failed to start kafka extraction factory");
-        cacheHandler.close();
+        shutdownExecutorAndCloseCache();
         return false;
       }
 
@@ -272,13 +284,7 @@ public class KafkaLookupExtractorFactory implements 
LookupExtractorFactory
         return !started.get();
       }
       started.set(false);
-      executorService.shutdown();
-
-      final ListenableFuture<?> future = this.future;
-      if (future != null) {
-        future.cancel(true);
-      }
-      cacheHandler.close();
+      shutdownExecutorAndCloseCache();
       return true;
     }
   }
@@ -381,6 +387,36 @@ public class KafkaLookupExtractorFactory implements 
LookupExtractorFactory
     return future;
   }
 
+  boolean awaitExecutorTermination(final long timeout, final TimeUnit unit) 
throws InterruptedException
+  {
+    return executorService.awaitTermination(timeout, unit);
+  }
+
+  private void shutdownExecutorAndCloseCache()
+  {
+    // The executor is single-threaded, so the cache is not closed until the 
Kafka worker stops using it.
+    executorService.execute(cacheHandler::close);
+    executorService.shutdown();
+
+    final ListenableFuture<?> future = this.future;
+    if (future != null) {
+      future.cancel(true);
+    }
+  }
+
+  private static boolean hasReachedEndOffsets(
+      final Consumer<String, String> consumer,
+      final Map<TopicPartition, Long> endOffsets
+  )
+  {
+    for (final Map.Entry<TopicPartition, Long> endOffset : 
endOffsets.entrySet()) {
+      if (consumer.position(endOffset.getKey()) < endOffset.getValue()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   /**
    * Check that the user has not set forbidden Kafka consumer props
    *
diff --git 
a/extensions-core/kafka-extraction-namespace/src/test/java/org/apache/druid/query/lookup/KafkaLookupExtractorFactoryTest.java
 
b/extensions-core/kafka-extraction-namespace/src/test/java/org/apache/druid/query/lookup/KafkaLookupExtractorFactoryTest.java
index c9466d28c7f..d81ad1fbf89 100644
--- 
a/extensions-core/kafka-extraction-namespace/src/test/java/org/apache/druid/query/lookup/KafkaLookupExtractorFactoryTest.java
+++ 
b/extensions-core/kafka-extraction-namespace/src/test/java/org/apache/druid/query/lookup/KafkaLookupExtractorFactoryTest.java
@@ -26,22 +26,31 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Bytes;
+import com.google.common.util.concurrent.Uninterruptibles;
 import org.apache.druid.jackson.DefaultObjectMapper;
 import org.apache.druid.java.util.common.IAE;
+import org.apache.druid.java.util.common.concurrent.Execs;
 import 
org.apache.druid.server.lookup.namespace.cache.MockNamespaceExtractionCacheManager;
 import 
org.apache.druid.server.lookup.namespace.cache.NamespaceExtractionCacheManager;
 import org.apache.kafka.clients.consumer.Consumer;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
 import org.apache.kafka.clients.consumer.MockConsumer;
-import org.apache.kafka.clients.consumer.OffsetResetStrategy;
+import org.apache.kafka.common.TopicPartition;
 import org.easymock.EasyMock;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicLong;
 
 public class KafkaLookupExtractorFactoryTest
@@ -75,6 +84,14 @@ public class KafkaLookupExtractorFactoryTest
     });
   }
 
+  private void verifyCacheManagerAfterExecutorTerminates(
+      final KafkaLookupExtractorFactory factory
+  ) throws InterruptedException
+  {
+    Assertions.assertTrue(factory.awaitExecutorTermination(10, 
TimeUnit.SECONDS));
+    EasyMock.verify(cacheManager);
+  }
+
   @Test
   public void testSimpleSerDe() throws Exception
   {
@@ -245,9 +262,13 @@ public class KafkaLookupExtractorFactoryTest
   }
 
   @Test
-  public void testStartStop()
+  public void testStartStop() throws InterruptedException
   {
-    Consumer<String, String> kafkaConsumer = new 
MockConsumer<>(OffsetResetStrategy.EARLIEST);
+    final MockConsumer<String, String> kafkaConsumer = new 
MockConsumer<>("earliest");
+    final TopicPartition topicPartition = new TopicPartition(TOPIC, 0);
+    kafkaConsumer.updateBeginningOffsets(ImmutableMap.of(topicPartition, 0L));
+    kafkaConsumer.updateEndOffsets(ImmutableMap.of(topicPartition, 0L));
+    kafkaConsumer.schedulePollTask(() -> 
kafkaConsumer.rebalance(Collections.singletonList(topicPartition)));
     EasyMock.replay(cacheManager);
 
     final KafkaLookupExtractorFactory factory = new 
KafkaLookupExtractorFactory(
@@ -268,13 +289,96 @@ public class KafkaLookupExtractorFactoryTest
     Assertions.assertTrue(factory.start());
     Assertions.assertTrue(factory.close());
     Assertions.assertTrue(factory.getFuture().isDone());
-    EasyMock.verify(cacheManager);
+    verifyCacheManagerAfterExecutorTerminates(factory);
+  }
+
+  @Test
+  public void testStartWaitsForInitialEndOffsets() throws Exception
+  {
+    final MockConsumer<String, String> kafkaConsumer = new 
MockConsumer<>("earliest");
+    final TopicPartition topicPartition = new TopicPartition(TOPIC, 0);
+    final CountDownLatch firstPollComplete = new CountDownLatch(1);
+    final CountDownLatch allowCatchUp = new CountDownLatch(1);
+
+    kafkaConsumer.schedulePollTask(() -> {
+      kafkaConsumer.updateBeginningOffsets(ImmutableMap.of(topicPartition, 
0L));
+      kafkaConsumer.updateEndOffsets(ImmutableMap.of(topicPartition, 2L));
+      kafkaConsumer.rebalance(Collections.singletonList(topicPartition));
+      kafkaConsumer.addRecord(new ConsumerRecord<>(TOPIC, 0, 0L, "key-0", 
"value-0"));
+      firstPollComplete.countDown();
+    });
+    kafkaConsumer.schedulePollTask(() -> {
+      try {
+        if (!allowCatchUp.await(10, TimeUnit.SECONDS)) {
+          throw new RuntimeException("Timed out waiting to finish the startup 
catch-up");
+        }
+      }
+      catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        throw new RuntimeException(e);
+      }
+      kafkaConsumer.addRecord(new ConsumerRecord<>(TOPIC, 0, 1L, "key-1", 
"value-1"));
+    });
+
+    EasyMock.replay(cacheManager);
+    final KafkaLookupExtractorFactory factory = new 
KafkaLookupExtractorFactory(
+        cacheManager,
+        TOPIC,
+        ImmutableMap.of("bootstrap.servers", "localhost"),
+        10_000L,
+        false
+    )
+    {
+      @Override
+      Consumer<String, String> getConsumer()
+      {
+        return kafkaConsumer;
+      }
+    };
+    final ExecutorService startExecutor = 
Execs.singleThreaded("kafka-lookup-start-test");
+    final Future<Boolean> startFuture = startExecutor.submit(factory::start);
+
+    try {
+      Assertions.assertTrue(firstPollComplete.await(10, TimeUnit.SECONDS));
+      Assertions.assertThrows(
+          TimeoutException.class,
+          () -> startFuture.get(100, TimeUnit.MILLISECONDS),
+          "start returned before the consumer reached its initial end offsets"
+      );
+      allowCatchUp.countDown();
+      Assertions.assertTrue(startFuture.get(10, TimeUnit.SECONDS));
+      Assertions.assertEquals("value-0", factory.get().apply("key-0"));
+      Assertions.assertEquals("value-1", factory.get().apply("key-1"));
+    }
+    finally {
+      allowCatchUp.countDown();
+      factory.close();
+      startExecutor.shutdownNow();
+    }
+    verifyCacheManagerAfterExecutorTerminates(factory);
   }
 
 
   @Test
-  public void testStartFailsFromTimeout()
+  public void testStartTimeoutReturnsBeforeConsumerStops() throws Exception
   {
+    final CountDownLatch pollStarted = new CountDownLatch(1);
+    final CountDownLatch allowPollToFinish = new CountDownLatch(1);
+    final CountDownLatch consumerClosed = new CountDownLatch(1);
+    final MockConsumer<String, String> kafkaConsumer = new 
MockConsumer<>("earliest")
+    {
+      @Override
+      public synchronized void close()
+      {
+        super.close();
+        consumerClosed.countDown();
+      }
+    };
+    kafkaConsumer.schedulePollTask(() -> {
+      pollStarted.countDown();
+      Uninterruptibles.awaitUninterruptibly(allowPollToFinish);
+    });
+
     EasyMock.replay(cacheManager);
     final KafkaLookupExtractorFactory factory = new 
KafkaLookupExtractorFactory(
         cacheManager,
@@ -285,28 +389,37 @@ public class KafkaLookupExtractorFactoryTest
     )
     {
       @Override
-      Consumer getConsumer()
+      Consumer<String, String> getConsumer()
       {
-        // Lock up
-        try {
-          Thread.currentThread().join();
-        }
-        catch (InterruptedException e) {
-          throw new RuntimeException(e);
-        }
-        throw new RuntimeException("shouldn't make it here");
+        return kafkaConsumer;
       }
     };
-    Assertions.assertFalse(factory.start());
-    Assertions.assertTrue(factory.getFuture().isDone());
-    Assertions.assertTrue(factory.getFuture().isCancelled());
+    final ExecutorService startExecutor = 
Execs.singleThreaded("kafka-lookup-timeout-test");
+    final Future<Boolean> startFuture = startExecutor.submit(factory::start);
+
+    try {
+      Assertions.assertTrue(pollStarted.await(10, TimeUnit.SECONDS));
+      Assertions.assertFalse(startFuture.get(500, TimeUnit.MILLISECONDS));
+      Assertions.assertEquals(1L, consumerClosed.getCount());
+      Assertions.assertFalse(factory.awaitExecutorTermination(100, 
TimeUnit.MILLISECONDS));
+
+      allowPollToFinish.countDown();
+      Assertions.assertTrue(consumerClosed.await(10, TimeUnit.SECONDS));
+      Assertions.assertTrue(factory.awaitExecutorTermination(10, 
TimeUnit.SECONDS));
+      Assertions.assertTrue(factory.getFuture().isDone());
+      Assertions.assertTrue(factory.getFuture().isCancelled());
+    }
+    finally {
+      allowPollToFinish.countDown();
+      startExecutor.shutdownNow();
+    }
     EasyMock.verify(cacheManager);
   }
 
   @Test
-  public void testStartStopStart()
+  public void testStartStopStart() throws InterruptedException
   {
-    Consumer<String, String> kafkaConsumer = new 
MockConsumer<>(OffsetResetStrategy.EARLIEST);
+    Consumer<String, String> kafkaConsumer = new MockConsumer<>("earliest");
     EasyMock.replay(cacheManager);
     final KafkaLookupExtractorFactory factory = new 
KafkaLookupExtractorFactory(
         cacheManager,
@@ -323,13 +436,17 @@ public class KafkaLookupExtractorFactoryTest
     Assertions.assertTrue(factory.start());
     Assertions.assertTrue(factory.close());
     Assertions.assertFalse(factory.start());
-    EasyMock.verify(cacheManager);
+    verifyCacheManagerAfterExecutorTerminates(factory);
   }
 
   @Test
-  public void testStartStartStopStop()
+  public void testStartStartStopStop() throws InterruptedException
   {
-    Consumer<String, String> kafkaConsumer = new 
MockConsumer<>(OffsetResetStrategy.EARLIEST);
+    final MockConsumer<String, String> kafkaConsumer = new 
MockConsumer<>("earliest");
+    final TopicPartition topicPartition = new TopicPartition(TOPIC, 0);
+    kafkaConsumer.updateBeginningOffsets(ImmutableMap.of(topicPartition, 0L));
+    kafkaConsumer.updateEndOffsets(ImmutableMap.of(topicPartition, 0L));
+    kafkaConsumer.schedulePollTask(() -> 
kafkaConsumer.rebalance(Collections.singletonList(topicPartition)));
     EasyMock.replay(cacheManager);
     final KafkaLookupExtractorFactory factory = new 
KafkaLookupExtractorFactory(
         cacheManager,
@@ -349,7 +466,7 @@ public class KafkaLookupExtractorFactoryTest
     Assertions.assertTrue(factory.start());
     Assertions.assertTrue(factory.close());
     Assertions.assertTrue(factory.close());
-    EasyMock.verify(cacheManager);
+    verifyCacheManagerAfterExecutorTerminates(factory);
   }
 
   @Test


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to