Task Scheduling

Usually, you have to configure a cron job or a task via Windows Task Scheduler to get a single or multiple re-occurring tasks to run.

With Coravel you can setup all your scheduled tasks in one place using a simple, elegant, fluent syntax - in code!

Scheduling is now a breeze!

Config

In your .NET Core app's Startup.cs file, inside the ConfigureServices() method, add the following:

services.AddScheduler()

Then in the Configure() method, you can use the scheduler:

var provider = app.ApplicationServices;
provider.UseScheduler(scheduler =>
{
    scheduler.Schedule(
        () => Console.WriteLine("Every minute during the week.")
    )
    .EveryMinute()
    .Weekday();
});

Simple enough?

Scheduling Tasks

Invocables

TIP

Using invocables is the recommended way to schedule your tasks.

To learn about creating invocables see the docs.

In essence, to schedule an invocable you must:

  1. Ensure that your invocable is registered with the service provider as a scoped or transient service.

  2. Use the Schedule method:

scheduler
    .Schedule<GrabDataFromApiAndPutInDBInvocable>()
    .EveryTenMinutes();

What a simple, terse and expressive syntax! Easy Peasy!

Cancel Long-Running Invocables

Make your long-running invocable classes implement Coravel.Invocable.ICancellableInvocable to enable it to gracefully abort on application shutdown.

The interface includes a property CancellationToken that you can check using CancellationToken.IsCancellationRequested, etc.

Async Tasks

Coravel will also handle scheduling async methods by using the ScheduleAsync() method.

TIP

ScheduleAsync does not have to be awaited. The method or Func you provide itself must be awaitable (e.g. returns a Task or Task<T>).

scheduler.ScheduleAsync(async () =>
{
    await Task.Delay(500);
    Console.WriteLine("async task");
})
.EveryMinute();

WARNING

You are able to register an async method when using Schedule() by mistake. Always use ScheduleAsync() when registering an async method.

Synchronous Tasks

While generally not recommended, there may be times when you aren't doing any async operations.

In this case, use Schedule().

scheduler.Schedule(
    () => Console.WriteLine("Scheduled task.")
)
.EveryMinute();

Scheduling With Additional Parameters

The ScheduleWithParams<T>() method allows you to schedule the same invocable multiple times with different parameters.

Note: For these specific invocables to work, do not register them with the DI services.

private class BackupDatabaseTableInvocable : IInvocable
{
    private DbContext _dbContext;
    private string _tableName;

    public BackupDatabaseTableInvocable(DbContext dbContext, string tableName)
    {
        this._dbContext = dbContext; // Injected via DI.
        this._tableName = tableName; // injected via schedule configuration (see next code block).
    }

    public Task Invoke()
    {
        // Do the logic.
    }
}

You might configure it like this:

// In this case, backing up products 
// more often than users is required.

scheduler
    .ScheduleWithParams<BackupDatabaseTableInvocable>("[dbo].[Users]")
    .Daily();

scheduler
    .ScheduleWithParams<BackupDatabaseTableInvocable>("[dbo].[Products]")
    .EveryHour();

WARNING

Ensure that any parameters to be injected via dependency injection are listed first in your constructor arguments.

Intervals

After calling Schedule or ScheduleAsync, methods to specify the schedule interval are available.

Method Description
EverySecond() Run the task every second
EveryFiveSeconds() Run the task every five seconds
EveryTenSeconds() Run the task every ten seconds
EveryFifteenSeconds() Run the task every fifteen seconds
EveryThirtySeconds() Run the task every thirty seconds
EverySeconds(3) Run the task every 3 seconds.
EveryMinute() Run the task once a minute
EveryFiveMinutes() Run the task every five minutes
EveryTenMinutes() Run the task every ten minutes
EveryFifteenMinutes() Run the task every fifteen minutes
EveryThirtyMinutes() Run the task every thirty minutes
Hourly() Run the task every hour
HourlyAt(12) Run the task at 12 minutes past every hour
Daily() Run the task once a day at midnight
DailyAtHour(13) Run the task once a day at 1 p.m. UTC
DailyAt(13, 30) Run the task once a day at 1:30 p.m. UTC
Weekly() Run the task once a week
Monthly() Run the task once a month (at midnight on the 1st day of the month)
Cron("* * * * *") Run the task using a Cron expression

TIP

The scheduler uses UTC time by default.

Cron Expressions

Supported types of Cron expressions are:

  • * * * * * run every minute
  • 00 13 * * * run at 1:00 pm daily
  • 00 1,2,3 * * * run at 1:00 pm, 2:00 pm and 3:00 pm daily
  • 00 1-3 * * * same as above
  • 00 */2 * * * run every two hours on the hour

Day Constraints

After specifying an interval, you can further chain to restrict what day(s) the scheduled task is allowed to run on.

  • Monday()
  • Tuesday()
  • Wednesday()
  • Thursday()
  • Friday()
  • Saturday()
  • Sunday()
  • Weekday()
  • Weekend()

All these methods are further chainable - like Monday().Wednesday(). This would mean only running the task on Mondays and Wednesdays.

WARNING

Be careful since you could do something like .Weekend().Weekday(), which means there are no constraints (it runs on any day).

Zoned Schedules

Sometimes you do want to run your schedules against a particular time zone. For these scenarios, use the Zoned method:

scheduler
    .Schedule<SendWelcomeUserEmail>()
    .DailyAt(13, 30)
    .Zoned(TimeZoneInfo.Local);

You'll need to supply an instance of TimeZoneInfo to the Zoned method.

WARNING

Creating a valid TimeZoneInfo differs depending on whether you're on Windows, Linux or OSX.

Also, you may get unexpected behavior due to daylight savings time. Be careful!

Prevent Overlapping Tasks

Sometimes you may have longer running tasks or tasks who's running time is variable. The normal behavior of the scheduler is to simply fire off a task if it is due.

But, what if the previous instance of this scheduled task is still running?

In this case, use the PreventOverlapping method to make sure there is only 1 running instance of your scheduled task.

In other words, if the same scheduled task is due but another instance of it is still running, Coravel will just ignore the currently due task.

scheduler
    .Schedule<SomeInvocable>()
    .EveryMinute()
    .PreventOverlapping(nameof(SomeInvocable));

This method takes in one parameter - a unique key (string) among all your scheduled tasks. This makes sure Coravel knows which task to lock and release.

Schedule Workers

In order to make Coravel work well in web scenarios, the scheduler will run all due tasks sequentially (although asynchronously).

If you have longer running tasks - especially tasks that do some CPU intensive work - this will cause any subsequent tasks to execute much later than you might have expected or desired.

What's A Worker?

Schedule workers solve this problem by allowing you to schedule groups of tasks that run in parallel!

In other words, a schedule worker is just a pipeline that you can assign to a group of tasks which will have a dedicated thread.

Usage

To begin assigning a schedule worker to a group of scheduled tasks use:

OnWorker(string workerName)

For example:

scheduler.OnWorker("EmailTasks");
scheduler
    .Schedule<SendNightlyReportsEmailJob>().Daily();
scheduler
    .Schedule<SendPendingNotifications>().EveryMinute();

scheduler.OnWorker("CPUIntensiveTasks");
scheduler
    .Schedule<RebuildStaticCachedData>().Hourly();

For this example, SendNightlyReportsEmailJob and SendPendingNotifications will share a dedicated pipeline/thread.

RebuildStaticCachedData has its own dedicated worker so it will not affect the other tasks if it does take a long time to run.

Useful For

This is useful, for example, when using Coravel in a console application.

You can choose to scale-out your scheduled tasks however you feel is most efficient. Any super intensive tasks can be put onto their own worker and therefore won't cause the other scheduled tasks to lag behind!

Run Job Once

By calling Once() after you specify the time interval or day constraints, this job will only run the first time it is due (internally, the job is unscheduled 💪).

scheduler
    .Schedule<SpecialJob>()
    .Hourly()
    .Once();

Custom Boolean Constraint

Using the When method you can add additional restrictions to determine when your scheduled task should be executed.

scheduler
    .Schedule(() => DoSomeStuff())
    .EveryMinute()
    .When(SomeMethodThatChecksStuff);

If you require access to dependencies that are registered with the service provider, it is recommended that you schedule your tasks by using an invocable and perform any further restriction logic there.

Global Error Handling

Any tasks that throw errors will just be skipped and the next task in line will be invoked.

If you want to catch errors and do something specific with them you may use the OnError() method.

provider.UseScheduler(scheduler =>
    // Assign your schedules
)
.OnError((exception) =>
    DoSomethingWithException(exception)
);

You can, of course, add error handling inside your specific tasks too.

Logging Executed Task Progress

If you want Coravel to output log statements about scheduled task progress (usually to debug issues), you can call LogScheduledTaskProgress():

provider.UseScheduler(scheduler =>
{
    // Assign scheduled tasks...
})
.LogScheduledTaskProgress();

Your logging level should also be set to Debug in your appsettings.json file.

TIP

This method had a breaking change in Coravel 6.0.0.

Logging Tick Catch Up

Coravel's scheduling runs on an internal Timer. Sometimes, due to a lack of resources or an overloaded system (common in small containerized processes), the next "tick" could occur after 1 second. For example, a constrained container process might be overloaded and the Timer misses 10 seconds of ticks. Coravel will chronologically replay each tick in order to catch up to "now".

You can enable Coravel to output a log statement by setting the following configuration value in appsettings.json:

"Coravel": {
    "Schedule": {
        "LogTickCatchUp": true
    }
}

This will be outputted as an Informational log statement.

Force Run A Task At App Startup

At times, you may want to run a task immediately at your application's startup. This could be for debugging, setting up a cache, etc.

For these scenarios, you can use RunOnceAtStart().

This will not override the assigned schedule of a task or invocable. Take the following:

scheduler.Schedule<CacheSomeStuff>()
    .Hourly()
    .Weekday()
    .RunOnceAtStart();

This will run immediately on application startup - even on weekends. After this initial run, any further runs will respect the assigned schedule of running once an hour only on weekdays.

On App Closing

When your app is stopped, Coravel will wait until any running scheduled tasks are completed. This will keep your app running in the background - as long as the parent process is not killed.

Examples

Run a task once an hour only on Mondays.

scheduler.Schedule(
    () => Console.WriteLine("Hourly on Mondays.")
)
.Hourly()
.Monday();

Run a task every day at 1pm

scheduler.Schedule(
    () => Console.WriteLine("Daily at 1 pm.")
)
.DailyAtHour(13); // Or .DailyAt(13, 00)

Run a task on the first day of the month.

scheduler.Schedule(
    () => Console.WriteLine("First day of the month.")
)
.Cron("0 0 1 * *") // At midnight on the 1st day of each month.

Scheduling An Invocable That Sends A Daily Report

Imagine you have a "daily report" that you send out to users at the end of each day. What would be a simple, elegant way to do this?

Using Coravel's Invocables, Scheduler and Mailer all together can make it happen!

Take this sample class as an example:

public class SendDailyReportsEmailJob : IInvocable
{
    private IMailer _mailer;
    private IUserRepository _repo;

    // Each param injected from the service container ;)
    public SendDailyReportsEmailJob(IMailer mailer, IUserRepository repo)
    {
        this._mailer = mailer;
        this._repo = repo;
    }

    public async Task Invoke()
    {
        var users = await this._repo.GetUsersAsync();

        foreach(var user in users)
        {
            var mailable = new NightlyReportMailable(user);
            await this._mailer.SendAsync(mailable);
        }        
    }
}

Now to schedule it:

scheduler
    .Schedule<SendDailyReportsEmailJob>()
    .Daily();

Easy Peasy!