mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-08 14:15:00 +03:00
aedd8fac1c
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.
191 lines
5.9 KiB
Go
191 lines
5.9 KiB
Go
package job
|
|
|
|
import (
|
|
"sync"
|
|
"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"
|
|
)
|
|
|
|
// periodicResetConcurrency bounds how many inbounds or clients one run resets at once:
|
|
// each waits on its node, so one at a time a few hanging nodes stretched a run for hours.
|
|
const periodicResetConcurrency = 8
|
|
|
|
// Period represents the time period for traffic resets.
|
|
type Period string
|
|
|
|
// PeriodicTrafficResetJob resets traffic statistics for inbounds based on their configured reset period.
|
|
type PeriodicTrafficResetJob struct {
|
|
inboundService service.InboundService
|
|
clientService service.ClientService
|
|
xrayService service.XrayService
|
|
period Period
|
|
location *time.Location
|
|
}
|
|
|
|
// NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period.
|
|
func NewPeriodicTrafficResetJob(period Period, location *time.Location) *PeriodicTrafficResetJob {
|
|
return &PeriodicTrafficResetJob{
|
|
period: period,
|
|
location: location,
|
|
}
|
|
}
|
|
|
|
func monthlyResetDue(resetDay int, now time.Time) bool {
|
|
if resetDay < 1 {
|
|
resetDay = 1
|
|
}
|
|
lastDay := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Day()
|
|
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
|
|
for i := range n {
|
|
wg.Add(1)
|
|
sem <- struct{}{}
|
|
common.GoRecover("periodic-traffic-reset", func() {
|
|
defer wg.Done()
|
|
defer func() { <-sem }()
|
|
reset(i)
|
|
})
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// Run resets traffic statistics for all inbounds that match the configured reset
|
|
// period, then for the clients carrying that period on their own (#5497).
|
|
func (j *PeriodicTrafficResetJob) Run() {
|
|
j.resetInboundsOnSchedule()
|
|
j.resetClientsOnTheirOwnCycle()
|
|
}
|
|
|
|
func (j *PeriodicTrafficResetJob) resetInboundsOnSchedule() {
|
|
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
|
|
if err != nil {
|
|
logger.Warning("Failed to get inbounds for traffic reset:", err)
|
|
return
|
|
}
|
|
|
|
if j.period == "monthly" {
|
|
now := time.Now().In(j.location)
|
|
due := inbounds[:0]
|
|
for _, inbound := range inbounds {
|
|
if monthlyResetDue(inbound.TrafficResetDay, now) {
|
|
due = append(due, inbound)
|
|
}
|
|
}
|
|
inbounds = due
|
|
}
|
|
if len(inbounds) == 0 {
|
|
return
|
|
}
|
|
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)
|
|
if resetClientErr != nil {
|
|
logger.Warning("Failed to reset traffic for all users of inbound", inbound.Id, ":", resetClientErr)
|
|
}
|
|
|
|
if resetInboundErr == nil && resetClientErr == nil {
|
|
resetCount.Add(1)
|
|
}
|
|
})
|
|
|
|
if needRestart.Load() {
|
|
j.xrayService.SetToNeedRestart()
|
|
}
|
|
|
|
if count := resetCount.Load(); count > 0 {
|
|
logger.Infof("Periodic traffic reset completed: %d inbounds reset", count)
|
|
}
|
|
}
|
|
|
|
// resetClientsOnTheirOwnCycle resets clients whose cycle is set individually. A
|
|
// client inside an inbound on the same cycle is reset twice, which is harmless.
|
|
func (j *PeriodicTrafficResetJob) resetClientsOnTheirOwnCycle() {
|
|
cycles, err := j.clientService.GetClientsByTrafficReset(string(j.period))
|
|
if err != nil {
|
|
logger.Warning("Failed to get clients for traffic reset:", err)
|
|
return
|
|
}
|
|
|
|
now := time.Now().In(j.location)
|
|
due := make([]service.ClientResetCycle, 0, len(cycles))
|
|
for _, c := range cycles {
|
|
// Monthly clients come due on their own day, the rule the inbound-level
|
|
// schedule already follows.
|
|
if j.period == "monthly" && !monthlyResetDue(c.TrafficResetDay, now) {
|
|
continue
|
|
}
|
|
// A reset re-enables, which is right for a client the quota switched off
|
|
// and wrong for one an operator switched off by hand.
|
|
if !c.Enable && !c.Depleted() {
|
|
continue
|
|
}
|
|
due = append(due, c)
|
|
}
|
|
if len(due) == 0 {
|
|
return
|
|
}
|
|
logger.Infof("Running periodic traffic reset job for period: %s (%d matching clients)", j.period, len(due))
|
|
|
|
var mu sync.Mutex
|
|
resetCount := 0
|
|
needRestart := false
|
|
forEachResetBounded(len(due), func(i int) {
|
|
c := due[i]
|
|
// ResetTrafficByEmail rather than a bulk UPDATE: it is the path that also
|
|
// propagates to the client's node and clears the MTProto sidecar quota.
|
|
nr, resetErr := j.clientService.ResetTrafficByEmail(&j.inboundService, c.Email)
|
|
if resetErr != nil {
|
|
logger.Warning("Failed to reset traffic for client", c.Email, ":", resetErr)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
needRestart = needRestart || nr
|
|
resetCount++
|
|
mu.Unlock()
|
|
})
|
|
// Dropping this leaves a re-enabled client absent from the running core until
|
|
// something unrelated restarts it.
|
|
if needRestart {
|
|
j.xrayService.SetToNeedRestart()
|
|
}
|
|
|
|
if resetCount > 0 {
|
|
logger.Infof("Periodic traffic reset completed: %d clients reset", resetCount)
|
|
}
|
|
}
|