stats_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. package nebula
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "log/slog"
  7. "net"
  8. "net/http"
  9. "net/http/httptest"
  10. "strconv"
  11. "sync/atomic"
  12. "testing"
  13. "time"
  14. "github.com/rcrowley/go-metrics"
  15. "github.com/slackhq/nebula/config"
  16. "github.com/stretchr/testify/assert"
  17. "github.com/stretchr/testify/require"
  18. )
  19. func newTestStatsServer(t *testing.T) (*statsServer, *config.C) {
  20. t.Helper()
  21. l := slog.New(slog.DiscardHandler)
  22. ctx, cancel := context.WithCancel(context.Background())
  23. t.Cleanup(cancel)
  24. return &statsServer{
  25. l: l,
  26. ctx: ctx,
  27. }, config.NewC(l)
  28. }
  29. func setStatsConfig(c *config.C, m map[string]any) {
  30. c.Settings["stats"] = m
  31. }
  32. func currentRuntime(s *statsServer) *statsRuntime {
  33. s.runMu.Lock()
  34. defer s.runMu.Unlock()
  35. return s.run
  36. }
  37. func TestStatsServer_reload_initial_disabled(t *testing.T) {
  38. s, c := newTestStatsServer(t)
  39. setStatsConfig(c, map[string]any{"type": "none"})
  40. require.NoError(t, s.reload(c, true))
  41. assert.False(t, s.enabled.Load())
  42. assert.Nil(t, currentRuntime(s))
  43. }
  44. func TestStatsServer_reload_initial_invalidInterval(t *testing.T) {
  45. s, c := newTestStatsServer(t)
  46. setStatsConfig(c, map[string]any{
  47. "type": "graphite",
  48. "host": "127.0.0.1:0",
  49. "prefix": "test",
  50. })
  51. err := s.reload(c, true)
  52. require.Error(t, err)
  53. assert.False(t, s.enabled.Load())
  54. }
  55. func TestStatsServer_reload_initial_unknownType(t *testing.T) {
  56. s, c := newTestStatsServer(t)
  57. setStatsConfig(c, map[string]any{
  58. "type": "carbon",
  59. "interval": "1s",
  60. })
  61. err := s.reload(c, true)
  62. require.Error(t, err)
  63. assert.False(t, s.enabled.Load())
  64. }
  65. func TestStatsServer_reload_unchanged_noOp(t *testing.T) {
  66. s, c := newTestStatsServer(t)
  67. setStatsConfig(c, map[string]any{"type": "none"})
  68. require.NoError(t, s.reload(c, true))
  69. require.NoError(t, s.reload(c, false))
  70. assert.False(t, s.enabled.Load())
  71. }
  72. func TestStatsServer_reload_initial_graphite(t *testing.T) {
  73. s, c := newTestStatsServer(t)
  74. setStatsConfig(c, map[string]any{
  75. "type": "graphite",
  76. "interval": "1s",
  77. "protocol": "tcp",
  78. "host": "127.0.0.1:2003",
  79. "prefix": "test",
  80. })
  81. require.NoError(t, s.reload(c, true))
  82. assert.True(t, s.enabled.Load())
  83. // reload only records config; Start builds the runtime.
  84. assert.Nil(t, currentRuntime(s))
  85. }
  86. func TestStatsServer_reload_initial_prometheus(t *testing.T) {
  87. s, c := newTestStatsServer(t)
  88. setStatsConfig(c, map[string]any{
  89. "type": "prometheus",
  90. "interval": "1s",
  91. "listen": "127.0.0.1:0",
  92. "path": "/metrics",
  93. })
  94. require.NoError(t, s.reload(c, true))
  95. assert.True(t, s.enabled.Load())
  96. // reload only records config; Start builds the runtime.
  97. assert.Nil(t, currentRuntime(s))
  98. }
  99. func TestStatsServer_Start_graphite_blocksUntilStop(t *testing.T) {
  100. h := newGraphiteHost(t, 0)
  101. s, c := newTestStatsServer(t)
  102. setStatsConfig(c, map[string]any{
  103. "type": "graphite",
  104. "interval": "1s",
  105. "protocol": "tcp",
  106. "host": h.ln.Addr().String(),
  107. "prefix": "test",
  108. })
  109. require.NoError(t, s.reload(c, true))
  110. done := make(chan struct{})
  111. go func() {
  112. s.Start()
  113. close(done)
  114. }()
  115. // Wait for Start to publish runtime state.
  116. waitFor(t, func() bool { return currentRuntime(s) != nil })
  117. rt := currentRuntime(s)
  118. require.NotNil(t, rt)
  119. assert.Nil(t, rt.listener, "graphite has no listener")
  120. s.Stop()
  121. select {
  122. case <-done:
  123. case <-time.After(5 * time.Second):
  124. t.Fatal("graphite Start did not return after Stop")
  125. }
  126. assert.Nil(t, currentRuntime(s))
  127. }
  128. func TestStatsServer_StartStop_lifecycle(t *testing.T) {
  129. port := freeTCPPort(t)
  130. s, c := newTestStatsServer(t)
  131. setStatsConfig(c, map[string]any{
  132. "type": "prometheus",
  133. "interval": "1s",
  134. "listen": "127.0.0.1:" + port,
  135. "path": "/metrics",
  136. })
  137. require.NoError(t, s.reload(c, true))
  138. done := make(chan struct{})
  139. go func() {
  140. s.Start()
  141. close(done)
  142. }()
  143. waitForListening(t, "127.0.0.1:"+port)
  144. rt := currentRuntime(s)
  145. require.NotNil(t, rt)
  146. require.NotNil(t, rt.listener)
  147. s.Stop()
  148. select {
  149. case <-done:
  150. case <-time.After(5 * time.Second):
  151. t.Fatal("Start did not return after Stop")
  152. }
  153. assert.Nil(t, currentRuntime(s))
  154. }
  155. func TestStatsServer_reload_disable_stopsRunningRuntime(t *testing.T) {
  156. port := freeTCPPort(t)
  157. s, c := newTestStatsServer(t)
  158. setStatsConfig(c, map[string]any{
  159. "type": "prometheus",
  160. "interval": "1s",
  161. "listen": "127.0.0.1:" + port,
  162. "path": "/metrics",
  163. })
  164. require.NoError(t, s.reload(c, true))
  165. done := make(chan struct{})
  166. go func() {
  167. s.Start()
  168. close(done)
  169. }()
  170. waitForListening(t, "127.0.0.1:"+port)
  171. setStatsConfig(c, map[string]any{"type": "none"})
  172. require.NoError(t, s.reload(c, false))
  173. select {
  174. case <-done:
  175. case <-time.After(5 * time.Second):
  176. t.Fatal("Start did not return after reload disabled stats")
  177. }
  178. assert.False(t, s.enabled.Load())
  179. assert.Nil(t, currentRuntime(s))
  180. }
  181. func TestStatsServer_reload_changeListener_restartsListener(t *testing.T) {
  182. port1 := freeTCPPort(t)
  183. s, c := newTestStatsServer(t)
  184. setStatsConfig(c, map[string]any{
  185. "type": "prometheus",
  186. "interval": "1s",
  187. "listen": "127.0.0.1:" + port1,
  188. "path": "/metrics",
  189. })
  190. require.NoError(t, s.reload(c, true))
  191. firstDone := make(chan struct{})
  192. go func() {
  193. s.Start()
  194. close(firstDone)
  195. }()
  196. waitForListening(t, "127.0.0.1:"+port1)
  197. first := currentRuntime(s)
  198. require.NotNil(t, first)
  199. port2 := freeTCPPort(t)
  200. setStatsConfig(c, map[string]any{
  201. "type": "prometheus",
  202. "interval": "1s",
  203. "listen": "127.0.0.1:" + port2,
  204. "path": "/metrics",
  205. })
  206. require.NoError(t, s.reload(c, false))
  207. select {
  208. case <-firstDone:
  209. case <-time.After(5 * time.Second):
  210. t.Fatal("old Start did not return after reload")
  211. }
  212. waitForListening(t, "127.0.0.1:"+port2)
  213. second := currentRuntime(s)
  214. require.NotNil(t, second)
  215. assert.NotSame(t, first, second, "expected a new runtime after listen address change")
  216. s.Stop()
  217. }
  218. func TestStatsServer_Stop_beforeStart_doesNotBlock(t *testing.T) {
  219. s, c := newTestStatsServer(t)
  220. setStatsConfig(c, map[string]any{
  221. "type": "prometheus",
  222. "interval": "1s",
  223. "listen": "127.0.0.1:0",
  224. "path": "/metrics",
  225. })
  226. require.NoError(t, s.reload(c, true))
  227. stopped := make(chan struct{})
  228. go func() {
  229. s.Stop()
  230. close(stopped)
  231. }()
  232. select {
  233. case <-stopped:
  234. case <-time.After(time.Second):
  235. t.Fatal("Stop hung with no runtime started")
  236. }
  237. }
  238. func TestStatsServer_configTest_validatesWithoutSpawning(t *testing.T) {
  239. s, c := newTestStatsServer(t)
  240. s.configTest = true
  241. setStatsConfig(c, map[string]any{
  242. "type": "prometheus",
  243. "interval": "1s",
  244. "listen": "127.0.0.1:0",
  245. "path": "/metrics",
  246. })
  247. require.NoError(t, s.reload(c, true))
  248. s.Start()
  249. assert.Nil(t, currentRuntime(s))
  250. }
  251. func TestStatsServer_ctxCancel_unblocksStart(t *testing.T) {
  252. // Ensures ctx cancellation alone (no explicit Stop) tears down both
  253. // graphite and prom Start invocations.
  254. port := freeTCPPort(t)
  255. l := slog.New(slog.DiscardHandler)
  256. ctx, cancel := context.WithCancel(context.Background())
  257. defer cancel()
  258. s := &statsServer{l: l, ctx: ctx}
  259. c := config.NewC(l)
  260. setStatsConfig(c, map[string]any{
  261. "type": "prometheus",
  262. "interval": "1s",
  263. "listen": "127.0.0.1:" + port,
  264. "path": "/metrics",
  265. })
  266. require.NoError(t, s.reload(c, true))
  267. done := make(chan struct{})
  268. go func() {
  269. s.Start()
  270. close(done)
  271. }()
  272. waitForListening(t, "127.0.0.1:"+port)
  273. cancel()
  274. select {
  275. case <-done:
  276. case <-time.After(5 * time.Second):
  277. t.Fatal("Start did not return after ctx cancel")
  278. }
  279. }
  280. func TestStatsServer_listenerBindFailure_sameCfgReloadRetries(t *testing.T) {
  281. // Hold the port so ListenAndServe will fail on first Start.
  282. blocker, err := net.Listen("tcp", "127.0.0.1:0")
  283. require.NoError(t, err)
  284. port := strconv.Itoa(blocker.Addr().(*net.TCPAddr).Port)
  285. s, c := newTestStatsServer(t)
  286. setStatsConfig(c, map[string]any{
  287. "type": "prometheus",
  288. "interval": "1s",
  289. "listen": "127.0.0.1:" + port,
  290. "path": "/metrics",
  291. })
  292. require.NoError(t, s.reload(c, true))
  293. done := make(chan struct{})
  294. go func() {
  295. s.Start()
  296. close(done)
  297. }()
  298. select {
  299. case <-done:
  300. case <-time.After(5 * time.Second):
  301. t.Fatal("Start did not return after bind failure")
  302. }
  303. // Bind failure should have dropped the cached config so a same-cfg
  304. // SIGHUP can retry.
  305. s.runMu.Lock()
  306. cfgAfterFailure := s.runCfg
  307. s.runMu.Unlock()
  308. assert.Nil(t, cfgAfterFailure)
  309. // Free the port and reload with the same config; Start should fire again.
  310. require.NoError(t, blocker.Close())
  311. require.NoError(t, s.reload(c, false))
  312. waitForListening(t, "127.0.0.1:"+port)
  313. require.NotNil(t, currentRuntime(s))
  314. s.Stop()
  315. }
  316. func waitForListening(t *testing.T, addr string) {
  317. t.Helper()
  318. waitFor(t, func() bool {
  319. conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
  320. if err != nil {
  321. return false
  322. }
  323. _ = conn.Close()
  324. return true
  325. })
  326. }
  327. // stopAtCleanup returns the ctx to build *s with, and stops it when the test ends. Cancelling first turns away a Start
  328. // a reload spawned that hasn't run yet, then Stop waits out the last pass, so nothing captures into the next test
  329. func stopAtCleanup(t *testing.T, s **statsServer) context.Context {
  330. ctx, cancel := context.WithCancel(context.Background())
  331. t.Cleanup(func() {
  332. cancel()
  333. if *s != nil {
  334. (*s).Stop()
  335. }
  336. })
  337. return ctx
  338. }
  339. func freeTCPPort(t *testing.T) string {
  340. t.Helper()
  341. ln, err := net.Listen("tcp", "127.0.0.1:0")
  342. require.NoError(t, err)
  343. port := ln.Addr().(*net.TCPAddr).Port
  344. require.NoError(t, ln.Close())
  345. return strconv.Itoa(port)
  346. }
  347. // Each export has to see what emitStats set in the same pass
  348. func TestStatsServer_emitStatsRunsBeforeExport(t *testing.T) {
  349. defer metrics.DefaultRegistry.Unregister("emitter.order.test")
  350. l := slog.New(slog.DiscardHandler)
  351. c := config.NewC(l)
  352. setStatsConfig(c, map[string]any{
  353. "type": "prometheus",
  354. "interval": "1s",
  355. "listen": "127.0.0.1:0",
  356. "path": "/metrics",
  357. })
  358. gauge := metrics.GetOrRegisterGauge("emitter.order.test", nil)
  359. s, err := newStatsServerFromConfig(t.Context(), l, c, "", false, func() { gauge.Update(42) })
  360. require.NoError(t, err)
  361. s.runMu.Lock()
  362. fns, srv := s.buildRuntime(t.Context(), *s.runCfg)
  363. s.runMu.Unlock()
  364. require.NotNil(t, srv)
  365. for _, fn := range fns {
  366. fn()
  367. }
  368. rec := httptest.NewRecorder()
  369. srv.Handler.ServeHTTP(rec, httptest.NewRequest("GET", "/metrics", nil))
  370. assert.Contains(t, rec.Body.String(), "emitter_order_test 42")
  371. }
  372. // A scrape that lands the moment the listener is up already sees what emitStats set (issue #907)
  373. func TestStatsServer_Start_primesBeforeServing(t *testing.T) {
  374. defer metrics.DefaultRegistry.Unregister("prime.before.serving")
  375. port := freeTCPPort(t)
  376. l := slog.New(slog.DiscardHandler)
  377. c := config.NewC(l)
  378. setStatsConfig(c, map[string]any{
  379. "type": "prometheus",
  380. "interval": "1h",
  381. "listen": "127.0.0.1:" + port,
  382. "path": "/metrics",
  383. })
  384. gauge := metrics.GetOrRegisterGauge("prime.before.serving", nil)
  385. // The pause would let a listener that came up before the prime serve the zero value
  386. var s *statsServer
  387. ctx := stopAtCleanup(t, &s)
  388. s, err := newStatsServerFromConfig(ctx, l, c, "", false, func() {
  389. time.Sleep(200 * time.Millisecond)
  390. gauge.Update(42)
  391. })
  392. require.NoError(t, err)
  393. go s.Start()
  394. waitForListening(t, "127.0.0.1:"+port)
  395. resp, err := http.Get("http://127.0.0.1:" + port + "/metrics")
  396. require.NoError(t, err)
  397. defer resp.Body.Close()
  398. body, err := io.ReadAll(resp.Body)
  399. require.NoError(t, err)
  400. assert.Contains(t, string(body), "prime_before_serving 42")
  401. }
  402. // Start captures once before the first tick, the interval here is an hour
  403. func TestStatsServer_Start_primes(t *testing.T) {
  404. ln, err := net.Listen("tcp", "127.0.0.1:0")
  405. require.NoError(t, err)
  406. defer ln.Close()
  407. l := slog.New(slog.DiscardHandler)
  408. c := config.NewC(l)
  409. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "1h", "host": ln.Addr().String()})
  410. primed := make(chan struct{}, 1)
  411. var s *statsServer
  412. ctx := stopAtCleanup(t, &s)
  413. s, err = newStatsServerFromConfig(ctx, l, c, "", false, func() {
  414. select {
  415. case primed <- struct{}{}:
  416. default:
  417. }
  418. })
  419. require.NoError(t, err)
  420. go s.Start()
  421. select {
  422. case <-primed:
  423. case <-time.After(5 * time.Second):
  424. t.Fatal("nothing was captured before the first tick")
  425. }
  426. }
  427. // Graphite has to ship what emitStats set in the same pass, not the pass before
  428. func TestStatsServer_emitStatsRunsBeforeGraphiteExport(t *testing.T) {
  429. defer metrics.DefaultRegistry.Unregister("emitter.graphite.test")
  430. ln, err := net.Listen("tcp", "127.0.0.1:0")
  431. require.NoError(t, err)
  432. defer ln.Close()
  433. received := make(chan string, 1)
  434. go func() {
  435. conn, err := ln.Accept()
  436. if err != nil {
  437. return
  438. }
  439. defer conn.Close()
  440. b, _ := io.ReadAll(conn)
  441. received <- string(b)
  442. }()
  443. l := slog.New(slog.DiscardHandler)
  444. c := config.NewC(l)
  445. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "1h", "host": ln.Addr().String()})
  446. gauge := metrics.GetOrRegisterGauge("emitter.graphite.test", nil)
  447. s, err := newStatsServerFromConfig(t.Context(), l, c, "", false, func() { gauge.Update(42) })
  448. require.NoError(t, err)
  449. s.runMu.Lock()
  450. fns, _ := s.buildRuntime(t.Context(), *s.runCfg)
  451. s.runMu.Unlock()
  452. for _, fn := range fns {
  453. fn()
  454. }
  455. select {
  456. case body := <-received:
  457. assert.Contains(t, body, ".emitter.graphite.test.value 42 ")
  458. case <-time.After(5 * time.Second):
  459. t.Fatal("graphite sent nothing")
  460. }
  461. }
  462. // stats.interval reloads, and the runtime a reload starts still has emitStats
  463. func TestStatsServer_reloadMovesTheInterval(t *testing.T) {
  464. var passes atomic.Int64
  465. l := slog.New(slog.DiscardHandler)
  466. c := config.NewC(l)
  467. // Nothing listens here, each export fails at once
  468. host := "127.0.0.1:" + freeTCPPort(t)
  469. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "1h", "host": host})
  470. var s *statsServer
  471. ctx := stopAtCleanup(t, &s)
  472. s, err := newStatsServerFromConfig(ctx, l, c, "", false, func() { passes.Add(1) })
  473. require.NoError(t, err)
  474. go s.Start()
  475. waitFor(t, func() bool { return passes.Load() == 1 })
  476. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "20ms", "host": host})
  477. require.NoError(t, s.reload(c, false))
  478. waitFor(t, func() bool { return passes.Load() >= 6 })
  479. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "1h", "host": host})
  480. require.NoError(t, s.reload(c, false))
  481. // The reload's Stop waited out the 20ms loop. The new runtime's prime may land either side of this read, it is the
  482. // only pass left to come
  483. stopped := passes.Load()
  484. time.Sleep(300 * time.Millisecond)
  485. assert.LessOrEqual(t, passes.Load()-stopped, int64(1), "the 20ms loop kept running after the reload")
  486. }
  487. // A reload's new runtime never captures while the old one is still in a pass, go-metrics keeps its GC capture state
  488. // in unguarded globals
  489. func TestStatsServer_reloadWaitsForTheLastPass(t *testing.T) {
  490. var active, most atomic.Int64
  491. l := slog.New(slog.DiscardHandler)
  492. c := config.NewC(l)
  493. host := "127.0.0.1:" + freeTCPPort(t)
  494. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "10ms", "host": host})
  495. var s *statsServer
  496. ctx := stopAtCleanup(t, &s)
  497. s, err := newStatsServerFromConfig(ctx, l, c, "", false, func() {
  498. n := active.Add(1)
  499. for {
  500. m := most.Load()
  501. if n <= m || most.CompareAndSwap(m, n) {
  502. break
  503. }
  504. }
  505. // Long enough that a reload almost always lands mid pass
  506. time.Sleep(50 * time.Millisecond)
  507. active.Add(-1)
  508. })
  509. require.NoError(t, err)
  510. go s.Start()
  511. for i := range 6 {
  512. setStatsConfig(c, map[string]any{"type": "graphite", "interval": fmt.Sprintf("%dms", 11-i%2), "host": host})
  513. require.NoError(t, s.reload(c, false))
  514. time.Sleep(30 * time.Millisecond)
  515. }
  516. assert.Equal(t, int64(1), most.Load())
  517. }
  518. // Nothing captures once Stop returns, a pass in progress is waited out
  519. func TestStatsServer_Stop_waitsForThePass(t *testing.T) {
  520. var inPass atomic.Bool
  521. entered := make(chan struct{}, 1)
  522. l := slog.New(slog.DiscardHandler)
  523. c := config.NewC(l)
  524. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "1h", "host": "127.0.0.1:" + freeTCPPort(t)})
  525. var s *statsServer
  526. ctx := stopAtCleanup(t, &s)
  527. s, err := newStatsServerFromConfig(ctx, l, c, "", false, func() {
  528. inPass.Store(true)
  529. select {
  530. case entered <- struct{}{}:
  531. default:
  532. }
  533. time.Sleep(100 * time.Millisecond)
  534. inPass.Store(false)
  535. })
  536. require.NoError(t, err)
  537. go s.Start()
  538. select {
  539. case <-entered:
  540. case <-time.After(5 * time.Second):
  541. t.Fatal("never captured")
  542. }
  543. s.Stop()
  544. assert.False(t, inPass.Load(), "Stop returned with a pass still running")
  545. }
  546. // A Start that arrives while Stop waits out the last pass doesn't capture alongside it
  547. func TestStatsServer_Start_duringStopWaitsForThePass(t *testing.T) {
  548. var active, most atomic.Int64
  549. entered := make(chan struct{}, 1)
  550. l := slog.New(slog.DiscardHandler)
  551. c := config.NewC(l)
  552. setStatsConfig(c, map[string]any{"type": "graphite", "interval": "1h", "host": "127.0.0.1:" + freeTCPPort(t)})
  553. var s *statsServer
  554. ctx := stopAtCleanup(t, &s)
  555. s, err := newStatsServerFromConfig(ctx, l, c, "", false, func() {
  556. n := active.Add(1)
  557. for {
  558. m := most.Load()
  559. if n <= m || most.CompareAndSwap(m, n) {
  560. break
  561. }
  562. }
  563. select {
  564. case entered <- struct{}{}:
  565. default:
  566. }
  567. time.Sleep(200 * time.Millisecond)
  568. active.Add(-1)
  569. })
  570. require.NoError(t, err)
  571. go s.Start()
  572. select {
  573. case <-entered:
  574. case <-time.After(5 * time.Second):
  575. t.Fatal("never captured")
  576. }
  577. stopped := make(chan struct{})
  578. go func() {
  579. s.Stop()
  580. close(stopped)
  581. }()
  582. // Stop holds runMu for its whole wait, the next Start has to arrive after it took it
  583. waitFor(t, func() bool {
  584. if s.runMu.TryLock() {
  585. s.runMu.Unlock()
  586. return false
  587. }
  588. return true
  589. })
  590. go s.Start()
  591. select {
  592. case <-stopped:
  593. case <-time.After(5 * time.Second):
  594. t.Fatal("Stop did not return")
  595. }
  596. waitFor(t, func() bool { return len(entered) == 1 })
  597. assert.Equal(t, int64(1), most.Load(), "a Start captured alongside the pass Stop was waiting out")
  598. }