Skip to content

Commit 7ab792f

Browse files
udayawuchathu
andauthored
[KafkaIO] Advance idle partition watermark without requiring a record (#40176)
Co-authored-by: Udaya Chathuranga <udaya.chathuranga@trivago.com>
1 parent a168361 commit 7ab792f

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,18 @@
3131
* watermark at any time is '({@code Min(now(), Max(event timestamp so far)) - max delay})'.
3232
* However, watermark is never set in future and capped to 'now - max delay'. In addition, watermark
3333
* advanced to 'now - max delay' when a partition is idle.
34+
*
35+
* <p>By default the idle advance requires at least one record to have been read, so a partition
36+
* which is caught up but has delivered nothing since the job started holds its watermark at {@link
37+
* BoundedWindow#TIMESTAMP_MIN_VALUE} until its first record arrives. See {@link
38+
* #CustomTimestampPolicyWithLimitedDelay(SerializableFunction, Duration, Optional, boolean)} to opt
39+
* out of that.
3440
*/
3541
public class CustomTimestampPolicyWithLimitedDelay<K, V> extends TimestampPolicy<K, V> {
3642

3743
private final Duration maxDelay;
3844
private final SerializableFunction<KafkaRecord<K, V>, Instant> timestampFunction;
45+
private final boolean advanceWatermarkBeforeFirstRecord;
3946
private Instant maxEventTimestamp;
4047

4148
/**
@@ -53,8 +60,28 @@ public CustomTimestampPolicyWithLimitedDelay(
5360
SerializableFunction<KafkaRecord<K, V>, Instant> timestampFunction,
5461
Duration maxDelay,
5562
Optional<Instant> previousWatermark) {
63+
this(timestampFunction, maxDelay, previousWatermark, false);
64+
}
65+
66+
/**
67+
* As {@link #CustomTimestampPolicyWithLimitedDelay(SerializableFunction, Duration, Optional)},
68+
* with control over whether an idle partition may advance its watermark before it has delivered
69+
* any record.
70+
*
71+
* @param advanceWatermarkBeforeFirstRecord When true, a partition with a backlog of zero advances
72+
* its watermark whether or not a record has been read. A zero backlog means the reader is at
73+
* the log end, so no unread record can arrive late. Leaving this false keeps such a partition
74+
* pinned at {@link BoundedWindow#TIMESTAMP_MIN_VALUE} until its first record arrives, which
75+
* holds back the watermark of every stage reading it.
76+
*/
77+
public CustomTimestampPolicyWithLimitedDelay(
78+
SerializableFunction<KafkaRecord<K, V>, Instant> timestampFunction,
79+
Duration maxDelay,
80+
Optional<Instant> previousWatermark,
81+
boolean advanceWatermarkBeforeFirstRecord) {
5682
this.maxDelay = maxDelay;
5783
this.timestampFunction = timestampFunction;
84+
this.advanceWatermarkBeforeFirstRecord = advanceWatermarkBeforeFirstRecord;
5885

5986
// 'previousWatermark' is not the same as maxEventTimestamp (e.g. it could have been in future).
6087
// Initialize it such that watermark before reading any event same as previousWatermark.
@@ -90,7 +117,8 @@ Instant getWatermark(PartitionContext ctx, Instant now) {
90117
return now.minus(maxDelay); // (a) above.
91118
} else if (ctx.getMessageBacklog() == 0
92119
&& ctx.getBacklogCheckTime().minus(maxDelay).isAfter(maxEventTimestamp) // Idle
93-
&& maxEventTimestamp.getMillis() > 0) { // Read at least one record with positive timestamp.
120+
// Read at least one record with positive timestamp, unless opted out of.
121+
&& (maxEventTimestamp.getMillis() > 0 || advanceWatermarkBeforeFirstRecord)) {
94122
return ctx.getBacklogCheckTime().minus(maxDelay);
95123
} else {
96124
return maxEventTimestamp.minus(maxDelay);

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,40 @@ record -> new Instant(record.getTimestamp()), maxDelay, Optional.empty());
115115

116116
assertThat(policy.getWatermark(ctx, now), is(backlogCheckTime.minus(maxDelay)));
117117
}
118+
119+
private static CustomTimestampPolicyWithLimitedDelay<String, String> policy(
120+
Duration maxDelay, boolean advanceWatermarkBeforeFirstRecord) {
121+
return new CustomTimestampPolicyWithLimitedDelay<>(
122+
record -> new Instant(record.getTimestamp()),
123+
maxDelay,
124+
Optional.empty(),
125+
advanceWatermarkBeforeFirstRecord);
126+
}
127+
128+
@Test
129+
public void testIdleWatermarkIsPinnedBeforeFirstRecordByDefault() {
130+
// Default behaviour: a caught up partition which has delivered nothing stays at the floor.
131+
Duration maxDelay = Duration.standardSeconds(60);
132+
CustomTimestampPolicyWithLimitedDelay<String, String> policy = policy(maxDelay, false);
133+
134+
Instant now = Instant.now();
135+
TimestampPolicy.PartitionContext ctx = mock(TimestampPolicy.PartitionContext.class);
136+
when(ctx.getMessageBacklog()).thenReturn(0L);
137+
when(ctx.getBacklogCheckTime()).thenReturn(now);
138+
139+
assertThat(policy.getWatermark(ctx, now), is(BoundedWindow.TIMESTAMP_MIN_VALUE));
140+
}
141+
142+
@Test
143+
public void testIdleWatermarkAdvancesBeforeFirstRecordWhenEnabled() {
144+
Duration maxDelay = Duration.standardSeconds(60);
145+
CustomTimestampPolicyWithLimitedDelay<String, String> policy = policy(maxDelay, true);
146+
147+
Instant now = Instant.now();
148+
TimestampPolicy.PartitionContext ctx = mock(TimestampPolicy.PartitionContext.class);
149+
when(ctx.getMessageBacklog()).thenReturn(0L);
150+
when(ctx.getBacklogCheckTime()).thenReturn(now);
151+
152+
assertThat(policy.getWatermark(ctx, now), is(now.minus(maxDelay)));
153+
}
118154
}

0 commit comments

Comments
 (0)