From 04c410217086659e27ac0cff5e1311911f04599c Mon Sep 17 00:00:00 2001 From: Mazyar Date: Sun, 12 Jul 2026 21:51:59 +0330 Subject: [PATCH] Enhance documents scraped filter with cancellation handling and caching improvements - Updated `documents_scraped_filter.go` to extend the caching mechanism for contract folder IDs, increasing the cache TTL to 15 minutes to prevent premature invalidation during slow scans. - Modified the `cachedContractFolderIDsWithDocuments` method to detach from the caller's cancellation, ensuring that shared scans are not aborted by client disconnects or timeouts. - Introduced a new test, `TestCachedContractFolderIDsSurvivesCallerCancellation`, in `documents_scraped_filter_test.go` to validate the behavior of the caching mechanism under cancellation scenarios. - Enhanced the `scanStartedProcedureLister` to manage scan initiation and cancellation more effectively, improving the robustness of the document retrieval process. This update improves the reliability and performance of the tender management system's document filtering capabilities, particularly in scenarios with long-running scans. --- internal/tender/documents_scraped_filter.go | 7 ++- .../tender/documents_scraped_filter_test.go | 62 ++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/internal/tender/documents_scraped_filter.go b/internal/tender/documents_scraped_filter.go index 8265fbc..899c0cc 100644 --- a/internal/tender/documents_scraped_filter.go +++ b/internal/tender/documents_scraped_filter.go @@ -12,7 +12,8 @@ import ( const ( // contractFoldersWithDocsCacheTTL avoids scanning MinIO on every documents_scraped list request. - contractFoldersWithDocsCacheTTL = 60 * time.Second + // Kept >= resolve timeout so a slow scan is not immediately invalidated for waiters. + contractFoldersWithDocsCacheTTL = 15 * time.Minute // contractFoldersWithDocsResolveTimeout bounds a single MinIO procedure scan. contractFoldersWithDocsResolveTimeout = 3 * time.Minute ) @@ -65,7 +66,9 @@ func (s *tenderService) cachedContractFolderIDsWithDocuments(ctx context.Context } s.contractFoldersCacheMu.Unlock() - resolveCtx, cancel := context.WithTimeout(ctx, contractFoldersWithDocsResolveTimeout) + // Detach from the caller's cancellation so a client disconnect or proxy timeout + // does not abort the shared singleflight scan for every waiter. + resolveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), contractFoldersWithDocsResolveTimeout) defer cancel() procedures, listErr := s.listProceduresWithDocuments(resolveCtx) diff --git a/internal/tender/documents_scraped_filter_test.go b/internal/tender/documents_scraped_filter_test.go index 33e8eaa..f208593 100644 --- a/internal/tender/documents_scraped_filter_test.go +++ b/internal/tender/documents_scraped_filter_test.go @@ -29,7 +29,9 @@ func (m *mockProcedureDocumentLister) ListProceduresWithDocuments(context.Contex } type stubProcedureDocumentStorage struct { - lister *mockProcedureDocumentLister + lister interface { + ListProceduresWithDocuments(ctx context.Context) ([]ai_summarizer.ProcedureDocumentsSummary, error) + } } func (s stubProcedureDocumentStorage) GetSummaryFromStorage(context.Context, string, string) (string, error) { @@ -156,6 +158,64 @@ func TestContractFolderIDsFromProceduresDedupesAndSkipsEmpty(t *testing.T) { } } +type scanStartedProcedureLister struct { + mockProcedureDocumentLister + started chan struct{} +} + +func (m *scanStartedProcedureLister) ListProceduresWithDocuments(ctx context.Context) ([]ai_summarizer.ProcedureDocumentsSummary, error) { + if m.started != nil { + select { + case <-m.started: + default: + close(m.started) + } + } + time.Sleep(50 * time.Millisecond) + if err := ctx.Err(); err != nil { + return nil, err + } + return m.mockProcedureDocumentLister.ListProceduresWithDocuments(ctx) +} + +func TestCachedContractFolderIDsSurvivesCallerCancellation(t *testing.T) { + started := make(chan struct{}) + lister := &scanStartedProcedureLister{ + mockProcedureDocumentLister: mockProcedureDocumentLister{ + procedures: []ai_summarizer.ProcedureDocumentsSummary{ + {ContractFolderID: "folder-1", DocumentCount: 1}, + }, + }, + started: started, + } + svc := &tenderService{aiSummarizerStorage: stubProcedureDocumentStorage{lister: lister}} + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + errCh := make(chan error, 1) + var ids []string + go func() { + var err error + ids, err = svc.cachedContractFolderIDsWithDocuments(ctx) + errCh <- err + }() + + select { + case <-started: + case <-time.After(2 * time.Second): + t.Fatal("MinIO scan did not start") + } + cancel() + + if err := <-errCh; err != nil { + t.Fatalf("expected shared scan to finish despite caller cancel: %v", err) + } + if len(ids) != 1 || ids[0] != "folder-1" { + t.Fatalf("unexpected folder ids: %#v", ids) + } +} + func TestContractFoldersWithDocsCacheExpires(t *testing.T) { lister := &mockProcedureDocumentLister{ procedures: []ai_summarizer.ProcedureDocumentsSummary{