51a1a6aa82
continuous-integration/drone/push Build is passing
- Updated comments and logging messages in the worker and related files to replace "daily-run" with "auto run" for clarity and consistency. - Adjusted the `WorkerConfig` struct to reflect the new terminology in configuration settings. - Renamed functions and test cases to align with the updated terminology, enhancing code readability and maintainability. This change improves the clarity of the AI pipeline's functionality within the tender management system.
222 lines
6.3 KiB
Go
222 lines
6.3 KiB
Go
package company
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"tm/pkg/ai_summarizer"
|
|
)
|
|
|
|
// AIPipelineStatusClient reports pipeline job status from the AI service.
|
|
type AIPipelineStatusClient interface {
|
|
GetPipelineLastAutoRun(ctx context.Context) (*ai_summarizer.PipelineReportResponse, error)
|
|
}
|
|
|
|
const (
|
|
recommendationPipelineWaitInitialDelay = 5 * time.Minute
|
|
recommendationPipelineWaitMaxAttempts = 24
|
|
recommendationRefreshConcurrency = 3
|
|
)
|
|
|
|
// ScheduleRefreshCachedAIRecommendationsAfterPipeline waits for the AI pipeline auto run to finish,
|
|
// then re-fetches ranked tenders for every company that already has a recommendation cache.
|
|
func (s *companyService) ScheduleRefreshCachedAIRecommendationsAfterPipeline() {
|
|
if s.aiRecommendationClient == nil {
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
ctx := context.Background()
|
|
s.logger.Info("Scheduling AI recommendation cache refresh after pipeline sync", map[string]interface{}{})
|
|
|
|
if err := s.refreshCachedAIRecommendationsAfterPipeline(ctx); err != nil {
|
|
s.logger.Error("AI recommendation cache refresh after pipeline failed", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
s.logger.Info("AI recommendation cache refresh after pipeline completed", map[string]interface{}{})
|
|
}()
|
|
}
|
|
|
|
func (s *companyService) refreshCachedAIRecommendationsAfterPipeline(ctx context.Context) error {
|
|
if err := s.waitForPipelineAutoCompletion(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
companyIDs, err := s.listCompanyIDsWithRecommendationCache(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(companyIDs) == 0 {
|
|
s.logger.Info("No companies with recommendation cache to refresh after pipeline", map[string]interface{}{})
|
|
return nil
|
|
}
|
|
|
|
s.logger.Info("Refreshing AI recommendation caches after pipeline", map[string]interface{}{
|
|
"company_count": len(companyIDs),
|
|
})
|
|
|
|
group, groupCtx := errgroup.WithContext(ctx)
|
|
group.SetLimit(recommendationRefreshConcurrency)
|
|
|
|
var refreshed atomic.Int32
|
|
var failed atomic.Int32
|
|
for _, companyID := range companyIDs {
|
|
companyID := companyID
|
|
group.Go(func() error {
|
|
if err := s.refreshAIRecommendationsCache(groupCtx, companyID); err != nil {
|
|
s.logger.Error("Failed to refresh AI recommendation cache for company", map[string]interface{}{
|
|
"company_id": companyID,
|
|
"error": err.Error(),
|
|
})
|
|
failed.Add(1)
|
|
return nil
|
|
}
|
|
refreshed.Add(1)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
if err := group.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.logger.Info("AI recommendation cache refresh summary", map[string]interface{}{
|
|
"company_count": len(companyIDs),
|
|
"refreshed": refreshed.Load(),
|
|
"failed": failed.Load(),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (s *companyService) waitForPipelineAutoCompletion(ctx context.Context) error {
|
|
if s.aiPipelineStatusClient == nil {
|
|
s.logger.Info("AI pipeline status client not configured; waiting before recommendation refresh", map[string]interface{}{
|
|
"delay_sec": int(recommendationPipelineWaitInitialDelay.Seconds()),
|
|
})
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(recommendationPipelineWaitInitialDelay):
|
|
}
|
|
return nil
|
|
}
|
|
|
|
delay := recommendationPipelineWaitInitialDelay
|
|
for attempt := 1; attempt <= recommendationPipelineWaitMaxAttempts; attempt++ {
|
|
if attempt == 1 {
|
|
s.logger.Info("Waiting before first AI pipeline status check", map[string]interface{}{
|
|
"delay_sec": int(delay.Seconds()),
|
|
})
|
|
} else {
|
|
s.logger.Info("Retrying AI pipeline status check", map[string]interface{}{
|
|
"attempt": attempt,
|
|
"delay_sec": int(delay.Seconds()),
|
|
})
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(delay):
|
|
}
|
|
|
|
if attempt > 1 {
|
|
delay *= 2
|
|
if delay > 30*time.Minute {
|
|
delay = 30 * time.Minute
|
|
}
|
|
}
|
|
|
|
report, err := s.aiPipelineStatusClient.GetPipelineLastAutoRun(ctx)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to read AI pipeline last-auto-run status", map[string]interface{}{
|
|
"attempt": attempt,
|
|
"error": err.Error(),
|
|
})
|
|
if attempt == recommendationPipelineWaitMaxAttempts {
|
|
s.logger.Warn("Proceeding with recommendation refresh without pipeline status", map[string]interface{}{})
|
|
return nil
|
|
}
|
|
continue
|
|
}
|
|
|
|
if !isPipelineRunInProgress(report) {
|
|
s.logger.Info("AI pipeline auto run finished; refreshing recommendation caches", map[string]interface{}{
|
|
"attempt": attempt,
|
|
"status": strings.TrimSpace(report.Status),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
if attempt == recommendationPipelineWaitMaxAttempts {
|
|
s.logger.Warn("AI pipeline still in progress after max wait; refreshing recommendation caches anyway", map[string]interface{}{
|
|
"attempts": attempt,
|
|
"status": strings.TrimSpace(report.Status),
|
|
})
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isPipelineRunInProgress(report *ai_summarizer.PipelineReportResponse) bool {
|
|
if report == nil {
|
|
return true
|
|
}
|
|
status := strings.ToLower(strings.TrimSpace(report.Status))
|
|
switch status {
|
|
case "", "running", "in_progress", "started", "processing", "pending", "queued":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (s *companyService) listCompanyIDsWithRecommendationCache(ctx context.Context) ([]string, error) {
|
|
allIDs, err := s.repository.ListIDs(ctx)
|
|
if err != nil {
|
|
s.logger.Error("Failed to list companies for recommendation refresh", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
cached := make([]string, 0, len(allIDs))
|
|
for _, companyID := range allIDs {
|
|
if _, ok := s.getCachedAIRecommendations(ctx, companyID); ok {
|
|
cached = append(cached, companyID)
|
|
}
|
|
}
|
|
return cached, nil
|
|
}
|
|
|
|
// refreshAIRecommendationsCache updates Redis when the AI service returns a non-empty ranked list.
|
|
// Empty or failed fetches leave the existing cache untouched.
|
|
func (s *companyService) refreshAIRecommendationsCache(ctx context.Context, companyID string) error {
|
|
responses, err := s.fetchAIRecommendations(ctx, companyID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(responses) == 0 {
|
|
s.logger.Info("Skipping recommendation cache update after pipeline: AI returned empty list", map[string]interface{}{
|
|
"company_id": companyID,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
s.cacheAIRecommendations(ctx, companyID, responses)
|
|
s.logger.Info("AI recommendation cache refreshed after pipeline", map[string]interface{}{
|
|
"company_id": companyID,
|
|
"count": len(responses),
|
|
})
|
|
return nil
|
|
}
|