Skip to content

fix: enforce --timeout as an idle limit without busy-waiting - #184

Closed
hohieuai wants to merge 1 commit into
googlecolab:mainfrom
hohieuai:fix/no-default-timeout
Closed

hohieuai wants to merge 1 commit into
googlecolab:mainfrom
hohieuai:fix/no-default-timeout

Conversation

@hohieuai

@hohieuai hohieuai commented Oct 24, 2026 •

Copy link
Copy Markdown

Fixes #183.

Summary

--timeout now works the way its design describes. It is an idle threshold: an execution fails only after that many seconds without output, so code that keeps printing is never cut off. While it waits, the local client no longer busy-loops. The flag, its 30-second default, and the TimeoutError message are unchanged.

Background

#38 added --timeout so users can "increase the idle execution threshold" for tasks that produce no output, and the docs describe it as letting long-running silent tasks run "without being prematurely killed". Runtime.execute_code, however, passed the value to jupyter-kernel-client's execute_interactive, which treats it as a deadline for the whole run. The measurements in #183 show the result:

Changes

  • runtime.py: for streaming execution without stdin (colab run, colab exec, colab repl), execute_code now waits for kernel messages itself, through the websocket client's public API (execute, get_iopub_msg, get_shell_msg).
    • It waits in blocking slices of up to one second.
    • Each message from this execution resets the idle clock. Messages from other requests don't count.
    • With no message for timeout seconds, it raises the same TimeoutError("Timeout waiting for output"). None waits without a limit.
    • It raises RuntimeError("Connection was lost.") if the websocket drops, which the old loop only noticed once a message arrived.
  • Unchanged paths: executions that use stdin (the auth and Drive automations, with their explicit 120-second timeout) and the non-streaming execute path keep using the library as before.
  • Tests:
    • The existing tests keep their names and what they assert: test_colab_runtime_execute_interactive_with_timeout and the two tests in test_streaming.py. They now drive MockKernelClient (tests/mock_kernel.py), a mock of the websocket client that replays scripted kernel messages on a simulated clock, instead of mocking execute_interactive, because the streaming path no longer calls it. For example, the timeout test still checks that timeout=600 reaches that branch. A 599-second silence completes, and a 601-second silence raises TimeoutError.
    • New tests in test_streaming.py cover:
      • a 10-minute run that prints every 20 seconds and is not cut off;
      • a timeout 30 seconds after the last message, not after the start;
      • no limit with None;
      • waits that are always bounded and non-zero;
      • messages from other requests, which don't count as output;
      • a lost connection.
  • docs/: dated log entries in both design docs.

Testing

  • uv run pytest: 366 passed, 19 skipped. Ruff check and format pass on the changed files.

  • I ran the branch on real CPU runtimes with jupyter-kernel-client 0.8.0 (Google's fork, via tool.uv.sources), 0.9.0 (the PyPI pin), and 1.0.2:

    Case --timeout Result
    colab run, prints every 5 s for 60 s (0.8.0, 0.9.0, 1.0.2) default (30) 12/12 steps, exit 0. The client used 0.0–0.1% CPU at 40 s (before: 98–99% with 0.9.0, and a failure after 6 steps with 1.0.2)
    colab run, silent for 45 s default (30) TimeoutError: Timeout waiting for output, exit 1, session stopped
    colab run, silent for 45 s 60 Completes, exit 0
    colab exec on an existing session, prints every 5 s for 60 s default (30) Completes, exit 0, 0.0% CPU, 0.28 s of CPU time in 71 s
    colab exec, silent for 45 s default (30) TimeoutError: Timeout waiting for output, exit 1
    Ctrl-C 40 s into a silent colab run 600 Exits 1.8 s after SIGINT, exit code 130, session stopped

Notes

@google-cla

google-cla Bot commented Oct 24, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@hohieuai
hohieuai force-pushed the fix/no-default-timeout branch from af354d4 to f033214 Compare October 24, 2026 00:48
@hohieuai hohieuai changed the title fix: wait for run and exec to finish instead of a 30-second deadline fix: enforce --timeout as an idle limit without busy-waiting Oct 24, 2026
@hohieuai
hohieuai force-pushed the fix/no-default-timeout branch 3 times, most recently from 259555c to 71054ce Compare October 24, 2026 00:56
`--timeout` (default 30 s) is meant as an idle threshold, so that
"long-running silent tasks" can raise it and code that keeps printing is
never cut off. Runtime.execute_code passed it to jupyter-kernel-client's
execute_interactive, where it is a deadline for the whole run. Past that
deadline, 0.9.0 polls without waiting and holds a CPU core at ~100%
until the run ends; 1.0.2 raises TimeoutError even while output flows.

For streaming execution without stdin (run, exec, repl), the runtime now
waits for kernel messages itself through the client's public API
(execute, get_iopub_msg, get_shell_msg), in blocking one-second slices.
It raises the same TimeoutError("Timeout waiting for output") once
`timeout` seconds pass with no message from the execution, and
RuntimeError if the connection drops. The default and the flag are
unchanged; executions that use stdin keep the library path.

Fixes googlecolab#183
@hohieuai
hohieuai force-pushed the fix/no-default-timeout branch from 71054ce to f23e38a Compare October 24, 2026 00:56
@hohieuai

Copy link
Copy Markdown
Author

Superseded by #185, which contains the same fix as a single commit with an updated description.

@hohieuai hohieuai closed this Oct 24, 2026
@hohieuai
hohieuai deleted the fix/no-default-timeout branch October 24, 2026 01:00
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.

Default --timeout 30 caps the whole run: every longer colab run busy-loops a CPU core today, and fails once jupyter-kernel-client is upgraded

1 participant