From 717f37a6383c536d9414d902e3a3597364b8a930 Mon Sep 17 00:00:00 2001 From: Sprite Date: Tue, 10 Feb 2026 04:58:38 +0000 Subject: [PATCH] Fix process hangs: add timeouts and fix atexit handler conflict - Add timeout=30 to quickstart example command to prevent indefinite blocking on WebSocket hangs - Change checkpoint HTTP client timeout from None to 300s to prevent indefinite blocking on slow API responses - Fix atexit handler to skip cleanup if the event loop was already stopped by loop.py's handler, preventing creation of a new loop during shutdown --- examples/quickstart.py | 2 +- src/sprites/checkpoint.py | 8 ++++---- src/sprites/control.py | 7 ++++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/quickstart.py b/examples/quickstart.py index 4a8f745..33d33eb 100644 --- a/examples/quickstart.py +++ b/examples/quickstart.py @@ -13,7 +13,7 @@ client.create_sprite(os.environ["SPRITE_NAME"]) # step: Run Python -output = client.sprite(os.environ["SPRITE_NAME"]).command("python", "-c", "print(2+2)").output() +output = client.sprite(os.environ["SPRITE_NAME"]).command("python", "-c", "print(2+2)", timeout=30).output() print(output.decode(), end="") # step: Clean up diff --git a/src/sprites/checkpoint.py b/src/sprites/checkpoint.py index a6aaf71..fc2561c 100644 --- a/src/sprites/checkpoint.py +++ b/src/sprites/checkpoint.py @@ -217,9 +217,9 @@ def create_checkpoint(sprite: Sprite, comment: str = "") -> CheckpointStream: if comment: payload["comment"] = comment - # Use a separate client for streaming with no timeout + # Use a separate client for streaming with a generous timeout with httpx.Client( - timeout=None, + timeout=300.0, headers={"Authorization": f"Bearer {sprite.client.token}"}, ) as client: try: @@ -269,9 +269,9 @@ def restore_checkpoint(sprite: Sprite, checkpoint_id: str) -> RestoreStream: """ url = f"{sprite.client.base_url}/v1/sprites/{sprite.name}/checkpoints/{checkpoint_id}/restore" - # Use a separate client for streaming with no timeout + # Use a separate client for streaming with a generous timeout with httpx.Client( - timeout=None, + timeout=300.0, headers={"Authorization": f"Bearer {sprite.client.token}"}, ) as client: try: diff --git a/src/sprites/control.py b/src/sprites/control.py index a93646c..1e7418a 100644 --- a/src/sprites/control.py +++ b/src/sprites/control.py @@ -645,7 +645,12 @@ def _cleanup_on_exit() -> None: return try: - from sprites.loop import get_loop, stop_loop + from sprites.loop import get_loop, stop_loop, _loop + + # Don't create a new loop just for cleanup — if the loop was already + # stopped by loop.py's atexit handler, just skip cleanup + if _loop is None: + return loop = get_loop() future = asyncio.run_coroutine_threadsafe(_close_all_pools(), loop)