Convert Figma logo to code with AI

asynkron logoprotoactor-go

Proto Actor - Ultra fast distributed actors for Go, C# and Java/Kotlin

5,016
521
5,016
76

Top Related Projects

Proto Actor - Ultra fast distributed actors for Go, C# and Java/Kotlin

23,940

Dapr is a portable, event-driven, runtime for building distributed applications across cloud and edge.

13,047

Build highly concurrent, distributed, and resilient message-driven applications on the JVM

9,695

ZeroMQ core engine in C++, implements ZMTP/3.1

Quick Overview

ProtoActor-Go is an actor model implementation for the Go programming language. It provides a framework for building concurrent and distributed systems using the actor pattern, offering high performance and scalability for complex applications.

Pros

  • High performance and scalability for concurrent systems
  • Supports both local and remote actors for distributed computing
  • Provides a simple and intuitive API for actor-based programming
  • Integrates well with Go's concurrency model and ecosystem

Cons

  • Learning curve for developers unfamiliar with the actor model
  • Limited ecosystem compared to more established actor frameworks
  • Documentation could be more comprehensive for advanced use cases
  • May introduce additional complexity for simpler applications

Code Examples

  1. Creating a simple actor:
type MyActor struct{}

func (a *MyActor) Receive(context actor.Context) {
    switch msg := context.Message().(type) {
    case string:
        fmt.Println("Received message:", msg)
    }
}

func main() {
    system := actor.NewActorSystem()
    props := actor.PropsFromProducer(func() actor.Actor { return &MyActor{} })
    pid := system.Root.Spawn(props)
    system.Root.Send(pid, "Hello, Actor!")
}
  1. Using actor supervision:
type Supervisor struct{}

func (s *Supervisor) Receive(context actor.Context) {
    switch msg := context.Message().(type) {
    case *actor.Started:
        fmt.Println("Supervisor started")
    case *actor.Terminated:
        fmt.Println("Child actor terminated:", msg.Who)
    }
}

func main() {
    system := actor.NewActorSystem()
    supervisorProps := actor.PropsFromProducer(func() actor.Actor { return &Supervisor{} })
    supervisorPid := system.Root.Spawn(supervisorProps)

    childProps := actor.PropsFromProducer(func() actor.Actor { return &MyActor{} })
    childPid := system.Root.SpawnNamed(childProps, "child")

    system.Root.Send(supervisorPid, &actor.Watch{Watcher: supervisorPid, Watchee: childPid})
}
  1. Remote actor communication:
func main() {
    system := actor.NewActorSystem()
    remoteConfig := remote.Configure("127.0.0.1", 8080)
    remoting := remote.NewRemote(system, remoteConfig)
    remoting.Start()

    remotePid := actor.NewPID("127.0.0.1:8080", "remote-actor")
    system.Root.Send(remotePid, "Hello, Remote Actor!")
}

Getting Started

To get started with ProtoActor-Go, follow these steps:

  1. Install the library:

    go get -u github.com/asynkron/protoactor-go
    
  2. Import the necessary packages in your Go code:

    import (
        "github.com/asynkron/protoactor-go/actor"
        "github.com/asynkron/protoactor-go/remote"
    )
    
  3. Create an actor system and define your actors:

    system := actor.NewActorSystem()
    props := actor.PropsFromProducer(func() actor.Actor { return &MyActor{} })
    pid := system.Root.Spawn(props)
    
  4. Send messages to actors and handle them in the Receive method:

    system.Root.Send(pid, "Hello, Actor!")
    

For more advanced usage and features, refer to the official documentation and examples in the GitHub repository.

Competitor Comparisons

Proto Actor - Ultra fast distributed actors for Go, C# and Java/Kotlin

Pros of protoactor-dotnet

  • Better integration with .NET ecosystem and libraries
  • Supports async/await pattern natively
  • More mature and feature-rich implementation

Cons of protoactor-dotnet

  • Potentially slower performance compared to Go implementation
  • Less suitable for systems requiring extreme concurrency

Code Comparison

protoactor-dotnet:

var props = Props.FromProducer(() => new MyActor());
var pid = Context.Spawn(props);
var result = await Context.RequestAsync<string>(pid, "Hello");

protoactor-go:

props := actor.PropsFromProducer(func() actor.Actor { return &MyActor{} })
pid := context.Spawn(props)
result, err := context.RequestFuture(pid, "Hello", timeout).Result()

Both implementations share similar concepts and syntax, but protoactor-dotnet leverages C#'s async/await pattern for more idiomatic code. The Go version uses channels and futures for asynchronous operations, which may be less intuitive for developers not familiar with Go's concurrency model.

protoactor-dotnet is generally more suitable for .NET developers and projects deeply integrated with the .NET ecosystem, while protoactor-go might be preferred for high-concurrency systems or projects already using Go.

23,940

Dapr is a portable, event-driven, runtime for building distributed applications across cloud and edge.

Pros of Dapr

  • Broader scope: Dapr is a comprehensive runtime for building microservices, offering a wide range of building blocks for common distributed system patterns
  • Language-agnostic: Supports multiple programming languages, making it more versatile for diverse development teams
  • Strong community and backing: Developed by Microsoft with a large and active community

Cons of Dapr

  • Steeper learning curve: Due to its broader scope, it may take longer to master all aspects of Dapr
  • Potential overhead: The additional abstraction layer might introduce some performance overhead in certain scenarios

Code Comparison

Dapr (using Go SDK):

client, err := dapr.NewClient()
if err != nil {
    panic(err)
}
err = client.SaveState(ctx, "statestore", "key", []byte("value"))

ProtoActor-Go:

props := actor.FromProducer(func() actor.Actor { return &MyActor{} })
pid := actor.Spawn(props)
pid.Tell(&MyMessage{})

While both libraries facilitate distributed systems development, Dapr provides a more comprehensive set of features for building microservices, whereas ProtoActor-Go focuses specifically on the actor model for concurrent and distributed programming.

13,047

Build highly concurrent, distributed, and resilient message-driven applications on the JVM

Pros of Akka

  • More mature and widely adopted ecosystem with extensive documentation
  • Supports multiple programming languages (Scala and Java)
  • Offers a broader range of features, including clustering and persistence

Cons of Akka

  • Steeper learning curve due to its complexity and extensive feature set
  • Heavier resource footprint, which may impact performance in some scenarios
  • Licensing changes have caused concerns in the community

Code Comparison

Akka (Scala):

class MyActor extends Actor {
  def receive = {
    case "hello" => println("Hello, Akka!")
    case _       => println("Unknown message")
  }
}

Proto.Actor (Go):

type MyActor struct{}

func (state *MyActor) Receive(context actor.Context) {
    switch msg := context.Message().(type) {
    case string:
        fmt.Println("Hello, Proto.Actor!")
    }
}

Both frameworks use the actor model for concurrent programming, but Proto.Actor focuses on simplicity and performance in Go, while Akka provides a more comprehensive toolkit for distributed systems in Scala and Java. Proto.Actor may be easier to learn and integrate into existing Go projects, whereas Akka offers more advanced features for complex distributed systems at the cost of increased complexity.

9,695

ZeroMQ core engine in C++, implements ZMTP/3.1

Pros of libzmq

  • Mature and battle-tested messaging library with extensive documentation
  • Supports multiple programming languages and platforms
  • Offers high-performance, low-latency communication

Cons of libzmq

  • Steeper learning curve due to its complex API and concepts
  • Requires manual management of message routing and patterns

Code Comparison

libzmq:

void *context = zmq_ctx_new();
void *socket = zmq_socket(context, ZMQ_REP);
zmq_bind(socket, "tcp://*:5555");
zmq_msg_t request;
zmq_msg_init(&request);
zmq_msg_recv(&request, socket, 0);

protoactor-go:

context := actor.NewActorSystem()
props := actor.PropsFromFunc(func(context actor.Context) {
    switch msg := context.Message().(type) {
    case string:
        fmt.Println("Received:", msg)
    }
})
pid := context.Spawn(props)

Summary

libzmq is a robust messaging library suitable for complex distributed systems, offering high performance across multiple languages. protoactor-go, on the other hand, is a lightweight actor framework specifically for Go, focusing on simplicity and ease of use for concurrent programming. While libzmq provides more flexibility in communication patterns, protoactor-go offers a more straightforward approach to building actor-based systems in Go.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

Go Report Card GoDoc checks Sourcegraph

Join our Slack channel

Cross platform actors

Introducing cross platform actor support between Go and C#.

Can I use this? The Go implementation is still in beta, there are users using Proto Actor for Go in production already. But be aware that the API might change over time until 1.0.

Sourcecode - Go

This is the Go repository for Proto Actor.

The C# implementation can be found here https://github.com/asynkron/protoactor-dotnet

Design principles:

Minimalistic API - 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, Proto Actor currently manages to pass over two million messages per second between nodes using only two actors, while still preserving message order! This is six times more the new super advanced UDP based Artery transport for Scala Akka, and 30 times faster than Akka.NET.

:> node1.exe
Started EndpointManager
Started Activator
Starting Proto.Actor server address="127.0.0.1:8081"
Started EndpointWatcher address="127.0.0.1:8080"
Started EndpointWriter address="127.0.0.1:8080"
EndpointWriter connecting address="127.0.0.1:8080"
EndpointWriter connected address="127.0.0.1:8080"
2020/06/22 10:45:20 Starting to send
2020/06/22 10:45:20 50000
2020/06/22 10:45:20 100000
2020/06/22 10:45:20 150000
... snip ...
2020/06/22 10:45:21 900000
2020/06/22 10:45:21 950000
2020/06/22 10:45:21 1000000
2020/06/22 10:45:21 Elapsed 732.9921ms
2020/06/22 10:45:21 Msg per sec 2728542 <--

History

As the creator of the Akka.NET project, I have come to some distinct conclusions while being involved in that project. In Akka.NET we created our own thread pool, our own networking layer, our own serialization support, our own configuration support etc. etc. This was all fun and challenging, it is however now my firm opinion that this is the wrong way to go about things.

If possible, software should be composed, not built, only add code to glue existing pieces together. This yields a much better time to market, and allows us to focus on solving the actual problem at hand, in this case concurrency and distributed programming.

Proto Actor builds on existing technologies, Protobuf for serialization, gRPC streams for network transport. This ensures cross platform compatibility, network protocol version tolerance and battle proven stability.

Another extremely important factor here is business agility and having an exit strategy. By being cross platform, your organization is no longer tied into a specific platform, if you are migrating from .NET to Go, This can be done while still allowing actor based services to communicate between platforms.

Reinvent by not reinventing.


Why Actors

batman

  • Decoupled Concurrency
  • Distributed by default
  • Fault tolerance

For a more indepth description of the differences, see this thread Actors vs. CSP

Building

You need to ensure that your $GOPATH variable is properly set.

Next, install the standard protocol buffer implementation and run the following commands to get all the necessary tooling:

go get github.com/asynkron/protoactor-go/...
cd $GOPATH/src/github.com/asynkron/protoactor-go
go get ./...
make

After invoking last command you will have generated protobuf definitions and built the project.

Windows users can use Cygwin to run make: www.cygwin.com

Testing

This command exectutes all tests in the repository except for consul integration tests (you need consul for running those tests). We also skip directories that don't contain any tests.

go test `go list ./... | grep -v "/examples/" | grep -v "/persistence" | grep -v "/scheduler"`

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() {
    context := actor.EmptyRootContext
    props := actor.PropsFromProducer(func() actor.Actor { return &HelloActor{} })
    pid, err := context.Spawn(props)
    if err != nil {
        panic(err)
    }
    context.Send(pid, Hello{Who: "Roger"})
    console.ReadLine()
}

State machines / SetBehavior, PushBehavior and PopBehavior

type Hello struct{ Who string }
type SetBehaviorActor struct{}

func (state *SetBehaviorActor) Receive(context actor.Context) {
    switch msg := context.Message().(type) {
    case Hello:
        fmt.Printf("Hello %v\n", msg.Who)
        context.SetBehavior(state.Other)
    }
}

func (state *SetBehaviorActor) 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 NewSetBehaviorActor() actor.Actor {
    return &SetBehaviorActor{}
}

func main() {
    context := actor.EmptyRootContext
    props := actor.PropsFromProducer(NewSetBehaviorActor)
    pid, err := context.Spawn(props)
    if err != nil {
        panic(err)
    }
    context.Send(pid, Hello{Who: "Roger"})
    context.Send(pid, Hello{Who: "Roger"})
    console.ReadLine()
}

Lifecycle events

Unlike Akka, Proto Actor 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 its children are stopped")
    case *actor.Restarting:
        fmt.Println("Restarting, actor is about restart")
    case Hello:
        fmt.Printf("Hello %v\n", msg.Who)
    }
}

func main() {
    context := actor.EmptyRootContext
    props := actor.PropsFromProducer(func() actor.Actor { return &HelloActor{} })
    pid, err := context.Spawn(props)
    if err != nil {
        panic(err)
    }
    context.Send(pid, 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)
    context.Stop(pid)

    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 Proto Actor.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.PropsFromProducer(NewChildActor)
        child := context.Spawn(props)
        context.Send(child, 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 its 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(reason interface{}) actor.Directive {
		log.Printf("handling failure for child. reason:%v", reason)

		// return actor.StopDirective
		return actor.RestartDirective
	}
	supervisor := actor.NewOneForOneStrategy(10, 1000, decider)

	ctx := actor.NewActorSystem().Root
	props := actor.PropsFromProducer(NewParentActor).WithSupervisor(supervisor)

	pid := ctx.Spawn(props)
	ctx.Send(pid, Hello{Who: "Roger"})

	console.ReadLine()
}

Networking / Remoting

Proto Actor'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 context.Message().(type) {
    case *messages.Response:
        state.count++
        fmt.Println(state.count)
    }
}

func main() {
    remote.Start("localhost:8090")

    context := actor.EmptyRootContext
    props := actor.PropsFromProducer(func() actor.Actor { return &MyActor{} })
    pid, _ := context.Spawn(props)
    message := &messages.Echo{Message: "hej", Sender: pid}

    // this is to spawn remote actor we want to communicate with
    spawnResponse, _ := remote.SpawnNamed("localhost:8091", "myactor", "hello", time.Second)

    // get spawned PID
    spawnedPID := spawnResponse.Pid
    for i := 0; i < 10; i++ {
        context.Send(spawnedPID, message)
    }

    console.ReadLine()
}

Node 2

type MyActor struct{}

func (*MyActor) Receive(context actor.Context) {
    switch msg := context.Message().(type) {
    case *messages.Echo:
        context.Send(msg.Sender, &messages.Response{
            SomeValue: "result",
        })
    }
}

func main() {
    remote.Start("localhost:8091")

    // register a name for our local actor so that it can be spawned remotely
    remote.Register("hello", actor.PropsFromProducer(func() actor.Actor { return &MyActor{} }))
    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;
}

Notice: always use "gogoslick_out" instead of "go_out" when generating proto code. "gogoslick_out" will create type names which will be used during serialization.

For more examples, see the example folder in this repository.

Contributors

Made with contributors-img.