Introduction
Microsoft Orleans is a framework that claims massively to simplify the creation of fault tolerant, asynchronous distributed systems by abstracting away the complexities of persistence and thread synchronization that arise in such systems. It does this by using the actor model (albeit calling the actors "Grains") and by restricting the operations you can perform on them so as to make them thread safe.
Prerequisites
- The code in this article and the Microsoft Orleans framework require the .NET Framework v4.5 (or higher when released) to run
- The Orleans Tools for Visual Studio plug in was used to create the project
- Visual Studio 2015 (professional or higher if you want to use the plug-in)
- The Orleans Templates plug in simplifies
Background
In order to demonstrate this technology, I have put together an outline of a financial services application that is used to track funds (mutual funds, hedge funds, etc.). This is (of course) a hello-world level application but I think sufficient to show the promise this technology holds.
In our simple system, we will have these entities that we are going to model:
- Investors being the people who put money into the fund
- Fund being a type of trading account the investor can invest in
- Assets being the things bought and sold by the fund
- Brokers being the companies through which we trade the assets
Getting Started
The first step is to create a new project that defines the grain interfaces (the ways that the different grain "entities" can communicate with each other).
For each of our entities, we need to decide how we are going to uniquely identify it. Out of the box, the options are by unique string, integer or by globally unique id (GUID). For this hello-world level demo, I will be using string
s but if your actual entities don't have any intrinsic unique identifiers, then either GUID or incremental integer can be used.
The interface defines what can happen to an instance of the grain (entity) - for example, we might say that an Investor can subscribe to or redeem from a fund:
namespace FundGrainInterfaces
{
public interface IInvestorGrain : IGrainWithStringKey
{
Task Subscribe(IFundGrain fundToSubscribe, decimal amountToSubscribe);
Task<decimal> Redeem(IFundGrain fundToRedeem, decimal redemptionAmount);
}
}
Then we need to create a project to implement these interfaces in concrete classes. Each concrete grain class must inherit from the abstract
class "Grain
" as well as implement the above declared interface.
To illustrate the communication between grains, when an investor subscribes to or redeems from a fund that information is passed on to the Fund grain
:
public class InvestorGrain : Grain, IInvestorGrain
{
private Dictionary<string, decimal> _holdings = new Dictionary<string, decimal>();
public Task<decimal> Redeem(IFundGrain fundToRedeem, decimal redemptionAmount)
{
if (_holdings.ContainsKey(fundToRedeem.GetPrimaryKeyString()))
{
if (redemptionAmount <= _holdings[fundToRedeem.GetPrimaryKeyString()])
{
redemptionAmount = _holdings[fundToRedeem.GetPrimaryKeyString()];
}
return fundToRedeem.Redemption(redemptionAmount);
}
else
{
return Task.FromResult(decimal.Zero);
}
}
public Task Subscribe(IFundGrain fundToSubscribe, decimal amountToSubscribe)
{
fundToSubscribe.Subscription(amountToSubscribe);
if (_holdings.ContainsKey(fundToSubscribe.GetPrimaryKeyString()))
{
_holdings[fundToSubscribe.GetPrimaryKeyString()] += amountToSubscribe;
}
else
{
_holdings.Add(fundToSubscribe.GetPrimaryKeyString(), amountToSubscribe);
}
return TaskDone.Done;
}
}
Where the corresponding Fund grain
can deal with subscriptions and redemptions form the funds own available liquid cash:
public class FundGrain : Grain, IFundGrain
{
private decimal _liquidCash ;
public Task<decimal> Redemption(decimal redemptionAmount)
{
if (_liquidCash >= redemptionAmount)
{
_liquidCash -= redemptionAmount;
}
else
{
redemptionAmount = _liquidCash;
_liquidCash = 0;
}
return Task.FromResult(redemptionAmount);
}
public Task Subscription(decimal subscriptionAmount)
{
_liquidCash += subscriptionAmount;
return TaskDone.Done;
}
}
Creating a silo
Instances of these grains (entities) need to be hosted by a silo, which is effectively a virtual machine environment for that grain
. In order to test these, we need to create a silo test project:
In this host, you instantiate instances of your grains (entities) and you can then interact with them via their defined interface:
IFundGrain myTestFund = GrainClient.GrainFactory.GetGrain
Now, if you run multiple instances of this silo, they will allow run off the same underlying fund grain without the developer having to implement (or care about) any concurrency checking at all.
Points of Interest
- There are a number of storage providers you can use to persist the grains between uses including the various Azure cloud storage options.
History
- 19th July, 2016: Initial version