Powrót do strony głównej

Jobify: asyncio planer bez brokerów

Jobify — asynchroniczny planer zadań dla Python bez zewnętrznej infrastruktury. Używa SQLite do perystencji, middleware do rozszerzenia, API w stylu FastAPI. Nadaje się dla programistów middle/senior potrzebujących lekkiej kontroli lifecycle zadań.

Tworzymy zadania w tle z Jobify bez Celery
Advertisement 728x90

Jobify: asynchroniczny planista zadań bez zewnętrznej infrastruktury

Jobify pozwala skonfigurować asynchroniczny planista zadań w kilku linijkach kodu. Bez brokerów wiadomości, z domyślnym SQLite do trwałości. Zadania są zapisywane na dysku i przetrwają restart aplikacji.

Podstawowy przykład:

import asyncio
from jobify import Jobify

app = Jobify()


@app.task
async def send_notification(user_id: int) -> None:
    print(f"Powiadom użytkownika {user_id}")


async def main() -> None:
    async with app:
        await send_notification.push(42)
        print("Zadanie zaplanowane w tle")


asyncio.run(main())

Metoda push() umieszcza zadanie w tle bez blokowania. Jobify używa loop.call_at do precyzyjnego czasowania zamiast cyklu pollingowego, zmniejszając obciążenie w stanie bezczynności.

Google AdInline article slot

Problemy skalowania zadań w tle

Wraz ze wzrostem liczby zadań proste rozwiązania przestają wystarczać. sched z biblioteki standardowej nadaje się tylko do synchronicznych odroczonych wywołań bez trwałości i ponownych prób.

Linuxowy cron jest dobry dla skryptów systemowych, ale nie integruje się z aplikacją. Celery wymaga brokerów (RabbitMQ/Redis) i infrastruktury. APScheduler jest bliższy, ale ogranicza zarządzanie cyklem życia zadań.

Jobify rozwiązuje to poprzez:

Google AdInline article slot
  • Pełny cykl życia zadania: statusy, ponowne próby, limity czasu
  • Middleware do wykonywania i planowania (outer middleware)
  • Wstrzykiwanie zależności przez lifespan
  • Automatyczną deduplikację zadań
  • Natywne wsparcie asyncio

API w stylu FastAPI

Deweloperzy FastAPI szybko opanują Jobify. Znane koncepcje:

  • Dekoratory zadań @app.task
  • Lifespan do inicjalizacji stanu
  • Middleware stos
  • Wstrzykiwanie stanu przez INJECT

Przykład z oczekiwaniem na wynik:

import asyncio
from jobify import Jobify

app = Jobify()


@app.task
async def send_email(to: str, subject: str) -> None:
    print(f"Wysyłanie e-maila do {to}: {subject}")


async def main() -> None:
    async with app:
        job = await send_email.schedule(
            to="[email protected]",
            subject="Witamy!",
        ).delay(10)
        await job.wait()


asyncio.run(main())

Obiekt Job daje dostęp do statusu, wyniku, anulowania.

Google AdInline article slot

Lifespan i wstrzykiwanie zależności

Konfiguracja przez lifespan:

import asyncio
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from jobify import INJECT, Jobify, State


@asynccontextmanager
async def lifespan(_: Jobify) -> AsyncIterator[dict[str, str]]:
    yield {"sender": "[email protected]"}


app = Jobify(lifespan=lifespan)


@app.task
def send_email(to: str, subject: str, state: State = INJECT) -> None:
    print(f"{state.sender} -> {to}: {subject}")

Polityki wykonania są określane na dekoratorze:

@app.task(retry=3, timeout=30)
async def sync_user(user_id: int) -> None:
    # ...

Middleware: zarządzanie planowaniem i wykonaniem

Outer middleware uruchamiają się przy umieszczaniu zadania:

import asyncio
from jobify import Jobify, OuterContext
from jobify.middleware import BaseOuterMiddleware, CallNextOuter


class ScheduleLoggerMiddleware(BaseOuterMiddleware):
    async def __call__(
        self,
        call_next: CallNextOuter,
        context: OuterContext,
    ) -> asyncio.Handle:
        print(f"Planowanie {context.job.id} z wyzwalaczem: {context.trigger}")
        return await call_next(context)


app = Jobify(outer_middleware=[ScheduleLoggerMiddleware()])

Zwykłe middleware rozszerzają wykonanie (logowanie, metryki, ponowne próby).

Architektura bez polling

Jobify rezygnuje z while True: sleep(1): check_tasks(). Każde zadanie jest natychmiast rejestrowane przez loop.call_at na dokładny czas.

Zalety:

  • Zerowe obciążenie w bezczynności
  • Precyzja czasowania
  • Nie blokuje event loop

Kompromis: powiązanie z czasem monotonicznym. Przy zresetowaniu czasu systemowego zadania wymagają przeliczenia.

Co jest ważne

  • Trwałość: SQLite domyślnie, zadania przetrwają po restartach
  • Deduplikacja: automatyczne zapobieganie duplikatom po ID
  • Middleware stos: ingerencja w planowanie i wykonanie
  • FastAPI-like API: minimalna krzywa uczenia dla znanych deweloperów
  • Lekkość: bez brokerów, workerów, zewnętrznych zależności

— Editorial Team

Advertisement 728x90

Czytaj dalej