Dajbych.net


Azure Service Fabric & Scheduled Tasks

, 5 minutes to read

service fabric logo

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

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.