1
0

graphite_test.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. package nebula
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net"
  9. "strings"
  10. "sync"
  11. "testing"
  12. "time"
  13. "github.com/rcrowley/go-metrics"
  14. "github.com/slackhq/nebula/test"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/require"
  17. )
  18. // graphiteHost accepts connections and hands each one to the test
  19. type graphiteHost struct {
  20. ln net.Listener
  21. conns chan net.Conn
  22. mu sync.Mutex
  23. closed bool
  24. open []net.Conn
  25. }
  26. // newGraphiteHost accepts connections for the test. A non-zero window shrinks each connection's receive buffer, so a
  27. // sender that is not read from blocks after a little whatever the host's TCP tuning. Only for tests that need a stall,
  28. // a small window makes reading very slow on Linux.
  29. func newGraphiteHost(t *testing.T, window int) *graphiteHost {
  30. t.Helper()
  31. ln, err := net.Listen("tcp", "127.0.0.1:0")
  32. require.NoError(t, err)
  33. h := &graphiteHost{ln: ln, conns: make(chan net.Conn, 8)}
  34. done := make(chan struct{})
  35. t.Cleanup(func() {
  36. _ = ln.Close()
  37. h.mu.Lock()
  38. h.closed = true
  39. for _, c := range h.open {
  40. _ = c.Close()
  41. }
  42. h.mu.Unlock()
  43. close(done)
  44. })
  45. go func() {
  46. for {
  47. c, err := ln.Accept()
  48. if err != nil {
  49. return
  50. }
  51. h.mu.Lock()
  52. if h.closed {
  53. h.mu.Unlock()
  54. _ = c.Close()
  55. return
  56. }
  57. h.open = append(h.open, c)
  58. h.mu.Unlock()
  59. if tc, ok := c.(*net.TCPConn); ok && window > 0 {
  60. _ = tc.SetReadBuffer(window)
  61. }
  62. select {
  63. case h.conns <- c:
  64. case <-done:
  65. return
  66. }
  67. }
  68. }()
  69. return h
  70. }
  71. func (h *graphiteHost) addr(t *testing.T) *net.TCPAddr {
  72. a, err := net.ResolveTCPAddr("tcp", h.ln.Addr().String())
  73. require.NoError(t, err)
  74. return a
  75. }
  76. // readExport reads what one send wrote, bounded so a send that never finishes fails the test rather than hanging it
  77. func readExport(t *testing.T, c net.Conn) []byte {
  78. t.Helper()
  79. require.NoError(t, c.SetReadDeadline(time.Now().Add(10*time.Second)))
  80. b, err := io.ReadAll(c)
  81. require.NoError(t, err)
  82. return b
  83. }
  84. func (h *graphiteHost) next(t *testing.T, wait time.Duration) net.Conn {
  85. t.Helper()
  86. select {
  87. case c := <-h.conns:
  88. return c
  89. case <-time.After(wait):
  90. t.Fatal("no connection")
  91. return nil
  92. }
  93. }
  94. // lockedBuffer is a log destination a test can read while the code under test writes to it from another goroutine
  95. type lockedBuffer struct {
  96. mu sync.Mutex
  97. buf bytes.Buffer
  98. }
  99. func (b *lockedBuffer) Write(p []byte) (int, error) {
  100. b.mu.Lock()
  101. defer b.mu.Unlock()
  102. return b.buf.Write(p)
  103. }
  104. func (b *lockedBuffer) String() string {
  105. b.mu.Lock()
  106. defer b.mu.Unlock()
  107. return b.buf.String()
  108. }
  109. func testGraphiteConfig(addr *net.TCPAddr, r metrics.Registry) graphiteConfigExport {
  110. return graphiteConfigExport{Addr: addr, Registry: r, FlushInterval: time.Second, DurationUnit: time.Nanosecond, Prefix: "nebula"}
  111. }
  112. // Enough metrics that one export overflows the socket buffers on any platform, so a send to a host that stops
  113. // reading blocks
  114. func bigGraphiteRegistry(r metrics.Registry, t *testing.T) {
  115. for i := range 200000 {
  116. name := fmt.Sprintf("big.gauge.%d", i)
  117. metrics.GetOrRegisterGauge(name, r).Update(int64(i))
  118. t.Cleanup(func() { r.Unregister(name) })
  119. }
  120. }
  121. func TestGraphiteOnce_sends(t *testing.T) {
  122. r := metrics.NewRegistry()
  123. metrics.GetOrRegisterGauge("g", r).Update(7)
  124. metrics.GetOrRegisterCounter("c", r).Inc(3)
  125. h := newGraphiteHost(t, 0)
  126. require.NoError(t, graphiteOnce(t.Context(), testGraphiteConfig(h.addr(t), r), time.Minute))
  127. b := readExport(t, h.next(t, 5*time.Second))
  128. assert.Contains(t, string(b), "nebula.g.value 7 ")
  129. assert.Contains(t, string(b), "nebula.c.count 3 ")
  130. assert.Contains(t, string(b), "nebula.c.count_ps 3.00 ")
  131. }
  132. // A host that accepts and never reads is given up on, and the failed write is reported
  133. func TestGraphiteOnce_stalledHostTimesOut(t *testing.T) {
  134. r := metrics.NewRegistry()
  135. bigGraphiteRegistry(r, t)
  136. h := newGraphiteHost(t, 4096)
  137. done := make(chan error, 1)
  138. cfg := testGraphiteConfig(h.addr(t), r)
  139. go func() { done <- graphiteOnce(t.Context(), cfg, 300*time.Millisecond) }()
  140. h.next(t, 5*time.Second) // accepted, never read
  141. select {
  142. case err := <-done:
  143. require.Error(t, err)
  144. case <-time.After(10 * time.Second):
  145. t.Fatal("the send did not give up on a host that stopped reading")
  146. }
  147. }
  148. // The timeout covers the whole send, a host that keeps reading but too slowly is still given up on
  149. func TestGraphiteOnce_timeoutCoversTheWholeSend(t *testing.T) {
  150. r := metrics.NewRegistry()
  151. bigGraphiteRegistry(r, t)
  152. h := newGraphiteHost(t, 0)
  153. done := make(chan error, 1)
  154. start := time.Now()
  155. cfg := testGraphiteConfig(h.addr(t), r)
  156. go func() { done <- graphiteOnce(t.Context(), cfg, 500*time.Millisecond) }()
  157. c := h.next(t, 5*time.Second)
  158. stopReading := make(chan struct{})
  159. defer close(stopReading)
  160. // 32KB every 10ms keeps every write well inside the timeout, the whole export takes seconds
  161. go func() {
  162. buf := make([]byte, 32<<10)
  163. for {
  164. select {
  165. case <-stopReading:
  166. return
  167. case <-time.After(10 * time.Millisecond):
  168. }
  169. if _, err := c.Read(buf); err != nil {
  170. return
  171. }
  172. }
  173. }()
  174. select {
  175. case err := <-done:
  176. require.Error(t, err)
  177. assert.Less(t, time.Since(start), 5*time.Second)
  178. case <-time.After(10 * time.Second):
  179. t.Fatal("a host that kept reading slowly held the send past its timeout")
  180. }
  181. }
  182. func TestGraphiteOnce_cancelledCtxDoesNotConnect(t *testing.T) {
  183. h := newGraphiteHost(t, 0)
  184. ctx, cancel := context.WithCancel(t.Context())
  185. cancel()
  186. require.Error(t, graphiteOnce(ctx, testGraphiteConfig(h.addr(t), metrics.NewRegistry()), time.Minute))
  187. select {
  188. case <-h.conns:
  189. t.Fatal("connected on a cancelled ctx")
  190. case <-time.After(200 * time.Millisecond):
  191. }
  192. }
  193. // A stop or reload abandons a stuck send rather than waiting out its timeout
  194. func TestGraphiteOnce_cancelAbandonsSend(t *testing.T) {
  195. r := metrics.NewRegistry()
  196. bigGraphiteRegistry(r, t)
  197. h := newGraphiteHost(t, 4096)
  198. ctx, cancel := context.WithCancel(t.Context())
  199. done := make(chan error, 1)
  200. cfg := testGraphiteConfig(h.addr(t), r)
  201. go func() { done <- graphiteOnce(ctx, cfg, time.Minute) }()
  202. h.next(t, 5*time.Second)
  203. select {
  204. case err := <-done:
  205. t.Fatalf("the send should be stuck on a host that is not reading, it ended with %v", err)
  206. case <-time.After(200 * time.Millisecond):
  207. }
  208. cancel()
  209. select {
  210. case err := <-done:
  211. require.Error(t, err)
  212. case <-time.After(5 * time.Second):
  213. t.Fatal("the send is still stuck after its ctx was cancelled")
  214. }
  215. }
  216. // buildRuntime hands graphite the configured prefix and interval
  217. func TestStatsServer_graphiteExportUsesConfig(t *testing.T) {
  218. const name = "graphite.wiring.test"
  219. metrics.GetOrRegisterCounter(name, nil).Inc(8)
  220. t.Cleanup(func() { metrics.DefaultRegistry.Unregister(name) })
  221. h := newGraphiteHost(t, 0)
  222. s, c := newTestStatsServer(t)
  223. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "4s", "prefix": "pfx", "host": h.ln.Addr().String()})
  224. cfg, err := loadStatsConfig(c)
  225. require.NoError(t, err)
  226. s.runMu.Lock()
  227. fns, _ := s.buildRuntime(t.Context(), cfg)
  228. s.runMu.Unlock()
  229. for _, fn := range fns {
  230. fn()
  231. }
  232. b := readExport(t, h.next(t, 5*time.Second))
  233. assert.Contains(t, string(b), "pfx."+name+".count 8 ")
  234. assert.Contains(t, string(b), "pfx."+name+".count_ps 2.00 ")
  235. }
  236. func TestStatsServer_graphiteFailureIsLogged(t *testing.T) {
  237. logs := &lockedBuffer{}
  238. s, c := newTestStatsServer(t)
  239. s.l = test.NewLoggerWithOutput(logs)
  240. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "1h", "host": "127.0.0.1:" + freeTCPPort(t)})
  241. cfg, err := loadStatsConfig(c)
  242. require.NoError(t, err)
  243. s.runMu.Lock()
  244. fns, _ := s.buildRuntime(t.Context(), cfg)
  245. s.runMu.Unlock()
  246. for _, fn := range fns {
  247. fn()
  248. }
  249. assert.Contains(t, logs.String(), "Graphite export failed")
  250. }
  251. // Stop abandons the running export, so it goes through Start rather than handing buildRuntime a ctx, and a stop is
  252. // not logged as a failure
  253. func TestStatsServer_stopAbandonsGraphiteSend(t *testing.T) {
  254. bigGraphiteRegistry(metrics.DefaultRegistry, t)
  255. h := newGraphiteHost(t, 4096)
  256. logs := &lockedBuffer{}
  257. s, c := newTestStatsServer(t)
  258. s.l = test.NewLoggerWithOutput(logs)
  259. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "50ms", "host": h.ln.Addr().String()})
  260. require.NoError(t, s.reload(c, true))
  261. started := make(chan struct{})
  262. go func() {
  263. s.Start()
  264. close(started)
  265. }()
  266. stuck := h.next(t, 30*time.Second)
  267. s.Stop()
  268. select {
  269. case <-started:
  270. case <-time.After(5 * time.Second):
  271. t.Fatal("Start did not return after Stop")
  272. }
  273. // A closed sender resets the connection once it sees this, one still sending just buffers it. Either way the
  274. // read below doesn't have to drain the export through the small window to find out
  275. _, err := stuck.Write([]byte("x"))
  276. require.NoError(t, err)
  277. require.NoError(t, stuck.SetReadDeadline(time.Now().Add(10*time.Second)))
  278. n, err := io.Copy(io.Discard, stuck)
  279. var ne net.Error
  280. if errors.As(err, &ne) && ne.Timeout() {
  281. t.Fatalf("the send kept the connection after Stop, read %d bytes", n)
  282. }
  283. assert.Less(t, n, int64(1<<20), "the send kept going after Stop")
  284. // The capture loop logs on its own goroutine after Start has returned, give it the chance
  285. assert.Never(t, func() bool { return strings.Contains(logs.String(), "Graphite export failed") }, 500*time.Millisecond, 10*time.Millisecond, "a stop is not a failure")
  286. }