From d4d259fa58c75d0130c4f5726ebca0a0dcd9444a Mon Sep 17 00:00:00 2001 From: Muse Date: Fri, 23 Oct 2026 18:15:05 +0800 Subject: [PATCH] probe ws: frame cadence follows probe_fast_interval (V2.0.2) --- internal/web/controller/probe_public.go | 21 ++++--- .../web/controller/probe_ws_cadence_test.go | 63 +++++++++++++++++++ 2 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 internal/web/controller/probe_ws_cadence_test.go diff --git a/internal/web/controller/probe_public.go b/internal/web/controller/probe_public.go index 1064d15..7b991a4 100644 --- a/internal/web/controller/probe_public.go +++ b/internal/web/controller/probe_public.go @@ -211,10 +211,12 @@ func (pc *PublicProbeController) nodeMetrics(c *gin.Context) { c.JSON(http.StatusOK, resp) } -// liveWS is the hub's browser stream: a snapshot frame every 2 s, -// text by default, binary gzip frames for the anonymous audience when -// opened with ?gzip. Audience eligibility is re-checked every tick — -// a socket outlives both the session and the public-page switch. +// liveWS is the hub's browser stream: a snapshot frame on the +// operator's fast-report cadence (V2.0.2; the hub's fixed 2 s tick +// made a 1 s agent cadence invisible up front), text by default, +// binary gzip frames for the anonymous audience when opened with +// ?gzip. Audience eligibility is re-checked every tick — a socket +// outlives both the session and the public-page switch. func (pc *PublicProbeController) liveWS(c *gin.Context) { authed := probeAuthed(c) && !fromOtherOrigin(c) if !authed && !service.ProbePublicPageEnabled() { @@ -280,18 +282,19 @@ func (pc *PublicProbeController) liveWS(c *gin.Context) { return conn.WriteMessage(frameType, frame) == nil } - // The first frame goes out immediately (hub's interval ticks at - // once), then one per 2 s tick. + // The first frame goes out immediately; later frames follow the + // fast-report cadence, re-read after every frame so a settings + // change lands on a live socket without a reconnect. if !send() { return } - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() for { + timer := time.NewTimer(time.Duration(service.ProbeFastIntervalSec()) * time.Second) select { case <-done: + timer.Stop() return - case <-ticker.C: + case <-timer.C: if !send() { return } diff --git a/internal/web/controller/probe_ws_cadence_test.go b/internal/web/controller/probe_ws_cadence_test.go new file mode 100644 index 0000000..0100f9b --- /dev/null +++ b/internal/web/controller/probe_ws_cadence_test.go @@ -0,0 +1,63 @@ +package controller + +import ( + "net/http/httptest" + "strings" + "testing" + "time" + + ws "github.com/gorilla/websocket" + + "github.com/mhsanaei/3x-ui/v3/internal/web/service" +) + +// V2.0.2: /api/ws frame cadence follows the operator's fast-report +// interval (probe_fast_interval) instead of the hub's fixed 2 s tick. +// A mid-stream settings change re-paces a live socket: the timer +// already in flight still fires on the old cadence, the next one is +// armed from the re-read setting. +func TestProbeWebSocketCadenceFollowsFastInterval(t *testing.T) { + rig := newProbeTestRig(t) + _, _ = rig.seedAgent(t, "CadenceNode", true) + rig.setSetting(t, "public_page", "on") + rig.setSetting(t, "probe_fast_interval", "1") + service.InvalidateProbeSnapshot() + + srv := httptest.NewServer(rig.engine) + defer srv.Close() + wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/api/ws" + + conn, _, err := ws.DefaultDialer.Dial(wsURL, nil) + if err != nil { + t.Fatalf("dial: %v", err) + } + defer conn.Close() + + readAt := func() time.Time { + t.Helper() + _ = conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + if _, _, err := conn.ReadMessage(); err != nil { + t.Fatalf("ws read: %v", err) + } + return time.Now() + } + + _ = readAt() // first frame: immediate + prev := readAt() + for i := 0; i < 2; i++ { + now := readAt() + if gap := now.Sub(prev); gap < 500*time.Millisecond || gap > 2500*time.Millisecond { + t.Fatalf("frame gap at 1 s cadence: %v", gap) + } + prev = now + } + + // Change to 3 s mid-stream: one in-flight 1 s frame, then the + // stream settles onto the new cadence without a reconnect. + rig.setSetting(t, "probe_fast_interval", "3") + prev = readAt() + now := readAt() + if gap := now.Sub(prev); gap < 2200*time.Millisecond || gap > 6*time.Second { + t.Fatalf("frame gap after change to 3 s: %v", gap) + } +}