Enhance dashboard repository and service with caching and aggregation improvements
- Introduced caching mechanisms for summary and statistics in the dashboard service, improving performance by reducing redundant data retrieval. - Refactored the Summary method to utilize MongoDB aggregation for more efficient data processing and retrieval. - Added synchronization features using singleflight to prevent duplicate processing of requests for cached data. - Updated the repository to include a cachedScrapedTendersScope method, enhancing the efficiency of scraped document statistics retrieval. This update significantly optimizes the dashboard's performance and data handling capabilities, ensuring faster response times and reduced load on the database.
This commit is contained in:
@@ -3,10 +3,13 @@ package dashboard
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"tm/pkg/logger"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,6 +19,7 @@ const (
|
||||
defaultCountriesLimit = 6
|
||||
defaultListLimit = 5
|
||||
maxListLimit = 20
|
||||
summaryCacheTTL = 60 * time.Second
|
||||
statisticsCacheTTL = 60 * time.Second
|
||||
)
|
||||
|
||||
@@ -30,16 +34,20 @@ type Service interface {
|
||||
Statistics(ctx context.Context, query StatisticsQuery) (*StatisticsReportResponse, error)
|
||||
}
|
||||
|
||||
type statisticsCacheEntry struct {
|
||||
type cacheEntry[T any] struct {
|
||||
expiresAt time.Time
|
||||
value *StatisticsReportResponse
|
||||
value T
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
logger logger.Logger
|
||||
statisticsMu sync.Mutex
|
||||
statistics map[int]statisticsCacheEntry
|
||||
repo Repository
|
||||
logger logger.Logger
|
||||
summaryMu sync.Mutex
|
||||
summary map[int64]cacheEntry[*SummaryResponse]
|
||||
summaryGroup singleflight.Group
|
||||
statisticsMu sync.Mutex
|
||||
statistics map[int]cacheEntry[*StatisticsReportResponse]
|
||||
statisticsGroup singleflight.Group
|
||||
}
|
||||
|
||||
// NewService creates a dashboard service.
|
||||
@@ -47,7 +55,8 @@ func NewService(repo Repository, log logger.Logger) Service {
|
||||
return &service{
|
||||
repo: repo,
|
||||
logger: log,
|
||||
statistics: make(map[int]statisticsCacheEntry),
|
||||
summary: make(map[int64]cacheEntry[*SummaryResponse]),
|
||||
statistics: make(map[int]cacheEntry[*StatisticsReportResponse]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,19 +64,36 @@ func (s *service) Summary(ctx context.Context, query SummaryQuery) (*SummaryResp
|
||||
windowHours := closingWindowHours(query.ClosingWindow)
|
||||
windowSec := int64(windowHours) * 3600
|
||||
|
||||
s.logger.Info("Fetching dashboard summary", map[string]interface{}{
|
||||
"closing_window_hours": windowHours,
|
||||
})
|
||||
|
||||
out, err := s.repo.Summary(ctx, windowSec)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to fetch dashboard summary", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("dashboard summary: %w", err)
|
||||
if cached, ok := s.cachedSummary(windowSec); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
return out, nil
|
||||
cacheKey := strconv.FormatInt(windowSec, 10)
|
||||
out, err, _ := s.summaryGroup.Do(cacheKey, func() (interface{}, error) {
|
||||
if cached, ok := s.cachedSummary(windowSec); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
s.logger.Info("Fetching dashboard summary", map[string]interface{}{
|
||||
"closing_window_hours": windowHours,
|
||||
})
|
||||
|
||||
result, err := s.repo.Summary(ctx, windowSec)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to fetch dashboard summary", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("dashboard summary: %w", err)
|
||||
}
|
||||
|
||||
s.storeSummary(windowSec, result)
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out.(*SummaryResponse), nil
|
||||
}
|
||||
|
||||
func (s *service) Trend(ctx context.Context, query TrendQuery) (*TrendResponse, error) {
|
||||
@@ -177,22 +203,61 @@ func (s *service) Statistics(ctx context.Context, query StatisticsQuery) (*Stati
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
startUnix := trendStartUnix(days)
|
||||
cacheKey := strconv.Itoa(days)
|
||||
out, err, _ := s.statisticsGroup.Do(cacheKey, func() (interface{}, error) {
|
||||
if cached, ok := s.cachedStatistics(days); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
s.logger.Info("Fetching dashboard statistics report", map[string]interface{}{
|
||||
"days": days,
|
||||
})
|
||||
startUnix := trendStartUnix(days)
|
||||
|
||||
out, err := s.repo.Statistics(ctx, days, startUnix)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to fetch dashboard statistics report", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
s.logger.Info("Fetching dashboard statistics report", map[string]interface{}{
|
||||
"days": days,
|
||||
})
|
||||
return nil, fmt.Errorf("dashboard statistics: %w", err)
|
||||
|
||||
result, err := s.repo.Statistics(ctx, days, startUnix)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to fetch dashboard statistics report", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("dashboard statistics: %w", err)
|
||||
}
|
||||
|
||||
s.storeStatistics(days, result)
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.storeStatistics(days, out)
|
||||
return out, nil
|
||||
return out.(*StatisticsReportResponse), nil
|
||||
}
|
||||
|
||||
func (s *service) cachedSummary(windowSec int64) (*SummaryResponse, bool) {
|
||||
now := time.Now()
|
||||
|
||||
s.summaryMu.Lock()
|
||||
defer s.summaryMu.Unlock()
|
||||
|
||||
entry, ok := s.summary[windowSec]
|
||||
if !ok || now.After(entry.expiresAt) {
|
||||
if ok {
|
||||
delete(s.summary, windowSec)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry.value, true
|
||||
}
|
||||
|
||||
func (s *service) storeSummary(windowSec int64, value *SummaryResponse) {
|
||||
s.summaryMu.Lock()
|
||||
defer s.summaryMu.Unlock()
|
||||
|
||||
s.summary[windowSec] = cacheEntry[*SummaryResponse]{
|
||||
expiresAt: time.Now().Add(summaryCacheTTL),
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) cachedStatistics(days int) (*StatisticsReportResponse, bool) {
|
||||
@@ -216,7 +281,7 @@ func (s *service) storeStatistics(days int, value *StatisticsReportResponse) {
|
||||
s.statisticsMu.Lock()
|
||||
defer s.statisticsMu.Unlock()
|
||||
|
||||
s.statistics[days] = statisticsCacheEntry{
|
||||
s.statistics[days] = cacheEntry[*StatisticsReportResponse]{
|
||||
expiresAt: time.Now().Add(statisticsCacheTTL),
|
||||
value: value,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user