refactor: govern background job lifecycle

This commit is contained in:
leefer
2026-08-02 03:40:53 +08:00
parent 93dac9c308
commit f4994780f3
7 changed files with 150 additions and 33 deletions
+65
View File
@@ -0,0 +1,65 @@
from __future__ import annotations
import ast
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
from backend.bootstrap import runtime
class BackgroundLifecycleTests(unittest.TestCase):
def test_service_construction_does_not_start_the_scheduler(self) -> None:
source = (Path(__file__).parents[1] / "backend" / "application.py").read_text(
encoding="utf-8"
)
tree = ast.parse(source)
service_class = next(
node for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "DashboardService"
)
constructor = next(
node for node in service_class.body
if isinstance(node, ast.FunctionDef) and node.name == "__init__"
)
called_methods = {
node.func.attr for node in ast.walk(constructor)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)
}
self.assertNotIn("start_scheduler", called_methods)
def test_runtime_starts_jobs_after_bind_and_stops_before_close(self) -> None:
events: list[str] = []
class Service:
def start_background_jobs(self) -> None:
events.append("start-jobs")
def stop_background_jobs(self) -> None:
events.append("stop-jobs")
class Server:
def __init__(self, address: tuple[str, int], handler: object) -> None:
events.append("bind")
def serve_forever(self) -> None:
events.append("serve")
raise KeyboardInterrupt
def server_close(self) -> None:
events.append("close")
with (
patch.object(runtime, "ThreadingHTTPServer", Server),
patch.object(sys, "argv", ["server.py", "--port", "8797"]),
):
runtime.main(object, Service())
self.assertEqual(
events, ["bind", "start-jobs", "serve", "stop-jobs", "close"]
)
if __name__ == "__main__":
unittest.main()
+23
View File
@@ -68,6 +68,29 @@ class JobRunnerTests(unittest.TestCase):
)
self.assertEqual(self.repository.recent(1)[0]["status"], "failed")
def test_scheduler_start_is_idempotent_and_stop_waits_for_exit(self) -> None:
first = self.runner.start_scheduler(
lambda: None, interval_seconds=60, initial_delay_seconds=60
)
second = self.runner.start_scheduler(
lambda: None, interval_seconds=60, initial_delay_seconds=60
)
self.assertIs(first, second)
self.assertTrue(first.is_alive())
self.assertTrue(self.runner.stop_scheduler())
self.assertFalse(first.is_alive())
def test_scheduler_can_restart_after_an_orderly_stop(self) -> None:
first = self.runner.start_scheduler(
lambda: None, interval_seconds=60, initial_delay_seconds=60
)
self.assertTrue(self.runner.stop_scheduler())
second = self.runner.start_scheduler(
lambda: None, interval_seconds=60, initial_delay_seconds=60
)
self.assertIsNot(first, second)
self.assertTrue(self.runner.stop_scheduler())
if __name__ == "__main__":
unittest.main()