Skip to content

Commit a15ef42

Browse files
committed
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 447899e commit a15ef42

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

@@ -395,6 +414,7 @@ public int hashCode() {
395414
private static class UnboundedCountingReader extends UnboundedReader<Long> {
396415
private UnboundedCountingSource source;
397416
private long current;
417+
private boolean done = false;
398418

399419
// Initialized on first advance()
400420
private @Nullable Instant currentTimestamp;
@@ -431,6 +451,10 @@ public boolean advance() throws IOException {
431451
return false;
432452
}
433453
long nextValue = current + source.stride;
454+
if (nextValue >= source.end) {
455+
done = true;
456+
return false;
457+
}
434458
if (expectedValue() < nextValue) {
435459
return false;
436460
}
@@ -453,7 +477,7 @@ private long expectedValue() {
453477

454478
@Override
455479
public Instant getWatermark() {
456-
return source.timestampFn.apply(current);
480+
return done ? BoundedWindow.TIMESTAMP_MAX_VALUE : 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
@@ -315,4 +315,40 @@ public void testUnboundedSourceCheckpointMark() throws Exception {
315315
assertEquals(numToSkip + 1, (long) reader.getCurrent());
316316
assertEquals(numToSkip + 1, reader.getCurrentTimestamp().getMillis());
317317
}
318+
319+
@Test
320+
@Category(NeedsRunner.class)
321+
public void testUnboundedSourceWithFromAndTo() {
322+
long start = 5;
323+
long end = 10;
324+
PCollection<Long> input = p.apply(Read.from(CountingSource.createUnboundedFrom(start).to(end)));
325+
326+
PAssert.that(input).containsInAnyOrder(5L, 6L, 7L, 8L, 9L);
327+
p.run();
328+
}
329+
330+
@Test
331+
@Category(NeedsRunner.class)
332+
public void testUnboundedSourceWithFromAndToAndRate() {
333+
long start = 5;
334+
long end = 15;
335+
long elementsPerPeriod = 2;
336+
Duration period = Duration.millis(10);
337+
PCollection<Long> input =
338+
p.apply(
339+
Read.from(
340+
CountingSource.createUnboundedFrom(start)
341+
.to(end)
342+
.withRate(elementsPerPeriod, period)));
343+
344+
PAssert.that(input).containsInAnyOrder(5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L);
345+
346+
Instant startTime = Instant.now();
347+
p.run();
348+
Instant endTime = Instant.now();
349+
350+
long expectedMinimumMillis = ((end - start) * period.getMillis()) / elementsPerPeriod;
351+
org.junit.Assert.assertFalse(
352+
endTime.isBefore(startTime.plus(Duration.millis(expectedMinimumMillis))));
353+
}
318354
}

0 commit comments

Comments
 (0)