mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-24 02:17:47 +02:00
fix: prevent WebSocket reconnect loops on slow agent collections (#2294)
The hub waited only 5s (the request manager default) for stats over WebSocket and closed the connection on any error. On hosts where `zpool list` stalls (seen on Proxmox, up to its 10s timeout), collection exceeded that limit, so the hub sent a close (code 1000) and the agent reconnected. The refresh ran every other cycle, which caused a disconnect roughly every 2 minutes. Hub: - Wait up to 30s for WebSocket stats responses. - Keep the connection open when a stats request times out; only close it (and fall back to SSH) for other errors. Agent: - After the first collection, refresh `zpool list` pool stats and `zfs list` dataset usage in the background and serve cached values meanwhile, so a hung utility cannot delay the stats response.
This commit is contained in:
82
internal/hub/systems/ws_data_timeout_test.go
Normal file
82
internal/hub/systems/ws_data_timeout_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
//go:build testing
|
||||
|
||||
package systems
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
esystem "github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/henrygd/beszel/internal/hub/ws"
|
||||
"github.com/lxzan/gws"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// slowDataClient answers GetData only after release is closed, simulating an
|
||||
// agent whose collection outlasts the hub's request timeout.
|
||||
type slowDataClient struct {
|
||||
gws.BuiltinEventHandler
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
func (c *slowDataClient) OnMessage(conn *gws.Conn, message *gws.Message) {
|
||||
defer message.Close()
|
||||
var req common.HubRequest[cbor.RawMessage]
|
||||
if err := cbor.Unmarshal(message.Bytes(), &req); err != nil || req.Action != common.GetData {
|
||||
return
|
||||
}
|
||||
<-c.release
|
||||
response, _ := cbor.Marshal(common.AgentResponse{Id: req.Id, SystemData: &esystem.CombinedData{}})
|
||||
_ = conn.WriteMessage(gws.OpcodeBinary, response)
|
||||
}
|
||||
|
||||
func TestFetchDataTimeoutKeepsWebSocketOpen(t *testing.T) {
|
||||
originalTimeout := wsDataRequestTimeout
|
||||
wsDataRequestTimeout = 50 * time.Millisecond
|
||||
t.Cleanup(func() { wsDataRequestTimeout = originalTimeout })
|
||||
|
||||
connections := make(chan *ws.WsConn, 1)
|
||||
upgrader := gws.NewUpgrader(&monitorSyncServer{}, nil)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
wsConn := ws.NewWsConnection(conn, semver.MustParse("0.20.0"))
|
||||
conn.Session().Store("wsConn", wsConn)
|
||||
connections <- wsConn
|
||||
conn.ReadLoop()
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := &slowDataClient{release: make(chan struct{})}
|
||||
conn, _, err := gws.NewClient(client, &gws.ClientOption{Addr: "ws" + strings.TrimPrefix(server.URL, "http")})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = conn.NetConn().Close() })
|
||||
go conn.ReadLoop()
|
||||
|
||||
var sys *System
|
||||
select {
|
||||
case wsConn := <-connections:
|
||||
sys = &System{WsConn: wsConn}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("websocket connection was not established")
|
||||
}
|
||||
|
||||
_, err = sys.fetchDataFromAgent(common.DataRequestOptions{})
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
require.True(t, sys.WsConn.IsConnected(), "a slow collection must not close the connection")
|
||||
|
||||
// The late response is discarded and the next request still succeeds.
|
||||
close(client.release)
|
||||
_, err = sys.fetchDataFromAgent(common.DataRequestOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user