Dodaj zadanie w tle¶
Praca w tle dzieje się poza cyklem żądanie-odpowiedź. Ten projekt używa
Prefect do wszystkiego, co jest zaplanowane albo długo trwa, oraz
app/core/background.py do pracy, którą przekazuje żądanie.
Krok po kroku¶
1. Utwórz zadanie¶
# app/worker/tasks/notifications.py
from uuid import UUID
from prefect import flow
from app.db.session import get_db_context
from app.repositories import notification_repo
@flow(name="send-notification", log_prints=True)
async def send_notification_flow(notification_id: str) -> dict:
"""Send a notification that a request has already written."""
# A flow opens its own session: the request's is long gone by now.
async with get_db_context() as db:
notification = await notification_repo.get_by_id(db, UUID(notification_id))
if notification is None:
# Reachable, and the whole reason for `spawn_after_commit` below.
raise ValueError(f"notification {notification_id} not found")
print(f"Sending {notification.title} on {notification.channel}")
return {"status": "sent", "notification_id": notification_id}
Ten flow przyjmuje identyfikator i odczytuje wiersz, co jest kształtem, jaki ma niemal każdy prawdziwy flow, i powodem, dla którego opisane niżej przekazanie ma znaczenie. Flow, który tylko wypisuje swoje argumenty, działałby tak czy inaczej i niczego by nie nauczył.
2. Wywołaj go z serwisu¶
Praca, która czyta wiersz zapisany przez to żądanie, bierze spawn_after_commit
spawn tworzy zadanie natychmiast, a pętla startuje je przed commitem
żądania, więc flow otwiera własną sesję i nie widzi wiersza, którego
identyfikator dostał — upload, który odpowiedział processing i taki
pozostał (#417). Gołe
asyncio.create_task ma ten problem i dodatkowo gubi wyjątek, bo nic nie
trzyma referencji do zadania ani nie odczytuje jego wyniku.
from app.core.background import spawn, spawn_after_commit
from app.worker.tasks.notifications import send_notification_flow
from app.worker.tasks.reports import nightly_digest_flow
# A row this request just wrote. The flow reads it by id, so the task must not
# start before the commit that makes it readable.
spawn_after_commit(
self.db,
send_notification_flow(str(notification.id)),
name=f"notify:{notification.id}",
)
# Work that reads nothing this request wrote can start immediately.
spawn(nightly_digest_flow(), name="digest:nightly")
Oba trzymają referencję do zadania, żeby nie zostało zebrane przez garbage
collector w locie, i oba logują błąd z nazwą name, którą im podałeś — a to
jedyny kontekst, jaki ten błąd kiedykolwiek poniesie, więc bądź konkretny.
3. Dodaj harmonogram (opcjonalnie)¶
Harmonogram odpala się ze stałymi parametrami, więc flow, który nazywa, musi
być takim, który ich nie potrzebuje — send_notification_flow powyżej przyjmuje
identyfikator wiersza, który ktoś zapisał, a o dziewiątej rano nie ma takiego
identyfikatora. Zarejestruj flow, który sam szuka swojej pracy:
from prefect.client.schemas.schedules import CronSchedule
from app.worker.tasks.reports import nightly_digest_flow
deployments.append(await nightly_digest_flow.ato_deployment(
name="daily-digest",
schedules=[CronSchedule(cron="0 9 * * *")], # Daily at 9 AM
))
4. Uruchom workera¶
# The prefect-server + prefect-runner containers start with `make dev`.
# To run the runner directly (registers deployments + polls for work):
uv run --directory backend python -m app.worker.prefect_app
# Prefect UI: http://localhost:4200
Krótki harmonogram jest tani w dodaniu, ale nie darmowy
Naraz wykonuje się najwyżej PREFECT_RUNNER_LIMIT runów (domyślnie 5), a
reszta czeka w kolejce. Każdy run to proces, który importuje całą aplikację,
więc wybieraj najdłuższy interwał, który wciąż odpowiada na pytanie.
Interwał decyduje też o tym, ile pracy czeka po przestoju.