Skip to content

Replace fire-and-forget PDF trigger with bounded queue#48

Merged
donaldgray merged 1 commit into
mainfrom
feature/background
May 28, 2026
Merged

Replace fire-and-forget PDF trigger with bounded queue#48
donaldgray merged 1 commit into
mainfrom
feature/background

Conversation

@donaldgray
Copy link
Copy Markdown
Member

Summary

The POST /pdf/v1/{**id} trigger endpoint previously used a bare Task.Run with no backpressure:

  • Unbounded concurrency → unbounded MemoryStream allocations under load
  • Dishonest 202 responses: if a trigger was silently dropped, the caller would wait then hit the slow synchronous GET path anyway
  • No distinct signal for "server at capacity"

Changes

New types:

  • PdfGenerationService / IPdfGenerationService — extracts shared lock + generate logic from PdfHandler; used by both the GET and background paths
  • PdfGenerationQueue / IPdfGenerationQueue — bounded Channel<string> wrapper; TryEnqueue returns false when full (BoundedChannelFullMode.Wait)
  • PdfGenerationBackgroundServiceBackgroundService draining the queue; SemaphoreSlim caps concurrent in-flight generations (and therefore peak MemoryStream memory)

Updated types:

  • PdfTriggerResult enum replaces the bool return from PdfTriggerRequest, enabling honest HTTP mapping: 202 when queued, 503 + Retry-After when queue full, 404 when no artefact (fixes a latent bug where disabled-service/no-artefact was returning 200)
  • SearchApiOptions gains PdfTriggerQueueCapacity (default 50) and PdfTriggerMaxConcurrency (default 2)
  • ServiceCollectionExtensions.AddPdfServices() consolidates PDF DI registration

Tests: PdfHandlerTests (10 tests covering all PdfTriggerResult and PdfRequest paths) and PdfGenerationQueueTests (2 capacity tests).

Test plan

  • dotnet test src/TextServices.Tests — all 477 tests pass
  • POST /pdf/v1/{id} on a valid ID → 202 with Location header
  • POST /pdf/v1/{id} when PDF already exists → 200 with location body
  • POST /pdf/v1/missing-id → 404
  • Reduce PdfTriggerQueueCapacity to 1 and PdfTriggerMaxConcurrency to 1 in dev config; fire two simultaneous POSTs to different IDs → second returns 503

🤖 Generated with Claude Code

The POST /pdf/v1/ trigger previously used Task.Run with no backpressure,
risking unbounded memory and giving dishonest 202 responses when
capacity was exceeded. Replaced with a bounded Channel<string> drained
by a BackgroundService, with a SemaphoreSlim capping concurrency.

* PdfGenerationService extracts shared lock + generate logic
* PdfGenerationQueue (IPdfGenerationQueue) owns the bounded channel;
  TryEnqueue returns false when full (BoundedChannelFullMode.Wait)
* PdfGenerationBackgroundService drains the queue; SemaphoreSlim bounds
  concurrent in-flight MemoryStream allocations
* PdfTriggerResult enum replaces bool return; 503 + Retry-After when
  queue full, fixes latent NotFound -> 200 bug on disabled services
* New PdfTriggerQueueCapacity (50) and PdfTriggerMaxConcurrency (2)
  config keys under TextServices:
* Unit tests for PdfHandler trigger result codes and queue capacity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@donaldgray donaldgray merged commit 1ad8276 into main May 28, 2026
4 checks passed
@donaldgray donaldgray deleted the feature/background branch May 28, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant