mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 01:55:25 +08:00
fix(traffic): re-enable a quota-disabled inbound on its periodic reset (#6834)
An inbound with a total traffic cap is switched off once up + down reaches it. Its periodic traffic reset then zeroes the counters but never sets enable back, so after the first used-up period the inbound stays off for good and the reset schedule does nothing for it. The reset job now notes, before resetting, whether a local inbound was off because of its own cap (and not expired), and switches it back on through SetInboundEnable once the counters are zero. An inbound switched off below its cap, or past its expiry, stays off.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
@@ -42,6 +43,13 @@ func monthlyResetDue(resetDay int, now time.Time) bool {
|
||||
return now.Day() == min(resetDay, lastDay)
|
||||
}
|
||||
|
||||
// inboundQuotaDisabled mirrors disableInvalidInbounds: a local inbound its own
|
||||
// traffic cap switched off, not one that expired or was switched off by hand.
|
||||
func inboundQuotaDisabled(ib *model.Inbound, nowMs int64) bool {
|
||||
return !ib.Enable && ib.NodeID == nil && ib.Total > 0 && ib.Up+ib.Down >= ib.Total &&
|
||||
(ib.ExpiryTime <= 0 || ib.ExpiryTime > nowMs)
|
||||
}
|
||||
|
||||
func forEachResetBounded(n int, reset func(i int)) {
|
||||
sem := make(chan struct{}, periodicResetConcurrency)
|
||||
var wg sync.WaitGroup
|
||||
@@ -87,11 +95,22 @@ func (j *PeriodicTrafficResetJob) resetInboundsOnSchedule() {
|
||||
logger.Infof("Running periodic traffic reset job for period: %s (%d matching inbounds)", j.period, len(inbounds))
|
||||
|
||||
var resetCount atomic.Int32
|
||||
var needRestart atomic.Bool
|
||||
nowMs := time.Now().UnixMilli()
|
||||
forEachResetBounded(len(inbounds), func(i int) {
|
||||
inbound := inbounds[i]
|
||||
quotaOff := inboundQuotaDisabled(inbound, nowMs)
|
||||
resetInboundErr := j.inboundService.ResetInboundTraffic(inbound.Id)
|
||||
if resetInboundErr != nil {
|
||||
logger.Warning("Failed to reset traffic for inbound", inbound.Id, ":", resetInboundErr)
|
||||
} else if quotaOff {
|
||||
nr, err := j.inboundService.SetInboundEnable(inbound.Id, true)
|
||||
if err != nil {
|
||||
logger.Warning("Failed to re-enable inbound", inbound.Id, "after traffic reset:", err)
|
||||
}
|
||||
if nr {
|
||||
needRestart.Store(true)
|
||||
}
|
||||
}
|
||||
|
||||
resetClientErr := j.clientService.ResetAllClientTraffics(&j.inboundService, inbound.Id)
|
||||
@@ -104,6 +123,10 @@ func (j *PeriodicTrafficResetJob) resetInboundsOnSchedule() {
|
||||
}
|
||||
})
|
||||
|
||||
if needRestart.Load() {
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
|
||||
if count := resetCount.Load(); count > 0 {
|
||||
logger.Infof("Periodic traffic reset completed: %d inbounds reset", count)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package job
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
func TestMonthlyResetDue(t *testing.T) {
|
||||
@@ -29,3 +32,46 @@ func TestMonthlyResetDue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeriodicTrafficResetReenablesQuotaDisabledInbound(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
total int64
|
||||
expiryTime int64
|
||||
want bool
|
||||
}{
|
||||
{"switched off by its own quota", 1000, 0, true},
|
||||
{"switched off by hand below its quota", 100000, 0, false},
|
||||
{"over quota but expired", 1000, time.Now().Add(-time.Hour).UnixMilli(), false},
|
||||
}
|
||||
for i, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
initResetJobDB(t)
|
||||
db := database.GetDB()
|
||||
ib := model.Inbound{
|
||||
UserId: 1, Enable: true, Port: 41201 + i, Protocol: model.VLESS, Tag: "inbound-quota",
|
||||
TrafficReset: "daily", Total: tc.total, ExpiryTime: tc.expiryTime, Up: 600, Down: 500,
|
||||
Settings: `{"clients":[]}`,
|
||||
}
|
||||
if err := db.Create(&ib).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
if err := db.Model(&model.Inbound{}).Where("id = ?", ib.Id).Update("enable", false).Error; err != nil {
|
||||
t.Fatalf("disable inbound: %v", err)
|
||||
}
|
||||
|
||||
NewPeriodicTrafficResetJob("daily", time.UTC).Run()
|
||||
|
||||
var got model.Inbound
|
||||
if err := db.First(&got, ib.Id).Error; err != nil {
|
||||
t.Fatalf("reload inbound: %v", err)
|
||||
}
|
||||
if got.Up != 0 || got.Down != 0 {
|
||||
t.Fatalf("inbound traffic not reset: up=%d down=%d", got.Up, got.Down)
|
||||
}
|
||||
if got.Enable != tc.want {
|
||||
t.Fatalf("inbound enable = %v after its reset, want %v", got.Enable, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user