Stage 4: honest health, explicit error policy, event-bus integrity
Engine mode is now explicit: USE_MOCK_SIP=true is the only way to get the mock engine; an unconfigured trunk fails startup with guidance instead of silently degrading. Root-caused why the engine always ran mock: nested pydantic-settings never read .env (no env_file on the sub-settings classes) — all 8 now declare it. /health stops lying: reports engine mode (sippy|mock), a live DB SELECT 1, trunk registration state with reason, and TTS/STT availability from their last real request; "healthy" now requires ready + db + sippy + registered trunk. Error policy: leaf services (tts/transcription/llm_client) raise and track availability; call-loop callers catch, publish EventType.ERROR naming the failed service, and apply an explicit fallback. Persistence writes get one bounded 3x exponential retry, then an ERROR log — no more silent data loss. Event bus: a full subscriber queue drops its oldest event (counted) instead of silently evicting the subscription; subscribe(replay_last=N) delivers the advertised history replay, used by /ws/events (25). Receptionist correctness: a matched TAKE_MESSAGE rule beats the LLM; voicemail polls for early hangup and stops/transcribes/hangs up in finally; RecordingSession finally keeps its leg_ids so taps detach. Dead code removed: models/contact.py + Contact table, dtmf_buffer, transcribe_stream stub, SMS stub in notification.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
73
main.py
73
main.py
@@ -172,18 +172,23 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
gateway.register_mode_handler(CallMode.HOLD_SLAYER, launch_hold_slayer)
|
||||
|
||||
gateway.sip_engine = build_sip_engine(
|
||||
settings,
|
||||
gateway.media_pipeline,
|
||||
on_leg_state_change=gateway._on_sip_leg_state,
|
||||
on_device_registered=gateway._on_sip_device_registered,
|
||||
on_incoming_call=receptionist.on_inbound_call,
|
||||
)
|
||||
try:
|
||||
gateway.sip_engine = build_sip_engine(
|
||||
settings,
|
||||
gateway.media_pipeline,
|
||||
on_leg_state_change=gateway._on_sip_leg_state,
|
||||
on_device_registered=gateway._on_sip_device_registered,
|
||||
on_incoming_call=receptionist.on_inbound_call,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.critical(f"\n❌ SIP engine failed to initialize:\n {e}")
|
||||
sys.exit(1)
|
||||
|
||||
await routing_svc.start()
|
||||
await gateway.start()
|
||||
app.state.gateway = gateway
|
||||
app.state.routing_service = routing_svc
|
||||
app.state.transcription_service = transcription
|
||||
|
||||
notification_svc = NotificationService(gateway.event_bus, settings)
|
||||
await notification_svc.start()
|
||||
@@ -298,21 +303,67 @@ async def root():
|
||||
|
||||
@app.get("/health", tags=["System"])
|
||||
async def health():
|
||||
"""Health check endpoint."""
|
||||
"""
|
||||
Health check. "healthy" means the gateway can actually do its job:
|
||||
real engine, registered trunk, reachable database. A mock engine or
|
||||
a failing dependency reports "degraded" with the reason visible.
|
||||
"""
|
||||
from core.sip_engine import MockSIPEngine
|
||||
from db.database import session_scope
|
||||
|
||||
gateway = getattr(app.state, "gateway", None)
|
||||
ready = gateway is not None and await gateway.sip_engine.is_ready()
|
||||
trunk_status = await gateway.sip_engine.get_trunk_status() if gateway else {"registered": False}
|
||||
return {
|
||||
"status": "healthy" if ready else "degraded",
|
||||
engine_mode = (
|
||||
"mock" if gateway is None or isinstance(gateway.sip_engine, MockSIPEngine)
|
||||
else "sippy"
|
||||
)
|
||||
|
||||
db_ok = False
|
||||
db_error = None
|
||||
try:
|
||||
from sqlalchemy import text
|
||||
async with session_scope() as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
db_ok = True
|
||||
except Exception as e:
|
||||
db_error = str(e)[:200]
|
||||
|
||||
healthy = (
|
||||
ready
|
||||
and db_ok
|
||||
and engine_mode == "sippy"
|
||||
and trunk_status.get("registered", False)
|
||||
)
|
||||
|
||||
checks = {
|
||||
"gateway": "ready" if gateway else "not initialized",
|
||||
"engine": engine_mode,
|
||||
"sip_engine": "ready" if ready else "not ready",
|
||||
"database": "ok" if db_ok else f"error: {db_error}",
|
||||
"sip_trunk": {
|
||||
"registered": trunk_status.get("registered", False),
|
||||
"host": trunk_status.get("host"),
|
||||
"mock": trunk_status.get("mock", False),
|
||||
"reason": trunk_status.get("reason"),
|
||||
},
|
||||
}
|
||||
if gateway is not None:
|
||||
tts = getattr(gateway, "_tts", None)
|
||||
checks["tts"] = _availability(tts)
|
||||
transcription = getattr(app.state, "transcription_service", None)
|
||||
checks["stt"] = _availability(transcription)
|
||||
|
||||
return {"status": "healthy" if healthy else "degraded", **checks}
|
||||
|
||||
|
||||
def _availability(service) -> str:
|
||||
"""Last-known reachability of an HTTP leaf service."""
|
||||
if service is None:
|
||||
return "not attached"
|
||||
available = getattr(service, "available", None)
|
||||
if available is None:
|
||||
return "unknown (no requests yet)"
|
||||
return "ok" if available else "unreachable"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user