Skip to content

feat(bigquery-storage): add Arrow options and cancel to read_rows - #18643

Open
ohmayr wants to merge 3 commits into
mainfrom
feat/bigquery-storage-arrow-read-rows
Open

ohmayr wants to merge 3 commits into
mainfrom
feat/bigquery-storage-arrow-read-rows

Conversation

@ohmayr

@ohmayr ohmayr commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Adds arrow_serialization_options to BigQueryReadClient.read_rows() and a ReadRowsStream.cancel() method, for reading Arrow results from a query job's default stream.

  • Sends the options on every ReadRows call, including reconnects.
  • cancel() stops the stream, including during a reconnect, without closing the client.
  • Adds unit tests for both, and system tests for LZ4_FRAME/ZSTD and cancel() on a default job stream.

google-cloud-bigquery will depend on the release that includes this.

@ohmayr
ohmayr requested review from a team as code owners October 13, 2026 07:58
@ohmayr
ohmayr requested review from shuoweil and removed request for a team October 13, 2026 07:58

@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 support for arrow_serialization_options in the BigQuery Storage read_rows client and stream reader, allowing users to specify options like buffer compression. It also implements a cancel() method on ReadRowsStream to stop reading before the end of a stream without reconnecting, along with corresponding unit and system tests. The review feedback highlights a potential race condition in _reconnect() where a newly established stream could be leaked if cancel() is called concurrently, and suggests a code change to safely assign and check the cancellation state.

Comment on lines +222 to 228
if self._cancelled:
cancel = getattr(wrapped, "cancel", None)
if cancel is not None:
cancel()
return
self._wrapped = wrapped
break

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

There is a potential race condition when cancel() is called concurrently from another thread (which is the standard way to cancel a blocking stream iterator) while _reconnect() is establishing a new stream.

If cancel() is called after the self._cancelled check on line 222 but before self._wrapped = wrapped on line 227, the cancel() call will execute on the old stream (or do nothing if it was None). Then, the new stream wrapped is assigned to self._wrapped and the loop breaks. Since self._cancelled is now True, the __iter__ loop will terminate, but the newly created wrapped stream is never cancelled or closed, leading to a resource leak.

To prevent this, assign self._wrapped = wrapped first, and then check self._cancelled. If it is cancelled, set self._wrapped = None (to preserve the expected state) and cancel the stream before returning.

Suggested change
if self._cancelled:
cancel = getattr(wrapped, "cancel", None)
if cancel is not None:
cancel()
return
self._wrapped = wrapped
break
self._wrapped = wrapped
if self._cancelled:
self._wrapped = None
cancel = getattr(wrapped, "cancel", None)
if cancel is not None:
cancel()
return
break

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.

Good catch, the window between the check and the assignment was real. Fixed in f759307: _reconnect() now stores the new call first and then checks _cancelled, so a concurrent cancel() either cancels it directly or gets picked up by the check.

Default job streams (projects/.../jobs/.../streams/_default) accept
ArrowSerializationOptions on each ReadRowsRequest, but
BigQueryReadClient.read_rows() had no way to send them, and
ReadRowsStream dropped them when it reconnected after a transient
error. There was also no way to stop a stream early without closing
the whole client.

read_rows() now takes arrow_serialization_options, and ReadRowsStream
sends them on the first request and on every reconnect, so all
batches in a stream use the same encoding. ReadRowsStream.cancel()
cancels the current call and stops further reconnects, including a
reconnect already in progress, without closing the client.

google-cloud-bigquery needs both to read query results in Arrow
format through the default job stream.
@ohmayr
ohmayr force-pushed the feat/bigquery-storage-arrow-read-rows branch from f759307 to ea74a33 Compare October 13, 2026 10:02
offset,
read_rows_kwargs,
retry_delay_callback=None,
arrow_serialization_options=None,

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 aren't you using the existing read_rows_kwargs dictionary?

Edit: I see that client is the super() / generated client. Still, I'd prefer adding a more general "request_template" or something like that so that we can pass any new request parameters more easily in future.

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.

replaced arrow_serialization_options on ReadRowsStream.__init__ with *, request_template=None, and copied it via gapic_types.ReadRowsRequest(self._request_template) in _reconnect() before setting read_stream and offset so caller-supplied templates aren't mutated across reconnects.


self._reconnect()

def cancel(self):

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.

Thought: should we automatically call cancel in __del__? That might help in cases where folks are just reading one batch and returning.

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.

added __del__ to call self.cancel() when not getattr(self, "_cancelled", True), and cleared self._wrapped = None when iteration completes naturally so finished or already-cancelled streams aren't cancelled again on GC.

…cancel in __del__

Require keyword argument syntax for arrow_serialization_options on
BigQueryReadClient.read_rows() and ReadRowsStream.__init__(). Cancel
unfinished streams automatically in ReadRowsStream.__del__(), and clear
_wrapped upon normal stream completion so completed or already-cancelled
streams are not cancelled again during garbage collection.
Replace arrow_serialization_options on ReadRowsStream.__init__ with a
keyword-only request_template parameter so future ReadRowsRequest
options can be passed without changing the stream constructor. Copy
the template via ReadRowsRequest(self._request_template) on reconnect
to avoid mutating caller-supplied request objects.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants