엔진 readiness 실패 캐시 TTL 분리와 프로브 타임아웃 보정
공개 런타임에서 reasoning_effort=high 콜드 프로브 1회가 20초를 넘기면 실패가 성공과 같은 TTL(운영 1800초)로 캐시되어 공개 API가 최장 30분간 engine=false로 오염되고, 워치독은 기본 /ready의 캐시된 200만 보고 API만 재시작하는 무한 루프에 빠졌다(2026-08-18 06:30 KST 실측). - 실패 항목 전용 ENGINE_READY_FAILURE_TTL_SECONDS(기본 30초)를 분리해 복구가 프로브 한 번 안에 감지되게 하고, 진짜 장애 중 프로브 폭풍은 실패 TTL이 계속 막는다. - ENGINE_READY_TIMEOUT_SECONDS 기본값을 20→45초로 올려 고효율 콜드 스폰의 정상 지연을 장애로 오판하지 않는다. - 회귀 4건 신설(실패 재프로브·성공 캐시 유지·실패 폭풍 억제·기본값 계약), 게이트웨이 62/62·API 전체 986 passed·ruff clean.
This commit is contained in:
parent
0c56f1857a
commit
4771b97c3a
2 changed files with 139 additions and 3 deletions
|
|
@ -51,7 +51,14 @@ DEFAULT_MODEL = os.environ.get("ENGINE_MODEL", "") # 비우면 CLI 기본(Opus
|
|||
FALLBACK_MODEL = os.environ.get("ENGINE_FALLBACK_MODEL", "")
|
||||
DEFAULT_BUDGET = float(os.environ.get("SESSION_BUDGET_USD", "5.0"))
|
||||
READY_TTL_SECONDS = float(os.environ.get("ENGINE_READY_TTL_SECONDS", "30"))
|
||||
READY_TIMEOUT_SECONDS = float(os.environ.get("ENGINE_READY_TIMEOUT_SECONDS", "20"))
|
||||
# 실패 결과를 성공과 같은 TTL로 캐시하면 콜드 프로브 1회 타임아웃이 운영 TTL(1800초)
|
||||
# 내내 공개 API를 engine=false로 오염시킨다(2026-08-18 06:30 KST 30분 장애). 실패만
|
||||
# 짧게 캐시해 재프로브하되, 진짜 장애 중 프로브 폭풍은 이 TTL이 막는다.
|
||||
READY_FAILURE_TTL_SECONDS = float(
|
||||
os.environ.get("ENGINE_READY_FAILURE_TTL_SECONDS", "30")
|
||||
)
|
||||
# reasoning_effort=high 콜드 스폰은 정상 상태에서도 20초를 넘길 수 있다(2026-08-18 실측).
|
||||
READY_TIMEOUT_SECONDS = float(os.environ.get("ENGINE_READY_TIMEOUT_SECONDS", "45"))
|
||||
READY_BUDGET_USD = float(os.environ.get("ENGINE_READY_BUDGET_USD", "0.5"))
|
||||
# 단발 생성(/v1/generate) 턴 타임아웃 — 페르소나 초안 생성 같은 대형 구조화 출력은
|
||||
# 120초를 넘길 수 있어 설정 가능하게 한다(2026-07-15). 호출부(app ENGINE_TIMEOUT)와 정합 필요.
|
||||
|
|
@ -459,6 +466,13 @@ async def health():
|
|||
}
|
||||
|
||||
|
||||
def _ready_entry_ttl(entry: dict[str, Any]) -> float:
|
||||
"""실패 항목은 짧은 TTL로만 캐시해 복구가 프로브 한 번 안에 감지되게 한다."""
|
||||
if entry.get("ok"):
|
||||
return READY_TTL_SECONDS
|
||||
return min(READY_TTL_SECONDS, READY_FAILURE_TTL_SECONDS)
|
||||
|
||||
|
||||
def _ready_response(
|
||||
entry: dict[str, Any],
|
||||
*,
|
||||
|
|
@ -505,7 +519,7 @@ async def ready(
|
|||
cache_key, {"checked_at": 0.0, "ok": False, "detail": "not checked"}
|
||||
)
|
||||
age = time.monotonic() - float(entry.get("checked_at", 0.0) or 0.0)
|
||||
if not force and age < READY_TTL_SECONDS:
|
||||
if not force and age < _ready_entry_ttl(entry):
|
||||
return _ready_response(
|
||||
entry,
|
||||
provider=provider,
|
||||
|
|
@ -520,7 +534,7 @@ async def ready(
|
|||
cache_key, {"checked_at": 0.0, "ok": False, "detail": "not checked"}
|
||||
)
|
||||
age = time.monotonic() - float(entry.get("checked_at", 0.0) or 0.0)
|
||||
if not force and age < READY_TTL_SECONDS:
|
||||
if not force and age < _ready_entry_ttl(entry):
|
||||
return _ready_response(
|
||||
entry,
|
||||
provider=provider,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue