xray_metrics_test.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. package service
  2. import (
  3. "path/filepath"
  4. "testing"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/eventbus"
  8. )
  9. // probe is one observatory sample: whether the outbound is alive and the
  10. // last_try_time xray reports for it (a new probe advances lastTry).
  11. type probe struct {
  12. alive bool
  13. lastTry int64
  14. }
  15. const testSentinel eventbus.EventType = "test.sentinel"
  16. // runObservatory feeds a probe sequence through applyObservatory with the given
  17. // threshold and returns the outbound.* events it published, in order.
  18. func runObservatory(t *testing.T, threshold int, seq []probe) []eventbus.EventType {
  19. t.Helper()
  20. ss := SettingService{}
  21. if err := ss.SetOutboundDownThreshold(threshold); err != nil {
  22. t.Fatalf("set threshold: %v", err)
  23. }
  24. bus := eventbus.New(256)
  25. events := make(chan eventbus.Event, 256)
  26. bus.Subscribe("test", func(e eventbus.Event) { events <- e })
  27. SetEventBus(bus)
  28. t.Cleanup(func() {
  29. SetEventBus(nil)
  30. bus.Stop()
  31. })
  32. s := &XrayMetricsService{settingService: ss}
  33. for _, p := range seq {
  34. s.applyObservatory(time.Unix(p.lastTry, 0), map[string]rawObsEntry{
  35. "proxy": {Alive: p.alive, Delay: 10, LastTryTime: p.lastTry, OutboundTag: "proxy"},
  36. })
  37. }
  38. bus.Publish(eventbus.Event{Type: testSentinel, Source: "x"})
  39. var got []eventbus.EventType
  40. for {
  41. select {
  42. case e := <-events:
  43. if e.Type == testSentinel {
  44. return got
  45. }
  46. got = append(got, e.Type)
  47. case <-time.After(2 * time.Second):
  48. t.Fatal("timed out waiting for events to drain")
  49. }
  50. }
  51. }
  52. func TestApplyObservatoryDebounce(t *testing.T) {
  53. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  54. t.Fatalf("init db: %v", err)
  55. }
  56. t.Cleanup(func() { _ = database.CloseDB() })
  57. tests := []struct {
  58. name string
  59. threshold int
  60. seq []probe
  61. want []eventbus.EventType
  62. }{
  63. {
  64. name: "notifies only after threshold consecutive failed probes",
  65. threshold: 3,
  66. seq: []probe{
  67. {true, 1},
  68. {false, 2},
  69. {false, 3},
  70. {false, 4},
  71. {false, 5},
  72. {true, 6},
  73. {false, 7},
  74. {true, 8},
  75. },
  76. want: []eventbus.EventType{eventbus.EventOutboundDown, eventbus.EventOutboundUp},
  77. },
  78. {
  79. name: "repeated samples of the same probe do not advance the streak",
  80. threshold: 3,
  81. seq: []probe{{false, 2}, {false, 2}, {false, 2}, {false, 2}, {false, 2}},
  82. want: nil,
  83. },
  84. {
  85. name: "single-probe blip never notifies",
  86. threshold: 3,
  87. seq: []probe{{true, 1}, {false, 2}, {true, 3}},
  88. want: nil,
  89. },
  90. {
  91. name: "threshold 1 keeps the legacy notify-on-first-failure behaviour",
  92. threshold: 1,
  93. seq: []probe{{true, 1}, {false, 2}},
  94. want: []eventbus.EventType{eventbus.EventOutboundDown},
  95. },
  96. }
  97. for _, tt := range tests {
  98. t.Run(tt.name, func(t *testing.T) {
  99. got := runObservatory(t, tt.threshold, tt.seq)
  100. if len(got) != len(tt.want) {
  101. t.Fatalf("events = %v, want %v", got, tt.want)
  102. }
  103. for i := range got {
  104. if got[i] != tt.want[i] {
  105. t.Fatalf("event[%d] = %q, want %q (full: %v)", i, got[i], tt.want[i], got)
  106. }
  107. }
  108. })
  109. }
  110. }