โ† Semua pembelajaran / Python untuk AI Engineer
Fase 5 ยท Bangun API dengan FastAPI

Lifespan Events

Lifespan adalah tempat membuat resource yang berumur selama aplikasi hidup: client, koneksi pool, model yang dimuat.

Intisari

  • Kode sebelum yield berjalan sekali saat startup; kode sesudahnya saat shutdown.
  • Tempat yang benar untuk: client LLM, HTTP client, connection pool DB, model embedding.
  • Simpan di app.state, ambil lewat dependency.
  • Menggantikan @app.on_event("startup") yang sudah usang.
  • Kalau setup gagal, aplikasi gagal start โ€” dan itu memang yang kamu mau.

Bentuknya

from contextlib import asynccontextmanager
from fastapi import FastAPI
import anthropic, httpx

@asynccontextmanager
async def lifespan(app: FastAPI):
    # โ”€โ”€ Startup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    app.state.llm = anthropic.AsyncAnthropic()
    app.state.http = httpx.AsyncClient(timeout=30.0)
    app.state.vector = await ChromaClient.connect(settings.chroma_url)
    logger.info("aplikasi siap")

    yield                                  # โ† aplikasi melayani request di sini

    # โ”€โ”€ Shutdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    await app.state.http.aclose()
    await app.state.llm.close()
    await app.state.vector.close()
    logger.info("aplikasi berhenti")


app = FastAPI(lifespan=lifespan)

@app.on_event("startup") dan ("shutdown") sudah usang. Kalau kamu menemukannya di tutorial, itu tandanya tutorial-nya lama. Pakai lifespan โ€” satu fungsi untuk keduanya, dan setup/teardown yang berpasangan jadi berdekatan di kode.

Kenapa ini penting

# โŒ client baru untuk SETIAP request
@app.post("/chat")
async def chat(req: ChatRequest):
    client = anthropic.AsyncAnthropic()      # pool baru, handshake TLS baru
    return await client.messages.create(...)

# โœ… satu client untuk seumur hidup aplikasi
@app.post("/chat")
async def chat(req: ChatRequest, client: ClientDep):
    return await client.messages.create(...)

Biayanya nyata: handshake TLS memakan 100โ€“300 ms. Pada 1.000 request per menit, membuat client baru tiap kali berarti membuang beberapa menit CPU dan latensi per jam โ€” untuk pekerjaan yang seharusnya nol.

Mengaksesnya lewat dependency

from typing import Annotated
from fastapi import Depends, Request

def get_llm(request: Request) -> anthropic.AsyncAnthropic:
    return request.app.state.llm

def get_http(request: Request) -> httpx.AsyncClient:
    return request.app.state.http

LlmDep = Annotated[anthropic.AsyncAnthropic, Depends(get_llm)]
HttpDep = Annotated[httpx.AsyncClient, Depends(get_http)]


@app.post("/chat")
async def chat(req: ChatRequest, llm: LlmDep) -> ChatResponse:
    r = await llm.messages.create(...)
    return ChatResponse(...)

Membungkusnya dalam dependency (bukan mengakses app.state langsung) membuatnya bisa di-override saat testing.

Apa yang layak masuk lifespan

ResourceKenapa
Client LLMConnection pool, konfigurasi retry
HTTP clientKeep-alive, connection pooling
Connection pool DBMembuat koneksi itu mahal
Client vector storeKoneksi persisten
Model embedding lokalMemuat ke memori butuh detik hingga menit
Client cache (Redis)Connection pool
Latar belakang berkalaTask yang harus dimulai dan dihentikan bersama aplikasi

Gagal saat startup itu bagus

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.settings = Settings()          # gagal kalau env var wajib kosong

    app.state.vector = await ChromaClient.connect(settings.chroma_url)
    if not await app.state.vector.ping():
        raise RuntimeError("vector store tidak bisa dihubungi")

    yield
    await app.state.vector.close()

Aplikasi yang gagal start lebih baik daripada aplikasi yang start lalu mengembalikan 500. Orkestrator container (Docker, ECS, Kubernetes) akan melihat kegagalan itu dan tidak mengalihkan trafik ke instance yang rusak. Deploy-nya gagal dengan jelas, bukan diam-diam melayani error.

Task latar belakang berkala

import asyncio

async def segarkan_index(app: FastAPI) -> None:
    while True:
        try:
            await asyncio.sleep(3600)
            await app.state.vector.reindex()
        except asyncio.CancelledError:
            raise                     # jangan telan โ€” ini sinyal shutdown
        except Exception:
            logger.exception("gagal reindex")


@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.vector = await ChromaClient.connect(...)
    task = asyncio.create_task(segarkan_index(app))

    yield

    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        pass
    await app.state.vector.close()

Lifespan saat testing

from fastapi.testclient import TestClient

# โŒ lifespan TIDAK berjalan
client = TestClient(app)
client.get("/")

# โœ… lifespan berjalan
with TestClient(app) as client:
    client.get("/")

Ini sumber kebingungan yang umum. Tanpa blok with, lifespan tidak berjalan dan app.state.llm tidak ada โ€” testmu akan gagal dengan AttributeError yang membingungkan. Selalu pakai TestClient sebagai context manager.

Health check

@app.get("/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}

@app.get("/ready")
async def ready(request: Request) -> dict[str, bool]:
    return {
        "vector_store": await request.app.state.vector.ping(),
        "database": await request.app.state.db.ping(),
    }

Bedanya: /health menjawab "proses ini hidup" (liveness), /ready menjawab "aplikasi ini siap melayani trafik" (readiness). Orkestrator memakai keduanya untuk keputusan yang berbeda.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.