Description
Every deferred queue in the API is drained opportunistically from the request that enqueued the work, not on a schedule. EmailService, NotificationService, WebhookService, and StellarFundingService each call this.processQueue().catch(...) immediately after enqueueing, so a row whose nextAttemptAt is in the future is only retried if unrelated traffic happens to trigger another drain. When traffic stops, retries stop, and failed deliveries can sit indefinitely. Only the account-lifecycle sweep has a real timer (setInterval in src/server.ts), and its own comment notes that a dedicated worker is the intended owner.
Introduce one scheduled runner process that owns every recurring queue drain, so delivery is driven by time rather than by incidental request volume, and so the work stops competing with request handling inside the API process.
File Location
learnault-api/src/workers/, learnault-api/src/server.ts, learnault-api/docker/, learnault-api/docker-compose.yml, and learnault-api/package.json
Design Reference
API Roadmap Phase 1.1: Add Scheduled Background Job Runner. Builds on the leasing primitives delivered by #122.
Dependencies
Tasks
Acceptance Criteria
- Queued work is retried on schedule with no inbound HTTP traffic
- A due
nextAttemptAt row is picked up within one configured interval
- Two concurrently running replicas never process the same row twice
- API request latency no longer includes queue-drain work
- Shutdown drains or releases in-flight leases without orphaning rows
Verification Evidence
Difficulty
Advanced
Description
Every deferred queue in the API is drained opportunistically from the request that enqueued the work, not on a schedule.
EmailService,NotificationService,WebhookService, andStellarFundingServiceeach callthis.processQueue().catch(...)immediately after enqueueing, so a row whosenextAttemptAtis in the future is only retried if unrelated traffic happens to trigger another drain. When traffic stops, retries stop, and failed deliveries can sit indefinitely. Only the account-lifecycle sweep has a real timer (setIntervalinsrc/server.ts), and its own comment notes that a dedicated worker is the intended owner.Introduce one scheduled runner process that owns every recurring queue drain, so delivery is driven by time rather than by incidental request volume, and so the work stops competing with request handling inside the API process.
File Location
learnault-api/src/workers/,learnault-api/src/server.ts,learnault-api/docker/,learnault-api/docker-compose.yml, andlearnault-api/package.jsonDesign Reference
API Roadmap Phase 1.1: Add Scheduled Background Job Runner. Builds on the leasing primitives delivered by #122.
Dependencies
Tasks
job-lease.service.tsso multiple replicas cannot double-processprocessQueue()calls from the request path, keeping enqueue synchronoussrc/server.tslifecycle sweep onto the runner and keep an opt-in flag for single-process deploymentsAcceptance Criteria
nextAttemptAtrow is picked up within one configured intervalVerification Evidence
Difficulty
Advanced