Browse Source

perf(traffic): skip cross-panel quota subquery when no globals exist (#5392, #5389)

disableInvalidClients ran a correlated EXISTS against client_global_traffics
on the full client_traffics table every 5s. On a panel no master pushes to,
that table is empty so the subquery can never match — yet it forced a full
scan that pegged Postgres at 100% CPU on large client counts. Probe the table
first and drop the EXISTS branch when it's empty (the common case), and add an
idx_client_global_email index so the subquery is an index lookup when globals
are present. Cross-panel enforcement is unchanged (TestGlobalUsage_DisablesClient).

This also relieves #5389 ('traffic writer queue full' / panel freeze): the
heavy query runs inside the serialized traffic write, so a slow DB backs the
shared writer queue up until request handlers block.
MHSanaei 11 giờ trước cách đây
mục cha
commit
8b6ccebfb0

+ 2 - 1
internal/database/index_tags_test.go

@@ -21,7 +21,7 @@ func TestAutoMigrateCreatesHotPathIndexes(t *testing.T) {
 	if err != nil {
 		t.Fatalf("open sqlite: %v", err)
 	}
-	if err := db.AutoMigrate(&model.ClientRecord{}, &xray.ClientTraffic{}); err != nil {
+	if err := db.AutoMigrate(&model.ClientRecord{}, &xray.ClientTraffic{}, &model.ClientGlobalTraffic{}); err != nil {
 		t.Fatalf("automigrate: %v", err)
 	}
 
@@ -32,6 +32,7 @@ func TestAutoMigrateCreatesHotPathIndexes(t *testing.T) {
 		{&model.ClientRecord{}, "idx_client_record_group"},
 		{&xray.ClientTraffic{}, "idx_client_traffics_inbound"},
 		{&xray.ClientTraffic{}, "idx_client_traffics_renew"},
+		{&model.ClientGlobalTraffic{}, "idx_client_global_email"},
 	}
 	for _, c := range cases {
 		if !db.Migrator().HasIndex(c.model, c.index) {

+ 1 - 1
internal/database/model/client_global_traffic.go

@@ -13,7 +13,7 @@ package model
 type ClientGlobalTraffic struct {
 	Id         int    `json:"id" gorm:"primaryKey;autoIncrement"`
 	MasterGuid string `json:"masterGuid" gorm:"uniqueIndex:idx_master_email,priority:1;not null"`
-	Email      string `json:"email" gorm:"uniqueIndex:idx_master_email,priority:2;not null"`
+	Email      string `json:"email" gorm:"uniqueIndex:idx_master_email,priority:2;index:idx_client_global_email;not null"`
 	Up         int64  `json:"up"`
 	Down       int64  `json:"down"`
 	UpdatedAt  int64  `json:"updatedAt" gorm:"autoUpdateTime:milli"`

+ 26 - 0
internal/web/service/global_traffic_test.go

@@ -64,6 +64,32 @@ func TestAcceptGlobalTraffic_OverwriteAndMultiMaster(t *testing.T) {
 	}
 }
 
+func TestDepletedCond_ProbeGuard(t *testing.T) {
+	db := initTrafficTestDB(t)
+	svc := &InboundService{}
+
+	// No global rows: the cross-panel EXISTS branch is skipped (#5392), but a
+	// client over its local quota is still disabled.
+	if got := depletedCond(db); got != depletedClientsCondLocal {
+		t.Fatalf("empty globals must use the local-only predicate")
+	}
+	seedClientRow(t, "local-cap", 1, 600, 600, 1000)
+	if _, count, _, err := svc.disableInvalidClients(db); err != nil {
+		t.Fatalf("disableInvalidClients: %v", err)
+	} else if count != 1 {
+		t.Fatalf("local over-quota client must be disabled, disabled %d", count)
+	}
+
+	// Once a master pushes a global row, the full predicate is used so combined
+	// quota is enforced.
+	if err := svc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{{Email: "local-cap", Up: 1, Down: 1}}); err != nil {
+		t.Fatalf("AcceptGlobalTraffic: %v", err)
+	}
+	if got := depletedCond(db); got != depletedClientsCond {
+		t.Fatalf("with globals present the cross-panel predicate must be used")
+	}
+}
+
 func TestGlobalUsage_DisablesClient(t *testing.T) {
 	db := initTrafficTestDB(t)
 	svc := &InboundService{}

+ 23 - 2
internal/web/service/inbound_disable.go

@@ -60,13 +60,34 @@ const depletedClientsCond = `((total > 0 AND up + down >= total)
 		WHERE g.email = client_traffics.email AND g.up + g.down >= client_traffics.total
 	)))`
 
+// depletedClientsCondLocal is depletedClientsCond without the cross-panel
+// client_global_traffics check. The EXISTS branch is a correlated subquery that
+// turns every traffic poll into a full client_traffics scan; on a panel no
+// master pushes to (the common case) client_global_traffics is empty, so the
+// branch can never match and is pure CPU cost (#5392).
+const depletedClientsCondLocal = `((total > 0 AND up + down >= total)
+	OR (expiry_time > 0 AND expiry_time <= ?))`
+
+// depletedCond returns the local-only predicate unless this panel actually
+// holds global-traffic rows, in which case the cross-panel EXISTS check is
+// needed to enforce combined quota. Both variants take the same single
+// expiry_time placeholder, so callers pass identical args either way.
+func depletedCond(tx *gorm.DB) string {
+	var probe int64
+	if err := tx.Model(&model.ClientGlobalTraffic{}).Limit(1).Count(&probe).Error; err == nil && probe > 0 {
+		return depletedClientsCond
+	}
+	return depletedClientsCondLocal
+}
+
 func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, []int, error) {
 	now := time.Now().Unix() * 1000
 	needRestart := false
+	cond := depletedCond(tx)
 
 	var depletedRows []xray.ClientTraffic
 	err := tx.Model(xray.ClientTraffic{}).
-		Where(depletedClientsCond+" AND enable = ?", now, true).
+		Where(cond+" AND enable = ?", now, true).
 		Find(&depletedRows).Error
 	if err != nil {
 		return false, 0, nil, err
@@ -142,7 +163,7 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, []int,
 	}
 
 	result := tx.Model(xray.ClientTraffic{}).
-		Where(depletedClientsCond+" AND enable = ?", now, true).
+		Where(cond+" AND enable = ?", now, true).
 		Update("enable", false)
 	err = result.Error
 	count := result.RowsAffected