mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] apps: the vite boot lock is loop-local here too, and the serve-static spawn test stops mocking a sync method with an async one
This commit is contained in:
@@ -46,12 +46,13 @@ from backend.apps.outputs.runtime_proc import (
|
||||
suspend_process_tree,
|
||||
write_env_value,
|
||||
)
|
||||
from backend.config.loop_local import loop_local
|
||||
from backend.config.paths import AUTH_TOKEN_FILE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Module-level lock so only ONE vite optimizeDeps runs at a time; must be acquired before manager.p_lock to avoid deadlock with manager.attach.
|
||||
p_vite_boot_lock = asyncio.Lock()
|
||||
get_vite_boot_lock = loop_local(asyncio.Lock)
|
||||
|
||||
# How often the manager stats each attached workspace for a restart sentinel; one stat/sec/runtime is noise.
|
||||
RESTART_SENTINEL_POLL_SECONDS = 1.0
|
||||
@@ -217,7 +218,7 @@ class AppRuntime:
|
||||
exists so the Terminal pane can host `[FRONTEND]` lines.
|
||||
|
||||
New-mode spawns are serialized through the module-level
|
||||
`p_vite_boot_lock` (see comment at the lock declaration) so a
|
||||
`the vite boot lock` (see comment at the lock declaration) so a
|
||||
burst of "create 3 apps in 5 seconds" doesn't trigger 3 parallel
|
||||
MUI pre-bundle runs each pegging a core.
|
||||
"""
|
||||
@@ -228,7 +229,8 @@ class AppRuntime:
|
||||
self.p_reset_terminal_log()
|
||||
if self.is_new_mode:
|
||||
# Acquire the module-level boot lock BEFORE the spawn so only one new-mode workspace is mid-bundle at a time. The lock is released by the bind-poll task the moment vite emits "frontend ready" (or its 180s timeout fires), which is the moment the next workspace can start its own vite without competing for the same CPU. See `p_await_frontend_bind` for the release.
|
||||
await p_vite_boot_lock.acquire()
|
||||
p_boot_lock = get_vite_boot_lock()
|
||||
await p_boot_lock.acquire()
|
||||
# Releasing is the DEFAULT; only an actual spawn hands the lock to its bind-poll task.
|
||||
# The old shape released on `not ok`, so the serve-static branch (which returns True
|
||||
# without spawning) held the global lock forever and every later app blocked on the
|
||||
@@ -239,7 +241,7 @@ class AppRuntime:
|
||||
return await self.p_start_new_mode()
|
||||
finally:
|
||||
if not self.p_boot_lock_handed_off:
|
||||
p_vite_boot_lock.release()
|
||||
p_boot_lock.release()
|
||||
return await self.p_start_old_mode()
|
||||
|
||||
async def p_start_new_mode(self) -> bool:
|
||||
@@ -370,7 +372,7 @@ class AppRuntime:
|
||||
shows the transition; flips `_frontend_ready` which the
|
||||
`frontend_url` property reads.
|
||||
|
||||
Also responsible for releasing the module-level `p_vite_boot_lock`
|
||||
Also responsible for releasing the module-level `the vite boot lock`
|
||||
; every exit path (success, process death, hard timeout) MUST
|
||||
release exactly once so the next queued workspace can start its
|
||||
own vite spawn. A try/finally on the lock guarantees that even
|
||||
@@ -384,7 +386,7 @@ class AppRuntime:
|
||||
return
|
||||
lock_released = True
|
||||
try:
|
||||
p_vite_boot_lock.release()
|
||||
get_vite_boot_lock().release()
|
||||
except RuntimeError:
|
||||
# Lock already released (e.g. start() failure path released synchronously before spawning the poll task).
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""One asyncio primitive per event loop, so module-level state can never outlive the loop it used."""
|
||||
|
||||
import asyncio
|
||||
from typing import Callable, Optional, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def loop_local(factory: Callable[[], T]) -> Callable[[], T]:
|
||||
"""Wrap an asyncio primitive so it is rebuilt whenever the running event loop changes.
|
||||
|
||||
A module-level ``asyncio.Lock()`` outlives the loop that used it. If that loop dies while the
|
||||
lock is HELD, the flag stays set forever and the next loop waits on a release that can never
|
||||
come: no error, no log line, just a process that stops. That is what wedged the entire backend
|
||||
test suite (ENG-219), and after a ``uvicorn --reload`` it is the same silent hang in the app.
|
||||
A Semaphore loses its count the same way; an Event raises "bound to a different event loop"
|
||||
and kills whatever loop was driving it.
|
||||
|
||||
Pass the CLASS, not an instance, and call the result::
|
||||
|
||||
p_boot_lock = loop_local(asyncio.Lock)
|
||||
|
||||
async with p_boot_lock():
|
||||
...
|
||||
|
||||
Needs a running loop, which is the whole point: nothing else can say which loop to build for.
|
||||
"""
|
||||
held: Optional[T] = None
|
||||
held_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
def get() -> T:
|
||||
nonlocal held, held_loop
|
||||
running = asyncio.get_running_loop()
|
||||
if held is None or held_loop is not running:
|
||||
held = factory()
|
||||
held_loop = running
|
||||
return held
|
||||
|
||||
return get
|
||||
@@ -50,7 +50,9 @@ def test_start_skips_serve_when_edited(tmp_path, monkeypatch):
|
||||
rt = AppRuntime("ws-t2", ws)
|
||||
spawned = {"n": 0}
|
||||
|
||||
async def p_no_spawn(env):
|
||||
# Sync on purpose: p_resolve_launch became a plain method, and an async mock here returns a
|
||||
# never-awaited coroutine whose body (the counter) never runs, failing the assert at 0.
|
||||
def p_no_spawn(env):
|
||||
spawned["n"] += 1
|
||||
return None, ws, "stub"
|
||||
monkeypatch.setattr(rt, "p_resolve_launch", p_no_spawn)
|
||||
|
||||
@@ -11,12 +11,7 @@ each non-spawning exit, which is the invariant, rather than asserting the shape
|
||||
import asyncio
|
||||
from typing import Any, List
|
||||
import pytest
|
||||
from backend.apps.outputs import runtime as p_runtime
|
||||
from backend.apps.outputs.runtime import AppRuntime
|
||||
|
||||
|
||||
def get_vite_boot_lock():
|
||||
return p_runtime.p_vite_boot_lock
|
||||
from backend.apps.outputs.runtime import AppRuntime, get_vite_boot_lock
|
||||
|
||||
|
||||
def p_make_runtime(tmp_path: Any, name: str = "ws") -> AppRuntime:
|
||||
|
||||
Reference in New Issue
Block a user