From 57ee874d6e42a91035bd71e33f044b223c614d81 Mon Sep 17 00:00:00 2001 From: Nick Sweeting Date: Thu, 3 Sep 2026 00:59:06 -0700 Subject: [PATCH] Trust ready OpenCode processes --- archivebox/opencode/views.py | 28 ++++++------------ archivebox/tests/test_opencode_agent.py | 38 ++++--------------------- 2 files changed, 14 insertions(+), 52 deletions(-) diff --git a/archivebox/opencode/views.py b/archivebox/opencode/views.py index 14876093..461141d7 100644 --- a/archivebox/opencode/views.py +++ b/archivebox/opencode/views.py @@ -38,7 +38,6 @@ _PROCESS_READY: subprocess.Popen | None = None _PROCESS_LOCK = threading.Lock() _SESSION_LOCK = threading.Lock() _LOGGER = logging.getLogger(__name__) -_PROCESS_HEALTH_GRACE = 5 _PROXY_PREFIX = "/admin/agent/opencode" _PROXY_PREFIX_REGEX = _PROXY_PREFIX.replace("/", r"\/") _PROXY_PREFIX_NO_SLASH_REGEX = _PROXY_PREFIX.lstrip("/").replace("/", r"\/") @@ -383,11 +382,11 @@ def _owned_process_ready() -> bool: return process is not None and process is _PROCESS and process.poll() is None -def _health(settings: dict, timeout: float = 2) -> bool: +def _health(settings: dict) -> bool: try: response = requests.get( f"{settings['origin']}/global/health", - timeout=timeout, + timeout=2, ) return response.status_code == 200 except requests.RequestException: @@ -413,23 +412,14 @@ def _ensure_opencode(settings: dict) -> tuple[bool, str]: workdir = settings["workdir"].resolve() with _PROCESS_LOCK: - if _owned_process_running(): - deadline = time.monotonic() + min(_PROCESS_HEALTH_GRACE, settings["timeout"]) - while _owned_process_running(): - remaining = deadline - time.monotonic() - if remaining <= 0: - break - if _health(settings, timeout=min(2, remaining)): - if time.monotonic() <= deadline: - _PROCESS_READY = _PROCESS - return True, "" - break - remaining = deadline - time.monotonic() - if remaining > 0: - time.sleep(min(0.25, remaining)) - _stop_owned_process(_PROCESS) - elif _health(settings): + if _owned_process_ready(): return True, "" + if _health(settings): + if _owned_process_running(): + _PROCESS_READY = _PROCESS + return True, "" + if _owned_process_running(): + _stop_owned_process(_PROCESS) try: binary, git_binary, binary_env = _resolve_binary( diff --git a/archivebox/tests/test_opencode_agent.py b/archivebox/tests/test_opencode_agent.py index 65c21b27..0d81c15d 100644 --- a/archivebox/tests/test_opencode_agent.py +++ b/archivebox/tests/test_opencode_agent.py @@ -3,7 +3,6 @@ import os import signal import socket import subprocess -import time from concurrent.futures import ThreadPoolExecutor from pathlib import Path from types import SimpleNamespace @@ -128,13 +127,14 @@ def test_opencode_disabled_route_does_not_start_server(client, initialized_archi assert views._PROCESS is None or views._PROCESS.poll() is not None -def test_stop_owned_process_falls_back_when_process_has_no_dedicated_group(): +def test_stop_owned_process_falls_back_for_stopped_process_without_dedicated_group(): from archivebox.opencode import views process = subprocess.Popen(["sleep", "60"]) try: + process.send_signal(signal.SIGSTOP) views._stop_owned_process(process) - assert process.poll() is not None + assert process.returncode == -signal.SIGTERM finally: if process.poll() is None: process.kill() @@ -287,47 +287,19 @@ def test_concurrent_opencode_startup_waits_until_server_is_ready(live_opencode): assert views._health(live_opencode.settings) -def test_opencode_restarts_an_unhealthy_owned_process(live_opencode): +def test_opencode_does_not_probe_or_replace_a_ready_owned_process(live_opencode): from archivebox.opencode import views - old_process = views._PROCESS + process = views._PROCESS settings = {**live_opencode.settings, "port": _free_port()} settings["origin"] = f"http://{settings['host']}:{settings['port']}" ok, error = views._ensure_opencode(settings) assert ok, error - assert old_process is not None - assert old_process.poll() is not None - assert views._PROCESS is not old_process - assert views._health(settings) - - -def test_opencode_preserves_a_transiently_unhealthy_owned_process(live_opencode): - from archivebox.opencode import views - - process = views._PROCESS assert process is not None - views._signal_owned_process(process, signal.SIGSTOP) - - def resume_process(): - time.sleep(1.5) - views._signal_owned_process(process, signal.SIGCONT) - - try: - with ThreadPoolExecutor(max_workers=1) as executor: - resumed = executor.submit(resume_process) - started_at = time.monotonic() - ok, error = views._ensure_opencode(live_opencode.settings) - elapsed = time.monotonic() - started_at - resumed.result() - finally: - views._signal_owned_process(process, signal.SIGCONT) - - assert ok, error assert views._PROCESS is process assert process.poll() is None - assert elapsed < views._PROCESS_HEALTH_GRACE def test_opencode_proxy_does_not_wait_for_recovery_lock(admin_client, live_opencode):