Go Actor Model
GAM is a MVP Actor Model framework for Go.
Design principles:
Minimalistic API -
In the spirit of Go, the API should be small and easy to use.
Avoid enterprisey JVM like containers and configurations.
Build on existing technologies - There are already a lot of great tech for e.g. networking and clustering, build on those.
e.g. gRPC streams for networking, Consul.IO for clustering.
Pass data, not objects - Serialization is an explicit concern, don't try to hide it.
Protobuf all the way.
Be fast - Do not trade performance for magic API trickery.
Ultra fast remoting, GAM currently manages to pass 800k+ messages between nodes using only two actors, while still preserving message order!
:> node1.exe
2016/04/30 20:33:48 Host is 127.0.0.1:55567
2016/04/30 20:33:48 Started EndpointManager
2016/04/30 20:33:48 Starting GAM server on 127.0.0.1:55567.
2016/04/30 20:33:48 Started EndpointWriter for host 127.0.0.1:8080
2016/04/30 20:33:48 Connecting to host 127.0.0.1:8080
2016/04/30 20:33:48 Connected to host 127.0.0.1:8080
2016/04/30 20:33:48 Getting stream from host 127.0.0.1:8080
2016/04/30 20:33:48 Got stream from host 127.0.0.1:8080
2016/04/30 20:33:48 Starting to send
2016/04/30 20:33:48 50000
2016/04/30 20:33:48 100000
...snip...
2016/04/30 20:33:50 950000
2016/04/30 20:33:50 1000000
2016/04/30 20:33:50 Elapsed 2.4237125s
2016/04/30 20:33:50 Msg per sec 825180 <---
Why Actors

- Decoupled Concurrency
- Distributed by default
- Fault tolerance
For a more indepth description of the differences, see this thread Actors vs. CSP
Hello world
type Hello struct{ Who string }
type HelloActor struct{}
func (state *HelloActor) Receive(context actor.Context) {
switch msg := context.Message().(type) {
case Hello:
fmt.Printf("Hello %v\n", msg.Who)
}
}
func main() {
props := actor.FromInstance(&HelloActor{})
pid := actor.Spawn(props)
pid.Tell(Hello{Who: "Roger"})
console.ReadLine()
}
State machines / Become and Unbecome
type Become struct{}
type Hello struct{ Who string }
type BecomeActor struct{}
func (state *BecomeActor) Receive(context actor.Context) {
switch msg := context.Message().(type) {
case Hello:
fmt.Printf("Hello %v\n", msg.Who)
context.Become(state.Other)
}
}
func (state *BecomeActor) Other(context actor.Context) {
switch msg := context.Message().(type) {
case Hello:
fmt.Printf("%v, ey we are now handling messages in another behavior", msg.Who)
}
}
func NewBecomeActor() actor.Actor {
return &BecomeActor{}
}
func main() {
props := actor.FromProducer(NewBecomeActor)
pid := actor.Spawn(props)
pid.Tell(Hello{Who: "Roger"})
pid.Tell(Hello{Who: "Roger"})
console.ReadLine()
}
Lifecycle events
Unlike Akka, GAM uses messages for lifecycle events instead of OOP method overrides
type Hello struct{ Who string }
type HelloActor struct{}
func (state *HelloActor) Receive(context actor.Context) {
switch msg := context.Message().(type) {
case actor.Started:
fmt.Println("Started, initialize actor here")
case actor.Stopping:
fmt.Println("Stopping, actor is about shut down")
case actor.Stopped:
fmt.Println("Stopped, actor and it's children are stopped")
case actor.Restarting:
fmt.Println("Restarting, actor is about restart")
case Hello:
fmt.Printf("Hello %v\n", msg.Who)
}
}
func main() {
props := actor.FromInstance(&HelloActor{})
actor := actor.Spawn(props)
actor.Tell(Hello{Who: "Roger"})
//why wait?
//Stop is a system message and is not processed through the user message mailbox
//thus, it will be handled _before_ any user message
//we only do this to show the correct order of events in the console
time.Sleep(1 * time.Second)
actor.Stop()
console.ReadLine()
}
Supervision
Root actors are supervised by the actor.DefaultSupervisionStrategy()
, which always issues a actor.RestartDirective
for failing actors
Child actors are supervised by their parents.
Parents can customize their child supervisor strategy using gam.Props
Example
type Hello struct{ Who string }
type ParentActor struct{}
func (state *ParentActor) Receive(context actor.Context) {
switch msg := context.Message().(type) {
case Hello:
props := actor.FromProducer(NewChildActor)
child := context.Spawn(props)
child.Tell(msg)
}
}
func NewParentActor() actor.Actor {
return &ParentActor{}
}
type ChildActor struct{}
func (state *ChildActor) Receive(context actor.Context) {
switch msg := context.Message().(type) {
case actor.Started:
fmt.Println("Starting, initialize actor here")
case actor.Stopping:
fmt.Println("Stopping, actor is about shut down")
case actor.Stopped:
fmt.Println("Stopped, actor and it's children are stopped")
case actor.Restarting:
fmt.Println("Restarting, actor is about restart")
case Hello:
fmt.Printf("Hello %v\n", msg.Who)
panic("Ouch")
}
}
func NewChildActor() actor.Actor {
return &ChildActor{}
}
func main() {
decider := func(child *actor.PID, reason interface{}) actor.Directive {
fmt.Println("handling failure for child")
return actor.StopDirective
}
supervisor := actor.NewOneForOneStrategy(10, 1000, decider)
props := actor.
FromProducer(NewParentActor).
WithSupervisor(supervisor)
pid := actor.Spawn(props)
pid.Tell(Hello{Who: "Roger"})
console.ReadLine()
}
Networking / Remoting
GAM's networking layer is built as a thin wrapper ontop of gRPC and message serialization is built on Protocol Buffers
Example
Node 1
type MyActor struct{
count int
}
func (state *MyActor) Receive(context actor.Context) {
switch msg := context.Message().(type) {
case *messages.Response:
state.count++
fmt.Println(state.count)
}
}
func main() {
remoting.StartServer("localhost:8090")
pid := actor.SpawnTemplate(&MyActor{})
message := &messages.Echo{Message: "hej", Sender: pid}
//this is the remote actor we want to communicate with
remote := actor.NewPID("localhost:8091", "myactor")
for i := 0; i < 10; i++ {
remote.Tell(message)
}
console.ReadLine()
}
Node 2
type MyActor struct{}
func (*MyActor) Receive(context actor.Context) {
switch msg := context.Message().(type) {
case *messages.Echo:
msg.Sender.Tell(&messages.Response{
SomeValue: "result",
})
}
}
func main() {
remoting.StartServer("localhost:8091")
pid := actor.SpawnTemplate(&MyActor{})
//register a name for our local actor so that it can be discovered remotely
actor.ProcessRegistry.Register("myactor", pid)
console.ReadLine()
}
Message Contracts
syntax = "proto3";
package messages;
import "actor.proto"; //we need to import actor.proto, so our messages can include PID's
//this is the message the actor on node 1 will send to the remote actor on node 2
message Echo {
actor.PID Sender = 1; //this is the PID the remote actor should reply to
string Message = 2;
}
//this is the message the remote actor should reply with
message Response {
string SomeValue = 1;
}
For more examples, see the example folder in this repository.