Spolehlivá zpracování webhooků v FastAPI s Redis frontou
Webhooky od platebních služeb vyžadují rychlou odezvu – obvykle ne více než 5 sekund. Synchronní zpracování na koncovém bodu vede k časovým limitům při zpožděních v databázi nebo externích službách. Příklad počáteční implementace:
@app.post("/webhook")
async def webhook(request: dict):
update_database(request)
send_email_to_user(request)
return {"status": "ok"}
Tento přístup je zranitelný: selhání serveru během zpracování způsobí ztrátu dat, duplicitní oznámení způsobí chyby jedinečnosti v DB. Řešením je oddělení přijetí a zpracování prostřednictvím fronty úloh.
Architektura s frontou úloh
Koncový bod API přijme webhook, ověří podpis, zajistí idempotence a uloží úlohu do Redis fronty. Samostatný worker asynchronně provádí obchodní logiku. Výhody:
- Okamžitá HTTP odpověď (50 ms).
- Uložení úloh při pádu workera.
- Horizontální škálování (více workerů).
Komponenty:
- FastAPI pro API.
- Redis jako fronta (LPUSH/BRPOP).
- Workers v samostatných procesech.
Implementace přijímače API
Koncový bod se zaměřuje na bezpečnost a rychlost. Ověřuje HMAC podpis, vylučuje duplikáty podle transaction_id a přidává úlohu do fronty.
from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel
import redis
import json
import hashlib
import os
app = FastAPI()
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
SECRET = os.getenv('WEBHOOK_SECRET', 'test_secret')
def check_signature(body: str, sign: str) -> bool:
my_sign = hashlib.sha256(f"{body}{SECRET}".encode()).hexdigest()
return my_sign == sign
@app.post("/api/payment")
async def payment_webhook(request: dict, x_sign: str = Header(None)):
if not check_signature(json.dumps(request), x_sign):
raise HTTPException(status_code=403, detail="Bad sign")
task_id = request.get('transaction_id')
if r.exists(f"processed:{task_id}"):
return {"status": "duplicate"}
task = {
"id": task_id,
"amount": request.get('amount'),
"user_id": request.get('user_id')
}
r.lpush("payment_queue", json.dumps(task))
return {"status": "accepted"}
Klíčové praktiky:
- HMAC podpis zabrání podvržení požadavků.
- Redis klíč
processed:{id}zajišťuje idempotenci. - LPUSH pro přidání do fronty.
Worker pro pozadní zpracování
Worker běží v cyklu, extrahuje úlohy blokujícím BRPOP (timeout 5s), provádí logiku, označuje jako zpracované (TTL 1 den). Při chybě se vrátí do fronty s prodlevou.
import redis
import json
import time
import logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
def process_task(data):
log.info(f"Zpracovávám platbu {data['id']}")
time.sleep(1)
return True
def main():
r = redis.Redis(host='localhost', port=6379)
log.info("Worker spuštěn...")
while True:
task = r.brpop("payment_queue", timeout=5)
if not task:
continue
_, raw_data = task
data = json.loads(raw_data)
try:
success = process_task(data)
if success:
r.setex(f"processed:{data['id']}", 86400, "1")
log.info(f"Úloha {data['id']} dokončena")
except Exception as e:
log.error(f"Chyba: {e}")
r.lpush("payment_queue", raw_data)
time.sleep(5)
if __name__ == "__main__":
main()
Běžné problémy a jejich řešení
- Chybějící ověření podpisu: Zranitelnost proti falešným POST požadavkům. Oprava – HMAC s tajným klíčem.
- Přechodnost Redis: Data v paměti se ztrácejí při restartu. Řešení – RDB/AOF persistence nebo PostgreSQL jako fronta.
- Cyklus chyb ve workerovi: Úloha se neustále vrací. Oprava – počítadlo pokusů, dead letter queue, prodlevy.
Další rizika:
- Časové limity externích služeb: použít timeouts ve funkci process_task.
- Škálování: sledovat délku fronty (LLEN), přidávat workerů.
Co je důležité
- Idempotence: Klíče
processed:{id}zabrání duplikátům. - Bezpečnost: Vždy ověřujte HMAC podpis webhooků.
- Spolehlivost: Fronty uchovávají úlohy při selhání.
- Škálovatelnost: Samostatné workerů umožňují paralelní zpracování.
- Monitorování: Logujte metriky fronty a chyb.
— Editorial Team
Zatím žádné komentáře.