Kodeloka
  1. Kodeloka
  2. Kelas FastAPI
  3. Materi

Advanced & Deploy ยท Modul 16 dari 24

Lifespan Events & Manajemen Sumber Daya Aplikasi

Mengganti on_event yang sudah usang dengan lifespan context manager untuk mengelola pool koneksi, klien HTTP, dan model machine learning.

๐ŸŽฏ Poin-Poin Utama Modul Ini:
  • @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:

Pola lama โ€” jangan dipakai lagi
@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

main.py
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)
๐Ÿ’ก Kenapa struktur ini lebih baik

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

Salah โ€” membuka koneksi baru setiap kali
@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()
Benar โ€” memakai klien bersama dari lifespan
@app.get("/produk")
async def ambil_produk(request: Request):
    r = await request.app.state.klien.get("http://layanan-katalog/produk")
    return r.json()
โš ๏ธ Dampaknya besar di bawah beban

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

dependencies.py
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):
    ...
๐Ÿ’ก Alias tipe dengan Annotated

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

health.py
@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"}
๐Ÿšซ Jangan cek database di endpoint liveness

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.