refactor: govern background job lifecycle
This commit is contained in:
@@ -128,14 +128,19 @@ class DashboardService(
|
||||
profile_supplier=self._resolved_llm_profile,
|
||||
)
|
||||
self.screener.ensure_builtin_strategies()
|
||||
self._background_stop = threading.Event()
|
||||
self._background_thread = self.jobs.start_scheduler(
|
||||
|
||||
def start_background_jobs(self) -> threading.Thread:
|
||||
return self.jobs.start_scheduler(
|
||||
self._background_refresh_tick,
|
||||
self._background_stop,
|
||||
interval_seconds=5,
|
||||
initial_delay_seconds=3,
|
||||
)
|
||||
|
||||
def stop_background_jobs(self, timeout_seconds: float = 5) -> bool:
|
||||
scheduler_stopped = self.jobs.stop_scheduler(timeout_seconds)
|
||||
workers_stopped = self.jobs.wait_for_idle(timeout_seconds)
|
||||
return scheduler_stopped and workers_stopped
|
||||
|
||||
|
||||
def _load_system_credentials(self, environment: dict[str, str]) -> dict[str, Any]:
|
||||
encrypted = self.database.get_system_setting("credentials")
|
||||
|
||||
@@ -16,12 +16,13 @@ def main(handler_class: type[Any] | None = None, service: Any | None = None) ->
|
||||
parser.add_argument("--port", type=int, default=8765)
|
||||
args = parser.parse_args()
|
||||
server = ThreadingHTTPServer((args.host, args.port), handler_class)
|
||||
print(f"Xiaobai Review Web is running at http://{args.host}:{args.port}")
|
||||
print("Press Ctrl+C to stop.")
|
||||
try:
|
||||
service.start_background_jobs()
|
||||
print(f"Xiaobai Review Web is running at http://{args.host}:{args.port}")
|
||||
print("Press Ctrl+C to stop.")
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
service._background_stop.set()
|
||||
service.stop_background_jobs()
|
||||
server.server_close()
|
||||
|
||||
+37
-19
@@ -18,6 +18,9 @@ class InProcessJobRunner:
|
||||
self.repository = repository
|
||||
self._locks: dict[str, threading.Lock] = {}
|
||||
self._locks_guard = threading.Lock()
|
||||
self._scheduler_guard = threading.Lock()
|
||||
self._scheduler_stop = threading.Event()
|
||||
self._scheduler_thread: threading.Thread | None = None
|
||||
|
||||
def submit(
|
||||
self, job_id: str, idempotency_key: str, action: JobAction,
|
||||
@@ -52,27 +55,42 @@ class InProcessJobRunner:
|
||||
return True
|
||||
|
||||
def start_scheduler(
|
||||
self, callback: Callable[[], None], stop_event: threading.Event,
|
||||
interval_seconds: float, initial_delay_seconds: float = 0,
|
||||
self, callback: Callable[[], None], interval_seconds: float,
|
||||
initial_delay_seconds: float = 0,
|
||||
) -> threading.Thread:
|
||||
def schedule_loop() -> None:
|
||||
if stop_event.wait(initial_delay_seconds):
|
||||
return
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
callback()
|
||||
except Exception:
|
||||
# Submitted jobs persist their own failures; the scheduler must stay alive.
|
||||
pass
|
||||
stop_event.wait(interval_seconds)
|
||||
with self._scheduler_guard:
|
||||
current = self._scheduler_thread
|
||||
if current is not None and current.is_alive():
|
||||
return current
|
||||
self._scheduler_stop.clear()
|
||||
|
||||
thread = threading.Thread(
|
||||
target=schedule_loop,
|
||||
name="background-job-scheduler",
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
return thread
|
||||
def schedule_loop() -> None:
|
||||
if self._scheduler_stop.wait(initial_delay_seconds):
|
||||
return
|
||||
while not self._scheduler_stop.is_set():
|
||||
try:
|
||||
callback()
|
||||
except Exception:
|
||||
# Submitted jobs persist failures; the scheduler must stay alive.
|
||||
pass
|
||||
self._scheduler_stop.wait(interval_seconds)
|
||||
|
||||
thread = threading.Thread(
|
||||
target=schedule_loop,
|
||||
name="background-job-scheduler",
|
||||
daemon=True,
|
||||
)
|
||||
self._scheduler_thread = thread
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
def stop_scheduler(self, timeout_seconds: float = 5) -> bool:
|
||||
with self._scheduler_guard:
|
||||
thread = self._scheduler_thread
|
||||
self._scheduler_stop.set()
|
||||
if thread is not None and thread is not threading.current_thread():
|
||||
thread.join(max(0, timeout_seconds))
|
||||
return thread is None or not thread.is_alive()
|
||||
|
||||
def wait_for_idle(self, timeout_seconds: float = 5) -> bool:
|
||||
deadline = time.monotonic() + max(0, timeout_seconds)
|
||||
|
||||
Reference in New Issue
Block a user