Skip to content

fix(bq_driver): transparently handle transient stream disconnections and resume HTAPI reads - #1692

Draft
KanchanShu wants to merge 2 commits into
googleapis:mainfrom
KanchanShu:htapi_stream_retry_fix
Draft

KanchanShu wants to merge 2 commits into
googleapis:mainfrom
KanchanShu:htapi_stream_retry_fix

Conversation

@KanchanShu

Copy link
Copy Markdown
Contributor

Description

Fixes an issue where long-running queries using the High-Throughput API (HTAPI / BigQuery Storage Read API) fail prematurely when intermediate network devices, proxies, or load balancers drop the connection (e.g., HTTP/2 RST_STREAM, TCP connection resets, proxy timeouts, or UNAVAILABLE errors).

Previously, any stream error in ReadNextResultsFromStream() immediately failed the query and returned a fatal error to the user application.

This PR implements transparent stream re-attachment and resumption:

  • Transient Error Detection: Added IsTransientStreamError() to identify retryable status codes (UNAVAILABLE, DEADLINE_EXCEEDED, ABORTED, RESOURCE_EXHAUSTED) as well as INTERNAL/UNKNOWN errors resulting from transport-level drops (rst_stream, connection reset, broken pipe, proxy, timeout, transport: lost connection).
  • Offset Resumption: Tracks the cumulative row offset (stream_row_offset_) as Arrow record batches are consumed. On a transient failure, the driver re-attaches to the existing ReadSession stream using ReadRowsRequest.set_offset(N) after an exponential backoff delay.
  • State Management: Persists read_stream_name_, stream_row_offset_, and was_htapi_enabled_ on StatementHandle, and resets them in CloseCursor() to avoid state leakage across reused statement handles.

Key Changes

  • google/cloud/odbc/bq_driver/internal/odbc_sql_execute_utils.h:
    • Declared IsTransientStreamError().
  • google/cloud/odbc/bq_driver/internal/odbc_sql_execute_utils.cc:
    • Implemented IsTransientStreamError() for status code and error message inspection.
    • Updated ReadNextResultsFromStream() to execute an exponential backoff retry loop up to max_retries with read_rows_request.set_offset(stream_row_offset_).
    • Initialized stream name and offset in FetchBQDataReadArrow().
  • google/cloud/odbc/bq_driver/internal/odbc_stmt_handle.h:
    • Added fields and accessors for read_stream_name_, stream_row_offset_, and was_htapi_enabled_.
  • google/cloud/odbc/bq_driver/internal/odbc_stmt_handle.cc:
    • Added cleanup of stream name, stream row offset, and HTAPI flags in CloseCursor().

Verification

  • Recompiled and verified that both libgoogle_cloud_odbc_bq_driver_internal.a and libgoogle_cloud_odbc_bq_driver.so build cleanly with zero errors.

std::string message = status.message();
std::transform(message.begin(), message.end(), message.begin(),
[](unsigned char c) { return std::tolower(c); });
if (message.find("rst_stream") != std::string::npos ||

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.

how do we know we need to retry for these messages? some probably can be permanent errors and we are unnecessarily retrying for them

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.

Updated, To avoid retrying permanent errors, I removed broad terms (timeout, proxy) and kept only explicit TCP/HTTP-2 transport disconnects (rst_stream, stream removed, connection reset, broken pipe).

}

int max_retries = conn_handle->GetDsn().max_retries;
if (max_retries <= 0) {

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.

don't change for max_retries = 0, as the user might not want retries

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.

fixed. We now use conn_handle->GetDsn().max_retries directly without overriding 0, so setting MaxRetries=0 disables retries as expected.

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