From f670e868e4cbec3ce9ac68bf640699114d740a36 Mon Sep 17 00:00:00 2001 From: henrygd Date: Thu, 2 Apr 2026 15:10:49 -0400 Subject: [PATCH] agent: add `SENSORS_TIMEOUT` env var (#1871) --- agent/sensors.go | 31 +++++++++++++++++---------- agent/sensors_test.go | 49 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/agent/sensors.go b/agent/sensors.go index bb4a1de7..8a6a6728 100644 --- a/agent/sensors.go +++ b/agent/sensors.go @@ -19,10 +19,16 @@ import ( "github.com/shirou/gopsutil/v4/sensors" ) +var errTemperatureFetchTimeout = errors.New("temperature collection timed out") + +// Matches sensors.TemperaturesWithContext to allow for panic recovery (gopsutil/issues/1832) +type getTempsFn func(ctx context.Context) ([]sensors.TemperatureStat, error) + type SensorConfig struct { context context.Context sensors map[string]struct{} primarySensor string + timeout time.Duration isBlacklist bool hasWildcards bool skipCollection bool @@ -34,24 +40,27 @@ func (a *Agent) newSensorConfig() *SensorConfig { sysSensors, _ := utils.GetEnv("SYS_SENSORS") sensorsEnvVal, sensorsSet := utils.GetEnv("SENSORS") skipCollection := sensorsSet && sensorsEnvVal == "" + sensorsTimeout, _ := utils.GetEnv("SENSORS_TIMEOUT") - return a.newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal, skipCollection) + return a.newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal, sensorsTimeout, skipCollection) } -// Matches sensors.TemperaturesWithContext to allow for panic recovery (gopsutil/issues/1832) -type getTempsFn func(ctx context.Context) ([]sensors.TemperatureStat, error) - -var ( - errTemperatureFetchTimeout = errors.New("temperature collection timed out") - temperatureFetchTimeout = 2 * time.Second -) - // newSensorConfigWithEnv creates a SensorConfig with the provided environment variables // sensorsSet indicates if the SENSORS environment variable was explicitly set (even to empty string) -func (a *Agent) newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal string, skipCollection bool) *SensorConfig { +func (a *Agent) newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal, sensorsTimeout string, skipCollection bool) *SensorConfig { + timeout := 2 * time.Second + if sensorsTimeout != "" { + if d, err := time.ParseDuration(sensorsTimeout); err == nil { + timeout = d + } else { + slog.Warn("Invalid SENSORS_TIMEOUT", "value", sensorsTimeout) + } + } + config := &SensorConfig{ context: context.Background(), primarySensor: primarySensor, + timeout: timeout, skipCollection: skipCollection, firstRun: true, sensors: make(map[string]struct{}), @@ -171,7 +180,7 @@ func (a *Agent) getTempsWithTimeout(getTemps getTempsFn) ([]sensors.TemperatureS // Use a longer timeout on the first run to allow for initialization // (e.g. Windows LHM subprocess startup) - timeout := temperatureFetchTimeout + timeout := a.sensorConfig.timeout if a.sensorConfig.firstRun { a.sensorConfig.firstRun = false timeout = 10 * time.Second diff --git a/agent/sensors_test.go b/agent/sensors_test.go index 23e2db6a..a56faec3 100644 --- a/agent/sensors_test.go +++ b/agent/sensors_test.go @@ -168,6 +168,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) { primarySensor string sysSensors string sensors string + sensorsTimeout string skipCollection bool expectedConfig *SensorConfig }{ @@ -179,12 +180,37 @@ func TestNewSensorConfigWithEnv(t *testing.T) { expectedConfig: &SensorConfig{ context: context.Background(), primarySensor: "", + timeout: 2 * time.Second, sensors: map[string]struct{}{}, isBlacklist: false, hasWildcards: false, skipCollection: false, }, }, + { + name: "Custom timeout", + primarySensor: "", + sysSensors: "", + sensors: "", + sensorsTimeout: "5s", + expectedConfig: &SensorConfig{ + context: context.Background(), + timeout: 5 * time.Second, + sensors: map[string]struct{}{}, + }, + }, + { + name: "Invalid timeout falls back to default", + primarySensor: "", + sysSensors: "", + sensors: "", + sensorsTimeout: "notaduration", + expectedConfig: &SensorConfig{ + context: context.Background(), + timeout: 2 * time.Second, + sensors: map[string]struct{}{}, + }, + }, { name: "Explicitly set to empty string", primarySensor: "", @@ -194,6 +220,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) { expectedConfig: &SensorConfig{ context: context.Background(), primarySensor: "", + timeout: 2 * time.Second, sensors: map[string]struct{}{}, isBlacklist: false, hasWildcards: false, @@ -208,6 +235,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) { expectedConfig: &SensorConfig{ context: context.Background(), primarySensor: "cpu_temp", + timeout: 2 * time.Second, sensors: map[string]struct{}{}, isBlacklist: false, hasWildcards: false, @@ -221,6 +249,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) { expectedConfig: &SensorConfig{ context: context.Background(), primarySensor: "cpu_temp", + timeout: 2 * time.Second, sensors: map[string]struct{}{ "cpu_temp": {}, "gpu_temp": {}, @@ -237,6 +266,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) { expectedConfig: &SensorConfig{ context: context.Background(), primarySensor: "cpu_temp", + timeout: 2 * time.Second, sensors: map[string]struct{}{ "cpu_temp": {}, "gpu_temp": {}, @@ -253,6 +283,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) { expectedConfig: &SensorConfig{ context: context.Background(), primarySensor: "cpu_temp", + timeout: 2 * time.Second, sensors: map[string]struct{}{ "cpu_*": {}, "gpu_temp": {}, @@ -269,6 +300,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) { expectedConfig: &SensorConfig{ context: context.Background(), primarySensor: "cpu_temp", + timeout: 2 * time.Second, sensors: map[string]struct{}{ "cpu_*": {}, "gpu_temp": {}, @@ -284,6 +316,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) { sensors: "cpu_temp", expectedConfig: &SensorConfig{ primarySensor: "cpu_temp", + timeout: 2 * time.Second, sensors: map[string]struct{}{ "cpu_temp": {}, }, @@ -295,7 +328,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := agent.newSensorConfigWithEnv(tt.primarySensor, tt.sysSensors, tt.sensors, tt.skipCollection) + result := agent.newSensorConfigWithEnv(tt.primarySensor, tt.sysSensors, tt.sensors, tt.sensorsTimeout, tt.skipCollection) // Check primary sensor assert.Equal(t, tt.expectedConfig.primarySensor, result.primarySensor) @@ -314,6 +347,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) { // Check flags assert.Equal(t, tt.expectedConfig.isBlacklist, result.isBlacklist) assert.Equal(t, tt.expectedConfig.hasWildcards, result.hasWildcards) + assert.Equal(t, tt.expectedConfig.timeout, result.timeout) // Check context if tt.sysSensors != "" { @@ -333,12 +367,14 @@ func TestNewSensorConfig(t *testing.T) { t.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", "test_primary") t.Setenv("BESZEL_AGENT_SYS_SENSORS", "/test/path") t.Setenv("BESZEL_AGENT_SENSORS", "test_sensor1,test_*,test_sensor3") + t.Setenv("BESZEL_AGENT_SENSORS_TIMEOUT", "7s") agent := &Agent{} result := agent.newSensorConfig() // Verify results assert.Equal(t, "test_primary", result.primarySensor) + assert.Equal(t, 7*time.Second, result.timeout) assert.NotNil(t, result.sensors) assert.Equal(t, 3, len(result.sensors)) assert.True(t, result.hasWildcards) @@ -532,15 +568,10 @@ func TestGetTempsWithTimeout(t *testing.T) { agent := &Agent{ sensorConfig: &SensorConfig{ context: context.Background(), + timeout: 10 * time.Millisecond, }, } - originalTimeout := temperatureFetchTimeout - t.Cleanup(func() { - temperatureFetchTimeout = originalTimeout - }) - temperatureFetchTimeout = 10 * time.Millisecond - t.Run("returns temperatures before timeout", func(t *testing.T) { temps, err := agent.getTempsWithTimeout(func(ctx context.Context) ([]sensors.TemperatureStat, error) { return []sensors.TemperatureStat{{SensorKey: "cpu_temp", Temperature: 42}}, nil @@ -567,15 +598,13 @@ func TestUpdateTemperaturesSkipsOnTimeout(t *testing.T) { systemInfo: system.Info{DashboardTemp: 99}, sensorConfig: &SensorConfig{ context: context.Background(), + timeout: 10 * time.Millisecond, }, } - originalTimeout := temperatureFetchTimeout t.Cleanup(func() { - temperatureFetchTimeout = originalTimeout getSensorTemps = sensors.TemperaturesWithContext }) - temperatureFetchTimeout = 10 * time.Millisecond getSensorTemps = func(ctx context.Context) ([]sensors.TemperatureStat, error) { time.Sleep(50 * time.Millisecond) return nil, nil