If you’ve been wanting to learn more about Microsoft’s Project Orleans programming model, this primer will give you the basic concepts you need to understand to get started.
Actor Based Programming
Actor based programming is designed to allow for objects representing multiple instances of related real-world artifacts to interact as independent single threaded entities in a distributed environment. Their location, activation, and state are managed by the Orleans framework which abstracts away the hardware resources allowing for scalability and high level of concurrency.
Specifically Orleans is designed to allow multiple instances of the same set of classes to interact in a highly distributed fashion. For example, Orleans would be used to create actors that represent multiple devices in the field, or people in social network. Each class of actor has the same basic functionality and interactions. The business logic of the system comes from the interaction of the actors, not necessarily the actors themselves.
A grain is a class that represents an actor in the environment. It can contain state and expose logic via methods that can be called by other grains or clients. All methods are asynchronous and return a Task. Grains are by their definition small and so should be kept as simple as possible.
Every grain has a GUID primary key for its ID. While you can reference the grains by long, GUID, string, or composite key all references are converted to a GUID by the framework.
A grain at its most basic implements an interface that derives from IGrain. IGrain is a marker interface that is used by the framework and then you define the methods for your grain in that interface. The following is an example of an IGrain Interface.
public interface IMyActor : IGrainWithIntegerKey
Grains then implement the interface you defined and derive from the Grain class and the defined Interface. The example below is an implementation of the IMyActor interface
public class MyActor : Grain, IMyActor
public Task<string> DoWork()
//Do something interesting
Grains can store state as private variables for the life of grain. For the state to survive a silo shutdown it must be persisted as described in a later section.
Silos are execution environments for grains and is usually run as one per machine/virtual machine. Silos can be linked together to form clusters for scalability and the Orleans framework manages resiliency if a silo is removed from the cluster.
While a silo can be hosted in any process as well as on Azure, the Orleans SDK includes a stand-alone host called OrleansHosts.exe that can be run on a server or a development machine. It also has the ability to store system data via a System Store Provider for data such as reminders.
//Start Orleans Client
var myGrain = GrainClient.GrainFactory.GetGrain<ICustomGrain>(Guid.NewGuid());
Orleans uses a single threaded execution model where each call to a grain is queued and execution takes place in a turn based model. The Orleans framework ensures that messages are delivered in order.
Grains interact by sending messages, which take the form of a C# class. When a class is sent between grains Orleans creates a deep copy of the object to avoid passing the object by reference. This ensures that the object doesn’t change before the execution on the grain. This can be a costly process so if you are sure that you won’t change the data being passed, you can mark the class with the [Immutable] attribute and then Orleans won’t create a deep copy.
The delayed execution model in Orleans can result in a deadlock when two grains interact with each other. To enable a grain to execute other messages while waiting for a response, the grain can be marked with the [Reentrant] attribute.
Don’t underestimate the difference in thinking required for a single threaded distribution model. Unpredicted behavior seems to be common in early stages of development.
While grains can survive the shutdown of clients, in order to maintain state between a silo shutdown the grain must declare their state as part of their type and reference a StorageProvider in an attribute on the grain.
[StorageProvider(ProviderName = "MemoryStore")]
public class MyActor : Grain<MyState>, IMyActor
In this case the referenced ProviderName must match one of the StorageProviders defined in the OrleansConfiguration.xml file.
<Provider Type="Orleans.Storage.MemoryStorage" Name="MemoryStore" />
There are a number of prebuilt StorageProviders available:
- MemoryStore – This is a development store which persists data in memory in the silo. If the silo is shutdown or suffers an error, the data is lost.
- HierarchicalKeyStore – This is a storage provider that uses a nested dictionary model to store state, but as with the MemoryStore does not actually persist the data and will lose information on silo shutdown or failure.
- MemoryStorageWithLatency – This is another development storage provider that includes intentional delays for simulating latency of moving the data out of process.
- SharedStorageProvider – This is a wrapper that allows you to use store state across multiple storage providers.
- AzureTableStorage – This provider is used to persist state to Azure Table Storage and is the primary storage provider for Azure based environments.
There are other Storage Providers available and creating your own merely involves implementing a simple interface.
Orleans is designed for massive numbers of grains. At this scale, any logic that requires an accumulation or interaction with a large number of grains can be expensive. This is particularly true for accumulations across silos in a cluster. A best practice for Orleans is to use aggregation grains at the silo level.
Orleans supports a model for stream processing similar to the Reactive Extensions for .NET which allow applications to create dynamic, durable event processing across multiple grains. Grains can consume or produce events into the streams. Grains can also have multiple subscriptions. There are currently three stream providers to supply the underlying framework for the streams.
- Simple Message Stream Provider – This is the standard stream provider which sends messages over TCP using the Orleans messaging system.
- Azure Queue Stream Provider – This provider delivers messages over the Azure Queue system. Produced messages are delivered onto the Azure Queue and the consumers receive messages from pulling agents listening on Azure queues.
- Persistent Stream Provider – This is the base class for the Azure Queue Stream provider and allows the developer to change the queueing mechanism being used to create a persistent stream for reliable message delivery.
Orleans offers two types of timers for grains. The first, called a Timer is similar to a System.Threading.Timer except that it does not survive across activations and follows the single threading model for Orleans.
var timer = RegisterTimer(
Reminders are timers that can be persisted and are tied to a grain, and not to a specific activation. Reminders can actually trigger grain activation and also survive silo failures. Reminders are stored in the system store.
The Orleans documentation specifically calls out that Reminders should not be used for high frequency timers and should be triggered at the minutes level or higher
Activation Garbage Collection
Grains are considered active when receiving messages or reminders. Inactive grains can be garbage collected by the Orleans framework based on settings in the configuration file. Configuration of timeouts for active grains can be configured globally or per type.
Project Orleans Documentation – http://dotnet.github.io/orleans/
Project Orleans Source Code – https://github.com/dotnet/orleans