1
0

traffic_writer.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "sync"
  7. "time"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database"
  9. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  10. "gorm.io/gorm"
  11. )
  12. const (
  13. trafficWriterQueueSize = 256
  14. trafficWriterSubmitTimeout = 5 * time.Second
  15. )
  16. type trafficWriteRequest struct {
  17. apply func() error
  18. done chan error
  19. }
  20. type serializedTxContextKey struct{}
  21. var (
  22. twMu sync.Mutex
  23. twQueue chan *trafficWriteRequest
  24. twCtx context.Context
  25. twCancel context.CancelFunc
  26. twDone chan struct{}
  27. )
  28. // StartTrafficWriter spins up the serial writer goroutine. Safe to call again
  29. // after StopTrafficWriter — each Start/Stop cycle gets fresh channels. The
  30. // previous sync.Once-based implementation deadlocked after a SIGHUP-driven
  31. // panel restart: Stop killed the consumer goroutine but Once prevented Start
  32. // from spawning a new one, so every later submitTrafficWrite blocked forever
  33. // on <-req.done with no consumer (including the AddTraffic call inside
  34. // XrayService.GetXrayConfig that runs from startTask).
  35. func StartTrafficWriter() {
  36. twMu.Lock()
  37. defer twMu.Unlock()
  38. if twCancel != nil && twDone != nil {
  39. select {
  40. case <-twDone:
  41. clearTrafficWriterState()
  42. default:
  43. return
  44. }
  45. }
  46. queue := make(chan *trafficWriteRequest, trafficWriterQueueSize)
  47. ctx, cancel := context.WithCancel(context.Background())
  48. done := make(chan struct{})
  49. twQueue = queue
  50. twCtx = ctx
  51. twCancel = cancel
  52. twDone = done
  53. go runTrafficWriter(ctx, queue, done)
  54. }
  55. // StopTrafficWriter cancels the writer context and waits for the goroutine to
  56. // drain any pending requests before returning. Resets the package state so a
  57. // subsequent StartTrafficWriter can spawn a fresh consumer.
  58. func StopTrafficWriter() {
  59. twMu.Lock()
  60. cancel := twCancel
  61. done := twDone
  62. if cancel == nil || done == nil {
  63. twMu.Unlock()
  64. return
  65. }
  66. cancel()
  67. twMu.Unlock()
  68. <-done
  69. twMu.Lock()
  70. if twDone == done {
  71. clearTrafficWriterState()
  72. }
  73. twMu.Unlock()
  74. }
  75. func clearTrafficWriterState() {
  76. twQueue = nil
  77. twCtx = nil
  78. twCancel = nil
  79. twDone = nil
  80. }
  81. func runTrafficWriter(ctx context.Context, queue chan *trafficWriteRequest, done chan struct{}) {
  82. defer close(done)
  83. for {
  84. select {
  85. case req := <-queue:
  86. req.done <- safeApply(req.apply)
  87. case <-ctx.Done():
  88. for {
  89. select {
  90. case req := <-queue:
  91. req.done <- safeApply(req.apply)
  92. default:
  93. return
  94. }
  95. }
  96. }
  97. }
  98. }
  99. // runSerializedTx runs fn inside one DB transaction on the shared serial
  100. // traffic-writer goroutine, so it can never execute concurrently with the
  101. // @every 5s traffic poll (AddTraffic). Both touch the hot client_traffics and
  102. // inbounds rows, and they acquire them in opposite order (the poll locks
  103. // inbounds then client_traffics; an admin client/inbound mutation does the
  104. // reverse), which Postgres aborts as a deadlock (SQLSTATE 40P01). Routing every
  105. // such mutation through this single writer removes that contention entirely.
  106. //
  107. // Keep network I/O (node pushes) OUT of fn: holding the single writer across a
  108. // remote node call would stall all traffic accounting for up to the remote
  109. // timeout. Apply runtime changes after this returns.
  110. func runSerializedTx(fn func(tx *gorm.DB) error) error {
  111. return submitTrafficWrite(func() error {
  112. return database.GetDB().Transaction(func(tx *gorm.DB) error {
  113. ctx := context.WithValue(tx.Statement.Context, serializedTxContextKey{}, true)
  114. return fn(tx.WithContext(ctx))
  115. })
  116. })
  117. }
  118. func isSerializedTx(tx *gorm.DB) bool {
  119. if tx == nil || tx.Statement == nil || tx.Statement.Context == nil {
  120. return false
  121. }
  122. active, _ := tx.Statement.Context.Value(serializedTxContextKey{}).(bool)
  123. return active
  124. }
  125. func safeApply(fn func() error) (err error) {
  126. defer func() {
  127. if r := recover(); r != nil {
  128. err = fmt.Errorf("traffic writer panic: %v", r)
  129. logger.Error(err.Error())
  130. }
  131. }()
  132. return fn()
  133. }
  134. func submitTrafficWrite(fn func() error) error {
  135. req := &trafficWriteRequest{apply: fn, done: make(chan error, 1)}
  136. twMu.Lock()
  137. queue := twQueue
  138. ctx := twCtx
  139. done := twDone
  140. if queue == nil || ctx == nil || done == nil {
  141. twMu.Unlock()
  142. return safeApply(fn)
  143. }
  144. select {
  145. case <-ctx.Done():
  146. twMu.Unlock()
  147. return safeApply(fn)
  148. default:
  149. }
  150. timer := time.NewTimer(trafficWriterSubmitTimeout)
  151. defer timer.Stop()
  152. select {
  153. case queue <- req:
  154. twMu.Unlock()
  155. case <-timer.C:
  156. twMu.Unlock()
  157. return errors.New("traffic writer queue full")
  158. }
  159. select {
  160. case err := <-req.done:
  161. return err
  162. case <-done:
  163. select {
  164. case err := <-req.done:
  165. return err
  166. default:
  167. return errors.New("traffic writer stopped before write completed")
  168. }
  169. }
  170. }