Skip to content

Commit ca07876

Browse files
authored
Merge pull request #25723: #25722 Add option to propagate successful storage-api writes
1 parent 2c075b3 commit ca07876

15 files changed

Lines changed: 434 additions & 83 deletions

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

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ public long getElementByteSize() {
329329

330330
@Override
331331
public PCollection<KV<K, Iterable<InputT>>> expand(PCollection<KV<K, InputT>> input) {
332-
332+
Duration allowedLateness = input.getWindowingStrategy().getAllowedLateness();
333333
checkArgument(
334334
input.getCoder() instanceof KvCoder,
335335
"coder specified in the input PCollection is not a KvCoder");
@@ -344,6 +344,7 @@ public PCollection<KV<K, Iterable<InputT>>> expand(PCollection<KV<K, InputT>> in
344344
params.getBatchSizeBytes(),
345345
weigher,
346346
params.getMaxBufferingDuration(),
347+
allowedLateness,
347348
valueCoder)));
348349
}
349350

@@ -357,12 +358,20 @@ private static class GroupIntoBatchesDoFn<K, InputT>
357358
@Nullable private final SerializableFunction<InputT, Long> weigher;
358359
private final Duration maxBufferingDuration;
359360

361+
private final Duration allowedLateness;
362+
360363
// The following timer is no longer set. We maintain the spec for update compatibility.
361364
private static final String END_OF_WINDOW_ID = "endOFWindow";
362365

363366
@TimerId(END_OF_WINDOW_ID)
364367
private final TimerSpec windowTimer = TimerSpecs.timer(TimeDomain.EVENT_TIME);
365368

369+
// This timer manages the watermark hold if there is no buffering timer.
370+
private static final String TIMER_HOLD_ID = "watermarkHold";
371+
372+
@TimerId(TIMER_HOLD_ID)
373+
private final TimerSpec holdTimerSpec = TimerSpecs.timer(TimeDomain.EVENT_TIME);
374+
366375
// This timer expires when it's time to batch and output the buffered data.
367376
private static final String END_OF_BUFFERING_ID = "endOfBuffering";
368377

@@ -410,11 +419,13 @@ private static class GroupIntoBatchesDoFn<K, InputT>
410419
long batchSizeBytes,
411420
@Nullable SerializableFunction<InputT, Long> weigher,
412421
Duration maxBufferingDuration,
422+
Duration allowedLateness,
413423
Coder<InputT> inputValueCoder) {
414424
this.batchSize = batchSize;
415425
this.batchSizeBytes = batchSizeBytes;
416426
this.weigher = weigher;
417427
this.maxBufferingDuration = maxBufferingDuration;
428+
this.allowedLateness = allowedLateness;
418429
this.batchSpec = StateSpecs.bag(inputValueCoder);
419430

420431
Combine.BinaryCombineLongFn sumCombineFn =
@@ -452,9 +463,18 @@ public long apply(long left, long right) {
452463
this.prefetchFrequency = ((batchSize / 5) <= 1) ? Long.MAX_VALUE : (batchSize / 5);
453464
}
454465

466+
@Override
467+
public Duration getAllowedTimestampSkew() {
468+
// This is required since flush is sometimes called from processElement. This is safe because
469+
// a watermark hold
470+
// will always be set using timer.withOutputTimestamp.
471+
return Duration.millis(Long.MAX_VALUE);
472+
}
473+
455474
@ProcessElement
456475
public void processElement(
457476
@TimerId(END_OF_BUFFERING_ID) Timer bufferingTimer,
477+
@TimerId(TIMER_HOLD_ID) Timer holdTimer,
458478
@StateId(BATCH_ID) BagState<InputT> batch,
459479
@StateId(NUM_ELEMENTS_IN_BATCH_ID) CombiningState<Long, long[], Long> storedBatchSize,
460480
@StateId(NUM_BYTES_IN_BATCH_ID) CombiningState<Long, long[], Long> storedBatchSizeBytes,
@@ -473,9 +493,10 @@ public void processElement(
473493
storedBatchSizeBytes.readLater();
474494
}
475495
storedBatchSize.readLater();
476-
if (shouldCareAboutMaxBufferingDuration) {
477-
minBufferedTs.readLater();
478-
}
496+
minBufferedTs.readLater();
497+
498+
// Make sure we always include the current timestamp in the minBufferedTs.
499+
minBufferedTs.add(elementTs.getMillis());
479500

480501
LOG.debug("*** BATCH *** Add element for window {} ", window);
481502
if (shouldCareAboutWeight) {
@@ -505,23 +526,26 @@ public void processElement(
505526
timerTs,
506527
minBufferedTs);
507528
bufferingTimer.clear();
529+
holdTimer.clear();
508530
}
509531
storedBatchSizeBytes.add(elementWeight);
510532
}
511533
batch.add(element.getValue());
512534
// Blind add is supported with combiningState
513535
storedBatchSize.add(1L);
536+
// Add the timestamp back into minBufferedTs as it might be cleared by flushBatch above.
537+
minBufferedTs.add(elementTs.getMillis());
514538

515539
final long num = storedBatchSize.read();
516-
if (shouldCareAboutMaxBufferingDuration) {
517-
long oldOutputTs =
518-
MoreObjects.firstNonNull(
519-
minBufferedTs.read(), BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis());
520-
minBufferedTs.add(elementTs.getMillis());
521-
// If this is the first element in the batch or if the timer's output timestamp needs
522-
// modifying, then set a
523-
// timer.
524-
if (num == 1 || minBufferedTs.read() != oldOutputTs) {
540+
541+
// If this is the first element in the batch or if the timer's output timestamp needs
542+
// modifying, then set a timer.
543+
long oldOutputTs =
544+
MoreObjects.firstNonNull(
545+
minBufferedTs.read(), BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis());
546+
boolean needsNewTimer = num == 1 || minBufferedTs.read() != oldOutputTs;
547+
if (needsNewTimer) {
548+
if (shouldCareAboutMaxBufferingDuration) {
525549
long targetTs =
526550
MoreObjects.firstNonNull(
527551
timerTs.read(),
@@ -530,6 +554,12 @@ public void processElement(
530554
bufferingTimer
531555
.withOutputTimestamp(Instant.ofEpochMilli(minBufferedTs.read()))
532556
.set(Instant.ofEpochMilli(targetTs));
557+
} else {
558+
// The only way to hold the watermark is to set a timer. Since there is no buffering
559+
// timer, we set a dummy
560+
// timer at the end of the window to manage the hold.
561+
Instant windowEnd = window.maxTimestamp().plus(allowedLateness);
562+
holdTimer.withOutputTimestamp(Instant.ofEpochMilli(minBufferedTs.read())).set(windowEnd);
533563
}
534564
}
535565

@@ -585,6 +615,11 @@ public void onWindowExpiration(
585615
receiver, key, batch, storedBatchSize, storedBatchSizeBytes, timerTs, minBufferedTs);
586616
}
587617

618+
@OnTimer(TIMER_HOLD_ID)
619+
public void onHoldTimer() {
620+
// Do nothing. The associated watermark hold will be automatically removed.
621+
}
622+
588623
// We no longer set this timer, since OnWindowExpiration takes care of his. However we leave the
589624
// callback in place
590625
// for existing jobs that have already set these timers.
@@ -618,7 +653,8 @@ private void flushBatch(
618653
Iterable<InputT> values = batch.read();
619654
// When the timer fires, batch state might be empty
620655
if (!Iterables.isEmpty(values)) {
621-
receiver.output(KV.of(key, values));
656+
receiver.outputWithTimestamp(
657+
KV.of(key, values), Instant.ofEpochMilli(minBufferedTs.read()));
622658
}
623659
clearState(batch, storedBatchSize, storedBatchSizeBytes, timerTs, minBufferedTs);
624660
}

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BatchLoads.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,8 @@ private WriteResult writeResult(Pipeline p, PCollection<TableDestination> succes
865865
new TupleTag<>("successfulInserts"),
866866
successfulWrites,
867867
null,
868+
null,
869+
null,
868870
null);
869871
}
870872

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIO.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2094,6 +2094,7 @@ public static <T> Write<T> write() {
20942094
.setAutoSchemaUpdate(false)
20952095
.setDeterministicRecordIdFn(null)
20962096
.setMaxRetryJobs(1000)
2097+
.setPropagateSuccessfulStorageApiWrites(false)
20972098
.build();
20982099
}
20992100

@@ -2211,6 +2212,8 @@ public enum Method {
22112212

22122213
abstract int getNumStorageWriteApiStreams();
22132214

2215+
abstract boolean getPropagateSuccessfulStorageApiWrites();
2216+
22142217
abstract int getMaxFilesPerPartition();
22152218

22162219
abstract long getMaxBytesPerPartition();
@@ -2306,6 +2309,9 @@ abstract Builder<T> setAvroSchemaFactory(
23062309

23072310
abstract Builder<T> setNumStorageWriteApiStreams(int numStorageApiStreams);
23082311

2312+
abstract Builder<T> setPropagateSuccessfulStorageApiWrites(
2313+
boolean propagateSuccessfulStorageApiWrites);
2314+
23092315
abstract Builder<T> setMaxFilesPerPartition(int maxFilesPerPartition);
23102316

23112317
abstract Builder<T> setMaxBytesPerPartition(long maxBytesPerPartition);
@@ -2763,6 +2769,17 @@ public Write<T> withNumStorageWriteApiStreams(int numStorageWriteApiStreams) {
27632769
return toBuilder().setNumStorageWriteApiStreams(numStorageWriteApiStreams).build();
27642770
}
27652771

2772+
/**
2773+
* If set to true, then all successful writes will be propagated to {@link WriteResult} and
2774+
* accessible via the {@link WriteResult#getSuccessfulStorageApiInserts} method.
2775+
*/
2776+
public Write<T> withPropagateSuccessfulStorageApiWrites(
2777+
boolean propagateSuccessfulStorageApiWrites) {
2778+
return toBuilder()
2779+
.setPropagateSuccessfulStorageApiWrites(propagateSuccessfulStorageApiWrites)
2780+
.build();
2781+
}
2782+
27662783
/**
27672784
* Provides a custom location on GCS for storing temporary files to be loaded via BigQuery batch
27682785
* load jobs. See "Usage with templates" in {@link BigQueryIO} documentation for discussion.
@@ -3270,6 +3287,9 @@ private <DestinationT> WriteResult continueExpandTyped(
32703287
checkArgument(
32713288
getSchemaUpdateOptions() == null || getSchemaUpdateOptions().isEmpty(),
32723289
"SchemaUpdateOptions are not supported when method == STREAMING_INSERTS");
3290+
checkArgument(
3291+
!getPropagateSuccessfulStorageApiWrites(),
3292+
"withPropagateSuccessfulStorageApiWrites only supported when using storage api writes.");
32733293

32743294
RowWriterFactory.TableRowWriterFactory<T, DestinationT> tableRowWriterFactory =
32753295
(RowWriterFactory.TableRowWriterFactory<T, DestinationT>) rowWriterFactory;
@@ -3301,6 +3321,9 @@ private <DestinationT> WriteResult continueExpandTyped(
33013321
rowWriterFactory.getOutputType() == OutputType.AvroGenericRecord,
33023322
"useAvroLogicalTypes can only be set with Avro output.");
33033323
}
3324+
checkArgument(
3325+
!getPropagateSuccessfulStorageApiWrites(),
3326+
"withPropagateSuccessfulStorageApiWrites only supported when using storage api writes.");
33043327

33053328
// Batch load jobs currently support JSON data insertion only with CSV files
33063329
if (getJsonSchema() != null && getJsonSchema().isAccessible()) {
@@ -3406,7 +3429,8 @@ private <DestinationT> WriteResult continueExpandTyped(
34063429
method == Method.STORAGE_API_AT_LEAST_ONCE,
34073430
getAutoSharding(),
34083431
getAutoSchemaUpdate(),
3409-
getIgnoreUnknownValues());
3432+
getIgnoreUnknownValues(),
3433+
getPropagateSuccessfulStorageApiWrites());
34103434
return input.apply("StorageApiLoads", storageApiLoads);
34113435
} else {
34123436
throw new RuntimeException("Unexpected write method " + method);

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/SplittingIterable.java

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,32 @@
1818
package org.apache.beam.sdk.io.gcp.bigquery;
1919

2020
import com.google.api.services.bigquery.model.TableRow;
21+
import com.google.auto.value.AutoValue;
2122
import com.google.cloud.bigquery.storage.v1.ProtoRows;
2223
import com.google.protobuf.ByteString;
2324
import java.util.Iterator;
25+
import java.util.List;
2426
import java.util.NoSuchElementException;
2527
import java.util.function.BiConsumer;
2628
import java.util.function.Function;
27-
import javax.annotation.Nullable;
29+
import org.apache.beam.sdk.values.TimestampedValue;
30+
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Lists;
31+
import org.checkerframework.checker.nullness.qual.Nullable;
32+
import org.joda.time.Instant;
2833

2934
/**
3035
* Takes in an iterable and batches the results into multiple ProtoRows objects. The splitSize
3136
* parameter controls how many rows are batched into a single ProtoRows object before we move on to
3237
* the next one.
3338
*/
34-
class SplittingIterable implements Iterable<ProtoRows> {
39+
class SplittingIterable implements Iterable<SplittingIterable.Value> {
40+
@AutoValue
41+
abstract static class Value {
42+
abstract ProtoRows getProtoRows();
43+
44+
abstract List<Instant> getTimestamps();
45+
}
46+
3547
interface ConvertUnknownFields {
3648
ByteString convert(TableRow tableRow, boolean ignoreUnknownValues)
3749
throws TableRowToStorageApiProto.SchemaConversionException;
@@ -42,30 +54,34 @@ ByteString convert(TableRow tableRow, boolean ignoreUnknownValues)
4254

4355
private final ConvertUnknownFields unknownFieldsToMessage;
4456
private final Function<ByteString, TableRow> protoToTableRow;
45-
private final BiConsumer<TableRow, String> failedRowsConsumer;
57+
private final BiConsumer<TimestampedValue<TableRow>, String> failedRowsConsumer;
4658
private final boolean autoUpdateSchema;
4759
private final boolean ignoreUnknownValues;
4860

61+
private final Instant elementsTimestamp;
62+
4963
public SplittingIterable(
5064
Iterable<StorageApiWritePayload> underlying,
5165
long splitSize,
5266
ConvertUnknownFields unknownFieldsToMessage,
5367
Function<ByteString, TableRow> protoToTableRow,
54-
BiConsumer<TableRow, String> failedRowsConsumer,
68+
BiConsumer<TimestampedValue<TableRow>, String> failedRowsConsumer,
5569
boolean autoUpdateSchema,
56-
boolean ignoreUnknownValues) {
70+
boolean ignoreUnknownValues,
71+
Instant elementsTimestamp) {
5772
this.underlying = underlying;
5873
this.splitSize = splitSize;
5974
this.unknownFieldsToMessage = unknownFieldsToMessage;
6075
this.protoToTableRow = protoToTableRow;
6176
this.failedRowsConsumer = failedRowsConsumer;
6277
this.autoUpdateSchema = autoUpdateSchema;
6378
this.ignoreUnknownValues = ignoreUnknownValues;
79+
this.elementsTimestamp = elementsTimestamp;
6480
}
6581

6682
@Override
67-
public Iterator<ProtoRows> iterator() {
68-
return new Iterator<ProtoRows>() {
83+
public Iterator<Value> iterator() {
84+
return new Iterator<Value>() {
6985
final Iterator<StorageApiWritePayload> underlyingIterator = underlying.iterator();
7086

7187
@Override
@@ -74,11 +90,12 @@ public boolean hasNext() {
7490
}
7591

7692
@Override
77-
public ProtoRows next() {
93+
public Value next() {
7894
if (!hasNext()) {
7995
throw new NoSuchElementException();
8096
}
8197

98+
List<Instant> timestamps = Lists.newArrayList();
8299
ProtoRows.Builder inserts = ProtoRows.newBuilder();
83100
long bytesSize = 0;
84101
while (underlyingIterator.hasNext()) {
@@ -107,7 +124,11 @@ public ProtoRows next() {
107124
// 24926 is fixed, we need to merge the unknownFields back into the main row
108125
// before outputting to the
109126
// failed-rows consumer.
110-
failedRowsConsumer.accept(tableRow, e.toString());
127+
Instant timestamp = payload.getTimestamp();
128+
if (timestamp == null) {
129+
timestamp = elementsTimestamp;
130+
}
131+
failedRowsConsumer.accept(TimestampedValue.of(tableRow, timestamp), e.toString());
111132
continue;
112133
}
113134
}
@@ -116,12 +137,17 @@ public ProtoRows next() {
116137
}
117138
}
118139
inserts.addSerializedRows(byteString);
140+
Instant timestamp = payload.getTimestamp();
141+
if (timestamp == null) {
142+
timestamp = elementsTimestamp;
143+
}
144+
timestamps.add(timestamp);
119145
bytesSize += byteString.size();
120146
if (bytesSize > splitSize) {
121147
break;
122148
}
123149
}
124-
return inserts.build();
150+
return new AutoValue_SplittingIterable_Value(inserts.build(), timestamps);
125151
}
126152
};
127153
}

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiConvertMessages.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.apache.beam.sdk.values.TupleTagList;
3434
import org.checkerframework.checker.nullness.qual.NonNull;
3535
import org.checkerframework.checker.nullness.qual.Nullable;
36+
import org.joda.time.Instant;
3637

3738
/**
3839
* A transform that converts messages to protocol buffers in preparation for writing to BigQuery.
@@ -129,14 +130,16 @@ public void processElement(
129130
ProcessContext c,
130131
PipelineOptions pipelineOptions,
131132
@Element KV<DestinationT, ElementT> element,
133+
@Timestamp Instant timestamp,
132134
MultiOutputReceiver o)
133135
throws Exception {
134136
dynamicDestinations.setSideInputAccessorFromProcessContext(c);
135137
MessageConverter<ElementT> messageConverter =
136138
messageConverters.get(
137139
element.getKey(), dynamicDestinations, getDatasetService(pipelineOptions));
138140
try {
139-
StorageApiWritePayload payload = messageConverter.toMessage(element.getValue());
141+
StorageApiWritePayload payload =
142+
messageConverter.toMessage(element.getValue()).withTimestamp(timestamp);
140143
o.get(successfulWritesTag).output(KV.of(element.getKey(), payload));
141144
} catch (TableRowToStorageApiProto.SchemaConversionException e) {
142145
TableRow tableRow = messageConverter.toTableRow(element.getValue());

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDynamicDestinationsBeamRow.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ public TableSchema getTableSchema() {
6262

6363
@Override
6464
@SuppressWarnings("nullness")
65-
public StorageApiWritePayload toMessage(T element) {
65+
public StorageApiWritePayload toMessage(T element) throws Exception {
6666
Message msg = BeamRowToStorageApiProto.messageFromBeamRow(descriptor, toRow.apply(element));
67-
return new AutoValue_StorageApiWritePayload(msg.toByteArray(), null);
67+
return StorageApiWritePayload.of(msg.toByteArray(), null);
6868
}
6969

7070
@Override

0 commit comments

Comments
 (0)