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:
- Model smart bulbs and a smart house using virtual actors/grains.
- Run these grains in a cluster of members (nodes).
- Send messages to and between these grains.
- 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:
clusterName
- any name will do.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.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.