An Introduction To gosiris, An Actor Framework For Go

This post is an introduction to an actor framework for Golang: gosiris. First of all, I will introduce the context, then we will dig into the framework and its capabilities.

Go

Go (or golang) is a programming language created by Google 8 years ago. It is compiled, statically typed, not object-oriented (even though it provides interfaces to define a set of methods) and has a garbage collector.

Initially, I started to learn Go because of its capabilities in terms of concurrent programming.

I think Node.js is not the best system to build a massive server web, I would use Go for that – Ryan Dahl, creator of Node.js

Actually, Go is based on the Communicating Sequential Processes principle (CSP). In a nutshell, CSP is a concurrency model to avoid sharing memory across processes. Instead, the idea is to implement sequential processes and having communication channels between these processes.

This principle is summarized by Rob Pike, co-creator of Go:

Don’t communicate by sharing memory; share memory by communicating – Rob Pike

The actor model

The core idea of the actor model is the same than CSP, namely to shared memory between processes and favor message passing instead. Yet there are two main differences.

The first one is that CSP is purely synchronous meaning a channel writer is blocked until a channel reader reads the message. This limitation was tackled in Golang, though, with the introduction of non-blocking channels (the so-called buffered channels).

The second main difference is that in the CSP model, the processes are somehow anonymous. A channel writer has only a reference to a channel without actually knowing who will receive the message. The actor model, though, is a point-to-point integration. An actor has a reference to another actor (through its identifier for example) to communicate with it.

Most of the actor frameworks implement also a hierarchy concept between the different actors. This simple idea is actually really powerful. A parent actor, for example, could be directly notified of a child actor failure and then decide about a failure strategy (Restart the child? Fail itself to implement a kind of circuit breaker? Etc.).

gosiris

gosiris is an actor framework for Go allowing local or remote communications between actors and providing runtime discoverability plus distributed tracing.

The remote communications can be done using either an AMQP broker or Kafka. The runtime discoverability is achieved using an etcd registry and the distributed tracing is based on Zipkin.

Hello world example

We will see in the following example how to implement a local send-only interaction.

The first step is to create an actor system:

gosiris.InitActorSystem(gosiris.SystemOptions{
    ActorSystemName: "ActorSystem",
})
defer gosiris.CloseActorSystem()

We will see later on an actor system can be distributed by simply configuring some options.

Then it’s time to create and register an actor:

parentActor := gosiris.Actor{}
defer parentActor.Close()

gosiris.ActorSystem().RegisterActor("parentActor", &parentActor, nil)

Each actor has a logical name, here parentActor.

Then we will create and register a child actor but we will implement a specific behavior on a given message type (message):

childActor := gosiris.Actor{}
defer childActor.Close()

childActor.React("message", func(context gosiris.Context) {
    context.Self.LogInfo(context, "Received %v\n", context.Data)
})

gosiris.ActorSystem().SpawnActor(&parentActor, "childActor", &childActor, nil)

An actor can be composed of multiple reactions. Each reaction is based on an event type and must define a specific behavior by implementing a simple function with a context parameter.

Finally, to send a message from the parent to the child:

parentActorRef, _ := gosiris.ActorSystem().ActorOf("parentActor")
childActorRef, _ := gosiris.ActorSystem().ActorOf("childActor")

childActorRef.Tell(gosiris.EmptyContext, "message", "Hi! How are you?", parentActorRef)

As you can see, we have not used the parentActor or childActor variable to send a message. This is not possible in gosiris. Instead we have done a lookup first in the actor system using the actor identifiers (ActorOf(string)). Each lookup returns an ActorRef structure which is simply a reference to an actor.

As a summary:

package main

import (
    "gosiris/gosiris"
)

func main() {
    gosiris.InitActorSystem(gosiris.SystemOptions{
        ActorSystemName: "ActorSystem",
    })

    parentActor := gosiris.Actor{}
    defer parentActor.Close()

    childActor := gosiris.Actor{}
    defer childActor.Close()
    childActor.React("message", func(context gosiris.Context) {
        context.Self.LogInfo(context, "Received %v\n", context.Data)
    })

    gosiris.ActorSystem().RegisterActor("parentActor", &parentActor, nil)
    gosiris.ActorSystem().SpawnActor(&parentActor, "childActor", &childActor, nil)

    parentActorRef, _ := gosiris.ActorSystem().ActorOf("parentActor")
    childActorRef, _ := gosiris.ActorSystem().ActorOf("childActor")

    childActorRef.Tell(gosiris.EmptyContext, "message", "Hi! How are you?", parentActorRef)
}

Stateful actor

It is also possible to manage a stateful actor like in the following example:

type StatefulActor struct {
    gosiris.Actor
    someState interface{}
}

statefulActor := new(StatefulActor).React("someEvent", func(context gosiris.Context) {
    //Some behavior
})

Actor supervision

As we’ve seen, one on the benefits of the actor model is to implement a hierarchy of the actors. In the following example, a parent actor is automatically notified of a child failure:

actor.React(gosiris.GosirisMsgChildClosed, func(context gosiris.Context) {
    context.Self.LogInfo(context, "My child is closed")
})

The parent actor can then decide on the strategy to adopt and decide for example to stop itself:

context.Self.AskForClose(context.Self)

Become/unbecome

In gosiris, it is also possible to implement the become/unbecome pattern familiar in Akka for example:

angry := func(context gosiris.Context) {
    if context.Data == "happy" {
        context.Self.LogInfo(context, "Unbecome\n")
        context.Self.Unbecome(context.MessageType)
    } else {
        context.Self.LogInfo(context, "Angrily receiving %v\n", context.Data)
    }
}

happy := func(context gosiris.Context) {
    if context.Data == "angry" {
        context.Self.LogInfo(context, "I shall become angry\n")
        context.Self.Become(context.MessageType, angry)
    } else {
        context.Self.LogInfo(context, "Happily receiving %v\n", context.Data)
    }
}

actor := gosiris.Actor{}
defer actor.Close()
actor.React("context", happy)

At first, the actor is configured to react by implementing the happy behavior. Then depending on the context, it may change at runtime its behavior to become angry and to unbecome happy.

Distributed example

Let’s see now how to create a distributed actor system:

gosiris.InitActorSystem(gosiris.SystemOptions{
    ActorSystemName: "ActorSystem",
    RegistryUrl:     "http://etcd:2379",
    ZipkinOptions: gosiris.ZipkinOptions{
        Url:      "http://zipkin:9411/api/v1/spans",
        Debug:    true,
        HostPort: "0.0.0.0",
        SameSpan: true,
    },
})
defer gosiris.CloseActorSystem()

Here we referenced an etcd server for the runtime discoverability and Zipkin server for distributed tracing.

In addition, we will implement a request/reply interaction with one actor deployed on AMQP while the other will be deployed on Kafka:

actor1 := new(gosiris.Actor).React("reply", func(context gosiris.Context) {
    context.Self.LogInfo(context, "Received: %v", context.Data)

})
defer actor1.Close()
gosiris.ActorSystem().RegisterActor("actor1", actor1, new(gosiris.ActorOptions)
	.SetRemote(true)
	.SetRemoteType(gosiris.Amqp)
	.SetUrl("amqp://guest:guest@amqp:5672/")
	.SetDestination("actor1"))

actor2 := new(gosiris.Actor).React("context", func(context gosiris.Context) {
    context.Self.LogInfo(context, "Received: %v", context.Data)
    context.Sender.Tell(context, "reply", "hello back", context.Self)
})
defer actor2.Close()
gosiris.ActorSystem().SpawnActor(actor1, "actor2", actor2, new(gosiris.ActorOptions)
	.SetRemote(true)
	.SetRemoteType(gosiris.Kafka)
	.SetUrl("kafka:9092")
	.SetDestination("actor2"))

We simply changed the call to RegisterActor() by adding additional actor options. Once an actor is defined as remote we must at least configure the transport type (AMQP or Kafka), an URL and a destination. Every time a new actor is registered, each actor system will receive a notification and modify its internal actor map.

Last but not least because the actor system has been initialized using a Zipkin reference, gosiris will also manage the distributed tracing part. It means each time an interaction is started (a tell with an empty context), a new parent span will be initialized. Then each reaction becomes automatically a child span and all logs are also forwarded to the Zipkin collector.

Conclusion

As you have seen in the last example, in less than 30 effective lines of code, we have implemented using gosiris a distributed request/reply interaction between two remote actors automatically discovered at runtime and using Zipkin for achieving distributed tracing.

If you are interested in gosiris, feel free to contact me @teivah.

Further reading