Enhance dashboard summary and statistics with new fields and caching improvements
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Added new field `Days` and `ScrapedTED` to `SummaryResponse` for tracking daily TED scrape counts. - Updated `SummaryQuery` to include `Days` parameter for querying scraped TED data. - Modified `Summary` handler to accept and process the new `Days` parameter. - Refactored `Summary` method in the repository to support the new `Days` parameter and improved aggregation logic. - Enhanced caching logic in the service layer to utilize a composite cache key based on `closingWindowSec` and `days`, improving cache management and retrieval efficiency. This update improves the dashboard's functionality by providing more detailed insights into TED scraping activities and optimizing the caching strategy for better performance.
This commit is contained in:
+163
-16
@@ -22,7 +22,8 @@ const (
|
||||
defaultCountriesLimit = 6
|
||||
defaultListLimit = 5
|
||||
maxListLimit = 20
|
||||
summaryCacheTTL = 60 * time.Second
|
||||
summaryCacheTTL = 5 * time.Minute
|
||||
summaryStaleGraceTTL = 15 * time.Minute
|
||||
statisticsCacheTTL = 5 * time.Minute
|
||||
statisticsStaleGraceTTL = 15 * time.Minute
|
||||
// statisticsColdLoadWait bounds how long a cold-cache request waits for a fresh
|
||||
@@ -54,7 +55,7 @@ type service struct {
|
||||
logger logger.Logger
|
||||
redis redis.Client
|
||||
summaryMu sync.Mutex
|
||||
summary map[int64]cacheEntry[*SummaryResponse]
|
||||
summary map[string]cacheEntry[*SummaryResponse]
|
||||
summaryGroup singleflight.Group
|
||||
statisticsMu sync.Mutex
|
||||
statistics map[int]cacheEntry[*StatisticsReportResponse]
|
||||
@@ -67,32 +68,46 @@ func NewService(repo Repository, log logger.Logger, redisClient redis.Client) Se
|
||||
repo: repo,
|
||||
logger: log,
|
||||
redis: redisClient,
|
||||
summary: make(map[int64]cacheEntry[*SummaryResponse]),
|
||||
summary: make(map[string]cacheEntry[*SummaryResponse]),
|
||||
statistics: make(map[int]cacheEntry[*StatisticsReportResponse]),
|
||||
}
|
||||
go s.warmStatisticsCache()
|
||||
go s.warmSummaryCache()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *service) Summary(ctx context.Context, query SummaryQuery) (*SummaryResponse, error) {
|
||||
windowHours := closingWindowHours(query.ClosingWindow)
|
||||
windowSec := int64(windowHours) * 3600
|
||||
days := trendDays(query.Days)
|
||||
cacheKey := summaryCacheKey(windowSec, days)
|
||||
|
||||
if cached, ok := s.cachedSummary(windowSec); ok {
|
||||
if cached, ok := s.cachedSummary(cacheKey); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
cacheKey := strconv.FormatInt(windowSec, 10)
|
||||
if stale, ok := s.staleSummary(cacheKey); ok {
|
||||
go s.refreshSummary(windowSec, days)
|
||||
return stale, nil
|
||||
}
|
||||
|
||||
if redisCached, ok := s.getRedisSummary(ctx, cacheKey); ok {
|
||||
s.storeSummary(cacheKey, redisCached)
|
||||
go s.refreshSummary(windowSec, days)
|
||||
return redisCached, nil
|
||||
}
|
||||
|
||||
out, err, _ := s.summaryGroup.Do(cacheKey, func() (interface{}, error) {
|
||||
if cached, ok := s.cachedSummary(windowSec); ok {
|
||||
if cached, ok := s.cachedSummary(cacheKey); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
s.logger.Info("Fetching dashboard summary", map[string]interface{}{
|
||||
"closing_window_hours": windowHours,
|
||||
"days": days,
|
||||
})
|
||||
|
||||
result, err := s.repo.Summary(context.WithoutCancel(ctx), windowSec)
|
||||
result, err := s.repo.Summary(context.WithoutCancel(ctx), windowSec, days)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to fetch dashboard summary", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -100,7 +115,8 @@ func (s *service) Summary(ctx context.Context, query SummaryQuery) (*SummaryResp
|
||||
return nil, fmt.Errorf("dashboard summary: %w", err)
|
||||
}
|
||||
|
||||
s.storeSummary(windowSec, result)
|
||||
s.storeSummary(cacheKey, result)
|
||||
s.storeRedisSummary(context.Background(), cacheKey, result)
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -279,7 +295,6 @@ func emptyStatisticsReport(days int) *StatisticsReportResponse {
|
||||
Days: days,
|
||||
GeneratedAt: time.Now().UTC().Unix(),
|
||||
Daily: StatisticsDailySeries{
|
||||
ScrapedTED: fillTrendSeries(days, nil),
|
||||
ScrapedDocuments: fillTrendSeries(days, nil),
|
||||
TranslatedNotices: fillTrendSeries(days, nil),
|
||||
},
|
||||
@@ -324,16 +339,94 @@ func (s *service) loadStatistics(ctx context.Context, days int) (*StatisticsRepo
|
||||
return out.(*StatisticsReportResponse), nil
|
||||
}
|
||||
|
||||
func (s *service) cachedSummary(windowSec int64) (*SummaryResponse, bool) {
|
||||
func (s *service) warmSummaryCache() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
windowSec := int64(defaultClosingWindowHours) * 3600
|
||||
cacheKey := summaryCacheKey(windowSec, defaultTrendDays)
|
||||
|
||||
if cached, ok := s.getRedisSummary(ctx, cacheKey); ok {
|
||||
s.storeSummary(cacheKey, cached)
|
||||
s.logger.Info("Dashboard summary cache warmed from Redis", map[string]interface{}{
|
||||
"closing_window_hours": defaultClosingWindowHours,
|
||||
"days": defaultTrendDays,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
go s.refreshSummary(windowSec, defaultTrendDays)
|
||||
}
|
||||
|
||||
func (s *service) refreshSummary(windowSec int64, days int) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
_, _ = s.loadSummary(ctx, windowSec, days)
|
||||
}
|
||||
|
||||
func (s *service) loadSummary(ctx context.Context, windowSec int64, days int) (*SummaryResponse, error) {
|
||||
cacheKey := summaryCacheKey(windowSec, days)
|
||||
out, err, _ := s.summaryGroup.Do(cacheKey, func() (interface{}, error) {
|
||||
if cached, ok := s.cachedSummary(cacheKey); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
s.logger.Info("Refreshing dashboard summary cache", map[string]interface{}{
|
||||
"closing_window_sec": windowSec,
|
||||
"days": days,
|
||||
})
|
||||
|
||||
result, err := s.repo.Summary(context.WithoutCancel(ctx), windowSec, days)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to refresh dashboard summary", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("dashboard summary: %w", err)
|
||||
}
|
||||
|
||||
s.storeSummary(cacheKey, result)
|
||||
s.storeRedisSummary(context.Background(), cacheKey, result)
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out.(*SummaryResponse), nil
|
||||
}
|
||||
|
||||
func summaryCacheKey(windowSec int64, days int) string {
|
||||
return fmt.Sprintf("%d:%d", windowSec, days)
|
||||
}
|
||||
|
||||
func summaryRedisKey(cacheKey string) string {
|
||||
return fmt.Sprintf("dashboard:summary:%s", cacheKey)
|
||||
}
|
||||
|
||||
func (s *service) cachedSummary(cacheKey string) (*SummaryResponse, bool) {
|
||||
now := time.Now()
|
||||
|
||||
s.summaryMu.Lock()
|
||||
defer s.summaryMu.Unlock()
|
||||
|
||||
entry, ok := s.summary[windowSec]
|
||||
entry, ok := s.summary[cacheKey]
|
||||
if !ok || now.After(entry.expiresAt) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry.value, true
|
||||
}
|
||||
|
||||
func (s *service) staleSummary(cacheKey string) (*SummaryResponse, bool) {
|
||||
now := time.Now()
|
||||
|
||||
s.summaryMu.Lock()
|
||||
defer s.summaryMu.Unlock()
|
||||
|
||||
entry, ok := s.summary[cacheKey]
|
||||
if !ok || now.After(entry.staleUntil) {
|
||||
if ok {
|
||||
delete(s.summary, windowSec)
|
||||
delete(s.summary, cacheKey)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -341,17 +434,71 @@ func (s *service) cachedSummary(windowSec int64) (*SummaryResponse, bool) {
|
||||
return entry.value, true
|
||||
}
|
||||
|
||||
func (s *service) storeSummary(windowSec int64, value *SummaryResponse) {
|
||||
func (s *service) storeSummary(cacheKey string, value *SummaryResponse) {
|
||||
now := time.Now()
|
||||
|
||||
s.summaryMu.Lock()
|
||||
defer s.summaryMu.Unlock()
|
||||
|
||||
s.summary[windowSec] = cacheEntry[*SummaryResponse]{
|
||||
expiresAt: time.Now().Add(summaryCacheTTL),
|
||||
staleUntil: time.Now().Add(summaryCacheTTL),
|
||||
s.summary[cacheKey] = cacheEntry[*SummaryResponse]{
|
||||
expiresAt: now.Add(summaryCacheTTL),
|
||||
staleUntil: now.Add(summaryCacheTTL + summaryStaleGraceTTL),
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) getRedisSummary(ctx context.Context, cacheKey string) (*SummaryResponse, bool) {
|
||||
if s.redis == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
raw, err := s.redis.Get(ctx, summaryRedisKey(cacheKey))
|
||||
if err != nil {
|
||||
if err != goredis.Nil {
|
||||
s.logger.Warn("Failed to read dashboard summary cache from Redis", map[string]interface{}{
|
||||
"cache_key": cacheKey,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var report SummaryResponse
|
||||
if err := json.Unmarshal([]byte(raw), &report); err != nil {
|
||||
s.logger.Warn("Failed to decode dashboard summary cache from Redis", map[string]interface{}{
|
||||
"cache_key": cacheKey,
|
||||
"error": err.Error(),
|
||||
})
|
||||
_ = s.redis.Del(ctx, summaryRedisKey(cacheKey))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &report, true
|
||||
}
|
||||
|
||||
func (s *service) storeRedisSummary(ctx context.Context, cacheKey string, value *SummaryResponse) {
|
||||
if s.redis == nil || value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to encode dashboard summary for Redis", map[string]interface{}{
|
||||
"cache_key": cacheKey,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ttl := summaryCacheTTL + summaryStaleGraceTTL
|
||||
if err := s.redis.Set(ctx, summaryRedisKey(cacheKey), string(encoded), ttl); err != nil {
|
||||
s.logger.Warn("Failed to store dashboard summary in Redis", map[string]interface{}{
|
||||
"cache_key": cacheKey,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) cachedStatistics(days int) (*StatisticsReportResponse, bool) {
|
||||
now := time.Now()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user