Skip to content

bug: Scheduler loses HTTP connection to backend during long-running executions, reporting false failures #101

Description

@vybe

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_taskPOST /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:

  1. Uvicorn worker recycling — Multiple uvicorn workers (5 observed) may recycle during long requests
  2. TCP keepalive defaults — The httpx client in the scheduler creates a new AsyncClient per request (no connection pooling or keepalive configuration)
  3. 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

  1. Create an agent with a scheduled task that takes >10 minutes to complete
  2. Configure the schedule with timeout_seconds: 3600 (1 hour)
  3. Wait for the schedule to trigger
  4. Observe scheduler logs: Server disconnected without sending a response
  5. Verify the agent actually completed the work (check session logs in the agent container)
  6. 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:

  1. POST to /api/internal/execute-task which returns immediately with an execution_id
  2. Poll GET /api/internal/execution/{id}/status periodically until completion
  3. 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.pyexecute_task method
  • src/backend/routers/internal.py/execute-task endpoint
  • src/backend/services/task_execution_service.pyexecute_task method

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions