馃敶 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:
uv pip install "google-adk[gcp]==2.9.0"
- 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.
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?:
馃敶 Required Information
Describe the Bug:
execute_sqlin the BigQuery toolset is a plaindef, and the BigQuery client it calls blocks (src/google/adk/integrations/bigquery/query_tool.py, L341 and the_execute_sqlhelper 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_sqlis async too. BigQuery still has noasyncio.to_threadanywhere.Steps to Reproduce:
uv pip install "google-adk[gcp]==2.9.0"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.python repro.pyExpected 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:
The gap is the query duration.
Environment Details:
mainat 7ae1c9b, still sync there.uv venv --python "python3.11"+uv sync --all-extras)Model Information:
馃煛 Optional Information
Regression:
No, it has always been sync.
Additional Context:
Same fix as Spanner: make
execute_sqla coroutine and push_execute_sqlinto a thread._execute_sqlhas to stay sync becauseforecast,analyze_contributionanddetect_anomaliescall 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:
How often has this issue occurred?: