| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- package tailhub
- import (
- "sync"
- "testing"
- "time"
- )
- func TestHub_PublishSubscribe(t *testing.T) {
- h := NewHub()
- s := h.Subscribe(Filter{})
- defer s.Unsubscribe()
- ev := &Event{AlertID: "a1", CompanyID: "acme-001", SourceID: "prom-prod"}
- h.Publish(ev)
- select {
- case got := <-s.C:
- if got.AlertID != "a1" {
- t.Fatalf("got %q, want a1", got.AlertID)
- }
- case <-time.After(time.Second):
- t.Fatal("timeout waiting for event")
- }
- }
- func TestHub_CompanyFilter(t *testing.T) {
- h := NewHub()
- all := h.Subscribe(Filter{})
- acme := h.Subscribe(Filter{CompanyID: "acme-001"})
- defer all.Unsubscribe()
- defer acme.Unsubscribe()
- h.Publish(&Event{AlertID: "a1", CompanyID: "acme-001"})
- h.Publish(&Event{AlertID: "g1", CompanyID: "globex-002"})
- // all sees both
- got := map[string]bool{}
- for i := 0; i < 2; i++ {
- select {
- case ev := <-all.C:
- got[ev.AlertID] = true
- case <-time.After(time.Second):
- t.Fatal("timeout")
- }
- }
- if !got["a1"] || !got["g1"] {
- t.Fatalf("all subscriber missing events, got %v", got)
- }
- // acme sees only a1
- select {
- case ev := <-acme.C:
- if ev.AlertID != "a1" {
- t.Fatalf("acme got %q, want a1", ev.AlertID)
- }
- case <-time.After(time.Second):
- t.Fatal("timeout")
- }
- select {
- case ev := <-acme.C:
- t.Fatalf("acme got unexpected %q (filter broken)", ev.AlertID)
- case <-time.After(50 * time.Millisecond):
- // expected: no more events
- }
- }
- func TestHub_DropsOnSlowConsumer(t *testing.T) {
- h := NewHub()
- s := h.Subscribe(Filter{})
- defer s.Unsubscribe()
- // Publish more than the buffer can hold. Buffer is 64.
- for i := 0; i < 100; i++ {
- h.Publish(&Event{AlertID: "x"})
- }
- // Drain — anything in the buffer is fine. We just need to
- // see Drops > 0 on the subscription.
- for i := 0; i < 64; i++ {
- select {
- case <-s.C:
- case <-time.After(time.Second):
- t.Fatalf("timeout at %d", i)
- }
- }
- // Allow the hub's lock-free drop path to register
- time.Sleep(10 * time.Millisecond)
- if s.Drops.Load() == 0 {
- t.Fatal("expected Drops > 0")
- }
- if stats := h.Stats(); stats.DroppedTotal == 0 {
- t.Fatal("expected hub.DroppedTotal > 0")
- }
- }
- func TestHub_UnsubscribeClosesChannel(t *testing.T) {
- h := NewHub()
- s := h.Subscribe(Filter{})
- s.Unsubscribe()
- // second unsubscribe is a no-op
- s.Unsubscribe()
- // Channel should be closed
- select {
- case _, ok := <-s.C:
- if ok {
- t.Fatal("channel still open after Unsubscribe")
- }
- case <-time.After(time.Second):
- t.Fatal("channel not closed after Unsubscribe")
- }
- }
- func TestHub_Concurrent(t *testing.T) {
- // Race detector: many publishers and one subscriber.
- h := NewHub()
- s := h.Subscribe(Filter{})
- defer s.Unsubscribe()
- const n = 1000
- var wg sync.WaitGroup
- for i := 0; i < 10; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for j := 0; j < n/10; j++ {
- h.Publish(&Event{AlertID: "x"})
- }
- }()
- }
- // Drain concurrently
- drained := 0
- done := make(chan struct{})
- go func() {
- for {
- select {
- case <-s.C:
- drained++
- case <-done:
- return
- }
- }
- }()
- wg.Wait()
- time.Sleep(50 * time.Millisecond)
- close(done)
- // We can't assert exact drained (some dropped), but it must
- // be > 0 and ≤ n.
- if drained == 0 {
- t.Fatal("drained=0 (consumer dead?)")
- }
- if stats := h.Stats(); stats.PublishedTotal != uint64(n) {
- t.Fatalf("PublishedTotal=%d, want %d", stats.PublishedTotal, n)
- }
- }
|