Skip to content

Commit 0650638

Browse files
committed
feat: sync worker_daemon, loader, readme from development
1 parent b795e7a commit 0650638

3 files changed

Lines changed: 75 additions & 30 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,9 @@ pip3 install omnipkg==3.3.1
14361436

14371437

14381438

1439+
1440+
1441+
14391442

14401443

14411444

src/omnipkg/isolation/worker_daemon.py

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2924,31 +2924,49 @@ def health_check(self) -> bool:
29242924
return False
29252925

29262926
def force_shutdown(self):
2927-
"""Forcefully shutdown worker."""
2927+
"""Forcefully shutdown worker using a belt-and-suspenders approach."""
29282928
with self.lock:
29292929
if self.process:
29302930
try:
2931-
# First check if already dead (workers that ran run_cli call sys.exit)
2931+
# 1. Check if already dead
29322932
if self.process.poll() is None:
2933-
# Still running — try graceful shutdown
2933+
# Try graceful shutdown via stdin first
29342934
try:
29352935
self.process.stdin.write(json.dumps({"type": "shutdown"}) + "\n")
29362936
self.process.stdin.flush()
29372937
except Exception:
29382938
pass
2939-
self.process.wait(timeout=2)
2940-
except Exception:
2939+
2940+
# Give it a tiny window to exit gracefully
29412941
try:
2942-
if not IS_WINDOWS:
2943-
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
2944-
else:
2945-
self.process.terminate()
2942+
self.process.wait(timeout=1)
29462943
except Exception:
29472944
pass
2945+
except Exception:
2946+
pass
2947+
2948+
# 2. THE NUCLEAR STRIKE
2949+
try:
2950+
if not IS_WINDOWS:
2951+
# Linux: Kill the entire process group
2952+
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
2953+
else:
2954+
# Windows: Step A - Standard Python termination
2955+
self.process.terminate()
2956+
2957+
# Windows: Step B - Recursive Tree Kill via taskkill
2958+
# This nukes the process AND every single child/grandchild it spawned
2959+
subprocess.run(
2960+
["taskkill", "/F", "/T", "/PID", str(self.process.pid)],
2961+
capture_output=True,
2962+
check=False
2963+
)
2964+
except Exception:
2965+
pass
29482966
finally:
29492967
self.process = None
29502968

2951-
# 🔥 FIX: Close log file handle
2969+
# Close handles to release file locks immediately
29522970
if hasattr(self, "log_file") and self.log_file:
29532971
try:
29542972
self.log_file.close()
@@ -5015,8 +5033,8 @@ def _handle_shutdown(self, signum, frame):
50155033
# Shutdown executor first (non-blocking)
50165034
try:
50175035
self.executor.shutdown(wait=False)
5018-
except Exception:
5019-
pass
5036+
except Exception as e:
5037+
self.log(f"[SHUTDOWN] ⚠️ Executor shutdown failed: {e}")
50205038

50215039
# Collect all PIDs before we start killing
50225040
all_pids = []
@@ -5026,8 +5044,8 @@ def _handle_shutdown(self, signum, frame):
50265044
try:
50275045
if worker.process and worker.process.pid:
50285046
all_pids.append(worker.process.pid)
5029-
except Exception:
5030-
pass
5047+
except Exception as e:
5048+
self.log(f" ⚠️ Could not retrieve PID for UV worker {py_exe}: {e}")
50315049

50325050
# Idle CLI workers
50335051
for py_exe, pool in list(getattr(self, "idle_pools", {}).items()):
@@ -5036,8 +5054,8 @@ def _handle_shutdown(self, signum, frame):
50365054
w = pool.get_nowait()
50375055
if w.process and w.process.pid:
50385056
all_pids.append(w.process.pid)
5039-
except Exception:
5040-
pass
5057+
except Exception as e:
5058+
self.log(f" ⚠️ Could not retrieve PID from idle pool {py_exe}: {e}")
50415059

50425060
# Spec workers
50435061
with self.pool_lock:
@@ -5049,13 +5067,14 @@ def _handle_shutdown(self, signum, frame):
50495067
if w and w.process and w.process.pid:
50505068
all_pids.append(w.process.pid)
50515069
info["worker"].force_shutdown()
5052-
except Exception:
5053-
pass
5070+
except Exception as e:
5071+
self.log(f" 🚨 Critical failure during worker force_shutdown: {e}")
50545072

50555073
# Wait up to 2s for graceful termination
50565074
import time as _st
50575075
deadline = _st.time() + 2.0
50585076
remaining = list(all_pids)
5077+
self.log(f"[SHUTDOWN] Waiting for {len(remaining)} processes to exit gracefully...")
50595078
while remaining and _st.time() < deadline:
50605079
_st.sleep(0.05)
50615080
still_alive = []
@@ -5071,23 +5090,36 @@ def _handle_shutdown(self, signum, frame):
50715090
os.kill(pid, 0)
50725091
still_alive.append(pid)
50735092
except (OSError, Exception):
5074-
pass
5093+
# Process is gone - this is the desired outcome
5094+
pass
50755095
remaining = still_alive
50765096

50775097
# Force kill anything still alive
50785098
for pid in remaining:
50795099
try:
50805100
if IS_WINDOWS:
5101+
# 1. THE NUCLEAR STRIKE FIRST
5102+
# We kill the tree while the parent is still alive
5103+
# so Windows can correctly identify and kill all descendants.
5104+
subprocess.run(
5105+
["taskkill", "/F", "/T", "/PID", str(pid)],
5106+
capture_output=True, check=False
5107+
)
5108+
5109+
# 2. THE CLEANUP (Belt and Suspenders)
5110+
# Just in case taskkill missed the root for some bizarre reason
50815111
import ctypes
50825112
handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
50835113
if handle:
50845114
ctypes.windll.kernel32.TerminateProcess(handle, 1)
50855115
ctypes.windll.kernel32.CloseHandle(handle)
50865116
else:
50875117
os.kill(pid, signal.SIGKILL)
5088-
self.log(f" Force-killed straggler PID {pid}")
5089-
except Exception:
5090-
pass
5118+
5119+
self.log(f" Force-killed tree for straggler PID {pid}")
5120+
except Exception as e:
5121+
# 🔥 LOG THE ERROR: Now we know if it was AccessDenied or ProcessNotFound
5122+
self.log(f" 🚨 Failed to kill PID {pid}: {e}")
50915123
# Stop fs_watcher BEFORE cleaning up socket/pid files
50925124
# The watchdog Observer thread is non-daemon and will block sys.exit forever
50935125
# if we don't explicitly stop it here.
@@ -5106,14 +5138,18 @@ def _handle_shutdown(self, signum, frame):
51065138
continue
51075139
try:
51085140
os.unlink(path)
5109-
except OSError:
5110-
pass
5141+
except FileNotFoundError:
5142+
pass # Already gone, no need to log
5143+
except Exception as e:
5144+
# 🔥 LOG THE ERROR: This tells us if a process is still locking the PID file
5145+
self.log(f" ⚠️ Failed to unlink {path}: {e}")
51115146
# Also clean connection announcement file
51125147
try:
51135148
conn_file = os.path.join(tempfile.gettempdir(), "omnipkg", "daemon_connection.txt")
51145149
os.unlink(conn_file)
5115-
except OSError:
5116-
pass
5150+
except Exception as e:
5151+
# 🔥 LOG THE ERROR: Now we can see if the temp folder has permission issues
5152+
self.log(f" ⚠️ Failed to unlink connection file {conn_file}: {e}")
51175153

51185154
@classmethod
51195155
def is_running(cls) -> bool:

src/omnipkg/loader.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ def print_report(self):
170170

171171

172172
class omnipkgLoader:
173+
_dep_cache_built_at = 0.0
174+
_dependency_cache = None
173175
"""
174176
Activates isolated package environments with optional persistent worker pool.
175177
"""
@@ -388,6 +390,7 @@ def _init_own_cache(self):
388390
# Set key prefix
389391
base = config.get("redis_key_prefix", "omnipkg:pkg:").split(":")[0]
390392
self.redis_key_prefix = f"{base}:env_{env_id}:{py_ver}:pkg:"
393+
omnipkgLoader._dep_cache_built_at = time.time()
391394

392395
except Exception as e:
393396
# If cache init fails, set to None (will skip KB optimization)
@@ -403,14 +406,16 @@ def _maybe_refresh_dependency_cache(self):
403406
"""
404407
try:
405408
from omnipkg.isolation.fs_lock_queue import DepCacheSentinel
406-
if sentinel_is_dirty := DepCacheSentinel(self.multiversion_base).is_dirty_since(
409+
sentinel_is_dirty = DepCacheSentinel(self.multiversion_base).is_dirty_since(
407410
omnipkgLoader._dep_cache_built_at
408-
):
411+
)
412+
if sentinel_is_dirty:
409413
if not self.quiet:
410414
safe_print(" ♻️ [cache] FS change detected — refreshing dep cache")
411415
omnipkgLoader._dependency_cache = None
412-
except Exception:
413-
pass
416+
except Exception as e:
417+
# FIXED: Replaced 'pass' with an error message
418+
safe_print(f" ⚠️ [cache] Error occurred while checking dependency cache: {e}")
414419

415420
def _profile_start(self, label):
416421
"""Start timing a profiled section"""
@@ -1025,6 +1030,7 @@ def _compute_omnipkg_dependencies(self) -> Dict[str, Path]:
10251030

10261031
# --- Store the result in the cache for next time ---
10271032
omnipkgLoader._dependency_cache = dependencies
1033+
omnipkgLoader._dep_cache_built_at = time.time()
10281034
return dependencies
10291035

10301036
def _detect_omnipkg_dependencies(self):

0 commit comments

Comments
 (0)