Skip to content

Commit bdfe080

Browse files
committed
feat(loader, daemon): Implement strict ABI contamination tracking and worker isolation
This commit introduces a comprehensive overhaul of the worker daemon and loader to prevent ABI (Application Binary Interface) conflicts and ensure process isolation, dramatically improving stability when switching between versions of C-extension-heavy packages like TensorFlow, NumPy, and PyTorch. Previously, reusing a worker process that had already loaded an ABI package (e.g., `numpy==1.24.3`) for a task requiring an incompatible version (e.g., `numpy==2.0.0`) could lead to segmentation faults or silent data corruption, as the OS linker would not reload the underlying `.so` file. This change implements a robust system to detect, manage, and prevent this "ABI contamination." **Key Changes & Improvements:** * **ABI Contamination Tracking & Eviction:** * Workers now track whether they have loaded an ABI-sensitive package (like TensorFlow, JAX, NumPy, etc.) and report this "contaminated" state back to the daemon. * The daemon will now automatically evict contaminated workers instead of returning them to the idle pool, ensuring a fresh, clean process is always used for tasks that could have conflicting ABI requirements. * **Robust JAX/TensorFlow Dependency Hot-Patching:** * The worker now intelligently detects broken or partially-initialized imports (specifically the `tensorflow -> jax -> ml_dtypes` conflict). * Instead of failing, it now purges the broken module and injects a functional stub in its place, allowing TensorFlow 2.13+ to load successfully even with an older `ml_dtypes` present in the environment. * **Centralized & Atomic Filesystem Locking:** * Replaced all manual `shutil.move` and disparate `filelock` logic with a new centralized, re-entrant locking mechanism (`fs_lock_queue`). * All package "cloaking" and "uncloaking" operations are now atomic and process-safe, eliminating filesystem race conditions between concurrent loaders. * **Proactive ABI Conflict Delegation:** * The `omnipkgLoader` can now detect a potential ABI conflict *before* activating a package (e.g., sees `numpy.core._multiarray_umath` is already mapped). * It raises a `ProcessCorruptedException`, which is caught to automatically delegate the task to a clean, ephemeral daemon worker (`run_once` mode), seamlessly avoiding the in-process crash. * **Massive Daemon Performance Boost (Overlay Fast Path):** * Nested `omnipkgLoader` calls inside a daemon worker now use a highly optimized "fast path." * It bypasses all filesystem cloaking and locking, simply swapping `sys.path` entries in-memory. This reduces nested activation overhead from 100+ms to <1ms. * **Enhanced Cleanup and Diagnostics:** * The loader's exit (`__exit__`) logic is now far more aggressive, performing deep scans for "zombie" modules and stale bubble paths left in `sys.path` to ensure a clean state. * Added a new `_prof` helper and enhanced test output for better timing and debugging of worker initialization. Modified: • src/omnipkg/core.py (+31/-12 lines) • src/omnipkg/isolation/patchers.py (+1/-1 lines) • src/omnipkg/isolation/worker_daemon.py (+164/-23 lines) • src/omnipkg/loader.py (+1856/-455 lines) • src/tests/test_tensorflow_switching.py (+18/-4 lines) [gitship-generated]
1 parent 41271d8 commit bdfe080

5 files changed

Lines changed: 2082 additions & 507 deletions

File tree

src/omnipkg/core.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,19 @@
8888
_DBG = os.environ.get("OMNIPKG_DEBUG", "0") == "1"
8989

9090
def _dbg(msg: str):
91-
"""Lightweight debug printer; no-op unless OMNIPKG_DEBUG=1."""
92-
if _DBG:
93-
print(f"[DEBUG-CORE] {msg}", file=sys.stderr, flush=True)
91+
if _DBG:
92+
print(f"[DEBUG-CORE] {msg}", file=sys.stderr, flush=True)
93+
94+
# ── PROFILE HELPER ───────────────────────────────────────────────────────────
95+
# Set UV_FFI_PROFILE=1 in environment to see timing info.
96+
# ─────────────────────────────────────────────────────────────────────────────
97+
_PROF = os.environ.get("UV_FFI_PROFILE", "0") == "1"
98+
99+
def _prof(msg: str):
100+
"""Lightweight timing printer; no-op unless UV_FFI_PROFILE=1."""
101+
if _PROF:
102+
# We use stderr so it doesn't pollute actual program output
103+
print(f"[TIMING] {msg}", file=sys.stderr, flush=True)
94104

95105
def _get_dynamic_omnipkg_version():
96106
"""
@@ -5825,13 +5835,13 @@ def _create_bubble_manifest(
58255835
manifest_path = bubble_path / ".omnipkg_manifest.json"
58265836

58275837
# --- ADDED EXPLICIT PATH PRINTING ---
5828-
print(f" [MANIFEST] Writing schema 2.0 manifest to: {manifest_path}")
5829-
5838+
_dbg(f"Writing schema 2.0 manifest to: {manifest_path}")
58305839
try:
58315840
with open(manifest_path, "w") as f:
58325841
json.dump(manifest_data, f, indent=2)
5833-
print(f" [MANIFEST] ✅ Successfully wrote {len(json.dumps(manifest_data))} bytes.")
5842+
_dbg(f"Successfully wrote {len(json.dumps(manifest_data))} bytes to manifest.")
58345843
except Exception as e:
5844+
# THIS IS THE CORRECTED PART - We actually print the critical error
58355845
print(f" [MANIFEST] ❌ CRITICAL ERROR writing manifest: {e}")
58365846

58375847
try:
@@ -17126,11 +17136,11 @@ def _get_latest_version_from_pypi(
1712617136
safe_print(
1712717137
f" -> Finding latest COMPATIBLE version for '{package_name}' using background caching..."
1712817138
)
17129-
print(f"[TIMING] after safe_print: {(_t.perf_counter()-_t0)*1000:.1f}ms", flush=True)
17139+
_prof(f"after safe_print: {(_t.perf_counter()-_t0)*1000:.1f}ms")
1713017140
import requests as http_requests
17131-
print(f"[TIMING] after import requests: {(_t.perf_counter()-_t0)*1000:.1f}ms", flush=True)
17141+
_prof(f"after import requests: {(_t.perf_counter()-_t0)*1000:.1f}ms")
1713217142
py_context = python_context_version or self.current_python_context
17133-
print(f"[TIMING] after py_context: {(_t.perf_counter()-_t0)*1000:.1f}ms", flush=True)
17143+
_prof(f"after py_context: {(_t.perf_counter()-_t0)*1000:.1f}ms")
1713417144
py_context = python_context_version or self.current_python_context
1713517145

1713617146
if not hasattr(self, "pypi_cache"):
@@ -17139,10 +17149,19 @@ def _get_latest_version_from_pypi(
1713917149
cached_version = self.pypi_cache.get_cached_version(package_name, py_context)
1714017150
if cached_version:
1714117151
import time as _t
17142-
_t0 = _t.perf_counter(); stale = self.pypi_cache.is_cache_entry_stale(package_name, py_context, max_age_seconds=3600); print(f"[TIMING] is_cache_entry_stale: {(_t.perf_counter()-_t0)*1000:.1f}ms stale={stale}", flush=True)
17152+
_t0 = _t.perf_counter()
17153+
stale = self.pypi_cache.is_cache_entry_stale(
17154+
package_name,
17155+
py_context,
17156+
max_age_seconds=3600
17157+
)
17158+
_prof(f"is_cache_entry_stale: {(_t.perf_counter()-_t0)*1000:.1f}ms stale={stale}")
17159+
1714317160
if stale:
17144-
_t0 = _t.perf_counter(); self._start_background_cache_refresh(package_name, py_context); print(f"[TIMING] _start_background_cache_refresh: {(_t.perf_counter()-_t0)*1000:.1f}ms", flush=True)
17145-
return cached_version
17161+
# 2. Background refresh with hidden timing
17162+
_t0 = _t.perf_counter()
17163+
self._start_background_cache_refresh(package_name, py_context)
17164+
_prof(f"_start_background_cache_refresh: {(_t.perf_counter()-_t0)*1000:.1f}ms")
1714617165

1714717166
# USE THE ROBUST TEST INSTALLATION APPROACH FIRST
1714817167
safe_print(f" 🧪 Using pip to find latest compatible version for Python {py_context}...")

src/omnipkg/isolation/patchers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ class DummyBackend:
282282
# ═══════════════════════════════════════════════════════════
283283
module = _original_import_func(name, globals, locals, fromlist, level)
284284

285-
if is_tf_import and module:
285+
if is_tf_import and module and name == "tensorflow":
286286
import os
287287
_tf_loaded_pids.add(os.getpid()) # Mark THIS worker as having loaded TF
288288
_patch_numpy_for_tf_recursion()
@@ -611,4 +611,4 @@ def _force_complete_initialization(module_name):
611611
try:
612612
importlib.import_module(submod)
613613
except ImportError:
614-
continue
614+
continue

0 commit comments

Comments
 (0)