mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-29 06:59:05 +01:00
scheduled_task: reuse metrics of unchanged tasks between scrapes (#2637)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
99c895a761
commit
18d7cc55d9
@@ -0,0 +1,112 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package metriccache reuses const metrics across scrapes for objects whose
|
||||
// values rarely change.
|
||||
//
|
||||
// Building a const metric allocates its label pairs and value, which dominates
|
||||
// the allocations of collectors that publish many state-set series per object.
|
||||
// Const metrics are immutable and a registry only reads them during Gather, so
|
||||
// sending the same metric again in a later scrape is safe.
|
||||
package metriccache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Cache holds the metrics built for each key during the last committed scrape,
|
||||
// together with the input value they were built from.
|
||||
//
|
||||
// The zero value is ready to use. A Cache may be used by concurrent scrapes:
|
||||
// each scrape reads the last committed generation, which is never modified,
|
||||
// and the last scrape to commit wins.
|
||||
type Cache[K, V comparable] struct {
|
||||
mu sync.Mutex
|
||||
entries map[K]entry[V]
|
||||
// resets counts Reset calls, so that a scrape begun before a Reset
|
||||
// doesn't commit afterwards.
|
||||
resets uint64
|
||||
}
|
||||
|
||||
type entry[V comparable] struct {
|
||||
value V
|
||||
metrics []prometheus.Metric
|
||||
}
|
||||
|
||||
// Scrape collects the entries of one scrape. It must not be shared between
|
||||
// goroutines.
|
||||
type Scrape[K, V comparable] struct {
|
||||
cache *Cache[K, V]
|
||||
resets uint64
|
||||
prev map[K]entry[V]
|
||||
next map[K]entry[V]
|
||||
}
|
||||
|
||||
// Begin starts a scrape based on the last committed generation.
|
||||
func (c *Cache[K, V]) Begin() Scrape[K, V] {
|
||||
c.mu.Lock()
|
||||
prev, resets := c.entries, c.resets
|
||||
c.mu.Unlock()
|
||||
|
||||
return Scrape[K, V]{
|
||||
cache: c,
|
||||
resets: resets,
|
||||
prev: prev,
|
||||
next: make(map[K]entry[V], len(prev)),
|
||||
}
|
||||
}
|
||||
|
||||
// Reset drops all cached metrics, for example after their descriptors changed.
|
||||
// Scrapes begun before Reset don't commit their entries.
|
||||
func (c *Cache[K, V]) Reset() {
|
||||
c.mu.Lock()
|
||||
c.entries = nil
|
||||
c.resets++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Load returns the metrics of the last scrape for key if they were built from
|
||||
// an equal value, and keeps them for the next scrape. The caller must not
|
||||
// modify the returned slice.
|
||||
func (s *Scrape[K, V]) Load(key K, value V) ([]prometheus.Metric, bool) {
|
||||
e, ok := s.prev[key]
|
||||
if !ok || e.value != value {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
s.next[key] = e
|
||||
|
||||
return e.metrics, true
|
||||
}
|
||||
|
||||
// Store keeps metrics built from value for key. The caller must not modify
|
||||
// metrics afterwards.
|
||||
func (s *Scrape[K, V]) Store(key K, value V, metrics []prometheus.Metric) {
|
||||
s.next[key] = entry[V]{value: value, metrics: metrics}
|
||||
}
|
||||
|
||||
// Commit replaces the cached entries with those loaded or stored in this
|
||||
// scrape, so keys that were not seen are dropped. It does nothing if the cache
|
||||
// was reset after the scrape began, for example by a Close or Build that ran
|
||||
// while a timed-out scrape was still collecting.
|
||||
func (s *Scrape[K, V]) Commit() {
|
||||
s.cache.mu.Lock()
|
||||
if s.cache.resets == s.resets {
|
||||
s.cache.entries = s.next
|
||||
}
|
||||
s.cache.mu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package metriccache_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/metriccache"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var desc = prometheus.NewDesc("test_state", "test", []string{"name", "state"}, nil)
|
||||
|
||||
func build(name, state string) []prometheus.Metric {
|
||||
return []prometheus.Metric{
|
||||
prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, 1, name, state),
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var cache metriccache.Cache[string, string]
|
||||
|
||||
scrape := cache.Begin()
|
||||
_, ok := scrape.Load("a", "running")
|
||||
require.False(t, ok, "empty cache")
|
||||
|
||||
a := build("a", "running")
|
||||
scrape.Store("a", "running", a)
|
||||
scrape.Store("b", "stopped", build("b", "stopped"))
|
||||
|
||||
_, ok = scrape.Load("a", "running")
|
||||
require.False(t, ok, "entries are visible after Commit")
|
||||
|
||||
scrape.Commit()
|
||||
|
||||
scrape = cache.Begin()
|
||||
metrics, ok := scrape.Load("a", "running")
|
||||
require.True(t, ok)
|
||||
require.Same(t, a[0], metrics[0])
|
||||
|
||||
_, ok = scrape.Load("a", "stopped")
|
||||
require.False(t, ok, "changed value")
|
||||
|
||||
// b is neither loaded nor stored in this scrape.
|
||||
scrape.Commit()
|
||||
|
||||
scrape = cache.Begin()
|
||||
_, ok = scrape.Load("b", "stopped")
|
||||
require.False(t, ok, "keys not seen in the last scrape are dropped")
|
||||
|
||||
metrics, ok = scrape.Load("a", "running")
|
||||
require.True(t, ok, "loaded keys are kept")
|
||||
require.Same(t, a[0], metrics[0])
|
||||
scrape.Commit()
|
||||
|
||||
cache.Reset()
|
||||
|
||||
scrape = cache.Begin()
|
||||
_, ok = scrape.Load("a", "running")
|
||||
require.False(t, ok, "reset")
|
||||
}
|
||||
|
||||
func TestCacheResetDuringScrape(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var cache metriccache.Cache[string, string]
|
||||
|
||||
// A scrape that is still running when the collector is closed or rebuilt
|
||||
// must not put its metrics back into the cache.
|
||||
scrape := cache.Begin()
|
||||
scrape.Store("a", "running", build("a", "running"))
|
||||
cache.Reset()
|
||||
scrape.Commit()
|
||||
|
||||
scrape = cache.Begin()
|
||||
_, ok := scrape.Load("a", "running")
|
||||
require.False(t, ok)
|
||||
|
||||
// Scrapes begun after the Reset commit as usual.
|
||||
scrape.Store("a", "running", build("a", "running"))
|
||||
scrape.Commit()
|
||||
|
||||
scrape = cache.Begin()
|
||||
_, ok = scrape.Load("a", "running")
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
func TestCacheConcurrentScrapes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
cache metriccache.Cache[string, int]
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
for i := range 8 {
|
||||
wg.Go(func() {
|
||||
for j := range 100 {
|
||||
scrape := cache.Begin()
|
||||
|
||||
for k := range 10 {
|
||||
name := strconv.Itoa(k)
|
||||
value := (i + j) % 3
|
||||
|
||||
metrics, ok := scrape.Load(name, value)
|
||||
if !ok {
|
||||
metrics = build(name, strconv.Itoa(value))
|
||||
scrape.Store(name, value, metrics)
|
||||
}
|
||||
|
||||
// require must not be called outside the test goroutine.
|
||||
assert.Len(t, metrics, 1)
|
||||
}
|
||||
|
||||
scrape.Commit()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkScrape(b *testing.B) {
|
||||
var cache metriccache.Cache[string, int]
|
||||
|
||||
names := make([]string, 1000)
|
||||
for i := range names {
|
||||
names[i] = strconv.Itoa(i)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
scrape := cache.Begin()
|
||||
|
||||
for _, name := range names {
|
||||
if _, ok := scrape.Load(name, 0); !ok {
|
||||
scrape.Store(name, 0, build(name, "0"))
|
||||
}
|
||||
}
|
||||
|
||||
scrape.Commit()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user