from uuid import UUID from sqlalchemy import select, update, func from sqlalchemy.ext.asyncio import AsyncSession from datetime import datetime from app.models import Job, Task, TaskStatus, JobStatus, TaskDependency from app.job_registry import list_tasks_for_job, list_dependencies, routing_key from app.rabbitmq import publish_task_message from app.config import settings async def create_job(session: AsyncSession, job_type: str, domain: str, payload: dict) -> Job: job = Job(job_type=job_type, domain=domain, payload=payload, status=JobStatus.PENDING) session.add(job) await session.flush() # 1) Tasks anlegen name_to_task: dict[str, Task] = {} for name in list_tasks_for_job(job_type): t = Task( job_id=job.id, name=name, status=TaskStatus.PENDING, routing_key=routing_key(job_type, name), max_retries=settings.MAX_RETRIES, ) session.add(t) await session.flush() name_to_task[name] = t # 2) Dependencies anlegen for name, task in name_to_task.items(): for dep_name in list_dependencies(job_type, name): dep_task = name_to_task[dep_name] session.add(TaskDependency(task_id=task.id, depends_on_task_id=dep_task.id)) await session.commit() # 3) Nur Tasks ohne Dependencies veröffentlichen for name, task in name_to_task.items(): deps = list_dependencies(job_type, name) if not deps: await _publish_task(session, job, task) await refresh_job_status(session, job.id) await session.commit() return job async def _publish_task(session: AsyncSession, job: Job, task: Task): await publish_task_message( routing_key=task.routing_key, message={ "job_id": str(job.id), "task_id": str(task.id), "task_name": task.name, "job_type": job.job_type, "payload": {"domain": job.domain, **(job.payload or {})}, }, ) await session.execute(update(Task).where(Task.id == task.id).values(status=TaskStatus.QUEUED)) await session.commit() async def release_dependent_tasks(session: AsyncSession, completed_task_id: UUID): # Finde Tasks, die von completed_task_id abhängen res = await session.execute(select(TaskDependency.task_id).where(TaskDependency.depends_on_task_id == completed_task_id)) candidate_ids = [row[0] for row in res.all()] if not candidate_ids: return # Für jeden Kandidaten prüfen, ob ALLE Dependencies SUCCESS sind for task_id in candidate_ids: # task + job laden res_task = await session.execute(select(Task, Job).join(Job, Task.job_id == Job.id).where(Task.id == task_id)) row = res_task.first() if not row: continue task, job = row # Alle deps status res_deps = await session.execute( select(func.count()).select_from(TaskDependency) .join(Task, TaskDependency.depends_on_task_id == Task.id) .where(TaskDependency.task_id == task.id, Task.status != TaskStatus.SUCCESS) ) remaining = res_deps.scalar_one() if remaining == 0 and task.status == TaskStatus.PENDING: await _publish_task(session, job, task) async def get_job(session: AsyncSession, job_id: UUID) -> Job | None: res = await session.execute(select(Job).where(Job.id == job_id)) return res.scalar_one_or_none() async def get_job_with_tasks(session: AsyncSession, job_id: UUID) -> Job | None: return await get_job(session, job_id) async def refresh_job_status(session: AsyncSession, job_id: UUID) -> None: res = await session.execute(select(Task).where(Task.job_id == job_id)) tasks = res.scalars().all() if not tasks: await session.execute(update(Job).where(Job.id == job_id).values(status=JobStatus.PENDING)) return if all(t.status == TaskStatus.SUCCESS for t in tasks): new_status = JobStatus.SUCCESS elif any(t.status == TaskStatus.FAILED for t in tasks) and not any( t.status in (TaskStatus.RUNNING, TaskStatus.QUEUED, TaskStatus.PENDING) for t in tasks ): new_status = JobStatus.FAILED elif any(t.status == TaskStatus.RUNNING for t in tasks): new_status = JobStatus.RUNNING elif any(t.status == TaskStatus.QUEUED for t in tasks): new_status = JobStatus.QUEUED else: new_status = JobStatus.PENDING await session.execute(update(Job).where(Job.id == job_id).values(status=new_status, updated_at=datetime.utcnow()))