feat(daemon): implement reload, fix PID race condition
- PluginSupervisor.reload(): cancels all running plugin tasks, resets restart counters, and re-creates them with fresh coroutines - IPC reload command now calls supervisor.reload() instead of being a stub - run_foreground(): wrap PID file acquisition in try/except PidFileError to produce a clean error if two daemon starts race on the PID file Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+22
-2
@@ -68,6 +68,22 @@ class PluginSupervisor:
|
|||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.gather(*tasks, return_exceptions=True)
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
async def reload(self) -> None:
|
||||||
|
"""Cancel all running tasks and restart them with fresh coroutines."""
|
||||||
|
for record in self._records:
|
||||||
|
if record.task and not record.task.done():
|
||||||
|
record.task.cancel()
|
||||||
|
try:
|
||||||
|
await record.task
|
||||||
|
except (asyncio.CancelledError, Exception):
|
||||||
|
pass
|
||||||
|
record.restart_count = 0
|
||||||
|
record.last_error = None
|
||||||
|
record.task = asyncio.create_task(
|
||||||
|
self._supervise(record), name=record.name
|
||||||
|
)
|
||||||
|
_log.info("Reloaded %d plugin task(s).", len(self._records))
|
||||||
|
|
||||||
def status(self) -> list[dict]:
|
def status(self) -> list[dict]:
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -131,8 +147,8 @@ def _make_ipc_handler(supervisor: PluginSupervisor):
|
|||||||
supervisor.request_shutdown()
|
supervisor.request_shutdown()
|
||||||
return {"ok": True, "data": {}}
|
return {"ok": True, "data": {}}
|
||||||
case "reload":
|
case "reload":
|
||||||
_log.info("Reload requested via IPC.")
|
await supervisor.reload()
|
||||||
return {"ok": True, "data": {}}
|
return {"ok": True, "data": {"tasks_reloaded": len(supervisor._records)}}
|
||||||
case _:
|
case _:
|
||||||
return {"ok": False, "data": {"error": f"unknown command: {cmd}"}}
|
return {"ok": False, "data": {"error": f"unknown command: {cmd}"}}
|
||||||
|
|
||||||
@@ -196,6 +212,7 @@ def run_foreground() -> None:
|
|||||||
|
|
||||||
_start_time = time.monotonic()
|
_start_time = time.monotonic()
|
||||||
|
|
||||||
|
try:
|
||||||
with pid_file:
|
with pid_file:
|
||||||
_log.info("Pyra daemon starting (PID %d).", os.getpid())
|
_log.info("Pyra daemon starting (PID %d).", os.getpid())
|
||||||
try:
|
try:
|
||||||
@@ -203,6 +220,9 @@ def run_foreground() -> None:
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
_log.info("Pyra daemon stopped.")
|
_log.info("Pyra daemon stopped.")
|
||||||
|
except PidFileError as exc:
|
||||||
|
_log.error("Could not acquire PID file: %s", exc)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
# ── Background spawn (pyra daemon start) ─────────────────────────────────────
|
# ── Background spawn (pyra daemon start) ─────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user