mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-08 18:57:43 +02:00
[eric] webapp-cache: validate-or-nuke half-installed warm cache, heal off the hot path
This commit is contained in:
@@ -319,13 +319,21 @@ def _ensure_warm_cache() -> str | None:
|
||||
with _warm_cache_lock:
|
||||
if _warm_cache_is_complete(cache_modules):
|
||||
return cache_modules
|
||||
# A node_modules that exists but flunks the completeness check is a
|
||||
# half-finished install; wipe it so the rebuild below starts on clean
|
||||
# ground instead of layering onto a broken tree.
|
||||
if os.path.isdir(cache_modules):
|
||||
shutil.rmtree(cache_modules, ignore_errors=True)
|
||||
# Fast path: pre-built archive shipped inside the release. The
|
||||
# build script generates this so users hitting OpenSwarm for the
|
||||
# first time skip the ~22 s live `npm install`. Falls through on
|
||||
# any failure so dev installs (no archive) keep working.
|
||||
if _try_extract_bundled_archive(cache_dir, _warm_cache_digest()):
|
||||
logger.info("webapp-template: warm cache ready from bundled archive")
|
||||
return cache_modules
|
||||
if _warm_cache_is_complete(cache_modules):
|
||||
logger.info("webapp-template: warm cache ready from bundled archive")
|
||||
return cache_modules
|
||||
# Archive unpacked a tree without the launch bin; don't trust it.
|
||||
shutil.rmtree(cache_modules, ignore_errors=True)
|
||||
try:
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
# Copy package.json + lockfile (if it exists) into the cache
|
||||
@@ -370,6 +378,10 @@ def _ensure_warm_cache() -> str | None:
|
||||
(result.stderr or "")[-1500:],
|
||||
)
|
||||
return None
|
||||
# Never hand back a tree the workspace can't actually launch from.
|
||||
if not _warm_cache_is_complete(cache_modules):
|
||||
logger.warning("webapp-template: warm-cache install left no .bin/vite; not caching")
|
||||
return None
|
||||
return cache_modules
|
||||
except Exception as exc:
|
||||
logger.warning("webapp-template warm-cache failed: %s", exc)
|
||||
@@ -549,7 +561,7 @@ def warm_cache_in_background() -> None:
|
||||
global _warm_cache_thread
|
||||
if _warm_cache_thread is not None and _warm_cache_thread.is_alive():
|
||||
return
|
||||
node_done = os.path.isdir(os.path.join(_warm_cache_dir(), "node_modules"))
|
||||
node_done = _warm_cache_is_complete(os.path.join(_warm_cache_dir(), "node_modules"))
|
||||
venv_done = os.path.isfile(os.path.join(_warm_venv_dir(), ".populated"))
|
||||
if node_done and venv_done:
|
||||
return
|
||||
|
||||
@@ -40,17 +40,18 @@ fi
|
||||
# Fast path: the seeder usually symlinks node_modules to a shared warm
|
||||
# cache (~/.openswarm/cache/webapp_template_node_modules/<hash>), so the
|
||||
# dependency install has already been done once and we can skip straight
|
||||
# to vite. Only run npm install when node_modules is genuinely missing
|
||||
# or empty — e.g. a workspace seeded before the warm-cache existed, or
|
||||
# the user's cache was cleared.
|
||||
# A non-empty node_modules is NOT proof of a finished install.
|
||||
# non-empty -> skip" check then trusted that, and `npm run dev` died with
|
||||
# `vite: command not found`. Gate on the bin we actually launch with so a
|
||||
# broken/partial tree self-heals via install instead of being skipped.
|
||||
#So thats why i explicitly have "/.bin/vite"
|
||||
# to vite. A non-empty node_modules is NOT proof of a finished install
|
||||
# (npm links .bin/* last, so a killed install leaves trees but no bin and
|
||||
# vite dies with "command not found"); gate on the bin we actually launch.
|
||||
if [ -e node_modules/.bin/vite ]; then
|
||||
echo "Dependencies already present - skipping install."
|
||||
else
|
||||
# Incomplete tree. If node_modules is a SYMLINK to the shared warm cache,
|
||||
# never install through it: that writes into the cache every other app
|
||||
# shares (corruption) and stampedes when several apps boot at once. Drop
|
||||
# the link and install a private tree so this app heals alone while the
|
||||
# backend's background warmer rebuilds the shared cache for everyone else.
|
||||
[ -L node_modules ] && rm -f node_modules
|
||||
echo "Installing dependencies..."
|
||||
"$NPM" install --prefer-offline --no-audit --no-fund
|
||||
fi
|
||||
|
||||
@@ -31,3 +31,38 @@ def test_no_bundled_tree_returns_none(monkeypatch, tmp_path):
|
||||
# caller falls through to the .tar.gz extract or live npm.
|
||||
monkeypatch.setattr(vt, "_BUNDLED_ARCHIVE_DIR", str(tmp_path / "empty"))
|
||||
assert vt._bundled_extracted_modules() is None
|
||||
|
||||
|
||||
def test_warm_cache_is_complete_requires_launch_bin(tmp_path):
|
||||
# A package tree on disk is NOT a finished install; the .bin/vite launch
|
||||
# shim is what proves npm finished its bin-linking phase.
|
||||
nm = tmp_path / "node_modules"
|
||||
(nm / "vite" / "bin").mkdir(parents=True)
|
||||
(nm / "vite" / "bin" / "vite.js").write_text("// vite")
|
||||
assert vt._warm_cache_is_complete(str(nm)) is False
|
||||
bindir = nm / ".bin"
|
||||
bindir.mkdir()
|
||||
(bindir / "vite").symlink_to("../vite/bin/vite.js")
|
||||
assert vt._warm_cache_is_complete(str(nm)) is True
|
||||
|
||||
|
||||
def test_ensure_warm_cache_wipes_partial_and_never_returns_incomplete(monkeypatch, tmp_path):
|
||||
# A half-finished cache (package tree present, .bin/vite missing) must be
|
||||
# WIPED and never handed back, so no workspace symlinks to an unlaunchable
|
||||
# tree and run.sh is never pushed into installing through the shared cache.
|
||||
digest = vt._warm_cache_digest()
|
||||
home = tmp_path / "home"
|
||||
monkeypatch.setenv("OPENSWARM_WEBAPP_CACHE_DIR", str(home))
|
||||
cache_modules = home / digest / "node_modules"
|
||||
(cache_modules / "vite" / "bin").mkdir(parents=True)
|
||||
(cache_modules / "vite" / "bin" / "vite.js").write_text("// vite")
|
||||
assert vt._warm_cache_is_complete(str(cache_modules)) is False
|
||||
|
||||
# No bundled tree, no archive, no npm: the only honest answer is "not ready"
|
||||
# (None), and the broken tree must be gone, not cached for the next caller.
|
||||
monkeypatch.setattr(vt, "_BUNDLED_ARCHIVE_DIR", str(tmp_path / "noresources"))
|
||||
monkeypatch.setattr(vt, "_try_extract_bundled_archive", lambda *a, **k: False)
|
||||
monkeypatch.setattr(vt, "_resolve_npm", lambda: None)
|
||||
|
||||
assert vt._ensure_warm_cache() is None
|
||||
assert not cache_modules.exists()
|
||||
|
||||
Reference in New Issue
Block a user