node_bulk_dispatch_test.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "sync/atomic"
  6. "testing"
  7. "github.com/google/uuid"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  11. )
  12. // fakeNodeRuntime is a runtime.Runtime stub that counts the per-client dispatch
  13. // calls so a test can assert a bulk op does NOT stream one RPC per client.
  14. type fakeNodeRuntime struct {
  15. addClient atomic.Int32
  16. deleteUser atomic.Int32
  17. deleteClient atomic.Int32
  18. updateUser atomic.Int32
  19. }
  20. func (f *fakeNodeRuntime) Name() string { return "fake-node" }
  21. func (f *fakeNodeRuntime) AddInbound(context.Context, *model.Inbound) error { return nil }
  22. func (f *fakeNodeRuntime) DelInbound(context.Context, *model.Inbound) error { return nil }
  23. func (f *fakeNodeRuntime) UpdateInbound(context.Context, *model.Inbound, *model.Inbound) error {
  24. return nil
  25. }
  26. func (f *fakeNodeRuntime) AddUser(context.Context, *model.Inbound, map[string]any) error { return nil }
  27. func (f *fakeNodeRuntime) RemoveUser(context.Context, *model.Inbound, string) error { return nil }
  28. func (f *fakeNodeRuntime) UpdateUser(context.Context, *model.Inbound, string, model.Client) error {
  29. f.updateUser.Add(1)
  30. return nil
  31. }
  32. func (f *fakeNodeRuntime) DeleteUser(context.Context, *model.Inbound, string) error {
  33. f.deleteUser.Add(1)
  34. return nil
  35. }
  36. func (f *fakeNodeRuntime) DeleteClient(context.Context, string) error {
  37. f.deleteClient.Add(1)
  38. return nil
  39. }
  40. func (f *fakeNodeRuntime) AddClient(context.Context, *model.Inbound, model.Client) error {
  41. f.addClient.Add(1)
  42. return nil
  43. }
  44. func (f *fakeNodeRuntime) RestartXray(context.Context) error { return nil }
  45. func (f *fakeNodeRuntime) ResetClientTraffic(context.Context, *model.Inbound, string) error {
  46. return nil
  47. }
  48. func (f *fakeNodeRuntime) ResetInboundTraffic(context.Context, *model.Inbound) error { return nil }
  49. func (f *fakeNodeRuntime) ResetAllTraffics(context.Context) error { return nil }
  50. // setupNodeRuntime wires an online node + a fake runtime override and returns the
  51. // node id and the fake so a test can drive the service node-dispatch path without
  52. // a network node.
  53. func setupNodeRuntime(t *testing.T) (int, *fakeNodeRuntime) {
  54. t.Helper()
  55. prev := runtime.GetManager()
  56. mgr := runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}})
  57. runtime.SetManager(mgr)
  58. t.Cleanup(func() { runtime.SetManager(prev) })
  59. node := &model.Node{Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
  60. if err := database.GetDB().Create(node).Error; err != nil {
  61. t.Fatalf("create node: %v", err)
  62. }
  63. fake := &fakeNodeRuntime{}
  64. mgr.SetRuntimeOverride(node.Id, fake)
  65. return node.Id, fake
  66. }
  67. func nodeInbound(t *testing.T, nodeID, port int, clients []model.Client) *model.Inbound {
  68. t.Helper()
  69. if clients == nil {
  70. clients = []model.Client{}
  71. }
  72. ib := &model.Inbound{
  73. UserId: 1, NodeID: &nodeID, Tag: fmt.Sprintf("in-%d", port), Enable: true,
  74. Port: port, Protocol: model.VLESS, Settings: clientsSettings(t, clients),
  75. }
  76. if err := database.GetDB().Create(ib).Error; err != nil {
  77. t.Fatalf("create node inbound: %v", err)
  78. }
  79. if err := (&ClientService{}).SyncInbound(nil, ib.Id, clients); err != nil {
  80. t.Fatalf("seed SyncInbound: %v", err)
  81. }
  82. return ib
  83. }
  84. func makeNodeClients(n int) []model.Client {
  85. out := make([]model.Client, n)
  86. for i := range n {
  87. out[i] = model.Client{ID: uuid.NewString(), Email: fmt.Sprintf("nu-%05d@x", i), Enable: true}
  88. }
  89. return out
  90. }
  91. // TestNodeBulk_LargeAddFoldsToDirty: adding more than the threshold of clients to
  92. // an online node inbound must NOT stream one AddClient RPC per client; it marks
  93. // the node dirty so a single reconcile push converges it instead.
  94. func TestNodeBulk_LargeAddFoldsToDirty(t *testing.T) {
  95. setupBulkDB(t)
  96. nodeID, fake := setupNodeRuntime(t)
  97. ib := nodeInbound(t, nodeID, 30001, nil)
  98. svc := &ClientService{}
  99. inboundSvc := &InboundService{}
  100. add := makeNodeClients(nodeBulkPushThreshold + 10)
  101. if _, err := svc.AddInboundClient(inboundSvc, &model.Inbound{Id: ib.Id, Protocol: model.VLESS, Settings: clientsSettings(t, add)}); err != nil {
  102. t.Fatalf("AddInboundClient: %v", err)
  103. }
  104. if got := fake.addClient.Load(); got != 0 {
  105. t.Fatalf("large add streamed %d AddClient RPCs, want 0 (should fold to dirty)", got)
  106. }
  107. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil {
  108. t.Fatalf("NodeSyncState: %v", err)
  109. } else if !dirty {
  110. t.Fatal("large add must mark the node dirty")
  111. }
  112. }
  113. // TestNodeBulk_SmallAddPushesLive: a small add stays on the live per-client path.
  114. func TestNodeBulk_SmallAddPushesLive(t *testing.T) {
  115. setupBulkDB(t)
  116. nodeID, fake := setupNodeRuntime(t)
  117. ib := nodeInbound(t, nodeID, 30002, nil)
  118. svc := &ClientService{}
  119. inboundSvc := &InboundService{}
  120. const small = 3
  121. add := makeNodeClients(small)
  122. if _, err := svc.AddInboundClient(inboundSvc, &model.Inbound{Id: ib.Id, Protocol: model.VLESS, Settings: clientsSettings(t, add)}); err != nil {
  123. t.Fatalf("AddInboundClient: %v", err)
  124. }
  125. if got := fake.addClient.Load(); got != int32(small) {
  126. t.Fatalf("small add streamed %d AddClient RPCs, want %d", got, small)
  127. }
  128. }
  129. func TestNodeUpdateInboundClientNoopSkipsRuntimeAndDirty(t *testing.T) {
  130. setupBulkDB(t)
  131. nodeID, fake := setupNodeRuntime(t)
  132. client := model.Client{
  133. ID: uuid.NewString(),
  134. Email: "noop@x",
  135. SubID: "sub-noop",
  136. Enable: true,
  137. CreatedAt: 111,
  138. UpdatedAt: 222,
  139. }
  140. ib := nodeInbound(t, nodeID, 30020, []model.Client{client})
  141. svc := &ClientService{}
  142. inboundSvc := &InboundService{}
  143. if _, err := svc.UpdateInboundClient(inboundSvc, &model.Inbound{
  144. Id: ib.Id,
  145. Protocol: model.VLESS,
  146. Settings: clientsSettings(t, []model.Client{client}),
  147. }, client.Email); err != nil {
  148. t.Fatalf("UpdateInboundClient: %v", err)
  149. }
  150. if got := fake.updateUser.Load(); got != 0 {
  151. t.Fatalf("no-op update streamed %d UpdateUser RPCs, want 0", got)
  152. }
  153. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil {
  154. t.Fatalf("NodeSyncState: %v", err)
  155. } else if dirty {
  156. t.Fatal("no-op update must not mark the node dirty")
  157. }
  158. reloaded, err := inboundSvc.GetInbound(ib.Id)
  159. if err != nil {
  160. t.Fatalf("GetInbound: %v", err)
  161. }
  162. if reloaded.Settings != ib.Settings {
  163. t.Fatal("no-op update rewrote inbound settings")
  164. }
  165. }
  166. func TestNodeUpdateInboundClientLivePushKeepsDirtyBackup(t *testing.T) {
  167. setupBulkDB(t)
  168. nodeID, fake := setupNodeRuntime(t)
  169. client := model.Client{
  170. ID: uuid.NewString(),
  171. Email: "edit@x",
  172. SubID: "sub-edit",
  173. Enable: true,
  174. CreatedAt: 111,
  175. UpdatedAt: 222,
  176. }
  177. ib := nodeInbound(t, nodeID, 30021, []model.Client{client})
  178. edited := client
  179. edited.Comment = "changed"
  180. svc := &ClientService{}
  181. inboundSvc := &InboundService{}
  182. if _, err := svc.UpdateInboundClient(inboundSvc, &model.Inbound{
  183. Id: ib.Id,
  184. Protocol: model.VLESS,
  185. Settings: clientsSettings(t, []model.Client{edited}),
  186. }, client.Email); err != nil {
  187. t.Fatalf("UpdateInboundClient: %v", err)
  188. }
  189. if got := fake.updateUser.Load(); got != 1 {
  190. t.Fatalf("edit streamed %d UpdateUser RPCs, want 1", got)
  191. }
  192. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil {
  193. t.Fatalf("NodeSyncState: %v", err)
  194. } else if !dirty {
  195. t.Fatal("successful live update should keep node dirty as reconcile backup")
  196. }
  197. }
  198. // TestNodeBulk_LargeDeleteFoldsToDirty: deleting more than the threshold from an
  199. // online node inbound must fold into a reconcile rather than per-client deletes.
  200. func TestNodeBulk_LargeDeleteFoldsToDirty(t *testing.T) {
  201. setupBulkDB(t)
  202. nodeID, fake := setupNodeRuntime(t)
  203. seed := makeNodeClients(nodeBulkPushThreshold + 10)
  204. nodeInbound(t, nodeID, 30003, seed)
  205. svc := &ClientService{}
  206. inboundSvc := &InboundService{}
  207. emails := make([]string, len(seed))
  208. for i := range seed {
  209. emails[i] = seed[i].Email
  210. }
  211. if _, _, err := svc.BulkDelete(inboundSvc, emails, false); err != nil {
  212. t.Fatalf("BulkDelete: %v", err)
  213. }
  214. if got := fake.deleteUser.Load(); got != 0 {
  215. t.Fatalf("large delete streamed %d DeleteUser RPCs, want 0 (should fold to dirty)", got)
  216. }
  217. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil {
  218. t.Fatalf("NodeSyncState: %v", err)
  219. } else if !dirty {
  220. t.Fatal("large delete must mark the node dirty")
  221. }
  222. }