refactor: govern background job execution
This commit is contained in:
@@ -20,14 +20,17 @@ class DatabaseMigrationTests(unittest.TestCase):
|
||||
).fetchall()
|
||||
self.assertEqual(
|
||||
[(row["version"], row["name"]) for row in rows],
|
||||
[("0001", "adopt_legacy_schema")],
|
||||
[
|
||||
("0001", "adopt_legacy_schema"),
|
||||
("0002", "create_job_runs"),
|
||||
],
|
||||
)
|
||||
ReviewDatabase(path)
|
||||
with database.connect() as connection:
|
||||
count = connection.execute(
|
||||
"SELECT COUNT(*) AS count FROM schema_migrations"
|
||||
).fetchone()["count"]
|
||||
self.assertEqual(count, 1)
|
||||
self.assertEqual(count, 2)
|
||||
|
||||
def test_connection_factory_enables_required_pragmas(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
|
||||
@@ -23,6 +23,7 @@ class GovernanceRegistryTests(unittest.TestCase):
|
||||
self.api = load("api.config.json")
|
||||
self.data = load("data-fields.config.json")
|
||||
self.quality = load("data-quality.config.json")
|
||||
self.jobs = load("jobs.config.json")
|
||||
|
||||
def test_features_are_unique_and_use_declared_roles(self) -> None:
|
||||
roles = set(self.features["roles"])
|
||||
@@ -92,6 +93,18 @@ class GovernanceRegistryTests(unittest.TestCase):
|
||||
all(str(rule.get("unit_profile") or "none") in profiles for rule in rules.values())
|
||||
)
|
||||
|
||||
def test_background_jobs_have_complete_unique_runtime_contracts(self) -> None:
|
||||
jobs = self.jobs["jobs"]
|
||||
ids = [item["id"] for item in jobs]
|
||||
self.assertEqual(len(ids), len(set(ids)))
|
||||
for item in jobs:
|
||||
self.assertTrue(item["schedule"])
|
||||
self.assertTrue(item["input_date_policy"])
|
||||
self.assertTrue(item["lock_key"])
|
||||
self.assertGreater(int(item["timeout_seconds"]), 0)
|
||||
self.assertGreater(int(item["max_attempts"]), 0)
|
||||
self.assertTrue(item["output_version"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from backend.jobs import InProcessJobRunner, JobRegistry, SQLiteJobRunRepository
|
||||
from database import ReviewDatabase
|
||||
|
||||
|
||||
class JobRunnerTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.database = ReviewDatabase(Path(self.temporary.name) / "review.db")
|
||||
self.repository = SQLiteJobRunRepository(self.database)
|
||||
self.runner = InProcessJobRunner(JobRegistry.load(), self.repository)
|
||||
|
||||
def test_successful_idempotent_job_runs_once(self) -> None:
|
||||
calls = []
|
||||
self.assertTrue(
|
||||
self.runner.run_inline(
|
||||
"screener.automatic", "20260729:v8", lambda: calls.append("run")
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
self.runner.run_inline(
|
||||
"screener.automatic", "20260729:v8", lambda: calls.append("again")
|
||||
)
|
||||
)
|
||||
self.assertEqual(calls, ["run"])
|
||||
self.assertEqual(self.repository.recent(1)[0]["status"], "success")
|
||||
|
||||
def test_failure_is_persisted_and_can_be_retried_later(self) -> None:
|
||||
def fail() -> None:
|
||||
raise RuntimeError("provider unavailable")
|
||||
|
||||
self.assertTrue(self.runner.run_inline("market.refresh", "failed-key", fail))
|
||||
failed = self.repository.recent(1)[0]
|
||||
self.assertEqual(failed["status"], "failed")
|
||||
self.assertEqual(failed["error_code"], "RuntimeError")
|
||||
self.assertTrue(
|
||||
self.runner.run_inline("market.refresh", "failed-key", lambda: None)
|
||||
)
|
||||
self.assertEqual(self.repository.recent(1)[0]["attempt"], 2)
|
||||
|
||||
def test_lock_key_rejects_concurrent_submission(self) -> None:
|
||||
entered = threading.Event()
|
||||
release = threading.Event()
|
||||
|
||||
def wait() -> None:
|
||||
entered.set()
|
||||
release.wait(2)
|
||||
|
||||
self.assertTrue(self.runner.submit("market.refresh", "first", wait))
|
||||
self.assertTrue(entered.wait(1))
|
||||
self.assertFalse(self.runner.submit("market.refresh", "second", lambda: None))
|
||||
release.set()
|
||||
self.assertTrue(self.runner.wait_for_idle())
|
||||
|
||||
def test_failed_status_payload_is_recorded_as_failure(self) -> None:
|
||||
self.assertTrue(
|
||||
self.runner.run_inline(
|
||||
"screener.automatic", "reported-failure",
|
||||
lambda: {"status": "failed", "error": "missing factors"},
|
||||
)
|
||||
)
|
||||
self.assertEqual(self.repository.recent(1)[0]["status"], "failed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user