Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request adds support for waiting on unfinished BigQuery queries when executing createQueryArrowStream. It introduces a polling mechanism (waitForJobCompletion) using job.getQueryResults with maxResults: 0 to wait for job completion before streaming results, and adds configuration for timeoutMs. Additionally, comprehensive unit and system tests are added to cover these scenarios. The feedback highlights a critical issue in waitForJobCompletion where polling is executed in a tight loop without any delay, which could lead to API rate limiting. It is recommended to introduce a delay using setTimeout and implement proper cleanup on stream destruction.
e9e8c87 to
1922007
Compare
1922007 to
bdcf7cc
Compare
bdcf7cc to
2e564cc
Compare
96ebe98 to
67aba4c
Compare
4469c39 to
d2751f2
Compare
d2751f2 to
632e0c9
Compare
632e0c9 to
9d734b9
Compare
9d734b9 to
09093e3
Compare
16abd40 to
c254891
Compare
c254891 to
f6cb6da
Compare
f6cb6da to
09fb20b
Compare
shivanee-p
left a comment
There was a problem hiding this comment.
It's great to see the progress!
I have some concerns with the polling logic. I think having it be asynchronous would be really beneficial.
| } | ||
| stream.push(batch); | ||
| } | ||
| if (!stream._ended && !stream.destroyed) { |
| function waitForJobCompletion( | ||
| job: Job, | ||
| stream: ArrowRecordBatchStream, | ||
| onComplete: () => void, | ||
| ): void { | ||
| const poll = () => { | ||
| if (stream._ended || stream.destroyed) { | ||
| return; | ||
| } | ||
| // Set maxResults: 0 so jobs.getQueryResults blocks up to the server's | ||
| // default wait interval and surfaces job errors without returning JSON rows. | ||
| job.getQueryResults( | ||
| { | ||
| autoPaginate: false, | ||
| maxResults: 0, | ||
| }, | ||
| (err, _rows, _nextQuery, res) => { | ||
| if (err) { | ||
| stream.destroy(err); | ||
| return; | ||
| } | ||
| if (stream._ended || stream.destroyed) { | ||
| return; | ||
| } | ||
| if (res?.jobComplete === false) { | ||
| poll(); | ||
| return; | ||
| } | ||
| onComplete(); | ||
| }, | ||
| ); | ||
| }; | ||
| poll(); | ||
| } |
There was a problem hiding this comment.
It makes sense regarding the polling mechanism. However there are a few concerns:
poll()is called immediately whenjobCompleteis false, but we should have a slight delay to prevent throwing rate limiting errors- I think transforming this code to be asynchronous would be a good call. Using an async/await loop would be preferred to avoid nested recursive chaining
There was a problem hiding this comment.
Made it an async loop. I left out a client-side delay: getQueryResults blocks on the server for about 10 s per call while the job runs (measured: 20 polls in 201 s on a long job), so the loop already polls about 6 times a minute, and a delay would only add latency after the job finishes. This matches how query() waits today.
There was a problem hiding this comment.
Following up on the polling delay: each jobs.getQueryResults call with maxResults: 0 waits on the server for up to about 10 s while the job runs, so polling again right away doesn't spin. A client-side delay would only add latency after the job finishes.
09fb20b to
8a9c7bd
Compare
|
Rebased on the #9598 changes. Waiting behaves the same; only how it's written changed:
|
Implements unfinished job polling (jobComplete === false) in
createQueryArrowStream:
- Adds ArrowQuery (Query plus Arrow-only options, beta like
createQueryArrowStream) with timeoutMs, and
forwards it into buildQueryRequest_ so jobs.query receives the
caller's server-side timeout.
- When jobs.query returns jobComplete === false:
- Fails immediately with a timeout error if a non-zero timeoutMs was
specified, matching BigQuery#query.
- Otherwise polls job.getQueryResults with maxResults: 0 (so
BigQuery blocks until completion without serializing any
STRUCT_ENCODING rows) and then streams all Arrow batches from
streams/_default at offset = 0.
- streams/_default sends nothing for a result without rows, so the
0-row RecordBatch reads its schema from the job's destination table
via CreateReadSession (none for statements without a result table).
- Adds a system test that waits on a job with a picosecond TIMESTAMP(12)
column, run when BIGQUERY_PICOSECOND_SUPPORT is set, and adds the
Arrow system tests to the high-precision re-run in
high_precision_timestamp.ts so CI runs it.
IReadSession is only used inside arrow.ts, so stop exporting it. CreateReadSessionRequest stays exported for the unit tests and is now documented as @Private, like the other test-only exports in arrow.ts.
8a9c7bd to
6fce0fd
Compare
| // the job's current metadata instead of the response that created it. | ||
| const [metadata] = await job.getMetadata(); | ||
| const destRef = metadata?.configuration?.query?.destinationTable; | ||
| if (!destRef) { |
There was a problem hiding this comment.
We should also check if (ctx.stream.destroyed) in the event that the caller destroys the stream
There was a problem hiding this comment.
Added in b31bbd7: readDestinationSchema returns without creating a read session once the stream is destroyed, with a test.
| } | ||
|
|
||
| /** | ||
| * Stubs `job.getQueryResults` with a callback-style `impl`, returning a |
There was a problem hiding this comment.
These stubs should be changed to be promises to mimic the behavior of the actual implementation.
There was a problem hiding this comment.
Converted in b31bbd7: stubJobGetQueryResults now takes an async function. I left stubCreateJob and the bq.request stubs callback-based, because createQueryJob calls createJob with a callback internally and request is callback-based.
Return from readDestinationSchema without a CreateReadSession call if the consumer destroyed the stream during jobs.get. Stub job.getQueryResults with async functions in the unit tests, matching the awaited promise API instead of adapting callbacks.
Extends
bigquery.createQueryArrowStream(query)to wait for unfinished queries (jobComplete === false) before streaming Arrow results from the job's default read stream (projects/{project}/locations/{location}/jobs/{job}/streams/_default).ArrowQueryoptions type (beta, like the method) withtimeoutMs.timeoutMstojobs.queryand fails ifjobComplete === falsewhen a caller-providedtimeoutMsexpires.job.getQueryResultswithmaxResults: 0andautoPaginate: falseuntil the job completes without fetching row data, then streams all batches viaReadRowsstarting atoffset = 0.RecordBatchwhen reading fromoffset = 0and the stream closes without any record batches.offset = 0has no rows, reads its schema from the job's destination table (jobs.get, thenCreateReadSession), becausestreams/_defaultsends nothing for an empty result. This is skipped if the consumer has destroyed the stream.BIGQUERY_PICOSECOND_SUPPORT(high_precision_timestamp.ts) to cover aTIMESTAMP(12)column during the job wait.