Skip to content

Sync BaseCodeExecutor.execute_code is called directly on the event loop in llm_flows/_code_execution — long executions freeze the whole server #6353

Description

@mhonda-blueish

** Please make sure you read the contribution guide and file the issues in the right place. **

Describe the bug

BaseCodeExecutor.execute_code() has a synchronous contract, and the LLM flow calls it directly on the running event loop:

  • src/google/adk/flows/llm_flows/_code_execution.py_run_pre_processor (call site around L246)
  • src/google/adk/flows/llm_flows/_code_execution.py_run_post_processor (call site around L356)

Both enclosing functions are async, so any executor whose execute_code performs blocking I/O freezes the entire event loop for the duration of the execution.

This is especially severe with AgentEngineSandboxCodeExecutor: a single sandboxes.execute_code call is a blocking HTTP request that regularly takes 2–3+ minutes for document-generation workloads. While it is in flight, the whole ADK FastAPI server (get_fast_api_app / api_server) is unresponsive:

  • /health-style endpoints and every other request stall (we measured 113–127 s of complete unresponsiveness that aligned exactly with in-flight execute_code calls),
  • on Kubernetes this fails liveness probes and the pod gets killed mid-task,
  • SSE streams of other sessions co-hosted on the same process stall as well,
  • in-process timers (e.g. per-step LLM timeouts of other agents) fire spuriously once the loop unfreezes.

To Reproduce

  1. Configure an LlmAgent with any BaseCodeExecutor whose execute_code blocks (e.g. AgentEngineSandboxCodeExecutor with a long-running snippet, or a toy executor with time.sleep(120)).
  2. Serve it with the ADK FastAPI app and trigger a code-execution turn via /run_sse.
  3. While the execution is in flight, request /health (or anything else) — the server does not respond until execute_code returns.

Expected behavior

Long-running code execution should not block the event loop / other requests served by the same process.

Proposed fix

Offload the call at both sites, keeping the executor contract unchanged:

code_execution_result = await asyncio.to_thread(
    code_executor.execute_code,
    invocation_context,
    CodeExecutionInput(...),
)

BuiltInCodeExecutor is not affected (it short-circuits earlier), and executor implementations need no changes. An alternative (larger) fix would be an async contract such as execute_code_async with a default to_thread wrapper around the sync method.

I'm happy to send a PR for the asyncio.to_thread variant if that direction is acceptable.

Desktop

  • OS: Linux (GKE) / macOS
  • Python version: 3.12 / 3.13
  • ADK version: 2.3.0 (call sites verified unchanged on current main)

Model Information

  • Not model-specific.

Metadata

Metadata

Assignees

Labels

core[Component] This issue is related to the core interface and implementation

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions