Microsoft Orleans — Reminders and grains calling grains

Orleans is an actor model framework — a framework used for easily creating distributed systems across a cluster of machines. In this post we’ll explore the “Reminders” feature of Orleans.

First off, the docs:


Reminders can be used in Orleans to perform tasks on a “schedule” more or less. I’m having trouble thinking of a simple example that actually makes sense, so, we’re going to go with an everything’s ok alarm:

To build the everything’s ok alarm, we’ll need to do a few things:

  • Enable a reminder service within the ISiloHostBuilder — we’ll use the in memory one just for simplicity's sake, and to not have to rely on additional infrastructure
  • A new reminder grain
  • Something for the reminder grain to do

I figured we could use the FakeEmailSender introduced in:

in order to send “Fake email” everything’s ok notifications.


Enable the Reminder service

Starting from https://github.com/Kritner-Blogs/OrleansGettingStarted/releases/tag/v0.40, we’ll enable the in-memory reminder service by adding the following line to our ISiloHostBuilder.

.UseInMemoryReminderService()

The full method is:

private static async Task<ISiloHost> StartSilo()
{
// define the cluster configuration
var builder = new SiloHostBuilder()
.UseLocalhostClustering()
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "dev";
options.ServiceId = "HelloWorldApp";
})
.Configure<EndpointOptions>(options => options.AdvertisedIPAddress = IPAddress.Loopback)
.AddMemoryGrainStorage(Constants.OrleansMemoryProvider)
.ConfigureApplicationParts(parts =>
{
parts.AddApplicationPart(typeof(IGrainMarker).Assembly).WithReferences();
})
.ConfigureServices(DependencyInjectionHelper.IocContainerRegistration)
.UseDashboard(options => { })
.UseInMemoryReminderService()
.ConfigureLogging(logging => logging.AddConsole());
 var host = builder.Build();
await host.StartAsync();
return host;
}

Some other possible reminder services to use include AzureTableReminderService and AdoNetReminderService.


Reminder Grain

Let’s create our everything’s ok alarm grain! I discovered in writing this, that there is a 1 minute minimum on the amount of time between “reminds”, so unfortunately, we’ll not be going with the originally planned 3 seconds :(

Anyway… our grain interface:

public interface IEverythingIsOkGrain : IGrainWithStringKey, IRemindable
{
Task Start();
Task Stop();
}

In the above, we’re doing a pretty standard grain interface, with the additional (to be) implemented IRemindable. Two methods are attached to the interface, one to start the reminder, one to stop it. Note that the IRemindable interface requires the implementing class to implement:

Task ReceiveReminder(string reminderName, TickStatus status);

The Grain Implementation — also Grainception!

As I mentioned previously, we’ll be using the FakeEmailSender created from a previous post, as well as having our to be created grain utilize other grains (grainception)!

That could look like:

[StorageProvider(ProviderName = Constants.OrleansMemoryProvider)]
public class EverythingIsOkGrain : Grain, IEverythingIsOkGrain
{
IGrainReminder _reminder = null;
 public async Task ReceiveReminder(string reminderName, TickStatus status)
{
// Grain-ception!
var emailSenderGrain = GrainFactory
.GetGrain<IEmailSenderGrain>(Guid.Empty);
  await emailSenderGrain.SendEmail(
"homer@anykey.com",
new[]
{
"marge@anykey.com",
"bart@anykey.com",
"lisa@anykey.com",
"maggie@anykey.com"
},
"Everything's ok!",
"This alarm will sound every 1 minute, as long as everything is ok!"
);
}
 public async Task Start()
{
if (_reminder != null)
{
return;
}
  _reminder = await RegisterOrUpdateReminder(
this.GetPrimaryKeyString(),
TimeSpan.FromSeconds(3),
TimeSpan.FromMinutes(1) // apparently the minimum
);
}
public async Task Stop()
{
if (_reminder == null)
{
return;
}
  await UnregisterReminder(_reminder);
_reminder = null;
}
}

A few things of note from the above:

  • [StorageProvider(ProviderName = Constants.OrleansMemoryProvider)] — we’re making the grain stateful so (theoretically) the reminder will persist on shutdown. Note, it will not in our case because of using in memory storage, I *think* it would otherwise.
  • IGrainReminder _reminder = null; — holds reference to our started reminder, used for stopping the reminder.
  • Task ReceiveReminder(string reminderName, TickStatus status) — this is the method where we actually define what happens when the reminder occurs.
  • var emailSenderGrain = GrainFactory.GetGrain<IEmailSenderGrain>(Guid.Empty); — here we’re using a slightly different means of retrieving a grain, since we’re actually doing it from the SiloHost, rather than Client. Note that this grain being pulled also makes use of dependency injection, but its dependency is only injected into the grain that actually needs it, not this reminder grain.

New grain menu option

as per usual, we’re going to create a new IOrleansFunction concretion for use in our menu system; that new grain will also be added to be returned from our IOrleansFunctionProvider.

public class EverythingIsOkReminder : IOrleansFunction
{
public string Description => "Demonstrates a reminder service, notifying the user that everything is ok... every three seconds...";
 public async Task PerformFunction(IClusterClient clusterClient)
{
var grain = clusterClient.GetGrain<IEverythingIsOkGrain>(
$"{nameof(IEverythingIsOkGrain)}-{Guid.NewGuid()}"
);
  Console.WriteLine("Starting everything's ok alerm after key press.");
Console.ReadKey();
  Console.WriteLine("Starting everything's ok reminder...");
await grain.Start();
  Console.WriteLine("Reminder started.  Press any key to stop reminder.");
Console.ReadKey();
  await grain.Stop();
  ConsoleHelpers.ReturnToMenu();
}
}

Trying it out

As per the norm, we’ll be starting the SiloHost, the Client, and trying the new grain out.

The everything’s ok reminder

In the above, you can see that our “FakeEmail” went out to the Orleans log, stating that everything’s ok.

One other cool thing we can see due to adding the Orleans Dashboard in a previous post is:

Reminder shows up on dashboard

Neat!


In this post we learned a little bit about another Orleans feature — Reminders! You can find the code as of this post at: