From 3eb214d0227b57a0ab341d3097e8ed376abccd3d Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 15 Jul 2026 02:08:06 +0200 Subject: [PATCH] fix(traffic): re-enable clients and serialize the write in Reset All Client Traffic ClientService.ResetAllTraffics zeroed up/down but, unlike every sibling reset path, never restored enable=true, so clients that had been auto-disabled for exceeding their quota stayed cut with zero usage after a reset. It also wrote client_traffics directly on the shared DB handle instead of through the serial traffic writer, reintroducing the cross-transaction lock-order deadlock the writer exists to prevent. Restore enable and run the reset inside submitTrafficWrite within one transaction. --- internal/web/service/client_traffic.go | 24 +++++++++++++-------- internal/web/service/traffic_commit_test.go | 24 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/internal/web/service/client_traffic.go b/internal/web/service/client_traffic.go index 539be2b05..78a25cc09 100644 --- a/internal/web/service/client_traffic.go +++ b/internal/web/service/client_traffic.go @@ -200,15 +200,21 @@ func (s *ClientService) resetAllClientTrafficsLocked(id int) error { } func (s *ClientService) ResetAllTraffics() (bool, error) { - db := database.GetDB() - res := db.Model(&xray.ClientTraffic{}). - Where("1 = 1"). - Updates(map[string]any{"up": 0, "down": 0}) - if res.Error != nil { - return false, res.Error - } - if err := db.Where("1 = 1").Delete(&model.ClientGlobalTraffic{}).Error; err != nil { + var affected int64 + err := submitTrafficWrite(func() error { + return database.GetDB().Transaction(func(tx *gorm.DB) error { + res := tx.Model(&xray.ClientTraffic{}). + Where("1 = 1"). + Updates(map[string]any{"enable": true, "up": 0, "down": 0}) + if res.Error != nil { + return res.Error + } + affected = res.RowsAffected + return tx.Where("1 = 1").Delete(&model.ClientGlobalTraffic{}).Error + }) + }) + if err != nil { return false, err } - return res.RowsAffected > 0, nil + return affected > 0, nil } diff --git a/internal/web/service/traffic_commit_test.go b/internal/web/service/traffic_commit_test.go index 8e5b65820..a59824601 100644 --- a/internal/web/service/traffic_commit_test.go +++ b/internal/web/service/traffic_commit_test.go @@ -52,3 +52,27 @@ func TestAddTrafficCommitsDespiteDisableHelperError(t *testing.T) { t.Fatalf("traffic tick was rolled back by a best-effort disable-helper error: up=%d down=%d, want 500/700", reloaded.Up, reloaded.Down) } } + +// Reset All Client Traffic must re-enable clients that were auto-disabled for +// exceeding their quota, matching every other reset path; otherwise a reset +// leaves depleted clients cut with zero usage. +func TestResetAllTrafficsReenablesDepletedClients(t *testing.T) { + db := initTrafficTestDB(t) + svc := &ClientService{} + + if err := db.Create(&xray.ClientTraffic{InboundId: 1, Email: "spent@x", Enable: false, Up: 60, Down: 60, Total: 100}).Error; err != nil { + t.Fatalf("seed: %v", err) + } + + if _, err := svc.ResetAllTraffics(); err != nil { + t.Fatalf("ResetAllTraffics: %v", err) + } + + row := readTraffic(t, db, "spent@x") + if row.Up != 0 || row.Down != 0 { + t.Fatalf("usage not reset: up=%d down=%d", row.Up, row.Down) + } + if !row.Enable { + t.Fatal("a depleted client must be re-enabled after Reset All Client Traffic, matching every other reset path") + } +}