diff --git a/cluster/gossip.go b/cluster/gossip.go index 6a5eed0b..98f1c30c 100644 --- a/cluster/gossip.go +++ b/cluster/gossip.go @@ -4,6 +4,7 @@ package cluster import ( "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" ) // customary type that defines a states sender callback. @@ -17,6 +18,7 @@ type GossipStateStorer interface { SetMapState(stateKey string, mapKey string, value proto.Message) RemoveMapState(stateKey string, mapKey string) GetMapKeys(stateKey string) []string + GetMapState(stateKey string, mapKey string) *anypb.Any } // This interface must be implemented by any value that diff --git a/cluster/gossip_actor.go b/cluster/gossip_actor.go index 7b1dae20..a18fa456 100644 --- a/cluster/gossip_actor.go +++ b/cluster/gossip_actor.go @@ -49,6 +49,8 @@ func (ga *GossipActor) Receive(ctx actor.Context) { ga.onSetGossipState(r, ctx) case *SetGossipMapState: ga.onSetGossipMapState(r, ctx) + case *GetGossipMapStateRequest: + ga.onGetGossipMapStateRequest(r, ctx) case *RemoveGossipMapState: ga.onRemoveGossipMapState(r, ctx) case *GetGossipMapKeysRequest: @@ -210,16 +212,37 @@ func (ga *GossipActor) sendGossipForMember(member *Member, memberStateDelta *Mem } func (ga *GossipActor) onSetGossipMapState(r *SetGossipMapState, ctx actor.Context) { + if ga.throttler() == actor.Open { + ctx.Logger().Debug("Setting GossipMapState", slog.String("key", r.MapKey), slog.Any("message", r.Value)) + } ga.gossip.SetMapState(r.GossipStateKey, r.MapKey, r.Value) } func (ga *GossipActor) onRemoveGossipMapState(r *RemoveGossipMapState, ctx actor.Context) { + if ga.throttler() == actor.Open { + ctx.Logger().Debug("Removing GossipMapState", slog.String("key", r.MapKey)) + } ga.gossip.RemoveMapState(r.GossipStateKey, r.MapKey) } func (ga *GossipActor) onGetGossipMapKeys(r *GetGossipMapKeysRequest, ctx actor.Context) { + if ga.throttler() == actor.Open { + ctx.Logger().Debug("Getting GossipMapKeys", slog.String("key", r.GossipStateKey)) + } + keys := ga.gossip.GetMapKeys(r.GossipStateKey) + res := &GetGossipMapKeysResponse{ + MapKeys: keys, + } + ctx.Respond(res) +} - res := ga.gossip.GetMapKeys(r.GossipStateKey) - +func (ga *GossipActor) onGetGossipMapStateRequest(r *GetGossipMapStateRequest, ctx actor.Context) { + if ga.throttler() == actor.Open { + ctx.Logger().Debug("Setting GossipMapState", slog.String("key", r.MapKey)) + } + a := ga.gossip.GetMapState(r.GossipStateKey, r.MapKey) + res := &GetGossipMapStateResponse{ + Value: a, + } ctx.Respond(res) } diff --git a/cluster/gossiper.go b/cluster/gossiper.go index 359828a3..b6874da6 100644 --- a/cluster/gossiper.go +++ b/cluster/gossiper.go @@ -70,7 +70,7 @@ func newGossiper(cl *Cluster, opts ...Option) (*Gossiper, error) { func (g *Gossiper) GetState(key string) (map[string]*GossipKeyValue, error) { if g.throttler() == actor.Open { - g.cluster.Logger().Debug("Gossiper getting state", slog.String("key", key), slog.String("remote", g.pid.String())) + g.cluster.Logger().Debug("Gossiper getting state", slog.String("key", key), slog.String("gossipPid", g.pid.String())) } msg := NewGetGossipStateRequest(key) @@ -79,13 +79,13 @@ func (g *Gossiper) GetState(key string) (map[string]*GossipKeyValue, error) { if err != nil { switch err { case actor.ErrTimeout: - g.cluster.Logger().Error("Could not get a response from GossipActor: request timeout", slog.Any("error", err), slog.String("remote", g.pid.String())) + g.cluster.Logger().Error("Could not get a response from GossipActor: request timeout", slog.Any("error", err), slog.String("gossipPid", g.pid.String())) return nil, err case actor.ErrDeadLetter: - g.cluster.Logger().Error("remote no longer exists", slog.Any("error", err), slog.String("remote", g.pid.String())) + g.cluster.Logger().Error("remote no longer exists", slog.Any("error", err), slog.String("gossipPid", g.pid.String())) return nil, err default: - g.cluster.Logger().Error("Could not get a response from GossipActor", slog.Any("error", err), slog.String("remote", g.pid.String())) + g.cluster.Logger().Error("Could not get a response from GossipActor", slog.Any("error", err), slog.String("gossipPid", g.pid.String())) return nil, err } } @@ -94,7 +94,7 @@ func (g *Gossiper) GetState(key string) (map[string]*GossipKeyValue, error) { response, ok := r.(*GetGossipStateResponse) if !ok { err := fmt.Errorf("could not promote %T interface to GetGossipStateResponse", r) - g.cluster.Logger().Error("Could not get a response from GossipActor", slog.Any("error", err), slog.String("remote", g.pid.String())) + g.cluster.Logger().Error("Could not get a response from GossipActor", slog.Any("error", err), slog.String("gossipPid", g.pid.String())) return nil, err } @@ -102,23 +102,112 @@ func (g *Gossiper) GetState(key string) (map[string]*GossipKeyValue, error) { } // SetState Sends fire and forget message to update member state -func (g *Gossiper) SetState(key string, value proto.Message) { +func (g *Gossiper) SetState(gossipStateKey string, value proto.Message) { if g.throttler() == actor.Open { - g.cluster.Logger().Debug("Gossiper setting state", slog.String("key", key), slog.String("remote", g.pid.String())) + g.cluster.Logger().Info("Gossiper setting state", slog.String("gossipStateKey", gossipStateKey), slog.String("gossipPid", g.pid.String())) } if g.pid == nil { return } - msg := NewGossipStateKey(key, value) + msg := NewGossipStateKey(gossipStateKey, value) + g.cluster.ActorSystem.Root.Send(g.pid, &msg) +} + +func (g *Gossiper) SetMapState(gossipStateKey string, mapKey string, value proto.Message) { + if g.throttler() == actor.Open { + g.cluster.Logger().Info("Gossiper setting map state", slog.String("gossipStateKey", gossipStateKey), slog.String("gossipPid", g.pid.String())) + } + + if g.pid == nil { + return + } + + msg := SetGossipMapState{ + GossipStateKey: gossipStateKey, + MapKey: mapKey, + Value: value, + } + g.cluster.ActorSystem.Root.Send(g.pid, &msg) } +func (g *Gossiper) GetMapState(gossipStateKey string, mapKey string) *anypb.Any { + if g.throttler() == actor.Open { + g.cluster.Logger().Info("Gossiper setting map state", slog.String("gossipStateKey", gossipStateKey), slog.String("gossipPid", g.pid.String())) + } + + msg := GetGossipMapStateRequest{ + GossipStateKey: gossipStateKey, + MapKey: mapKey, + } + + x, err := g.cluster.ActorSystem.Root.RequestFuture(g.pid, &msg, g.cluster.Config.TimeoutTime).Result() + if err != nil { + g.cluster.Logger().Error("Could not get a response from GossipActor", slog.Any("error", err), slog.String("gossipPid", g.pid.String())) + return nil + } + //cast x to GetGossipMapStateResponse + response, ok := x.(*GetGossipMapStateResponse) + if !ok { + err := fmt.Errorf("could not promote %T interface to GetGossipMapStateResponse", x) + g.cluster.Logger().Error("Could not get a response from GossipActor", slog.Any("error", err), slog.String("gossipPid", g.pid.String())) + return nil + } + return response.Value +} + +func (g *Gossiper) RemoveMapState(gossipStateKey string, mapKey string) { + if g.throttler() == actor.Open { + g.cluster.Logger().Info("Gossiper setting map state", slog.String("gossipStateKey", gossipStateKey), slog.String("gossipPid", g.pid.String())) + } + + if g.pid == nil { + return + } + + msg := RemoveGossipMapState{ + GossipStateKey: gossipStateKey, + MapKey: mapKey, + } + + g.cluster.ActorSystem.Root.Send(g.pid, &msg) +} + +func (g *Gossiper) GetMapKeys(gossipStateKey string) []string { + if g.throttler() == actor.Open { + g.cluster.Logger().Info("Gossiper setting map state", slog.String("gossipStateKey", gossipStateKey), slog.String("gossipPid", g.pid.String())) + } + + if g.pid == nil { + return []string{} + } + + msg := GetGossipMapKeysRequest{ + GossipStateKey: gossipStateKey, + } + + res, err := g.cluster.ActorSystem.Root.RequestFuture(g.pid, &msg, g.cluster.Config.TimeoutTime).Result() + + if err != nil { + g.cluster.Logger().Error("Could not get a response from GossipActor", slog.Any("error", err), slog.String("gossipPid", g.pid.String())) + return []string{} + } + //cast res to GetGossipMapKeysResponse + response, ok := res.(*GetGossipMapKeysResponse) + if !ok { + err := fmt.Errorf("could not promote %T interface to GetGossipMapKeysResponse", res) + g.cluster.Logger().Error("Could not get a response from GossipActor", slog.Any("error", err), slog.String("gossipPid", g.pid.String())) + return []string{} + } + return response.MapKeys +} + // SetStateRequest Sends a Request (that blocks) to update member state func (g *Gossiper) SetStateRequest(key string, value proto.Message) error { if g.throttler() == actor.Open { - g.cluster.Logger().Debug("Gossiper setting state", slog.String("key", key), slog.String("remote", g.pid.String())) + g.cluster.Logger().Debug("Gossiper setting state", slog.String("key", key), slog.String("gossipPid", g.pid.String())) } if g.pid == nil { @@ -129,10 +218,10 @@ func (g *Gossiper) SetStateRequest(key string, value proto.Message) error { r, err := g.cluster.ActorSystem.Root.RequestFuture(g.pid, &msg, g.cluster.Config.TimeoutTime).Result() if err != nil { if err == actor.ErrTimeout { - g.cluster.Logger().Error("Could not get a response from Gossiper Actor: request timeout", slog.String("remote", g.pid.String())) + g.cluster.Logger().Error("Could not get a response from Gossiper Actor: request timeout", slog.String("gossipPid", g.pid.String())) return err } - g.cluster.Logger().Error("Could not get a response from Gossiper Actor", slog.Any("error", err), slog.String("remote", g.pid.String())) + g.cluster.Logger().Error("Could not get a response from Gossiper Actor", slog.Any("error", err), slog.String("gossipPid", g.pid.String())) return err } @@ -140,7 +229,7 @@ func (g *Gossiper) SetStateRequest(key string, value proto.Message) error { _, ok := r.(*SetGossipStateResponse) if !ok { err := fmt.Errorf("could not promote %T interface to SetGossipStateResponse", r) - g.cluster.Logger().Error("Could not get a response from Gossip Actor", slog.Any("error", err), slog.String("remote", g.pid.String())) + g.cluster.Logger().Error("Could not get a response from Gossip Actor", slog.Any("error", err), slog.String("gossipPid", g.pid.String())) return err } return nil @@ -311,5 +400,5 @@ func (g *Gossiper) blockGracefullyLeft() { } func (g *Gossiper) throttledLog(counter int32) { - g.cluster.Logger().Debug("Gossiper Setting State", slog.String("remote", g.pid.String()), slog.Int("throttled", int(counter))) + g.cluster.Logger().Debug("Gossiper Setting State", slog.String("gossipPid", g.pid.String()), slog.Int("throttled", int(counter))) } diff --git a/cluster/informer.go b/cluster/informer.go index f13744ef..4118aa37 100644 --- a/cluster/informer.go +++ b/cluster/informer.go @@ -300,11 +300,19 @@ func (inf *Informer) SetMapState(stateKey string, mapKey string, value proto.Mes if err != nil { inf.logger.Error("Failed to create Any", slog.Any("error", err)) } + gmap.Items[mapKey] = v inf.SetState(stateKey, gmap) } +func (inf *Informer) GetMapState(stateKey string, mapKey string) *anypb.Any { + gmap := inf.getGossipMap(stateKey) + + a := gmap.Items[mapKey] + return a +} + func (inf *Informer) RemoveMapState(stateKey string, mapKey string) { gmap := inf.getGossipMap(stateKey) @@ -328,6 +336,7 @@ func (inf *Informer) getGossipMap(stateKey string) *GossipMap { s := inf.GetState(stateKey) mys := s[inf.myID] gmap := &GossipMap{} + gmap.Items = make(map[string]*anypb.Any) if mys != nil { err := mys.Value.UnmarshalTo(gmap) if err != nil { diff --git a/cluster/messages.go b/cluster/messages.go index 130008e1..dbd68252 100644 --- a/cluster/messages.go +++ b/cluster/messages.go @@ -2,6 +2,7 @@ package cluster import ( "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" ) // Used to query the GossipActor about a given key status @@ -40,6 +41,17 @@ type SetGossipMapState struct { Value proto.Message } +// Used to query the Gossip State containing GossipMap data type in the GossipActor +type GetGossipMapStateRequest struct { + GossipStateKey string + MapKey string +} + +// Used by the GossipActor to send back the GossipMap value of a given key +type GetGossipMapStateResponse struct { + Value *anypb.Any +} + // Used to remove Gossip State containing GossipMap data type in the GossipActor type RemoveGossipMapState struct { GossipStateKey string diff --git a/examples/cluster-gossip/go.mod b/examples/cluster-gossip/go.mod index 05a0ec1d..700b5765 100644 --- a/examples/cluster-gossip/go.mod +++ b/examples/cluster-gossip/go.mod @@ -9,10 +9,9 @@ require ( replace github.com/asynkron/protoactor-go => ../.. -require ( - github.com/asynkron/protoactor-go v0.0.0-20240406090656-8c90bda12e81 - github.com/lmittmann/tint v1.0.3 -) +require github.com/asynkron/protoactor-go v0.0.0-20240408071539-f4cee9b2a813 + +require github.com/lmittmann/tint v1.0.3 require ( github.com/Workiva/go-datastructures v1.1.1 // indirect diff --git a/examples/cluster-gossip/node1/main.go b/examples/cluster-gossip/node1/main.go index e7d4fdbd..8d612ba5 100644 --- a/examples/cluster-gossip/node1/main.go +++ b/examples/cluster-gossip/node1/main.go @@ -18,21 +18,35 @@ import ( func main() { c := startNode() + fmt.Printf("Cluster %v\n", c.ActorSystem.ID) + + //example on how to work with maps inside the gossip + c.Gossip.SetMapState("someGossipEntry", "abc", &shared.StringValue{Value: "Hello World"}) + c.Gossip.SetMapState("someGossipEntry", "def", &shared.StringValue{Value: "Lorem Ipsum"}) + keys := c.Gossip.GetMapKeys("someGossipEntry") + fmt.Printf("Keys %v\n", keys) + for _, key := range keys { + value := c.Gossip.GetMapState("someGossipEntry", key) + v := &shared.StringValue{} + _ = value.UnmarshalTo(v) + fmt.Printf("Key %v Value %v\n", key, v.Value) + } + fmt.Print("\nBoot other nodes and press Enter\n") - console.ReadLine() + _, _ = console.ReadLine() pid := c.Get("abc", "hello") fmt.Printf("Got pid %v\n", pid) res, _ := c.Request("abc", "hello", &shared.HelloRequest{Name: "Roger"}) fmt.Printf("Got response %v\n", res) fmt.Println() - console.ReadLine() + _, _ = console.ReadLine() c.Shutdown(true) } func coloredConsoleLogging(system *actor.ActorSystem) *slog.Logger { return slog.New(tint.NewHandler(os.Stdout, &tint.Options{ - Level: slog.LevelError, + Level: slog.LevelInfo, TimeFormat: time.RFC3339, AddSource: true, })).With("lib", "Proto.Actor"). @@ -41,28 +55,36 @@ func coloredConsoleLogging(system *actor.ActorSystem) *slog.Logger { func startNode() *cluster.Cluster { system := actor.NewActorSystem(actor.WithLoggerFactory(coloredConsoleLogging)) - system.EventStream.Subscribe(func(evt interface{}) { - switch msg := evt.(type) { - case *cluster.ClusterTopology: - fmt.Printf("\nClusterTopology %v\n\n", msg) - case *cluster.GossipUpdate: - - heartbeat := &cluster.MemberHeartbeat{} - - fmt.Printf("Member %v\n", msg.MemberID) - fmt.Printf("Sequence Number %v\n", msg.SeqNumber) - unpackErr := msg.Value.UnmarshalTo(heartbeat) - if unpackErr != nil { - fmt.Printf("Unpack error %v\n", unpackErr) - } else { - //loop over as.ActorCount map - for k, v := range heartbeat.ActorStatistics.ActorCount { - fmt.Printf("ActorCount %v %v\n", k, v) - } - } - } - }) + //system.EventStream.Subscribe(func(evt interface{}) { + // switch msg := evt.(type) { + // + // //subscribe to Cluster Topology changes + // case *cluster.ClusterTopology: + // fmt.Printf("\nClusterTopology %v\n\n", msg) + // + // //subscribe to Gossip updates, specifically MemberHeartbeat + // case *cluster.GossipUpdate: + // if msg.Key != "heartbeat" { + // return + // } + // + // heartbeat := &cluster.MemberHeartbeat{} + // + // fmt.Printf("Member %v\n", msg.MemberID) + // fmt.Printf("Sequence Number %v\n", msg.SeqNumber) + // + // unpackErr := msg.Value.UnmarshalTo(heartbeat) + // if unpackErr != nil { + // fmt.Printf("Unpack error %v\n", unpackErr) + // } else { + // //loop over as.ActorCount map + // for k, v := range heartbeat.ActorStatistics.ActorCount { + // fmt.Printf("ActorCount %v %v\n", k, v) + // } + // } + // } + //}) provider, _ := consul.New() lookup := disthash.New() diff --git a/examples/cluster-gossip/shared/protos.pb.go b/examples/cluster-gossip/shared/protos.pb.go index aa960849..ddbf307c 100644 --- a/examples/cluster-gossip/shared/protos.pb.go +++ b/examples/cluster-gossip/shared/protos.pb.go @@ -105,6 +105,53 @@ func (*HelloResponse) Descriptor() ([]byte, []int) { return file_protos_proto_rawDescGZIP(), []int{1} } +type StringValue struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *StringValue) Reset() { + *x = StringValue{} + if protoimpl.UnsafeEnabled { + mi := &file_protos_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StringValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringValue) ProtoMessage() {} + +func (x *StringValue) ProtoReflect() protoreflect.Message { + mi := &file_protos_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StringValue.ProtoReflect.Descriptor instead. +func (*StringValue) Descriptor() ([]byte, []int) { + return file_protos_proto_rawDescGZIP(), []int{2} +} + +func (x *StringValue) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + var File_protos_proto protoreflect.FileDescriptor var file_protos_proto_rawDesc = []byte{ @@ -112,12 +159,14 @@ var file_protos_proto_rawDesc = []byte{ 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x48, 0x65, - 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x42, 0x5a, 0x40, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x73, 0x79, 0x6e, 0x6b, 0x72, - 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x2d, 0x67, 0x6f, - 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, - 0x72, 0x2d, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x23, 0x0a, 0x0b, 0x53, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, + 0x73, 0x79, 0x6e, 0x6b, 0x72, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61, 0x63, 0x74, + 0x6f, 0x72, 0x2d, 0x67, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x63, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x2d, 0x67, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x2f, 0x73, 0x68, + 0x61, 0x72, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -132,10 +181,11 @@ func file_protos_proto_rawDescGZIP() []byte { return file_protos_proto_rawDescData } -var file_protos_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_protos_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_protos_proto_goTypes = []interface{}{ (*HelloRequest)(nil), // 0: shared.HelloRequest (*HelloResponse)(nil), // 1: shared.HelloResponse + (*StringValue)(nil), // 2: shared.StringValue } var file_protos_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -175,6 +225,18 @@ func file_protos_proto_init() { return nil } } + file_protos_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StringValue); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -182,7 +244,7 @@ func file_protos_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_protos_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/examples/cluster-gossip/shared/protos.proto b/examples/cluster-gossip/shared/protos.proto index ef6dceff..ee32c4f1 100644 --- a/examples/cluster-gossip/shared/protos.proto +++ b/examples/cluster-gossip/shared/protos.proto @@ -8,4 +8,8 @@ message HelloRequest { message HelloResponse { +} + +message StringValue { + string value = 1; } \ No newline at end of file