diff --git a/internal/collector/collector.go b/internal/collector/collector.go index eee0f20..20bedd6 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -155,25 +155,25 @@ func (c *Collector) Refresh(ctx context.Context) { c.limited.Store(false) c.scanDone.Store(0) c.scanTotal.Store(int64(len(c.names))) - results := make([]model.Repo, len(c.names)) sem := make(chan struct{}, c.concurrency) var wg sync.WaitGroup - for i, name := range c.names { + for _, name := range c.names { wg.Add(1) sem <- struct{}{} go func() { defer wg.Done() defer func() { <-sem }() - results[i] = c.refreshOne(ctx, name, start, true) + r := c.refreshOne(ctx, name, start, true) + // Publish per repo so a running scan shows new and resolved alerts as they land. + c.mu.Lock() + c.repos[r.FullName] = r + c.mu.Unlock() c.scanDone.Add(1) }() } wg.Wait() c.mu.Lock() - for _, r := range results { - c.repos[r.FullName] = r - } c.lastRefresh = start c.lastDur = c.now().Sub(start) c.loaded = true diff --git a/internal/collector/collector_test.go b/internal/collector/collector_test.go index 697a2fe..bb2ae7e 100644 --- a/internal/collector/collector_test.go +++ b/internal/collector/collector_test.go @@ -553,3 +553,34 @@ func TestRefreshRepoFailureSkipsRefreshHooks(t *testing.T) { case <-time.After(200 * time.Millisecond): } } + +func TestRefreshPublishesEachRepoBeforeTheScanEnds(t *testing.T) { + release := make(chan struct{}) + f := &fakeFetcher{fn: func(name string) (model.Repo, error) { + if name == "o/slow" { + <-release + } + return model.Repo{FullName: name, Err: ""}, nil + }} + cfg := &config.Config{Repos: []string{"o/fast", "o/slow"}, RefreshInterval: time.Hour, Concurrency: 2, Thresholds: th()} + c := New(f, cfg, slog.New(slog.NewTextHandler(io.Discard, nil))) + c.now = func() time.Time { return t0 } + done := make(chan struct{}) + go func() { c.Refresh(context.Background()); close(done) }() + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + if v := c.View(); len(v.Repos) == 1 && v.Repos[0].FullName == "o/fast" { + break + } + time.Sleep(5 * time.Millisecond) + } + v := c.View() + if len(v.Repos) != 1 || v.Repos[0].FullName != "o/fast" || !v.Refreshing || v.Loaded { + t.Fatalf("mid-scan view: repos=%d refreshing=%v loaded=%v", len(v.Repos), v.Refreshing, v.Loaded) + } + close(release) + <-done + if v := c.View(); len(v.Repos) != 2 || !v.Loaded { + t.Fatalf("after scan: repos=%d loaded=%v", len(v.Repos), v.Loaded) + } +}