Files
xiaobaifupan/next/backend/jobs/repository.py
T

155 lines
4.8 KiB
Python

from __future__ import annotations
import json
import sqlite3
from datetime import datetime, timedelta
from typing import Any
class JobRepository:
def begin(
self,
connection: sqlite3.Connection,
*,
kind: str,
run_key: str,
requested_date: str,
trigger: str,
started_at: datetime,
stale_after_seconds: int,
) -> sqlite3.Row:
stale_before = (started_at - timedelta(seconds=stale_after_seconds)).isoformat(
timespec="seconds"
)
connection.execute(
"""
UPDATE job_runs SET status = 'failed', finished_at = ?, duration_ms = ?,
error_code = 'stale_job', error_message = '任务进程中断或超过最长运行时间'
WHERE kind = ? AND status = 'running' AND started_at < ?
""",
(
started_at.isoformat(timespec="seconds"),
stale_after_seconds * 1000,
kind,
stale_before,
),
)
attempt_row = connection.execute(
"""
SELECT COALESCE(MAX(attempt), 0) + 1 AS attempt
FROM job_runs WHERE kind = ? AND run_key = ?
""",
(kind, run_key),
).fetchone()
attempt = int(attempt_row["attempt"] if attempt_row else 1)
cursor = connection.execute(
"""
INSERT INTO job_runs (
kind, run_key, requested_date, trigger, status, attempt, started_at
) VALUES (?, ?, ?, ?, 'running', ?, ?)
""",
(
kind,
run_key,
requested_date,
trigger,
attempt,
started_at.isoformat(timespec="seconds"),
),
)
return connection.execute(
"SELECT * FROM job_runs WHERE id = ?", (cursor.lastrowid,)
).fetchone()
def finish(
self,
connection: sqlite3.Connection,
run_id: int,
*,
finished_at: datetime,
duration_ms: int,
coverage: float | None,
source_set: list[str],
output_version: str,
payload: dict[str, Any],
) -> None:
connection.execute(
"""
UPDATE job_runs SET status = 'completed', finished_at = ?, duration_ms = ?,
coverage = ?, source_set_json = ?, output_version = ?, payload_json = ?
WHERE id = ? AND status = 'running'
""",
(
finished_at.isoformat(timespec="seconds"),
duration_ms,
coverage,
_json(source_set),
output_version,
_json(payload),
run_id,
),
)
def fail(
self,
connection: sqlite3.Connection,
run_id: int,
*,
finished_at: datetime,
duration_ms: int,
error_code: str,
error_message: str,
) -> None:
connection.execute(
"""
UPDATE job_runs SET status = 'failed', finished_at = ?, duration_ms = ?,
error_code = ?, error_message = ? WHERE id = ? AND status = 'running'
""",
(
finished_at.isoformat(timespec="seconds"),
duration_ms,
error_code,
error_message[:500],
run_id,
),
)
def latest(self, connection: sqlite3.Connection, limit: int = 40) -> tuple[sqlite3.Row, ...]:
return tuple(
connection.execute(
"SELECT * FROM job_runs ORDER BY started_at DESC, id DESC LIMIT ?",
(limit,),
).fetchall()
)
def latest_for_kind(
self, connection: sqlite3.Connection, kind: str
) -> sqlite3.Row | None:
return connection.execute(
"SELECT * FROM job_runs WHERE kind = ? ORDER BY started_at DESC, id DESC LIMIT 1",
(kind,),
).fetchone()
def latest_success(
self, connection: sqlite3.Connection, kind: str, requested_date: str = ""
) -> sqlite3.Row | None:
return connection.execute(
"""
SELECT * FROM job_runs
WHERE kind = ? AND status = 'completed' AND (? = '' OR requested_date = ?)
ORDER BY finished_at DESC, id DESC LIMIT 1
""",
(kind, requested_date, requested_date),
).fetchone()
def public_job(row: sqlite3.Row) -> dict[str, Any]:
result = dict(row)
result["source_set"] = json.loads(str(result.pop("source_set_json")))
result["payload"] = json.loads(str(result.pop("payload_json")))
return result
def _json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)