From c2a046ac8c41a17e3143375de73d5763ffa7600a Mon Sep 17 00:00:00 2001 From: riccardom Date: Mon, 22 Jun 2026 18:22:24 +0200 Subject: [PATCH] Distinguish timeout from cancellation in logs --- client/system/info.go | 8 +++++++- client/system/info_test.go | 15 +++++++++++++++ client/system/process_test.go | 26 ++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/client/system/info.go b/client/system/info.go index 00c66afb7..852b01685 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -2,6 +2,7 @@ package system import ( "context" + "errors" "net/netip" "strings" "time" @@ -194,7 +195,12 @@ func GetInfoWithChecksTimeout(ctx context.Context, timeout time.Duration, checks case info := <-infoCh: return info, true case <-ctx.Done(): - log.Warnf("gathering system info with checks timed out after %s", timeout) + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + log.Warnf("gathering system info with checks timed out after %s", timeout) + } else { + // Parent context canceled (e.g. shutdown), not a timeout. + log.Warnf("gathering system info with checks canceled: %v", ctx.Err()) + } return nil, false } } diff --git a/client/system/info_test.go b/client/system/info_test.go index 27821f3c5..90dc9f7af 100644 --- a/client/system/info_test.go +++ b/client/system/info_test.go @@ -3,6 +3,7 @@ package system import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" "google.golang.org/grpc/metadata" @@ -34,6 +35,20 @@ func Test_CustomHostname(t *testing.T) { assert.Equal(t, want, got.Hostname) } +func TestGetInfoWithChecksTimeout_Success(t *testing.T) { + info, ok := GetInfoWithChecksTimeout(context.Background(), 30*time.Second, nil) + assert.True(t, ok, "expected gathering to complete within the timeout") + assert.NotNil(t, info) +} + +func TestGetInfoWithChecksTimeout_Timeout(t *testing.T) { + // A 1ns budget expires before the (real) system-info gathering can finish, so the + // caller must get (nil, false) instead of blocking on the in-flight goroutine. + info, ok := GetInfoWithChecksTimeout(context.Background(), time.Nanosecond, nil) + assert.False(t, ok, "expected timeout to be reported") + assert.Nil(t, info) +} + func Test_NetAddresses(t *testing.T) { addr, err := networkAddresses() if err != nil { diff --git a/client/system/process_test.go b/client/system/process_test.go index 2216b3544..44a1c8ba0 100644 --- a/client/system/process_test.go +++ b/client/system/process_test.go @@ -36,6 +36,32 @@ func Benchmark_getRunningProcesses(b *testing.B) { b.Logf("getRunningProcessesOld returned %d processes", len(s)) } +func TestCheckFileAndProcess_ContextCanceled(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + // With a canceled context and non-empty paths the gathering must bail with an error + // instead of running the (potentially blocking) process scan / stat loop. + if _, err := checkFileAndProcess(ctx, []string{"/does/not/exist"}); err == nil { + t.Fatal("expected error on canceled context, got nil") + } +} + +func TestCheckFileAndProcess_EmptyPaths(t *testing.T) { + // No check paths means no work to do: it must return immediately with no error, + // even on a canceled context (nothing to scan or stat). + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + files, err := checkFileAndProcess(ctx, nil) + if err != nil { + t.Fatalf("unexpected error for empty paths: %v", err) + } + if len(files) != 0 { + t.Fatalf("expected no files, got %d", len(files)) + } +} + func getRunningProcessesOld() ([]string, error) { processes, err := process.Processes() if err != nil {