Introduction to Orleankka

[Reentrant Actors]

In Part II we have covered “error handling and reply to sender”. In Part III, I would like to consider a very powerful feature called Reentrancy. Before going any further I would admit that this feature should be used only for read-only operations. As you may know, the Actor Model is allowing to handle incoming messages only in sequential manner, one by one. Following this rule, the Actor Model gives the important guarantee of execution code in a multithreaded environment without painful deadlocks. In addition to that we also get a better reasoning for our code - unfortunately, this is so underestimated property, especially in OOP camp.

Modeling a real-time consistent counter

Let’s assume that we need to model a real-time consistent counter which should handle a tons of service/users requests to get current count.

type Counter() =
inherit Actor<Message>()

This actor should react on these messages:

type Message =
| Increment
// write operation, mutate state, sync state with db
| Decrement // write operation, mutate state, sync state with db
| GetCount // read-only operation, do not mutate state

In addition to that, each state change(increment/decrement) should be synced with database first which takes some time.

let mutable count = 0 // state
override this.Receive msg = task {
match msg with
| Increment -> do! _db.Save(count + 1)
// sync state with db
count <- count + 1 // update state

Ok, everything is pretty obvious except handling of read-only operations, consider, for example, GetCount.

let! count = counter <? GetCount // read-only operation

Very important point — is that this operation shouldn’t mutate a state of the actor, in other words, it’s pure function. Furthermore, now we can say that this operation can be safely parallelized and it potentially should bring a very good performance/throughput.

But one sec, what about the single threaded execution of the Actor Model?
We can’t ignore this, right? By default, we can’t ignore this behavior, but for certain cases, the ignoring of actor’s rule can bring a real value.

Let’s consider a worst case - we send a message Increment to our actor and handling of this IO bound operation takes 15 secs.

do! counter <! Increment // takes 15 secs to complete

What if we send a GetCount message in order to read a pre-calculated value from memory? The behavior is quite obvious — we need to wait until Increment operation finish. Why do we need to wait for completion of IO bound operation when we just want to read (do not mutate a state) a value from memory? Because of the default Actor Model behavior. Ok, is it really reasonable to wait 15 secs in this case? Of course, no - we could cheat a little bit for such cases. And it’s where reentrancy is coming:) Basically, reentrancy is needed for cases when you want to perform a read-only operation and you don’t want to wait for completion of any IO bound operation. It’s can dramatically boost your performance.

Modeling by Reentrant Actor

Ok, it seems we have gotten some background and can model our counter using Reentrant Actor. Let’s look at code example:

So far nothing special, we have defined a regular actor type without any reentrancy. What next? Now we need to specify which message should be treated as a reentrant message. For this Orleankka provides a very simple solution based on convention over configuration approach. What you really need it’s just add one static private method to your actor:

This method is just a filter which will be invoked(by Orleankka run-time) for each incoming message. Within this method, you should define your logic to filter all incoming messages based on their types.

Let’s add this method into our actor:

Here is our demo program:

Summary

Today we learned very important concept - reentrancy. I hope you enjoy and want to give us feedback☺. 
The full demo you can find here.
For more information about reentrancy, please read this.