Skip to content

fix(bigquery): wait on job status instead of getQueryResults (BRU-5103)#2294

Open
batikankarakan wants to merge 6 commits into
mainfrom
batikankarakan/bru-5103-bq-status-poll
Open

fix(bigquery): wait on job status instead of getQueryResults (BRU-5103)#2294
batikankarakan wants to merge 6 commits into
mainfrom
batikankarakan/bru-5103-bq-status-poll

Conversation

@batikankarakan

Copy link
Copy Markdown
Contributor

Problem (BRU-5103)

A CREATE TABLE IF NOT EXISTS in the energy_unlicensed pipeline hit BigQuery's per-table update-rate quota and failed after 0 seconds — yet the task stayed RUNNING for a full 24 hours, delaying client deliveries, until the orchestrator's 24h run-timeout finally cancelled it.

Confirmed from OXR logs for the stuck task:

07:13:06  BigQuery query ID: bruin_main-seC0MmtKhTs9jF49SmTuR8A41M9
   ⬇  ~24h with no output (CPU idle ~0.0002 cores, worker heartbeating every 10s)  ⬇
+24h      Failed 1 tasks in 23h59m57.05s
          Job exceeded rate limits: Your table exceeded quota for table update operations

Root cause

RunQueryWithoutResult / Select / SelectWithSchema waited for the query via job.Read, which polls jobs.getQueryResults. When a job fails for a retryable reason (rateLimitExceeded, backendError, …), getQueryResults surfaces that terminal failure as a retryable API error, so the BigQuery Go client's retryer keeps polling the already-failed job. The execution context has no deadline, so it retries ~once/minute forever. The process sits blocked-but-alive, so the worker keeps heartbeating and OXR's zombie detector (1-min heartbeat staleness) never fires.

Fix

Add waitForJobCompletion, which polls job.Status (jobs.get) until the job reaches a terminal state and returns the job's own error:

  • jobs.get reports a failed job's state as data (status.Err()), not a retryable API error — so failures surface immediately.
  • Genuinely running queries keep being polled until they finish → long-running queries are not cut short (no wall-clock timeout).
  • Select/SelectWithSchema call it before job.Read, so Read runs only once the job is known-complete and returns without retrying.
  • Fast-path via job.LastStatus() avoids an extra RPC when the submit/prior poll already reports a terminal state.

Tests

  • Added the jobs.get route to mockBqHandler (previously unmocked → status polling would hang in tests).
  • New regression test TestDB_RunQueryWithoutResultSurfacesTerminalJobError: submit returns RUNNING, jobs.get returns DONE + rateLimitExceeded → error surfaces immediately.
  • Full pkg/bigquery suite passes in ~1s (before this fix the same suite hit the 10-min test timeout on the hang).

🤖 Generated with Claude Code

batikankarakan and others added 3 commits July 2, 2026 09:58
RunQueryWithoutResult/Select/SelectWithSchema waited for a query via
job.Read, which polls jobs.getQueryResults. When a job fails for a
retryable reason (e.g. "rateLimitExceeded" on a table over its update
quota), getQueryResults surfaces that terminal failure as a retryable
API error, so the BigQuery client's retryer keeps polling the
already-failed job. With no context deadline this hangs indefinitely:
in BRU-5103 a CREATE TABLE hit the rate limit, failed after 0s, but the
task stayed RUNNING for a full 24h (worker kept heartbeating a blocked,
idle process) until the orchestrator's 24h run-timeout finally cancelled
it.

Add waitForJobCompletion, which polls job.Status (jobs.get) until the
job reaches a terminal state and returns the job's own error. jobs.get
reports a failed job's state immediately rather than as a retryable
error, so failures surface at once; genuinely running queries keep being
polled until they finish, so long-running queries are not cut short.
Select/SelectWithSchema call it before job.Read, so Read only runs once
the job is known-complete and returns without retrying.

Also add the jobs.get route to mockBqHandler (previously unmocked, which
made status polling hang in tests) and a regression test covering a job
that completes with a rate-limit error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
pkg/bigquery/db.go:290-292
**Transient Status Errors Abort Jobs**

When a long-running query is still active, any temporary `jobs.get` failure now returns from `waitForJobCompletion` and fails the Bruin task. The previous `job.Read` polling path retried transient BigQuery API errors, so a short 5xx or network blip can now fail a query that would otherwise complete successfully.

### Issue 2 of 3
pkg/bigquery/db.go:297
**Terminal Errors Lose Shape**

Terminal job failures now return `status.Err()`, which is a BigQuery job error rather than the `googleapi.Error` shape callers previously received from `job.Read`. Any caller that classifies BigQuery failures with `errors.As` for HTTP code or structured API errors can no longer detect those fields for failed jobs.

### Issue 3 of 3
pkg/bigquery/db_test.go:541-542
**Running Mock Never Completes**

This handler returns the same response for `jobs.insert` and `jobs.get`. A test that submits a `RUNNING` job through `mockBqHandler` will keep receiving `RUNNING` from status polling and hang until its context expires, so the shared mock is unsafe for the new polling behavior.

Reviews (1): Last reviewed commit: "fix lint (ifElseChain->switch) and short..." | Re-trigger Greptile

Comment thread pkg/bigquery/db.go
Comment on lines +290 to +292
fresh, err := job.Status(ctx)
if err != nil {
return err

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.

P1 Transient Status Errors Abort Jobs

When a long-running query is still active, any temporary jobs.get failure now returns from waitForJobCompletion and fails the Bruin task. The previous job.Read polling path retried transient BigQuery API errors, so a short 5xx or network blip can now fail a query that would otherwise complete successfully.

Rule Used: What: Comments should be concise and targeted to s... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/bigquery/db.go
Line: 290-292

Comment:
**Transient Status Errors Abort Jobs**

When a long-running query is still active, any temporary `jobs.get` failure now returns from `waitForJobCompletion` and fails the Bruin task. The previous `job.Read` polling path retried transient BigQuery API errors, so a short 5xx or network blip can now fail a query that would otherwise complete successfully.

**Rule Used:** What: Comments should be concise and targeted to s... ([source](https://app.greptile.com/bruin/-/custom-context?memory=6adfbd2b-1b23-4560-9e00-6254e7cb7c70))

How can I resolve this? If you propose a fix, please make it concise.

Comment thread pkg/bigquery/db.go
status = fresh
}
if status.Done() {
return status.Err()

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.

P2 Terminal Errors Lose Shape

Terminal job failures now return status.Err(), which is a BigQuery job error rather than the googleapi.Error shape callers previously received from job.Read. Any caller that classifies BigQuery failures with errors.As for HTTP code or structured API errors can no longer detect those fields for failed jobs.

Rule Used: What: Comments should be concise and targeted to s... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/bigquery/db.go
Line: 297

Comment:
**Terminal Errors Lose Shape**

Terminal job failures now return `status.Err()`, which is a BigQuery job error rather than the `googleapi.Error` shape callers previously received from `job.Read`. Any caller that classifies BigQuery failures with `errors.As` for HTTP code or structured API errors can no longer detect those fields for failed jobs.

**Rule Used:** What: Comments should be concise and targeted to s... ([source](https://app.greptile.com/bruin/-/custom-context?memory=6adfbd2b-1b23-4560-9e00-6254e7cb7c70))

How can I resolve this? If you propose a fix, please make it concise.

Comment thread pkg/bigquery/db_test.go
Comment on lines +541 to +542
case r.Method == http.MethodGet && strings.HasPrefix(r.RequestURI, fmt.Sprintf("/projects/%s/jobs/%s", projectID, jobID)):
write(w, jsr.statusCode, jsr.response) // jobs.get (job.Status)

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.

P2 Running Mock Never Completes

This handler returns the same response for jobs.insert and jobs.get. A test that submits a RUNNING job through mockBqHandler will keep receiving RUNNING from status polling and hang until its context expires, so the shared mock is unsafe for the new polling behavior.

Rule Used: What: Comments should be concise and targeted to s... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/bigquery/db_test.go
Line: 541-542

Comment:
**Running Mock Never Completes**

This handler returns the same response for `jobs.insert` and `jobs.get`. A test that submits a `RUNNING` job through `mockBqHandler` will keep receiving `RUNNING` from status polling and hang until its context expires, so the shared mock is unsafe for the new polling behavior.

**Rule Used:** What: Comments should be concise and targeted to s... ([source](https://app.greptile.com/bruin/-/custom-context?memory=6adfbd2b-1b23-4560-9e00-6254e7cb7c70))

How can I resolve this? If you propose a fix, please make it concise.

Addresses Greptile P3 review note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "test: document mockBqHandler jobs.get/in..." | Re-trigger Greptile

Terminal job failures come back as *bigquery.Error (via job.Status),
whose default string is a verbose struct dump. Handle it in formatError
so callers get the same clean message the googleapi path produces.
Addresses Greptile P2 review note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
pkg/bigquery/db.go:775-776
**Preserve job errors** This converts terminal `*bigquery.Error` values into plain string errors. When a completed job fails with `rateLimitExceeded`, callers can no longer use `errors.As` to read `Reason` or `Location`, so retry and classification logic loses the BigQuery failure details even though the message is cleaner.

Reviews (3): Last reviewed commit: "fix(bigquery): surface clean message for..." | Re-trigger Greptile

Comment thread pkg/bigquery/db.go Outdated
Comment on lines +775 to +776
if errors.As(err, &bqError) && bqError.Message != "" {
return fmt.Errorf("%s", bqError.Message)

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.

P1 Preserve job errors This converts terminal *bigquery.Error values into plain string errors. When a completed job fails with rateLimitExceeded, callers can no longer use errors.As to read Reason or Location, so retry and classification logic loses the BigQuery failure details even though the message is cleaner.

Rule Used: What: Comments should be concise and targeted to s... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/bigquery/db.go
Line: 775-776

Comment:
**Preserve job errors** This converts terminal `*bigquery.Error` values into plain string errors. When a completed job fails with `rateLimitExceeded`, callers can no longer use `errors.As` to read `Reason` or `Location`, so retry and classification logic loses the BigQuery failure details even though the message is cleaner.

**Rule Used:** What: Comments should be concise and targeted to s... ([source](https://app.greptile.com/bruin/-/custom-context?memory=6adfbd2b-1b23-4560-9e00-6254e7cb7c70))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Wrap the terminal *bigquery.Error in jobError, which presents a clean
message via Error() but Unwraps to the original, so callers can still
errors.As it for Reason/Location. Addresses Greptile review note on the
previous stringify.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Reviews (4): Last reviewed commit: "fix(bigquery): preserve structured job e..." | Re-trigger Greptile

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.

1 participant