Skip to content

Commit c111c8b

Browse files
authored
[KafkaIO] Make ReadFromKafkaDoFn restriction trackers unsplittable (#36935)
1 parent ddc4976 commit c111c8b

4 files changed

Lines changed: 117 additions & 5 deletions

File tree

sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/splittabledofn/GrowableOffsetRangeTracker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* used as the end of the range to indicate infinity.
3131
*
3232
* <p>An offset range is considered growable when the end offset could grow (or change) during
33-
* execution time (e.g., Kafka topic partition offset, appended file, ...).
33+
* execution time (e.g., appended file, ...).
3434
*
3535
* <p>The growable range is marked as done by claiming {@code Long.MAX_VALUE}.
3636
*/
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.transforms.splittabledofn;
19+
20+
import org.checkerframework.checker.nullness.qual.Nullable;
21+
22+
/**
23+
* A {@link RestrictionTracker} for wrapping a {@link RestrictionTracker} with unsplittable
24+
* restrictions.
25+
*
26+
* <p>A restriction is considered unsplittable when restrictions of an element must not be processed
27+
* simultaneously (e.g., Kafka topic partition).
28+
*/
29+
public class UnsplittableRestrictionTracker<RestrictionT, PositionT>
30+
extends RestrictionTracker<RestrictionT, PositionT> implements RestrictionTracker.HasProgress {
31+
private final RestrictionTracker<RestrictionT, PositionT> tracker;
32+
33+
public UnsplittableRestrictionTracker(RestrictionTracker<RestrictionT, PositionT> tracker) {
34+
this.tracker = tracker;
35+
}
36+
37+
@Override
38+
public boolean tryClaim(PositionT position) {
39+
return tracker.tryClaim(position);
40+
}
41+
42+
@Override
43+
public RestrictionT currentRestriction() {
44+
return tracker.currentRestriction();
45+
}
46+
47+
@Override
48+
public @Nullable SplitResult<RestrictionT> trySplit(double fractionOfRemainder) {
49+
return fractionOfRemainder > 0.0 && fractionOfRemainder < 1.0
50+
? null
51+
: tracker.trySplit(fractionOfRemainder);
52+
}
53+
54+
@Override
55+
public void checkDone() throws IllegalStateException {
56+
tracker.checkDone();
57+
}
58+
59+
@Override
60+
public IsBounded isBounded() {
61+
return tracker.isBounded();
62+
}
63+
64+
@Override
65+
public Progress getProgress() {
66+
return tracker instanceof RestrictionTracker.HasProgress
67+
? ((RestrictionTracker.HasProgress) tracker).getProgress()
68+
: Progress.NONE;
69+
}
70+
}

sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/ReadFromKafkaDoFn.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.apache.beam.sdk.transforms.splittabledofn.OffsetRangeTracker;
4646
import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker;
4747
import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker.HasProgress;
48+
import org.apache.beam.sdk.transforms.splittabledofn.UnsplittableRestrictionTracker;
4849
import org.apache.beam.sdk.transforms.splittabledofn.WatermarkEstimator;
4950
import org.apache.beam.sdk.transforms.splittabledofn.WatermarkEstimators.MonotonicallyIncreasing;
5051
import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
@@ -108,6 +109,15 @@
108109
*
109110
* <h4>Splitting</h4>
110111
*
112+
* <p>Consumer group members must not consume from the same {@link TopicPartition} simultaneously
113+
* when {@code enable.auto.commit} is set. Doing so may arbitrarily overwrite a consumer group's
114+
* committed offset for a {@link TopicPartition}. Restriction trackers for a {@link
115+
* KafkaSourceDescriptor} are wrapped as {@link UnsplittableRestrictionTracker<OffsetRange, Long>}
116+
* and will only return a non-null {@link org.apache.beam.sdk.transforms.splittabledofn.SplitResult}
117+
* for a checkpoint. To the extent possible in the SDK, this reduces the risk of overwriting
118+
* committed offsets when {@code enable.auto.commit} is set and prevents concurrent use of
119+
* per-{@TopicPartition} cached {@link Consumer} resources.
120+
*
111121
* <p>TODO(https://github.com/apache/beam/issues/20280): Add support for initial splitting.
112122
*
113123
* <h4>Checkpoint and Resume Processing</h4>
@@ -488,20 +498,21 @@ public double getSize(
488498

489499
@NewTracker
490500
@RequiresNonNull({"latestOffsetEstimatorCache"})
491-
public OffsetRangeTracker restrictionTracker(
501+
public UnsplittableRestrictionTracker<OffsetRange, Long> restrictionTracker(
492502
@Element KafkaSourceDescriptor kafkaSourceDescriptor, @Restriction OffsetRange restriction) {
493503
final LoadingCache<KafkaSourceDescriptor, KafkaLatestOffsetEstimator>
494504
latestOffsetEstimatorCache = this.latestOffsetEstimatorCache;
495505

496506
if (restriction.getTo() < Long.MAX_VALUE) {
497-
return new OffsetRangeTracker(restriction);
507+
return new UnsplittableRestrictionTracker<>(new OffsetRangeTracker(restriction));
498508
}
499509

500510
// OffsetEstimators are cached for each topic-partition because they hold a stateful connection,
501511
// so we want to minimize the amount of connections that we start and track with Kafka. Another
502512
// point is that it has a memoized backlog, and this should make that more reusable estimations.
503-
return new GrowableOffsetRangeTracker(
504-
restriction.getFrom(), latestOffsetEstimatorCache.getUnchecked(kafkaSourceDescriptor));
513+
return new UnsplittableRestrictionTracker<>(
514+
new GrowableOffsetRangeTracker(
515+
restriction.getFrom(), latestOffsetEstimatorCache.getUnchecked(kafkaSourceDescriptor)));
505516
}
506517

507518
@ProcessElement

sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/KafkaIOIT.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,37 @@ public void testKafkaWithDelayedStopReadingFunction() {
813813
runWithStopReadingFn(checkStopReadingFn, "delayed-stop-reading", sourceOptions.numRecords);
814814
}
815815

816+
@Test
817+
public void testKafkaWithStopReadTime() throws IOException {
818+
writePipeline
819+
.apply("Generate records", Read.from(new SyntheticBoundedSource(sourceOptions)))
820+
.apply("Measure write time", ParDo.of(new TimeMonitor<>(NAMESPACE, WRITE_TIME_METRIC_NAME)))
821+
.apply(
822+
"Write to Kafka",
823+
writeToKafka().withTopic(options.getKafkaTopic() + "-stop-read-time"));
824+
825+
PipelineResult writeResult = writePipeline.run();
826+
PipelineResult.State writeState = writeResult.waitUntilFinish();
827+
assertNotEquals(PipelineResult.State.FAILED, writeState);
828+
829+
sdfReadPipeline.getOptions().as(Options.class).setStreaming(false);
830+
PCollection<KafkaRecord<byte[], byte[]>> rows =
831+
sdfReadPipeline.apply(
832+
"Read from bounded Kafka",
833+
readFromKafka()
834+
.withTopic(options.getKafkaTopic() + "-stop-read-time")
835+
.withStopReadTime(
836+
org.joda.time.Instant.ofEpochMilli(
837+
new MetricsReader(writeResult, NAMESPACE)
838+
.getEndTimeMetric(WRITE_TIME_METRIC_NAME))));
839+
840+
PipelineResult readResult = sdfReadPipeline.run();
841+
PipelineResult.State readState =
842+
readResult.waitUntilFinish(Duration.standardSeconds(options.getReadTimeout()));
843+
cancelIfTimeouted(readResult, readState);
844+
assertNotEquals(PipelineResult.State.FAILED, readState);
845+
}
846+
816847
public static final Schema KAFKA_TOPIC_SCHEMA =
817848
Schema.builder()
818849
.addStringField("name")

0 commit comments

Comments
 (0)