Modeling Non-Blocking Interactions with Actors in Pony
In a recent blog post, I talked about things like promises in Pony that help facilitate the safe, race-free, non-blocking development style that is one of the hallmarks of Pony. Promises are just a means to an end: the hard part has been switching my mental model from functional/imperative to the actor model.
In PonyMUD — a multiplayer text adventure game that I’m writing as a means to teach myself Pony — players have access to a command called who
. This command displays the list of all connected players.
If I were to write some psuedo-code to solve this problem using the mental model I’ve had since I started programming 30 years ago, I might end up with something like this:
for player in players do
_conn.write(player.getname() + "\n")
end
_conn.write("There are " + players.size().string() + " players connected.\n")
This looks pretty straightforward, as it should. Solved in the traditional fashion it’s a brain-dead simple problem. First, we iterate through the player list. For each player, we display their name and then when we’re done, we display the total number of players.
But what if players were actors? What if you couldn’t synchronously query state? If a player is an actor in Pony, you can’t get an answer from the getname()
function. Actors are required to only have one-way messaging behaviors.
In my previous blog post, I talked about how the who
command is implemented, but I didn’t really discuss that everything in the chain from player input to output to the player’s socket is a uni-directional, non-blocking message sent through actors.
The flow goes something like this:
- Player types “who”
- ConnectionManager gets text via a TCP notification
- Player actor parses text and dispatches to appropriate handler actor
- CmdWho actor sends promise to ConnectionManager
- ConnectionManager iterates over players to fulfill promise
- CmdWho defines what happens after promise is fulfilled
- Player actor sends text to socket
The important thing about this flow is no part of it is blocking. There isn’t a single thing that happens in response to a user entering text into the game that “sits and waits” synchronously. While the impact may seem minimal on a text adventure game, the ability to model your process this way in performance-constrained environments is incredibly powerful. When you’re handling hundreds of thousands or millions of requests per second, every blocking operation could be the death of your process performance.
With the release of Pony 0.17.0 I’ve actually been able to delete some of the code I wrote in my previous blog post and replace it with some more elegant use of the promises
library.
Let’s take a look at the important methods of the CmdWho
actor:
be handle_verb(verb: String val, params: Array[String] val) =>
let p = Promise[Array[String val
] val]
_cm.who(p)
p.next[None](recover this~_whofulfilled() end)be _whofulfilled(players: Array[String val] val) =>
_parent.tell("\n==> Player Listing:\n")
for name in players.values() do
_parent.tell(name + "\n")
end
_parent.tell("\nThere are " + players.size().string() + " players connected.\n")
When the command dispatcher matches a player-entered verb with one of the verbs CmdWho
handles, it invokes CmdWho
's handle_verb
function. This in turn sends a promise to the connection manager. This is a promise that indicates that at some point in the future the connection manager will give us back an array of strings containing the names of connected players.
When we get this fulfilled promise, we invoke _whofulfilled
(via next
promise chaining), which just runs through the array and sends text to the player’s socket via the _parent.tell
behavior (remember — everything is an an actor here).
Because the connection manager is the only thing with a raw list of player actors, it is the only actor that can fulfill the promise to get their names. Since players are actors and we can’t synchronously query their names, we have to ask for the name with another promise. This is where the new join
method from 0.17.0 makes the code very simple:
be who(p: Promise[Array[String val] val]) =>
let names: Array[Promise[String]] = Array[Promise[String]]
for player in _players.values() do
let pname = Promise[String]
player.gathername(pname)
names.push(pname)
end
let joined: Promise[Array[String val] val] = Promises[String].join(names.values())
joined.next[None](recover p~apply() end)
First, we create an array of promises, all of which have been sent to the player actors to obtain the name of that player via the gathername
behavior.
Once we have an array of promises, we can convert that into a promise of an array by using Promises.join
on an iterator of promises. When the joined promise has been fulfilled, we then invoke apply
on the promise sent to the connection manager. Fulfilling one promise in response to fulfilling a nested promise may feel a little awkward at first, but I am really excited by the potential and power of this pattern.
In conclusion, modeling everything as asynchronous actors that can only send one-way messages is definitely a paradigm shift. It takes some time and a lot of extra thought, but the end result is, I think, worth it.
While the initial conversion is a struggle, I’ve found that the problem decomposition that I’ve been coming up with in order to “actor-ify” the solution has been more elegant than if I had just brute-forced things in an easy, synchronous for
loop.
Pony’s promise capabilities (especially the new syntax available in 0.17.0) make it easy to logically chain results to build complex behaviors without ever stopping to wait for a synchronous block or sacrificing memory safety.