Files
xiaobaifupan/app/backend/jobs/repository.py
T
2026-08-07 12:29:01 +08:00

117 lines
4.3 KiB
Python

from __future__ import annotations
import json
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any
from database import ReviewDatabase
@dataclass(frozen=True)
class SQLiteJobRunRepository:
database: ReviewDatabase
def completed(self, job_id: str, idempotency_key: str) -> bool:
with self.database.connect() as connection:
row = connection.execute(
"""
SELECT 1 FROM job_runs
WHERE job_id = ? AND idempotency_key = ? AND status = 'success'
LIMIT 1
""",
(job_id, idempotency_key),
).fetchone()
return row is not None
def start(
self, job_id: str, idempotency_key: str, output_version: str,
metadata: dict[str, Any] | None = None,
stale_after_seconds: int = 0,
) -> int | None:
current = datetime.now().astimezone()
now = current.isoformat(timespec="seconds")
stale_before = (
current - timedelta(seconds=max(1, int(stale_after_seconds or 1)))
).isoformat(timespec="seconds")
with self.database.connect() as connection:
# The process-local runner lock cannot protect two server processes.
connection.execute("BEGIN IMMEDIATE")
connection.execute(
"""
UPDATE job_runs
SET status = 'failed', finished_at = ?, error_code = 'StaleRun',
message = 'Previous run exceeded its execution window.'
WHERE job_id = ? AND idempotency_key = ? AND status = 'running'
AND started_at < ?
""",
(now, job_id, idempotency_key, stale_before),
)
claimed = connection.execute(
"""
SELECT 1 FROM job_runs
WHERE job_id = ? AND idempotency_key = ?
AND status IN ('running', 'success')
LIMIT 1
""",
(job_id, idempotency_key),
).fetchone()
if claimed:
return None
row = connection.execute(
"""
SELECT COALESCE(MAX(attempt), 0) + 1 AS attempt FROM job_runs
WHERE job_id = ? AND idempotency_key = ?
""",
(job_id, idempotency_key),
).fetchone()
cursor = connection.execute(
"""
INSERT INTO job_runs
(job_id, idempotency_key, status, attempt, started_at,
output_version, metadata)
VALUES (?, ?, 'running', ?, ?, ?, ?)
""",
(
job_id, idempotency_key, int(row["attempt"]), now,
output_version,
json.dumps(metadata or {}, ensure_ascii=False, separators=(",", ":")),
),
)
connection.execute(
"""
DELETE FROM job_runs
WHERE id < (SELECT COALESCE(MAX(id), 0) - 20000 FROM job_runs)
AND status != 'running'
"""
)
return int(cursor.lastrowid)
def finish(
self, run_id: int, status: str, elapsed_ms: int,
error_code: str = "", message: str = "",
) -> None:
now = datetime.now().astimezone().isoformat(timespec="seconds")
with self.database.connect() as connection:
connection.execute(
"""
UPDATE job_runs
SET status = ?, finished_at = ?, elapsed_ms = ?,
error_code = ?, message = ?
WHERE id = ?
""",
(status, now, elapsed_ms, error_code, message[:1000], int(run_id)),
)
def recent(self, limit: int = 20) -> list[dict[str, Any]]:
with self.database.connect() as connection:
rows = connection.execute(
"""
SELECT id, job_id, idempotency_key, status, attempt, started_at,
finished_at, elapsed_ms, error_code, message, output_version
FROM job_runs ORDER BY id DESC LIMIT ?
""",
(max(1, min(100, int(limit))),),
).fetchall()
return [dict(row) for row in rows]