Repository navigation
fix(bq_driver): transparently handle transient stream disconnections and resume HTAPI reads - #1692
Conversation
| 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 || |
There was a problem hiding this comment.
how do we know we need to retry for these messages? some probably can be permanent errors and we are unnecessarily retrying for them
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
don't change for max_retries = 0, as the user might not want retries
There was a problem hiding this comment.
fixed. We now use conn_handle->GetDsn().max_retries directly without overriding 0, so setting MaxRetries=0 disables retries as expected.
| #ifdef BQ_DRIVER_INTEGRATION_TESTS | ||
| TEST(StatementTest, SQLExecDirect_HTAPI_StreamRetryAndResume) { | ||
| #ifdef _WIN32 | ||
| _putenv_s("BIGQUERY_ODBC_TEST_INJECT_STREAM_ERROR", "1"); |
There was a problem hiding this comment.
what is the utility of this flag?
There was a problem hiding this comment.
Because transient network disconnects (RST_STREAM) cannot be deterministically reproduced against live BigQuery servers in CI, this test-only flag injects a simulated stream error mid-query.
It allows the integration test to reliably verify that the retry loop catches the error, re-attaches to the stream from the last offset (set_offset(N)), and resumes reading all rows without failing the query.
0f7d73a to
ff3c244
Compare
0ce78da to
57f7077
Compare
|
/gcbrun |
62caf1a to
cc3cff8
Compare
|
/gcbrun |
cc3cff8 to
1cbd0d0
Compare
|
/gcbrun |
1cbd0d0 to
8550ef6
Compare
|
/gcbrun |
…and resume HTAPI reads
8550ef6 to
d0f0f81
Compare
|
/gcbrun |
|
Here is summary of changes I made on top of the original PR to simplify the logic: 1. Implementation Summary:
|
| Target | Result | Details |
|---|---|---|
origin/main |
FAILED | Tunnel #3 killed at 4,013,127 B; Tunnel #4 reconnected at offset = 668 and killed at 105,765 B. Failed immediately with: SQLFetch = [Google][ODBC BigQuery Driver] [BigQuery] Stream removed (Unwrap failed (TSI_DATA_CORRUPTED)) (500) SQLSTATE=HY000 (kills: 2, reconnects: 1, storage_tunnels: 2). |
htapi_stream_retry_fix |
PASSED | Tunnel #3 killed at 4,013,127 B; Tunnel #4 reconnected and killed at 105,765 B; HtapiStreamRetryPolicy classified kUnknown ("Stream removed...") as transient, opened Tunnel #5 (kills: 2, reconnects: 2, storage_tunnels: 3), resumed at offset = 668, and verified all 3,000 rows × 300 columns. |
|
Windows benchmarks and linux benchmarks show no performance degradation. |
sachinpro
left a comment
There was a problem hiding this comment.
Thanks a lot for fixing this.
I simplified the logic and added the integration tests to google/cloud/odbc/integration_tests/odbc_driver_tests/resilience_test.cc.
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, orUNAVAILABLEerrors).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:
IsTransientStreamError()to identify retryable status codes (UNAVAILABLE,DEADLINE_EXCEEDED,ABORTED,RESOURCE_EXHAUSTED) as well asINTERNAL/UNKNOWNerrors resulting from transport-level drops (rst_stream,connection reset,broken pipe,proxy,timeout,transport: lost connection).stream_row_offset_) as Arrow record batches are consumed. On a transient failure, the driver re-attaches to the existingReadSessionstream usingReadRowsRequest.set_offset(N)after an exponential backoff delay.read_stream_name_,stream_row_offset_, andwas_htapi_enabled_onStatementHandle, and resets them inCloseCursor()to avoid state leakage across reused statement handles.Key Changes
google/cloud/odbc/bq_driver/internal/odbc_sql_execute_utils.h:IsTransientStreamError().google/cloud/odbc/bq_driver/internal/odbc_sql_execute_utils.cc:IsTransientStreamError()for status code and error message inspection.ReadNextResultsFromStream()to execute an exponential backoff retry loop up tomax_retrieswithread_rows_request.set_offset(stream_row_offset_).FetchBQDataReadArrow().google/cloud/odbc/bq_driver/internal/odbc_stmt_handle.h:read_stream_name_,stream_row_offset_, andwas_htapi_enabled_.google/cloud/odbc/bq_driver/internal/odbc_stmt_handle.cc:CloseCursor().Verification
libgoogle_cloud_odbc_bq_driver_internal.aandlibgoogle_cloud_odbc_bq_driver.sobuild cleanly with zero errors.