periodic_traffic_reset_nodes_test.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  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 whose every traffic reset hangs until the gate opens.
  38. func resetNode(t *testing.T, gate *resetGate, name string) int {
  39. t.Helper()
  40. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  41. _, _ = io.Copy(io.Discard, r.Body)
  42. if strings.Contains(r.URL.Path, "resetTraffic") {
  43. gate.entered.Add(1)
  44. select {
  45. case <-r.Context().Done():
  46. case <-gate.release:
  47. }
  48. }
  49. w.Header().Set("Content-Type", "application/json")
  50. _, _ = w.Write([]byte(`{"success":true}`))
  51. }))
  52. t.Cleanup(srv.Close)
  53. host, port, _ := strings.Cut(strings.TrimPrefix(srv.URL, "http://"), ":")
  54. portNum, _ := strconv.Atoi(port)
  55. node := &model.Node{
  56. Name: name, Scheme: "http", Address: host, Port: portNum, BasePath: "/", ApiToken: "tok",
  57. Enable: true, Status: "online", AllowPrivateAddress: true, TlsVerifyMode: "verify",
  58. }
  59. if err := database.GetDB().Create(node).Error; err != nil {
  60. t.Fatalf("create node: %v", err)
  61. }
  62. return node.Id
  63. }
  64. func runResetJobAgainstGate(t *testing.T, gate *resetGate, want int32) {
  65. t.Helper()
  66. done := make(chan struct{})
  67. go func() {
  68. defer close(done)
  69. NewPeriodicTrafficResetJob("daily", time.UTC).Run()
  70. }()
  71. t.Cleanup(func() { gate.open(); <-done })
  72. gate.waitAll(t, want)
  73. }
  74. func newResetFleet(t *testing.T) *resetGate {
  75. t.Helper()
  76. initResetJobDB(t)
  77. runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}}))
  78. t.Cleanup(func() { runtime.SetManager(nil) })
  79. return &resetGate{release: make(chan struct{})}
  80. }
  81. // The job reset due clients and inbounds one by one, each waiting on its node,
  82. // so a few hanging nodes stretched one run across hours.
  83. func TestPeriodicResetReachesClientNodesConcurrently(t *testing.T) {
  84. gate := newResetFleet(t)
  85. db := database.GetDB()
  86. for i := range 3 {
  87. nodeID := resetNode(t, gate, fmt.Sprintf("client-node-%d", i))
  88. email := fmt.Sprintf("cycle-%d@node", i)
  89. client := model.Client{Email: email, ID: fmt.Sprintf("00000000-0000-4000-8000-00000000000%d", i), Enable: true, TrafficReset: "daily"}
  90. settings, _ := json.Marshal(map[string]any{"clients": []model.Client{client}})
  91. ib := model.Inbound{
  92. UserId: 1, Enable: true, Port: 47000 + i, Protocol: model.VLESS, NodeID: &nodeID,
  93. Tag: "reset-client-" + strconv.Itoa(i), TrafficReset: "never", Settings: string(settings),
  94. }
  95. if err := db.Create(&ib).Error; err != nil {
  96. t.Fatalf("create inbound: %v", err)
  97. }
  98. rec := model.ClientRecord{Email: email, UUID: client.ID, Enable: true, TrafficReset: "daily"}
  99. if err := db.Create(&rec).Error; err != nil {
  100. t.Fatalf("create client record: %v", err)
  101. }
  102. if err := db.Create(&model.ClientInbound{ClientId: rec.Id, InboundId: ib.Id}).Error; err != nil {
  103. t.Fatalf("link client: %v", err)
  104. }
  105. if err := db.Create(&xray.ClientTraffic{InboundId: ib.Id, Email: email, Enable: true, Up: 500, Down: 700}).Error; err != nil {
  106. t.Fatalf("create traffic: %v", err)
  107. }
  108. }
  109. runResetJobAgainstGate(t, gate, 3)
  110. }
  111. func TestPeriodicResetReachesInboundNodesConcurrently(t *testing.T) {
  112. gate := newResetFleet(t)
  113. for i := range 3 {
  114. nodeID := resetNode(t, gate, fmt.Sprintf("inbound-node-%d", i))
  115. ib := model.Inbound{
  116. UserId: 1, Enable: true, Port: 47100 + i, Protocol: model.VLESS, NodeID: &nodeID,
  117. Tag: "reset-inbound-" + strconv.Itoa(i), TrafficReset: "daily", Settings: `{"clients":[]}`,
  118. }
  119. if err := database.GetDB().Create(&ib).Error; err != nil {
  120. t.Fatalf("create inbound: %v", err)
  121. }
  122. }
  123. runResetJobAgainstGate(t, gate, 3)
  124. }