修复二开 agent 流量增量丢失: 基线改上报确认后推进并落盘持久化
- xray 新增快照/确认接口 GetTrafficSnapshot/CommitTrafficSnapshot/ RestoreTrafficBaseline, 与面板 GetTraffic 的 StatsLastValues 完全隔离 - agent 改走快照: 仅主控 HTTP 200 后才推进基线, 失败轮增量由累计 计数做差在下轮自动补齐; 已确认基线原子落盘, agent 重启恢复续差 - 核心重启计数归零的从零规则保留; Onlines 仍每轮新鲜采集直报 - 新增 11 个测试覆盖断网恢复/连续失败补齐/重启恢复/计数归零/ 面板语义隔离, xray 与 agent 两包全绿
This commit is contained in:
@@ -61,6 +61,19 @@ type XrayAPI struct {
|
||||
grpcClient *grpc.ClientConn
|
||||
isConnected bool
|
||||
StatsLastValues map[string]int64
|
||||
|
||||
// committedStats is the agent reporting path's last ACKNOWLEDGED
|
||||
// baseline: the raw core counters as of the most recent snapshot the
|
||||
// master confirmed receiving (HTTP 200). It is deliberately separate
|
||||
// from StatsLastValues — GetTraffic (the panel's local accounting)
|
||||
// advances its baseline at read time, while the agent path may only
|
||||
// advance on CommitTrafficSnapshot, so a failed upload never loses
|
||||
// the deltas it carried. committedReady is false until a baseline has
|
||||
// been established (first snapshot committed, or one restored from
|
||||
// disk); while false, snapshots report no deltas, mirroring
|
||||
// GetTraffic's first-poll rule.
|
||||
committedStats map[string]int64
|
||||
committedReady bool
|
||||
}
|
||||
|
||||
// Init connects to the Xray API server.
|
||||
@@ -650,6 +663,114 @@ func (x *XrayAPI) GetTraffic() ([]*Traffic, []*ClientTraffic, error) {
|
||||
return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil
|
||||
}
|
||||
|
||||
// TrafficSnapshot is one read of the core counters taken for the agent
|
||||
// reporting path: the deltas accrued since the last COMMITTED baseline,
|
||||
// plus Values, the raw cumulative counters the deltas were computed from.
|
||||
//
|
||||
// Unlike GetTraffic — which advances its baseline as it reads, the right
|
||||
// contract for the panel's local accounting job — a snapshot changes no
|
||||
// state. The caller reports the deltas and only then calls
|
||||
// CommitTrafficSnapshot; if the report fails, the next snapshot recomputes
|
||||
// against the same committed baseline and the unreported traffic is
|
||||
// included again, because the core counters are cumulative and are never
|
||||
// reset by polling.
|
||||
type TrafficSnapshot struct {
|
||||
Traffics []*Traffic
|
||||
ClientTraffics []*ClientTraffic
|
||||
// Values holds every counter returned by the core at snapshot time,
|
||||
// keyed by stat name. CommitTrafficSnapshot installs exactly this map
|
||||
// as the new committed baseline.
|
||||
Values map[string]int64
|
||||
}
|
||||
|
||||
// BaselineValues returns a copy of the baseline CommitTrafficSnapshot
|
||||
// would install for this snapshot, so callers can persist it (the agent
|
||||
// writes it to disk before/while committing).
|
||||
func (s *TrafficSnapshot) BaselineValues() map[string]int64 {
|
||||
out := make(map[string]int64, len(s.Values))
|
||||
for k, v := range s.Values {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetTrafficSnapshot reads the core counters and computes deltas against
|
||||
// the committed baseline WITHOUT advancing it. Per-stat rules match
|
||||
// GetTraffic exactly: while no baseline has been established the snapshot
|
||||
// reports no deltas (it only proposes one), a counter the baseline has
|
||||
// never seen counts from zero, and a counter below its baseline (the core
|
||||
// restarted) counts from zero as well.
|
||||
func (x *XrayAPI) GetTrafficSnapshot() (*TrafficSnapshot, error) {
|
||||
if x.grpcClient == nil {
|
||||
return nil, common.NewError("xray api is not initialized")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
stats, err := x.queryStats(ctx)
|
||||
if err != nil {
|
||||
logger.Debug("Failed to query Xray stats:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snap := &TrafficSnapshot{Values: make(map[string]int64, len(stats))}
|
||||
tagTrafficMap := make(map[string]*Traffic)
|
||||
emailTrafficMap := make(map[string]*ClientTraffic)
|
||||
|
||||
for _, stat := range stats {
|
||||
snap.Values[stat.Name] = stat.Value
|
||||
if !x.committedReady {
|
||||
continue
|
||||
}
|
||||
lastValue, ok := x.committedStats[stat.Name]
|
||||
if !ok || stat.Value < lastValue {
|
||||
lastValue = 0
|
||||
}
|
||||
value := stat.Value - lastValue
|
||||
if matches := trafficRegex.FindStringSubmatch(stat.Name); len(matches) == 4 {
|
||||
processTraffic(matches, value, tagTrafficMap)
|
||||
} else if matches := clientTrafficRegex.FindStringSubmatch(stat.Name); len(matches) == 3 {
|
||||
processClientTraffic(matches, value, emailTrafficMap)
|
||||
}
|
||||
}
|
||||
|
||||
snap.Traffics = mapToSlice(tagTrafficMap)
|
||||
snap.ClientTraffics = mapToSlice(emailTrafficMap)
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
// CommitTrafficSnapshot marks a snapshot as acknowledged by the master:
|
||||
// its Values become the committed baseline that later snapshots diff
|
||||
// against. Installing only the counters the core currently reports also
|
||||
// retires baselines for deleted inbounds/clients, the same end state
|
||||
// GetTraffic's pruning converges to.
|
||||
func (x *XrayAPI) CommitTrafficSnapshot(s *TrafficSnapshot) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
x.committedStats = s.BaselineValues()
|
||||
x.committedReady = true
|
||||
}
|
||||
|
||||
// RestoreTrafficBaseline installs a committed baseline recovered from
|
||||
// persistent storage (the agent saves one after every acknowledged
|
||||
// report), so a restarted agent keeps diffing against the last values the
|
||||
// master confirmed instead of swallowing the unreported tail into a
|
||||
// fresh baseline. An empty map leaves the baseline unestablished: the
|
||||
// next snapshot is the baseline-only first pass, exactly like a first
|
||||
// run with no state file.
|
||||
func (x *XrayAPI) RestoreTrafficBaseline(values map[string]int64) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
x.committedStats = make(map[string]int64, len(values))
|
||||
for k, v := range values {
|
||||
x.committedStats[k] = v
|
||||
}
|
||||
x.committedReady = true
|
||||
}
|
||||
|
||||
// OnlineIP is one source address of a live connection, with the unix time (seconds)
|
||||
// the core last dispatched a link from it.
|
||||
type OnlineIP struct {
|
||||
|
||||
Reference in New Issue
Block a user