Generated Code¶
The Background Jobs generator produces standard .NET interfaces for job scheduling and persistence. These work with plain async/await — no effects required.
IJobScheduler¶
Controls job lifecycle — enqueue, schedule, cancel, retry.
| Method | Returns | Description |
|---|---|---|
EnqueueAsync<T>(job) |
Task<JobId> |
Enqueue a job for immediate execution. Deduplicates if configured. |
ScheduleAsync<T>(job, runAt) |
Task<JobId> |
Schedule a job for a specific time. Deduplicates if configured. |
CancelAsync(jobId) |
Task<bool> |
Cancel a pending/scheduled job |
GetStatusAsync(jobId) |
Task<JobInfo?> |
Get current job status |
RetryAsync(jobId) |
Task<bool> |
Re-enqueue a failed/dead-lettered job |
ScheduleRawAsync(jobType, payload, config, runAt) |
Task<JobId> |
Schedule by raw type name (used internally for cron re-scheduling) |
// Plain .NET usage (no effects)
public class OrderService(IJobScheduler scheduler)
{
public async Task<string> EnqueueWelcomeEmail(string userId) =>
await scheduler.EnqueueAsync(new SendWelcomeEmail(userId));
public async Task<string> ScheduleInventorySync(string warehouseId) =>
await scheduler.ScheduleAsync(
new SyncInventory(warehouseId),
DateTimeOffset.UtcNow.AddMinutes(30));
public async Task RetryFailedJob(string jobId) =>
await scheduler.RetryAsync(jobId);
}
IJobStore¶
Queries persisted job state for monitoring and management.
| Method | Returns | Description |
|---|---|---|
GetJobsAsync(status?, offset?, limit?) |
Task<IReadOnlyList<JobInfo>> |
Query jobs by status |
GetDeadLetterAsync(offset?, limit?) |
Task<IReadOnlyList<JobInfo>> |
Jobs that exhausted all retries |
GetHistoryAsync(jobId) |
Task<IReadOnlyList<JobInfo>> |
Execution attempt history for a job |
ClaimNextAsync() |
Task<JobInfo?> |
Atomically claim the next available job (priority-ordered) |
CompleteAsync(jobId) |
Task |
Mark a running job as completed |
FailAsync(jobId, error?) |
Task |
Mark a running job as failed (retries or dead-letters) |
HeartbeatAsync(jobId) |
Task |
Extend the lock on a running job |
RecoverStalledJobsAsync() |
Task<int> |
Recover jobs with expired locks |
// Monitor job state
public class JobDashboard(IJobStore store)
{
public async Task<IReadOnlyList<JobInfo>> GetRunningJobs() =>
await store.GetJobsAsync(status: JobStatus.Running);
public async Task<IReadOnlyList<JobInfo>> GetDeadLettered() =>
await store.GetDeadLetterAsync(limit: 50);
}
JobInfo¶
| Property | Type | Description |
|---|---|---|
Id |
JobId |
Unique job identifier |
JobType |
string |
Fully qualified type name of the payload |
Status |
JobStatus |
Current execution status |
Payload |
string |
Serialized job payload (JSON) |
Schedule |
string? |
Cron expression, if recurring |
AttemptCount |
int |
Number of execution attempts |
MaxRetries |
int |
Maximum retry attempts |
RetryDelayMs |
int |
Base retry delay in milliseconds |
TimeoutMs |
int |
Execution timeout in milliseconds |
Priority |
int |
Job priority (higher = first) |
Error |
string? |
Error from most recent failure |
CreatedAt |
DateTimeOffset |
When the job was created |
StartedAt |
DateTimeOffset? |
When the latest attempt started |
CompletedAt |
DateTimeOffset? |
When the job completed |
NextRunAt |
DateTimeOffset? |
Next scheduled run time |
IdempotencyKey |
string? |
Deduplication key (from DeduplicateBy) |
JobStatus¶
| Value | Description |
|---|---|
Pending |
Waiting to be executed |
Running |
Currently executing |
Completed |
Finished successfully |
Failed |
Failed, may be retried |
DeadLettered |
Exhausted all retry attempts |
Cancelled |
Cancelled before completion |
Dispatch Handler Composition¶
The generator composes a dispatch function for each [JobHandler], wiring up retry logic, timeout enforcement, and status tracking. When a job is dequeued, the runtime:
- Recovers any stalled jobs with expired locks (heartbeat protection)
- Claims the next available job, ordered by priority
- Sets a lock expiry (
now + TimeoutMs) on the claimed job - Starts a heartbeat timer (beats at 1/3 of the timeout interval)
- Deserializes the payload to the handler's parameter type
- Invokes the matched
[JobHandler]method with timeout enforcement - On success, marks the job as completed and re-schedules if it has a cron
Schedule - On failure, increments
AttemptCountand re-enqueues with exponential backoff (RetryDelayMs × 2^attempt) - After exhausting retries, moves the job to
DeadLetteredstatus - On timeout, marks the job as failed with a timeout error message
Heartbeat & Lock Extension¶
While a job is processing, the worker sends periodic heartbeat signals to extend the lock. This prevents other workers from claiming the job while it's still being processed. If a worker crashes without completing a job, the lock expires and RecoverStalledJobsAsync resets it to Pending for another worker to claim.
Lock expires at: now + TimeoutMs
Heartbeat interval: TimeoutMs / 3
Recovery: stalled jobs reset to Pending each poll cycle
Cron Re-Scheduling¶
When a job with a Schedule completes successfully, the worker automatically computes the next occurrence using Cronos and enqueues a new job for that time. The CronSchedulerService also seeds cron jobs at startup to ensure they exist after a fresh deployment.
Event Queue vs Background Jobs¶
| Event Queue | Background Jobs | |
|---|---|---|
| Durability | In-process, lost on restart | Persisted, survives restarts |
| Retries | No built-in retry | Configurable retry with dead-letter |
| Scheduling | Immediate only | Cron and deferred scheduling |
| Use case | Lightweight fire-and-forget | Durable, retryable, scheduled work |
| Acknowledgement | Optional ack/nack | Automatic via status tracking |
Event queue integration
For lightweight in-process event processing (fire-and-forget, acknowledgement patterns), see the Event Queue module. Background Jobs are better suited for durable, retryable, and scheduled work.