Recurring Schedule
JobMaster provides a flexible system for recurring tasks. You can define schedules using:
- Time intervals (
TimeSpan) - Natural language expressions (NaturalCron)
Providers
The scheduler accepts compiled expressions from different providers (by TypeId):
- TimeSpanInterval — simple intervals like every N seconds/minutes/hours.
- NaturalCron — human-friendly schedules with rich rules and optional timezone.
Both dynamic (created at runtime) and static (code-defined profiles) are supported.
Dynamic Recurring Jobs
Dynamic schedules are tied to specific data (e.g., a specific subscription renewal or a per-user cleanup task).
// 1) Using the built-in TimeSpanInterval provider (runs every 5 minutes)
await scheduler.RecurringAsync<HelloJobHandler>(TimeSpan.FromMinutes(5));
// 2) Using NaturalCron via expression TypeId
var data = WriteableMessageData.New().SetStringValue("SubscriptionId", "sub_123");
await scheduler.RecurringAsync<RenewalHandler>(
NaturalCronExprCompiler.TypeId,
"every day between mon and fri at 18:00",
data: data);
// 3) Using NaturalCron fluent builder
var schedule = NaturalCronBuilder
.Every(30).Minutes()
.In(NaturalCronMonth.Jan)
.Between("09:00", "18:00")
.Build();
await scheduler.RecurringAsync<HelloJobHandler>(schedule);
Timezone handling (NaturalCron)
If your NaturalCron expression includes a timezone (IANA id) like in America/New_York, the engine computes occurrences in that zone. If no timezone is present, the cluster's configured IANA timezone is used.
Planning window
The planner generates occurrences within a moving horizon (configurable). Dates are produced strictly after the base time and on/before the planning horizon. End boundaries (endBefore) and start delays (startAfter) are respected.
Static Recurring Profiles (System Jobs)
For system-wide routines like backups or maintenance, define a StaticRecurringSchedulesProfile. These do not transport message data and are typically used for global background tasks.
public class MaintenanceProfile : IStaticRecurringSchedulesProfile
{
public static string ProfileId => "Maintenance";
public static void Config(RecurringScheduleDefinitionCollection collection)
{
collection
.Add<CleanupHandler>(TimeSpan.FromDays(1))
.Add<SyncHandler>(
NaturalCronExprCompiler.TypeId,
"every 1 hour between 09:00 and 18:00",
defId: "HourlySync");
}
}
Dynamic vs. Static: Comparison
| Feature | Dynamic Recurring | Static Profile |
|---|---|---|
| Primary Use Case | Per-entity logic (e.g., specific Subscription, User cleanup). | Global system routines (e.g., Database backup, Log rotation). |
| Data Payload | Supported. Can transport unique MsgData for each instance. | Not Supported. Handlers run without specific message data. |
| Where to Define | Enqueued at runtime via IJobMasterScheduler. | Defined in code by implementing IStaticRecurringSchedulesProfile. |
| Persistence | Stored and managed permanently in the Cluster Database. | Stored in the Cluster DB for monitoring, but automatically inactivated if the profile is removed from code. |
| Scalability | Can be created/deleted dynamically by your business logic. | Fixed at deployment time; requires a code change or profile update to modify. |
Configuration
Recurring schedules accept the same configuration options as one-off jobs — priority, worker lane, timeout, max retries, metadata, and clusterId.
var customMeta = WritableMetadata.New().SetStringValue("Source", "Scheduler");
await scheduler.RecurringAsync<HelloJobHandler>(
TimeSpan.FromMinutes(30),
metadata: customMeta,
priority: JobMasterPriority.Low,
workerLane: "BackgroundTasks"
);
Configuration provided in a IStaticRecurringSchedulesProfile or via RecurringAsync overrides attributes defined on the IJobHandler class. This lets you reuse the same handler across different schedules with different priorities or lanes.
Sub-minute recurring schedules are supported, but keep in mind that jobs may fail due to execution overlaps and start delays of 10–20 seconds are expected in a distributed cluster. For tasks that need precise second-level execution, a .NET IHostedService or a dedicated background loop is a better fit.
Custom Recurrence Engines
If the built-in TimeSpanInterval and NaturalCron providers don't cover your needs (e.g. standard cron syntax, custom business rules), you can plug in your own engine by implementing two interfaces and registering the compiler.
1. Implement IRecurrenceCompiledExpr
This holds the parsed schedule and calculates the next occurrence.
public class MyCronCompiledExpr : IRecurrenceCompiledExpr
{
public string Expression { get; }
public string ExpressionTypeId => MyCronCompiler.TypeId;
private readonly CronExpression _cron; // your parsing library
public MyCronCompiledExpr(string expression, CronExpression cron)
{
Expression = expression;
_cron = cron;
}
public DateTime? GetNextOccurrence(DateTime dateTime, string ianaTimeZoneId)
=> _cron.GetNextOccurrence(dateTime);
public bool HasEnded(DateTime dateTime, string ianaTimeZoneId)
=> GetNextOccurrence(dateTime, ianaTimeZoneId) == null;
}
2. Implement IRecurrenceExprCompiler
This parses raw expression strings into compiled instances. The ExpressionTypeId is the key used everywhere in scheduling calls.
public class MyCronCompiler : IRecurrenceExprCompiler
{
public const string TypeId = "MyCron";
public string ExpressionTypeId => TypeId;
public IRecurrenceCompiledExpr? TryCompile(string expression)
{
var cron = CronExpression.TryParse(expression);
return cron is null ? null : new MyCronCompiledExpr(expression, cron);
}
public IRecurrenceCompiledExpr Compile(string expression)
=> TryCompile(expression) ?? throw new ArgumentException($"Invalid MyCron expression: {expression}");
}
3. Register at Startup
Call RegisterCompiler before AddJobMasterCluster. Built-in compilers are auto-discovered, but custom ones must be registered manually.
RecurrenceCompilerFactory.RegisterCompiler(new MyCronCompiler());
builder.Services.AddJobMasterCluster(config => { ... });
4. Use Your Custom TypeId
Once registered, use the TypeId anywhere you schedule a recurring job — dynamic or static:
await scheduler.RecurringAsync<MyHandler>(MyCronCompiler.TypeId, "0 18 * * 1-5");