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:
Ensure that your invocable is registered with the service provider as a scoped or transient service.
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 minute00 13 * * *
run at 1:00 pm daily00 1,2,3 * * *
run at 1:00 pm, 2:00 pm and 3:00 pm daily00 1-3 * * *
same as above00 */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!