Conquest of Distributed Systems (part 3) — Actor Model Hidden in Plain Sight
“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!”.
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.”
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.
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.”
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 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.
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:
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…