node_reset_undelivered_test.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "slices"
  7. "sync"
  8. "sync/atomic"
  9. "testing"
  10. "time"
  11. "github.com/mhsanaei/3x-ui/v3/internal/database"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  13. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  14. "gorm.io/gorm"
  15. )
  16. const (
  17. resetLostOn = `{"clients":[{"email":"reset-lost","totalGB":100,"enable":true}]}`
  18. resetLostOff = `{"clients":[{"email":"reset-lost","totalGB":100,"enable":false}]}`
  19. )
  20. // seedLatchedNodeClient leaves reset-lost depleted and latched off on the
  21. // master by its node's own usage, as a real node sync does.
  22. func seedLatchedNodeClient(t *testing.T, svc *InboundService) (*gorm.DB, *model.Inbound) {
  23. t.Helper()
  24. db := initTrafficTestDB(t)
  25. createNodeInboundWithClient(t, db, 1, "n1-in", 41901, "reset-lost")
  26. syncNodeWithSettings(t, svc, 1, "n1-in", resetLostOn, xray.ClientTraffic{Email: "reset-lost", Up: 10, Down: 10, Total: 100, Enable: true})
  27. syncNodeWithSettings(t, svc, 1, "n1-in", resetLostOff, xray.ClientTraffic{Email: "reset-lost", Up: 60, Down: 60, Total: 100, Enable: false})
  28. if got := readTraffic(t, db, "reset-lost"); got.Enable {
  29. t.Fatal("setup: the depleted client should be latched off")
  30. }
  31. var ib model.Inbound
  32. if err := db.Where("tag = ?", "n1-in").First(&ib).Error; err != nil {
  33. t.Fatalf("load inbound: %v", err)
  34. }
  35. return db, &ib
  36. }
  37. // A reset the node never received leaves its old counters, so the node keeps
  38. // switching the client off; the master must not adopt that verdict.
  39. func TestNodeResetNotDeliveredDoesNotRedisableClient(t *testing.T) {
  40. resets := []struct {
  41. name string
  42. run func(svc *InboundService, ib *model.Inbound) error
  43. }{
  44. {"single", func(svc *InboundService, ib *model.Inbound) error {
  45. _, err := svc.ResetClientTraffic(ib.Id, "reset-lost")
  46. return err
  47. }},
  48. {"bulk", func(svc *InboundService, _ *model.Inbound) error {
  49. _, err := (&ClientService{}).BulkResetTraffic(svc, []string{"reset-lost"})
  50. return err
  51. }},
  52. {"inbound", func(svc *InboundService, ib *model.Inbound) error {
  53. return (&ClientService{}).ResetAllClientTraffics(svc, ib.Id)
  54. }},
  55. {"all", func(*InboundService, *model.Inbound) error {
  56. _, err := (&ClientService{}).ResetAllTraffics()
  57. return err
  58. }},
  59. }
  60. for _, reset := range resets {
  61. t.Run(reset.name, func(t *testing.T) {
  62. svc := &InboundService{}
  63. db, ib := seedLatchedNodeClient(t, svc)
  64. if err := reset.run(svc, ib); err != nil {
  65. t.Fatalf("reset: %v", err)
  66. }
  67. syncNodeWithSettings(t, svc, 1, "n1-in", resetLostOff, xray.ClientTraffic{Email: "reset-lost", Up: 60, Down: 60, Total: 100, Enable: false})
  68. got := readTraffic(t, db, "reset-lost")
  69. if !got.Enable || got.Up+got.Down != 0 {
  70. t.Fatalf("after reset: enable=%v used=%d, want enabled at 0 — the undelivered reset re-disabled it", got.Enable, got.Up+got.Down)
  71. }
  72. })
  73. }
  74. }
  75. // resetRecordingRuntime is a node that accepts per-client resets unless failing.
  76. type resetRecordingRuntime struct {
  77. fakeNodeRuntime
  78. mu sync.Mutex
  79. fail bool
  80. got []string
  81. }
  82. func (r *resetRecordingRuntime) ResetClientTraffic(_ context.Context, _ *model.Inbound, email string) error {
  83. r.mu.Lock()
  84. defer r.mu.Unlock()
  85. if r.fail {
  86. return errors.New("node unreachable")
  87. }
  88. r.got = append(r.got, email)
  89. return nil
  90. }
  91. func (r *resetRecordingRuntime) delivered() []string {
  92. r.mu.Lock()
  93. defer r.mu.Unlock()
  94. return slices.Clone(r.got)
  95. }
  96. func pendingResetEmails(t *testing.T, nodeID int) []string {
  97. t.Helper()
  98. var emails []string
  99. if err := database.GetDB().Model(&model.NodePendingReset{}).Where("node_id = ?", nodeID).
  100. Order("email").Pluck("email", &emails).Error; err != nil {
  101. t.Fatalf("read pending resets: %v", err)
  102. }
  103. return emails
  104. }
  105. func setupRecordingNode(t *testing.T, fail bool) (int, *resetRecordingRuntime, *model.Inbound) {
  106. t.Helper()
  107. setupBulkDB(t)
  108. mgr := useTestRuntimeManager(t)
  109. node := &model.Node{Name: "reset-node", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
  110. if err := database.GetDB().Create(node).Error; err != nil {
  111. t.Fatalf("create node: %v", err)
  112. }
  113. rec := &resetRecordingRuntime{fail: fail}
  114. mgr.SetRuntimeOverride(node.Id, rec)
  115. ib := nodeInbound(t, node.Id, 41911, []model.Client{{Email: "reset-lost", ID: "11111111-1111-1111-1111-1111111111aa", Enable: true}})
  116. if err := (&InboundService{}).AddClientStat(database.GetDB(), ib.Id, &model.Client{Email: "reset-lost", Enable: true}); err != nil {
  117. t.Fatalf("AddClientStat: %v", err)
  118. }
  119. return node.Id, rec, ib
  120. }
  121. // A reachable node gets the reset right after the master commits it.
  122. func TestNodeResetDeliveredRightAway(t *testing.T) {
  123. resets := []struct {
  124. name string
  125. run func(svc *InboundService, ib *model.Inbound) error
  126. }{
  127. {"single", func(svc *InboundService, ib *model.Inbound) error {
  128. _, err := svc.ResetClientTraffic(ib.Id, "reset-lost")
  129. return err
  130. }},
  131. {"bulk", func(svc *InboundService, _ *model.Inbound) error {
  132. _, err := (&ClientService{}).BulkResetTraffic(svc, []string{"reset-lost"})
  133. return err
  134. }},
  135. {"inbound", func(svc *InboundService, ib *model.Inbound) error {
  136. return (&ClientService{}).ResetAllClientTraffics(svc, ib.Id)
  137. }},
  138. {"all", func(*InboundService, *model.Inbound) error {
  139. _, err := (&ClientService{}).ResetAllTraffics()
  140. return err
  141. }},
  142. }
  143. for _, reset := range resets {
  144. t.Run(reset.name, func(t *testing.T) {
  145. nodeID, rec, ib := setupRecordingNode(t, false)
  146. if err := reset.run(&InboundService{}, ib); err != nil {
  147. t.Fatalf("reset: %v", err)
  148. }
  149. if got := rec.delivered(); !slices.Equal(got, []string{"reset-lost"}) {
  150. t.Fatalf("node received resets %v, want [reset-lost]", got)
  151. }
  152. if left := pendingResetEmails(t, nodeID); len(left) != 0 {
  153. t.Fatalf("delivered reset still queued: %v", left)
  154. }
  155. })
  156. }
  157. }
  158. // bulkResetRuntime also takes a batch in one call.
  159. type bulkResetRuntime struct {
  160. resetRecordingRuntime
  161. batches [][]string
  162. }
  163. func (b *bulkResetRuntime) ResetClientTraffics(_ context.Context, emails []string) error {
  164. b.mu.Lock()
  165. defer b.mu.Unlock()
  166. b.batches = append(b.batches, slices.Clone(emails))
  167. return nil
  168. }
  169. // Above the per-client push threshold a backlog goes out as one bulk request,
  170. // not one round-trip per client.
  171. func TestNodeResetBacklogUsesBulkRequest(t *testing.T) {
  172. setupBulkDB(t)
  173. const nodeID = 7
  174. rows := make([]model.NodePendingReset, nodeBulkPushThreshold+1)
  175. for i := range rows {
  176. rows[i] = model.NodePendingReset{NodeId: nodeID, Email: fmt.Sprintf("owed-%02d", i), QueuedAt: 1}
  177. }
  178. if err := database.GetDB().Create(&rows).Error; err != nil {
  179. t.Fatalf("seed pending resets: %v", err)
  180. }
  181. rt := &bulkResetRuntime{}
  182. if err := (&InboundService{}).DeliverNodeResets(context.Background(), nodeID, rt); err != nil {
  183. t.Fatalf("DeliverNodeResets: %v", err)
  184. }
  185. if len(rt.batches) != 1 || len(rt.batches[0]) != len(rows) || len(rt.delivered()) != 0 {
  186. t.Fatalf("bulk batches %d (first %d emails), per-client calls %d; want one batch of %d",
  187. len(rt.batches), len(rt.batches[0]), len(rt.delivered()), len(rows))
  188. }
  189. if left := pendingResetEmails(t, nodeID); len(left) != 0 {
  190. t.Fatalf("delivered backlog still queued: %d rows", len(left))
  191. }
  192. }
  193. // An unreachable node keeps the reset queued until a later delivery lands.
  194. func TestNodeResetReplayedAfterFailure(t *testing.T) {
  195. nodeID, rec, ib := setupRecordingNode(t, true)
  196. if _, err := (&InboundService{}).ResetClientTraffic(ib.Id, "reset-lost"); err != nil {
  197. t.Fatalf("ResetClientTraffic: %v", err)
  198. }
  199. if left := pendingResetEmails(t, nodeID); !slices.Equal(left, []string{"reset-lost"}) {
  200. t.Fatalf("pending after failed delivery = %v, want [reset-lost]", left)
  201. }
  202. rec.mu.Lock()
  203. rec.fail = false
  204. rec.mu.Unlock()
  205. if err := (&InboundService{}).DeliverNodeResets(context.Background(), nodeID, rec); err != nil {
  206. t.Fatalf("DeliverNodeResets: %v", err)
  207. }
  208. if got := rec.delivered(); !slices.Equal(got, []string{"reset-lost"}) {
  209. t.Fatalf("node received resets %v, want [reset-lost]", got)
  210. }
  211. if left := pendingResetEmails(t, nodeID); len(left) != 0 {
  212. t.Fatalf("delivered reset still queued: %v", left)
  213. }
  214. }
  215. // slowResetRuntime holds each reset until a second one arrives or a short
  216. // timeout passes, so two unserialized deliveries both reach the node.
  217. type slowResetRuntime struct {
  218. resetRecordingRuntime
  219. calls atomic.Int32
  220. both chan struct{}
  221. }
  222. func (r *slowResetRuntime) ResetClientTraffic(ctx context.Context, ib *model.Inbound, email string) error {
  223. if r.calls.Add(1) == 2 {
  224. close(r.both)
  225. }
  226. select {
  227. case <-r.both:
  228. case <-time.After(300 * time.Millisecond):
  229. }
  230. return r.resetRecordingRuntime.ResetClientTraffic(ctx, ib, email)
  231. }
  232. // The sync job and a reset's own delivery can run at once; the node must still
  233. // get each owed reset once, or usage made in between is wiped a second time.
  234. func TestConcurrentNodeResetDeliveriesSendOnce(t *testing.T) {
  235. setupBulkDB(t)
  236. const nodeID = 9
  237. if err := database.GetDB().Create(&model.NodePendingReset{NodeId: nodeID, Email: "once", QueuedAt: 1}).Error; err != nil {
  238. t.Fatalf("seed pending reset: %v", err)
  239. }
  240. rt := &slowResetRuntime{both: make(chan struct{})}
  241. var wg sync.WaitGroup
  242. for range 2 {
  243. wg.Add(1)
  244. go func() {
  245. defer wg.Done()
  246. if err := (&InboundService{}).DeliverNodeResets(context.Background(), nodeID, rt); err != nil {
  247. t.Errorf("DeliverNodeResets: %v", err)
  248. }
  249. }()
  250. }
  251. wg.Wait()
  252. if got := rt.delivered(); !slices.Equal(got, []string{"once"}) {
  253. t.Fatalf("node received resets %v, want exactly [once]", got)
  254. }
  255. }