Files
windows_exporter/pkg/collector/state.go
T
2026-10-08 14:25:17 +02:00

243 lines
5.7 KiB
Go

// 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.
//go:build windows
package collector
import (
"context"
"errors"
"fmt"
"log/slog"
"runtime/debug"
"sync"
"github.com/prometheus-community/windows_exporter/internal/mi"
)
var (
errCollectorBusy = errors.New("collector is still running")
errCollectionClosed = errors.New("collection is closed")
)
// collectionState is the state of a Collection that its filtered views share.
type collectionState struct {
mu sync.Mutex
collectors map[string]*collectorState
miApp *mi.Application
miSession *mi.Session
// closeMu serializes Close calls.
closeMu sync.Mutex
}
func newCollectionState() *collectionState {
return &collectionState{collectors: make(map[string]*collectorState)}
}
// collector returns the state of the named collector, creating it on first use.
func (s *collectionState) collector(name string) *collectorState {
s.mu.Lock()
defer s.mu.Unlock()
state, ok := s.collectors[name]
if !ok {
state = &collectorState{slot: make(chan struct{}, 1)}
s.collectors[name] = state
}
return state
}
func (s *collectionState) session() *mi.Session {
s.mu.Lock()
defer s.mu.Unlock()
return s.miSession
}
func (s *collectionState) setMI(app *mi.Application, session *mi.Session) {
s.mu.Lock()
defer s.mu.Unlock()
s.miApp = app
s.miSession = session
}
// closeMI closes the MI session and application. Later calls do nothing.
func (s *collectionState) closeMI() []error {
s.mu.Lock()
app, session := s.miApp, s.miSession
s.miApp, s.miSession = nil, nil
s.mu.Unlock()
var errs []error
if session != nil {
if err := session.Close(); err != nil && !errors.Is(err, mi.ErrNotInitialized) {
errs = append(errs, fmt.Errorf("error from close MI session: %w", err))
}
}
if app != nil {
if err := app.Close(); err != nil && !errors.Is(err, mi.ErrNotInitialized) {
errs = append(errs, fmt.Errorf("error from close MI application: %w", err))
}
}
return errs
}
type startResult int
const (
started startResult = iota
busy
notAvailable
)
// collectorState tracks the lifecycle of one collector instance.
type collectorState struct {
// slot is held while Build, Collect or Close of the collector runs.
// A Collect call that outlives its scrape keeps the slot until it returns,
// so no scrape starts a second call on the same instance.
slot chan struct{}
mu sync.Mutex
// buildErr is the error of the last Build. A collector that failed to build is not collected.
buildErr error
// built is true once Build was called, so Close only closes collectors that may hold resources.
built bool
// closing is true once Close was called. No Build or Collect starts afterwards.
closing bool
// closed is true once Collector.Close was called.
closed bool
// busyLogged is true once a scrape logged that it skipped the current run.
busyLogged bool
}
// tryCollect acquires the slot for a Collect call without waiting.
func (s *collectorState) tryCollect() startResult {
s.mu.Lock()
defer s.mu.Unlock()
if s.closing || s.buildErr != nil {
return notAvailable
}
select {
case s.slot <- struct{}{}:
return started
default:
return busy
}
}
// release releases the slot acquired by tryCollect.
func (s *collectorState) release() {
s.mu.Lock()
s.busyLogged = false
s.mu.Unlock()
<-s.slot
}
// logBusy reports whether a skipped scrape is the first one of the current run.
func (s *collectorState) logBusy() bool {
s.mu.Lock()
defer s.mu.Unlock()
first := !s.busyLogged
s.busyLogged = true
return first
}
// build runs Collector.Build while it holds the slot and records the result.
// A panic in Collector.Build is returned as an error.
func (s *collectorState) build(ctx context.Context, logger *slog.Logger, miSession *mi.Session, collector Collector) (err error) {
select {
case s.slot <- struct{}{}:
case <-ctx.Done():
err = context.Cause(ctx)
s.mu.Lock()
s.buildErr = err
s.mu.Unlock()
return err
}
defer func() {
s.mu.Lock()
s.buildErr = err
s.mu.Unlock()
<-s.slot
}()
s.mu.Lock()
closing := s.closing
s.built = !closing
s.mu.Unlock()
if closing {
return errCollectionClosed
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v. stack: %s", r, string(debug.Stack()))
}
}()
return collector.Build(logger, miSession)
}
// close calls Collector.Close once the collector is idle. It waits until ctx is done for a running
// Build or Collect call to return. If the call does not return in time, the collector is left open,
// because closing it would release resources that are still in use.
// After close, the collector is never built or collected again.
func (s *collectorState) close(ctx context.Context, collector Collector) (err error) {
s.mu.Lock()
s.closing = true
done := s.closed || !s.built
s.mu.Unlock()
if done {
return nil
}
select {
case s.slot <- struct{}{}:
// The slot is never released, so no call starts after Close.
case <-ctx.Done():
return errCollectorBusy
}
s.mu.Lock()
s.closed = true
s.mu.Unlock()
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v. stack: %s", r, string(debug.Stack()))
}
}()
return collector.Close()
}