Files
beavergram/backend/src/userbot/modules/jobs/repository.py
T

118 lines
3.6 KiB
Python

import json
from typing import Any
import asyncpg
from utils.db.models import Job, JobStatus
def _row_to_job(row: asyncpg.Record) -> Job:
data = dict(row)
data["params"] = json.loads(data["params"])
data["progress"] = json.loads(data["progress"])
data["cursor"] = json.loads(data["cursor"]) if data["cursor"] is not None else None
return Job(**data)
async def enqueue(
pool: asyncpg.Pool, account_id: int, kind: str, params: dict[str, Any]
) -> int:
return await pool.fetchval(
"INSERT INTO jobs (account_id, kind, params) "
"VALUES ($1, $2, $3::jsonb) RETURNING id",
account_id,
kind,
json.dumps(params),
)
async def claim_next(pool: asyncpg.Pool, account_id: int) -> Job | None:
row = await pool.fetchrow(
"UPDATE jobs SET status = 'running', started_at = now(), "
"updated_at = now(), attempts = attempts + 1 "
"WHERE id = ("
" SELECT id FROM jobs WHERE account_id = $1 AND status = 'pending' "
" AND (run_after IS NULL OR run_after <= now()) "
" ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 1"
") RETURNING *",
account_id,
)
return _row_to_job(row) if row else None
async def requeue_running(pool: asyncpg.Pool, account_id: int) -> int:
result = await pool.execute(
"UPDATE jobs SET status = 'pending', updated_at = now() "
"WHERE account_id = $1 AND status = 'running'",
account_id,
)
return int(result.split()[-1])
async def prune_finished(pool: asyncpg.Pool, account_id: int) -> int:
result = await pool.execute(
"DELETE FROM jobs WHERE account_id = $1 "
"AND status IN ('done', 'canceled', 'failed') "
"AND finished_at < now() - interval '30 days'",
account_id,
)
return int(result.split()[-1])
async def save_cursor(pool: asyncpg.Pool, job_id: int, cursor: dict[str, Any]) -> None:
await pool.execute(
"UPDATE jobs SET cursor = $2::jsonb, updated_at = now() WHERE id = $1",
job_id,
json.dumps(cursor),
)
async def report_progress(
pool: asyncpg.Pool, job_id: int, progress: dict[str, Any]
) -> None:
await pool.execute(
"UPDATE jobs SET progress = $2::jsonb, updated_at = now() WHERE id = $1",
job_id,
json.dumps(progress),
)
async def defer_flood_wait(pool: asyncpg.Pool, job_id: int, seconds: int) -> None:
await pool.execute(
"UPDATE jobs SET status = 'pending', flood_waits = flood_waits + 1, "
"run_after = now() + make_interval(secs => $2), updated_at = now() "
"WHERE id = $1 AND status = 'running'",
job_id,
seconds,
)
async def seconds_to_next_due(pool: asyncpg.Pool, account_id: int) -> float | None:
return await pool.fetchval(
"SELECT extract(epoch FROM min(run_after) - now())::float FROM jobs "
"WHERE account_id = $1 AND status = 'pending' AND run_after > now()",
account_id,
)
async def finish(
pool: asyncpg.Pool, job_id: int, status: JobStatus, error: str | None = None
) -> None:
await pool.execute(
"UPDATE jobs SET status = $2, error = $3, finished_at = now(), "
"updated_at = now() WHERE id = $1 AND status = 'running'",
job_id,
status.value,
error,
)
async def is_canceled(pool: asyncpg.Pool, job_id: int) -> bool:
status = await pool.fetchval("SELECT status FROM jobs WHERE id = $1", job_id)
return status == JobStatus.CANCELED.value
async def get_job(pool: asyncpg.Pool, job_id: int) -> Job | None:
row = await pool.fetchrow("SELECT * FROM jobs WHERE id = $1", job_id)
return _row_to_job(row) if row else None