In Part I we have covered some fundamentals of Orleankka. In Part II we will learn a more sophisticated concepts behind Orleankka, such as error handling and replying to the sender. During this process, we will model a simple demo where we have a shop and consumers which are buying products.
Modeling a user account actor
Let’s start from modeling the consumer account. First we need to define messages for speaking to the actor, then the body of the actor.
The above code sample contains definitions of Account actor. The main responsibility of Account actor is handling consumer balance in thread safe manner. In order to create an actor, you need to inherit your custom type from Actor<TMessage>. The TMessage plays very important role, it defines all possible messages which your actor will respond to. As you noticed we use AccountMessage to incorporate with Account actor, it’s mean that the actor will react only on this type of messages. Also, there is Receive method which was overridden and has message and reply arguments. The message represents an incoming message that is pretty obvious. The reply is a function (obj -> unit) which returns a result to the caller.
| Balance -> reply balance // return balance to caller
When your actor wants to respond to the caller you need to invoke reply function and put the result in. In the current actor, we use reply function only one time to return the current balance. Of course, it’s possible to have an actor which returns nothing. The body of Receive method contains the pattern match expression which matches the message by type and executes all supported actions like Deposit, Withdraw and Balance.
| Withdraw amount ->
if balance >= amount then balance <- balance - amount
else invalidOp "Amount may not be larger than account balance. \n"
The withdraw case action contains validation for the current balance. In the invalid case, the actor will throw InvalidOperation exception.(We use very general exception type rather specific one, but it’s enough for the demo purposes.)
Modeling a shop actor
It’s time to inspect Shop actor. Shop actor represents the shop and is responsible for handling price, cash and stock. As you can expect this actor should interact with consumers(Account actors) in order to do withdraw money. How it can be done?
The answer is pretty simple, you can put consumer account information into message explicitly and Shop actor will use this information to send a response to the sender(Account actor). Let me show you full sample:
Ok, now let consider the Sell message definition:
Sell of account:ActorRef * count:int
What is ActorRef type? It’s client proxy to actor. You can think about it as a gateway for talking to an actor. We use account:ActorRef to send messages to Account actor. Basically Account actor will put kind of a “reference” (account:ActorRef) to himself in Sell message and send it to Shop actor, afterwards Shop actor will use this “reference” to send a response to Account actor.
I think it’s more clear now and we still have one important point to look into. Let me show you how we do withdraw operation:
// we send the Withdraw message from Shop to Account actor and
// asynchronously waits for response from actor
do! account <! Withdraw(amount)
I want to point out that in the sample above we are asynchronously waiting for a response(which is empty — Task<unit>) from Account actor. For example in C#, it also looks very natural, clear and concise.
await account.Tell(new Withdraw(amount));
It’s time to consume our actors, we do it by creating an ActorSystem and calling ActorOf<TActor>
As you may noticed in above sample we handled exceptions via standard try…with control-flow expression.
printfn "Let's sell 100 items to user \n"
do! shop <! Sell(account, 100)
with e -> printf "[Exception]: %s \n" e.Message
It’s very simple, widely used and concise technique but in addition to that Orleankka provides option for Railway Oriented Programming.
After that you can use your lovely bind (>>=) function to do composition of monadic functions.
let result = job |> Task.run >>= func1 >>= func2 >>= func3
Is it scalable architecture?
I want to point out that the actor models(Account, Shop) which we used for our demo - not good for production. Basically just imagine the situation that we are going to build e-commerce product like Amazon with these actor models. It’s obvious that Shop actor will be a hot actor and your solution will not scale well. The good models for actors might be ShopItem and Account. A shop can be modeled as projection instead of an actor.
Today we learned very important concepts as error handling and reply to a sender. I hope you are enjoy and want to give us feedback☺. In next part we will discuss an experimental Func API for F#.