Skip to content

Diagram — Listener Loops

Referenced from docs/technical/ARCHITECTURE.md. Shows the four concurrently-watched tasks fleet-dispatch serve starts, and what tears the process down.

flowchart TB
    subgraph serve["fleet-dispatch serve"]
        direction TB

        subgraph http["1. HTTP accept loop — DispatchServer.RunAsync"]
            H1[HttpListener bound to LAN/Tailscale addr + 127.0.0.1] --> H2[GetContextAsync]
            H2 --> H3[HandleSafeAsync per request, Task.Run]
            H3 --> H2
        end

        subgraph queue["2. Queue-drain loop — RunQueueDrainAsync"]
            Q1[Sleep poll interval, default 30s] --> Q2[QueueService.ListenOnceAsync]
            Q2 --> Q1
            Q2 -. "--no-queue disables this task entirely" .-> Q1
        end

        subgraph avail["3. Availability presence loop — AvailabilityDetector.RunAsync"]
            A1[Sleep ~4s] --> A2[Tick: read live signals, apply hysteresis]
            A2 --> A3[Publish WorkerAvailability for /health]
            A3 --> A1
            A2 -. "bad tick swallowed, never stops the loop" .-> A1
        end

        subgraph repo["4. Repo-scan loop — RunRepoScanLoopAsync"]
            R1[Immediate boot scan] --> R2[Sleep --repo-scan-poll-hours, default 24h]
            R2 --> R3[RepoRegistryScanner.ScanAndWriteAsync]
            R3 --> R2
            R3 -. "--no-repo-scan disables this task entirely" .-> R2
            R3 -. "failed scan logs + retries next tick" .-> R2
        end
    end

    WhenAny{{Task.WhenAny watches all 4}} --> Fault[Any task faults or Ctrl-C cancels]
    Fault --> Shutdown[Process exits — a half-dead listener is never left running]

    http --> WhenAny
    queue --> WhenAny
    avail --> WhenAny
    repo --> WhenAny

Key points this diagram encodes:

  • The availability presence loop (3) is always on, independent of whether jobs are currently being refused by the hard gate — it is how the box ever detects its own re-availability.
  • The queue-drain loop (2) and the repo-scan loop (4) can each be individually disabled (--no-queue, --no-repo-scan); the HTTP loop (1) and the availability loop (3) cannot be.
  • Task.WhenAny, not Task.WhenAll, watches the set — this is deliberate (FLT-71): a WhenAll cannot observe one faulted task while an infinite loop among the others keeps running, which previously left a dead HTTP listener next to a live queue-drainer with no way to notice.