Go C#

This page was generated by AI from the .NET version. Please report any errors you find

Getting Started With Grains / Virtual Actors (Go)

In this tutorial we will:

  1. Model smart bulbs and a smart house using virtual actors/grains.
  2. Run these grains in a cluster of members (nodes).
  3. Send messages to and between these grains.
  4. Host everything in a simple Go app.

The code from this tutorial is available on GitHub: Go version | .NET version.

Setting up the project

First things first, let’s get the project setup and basic configuration out of the way, so we can later focus on grains and clustering.

Required packages

Create a folder named proto-cluster-tutorial and initialize a Go module:

go mod init github.com/you/proto-cluster-tutorial

We’ll need the following packages:

  • github.com/asynkron/protoactor-go/actor
  • github.com/asynkron/protoactor-go/cluster
  • github.com/asynkron/protoactor-go/remote
  • github.com/asynkron/protoactor-go/cluster/clusterproviders/consul (for development we can keep everything local)
  • github.com/asynkron/protoactor-go/cluster/identitylookup/disthash

Install them using go get:

go get github.com/asynkron/protoactor-go@latest

Base app

Let’s establish what our base app code should look like. Create a main.go file:

package main

import "fmt"

func main() {
    fmt.Println("Hello, Proto.Cluster!")
}

Try running your app to see if everything works so far.

Basic Proto.Cluster infrastructure and configuration

First, we’ll get the basic infrastructure of the cluster going.

We need to create, configure and start an ActorSystem. We’ll also configure the remote and cluster components.

main.go:

package main

import (
    "fmt"

    "github.com/asynkron/protoactor-go/actor"
    "github.com/asynkron/protoactor-go/cluster"
    "github.com/asynkron/protoactor-go/cluster/clusterproviders/consul"
    "github.com/asynkron/protoactor-go/cluster/identitylookup/disthash"
    "github.com/asynkron/protoactor-go/remote"
)

func main() {
    system := actor.NewActorSystem()

    provider, _ := consul.New()
    lookup := disthash.New()
    remoteConfig := remote.Configure("localhost", 0)

    clusterConfig := cluster.Configure("ProtoClusterTutorial", provider, lookup, remoteConfig)
    c := cluster.New(system, clusterConfig)

    c.StartMember()
    fmt.Println("Cluster member started. Press Enter to exit...")
    fmt.Scanln()
    c.Shutdown(true)
}

Let’s go through each configuration section one by one:

Actor System configuration

This is a standard Proto.Actor configuration. It’s out of the scope for this tutorial; if you want to learn more, you should check out the Actors section of Proto.Actor’s documentation.

Remote configuration

Proto.Cluster uses Proto.Remote for transport. Again, its configuration is out of scope for this tutorial; if you want to learn more, you should check out the Remote section of Proto.Actor’s documentation.

Cluster configuration

This is where we configure Proto.Cluster. Let’s explain its parameters:

  1. clusterName - any name will do.
  2. clusterProvider - a Cluster Provider is an abstraction that provides information about currently available members (nodes) in a cluster. Since right now our cluster only has one member, it’s ok to use a Consul provider.
  3. identityLookup - an Identity Lookup is an abstraction that allows a cluster to locate grains. disthash is generally a good choice for most cases. You can read more about Identity Lookup here.

Cluster object

Most of the time we’ll want to interact with the cluster, we will use a Cluster object. You can get it from an ActorSystem instance:

import "github.com/asynkron/protoactor-go/cluster"

// ...

c := cluster.New(system, clusterConfig)

Starting a cluster member

Cluster members need to be explicitly started and shut down. In the sample above, StartMember and Shutdown take care of it.

Implementing grains

Now that the infrastructure is in place we can create grains. We’ll model a smart bulb that can be turned on and off and can report its state.

Define messages and grain contract

Create a file named smartbulb.proto:

syntax = "proto3";
package smartbulb;
option go_package = "github.com/you/proto-cluster-tutorial/smartbulb";

import "google/protobuf/empty.proto";

message GetStateResponse {
  bool isOn = 1;
}

service SmartBulb {
  rpc TurnOn(google.protobuf.Empty) returns (google.protobuf.Empty);
  rpc TurnOff(google.protobuf.Empty) returns (google.protobuf.Empty);
  rpc GetState(google.protobuf.Empty) returns (GetStateResponse);
}

Once you have this, you can generate your code using protoc:

protoc --go_out=. --go_opt=paths=source_relative \
    --plugin=protoc-gen-go-grain=PATH_TO_PROTOC_GEN_GO_GRAIN \
    --go-grain_out=. --go-grain_opt=paths=source_relative \
    smartbulb.proto

Implementing the grain

When the contracts have been generated, you can start implementing your grain:

package smartbulb

import (
    "github.com/asynkron/protoactor-go/cluster"
    "google.golang.org/protobuf/types/known/emptypb"
)

type Bulb struct {
    on bool
}

func (b *Bulb) Init(ctx cluster.GrainContext)           {}
func (b *Bulb) Terminate(ctx cluster.GrainContext)      {}
func (b *Bulb) ReceiveDefault(ctx cluster.GrainContext) {}

func (b *Bulb) TurnOn(_ *emptypb.Empty, ctx cluster.GrainContext) (*emptypb.Empty, error) {
    b.on = true
    return &emptypb.Empty{}, nil
}

func (b *Bulb) TurnOff(_ *emptypb.Empty, ctx cluster.GrainContext) (*emptypb.Empty, error) {
    b.on = false
    return &emptypb.Empty{}, nil
}

func (b *Bulb) GetState(_ *emptypb.Empty, ctx cluster.GrainContext) (*GetStateResponse, error) {
    return &GetStateResponse{IsOn: b.on}, nil
}

func init() {
    SmartBulbFactory(func() SmartBulb { return &Bulb{} })
}

Register the grain kind when configuring the cluster:

bulbKind := smartbulb.NewSmartBulbKind(func() smartbulb.SmartBulb { return &smartbulb.Bulb{} }, 0)
clusterConfig := cluster.Configure("ProtoClusterTutorial", provider, lookup, remoteConfig, cluster.WithKinds(bulbKind))

Interacting with the grain

You can obtain a client for a specific smart bulb grain via the generated helper:

bulbClient := smartbulb.GetSmartBulbGrainClient(c, "kitchen")
if _, err := bulbClient.TurnOn(nil); err != nil {
    panic(err)
}
state, _ := bulbClient.GetState(nil)
fmt.Println("Is on:", state.IsOn)

Running multiple members

To showcase how grains work in a distributed system, run two instances of the application. Each instance will host grains and will be able to communicate with grains on other nodes.

Start the first node:

go run main.go

In another terminal, run the second node:

go run main.go

Try turning smart bulbs on and off from either node; state will be kept per grain identity across the cluster.

That’s it – you’ve created and run your first grains in Proto.Actor using Go.

Icon