collector: bound MI probe queries in Build with a timeout (#2657)

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:
Jan-Otto Kröpke
2026-10-10 18:05:15 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 1302cf6899
commit 465fccbe69
9 changed files with 45 additions and 5 deletions
+2
View File
@@ -95,6 +95,8 @@ exercised a live Windows role.
the original `Collection` owns collectors and MI resources. A timed-out call
may still be running: prevent overlap, drain late metrics and do not free its
resources while it runs. Honor the supplied scrape budget in blocking APIs.
`Build` has no budget: bound its MI probe queries with `mi.BuildQueryTimeout`,
never `0` (no MI timeout), so one stalled provider cannot block startup.
- Release resources after partial initialization and on error paths. A non-nil
PDH collector returned with an error still belongs to the caller and must be
closed. Sub-collector cleanup closures must read the initialized field when
+1 -1
View File
@@ -322,7 +322,7 @@ func (c *Collector) buildErrorStatsCollector(miSession *mi.Session) error {
c.miQuery = query
var stats []Statistic
if err := c.miSession.Query(&stats, mi.NamespaceRootMicrosoftDNS, c.miQuery, 0); err != nil {
if err := c.miSession.Query(&stats, mi.NamespaceRootMicrosoftDNS, c.miQuery, mi.BuildQueryTimeout); err != nil {
return fmt.Errorf("failed to query DNS statistics: %w", err)
}
+1 -1
View File
@@ -94,7 +94,7 @@ func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error {
c.buildDescriptors()
var dst []msftFSRMQuota
if err := c.miSession.Query(&dst, mi.NamespaceRootWindowsFSRM, c.miQuery, 0); err != nil {
if err := c.miSession.Query(&dst, mi.NamespaceRootWindowsFSRM, c.miQuery, mi.BuildQueryTimeout); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
}
+1 -1
View File
@@ -322,7 +322,7 @@ func (c *Collector) buildWorkerProcessWMI(miSession *mi.Session) error {
var workerProcesses []WorkerProcess
if err = miSession.Query(&workerProcesses, mi.NamespaceRootWebAdministration, miQuery, 0); err != nil {
if err = miSession.Query(&workerProcesses, mi.NamespaceRootWebAdministration, miQuery, mi.BuildQueryTimeout); err != nil {
c.logger.LogAttrs(context.Background(), slog.LevelDebug,
`root\WebAdministration isn't available, reading IIS application pool names from the w3wp command line only`,
slog.Any("err", err),
@@ -107,7 +107,7 @@ func (c *Collector) buildStoragePool() error {
var dst []msftStoragePool
if err := c.miSession.Query(&dst, mi.NamespaceRootStorage, c.storagePoolMIQuery, 0); err != nil {
if err := c.miSession.Query(&dst, mi.NamespaceRootStorage, c.storagePoolMIQuery, mi.BuildQueryTimeout); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
}
@@ -104,7 +104,7 @@ func (c *Collector) buildVirtualDisk() error {
var dst []msftVirtualDisk
if err := c.miSession.Query(&dst, mi.NamespaceRootStorage, c.virtualDiskMIQuery, 0); err != nil {
if err := c.miSession.Query(&dst, mi.NamespaceRootStorage, c.virtualDiskMIQuery, mi.BuildQueryTimeout); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
}
+29
View File
@@ -772,6 +772,35 @@ func Test_MI_QueryTimeout(t *testing.T) {
require.NoError(t, err)
}
// Test_MI_Session_QueryTimeout checks that the timeout passed to Session.Query,
// e.g. mi.BuildQueryTimeout, reaches MI and surfaces as the native result.
func Test_MI_Session_QueryTimeout(t *testing.T) {
application, err := mi.ApplicationInitialize()
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, application.Close()) })
destinationOptions, err := application.NewDestinationOptions()
require.NoError(t, err)
require.NoError(t, destinationOptions.SetLocale(mi.LocaleEnglish))
session, err := application.NewSession(destinationOptions)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, session.Close()) })
query, err := mi.NewQuery("select * from win32_process")
require.NoError(t, err)
var processes []win32Process
err = session.Query(&processes, mi.NamespaceRootCIMv2, query, time.Millisecond)
require.ErrorIs(t, err, mi.MI_RESULT_INVALID_OPERATION_TIMEOUT)
processes = nil
require.NoError(t, session.Query(&processes, mi.NamespaceRootCIMv2, query, mi.BuildQueryTimeout))
require.NotEmpty(t, processes)
}
func Test_MI_Query_Uint16Array(t *testing.T) {
application, err := mi.ApplicationInitialize()
require.NoError(t, err)
+6
View File
@@ -29,6 +29,12 @@ import (
"golang.org/x/sys/windows"
)
// BuildQueryTimeout bounds the probe queries collectors run in Build. Build
// has no scrape budget, and a zero timeout sets no MI timeout, so a stalled
// provider would otherwise block startup. A timed-out query fails with
// MI_RESULT_INVALID_OPERATION_TIMEOUT. MI ignores timeouts below 1ms.
const BuildQueryTimeout = 30 * time.Second
// Session represents a session.
//
// https://learn.microsoft.com/en-us/windows/win32/api/mi/ns-mi-mi_session
+3
View File
@@ -76,6 +76,9 @@ func TestCollectionBuildAndClose(t *testing.T) {
{name: "joined fatal error", buildErr: errors.Join(errors.ErrUnsupported, buildFailure), wantBuildErr: buildFailure},
{name: "nested fatal error", buildErr: errors.Join(pdh.ErrNoData, fmt.Errorf("nested: %w", errors.Join(errors.ErrUnsupported, buildFailure))), wantBuildErr: buildFailure},
{name: "multiple wrapped fatal error", buildErr: fmt.Errorf("%w and %w", errors.ErrUnsupported, buildFailure), wantBuildErr: buildFailure},
// The error chain of a Build probe query that hit mi.BuildQueryTimeout.
{name: "MI probe query timeout", buildErr: fmt.Errorf("failed to query: %w", fmt.Errorf("instance result: %w", mi.MI_RESULT_INVALID_OPERATION_TIMEOUT))},
{name: "MI probe query timeout joined with fatal error", buildErr: errors.Join(mi.MI_RESULT_INVALID_OPERATION_TIMEOUT, buildFailure), wantBuildErr: buildFailure},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()