Introduction to Grain Persistence with Microsoft Orleans

In this article, we’re going to take a look how the state of virtual grains can be saved to and loaded from a data store. Since there are various storage providers available for Microsoft Orleans, we will use the in-memory storage provider to introduce the concepts in a simple manner. This is not a real storage provider, and its state is lost if the silo goes down, but it gives us an easy way to┬álearn to set up and interact with storage providers.

The source code for this article is available in the Gigi Labs BitBucket repository. Look for the OrleansMemoryStorage folder.

Update 9th June 2017: Some material from this article was contributed towards the official Grain Persistence documentation.

A Simple Sample

Before we can see how grain persistence works, we need a minimal example. Let’s create a new Orleans Dev/Test Host project using the project templates from the Microsoft Orleans Tools for Visual Studio extension.

Let’s add a new interface that our grain will implement:

    public interface IPersonGrain : IGrainWithStringKey
    {
        Task SayHelloAsync();
    }

Next, we’ll implement a really simple grain:

    public class PersonGrain : Grain, IPersonGrain
    {
        public Task SayHelloAsync()
        {
            string primaryKey = this.GetPrimaryKeyString();

            Console.WriteLine($"{primaryKey} said hello!");

            return Task.CompletedTask;
        }
    }

Note: if you’re using a .NET Framework version prior to 4.6, use TaskDone.Done instead of Task.CompletedTask.

All we need to do now is actually use our grain. Locate the following comment in the template code generated in Main():

            // TODO: once the previous call returns, the silo is up and running.
            //       This is the place your custom logic, for example calling client logic
            //       or initializing an HTTP front end for accepting incoming requests.

…and replace it with the following:

            var joe = GrainClient.GrainFactory.GetGrain<IPersonGrain>("Joe");
            joe.SayHelloAsync();

We can now run the program to ensure that the grain’s SayHelloAsync() is actually called:

Adding State

While it is perfectly valid to have grain state in member variables within the grain itself, if we want to persist that state, we need something a little different.

The first thing we need to do is create a class that holds the state:

    public class PersonGrainState
    {
        public bool SaidHello { get; set; }
    }

Then, we need to change the definition of our grain class to inherit not from Grain, but from Grain<T>, where T is the grain state class:

    public class PersonGrain : Grain<PersonGrainState>, IPersonGrain

We can then change the code within our grain to save its state and do something a little more interesting:

    public class PersonGrain : Grain<PersonGrainState>, IPersonGrain
    {
        public async Task SayHelloAsync()
        {
            string primaryKey = this.GetPrimaryKeyString();

            bool saidHelloBefore = this.State.SaidHello;
            string saidHelloBeforeStr = saidHelloBefore ? " already" : null;

            Console.WriteLine($"{primaryKey}{saidHelloBeforeStr} said hello!");

            this.State.SaidHello = true;
            await this.WriteStateAsync();
        }
    }

All we’re doing here is that if the SaidHello property was set, then we write “Joe already said hello!” instead of “Joe said hello!”. However, note the last two lines. We’re setting the value of SaidHello to true, and then calling WriteStateAsync().

Orleans provides a very simple API for dealing with persistent state, as you can see above. You can access your grain’s state object using the State property. The WriteStateAsync(), ReadStateAsync() and ClearStateAsync() methods allow you to save, load, and clear the grain’s state from the data store respectively.

Note: we haven’t yet set up our data store, but we’ll do that shortly.

The logic for saving state is entirely up to you. You can save state with every message you receive (i.e. with every method call), or periodically, or in whatever manner makes most sense for your application. For instance, in our example above, we are saving state every time the SayHello() method is called. However, it would save unnecessary writes to the data store to do a simple check on the value of SaidHello, and write to the data store only if the value was previously false.

State is loaded automatically from the data store when the grain is activated, so you don’t actually need to call ReadStateAsync() unless it makes logical sense to do so, e.g. if your grain has ended up in an inconsistent state due to errors and you need to reload its original state from the data store.

Configuring a Persistence Store

Let’s run the application as it is now:

It is failing because we haven’t configured our storage provider yet. A grain having persistent state will by default look for a storage provider configured with the name “Default”. We can use an arbitrarily named storage provider by using an attribute:

    [StorageProvider(ProviderName = "OrleansStorage")]
    public class Person : Grain<PersonState>, IPerson

We now have to configure that OrleansStorage data store so that Orleans will know where to find it and how to interact with it. There are various different storage providers, as you can see from the Grain Persistence documentation. All of these can be configured either in code or using XML configuration.

In this example, we’ll use the MemoryStorage provider. This is not a real persistent data store, in the sense that it holds data in memory which is lost when the silo stops. While this does not allow for very interesting storage-and-retrieval scenarios, it allows us to experiment with storage providers without the hassle of setting up a real database. We’ll see how to set up proper storage providers with an underlying database in an upcoming article.

Configuring MemoryStorage in Code

You should have an OrleansHostWrapper class that was automatically created as part of the project template. In the Init() method, we can add the configuration code we need:

        private void Init()
        {
            siloHost.LoadOrleansConfig();

            siloHost.Config.AddMemoryStorageProvider("OrleansStorage");
        }

If you run it now, there will be no error:

The AddMemoryStorageProvider() is just a shortcut method. It takes a name and an optional number of grains used to store the state, which we can leave well enough alone with its default of 10:

The real way to set up any configuration provider is to use RegisterStorageProvider(), and pass in the type of the provider (either as a typename string or as a generic parameter), along with the storage provider name (in our case, “OrleansStorage”) and any additional parameters (in the case of MemoryStorage, we can set that NumStorageGrains here).

Using generics, it looks like this:

        private void Init()
        {
            siloHost.LoadOrleansConfig();

            siloHost.Config.Globals.RegisterStorageProvider<MemoryStorage>("OrleansStorage");
        }

On the other hand, this is how you’d do it if you specified the type name as a string:

        private void Init()
        {
            siloHost.LoadOrleansConfig();

            siloHost.Config.Globals.RegisterStorageProvider("Orleans.Storage.MemoryStorage",
                "OrleansStorage");
        }

Configuring MemoryStorage in XML

Instead of hardcoding your configuration, you can set it up in an OrleansConfiguration.xml file that your silo will then pick up.

Let’s add an OrleansConfiguration.xml file to the project. We also need to set the file’s Copy to Output Directory property to either Copy always or Copy if newer. Then, we can add the following to it:

<?xml version="1.0" encoding="utf-8"?>
<OrleansConfiguration xmlns="urn:orleans">
  <Globals>
    <StorageProviders>
      <Provider Type="Orleans.Storage.MemoryStorage"
                Name="OrleansStorage"
                NumStorageGrains="10" />
    </StorageProviders>
    <SeedNode Address="localhost" Port="22222" />
  </Globals>
  <Defaults>
    <Networking Address="localhost" Port="22222" />
    <ProxyingGateway Address="localhost" Port="40000" />
  </Defaults>
</OrleansConfiguration>

You can see how we’re configuring pretty much the same things that we did earlier in code. Any optional parameters, such as NumStorageGrains in this case, can also be added.

The StorageProviders section is all we need to set up a storage provider, but you’ll notice we have a lot more configuration in the file. The Dev/Test Host project template uses LocalhostPrimarySilo() configuration, which is good to set something up quickly, but it’s a bit of a pain to get to work with configuration files. Thus, we’ll remove it and replace its default configuration with that in the file. With a proper Orleans project setup, you won’t need to do the following steps.

Locate the following code in OrleansHostWrapper.cs:

            var config = ClusterConfiguration.LocalhostPrimarySilo();
            config.AddMemoryStorageProvider();
            siloHost = new SiloHost(siloName, config);

Replace it with the following:

            siloHost = new SiloHost(siloName);
            siloHost.ConfigFileName = "OrleansConfiguration.xml";

The Init() method should be left as it originally was:

        private void Init()
        {
            siloHost.LoadOrleansConfig();
        }

You can now run the program. It should work without problems, using the configuration from the file to set up the storage provider.

Summary

In order to persist a grain’s state, it is necessary to:

  1. Create a class that holds the grain’s state.
  2. Have the grain inherit from Grain<T>, where T is the grain state class.
  3. Access the grain’s state object using the State property.
  4. Use WriteStateAsync(), ReadStateAsync() and ClearStateAsync() to write, read or clear the grain’s state in the data store respectively.
  5. A grain’s state is automatically loaded from the data store when it is activated.
  6. A grain’s storage provider name is set using the StorageProvider attribute, in absence of which it uses one named “Default”.
  7. There are various storage providers available that work with different data stores. We have so far used only MemoryStorage, which is a volatile implementation to be used only for testing.
  8. Storage providers may be configured either in code or using an XML configuration file.