Add Set for controlling metric sets to be exported via WritePrometheus call

This commit is contained in:
Aliaksandr Valialkin 2019-06-01 23:18:41 +03:00
parent 487af0fbfc
commit ebce157dde
7 changed files with 410 additions and 195 deletions

View File

@ -17,9 +17,7 @@ import (
// //
// The returned counter is safe to use from concurrent goroutines. // The returned counter is safe to use from concurrent goroutines.
func NewCounter(name string) *Counter { func NewCounter(name string) *Counter {
c := &Counter{} return defaultSet.NewCounter(name)
registerMetric(name, c)
return c
} }
// Counter is a counter. // Counter is a counter.
@ -75,30 +73,5 @@ func (c *Counter) marshalTo(prefix string, w io.Writer) {
// //
// Performance tip: prefer NewCounter instead of GetOrCreateCounter. // Performance tip: prefer NewCounter instead of GetOrCreateCounter.
func GetOrCreateCounter(name string) *Counter { func GetOrCreateCounter(name string) *Counter {
metricsMapLock.Lock() return defaultSet.GetOrCreateCounter(name)
nm := metricsMap[name]
metricsMapLock.Unlock()
if nm == nil {
// Slow path - create and register missing counter.
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: &Counter{},
}
metricsMapLock.Lock()
nm = metricsMap[name]
if nm == nil {
nm = nmNew
metricsMap[name] = nm
metricsList = append(metricsList, nm)
}
metricsMapLock.Unlock()
}
c, ok := nm.metric.(*Counter)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Counter. It is %T", name, nm.metric))
}
return c
} }

View File

@ -19,14 +19,7 @@ import (
// //
// The returned gauge is safe to use from concurrent goroutines. // The returned gauge is safe to use from concurrent goroutines.
func NewGauge(name string, f func() float64) *Gauge { func NewGauge(name string, f func() float64) *Gauge {
if f == nil { return defaultSet.NewGauge(name, f)
panic(fmt.Errorf("BUG: f cannot be nil"))
}
g := &Gauge{
f: f,
}
registerMetric(name, g)
return g
} }
// Gauge is a float64 gauge. // Gauge is a float64 gauge.
@ -66,35 +59,5 @@ func (g *Gauge) marshalTo(prefix string, w io.Writer) {
// //
// Performance tip: prefer NewGauge instead of GetOrCreateGauge. // Performance tip: prefer NewGauge instead of GetOrCreateGauge.
func GetOrCreateGauge(name string, f func() float64) *Gauge { func GetOrCreateGauge(name string, f func() float64) *Gauge {
metricsMapLock.Lock() return defaultSet.GetOrCreateGauge(name, f)
nm := metricsMap[name]
metricsMapLock.Unlock()
if nm == nil {
// Slow path - create and register missing gauge.
if f == nil {
panic(fmt.Errorf("BUG: f cannot be nil"))
}
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: &Gauge{
f: f,
},
}
metricsMapLock.Lock()
nm = metricsMap[name]
if nm == nil {
nm = nmNew
metricsMap[name] = nm
metricsList = append(metricsList, nm)
}
metricsMapLock.Unlock()
}
g, ok := nm.metric.(*Gauge)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Gauge. It is %T", name, nm.metric))
}
return g
} }

View File

@ -16,18 +16,10 @@ import (
"fmt" "fmt"
"io" "io"
"runtime" "runtime"
"sort"
"sync"
"github.com/valyala/histogram" "github.com/valyala/histogram"
) )
var (
metricsMapLock sync.Mutex
metricsList []*namedMetric
metricsMap = make(map[string]*namedMetric)
)
type namedMetric struct { type namedMetric struct {
name string name string
metric metric metric metric
@ -37,6 +29,8 @@ type metric interface {
marshalTo(prefix string, w io.Writer) marshalTo(prefix string, w io.Writer)
} }
var defaultSet = NewSet()
// WritePrometheus writes all the registered metrics in Prometheus format to w. // WritePrometheus writes all the registered metrics in Prometheus format to w.
// //
// If exposeProcessMetrics is true, then various `go_*` metrics are exposed // If exposeProcessMetrics is true, then various `go_*` metrics are exposed
@ -49,17 +43,7 @@ type metric interface {
// }) // })
// //
func WritePrometheus(w io.Writer, exposeProcessMetrics bool) { func WritePrometheus(w io.Writer, exposeProcessMetrics bool) {
lessFunc := func(i, j int) bool { defaultSet.WritePrometheus(w)
return metricsList[i].name < metricsList[j].name
}
metricsMapLock.Lock()
if !sort.SliceIsSorted(metricsList, lessFunc) {
sort.Slice(metricsList, lessFunc)
}
for _, nm := range metricsList {
nm.metric.marshalTo(nm.name, w)
}
metricsMapLock.Unlock()
if exposeProcessMetrics { if exposeProcessMetrics {
writeProcessMetrics(w) writeProcessMetrics(w)
} }
@ -119,23 +103,3 @@ func writeProcessMetrics(w io.Writer) {
fmt.Fprintf(w, "go_info_ext{compiler=%q, GOARCH=%q, GOOS=%q, GOROOT=%q} 1\n", fmt.Fprintf(w, "go_info_ext{compiler=%q, GOARCH=%q, GOOS=%q, GOROOT=%q} 1\n",
runtime.Compiler, runtime.GOARCH, runtime.GOOS, runtime.GOROOT()) runtime.Compiler, runtime.GOARCH, runtime.GOOS, runtime.GOROOT())
} }
func registerMetric(name string, m metric) {
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
metricsMapLock.Lock()
nm, ok := metricsMap[name]
if !ok {
nm = &namedMetric{
name: name,
metric: m,
}
metricsMap[name] = nm
metricsList = append(metricsList, nm)
}
metricsMapLock.Unlock()
if ok {
panic(fmt.Errorf("BUG: metric %q is already registered", name))
}
}

308
set.go Normal file
View File

@ -0,0 +1,308 @@
package metrics
import (
"fmt"
"io"
"sort"
"sync"
"time"
)
// Set is a set of metrics.
//
// Metrics belonging to a set are exported separately from global metrics.
//
// Set.WritePrometheus must be called for exporting metrics from the set.
type Set struct {
mu sync.Mutex
a []*namedMetric
m map[string]*namedMetric
}
// NewSet creates new set of metrics.
func NewSet() *Set {
return &Set{
m: make(map[string]*namedMetric),
}
}
// WritePrometheus writes all the metrics from s to w in Prometheus format.
func (s *Set) WritePrometheus(w io.Writer) {
lessFunc := func(i, j int) bool {
return s.a[i].name < s.a[j].name
}
s.mu.Lock()
if !sort.SliceIsSorted(s.a, lessFunc) {
sort.Slice(s.a, lessFunc)
}
for _, nm := range s.a {
nm.metric.marshalTo(nm.name, w)
}
s.mu.Unlock()
}
// NewCounter registers and returns new counter with the given name in the s.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned counter is safe to use from concurrent goroutines.
func (s *Set) NewCounter(name string) *Counter {
c := &Counter{}
s.registerMetric(name, c)
return c
}
// GetOrCreateCounter returns registered counter in s with the given name
// or creates new counter if s doesn't contain counter with the given name.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned counter is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewCounter instead of GetOrCreateCounter.
func (s *Set) GetOrCreateCounter(name string) *Counter {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing counter.
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: &Counter{},
}
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
}
c, ok := nm.metric.(*Counter)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Counter. It is %T", name, nm.metric))
}
return c
}
// NewGauge registers and returns gauge with the given name in s, which calls f
// to obtain gauge value.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// f must be safe for concurrent calls.
//
// The returned gauge is safe to use from concurrent goroutines.
func (s *Set) NewGauge(name string, f func() float64) *Gauge {
if f == nil {
panic(fmt.Errorf("BUG: f cannot be nil"))
}
g := &Gauge{
f: f,
}
s.registerMetric(name, g)
return g
}
// GetOrCreateGauge returns registered gauge with the given name in s
// or creates new gauge if s doesn't contain gauge with the given name.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned gauge is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewGauge instead of GetOrCreateGauge.
func (s *Set) GetOrCreateGauge(name string, f func() float64) *Gauge {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing gauge.
if f == nil {
panic(fmt.Errorf("BUG: f cannot be nil"))
}
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: &Gauge{
f: f,
},
}
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
}
g, ok := nm.metric.(*Gauge)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Gauge. It is %T", name, nm.metric))
}
return g
}
// NewSummary creates and returns new summary with the given name in s.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
func (s *Set) NewSummary(name string) *Summary {
return s.NewSummaryExt(name, defaultSummaryWindow, defaultSummaryQuantiles)
}
// NewSummaryExt creates and returns new summary in s with the given name,
// window and quantiles.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
func (s *Set) NewSummaryExt(name string, window time.Duration, quantiles []float64) *Summary {
sm := newSummary(window, quantiles)
s.registerMetric(name, sm)
registerSummary(sm)
s.registerSummaryQuantiles(name, sm)
return sm
}
// GetOrCreateSummary returns registered summary with the given name in s
// or creates new summary if s doesn't contain summary with the given name.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewSummary instead of GetOrCreateSummary.
func (s *Set) GetOrCreateSummary(name string) *Summary {
return s.GetOrCreateSummaryExt(name, defaultSummaryWindow, defaultSummaryQuantiles)
}
// GetOrCreateSummaryExt returns registered summary with the given name,
// window and quantiles in s or creates new summary if s doesn't
// contain summary with the given name.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewSummaryExt instead of GetOrCreateSummaryExt.
func (s *Set) GetOrCreateSummaryExt(name string, window time.Duration, quantiles []float64) *Summary {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing summary.
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
sm := newSummary(window, quantiles)
nmNew := &namedMetric{
name: name,
metric: sm,
}
mustRegisterQuantiles := false
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
registerSummary(sm)
mustRegisterQuantiles = true
}
s.mu.Unlock()
if mustRegisterQuantiles {
s.registerSummaryQuantiles(name, sm)
}
}
sm, ok := nm.metric.(*Summary)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Summary. It is %T", name, nm.metric))
}
if sm.window != window {
panic(fmt.Errorf("BUG: invalid window requested for the summary %q; requested %s; need %s", name, window, sm.window))
}
if !isEqualQuantiles(sm.quantiles, quantiles) {
panic(fmt.Errorf("BUG: invalid quantiles requested from the summary %q; requested %v; need %v", name, quantiles, sm.quantiles))
}
return sm
}
func (s *Set) registerSummaryQuantiles(name string, sm *Summary) {
for i, q := range sm.quantiles {
quantileValueName := addTag(name, fmt.Sprintf(`quantile="%g"`, q))
qv := &quantileValue{
sm: sm,
idx: i,
}
s.registerMetric(quantileValueName, qv)
}
}
func (s *Set) registerMetric(name string, m metric) {
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
s.mu.Lock()
nm, ok := s.m[name]
if !ok {
nm = &namedMetric{
name: name,
metric: m,
}
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
if ok {
panic(fmt.Errorf("BUG: metric %q is already registered", name))
}
}

28
set_example_test.go Normal file
View File

@ -0,0 +1,28 @@
package metrics_test
import (
"bytes"
"fmt"
"github.com/VictoriaMetrics/metrics"
)
func ExampleSet() {
// Create a set with a counter
s := metrics.NewSet()
sc := s.NewCounter("set_counter")
sc.Inc()
s.NewGauge(`set_gauge{foo="bar"}`, func() float64 { return 42 })
// Dump global metrics
var bb bytes.Buffer
// Dump metrics from s.
bb.Reset()
s.WritePrometheus(&bb)
fmt.Printf("set metrics:\n%s\n", bb.String())
// Output:
// set metrics:
// set_counter 1
// set_gauge{foo="bar"} 42
}

32
set_test.go Normal file
View File

@ -0,0 +1,32 @@
package metrics
import (
"fmt"
"testing"
)
func TestNewSet(t *testing.T) {
var ss []*Set
for i := 0; i < 10; i++ {
s := NewSet()
ss = append(ss, s)
}
for i := 0; i < 10; i++ {
s := ss[i]
for j := 0; j < 10; j++ {
c := s.NewCounter(fmt.Sprintf("counter_%d", j))
c.Inc()
if n := c.Get(); n != 1 {
t.Fatalf("unexpected counter value; got %d; want %d", n, 1)
}
g := s.NewGauge(fmt.Sprintf("gauge_%d", j), func() float64 { return 123 })
if v := g.Get(); v != 123 {
t.Fatalf("unexpected gauge value; got %v; want %v", v, 123)
}
sm := s.NewSummary(fmt.Sprintf("summary_%d", j))
if sm == nil {
t.Fatalf("NewSummary returned nil")
}
}
}
}

View File

@ -38,7 +38,7 @@ type Summary struct {
// //
// The returned summary is safe to use from concurrent goroutines. // The returned summary is safe to use from concurrent goroutines.
func NewSummary(name string) *Summary { func NewSummary(name string) *Summary {
return NewSummaryExt(name, defaultSummaryWindow, defaultSummaryQuantiles) return defaultSet.NewSummary(name)
} }
// NewSummaryExt creates and returns new summary with the given name, // NewSummaryExt creates and returns new summary with the given name,
@ -53,36 +53,21 @@ func NewSummary(name string) *Summary {
// //
// The returned summary is safe to use from concurrent goroutines. // The returned summary is safe to use from concurrent goroutines.
func NewSummaryExt(name string, window time.Duration, quantiles []float64) *Summary { func NewSummaryExt(name string, window time.Duration, quantiles []float64) *Summary {
s := newSummary(window, quantiles) return defaultSet.NewSummaryExt(name, window, quantiles)
registerMetric(name, s)
registerSummary(s)
registerSummaryQuantiles(name, s)
return s
} }
func newSummary(window time.Duration, quantiles []float64) *Summary { func newSummary(window time.Duration, quantiles []float64) *Summary {
// Make a copy of quantiles in order to prevent from their modification by the caller. // Make a copy of quantiles in order to prevent from their modification by the caller.
quantiles = append([]float64{}, quantiles...) quantiles = append([]float64{}, quantiles...)
validateQuantiles(quantiles) validateQuantiles(quantiles)
s := &Summary{ sm := &Summary{
curr: histogram.NewFast(), curr: histogram.NewFast(),
next: histogram.NewFast(), next: histogram.NewFast(),
quantiles: quantiles, quantiles: quantiles,
quantileValues: make([]float64, len(quantiles)), quantileValues: make([]float64, len(quantiles)),
window: window, window: window,
} }
return s return sm
}
func registerSummaryQuantiles(name string, s *Summary) {
for i, q := range s.quantiles {
quantileValueName := addTag(name, fmt.Sprintf(`quantile="%g"`, q))
qv := &quantileValue{
s: s,
idx: i,
}
registerMetric(quantileValueName, qv)
}
} }
func validateQuantiles(quantiles []float64) { func validateQuantiles(quantiles []float64) {
@ -94,29 +79,29 @@ func validateQuantiles(quantiles []float64) {
} }
// Update updates the summary. // Update updates the summary.
func (s *Summary) Update(v float64) { func (sm *Summary) Update(v float64) {
s.mu.Lock() sm.mu.Lock()
s.curr.Update(v) sm.curr.Update(v)
s.next.Update(v) sm.next.Update(v)
s.mu.Unlock() sm.mu.Unlock()
} }
// UpdateDuration updates request duration based on the given startTime. // UpdateDuration updates request duration based on the given startTime.
func (s *Summary) UpdateDuration(startTime time.Time) { func (sm *Summary) UpdateDuration(startTime time.Time) {
d := time.Since(startTime).Seconds() d := time.Since(startTime).Seconds()
s.Update(d) sm.Update(d)
} }
func (s *Summary) marshalTo(prefix string, w io.Writer) { func (sm *Summary) marshalTo(prefix string, w io.Writer) {
// Just update s.quantileValues and don't write anything to w. // Just update sm.quantileValues and don't write anything to w.
// s.quantileValues will be marshaled later via quantileValue.marshalTo. // sm.quantileValues will be marshaled later via quantileValue.marshalTo.
s.updateQuantiles() sm.updateQuantiles()
} }
func (s *Summary) updateQuantiles() { func (sm *Summary) updateQuantiles() {
s.mu.Lock() sm.mu.Lock()
s.quantileValues = s.curr.Quantiles(s.quantileValues[:0], s.quantiles) sm.quantileValues = sm.curr.Quantiles(sm.quantileValues[:0], sm.quantiles)
s.mu.Unlock() sm.mu.Unlock()
} }
// GetOrCreateSummary returns registered summary with the given name // GetOrCreateSummary returns registered summary with the given name
@ -134,7 +119,7 @@ func (s *Summary) updateQuantiles() {
// //
// Performance tip: prefer NewSummary instead of GetOrCreateSummary. // Performance tip: prefer NewSummary instead of GetOrCreateSummary.
func GetOrCreateSummary(name string) *Summary { func GetOrCreateSummary(name string) *Summary {
return GetOrCreateSummaryExt(name, defaultSummaryWindow, defaultSummaryQuantiles) return defaultSet.GetOrCreateSummary(name)
} }
// GetOrCreateSummaryExt returns registered summary with the given name, // GetOrCreateSummaryExt returns registered summary with the given name,
@ -152,45 +137,7 @@ func GetOrCreateSummary(name string) *Summary {
// //
// Performance tip: prefer NewSummaryExt instead of GetOrCreateSummaryExt. // Performance tip: prefer NewSummaryExt instead of GetOrCreateSummaryExt.
func GetOrCreateSummaryExt(name string, window time.Duration, quantiles []float64) *Summary { func GetOrCreateSummaryExt(name string, window time.Duration, quantiles []float64) *Summary {
metricsMapLock.Lock() return defaultSet.GetOrCreateSummaryExt(name, window, quantiles)
nm := metricsMap[name]
metricsMapLock.Unlock()
if nm == nil {
// Slow path - create and register missing summary.
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
s := newSummary(window, quantiles)
nmNew := &namedMetric{
name: name,
metric: s,
}
mustRegisterQuantiles := false
metricsMapLock.Lock()
nm = metricsMap[name]
if nm == nil {
nm = nmNew
metricsMap[name] = nm
metricsList = append(metricsList, nm)
registerSummary(s)
mustRegisterQuantiles = true
}
metricsMapLock.Unlock()
if mustRegisterQuantiles {
registerSummaryQuantiles(name, s)
}
}
s, ok := nm.metric.(*Summary)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Summary. It is %T", name, nm.metric))
}
if s.window != window {
panic(fmt.Errorf("BUG: invalid window requested for the summary %q; requested %s; need %s", name, window, s.window))
}
if !isEqualQuantiles(s.quantiles, quantiles) {
panic(fmt.Errorf("BUG: invalid quantiles requested from the summary %q; requested %v; need %v", name, quantiles, s.quantiles))
}
return s
} }
func isEqualQuantiles(a, b []float64) bool { func isEqualQuantiles(a, b []float64) bool {
@ -207,14 +154,14 @@ func isEqualQuantiles(a, b []float64) bool {
} }
type quantileValue struct { type quantileValue struct {
s *Summary sm *Summary
idx int idx int
} }
func (qv *quantileValue) marshalTo(prefix string, w io.Writer) { func (qv *quantileValue) marshalTo(prefix string, w io.Writer) {
qv.s.mu.Lock() qv.sm.mu.Lock()
v := qv.s.quantileValues[qv.idx] v := qv.sm.quantileValues[qv.idx]
qv.s.mu.Unlock() qv.sm.mu.Unlock()
if !math.IsNaN(v) { if !math.IsNaN(v) {
fmt.Fprintf(w, "%s %g\n", prefix, v) fmt.Fprintf(w, "%s %g\n", prefix, v)
} }
@ -227,10 +174,10 @@ func addTag(name, tag string) string {
return fmt.Sprintf("%s,%s}", name[:len(name)-1], tag) return fmt.Sprintf("%s,%s}", name[:len(name)-1], tag)
} }
func registerSummary(s *Summary) { func registerSummary(sm *Summary) {
window := s.window window := sm.window
summariesLock.Lock() summariesLock.Lock()
summaries[window] = append(summaries[window], s) summaries[window] = append(summaries[window], sm)
if len(summaries[window]) == 1 { if len(summaries[window]) == 1 {
go summariesSwapCron(window) go summariesSwapCron(window)
} }
@ -241,13 +188,13 @@ func summariesSwapCron(window time.Duration) {
for { for {
time.Sleep(window / 2) time.Sleep(window / 2)
summariesLock.Lock() summariesLock.Lock()
for _, s := range summaries[window] { for _, sm := range summaries[window] {
s.mu.Lock() sm.mu.Lock()
tmp := s.curr tmp := sm.curr
s.curr = s.next sm.curr = sm.next
s.next = tmp sm.next = tmp
s.next.Reset() sm.next.Reset()
s.mu.Unlock() sm.mu.Unlock()
} }
summariesLock.Unlock() summariesLock.Unlock()
} }