periodic_traffic_reset_job.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. package job
  2. import (
  3. "sync"
  4. "sync/atomic"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  7. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  8. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  9. )
  10. // periodicResetConcurrency bounds how many inbounds or clients one run resets at once:
  11. // each waits on its node, so one at a time a few hanging nodes stretched a run for hours.
  12. const periodicResetConcurrency = 8
  13. // Period represents the time period for traffic resets.
  14. type Period string
  15. // PeriodicTrafficResetJob resets traffic statistics for inbounds based on their configured reset period.
  16. type PeriodicTrafficResetJob struct {
  17. inboundService service.InboundService
  18. clientService service.ClientService
  19. xrayService service.XrayService
  20. period Period
  21. location *time.Location
  22. }
  23. // NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period.
  24. func NewPeriodicTrafficResetJob(period Period, location *time.Location) *PeriodicTrafficResetJob {
  25. return &PeriodicTrafficResetJob{
  26. period: period,
  27. location: location,
  28. }
  29. }
  30. func monthlyResetDue(resetDay int, now time.Time) bool {
  31. if resetDay < 1 {
  32. resetDay = 1
  33. }
  34. lastDay := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Day()
  35. return now.Day() == min(resetDay, lastDay)
  36. }
  37. func forEachResetBounded(n int, reset func(i int)) {
  38. sem := make(chan struct{}, periodicResetConcurrency)
  39. var wg sync.WaitGroup
  40. for i := range n {
  41. wg.Add(1)
  42. sem <- struct{}{}
  43. common.GoRecover("periodic-traffic-reset", func() {
  44. defer wg.Done()
  45. defer func() { <-sem }()
  46. reset(i)
  47. })
  48. }
  49. wg.Wait()
  50. }
  51. // Run resets traffic statistics for all inbounds that match the configured reset
  52. // period, then for the clients carrying that period on their own (#5497).
  53. func (j *PeriodicTrafficResetJob) Run() {
  54. j.resetInboundsOnSchedule()
  55. j.resetClientsOnTheirOwnCycle()
  56. }
  57. func (j *PeriodicTrafficResetJob) resetInboundsOnSchedule() {
  58. inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
  59. if err != nil {
  60. logger.Warning("Failed to get inbounds for traffic reset:", err)
  61. return
  62. }
  63. if j.period == "monthly" {
  64. now := time.Now().In(j.location)
  65. due := inbounds[:0]
  66. for _, inbound := range inbounds {
  67. if monthlyResetDue(inbound.TrafficResetDay, now) {
  68. due = append(due, inbound)
  69. }
  70. }
  71. inbounds = due
  72. }
  73. if len(inbounds) == 0 {
  74. return
  75. }
  76. logger.Infof("Running periodic traffic reset job for period: %s (%d matching inbounds)", j.period, len(inbounds))
  77. var resetCount atomic.Int32
  78. forEachResetBounded(len(inbounds), func(i int) {
  79. inbound := inbounds[i]
  80. resetInboundErr := j.inboundService.ResetInboundTraffic(inbound.Id)
  81. if resetInboundErr != nil {
  82. logger.Warning("Failed to reset traffic for inbound", inbound.Id, ":", resetInboundErr)
  83. }
  84. resetClientErr := j.clientService.ResetAllClientTraffics(&j.inboundService, inbound.Id)
  85. if resetClientErr != nil {
  86. logger.Warning("Failed to reset traffic for all users of inbound", inbound.Id, ":", resetClientErr)
  87. }
  88. if resetInboundErr == nil && resetClientErr == nil {
  89. resetCount.Add(1)
  90. }
  91. })
  92. if count := resetCount.Load(); count > 0 {
  93. logger.Infof("Periodic traffic reset completed: %d inbounds reset", count)
  94. }
  95. }
  96. // resetClientsOnTheirOwnCycle resets clients whose cycle is set individually. A
  97. // client inside an inbound on the same cycle is reset twice, which is harmless.
  98. func (j *PeriodicTrafficResetJob) resetClientsOnTheirOwnCycle() {
  99. cycles, err := j.clientService.GetClientsByTrafficReset(string(j.period))
  100. if err != nil {
  101. logger.Warning("Failed to get clients for traffic reset:", err)
  102. return
  103. }
  104. now := time.Now().In(j.location)
  105. due := make([]service.ClientResetCycle, 0, len(cycles))
  106. for _, c := range cycles {
  107. // Monthly clients come due on their own day, the rule the inbound-level
  108. // schedule already follows.
  109. if j.period == "monthly" && !monthlyResetDue(c.TrafficResetDay, now) {
  110. continue
  111. }
  112. // A reset re-enables, which is right for a client the quota switched off
  113. // and wrong for one an operator switched off by hand.
  114. if !c.Enable && !c.Depleted() {
  115. continue
  116. }
  117. due = append(due, c)
  118. }
  119. if len(due) == 0 {
  120. return
  121. }
  122. logger.Infof("Running periodic traffic reset job for period: %s (%d matching clients)", j.period, len(due))
  123. var mu sync.Mutex
  124. resetCount := 0
  125. needRestart := false
  126. forEachResetBounded(len(due), func(i int) {
  127. c := due[i]
  128. // ResetTrafficByEmail rather than a bulk UPDATE: it is the path that also
  129. // propagates to the client's node and clears the MTProto sidecar quota.
  130. nr, resetErr := j.clientService.ResetTrafficByEmail(&j.inboundService, c.Email)
  131. if resetErr != nil {
  132. logger.Warning("Failed to reset traffic for client", c.Email, ":", resetErr)
  133. return
  134. }
  135. mu.Lock()
  136. needRestart = needRestart || nr
  137. resetCount++
  138. mu.Unlock()
  139. })
  140. // Dropping this leaves a re-enabled client absent from the running core until
  141. // something unrelated restarts it.
  142. if needRestart {
  143. j.xrayService.SetToNeedRestart()
  144. }
  145. if resetCount > 0 {
  146. logger.Infof("Periodic traffic reset completed: %d clients reset", resetCount)
  147. }
  148. }