diff --git a/internal/dashboard/service.go b/internal/dashboard/service.go index 2bdef2a..6487cb2 100644 --- a/internal/dashboard/service.go +++ b/internal/dashboard/service.go @@ -25,6 +25,11 @@ const ( summaryCacheTTL = 60 * time.Second statisticsCacheTTL = 5 * time.Minute statisticsStaleGraceTTL = 15 * time.Minute + // statisticsColdLoadWait bounds how long a cold-cache request waits for a fresh + // load before falling back to a placeholder. Mongo-only loads (MinIO scope + // already cached) comfortably finish within this window; a first-ever MinIO + // bucket scan does not, so we don't block the request for that. + statisticsColdLoadWait = 4 * time.Second ) // Service defines dashboard business operations. @@ -224,13 +229,33 @@ func (s *service) Statistics(ctx context.Context, query StatisticsQuery) (*Stati } // Cold cache: the underlying load performs a full MinIO bucket scan plus Mongo - // aggregations that can take minutes. Never block the request on it. Kick off a - // background refresh (deduped via singleflight) and serve an empty report so the - // endpoint stays fast; subsequent requests get full data once the cache warms. - go s.refreshStatistics(days) - s.logger.Info("Serving placeholder dashboard statistics while cache warms", map[string]interface{}{ - "days": days, - }) + // aggregations that can take minutes on a first run. Start the load in the + // background (deduped via singleflight, and detached from this request's + // context so it keeps running even if we give up waiting) and wait briefly for + // it. If it finishes in time we return real data; otherwise we serve a + // placeholder so the endpoint stays fast, and the load keeps warming the cache + // for the next request. + resultCh := make(chan *StatisticsReportResponse, 1) + go func() { + result, err := s.loadStatistics(context.Background(), days) + if err != nil { + resultCh <- nil + return + } + resultCh <- result + }() + + select { + case result := <-resultCh: + if result != nil { + return result, nil + } + case <-time.After(statisticsColdLoadWait): + s.logger.Info("Serving placeholder dashboard statistics while cache warms", map[string]interface{}{ + "days": days, + }) + } + return emptyStatisticsReport(days), nil }