fix(traffic): re-enable quota-disabled clients on inbound and reset-all resets (#6833)

Running out of quota switches a client off in client_traffics, in its client
record and in the inbound settings JSON. The inbound's periodic reset and the
Reset All Client Traffic button only set client_traffics.enable back, so the
client showed zero usage but stayed out of the running core every period.

Both paths now read the disabled, depleted emails inside the transaction that
zeroes the counters, on the traffic writer, and re-enable them through
BulkSetEnable after the commit, like every other reset path. Reading them inside
that transaction keeps a traffic tick from disabling a client between the read
and the reset.
This commit is contained in:
Jay Hemnani
2026-10-24 07:51:11 +05:30
committed by GitHub
parent c3394fefed
commit 1dbb37432c
7 changed files with 145 additions and 23 deletions
+31 -3
View File
@@ -128,13 +128,17 @@ func (c ClientResetCycle) Depleted() bool {
return c.Total > 0 && c.Used >= c.Total
}
func clientResetCycles(tx *gorm.DB) *gorm.DB {
return tx.Table("clients c").
Select("c.email, c.traffic_reset_day, c.enable, COALESCE(ct.total, 0) AS total, COALESCE(ct.up, 0) + COALESCE(ct.down, 0) AS used").
Joins("LEFT JOIN client_traffics ct ON ct.email = c.email")
}
// GetClientsByTrafficReset returns the clients whose own reset cycle matches the
// period, independent of the cycle configured on the inbounds they belong to.
func (s *ClientService) GetClientsByTrafficReset(period string) ([]ClientResetCycle, error) {
var cycles []ClientResetCycle
err := database.GetDB().Table("clients c").
Select("c.email, c.traffic_reset_day, c.enable, COALESCE(ct.total, 0) AS total, COALESCE(ct.up, 0) + COALESCE(ct.down, 0) AS used").
Joins("LEFT JOIN client_traffics ct ON ct.email = c.email").
err := clientResetCycles(database.GetDB()).
Where("c.traffic_reset = ?", period).
Scan(&cycles).Error
if err != nil {
@@ -143,6 +147,30 @@ func (s *ClientService) GetClientsByTrafficReset(period string) ([]ClientResetCy
return cycles, nil
}
// quotaDisabledEmails lists the disabled clients whose usage reached their quota,
// only those in within unless it is nil. Read it before zeroing the counters,
// which hides why a client is off.
func quotaDisabledEmails(tx *gorm.DB, within []string) ([]string, error) {
var cycles []ClientResetCycle
if err := clientResetCycles(tx).Where("c.enable = ?", false).Scan(&cycles).Error; err != nil {
return nil, err
}
var in map[string]bool
if within != nil {
in = make(map[string]bool, len(within))
for _, e := range within {
in[e] = true
}
}
var emails []string
for _, c := range cycles {
if c.Depleted() && (in == nil || in[c.Email]) {
emails = append(emails, c.Email)
}
}
return emails, nil
}
// Create applies the client to every requested inbound: one failing inbound no
// longer aborts the others, so the error can name several and needRestart holds.
func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreatePayload) (bool, error) {