Files
MemeMeow/backend/library_processing.py
2026-09-24 16:09:01 +08:00

73 lines
3.9 KiB
Python

# 图片库后台扫描:通过普通 Task 保存枚举范围、游标与提交统计。
from __future__ import annotations
from typing import Any, Callable
from datetime import datetime
from uuid import UUID
from loguru import logger
from sqlalchemy import select
from backend.image_processing import ImageProcessingError
from backend.persistence.models import Meme, utcnow
LIBRARY_TASK_TYPE = "image_library_processing"
def submit_library_processing(tasks: Any, options: dict[str, Any]) -> Any:
"""固定当前图片范围后提交扫描任务,返回可查询的 Task 快照。"""
accepted_at = utcnow()
with tasks.resources.environment(tasks.scope) as environment:
upper_id = environment.uow.session.scalar(
select(Meme.id).where(Meme.scope_id == tasks.scope.scope_id).order_by(Meme.id.desc()).limit(1)
)
return tasks.submit(LIBRARY_TASK_TYPE, {"options": options, "accepted_at": accepted_at.isoformat(), "upper_id": str(upper_id) if upper_id else None})
def run_library_processing(tasks: Any, payload: dict[str, Any], process_image: Callable[[Any, dict[str, Any]], str], progress: Callable[..., Any], *, page_size: int = 100) -> dict[str, int]:
"""分页提交图片处理,逐图保存游标;租约失效立即停止当前执行者。"""
resources = tasks.resources
counts = dict(payload.get("counts") or {"scanned_count": 0, "submitted_count": 0, "conflict_count": 0, "not_needed_count": 0})
upper_id = UUID(payload["upper_id"]) if payload.get("upper_id") else None
cursor = UUID(payload["cursor"]) if payload.get("cursor") else None
accepted_at = datetime.fromisoformat(payload["accepted_at"])
if upper_id is None:
return counts
while True:
with resources.environment(tasks.scope) as environment:
statement = select(Meme).where(Meme.scope_id == tasks.scope.scope_id, Meme.id <= upper_id, Meme.created_at <= accepted_at)
if cursor is not None:
statement = statement.where(Meme.id > cursor)
rows = list(environment.uow.session.scalars(statement.order_by(Meme.id).limit(page_size)))
if not rows:
return counts
for record in rows:
if tasks.stopping:
raise RuntimeError("task_interrupted")
# 检查与保存都使用租约条件;图片操作期间不持有数据库事务。
with resources.environment(tasks.scope) as environment:
current = environment.tasks.update_payload_fenced(payload["_claim_task_id"], payload["_claim_generation"], payload["_claim_owner"], {})
if not current:
raise RuntimeError("task_claim_expired")
try:
category = process_image(record, payload["options"])
except ImageProcessingError as exc:
if exc.code == "image_processing_active":
category = "conflict_count"
elif exc.code == "already_ready":
category = "not_needed_count"
else:
logger.error("library_processing_error task_id={} meme_id={} error={}", payload["_claim_task_id"], record.id, exc.code)
raise
counts[category] += 1
counts["scanned_count"] += 1
cursor = record.id
with resources.environment(tasks.scope) as environment:
saved = environment.tasks.update_payload_fenced(payload["_claim_task_id"], payload["_claim_generation"], payload["_claim_owner"], {"cursor": str(cursor), "counts": counts})
if not saved:
raise RuntimeError("task_claim_expired")
progress(None, f"已检查 {counts['scanned_count']} 张图片,已提交 {counts['submitted_count']} 张")
logger.info("library_processing_page task_id={} scanned={} submitted={}", payload["_claim_task_id"], counts["scanned_count"], counts["submitted_count"])