The Orleans Persistence Model
Persistence in Orleans is a simple declarative model where you identify the data to be saved in permanent storage via convention, and the programmer controls when and where the data is stored. Using this model is not required however, you can roll your own.How It Works
You declare what data needs to be saved using the IGrainState interface, and you pass this interface into the GrainBase when creating your grain class. You will also need to set a reference to the provider in the host project and set the provider type in the server configuration XML. Once this is done, the framework will attempt to load the grain's state information from permanent storage on activation. Saving is up to the developer and is done with a simple call to the provider's WriteStateAsync() method.
The Provider Interface
Orleans provides an IStorageProvider interface that we must implement if we are going to create an Orchestrate provider. It is fairly simple fortunately and here it is:
class OrchestrateProvider : IStorageProvider
{
#region IStorageProvider Members
public Task ClearStateAsync(string grainType, Orleans.GrainReference grainReference, Orleans.GrainState grainState)
{
throw new NotImplementedException();
}
public Task Close()
{
throw new NotImplementedException();
}
public Orleans.OrleansLogger Log
{
get { throw new NotImplementedException(); }
}
public Task ReadStateAsync(string grainType, Orleans.GrainReference grainReference, Orleans.IGrainState grainState)
{
throw new NotImplementedException();
}
public Task WriteStateAsync(string grainType, Orleans.GrainReference grainReference, Orleans.IGrainState grainState)
{
throw new NotImplementedException();
}
#endregion
#region IOrleansProvider Members
public Task Init(string name, Orleans.Providers.IProviderRuntime providerRuntime, Orleans.Providers.IProviderConfiguration config)
{
throw new NotImplementedException();
}
public string Name
{
get { throw new NotImplementedException(); }
}
#endregion
}
Orchestrate Provider
Lets start with the OrleansProvider members. These are bits that set up the storage mechanism on first use. The name property interface can be satisfied with a simple private string with a public getter. I will leave that to you to do on your own. The Init task is a bit more interesting. Here we need to instantiate an Orchestrate.NET instance and configure it with our API key.In the init function we will set the name property, get our API key and instantiate our Orchestrate.NET instance. The config parameter has a dictionary of all of the items declared in the server configuration file.
public Task Init(string name, IProviderRuntime providerRuntime, IProviderConfiguration config)
{
Name = name;
if (string.IsNullOrWhiteSpace(config.Properties["APIKey"]))
throw new ArgumentException("APIKey property not set");
var apiKey = config.Properties["APIKey"];
_orchestrate = new Orchestrate.Net.Orchestrate(apiKey);
return TaskDone.Done;
}
Next lets set up our ReadStateAsync. Now we have a decision to make, what will be the name of our Orchestrate collection and what will be the items key? We will use the grain state's type name for the name of our collection and the grains key for our key. As you know when we read items out of Orchestrate we will get back a json string. So the next step is to deserialize it back out to the grain state type and cast that to IGrainState. Now we can call the grain states SetAll() method and we are set. Let's see the code:
public async Task ReadStateAsync(string grainType, GrainReference grainReference, IGrainState grainState)
{
var collectionName = grainState.GetType().Name;
var key = grainReference.ToKeyString();
try
{
var results = await _orchestrate.GetAsync(collectionName, key);
var dict = ((IGrainState)JsonConvert.DeserializeObject(results.Value.ToString(), grainState.GetType())).AsDictionary();
grainState.SetAll(dict);
}
catch (Exception ex)
{
Console.WriteLine("==> No record found in {0} collection for id {1}\n\r", new object[] { collectionName, key });
WriteStateAsync(grainType, grainReference, grainState);
}
}
Now if we have an exception thrown, it means that item does not exist in our collection. Because all grains always exist in Orleans, we will go ahead and write the item to our collection.
Speaking of writing, lets go ahead and look at the WriteStateAsync code:
public async Task WriteStateAsync(string grainType, GrainReference grainReference, IGrainState grainState)
{
var collectionName = grainState.GetType().Name;
var key = grainReference.ToKeyString();
try
{
var results = await _orchestrate.PutAsync(collectionName, key, grainState);
}
catch (Exception ex)
{
Console.WriteLine("==> Write failed in {0} collection for id {1}", new object[] { collectionName, key });
}
}
And the ClearStateAsync:
public async Task ClearStateAsync(string grainType, GrainReference grainReference, GrainState grainState)
{
var collectionName = grainState.GetType().Name;
var key = grainReference.ToKeyString();
await _orchestrate.DeleteAsync(collectionName, key, false);
}
The last method is the Close, with the Orchestrate.NET provider we can simply set our instance to null and be done. I will leave that code to you.
Wrap Up
And that is all there is to it. Check out the source code and sample project on github. I believe Orleans will have a place in your tool box and Orchestrate makes for a powerful persistence mechanism to pair with it.If you are going to download and run the code yourself, make sure you create an app in Orchestrate, create ManagerState and EmployeeState collections, then grab your API key and put it at the appropriate place in the DevTestServerConfiguration.xml file.