Refactor AI recommendation fetching and caching in company service
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Introduced retry logic for fetching AI recommendations after onboarding, enhancing reliability in recommendation retrieval. - Updated logging levels for better observability, changing cache miss logs to Info level. - Renamed methods for clarity, replacing `refreshAIRecommendationsCacheAsync` with `scheduleRecommendationRefreshAfterOnboarding` and `fetchAndCacheAIRecommendations` with `fetchAIRecommendations`. - Implemented a mechanism to clear the cache if no recommendations are returned, improving cache management. This update optimizes the AI recommendation process, ensuring more robust handling of recommendation fetching and caching during onboarding.
This commit is contained in:
@@ -19,6 +19,11 @@ var ErrAIRecommendationNotConfigured = errors.New("AI recommendation service is
|
||||
|
||||
const aiRecommendationCacheKeyPrefix = "ai:recommendations:"
|
||||
|
||||
const (
|
||||
recommendationFetchMaxAttempts = 6
|
||||
recommendationFetchInitialDelay = 10 * time.Second
|
||||
)
|
||||
|
||||
func aiRecommendationCacheKey(companyID string) string {
|
||||
return aiRecommendationCacheKeyPrefix + companyID
|
||||
}
|
||||
@@ -53,7 +58,7 @@ func (s *companyService) StartAIOnboarding(ctx context.Context, companyID string
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.refreshAIRecommendationsCacheAsync(companyID)
|
||||
s.scheduleRecommendationRefreshAfterOnboarding(companyID)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -86,7 +91,7 @@ func (s *companyService) triggerAIOnboardingAsync(companyID string) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.fetchAndCacheAIRecommendations(ctx, companyID); err != nil {
|
||||
if err := s.fetchAndCacheAIRecommendationsWithRetry(ctx, companyID); err != nil {
|
||||
s.logger.Error("Failed to refresh AI recommendations after onboarding", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"error": err.Error(),
|
||||
@@ -170,14 +175,14 @@ func (s *companyService) GetAIRecommendations(ctx context.Context, companyID str
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
s.logger.Debug("AI recommendations cache miss; returning empty list", map[string]interface{}{
|
||||
s.logger.Info("AI recommendations cache miss; returning empty list", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
})
|
||||
|
||||
return []RecommendedTenderResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *companyService) refreshAIRecommendationsCacheAsync(companyID string) {
|
||||
func (s *companyService) scheduleRecommendationRefreshAfterOnboarding(companyID string) {
|
||||
if s.aiRecommendationClient == nil {
|
||||
return
|
||||
}
|
||||
@@ -189,33 +194,76 @@ func (s *companyService) refreshAIRecommendationsCacheAsync(companyID string) {
|
||||
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
s.logger.Info("Refreshing AI recommendation cache in background", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
})
|
||||
|
||||
if _, err := s.repository.GetByID(ctx, companyID); err != nil {
|
||||
s.logger.Error("Failed to load company for AI recommendation refresh", map[string]interface{}{
|
||||
if err := s.fetchAndCacheAIRecommendationsWithRetry(ctx, companyID); err != nil {
|
||||
s.logger.Error("Failed to refresh AI recommendation cache after onboarding", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.fetchAndCacheAIRecommendations(ctx, companyID); err != nil {
|
||||
s.logger.Error("Failed to refresh AI recommendation cache", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info("AI recommendation cache refreshed", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *companyService) fetchAndCacheAIRecommendations(ctx context.Context, companyID string) ([]RecommendedTenderResponse, error) {
|
||||
// fetchAndCacheAIRecommendationsWithRetry waits for the AI service to finish indexing after onboarding.
|
||||
// Empty results are not cached so reads keep returning empty until real recommendations exist.
|
||||
func (s *companyService) fetchAndCacheAIRecommendationsWithRetry(ctx context.Context, companyID string) error {
|
||||
delay := recommendationFetchInitialDelay
|
||||
|
||||
for attempt := 1; attempt <= recommendationFetchMaxAttempts; attempt++ {
|
||||
if attempt > 1 {
|
||||
s.logger.Info("Retrying AI recommendation fetch after onboarding", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"attempt": attempt,
|
||||
"delay_sec": int(delay.Seconds()),
|
||||
})
|
||||
} else {
|
||||
s.logger.Info("Waiting before first AI recommendation fetch after onboarding", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"delay_sec": int(delay.Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(delay):
|
||||
}
|
||||
|
||||
if attempt > 1 {
|
||||
delay *= 2
|
||||
}
|
||||
|
||||
responses, err := s.fetchAIRecommendations(ctx, companyID)
|
||||
if err != nil {
|
||||
if attempt == recommendationFetchMaxAttempts {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(responses) == 0 {
|
||||
if attempt == recommendationFetchMaxAttempts {
|
||||
s.logger.Warn("AI returned no recommendations after onboarding retries; cache not updated", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"attempts": attempt,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
s.cacheAIRecommendations(ctx, companyID, responses)
|
||||
s.logger.Info("AI recommendation cache populated after onboarding", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"count": len(responses),
|
||||
"attempt": attempt,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *companyService) fetchAIRecommendations(ctx context.Context, companyID string) ([]RecommendedTenderResponse, error) {
|
||||
s.logger.Info("Fetching AI tender recommendations from AI service", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
})
|
||||
@@ -240,8 +288,6 @@ func (s *companyService) fetchAndCacheAIRecommendations(ctx context.Context, com
|
||||
})
|
||||
}
|
||||
|
||||
s.cacheAIRecommendations(ctx, companyID, responses)
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
@@ -271,6 +317,11 @@ func (s *companyService) getCachedAIRecommendations(ctx context.Context, company
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if len(responses) == 0 {
|
||||
_ = s.redisClient.Del(ctx, aiRecommendationCacheKey(companyID))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
s.logger.Debug("AI tender recommendations served from cache", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"count": len(responses),
|
||||
@@ -280,7 +331,7 @@ func (s *companyService) getCachedAIRecommendations(ctx context.Context, company
|
||||
}
|
||||
|
||||
func (s *companyService) cacheAIRecommendations(ctx context.Context, companyID string, responses []RecommendedTenderResponse) {
|
||||
if s.redisClient == nil {
|
||||
if s.redisClient == nil || len(responses) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user