- @app.on_event('startup'/'shutdown') sudah deprecated โ gunakan lifespan
- Lifespan adalah async context manager: kode sebelum yield saat menyala, sesudah yield saat mati
- Pool koneksi dan klien HTTP dibuat SEKALI, bukan per-request
- app.state untuk menyimpan objek bersama lintas request
โ ๏ธ on_event Sudah Usang
Banyak tutorial masih mengajarkan pola lama ini:
@app.on_event("startup")
async def mulai():
...
@app.on_event("shutdown")
async def berhenti():
...
Pola ini deprecated. Masalah mendasarnya: kedua fungsi terpisah, sehingga tidak ada cara alami untuk membagikan variabel di antara keduanya, dan tidak ada jaminan pembersihan bila startup gagal di tengah jalan.
โ Lifespan Context Manager
from contextlib import asynccontextmanager
from fastapi import FastAPI
import httpx
from sqlalchemy.ext.asyncio import create_async_engine
@asynccontextmanager
async def lifespan(app: FastAPI):
# โโ SAAT MENYALA โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
engine = create_async_engine(DATABASE_URL, pool_size=20, max_overflow=10)
klien = httpx.AsyncClient(timeout=10.0)
app.state.engine = engine
app.state.klien = klien
print("aplikasi siap")
yield # โ aplikasi melayani permintaan di sini
# โโ SAAT MATI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
await klien.aclose()
await engine.dispose()
print("sumber daya dilepas")
app = FastAPI(lifespan=lifespan)
Karena ini context manager, variabel yang dibuat sebelum yield tetap terlihat setelahnya. Anda tidak perlu lagi menaruh objek di variabel global hanya agar fungsi shutdown bisa mengaksesnya. Bila terjadi exception saat aplikasi berjalan, blok setelah yield tetap dijalankan.
๐ซ Kesalahan Fatal: Membuat Klien per Request
@app.get("/produk")
async def ambil_produk():
async with httpx.AsyncClient() as klien: # โ TCP handshake tiap request
r = await klien.get("http://layanan-katalog/produk")
return r.json()
@app.get("/produk")
async def ambil_produk(request: Request):
r = await request.app.state.klien.get("http://layanan-katalog/produk")
return r.json()
Membuat AsyncClient baru untuk setiap permintaan berarti TCP handshake dan negosiasi TLS berulang, tanpa memanfaatkan connection pooling sama sekali. Pada trafik tinggi ini menghabiskan port ephemeral dan menambah latensi puluhan milidetik per panggilan. Klien HTTP dirancang untuk berumur panjang dan dibagikan.
๐งฉ Dependency yang Mengambil dari State
from typing import Annotated
from fastapi import Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
async def dapatkan_sesi(request: Request) -> AsyncSession:
pembuat = async_sessionmaker(request.app.state.engine, expire_on_commit=False)
async with pembuat() as sesi:
yield sesi
SesiDB = Annotated[AsyncSession, Depends(dapatkan_sesi)]
# Dipakai di endpoint dengan sangat ringkas:
@app.get("/pesanan/{id}")
async def ambil(id: int, db: SesiDB):
...
Mendefinisikan SesiDB = Annotated[AsyncSession, Depends(dapatkan_sesi)] sekali lalu memakainya di puluhan endpoint jauh lebih rapi daripada mengulang db: AsyncSession = Depends(dapatkan_sesi) di mana-mana. Ini pola yang dianjurkan dokumentasi resmi FastAPI sekarang.
๐ฅ Health Check yang Benar
@app.get("/healthz") # liveness: HANYA cek proses ini
async def healthz():
return {"status": "ok"}
@app.get("/readyz") # readiness: boleh cek dependensi
async def readyz(request: Request):
try:
async with request.app.state.engine.connect() as conn:
await conn.execute(text("SELECT 1"))
except Exception:
raise HTTPException(status_code=503, detail="database tidak siap")
return {"status": "siap"}
Bila /healthz memeriksa database dan database sedang lambat, Kubernetes akan menganggap semua pod rusak dan membunuh seluruhnya sekaligus โ mengubah gangguan database menjadi pemadaman total aplikasi. Liveness hanya boleh menjawab "apakah proses ini masih waras". Ketergantungan eksternal diperiksa readiness, yang cukup menghentikan trafik tanpa membunuh pod.