Skip to content

Commit 0165344

Browse files
authored
Add UnboundedCountingSource::to for bounded reads from an UnboundedCountingSource (#39084)
* Add UnboundedCountingSource::to for bounded reads from an UnboundedCountingSource UnboundedCountingSource currently does not support configuring an end limit, this PR adds that capability. GenerateSequence with a from + to + rate boils down to a BoundedReadFromUnboundedSource. BoundedReadFromUnboundedSource splits emit all outputs from a single bundle that does not work well on Streaming jobs. UnboundedCountingSource::to is currently not exposed outside CountingSource. Eventually GenerateSequence can use UnboundedCountingSource::to instead of BoundedReadFromUnboundedSource
1 parent f7f0e16 commit 0165344

2 files changed

Lines changed: 68 additions & 8 deletions

File tree

sdks/java/core/src/main/java/org/apache/beam/sdk/io/CountingSource.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.apache.beam.sdk.metrics.SourceMetrics;
4040
import org.apache.beam.sdk.options.PipelineOptions;
4141
import org.apache.beam.sdk.transforms.SerializableFunction;
42+
import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
4243
import org.apache.beam.sdk.values.PCollection;
4344
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
4445
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -95,7 +96,8 @@ static BoundedSource<Long> createSourceForSubrange(long startIndex, long endInde
9596
/** Create a new {@link UnboundedCountingSource}. */
9697
// package-private to return a typed UnboundedCountingSource rather than the UnboundedSource type.
9798
static UnboundedCountingSource createUnboundedFrom(long start) {
98-
return new UnboundedCountingSource(start, 1, 1L, Duration.ZERO, new NowTimestampFn());
99+
return new UnboundedCountingSource(
100+
start, 1, Long.MAX_VALUE, 1L, Duration.ZERO, new NowTimestampFn());
99101
}
100102

101103
/**
@@ -130,7 +132,7 @@ public static UnboundedSource<Long, CounterMark> unbounded() {
130132
@Deprecated
131133
public static UnboundedSource<Long, CounterMark> unboundedWithTimestampFn(
132134
SerializableFunction<Long, Instant> timestampFn) {
133-
return new UnboundedCountingSource(0, 1, 1L, Duration.ZERO, timestampFn);
135+
return new UnboundedCountingSource(0, 1, Long.MAX_VALUE, 1L, Duration.ZERO, timestampFn);
134136
}
135137

136138
/////////////////////////////////////////////////////////////////////////////////////////////
@@ -267,11 +269,13 @@ public void close() throws IOException {}
267269
}
268270

269271
/** An implementation of {@link CountingSource} that produces an unbounded {@link PCollection}. */
270-
static class UnboundedCountingSource extends UnboundedSource<Long, CounterMark> {
272+
public static class UnboundedCountingSource extends UnboundedSource<Long, CounterMark> {
271273
/** The first number (>= 0) generated by this {@link UnboundedCountingSource}. */
272274
private final long start;
273275
/** The interval between numbers generated by this {@link UnboundedCountingSource}. */
274276
private final long stride;
277+
/** The exclusive limit for the sequence. */
278+
private final long end;
275279
/** The number of elements to produce each period. */
276280
private final long elementsPerPeriod;
277281
/** The time between producing numbers from this {@link UnboundedCountingSource}. */
@@ -291,11 +295,13 @@ static class UnboundedCountingSource extends UnboundedSource<Long, CounterMark>
291295
private UnboundedCountingSource(
292296
long start,
293297
long stride,
298+
long end,
294299
long elementsPerPeriod,
295300
Duration period,
296301
SerializableFunction<Long, Instant> timestampFn) {
297302
this.start = start;
298303
this.stride = stride;
304+
this.end = end;
299305
checkArgument(
300306
elementsPerPeriod > 0L,
301307
"Must produce at least one element per period, got %s",
@@ -312,7 +318,8 @@ private UnboundedCountingSource(
312318
* will be produced with an interval between them equal to the period.
313319
*/
314320
public UnboundedCountingSource withRate(long elementsPerPeriod, Duration period) {
315-
return new UnboundedCountingSource(start, stride, elementsPerPeriod, period, timestampFn);
321+
return new UnboundedCountingSource(
322+
start, stride, end, elementsPerPeriod, period, timestampFn);
316323
}
317324

318325
/**
@@ -324,7 +331,18 @@ public UnboundedCountingSource withRate(long elementsPerPeriod, Duration period)
324331
public UnboundedCountingSource withTimestampFn(
325332
SerializableFunction<Long, Instant> timestampFn) {
326333
checkNotNull(timestampFn);
327-
return new UnboundedCountingSource(start, stride, elementsPerPeriod, period, timestampFn);
334+
return new UnboundedCountingSource(
335+
start, stride, end, elementsPerPeriod, period, timestampFn);
336+
}
337+
338+
/**
339+
* Returns an {@link UnboundedCountingSource} like this one but with the specified exclusive
340+
* limit.
341+
*/
342+
public UnboundedCountingSource to(long end) {
343+
checkArgument(end >= start, "end (%s) must be >= start (%s)", end, start);
344+
return new UnboundedCountingSource(
345+
start, stride, end, elementsPerPeriod, period, timestampFn);
328346
}
329347

330348
/**
@@ -348,7 +366,7 @@ public List<? extends UnboundedSource<Long, CountingSource.CounterMark>> split(
348366
// 0, 2, and 4.
349367
splits.add(
350368
new UnboundedCountingSource(
351-
start + i * stride, newStride, elementsPerPeriod, period, timestampFn));
369+
start + i * stride, newStride, end, elementsPerPeriod, period, timestampFn));
352370
}
353371
return splits.build();
354372
}
@@ -376,14 +394,15 @@ public boolean equals(@Nullable Object other) {
376394
UnboundedCountingSource that = (UnboundedCountingSource) other;
377395
return this.start == that.start
378396
&& this.stride == that.stride
397+
&& this.end == that.end
379398
&& this.elementsPerPeriod == that.elementsPerPeriod
380399
&& Objects.equals(this.period, that.period)
381400
&& Objects.equals(this.timestampFn, that.timestampFn);
382401
}
383402

384403
@Override
385404
public int hashCode() {
386-
return Objects.hash(start, stride, elementsPerPeriod, period, timestampFn);
405+
return Objects.hash(start, stride, end, elementsPerPeriod, period, timestampFn);
387406
}
388407
}
389408

@@ -431,6 +450,9 @@ public boolean advance() throws IOException {
431450
return false;
432451
}
433452
long nextValue = current + source.stride;
453+
if (nextValue >= source.end) {
454+
return false;
455+
}
434456
if (expectedValue() < nextValue) {
435457
return false;
436458
}
@@ -453,7 +475,9 @@ private long expectedValue() {
453475

454476
@Override
455477
public Instant getWatermark() {
456-
return source.timestampFn.apply(current);
478+
return (current >= source.end - source.stride)
479+
? BoundedWindow.TIMESTAMP_MAX_VALUE
480+
: source.timestampFn.apply(current);
457481
}
458482

459483
@Override

sdks/java/core/src/test/java/org/apache/beam/sdk/io/CountingSourceTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static org.hamcrest.Matchers.is;
2222
import static org.hamcrest.Matchers.lessThan;
2323
import static org.junit.Assert.assertEquals;
24+
import static org.junit.Assert.assertFalse;
2425
import static org.junit.Assert.assertTrue;
2526

2627
import java.io.IOException;
@@ -315,4 +316,39 @@ public void testUnboundedSourceCheckpointMark() throws Exception {
315316
assertEquals(numToSkip + 1, (long) reader.getCurrent());
316317
assertEquals(numToSkip + 1, reader.getCurrentTimestamp().getMillis());
317318
}
319+
320+
@Test
321+
@Category(NeedsRunner.class)
322+
public void testUnboundedSourceWithFromAndTo() {
323+
long start = 5;
324+
long end = 10;
325+
PCollection<Long> input = p.apply(Read.from(CountingSource.createUnboundedFrom(start).to(end)));
326+
327+
PAssert.that(input).containsInAnyOrder(5L, 6L, 7L, 8L, 9L);
328+
p.run();
329+
}
330+
331+
@Test
332+
@Category(NeedsRunner.class)
333+
public void testUnboundedSourceWithFromAndToAndRate() {
334+
long start = 5;
335+
long end = 15;
336+
long elementsPerPeriod = 2;
337+
Duration period = Duration.millis(10);
338+
PCollection<Long> input =
339+
p.apply(
340+
Read.from(
341+
CountingSource.createUnboundedFrom(start)
342+
.to(end)
343+
.withRate(elementsPerPeriod, period)));
344+
345+
PAssert.that(input).containsInAnyOrder(5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L);
346+
347+
Instant startTime = Instant.now();
348+
p.run();
349+
Instant endTime = Instant.now();
350+
351+
long expectedMinimumMillis = ((end - start) * period.getMillis()) / elementsPerPeriod;
352+
assertFalse(endTime.isBefore(startTime.plus(Duration.millis(expectedMinimumMillis))));
353+
}
318354
}

0 commit comments

Comments
 (0)