Dajbych.net


Azure Service Fabric & Scheduled Tasks

, 7 minutes to read

Sched­uled tasks have many names. In Win­dows, it is tra­di­tion­ally called Task Sched­uler. In Unix-like en­vi­ron­ments job sched­uler is called Cron dae­mon. Mi­crosoft Azure con­tains Azure Sched­uler and Azure Web Apps have We­b­Jobs. Ser­vice Fab­ric has its own mech­a­nism called Ac­tor Remin­der. This ar­ti­cle ex­plains how to im­ple­ment them. Mul­ti­ple jobs could be encap­su­lated in a sin­gle as­sem­bly.

Quick overview of Actor service

Ser­vice Fab­ric clus­ter can host mul­ti­ple ap­pli­ca­tions. One ap­pli­ca­tion con­tains one or mul­ti­ple ser­vices. The ap­pli­ca­tion can com­bine sev­eral ser­vice types. For ex­am­ple, it can con­sist of one State­less ser­vice, two State­ful ser­vices and ten Ac­tors. Sched­uled jobs in Ser­vice Fab­ric are built on top of Actor model.

The ac­tor is a class with or without per­sis­tent state. The ac­tor is called via its proxy. Data se­ri­al­iza­tion is done au­to­mat­i­cally. One ac­tor has at most one thread. All calls to a sin­gle ac­tor are queued. The ac­tor is scaled out based on range of its iden­ti­fier. When the caller ob­tains a proxy of some ac­tor, it must pro­vide actor’s iden­ti­fier. If the ac­tor with a given iden­ti­fier does not ex­ists it is in­s­tan­ti­ated. Ev­ery ac­tor in­s­tance has its own state. If the in­s­tance is not used for one hour (a de­fault in­ter­val) the in­s­tance is garbage col­lected.

Create a new project

Start Page → Cre­ate new project… → In­stalled → Tem­plates → Vi­sual C# → Cloud → Ser­vice Fab­ric Ap­pli­ca­tion → OK Ac­tor Ser­vice OK

IAc­tor1.cs:

public interface IActor1 : IActor { Task RegisterReminder(); }

This is the in­ter­face of Ac­tor1. The ac­tor must be called only through its in­ter­face im­ple­mented by ac­tor’s proxy. Ac­tor1 ex­poses only one method to set up a timer.

Ac­tor1.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 im­ple­men­ta­tion of Ac­tor1 class con­tains a logic of sched­uled job. When the re­min­der ticks, it in­vokes the Re­ceiveRe­min­derAsync method through IRe­mind­able in­ter­face.

In this ex­am­ple the method writes time of start and end of its ex­e­cu­tion to the file in the work­ing di­rec­tory. You can tog­gle break­point af­ter the Di­rec­tory.GetCur­rent­Di­rec­tory() call to see the file lo­ca­tion. The file lo­ca­tion is changed with each de­ploy to the clus­ter (which is not an up­grade).

The Reg­is­ter­Re­min­der method is re­spon­si­ble for schedul­ing (or rescheduling) of the re­min­der named Re­min­der1. The re­min­der ticks if the ac­tor is garbage col­lected (new in­s­tance is cre­ated). The timer ticks be­fore garbage col­lec­tion only. The times stops along with garbage col­lec­tion.

Sched­uledAc­torSer­vice .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 trick­i­est in this ex­am­ple. The RunAsync method will ex­e­cute ev­ery time the pri­mary replica of your ac­tor ser­vice is started up (af­ter failover, re­source bal­anc­ing, ap­pli­ca­tion up­grade, etc.). It sched­ules re­min­der when the ser­vice is started for the first time, or resched­ules re­min­der af­ter each up­grade (re­min­der in­ter­val may be dif­fer­ent in a new ver­sion). Do not re­move the base.RunAsync method call.

The switch is nec­es­sary, be­cause the ac­tor must im­ple­ment an in­ter­face which is in­her­ited from IAc­tor di­rectly. Other­wise we could place the Reg­is­ter­Re­min­der method into a sep­arate in­ter­face be­tween IAc­tor and IAc­tor1. No in­ter­face hi­er­ar­chy is cur­rently sup­ported. Vi­o­lat­ing this rule will break the build.

Pro­gram.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 reg­is­ters ac­tors with Sched­uledAc­torSer­vice<T> class which is re­spon­si­ble for reg­is­tra­tion of re­min­ders. The class must be de­clared with cor­re­spond­ing ac­tor in­ter­face.

You can ex­per­i­ment with re­min­der in­ter­vals and ex­e­cu­tion du­ra­tions to an­swer sev­eral ques­tions about re­min­ders be­hav­ior.