Skip to content

feat: Async payrun job processing to prevent HTTP timeouts#6

Closed
gsayer wants to merge 3 commits intoPayroll-Engine:mainfrom
versohq:feat/async-payrun-jobs
Closed

feat: Async payrun job processing to prevent HTTP timeouts#6
gsayer wants to merge 3 commits intoPayroll-Engine:mainfrom
versohq:feat/async-payrun-jobs

Conversation

@gsayer
Copy link
Copy Markdown
Contributor

@gsayer gsayer commented Jan 11, 2026

Summary

  • Implement asynchronous payrun job processing using Channel<T> and BackgroundService
  • Return HTTP 202 Accepted immediately with Location header for status polling
  • Prevent HTTP 524 timeout errors when processing large payrolls (500+ employees)

Problem

When starting a payrun job via POST /tenants/{tenantId}/payruns/jobs with a large number of employees, the HTTP request blocks until all payroll calculations are complete. For 1,000 employees at ~0.5-1s each, this results in 8-15+ minutes of blocking time, far exceeding typical gateway timeouts (e.g., Cloudflare's 100s limit results in HTTP 524 errors).

Solution

Decouple the HTTP response from job completion:

  1. Create the PayrunJob record immediately in "Process" status
  2. Enqueue the job for background processing via Channel<T>
  3. Return HTTP 202 Accepted with Location header pointing to status endpoint
  4. BackgroundService processes jobs from the queue asynchronously
  5. Clients poll GET /api/tenants/{id}/payruns/jobs/{jobId}/status for completion

Changes

New files

  • Domain/Domain.Application/Service/IPayrunJobQueue.cs - Queue interface
  • Domain/Domain.Application/Service/PayrunJobQueueItem.cs - Queue item DTO
  • Domain/Domain.Application/PayrunJobQueue.cs - Channel-based queue implementation
  • Backend.Server/PayrunJobWorkerService.cs - Background worker service
  • Api/Api.Core/AcceptedObjectResult.cs - HTTP 202 result helper

Modified files

  • Api/Api.Controller/PayrunJobController.cs - Async job creation + queue integration
  • Backend.Controller/PayrunJobController.cs - Updated constructor and HTTP attributes
  • Backend.Server/ProviderStartupExtensions.cs - DI registration
  • Backend.Server/Startup.cs - Queue and worker service registration

Breaking Changes

  • POST /api/tenants/{id}/payruns/jobs now returns HTTP 202 Accepted instead of HTTP 201 Created
  • Response includes Location header for status polling
  • Clients must poll status endpoint to determine job completion
  • Job is returned in "Process" status, not "Complete"

Test plan

  • Build succeeds: dotnet build PayrollEngine.Backend.sln
  • Unit tests pass: dotnet test Domain/Domain.Model.Tests/
  • POST new payrun job returns HTTP 202 with Location header
  • GET status endpoint returns current job status
  • ProcessedEmployeeCount increments during processing
  • Job status changes to Complete when finished
  • Error scenario: job marked as Abort with ErrorMessage
  • Graceful shutdown: in-progress job marked as Abort

Implement asynchronous payrun job processing using Channel<T> and BackgroundService:
- Return HTTP 202 Accepted immediately with Location header for status polling
- Process employees in background worker service
- Prevent HTTP 524 timeout errors when processing large payrolls (500+ employees)

Breaking change: POST /api/tenants/{id}/payruns/jobs now returns HTTP 202 instead of 201
@gsayer gsayer force-pushed the feat/async-payrun-jobs branch from 53e44f0 to dbfa109 Compare January 11, 2026 15:55
Your Name added 2 commits January 12, 2026 14:17
PayrunProcessor now checks if a job was pre-created by the async
controller (PayrunJobId > 0) and loads/updates it instead of always
creating a new job.

This fixes the bug where:
- Controller creates job ID=3, enqueues with PayrunJobId=3
- Processor ignored PayrunJobId, created new job ID=4
- Result: job 3 stuck in "Process", job 4 orphaned with results

Changes:
- PayrunProcessor: detect pre-created jobs and load them
- PayrunProcessor: use UpdateAsync instead of CreateAsync for existing jobs
- PayrunJobFactory: add UpdatePayrunJob() method for async flow
CompleteJobAsync was missing the JobStatus update, causing jobs
to remain stuck in "Process" status after successful completion.

Now sets JobStatus = Draft to match the original sync behavior
where completed jobs await user review before Release → Complete.
@Giannoudis
Copy link
Copy Markdown
Contributor

Feature implement 3a845ac

@Giannoudis Giannoudis closed this Jan 16, 2026
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.

2 participants