Microsoft Orleans — Reusing Grains and Grain State

Microsoft Orleans — Reusing Grains and Grain State

We’ve explored Orleans for distributing application logic across a cluster. Next, we’ll be looking at grain reuse and grain state…

Recap

Note the starting point of this code can be found here. As described previously, grains are the “primitives” that are created for use with Orleans code. You invoke grains in a very similar manner to your “normal” code to make it as simple as possible. In the previous example we simply called the grain a single time; it takes in a value, and spits it back:

1
2
var grain = client.GetGrain<IHelloWorld>(Guid.NewGuid());
var response = await grain.SayHello(name); Console.WriteLine($"\n\n{response}\n\n");

Returns:

Entering “Kritner” returns “Kritner” along with a message that’s just the bee’s knees.

Microsoft Orleans — Reusing Grains and Grain State

Grain Reuse

Grains don’t have to be used only once, in most situations I would wager they’re used in the hundreds and thousands of times. Though the current grain we’re working with doesn’t have much use to be invoked multiple times, it can still make for a (good?) example.

Let’s change our grain implementation a bit from:

1
2
3
4
5
6
7
public class HelloWorld : Grain, IHelloWorld
{
public Task<string> SayHello(string name)
{
return Task.FromResult($"Hello World! Orleans is neato torpedo, eh {name}?");
}
}

To:

1
2
3
4
5
6
7
public class HelloWorld : Grain, IHelloWorld
{
public Task<string> SayHello(string name)
{
return Task.FromResult($"Hello from grain {this.GetGrainIdentity()}, {name}!");
}
}

In the above, we’re now printing out the grain’s uniqueId/primary key. The primary key isn’t super important in the current state of the grain implementation, but patience you must have, my young padawan.

Next, let’s change our Client to call a single grain, several times. The original:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static async Task DoClientWork(IClusterClient client)
{
Console.WriteLine("Hello, what should I call you?");
var name = Console.ReadLine();

if (string.IsNullOrEmpty(name))
{
name = "anon";
}

// example of calling grains from the initialized client
var grain = client.GetGrain<IHelloWorld>(Guid.NewGuid());

var response = await grain.SayHello(name);
Console.WriteLine($"\n\n{response}\n\n");
}

Should change to:

1
2
3
4
5
6
7
8
9
private static async Task DoClientWork(IClusterClient client)
{
// example of calling grains from the initialized client
var grain = client.GetGrain<IHelloWorld>(Guid.NewGuid());

Console.WriteLine($"{await grain.SayHello("1")}");
Console.WriteLine($"{await grain.SayHello("2")}");
Console.WriteLine($"{await grain.SayHello("3")}");
}

Running the above code will present us with:

Grain executed 3 times with different inputs

Above, we can see that the same grain (as indicated by *grn/CD25ADD4/ba676182) was used for all three invokes of grain.SayHello.

Now you may be wondering to yourself:

Can we have multiple instantiations of the same grain, with separate primary keys?

We sure can! Let’s take a look:

1
2
3
4
5
6
7
8
9
10
private static async Task DoClientWork(IClusterClient client)
{
// example of calling grains from the initialized client
var grain = client.GetGrain<IHelloWorld>(Guid.NewGuid());
var grain2 = client.GetGrain<IHelloWorld>(Guid.NewGuid());

Console.WriteLine($"{await grain.SayHello("1")}");
Console.WriteLine($"{await grain2.SayHello("2")}");
Console.WriteLine($"{await grain.SayHello("3")}");
}

output:

Multiple instantiations of the same grain. Code here.

What does the above all mean? In part, it means that grains can be instantiated one or multiple times depending on need. What kind of need would we have in multiple instantiations? Well in this grain’s case, none that I can think of since the grain always returns what it receives. Where multiple instantiations can really shine is when it comes to grains containing state and/or contextual data.

Stateful Grains

Grains can have a notion of “state”, or instance variables pertaining to some internals of the grain, or its context. Orleans has two methods of tracking grain state:

  • Extend Grain<T> rather than Grain
  • Do it yo’self

Generally, I’d prefer to not write code that’s already been written, so I’ll be sticking with the first option.

Grain Persistence

In order to track grain state, our grains state needs to be persisted somewhere. Orleans offers several methods of grain state persistence (doc). For demonstration purposes, I’ll be using the MemoryGrainStorage. Be advised this this method of persistence is destroyed when the silo goes down, so is probably not especially useful in production like scenarios.

To utilize stateful grains you need a few things:

  • configured persistence store(s)
  • stateful grain(s)

Grain Persistence how-to

We need to configure our storage provider — some of the providers can be “more involved” in that you need some sort of backing infrastructure and/or cloud capabilities/money (:D). That’s a big reason I’m going for the memory provider!

To register the memory provider, let’s update our original SiloHost’s builder from:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
.ConfigureLogging(logging => logging.AddConsole());

var host = builder.Build();
await host.StartAsync();
return host;
}

to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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("OrleansStorage")
.ConfigureLogging(logging => logging.AddConsole());

var host = builder.Build();
await host.StartAsync();
return host;
}

Note that since the above is using a Builder Pattern, you could just add:

1
builder.AddMemoryGrainStorage("OrleansStorage");

as a separate line in between the instantiation of the builder, and the var host = builder.Build();.

That’s it!

A persistent grain

Next, let’s slap together a grain with some state.

We’ll create a grain that can track the number of times a user visits our “site” (pretend it’s a website). The first thing is to define the interface:

1
2
3
4
5
public interface IVisitTracker : IGrainWithStringKey
{
Task<int> GetNumberOfVisits();
Task Visit();
}

Properties of the above:

  • String key — since we’re using it to track visits to our site, using the account email seems to make sense as a unique key
  • Task<int> GetNumberOfVisits() — this method will be used to retrieve the number of times a user has visited.
  • Task Visit() — this method will be invoked when a user visits the site.

The grain implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[StorageProvider(ProviderName = Constants.OrleansMemoryProvider)]
public class VisitTracker : Grain<VisitTrackerState>, IVisitTracker
{
public Task<int> GetNumberOfVisits()
{
return Task.FromResult(State.NumberOfVisits);
}

public async Task Visit()
{
var now = DateTime.Now;

if (!State.FirstVisit.HasValue)
{
State.FirstVisit = now;
}

State.NumberOfVisits++;
State.LastVisit = now;

await WriteStateAsync();
}
}

public class VisitTrackerState
{
public DateTime? FirstVisit { get; set; }
public DateTime? LastVisit { get; set; }
public int NumberOfVisits { get; set; }
}

A few new things going on above:

  • Specifying a storage provider as a class level attribute — this is the storage provider we defined in the changes to the SiloHost earlier.
  • Extending Grain<T> instead of Grain where <T> is a state class.
  • Manipulation of this.State, in order to keep track of the specific instantiation “state”.
  • A state class VisitTrackerState

Seeing Grain State in action

Finally, let’s see what sort of things we can do in our Client app using our new stateful grain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private static async Task DoStatefulWork(IClusterClient client)
{
var kritnerGrain = client.GetGrain<IVisitTracker>("kritner@gmail.com");
var notKritnerGrain = client.GetGrain<IVisitTracker>("notKritner@gmail.com");

await PrettyPrintGrainVisits(kritnerGrain);
await PrettyPrintGrainVisits(notKritnerGrain);

PrintSeparatorThing();
Console.WriteLine("Ayyy some people are visiting!");

await kritnerGrain.Visit();
await kritnerGrain.Visit();
await notKritnerGrain.Visit();

PrintSeparatorThing();

await PrettyPrintGrainVisits(kritnerGrain);
await PrettyPrintGrainVisits(notKritnerGrain);

PrintSeparatorThing();
Console.Write("ayyy kritner's visiting even more!");

for (int i = 0; i < 5; i++)
{
await kritnerGrain.Visit();
}

PrintSeparatorThing();

await PrettyPrintGrainVisits(kritnerGrain);
await PrettyPrintGrainVisits(notKritnerGrain);
}

private static async Task PrettyPrintGrainVisits(IVisitTracker grain)
{
Console.WriteLine($"{grain.GetPrimaryKeyString()} has visited {await grain.GetNumberOfVisits()} times");
}

private static void PrintSeparatorThing()
{
Console.WriteLine($"{Environment.NewLine}-----{Environment.NewLine}");
}

Nothing in the above that we haven’t really done before, getting instances of the same type of grain using two separate “users”, invoking grain methods on them several times, and printing the results.

When running the app, we are presented with:

Demonstrating stateful grains, first run

You can see in the above that our visit counter is incrementing with each visit, and kritner is visiting a lot more than notKritner.

What happens if we run this same app again?

Second run

You can see that our visit counter left off from the first run — but of course it did; we’re using stateful grains! Just as a note again, because we’re using the memory provider, once the SiloHost is brought down, the grain’s state will not be kept. This state would not be destroyed on silo shutdown when using other grain storage providers.

Hopefully this helps others start to see the powerful possibilities that Orleans offers — Actors, grains, and grain state are barely scratching the surface of what Orleans can do. Hopefully I’ll have more to write about regarding Orleans sometime soon!

Full code at this point can be found at https://github.com/Kritner-Blogs/OrleansGettingStarted/releases/tag/v0.11

Related:

Author

Russ Hammett

Posted on

2018-10-17

Updated on

2022-10-13

Licensed under

Comments