client_bulk_fanout_test.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. package service
  2. import (
  3. "fmt"
  4. "testing"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. )
  9. // The bulk ops walked their inbounds one node round-trip at a time, so a client
  10. // spanning several nodes cost the SUM of every node's latency. Each test below
  11. // times out on the barrier unless the pushes overlap.
  12. func TestBulkDeleteAcrossNodesPushesConcurrently(t *testing.T) {
  13. setupBulkDB(t)
  14. startSerializedWriter(t)
  15. const nodes = inboundFanoutConcurrency + 1
  16. bar := newApplyBarrier(inboundFanoutConcurrency)
  17. seedClientAcrossNodes(t, bar, nodes, 46101, "bulkdel@x", "aaaaaaaa-1111-2222-3333-444444444444")
  18. bar.arm()
  19. if _, _, err := (&ClientService{}).BulkDelete(&InboundService{}, []string{"bulkdel@x"}, false); err != nil {
  20. t.Fatalf("BulkDelete across %d node inbounds: %v", nodes, err)
  21. }
  22. if got := bar.deleteClient.Load(); got == 0 {
  23. t.Fatalf("no node push reached the barrier at all")
  24. }
  25. if got := bar.maxPar.Load(); got != inboundFanoutConcurrency {
  26. t.Fatalf("peak node pushes in flight = %d, want overlap at the %d cap (barrier timed out: %v)",
  27. got, inboundFanoutConcurrency, bar.expired.Load())
  28. }
  29. }
  30. func TestBulkSetEnableAcrossNodesPushesConcurrently(t *testing.T) {
  31. setupBulkDB(t)
  32. startSerializedWriter(t)
  33. const nodes = inboundFanoutConcurrency + 1
  34. bar := newApplyBarrier(inboundFanoutConcurrency)
  35. seedClientAcrossNodes(t, bar, nodes, 46201, "bulkena@x", "bbbbbbbb-1111-2222-3333-444444444444")
  36. bar.arm()
  37. if _, _, err := (&ClientService{}).BulkSetEnable(&InboundService{}, []string{"bulkena@x"}, false); err != nil {
  38. t.Fatalf("BulkSetEnable across %d node inbounds: %v", nodes, err)
  39. }
  40. if got := bar.updateUser.Load(); got == 0 {
  41. t.Fatalf("no node push reached the barrier at all")
  42. }
  43. if got := bar.maxPar.Load(); got != inboundFanoutConcurrency {
  44. t.Fatalf("peak node pushes in flight = %d, want overlap at the %d cap (barrier timed out: %v)",
  45. got, inboundFanoutConcurrency, bar.expired.Load())
  46. }
  47. }
  48. func TestBulkAdjustAcrossNodesPushesConcurrently(t *testing.T) {
  49. setupBulkDB(t)
  50. startSerializedWriter(t)
  51. const nodes = inboundFanoutConcurrency + 1
  52. const email = "bulkadj@x"
  53. bar := newApplyBarrier(inboundFanoutConcurrency)
  54. mgr := useTestRuntimeManager(t)
  55. ids := fanoutNodeInbounds(t, mgr, bar, nodes, 46301)
  56. // An expiry to extend, or BulkAdjust reports the client ineligible and
  57. // never reaches a node at all.
  58. if _, err := (&ClientService{}).Create(&InboundService{}, &ClientCreatePayload{
  59. Client: model.Client{
  60. Email: email, ID: "cccccccc-1111-2222-3333-444444444444", SubID: "sub-" + email,
  61. Enable: true, ExpiryTime: time.Now().Add(24 * time.Hour).UnixMilli(),
  62. },
  63. InboundIds: ids,
  64. }); err != nil {
  65. t.Fatalf("seed Create across %d node inbounds: %v", nodes, err)
  66. }
  67. bar.arm()
  68. if _, _, err := (&ClientService{}).BulkAdjust(&InboundService{}, []string{email}, 1, 0, ""); err != nil {
  69. t.Fatalf("BulkAdjust across %d node inbounds: %v", nodes, err)
  70. }
  71. if got := bar.updateUser.Load(); got == 0 {
  72. t.Fatalf("no node push reached the barrier at all")
  73. }
  74. if got := bar.maxPar.Load(); got != inboundFanoutConcurrency {
  75. t.Fatalf("peak node pushes in flight = %d, want overlap at the %d cap (barrier timed out: %v)",
  76. got, inboundFanoutConcurrency, bar.expired.Load())
  77. }
  78. }
  79. func TestBulkAttachAcrossNodesPushesConcurrently(t *testing.T) {
  80. setupBulkDB(t)
  81. startSerializedWriter(t)
  82. const nodes = inboundFanoutConcurrency + 1
  83. const email = "bulkatt@x"
  84. bar := newApplyBarrier(inboundFanoutConcurrency)
  85. mgr := useTestRuntimeManager(t)
  86. // Seeded on the first inbound only, so the other nodes are all attach work.
  87. ids := fanoutNodeInbounds(t, mgr, bar, nodes, 46501)
  88. if _, err := (&ClientService{}).Create(&InboundService{}, &ClientCreatePayload{
  89. Client: model.Client{Email: email, ID: "eeeeeeee-1111-2222-3333-444444444444", SubID: "sub-" + email, Enable: true},
  90. InboundIds: ids[:1],
  91. }); err != nil {
  92. t.Fatalf("seed Create: %v", err)
  93. }
  94. bar.arm()
  95. if _, _, err := (&ClientService{}).BulkAttach(&InboundService{}, []string{email}, ids[1:]); err != nil {
  96. t.Fatalf("BulkAttach across %d node inbounds: %v", nodes-1, err)
  97. }
  98. if got := bar.addClient.Load(); got == 0 {
  99. t.Fatalf("no node push reached the barrier at all")
  100. }
  101. if got := bar.maxPar.Load(); got != inboundFanoutConcurrency {
  102. t.Fatalf("peak node pushes in flight = %d, want overlap at the %d cap (barrier timed out: %v)",
  103. got, inboundFanoutConcurrency, bar.expired.Load())
  104. }
  105. }
  106. func TestBulkCreateAcrossNodesPushesConcurrently(t *testing.T) {
  107. setupBulkDB(t)
  108. startSerializedWriter(t)
  109. const nodes = inboundFanoutConcurrency + 1
  110. bar := newApplyBarrier(inboundFanoutConcurrency)
  111. mgr := useTestRuntimeManager(t)
  112. ids := fanoutNodeInbounds(t, mgr, bar, nodes, 46601)
  113. bar.arm()
  114. if _, _, err := (&ClientService{}).BulkCreate(&InboundService{}, []ClientCreatePayload{{
  115. Client: model.Client{Email: "bulknew@x", ID: "ffffffff-1111-2222-3333-444444444444", SubID: "sub-bulknew", Enable: true},
  116. InboundIds: ids,
  117. }}); err != nil {
  118. t.Fatalf("BulkCreate across %d node inbounds: %v", nodes, err)
  119. }
  120. if got := bar.addClient.Load(); got == 0 {
  121. t.Fatalf("no node push reached the barrier at all")
  122. }
  123. if got := bar.maxPar.Load(); got != inboundFanoutConcurrency {
  124. t.Fatalf("peak node pushes in flight = %d, want overlap at the %d cap (barrier timed out: %v)",
  125. got, inboundFanoutConcurrency, bar.expired.Load())
  126. }
  127. }
  128. func TestBulkDetachAcrossNodesPushesConcurrently(t *testing.T) {
  129. setupBulkDB(t)
  130. startSerializedWriter(t)
  131. const nodes = inboundFanoutConcurrency + 1
  132. const email = "bulkdet@x"
  133. bar := newApplyBarrier(inboundFanoutConcurrency)
  134. recID := seedClientAcrossNodes(t, bar, nodes, 46401, email, "dddddddd-1111-2222-3333-444444444444")
  135. ids, err := (&ClientService{}).GetInboundIdsForRecord(recID)
  136. if err != nil {
  137. t.Fatalf("GetInboundIdsForRecord: %v", err)
  138. }
  139. bar.arm()
  140. if _, _, err := (&ClientService{}).BulkDetach(&InboundService{}, []string{email}, ids); err != nil {
  141. t.Fatalf("BulkDetach across %d node inbounds: %v", nodes, err)
  142. }
  143. if got := bar.deleteUser.Load(); got == 0 {
  144. t.Fatalf("no node push reached the barrier at all")
  145. }
  146. if got := bar.maxPar.Load(); got != inboundFanoutConcurrency {
  147. t.Fatalf("peak node pushes in flight = %d, want overlap at the %d cap (barrier timed out: %v)",
  148. got, inboundFanoutConcurrency, bar.expired.Load())
  149. }
  150. }
  151. // TestApplyClientFieldAcrossNodesPushesConcurrently covers the field-edit path
  152. // the Telegram bot and the LDAP job use (enable toggle, ip/expiry/traffic reset).
  153. func TestApplyClientFieldAcrossNodesPushesConcurrently(t *testing.T) {
  154. setupBulkDB(t)
  155. startSerializedWriter(t)
  156. const nodes = inboundFanoutConcurrency + 1
  157. const email = "fieldedit@x"
  158. bar := newApplyBarrier(inboundFanoutConcurrency)
  159. seedClientAcrossNodes(t, bar, nodes, 46701, email, "99999999-1111-2222-3333-444444444444")
  160. bar.arm()
  161. if _, err := (&ClientService{}).ResetClientIpLimitByEmail(&InboundService{}, email, 3); err != nil {
  162. t.Fatalf("ResetClientIpLimitByEmail across %d node inbounds: %v", nodes, err)
  163. }
  164. if got := bar.updateUser.Load(); got == 0 {
  165. t.Fatalf("no node push reached the barrier at all")
  166. }
  167. if got := bar.maxPar.Load(); got != inboundFanoutConcurrency {
  168. t.Fatalf("peak node pushes in flight = %d, want overlap at the %d cap (barrier timed out: %v)",
  169. got, inboundFanoutConcurrency, bar.expired.Load())
  170. }
  171. }
  172. // TestBulkCreateSerializesTunnelAddressAllocation pins that a bulk create over
  173. // WireGuard inbounds does not overlap: allocation reads every inbound's used-set
  174. // before writing, so two concurrent picks collide and the second is refused.
  175. func TestBulkCreateSerializesTunnelAddressAllocation(t *testing.T) {
  176. setupBulkDB(t)
  177. startSerializedWriter(t)
  178. db := database.GetDB()
  179. ids := make([]int, 0, 2)
  180. for i := range 2 {
  181. ib := &model.Inbound{
  182. UserId: 1, Enable: true, Port: 51820 + i,
  183. Tag: fmt.Sprintf("wg-%d", i), Protocol: model.WireGuard,
  184. Settings: `{"clients":[],"mtu":1420,"secretKey":"QO3O1V0m0Sm1yQ0hVvJ0kM0kQe0mYq0Wc0Zk0Xs0Zm8=","peers":[]}`,
  185. }
  186. if err := db.Create(ib).Error; err != nil {
  187. t.Fatalf("create wg inbound: %v", err)
  188. }
  189. ids = append(ids, ib.Id)
  190. }
  191. res, _, err := (&ClientService{}).BulkCreate(&InboundService{}, []ClientCreatePayload{
  192. {Client: model.Client{Email: "a@wg", SubID: "sa", Enable: true}, InboundIds: []int{ids[0]}},
  193. {Client: model.Client{Email: "b@wg", SubID: "sb", Enable: true}, InboundIds: []int{ids[1]}},
  194. })
  195. if err != nil {
  196. t.Fatalf("BulkCreate over two wg inbounds: %v", err)
  197. }
  198. if res.Created != 2 {
  199. t.Fatalf("created = %d, want 2 — concurrent allocation handed out one address twice: %+v", res.Created, res.Skipped)
  200. }
  201. }