hub_test.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package tailhub
  2. import (
  3. "sync"
  4. "testing"
  5. "time"
  6. )
  7. func TestHub_PublishSubscribe(t *testing.T) {
  8. h := NewHub()
  9. s := h.Subscribe(Filter{})
  10. defer s.Unsubscribe()
  11. ev := &Event{AlertID: "a1", CompanyID: "acme-001", SourceID: "prom-prod"}
  12. h.Publish(ev)
  13. select {
  14. case got := <-s.C:
  15. if got.AlertID != "a1" {
  16. t.Fatalf("got %q, want a1", got.AlertID)
  17. }
  18. case <-time.After(time.Second):
  19. t.Fatal("timeout waiting for event")
  20. }
  21. }
  22. func TestHub_CompanyFilter(t *testing.T) {
  23. h := NewHub()
  24. all := h.Subscribe(Filter{})
  25. acme := h.Subscribe(Filter{CompanyID: "acme-001"})
  26. defer all.Unsubscribe()
  27. defer acme.Unsubscribe()
  28. h.Publish(&Event{AlertID: "a1", CompanyID: "acme-001"})
  29. h.Publish(&Event{AlertID: "g1", CompanyID: "globex-002"})
  30. // all sees both
  31. got := map[string]bool{}
  32. for i := 0; i < 2; i++ {
  33. select {
  34. case ev := <-all.C:
  35. got[ev.AlertID] = true
  36. case <-time.After(time.Second):
  37. t.Fatal("timeout")
  38. }
  39. }
  40. if !got["a1"] || !got["g1"] {
  41. t.Fatalf("all subscriber missing events, got %v", got)
  42. }
  43. // acme sees only a1
  44. select {
  45. case ev := <-acme.C:
  46. if ev.AlertID != "a1" {
  47. t.Fatalf("acme got %q, want a1", ev.AlertID)
  48. }
  49. case <-time.After(time.Second):
  50. t.Fatal("timeout")
  51. }
  52. select {
  53. case ev := <-acme.C:
  54. t.Fatalf("acme got unexpected %q (filter broken)", ev.AlertID)
  55. case <-time.After(50 * time.Millisecond):
  56. // expected: no more events
  57. }
  58. }
  59. func TestHub_DropsOnSlowConsumer(t *testing.T) {
  60. h := NewHub()
  61. s := h.Subscribe(Filter{})
  62. defer s.Unsubscribe()
  63. // Publish more than the buffer can hold. Buffer is 64.
  64. for i := 0; i < 100; i++ {
  65. h.Publish(&Event{AlertID: "x"})
  66. }
  67. // Drain — anything in the buffer is fine. We just need to
  68. // see Drops > 0 on the subscription.
  69. for i := 0; i < 64; i++ {
  70. select {
  71. case <-s.C:
  72. case <-time.After(time.Second):
  73. t.Fatalf("timeout at %d", i)
  74. }
  75. }
  76. // Allow the hub's lock-free drop path to register
  77. time.Sleep(10 * time.Millisecond)
  78. if s.Drops.Load() == 0 {
  79. t.Fatal("expected Drops > 0")
  80. }
  81. if stats := h.Stats(); stats.DroppedTotal == 0 {
  82. t.Fatal("expected hub.DroppedTotal > 0")
  83. }
  84. }
  85. func TestHub_UnsubscribeClosesChannel(t *testing.T) {
  86. h := NewHub()
  87. s := h.Subscribe(Filter{})
  88. s.Unsubscribe()
  89. // second unsubscribe is a no-op
  90. s.Unsubscribe()
  91. // Channel should be closed
  92. select {
  93. case _, ok := <-s.C:
  94. if ok {
  95. t.Fatal("channel still open after Unsubscribe")
  96. }
  97. case <-time.After(time.Second):
  98. t.Fatal("channel not closed after Unsubscribe")
  99. }
  100. }
  101. func TestHub_Concurrent(t *testing.T) {
  102. // Race detector: many publishers and one subscriber.
  103. h := NewHub()
  104. s := h.Subscribe(Filter{})
  105. defer s.Unsubscribe()
  106. const n = 1000
  107. var wg sync.WaitGroup
  108. for i := 0; i < 10; i++ {
  109. wg.Add(1)
  110. go func() {
  111. defer wg.Done()
  112. for j := 0; j < n/10; j++ {
  113. h.Publish(&Event{AlertID: "x"})
  114. }
  115. }()
  116. }
  117. // Drain concurrently
  118. drained := 0
  119. done := make(chan struct{})
  120. go func() {
  121. for {
  122. select {
  123. case <-s.C:
  124. drained++
  125. case <-done:
  126. return
  127. }
  128. }
  129. }()
  130. wg.Wait()
  131. time.Sleep(50 * time.Millisecond)
  132. close(done)
  133. // We can't assert exact drained (some dropped), but it must
  134. // be > 0 and ≤ n.
  135. if drained == 0 {
  136. t.Fatal("drained=0 (consumer dead?)")
  137. }
  138. if stats := h.Stats(); stats.PublishedTotal != uint64(n) {
  139. t.Fatalf("PublishedTotal=%d, want %d", stats.PublishedTotal, n)
  140. }
  141. }