mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-10 08:21:19 +07:00
feat(nodes): traffic-writer queue, full-mirror sync, WS event fixes
- Traffic-writer single-consumer queue (web/service/traffic_writer.go) serialises every DB write that touches up/down/all_time/last_online (AddTraffic, SetRemoteTraffic, Reset*, UpdateClientTrafficByEmail) so overlapping goroutines can no longer clobber each other's column-scoped Updates with a stale tx.Save. - DB pool: WAL + busy_timeout=10s + synchronous=NORMAL + _txlock= immediate, MaxOpenConns=8 / MaxIdleConns=4. The immediate-tx PRAGMA fixes residual "database is locked [0ms]" cases where deferred-tx writer-upgrade conflicts bypass busy_timeout. - SetRemoteTraffic full-mirrors node-authoritative state into central: settings JSON, remark, listen, port, total, expiry, all_time, enable, plus per-client total/expiry/reset/all_time. Inbounds and client_traffics rows present on node but missing from central are created; rows missing from snap are deleted (with cascading client_traffics removal). - NodeTrafficSyncJob detects structural changes from the mirror and broadcasts invalidate(inbounds) so open central UIs re-fetch via REST on node-side add/del/edit without manual refresh. - XrayTrafficJob broadcasts invalidate(inbounds) when auto-disable flips client_traffics.enable so the per-client toggle reflects depletion without manual refresh. - Frontend: inbounds page now subscribes to the BroadcastInbounds 'inbounds' WS event (full-list pushes from add/del/update controllers were silently dropped). Fixes invalidate payload field (dataType -> type). Restart- panel modal switched from Promise-wrap to onOk-only so Cancel actually cancels. - Node files trimmed of stale prose-comments; cron cadence dropped 10s -> 5s to match the inbounds page UX. - README badges and Go module path bumped v2 -> v3 to match module rename. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
24cd271486
commit
8e7d215b4a
@@ -11,37 +11,22 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/web/websocket"
|
||||
)
|
||||
|
||||
// nodeHeartbeatConcurrency caps how many remote panels we probe at once.
|
||||
// Plenty of headroom for typical deployments (tens of nodes) without
|
||||
// letting a misconfigured run open thousands of sockets at once.
|
||||
const nodeHeartbeatConcurrency = 32
|
||||
const (
|
||||
nodeHeartbeatConcurrency = 32
|
||||
nodeHeartbeatRequestTimeout = 4 * time.Second
|
||||
)
|
||||
|
||||
// nodeHeartbeatRequestTimeout bounds a single probe. The cron is @every 10s,
|
||||
// so this needs to stay well under that to avoid run pile-up.
|
||||
const nodeHeartbeatRequestTimeout = 6 * time.Second
|
||||
|
||||
// NodeHeartbeatJob probes every enabled remote node once per cron tick
|
||||
// and persists the result. Disabled nodes are skipped entirely so a
|
||||
// long-broken node can be parked without burning sockets every 10s.
|
||||
type NodeHeartbeatJob struct {
|
||||
nodeService service.NodeService
|
||||
|
||||
// Coarse mutex prevents two ticks running concurrently if probes
|
||||
// pile up under network failure. The next tick simply skips when
|
||||
// the previous one is still draining.
|
||||
running sync.Mutex
|
||||
running sync.Mutex
|
||||
}
|
||||
|
||||
// NewNodeHeartbeatJob constructs a heartbeat job. The robfig/cron
|
||||
// scheduler will hand the same instance to every tick, so the
|
||||
// running mutex carries across runs as intended.
|
||||
func NewNodeHeartbeatJob() *NodeHeartbeatJob {
|
||||
return &NodeHeartbeatJob{}
|
||||
}
|
||||
|
||||
func (j *NodeHeartbeatJob) Run() {
|
||||
if !j.running.TryLock() {
|
||||
// Previous tick still in flight — skip this one.
|
||||
return
|
||||
}
|
||||
defer j.running.Unlock()
|
||||
@@ -71,10 +56,6 @@ func (j *NodeHeartbeatJob) Run() {
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Push the fresh list to any open Nodes page over WebSocket so the
|
||||
// status / latency / cpu / mem cells update without the user clicking
|
||||
// refresh. Skip the DB read entirely when no browser is connected —
|
||||
// matches the gating pattern in xray_traffic_job.
|
||||
if !websocket.HasClients() {
|
||||
return
|
||||
}
|
||||
@@ -86,10 +67,6 @@ func (j *NodeHeartbeatJob) Run() {
|
||||
websocket.BroadcastNodes(updated)
|
||||
}
|
||||
|
||||
// probeOne runs a single probe and persists the result. We deliberately
|
||||
// don't return errors — partial failures across the node set should not
|
||||
// abort other probes, and the LastError column carries the message for
|
||||
// the UI to surface.
|
||||
func (j *NodeHeartbeatJob) probeOne(n *model.Node) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), nodeHeartbeatRequestTimeout)
|
||||
defer cancel()
|
||||
@@ -100,8 +77,6 @@ func (j *NodeHeartbeatJob) probeOne(n *model.Node) {
|
||||
patch.Status = "online"
|
||||
}
|
||||
if updErr := j.nodeService.UpdateHeartbeat(n.Id, patch); updErr != nil {
|
||||
// A row deleted mid-tick produces "rows affected = 0", which
|
||||
// gorm reports as nil — so any error we get here is real.
|
||||
logger.Warning("node heartbeat: update node", n.Id, "failed:", updErr)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user