Skip to content

BigQuery execute_sql blocks the event loop for the whole query#7160

Description

@yumizu-da

馃敶 Required Information

Describe the Bug:
execute_sql in the BigQuery toolset is a plain def, and the BigQuery client it calls blocks (src/google/adk/integrations/bigquery/query_tool.py, L341 and the _execute_sql helper it delegates to at L169). ADK awaits a sync tool inline on the running loop, so nothing else in the process runs until the query comes back. On a server handling more than one session, the others just wait.

Spanner hit the same thing and was fixed in 1dbcecc. Bigtable's execute_sql is async too. BigQuery still has no asyncio.to_thread anywhere.

Steps to Reproduce:

  1. uv pip install "google-adk[gcp]==2.9.0"
  2. Save the script below (also under Minimal Reproduction Code) as repro.py. It replaces the BigQuery client with one that takes 2 seconds, so no credentials or real dataset are needed, and counts how many times a 100 ms heartbeat task gets to run during the query.
  3. python repro.py

Expected Behavior:
The heartbeat keeps ticking every 100 ms while the query is in flight.

Observed Behavior:
It doesn't tick at all until the query returns:

heartbeat ran 3 times
longest gap between heartbeats: 2.108s

The gap is the query duration.

Environment Details:

  • ADK Library Version (pip show google-adk): 2.9.0. Also checked main at 7ae1c9b, still sync there.
  • Desktop OS: macOS 25.6.0 (arm64)
  • Python Version (python -V): 3.11.12, in a uv venv set up per the contribution guide (uv venv --python "python3.11" + uv sync --all-extras)

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: N/A, no model involved

馃煛 Optional Information

Regression:
No, it has always been sync.

Additional Context:
Same fix as Spanner: make execute_sql a coroutine and push _execute_sql into a thread. _execute_sql has to stay sync because forecast, analyze_contribution and detect_anomalies call it directly. Those three block the loop as well, but that's a separate change.

I have a patch and tests ready, so please assign this to me.

Minimal Reproduction Code:

import asyncio
import time
from unittest import mock

from google.adk.integrations.bigquery import query_tool
from google.adk.integrations.bigquery.config import BigQueryToolConfig
from google.adk.tools.tool_context import ToolContext
from google.auth.credentials import Credentials
from google.cloud import bigquery

QUERY_SECONDS = 2.0


async def heartbeat(stop):
    ticks = []
    start = time.monotonic()
    while not stop.is_set():
        await asyncio.sleep(0.1)
        ticks.append(time.monotonic() - start)
    return ticks


def slow_query_and_wait(*args, **kwargs):
    time.sleep(QUERY_SECONDS)
    return [{"num": 123}]


async def main():
    credentials = mock.create_autospec(Credentials, instance=True)
    tool_context = mock.create_autospec(ToolContext, instance=True)
    settings = BigQueryToolConfig()

    with mock.patch.object(bigquery, "Client", autospec=True) as client:
        bq_client = client.return_value
        query_job = mock.create_autospec(bigquery.QueryJob)
        query_job.statement_type = "SELECT"
        bq_client.query.return_value = query_job
        bq_client.query_and_wait.side_effect = slow_query_and_wait

        stop = asyncio.Event()
        beat = asyncio.create_task(heartbeat(stop))
        await asyncio.sleep(0.3)

        result = query_tool.execute_sql(
            "my_project", "SELECT 123 AS num", credentials, settings,
            tool_context,
        )
        if asyncio.iscoroutine(result):
            result = await result

        stop.set()
        ticks = await beat

    gaps = [b - a for a, b in zip(ticks, ticks[1:])]
    print(f"heartbeat ran {len(ticks)} times")
    print(f"longest gap between heartbeats: {max(gaps):.3f}s")
    print(f"result: {result}")


asyncio.run(main())

How often has this issue occurred?:

  • Always (100%)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

request clarification[Status] The maintainer need clarification or more information from the authortools[Component] This issue is related to tools

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions