Creating an Actor system in Go for scalable intra-process message-passing

Midhun
Custom App Development
4 min readFeb 14, 2024

What is an Actor system? An actor system is a model used in concurrent and distributed systems for building scalable and resilient applications.

Key points of an actor system include concurrency, isolation, and message passing. We will try to implement a simple and robust actor system in Golang from scratch.

Let’s first define the features we want for our Actor system.

  1. Add actors on a tag.
  2. Be able to add multiple actors on the same tag to create a room of actors.
  3. Be able to broadcast messages to the room of actors so that each actor in the room receives them.
  4. Be able to send and receive messages among actors under different tags.
  5. Be able to close an actor to remove it from the system.
  6. Ensure it’s thread-safe.

Now let’s define the architecture that will be able to help us achieve the above features.

We will use a Map data structure to store tags under the key and value as the reference to the starting node of a single linked list of Actor instances. Actors under the same tag will be stored in the single linked list and the starting node of that linked list will be referenced from the Hash Map with the key value as the ‘tag’.

This will allow O(1) constant time access to the start of the linked list of actors.

A reference diagram is attached below:

Actors Data structure

Our Actor structure in Go will look something like this:

type Actor struct {
id uint // internal id of this actor under the same id
//can have multiple actors with different tag under the same id
next *Actor //next actor with the same id but with different tag
recvCh chan string //receive message on this channel
tag string //actor tag under which it is stored
}

We will create an Actors Hub which will listen for commands like:

AddActor, RemoveActor, and SendMessages.

ActorsHub structure will be as given below:

// ActorHub ... controls message sending among actors
type ActorHub struct {
store map[string]*Actor //actor store
eventChan chan Event //listen for commands from the actor
}

The Actorshub will be a singleton instance to control the message passing.

We will use the sync.Once package and call the ‘Do’ function to make sure when someone uses our actors package, they can create only one instance of the Actorshub.

once.Do(func() {
//initialize the member variables of Actorshub and start the event listener
})

Now let’s define the AddActor, RemoveActor, and SendMessage functions.

AddActor:

Pass the tag under which you need to add this actor. In return, you will get the pointer to the actor, which you can use to call methods under this actor.

We will check if there are any existing nodes on the HashMap under the given tag. If no, then add this as the new root node and provide an ID of 1. If there is an existing linked list then get the last node of the list, add this new actor node after the last node, and provide an ID that is equal to one more than the last node’s ID.

RemoveActor:

Pass in the ID of the Actor and the tag under which it is saved.

We will check if there is only one node at the provided tag under HashMap then delete that tag from the HashMap and close the channel. If there are multiple nodes, then navigate to the required ID and point the previous node’s next pointer to the next of the current actor. This way, we will be removing the actor from the linked list and then closing the channel associated with this actor.

SendMessage :

Pass in the tag and the message bytes to be sent.

Get the root node at the tag under HashMap and then loop over the linked list and send a message through the channel associated with each actor.

Initial testing on my Mac, with a million actors sending a million messages, was completed in 5.4 seconds. More fine-tuning can be done as time permits. In the future, I am planning to add messages passing over the network so that actors can communicate over the network under a cluster.

You can use the package by pulling it from the Gitrepo provided below.

The repo has some test cases and more will be added to the branch to cover all the tests.

Usage: go get github.com/michaelmenon/actors

import (
github.com/michaelmenon/actors/cmd
)
//first create a Hub instance
ah := cmd.GetActorsHub()


actor1, err := ah.NewActor("actortag1")

//create another actor
actor2, err := ah.NewActor("actortag2")

//listen for messages
for msg := <-actor1.Get(){
fmt.Println(msg)
}
//in a seperate Goroutine send a message from actor1 to actor2 wiht the same node(same service)
actor1.SendLocal("actortag2", []byte("my message"))

//to remove an actor
actor1.Close()
actor2.Close()

//To clear all the actors
ah.Clear()

--

--

Midhun
Custom App Development

Midhun is a Senior Technical Manager at Zco. Loves programming in Rust and Golang. In free time works on Rust and GoLang projects for fun.