diff --git a/api/internal/ckpool/client.go b/api/internal/ckpool/client.go index 53343db..ce4055b 100644 --- a/api/internal/ckpool/client.go +++ b/api/internal/ckpool/client.go @@ -51,7 +51,24 @@ var ErrProtocol = errors.New("ckpool: protocol error") // Send delivers a single command to the named ckpool socket and returns the // raw response bytes. `sockName` is one of SocketListener, SocketStratifier, // or SocketConnector. +// +// If the server closes the connection before writing a response (common +// during ckpool warm-up — the stratifier accepts the socket but its stats +// aren't populated yet), Send retries once after a short backoff. func (c *Client) Send(ctx context.Context, sockName, cmd string) ([]byte, error) { + buf, err := c.sendOnce(ctx, sockName, cmd) + if err != nil && errors.Is(err, io.EOF) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(200 * time.Millisecond): + } + return c.sendOnce(ctx, sockName, cmd) + } + return buf, err +} + +func (c *Client) sendOnce(ctx context.Context, sockName, cmd string) ([]byte, error) { if cmd == "" { return nil, fmt.Errorf("ckpool: empty command") } diff --git a/api/internal/state/aggregator.go b/api/internal/state/aggregator.go index 88407c9..cb6f2d4 100644 --- a/api/internal/state/aggregator.go +++ b/api/internal/state/aggregator.go @@ -59,6 +59,12 @@ type Aggregator struct { mu sync.RWMutex snap Snapshot blocks []BlockRecord + + // ckFailStreak counts consecutive refreshes where CKPool returned an + // error. The first failure after a streak of successes is logged at + // DEBUG (likely a transient warm-up or lock hiccup); repeated failures + // escalate to WARN. + ckFailStreak int } func New(ck *ckpool.Client, rpc *bitcoind.RPC, interval time.Duration, log *slog.Logger) *Aggregator { @@ -108,8 +114,17 @@ func (a *Aggregator) refresh(ctx context.Context) { next.HashrateHs1h = ckpool.DSPSToHashrate(ps.DSPS60) next.HashrateHs24h = ckpool.DSPSToHashrate(ps.DSPS1440) next.CKPoolOK = true + a.ckFailStreak = 0 } else { - a.Log.Warn("ckpool poolstats failed", "err", err) + a.ckFailStreak++ + // First failure or two: likely transient (stratifier warm-up or + // a lock hiccup during a reconnect). Only escalate to WARN after + // three consecutive failures. + if a.ckFailStreak >= 3 { + a.Log.Warn("ckpool poolstats failed", "err", err, "streak", a.ckFailStreak) + } else { + a.Log.Debug("ckpool poolstats transient error", "err", err, "streak", a.ckFailStreak) + } next.LastError = err.Error() } if u, err := a.CK.Uptime(ctx); err == nil { diff --git a/ui/package.json b/ui/package.json index b0afe63..8080350 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,6 +16,6 @@ "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "^5.6.0", - "vite": "^5.4.0" + "vite": "^6.0.0" } }