FabulousScheduler.Cron 5.1.0

dotnet add package FabulousScheduler.Cron --version 5.1.0
                    
NuGet\Install-Package FabulousScheduler.Cron -Version 5.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FabulousScheduler.Cron" Version="5.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FabulousScheduler.Cron" Version="5.1.0" />
                    
Directory.Packages.props
<PackageReference Include="FabulousScheduler.Cron" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FabulousScheduler.Cron --version 5.1.0
                    
#r "nuget: FabulousScheduler.Cron, 5.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FabulousScheduler.Cron@5.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FabulousScheduler.Cron&version=5.1.0
                    
Install as a Cake Addin
#tool nuget:?package=FabulousScheduler.Cron&version=5.1.0
                    
Install as a Cake Tool

Recurring Scheduler

Recurring jobs run again and again. After a successful run a job sleeps for its SleepDuration, then it's eligible to run again.

πŸ“– Contents


Default Job Manager <a id="default" />

RecurringJobManager (in FabulousScheduler.Recurring) is a static, process-wide facade. Call it in this order:

1. SetConfig(config)   // optional, only before RunScheduler
2. RunScheduler()      // start the background loop
3. JobResultEvent +=   // subscribe to results
4. Register(...)       // add jobs, only after RunScheduler

The order is enforced. Register(...) before RunScheduler() throws SchedulerNotRunnableException, and SetConfig(...) after RunScheduler() throws SetConfigAfterRunSchedulingException.

Register overloads

Register returns the new job's Guid. It takes either a synchronous (Action) or an asynchronous (Func<Task>) delegate, with optional name and category:

Guid Register(Action     action,                                 TimeSpan sleepDuration);
Guid Register(Action     action, string name,                    TimeSpan sleepDuration);
Guid Register(Action     action, string name, string category,   TimeSpan sleepDuration);
Guid Register(Func<Task> action,                                 TimeSpan sleepDuration);
Guid Register(Func<Task> action, string name,                    TimeSpan sleepDuration);
Guid Register(Func<Task> action, string name, string category,   TimeSpan sleepDuration);

Leave name/category out and you get name = "anonymous", category = "internal".

The result callback

RecurringJobManager.JobResultEvent +=
    (ref IRecurringJob job, ref JobResult<JobOk, JobFail> res) =>
    {
        if (res.IsSuccess)
            Console.WriteLine("{0} ok", job.Name);
        else
            Console.WriteLine("{0} failed: {1}", job.Name, res.GetFail()!.Message);
    };

The parameters are ref so the result isn't copied when it's handed to you. Don't throw from this handler. If you do, the scheduler swallows it (so one bad handler can't take a worker down), and you'll never see the exception.

State machine (JobStateEnum)

A job moves through these states:

Ready ──scheduler picks it──► Waiting ──worker takes it──► Running
  β–²                                                          β”‚
  β”‚                                                          β”‚ run finished
  └────────────── sleep window elapsed ──────────────── Sleeping

  Any state ──Dispose()──► Disposed

SleepDuration decides the gap between runs:

SleepDuration What happens after a run
TimeSpan.Zero or TimeSpan.MinValue Counted as zero. The job runs again as soon as the next slot is free (the gap is floored to SleepAfterCheck, so it can't spin).
TimeSpan.MaxValue The job runs once and is then dropped. A one-shot job.
anything else The job runs again after that much time.

The gap is the same whether a run succeeds or fails: a job that keeps failing retries on its normal cadence, it does not busy-loop. If you want a real backoff (grow the delay on repeated failures), build it into your delegate.

Fail reasons (JobFailEnum)

Value When
IncorrectState ExecuteAsync ran while the job wasn't Ready/Waiting.
InternalException Your delegate threw. The exception is on JobFail.Exception.
FailedExecute The job returned a JobFail on purpose.
Disposed The job was disposed before it could run.

Make my own job manager <a id="myself" />

RecurringJobManager is a thin static wrapper over two public base classes. When you want more (custom job state, your own registry, DI instead of a static singleton), subclass them.

1. A custom job. Derive from BaseRecurringJob and implement ActionJob():

using FabulousScheduler.Recurring.Abstraction;
using FabulousScheduler.Recurring.Result;
using FabulousScheduler.Core.Types;

public sealed class MyRecurringJob : BaseRecurringJob
{
    // isAsyncAction tells the base whether ActionJob awaits real work.
    public MyRecurringJob(string name, string category, TimeSpan sleepDuration)
        : base(name, category, sleepDuration, isAsyncAction: true) { }

    protected override async Task<JobResult<JobOk, JobFail>> ActionJob()
    {
        // ... your work ...
        await Task.Delay(10);

        return new JobOk(ID);                 // success
        // return new JobFail(JobFailEnum.FailedExecute, ID, "reason"); // failure
    }
}

2. A custom scheduler. Derive from BaseRecurringScheduler and expose registration. The base gives you protected bool Register(IRecurringJob) (and an IEnumerable<IRecurringJob> overload), a matching Unregister(Guid), the background engine, and the JobResultEvent:

using FabulousScheduler.Recurring.Abstraction;

public sealed class MyRecurringScheduler : BaseRecurringScheduler
{
    public MyRecurringScheduler(Configuration? config) : base(config) { }

    public Guid Add(MyRecurringJob job)
    {
        base.Register(job);   // adds the job to the registry
        return job.ID;
    }

    public bool Remove(Guid id) => base.Unregister(id); // stops scheduling it
}

It works like the default manager: new MyRecurringScheduler(config), subscribe to JobResultEvent, RunScheduler(), then Add(job).


Usage <a id="usage" />

BaseRecurringJob <a id="baserecurringjob" />

public abstract class BaseRecurringJob : IRecurringJob. A thread-safe state machine that hands the actual work to the abstract ActionJob().

Member Description
ID, Name, Category Identity (see Core.md).
SleepDuration The sleep window after a successful run. Zero/MinValue become Zero in the constructor.
State The current JobStateEnum. Reading it can move a finished job from Sleeping to Ready once its sleep is up.
TotalRun ulong, how many times the job started.
TotalFail ulong, how many runs failed.
LastExecute / LastSuccessExecute Timestamps (nullable).
Task<JobResult<JobOk,JobFail>> ExecuteAsync() Runs the job once and updates the state and counters.
protected abstract Task<JobResult<JobOk,JobFail>> ActionJob() Your work. Return JobOk or JobFail.
Dispose() / DisposeAsync() Marks the job disposed. DisposeAsync just calls Dispose.

Constructor:

protected BaseRecurringJob(string name, string category, TimeSpan sleepDuration, bool isAsyncAction);

BaseRecurringScheduler <a id="baserecurringshceduler" />

public abstract class BaseRecurringScheduler : IRecurringJobScheduler.

Member Description
event JobResultEventHandler JobResultEvent Fires after each run with (ref IRecurringJob, ref JobResult<JobOk,JobFail>).
void RunScheduler() Starts the engine. A second call does nothing.
int CurrentRunnableJobCount() How many jobs are running right now.
protected bool Register(IRecurringJob job) Adds one job. Returns false if its ID is already there.
protected int Register(IEnumerable<IRecurringJob> jobs) Adds many. Returns how many were added.
protected bool Unregister(Guid id) Stops scheduling a job. Any run already in flight finishes first.
void Dispose() Cancels the engine and waits for it to stop.

How it runs: a single producer keeps the registered jobs in a min-heap ordered by their next run time, so finding the next one to run is cheap even with a lot of jobs. The producer sleeps until that next job is due (it doesn't poll), then pushes the due jobs onto a channel. A fixed pool of MaxParallelJobExecute workers reads the channel and runs them, which is what caps the parallelism. When a job finishes, it's scheduled for its next run, or dropped from the registry if it was a one-shot (SleepDuration == MaxValue) or you unregistered it.

Configuration

FabulousScheduler.Recurring.Configuration:

Property Default Meaning
MaxParallelJobExecute Environment.ProcessorCount * 10 Number of workers, so the most jobs running at once.
SleepAfterCheck 200 ms The smallest gap between a job's runs (the floor for short sleep durations). Zero/MinValue become 10 ms.
var config = new Configuration(maxParallelJobExecute: 5, sleepAfterCheck: TimeSpan.FromMilliseconds(100));
// or: new Configuration(maxParallelJobExecute: 5);   // SleepAfterCheck = 100 ms
// or: Configuration.Default;

Example <a id="example" />

using FabulousScheduler.Core.Types;
using FabulousScheduler.Recurring;
using FabulousScheduler.Recurring.Interfaces;
using FabulousScheduler.Recurring.Result;

var config = new Configuration(
    maxParallelJobExecute: 5,
    sleepAfterCheck: TimeSpan.FromMilliseconds(100)
);
RecurringJobManager.SetConfig(config);

// Start the scheduler before registering jobs
RecurringJobManager.RunScheduler();

// Subscribe to results
RecurringJobManager.JobResultEvent += (ref IRecurringJob job, ref JobResult<JobOk, JobFail> res) =>
{
    var now = DateTime.Now;
    if (res.IsSuccess)
        Console.WriteLine("[{0:hh:mm:ss}] {1} {2} OK", now, job.Name, res.JobID);
    else
        Console.WriteLine("[{0:hh:mm:ss}] {1} {2} FAIL", now, job.Name, res.JobID);
};

// A job that repeats one second after each success
RecurringJobManager.Register(
    action: () =>
    {
        int a = 10, b = 100;
        int c = a + b;
        _ = c;
    },
    sleepDuration: TimeSpan.FromSeconds(1),
    name: "ExampleJob"
);

Thread.Sleep(-1); // keep the process alive

Benchmarks <a id="benchmarks" />

The numbers for the recurring scheduler (how it picks the next job, and end-to-end throughput) are on the Benchmarks page.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on FabulousScheduler.Cron:

Package Downloads
FabulousScheduler

High-performance scheduler for recurring and queue-based jobs.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.1.0 39 8/1/2026
5.0.1 148 6/18/2026
5.0.0 132 6/18/2026
4.0.0 312 3/20/2025
3.1.3 10,037 6/14/2024
3.1.0 11,916 2/3/2024
3.0.1 282 1/27/2024
2.2.7 613 1/17/2024
2.2.6 246 1/17/2024
2.2.5 244 1/17/2024
2.2.4 242 1/17/2024
2.2.3 257 1/12/2024
2.2.2 265 1/9/2024
2.2.1 273 1/8/2024
2.1.6 306 1/2/2024
2.1.5 304 1/1/2024
2.1.2 279 1/1/2024
2.1.1 289 1/1/2024