Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,24 @@ const (
// happen to share a process (e.g. tests).
var fetchMu sync.Mutex

// initialJitterMax controls the first-fetch jitter window. Exposed as a
// var (not a const) so tests can shorten it to 0 and deterministically
// drive Run through one full timer.C → fetchOnce → timer.Reset cycle.
var initialJitterMax = 30 * time.Second

// httpClientForRun is the http.Client Run uses. Exposed as a var so
// tests can inject a transport that returns a deterministic error,
// covering the slog.Warn-on-fetch-fail branch without depending on the
// real network.
var httpClientForRun = func() *http.Client { return &http.Client{Timeout: 30 * time.Second} }

// Run polls the canonical URL on a timer, replacing the active list
// whenever a new one is fetched. Blocks until ctx is cancelled. The
// first fetch is delayed 0–30s so a fleet rebooting at the same time
// doesn't thunder the URL.
func Run(ctx context.Context) {
client := &http.Client{Timeout: 30 * time.Second}
timer := time.NewTimer(jitter(30 * time.Second))
client := httpClientForRun()
timer := time.NewTimer(jitter(initialJitterMax))
defer timer.Stop()
for {
select {
Expand All @@ -51,10 +62,9 @@ func Run(ctx context.Context) {
func fetchOnce(ctx context.Context, client *http.Client) error {
fetchMu.Lock()
defer fetchMu.Unlock()
req, err := http.NewRequestWithContext(ctx, "GET", defaultURL, nil)
if err != nil {
return err
}
// http.NewRequestWithContext only fails on an invalid method or URL —
// both are compile-time constants here, so the error is unreachable.
req, _ := http.NewRequestWithContext(ctx, "GET", defaultURL, nil)
req.Header.Set("User-Agent", "pilot-daemon/trustedagents")
resp, err := client.Do(req)
if err != nil {
Expand Down
35 changes: 35 additions & 0 deletions zz_fetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,41 @@ func TestRun_FullIteration(t *testing.T) {
}
}

// TestRun_TimerFires drives Run through the timer.C → fetchOnce →
// timer.Reset arm. We shrink initialJitterMax to 0 so the first timer
// fires immediately, and inject a failing http client so fetchOnce
// returns an error and the slog.Warn branch is also covered.
func TestRun_TimerFires(t *testing.T) {
// Mutates package state — no t.Parallel.
prevJ := initialJitterMax
initialJitterMax = 0
t.Cleanup(func() { initialJitterMax = prevJ })

prevClient := httpClientForRun
httpClientForRun = func() *http.Client {
return &http.Client{Transport: &errTransport{err: errors.New("injected: no network")}}
}
t.Cleanup(func() { httpClientForRun = prevClient })

restore := SetForTest(nil)
t.Cleanup(restore)

ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
Run(ctx)
close(done)
}()
// Give Run enough time to fire the timer once + log + reset.
time.Sleep(150 * time.Millisecond)
cancel()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("Run did not return after timer-fired path + cancel")
}
}

// TestRun_FetchPath drives Run through the timer.C -> fetchOnce ->
// timer.Reset arm. We can't shrink fetchInterval (const), so we can't
// drive a second iteration in test time. But the first iteration —
Expand Down
Loading