Pada arsitektur LLM trading research agent yang menjalankan analisis portofolio multi-langkah via Model Context Protocol (MCP) atau tool calling eksternal, latensi proses sering kali melampaui batas toleransi default gateway. Masalah umum yang kerap muncul di lingkungan produksi adalah terputusnya Server-Sent Events (SSE) dengan status HTTP 504 Gateway Timeout di tengah proses sintesis data pasar.
Artikel ini membedah akar penyebab kegagalan stream timeout tersebut dan memberikan langkah perbaikan konkret: mitigasi async blocking I/O, penambahan heartbeat frame pada FastAPI, serta konfigurasi batas waktu upstream pada reverse-proxy.
Gejala Masalah dan Error Trace
Ketika agent mengeksekusi analisis portofolio kompleks—seperti agregasi data historis harga saham, kalkulasi volatilitas, dan pemindaian news radar secara paralel—koneksi HTTP streaming ke client mendadak terhenti setelah 60 detik.
Client menerima respons:
HTTP/1.1 504 Gateway Timeout
Content-Type: text/html
<html><head><title>504 Gateway Time-out</title></head>...Sementara pada log aplikasi backend (FastAPI/Uvicorn), muncul trace yang mengindikasikan koneksi diputus sepihak oleh klien/proxy:
ERROR:uvicorn.error:Exception in ASGI application
Traceback (most recent call last):
File "uvicorn/protocols/http/httptools_impl.py", line 426, in send
await self.transport.write(data)
File "asyncio/selector_events.py", line 1084, in write
raise ConnectionResetError("Cannot write to closing transport")
ConnectionResetError: Cannot write to closing transportAnalisis Root Cause
Kegagalan ini umumnya disebabkan oleh tiga faktor teknis yang saling berkaitan:
- Async Blocking I/O dan Pool Exhaustion: Penggunaan client HTTP sinkron (seperti library
requests) di dalam coroutine async, atau instansiasihttpx.AsyncClienttanpa batas koneksi yang tepat di setiap iterasi tool MCP. Ini memblokir thread asyncio event loop dan menguras connection pool kernel (TCP TIME_WAIT exhaustion). - Ketiadaan SSE Heartbeat (Ping Frame): Saat LLM memproses tool call berat selama 30–90 detik, backend tidak mengirimkan byte apa pun ke stream. Reverse-proxy (seperti Nginx atau Cloudflare) menganggap upstream mati jika tidak ada data yang melewati socket dalam durasi tertentu (default
proxy_read_timeoutbiasanya 60 detik). - Unhandled Backpressure: Konsumsi token LLM terhambat saat jaringan lambat, menyebabkan buffer transfer internal penuh dan memicu timeout pada socket writer.
Langkah Perbaikan
1. Ganti Blocking Client dengan Singleton Async HTTPX Client
Hindari membuat instance client baru di setiap tool call MCP. Gunakan koneksi persisten berbasis connection pooling non-blocking:
import httpx
# Singleton async client dengan batas pool eksplisit
http_client = httpx.AsyncClient(
limits=httpx.Limits(max_keepalive_connections=50, max_connections=200),
timeout=httpx.Timeout(connect=5.0, read=30.0, write=5.0, pool=5.0)
)
async def fetch_stock_radar(ticker: str) -> dict:
# Non-blocking async I/O
response = await http_client.get(f"https://api.marketdata.internal/v1/ticker/{ticker}")
response.raise_for_status()
return response.json()2. Implementasi Keep-Alive Ping Frame pada FastAPI StreamingResponse
Gunakan generator berbasis interval yang menyisipkan SSE comment line (: ping\n\n) saat LLM agent sedang menunggu hasil tool execution. SSE comment line diabaikan oleh parser EventSource pada browser, namun cukup untuk me-reset timer idle timeout reverse-proxy.
import asyncio
import json
from typing import AsyncGenerator
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def research_agent_stream(query: str) -> AsyncGenerator[str, None]:
queue: asyncio.Queue[str | None] = asyncio.Queue()
async def agent_worker():
try:
# Tahap 1: Eksekusi Tool Call MCP
await queue.put(f"data: {json.dumps({'status': 'fetching_market_data'})}\n\n")
stock_data = await fetch_stock_radar("BBCA")
# Tahap 2: Agregasi News Radar (Operasi latency tinggi)
await queue.put(f"data: {json.dumps({'status': 'processing_news_radar'})}\n\n")
await asyncio.sleep(40) # Simulasi latensi agregasi
# Tahap 3: Sintesis akhir
await queue.put(f"data: {json.dumps({'result': 'Portfolio bullish'})}\n\n")
finally:
await queue.put(None) # Sentinel penanda selesai
worker_task = asyncio.create_task(agent_worker())
while True:
try:
# Kirim ping frame setiap 15 detik jika tidak ada chunk baru
item = await asyncio.wait_for(queue.get(), timeout=15.0)
if item is None:
break
yield item
except asyncio.TimeoutError:
# Keep-alive frame untuk mencegah reverse proxy 504
yield ": keep-alive-ping\n\n"
await worker_task
@app.get("/api/v1/trade/research-stream")
async def trade_stream(query: str):
return StreamingResponse(
research_agent_stream(query),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # Wajib untuk Nginx
}
)3. Konfigurasi Upstream Reverse-Proxy (Nginx)
Nonaktifkan proxy buffering dan sesuaikan timeout transport agar tidak mendahului siklus timeout runtime aplikasi:
location /api/v1/trade/ {\n proxy_pass http://fastapi_backend;\n proxy_http_version 1.1;\n proxy_set_header Connection "";\n \n # Mematikan buffer agar chunk SSE langsung diteruskan ke client\n proxy_buffering off;\n proxy_cache off;\n \n # Atur idle timeout lebih tinggi dari interval ping frame aplikasi\n proxy_read_timeout 300s;\n proxy_connect_timeout 10s;\n proxy_send_timeout 300s;\n}Kesimpulan
Error stream timeout pada arsitektur LLM trading agent bukan disebabkan oleh bug model sintesis, melainkan disrupsi pada transport layer TCP/HTTP. Kombinasi X-Accel-Buffering: no, client HTTP asynchronous dengan batas connection pool terukur, serta SSE ping interval di bawah ambang batas reverse proxy memastikan pipeline riset portofolio tetap stabil terdistribusi secara real-time tanpa drop koneksi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!