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.
This commit is contained in:
Mazyar
2026-07-12 21:51:59 +03:30
parent 0fde5772e2
commit 04c4102170
2 changed files with 66 additions and 3 deletions
+5 -2
View File
@@ -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)
@@ -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{