1919
2020import static org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .base .Preconditions .checkState ;
2121
22- import java .io .Closeable ;
2322import java .math .BigDecimal ;
2423import java .math .MathContext ;
2524import java .time .Duration ;
2625import java .util .Collections ;
2726import java .util .List ;
2827import java .util .Map ;
2928import java .util .Optional ;
30- import java .util .function . Supplier ;
29+ import java .util .concurrent . atomic . AtomicLong ;
3130import org .apache .beam .sdk .coders .Coder ;
3231import org .apache .beam .sdk .io .kafka .KafkaIO .ReadSourceDescriptors ;
3332import org .apache .beam .sdk .io .kafka .KafkaIOUtils .MovingAvg ;
4948import org .apache .beam .sdk .transforms .splittabledofn .WatermarkEstimator ;
5049import org .apache .beam .sdk .transforms .splittabledofn .WatermarkEstimators .MonotonicallyIncreasing ;
5150import org .apache .beam .sdk .transforms .windowing .BoundedWindow ;
52- import org .apache .beam .sdk .util .ExpiringMemoizingSerializableSupplier ;
5351import org .apache .beam .sdk .util .MemoizingPerInstantiationSerializableSupplier ;
5452import org .apache .beam .sdk .util .Preconditions ;
5553import org .apache .beam .sdk .util .SerializableSupplier ;
7169import org .apache .kafka .clients .consumer .ConsumerConfig ;
7270import org .apache .kafka .clients .consumer .ConsumerRecord ;
7371import org .apache .kafka .clients .consumer .ConsumerRecords ;
72+ import org .apache .kafka .clients .consumer .InvalidOffsetException ;
73+ import org .apache .kafka .common .KafkaException ;
7474import org .apache .kafka .common .PartitionInfo ;
7575import org .apache .kafka .common .TopicPartition ;
7676import org .apache .kafka .common .config .ConfigDef ;
@@ -235,30 +235,12 @@ public MovingAvg load(KafkaSourceDescriptor kafkaSourceDescriptor)
235235 CacheBuilder .newBuilder ()
236236 .concurrencyLevel (Runtime .getRuntime ().availableProcessors ())
237237 .weakValues ()
238- .removalListener (
239- (RemovalNotification <KafkaSourceDescriptor , KafkaLatestOffsetEstimator >
240- notification ) -> {
241- final @ Nullable KafkaLatestOffsetEstimator value ;
242- if (notification .getCause () == RemovalCause .COLLECTED
243- && (value = notification .getValue ()) != null ) {
244- value .close ();
245- }
246- })
247238 .build (
248- new CacheLoader <KafkaSourceDescriptor , KafkaLatestOffsetEstimator >() {
239+ new CacheLoader <KafkaSourceDescriptor , AtomicLong >() {
249240 @ Override
250- public KafkaLatestOffsetEstimator load (
251- final KafkaSourceDescriptor sourceDescriptor ) {
252- LOG .info (
253- "Creating Kafka consumer for offset estimation for {}" ,
254- sourceDescriptor );
255- final Map <String , Object > config =
256- KafkaIOUtils .overrideBootstrapServersConfig (
257- consumerConfig , sourceDescriptor );
258- final Consumer <byte [], byte []> consumer =
259- consumerFactoryFn .apply (config );
260- return new KafkaLatestOffsetEstimator (
261- consumer , sourceDescriptor .getTopicPartition ());
241+ public AtomicLong load (final KafkaSourceDescriptor sourceDescriptor ) {
242+ LOG .info ("Creating end offset estimator for {}" , sourceDescriptor );
243+ return new AtomicLong (Long .MIN_VALUE );
262244 }
263245 }));
264246 this .pollConsumerCacheSupplier =
@@ -319,8 +301,7 @@ public Consumer<byte[], byte[]> load(
319301 private final SerializableSupplier <LoadingCache <KafkaSourceDescriptor , MovingAvg >>
320302 avgRecordSizeCacheSupplier ;
321303
322- private final SerializableSupplier <
323- LoadingCache <KafkaSourceDescriptor , KafkaLatestOffsetEstimator >>
304+ private final SerializableSupplier <LoadingCache <KafkaSourceDescriptor , AtomicLong >>
324305 latestOffsetEstimatorCacheSupplier ;
325306
326307 private final SerializableSupplier <LoadingCache <KafkaSourceDescriptor , Consumer <byte [], byte []>>>
@@ -329,8 +310,7 @@ public Consumer<byte[], byte[]> load(
329310 private transient @ MonotonicNonNull LoadingCache <KafkaSourceDescriptor , MovingAvg >
330311 avgRecordSizeCache ;
331312
332- private transient @ MonotonicNonNull LoadingCache <
333- KafkaSourceDescriptor , KafkaLatestOffsetEstimator >
313+ private transient @ MonotonicNonNull LoadingCache <KafkaSourceDescriptor , AtomicLong >
334314 latestOffsetEstimatorCache ;
335315
336316 private transient @ MonotonicNonNull LoadingCache <KafkaSourceDescriptor , Consumer <byte [], byte []>>
@@ -349,46 +329,6 @@ public Consumer<byte[], byte[]> load(
349329 @ VisibleForTesting
350330 static final String RAW_SIZE_METRIC_PREFIX = KafkaUnboundedReader .RAW_SIZE_METRIC_PREFIX ;
351331
352- /**
353- * A {@link GrowableOffsetRangeTracker.RangeEndEstimator} which uses a Kafka {@link Consumer} to
354- * fetch backlog.
355- */
356- private static class KafkaLatestOffsetEstimator
357- implements GrowableOffsetRangeTracker .RangeEndEstimator , Closeable {
358- private final Consumer <byte [], byte []> offsetConsumer ;
359- private final Supplier <Long > offsetSupplier ;
360-
361- KafkaLatestOffsetEstimator (
362- final Consumer <byte [], byte []> offsetConsumer , final TopicPartition topicPartition ) {
363- this .offsetConsumer = offsetConsumer ;
364- this .offsetSupplier =
365- new ExpiringMemoizingSerializableSupplier <>(
366- () -> {
367- try {
368- return offsetConsumer
369- .endOffsets (Collections .singleton (topicPartition ))
370- .getOrDefault (topicPartition , Long .MIN_VALUE );
371- } catch (Throwable t ) {
372- LOG .error ("Failed to get end offset for {}" , topicPartition , t );
373- return Long .MIN_VALUE ;
374- }
375- },
376- Duration .ofSeconds (1 ),
377- Long .MIN_VALUE ,
378- Duration .ZERO );
379- }
380-
381- @ Override
382- public long estimate () {
383- return offsetSupplier .get ();
384- }
385-
386- @ Override
387- public void close () {
388- offsetConsumer .close ();
389- }
390- }
391-
392332 @ GetInitialRestriction
393333 @ RequiresNonNull ({"pollConsumerCache" })
394334 public OffsetRange initialRestriction (@ Element KafkaSourceDescriptor kafkaSourceDescriptor ) {
@@ -500,8 +440,8 @@ public double getSize(
500440 @ RequiresNonNull ({"latestOffsetEstimatorCache" })
501441 public UnsplittableRestrictionTracker <OffsetRange , Long > restrictionTracker (
502442 @ Element KafkaSourceDescriptor kafkaSourceDescriptor , @ Restriction OffsetRange restriction ) {
503- final LoadingCache <KafkaSourceDescriptor , KafkaLatestOffsetEstimator >
504- latestOffsetEstimatorCache = this .latestOffsetEstimatorCache ;
443+ final LoadingCache <KafkaSourceDescriptor , AtomicLong > latestOffsetEstimatorCache =
444+ this .latestOffsetEstimatorCache ;
505445
506446 if (restriction .getTo () < Long .MAX_VALUE ) {
507447 return new UnsplittableRestrictionTracker <>(new OffsetRangeTracker (restriction ));
@@ -510,9 +450,10 @@ public UnsplittableRestrictionTracker<OffsetRange, Long> restrictionTracker(
510450 // OffsetEstimators are cached for each topic-partition because they hold a stateful connection,
511451 // so we want to minimize the amount of connections that we start and track with Kafka. Another
512452 // point is that it has a memoized backlog, and this should make that more reusable estimations.
453+ final AtomicLong latestOffsetEstimator =
454+ latestOffsetEstimatorCache .getUnchecked (kafkaSourceDescriptor );
513455 return new UnsplittableRestrictionTracker <>(
514- new GrowableOffsetRangeTracker (
515- restriction .getFrom (), latestOffsetEstimatorCache .getUnchecked (kafkaSourceDescriptor )));
456+ new GrowableOffsetRangeTracker (restriction .getFrom (), latestOffsetEstimator ::get ));
516457 }
517458
518459 @ ProcessElement
@@ -525,14 +466,13 @@ public ProcessContinuation processElement(
525466 throws Exception {
526467 final LoadingCache <KafkaSourceDescriptor , MovingAvg > avgRecordSizeCache =
527468 this .avgRecordSizeCache ;
528- final LoadingCache <KafkaSourceDescriptor , KafkaLatestOffsetEstimator >
529- latestOffsetEstimatorCache = this .latestOffsetEstimatorCache ;
469+ final LoadingCache <KafkaSourceDescriptor , AtomicLong > latestOffsetEstimatorCache =
470+ this .latestOffsetEstimatorCache ;
530471 final LoadingCache <KafkaSourceDescriptor , Consumer <byte [], byte []>> pollConsumerCache =
531472 this .pollConsumerCache ;
532473
533474 final MovingAvg avgRecordSize = avgRecordSizeCache .get (kafkaSourceDescriptor );
534- final KafkaLatestOffsetEstimator latestOffsetEstimator =
535- latestOffsetEstimatorCache .get (kafkaSourceDescriptor );
475+ final AtomicLong latestOffsetEstimator = latestOffsetEstimatorCache .get (kafkaSourceDescriptor );
536476 final Consumer <byte [], byte []> consumer = pollConsumerCache .get (kafkaSourceDescriptor );
537477 final Deserializer <K > keyDeserializerInstance =
538478 Preconditions .checkStateNotNull (this .keyDeserializerInstance );
@@ -580,6 +520,20 @@ public ProcessContinuation processElement(
580520 // Fetch the next records.
581521 final ConsumerRecords <byte [], byte []> rawRecords = consumer .poll (remainingTimeout );
582522 final Duration elapsed = pollTimer .elapsed ();
523+ try {
524+ final long position = consumer .position (topicPartition );
525+ consumer
526+ .currentLag (topicPartition )
527+ .ifPresent (lag -> latestOffsetEstimator .lazySet (position + lag ));
528+ } catch (InvalidOffsetException e ) {
529+ // The position is undefined or out of range.
530+ latestOffsetEstimator .lazySet (Long .MIN_VALUE );
531+ } catch (KafkaException e ) {
532+ // Wakeups, interrupts, authentication failures, authorization failures, timeouts and
533+ // unrecoverable errors can be ignored. This routine is reattempted on every iteration
534+ // and fallback methods would likely trigger the same unrecoverable errors.
535+ }
536+
583537 try {
584538 remainingTimeout = remainingTimeout .minus (elapsed );
585539 } catch (ArithmeticException e ) {
@@ -687,7 +641,7 @@ public ProcessContinuation processElement(
687641
688642 final long estimatedBacklogBytes =
689643 (long )
690- (BigDecimal .valueOf (latestOffsetEstimator .estimate ())
644+ (BigDecimal .valueOf (latestOffsetEstimator .get ())
691645 .subtract (BigDecimal .valueOf (expectedOffset ), MathContext .DECIMAL128 )
692646 .doubleValue ()
693647 * avgRecordSize .get ());
@@ -752,8 +706,8 @@ public void setup() throws Exception {
752706 public void teardown () throws Exception {
753707 final LoadingCache <KafkaSourceDescriptor , MovingAvg > avgRecordSizeCache =
754708 this .avgRecordSizeCache ;
755- final LoadingCache <KafkaSourceDescriptor , KafkaLatestOffsetEstimator >
756- latestOffsetEstimatorCache = this .latestOffsetEstimatorCache ;
709+ final LoadingCache <KafkaSourceDescriptor , AtomicLong > latestOffsetEstimatorCache =
710+ this .latestOffsetEstimatorCache ;
757711 final LoadingCache <KafkaSourceDescriptor , Consumer <byte [], byte []>> pollConsumerCache =
758712 this .pollConsumerCache ;
759713
0 commit comments