From 784c3d6563ef9ec76a1ce3bbbb547c91031977b8 Mon Sep 17 00:00:00 2001 From: Mazyar Date: Wed, 8 Jul 2026 02:20:16 +0330 Subject: [PATCH] Refactor recommendation page cache handling and configuration - Removed the `RecommendationPageCacheLanguages` configuration from `AISummarizerConfig` to streamline cache management. - Updated the `companyService` and `tenderService` to utilize the new `InvalidateRecommendedTendersPageCache` method for cache invalidation, enhancing clarity and efficiency. - Refactored the `invalidateRecommendedTendersPageCache` method to eliminate unnecessary context parameters, simplifying the function signature. - Improved the handling of page cache refresh logic by consolidating language handling within the `tenderService`, ensuring consistent behavior across services. - Cleaned up related tests and removed deprecated functions to maintain code quality and readability. This update enhances the maintainability of the recommendation caching system by simplifying configuration and improving cache invalidation logic. --- cmd/web/bootstrap/config.go | 2 - cmd/web/main.go | 7 -- cmd/worker/bootstrap/bootstrap.go | 5 -- cmd/worker/bootstrap/config.go | 1 - internal/company/page_cache_refresher.go | 1 + internal/company/recommendation.go | 4 +- internal/company/recommendation_page_cache.go | 57 +-------------- internal/company/service.go | 1 - internal/tender/recommendation_page_cache.go | 71 +++++++------------ .../tender/recommendation_page_cache_test.go | 10 --- internal/tender/service.go | 7 +- 11 files changed, 32 insertions(+), 134 deletions(-) diff --git a/cmd/web/bootstrap/config.go b/cmd/web/bootstrap/config.go index 34dfc65..1092cc0 100644 --- a/cmd/web/bootstrap/config.go +++ b/cmd/web/bootstrap/config.go @@ -73,8 +73,6 @@ type AISummarizerConfig struct { // RecommendationCacheTTL is an optional max Redis TTL for cached recommendations. // When 0 (default), entries persist until the company is created, updated, or onboarded. RecommendationCacheTTL time.Duration `env:"AI_RECOMMENDATION_CACHE_TTL" envDefault:"0"` - // RecommendationPageCacheLanguages lists languages pre-built for the recommendation page cache. - RecommendationPageCacheLanguages string `env:"AI_RECOMMENDATION_PAGE_CACHE_LANGUAGES" envDefault:"en"` // MinIO storage settings MinioEndpoint string `env:"AI_SUMMARIZER_MINIO_ENDPOINT" envDefault:""` diff --git a/cmd/web/main.go b/cmd/web/main.go index 31f7cea..ebc33e3 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -241,12 +241,6 @@ func main() { userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK, redisClient, auditLogger) categoryService := company_category.NewService(categoryRepository, logger) companyService := company.NewService(companyRepository, categoryService, fileStoreService, aiRecommendationClient, aiPipelineClient, redisClient, conf.AISummarizer.RecommendationCacheTTL, logger) - pageCacheLanguages := tender.ParsePageCacheLanguagesConfig(conf.AISummarizer.RecommendationPageCacheLanguages) - if configurer, ok := companyService.(interface { - SetPageCacheLanguages([]string) - }); ok { - configurer.SetPageCacheLanguages(pageCacheLanguages) - } customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK, customerValidator, auditLogger) tenderService := tender.NewService( tenderRepository, @@ -258,7 +252,6 @@ func main() { aiSummarizerStorage, redisClient, conf.AISummarizer.RecommendationCacheTTL, - pageCacheLanguages, conf.AISummarizer.DefaultLanguage, ) if configurer, ok := companyService.(interface { diff --git a/cmd/worker/bootstrap/bootstrap.go b/cmd/worker/bootstrap/bootstrap.go index c23d83b..6c70d08 100644 --- a/cmd/worker/bootstrap/bootstrap.go +++ b/cmd/worker/bootstrap/bootstrap.go @@ -121,11 +121,6 @@ func InitWorker(config Config, mongoManager *mongo.ConnectionManager, appLogger config.AISummarizer.RecommendationCacheTTL, appLogger, ) - if configurer, ok := recommendationRefresher.(interface { - SetPageCacheLanguages([]string) - }); ok { - configurer.SetPageCacheLanguages(tender.ParsePageCacheLanguagesConfig(config.AISummarizer.RecommendationPageCacheLanguages)) - } appLogger.Info("Company recommendation refresh after pipeline enabled", map[string]interface{}{}) } else if config.Worker.RecommendationRefreshAfterPipelineEnabled { appLogger.Warn("Company recommendation refresh after pipeline disabled: AI client or Redis unavailable", map[string]interface{}{}) diff --git a/cmd/worker/bootstrap/config.go b/cmd/worker/bootstrap/config.go index 0b9f3d9..e8652ed 100644 --- a/cmd/worker/bootstrap/config.go +++ b/cmd/worker/bootstrap/config.go @@ -60,5 +60,4 @@ type AISummarizerConfig struct { MinioRegion string `env:"AI_SUMMARIZER_MINIO_REGION" envDefault:"us-east-1"` MinioBucket string `env:"AI_SUMMARIZER_MINIO_BUCKET" envDefault:"opplens"` RecommendationCacheTTL time.Duration `env:"AI_RECOMMENDATION_CACHE_TTL" envDefault:"0"` - RecommendationPageCacheLanguages string `env:"AI_RECOMMENDATION_PAGE_CACHE_LANGUAGES" envDefault:"en"` } diff --git a/internal/company/page_cache_refresher.go b/internal/company/page_cache_refresher.go index 6450563..eca1ddf 100644 --- a/internal/company/page_cache_refresher.go +++ b/internal/company/page_cache_refresher.go @@ -4,4 +4,5 @@ package company // Implemented by the tender service on the web API; optional on the worker. type RecommendedTendersPageCacheRefresher interface { ScheduleRefreshRecommendedTendersPageCache(companyID string) + InvalidateRecommendedTendersPageCache(companyID string) } diff --git a/internal/company/recommendation.go b/internal/company/recommendation.go index 9c47982..b787e0d 100644 --- a/internal/company/recommendation.go +++ b/internal/company/recommendation.go @@ -467,7 +467,7 @@ func (s *companyService) cacheAIRecommendations(ctx context.Context, companyID s return } - s.invalidateRecommendedTendersPageCache(ctx, companyID) + s.invalidateRecommendedTendersPageCache(companyID) s.scheduleRecommendedTendersPageCacheRefresh(companyID) } @@ -489,7 +489,7 @@ func (s *companyService) invalidateAIRecommendationCache(ctx context.Context, co "error": err.Error(), }) } - s.invalidateRecommendedTendersPageCache(ctx, companyID) + s.invalidateRecommendedTendersPageCache(companyID) } func (s *companyService) buildOnboardingDocuments(fileIDs []string) (string, error) { diff --git a/internal/company/recommendation_page_cache.go b/internal/company/recommendation_page_cache.go index 24cfc07..ec0ea85 100644 --- a/internal/company/recommendation_page_cache.go +++ b/internal/company/recommendation_page_cache.go @@ -1,30 +1,10 @@ package company -import ( - "context" - "strings" -) - -const recommendedTendersPageCacheKeyPrefix = "ai:recommendations:page:" - -func recommendedTendersPageCacheKey(companyID, language string) string { - return recommendedTendersPageCacheKeyPrefix + - strings.TrimSpace(companyID) + ":" + - strings.ToLower(strings.TrimSpace(language)) -} - -func (s *companyService) invalidateRecommendedTendersPageCache(ctx context.Context, companyID string) { - if s.redisClient == nil { +func (s *companyService) invalidateRecommendedTendersPageCache(companyID string) { + if s.pageCacheRefresher == nil { return } - - for _, language := range s.pageCacheLanguages { - lang := strings.ToLower(strings.TrimSpace(language)) - if lang == "" { - continue - } - _ = s.redisClient.Del(ctx, recommendedTendersPageCacheKey(companyID, lang)) - } + s.pageCacheRefresher.InvalidateRecommendedTendersPageCache(companyID) } func (s *companyService) scheduleRecommendedTendersPageCacheRefresh(companyID string) { @@ -38,34 +18,3 @@ func (s *companyService) scheduleRecommendedTendersPageCacheRefresh(companyID st func (s *companyService) SetRecommendedTendersPageCacheRefresher(refresher RecommendedTendersPageCacheRefresher) { s.pageCacheRefresher = refresher } - -// SetPageCacheLanguages configures which language variants are invalidated in Redis. -func (s *companyService) SetPageCacheLanguages(languages []string) { - s.pageCacheLanguages = normalizePageCacheLanguages("", languages) -} - -func normalizePageCacheLanguages(defaultLanguage string, languages []string) []string { - seen := make(map[string]struct{}) - out := make([]string, 0, len(languages)+1) - - add := func(lang string) { - lang = strings.ToLower(strings.TrimSpace(lang)) - if lang == "" { - return - } - if _, ok := seen[lang]; ok { - return - } - seen[lang] = struct{}{} - out = append(out, lang) - } - - add(defaultLanguage) - for _, lang := range languages { - add(lang) - } - if len(out) == 0 { - out = append(out, "en") - } - return out -} diff --git a/internal/company/service.go b/internal/company/service.go index 7fec802..1ca0d19 100644 --- a/internal/company/service.go +++ b/internal/company/service.go @@ -63,7 +63,6 @@ type companyService struct { pageCacheRefresher RecommendedTendersPageCacheRefresher redisClient redis.Client recommendationCacheTTL time.Duration - pageCacheLanguages []string logger logger.Logger } diff --git a/internal/tender/recommendation_page_cache.go b/internal/tender/recommendation_page_cache.go index a49759d..3136aa9 100644 --- a/internal/tender/recommendation_page_cache.go +++ b/internal/tender/recommendation_page_cache.go @@ -35,29 +35,40 @@ func recommendedTendersPageCacheKey(companyID, language string) string { strings.ToLower(strings.TrimSpace(language)) } -// ScheduleRefreshRecommendedTendersPageCache rebuilds resolved recommendation pages asynchronously. +// ScheduleRefreshRecommendedTendersPageCache rebuilds the page cache for the service default language. func (s *tenderService) ScheduleRefreshRecommendedTendersPageCache(companyID string) { + s.schedulePageCacheRefreshForLanguage(companyID, s.defaultLanguage) +} + +// InvalidateRecommendedTendersPageCache drops the cached page for the service default language. +func (s *tenderService) InvalidateRecommendedTendersPageCache(companyID string) { companyID = strings.TrimSpace(companyID) if companyID == "" || s.redisClient == nil { return } + ctx, cancel := context.WithTimeout(context.Background(), recommendationPageCacheReadTimeout) + defer cancel() + s.deleteRecommendedTendersPageCache(ctx, companyID, s.defaultLanguage) +} + +func (s *tenderService) schedulePageCacheRefreshForLanguage(companyID, language string) { + companyID = strings.TrimSpace(companyID) + language = strings.ToLower(strings.TrimSpace(language)) + if companyID == "" || language == "" || s.redisClient == nil { + return + } + go func() { ctx, cancel := context.WithTimeout(context.Background(), recommendationPageCacheBuildTimeout) defer cancel() - for _, language := range s.pageCacheLanguages { - lang := strings.ToLower(strings.TrimSpace(language)) - if lang == "" { - continue - } - if err := s.buildRecommendedTendersPageCache(ctx, companyID, lang); err != nil { - s.logger.Warn("Failed to refresh recommended tenders page cache", map[string]interface{}{ - "company_id": companyID, - "language": lang, - "error": err.Error(), - }) - } + if err := s.buildRecommendedTendersPageCache(ctx, companyID, language); err != nil { + s.logger.Warn("Failed to refresh recommended tenders page cache", map[string]interface{}{ + "company_id": companyID, + "language": language, + "error": err.Error(), + }) } }() } @@ -380,40 +391,6 @@ func tenderFromRecommendationResponse(item TenderResponse) Tender { return tender } -func normalizePageCacheLanguages(defaultLanguage string, languages []string) []string { - seen := make(map[string]struct{}) - out := make([]string, 0, len(languages)+1) - - add := func(lang string) { - lang = strings.ToLower(strings.TrimSpace(lang)) - if lang == "" { - return - } - if _, ok := seen[lang]; ok { - return - } - seen[lang] = struct{}{} - out = append(out, lang) - } - - add(defaultLanguage) - for _, lang := range languages { - add(lang) - } - if len(out) == 0 { - out = append(out, "en") - } - return out -} - -// ParsePageCacheLanguagesConfig splits a comma-separated language list for cache prewarm. -func ParsePageCacheLanguagesConfig(raw string) []string { - if strings.TrimSpace(raw) == "" { - return normalizePageCacheLanguages("en", nil) - } - return normalizePageCacheLanguages("", strings.Split(raw, ",")) -} - // RecommendedTendersPageCacheKeyPrefix is shared with company invalidation logic. const RecommendedTendersPageCacheKeyPrefix = recommendedTendersPageCacheKeyPrefix diff --git a/internal/tender/recommendation_page_cache_test.go b/internal/tender/recommendation_page_cache_test.go index e0e6d1b..356ea01 100644 --- a/internal/tender/recommendation_page_cache_test.go +++ b/internal/tender/recommendation_page_cache_test.go @@ -42,13 +42,3 @@ func TestFilterCachedRecommendedTendersAppliesActiveAndRejectedFilters(t *testin t.Fatalf("got ID %q, want active tender", got[0].ID) } } - -func TestParsePageCacheLanguagesConfig(t *testing.T) { - got := ParsePageCacheLanguagesConfig("en,fr, en") - if len(got) != 2 { - t.Fatalf("len = %d, want 2", len(got)) - } - if got[0] != "en" || got[1] != "fr" { - t.Fatalf("got %v, want [en fr]", got) - } -} diff --git a/internal/tender/service.go b/internal/tender/service.go index ed55f7a..205d2b0 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -92,7 +92,6 @@ type tenderService struct { aiSummarizerStorage AISummarizerStorage redisClient redis.Client pageCacheTTL time.Duration - pageCacheLanguages []string defaultLanguage string } @@ -117,7 +116,6 @@ func NewService( aiStorage AISummarizerStorage, redisClient redis.Client, recommendationPageCacheTTL time.Duration, - recommendationPageCacheLanguages []string, defaultLanguage string, ) Service { if strings.TrimSpace(defaultLanguage) == "" { @@ -134,7 +132,6 @@ func NewService( aiSummarizerStorage: aiStorage, redisClient: redisClient, pageCacheTTL: recommendationPageCacheTTL, - pageCacheLanguages: normalizePageCacheLanguages(defaultLanguage, recommendationPageCacheLanguages), defaultLanguage: strings.ToLower(defaultLanguage), } } @@ -999,7 +996,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat } if len(companyIDs) == 0 && companyID != "" { - s.ScheduleRefreshRecommendedTendersPageCache(companyID) + s.schedulePageCacheRefreshForLanguage(companyID, lang) } excluded, excludedByCompany, err := s.loadRecommendationExclusions(ctx, form, companyID, companyIDs) @@ -1030,7 +1027,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat return nil, err } - paged := s.buildRecommendedTenderListResponses(pageResult.items, lang) + paged := s.buildRecommendedTenderResponsesParallel(ctx, pageResult.items, lang) return &SearchResponse{ Tenders: paged,