Skip to content

Allow strongly-consistent schema updates in BigQueryIO#38854

Open
reuvenlax wants to merge 10 commits into
apache:masterfrom
reuvenlax:schema_update_handle_unknown_fields
Open

Allow strongly-consistent schema updates in BigQueryIO#38854
reuvenlax wants to merge 10 commits into
apache:masterfrom
reuvenlax:schema_update_handle_unknown_fields

Conversation

@reuvenlax

Copy link
Copy Markdown
Contributor

No description provided.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances BigQueryIO's schema update capabilities by introducing a strongly-consistent mode for Storage API writes. This ensures that data integrity is maintained during schema evolution by preventing the ingestion of rows with new schemas until the updated table schema is detected. The changes include new configuration options for timeouts, improved schema change detection logic, and internal refactoring to support more robust error handling and row splitting during schema transitions.

Highlights

  • Strongly-consistent schema updates: Introduced a new 'withAutoSchemaUpdateConsistent' method in BigQueryIO to allow for strongly-consistent schema updates, ensuring rows with new schemas are not sent to BigQuery until the updated schema is observed.
  • Schema change detection: Added 'SchemaChangeDetectorHelper' to centralize logic for detecting and merging schema updates during BigQuery Storage API writes.
  • Timeout support: Added support for a strict timeout parameter when waiting for schema updates, after which rows are routed to the failed-rows collection.
  • SplittingIterable refactoring: Refactored 'SplittingIterable' to handle schema mismatches using a BitSet, allowing for more granular control over row processing during schema transitions.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@reuvenlax

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a consistent auto-schema update mechanism for BigQuery Storage API writes, allowing users to specify a timeout for waiting for schema updates. However, there are several critical compilation issues in the current implementation. These include a stray 'T' character in StorageApiDynamicDestinationsTableRow.java, a missing semicolon in StorageApiWritesShardedRecords.java, a call to a non-existent method bufferMismatchedRows, and calls to updateSchemaFromTable() on the MessageConverter interface where it is not defined.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

public void updateSchemaFromTable() throws IOException, InterruptedException {
SCHEMA_CACHE.refreshSchema(
delegate.get().tableReference, datasetService, writeStreamService, bigQueryOptions);
T

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a stray 'T' on line 154, which is a syntax error and will cause a compilation failure. This line should be removed.

org.joda.time.Instant targetExpirationTime = startMismatchTime.plus(autoUpdateSchemaStrictTimeout);
Iterable<SplittingIterable.Value> mismatchedRows =
Iterables.transform(messages, SplittingIterable.Value::getSchemaMismatchedRowsOnly);
bufferMismatchedRows(mismatchedRows, targetExpirationTime)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a missing semicolon at the end of the bufferMismatchedRows call, which will cause a compilation failure.

Suggested change
bufferMismatchedRows(mismatchedRows, targetExpirationTime)
bufferMismatchedRows(mismatchedRows, targetExpirationTime);

// Start off with the base schema. As we get notified of schema updates, we
// will update the descriptor.
if (autoUpdateSchemaStrictTimeout != null) {
messageConverter.updateSchemaFromTable();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The method updateSchemaFromTable() is called on messageConverter at lines 864 and 1116, but this method is not defined in the StorageApiDynamicDestinations.MessageConverter interface. This will cause a compilation failure. To resolve this, please add updateSchemaFromTable() to the MessageConverter interface in StorageApiDynamicDestinations.java and provide appropriate implementations in all classes implementing this interface.

org.joda.time.Instant targetExpirationTime = startMismatchTime.plus(autoUpdateSchemaStrictTimeout);
Iterable<SplittingIterable.Value> mismatchedRows =
Iterables.transform(messages, SplittingIterable.Value::getSchemaMismatchedRowsOnly);
bufferMismatchedRows(mismatchedRows, targetExpirationTime)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The method bufferMismatchedRows is called here, but its definition is missing from this class and the rest of the PR. Please ensure that this method is implemented and included in the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a strict timeout option for automatic schema updates in BigQueryIO (autoSchemaUpdateStrictTimeout), adding a new SchemaChangeDetectorHelper and updating several Storage API writing classes to support local retries and buffering of mismatched rows. The review feedback highlights several critical compilation issues that must be addressed, including a stray T character in StorageApiDynamicDestinationsTableRow.java, a missing semicolon and an undefined method bufferMismatchedRows in StorageApiWritesShardedRecords.java, and a missing interface method updateSchemaFromTable in StorageApiDynamicDestinations.java. Additionally, the reviewer recommended removing redundant loop conditions in SplittingIterable.java and simplifying a fully-qualified Preconditions call.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 153 to 155
delegate.get().tableReference, datasetService, writeStreamService, bigQueryOptions);
T
// Recycle the internal MessageConverter so that we pick up the new schema from the cache.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a stray T character on line 154 which will cause a compilation failure. It should be removed.

Suggested change
delegate.get().tableReference, datasetService, writeStreamService, bigQueryOptions);
T
// Recycle the internal MessageConverter so that we pick up the new schema from the cache.
delegate.get().tableReference, datasetService, writeStreamService, bigQueryOptions);
// Recycle the internal MessageConverter so that we pick up the new schema from the cache.

org.joda.time.Instant targetExpirationTime = startMismatchTime.plus(autoUpdateSchemaStrictTimeout);
Iterable<SplittingIterable.Value> mismatchedRows =
Iterables.transform(messages, SplittingIterable.Value::getSchemaMismatchedRowsOnly);
bufferMismatchedRows(mismatchedRows, targetExpirationTime)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a missing semicolon at the end of this statement. Additionally, the method bufferMismatchedRows is called here but is not defined anywhere in this class or the PR changes, which will cause a compilation failure.

Suggested change
bufferMismatchedRows(mismatchedRows, targetExpirationTime)
bufferMismatchedRows(mismatchedRows, targetExpirationTime);

Comment on lines +33 to 35
TableSchema getTableSchema();

DescriptorProtos.DescriptorProto getDescriptor(boolean includeCdcColumns) throws Exception;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The MessageConverter interface does not define updateSchemaFromTable(), but it is called on messageConverter in StorageApiWritesShardedRecords.java. Adding a default no-op implementation to the interface will prevent compilation failures for other implementations.

    TableSchema getTableSchema();

    DescriptorProtos.DescriptorProto getDescriptor(boolean includeCdcColumns) throws Exception;

    default void updateSchemaFromTable() throws IOException, InterruptedException {}

Comment on lines +64 to +70
for (int i = 0; ! getSchemaMismatchedRows().isEmpty() && i < getProtoRows().getSerializedRowsCount(); i++) {
if (getSchemaMismatchedRows().get(i)) {
inserts.addSerializedRows(getProtoRows().getSerializedRows(i));
timestamps.add(getTimestamps().get(i));
failsafeTableRows.add(getFailsafeTableRows().get(i));
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The loop condition contains ! getSchemaMismatchedRows().isEmpty() && which is redundant and evaluated on every iteration. It can be safely removed.

Suggested change
for (int i = 0; ! getSchemaMismatchedRows().isEmpty() && i < getProtoRows().getSerializedRowsCount(); i++) {
if (getSchemaMismatchedRows().get(i)) {
inserts.addSerializedRows(getProtoRows().getSerializedRows(i));
timestamps.add(getTimestamps().get(i));
failsafeTableRows.add(getFailsafeTableRows().get(i));
}
}
for (int i = 0; i < getProtoRows().getSerializedRowsCount(); i++) {
if (getSchemaMismatchedRows().get(i)) {
inserts.addSerializedRows(getProtoRows().getSerializedRows(i));
timestamps.add(getTimestamps().get(i));
failsafeTableRows.add(getFailsafeTableRows().get(i));
}
}

Comment on lines +84 to +90
for (int i = 0; ! getSchemaMismatchedRows().isEmpty() && i < getProtoRows().getSerializedRowsCount(); i++) {
if (!getSchemaMismatchedRows().get(i)) {
inserts.addSerializedRows(getProtoRows().getSerializedRows(i));
timestamps.add(getTimestamps().get(i));
failsafeTableRows.add(getFailsafeTableRows().get(i));
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The loop condition contains ! getSchemaMismatchedRows().isEmpty() && which is redundant and evaluated on every iteration. It can be safely removed.

Suggested change
for (int i = 0; ! getSchemaMismatchedRows().isEmpty() && i < getProtoRows().getSerializedRowsCount(); i++) {
if (!getSchemaMismatchedRows().get(i)) {
inserts.addSerializedRows(getProtoRows().getSerializedRows(i));
timestamps.add(getTimestamps().get(i));
failsafeTableRows.add(getFailsafeTableRows().get(i));
}
}
for (int i = 0; i < getProtoRows().getSerializedRowsCount(); i++) {
if (!getSchemaMismatchedRows().get(i)) {
inserts.addSerializedRows(getProtoRows().getSerializedRows(i));
timestamps.add(getTimestamps().get(i));
failsafeTableRows.add(getFailsafeTableRows().get(i));
}
}

Comment on lines +1059 to +1060
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState(
!createRetryManagerResult.getSchemaMismatchSeen());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fully-qualified class name for Preconditions is used here, but checkState is already statically imported at the top of the file. Simplifying this improves readability.

                checkState(!createRetryManagerResult.getSchemaMismatchSeen());

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Checks are failing. Will not request review until checks are succeeding. If you'd like to override that behavior, comment assign set of reviewers

@reuvenlax

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a consistent schema update mechanism for the BigQuery Storage Write API in Apache Beam, allowing rows with mismatched schemas to be buffered and retried until the updated schema is observed or a timeout is reached. The review feedback highlights critical issues, including missing side input accessors for dynamic destinations during timer executions (which would cause runtime failures), a potential integer overflow in the sharding logic, and the need to prevent unnecessary empty RPC calls when all rows are buffered. Additionally, suggestions were made to support standard Java maps in schema checks and to clean up leftover debugging print statements.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

PipelineOptions pipelineOptions,
MultiOutputReceiver o)
throws Exception {
// TODO XXX SET DYNAMIC DESTINATIONS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The side input accessor for dynamicDestinations is not set during the timer execution. If dynamicDestinations relies on side inputs, calling dynamicDestinations.getTable(...) on line 238 will fail at runtime. You should add OnTimerContext (or WindowContext) to the onTimer method parameters and use it to set the side input accessor on dynamicDestinations before accessing any tables.

currentTimerValue.readLater();
minPendingTimestamp.readLater();

// TODOTDO XXX add context for side inputs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The side input accessor for dynamicDestinations is not set during the timer execution. If dynamicDestinations relies on side inputs, calling dynamicDestinations.getTable(...) on line 1272 will fail at runtime. You should add OnTimerContext to the onMismatchedRowsTimer method parameters and use it to set the side input accessor on dynamicDestinations before accessing the table destination.

Comment on lines +803 to +804
Instant now = Instant.now();
System.err.println("PROCESS " + now);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Remove leftover debugging System.err.println statements and the unused now variable.

idleTimer,
o,
processMismatchedRows);
System.err.println("PROCESS DONE AFTER " + java.time.Duration.between(now, Instant.now()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Remove leftover debugging System.err.println statement.

@StateId("currentMismatchedRowTimerValue") ValueState<Long> currentTimerValue,
@StateId("minPendingTimestamp") ValueState<Long> minPendingTimestamp)
throws Exception {
System.err.println("RETRY TIMER " + Instant.now());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Remove leftover debugging System.err.println statement.

Comment on lines +395 to +397
} else if (value instanceof TableRow) {
return missingUnknownField((TableRow) value, fieldDescriptor.getMessageType());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Check for AbstractMap instead of TableRow to ensure that nested records represented as standard Java maps (e.g., LinkedHashMap) are also correctly checked for missing unknown fields.

Suggested change
} else if (value instanceof TableRow) {
return missingUnknownField((TableRow) value, fieldDescriptor.getMessageType());
}
} else if (value instanceof AbstractMap) {
return missingUnknownField((AbstractMap<String, Object>) value, fieldDescriptor.getMessageType());
}

@reuvenlax
reuvenlax force-pushed the schema_update_handle_unknown_fields branch from 7e959e9 to c3ca6b5 Compare June 9, 2026 18:48
@reuvenlax
reuvenlax force-pushed the schema_update_handle_unknown_fields branch from e426391 to 4ac6c18 Compare June 9, 2026 22:22
@reuvenlax
reuvenlax requested a review from ahmedabu98 June 10, 2026 00:04
@reuvenlax

Copy link
Copy Markdown
Contributor Author

RE: @ahmedabu98

@reuvenlax

Copy link
Copy Markdown
Contributor Author

@ahmedabu98 friendly ping. I know it's a large PR :)


class BufferingDoFn
extends DoFn<KV<ShardedKey<DestinationT>, MismatchedRow>, KV<String, String>> {
private final StorageApiWriteUnshardedRecords.WriteRecordsDoFn<DestinationT, ElementT>

@stankiewicz stankiewicz Jun 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe WriteRecordsDoFn should have service part extracted instead of wrapping whole doFn here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

this.writeDoFn = writeDoFn;
}

@StartBundle

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writeDoFn also also has teardown

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - this is implemented later in this file.

}

@Override
public MismatchedRow decode(InputStream inStream) throws CoderException, IOException {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

custom coders are not so friendly when it comes to updates and adding new fields (e.g. to StorageApiWritePayload), any chance to add some versioning before innerPayload.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - I originally tried to use SchemaCoder for that reason. However schema-coder update today (in Dataflow) only works on PCollections - not necessarily when stored in state, and we store this in a BagState.

PCollectionTuple> {
private static final Logger LOG = LoggerFactory.getLogger(StorageApiWritesShardedRecords.class);
private static final Duration DEFAULT_STREAM_IDLE_TIME = Duration.standardHours(1);
private static final Duration RETRY_MISMATCHED_ROWS_PERIOD = Duration.standardMinutes(1);

@stankiewicz stankiewicz Jun 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not configurable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@Hidden
@Description("The initial retry time in milliseconds when a schema mismatch is detected.")
@Default.Integer(5000)
Integer getStorageApiInitialMismatchRetryTimeMilliSec();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this is for first local retry scenario, do you think it would be valuable to add second option for 1 min timer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@stankiewicz stankiewicz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core writing logic inside WriteRecordsDoFn should be extracted into a separate service. Both the original WriteRecordsDoFn and this new BufferingDoFn should instantiate and call that service.

@stankiewicz

Copy link
Copy Markdown
Contributor

I'm ooo for next couple of weeks, lgtm! outstanding items are customcoder and failing tests.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @ahmedabu98

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @chamikaramj for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@reuvenlax

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces consistent auto-schema updates for BigQuery Storage API writes, adding support for strict timeouts and buffering mismatched rows. The review feedback highlights critical performance and reliability issues, specifically tight loops in BufferMismatchedRows and StorageApiWritesShardedRecords that could cause high CPU usage and API quota exhaustion during window expiration due to a lack of backoff. Additionally, the reviewer noted a validation bug in BigQueryIO where strict timeout checks are incorrectly run even when auto-schema updates are disabled, and suggested optimizing double map lookups in AppendRowsPacket to improve efficiency.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +311 to +340
Instant start = Instant.now();
while (!Iterables.isEmpty(bufferedRows) && start.plus(waitTime).isAfter(Instant.now())) {
writeDoFn.startBundle();
List<Iterable<KV<DestinationT, StoragePayloadWithDeadline>>> mismatchedRowsList =
Lists.newArrayList();
for (StoragePayloadWithDeadline row : bufferedRows) {
Iterable<KV<DestinationT, StoragePayloadWithDeadline>> mismatchedRows =
writeDoFn.processElement(
pipelineOptions, KV.of(shardedDestination.getKey(), row), null, o);
if (!Iterables.isEmpty(mismatchedRows)) {
mismatchedRowsList.add(mismatchedRows);
}
}
// Once we're done, delegate to finishBundle to finish things.
Iterable<KV<DestinationT, StoragePayloadWithDeadline>> mismatchedDestRows =
writeDoFn.finishBundle(
o.get(failedRowsTag),
successfulRowsTag != null ? o.get(successfulRowsTag) : null,
o.get(finalizeTag),
null);
if (!Iterables.isEmpty(mismatchedDestRows)) {
mismatchedRowsList.add(mismatchedDestRows);
}

bufferedRows =
() ->
StreamSupport.stream(Iterables.concat(mismatchedRowsList).spliterator(), false)
.map(KV::getValue)
.iterator();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This while loop tightly spins without any backoff or sleep. If the schema update takes time, this loop will continuously call writeDoFn.processElement and finishBundle as fast as possible, consuming 100% CPU and hammering the BigQuery Storage API. Please introduce a backoff sleep (e.g., using FluentBackoff) inside the loop to prevent tight spinning.

      Iterable<StoragePayloadWithDeadline> bufferedRows = mismatchedRowsBag.read();
      Instant start = Instant.now();
      org.apache.beam.sdk.util.BackOff backoff =
          org.apache.beam.sdk.util.FluentBackoff.DEFAULT
              .withInitialBackoff(Duration.standardSeconds(1))
              .withMaxBackoff(Duration.standardMinutes(1))
              .backoff();
      while (!Iterables.isEmpty(bufferedRows) && start.plus(waitTime).isAfter(Instant.now())) {
        writeDoFn.startBundle();
        List<Iterable<KV<DestinationT, StoragePayloadWithDeadline>>> mismatchedRowsList =
            Lists.newArrayList();
        for (StoragePayloadWithDeadline row : bufferedRows) {
          Iterable<KV<DestinationT, StoragePayloadWithDeadline>> mismatchedRows =
              writeDoFn.processElement(
                  pipelineOptions, KV.of(shardedDestination.getKey(), row), null, o);
          if (!Iterables.isEmpty(mismatchedRows)) {
            mismatchedRowsList.add(mismatchedRows);
          }
        }
        // Once we're done, delegate to finishBundle to finish things.
        Iterable<KV<DestinationT, StoragePayloadWithDeadline>> mismatchedDestRows =
            writeDoFn.finishBundle(
                o.get(failedRowsTag),
                successfulRowsTag != null ? o.get(successfulRowsTag) : null,
                o.get(finalizeTag),
                null);
        if (!Iterables.isEmpty(mismatchedDestRows)) {
          mismatchedRowsList.add(mismatchedDestRows);
        }

        bufferedRows =
            () ->
                StreamSupport.stream(Iterables.concat(mismatchedRowsList).spliterator(), false)
                    .map(KV::getValue)
                    .iterator();
        if (!Iterables.isEmpty(bufferedRows)) {
          Thread.sleep(backoff.nextBackOffMillis());
        }
      }

Comment on lines +1464 to +1485
AtomicReference<Iterable<StoragePayloadWithDeadline>> mismatchedRows =
new AtomicReference<>(mismatchedRowsBag.read());
Instant start = Instant.now();
while (!Iterables.isEmpty(mismatchedRows.get())
&& start.plus(waitTime).isAfter(Instant.now())) {
messageConverter.updateSchemaFromTable();
APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(key));
processPayloads(
pipelineOptions,
key,
tableDestination,
messageConverter,
mismatchedRows.get(),
null,
elementTs,
streamName,
streamOffset,
updatedSchema,
null,
o,
mismatchedRows::set);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This while loop tightly spins and calls messageConverter.updateSchemaFromTable() (which performs a synchronous BigQuery API RPC) on every iteration. Without a backoff sleep, this will immediately exhaust your BigQuery API quota and hammer the service. Please add a backoff sleep inside the loop.

      AtomicReference<Iterable<StoragePayloadWithDeadline>> mismatchedRows =
          new AtomicReference<>(mismatchedRowsBag.read());
      Instant start = Instant.now();
      org.apache.beam.sdk.util.BackOff backoff =
          org.apache.beam.sdk.util.FluentBackoff.DEFAULT
              .withInitialBackoff(Duration.standardSeconds(1))
              .withMaxBackoff(Duration.standardMinutes(1))
              .backoff();
      while (!Iterables.isEmpty(mismatchedRows.get())
          && start.plus(waitTime).isAfter(Instant.now())) {
        messageConverter.updateSchemaFromTable();
        APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(key));
        processPayloads(
            pipelineOptions,
            key,
            tableDestination,
            messageConverter,
            mismatchedRows.get(),
            null,
            elementTs,
            streamName,
            streamOffset,
            updatedSchema,
            null,
            o,
            mismatchedRows::set);
        if (!Iterables.isEmpty(mismatchedRows.get())) {
          Thread.sleep(backoff.nextBackOffMillis());
        }
      }

Comment on lines +4134 to +4141
if (getAutoSchemaUpdateStrictTimeout() != null) {
checkArgument(
method == Method.STORAGE_API_AT_LEAST_ONCE || method == Method.STORAGE_WRITE_API,
"Auto update schema only supported when using storage write API");
checkArgument(
input.getPipeline().getOptions().as(StreamingOptions.class).isStreaming(),
"auto update schema only supported on streaming pipelines");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The validation checks for getAutoSchemaUpdateStrictTimeout() are executed even if getAutoSchemaUpdate() is false. If a user disables auto schema updates but has a strict timeout set (or if it's left as default/configured elsewhere), this will throw an IllegalArgumentException unnecessarily. The check should only run if getAutoSchemaUpdate() is enabled.

Suggested change
if (getAutoSchemaUpdateStrictTimeout() != null) {
checkArgument(
method == Method.STORAGE_API_AT_LEAST_ONCE || method == Method.STORAGE_WRITE_API,
"Auto update schema only supported when using storage write API");
checkArgument(
input.getPipeline().getOptions().as(StreamingOptions.class).isStreaming(),
"auto update schema only supported on streaming pipelines");
}
if (getAutoSchemaUpdate() && getAutoSchemaUpdateStrictTimeout() != null) {
checkArgument(
method == Method.STORAGE_API_AT_LEAST_ONCE || method == Method.STORAGE_WRITE_API,
"Auto update schema only supported when using storage write API");
checkArgument(
input.getPipeline().getOptions().as(StreamingOptions.class).isStreaming(),
"auto update schema only supported on streaming pipelines");
}

Comment on lines +177 to +187
if (getUnknownFields().containsKey(i)) {
unknownFields.put(newIndex, Preconditions.checkStateNotNull(getUnknownFields().get(i)));
}
if (getOriginalPayloads().containsKey(i)) {
originalPayloads.put(
newIndex, Preconditions.checkStateNotNull(getOriginalPayloads().get(i)));
}
deadlines.add(getDeadlines().get(i));
if (getSchemaHashes().containsKey(i)) {
schemaHashes.put(newIndex, Preconditions.checkStateNotNull(getSchemaHashes().get(i)));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid double map lookups (containsKey followed by get). Instead, perform a single get and check if the returned value is non-null. This is cleaner and more efficient.

          TableRow unknownField = getUnknownFields().get(i);
          if (unknownField != null) {
            unknownFields.put(newIndex, unknownField);
          }
          byte[] originalPayload = getOriginalPayloads().get(i);
          if (originalPayload != null) {
            originalPayloads.put(newIndex, originalPayload);
          }
          deadlines.add(getDeadlines().get(i));
          byte[] schemaHash = getSchemaHashes().get(i);
          if (schemaHash != null) {
            schemaHashes.put(newIndex, schemaHash);
          }

Comment on lines +223 to +233
if (getSchemaHashes().containsKey(i)) {
schemaHashes.put(newIndex, Preconditions.checkStateNotNull(getSchemaHashes().get(i)));
}
if (getUnknownFields().containsKey(i)) {
unknownFields.put(newIndex, Preconditions.checkStateNotNull(getUnknownFields().get(i)));
}
if (getOriginalPayloads().containsKey(i)) {
originalPayloads.put(
newIndex, Preconditions.checkStateNotNull(getOriginalPayloads().get(i)));
}
deadlines.add(getDeadlines().get(i));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid double map lookups (containsKey followed by get). Perform a single get and check for null instead.

Suggested change
if (getSchemaHashes().containsKey(i)) {
schemaHashes.put(newIndex, Preconditions.checkStateNotNull(getSchemaHashes().get(i)));
}
if (getUnknownFields().containsKey(i)) {
unknownFields.put(newIndex, Preconditions.checkStateNotNull(getUnknownFields().get(i)));
}
if (getOriginalPayloads().containsKey(i)) {
originalPayloads.put(
newIndex, Preconditions.checkStateNotNull(getOriginalPayloads().get(i)));
}
deadlines.add(getDeadlines().get(i));
byte[] schemaHash = getSchemaHashes().get(i);
if (schemaHash != null) {
schemaHashes.put(newIndex, schemaHash);
}
TableRow unknownField = getUnknownFields().get(i);
if (unknownField != null) {
unknownFields.put(newIndex, unknownField);
}
byte[] originalPayload = getOriginalPayloads().get(i);
if (originalPayload != null) {
originalPayloads.put(newIndex, originalPayload);
}

@github-actions

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @chamikaramj @ahmedabu98

@github-actions

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @Abacn for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants