mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-25 08:42:20 +07:00
fix(node): import per-client traffic history on first sync of a node-hosted inbound
On the first sync of a node-hosted inbound, the central inbound adopted the node's full lifetime counter but every client_traffics row was seeded at 0 (with the delta baseline set to the node's current counter). So adding or migrating a node that already had traffic kept the inbound total correct while every per-client counter restarted from zero, and the master under-reported per-client usage by the entire pre-attach history. Seed a new client_traffics row from the node counter only when the inbound was created during the same sync (a genuine node-add / inbound re-import); a client reappearing under a pre-existing inbound still seeds 0, preserving the ghost protection in TestGhostData_NoPhantomTraffic. The seed is additionally gated on the delete tombstone so a just-deleted client cannot be resurrected if its inbound is recreated. Baseline still equals the seeded value, so the next sync delta is 0 and no traffic is double counted. Adds TestNodeAdd_ImportsClientHistoryWithNewInbound and TestNodeAdd_TombstonedClientNotResurrected.
This commit is contained in:
@@ -380,6 +380,8 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
|||||||
|
|
||||||
structuralChange := false
|
structuralChange := false
|
||||||
|
|
||||||
|
newInboundIDs := make(map[int]struct{})
|
||||||
|
|
||||||
snapTags := make(map[string]struct{}, len(snap.Inbounds))
|
snapTags := make(map[string]struct{}, len(snap.Inbounds))
|
||||||
for _, snapIb := range snap.Inbounds {
|
for _, snapIb := range snap.Inbounds {
|
||||||
if snapIb == nil {
|
if snapIb == nil {
|
||||||
@@ -466,6 +468,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
|||||||
if newIb.Tag != snapIb.Tag {
|
if newIb.Tag != snapIb.Tag {
|
||||||
tagToCentral[newIb.Tag] = &newIb
|
tagToCentral[newIb.Tag] = &newIb
|
||||||
}
|
}
|
||||||
|
newInboundIDs[newIb.Id] = struct{}{}
|
||||||
structuralChange = true
|
structuralChange = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -620,6 +623,10 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
|||||||
if dirty {
|
if dirty {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
var seedUp, seedDown int64
|
||||||
|
if _, isNewInbound := newInboundIDs[c.Id]; isNewInbound && !isClientEmailTombstoned(cs.Email) {
|
||||||
|
seedUp, seedDown = canon.Up, canon.Down
|
||||||
|
}
|
||||||
row := &xray.ClientTraffic{
|
row := &xray.ClientTraffic{
|
||||||
InboundId: c.Id,
|
InboundId: c.Id,
|
||||||
Email: cs.Email,
|
Email: cs.Email,
|
||||||
@@ -627,8 +634,8 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
|||||||
Total: cs.Total,
|
Total: cs.Total,
|
||||||
ExpiryTime: cs.ExpiryTime,
|
ExpiryTime: cs.ExpiryTime,
|
||||||
Reset: cs.Reset,
|
Reset: cs.Reset,
|
||||||
Up: 0,
|
Up: seedUp,
|
||||||
Down: 0,
|
Down: seedDown,
|
||||||
LastOnline: cs.LastOnline,
|
LastOnline: cs.LastOnline,
|
||||||
}
|
}
|
||||||
if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "email"}}, DoNothing: true}).
|
if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "email"}}, DoNothing: true}).
|
||||||
|
|||||||
@@ -120,6 +120,32 @@ func TestSingleNode_MirrorsCorrectly(t *testing.T) {
|
|||||||
assertUpDown(t, readTraffic(t, db, email), 200, 200, "second sync — delta accrues")
|
assertUpDown(t, readTraffic(t, db, email), 200, 200, "second sync — delta accrues")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNodeAdd_ImportsClientHistoryWithNewInbound(t *testing.T) {
|
||||||
|
db := initTrafficTestDB(t)
|
||||||
|
svc := &InboundService{}
|
||||||
|
|
||||||
|
const email = "newnode-client"
|
||||||
|
const histUp, histDown int64 = 6_000_000_000, 200_000_000_000
|
||||||
|
|
||||||
|
syncNode(t, svc, 1, "fresh-in", xray.ClientTraffic{Email: email, Up: histUp, Down: histDown, Enable: true})
|
||||||
|
assertUpDown(t, readTraffic(t, db, email), histUp, histDown, "node-add: client history imported with its brand-new inbound")
|
||||||
|
|
||||||
|
syncNode(t, svc, 1, "fresh-in", xray.ClientTraffic{Email: email, Up: histUp + 1024, Down: histDown + 2048, Enable: true})
|
||||||
|
assertUpDown(t, readTraffic(t, db, email), histUp+1024, histDown+2048, "post-import delta accrues, no double count")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeAdd_TombstonedClientNotResurrected(t *testing.T) {
|
||||||
|
db := initTrafficTestDB(t)
|
||||||
|
svc := &InboundService{}
|
||||||
|
|
||||||
|
const email = "deleted-ghost"
|
||||||
|
const stale int64 = 50_000_000_000
|
||||||
|
|
||||||
|
tombstoneClientEmail(email)
|
||||||
|
syncNode(t, svc, 1, "fresh-in", xray.ClientTraffic{Email: email, Up: stale, Down: stale, Enable: true})
|
||||||
|
assertUpDown(t, readTraffic(t, db, email), 0, 0, "tombstoned client must not resurrect via node-add seed")
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpgrade_PreExistingRow_NoDoubleCount(t *testing.T) {
|
func TestUpgrade_PreExistingRow_NoDoubleCount(t *testing.T) {
|
||||||
db := initTrafficTestDB(t)
|
db := initTrafficTestDB(t)
|
||||||
createNodeInbound(t, db, 1, "n1-in", 41001)
|
createNodeInbound(t, db, 1, "n1-in", 41001)
|
||||||
|
|||||||
Reference in New Issue
Block a user