perf(inbounds): batch client identities for stats enrichment (#6929)

backfillClientStats loaded every column of every client once per inbound,
although enrichment only reads UUID, email and subscription ID. Each inbound
list read (panel list, the master's node traffic poll, Xray config builds,
bots) repeated that wide join per inbound. Identities now come from one narrow
clients/client_inbounds projection per 400 inbounds, honouring the supplied
transaction; stats output, sibling-traffic append order and the #6436
clients-table source of truth are unchanged.

Closes #6926
This commit is contained in:
Hamed
2026-10-25 16:31:46 +03:30
committed by GitHub
parent be25314157
commit 952c1c751a
11 changed files with 561 additions and 38 deletions
+19 -20
View File
@@ -24,48 +24,47 @@ type CopyClientsResult struct {
Errors []string `json:"errors"`
}
// enrichClientStats resolves each inbound's clients from the clients table
// once, fills in the UUID/SubId fields on the preloaded ClientStats, and tops
// up rows owned by a sibling inbound (shared-email mode — the row is keyed on
// email so it only preloads on its owning inbound). Reading identity from the
// clients table keeps /inbounds/list in sync with the running Xray config
// when the embedded settings JSON is stale (#6436).
// enrichClientStats refreshes identities from clients while preserving sibling-owned traffic.
// The clients table, not the embedded settings JSON, matches the running Xray config (#6436).
func (s *InboundService) enrichClientStats(db *gorm.DB, inbounds []*model.Inbound) {
if len(inbounds) == 0 {
return
}
clientsByInbound := s.backfillClientStats(db, inbounds)
for i, inbound := range inbounds {
clients := clientsByInbound[i]
for _, inbound := range inbounds {
clients := clientsByInbound[inbound.Id]
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
continue
}
cMap := make(map[string]model.Client, len(clients))
cMap := make(map[string]clientStatsIdentity, len(clients))
for _, c := range clients {
cMap[strings.ToLower(c.Email)] = c
}
for j := range inbound.ClientStats {
email := strings.ToLower(inbound.ClientStats[j].Email)
if c, ok := cMap[email]; ok {
inbound.ClientStats[j].UUID = c.ID
inbound.ClientStats[j].UUID = c.UUID
inbound.ClientStats[j].SubId = c.SubID
}
}
}
}
// backfillClientStats tops up each inbound's preloaded ClientStats with rows
// owned by a sibling inbound: client_traffics is keyed on email, so a client
// attached to several inbounds has one row that only preloads on the inbound
// it was created on. Returns the clients-table clients per inbound for reuse
// (not the embedded settings JSON, which can lag the live UUID — #6436).
func (s *InboundService) backfillClientStats(db *gorm.DB, inbounds []*model.Inbound) [][]model.Client {
clientsByInbound := make([][]model.Client, len(inbounds))
// backfillClientStats loads identities once, then appends missing sibling-owned traffic.
func (s *InboundService) backfillClientStats(db *gorm.DB, inbounds []*model.Inbound) map[int][]clientStatsIdentity {
ids := make([]int, 0, len(inbounds))
for _, inbound := range inbounds {
ids = append(ids, inbound.Id)
}
clientsByInbound, err := s.clientService.listStatsIdentitiesForInbounds(db, ids)
if err != nil {
logger.Warning("backfillClientStats:", err)
return nil
}
seenByInbound := make([]map[string]struct{}, len(inbounds))
missing := make(map[string]struct{})
for i, inbound := range inbounds {
clients, _ := s.clientService.ListForInbound(db, inbound.Id)
clientsByInbound[i] = clients
clients := clientsByInbound[inbound.Id]
seen := make(map[string]struct{}, len(inbound.ClientStats))
for _, st := range inbound.ClientStats {
if st.Email != "" {
@@ -105,7 +104,7 @@ func (s *InboundService) backfillClientStats(db *gorm.DB, inbounds []*model.Inbo
byEmail[strings.ToLower(st.Email)] = st
}
for i, inbound := range inbounds {
for _, c := range clientsByInbound[i] {
for _, c := range clientsByInbound[inbound.Id] {
if c.Email == "" {
continue
}