fix(traffic): disable depleted clients by id instead of a second full scan

disableInvalidClients evaluated the depleted predicate twice per poll:
once to SELECT the rows (for xray removal and settings sync) and again in
the UPDATE that flips enable off — each a full client_traffics scan, the
second also re-running the cross-panel EXISTS subquery when global rows
exist.

The UPDATE now flips the already-collected rows by primary key in
sqlInChunk batches, sorted for stable lock order. Same rows, same
RowsAffected, half the scan cost; id-based matching also stays correct
for rows with empty emails.
This commit is contained in:
MHSanaei
2026-07-02 16:24:18 +02:00
parent fb1d055b06
commit 97588dd0b9
+18 -7
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"strings"
"time"
@@ -162,13 +163,23 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
}
}
result := tx.Model(xray.ClientTraffic{}).
Where(cond+" AND enable = ?", now, true).
Update("enable", false)
err = result.Error
count := result.RowsAffected
if err != nil {
return needRestart, count, err
// Flip the rows already collected above by primary key instead of
// re-evaluating the depleted predicate, which was a second full scan of
// client_traffics on every poll. Sorted ids keep the lock order stable.
ids := make([]int, 0, len(depletedRows))
for i := range depletedRows {
ids = append(ids, depletedRows[i].Id)
}
slices.Sort(ids)
var count int64
for _, batch := range chunkInts(ids, sqlInChunk) {
result := tx.Model(xray.ClientTraffic{}).
Where("id IN ? AND enable = ?", batch, true).
Update("enable", false)
if result.Error != nil {
return needRestart, count, result.Error
}
count += result.RowsAffected
}
if len(depletedEmails) > 0 {