Implementing an Actor Model in Golang
Harness the power of concurrency using this programming construct
With the advent of multicore CPUs, we need programming constructs that can make use of those extra cores by processing tasks in concurrent fashion.
The actor model is one such programming construct that models a large number of independent jobs, being processed in any order with no need for a lock synchronisation.
A very common usage of the actor model can be found in web servers, Play! Framework in Java is an example. In general, any concurrent application can be built on top of an Actor Model.
Here, in this article, I’ll describe how to implement a primitive actor model in golang. We’ll be making use of the tools provided by golang for concurrent processing — goroutines, channels, and wait groups.
First, let’s look at an actor:
An actor has a task queue and goroutine that listens to the task queue and execute task.
Here A is a goroutine that blocks on task queue and keeps executing the task from the queue.
Here is what the interface of an actor looks like:
type Actor interface {
AddTask(task Task)
Start()
Stop()
}
Now let’s look at task
The task is executed in an actor. It is an implementation of a given interface with Execute method. Anything which can be executed by making Execute call. Task is a business implementation of the work we need to do.
In a web server framework, it would make a call to a receiver which defines an API implementation.
type Task interface {
Execute()
}
The overall system looks like this:
Let’s look at the actor system interface.
type ActorSystem interface {
Run()
SubmitTask(task Task)
Shutdown(shutdownWG *sync.WaitGroup)
}
Task
s are submitted to ActorSystem
using the SubmitTask
method. A taskAssigner
assigns each of the task to one of the Actor
s. Each Actor
also has a small queue, in which it buffers the tasks and executes one by one.
Now let’s dive deep into each of the components
ActorSystem
Here is a gist of ActorSystem
:
When the ActorSystem
starts, It start ataskAssigner
actor . Each incoming Task
to system is added to taskAssigneractor
by invoking AddTask method on actor.
Tasks
are submitted to ActorSystem
using the SubmitTask
method. We put each of the incoming Tasks
to taskAssigner
by invoking AddTask
method.
On Shutdown
it closes the tasks
channel blocking any new incoming tasks, waits for all received tasks to be assigned to Actors. Then it invokes Stop
on each Actor
and waits on them to finish.
Task Assigner
We put each of the incoming Tasks
in a channel tasks
, taskAssigner
and Task
in the internal queue of an Actor
.
taskAssigner
internal process tasks channel and route task to one of the task actor within the pool by invokingAddTask
on it.
autoScalar
keeps a watch on the no of items in tasks
and increases or decreases the size of task actor pool
.
Task Actor
It is also an actor and its job is to execute task which is added to it channel tasks similar to assigner actor.
Benchmarks
Here we have simulated a web server.
- 100k requests are sent linearly with 2-millisecond interval
- Each request take [0,50) ~25 millseconds when clock is in first 30 second of minute and [50–100) ~75 millisecond in last 30 second of a minute.
- This simulates a situation where we have sudden variation in latencies from a downstream service. We want to keep our throughput in check so as not to increase wait times for any task
Here is code for the io simulation benchmark:
Result
Here is the result of simulation. We are tracking 3 metrics every 100 millisecond interval
- submitted task: This is a constant linear orange line as we add a task every 2 millisecond.
- completed task: It is the yellow line and it tries to closely follow orange line as we want to complete submitted task as soon as possible.
- active-actors: It is the blue line and shows number of active actor which the system needs to be able to provide short wait time for a task. Number of actors increases when task latencies increase as we require more actors to achive similar throughput.
Observations
- At about 30 second mark latencies increased from ~25 milliseconds to ~75 milliseconds
- completed metric dropped as with current actors we can no longer process similar number of tasks.
- auto scalar notices increased queue size and starts increasing actors which stabalises around 30 actors
- We return to original state around 60 second mark when latencies drops back to ~25 millsecond.
The full code for the project can be found at: