node_sync_link_churn_test.go 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. package service
  2. import (
  3. "fmt"
  4. "strings"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  8. )
  9. // The node traffic poll runs every 5s and re-syncs every node inbound from its
  10. // snapshot. A steady-state poll must not rewrite the inbound's whole membership
  11. // set — that churn was the bulk of the write load in #6252.
  12. func TestNodeSyncDoesNotChurnInboundLinks(t *testing.T) {
  13. db := initTrafficTestDB(t)
  14. svc := &InboundService{}
  15. emails := []string{"n1@x", "n2@x", "n3@x"}
  16. entries := make([]string, 0, len(emails))
  17. stats := make([]xray.ClientTraffic, 0, len(emails))
  18. for i, e := range emails {
  19. entries = append(entries, fmt.Sprintf(`{"email": %q, "enable": true}`, e))
  20. stats = append(stats, xray.ClientTraffic{Email: e, Up: int64(100 * (i + 1)), Down: 100, Enable: true})
  21. }
  22. settings := `{"clients": [` + strings.Join(entries, ",") + `]}`
  23. createNodeInbound(t, db, 1, "n1-in", 41101)
  24. syncNodeWithSettings(t, svc, 1, "n1-in", settings, stats...)
  25. var ib model.Inbound
  26. if err := db.Where("tag = ?", "n1-in").First(&ib).Error; err != nil {
  27. t.Fatalf("load inbound: %v", err)
  28. }
  29. before := linksOf(t, ib.Id)
  30. if len(before) != len(emails) {
  31. t.Fatalf("link count after first sync = %d, want %d", len(before), len(emails))
  32. }
  33. stampLinkCreatedAt(t, ib.Id)
  34. // Second poll: identical client set, counters have grown.
  35. for i := range stats {
  36. stats[i].Up += 500
  37. stats[i].Down += 500
  38. }
  39. syncNodeWithSettings(t, svc, 1, "n1-in", settings, stats...)
  40. after := linksOf(t, ib.Id)
  41. if len(after) != len(before) {
  42. t.Fatalf("link count after second sync = %d, want %d", len(after), len(before))
  43. }
  44. for id, link := range after {
  45. if link.CreatedAt != 1 {
  46. t.Errorf("client %d: link created_at = %d, want the 1 sentinel: a steady-state node poll rebuilt the membership set",
  47. id, link.CreatedAt)
  48. }
  49. }
  50. // A client removed on the node must still lose its link, or the soft-orphan
  51. // sweep that reads this table would stop seeing remote deletions.
  52. shrunk := `{"clients": [` + strings.Join(entries[:2], ",") + `]}`
  53. syncNodeWithSettings(t, svc, 1, "n1-in", shrunk, stats[:2]...)
  54. pruned := linksOf(t, ib.Id)
  55. if len(pruned) != 2 {
  56. t.Fatalf("link count after shrink = %d, want 2", len(pruned))
  57. }
  58. if _, still := pruned[recordID(t, "n3@x")]; still {
  59. t.Error("n3@x link survived a snapshot that dropped it")
  60. }
  61. }