From d07e1c9cf07da405be13ebd3e9001f02d0a23c84 Mon Sep 17 00:00:00 2001 From: Mazyar Date: Mon, 8 Jun 2026 20:24:02 +0330 Subject: [PATCH] Fix summarizer worker infinite loop and map AI trigger errors to safe HTTP responses. Paginate un-summarized tenders and mark rows missing notice/folder IDs as handled so the worker cannot spin forever. Return 400/404 for validation and not-found cases on AI summarize/analyze triggers instead of leaking internal errors as 500. --- cmd/worker/workers/summarizer.go | 11 ++++++++++- internal/tender/errors.go | 13 +++++++++++++ internal/tender/handler.go | 20 +++++++++++++++++-- internal/tender/service.go | 33 ++++++++++++++++++++++---------- 4 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 internal/tender/errors.go diff --git a/cmd/worker/workers/summarizer.go b/cmd/worker/workers/summarizer.go index 51f80f7..dcc352f 100644 --- a/cmd/worker/workers/summarizer.go +++ b/cmd/worker/workers/summarizer.go @@ -54,8 +54,9 @@ func (w *DocumentSummarizationWorker) Run() { } limit := 5 + skip := 0 for { - tenders, totalCount, err := w.TenderRepo.GetUnSummarizedTenders(context.Background(), limit, 0) + tenders, totalCount, err := w.TenderRepo.GetUnSummarizedTenders(context.Background(), limit, skip) if err != nil { w.Logger.Error("Failed to get un-summarized tenders", map[string]interface{}{ "error": err.Error(), @@ -66,6 +67,7 @@ func (w *DocumentSummarizationWorker) Run() { w.Logger.Info("Found tenders for document summarization", map[string]interface{}{ "count": len(tenders), "total_count": totalCount, + "skip": skip, }) if len(tenders) == 0 { @@ -80,6 +82,11 @@ func (w *DocumentSummarizationWorker) Run() { }) w.summarizeTender(t) } + + skip += len(tenders) + if int64(skip) >= totalCount { + break + } } w.Logger.Info("Document summarization worker completed", map[string]interface{}{}) @@ -90,12 +97,14 @@ func (w *DocumentSummarizationWorker) summarizeTender(t *tender.Tender) { w.Logger.Warn("Skipping tender summarization: missing notice_publication_id", map[string]interface{}{ "tender_id": t.ID.Hex(), }) + w.markTenderSummarized(t) return } if strings.TrimSpace(t.ContractFolderID) == "" { w.Logger.Warn("Skipping tender summarization: missing contract_folder_id", map[string]interface{}{ "tender_id": t.ID.Hex(), }) + w.markTenderSummarized(t) return } diff --git a/internal/tender/errors.go b/internal/tender/errors.go new file mode 100644 index 0000000..a3f3486 --- /dev/null +++ b/internal/tender/errors.go @@ -0,0 +1,13 @@ +package tender + +import "errors" + +// Sentinel errors for AI trigger operations and HTTP mapping. +var ( + ErrEmptyTenderID = errors.New("tender: id is required") + ErrInvalidTenderID = errors.New("tender: invalid id") + ErrTenderNotFound = errors.New("tender: not found") + ErrTenderMissingNoticeID = errors.New("tender: missing notice publication id") + ErrTenderMissingContractFolderID = errors.New("tender: missing contract folder id") + ErrAINotConfigured = errors.New("tender: ai service not configured") +) diff --git a/internal/tender/handler.go b/internal/tender/handler.go index c239f1f..dc092c3 100644 --- a/internal/tender/handler.go +++ b/internal/tender/handler.go @@ -589,7 +589,7 @@ func (h *TenderHandler) TriggerAISummarize(c echo.Context) error { "tender_id": id, "error": err.Error(), }) - return response.InternalServerError(c, err.Error()) + return mapAITriggerHTTPError(c, err, "AI summarization") } return response.Success(c, result, "AI summarization completed successfully") @@ -619,12 +619,28 @@ func (h *TenderHandler) TriggerAIAnalyze(c echo.Context) error { "tender_id": id, "error": err.Error(), }) - return response.InternalServerError(c, err.Error()) + return mapAITriggerHTTPError(c, err, "AI analysis") } return response.Success(c, result, "AI analysis completed successfully") } +// mapAITriggerHTTPError maps AI trigger service errors to safe HTTP responses. +func mapAITriggerHTTPError(c echo.Context, err error, action string) error { + switch { + case errors.Is(err, ErrEmptyTenderID), errors.Is(err, ErrInvalidTenderID): + return response.BadRequest(c, "Invalid request", "Invalid tender ID") + case errors.Is(err, ErrTenderNotFound), errors.Is(err, orm.ErrDocumentNotFound): + return response.NotFound(c, "Tender not found") + case errors.Is(err, ErrTenderMissingNoticeID), errors.Is(err, ErrTenderMissingContractFolderID): + return response.BadRequest(c, "Tender not eligible", "Tender is missing required identifiers for AI processing") + case errors.Is(err, ErrAINotConfigured): + return response.InternalServerError(c, "AI service is not available") + default: + return response.InternalServerError(c, action+" failed") + } +} + // TriggerAITranslate triggers on-demand AI translation for a tender // @Summary Trigger AI translation // @Description Trigger on-demand AI translation for tender title and description (cached in MinIO by the AI service) diff --git a/internal/tender/service.go b/internal/tender/service.go index 44cd986..840c9e2 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -15,6 +15,7 @@ import ( "tm/internal/customer" "tm/pkg/ai_summarizer" "tm/pkg/logger" + orm "tm/pkg/mongo" "tm/pkg/response" ) @@ -1187,11 +1188,11 @@ func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummary // It calls the external AI service HTTP API to generate summaries. func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AISummarizeResponse, error) { if id == "" { - return nil, fmt.Errorf("tender ID cannot be empty") + return nil, ErrEmptyTenderID } if s.aiSummarizerClient == nil { - return nil, fmt.Errorf("AI summarizer service is not configured") + return nil, ErrAINotConfigured } // Get tender to build the request @@ -1201,18 +1202,24 @@ func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AIS "tender_id": id, "error": err.Error(), }) + if errors.Is(err, orm.ErrDocumentNotFound) { + return nil, ErrTenderNotFound + } + if strings.Contains(err.Error(), "invalid id format") { + return nil, ErrInvalidTenderID + } return nil, fmt.Errorf("failed to get tender: %w", err) } if t == nil { - return nil, fmt.Errorf("tender not found") + return nil, ErrTenderNotFound } if t.NoticePublicationID == "" { - return nil, fmt.Errorf("tender has no notice publication ID") + return nil, ErrTenderMissingNoticeID } if strings.TrimSpace(t.ContractFolderID) == "" { - return nil, fmt.Errorf("tender has no contract folder ID") + return nil, ErrTenderMissingContractFolderID } reqBody := ai_summarizer.NewSummarizeRequest(t.ContractFolderID, t.NoticePublicationID) @@ -1274,10 +1281,10 @@ func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AIS // TriggerAIAnalyze triggers on-demand agentic analysis for a tender. func (s *tenderService) TriggerAIAnalyze(ctx context.Context, id string) (*AIAnalyzeResponse, error) { if id == "" { - return nil, fmt.Errorf("tender ID cannot be empty") + return nil, ErrEmptyTenderID } if s.aiSummarizerClient == nil { - return nil, fmt.Errorf("AI summarizer service is not configured") + return nil, ErrAINotConfigured } t, err := s.repository.GetByID(ctx, id) @@ -1286,16 +1293,22 @@ func (s *tenderService) TriggerAIAnalyze(ctx context.Context, id string) (*AIAna "tender_id": id, "error": err.Error(), }) + if errors.Is(err, orm.ErrDocumentNotFound) { + return nil, ErrTenderNotFound + } + if strings.Contains(err.Error(), "invalid id format") { + return nil, ErrInvalidTenderID + } return nil, fmt.Errorf("failed to get tender: %w", err) } if t == nil { - return nil, fmt.Errorf("tender not found") + return nil, ErrTenderNotFound } if t.NoticePublicationID == "" { - return nil, fmt.Errorf("tender has no notice publication ID") + return nil, ErrTenderMissingNoticeID } if strings.TrimSpace(t.ContractFolderID) == "" { - return nil, fmt.Errorf("tender has no contract folder ID") + return nil, ErrTenderMissingContractFolderID } reqBody := ai_summarizer.NewAnalyzeRequest(t.ContractFolderID, t.NoticePublicationID)