Orleans with F# story

Orleans is an actor based framework and runtime aimed to make distributed OOP as easy as possible. Orleans was designed to run on several machines or containers and thus cloud-friendly is very easy to use when you have to anticipate an application with high load and/or complex interactions. Here I will describe only basics because advanced topics are better described in official docs https://dotnet.github.io/orleans/

In terms of Orleans, a single “actor” is an object which encapsulates in own state and can make remote calls of other grains via grain reference. If you prefer message-based protocols or frameworks (ServiceStack.net, akka, NServiceBus) then you might prefer to use such interface implemented via Orleannka https://github.com/orleanscontrib/orleankka project.

In Orleannka (version 2) everything a grain receives or sends is a message. Imagine we’d like to implement an account handling grain. A grain’s contract would be a set of messages which it would accept and responses. It is also safe to define exceptions as part of the contract if needed since Orleans sends them back calling code and rethrows them there.

module Account =
  open Orleankka.FSharp
  // Message with 2 commands and a query
type Message =
| Topup of decimal
| Charge of decimal
| GetBalance
  type IAccountGrain =
inherit IActorGrain<Message>

This code is usually hosted in a separate project so that it can be shared with a client (web-api for example) and silo (Orleans’ backend) code

Before going to implementation details let’s also describe system message types (https://github.com/OrleansContrib/Orleankka/blob/09c1a8dd3f32abb64b16ffdea23f2576fa524d8a/Source/Orleankka.Runtime/ActorGrainMessages.cs):

  • Activate: it is being sent by the runtime when a grain is being initialized. Mainly this is a place to load state from a database.
  • Deactivate??: the opposite of Activate. It is being sent when the runtime cleans up memory or moves a grain from one silo to another. There is a chance grain will go offline without receiving this message (e.g. on server host crash) so it is generally suggested to persist state changes at the same operation as state change occurred.
  • A reminder with a name: sent when reminder triggered. Reminders are a part of scheduling part of Orleans. They are persistent, service silo restarts and are aimed to provide resilient invocation of anything happening in the future (starting from one minute). If a grain is inactive during when a reminder is triggered it will be activated starting with Activate message.

Everything else sent should be part a contract published by your grain.

Grain implementations are classes of two types. The very basic is an ActorGrain class which has only a single “Task<Object> Receive(object message)” method which should be implemented in grain classes. It is not very convenient if you are using C#, thus there is a DispatchActorGrain class which adds some reflection goodness by treating any method with names of “On”, “Handle”, “Answer” or “Apply” to be treated like receive method of a particular message. This adds you a way of adding some DSL for handlers like the following:

type Example() =
inherit DispatchActorGrain()
    member x.Handle(Command x) = task {
return
none()
}

member x.Answer(MyQuery qry) = 42

When you are using F# you can use the above syntax as well, but I prefer pattern matching on a raw message received, which ends up like the following:

type AccountGrain() =
inherit ActorGrain()
  let mutable balance = 0m
  interface IAccountGrain
override x.Receive(message:obj) = task {
match
message with
| :? Activate ->
balance <- 5m // load from actual db here
return none()
| :? Message as msg ->
match
msg with
| Topup amount ->
balance <- balance + amount
return none()
| Charge amount ->
balance <- balance - amount
return none()
| GetBalance ->
return
some(balance)
}

client to silo or mid grains interactions will like the following:

let accountRef = ActorSystem.actorOf<IUserWallet>(client, "UserId")
do! accountRef <! Topup 100m
let! balance = accountRef <? GetBalance

“client” is one of IActorSystem or “this.System” when called from inside actor grain.

Notice that the code of actor implementation is pretty raw and really needs some custom DSL to be provided. For example, for classic state persistence, you might have a base class requiring function implementation of “handle message -> State” and a wrapper which would persist state to a database after each successful operation. Event sourcing works well since you might store a state in-memory and read events only on a grain activation.

Last but not least, Orleannka has an extensive test fake implementation which helps a lot unit testing grains. Runtime, actor interactions, reminders setup are all implemented via TestKit. While it is still possible to mock everything, I find it much more convenient to use TestKit and mock only some external actor interaction by specifying which message to be returned on which request.

Features worth looking for but not mentioned here are:

  • state handling implementations for key/value and event sourcing (this one was not yet migrated to latest major version)
  • Stateless “worker” grains, which don’t have their own identity and are automatically scaled
  • Streams/observers for pub/sub

To sum up, Orleans + Orleanka is a great pair to run a project. They helped me a lot in a couple of applications by providing a sane and clean programming model and removing some problems which arise from using “stateless” architecture. While the F# story is still worth improving it is usable and can be shaped to the needs of a project.