Scheduled tasks have many names. In Windows, it is traditionally called Task Scheduler. In Unix-like environments, the 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 can be encapsulated in a single assembly.
Quick overview of Actor service
A 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 the Actor model.
An 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 the range of its identifier. When the caller obtains a proxy of some actor, it must provide the actor’s identifier. If the actor with a given identifier does not exist, 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 the
Actor1.Interfaces
to theActor1
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 the IActor1.cs interface and remove the
SetCountAsync
andGetCountAsync
methods. Add theTask RegisterReminder()
method instead (see code below). - Open the Actor1.cs class and change the
StatePersistence
attribute fromPersisted
toNone
. - Remove the
SetCountAsync
andGetCountAsync
methods. - Implement the
IActor1
andIRemindable
interfaces (see code below). - Create a new class
ScheduledActorService<T>
and implement it in accordance with the example below. - In the Program.cs file in the
Main
method, replace theActorService
class with theScheduledActorService<T>
class (see code below). - Create a copy of the
IActor1
interface and name itIActor2
. - Create a copy of the
Actor1
class and name itActor2
. InActor2
, in theRegisterReminder
method, change all occurrences of the"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, which is implemented by the 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 the Actor1
class contains the logic of a scheduled job. When the reminder ticks, it invokes the ReceiveReminderAsync
method through the IRemindable
interface.
In this example, the method writes the start and end times of its execution to a file in the working directory. You can toggle a breakpoint after the Directory.GetCurrentDirectory()
call to see the file location. The file location changes with each deployment to the cluster (which is not an upgrade).
The RegisterReminder
method is responsible for scheduling (or rescheduling) the reminder named Reminder1
. The reminder ticks if the actor is garbage collected (a new instance is created). The timer ticks only before garbage collection. The timer 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 a reminder when the service is started for the first time, or reschedules the reminder after each upgrade (the 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 that 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 the ScheduledActorService<T>
class, which is responsible for the registration of reminders. The class must be declared with the corresponding actor interface.
You can experiment with reminder intervals and execution durations to answer several questions about reminder behavior.
- What happens when the job execution takes longer than the period interval?
TheReceiveReminderAsync
method is awaitable for a good reason. The next tick will happen after the method has finished its execution. - What if the hosting node fails or needs to update underlying systems?
The service execution is interrupted and instantiated on a different node. TheScheduledActorService
will call theRegisterReminder
method for every registered actor. - What if I want to run a job every day at a given time?
TheRegisterReminderAsync
method has adueTime
parameter. You must determine the duration to the nearest occurrence based on the current time.