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,