periodic_traffic_reset_nodes_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. package job
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/http/httptest"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "sync/atomic"
  12. "testing"
  13. "time"
  14. "github.com/mhsanaei/3x-ui/v3/internal/database"
  15. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  16. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  17. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  18. )
  19. // resetGate holds every node reset open until released, counting how many nodes
  20. // the job reaches at once.
  21. type resetGate struct {
  22. entered atomic.Int32
  23. release chan struct{}
  24. once sync.Once
  25. }
  26. func (g *resetGate) open() { g.once.Do(func() { close(g.release) }) }
  27. func (g *resetGate) waitAll(t *testing.T, want int32) {
  28. t.Helper()
  29. deadline := time.Now().Add(3 * time.Second)
  30. for g.entered.Load() < want {
  31. if time.Now().After(deadline) {
  32. t.Fatalf("periodic reset reached %d of %d hanging nodes, want all of them at once", g.entered.Load(), want)
  33. }
  34. time.Sleep(10 * time.Millisecond)
  35. }
  36. }
  37. // resetNode is a node hosting inboundTag whose every traffic reset hangs until the
  38. // gate opens; it lists the inbound so the master can resolve its node-side id.
  39. func resetNode(t *testing.T, gate *resetGate, name, inboundTag string) int {
  40. t.Helper()
  41. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  42. _, _ = io.Copy(io.Discard, r.Body)
  43. if strings.HasSuffix(r.URL.Path, "/panel/api/inbounds/list") {
  44. w.Header().Set("Content-Type", "application/json")
  45. _, _ = fmt.Fprintf(w, `{"success":true,"obj":[{"id":1,"tag":%q}]}`, inboundTag)
  46. return
  47. }
  48. if strings.Contains(r.URL.Path, "resetTraffic") {
  49. gate.entered.Add(1)
  50. select {
  51. case <-r.Context().Done():
  52. case <-gate.release:
  53. }
  54. }
  55. w.Header().Set("Content-Type", "application/json")
  56. _, _ = w.Write([]byte(`{"success":true}`))
  57. }))
  58. t.Cleanup(srv.Close)
  59. host, port, _ := strings.Cut(strings.TrimPrefix(srv.URL, "http://"), ":")
  60. portNum, _ := strconv.Atoi(port)
  61. node := &model.Node{
  62. Name: name, Scheme: "http", Address: host, Port: portNum, BasePath: "/", ApiToken: "tok",
  63. Enable: true, Status: "online", AllowPrivateAddress: true, TlsVerifyMode: "verify",
  64. }
  65. if err := database.GetDB().Create(node).Error; err != nil {
  66. t.Fatalf("create node: %v", err)
  67. }
  68. return node.Id
  69. }
  70. func runResetJobAgainstGate(t *testing.T, gate *resetGate, want int32) {
  71. t.Helper()
  72. done := make(chan struct{})
  73. go func() {
  74. defer close(done)
  75. NewPeriodicTrafficResetJob("daily", time.UTC).Run()
  76. }()
  77. t.Cleanup(func() { gate.open(); <-done })
  78. gate.waitAll(t, want)
  79. }
  80. func newResetFleet(t *testing.T) *resetGate {
  81. t.Helper()
  82. initResetJobDB(t)
  83. runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}}))
  84. t.Cleanup(func() { runtime.SetManager(nil) })
  85. return &resetGate{release: make(chan struct{})}
  86. }
  87. // The job reset due clients and inbounds one by one, each waiting on its node,
  88. // so a few hanging nodes stretched one run across hours.
  89. func TestPeriodicResetReachesClientNodesConcurrently(t *testing.T) {
  90. gate := newResetFleet(t)
  91. db := database.GetDB()
  92. for i := range 3 {
  93. nodeID := resetNode(t, gate, fmt.Sprintf("client-node-%d", i), "reset-client-"+strconv.Itoa(i))
  94. email := fmt.Sprintf("cycle-%d@node", i)
  95. client := model.Client{Email: email, ID: fmt.Sprintf("00000000-0000-4000-8000-00000000000%d", i), Enable: true, TrafficReset: "daily"}
  96. settings, _ := json.Marshal(map[string]any{"clients": []model.Client{client}})
  97. ib := model.Inbound{
  98. UserId: 1, Enable: true, Port: 47000 + i, Protocol: model.VLESS, NodeID: &nodeID,
  99. Tag: "reset-client-" + strconv.Itoa(i), TrafficReset: "never", Settings: string(settings),
  100. }
  101. if err := db.Create(&ib).Error; err != nil {
  102. t.Fatalf("create inbound: %v", err)
  103. }
  104. rec := model.ClientRecord{Email: email, UUID: client.ID, Enable: true, TrafficReset: "daily"}
  105. if err := db.Create(&rec).Error; err != nil {
  106. t.Fatalf("create client record: %v", err)
  107. }
  108. if err := db.Create(&model.ClientInbound{ClientId: rec.Id, InboundId: ib.Id}).Error; err != nil {
  109. t.Fatalf("link client: %v", err)
  110. }
  111. if err := db.Create(&xray.ClientTraffic{InboundId: ib.Id, Email: email, Enable: true, Up: 500, Down: 700}).Error; err != nil {
  112. t.Fatalf("create traffic: %v", err)
  113. }
  114. }
  115. runResetJobAgainstGate(t, gate, 3)
  116. }
  117. func TestPeriodicResetReachesInboundNodesConcurrently(t *testing.T) {
  118. gate := newResetFleet(t)
  119. for i := range 3 {
  120. nodeID := resetNode(t, gate, fmt.Sprintf("inbound-node-%d", i), "reset-inbound-"+strconv.Itoa(i))
  121. ib := model.Inbound{
  122. UserId: 1, Enable: true, Port: 47100 + i, Protocol: model.VLESS, NodeID: &nodeID,
  123. Tag: "reset-inbound-" + strconv.Itoa(i), TrafficReset: "daily", Settings: `{"clients":[]}`,
  124. }
  125. if err := database.GetDB().Create(&ib).Error; err != nil {
  126. t.Fatalf("create inbound: %v", err)
  127. }
  128. }
  129. runResetJobAgainstGate(t, gate, 3)
  130. }