Skip to content

Commit

Permalink
metrics: fix goroutine leaks in tests
Browse files Browse the repository at this point in the history
* adds Close() method to the Metrics interface
* implements CodaHale metrics stats collector goroutines cleanup
* fixes goroutine leaks in test

Signed-off-by: Alexander Yastrebov <[email protected]>
  • Loading branch information
AlexanderYastrebov committed Aug 7, 2023
1 parent 13ccac4 commit ec1fc49
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 4 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.16
v0.17
5 changes: 5 additions & 0 deletions metrics/all_kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ func (a *All) IncErrorsStreaming(routeId string) {
a.codaHale.IncErrorsStreaming(routeId)

}

func (a *All) Close() {
a.codaHale.Close()
}

func (a *All) RegisterHandler(path string, handler *http.ServeMux) {
a.prometheusHandler = a.prometheus.getHandler()
a.codaHaleHandler = a.codaHale.getHandler(path)
Expand Down
24 changes: 22 additions & 2 deletions metrics/codahale.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,16 @@ type CodaHale struct {
createGauge func() metrics.GaugeFloat64
options Options
handler http.Handler
quit chan struct{}
}

// NewCodaHale returns a new CodaHale backend of metrics.
func NewCodaHale(o Options) *CodaHale {
o = applyCompatibilityDefaults(o)

c := &CodaHale{}

c.quit = make(chan struct{})
c.reg = metrics.NewRegistry()

var createSample func() metrics.Sample
Expand All @@ -68,12 +71,12 @@ func NewCodaHale(o Options) *CodaHale {

if o.EnableDebugGcMetrics {
metrics.RegisterDebugGCStats(c.reg)
go metrics.CaptureDebugGCStats(c.reg, statsRefreshDuration)
go c.collectStats(metrics.CaptureDebugGCStatsOnce)
}

if o.EnableRuntimeMetrics {
metrics.RegisterRuntimeMemStats(c.reg)
go metrics.CaptureRuntimeMemStats(c.reg, statsRefreshDuration)
go c.collectStats(metrics.CaptureRuntimeMemStatsOnce)
}

return c
Expand Down Expand Up @@ -234,6 +237,23 @@ func (c *CodaHale) IncErrorsStreaming(routeId string) {
}
}

func (c *CodaHale) Close() {
close(c.quit)
}

func (c *CodaHale) collectStats(capture func(r metrics.Registry)) {
ticker := time.NewTicker(statsRefreshDuration)
defer ticker.Stop()
for {
select {
case <-ticker.C:
capture(c.reg)
case <-c.quit:
return
}
}
}

func (c *CodaHale) RegisterHandler(path string, handler *http.ServeMux) {
h := c.getHandler(path)
handler.Handle(path, h)
Expand Down
8 changes: 8 additions & 0 deletions metrics/codahale_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestUseVoidByDefaultOptions(t *testing.T) {
func TestDefaultOptionsWithListener(t *testing.T) {
o := Options{}
c := NewCodaHale(o)
defer c.Close()

if c == Void {
t.Error("Options containing a listener should create a registry")
Expand All @@ -53,6 +54,7 @@ func TestDefaultOptionsWithListener(t *testing.T) {
func TestCodaHaleDebugGcStats(t *testing.T) {
o := Options{EnableDebugGcMetrics: true}
c := NewCodaHale(o)
defer c.Close()

if c.reg.Get("debug.GCStats.LastGC") == nil {
t.Error("Options enabled debug gc stats but failed to find the key 'debug.GCStats.LastGC'")
Expand All @@ -62,6 +64,7 @@ func TestCodaHaleDebugGcStats(t *testing.T) {
func TestCodaHaleRuntimeStats(t *testing.T) {
o := Options{EnableRuntimeMetrics: true}
c := NewCodaHale(o)
defer c.Close()

if c.reg.Get("runtime.MemStats.Alloc") == nil {
t.Error("Options enabled runtime stats but failed to find the key 'runtime.MemStats.Alloc'")
Expand All @@ -71,6 +74,7 @@ func TestCodaHaleRuntimeStats(t *testing.T) {
func TestCodaHaleMeasurement(t *testing.T) {
o := Options{}
c := NewCodaHale(o)
defer c.Close()

g1 := c.getGauge("TestGauge")
c.UpdateGauge("TestGauge", 1)
Expand Down Expand Up @@ -172,6 +176,8 @@ func TestCodaHaleProxyMetrics(t *testing.T) {
for _, pmt := range proxyMetricsTests {
t.Run(pmt.metricsKey, func(t *testing.T) {
m := NewCodaHale(Options{})
defer m.Close()

pmt.measureFunc(m)

time.Sleep(20 * time.Millisecond)
Expand Down Expand Up @@ -496,6 +502,8 @@ func TestCodaHaleServeMetrics(t *testing.T) {
}

m := NewCodaHale(ti.options)
defer m.Close()

for _, mi := range ti.measures {
m.MeasureServe(mi.route, mi.host, mi.method, mi.status, time.Now().Add(-mi.duration))
}
Expand Down
18 changes: 18 additions & 0 deletions metrics/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package metrics

import (
"os"
"testing"

"github.com/AlexanderYastrebov/noleak"
codahale "github.com/rcrowley/go-metrics"
)

func TestMain(m *testing.M) {
// codahale library has a global arbiter goroutine to update timers and there is no way to stop it,
// see https://github.com/rcrowley/go-metrics/pull/46
// Create a dummy timer to start the arbiter goroutine before running tests such that leak detector does not report it.
_ = codahale.NewTimer()

os.Exit(noleak.CheckMain(m))
}
1 change: 1 addition & 0 deletions metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type Metrics interface {
IncErrorsStreaming(routeId string)
RegisterHandler(path string, handler *http.ServeMux)
UpdateGauge(key string, value float64)
Close()
}

// Options for initializing metrics collection.
Expand Down
15 changes: 14 additions & 1 deletion metrics/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ func TestHandlerCodaHaleBadRequests(t *testing.T) {
Format: metrics.CodaHaleKind,
EnableRuntimeMetrics: true,
}
mh := metrics.NewDefaultHandler(o)
m := metrics.NewMetrics(o)
defer m.Close()

mh := metrics.NewHandler(o, m)

r1, _ := http.NewRequest("GET", "/", nil)
rw1 := httptest.NewRecorder()
Expand All @@ -76,6 +79,8 @@ func TestHandlerCodaHaleAllMetricsRequest(t *testing.T) {
EnableRuntimeMetrics: true,
}
m := metrics.NewCodaHale(o)
defer m.Close()

mh := metrics.NewHandler(o, m)
m.IncCounter("TestHandlerCodaHaleAllMetricsRequest")

Expand Down Expand Up @@ -103,6 +108,8 @@ func TestHandlerCodaHaleSingleMetricsRequest(t *testing.T) {
EnableRuntimeMetrics: true,
}
m := metrics.NewCodaHale(o)
defer m.Close()

mh := metrics.NewHandler(o, m)
m.IncCounter("TestHandlerCodaHaleSingleMetricsRequest")

Expand Down Expand Up @@ -134,6 +141,8 @@ func TestHandlerCodaHaleSingleMetricsRequestWhenUsingPrefix(t *testing.T) {
EnableRuntimeMetrics: true,
}
m := metrics.NewCodaHale(o)
defer m.Close()

mh := metrics.NewHandler(o, m)
m.IncCounter("TestHandlerCodaHaleSingleMetricsRequestWhenUsingPrefix")

Expand Down Expand Up @@ -164,6 +173,8 @@ func TestHandlerCodaHaleMetricsRequestWithPattern(t *testing.T) {
EnableRuntimeMetrics: true,
}
m := metrics.NewCodaHale(o)
defer m.Close()

mh := metrics.NewHandler(o, m)
m.UpdateGauge("runtime.Num", 5.0)

Expand Down Expand Up @@ -202,6 +213,8 @@ func TestHandlerCodaHaleUnknownMetricRequest(t *testing.T) {
EnableRuntimeMetrics: true,
}
m := metrics.NewCodaHale(o)
defer m.Close()

mh := metrics.NewHandler(o, m)

r, _ := http.NewRequest("GET", "/metrics/DOES-NOT-EXIST", nil)
Expand Down
2 changes: 2 additions & 0 deletions metrics/metricstest/metricsmock.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,5 @@ func (m *MockMetrics) Gauge(key string) (v float64, ok bool) {

return
}

func (m *MockMetrics) Close() {}
2 changes: 2 additions & 0 deletions metrics/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,5 @@ func (p *Prometheus) MeasureBackend5xx(start time.Time) {
func (p *Prometheus) IncErrorsStreaming(routeID string) {
p.proxyStreamingErrorsM.WithLabelValues(routeID).Inc()
}

func (p *Prometheus) Close() {}

0 comments on commit ec1fc49

Please sign in to comment.