From c006c3988e607ca77ecff817766d5495ab697967 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 11 Aug 2026 17:37:49 +0800 Subject: [PATCH] perf(ui): probe sources asynchronously so /api/status no longer blocks on upstream reachability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ensureProbe() called Registry.ProbeAll() synchronously on every status request when >30s had passed since the last probe, blocking the response on the slowest upstream (zen ~1s, frank ~1.9s, total ~1.5s) — this stalled WebUI page loads and tab switches. Now the probe runs in a background goroutine (15s budget); status returns in ~7ms and the 3s stats poll picks up updated reachability next tick. Deployed (bak .bak.20260811s), server active. --- internal/gateway/server.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/gateway/server.go b/internal/gateway/server.go index 9bd591b..1f54ce9 100644 --- a/internal/gateway/server.go +++ b/internal/gateway/server.go @@ -377,6 +377,9 @@ func (g *Gateway) handleModels(w http.ResponseWriter, r *http.Request) { } // ensureProbe triggers a live source probe at most once every 30s. +// The probe runs asynchronously so a slow/stuck upstream never blocks the +// status response — reachability data is eventually-consistent and the UI +// polls /api/stats every 3s anyway, so the next refresh picks it up. func (g *Gateway) ensureProbe(ctx context.Context) { g.probeMu.Lock() due := time.Since(g.lastProbe) > 30*time.Second @@ -384,9 +387,14 @@ func (g *Gateway) ensureProbe(ctx context.Context) { g.lastProbe = time.Now() } g.probeMu.Unlock() - if due { - g.core.Registry().ProbeAll(ctx) + if !due { + return } + go func() { + probeCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + g.core.Registry().ProbeAll(probeCtx) + }() } // handleResetHealth (admin) clears the per-source backoff state so a fixed