Summary
The scheduler's HTTP connection to the backend drops during long-running agent executions (10-20+ minutes), causing Server disconnected without sending a response errors. The agent tasks actually complete successfully on the container side, but the scheduler never receives the response. This results in zero execution records for affected agents and misleading error logs.
Component
Scheduler / Backend (TaskExecutionService)
Priority
P2
Error
[ERROR] scheduler.service: Schedule Heartbeat execution failed: Server disconnected without sending a response.
This is an httpx RemoteProtocolError — the TCP connection from the scheduler to the backend is reset by the server before a response is returned.
Location
- File:
src/scheduler/service.py (line ~713, _call_backend_execute_task)
- File:
src/backend/routers/internal.py (line ~185, execute_task_internal)
- Function:
_call_backend_execute_task → POST /api/internal/execute-task
Root Cause
The scheduler calls POST /api/internal/execute-task with httpx and waits for the backend to return. The backend's endpoint is a synchronous HTTP request that blocks until the agent finishes (via agent_post_with_retry → agent container's /api/task). For long-running tasks (10-20+ minutes), the underlying TCP connection between scheduler and backend is reset.
Possible causes:
- Uvicorn worker recycling — Multiple uvicorn workers (5 observed) may recycle during long requests
- TCP keepalive defaults — The httpx client in the scheduler creates a new
AsyncClient per request (no connection pooling or keepalive configuration)
- Docker network timeouts — The trinity-network bridge may have default TCP timeout settings that close idle-appearing connections
The key issue is the architecture: the scheduler makes a blocking HTTP request that must stay open for the entire duration of the agent's execution (potentially 30-60 minutes). Any connection instability causes a false failure.
Observed Impact
- False failure logging: Scheduler logs errors even though agent work completes successfully
- Zero execution records:
schedule_executions table has 0 rows for affected agents — the scheduler can't record results it never receives
- Stale slot cleanups: Every execution cycle triggers a stale slot cleanup warning because the previous slot wasn't released cleanly
- No actual data loss: Agent work (predictions, commits, dashboard updates) completes normally on the container side
Reproduction Steps
- Create an agent with a scheduled task that takes >10 minutes to complete
- Configure the schedule with
timeout_seconds: 3600 (1 hour)
- Wait for the schedule to trigger
- Observe scheduler logs:
Server disconnected without sending a response
- Verify the agent actually completed the work (check session logs in the agent container)
- Check
schedule_executions table — no records for the agent
Suggested Fix
Option A (Recommended): Fire-and-forget with polling
Instead of blocking on the HTTP response, the scheduler should:
- POST to
/api/internal/execute-task which returns immediately with an execution_id
- Poll
GET /api/internal/execution/{id}/status periodically until completion
- This eliminates the long-lived HTTP connection entirely
# In scheduler/service.py _call_backend_execute_task:
async with httpx.AsyncClient() as client:
# Step 1: Start execution (returns immediately)
response = await client.post(
f"{config.backend_url}/api/internal/execute-task",
headers=headers,
json={**payload, "async": True},
timeout=30.0, # Short timeout for dispatch
)
execution_id = response.json()["execution_id"]
# Step 2: Poll for completion
deadline = time.monotonic() + request_timeout
while time.monotonic() < deadline:
await asyncio.sleep(10)
status_resp = await client.get(
f"{config.backend_url}/api/internal/execution/{execution_id}/status",
headers=headers,
timeout=10.0,
)
result = status_resp.json()
if result["status"] in ("completed", "failed"):
return result
Option B (Quick fix): Increase TCP keepalive and connection resilience
# In scheduler/service.py _call_backend_execute_task:
transport = httpx.AsyncHTTPTransport(
retries=1,
keepalive_expiry=30.0,
)
async with httpx.AsyncClient(
transport=transport,
timeout=httpx.Timeout(request_timeout, connect=10.0, pool=10.0),
) as client:
...
Environment
- Trinity version:
d46281f
- Scheduler: Python/APScheduler with httpx client
- Backend: FastAPI/Uvicorn (multiple workers)
- Affected agents: Any agent with scheduled tasks running >10 minutes
Related
src/scheduler/service.py — _call_backend_execute_task method
src/scheduler/agent_client.py — execute_task method
src/backend/routers/internal.py — /execute-task endpoint
src/backend/services/task_execution_service.py — execute_task method
Summary
The scheduler's HTTP connection to the backend drops during long-running agent executions (10-20+ minutes), causing
Server disconnected without sending a responseerrors. The agent tasks actually complete successfully on the container side, but the scheduler never receives the response. This results in zero execution records for affected agents and misleading error logs.Component
Scheduler / Backend (TaskExecutionService)
Priority
P2
Error
This is an httpx
RemoteProtocolError— the TCP connection from the scheduler to the backend is reset by the server before a response is returned.Location
src/scheduler/service.py(line ~713,_call_backend_execute_task)src/backend/routers/internal.py(line ~185,execute_task_internal)_call_backend_execute_task→POST /api/internal/execute-taskRoot Cause
The scheduler calls
POST /api/internal/execute-taskwith httpx and waits for the backend to return. The backend's endpoint is a synchronous HTTP request that blocks until the agent finishes (viaagent_post_with_retry→ agent container's/api/task). For long-running tasks (10-20+ minutes), the underlying TCP connection between scheduler and backend is reset.Possible causes:
AsyncClientper request (no connection pooling or keepalive configuration)The key issue is the architecture: the scheduler makes a blocking HTTP request that must stay open for the entire duration of the agent's execution (potentially 30-60 minutes). Any connection instability causes a false failure.
Observed Impact
schedule_executionstable has 0 rows for affected agents — the scheduler can't record results it never receivesReproduction Steps
timeout_seconds: 3600(1 hour)Server disconnected without sending a responseschedule_executionstable — no records for the agentSuggested Fix
Option A (Recommended): Fire-and-forget with polling
Instead of blocking on the HTTP response, the scheduler should:
/api/internal/execute-taskwhich returns immediately with anexecution_idGET /api/internal/execution/{id}/statusperiodically until completionOption B (Quick fix): Increase TCP keepalive and connection resilience
Environment
d46281fRelated
src/scheduler/service.py—_call_backend_execute_taskmethodsrc/scheduler/agent_client.py—execute_taskmethodsrc/backend/routers/internal.py—/execute-taskendpointsrc/backend/services/task_execution_service.py—execute_taskmethod