Conquest of Distributed Systems (part 3) — Actor Model Hidden in Plain Sight

Serge Semenov
5 min readNov 18, 2018

--

By Orly Faya (body art artist)

“That guy literally blends into the background like a chameleon!” — shouted Jim when stumbled upon surprising discovery while looking at the original unreliable RPC-style code and the implementation using an Actor Model framework. “We need the simple code!”.

The ‘simple code’ uses service clients and guarantees no consistency (part 1)
A more complex implementation using an Actor Model framework (part 2)

Recap of part 2. A software development team (starring Jim) convinced their manager to spend some time on designing a more robust solution that addresses problems of distributed computing. Jim and his colleague make a pragmatic decision and go with an Actor Model framework to implement business workflows. They realize how time-consuming it is, and they are not sure if they can afford it.

Little Bit of Madness

Jim’s co-worker: “You say that we are sacrificing consistency in favor of productivity, and it is cheaper to deal with production issues from time to time. Is that correct?”

Jim: “No, I have an idea on how to achieve consistency without changing the simple code that uses service clients. Let me do something crazy to the Actor code first.”

Less readable and maintainable code of an Actor

Jim: Behold, we created a Finite State Machine!”

Then he explains that new persisted value state represents a step in the workflow and all Receive methods simply take in input values and delegate the execution to the single MoveNext method. Thus every business case in the workflow can have a unique number and running of next steps represents state transitions of a state machine.

Workflow is a finite state machine

His co-worker looks very skeptical, but Jim has not finished yet.

Unexpected Surprise

Jim asks his colleague to look closer at the simple version of the code once again.

Jim: “Remember how async methods work? Let me show you what C# compiler does to this method.”

First lines of the InstantBuy async method which compiles into a state machine

He continued: “The InstantBuy method is transpiled into a similar state machine to the one we built by hand inside the Actor. You can see a numerical state — the <>1__state field, the captured input of the method — itemId and quantity , and locally defined variables — <grandTotal>5__1.”

Now that caught the attention of his colleague. They keep digging.

Jim: Look how the await statement works.”

The ‘await’ statement behind the scenes

The call to MakePayment method returns a Task, then in case if the task does not immediately complete the state machine subscribes to its completion with AwaitUnsafeOnCompleted method, where the continuation action is the MoveNext method of the state machine. In the case of Actor Model, instead of calling a method we send a command to another Actor and subscribe to the reply message by using framework’s syntax (the IReceive<T> interface). Calling an async method can be viewed as a message passing analogous of an Actor Framework — the execution can be scheduled on a thread pool and not necessarily complete immediately — this is the point of a Task.

It is not clear from the code above how it gets the result of the MakePayment method, but the answer lies in a field with a peculiar name of<>u__1. That field holds an awaiter associated with the Task returned by MakePayment method.

How async state machine gets a result on await

When the MakePayment method completes eventually, it calls all continuations for the Task it returns— the MoveNext method of a calling state machine. Then the calling state machine has an indirect reference to that Task through the awaiter stored in <>u__1 field. So calling GetResult on the awaiter returns the result of the Task returned by MakePayment method. In this case continuation of an async method can also be viewed as a message passing mechanism — the reply message of an Actor Model.

The underlying code also shows how exceptions are handled by the calling state machine — in this case, it moves the state machine to the terminal state and re-throws the exception to the caller via <>t__builder. I.e. an exception can be viewed as a ‘sad path’ of our business scenario and also as a reply message.

Putting all together, we have an async method playing a role of an actor, input arguments and local variables are persisted values, calls to other async methods are sending commands to other actors, and awaiting on them — receiving reply messages. After aligning the code with previously implemented Actor, Jim and his colleague expressed exact business workflow in plain C# code as follows:

A simple async method can be viewed as a complex Actor if you look ‘under the hood’

This 20-line plain C# code looks much more concise than its 150-line Actor Model counterpart, and it only took less than 5 minutes to write.

In the end, Jim’s co-worker extends the thinking by mentioning Domain-Driven Design — besides persisted values of an orchestrating method, you would also add persistence of domain entities (or aggregates) within the scope of a state transition — a unit of work that should be committed in an ACID transaction.

Jim and his colleague found these similarities as an interesting thought experiment, but can they do anything with it?

Keep reading to the Part 4 — The Now and the Vision

--

--

Serge Semenov

‘I believe in giving every developer a superpower of creating microservices without using any framework’ — https://dasync.io 🦸‍♂️