Refactor recommendation page cache handling and configuration
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- 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.
This commit is contained in:
@@ -73,8 +73,6 @@ type AISummarizerConfig struct {
|
|||||||
// RecommendationCacheTTL is an optional max Redis TTL for cached recommendations.
|
// RecommendationCacheTTL is an optional max Redis TTL for cached recommendations.
|
||||||
// When 0 (default), entries persist until the company is created, updated, or onboarded.
|
// When 0 (default), entries persist until the company is created, updated, or onboarded.
|
||||||
RecommendationCacheTTL time.Duration `env:"AI_RECOMMENDATION_CACHE_TTL" envDefault:"0"`
|
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
|
// MinIO storage settings
|
||||||
MinioEndpoint string `env:"AI_SUMMARIZER_MINIO_ENDPOINT" envDefault:""`
|
MinioEndpoint string `env:"AI_SUMMARIZER_MINIO_ENDPOINT" envDefault:""`
|
||||||
|
|||||||
@@ -241,12 +241,6 @@ func main() {
|
|||||||
userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK, redisClient, auditLogger)
|
userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK, redisClient, auditLogger)
|
||||||
categoryService := company_category.NewService(categoryRepository, logger)
|
categoryService := company_category.NewService(categoryRepository, logger)
|
||||||
companyService := company.NewService(companyRepository, categoryService, fileStoreService, aiRecommendationClient, aiPipelineClient, redisClient, conf.AISummarizer.RecommendationCacheTTL, 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)
|
customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK, customerValidator, auditLogger)
|
||||||
tenderService := tender.NewService(
|
tenderService := tender.NewService(
|
||||||
tenderRepository,
|
tenderRepository,
|
||||||
@@ -258,7 +252,6 @@ func main() {
|
|||||||
aiSummarizerStorage,
|
aiSummarizerStorage,
|
||||||
redisClient,
|
redisClient,
|
||||||
conf.AISummarizer.RecommendationCacheTTL,
|
conf.AISummarizer.RecommendationCacheTTL,
|
||||||
pageCacheLanguages,
|
|
||||||
conf.AISummarizer.DefaultLanguage,
|
conf.AISummarizer.DefaultLanguage,
|
||||||
)
|
)
|
||||||
if configurer, ok := companyService.(interface {
|
if configurer, ok := companyService.(interface {
|
||||||
|
|||||||
@@ -121,11 +121,6 @@ func InitWorker(config Config, mongoManager *mongo.ConnectionManager, appLogger
|
|||||||
config.AISummarizer.RecommendationCacheTTL,
|
config.AISummarizer.RecommendationCacheTTL,
|
||||||
appLogger,
|
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{}{})
|
appLogger.Info("Company recommendation refresh after pipeline enabled", map[string]interface{}{})
|
||||||
} else if config.Worker.RecommendationRefreshAfterPipelineEnabled {
|
} else if config.Worker.RecommendationRefreshAfterPipelineEnabled {
|
||||||
appLogger.Warn("Company recommendation refresh after pipeline disabled: AI client or Redis unavailable", map[string]interface{}{})
|
appLogger.Warn("Company recommendation refresh after pipeline disabled: AI client or Redis unavailable", map[string]interface{}{})
|
||||||
|
|||||||
@@ -60,5 +60,4 @@ type AISummarizerConfig struct {
|
|||||||
MinioRegion string `env:"AI_SUMMARIZER_MINIO_REGION" envDefault:"us-east-1"`
|
MinioRegion string `env:"AI_SUMMARIZER_MINIO_REGION" envDefault:"us-east-1"`
|
||||||
MinioBucket string `env:"AI_SUMMARIZER_MINIO_BUCKET" envDefault:"opplens"`
|
MinioBucket string `env:"AI_SUMMARIZER_MINIO_BUCKET" envDefault:"opplens"`
|
||||||
RecommendationCacheTTL time.Duration `env:"AI_RECOMMENDATION_CACHE_TTL" envDefault:"0"`
|
RecommendationCacheTTL time.Duration `env:"AI_RECOMMENDATION_CACHE_TTL" envDefault:"0"`
|
||||||
RecommendationPageCacheLanguages string `env:"AI_RECOMMENDATION_PAGE_CACHE_LANGUAGES" envDefault:"en"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ package company
|
|||||||
// Implemented by the tender service on the web API; optional on the worker.
|
// Implemented by the tender service on the web API; optional on the worker.
|
||||||
type RecommendedTendersPageCacheRefresher interface {
|
type RecommendedTendersPageCacheRefresher interface {
|
||||||
ScheduleRefreshRecommendedTendersPageCache(companyID string)
|
ScheduleRefreshRecommendedTendersPageCache(companyID string)
|
||||||
|
InvalidateRecommendedTendersPageCache(companyID string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ func (s *companyService) cacheAIRecommendations(ctx context.Context, companyID s
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.invalidateRecommendedTendersPageCache(ctx, companyID)
|
s.invalidateRecommendedTendersPageCache(companyID)
|
||||||
s.scheduleRecommendedTendersPageCacheRefresh(companyID)
|
s.scheduleRecommendedTendersPageCacheRefresh(companyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,7 +489,7 @@ func (s *companyService) invalidateAIRecommendationCache(ctx context.Context, co
|
|||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
s.invalidateRecommendedTendersPageCache(ctx, companyID)
|
s.invalidateRecommendedTendersPageCache(companyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *companyService) buildOnboardingDocuments(fileIDs []string) (string, error) {
|
func (s *companyService) buildOnboardingDocuments(fileIDs []string) (string, error) {
|
||||||
|
|||||||
@@ -1,30 +1,10 @@
|
|||||||
package company
|
package company
|
||||||
|
|
||||||
import (
|
func (s *companyService) invalidateRecommendedTendersPageCache(companyID string) {
|
||||||
"context"
|
if s.pageCacheRefresher == nil {
|
||||||
"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 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.pageCacheRefresher.InvalidateRecommendedTendersPageCache(companyID)
|
||||||
for _, language := range s.pageCacheLanguages {
|
|
||||||
lang := strings.ToLower(strings.TrimSpace(language))
|
|
||||||
if lang == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_ = s.redisClient.Del(ctx, recommendedTendersPageCacheKey(companyID, lang))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *companyService) scheduleRecommendedTendersPageCacheRefresh(companyID string) {
|
func (s *companyService) scheduleRecommendedTendersPageCacheRefresh(companyID string) {
|
||||||
@@ -38,34 +18,3 @@ func (s *companyService) scheduleRecommendedTendersPageCacheRefresh(companyID st
|
|||||||
func (s *companyService) SetRecommendedTendersPageCacheRefresher(refresher RecommendedTendersPageCacheRefresher) {
|
func (s *companyService) SetRecommendedTendersPageCacheRefresher(refresher RecommendedTendersPageCacheRefresher) {
|
||||||
s.pageCacheRefresher = refresher
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ type companyService struct {
|
|||||||
pageCacheRefresher RecommendedTendersPageCacheRefresher
|
pageCacheRefresher RecommendedTendersPageCacheRefresher
|
||||||
redisClient redis.Client
|
redisClient redis.Client
|
||||||
recommendationCacheTTL time.Duration
|
recommendationCacheTTL time.Duration
|
||||||
pageCacheLanguages []string
|
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,29 +35,40 @@ func recommendedTendersPageCacheKey(companyID, language string) string {
|
|||||||
strings.ToLower(strings.TrimSpace(language))
|
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) {
|
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)
|
companyID = strings.TrimSpace(companyID)
|
||||||
if companyID == "" || s.redisClient == nil {
|
if companyID == "" || s.redisClient == nil {
|
||||||
return
|
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() {
|
go func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), recommendationPageCacheBuildTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), recommendationPageCacheBuildTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
for _, language := range s.pageCacheLanguages {
|
if err := s.buildRecommendedTendersPageCache(ctx, companyID, language); err != nil {
|
||||||
lang := strings.ToLower(strings.TrimSpace(language))
|
s.logger.Warn("Failed to refresh recommended tenders page cache", map[string]interface{}{
|
||||||
if lang == "" {
|
"company_id": companyID,
|
||||||
continue
|
"language": language,
|
||||||
}
|
"error": err.Error(),
|
||||||
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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -380,40 +391,6 @@ func tenderFromRecommendationResponse(item TenderResponse) Tender {
|
|||||||
return 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.
|
// RecommendedTendersPageCacheKeyPrefix is shared with company invalidation logic.
|
||||||
const RecommendedTendersPageCacheKeyPrefix = recommendedTendersPageCacheKeyPrefix
|
const RecommendedTendersPageCacheKeyPrefix = recommendedTendersPageCacheKeyPrefix
|
||||||
|
|
||||||
|
|||||||
@@ -42,13 +42,3 @@ func TestFilterCachedRecommendedTendersAppliesActiveAndRejectedFilters(t *testin
|
|||||||
t.Fatalf("got ID %q, want active tender", got[0].ID)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ type tenderService struct {
|
|||||||
aiSummarizerStorage AISummarizerStorage
|
aiSummarizerStorage AISummarizerStorage
|
||||||
redisClient redis.Client
|
redisClient redis.Client
|
||||||
pageCacheTTL time.Duration
|
pageCacheTTL time.Duration
|
||||||
pageCacheLanguages []string
|
|
||||||
defaultLanguage string
|
defaultLanguage string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +116,6 @@ func NewService(
|
|||||||
aiStorage AISummarizerStorage,
|
aiStorage AISummarizerStorage,
|
||||||
redisClient redis.Client,
|
redisClient redis.Client,
|
||||||
recommendationPageCacheTTL time.Duration,
|
recommendationPageCacheTTL time.Duration,
|
||||||
recommendationPageCacheLanguages []string,
|
|
||||||
defaultLanguage string,
|
defaultLanguage string,
|
||||||
) Service {
|
) Service {
|
||||||
if strings.TrimSpace(defaultLanguage) == "" {
|
if strings.TrimSpace(defaultLanguage) == "" {
|
||||||
@@ -134,7 +132,6 @@ func NewService(
|
|||||||
aiSummarizerStorage: aiStorage,
|
aiSummarizerStorage: aiStorage,
|
||||||
redisClient: redisClient,
|
redisClient: redisClient,
|
||||||
pageCacheTTL: recommendationPageCacheTTL,
|
pageCacheTTL: recommendationPageCacheTTL,
|
||||||
pageCacheLanguages: normalizePageCacheLanguages(defaultLanguage, recommendationPageCacheLanguages),
|
|
||||||
defaultLanguage: strings.ToLower(defaultLanguage),
|
defaultLanguage: strings.ToLower(defaultLanguage),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -999,7 +996,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(companyIDs) == 0 && companyID != "" {
|
if len(companyIDs) == 0 && companyID != "" {
|
||||||
s.ScheduleRefreshRecommendedTendersPageCache(companyID)
|
s.schedulePageCacheRefreshForLanguage(companyID, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
excluded, excludedByCompany, err := s.loadRecommendationExclusions(ctx, form, companyID, companyIDs)
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
paged := s.buildRecommendedTenderListResponses(pageResult.items, lang)
|
paged := s.buildRecommendedTenderResponsesParallel(ctx, pageResult.items, lang)
|
||||||
|
|
||||||
return &SearchResponse{
|
return &SearchResponse{
|
||||||
Tenders: paged,
|
Tenders: paged,
|
||||||
|
|||||||
Reference in New Issue
Block a user