Asynchroniczność: async/await, asyncio

Backend ZEUS jest asynchroniczny od HTTP aż po bazę i kolejkę ARQ. Asynchroniczność daje ogromną przepustowość I/O, ale tylko gdy nie blokujesz pętli zdarzeń. Ta lekcja to zestaw zasad, których trzymamy się w praktyce.

Co robi async/await

async def tworzy korutynę. await oddaje sterowanie pętli zdarzeń na czas operacji I/O (sieć, baza, dysk), żeby obsłużyć inne żądania.

async def fetch_forest(forest: str) -> dict:
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"/api/forest/{forest}")
        return resp.json()

Najczęstszy błąd: blokowanie pętli

Wywołanie synchronicznej, wolnej operacji w korutynie blokuje cały serwer — wszystkie inne żądania czekają.

# źle — blokuje pętlę zdarzeń
async def bad():
    time.sleep(5)                 # zatrzymuje cały event loop
    data = requests.get(url)      # synchroniczny HTTP — też blokuje

# dobrze
async def good():
    await asyncio.sleep(5)
    async with httpx.AsyncClient() as c:
        data = await c.get(url)

Standard ProfessNet: w kodzie async używamy bibliotek async (httpx, asyncpg/SQLAlchemy async, redis.asyncio). Bibliotek synchronicznych nie wołamy bezpośrednio.

Kod synchroniczny w świecie async

Gdy musisz użyć blokującej biblioteki (np. CPU-bound parsing albo stary klient SDK), zepchnij ją do puli wątków:

import asyncio

result = await asyncio.to_thread(blocking_parse, raw_bytes)

Równoległość: asyncio.gather

Kilka niezależnych operacji I/O uruchamiamy współbieżnie, nie sekwencyjnie.

# sekwencyjnie — wolno (suma czasów)
a = await fetch_forest("corp")
b = await fetch_forest("dmz")

# współbieżnie — szybko (czas najwolniejszego)
a, b = await asyncio.gather(
    fetch_forest("corp"),
    fetch_forest("dmz"),
)

Limit współbieżności

Przy wielu probe'ach nie odpalamy tysiąca zadań naraz — ograniczamy semaforem, żeby nie zatkać sieci ani serwera docelowego.

sem = asyncio.Semaphore(10)


async def guarded(forest: str) -> dict:
    async with sem:
        return await fetch_forest(forest)


results = await asyncio.gather(*(guarded(f) for f in forests))

Timeouty i anulowanie

Każde zewnętrzne wywołanie ma timeout. Bez tego zawieszony peer wisi w nieskończoność.

async with asyncio.timeout(30):
    data = await collect_inventory(forest)
WzorzecDo czego
awaitpojedyncza operacja I/O
asyncio.gatherwiele operacji współbieżnie
Semaphorelimit równoległości
asyncio.to_threadwepchnięcie kodu blokującego
asyncio.timeouttwardy limit czasu

Wskazówka: długie zadania (skany całego forestu) nie żyją w handlerze HTTP — trafiają do ARQ. Endpoint zwraca 202 i job_id, a worker robi resztę.


Złota zasada: w korutynie albo await-ujesz coś asynchronicznego, albo spychasz blok do to_thread. Wszystko inne zatrzymuje serwer. Trzymaj się tego, a async da ci wydajność bez niespodzianek.