Home

Awesome

GoAkt

build Go Reference GitHub Release GitHub Tag Go Report Card codecov

Distributed Go actor framework to build reactive and distributed system in golang using protocol buffers as actor messages.

GoAkt is highly scalable and available when running in cluster mode. It comes with the necessary features require to build a distributed actor-based system without sacrificing performance and reliability. With GoAkt, you can instantly create a fast, scalable, distributed system across a cluster of computers.

If you are not familiar with the actor model, the blog post from Brian Storti here is an excellent and short introduction to the actor model. Also, check reference section at the end of the post for more material regarding actor model.

Table of Content

Design Principles

This framework has been designed:

Use Cases

Installation

go get github.com/tochemey/goakt/v2

Versioning

The version system adopted in GoAkt deviates a bit from the standard semantic versioning system. The version format is as follows:

The versioning will remain like v2.x.x until further notice.

Examples

Kindly check out the examples' repository.

Features

Actors

The fundamental building blocks of GoAkt are actors.

Passivation

Actors can be passivated when they are idle after some period of time. Passivated actors are removed from the actor system to free-up resources. When cluster mode is enabled, passivated actors are removed from the entire cluster. To bring back such actors to live, one needs to Spawn them again. By default, all actors are passivated and the passivation time is two minutes.

Supervision

In GoAkt, supervision allows to define the various strategies to apply when a given actor is faulty. The supervisory strategy to adopt is set during the creation of the actor system. In GoAkt each child actor is treated separately. There is no concept of one-for-one and one-for-all strategies. The following directives are supported:

With the Restart directive, every child actor of the faulty is stopped and garbage-collected when the given parent is restarted. This helps avoid resources leaking. There are only two scenarios where an actor can supervise another actor:

Actor System

Without an actor system, it is not possible to create actors in GoAkt. Only a single actor system is recommended to be created per application when using GoAkt. At the moment the single instance is not enforced in GoAkt, this simple implementation is left to the discretion of the developer. To create an actor system one just need to use the NewActorSystem method with the various Options. GoAkt ActorSystem has the following characteristics:

Behaviors

Actors in GoAkt have the power to switch their behaviors at any point in time. When you change the actor behavior, the new behavior will take effect for all subsequent messages until the behavior is changed again. The current message will continue processing with the existing behavior. You can use Stashing to reprocess the current message with the new behavior.

To change the behavior, call the following methods on the ReceiveContext interface when handling a message:

Router

Routers help send the same type of message to a set of actors to be processed in parallel depending upon the type of the router used. Routers should be used with caution because they can hinder performance. When the router receives a message to broadcast, every routee is checked whether alive or not. When a routee is not alive the router removes it from its set of routees. When the last routee stops the router itself stops.

GoAkt comes shipped with the following routing strategies:

A router a just like any other actor that can be spawned. To spawn router just call the ActorSystem SpawnRouter method. Router as well as their routees are not passivated.

Mailbox

Once can implement a custom mailbox. See Mailbox. GoAkt comes with the following mailboxes built-in:

Events Stream

To receive some system events and act on them for some particular business cases, you just need to call the actor system Subscribe. Make sure to Unsubscribe whenever the subscription is no longer needed to free allocated resources. The subscription methods can be found on the ActorSystem interface.

Supported events

Messaging

Communication between actors is achieved exclusively through message passing. In GoAkt Google Protocol Buffers is used to define messages. The choice of protobuf is due to easy serialization over wire and strong schema definition. As stated previously the following messaging patterns are supported:

Scheduler

You can schedule sending messages to actor that will be acted upon in the future. To achieve that you can use the following methods on the Actor System:

Cron Expression Format

FieldRequiredAllowed ValuesAllowed Special Characters
Secondsyes0-59, - * /
Minutesyes0-59, - * /
Hoursyes0-23, - * /
Day of monthyes1-31, - * ? /
Monthyes1-12 or JAN-DEC, - * /
Day of weekyes1-7 or SUN-SAT, - * ? /
Yearnoempty, 1970-, - * /

Note

When running the actor system in a cluster only one instance of a given scheduled message will be running across the entire cluster.

Stashing

Stashing is a mechanism you can enable in your actors, so they can temporarily stash away messages they cannot or should not handle at the moment. Another way to see it is that stashing allows you to keep processing messages you can handle while saving for later messages you can't. Stashing are handled by GoAkt out of the actor instance just like the mailbox, so if the actor dies while processing a message, all messages in the stash are processed. This feature is usually used together with Become/UnBecome, as they fit together very well, but this is not a requirement.

It’s recommended to avoid stashing too many messages to avoid too much memory usage. If you try to stash more messages than the capacity the actor will panic. To use the stashing feature, call the following methods on the ReceiveContext when handling a message:

Remoting

Remoting allows remote actors to communicate. The underlying technology is gRPC. To enable remoting just use the WithRemoting option when creating the actor system. See actor system options. These are the following remoting features available:

These methods can be found as well as on the PID which is the actor reference when an actor is created.

Cluster

This offers simple scalability, partitioning (sharding), and re-balancing out-of-the-box. GoAkt nodes are automatically discovered. See Clustering. Beware that at the moment, within the cluster the existence of an actor is unique.

Observability

Observability is key in distributed system. It helps to understand and track the performance of a system. GoAkt offers out of the box features that can help track, monitor and measure the performance of a GoAkt based system.

Metrics

The following methods have been implemented to help push some metrics to any observability tool:

Logging

A simple logging interface to allow custom logger to be implemented instead of using the default logger.

Encryption and mTLS

GoAkt does not support at the moment any form of data encryption or TLS to prevent any form of mitm attack. This feature may come in the future. At the moment, I will recommend a GoAkt-based application should be deployed behind a vpc or using a service mesh like Linkerd or Istio which offers great mTLS support when it comes to service communucation.

Testkit

GoAkt comes packaged with a testkit that can help test that actors receive expected messages within unit tests. The teskit in GoAkt uses underneath the https://github.com/stretchr/testify package. To test that an actor receive and respond to messages one will have to:

  1. Create an instance of the testkit: testkit := New(ctx, t) where ctx is a go context and t the instance of *testing.T. This can be done in setup before the run of each test.
  2. Create the instance of the actor under test. Example: pinger := testkit.Spawn(ctx, "pinger", &pinger{})
  3. Create an instance of test probe: probe := testkit.NewProbe(ctx) where ctx is a go context. One can set some options
  4. Use the probe to send a message to the actor under test. Example: probe.Send(pinger, new(testpb.Ping))
  5. Assert that the actor under test has received the message and responded as expected using the probe methods:
  1. Make sure to shut down the testkit and the probe. Example: probe.Stop(), testkit.Shutdown(ctx) where ctx is a go context. These two calls can be in a tear down after all tests run.

To help implement unit tests in GoAkt-based applications. See Testkit

API

The API interface helps interact with a GoAkt actor system as kind of client. The following features are available:

Client

The GoAkt client facilitates interaction with a specified GoAkt cluster, contingent upon the activation of cluster mode. The client operates without knowledge of the specific node within the cluster that will process the request. This feature is particularly beneficial when interfacing with a GoAkt cluster from an external system. GoAkt client is equipped with a mini load-balancer that helps route requests to the appropriate node.

Balancer strategies

Features

Clustering

The cluster engine depends upon the discovery mechanism to find other nodes in the cluster. Under the hood, it leverages Olric to scale out and guarantee performant, reliable persistence, simple scalability, partitioning (sharding), and re-balancing out-of-the-box. It requires remoting to be enabled. One can implement a custom hasher for the partitioning using the Hasher interface and the Actor System option to set it. The default hasher uses the XXH3 algorithm.

At the moment the following providers are implemented:

Note: One can add additional discovery providers using the following discovery provider.

Operations Guide

The following outlines the cluster mode operations which can help have a healthy GoAkt cluster:

Redeployment

When a node leaves the cluster, as long as the cluster quorum is stable, its actors are redeployed on the remaining nodes of the cluster. The redeployed actors are created with their initial state. Every field of the Actor set using the PreStart will have their value set as expected. On the contrary every field of the Actor will be set to their default go type value because actors are created using reflection.

Built-in Discovery Providers

Kubernetes Discovery Provider Setup

To get the kubernetes discovery working as expected, the following pod labels need to be set:

Get Started
package main

import "github.com/tochemey/goakt/v2/discovery/kubernetes"

const (
    namespace          = "default"
    applicationName    = "accounts"
    actorSystemName    = "AccountsSystem"
    discoveryPortName  = "discovery-port"
    peersPortName      = "peers-port"
    remotingPortName   = "remoting-port"
)

// define the discovery config
config := kubernetes.Config{
    ApplicationName:  applicationName,
    ActorSystemName:  actorSystemName,
    Namespace:        namespace,
    DiscoveryPortName:   gossipPortName,
    RemotingPortName: remotingPortName,
    PeersPortName:  peersPortName,
}

// instantiate the k8 discovery provider
disco := kubernetes.NewDiscovery(&config)

// pass the service discovery when enabling cluster mode in the actor system
Role Based Access

You’ll also have to grant the Service Account that your pods run under access to list pods. The following configuration can be used as a starting point. It creates a Role, pod-reader, which grants access to query pod information. It then binds the default Service Account to the Role by creating a RoleBinding. Adjust as necessary:

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: pod-reader
rules:
  - apiGroups: [""] # "" indicates the core API group
    resources: ["pods"]
    verbs: ["get", "watch", "list"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: read-pods
subjects:
  # Uses the default service account. Consider creating a new one.
  - kind: ServiceAccount
    name: default
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

A working example can be found here

mDNS Discovery Provider Setup

NATS Discovery Provider Setup

To use the NATS discovery provider one needs to provide the following:

package main

import "github.com/tochemey/goakt/v2/discovery/nats"

const (
    natsServerAddr   = "nats://127.0.0.1:4248"
    natsSubject      = "goakt-gossip"
    applicationName  = "accounts"
    actorSystemName  = "AccountsSystem"
)

// define the discovery options
config := nats.Config{
    ApplicationName: applicationName,
    ActorSystemName: actorSystemName,
    NatsServer:      natsServerAddr,
    NatsSubject:     natsSubject,
    Host:            "127.0.0.1",
    DiscoveryPort:   20380,
}

// instantiate the NATS discovery provider by passing the config and the hostNode
disco := nats.NewDiscovery(&config)

// pass the service discovery when enabling cluster mode in the actor system

DNS Provider Setup

This provider performs nodes discovery based upon the domain name provided. This is very useful when doing local development using docker.

To use the DNS discovery provider one needs to provide the following:

package main

import "github.com/tochemey/goakt/v2/discovery/dnssd"

const domainName = "accounts"

// define the discovery options
config := dnssd.Config{
    dnssd.DomainName: domainName,
    dnssd.IPv6:       false,
}
// instantiate the dnssd discovery provider
disco := dnssd.NewDiscovery(&config)

// pass the service discovery when enabling cluster mode in the actor system

A working example can be found here

Static Provider Setup

This provider performs nodes discovery based upon the list of static hosts addresses. The address of each host is the form of host:port where port is the gossip protocol port.

package main

import "github.com/tochemey/goakt/v2/discovery/static"

// define the discovery configuration
config := static.Config{
    Hosts: []string{
    "node1:3322",
    "node2:3322",
    "node3:3322",
    },
}
// instantiate the dnssd discovery provider
disco := static.NewDiscovery(&config)

// pass the service discovery when enabling cluster mode in the actor system

A working example can be found here

Contribution

Contributions are welcome! The project adheres to Semantic Versioning and Conventional Commits. This repo uses Earthly.

To contribute please:

Test & Linter

Prior to submitting a pull request, please run:

earthly +test

Benchmark

One can run the benchmark test from the bench package: