Scheduled tasks have many names. In Windows, it is traditionally called Task Scheduler. In Unix-like environments job scheduler is called Cron daemon. Microsoft Azure contains Azure Scheduler and Azure Web Apps have WebJobs. Service Fabric has its own mechanism called Actor Reminder. This article explains how to implement them. Multiple jobs could be encapsulated in a single assembly.
Quick overview of Actor service
Service Fabric cluster can host multiple applications. One application contains one or multiple services. The application can combine several service types. For example, it can consist of one Stateless service, two Stateful services and ten Actors. Scheduled jobs in Service Fabric are built on top of Actor model.
The actor is a class with or without persistent state. The actor is called via its proxy. Data serialization is done automatically. One actor has at most one thread. All calls to a single actor are queued. The actor is scaled out based on range of its identifier. When the caller obtains a proxy of some actor, it must provide actor’s identifier. If the actor with a given identifier does not exists it is instantiated. Every actor instance has its own state. If the instance is not used for one hour (a default interval) the instance is garbage collected.
Create a new project
Start Page → Create new project… → Installed → Templates → Visual C# → Cloud → Service Fabric Application → OK → Actor Service → OK
- Move IActor1.cs from Actor1.Interfaces to Actor1 project.
- Delete Actor1.Interfaces from the solution. One actor will represent one scheduled job. The job will be invoked internally, thus interfaces do not need to be exposed.
- Open IActor1.cs interface and remove SetCountAsync and GetCountAsync methods. Add the Task RegisterReminder() method instead (see code below).
- Open Actor1.cs class and change StatePersistence attribute from Persisted to None.
- Remove SetCountAsync and GetCountAsync methods.
- Implement IActor1 and IRemindable interfaces (see code below).
- Create a new class ScheduledActorService<T> and implement it in accordance with example below.
- In Program.cs file in Main method, replace ActorService class with ScheduledActorService<T> class (see code below).
- Create a copy of IActor1 interface and name it IActor2.
- Create a copy of Actor1 class and name it Actor2. In Actor2 in RegisterReminder method, change all occurrences of "Reminder1" string to "Reminder2".
IActor1.cs:
public interface IActor1 : IActor {
Task RegisterReminder();
}
This is the interface of Actor1. The actor must be called only through its interface implemented by actor’s proxy. Actor1 exposes only one method to set up a timer.
Actor1.cs:
[StatePersistence(StatePersistence.None)]
internal class Actor1 : Actor, IActor1, IRemindable {
public Actor1(ActorService actorService, ActorId actorId) : base(actorService, actorId) { }
public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) {
var location = Directory.GetCurrentDirectory();
var current = DateTime.Now;
Thread.Sleep(2 * 60 * 1000);
using (var writer = File.AppendText("actor.txt")) {
await writer.WriteLineAsync("1 :: " + current.ToString() + " --> " + DateTime.Now.ToString());
}
}
public async Task RegisterReminder() {
try {
var previousRegistration = GetReminder("Reminder1");
await UnregisterReminderAsync(previousRegistration);
} catch (ReminderNotFoundException) { }
var reminderRegistration = await RegisterReminderAsync("Reminder1", null, TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(1));
}
}
The implementation of Actor1 class contains a logic of scheduled job. When the reminder ticks, it invokes the ReceiveReminderAsync method through IRemindable interface.
In this example the method writes time of start and end of its execution to the file in the working directory. You can toggle breakpoint after the Directory.GetCurrentDirectory() call to see the file location. The file location is changed with each deploy to the cluster (which is not an upgrade).
The RegisterReminder method is responsible for scheduling (or rescheduling) of the reminder named Reminder1. The reminder ticks if the actor is garbage collected (new instance is created). The timer ticks before garbage collection only. The times stops along with garbage collection.
ScheduledActorService.cs:
internal class ScheduledActorService<T> : ActorService where T : IActor {
public ScheduledActorService(StatefulServiceContext context, ActorTypeInformation actorType) : base(context, actorType) { }
protected async override Task RunAsync(CancellationToken cancellationToken) {
await base.RunAsync(cancellationToken);
var proxy = ActorProxy.Create<T>(new ActorId(0));
switch (proxy) {
case IActor1 a1:
await a1.RegisterReminder();
break;
case IActor2 a2:
await a2.RegisterReminder();
break;
default:
throw new NotImplementedException($"{GetType().FullName}.{nameof(RunAsync)}");
}
}
}
This class is the trickiest in this example. The RunAsync method will execute every time the primary replica of your actor service is started up (after failover, resource balancing, application upgrade, etc.). It schedules reminder when the service is started for the first time, or reschedules reminder after each upgrade (reminder interval may be different in a new version). Do not remove the base.RunAsync method call.
The switch is necessary, because the actor must implement an interface which is inherited from IActor directly. Otherwise we could place the RegisterReminder method into a separate interface between IActor and IActor1. No interface hierarchy is currently supported. Violating this rule will break the build.
Program.cs:
internal static class Program {
private static void Main() {
try {
ActorRuntime.RegisterActorAsync<Actor1>((context, actorType) => new ScheduledActorService<IActor1>(context, actorType)).Wait();
ActorRuntime.RegisterActorAsync<Actor2>((context, actorType) => new ScheduledActorService<IActor2>(context, actorType)).Wait();
Thread.Sleep(Timeout.Infinite);
} catch {
throw;
}
}
}
The Main method registers actors with ScheduledActorService<T> class which is responsible for registration of reminders. The class must be declared with corresponding actor interface.
You can experiment with reminder intervals and execution durations to answer several questions about reminders behavior.
- What happens when the job execution takes longer than period interval?
The ReceiveReminderAsync method is awaitable for a good reason. Next tick will happen after methods finished its execution. - What if hosting node fails or needs to update underlying systems?
The service execution is interrupted and instantiated on a different node. The ScheduledActorService will call the RegisterReminder method for every registered actor. - What if I want to run a job every day at a given time?
The RegisterReminderAsync method has a dueTime parameter. I must determine a duration to the nearest occurrence based on a current time.