app/updater: drain background update-check goroutine before returning (#17446)

DownloadNewRelease spawned a background checkForUpdate loop that read
package-level knobs (UpdateCheckInterval et al.) and returned without
waiting for it, so under -race the next test rewrote those globals while
the orphaned goroutine was still reading them. waitDownloadIdle (from

Cancel and WaitGroup-drain the loop before DownloadNewRelease returns,
and have TestCancelOngoingDownload join its download goroutine so the
drain is observable before the test exits.
This commit is contained in:
Daniel Hiltgen 2026-07-31 10:42:40 -07:00 • committed by GitHub
parent 4f9d09ef52
commit b63eed94b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 15 additions and 3 deletions

View file

@ -153,10 +153,12 @@ func (u *Updater) DownloadNewRelease(ctx context.Context, updateResp UpdateRespo
return err
}
// In case of slow downloads, continue the update check in the background
// In case of slow downloads, continue the update check in the background.
// Drain the goroutine before returning: it reads package-level knobs
// (e.g. UpdateCheckInterval), which callers may mutate once we return.
bgctx, bgcancel := context.WithCancel(downloadCtx)
defer bgcancel()
go func() {
var bgwg sync.WaitGroup
bgwg.Go(func() {
for {
select {
case <-bgctx.Done():
@ -165,6 +167,10 @@ func (u *Updater) DownloadNewRelease(ctx context.Context, updateResp UpdateRespo
u.checkForUpdate(bgctx)
}
}
})
defer func() {
bgcancel()
bgwg.Wait()
}()
resp, err := http.DefaultClient.Do(req)

View file

@ -577,7 +577,9 @@ func TestCancelOngoingDownload(t *testing.T) {
_, resp := updater.checkForUpdate(ctx)
// Start download in goroutine
downloadDone := make(chan struct{})
go func() {
defer close(downloadDone)
_ = updater.DownloadNewRelease(ctx, resp)
}()
@ -598,6 +600,10 @@ func TestCancelOngoingDownload(t *testing.T) {
case <-time.After(2 * time.Second):
t.Fatal("download cancellation was not received by server")
}
// Wait for the download goroutine to unwind: it drags along a background
// update-check loop that reads package-level knobs the next test rewrites.
<-downloadDone
}
func TestTriggerImmediateCheck(t *testing.T) {