Files
windows_exporter/internal/mi/AGENTS.md
T

4.2 KiB

MI package guidance

Session.Query, QueryUnmarshal and QueryFunc receive results through MI_OperationCallbacks (callbacks.go). Do not move them back to the synchronous MI_Operation_GetInstance loop: when its result hand-over waits, e.g. for parallel queries or slow providers such as MSFT_StoragePool, the WMI client leaks Event handles (miutils!RtlInterlockedCompareWait). Test_MI_ParallelQuery_HandleGrowth guards this. QueryInstances and Operation.GetInstance remain as the synchronous low-level API for tests.

Rules for the callback path:

  • Create native callbacks once (sync.OnceValue) and identify the query by the callback context. windows.NewCallback never frees a callback and a process can create only about 2000.
  • Keep the callback context and the MI_OperationCallbacks pinned until MI_Operation_Close has returned.
  • Never call Cancel or Close from a callback. The goroutine that started the query cancels on request and closes after the final callback (moreResults == MI_FALSE).
  • Recover panics in callbacks; they must not unwind into MI.
  • Never block a callback on a goroutine. QueryFunc copies each instance (MI_Instance_Clone) in the callback and runs fn for the copies on the calling goroutine. Handing the original over and waiting for fn costs two thread wake-ups per instance, about 25% more process CPU than the copy (Benchmark_MI_ProcessCPU).
  • MI reports parameter errors from within MI_Session_QueryInstances; the runtime runs that callback on the calling goroutine. Never run caller code such as the QueryFunc handler inside a callback: a panic cannot reach the caller there and runtime.Goexit is fatal.
  • While waiting for callbacks, keep a timer pending. Otherwise the runtime can declare a false deadlock (golang/go#55015, still open in Go 1.27); Test_MI_Query_DeadlockDetector reproduces it. Do not use a goroutine that sleeps forever.
  • Guard shared callback state with a mutex: results of one operation may arrive on different MI threads. -race cannot reveal a missing lock there, because the runtime treats every callback as synchronized with every native call.

Query timeouts and options:

  • Every query runs with an MI timeout. MI treats a zero timeout as unset, so a non-positive timeout selects DefaultQueryTimeout (the session default options) and a positive one below 1ms is raised to 1ms.
  • Operation options must outlive every operation that uses them. Options for whole-second timeouts are cached per session and shared by concurrent operations; Session.Close deletes them after MI_Session_Close has closed every operation. Other timeouts, e.g. the remaining scrape budget, get per-query options that are deleted after the operation is closed.
  • startQuery holds the read lock of Session.lifecycle from the closed check until MI_Session_QueryInstances has returned; Close holds the write lock across MI_Session_Close and the option deletion. Start every callback query through runQuery/startQuery, never by calling asyncQuery.start with session options directly.

Cancellation: QueryContext and QueryFuncContext do not start a query for a done context. When it is done during the query, wait fails the query through fail/cancelRequested, so Cancel runs on the waiting goroutine, and still waits for the final callback before close frees anything. The handler is not called after the context is done; a copy it skips fails the query with the context's cause, even after the final result, so partial results never look complete. A context that is done after the final result (complete, set under mu) does not fail a query that has nothing left to hand over.

Tests that need a slow query must bound the work it causes. Cancelling an operation may end it only on the client: providers such as CIMWin32 (CIM_DataFile) keep enumerating in WmiPrvSE. CIM_DataFile for a whole drive kept the CI runner busy for minutes after the Go tests, which slowed every later step and made the end-to-end /metrics request time out. Restrict such queries to one directory.

Run CGO_ENABLED=1 go test -race (gcc from w64devkit) and a -gcflags=all=-d=checkptr run for changes in this package.