tx_pool_starvation_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. package service
  2. import (
  3. "testing"
  4. "time"
  5. "gorm.io/gorm"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  9. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  10. )
  11. // capPoolAtOneConn models a pool whose other connections are all parked on the
  12. // transaction's row locks: any query outside tx then waits forever.
  13. func capPoolAtOneConn(t *testing.T) {
  14. t.Helper()
  15. sqlDB, err := database.GetDB().DB()
  16. if err != nil {
  17. t.Fatalf("sql.DB: %v", err)
  18. }
  19. sqlDB.SetMaxOpenConns(1)
  20. }
  21. func runWithoutSecondConn(t *testing.T, name string, fn func() error) error {
  22. t.Helper()
  23. done := make(chan error, 1)
  24. go func() { done <- fn() }()
  25. select {
  26. case err := <-done:
  27. return err
  28. case <-time.After(5 * time.Second):
  29. // The DB cleanup's Close fails the parked pool request, which unwinds fn.
  30. t.Fatalf("%s blocked waiting for a second pool connection while its transaction is open", name)
  31. return nil
  32. }
  33. }
  34. func createNodeInboundWithSettings(t *testing.T, db *gorm.DB, nodeID int, tag string, port int, settings string) {
  35. t.Helper()
  36. nid := nodeID
  37. ib := &model.Inbound{UserId: 1, Tag: tag, Enable: true, Port: port, Protocol: model.VLESS, NodeID: &nid, Settings: settings}
  38. if err := db.Create(ib).Error; err != nil {
  39. t.Fatalf("create node inbound %q: %v", tag, err)
  40. }
  41. }
  42. func syncNodeSnapshot(t *testing.T, svc *InboundService, nodeID int, inbounds ...*model.Inbound) error {
  43. t.Helper()
  44. _, err := svc.setRemoteTrafficLocked(nodeID, &runtime.TrafficSnapshot{Inbounds: inbounds}, false, false)
  45. return err
  46. }
  47. func assertNoTraffic(t *testing.T, email string) {
  48. t.Helper()
  49. var n int64
  50. if err := database.GetDB().Model(xray.ClientTraffic{}).Where("email = ?", email).Count(&n).Error; err != nil {
  51. t.Fatalf("count traffic %q: %v", email, err)
  52. }
  53. if n != 0 {
  54. t.Fatalf("traffic rows for %q = %d, want 0", email, n)
  55. }
  56. }
  57. func TestNodeSyncInboundRemovalStaysOnOneConnection(t *testing.T) {
  58. db := initTrafficTestDB(t)
  59. goneSettings := `{"clients": [{"email": "gone", "enable": true}]}`
  60. keepSettings := `{"clients": [{"email": "keep", "enable": true}]}`
  61. createNodeInboundWithSettings(t, db, 1, "n1-gone", 41001, goneSettings)
  62. createNodeInboundWithSettings(t, db, 1, "n1-keep", 41002, keepSettings)
  63. svc := &InboundService{}
  64. keep := &model.Inbound{Tag: "n1-keep", Enable: true, Settings: keepSettings, ClientStats: []xray.ClientTraffic{{Email: "keep", Up: 10, Down: 10, Enable: true}}}
  65. if err := syncNodeSnapshot(t, svc, 1,
  66. &model.Inbound{Tag: "n1-gone", Enable: true, Settings: goneSettings, ClientStats: []xray.ClientTraffic{{Email: "gone", Up: 10, Down: 10, Enable: true}}},
  67. keep); err != nil {
  68. t.Fatalf("seed sync: %v", err)
  69. }
  70. readTraffic(t, db, "gone")
  71. capPoolAtOneConn(t)
  72. err := runWithoutSecondConn(t, "node sync inbound-removal sweep", func() error {
  73. return syncNodeSnapshot(t, svc, 1, keep)
  74. })
  75. if err != nil {
  76. t.Fatalf("sync node 1 without n1-gone: %v", err)
  77. }
  78. assertNoTraffic(t, "gone")
  79. }
  80. func TestNodeSyncClientGoneStaysOnOneConnection(t *testing.T) {
  81. db := initTrafficTestDB(t)
  82. bothSettings := `{"clients": [{"email": "gone", "enable": true}, {"email": "keep", "enable": true}]}`
  83. createNodeInboundWithSettings(t, db, 1, "n1-in", 41001, bothSettings)
  84. svc := &InboundService{}
  85. if err := syncNodeSnapshot(t, svc, 1, &model.Inbound{Tag: "n1-in", Enable: true, Settings: bothSettings, ClientStats: []xray.ClientTraffic{
  86. {Email: "gone", Up: 10, Down: 10, Enable: true},
  87. {Email: "keep", Up: 10, Down: 10, Enable: true},
  88. }}); err != nil {
  89. t.Fatalf("seed sync: %v", err)
  90. }
  91. readTraffic(t, db, "gone")
  92. capPoolAtOneConn(t)
  93. err := runWithoutSecondConn(t, "node sync per-client sweep", func() error {
  94. return syncNodeSnapshot(t, svc, 1, &model.Inbound{
  95. Tag: "n1-in",
  96. Enable: true,
  97. Settings: `{"clients": [{"email": "keep", "enable": true}]}`,
  98. ClientStats: []xray.ClientTraffic{{Email: "keep", Up: 20, Down: 20, Enable: true}},
  99. })
  100. })
  101. if err != nil {
  102. t.Fatalf("sync node 1 without the gone client: %v", err)
  103. }
  104. var baselines int64
  105. if err := db.Model(&model.NodeClientTraffic{}).Where("node_id = ? AND email = ?", 1, "gone").Count(&baselines).Error; err != nil {
  106. t.Fatalf("count node baselines: %v", err)
  107. }
  108. if baselines != 0 {
  109. t.Fatalf("node baseline for the gone client = %d rows, want 0", baselines)
  110. }
  111. }
  112. func TestUpdateClientTrafficsStaysOnOneConnection(t *testing.T) {
  113. setupBulkDB(t)
  114. svc := &InboundService{}
  115. clients := []model.Client{{Email: "gone@x", ID: "aaaaaaaa-0000-0000-0000-000000000101", Enable: true}}
  116. oldIb := mkInbound(t, 22101, model.VLESS, clientsSettings(t, clients))
  117. if err := database.GetDB().Create(&xray.ClientTraffic{InboundId: oldIb.Id, Email: "gone@x", Enable: true}).Error; err != nil {
  118. t.Fatalf("seed traffic: %v", err)
  119. }
  120. newIb := *oldIb
  121. newIb.Settings = `{"clients": []}`
  122. capPoolAtOneConn(t)
  123. err := runWithoutSecondConn(t, "inbound edit dropping a client", func() error {
  124. return database.GetDB().Transaction(func(tx *gorm.DB) error {
  125. return svc.updateClientTraffics(tx, oldIb, &newIb)
  126. })
  127. })
  128. if err != nil {
  129. t.Fatalf("updateClientTraffics: %v", err)
  130. }
  131. var n int64
  132. database.GetDB().Model(xray.ClientTraffic{}).Where("email = ?", "gone@x").Count(&n)
  133. if n != 0 {
  134. t.Fatalf("traffic rows for dropped client = %d, want 0", n)
  135. }
  136. }
  137. func TestUpdateInboundClientRenameOntoExistingStatStaysOnOneConnection(t *testing.T) {
  138. setupBulkDB(t)
  139. svc := &ClientService{}
  140. inboundSvc := &InboundService{}
  141. source := []model.Client{{Email: "old@x", ID: "aaaaaaaa-0000-0000-0000-000000000102", SubID: "sub-102", Enable: true}}
  142. ib := mkInbound(t, 22102, model.VLESS, clientsSettings(t, source))
  143. if err := svc.SyncInbound(nil, ib.Id, source); err != nil {
  144. t.Fatalf("seed linkage: %v", err)
  145. }
  146. if err := database.GetDB().Create(&xray.ClientTraffic{InboundId: ib.Id, Email: "new@x", Enable: true}).Error; err != nil {
  147. t.Fatalf("seed target traffic: %v", err)
  148. }
  149. renamed := source
  150. renamed[0].Email = "new@x"
  151. capPoolAtOneConn(t)
  152. err := runWithoutSecondConn(t, "client rename onto an existing stats row", func() error {
  153. _, err := svc.UpdateInboundClient(inboundSvc, &model.Inbound{Id: ib.Id, Settings: clientsSettings(t, renamed)}, "old@x")
  154. return err
  155. })
  156. if err != nil {
  157. t.Fatalf("UpdateInboundClient: %v", err)
  158. }
  159. var n int64
  160. database.GetDB().Model(xray.ClientTraffic{}).Where("email = ?", "old@x").Count(&n)
  161. if n != 0 {
  162. t.Fatalf("traffic rows for renamed-away email = %d, want 0", n)
  163. }
  164. }