1
0

sub_scale_test.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package sub
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strconv"
  8. "strings"
  9. "testing"
  10. "time"
  11. "github.com/google/uuid"
  12. "github.com/op/go-logging"
  13. "gorm.io/gorm"
  14. "github.com/mhsanaei/3x-ui/v3/internal/config"
  15. "github.com/mhsanaei/3x-ui/v3/internal/database"
  16. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  17. xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
  18. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  19. )
  20. const scaleTargetSubId = "scale-target-sub"
  21. // setupScaleSubDB mirrors the service package's scale gating: Postgres via
  22. // XUI_DB_TYPE/XUI_DB_DSN, SQLite via XUI_SCALE_TEST=1, skip otherwise.
  23. func setupScaleSubDB(t *testing.T) {
  24. t.Helper()
  25. xuilogger.InitLogger(logging.ERROR)
  26. if os.Getenv("XUI_DB_TYPE") == "postgres" && strings.TrimSpace(os.Getenv("XUI_DB_DSN")) != "" {
  27. if err := database.InitDB(""); err != nil {
  28. t.Fatalf("InitDB(postgres): %v", err)
  29. }
  30. t.Cleanup(func() { _ = database.CloseDB() })
  31. return
  32. }
  33. switch strings.ToLower(strings.TrimSpace(os.Getenv("XUI_SCALE_TEST"))) {
  34. case "1", "true", "yes":
  35. if err := database.InitDB(filepath.Join(t.TempDir(), "scale.db")); err != nil {
  36. t.Fatalf("InitDB(sqlite): %v", err)
  37. }
  38. t.Cleanup(func() { _ = database.CloseDB() })
  39. return
  40. }
  41. t.Skip("set XUI_SCALE_TEST=1 (sqlite) or XUI_DB_TYPE=postgres + XUI_DB_DSN (postgres) to run the scale benchmark")
  42. }
  43. func scaleSubSizes(t *testing.T, def ...int) []int {
  44. t.Helper()
  45. raw := strings.TrimSpace(os.Getenv("XUI_SCALE_SIZES"))
  46. if raw == "" {
  47. return def
  48. }
  49. var out []int
  50. for _, part := range strings.Split(raw, ",") {
  51. part = strings.TrimSpace(part)
  52. if part == "" {
  53. continue
  54. }
  55. n, err := strconv.Atoi(part)
  56. if err != nil || n <= 0 {
  57. t.Fatalf("XUI_SCALE_SIZES: invalid size %q", part)
  58. }
  59. out = append(out, n)
  60. }
  61. if len(out) == 0 {
  62. return def
  63. }
  64. return out
  65. }
  66. func resetScaleSubTables(t *testing.T, db *gorm.DB) {
  67. t.Helper()
  68. if config.GetDBKind() == "postgres" {
  69. if err := db.Exec("TRUNCATE TABLE inbounds, clients, client_inbounds, client_traffics RESTART IDENTITY CASCADE").Error; err != nil {
  70. t.Fatalf("truncate: %v", err)
  71. }
  72. } else {
  73. for _, tbl := range []string{"inbounds", "clients", "client_inbounds", "client_traffics"} {
  74. if err := db.Exec("DELETE FROM " + tbl).Error; err != nil {
  75. t.Fatalf("delete %s: %v", tbl, err)
  76. }
  77. }
  78. db.Exec("DELETE FROM sqlite_sequence")
  79. }
  80. if err := db.Where("1 = 1").Delete(&model.ClientExternalLink{}).Error; err != nil {
  81. t.Fatalf("clear client_external_links: %v", err)
  82. }
  83. }
  84. // seedScaleSubDataset seeds one VLESS inbound holding n clients (the sub
  85. // server's worst case: matchingClients parses the whole settings blob and
  86. // getInboundsBySubId preloads every ClientStats row). Three clients share
  87. // scaleTargetSubId; everyone else gets a unique subId.
  88. func seedScaleSubDataset(t *testing.T, n int) {
  89. t.Helper()
  90. db := database.GetDB()
  91. resetScaleSubTables(t, db)
  92. clients := make([]model.Client, n)
  93. exp := time.Now().AddDate(1, 0, 0).UnixMilli()
  94. targets := map[int]bool{n / 4: true, n / 2: true, 3 * n / 4: true}
  95. for i := range n {
  96. subId := fmt.Sprintf("sub-%07d", i)
  97. if targets[i] {
  98. subId = scaleTargetSubId
  99. }
  100. clients[i] = model.Client{
  101. ID: uuid.NewString(),
  102. Email: fmt.Sprintf("user-%07d@subscale", i),
  103. SubID: subId,
  104. Enable: true,
  105. ExpiryTime: exp,
  106. TotalGB: 100 << 30,
  107. }
  108. }
  109. settingsMap := map[string]any{"clients": clients, "decryption": "none"}
  110. settings, err := json.Marshal(settingsMap)
  111. if err != nil {
  112. t.Fatalf("marshal settings: %v", err)
  113. }
  114. tx := db.Begin()
  115. if tx.Error != nil {
  116. t.Fatalf("begin seed tx: %v", tx.Error)
  117. }
  118. committed := false
  119. defer func() {
  120. if !committed {
  121. tx.Rollback()
  122. }
  123. }()
  124. ib := &model.Inbound{
  125. UserId: 1,
  126. Tag: fmt.Sprintf("subscale-%d", n),
  127. Remark: "subscale",
  128. Enable: true,
  129. Listen: "203.0.113.1",
  130. Port: 443,
  131. Protocol: model.VLESS,
  132. Settings: string(settings),
  133. StreamSettings: `{"network":"tcp","security":"none"}`,
  134. }
  135. if err := tx.Create(ib).Error; err != nil {
  136. t.Fatalf("seed inbound: %v", err)
  137. }
  138. records := make([]*model.ClientRecord, n)
  139. for i := range clients {
  140. records[i] = clients[i].ToRecord()
  141. }
  142. if err := tx.CreateInBatches(records, 500).Error; err != nil {
  143. t.Fatalf("seed clients: %v", err)
  144. }
  145. links := make([]model.ClientInbound, n)
  146. for i := range records {
  147. links[i] = model.ClientInbound{ClientId: records[i].Id, InboundId: ib.Id}
  148. }
  149. if err := tx.CreateInBatches(links, 1000).Error; err != nil {
  150. t.Fatalf("seed client_inbounds: %v", err)
  151. }
  152. traffics := make([]xray.ClientTraffic, n)
  153. for i := range clients {
  154. traffics[i] = xray.ClientTraffic{
  155. InboundId: ib.Id,
  156. Email: clients[i].Email,
  157. Enable: true,
  158. Total: clients[i].TotalGB,
  159. ExpiryTime: clients[i].ExpiryTime,
  160. }
  161. }
  162. if err := tx.CreateInBatches(traffics, 1000).Error; err != nil {
  163. t.Fatalf("seed client_traffics: %v", err)
  164. }
  165. if err := tx.Commit().Error; err != nil {
  166. t.Fatalf("commit seed tx: %v", err)
  167. }
  168. committed = true
  169. db.Exec("ANALYZE")
  170. }
  171. // TestGetSubsScale measures one subscription fetch (raw and JSON format) for a
  172. // 3-client subId living inside an n-client inbound, plus a subId miss — the
  173. // per-request cost every subscriber pays.
  174. func TestGetSubsScale(t *testing.T) {
  175. for _, n := range scaleSubSizes(t, 10000, 100000) {
  176. t.Run(fmt.Sprintf("N=%d", n), func(t *testing.T) {
  177. setupScaleSubDB(t)
  178. seedScaleSubDataset(t, n)
  179. svc := &SubService{}
  180. const reps = 5
  181. start := time.Now()
  182. var links []string
  183. for range reps {
  184. var err error
  185. links, _, _, _, err = svc.GetSubs(scaleTargetSubId, "sub.example.com")
  186. if err != nil {
  187. t.Fatalf("GetSubs: %v", err)
  188. }
  189. }
  190. rawDur := time.Since(start) / reps
  191. if len(links) != 3 {
  192. t.Fatalf("GetSubs links = %d, want 3", len(links))
  193. }
  194. jsonSvc := NewSubJsonService("", "", "", &SubService{})
  195. start = time.Now()
  196. for range reps {
  197. body, _, err := jsonSvc.GetJson(scaleTargetSubId, "sub.example.com")
  198. if err != nil {
  199. t.Fatalf("GetJson: %v", err)
  200. }
  201. if body == "" {
  202. t.Fatal("GetJson returned empty body")
  203. }
  204. }
  205. jsonDur := time.Since(start) / reps
  206. start = time.Now()
  207. for range reps {
  208. missLinks, _, _, _, err := svc.GetSubs("no-such-sub", "sub.example.com")
  209. if err != nil {
  210. t.Fatalf("GetSubs miss: %v", err)
  211. }
  212. if len(missLinks) != 0 {
  213. t.Fatalf("GetSubs miss links = %d, want 0", len(missLinks))
  214. }
  215. }
  216. missDur := time.Since(start) / reps
  217. t.Logf("N=%-7d raw=%-10v json=%-10v miss=%v",
  218. n, rawDur.Round(time.Millisecond), jsonDur.Round(time.Millisecond), missDur.Round(time.Millisecond))
  219. })
  220. }
  221. }