Skip to content

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:

  1. Recovers any stalled jobs with expired locks (heartbeat protection)
  2. Claims the next available job, ordered by priority
  3. Sets a lock expiry (now + TimeoutMs) on the claimed job
  4. Starts a heartbeat timer (beats at 1/3 of the timeout interval)
  5. Deserializes the payload to the handler's parameter type
  6. Invokes the matched [JobHandler] method with timeout enforcement
  7. On success, marks the job as completed and re-schedules if it has a cron Schedule
  8. On failure, increments AttemptCount and re-enqueues with exponential backoff (RetryDelayMs × 2^attempt)
  9. After exhausting retries, moves the job to DeadLettered status
  10. 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.