TurboMediator
Scheduling

Job Store

Persistence, records, and runtime management for scheduled jobs

IJobStore

The IJobStore interface defines how job definitions and occurrence records are persisted:

public interface IJobStore
{
    // Job definitions
    Task UpsertJobAsync(RecurringJobRecord job, CancellationToken ct = default);
    Task<RecurringJobRecord?> GetJobAsync(string jobId, CancellationToken ct = default);
    Task<IReadOnlyCollection<RecurringJobRecord>> GetAllJobsAsync(CancellationToken ct = default);
    Task<IReadOnlyCollection<RecurringJobRecord>> GetDueJobsAsync(DateTimeOffset now, CancellationToken ct = default);
    Task RemoveJobAsync(string jobId, CancellationToken ct = default);

    // Locking (concurrency)
    Task<bool> TryLockJobAsync(string jobId, CancellationToken ct = default);
    Task ReleaseJobAsync(string jobId, CancellationToken ct = default);

    // Occurrence tracking
    Task AddOccurrenceAsync(JobOccurrenceRecord occurrence, CancellationToken ct = default);
    Task UpdateOccurrenceAsync(JobOccurrenceRecord occurrence, CancellationToken ct = default);
    Task<IReadOnlyCollection<JobOccurrenceRecord>> GetOccurrencesAsync(
        string jobId, int limit = 20, CancellationToken ct = default);
}

Built-in: In-Memory Store

For development and testing, a fast in-memory store is provided by default:

scheduling.UseInMemoryStore();
  • Uses ConcurrentDictionary and SemaphoreSlim for thread safety
  • Data is lost on application restart
  • Locking is process-local (not suitable for multi-instance deployments)

RecurringJobRecord

The job definition model:

PropertyTypeDescription
JobIdstringUnique identifier
MessageTypeNamestringFull type name of the command
CronExpressionstring?Cron expression (null if interval-based)
IntervalTimeSpan?Fixed interval (null if cron-based)
NextRunAtDateTimeOffset?Next scheduled execution
LastRunAtDateTimeOffset?Last completed execution
StatusJobStatusCurrent status (Scheduled, Running, Paused, etc.)
RetryIntervalSecondsint[]Retry intervals derived from RetryStrategy
SkipIfAlreadyRunningboolWhether to skip overlapping executions
PriorityJobPriorityExecution priority
MessagePayloadstring?Serialized command data (JSON)
TimeZoneIdstring?IANA timezone identifier

JobOccurrenceRecord

Each execution of a recurring job creates an occurrence:

PropertyTypeDescription
IdGuidUnique occurrence ID
JobIdstringParent job identifier
StartedAtDateTimeOffsetWhen execution began
CompletedAtDateTimeOffset?When execution finished
StatusJobStatusOutcome (Done, Failed, Skipped, Cancelled)
RetryCountintNumber of retries performed
Errorstring?Error message if failed
ParentOccurrenceIdGuid?Reserved for future chaining
RunConditionRunConditionReserved for future chaining

IJobScheduler (Runtime Management)

The IJobScheduler interface allows runtime control of jobs:

public interface IJobScheduler
{
    Task PauseJobAsync(string jobId, CancellationToken ct = default);
    Task ResumeJobAsync(string jobId, CancellationToken ct = default);
    Task RemoveJobAsync(string jobId, CancellationToken ct = default);
    Task TriggerNowAsync(string jobId, CancellationToken ct = default);
    Task<IReadOnlyCollection<JobOccurrenceRecord>> GetOccurrencesAsync(
        string jobId, int limit = 20, CancellationToken ct = default);
    Task<RecurringJobRecord?> GetJobAsync(string jobId, CancellationToken ct = default);
    Task<IReadOnlyCollection<RecurringJobRecord>> GetAllJobsAsync(CancellationToken ct = default);
}

Exposing via API Endpoints

var jobs = app.MapGroup("/api/jobs");

jobs.MapGet("/", async (IJobScheduler scheduler) =>
    Results.Ok(await scheduler.GetAllJobsAsync()));

jobs.MapPost("/{id}/pause", async (string id, IJobScheduler scheduler) =>
{
    await scheduler.PauseJobAsync(id);
    return Results.NoContent();
});

jobs.MapPost("/{id}/resume", async (string id, IJobScheduler scheduler) =>
{
    await scheduler.ResumeJobAsync(id);
    return Results.NoContent();
});

jobs.MapPost("/{id}/trigger", async (string id, IJobScheduler scheduler) =>
{
    await scheduler.TriggerNowAsync(id);
    return Results.Accepted();
});

jobs.MapDelete("/{id}", async (string id, IJobScheduler scheduler) =>
{
    await scheduler.RemoveJobAsync(id);
    return Results.NoContent();
});

jobs.MapGet("/{id}/occurrences", async (string id, IJobScheduler scheduler) =>
    Results.Ok(await scheduler.GetOccurrencesAsync(id)));

Custom Job Store

Implement IJobStore for any persistence backend:

public class MongoJobStore : IJobStore
{
    private readonly IMongoCollection<RecurringJobRecord> _jobs;
    private readonly IMongoCollection<JobOccurrenceRecord> _occurrences;

    // Implement all IJobStore methods...
}

Register it:

scheduling.UseStore<MongoJobStore>();

For production use with Entity Framework Core, see the Entity Framework page which provides a ready-made IJobStore implementation with optimistic concurrency.

Polling Configuration

scheduling.Configure(options =>
{
    options.PollingInterval = TimeSpan.FromSeconds(5); // default: 10s
});

The processor uses PeriodicTimer for accurate, non-drifting intervals. Shorter polling intervals mean lower latency but higher database load.

On this page