Implementing an Actor Model in Golang

Harness the power of concurrency using this programming construct

Gaurav Sharma
Better Programming

--

Photo by George Howden on Unsplash

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:

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)
}

Tasks are submitted to ActorSystem using the SubmitTask method. A taskAssigner assigns each of the task to one of the Actors. 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 Taskin 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.

  1. 100k requests are sent linearly with 2-millisecond interval
  2. 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.
  3. 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

  1. submitted task: This is a constant linear orange line as we add a task every 2 millisecond.
  2. 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.
  3. 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

  1. At about 30 second mark latencies increased from ~25 milliseconds to ~75 milliseconds
  2. completed metric dropped as with current actors we can no longer process similar number of tasks.
  3. auto scalar notices increased queue size and starts increasing actors which stabalises around 30 actors
  4. 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:

--

--