Repository navigation
feat(bigquery): share one BigQuery Storage client per Client #18652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |
| import math | ||
| import os | ||
| import tempfile | ||
| import threading | ||
| import typing | ||
| import uuid | ||
| import warnings | ||
|
|
@@ -166,6 +167,35 @@ | |
| # https://github.com/googleapis/python-bigquery/issues/438 | ||
| _MIN_GET_QUERY_RESULTS_TIMEOUT = 120 | ||
|
|
||
| # While a stream is open and the connection is idle, the shared BigQuery | ||
| # Storage client pings the server after a minute, and closes the connection | ||
| # if a ping isn't answered within a minute. A dropped ReadRows stream is | ||
| # resumed at the next unread row. | ||
| _BQSTORAGE_KEEPALIVE_TIME_MS = 60 * 1000 | ||
| _BQSTORAGE_KEEPALIVE_TIMEOUT_MS = 60 * 1000 | ||
|
|
||
|
|
||
| def _create_bqstorage_transport(**kwargs): | ||
| """Create a BigQuery Storage gRPC transport whose channel sends keepalive | ||
| pings. | ||
|
|
||
| ``BigQueryReadClient`` calls this with the arguments it passes to its | ||
| default transport. | ||
| """ | ||
| from google.cloud import bigquery_storage | ||
|
|
||
| transport_class = bigquery_storage.BigQueryReadClient.get_transport_class("grpc") | ||
|
|
||
| def create_channel(*args, options=(), **channel_kwargs): | ||
| options = list(options) + [ | ||
| ("grpc.keepalive_time_ms", _BQSTORAGE_KEEPALIVE_TIME_MS), | ||
| ("grpc.keepalive_timeout_ms", _BQSTORAGE_KEEPALIVE_TIMEOUT_MS), | ||
| ] | ||
| return transport_class.create_channel(*args, options=options, **channel_kwargs) | ||
|
Comment on lines
+190
to
+194
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make more sense to upstream this to the google-cloud-bigquery-storage client library? |
||
|
|
||
| return transport_class(channel=create_channel, **kwargs) | ||
|
|
||
|
|
||
| _LOAD_TABLE_FROM_DATAFRAME_DEPRECATED = ( | ||
| "Loading DataFrames via google-cloud-bigquery is deprecated. " | ||
| "For direct, optimized loading, please call 'pandas_gbq.to_gbq()' directly." | ||
|
|
@@ -300,6 +330,14 @@ def __init__( | |
| self._default_load_job_config = copy.deepcopy(default_load_job_config) | ||
| self.default_job_creation_mode = default_job_creation_mode | ||
|
|
||
| # ARROW query results share one BigQuery Storage client. It's created | ||
| # the first time a query needs a stream, and close() closes it. | ||
| self._universe_domain = _get_client_universe(client_options) | ||
| self._shared_bqstorage_client: Optional[ | ||
| "google.cloud.bigquery_storage.BigQueryReadClient" | ||
| ] = None | ||
| self._shared_bqstorage_client_lock = threading.Lock() | ||
|
|
||
| # Use property setter so validation can run. | ||
| self.default_query_job_config = default_query_job_config | ||
|
|
||
|
|
@@ -348,6 +386,9 @@ def default_load_job_config(self, value: LoadJobConfig): | |
| def close(self): | ||
| """Close the underlying transport objects, releasing system resources. | ||
|
|
||
| This also closes the BigQuery Storage client that ``ARROW`` query | ||
| results share. | ||
|
|
||
| .. note:: | ||
|
|
||
| The client instance can be used for making additional requests even | ||
|
|
@@ -356,6 +397,11 @@ def close(self): | |
| """ | ||
| self._http._auth_request.session.close() | ||
| self._http.close() | ||
| with self._shared_bqstorage_client_lock: | ||
| bqstorage_client = self._shared_bqstorage_client | ||
| self._shared_bqstorage_client = None | ||
| if bqstorage_client is not None: | ||
| bqstorage_client._transport.close() | ||
|
|
||
| def get_service_account_email( | ||
| self, | ||
|
|
@@ -600,6 +646,7 @@ def _ensure_bqstorage_client( | |
| client_info: Optional[ | ||
| "google.api_core.gapic_v1.client_info.ClientInfo" | ||
| ] = DEFAULT_BQSTORAGE_CLIENT_INFO, | ||
| transport: Optional[Callable[..., Any]] = None, | ||
| ) -> Optional["google.cloud.bigquery_storage.BigQueryReadClient"]: | ||
| """Create a BigQuery Storage API client using this client's credentials. | ||
|
|
||
|
|
@@ -613,6 +660,9 @@ def _ensure_bqstorage_client( | |
| client_info: | ||
| The client info used with a new BigQuery Storage client | ||
| instance if one is created. | ||
| transport: | ||
| A callable that creates the transport of a new BigQuery Storage | ||
| client instance. If ``None``, the default transport is used. | ||
|
|
||
| Returns: | ||
| A BigQuery Storage API client. | ||
|
|
@@ -680,10 +730,37 @@ def _ensure_bqstorage_client( | |
| credentials=self._credentials, | ||
| client_options=client_options, | ||
| client_info=amended_client_info, | ||
| transport=transport, | ||
| ) | ||
|
|
||
| return bqstorage_client | ||
|
|
||
| def _get_shared_bqstorage_client( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why introduce a new method instead of doing this in |
||
| self, | ||
| ) -> Optional["google.cloud.bigquery_storage.BigQueryReadClient"]: | ||
| """Return the BigQuery Storage client that ``ARROW`` query results share. | ||
|
|
||
| The client is created on first use, with this client's credentials and | ||
| universe domain and a gRPC channel that sends keepalive pings. | ||
| :meth:`close` closes it, and the next call creates a new one. | ||
|
|
||
| Returns: | ||
| A BigQuery Storage API client, or ``None`` if | ||
| google-cloud-bigquery-storage is missing or outdated. | ||
| """ | ||
| with self._shared_bqstorage_client_lock: | ||
| if self._shared_bqstorage_client is None: | ||
| client_options = None | ||
| if self._universe_domain != _DEFAULT_UNIVERSE: | ||
| client_options = google.api_core.client_options.ClientOptions( | ||
| universe_domain=self._universe_domain | ||
| ) | ||
|
Comment on lines
+755
to
+757
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Careful here to not lose other client options. Are there any we care about? |
||
| self._shared_bqstorage_client = self._ensure_bqstorage_client( | ||
| client_options=client_options, | ||
| transport=_create_bqstorage_transport, | ||
| ) | ||
| return self._shared_bqstorage_client | ||
|
|
||
| def _dataset_from_arg(self, dataset) -> Union[Dataset, DatasetReference]: | ||
| if isinstance(dataset, str): | ||
| dataset = DatasetReference.from_string( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Creating a client can be expensive.