traffic_depletion_node_push_test.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "testing"
  6. "time"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  9. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  10. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  11. )
  12. type hangingUpdateRuntime struct {
  13. fakeNodeRuntime
  14. entered chan struct{}
  15. release chan struct{}
  16. }
  17. func (h *hangingUpdateRuntime) UpdateInbound(ctx context.Context, _, _ *model.Inbound) error {
  18. h.updateInbound.Add(1)
  19. select {
  20. case h.entered <- struct{}{}:
  21. default:
  22. }
  23. select {
  24. case <-ctx.Done():
  25. return ctx.Err()
  26. case <-h.release:
  27. return nil
  28. }
  29. }
  30. func seedDepletedNodeClient(t *testing.T, nodeID, port int) {
  31. t.Helper()
  32. client := model.Client{Email: fmt.Sprintf("spent-%d", port), Enable: true}
  33. ib := nodeInbound(t, nodeID, port, []model.Client{client})
  34. if err := database.GetDB().Create(&xray.ClientTraffic{
  35. InboundId: ib.Id, Email: client.Email, Enable: true, Up: 100, Total: 100,
  36. }).Error; err != nil {
  37. t.Fatalf("seed traffic: %v", err)
  38. }
  39. }
  40. // A depletion wave used to push every node inbound on the serial writer, one by
  41. // one with no deadline, so a hanging node froze traffic accounting and client edits.
  42. func TestTrafficDisableNodePushLeavesWriterFreeAndGivesUp(t *testing.T) {
  43. setupConflictDB(t)
  44. StartTrafficWriter()
  45. t.Cleanup(StopTrafficWriter)
  46. nodeID, _ := setupNodeRuntime(t)
  47. hanging := &hangingUpdateRuntime{entered: make(chan struct{}, 1), release: make(chan struct{})}
  48. runtime.GetManager().SetRuntimeOverride(nodeID, hanging)
  49. t.Cleanup(func() { close(hanging.release) })
  50. seedDepletedNodeClient(t, nodeID, 46311)
  51. seedDepletedNodeClient(t, nodeID, 46313)
  52. returned := make(chan error, 1)
  53. go func() {
  54. _, _, err := (&InboundService{}).AddTraffic(nil, nil)
  55. returned <- err
  56. }()
  57. select {
  58. case <-hanging.entered:
  59. case <-time.After(5 * time.Second):
  60. t.Fatal("depleted node client was never pushed to its node")
  61. }
  62. writerFree := make(chan error, 1)
  63. go func() { writerFree <- submitTrafficWrite(func() error { return nil }) }()
  64. select {
  65. case err := <-writerFree:
  66. if err != nil {
  67. t.Fatalf("traffic write while node push hangs: %v", err)
  68. }
  69. case <-time.After(time.Second):
  70. t.Fatal("traffic writer stayed held while a node push hung")
  71. }
  72. // Two hanging pushes: one at a time they would take twice the push timeout.
  73. select {
  74. case err := <-returned:
  75. if err != nil {
  76. t.Fatalf("AddTraffic: %v", err)
  77. }
  78. case <-time.After(nodeClientPushTimeout + 2*time.Second):
  79. t.Fatal("AddTraffic kept waiting on hanging node pushes past one push timeout")
  80. }
  81. }
  82. func TestTrafficDisableSkipsOfflineNodePushButMarksDirty(t *testing.T) {
  83. setupConflictDB(t)
  84. nodeID, fake := setupNodeRuntime(t)
  85. if err := database.GetDB().Model(&model.Node{}).Where("id = ?", nodeID).Update("status", "offline").Error; err != nil {
  86. t.Fatalf("mark node offline: %v", err)
  87. }
  88. seedDepletedNodeClient(t, nodeID, 46312)
  89. if _, _, err := (&InboundService{}).AddTraffic(nil, nil); err != nil {
  90. t.Fatalf("AddTraffic: %v", err)
  91. }
  92. if got := fake.updateInbound.Load(); got != 0 {
  93. t.Fatalf("UpdateInbound calls to an offline node = %d, want 0", got)
  94. }
  95. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil || !dirty {
  96. t.Fatalf("node dirty = %v (err %v), want true so reconcile applies the disable", dirty, err)
  97. }
  98. }
  99. type hangingRestartRuntime struct {
  100. fakeNodeRuntime
  101. entered chan struct{}
  102. release chan struct{}
  103. }
  104. func (h *hangingRestartRuntime) RestartXray(ctx context.Context) error {
  105. select {
  106. case h.entered <- struct{}{}:
  107. default:
  108. }
  109. select {
  110. case <-ctx.Done():
  111. return ctx.Err()
  112. case <-h.release:
  113. return nil
  114. }
  115. }
  116. // The opt-in restart is best-effort and never replayed, so a hanging node must
  117. // not hold the traffic poll that disabled its client.
  118. func TestTrafficDisableNodeRestartDoesNotBlockTrafficPoll(t *testing.T) {
  119. setupConflictDB(t)
  120. setRestartOnClientDisable(t, true)
  121. nodeID, _ := setupNodeRuntime(t)
  122. hanging := &hangingRestartRuntime{entered: make(chan struct{}, 1), release: make(chan struct{})}
  123. runtime.GetManager().SetRuntimeOverride(nodeID, hanging)
  124. t.Cleanup(func() { close(hanging.release) })
  125. seedDepletedNodeClient(t, nodeID, 46314)
  126. returned := make(chan error, 1)
  127. go func() {
  128. _, _, err := (&InboundService{}).AddTraffic(nil, nil)
  129. returned <- err
  130. }()
  131. select {
  132. case <-hanging.entered:
  133. case <-time.After(5 * time.Second):
  134. t.Fatal("node Xray was never restarted after its client was disabled")
  135. }
  136. select {
  137. case err := <-returned:
  138. if err != nil {
  139. t.Fatalf("AddTraffic: %v", err)
  140. }
  141. case <-time.After(time.Second):
  142. t.Fatal("AddTraffic waited on a hanging node restart")
  143. }
  144. }