node_admin_fanout_test.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package service
  2. import (
  3. "context"
  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. )
  17. // fanoutGate holds every node call open until released, so a test sees how many
  18. // nodes an operation reaches at once.
  19. type fanoutGate struct {
  20. entered atomic.Int32
  21. release chan struct{}
  22. once sync.Once
  23. }
  24. func newFanoutGate() *fanoutGate { return &fanoutGate{release: make(chan struct{})} }
  25. func (g *fanoutGate) hold(ctx context.Context) error {
  26. g.entered.Add(1)
  27. select {
  28. case <-ctx.Done():
  29. return ctx.Err()
  30. case <-g.release:
  31. return nil
  32. }
  33. }
  34. func (g *fanoutGate) open() { g.once.Do(func() { close(g.release) }) }
  35. func (g *fanoutGate) waitAll(t *testing.T, want int32, op string) {
  36. t.Helper()
  37. deadline := time.Now().Add(3 * time.Second)
  38. for g.entered.Load() < want {
  39. if time.Now().After(deadline) {
  40. t.Fatalf("%s reached %d of %d hanging nodes, want all of them at once", op, g.entered.Load(), want)
  41. }
  42. time.Sleep(10 * time.Millisecond)
  43. }
  44. }
  45. type gatedNodeRuntime struct {
  46. fakeNodeRuntime
  47. gate *fanoutGate
  48. }
  49. func (r *gatedNodeRuntime) ResetAllTraffics(ctx context.Context) error { return r.gate.hold(ctx) }
  50. func (r *gatedNodeRuntime) DelInbound(ctx context.Context, _ *model.Inbound) error {
  51. return r.gate.hold(ctx)
  52. }
  53. func gatedNodes(t *testing.T, gate *fanoutGate, n int) []int {
  54. t.Helper()
  55. mgr := useTestRuntimeManager(t)
  56. ids := make([]int, 0, n)
  57. for i := range n {
  58. node := &model.Node{Name: fmt.Sprintf("fanout-%d", i), Address: "127.0.0.1", Port: 2100 + i, ApiToken: "tok", Enable: true, Status: "online"}
  59. if err := database.GetDB().Create(node).Error; err != nil {
  60. t.Fatalf("create node: %v", err)
  61. }
  62. mgr.SetRuntimeOverride(node.Id, &gatedNodeRuntime{gate: gate})
  63. ids = append(ids, node.Id)
  64. }
  65. return ids
  66. }
  67. // Operations that touch every node walked them one at a time, so a few hanging
  68. // nodes kept the request running for minutes past the panel's write timeout.
  69. func TestResetAllTrafficsReachesNodesConcurrently(t *testing.T) {
  70. setupConflictDB(t)
  71. gate := newFanoutGate()
  72. gatedNodes(t, gate, 3)
  73. done := make(chan struct{})
  74. go func() {
  75. defer close(done)
  76. _ = (&InboundService{}).ResetAllTraffics()
  77. }()
  78. t.Cleanup(func() { gate.open(); <-done })
  79. gate.waitAll(t, 3, "ResetAllTraffics")
  80. }
  81. func TestDelInboundsPushesNodeDeletesConcurrently(t *testing.T) {
  82. setupConflictDB(t)
  83. gate := newFanoutGate()
  84. var inboundIDs []int
  85. for i, nodeID := range gatedNodes(t, gate, 3) {
  86. inboundIDs = append(inboundIDs, nodeInbound(t, nodeID, 46400+i, nil).Id)
  87. }
  88. done := make(chan struct{})
  89. var result BulkDelInboundResult
  90. var err error
  91. go func() {
  92. defer close(done)
  93. result, _, err = (&InboundService{}).DelInbounds(inboundIDs)
  94. }()
  95. t.Cleanup(func() { gate.open(); <-done })
  96. gate.waitAll(t, 3, "DelInbounds")
  97. gate.open()
  98. <-done
  99. if err != nil || result.Deleted != 3 || len(result.Skipped) != 0 {
  100. t.Fatalf("DelInbounds = %+v, %v; want 3 deleted", result, err)
  101. }
  102. }
  103. func TestUpdatePanelsReachesNodesConcurrently(t *testing.T) {
  104. setupConflictDB(t)
  105. useTestRuntimeManager(t)
  106. gate := newFanoutGate()
  107. var ids []int
  108. for i := range 3 {
  109. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  110. _, _ = io.Copy(io.Discard, r.Body)
  111. if strings.HasSuffix(r.URL.Path, "server/updatePanel") {
  112. _ = gate.hold(r.Context())
  113. }
  114. w.Header().Set("Content-Type", "application/json")
  115. _, _ = w.Write([]byte(`{"success":true}`))
  116. }))
  117. t.Cleanup(srv.Close)
  118. host, port, _ := strings.Cut(strings.TrimPrefix(srv.URL, "http://"), ":")
  119. portNum, _ := strconv.Atoi(port)
  120. node := &model.Node{
  121. Name: fmt.Sprintf("panel-%d", i), Scheme: "http", Address: host, Port: portNum, BasePath: "/",
  122. ApiToken: "tok", Enable: true, Status: "online", AllowPrivateAddress: true, TlsVerifyMode: "verify",
  123. }
  124. if err := database.GetDB().Create(node).Error; err != nil {
  125. t.Fatalf("create node: %v", err)
  126. }
  127. ids = append(ids, node.Id)
  128. }
  129. done := make(chan struct{})
  130. var results []NodeUpdateResult
  131. go func() {
  132. defer close(done)
  133. results, _ = (&NodeService{}).UpdatePanels(ids, false)
  134. }()
  135. t.Cleanup(func() { gate.open(); <-done })
  136. gate.waitAll(t, 3, "UpdatePanels")
  137. gate.open()
  138. <-done
  139. if len(results) != len(ids) {
  140. t.Fatalf("UpdatePanels returned %d results for %d nodes", len(results), len(ids))
  141. }
  142. for i, res := range results {
  143. if res.Id != ids[i] || !res.OK {
  144. t.Fatalf("UpdatePanels result %d = %+v, want node %d updated, in request order", i, res, ids[i])
  145. }
  146. }
  147. }