From 52dd4dd9a9c38e8b358b2d3998316d9728864500 Mon Sep 17 00:00:00 2001 From: Marcel Lanz Date: Fri, 27 Sep 2019 15:59:14 +0200 Subject: [PATCH 01/10] [doc] Go is called Go, GoLang is used where the term "go" is ambiguous (https://golang.org/doc/faq#go_or_golang) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 60c9249..7606479 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # go-support -GoLang support for CloudState +Go support for CloudState \ No newline at end of file From a69533c8a7824f16e9fab37d63db91dd7fc0f92d Mon Sep 17 00:00:00 2001 From: Marcel Lanz Date: Sat, 28 Sep 2019 22:18:17 +0200 Subject: [PATCH 02/10] [doc] Go is called Go, GoLang is used where the term "go" is ambiguous (https://golang.org/doc/faq#go_or_golang) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0bd8abc..3f882d3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # go-support -GoLang support for [CloudState](https://github.com/cloudstateio/cloudstate) +Go support for [CloudState](https://github.com/cloudstateio/cloudstate) From cb840719a7435c439be8c3a9435087a404fc7438 Mon Sep 17 00:00:00 2001 From: Marcel Lanz Date: Sat, 28 Sep 2019 22:52:35 +0200 Subject: [PATCH 03/10] [initial] moved over from https://github.com/marcellanz/cloudstate/tree/feature/go-support on c94dd10 --- .travis.yml | 7 + README.md | 7 +- cloudstate/cloudstate.go | 250 ++++++ cloudstate/cloudstate_test.go | 96 +++ cloudstate/doc.go | 17 + cloudstate/entity_key.pb.go | 51 ++ cloudstate/event.go | 95 +++ cloudstate/event_test.go | 84 ++ cloudstate/eventsourced.go | 441 +++++++++++ cloudstate/eventsourced_reply.go | 56 ++ cloudstate/eventsourced_test.go | 340 ++++++++ cloudstate/protocol/entity.pb.go | 979 ++++++++++++++++++++++++ cloudstate/protocol/event_sourced.pb.go | 624 +++++++++++++++ cloudstate/protocol/function.pb.go | 489 ++++++++++++ cloudstate/protosupport.go | 44 ++ cloudstate/types.go | 20 + go.mod | 9 + go.sum | 27 + 18 files changed, 3634 insertions(+), 2 deletions(-) create mode 100644 .travis.yml create mode 100644 cloudstate/cloudstate.go create mode 100644 cloudstate/cloudstate_test.go create mode 100644 cloudstate/doc.go create mode 100644 cloudstate/entity_key.pb.go create mode 100644 cloudstate/event.go create mode 100644 cloudstate/event_test.go create mode 100644 cloudstate/eventsourced.go create mode 100644 cloudstate/eventsourced_reply.go create mode 100644 cloudstate/eventsourced_test.go create mode 100644 cloudstate/protocol/entity.pb.go create mode 100644 cloudstate/protocol/event_sourced.pb.go create mode 100644 cloudstate/protocol/function.pb.go create mode 100644 cloudstate/protosupport.go create mode 100644 cloudstate/types.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..effc654 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: go + +go: + - "1.13" + +script: + - go test -v ./... \ No newline at end of file diff --git a/README.md b/README.md index 3f882d3..cadc75d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ -# go-support -Go support for [CloudState](https://github.com/cloudstateio/cloudstate) +# CloudState stateful service support in Go + +This package provides support for writing [CloudState](https://github.com/cloudstateio/cloudstate) stateful functions in Go. + +For more information see https://cloudstate.io. \ No newline at end of file diff --git a/cloudstate/cloudstate.go b/cloudstate/cloudstate.go new file mode 100644 index 0000000..dadf61d --- /dev/null +++ b/cloudstate/cloudstate.go @@ -0,0 +1,250 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstate + +import ( + "context" + "errors" + "fmt" + "github.com/cloudstateio/go-support/cloudstate/protocol" + "github.com/golang/protobuf/descriptor" + "github.com/golang/protobuf/proto" + filedescr "github.com/golang/protobuf/protoc-gen-go/descriptor" + "github.com/golang/protobuf/ptypes/empty" + "google.golang.org/grpc" + "log" + "net" + "os" + "runtime" +) + +const ( + SupportLibraryVersion = "0.4.4" + SupportLibraryName = "cloudstate-go-support" +) + +// CloudState is an instance of a CloudState User Function +type CloudState struct { + server *grpc.Server + entityDiscoveryResponder *EntityDiscoveryResponder + eventSourcedHandler *EventSourcedHandler +} + +// NewCloudState returns a new CloudState instance. +func NewCloudState(options *Options) *CloudState { + cs := &CloudState{ + server: grpc.NewServer(), + entityDiscoveryResponder: NewEntityDiscoveryResponder(options), + eventSourcedHandler: NewEventSourcedHandler(), + } + protocol.RegisterEntityDiscoveryServer(cs.server, cs.entityDiscoveryResponder) + log.Println("RegisterEntityDiscoveryServer") + protocol.RegisterEventSourcedServer(cs.server, cs.eventSourcedHandler) + log.Println("RegisterEventSourcedServer") + return cs +} + +// Options go get a CloudState instance configured. +type Options struct { + ServiceName string + ServiceVersion string +} + +var NoOptions = Options{} + +// DescriptorConfig configures service and dependent descriptors. +type DescriptorConfig struct { + Service string + ServiceMsg descriptor.Message + Domain []string + DomainMessages []descriptor.Message +} + +func (dc DescriptorConfig) AddDomainMessage(m descriptor.Message) DescriptorConfig { + dc.DomainMessages = append(dc.DomainMessages, m) + return dc +} + +func (dc DescriptorConfig) AddDomainDescriptor(filename string) DescriptorConfig { + dc.Domain = append(dc.Domain, filename) + return dc +} + +// Register registers an event sourced entity for CloudState. +func (cs *CloudState) Register(ese *EventSourcedEntity, config DescriptorConfig) (err error) { + ese.once.Do(func() { + if err = ese.initZeroValue(); err != nil { + return + } + if err = cs.eventSourcedHandler.registerEntity(ese); err != nil { + return + } + if err = cs.entityDiscoveryResponder.registerEntity(ese, config); err != nil { + return + } + }) + return +} + +// Run runs the CloudState instance. +func (cs *CloudState) Run() error { + host, ok := os.LookupEnv("HOST") + if !ok { + return fmt.Errorf("unable to get environment variable \"HOST\"") + } + port, ok := os.LookupEnv("PORT") + if !ok { + return fmt.Errorf("unable to get environment variable \"PORT\"") + } + lis, err := net.Listen("tcp", fmt.Sprintf("%s:%s", host, port)) + if err != nil { + return fmt.Errorf("failed to listen: %v\n", err) + } + log.Printf("starting grpcServer at: %s:%s", host, port) + if e := cs.server.Serve(lis); e != nil { + return fmt.Errorf("failed to grpcServer.Serve for: %v", lis) + } + return nil +} + +// EntityDiscoveryResponder implements the CloudState discovery protocol. +type EntityDiscoveryResponder struct { + fileDescriptorSet *filedescr.FileDescriptorSet + entitySpec *protocol.EntitySpec + message *descriptor.Message +} + +// NewEntityDiscoveryResponder returns a new and initialized EntityDiscoveryResponder. +func NewEntityDiscoveryResponder(options *Options) *EntityDiscoveryResponder { + responder := &EntityDiscoveryResponder{} + responder.entitySpec = &protocol.EntitySpec{ + Entities: make([]*protocol.Entity, 0), + ServiceInfo: &protocol.ServiceInfo{ + ServiceName: options.ServiceName, + ServiceVersion: options.ServiceVersion, + ServiceRuntime: fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH), + SupportLibraryName: SupportLibraryName, + SupportLibraryVersion: SupportLibraryVersion, + }, + } + responder.fileDescriptorSet = &filedescr.FileDescriptorSet{ + File: make([]*filedescr.FileDescriptorProto, 0), + } + return responder +} + +// Discover returns an entity spec for +func (r *EntityDiscoveryResponder) Discover(c context.Context, pi *protocol.ProxyInfo) (*protocol.EntitySpec, error) { + log.Printf("Received discovery call from sidecar [%s w%s] supporting CloudState %v.%v\n", + pi.ProxyName, + pi.ProxyVersion, + pi.ProtocolMajorVersion, + pi.ProtocolMinorVersion, + ) + for _, filename := range []string{ + "google/protobuf/empty.proto", + "google/protobuf/any.proto", + "google/protobuf/descriptor.proto", + "google/api/annotations.proto", + "google/api/http.proto", + "cloudstate/event_sourced.proto", + "cloudstate/entity.proto", + "cloudstate/entity_key.proto", + } { + if err := r.registerFileDescriptorProto(filename); err != nil { + return nil, err + } + } + log.Printf("Responding with: %v\n", r.entitySpec.GetServiceInfo()) + return r.entitySpec, nil +} + +// ReportError logs +func (r *EntityDiscoveryResponder) ReportError(c context.Context, fe *protocol.UserFunctionError) (*empty.Empty, error) { + log.Printf("ReportError: %v\n", fe) + return &empty.Empty{}, nil +} + +func (r *EntityDiscoveryResponder) updateSpec() (err error) { + protoBytes, err := proto.Marshal(r.fileDescriptorSet) + if err != nil { + return errors.New("unable to Marshal FileDescriptorSet") + } + r.entitySpec.Proto = protoBytes + return nil +} + +func (r *EntityDiscoveryResponder) resolveFileDescriptors(dc DescriptorConfig) error { + // service + if dc.Service != "" { + if err := r.registerFileDescriptorProto(dc.Service); err != nil { + return err + } + } else { + if dc.ServiceMsg != nil { + if err := r.registerFileDescriptor(dc.ServiceMsg); err != nil { + return err + } + } + } + // and dependent domain descriptors + for _, dp := range dc.Domain { + if err := r.registerFileDescriptorProto(dp); err != nil { + return err + } + } + for _, dm := range dc.DomainMessages { + if err := r.registerFileDescriptor(dm); err != nil { + return err + } + } + return nil +} + +func (r *EntityDiscoveryResponder) registerEntity(e *EventSourcedEntity, config DescriptorConfig) error { + if err := r.resolveFileDescriptors(config); err != nil { + return fmt.Errorf("failed to resolveFileDescriptor for DescriptorConfig: %+v: %w", config, err) + } + persistenceID := e.entityName + if e.PersistenceID != "" { + persistenceID = e.PersistenceID + } + r.entitySpec.Entities = append(r.entitySpec.Entities, &protocol.Entity{ + EntityType: EventSourced, + ServiceName: e.ServiceName, + PersistenceId: persistenceID, + }) + // TODO: e.SnapshotEvery + return r.updateSpec() +} + +func (r *EntityDiscoveryResponder) registerFileDescriptorProto(filename string) error { + descriptorProto, err := unpackFile(proto.FileDescriptor(filename)) + if err != nil { + return fmt.Errorf("failed to registerFileDescriptorProto for filename: %s: %w", filename, err) + } + r.fileDescriptorSet.File = append(r.fileDescriptorSet.File, descriptorProto) + return r.updateSpec() +} + +func (r *EntityDiscoveryResponder) registerFileDescriptor(msg descriptor.Message) error { + fd, _ := descriptor.ForMessage(msg) // this can panic + if r := recover(); r != nil { + return fmt.Errorf("descriptor.ForMessage panicked (%v) for: %+v", r, msg) + } + r.fileDescriptorSet.File = append(r.fileDescriptorSet.File, fd) + return nil +} diff --git a/cloudstate/cloudstate_test.go b/cloudstate/cloudstate_test.go new file mode 100644 index 0000000..607ea32 --- /dev/null +++ b/cloudstate/cloudstate_test.go @@ -0,0 +1,96 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package cloudstate implements the CloudState event sourced and entity discovery protocol. +package cloudstate + +import ( + "bytes" + "context" + "github.com/cloudstateio/go-support/cloudstate/protocol" + _ "google.golang.org/genproto/googleapis/api/annotations" + "log" + "os" + "strings" + "testing" +) + +func TestNewCloudState(t *testing.T) { + cloudState := NewCloudState(&Options{}) + si := cloudState.server.GetServiceInfo() + if si == nil { + t.Fail() + } +} + +func TestEntityDiscoveryResponderDiscover(t *testing.T) { + responder := NewEntityDiscoveryResponder(&Options{ + ServiceName: "service.one", + ServiceVersion: "0.0.1", + }) + info := &protocol.ProxyInfo{ + ProtocolMajorVersion: 0, + ProtocolMinorVersion: 0, + ProxyName: "test-proxy", + ProxyVersion: "9.8.7", + SupportedEntityTypes: nil, + } + spec, err := responder.Discover(context.Background(), info) + if err != nil { + t.Errorf("responder.Discover returned err: %v", err) + } + if spec == nil { + t.Errorf("no EntitySpec returned by responder.Discover") + } + + if spec.ServiceInfo.ServiceName != "service.one" { + t.Errorf("spec.ServiceInfo.ServiceName != 'service.one' but is: %s", spec.ServiceInfo.ServiceName) + } + if spec.ServiceInfo.ServiceVersion != "0.0.1" { + t.Errorf("spec.ServiceInfo.ServiceVersion != '0.0.1' but is: %s", spec.ServiceInfo.ServiceVersion) + } + if !strings.HasPrefix(spec.ServiceInfo.ServiceRuntime, "go") { + t.Errorf("spec.ServiceInfo.ServiceRuntime does not start with prefix: go") + } + if spec.ServiceInfo.SupportLibraryName != "cloudstate-go-support" { + t.Errorf("spec.ServiceInfo.SupportLibraryName != 'cloudstate-go-support'") + } +} + +func captureOutput(f func()) string { + var buf bytes.Buffer + log.SetOutput(&buf) + defer log.SetOutput(os.Stderr) + f() + return buf.String() +} + +func TestEntityDiscoveryResponderReportError(t *testing.T) { + responder := NewEntityDiscoveryResponder(&Options{ + ServiceName: "service.one", + ServiceVersion: "0.0.1", + }) + output := captureOutput(func() { + empty, err := responder.ReportError(context.Background(), &protocol.UserFunctionError{ + Message: "unable to do XYZ", + }) + if err != nil || empty == nil { + t.Errorf("responder.ReportError failed with err: %v, empty: %v", err, empty) + } + }) + if !strings.Contains(output, "unable to do XYZ") { + t.Errorf("'unable to do XYZ' not found in output: %s", output) + } +} diff --git a/cloudstate/doc.go b/cloudstate/doc.go new file mode 100644 index 0000000..fe79b31 --- /dev/null +++ b/cloudstate/doc.go @@ -0,0 +1,17 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package cloudstate implements the CloudState event sourced and entity discovery protocol. +package cloudstate diff --git a/cloudstate/entity_key.pb.go b/cloudstate/entity_key.pb.go new file mode 100644 index 0000000..c9bbd22 --- /dev/null +++ b/cloudstate/entity_key.pb.go @@ -0,0 +1,51 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: cloudstate/entity_key.proto + +package cloudstate + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +var E_EntityKey = &proto.ExtensionDesc{ + ExtendedType: (*descriptor.FieldOptions)(nil), + ExtensionType: (*bool)(nil), + Field: 50002, + Name: "cloudstate.entity_key", + Tag: "varint,50002,opt,name=entity_key", + Filename: "cloudstate/entity_key.proto", +} + +func init() { + proto.RegisterExtension(E_EntityKey) +} + +func init() { proto.RegisterFile("cloudstate/entity_key.proto", fileDescriptor_7bcabc3af9eb79b9) } + +var fileDescriptor_7bcabc3af9eb79b9 = []byte{ + // 160 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4e, 0xce, 0xc9, 0x2f, + 0x4d, 0x29, 0x2e, 0x49, 0x2c, 0x49, 0xd5, 0x4f, 0xcd, 0x2b, 0xc9, 0x2c, 0xa9, 0x8c, 0xcf, 0x4e, + 0xad, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x48, 0x4a, 0x29, 0xa4, 0xe7, 0xe7, + 0xa7, 0xe7, 0xa4, 0xea, 0x83, 0x65, 0x92, 0x4a, 0xd3, 0xf4, 0x53, 0x52, 0x8b, 0x93, 0x8b, 0x32, + 0x0b, 0x4a, 0xf2, 0x8b, 0x20, 0xaa, 0xad, 0xec, 0xb8, 0xb8, 0x10, 0x26, 0x08, 0xc9, 0xea, 0x41, + 0x34, 0xe8, 0xc1, 0x34, 0xe8, 0xb9, 0x65, 0xa6, 0xe6, 0xa4, 0xf8, 0x17, 0x94, 0x64, 0xe6, 0xe7, + 0x15, 0x4b, 0x5c, 0x6a, 0x63, 0x56, 0x60, 0xd4, 0xe0, 0x08, 0xe2, 0x84, 0x68, 0xf1, 0x4e, 0xad, + 0x74, 0x32, 0xe6, 0xe2, 0xcd, 0xcc, 0xd7, 0x43, 0x58, 0x19, 0xa5, 0x84, 0x60, 0xeb, 0x65, 0xe6, + 0xeb, 0xa7, 0xe7, 0x17, 0x97, 0x16, 0x14, 0xe4, 0x17, 0x95, 0xe8, 0x23, 0xc4, 0x93, 0xd8, 0xc0, + 0xc6, 0x1b, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x40, 0xb5, 0x41, 0x6c, 0xc8, 0x00, 0x00, 0x00, +} diff --git a/cloudstate/event.go b/cloudstate/event.go new file mode 100644 index 0000000..eaa599b --- /dev/null +++ b/cloudstate/event.go @@ -0,0 +1,95 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstate + +import "fmt" + +type OnNext func(event interface{}) error +type OnErr func(err error) +type Subscription struct { + OnNext + OnErr + active bool +} + +func (s *Subscription) Unsubscribe() { + s.active = false +} + +type EventEmitter interface { + Emit(event interface{}) + Subscribe(subs *Subscription) *Subscription + Events() []interface{} + Clear() +} + +func NewEmitter() *eventEmitter { + return &eventEmitter{ + events: make([]interface{}, 0), + subscriptions: make([]*Subscription, 0), + } +} + +type eventEmitter struct { + events []interface{} + subscriptions []*Subscription +} + +// Emit will immediately invoke the associated event handler for that event - +// this both validates that the event can be applied to the current state, as well as +// updates the state so that subsequent processing in the command handler can use it. +func (e *eventEmitter) Emit(event interface{}) { + for _, subs := range e.subscriptions { + if !subs.active { + continue + } + err := subs.OnNext(event) + if r := recover(); r != nil { + subs.OnErr(fmt.Errorf("panicked with: %v", r)) + continue + } + if err != nil && subs.OnErr != nil { + subs.OnErr(err) + } + } + e.events = append(e.events, event) +} + +func (e *eventEmitter) Events() []interface{} { + return e.events +} + +func (e *eventEmitter) Subscribe(subs *Subscription) *Subscription { + subs.active = true + e.subscriptions = append(e.subscriptions, subs) + return subs +} + +func (e *eventEmitter) Clear() { + e.events = make([]interface{}, 0) +} + +type EventHandler interface { + HandleEvent(event interface{}) (handled bool, err error) +} + +type Snapshotter interface { + Snapshot() (snapshot interface{}, err error) +} + +type SnapshotHandler interface { + HandleSnapshot(snapshot interface{}) (handled bool, err error) +} diff --git a/cloudstate/event_test.go b/cloudstate/event_test.go new file mode 100644 index 0000000..310c24d --- /dev/null +++ b/cloudstate/event_test.go @@ -0,0 +1,84 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstate + +import ( + "fmt" + "sync/atomic" + "testing" +) + +type AnEntity struct { + EventEmitter +} + +func TestMultipleSubscribers(t *testing.T) { + e := AnEntity{EventEmitter: NewEmitter()} + n := int64(0) + e.Subscribe(&Subscription{ + OnNext: func(event interface{}) error { + atomic.AddInt64(&n, 1) + return nil + }, + }) + e.Emit(1) + if n != 1 { + t.Fail() + } + e.Emit(1) + if n != 2 { + t.Fail() + } + e.Subscribe(&Subscription{ + OnNext: func(event interface{}) error { + atomic.AddInt64(&n, 1) + return nil + }, + }) + e.Emit(1) + if n != 4 { + t.Fail() + } +} + +func TestEventEmitter(t *testing.T) { + e := AnEntity{EventEmitter: NewEmitter()} + s := make([]string, 0) + ee := fmt.Errorf("int types are no supported") + e.Subscribe(&Subscription{ + OnNext: func(event interface{}) error { + switch v := event.(type) { + case int: + return ee + case string: + s = append(s, v) + } + return nil + }, + OnErr: func(err error) { + if err != ee { + t.Errorf("received unexpected error: %v", err) + } + }, + }) + // emit something that triggers an error we'd expect to happen + e.Emit(1) + // emit a string that gets to the list + e.Emit("john") + if len(s) != 1 || s[0] != "john" { + t.Errorf("john was not in the list: %+v", s) + } +} diff --git a/cloudstate/eventsourced.go b/cloudstate/eventsourced.go new file mode 100644 index 0000000..18e684e --- /dev/null +++ b/cloudstate/eventsourced.go @@ -0,0 +1,441 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstate + +import ( + "errors" + "fmt" + "github.com/cloudstateio/go-support/cloudstate/protocol" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes/any" + "io" + "log" + "reflect" + "strings" + "sync" +) + +// Entity +type Entity interface { + EntityInitializer +} + +// An EntityInitializer knows how to initialize an Entity +type EntityInitializer interface { + New() interface{} +} + +type EntityInstance struct { + EventSourcedEntity *EventSourcedEntity + Instance interface{} +} + +type EventSourcedEntity struct { + // Entity is a nil or Zero-Initialized reference + // to the entity to be event sourced. It has to + // implement the EntityInitializer interface + // so that CloudState can create new entity instances. + Entity Entity + // ServiceName is used to… + // Setting it is optional. + ServiceName string + // PersistenceID is used to namespace events in the journal, useful for + // when you share the same database between multiple entities. It defaults to + // the simple name for the entity type. + // It’s good practice to select one explicitly, this means your database + // isn’t depend on type names in your code. + // Setting it is optional. + PersistenceID string + // The snapshotEvery parameter controls how often snapshots are taken, + // so that the entity doesn't need to be recovered from the whole journal + // each time it’s loaded. If left unset, it defaults to 100. + // Setting it to a negative number will result in snapshots never being taken. + SnapshotEvery int + + // internal + entityName string + once sync.Once +} + +// initZeroValue get its Entity type and Zero-Value it to +// something we can use as an initializer. +func (e *EventSourcedEntity) initZeroValue() error { + if reflect.ValueOf(e.Entity).IsNil() { + t := reflect.TypeOf(e.Entity) + if t.Kind() == reflect.Ptr { // TODO: how deep can that go? + t = t.Elem() + } + value := reflect.New(t).Interface() + if ei, ok := value.(EntityInitializer); ok { + e.Entity = ei + } else { + return errors.New("the Entity does not implement EntityInitializer") + } + e.entityName = t.Name() + } + return nil +} + +// A EntityInstanceContext represents a event sourced entity together with its +// associated service. +// Commands are dispatched through this context. +type EntityInstanceContext struct { + EntityInstance EntityInstance +} + +// ServiceName returns the contexts service name. +func (ec *EntityInstanceContext) ServiceName() string { + return ec.EntityInstance.EventSourcedEntity.ServiceName +} + +// EventSourcedHandler is the implementation of the EventSourcedHandler server API for EventSourced service. +type EventSourcedHandler struct { + // entities are indexed by their service name + entities map[string]*EventSourcedEntity + // entity instance contexts for all + // event sourced entities indexed by their entity ids + contexts map[string]*EntityInstanceContext + // method cache + methodCache map[string]reflect.Method +} + +// NewEventSourcedHandler returns an initialized EventSourcedHandler +func NewEventSourcedHandler() *EventSourcedHandler { + return &EventSourcedHandler{ + entities: make(map[string]*EventSourcedEntity), + contexts: make(map[string]*EntityInstanceContext), + methodCache: make(map[string]reflect.Method), + } +} + +func (esh *EventSourcedHandler) registerEntity(ese *EventSourcedEntity) error { + esh.entities[ese.ServiceName] = ese + return nil +} + +// see EventSourcedServer.Handle +func (esh *EventSourcedHandler) Handle(server protocol.EventSourced_HandleServer) error { + for { + msg, err := server.Recv() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + if cmd := msg.GetCommand(); cmd != nil { + if err := esh.handleCommand(cmd, server); err != nil { + if errors.Is(err, ErrSendFailure) { + } + return err + } + } + if event := msg.GetEvent(); event != nil { + log.Fatalf("event handling is not implemented yet") + } + if init := msg.GetInit(); init != nil { + if err := esh.handleInit(init, server); err != nil { // TODO: unwrap the error and see if its a server.Send error + return err + } + } + } +} + +func (esh *EventSourcedHandler) handleInit(init *protocol.EventSourcedInit, server protocol.EventSourced_HandleServer) error { + eid := init.GetEntityId() + if _, present := esh.contexts[eid]; present { + if err := server.Send(&protocol.EventSourcedStreamOut{ + Message: &protocol.EventSourcedStreamOut_Failure{ + Failure: &protocol.Failure{ + Description: "entity already initialized", + }}}); err != nil { + return fmt.Errorf("unable to server.Send, %w", err) + } + return nil + } + entity := esh.entities[init.GetServiceName()] + if initializer, ok := entity.Entity.(EntityInitializer); ok { + instance := initializer.New() + esh.contexts[eid] = &EntityInstanceContext{ + EntityInstance: EntityInstance{ + Instance: instance, + EventSourcedEntity: entity, + }, + } + } else { + return fmt.Errorf("unable to handle init entity.Entity does not implement EntityInitializer") + } + esh.subscribeEvents(esh.contexts[eid].EntityInstance.Instance) + return nil +} + +func (esh *EventSourcedHandler) subscribeEvents(inst interface{}) { + if emitter, ok := inst.(EventEmitter); ok { + emitter.Subscribe(&Subscription{ + OnNext: func(event interface{}) error { + anyEvent, err := esh.marshalEvent(event) + if err != nil { + return err + } + return esh.handleEvents(reflect.ValueOf(inst), anyEvent) + }, + OnErr: func(err error) {}, // TODO: investigate what to report to the proxy + }) + } +} + +// handleCommand handles a command received from the CloudState proxy. +// +// TODO: remove these following lines of comment +// "Unary RPCs where the client sends a single request to the server and +// gets a single response back, just like a normal function call." are supported right now. +// +// to handle a command we need +// - the entity id, which identifies the entity (its instance) uniquely(?) for this user function instance +// - the service name, like "com.example.shoppingcart.ShoppingCart" +// - a command id +// - a command name, which is one of the gRPC service rpcs defined by this entities service +// - the command payload, which is the message sent for the command as a protobuf.Any blob +// - a streamed flag, (TODO: for what?) +// +// together, these properties allow to call a method of the entities registered service and +// return its response as a reply to the CloudState proxy. +// +// Events: +// Beside calling the service method, we have to collect "events" the service might emit. +// These events afterwards have to be handled by a EventHandler to update the state of the +// entity. The CloudState proxy can re-play these events at any time +// TODO: split it up +func (esh *EventSourcedHandler) handleCommand(cmd *protocol.Command, server protocol.EventSourced_HandleServer) error { + entityContext := esh.contexts[cmd.GetEntityId()] + entity := esh.entities[entityContext.ServiceName()] + entityValue := reflect.ValueOf(entityContext.EntityInstance.Instance) + + cacheKey := entityContext.ServiceName() + cmd.Name + method, hit := esh.methodCache[cacheKey] + // as measured this cache saves us about 75% of a call + // to be prepared with 4.4µs vs. 17.6µs where a typical + // call by reflection like GetCart() with Func.Call() + // takes ~10µs and to get return values processed somewhere 0.7µs. + if !hit { + // entities implement the proxied grpc service + // we try to find the method we're called by name with the + // received command. + methodByName := entityValue.MethodByName(cmd.Name) + if !methodByName.IsValid() { + return fmt.Errorf("no method named: %s found for: %v", cmd.Name, entity) + } + // gRPC services are unary rpc methods, always. + // They have one message in and one message out. + if methodByName.Type().NumIn() != 2 { + // this should never happen + failure := &protocol.Failure{ + Description: fmt.Sprintf("non-unary method %s of entity: %v found", methodByName.String(), entityValue.String()), + } + if err := sendFailure(failure, server); err != nil { + return ErrSendFailure + } + } + // The first argument in the gRPC implementation + // is always a context.Context. + methodArg0Type := methodByName.Type().In(0) + if !reflect.TypeOf(server.Context()).Implements(methodArg0Type) { + return fmt.Errorf( + "first argument for method: %s is not of type: %s", + methodByName.String(), reflect.TypeOf(server.Context()).Name(), + ) + } + method, _ = reflect.TypeOf(entityContext.EntityInstance.Instance).MethodByName(cmd.Name) + esh.methodCache[cacheKey] = method + } + + // build the input arguments for the method we're about to call + inputs := make([]reflect.Value, method.Type.NumIn()) + inputs[0] = entityValue + inputs[1] = reflect.ValueOf(server.Context()) + + // create a zero-value for the type of the + // message we call the method with + arg1 := method.Type.In(2) + ptr := false + for arg1.Kind() == reflect.Ptr { + ptr = true + arg1 = arg1.Elem() + } + var msg proto.Message + if ptr { + msg = reflect.New(arg1).Interface().(proto.Message) + } else { + msg = reflect.Zero(arg1).Interface().(proto.Message) + } + if err := proto.Unmarshal(cmd.GetPayload().GetValue(), msg); err != nil { + return fmt.Errorf("failed to unmarshal: %w", err) + } + + inputs[2] = reflect.ValueOf(msg) + // call it + called := method.Func.Call(inputs) + // The gRPC implementation returns the rpc return method + // and an error as a second return value. + errReturned := called[1] + if errReturned.CanInterface() && errReturned.Interface() != nil && errReturned.Type().Name() == "error" { // FIXME: looks ugly + // TCK says: FIXME Expects entity.Failure, but gets lientAction.Action.Failure(Failure(commandId, msg))) + failure := &protocol.Failure{ + CommandId: cmd.GetId(), + Description: errReturned.Interface().(error).Error(), + } + if err := sendClientActionFailure(failure, server); err != nil { + return ErrSendFailure + } + return nil + } + // the reply + callReply, ok := called[0].Interface().(proto.Message) + if !ok { + // this should never happen + return fmt.Errorf("called return value at index 0 is no proto.Message") + } + typeUrl := fmt.Sprintf("%s/%s", protoAnyBase, proto.MessageName(callReply)) + marshal, err := proto.Marshal(callReply) + if err != nil { + return fmt.Errorf("unable to Marshal command reply: %w", ErrMarshal) + } + + // emitted events + events, err := esh.marshalEventsTo(entityValue) + if err != nil { + return err + } + err = sendEventSourcedReply(&protocol.EventSourcedReply{ + CommandId: cmd.GetId(), + ClientAction: &protocol.ClientAction{ + Action: &protocol.ClientAction_Reply{ + Reply: &protocol.Reply{ + Payload: &any.Any{ + TypeUrl: typeUrl, + Value: marshal, + }, + }, + }, + }, + Events: events, + }, server) + if err != nil { + return fmt.Errorf("%s, %w", err, ErrSend) + } + return nil +} + +func (_ *EventSourcedHandler) marshalEvent(evt interface{}) (*any.Any, error) { + // TODO: protobufs are expected here, but CloudState supports other formats + message, ok := evt.(proto.Message) + if !ok { + return nil, fmt.Errorf("got a non-proto message as event") + } + marshal, err := proto.Marshal(message) + if err != nil { + return nil, fmt.Errorf("%s, %w", err, ErrMarshal) + } + return &any.Any{ + TypeUrl: fmt.Sprintf("%s/%s", protoAnyBase, proto.MessageName(message)), + Value: marshal, + }, nil +} + +// marshalEventsTo receives the events emitted through the handling of a command +// and marshals them to the event serialized form. +func (_ *EventSourcedHandler) marshalEventsTo(entityValue reflect.Value) ([]*any.Any, error) { + events := make([]*any.Any, 0) + if emitter, ok := entityValue.Interface().(EventEmitter); ok { + for _, evt := range emitter.Events() { + // TODO: protobufs are expected here, but CloudState supports other formats + message, ok := evt.(proto.Message) + if !ok { + return nil, fmt.Errorf("got a non-proto message as event") + } + marshal, err := proto.Marshal(message) + if err != nil { + return nil, fmt.Errorf("%s, %w", err, ErrMarshal) + } + events = append(events, + &any.Any{ + TypeUrl: fmt.Sprintf("%s/%s", protoAnyBase, proto.MessageName(message)), + Value: marshal, + }, + ) + } + emitter.Clear() + } + return events, nil +} + +// handleEvents handles a list of events encoded as protobuf Any messages. +// +// Event sourced entities persist events and snapshots, and these need to be +// serialized when persisted. The most straight forward way to persist events +// and snapshots is to use protobufs. CloudState will automatically detect if +// an emitted event is a protobuf, and serialize it as such. For other +// serialization options, including JSON, see Serialization. +func (_ *EventSourcedHandler) handleEvents(entityValue reflect.Value, events ...*any.Any) error { + eventHandler, implementsEventHandler := entityValue.Interface().(EventHandler) + for _, event := range events { + // TODO: here's the point where events can be protobufs, serialized as json or other formats + msgName := strings.TrimPrefix(event.GetTypeUrl(), protoAnyBase+"/") + messageType := proto.MessageType(msgName) + if messageType.Kind() == reflect.Ptr { + // get a zero-ed message of this type + if message, ok := reflect.New(messageType.Elem()).Interface().(proto.Message); ok { + // and marshal onto it what we got as an any.Any onto it + err := proto.Unmarshal(event.Value, message) + if err != nil { + return fmt.Errorf("%s, %w", err, ErrMarshal) + } else { + // we're ready to handle the proto message + // and we might have a handler + handled := false + if implementsEventHandler { + handled, err = eventHandler.HandleEvent(message) + if err != nil { + return err + } + } + // if not, we try to find one + // currently we support a method that has one argument that equals + // to the type of the message received. + if !handled { + // find a concrete handling method + entityType := entityValue.Type() + for tmi := 0; tmi < entityType.NumMethod(); tmi++ { + method := entityType.Method(tmi) + // we expect one argument for now, the domain message + // the first argument is the receiver itself + if method.Func.Type().NumIn() == 2 { + argumentType := method.Func.Type().In(1) + if argumentType.AssignableTo(messageType) { + entityValue.MethodByName(method.Name).Call([]reflect.Value{reflect.ValueOf(message)}) + } + } else { + // we have not found a one-argument method maching the + // TODO: what to do here? we might support more variations of possible handlers we can detect + } + } + } + } + } + } // TODO: what do we do if we haven't handled the events? + } + return nil +} diff --git a/cloudstate/eventsourced_reply.go b/cloudstate/eventsourced_reply.go new file mode 100644 index 0000000..d1053c8 --- /dev/null +++ b/cloudstate/eventsourced_reply.go @@ -0,0 +1,56 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstate + +import ( + "errors" + "github.com/cloudstateio/go-support/cloudstate/protocol" +) + +var ErrSendFailure = errors.New("unable to send a failure message") +var ErrSend = errors.New("unable to send a message") +var ErrMarshal = errors.New("unable to marshal a message") + +func sendEventSourcedReply(reply *protocol.EventSourcedReply, server protocol.EventSourced_HandleServer) error { + return server.Send(&protocol.EventSourcedStreamOut{ + Message: &protocol.EventSourcedStreamOut_Reply{ + Reply: reply, + }, + }) +} + +func sendFailure(failure *protocol.Failure, server protocol.EventSourced_HandleServer) error { + return server.Send(&protocol.EventSourcedStreamOut{ + Message: &protocol.EventSourcedStreamOut_Failure{ + Failure: failure, + }, + }) +} + +func sendClientActionFailure(failure *protocol.Failure, server protocol.EventSourced_HandleServer) error { + return server.Send(&protocol.EventSourcedStreamOut{ + Message: &protocol.EventSourcedStreamOut_Reply{ + Reply: &protocol.EventSourcedReply{ + CommandId: failure.CommandId, + ClientAction: &protocol.ClientAction{ + Action: &protocol.ClientAction_Failure{ + Failure: failure, + }, + }, + }, + }, + }) +} diff --git a/cloudstate/eventsourced_test.go b/cloudstate/eventsourced_test.go new file mode 100644 index 0000000..bbd36fc --- /dev/null +++ b/cloudstate/eventsourced_test.go @@ -0,0 +1,340 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstate + +import ( + "context" + "errors" + "fmt" + "github.com/cloudstateio/go-support/cloudstate/protocol" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes/any" + "github.com/golang/protobuf/ptypes/empty" + "google.golang.org/grpc" + "os" + "sync" + "testing" +) + +type TestEntity struct { + Value int64 + EventEmitter +} + +func (te *TestEntity) IncrementBy(n int64) (int64, error) { + te.Value += n + return te.Value, nil +} + +func (te *TestEntity) DecrementBy(n int64) (int64, error) { + te.Value -= n + return te.Value, nil +} + +// initialize value to <0 let us check whether an initCommand works +var testEntity = &TestEntity{ + Value: -1, + EventEmitter: NewEmitter(), +} + +func resetTestEntity() { + testEntity = &TestEntity{ + Value: -1, + EventEmitter: NewEmitter(), + } +} + +func (te *TestEntity) New() interface{} { + testEntity.Value = 0 + return testEntity +} + +func (te *TestEntity) IncrementByCommand(c context.Context, ibc *IncrementByCommand) (*empty.Empty, error) { + te.Emit(&IncrementByEvent{ + Value: ibc.Amount, + }) + return &empty.Empty{}, nil +} + +func (te *TestEntity) DecrementByCommand(c context.Context, ibc *DecrementByCommand) (*empty.Empty, error) { + te.Emit(&DecrementByEvent{ + Value: ibc.Amount, + }) + return &empty.Empty{}, nil +} + +type IncrementByEvent struct { + Value int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` +} + +func (inc IncrementByEvent) String() string { + return proto.CompactTextString(inc) +} + +func (inc IncrementByEvent) ProtoMessage() { +} + +func (inc IncrementByEvent) Reset() { +} + +type DecrementByEvent struct { + Value int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` +} + +func (inc DecrementByEvent) String() string { + return proto.CompactTextString(inc) +} + +func (inc DecrementByEvent) ProtoMessage() { +} + +func (inc DecrementByEvent) Reset() { +} + +func (te *TestEntity) DecrementByEvent(d *DecrementByEvent) error { + _, err := te.DecrementBy(d.Value) + return err +} + +func (te *TestEntity) HandleEvent(event interface{}) (handled bool, err error) { + switch e := event.(type) { + case *IncrementByEvent: + _, err := te.IncrementBy(e.Value) + return true, err + default: + return false, nil + } +} + +type IncrementByCommand struct { + Amount int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` +} + +func (inc IncrementByCommand) String() string { + return proto.CompactTextString(inc) +} + +func (inc IncrementByCommand) ProtoMessage() { +} + +func (inc IncrementByCommand) Reset() { +} + +type DecrementByCommand struct { + Amount int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` +} + +func (inc DecrementByCommand) String() string { + return proto.CompactTextString(inc) +} + +func (inc DecrementByCommand) ProtoMessage() { +} + +func (inc DecrementByCommand) Reset() { +} + +// TestEventSourcedHandleServer is a grpc.ServerStream mock +type TestEventSourcedHandleServer struct { + grpc.ServerStream +} + +func (t TestEventSourcedHandleServer) Context() context.Context { + return context.Background() +} + +func (t TestEventSourcedHandleServer) Send(out *protocol.EventSourcedStreamOut) error { + return nil +} +func (t TestEventSourcedHandleServer) Recv() (*protocol.EventSourcedStreamIn, error) { + return nil, nil +} + +func newHandler(t *testing.T) *EventSourcedHandler { + handler := NewEventSourcedHandler() + entity := EventSourcedEntity{ + Entity: (*TestEntity)(nil), + ServiceName: "TestEventSourcedHandler-Service", + SnapshotEvery: 0, + once: sync.Once{}, + } + err := entity.initZeroValue() + if err != nil { + t.Errorf("%v", err) + } + err = handler.registerEntity(&entity) + if err != nil { + t.Errorf("%v", err) + } + return handler +} + +func initHandler(handler *EventSourcedHandler, t *testing.T) { + err := handler.handleInit(&protocol.EventSourcedInit{ + ServiceName: "TestEventSourcedHandler-Service", + EntityId: "entity-0", + }, nil) + if err != nil { + t.Errorf("%v", err) + t.Fail() + } +} + +func marshal(msg proto.Message, t *testing.T) ([]byte, error) { + cmd, err := proto.Marshal(msg) + if err != nil { + t.Errorf("%v", err) + } + return cmd, err +} + +func TestMain(m *testing.M) { + proto.RegisterType((*IncrementByEvent)(nil), "IncrementByEvent") + proto.RegisterType((*DecrementByEvent)(nil), "DecrementByEvent") + resetTestEntity() + defer resetTestEntity() + os.Exit(m.Run()) +} + +func TestErrSend(t *testing.T) { + err0 := ErrSendFailure + err1 := fmt.Errorf("on reply: %w", ErrSendFailure) + if !errors.Is(err1, err0) { + t.Fatalf("err1 is no err0 but should") + } +} + +func TestEventSourcedHandlerHandlesCommandAndEvents(t *testing.T) { + handler := newHandler(t) + if testEntity.Value >= 0 { + t.Errorf("testEntity.Value should be <0 but was not: %+v", testEntity) + } + initHandler(handler, t) + if testEntity.Value != 0 { + t.Errorf("testEntity.Value should be 0 but was not: %+v", testEntity) + } + incrementedTo := int64(7) + incrCmdValue, err := marshal(&IncrementByCommand{Amount: incrementedTo}, t) + incrCommand := protocol.Command{ + EntityId: "entity-0", + Id: 1, + Name: "IncrementByCommand", + Payload: &any.Any{ + TypeUrl: "type.googleapis.com/IncrementByCommand", + Value: incrCmdValue, + }, + } + err = handler.handleCommand(&incrCommand, TestEventSourcedHandleServer{}) + if err != nil { + t.Errorf("%v", err) + } + if testEntity.Value != incrementedTo { + t.Errorf("testEntity.Value != incrementedTo") + } + + decrCmdValue, err := proto.Marshal(&DecrementByCommand{Amount: incrementedTo}) + if err != nil { + t.Errorf("%v", err) + } + decrCommand := protocol.Command{ + EntityId: "entity-0", + Id: 1, + Name: "DecrementByCommand", + Payload: &any.Any{ + TypeUrl: "type.googleapis.com/DecrementByCommand", + Value: decrCmdValue, + }, + } + err = handler.handleCommand(&decrCommand, TestEventSourcedHandleServer{}) + if err != nil { + t.Errorf("%v", err) + } + if testEntity.Value != 0 { + t.Errorf("testEntity.Value != 0") + } +} + +//type IncrementTwiceByCommand struct { +// Amount int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` +//} +// +//func (inc IncrementTwiceByCommand) String() string { +// return proto.CompactTextString(inc) +//} +// +//func (inc IncrementTwiceByCommand) ProtoMessage() { +//} +// +//func (inc IncrementTwiceByCommand) Reset() { +//} +// +//type IncrementTwiceByEvent struct { +// Value int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` +//} +// +//func (inc IncrementTwiceByEvent) String() string { +// return proto.CompactTextString(inc) +//} +// +//func (inc IncrementTwiceByEvent) ProtoMessage() { +//} +// +//func (inc IncrementTwiceByEvent) Reset() { +//} +// +//func TestEventSourcedHandlerEventsAreAppliedForEveryEmit(t *testing.T) { +// handler := newHandler(t) +// initHandler(handler, t) +// incrementedTo := int64(7) +// cmd, err := marshal(&IncrementTwiceByCommand{Amount: incrementedTo}, t) +// incrCommand := protocol.Command{ +// EntityId: "entity-0", +// Id: 1, +// Name: "IncrementTwiceByCommand", +// Payload: &any.Any{ +// TypeUrl: "type.googleapis.com/IncrementTwiceByCommand", +// Value: cmd, +// }, +// } +// err = handler.handleCommand(&incrCommand, TestEventSourcedHandleServer{}) +// if err != nil { +// t.Errorf("%v", err) +// } +// if testEntity.Value != incrementedTo { +// t.Errorf("testEntity.Value != incrementedTo") +// } +// +// decrCmdValue, err := proto.Marshal(&DecrementByCommand{Amount: incrementedTo}) +// if err != nil { +// t.Errorf("%v", err) +// } +// decrCommand := protocol.Command{ +// EntityId: "entity-0", +// Id: 1, +// Name: "DecrementByCommand", +// Payload: &any.Any{ +// TypeUrl: "type.googleapis.com/DecrementByCommand", +// Value: decrCmdValue, +// }, +// } +// err = handler.handleCommand(&decrCommand, TestEventSourcedHandleServer{}) +// if err != nil { +// t.Errorf("%v", err) +// } +// if testEntity.Value != 0 { +// t.Errorf("testEntity.Value != incrementedTo") +// } +//} diff --git a/cloudstate/protocol/entity.pb.go b/cloudstate/protocol/entity.pb.go new file mode 100644 index 0000000..0e4297f --- /dev/null +++ b/cloudstate/protocol/entity.pb.go @@ -0,0 +1,979 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: cloudstate/entity.proto + +package protocol + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + _ "github.com/golang/protobuf/protoc-gen-go/descriptor" + any "github.com/golang/protobuf/ptypes/any" + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +// A reply to the sender. +type Reply struct { + // The reply payload + Payload *any.Any `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Reply) Reset() { *m = Reply{} } +func (m *Reply) String() string { return proto.CompactTextString(m) } +func (*Reply) ProtoMessage() {} +func (*Reply) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{0} +} + +func (m *Reply) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Reply.Unmarshal(m, b) +} +func (m *Reply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Reply.Marshal(b, m, deterministic) +} +func (m *Reply) XXX_Merge(src proto.Message) { + xxx_messageInfo_Reply.Merge(m, src) +} +func (m *Reply) XXX_Size() int { + return xxx_messageInfo_Reply.Size(m) +} +func (m *Reply) XXX_DiscardUnknown() { + xxx_messageInfo_Reply.DiscardUnknown(m) +} + +var xxx_messageInfo_Reply proto.InternalMessageInfo + +func (m *Reply) GetPayload() *any.Any { + if m != nil { + return m.Payload + } + return nil +} + +// Forwards handling of this request to another entity. +type Forward struct { + // The name of the service to forward to. + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + // The name of the command. + CommandName string `protobuf:"bytes,2,opt,name=command_name,json=commandName,proto3" json:"command_name,omitempty"` + // The payload. + Payload *any.Any `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Forward) Reset() { *m = Forward{} } +func (m *Forward) String() string { return proto.CompactTextString(m) } +func (*Forward) ProtoMessage() {} +func (*Forward) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{1} +} + +func (m *Forward) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Forward.Unmarshal(m, b) +} +func (m *Forward) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Forward.Marshal(b, m, deterministic) +} +func (m *Forward) XXX_Merge(src proto.Message) { + xxx_messageInfo_Forward.Merge(m, src) +} +func (m *Forward) XXX_Size() int { + return xxx_messageInfo_Forward.Size(m) +} +func (m *Forward) XXX_DiscardUnknown() { + xxx_messageInfo_Forward.DiscardUnknown(m) +} + +var xxx_messageInfo_Forward proto.InternalMessageInfo + +func (m *Forward) GetServiceName() string { + if m != nil { + return m.ServiceName + } + return "" +} + +func (m *Forward) GetCommandName() string { + if m != nil { + return m.CommandName + } + return "" +} + +func (m *Forward) GetPayload() *any.Any { + if m != nil { + return m.Payload + } + return nil +} + +// An action for the client +type ClientAction struct { + // Types that are valid to be assigned to Action: + // *ClientAction_Reply + // *ClientAction_Forward + // *ClientAction_Failure + Action isClientAction_Action `protobuf_oneof:"action"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ClientAction) Reset() { *m = ClientAction{} } +func (m *ClientAction) String() string { return proto.CompactTextString(m) } +func (*ClientAction) ProtoMessage() {} +func (*ClientAction) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{2} +} + +func (m *ClientAction) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ClientAction.Unmarshal(m, b) +} +func (m *ClientAction) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ClientAction.Marshal(b, m, deterministic) +} +func (m *ClientAction) XXX_Merge(src proto.Message) { + xxx_messageInfo_ClientAction.Merge(m, src) +} +func (m *ClientAction) XXX_Size() int { + return xxx_messageInfo_ClientAction.Size(m) +} +func (m *ClientAction) XXX_DiscardUnknown() { + xxx_messageInfo_ClientAction.DiscardUnknown(m) +} + +var xxx_messageInfo_ClientAction proto.InternalMessageInfo + +type isClientAction_Action interface { + isClientAction_Action() +} + +type ClientAction_Reply struct { + Reply *Reply `protobuf:"bytes,1,opt,name=reply,proto3,oneof"` +} + +type ClientAction_Forward struct { + Forward *Forward `protobuf:"bytes,2,opt,name=forward,proto3,oneof"` +} + +type ClientAction_Failure struct { + Failure *Failure `protobuf:"bytes,3,opt,name=failure,proto3,oneof"` +} + +func (*ClientAction_Reply) isClientAction_Action() {} + +func (*ClientAction_Forward) isClientAction_Action() {} + +func (*ClientAction_Failure) isClientAction_Action() {} + +func (m *ClientAction) GetAction() isClientAction_Action { + if m != nil { + return m.Action + } + return nil +} + +func (m *ClientAction) GetReply() *Reply { + if x, ok := m.GetAction().(*ClientAction_Reply); ok { + return x.Reply + } + return nil +} + +func (m *ClientAction) GetForward() *Forward { + if x, ok := m.GetAction().(*ClientAction_Forward); ok { + return x.Forward + } + return nil +} + +func (m *ClientAction) GetFailure() *Failure { + if x, ok := m.GetAction().(*ClientAction_Failure); ok { + return x.Failure + } + return nil +} + +// XXX_OneofWrappers is for the internal use of the proto package. +func (*ClientAction) XXX_OneofWrappers() []interface{} { + return []interface{}{ + (*ClientAction_Reply)(nil), + (*ClientAction_Forward)(nil), + (*ClientAction_Failure)(nil), + } +} + +// A side effect to be done after this command is handled. +type SideEffect struct { + // The name of the service to perform the side effect on. + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + // The name of the command. + CommandName string `protobuf:"bytes,2,opt,name=command_name,json=commandName,proto3" json:"command_name,omitempty"` + // The payload of the command. + Payload *any.Any `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"` + // Whether this side effect should be performed synchronously, ie, before the reply is eventually + // sent, or not. + Synchronous bool `protobuf:"varint,4,opt,name=synchronous,proto3" json:"synchronous,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SideEffect) Reset() { *m = SideEffect{} } +func (m *SideEffect) String() string { return proto.CompactTextString(m) } +func (*SideEffect) ProtoMessage() {} +func (*SideEffect) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{3} +} + +func (m *SideEffect) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SideEffect.Unmarshal(m, b) +} +func (m *SideEffect) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SideEffect.Marshal(b, m, deterministic) +} +func (m *SideEffect) XXX_Merge(src proto.Message) { + xxx_messageInfo_SideEffect.Merge(m, src) +} +func (m *SideEffect) XXX_Size() int { + return xxx_messageInfo_SideEffect.Size(m) +} +func (m *SideEffect) XXX_DiscardUnknown() { + xxx_messageInfo_SideEffect.DiscardUnknown(m) +} + +var xxx_messageInfo_SideEffect proto.InternalMessageInfo + +func (m *SideEffect) GetServiceName() string { + if m != nil { + return m.ServiceName + } + return "" +} + +func (m *SideEffect) GetCommandName() string { + if m != nil { + return m.CommandName + } + return "" +} + +func (m *SideEffect) GetPayload() *any.Any { + if m != nil { + return m.Payload + } + return nil +} + +func (m *SideEffect) GetSynchronous() bool { + if m != nil { + return m.Synchronous + } + return false +} + +// A command. For each command received, a reply must be sent with a matching command id. +type Command struct { + // The ID of the entity. + EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + // A command id. + Id int64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` + // Command name + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // The command payload. + Payload *any.Any `protobuf:"bytes,4,opt,name=payload,proto3" json:"payload,omitempty"` + // Whether the command is streamed or not + Streamed bool `protobuf:"varint,5,opt,name=streamed,proto3" json:"streamed,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Command) Reset() { *m = Command{} } +func (m *Command) String() string { return proto.CompactTextString(m) } +func (*Command) ProtoMessage() {} +func (*Command) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{4} +} + +func (m *Command) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Command.Unmarshal(m, b) +} +func (m *Command) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Command.Marshal(b, m, deterministic) +} +func (m *Command) XXX_Merge(src proto.Message) { + xxx_messageInfo_Command.Merge(m, src) +} +func (m *Command) XXX_Size() int { + return xxx_messageInfo_Command.Size(m) +} +func (m *Command) XXX_DiscardUnknown() { + xxx_messageInfo_Command.DiscardUnknown(m) +} + +var xxx_messageInfo_Command proto.InternalMessageInfo + +func (m *Command) GetEntityId() string { + if m != nil { + return m.EntityId + } + return "" +} + +func (m *Command) GetId() int64 { + if m != nil { + return m.Id + } + return 0 +} + +func (m *Command) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Command) GetPayload() *any.Any { + if m != nil { + return m.Payload + } + return nil +} + +func (m *Command) GetStreamed() bool { + if m != nil { + return m.Streamed + } + return false +} + +type StreamCancelled struct { + // The ID of the entity + EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + // The command id + Id int64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *StreamCancelled) Reset() { *m = StreamCancelled{} } +func (m *StreamCancelled) String() string { return proto.CompactTextString(m) } +func (*StreamCancelled) ProtoMessage() {} +func (*StreamCancelled) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{5} +} + +func (m *StreamCancelled) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_StreamCancelled.Unmarshal(m, b) +} +func (m *StreamCancelled) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_StreamCancelled.Marshal(b, m, deterministic) +} +func (m *StreamCancelled) XXX_Merge(src proto.Message) { + xxx_messageInfo_StreamCancelled.Merge(m, src) +} +func (m *StreamCancelled) XXX_Size() int { + return xxx_messageInfo_StreamCancelled.Size(m) +} +func (m *StreamCancelled) XXX_DiscardUnknown() { + xxx_messageInfo_StreamCancelled.DiscardUnknown(m) +} + +var xxx_messageInfo_StreamCancelled proto.InternalMessageInfo + +func (m *StreamCancelled) GetEntityId() string { + if m != nil { + return m.EntityId + } + return "" +} + +func (m *StreamCancelled) GetId() int64 { + if m != nil { + return m.Id + } + return 0 +} + +// A failure reply. If this is returned, it will be translated into a gRPC unknown +// error with the corresponding description if supplied. +type Failure struct { + // The id of the command being replied to. Must match the input command. + CommandId int64 `protobuf:"varint,1,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"` + // A description of the error. + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Failure) Reset() { *m = Failure{} } +func (m *Failure) String() string { return proto.CompactTextString(m) } +func (*Failure) ProtoMessage() {} +func (*Failure) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{6} +} + +func (m *Failure) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Failure.Unmarshal(m, b) +} +func (m *Failure) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Failure.Marshal(b, m, deterministic) +} +func (m *Failure) XXX_Merge(src proto.Message) { + xxx_messageInfo_Failure.Merge(m, src) +} +func (m *Failure) XXX_Size() int { + return xxx_messageInfo_Failure.Size(m) +} +func (m *Failure) XXX_DiscardUnknown() { + xxx_messageInfo_Failure.DiscardUnknown(m) +} + +var xxx_messageInfo_Failure proto.InternalMessageInfo + +func (m *Failure) GetCommandId() int64 { + if m != nil { + return m.CommandId + } + return 0 +} + +func (m *Failure) GetDescription() string { + if m != nil { + return m.Description + } + return "" +} + +type EntitySpec struct { + // This should be the Descriptors.FileDescriptorSet in proto serialized from as generated by: + // protoc --include_imports \ + // --proto_path= \ + // --descriptor_set_out=user-function.desc \ + // + Proto []byte `protobuf:"bytes,1,opt,name=proto,proto3" json:"proto,omitempty"` + // The entities being served. + Entities []*Entity `protobuf:"bytes,2,rep,name=entities,proto3" json:"entities,omitempty"` + // Optional information about the service. + ServiceInfo *ServiceInfo `protobuf:"bytes,3,opt,name=service_info,json=serviceInfo,proto3" json:"service_info,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *EntitySpec) Reset() { *m = EntitySpec{} } +func (m *EntitySpec) String() string { return proto.CompactTextString(m) } +func (*EntitySpec) ProtoMessage() {} +func (*EntitySpec) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{7} +} + +func (m *EntitySpec) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_EntitySpec.Unmarshal(m, b) +} +func (m *EntitySpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_EntitySpec.Marshal(b, m, deterministic) +} +func (m *EntitySpec) XXX_Merge(src proto.Message) { + xxx_messageInfo_EntitySpec.Merge(m, src) +} +func (m *EntitySpec) XXX_Size() int { + return xxx_messageInfo_EntitySpec.Size(m) +} +func (m *EntitySpec) XXX_DiscardUnknown() { + xxx_messageInfo_EntitySpec.DiscardUnknown(m) +} + +var xxx_messageInfo_EntitySpec proto.InternalMessageInfo + +func (m *EntitySpec) GetProto() []byte { + if m != nil { + return m.Proto + } + return nil +} + +func (m *EntitySpec) GetEntities() []*Entity { + if m != nil { + return m.Entities + } + return nil +} + +func (m *EntitySpec) GetServiceInfo() *ServiceInfo { + if m != nil { + return m.ServiceInfo + } + return nil +} + +// Information about the service that proxy is proxying to. +// All of the information in here is optional. It may be useful for debug purposes. +type ServiceInfo struct { + // The name of the service, eg, "shopping-cart". + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + // The version of the service. + ServiceVersion string `protobuf:"bytes,2,opt,name=service_version,json=serviceVersion,proto3" json:"service_version,omitempty"` + // A description of the runtime for the service. Can be anything, but examples might be: + // - node v10.15.2 + // - OpenJDK Runtime Environment 1.8.0_192-b12 + ServiceRuntime string `protobuf:"bytes,3,opt,name=service_runtime,json=serviceRuntime,proto3" json:"service_runtime,omitempty"` + // If using a support library, the name of that library, eg "cloudstate" + SupportLibraryName string `protobuf:"bytes,4,opt,name=support_library_name,json=supportLibraryName,proto3" json:"support_library_name,omitempty"` + // The version of the support library being used. + SupportLibraryVersion string `protobuf:"bytes,5,opt,name=support_library_version,json=supportLibraryVersion,proto3" json:"support_library_version,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServiceInfo) Reset() { *m = ServiceInfo{} } +func (m *ServiceInfo) String() string { return proto.CompactTextString(m) } +func (*ServiceInfo) ProtoMessage() {} +func (*ServiceInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{8} +} + +func (m *ServiceInfo) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServiceInfo.Unmarshal(m, b) +} +func (m *ServiceInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServiceInfo.Marshal(b, m, deterministic) +} +func (m *ServiceInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServiceInfo.Merge(m, src) +} +func (m *ServiceInfo) XXX_Size() int { + return xxx_messageInfo_ServiceInfo.Size(m) +} +func (m *ServiceInfo) XXX_DiscardUnknown() { + xxx_messageInfo_ServiceInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_ServiceInfo proto.InternalMessageInfo + +func (m *ServiceInfo) GetServiceName() string { + if m != nil { + return m.ServiceName + } + return "" +} + +func (m *ServiceInfo) GetServiceVersion() string { + if m != nil { + return m.ServiceVersion + } + return "" +} + +func (m *ServiceInfo) GetServiceRuntime() string { + if m != nil { + return m.ServiceRuntime + } + return "" +} + +func (m *ServiceInfo) GetSupportLibraryName() string { + if m != nil { + return m.SupportLibraryName + } + return "" +} + +func (m *ServiceInfo) GetSupportLibraryVersion() string { + if m != nil { + return m.SupportLibraryVersion + } + return "" +} + +type Entity struct { + // The type of entity. By convention, this should be a fully qualified entity protocol grpc + // service name, for example, cloudstate.eventsourced.EventSourced. + EntityType string `protobuf:"bytes,1,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"` + // The name of the service to load from the protobuf file. + ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + // The ID to namespace state by. How this is used depends on the type of entity, for example, + // event sourced entities will prefix this to the persistence id. + PersistenceId string `protobuf:"bytes,3,opt,name=persistence_id,json=persistenceId,proto3" json:"persistence_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Entity) Reset() { *m = Entity{} } +func (m *Entity) String() string { return proto.CompactTextString(m) } +func (*Entity) ProtoMessage() {} +func (*Entity) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{9} +} + +func (m *Entity) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Entity.Unmarshal(m, b) +} +func (m *Entity) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Entity.Marshal(b, m, deterministic) +} +func (m *Entity) XXX_Merge(src proto.Message) { + xxx_messageInfo_Entity.Merge(m, src) +} +func (m *Entity) XXX_Size() int { + return xxx_messageInfo_Entity.Size(m) +} +func (m *Entity) XXX_DiscardUnknown() { + xxx_messageInfo_Entity.DiscardUnknown(m) +} + +var xxx_messageInfo_Entity proto.InternalMessageInfo + +func (m *Entity) GetEntityType() string { + if m != nil { + return m.EntityType + } + return "" +} + +func (m *Entity) GetServiceName() string { + if m != nil { + return m.ServiceName + } + return "" +} + +func (m *Entity) GetPersistenceId() string { + if m != nil { + return m.PersistenceId + } + return "" +} + +type UserFunctionError struct { + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *UserFunctionError) Reset() { *m = UserFunctionError{} } +func (m *UserFunctionError) String() string { return proto.CompactTextString(m) } +func (*UserFunctionError) ProtoMessage() {} +func (*UserFunctionError) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{10} +} + +func (m *UserFunctionError) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_UserFunctionError.Unmarshal(m, b) +} +func (m *UserFunctionError) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_UserFunctionError.Marshal(b, m, deterministic) +} +func (m *UserFunctionError) XXX_Merge(src proto.Message) { + xxx_messageInfo_UserFunctionError.Merge(m, src) +} +func (m *UserFunctionError) XXX_Size() int { + return xxx_messageInfo_UserFunctionError.Size(m) +} +func (m *UserFunctionError) XXX_DiscardUnknown() { + xxx_messageInfo_UserFunctionError.DiscardUnknown(m) +} + +var xxx_messageInfo_UserFunctionError proto.InternalMessageInfo + +func (m *UserFunctionError) GetMessage() string { + if m != nil { + return m.Message + } + return "" +} + +type ProxyInfo struct { + ProtocolMajorVersion int32 `protobuf:"varint,1,opt,name=protocol_major_version,json=protocolMajorVersion,proto3" json:"protocol_major_version,omitempty"` + ProtocolMinorVersion int32 `protobuf:"varint,2,opt,name=protocol_minor_version,json=protocolMinorVersion,proto3" json:"protocol_minor_version,omitempty"` + ProxyName string `protobuf:"bytes,3,opt,name=proxy_name,json=proxyName,proto3" json:"proxy_name,omitempty"` + ProxyVersion string `protobuf:"bytes,4,opt,name=proxy_version,json=proxyVersion,proto3" json:"proxy_version,omitempty"` + SupportedEntityTypes []string `protobuf:"bytes,5,rep,name=supported_entity_types,json=supportedEntityTypes,proto3" json:"supported_entity_types,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ProxyInfo) Reset() { *m = ProxyInfo{} } +func (m *ProxyInfo) String() string { return proto.CompactTextString(m) } +func (*ProxyInfo) ProtoMessage() {} +func (*ProxyInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_a4a280e6d8a8fec2, []int{11} +} + +func (m *ProxyInfo) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ProxyInfo.Unmarshal(m, b) +} +func (m *ProxyInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ProxyInfo.Marshal(b, m, deterministic) +} +func (m *ProxyInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_ProxyInfo.Merge(m, src) +} +func (m *ProxyInfo) XXX_Size() int { + return xxx_messageInfo_ProxyInfo.Size(m) +} +func (m *ProxyInfo) XXX_DiscardUnknown() { + xxx_messageInfo_ProxyInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_ProxyInfo proto.InternalMessageInfo + +func (m *ProxyInfo) GetProtocolMajorVersion() int32 { + if m != nil { + return m.ProtocolMajorVersion + } + return 0 +} + +func (m *ProxyInfo) GetProtocolMinorVersion() int32 { + if m != nil { + return m.ProtocolMinorVersion + } + return 0 +} + +func (m *ProxyInfo) GetProxyName() string { + if m != nil { + return m.ProxyName + } + return "" +} + +func (m *ProxyInfo) GetProxyVersion() string { + if m != nil { + return m.ProxyVersion + } + return "" +} + +func (m *ProxyInfo) GetSupportedEntityTypes() []string { + if m != nil { + return m.SupportedEntityTypes + } + return nil +} + +func init() { + proto.RegisterType((*Reply)(nil), "cloudstate.Reply") + proto.RegisterType((*Forward)(nil), "cloudstate.Forward") + proto.RegisterType((*ClientAction)(nil), "cloudstate.ClientAction") + proto.RegisterType((*SideEffect)(nil), "cloudstate.SideEffect") + proto.RegisterType((*Command)(nil), "cloudstate.Command") + proto.RegisterType((*StreamCancelled)(nil), "cloudstate.StreamCancelled") + proto.RegisterType((*Failure)(nil), "cloudstate.Failure") + proto.RegisterType((*EntitySpec)(nil), "cloudstate.EntitySpec") + proto.RegisterType((*ServiceInfo)(nil), "cloudstate.ServiceInfo") + proto.RegisterType((*Entity)(nil), "cloudstate.Entity") + proto.RegisterType((*UserFunctionError)(nil), "cloudstate.UserFunctionError") + proto.RegisterType((*ProxyInfo)(nil), "cloudstate.ProxyInfo") +} + +func init() { proto.RegisterFile("cloudstate/entity.proto", fileDescriptor_a4a280e6d8a8fec2) } + +var fileDescriptor_a4a280e6d8a8fec2 = []byte{ + // 795 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x55, 0xcd, 0x6e, 0xf3, 0x44, + 0x14, 0x8d, 0xf3, 0xd3, 0x24, 0xd7, 0x69, 0xab, 0x4e, 0xd3, 0x34, 0xa4, 0xaa, 0x08, 0x46, 0x88, + 0xb0, 0xa8, 0x83, 0x02, 0x02, 0x09, 0x24, 0xa4, 0xb6, 0xa4, 0x6a, 0x10, 0x20, 0xe4, 0x00, 0x0b, + 0x36, 0x91, 0x6b, 0x4f, 0xca, 0x20, 0x7b, 0xc6, 0x9a, 0x71, 0x0a, 0x5e, 0xf1, 0x06, 0x2c, 0xfb, + 0x04, 0xf0, 0x76, 0x6c, 0x78, 0x03, 0xe4, 0xf9, 0x71, 0xa6, 0xc9, 0xa6, 0xdf, 0xea, 0xdb, 0x79, + 0xee, 0x39, 0x67, 0xe6, 0xdc, 0x33, 0x77, 0x12, 0x38, 0x8f, 0x12, 0xb6, 0x89, 0x45, 0x1e, 0xe6, + 0x78, 0x8a, 0x69, 0x4e, 0xf2, 0xc2, 0xcf, 0x38, 0xcb, 0x19, 0x82, 0x2d, 0x30, 0x7a, 0xe7, 0x91, + 0xb1, 0xc7, 0x04, 0x4f, 0x25, 0xf2, 0xb0, 0x59, 0x4f, 0x43, 0xaa, 0x69, 0xa3, 0x8b, 0x5d, 0x08, + 0xa7, 0x99, 0xd9, 0x63, 0x34, 0xde, 0x05, 0x63, 0x2c, 0x22, 0x4e, 0xb2, 0x9c, 0x71, 0xc5, 0xf0, + 0x3e, 0x87, 0x56, 0x80, 0xb3, 0xa4, 0x40, 0x3e, 0xb4, 0xb3, 0xb0, 0x48, 0x58, 0x18, 0x0f, 0x9d, + 0xb1, 0x33, 0x71, 0x67, 0x7d, 0x5f, 0x89, 0x7d, 0x23, 0xf6, 0xaf, 0x69, 0x11, 0x18, 0x92, 0xf7, + 0x27, 0xb4, 0xef, 0x18, 0xff, 0x3d, 0xe4, 0x31, 0x7a, 0x0f, 0x7a, 0x02, 0xf3, 0x27, 0x12, 0xe1, + 0x15, 0x0d, 0x53, 0x2c, 0xf5, 0xdd, 0xc0, 0xd5, 0xb5, 0xef, 0xc3, 0x14, 0x97, 0x94, 0x88, 0xa5, + 0x69, 0x48, 0x63, 0x45, 0xa9, 0x2b, 0x8a, 0xae, 0x49, 0x8a, 0x65, 0xa0, 0xf1, 0x1a, 0x03, 0xff, + 0x38, 0xd0, 0xbb, 0x4d, 0x08, 0xa6, 0xf9, 0x75, 0x94, 0x13, 0x46, 0xd1, 0x47, 0xd0, 0xe2, 0x65, + 0x2b, 0xda, 0xff, 0x89, 0xbf, 0x0d, 0xd0, 0x97, 0x3d, 0xde, 0xd7, 0x02, 0xc5, 0x40, 0x53, 0x68, + 0xaf, 0x95, 0x79, 0xe9, 0xc4, 0x9d, 0x9d, 0xda, 0x64, 0xdd, 0xd7, 0x7d, 0x2d, 0x30, 0x2c, 0x29, + 0x08, 0x49, 0xb2, 0xe1, 0x58, 0x9b, 0x7b, 0x29, 0x50, 0x90, 0x14, 0xa8, 0xcf, 0x9b, 0x0e, 0x1c, + 0x84, 0xd2, 0x96, 0xf7, 0xb7, 0x03, 0xb0, 0x24, 0x31, 0x9e, 0xaf, 0xd7, 0x38, 0xca, 0xdf, 0x4e, + 0x58, 0x68, 0x0c, 0xae, 0x28, 0x68, 0xf4, 0x2b, 0x67, 0x94, 0x6d, 0xc4, 0xb0, 0x39, 0x76, 0x26, + 0x9d, 0xc0, 0x2e, 0x79, 0xcf, 0x0e, 0xb4, 0x6f, 0xd5, 0x09, 0xe8, 0x02, 0xba, 0x6a, 0x14, 0x57, + 0x24, 0xd6, 0x06, 0x3b, 0xaa, 0xb0, 0x88, 0xd1, 0x11, 0xd4, 0x89, 0x8a, 0xad, 0x11, 0xd4, 0x49, + 0x8c, 0x10, 0x34, 0xa5, 0xcb, 0x86, 0xe4, 0xc9, 0x6f, 0xdb, 0x5e, 0xf3, 0x35, 0xf6, 0x46, 0xd0, + 0x11, 0x39, 0xc7, 0x61, 0x8a, 0xe3, 0x61, 0x4b, 0x7a, 0xab, 0xd6, 0xde, 0x57, 0x70, 0xbc, 0x94, + 0xdf, 0xb7, 0x21, 0x8d, 0x70, 0x92, 0xe0, 0x37, 0xf3, 0xe7, 0x7d, 0x03, 0x6d, 0x7d, 0x3f, 0xe8, + 0x12, 0xc0, 0x04, 0xab, 0x85, 0x8d, 0xa0, 0xab, 0x2b, 0x0b, 0x19, 0x92, 0x79, 0x1f, 0x84, 0x51, + 0x13, 0xbb, 0x55, 0xf2, 0xfe, 0x72, 0x00, 0xe6, 0xf2, 0xa0, 0x65, 0x86, 0x23, 0xd4, 0x87, 0x96, + 0xec, 0x47, 0x6e, 0xd5, 0x0b, 0xd4, 0x02, 0xf9, 0xa0, 0xcc, 0x10, 0x2c, 0x86, 0xf5, 0x71, 0x63, + 0xe2, 0xce, 0x90, 0x3d, 0x2c, 0x4a, 0x1f, 0x54, 0x1c, 0xf4, 0xc5, 0x76, 0x22, 0x08, 0x5d, 0x33, + 0x7d, 0xa1, 0xe7, 0xb6, 0x66, 0xa9, 0xf0, 0x05, 0x5d, 0xb3, 0x6a, 0x54, 0xca, 0x85, 0xf7, 0xaf, + 0x03, 0xae, 0x05, 0xbe, 0x66, 0xba, 0x3e, 0x84, 0x63, 0x43, 0x79, 0xc2, 0x5c, 0x6c, 0x3b, 0x3d, + 0xd2, 0xe5, 0x9f, 0x55, 0xd5, 0x26, 0xf2, 0x0d, 0xcd, 0x49, 0x75, 0xc7, 0x86, 0x18, 0xa8, 0x2a, + 0xfa, 0x18, 0xfa, 0x62, 0x93, 0x65, 0x8c, 0xe7, 0xab, 0x84, 0x3c, 0xf0, 0x90, 0x17, 0xea, 0xf0, + 0xa6, 0x64, 0x23, 0x8d, 0x7d, 0xab, 0x20, 0xe9, 0xe1, 0x33, 0x38, 0xdf, 0x55, 0x18, 0x2f, 0x2d, + 0x29, 0x3a, 0x7b, 0x29, 0xd2, 0x96, 0x3c, 0x01, 0x07, 0x2a, 0x3e, 0xf4, 0x2e, 0xb8, 0x7a, 0x04, + 0xf2, 0x22, 0x33, 0x7d, 0x82, 0x2a, 0xfd, 0x58, 0x64, 0x78, 0x2f, 0x89, 0xfa, 0x7e, 0x12, 0x1f, + 0xc0, 0x51, 0x56, 0x6e, 0x2c, 0x72, 0x4c, 0xcb, 0xf0, 0x63, 0xdd, 0xdf, 0xa1, 0x55, 0x5d, 0xc4, + 0xde, 0x15, 0x9c, 0xfc, 0x24, 0x30, 0xbf, 0xdb, 0x50, 0xf9, 0xa0, 0xe7, 0x9c, 0x33, 0x8e, 0x86, + 0xd0, 0x4e, 0xb1, 0x10, 0xe1, 0xa3, 0x39, 0xdb, 0x2c, 0xbd, 0xff, 0x1c, 0xe8, 0xfe, 0xc0, 0xd9, + 0x1f, 0x85, 0xbc, 0x90, 0x4f, 0x61, 0x20, 0xa7, 0x22, 0x62, 0xc9, 0x2a, 0x0d, 0x7f, 0x63, 0xbc, + 0x6a, 0xb4, 0x94, 0xb5, 0x82, 0xbe, 0x41, 0xbf, 0x2b, 0x41, 0x13, 0xfd, 0x0b, 0x15, 0xa1, 0x96, + 0xaa, 0xbe, 0xa3, 0x2a, 0x41, 0xa3, 0xba, 0x04, 0xc8, 0xca, 0x83, 0x57, 0xd6, 0x7b, 0xec, 0xca, + 0x8a, 0x6c, 0xf7, 0x7d, 0x38, 0x54, 0xb0, 0xd9, 0x4b, 0xdd, 0x4f, 0x4f, 0x16, 0xad, 0x93, 0x75, + 0xf4, 0x38, 0x5e, 0x59, 0x09, 0x8b, 0x61, 0x6b, 0xdc, 0x98, 0x74, 0x83, 0x7e, 0x85, 0xce, 0xab, + 0xac, 0xc5, 0xec, 0xd9, 0x81, 0x63, 0xb5, 0xfe, 0x9a, 0x88, 0x88, 0x3d, 0x61, 0x5e, 0xa0, 0x2f, + 0xa1, 0x13, 0xeb, 0x05, 0x3a, 0xb3, 0x87, 0xb9, 0x0a, 0x67, 0x34, 0xd8, 0x7f, 0x17, 0xe5, 0xbb, + 0xf2, 0x6a, 0xe8, 0x0e, 0x5c, 0x8e, 0xcb, 0x73, 0x54, 0xda, 0x97, 0x36, 0x71, 0xef, 0x32, 0x46, + 0x83, 0xbd, 0x5f, 0x97, 0x79, 0xf9, 0x27, 0xe8, 0xd5, 0x6e, 0xae, 0x60, 0x40, 0x98, 0x2d, 0x36, + 0xc1, 0xfd, 0x72, 0x6a, 0xfd, 0xef, 0x9a, 0xe2, 0xc3, 0x81, 0xfc, 0xfa, 0xe4, 0xff, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xb9, 0x13, 0x29, 0x57, 0x95, 0x07, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// EntityDiscoveryClient is the client API for EntityDiscovery service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type EntityDiscoveryClient interface { + // Discover what entities the user function wishes to serve. + Discover(ctx context.Context, in *ProxyInfo, opts ...grpc.CallOption) (*EntitySpec, error) + // Report an error back to the user function. This will only be invoked to tell the user function + // that it has done something wrong, eg, violated the protocol, tried to use an entity type that + // isn't supported, or attempted to forward to an entity that doesn't exist, etc. These messages + // should be logged clearly for debugging purposes. + ReportError(ctx context.Context, in *UserFunctionError, opts ...grpc.CallOption) (*empty.Empty, error) +} + +type entityDiscoveryClient struct { + cc *grpc.ClientConn +} + +func NewEntityDiscoveryClient(cc *grpc.ClientConn) EntityDiscoveryClient { + return &entityDiscoveryClient{cc} +} + +func (c *entityDiscoveryClient) Discover(ctx context.Context, in *ProxyInfo, opts ...grpc.CallOption) (*EntitySpec, error) { + out := new(EntitySpec) + err := c.cc.Invoke(ctx, "/cloudstate.EntityDiscovery/discover", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *entityDiscoveryClient) ReportError(ctx context.Context, in *UserFunctionError, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/cloudstate.EntityDiscovery/reportError", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// EntityDiscoveryServer is the server API for EntityDiscovery service. +type EntityDiscoveryServer interface { + // Discover what entities the user function wishes to serve. + Discover(context.Context, *ProxyInfo) (*EntitySpec, error) + // Report an error back to the user function. This will only be invoked to tell the user function + // that it has done something wrong, eg, violated the protocol, tried to use an entity type that + // isn't supported, or attempted to forward to an entity that doesn't exist, etc. These messages + // should be logged clearly for debugging purposes. + ReportError(context.Context, *UserFunctionError) (*empty.Empty, error) +} + +// UnimplementedEntityDiscoveryServer can be embedded to have forward compatible implementations. +type UnimplementedEntityDiscoveryServer struct { +} + +func (*UnimplementedEntityDiscoveryServer) Discover(ctx context.Context, req *ProxyInfo) (*EntitySpec, error) { + return nil, status.Errorf(codes.Unimplemented, "method Discover not implemented") +} +func (*UnimplementedEntityDiscoveryServer) ReportError(ctx context.Context, req *UserFunctionError) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method ReportError not implemented") +} + +func RegisterEntityDiscoveryServer(s *grpc.Server, srv EntityDiscoveryServer) { + s.RegisterService(&_EntityDiscovery_serviceDesc, srv) +} + +func _EntityDiscovery_Discover_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProxyInfo) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EntityDiscoveryServer).Discover(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/cloudstate.EntityDiscovery/Discover", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EntityDiscoveryServer).Discover(ctx, req.(*ProxyInfo)) + } + return interceptor(ctx, in, info, handler) +} + +func _EntityDiscovery_ReportError_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserFunctionError) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EntityDiscoveryServer).ReportError(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/cloudstate.EntityDiscovery/ReportError", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EntityDiscoveryServer).ReportError(ctx, req.(*UserFunctionError)) + } + return interceptor(ctx, in, info, handler) +} + +var _EntityDiscovery_serviceDesc = grpc.ServiceDesc{ + ServiceName: "cloudstate.EntityDiscovery", + HandlerType: (*EntityDiscoveryServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "discover", + Handler: _EntityDiscovery_Discover_Handler, + }, + { + MethodName: "reportError", + Handler: _EntityDiscovery_ReportError_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "cloudstate/entity.proto", +} diff --git a/cloudstate/protocol/event_sourced.pb.go b/cloudstate/protocol/event_sourced.pb.go new file mode 100644 index 0000000..ebd90d6 --- /dev/null +++ b/cloudstate/protocol/event_sourced.pb.go @@ -0,0 +1,624 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: cloudstate/event_sourced.proto + +package protocol + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + any "github.com/golang/protobuf/ptypes/any" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +// The init message. This will always be the first message sent to the entity when +// it is loaded. +type EventSourcedInit struct { + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + // The ID of the entity. + EntityId string `protobuf:"bytes,2,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + // If present the entity should initialise its state using this snapshot. + Snapshot *EventSourcedSnapshot `protobuf:"bytes,3,opt,name=snapshot,proto3" json:"snapshot,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *EventSourcedInit) Reset() { *m = EventSourcedInit{} } +func (m *EventSourcedInit) String() string { return proto.CompactTextString(m) } +func (*EventSourcedInit) ProtoMessage() {} +func (*EventSourcedInit) Descriptor() ([]byte, []int) { + return fileDescriptor_e62d03156a02a758, []int{0} +} + +func (m *EventSourcedInit) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_EventSourcedInit.Unmarshal(m, b) +} +func (m *EventSourcedInit) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_EventSourcedInit.Marshal(b, m, deterministic) +} +func (m *EventSourcedInit) XXX_Merge(src proto.Message) { + xxx_messageInfo_EventSourcedInit.Merge(m, src) +} +func (m *EventSourcedInit) XXX_Size() int { + return xxx_messageInfo_EventSourcedInit.Size(m) +} +func (m *EventSourcedInit) XXX_DiscardUnknown() { + xxx_messageInfo_EventSourcedInit.DiscardUnknown(m) +} + +var xxx_messageInfo_EventSourcedInit proto.InternalMessageInfo + +func (m *EventSourcedInit) GetServiceName() string { + if m != nil { + return m.ServiceName + } + return "" +} + +func (m *EventSourcedInit) GetEntityId() string { + if m != nil { + return m.EntityId + } + return "" +} + +func (m *EventSourcedInit) GetSnapshot() *EventSourcedSnapshot { + if m != nil { + return m.Snapshot + } + return nil +} + +// A snapshot +type EventSourcedSnapshot struct { + // The sequence number when the snapshot was taken. + SnapshotSequence int64 `protobuf:"varint,1,opt,name=snapshot_sequence,json=snapshotSequence,proto3" json:"snapshot_sequence,omitempty"` + // The snapshot. + Snapshot *any.Any `protobuf:"bytes,2,opt,name=snapshot,proto3" json:"snapshot,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *EventSourcedSnapshot) Reset() { *m = EventSourcedSnapshot{} } +func (m *EventSourcedSnapshot) String() string { return proto.CompactTextString(m) } +func (*EventSourcedSnapshot) ProtoMessage() {} +func (*EventSourcedSnapshot) Descriptor() ([]byte, []int) { + return fileDescriptor_e62d03156a02a758, []int{1} +} + +func (m *EventSourcedSnapshot) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_EventSourcedSnapshot.Unmarshal(m, b) +} +func (m *EventSourcedSnapshot) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_EventSourcedSnapshot.Marshal(b, m, deterministic) +} +func (m *EventSourcedSnapshot) XXX_Merge(src proto.Message) { + xxx_messageInfo_EventSourcedSnapshot.Merge(m, src) +} +func (m *EventSourcedSnapshot) XXX_Size() int { + return xxx_messageInfo_EventSourcedSnapshot.Size(m) +} +func (m *EventSourcedSnapshot) XXX_DiscardUnknown() { + xxx_messageInfo_EventSourcedSnapshot.DiscardUnknown(m) +} + +var xxx_messageInfo_EventSourcedSnapshot proto.InternalMessageInfo + +func (m *EventSourcedSnapshot) GetSnapshotSequence() int64 { + if m != nil { + return m.SnapshotSequence + } + return 0 +} + +func (m *EventSourcedSnapshot) GetSnapshot() *any.Any { + if m != nil { + return m.Snapshot + } + return nil +} + +// An event. These will be sent to the entity when the entity starts up. +type EventSourcedEvent struct { + // The sequence number of the event. + Sequence int64 `protobuf:"varint,1,opt,name=sequence,proto3" json:"sequence,omitempty"` + // The event payload. + Payload *any.Any `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *EventSourcedEvent) Reset() { *m = EventSourcedEvent{} } +func (m *EventSourcedEvent) String() string { return proto.CompactTextString(m) } +func (*EventSourcedEvent) ProtoMessage() {} +func (*EventSourcedEvent) Descriptor() ([]byte, []int) { + return fileDescriptor_e62d03156a02a758, []int{2} +} + +func (m *EventSourcedEvent) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_EventSourcedEvent.Unmarshal(m, b) +} +func (m *EventSourcedEvent) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_EventSourcedEvent.Marshal(b, m, deterministic) +} +func (m *EventSourcedEvent) XXX_Merge(src proto.Message) { + xxx_messageInfo_EventSourcedEvent.Merge(m, src) +} +func (m *EventSourcedEvent) XXX_Size() int { + return xxx_messageInfo_EventSourcedEvent.Size(m) +} +func (m *EventSourcedEvent) XXX_DiscardUnknown() { + xxx_messageInfo_EventSourcedEvent.DiscardUnknown(m) +} + +var xxx_messageInfo_EventSourcedEvent proto.InternalMessageInfo + +func (m *EventSourcedEvent) GetSequence() int64 { + if m != nil { + return m.Sequence + } + return 0 +} + +func (m *EventSourcedEvent) GetPayload() *any.Any { + if m != nil { + return m.Payload + } + return nil +} + +// A reply to a command. +type EventSourcedReply struct { + // The id of the command being replied to. Must match the input command. + CommandId int64 `protobuf:"varint,1,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"` + // The action to take + ClientAction *ClientAction `protobuf:"bytes,2,opt,name=client_action,json=clientAction,proto3" json:"client_action,omitempty"` + // Any side effects to perform + SideEffects []*SideEffect `protobuf:"bytes,3,rep,name=side_effects,json=sideEffects,proto3" json:"side_effects,omitempty"` + // A list of events to persist - these will be persisted before the reply + // is sent. + Events []*any.Any `protobuf:"bytes,4,rep,name=events,proto3" json:"events,omitempty"` + // An optional snapshot to persist. It is assumed that this snapshot will have + // the state of any events in the events field applied to it. It is illegal to + // send a snapshot without sending any events. + Snapshot *any.Any `protobuf:"bytes,5,opt,name=snapshot,proto3" json:"snapshot,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *EventSourcedReply) Reset() { *m = EventSourcedReply{} } +func (m *EventSourcedReply) String() string { return proto.CompactTextString(m) } +func (*EventSourcedReply) ProtoMessage() {} +func (*EventSourcedReply) Descriptor() ([]byte, []int) { + return fileDescriptor_e62d03156a02a758, []int{3} +} + +func (m *EventSourcedReply) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_EventSourcedReply.Unmarshal(m, b) +} +func (m *EventSourcedReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_EventSourcedReply.Marshal(b, m, deterministic) +} +func (m *EventSourcedReply) XXX_Merge(src proto.Message) { + xxx_messageInfo_EventSourcedReply.Merge(m, src) +} +func (m *EventSourcedReply) XXX_Size() int { + return xxx_messageInfo_EventSourcedReply.Size(m) +} +func (m *EventSourcedReply) XXX_DiscardUnknown() { + xxx_messageInfo_EventSourcedReply.DiscardUnknown(m) +} + +var xxx_messageInfo_EventSourcedReply proto.InternalMessageInfo + +func (m *EventSourcedReply) GetCommandId() int64 { + if m != nil { + return m.CommandId + } + return 0 +} + +func (m *EventSourcedReply) GetClientAction() *ClientAction { + if m != nil { + return m.ClientAction + } + return nil +} + +func (m *EventSourcedReply) GetSideEffects() []*SideEffect { + if m != nil { + return m.SideEffects + } + return nil +} + +func (m *EventSourcedReply) GetEvents() []*any.Any { + if m != nil { + return m.Events + } + return nil +} + +func (m *EventSourcedReply) GetSnapshot() *any.Any { + if m != nil { + return m.Snapshot + } + return nil +} + +// Input message type for the gRPC stream in. +type EventSourcedStreamIn struct { + // Types that are valid to be assigned to Message: + // *EventSourcedStreamIn_Init + // *EventSourcedStreamIn_Event + // *EventSourcedStreamIn_Command + Message isEventSourcedStreamIn_Message `protobuf_oneof:"message"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *EventSourcedStreamIn) Reset() { *m = EventSourcedStreamIn{} } +func (m *EventSourcedStreamIn) String() string { return proto.CompactTextString(m) } +func (*EventSourcedStreamIn) ProtoMessage() {} +func (*EventSourcedStreamIn) Descriptor() ([]byte, []int) { + return fileDescriptor_e62d03156a02a758, []int{4} +} + +func (m *EventSourcedStreamIn) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_EventSourcedStreamIn.Unmarshal(m, b) +} +func (m *EventSourcedStreamIn) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_EventSourcedStreamIn.Marshal(b, m, deterministic) +} +func (m *EventSourcedStreamIn) XXX_Merge(src proto.Message) { + xxx_messageInfo_EventSourcedStreamIn.Merge(m, src) +} +func (m *EventSourcedStreamIn) XXX_Size() int { + return xxx_messageInfo_EventSourcedStreamIn.Size(m) +} +func (m *EventSourcedStreamIn) XXX_DiscardUnknown() { + xxx_messageInfo_EventSourcedStreamIn.DiscardUnknown(m) +} + +var xxx_messageInfo_EventSourcedStreamIn proto.InternalMessageInfo + +type isEventSourcedStreamIn_Message interface { + isEventSourcedStreamIn_Message() +} + +type EventSourcedStreamIn_Init struct { + Init *EventSourcedInit `protobuf:"bytes,1,opt,name=init,proto3,oneof"` +} + +type EventSourcedStreamIn_Event struct { + Event *EventSourcedEvent `protobuf:"bytes,2,opt,name=event,proto3,oneof"` +} + +type EventSourcedStreamIn_Command struct { + Command *Command `protobuf:"bytes,3,opt,name=command,proto3,oneof"` +} + +func (*EventSourcedStreamIn_Init) isEventSourcedStreamIn_Message() {} + +func (*EventSourcedStreamIn_Event) isEventSourcedStreamIn_Message() {} + +func (*EventSourcedStreamIn_Command) isEventSourcedStreamIn_Message() {} + +func (m *EventSourcedStreamIn) GetMessage() isEventSourcedStreamIn_Message { + if m != nil { + return m.Message + } + return nil +} + +func (m *EventSourcedStreamIn) GetInit() *EventSourcedInit { + if x, ok := m.GetMessage().(*EventSourcedStreamIn_Init); ok { + return x.Init + } + return nil +} + +func (m *EventSourcedStreamIn) GetEvent() *EventSourcedEvent { + if x, ok := m.GetMessage().(*EventSourcedStreamIn_Event); ok { + return x.Event + } + return nil +} + +func (m *EventSourcedStreamIn) GetCommand() *Command { + if x, ok := m.GetMessage().(*EventSourcedStreamIn_Command); ok { + return x.Command + } + return nil +} + +// XXX_OneofWrappers is for the internal use of the proto package. +func (*EventSourcedStreamIn) XXX_OneofWrappers() []interface{} { + return []interface{}{ + (*EventSourcedStreamIn_Init)(nil), + (*EventSourcedStreamIn_Event)(nil), + (*EventSourcedStreamIn_Command)(nil), + } +} + +// Output message type for the gRPC stream out. +type EventSourcedStreamOut struct { + // Types that are valid to be assigned to Message: + // *EventSourcedStreamOut_Reply + // *EventSourcedStreamOut_Failure + Message isEventSourcedStreamOut_Message `protobuf_oneof:"message"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *EventSourcedStreamOut) Reset() { *m = EventSourcedStreamOut{} } +func (m *EventSourcedStreamOut) String() string { return proto.CompactTextString(m) } +func (*EventSourcedStreamOut) ProtoMessage() {} +func (*EventSourcedStreamOut) Descriptor() ([]byte, []int) { + return fileDescriptor_e62d03156a02a758, []int{5} +} + +func (m *EventSourcedStreamOut) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_EventSourcedStreamOut.Unmarshal(m, b) +} +func (m *EventSourcedStreamOut) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_EventSourcedStreamOut.Marshal(b, m, deterministic) +} +func (m *EventSourcedStreamOut) XXX_Merge(src proto.Message) { + xxx_messageInfo_EventSourcedStreamOut.Merge(m, src) +} +func (m *EventSourcedStreamOut) XXX_Size() int { + return xxx_messageInfo_EventSourcedStreamOut.Size(m) +} +func (m *EventSourcedStreamOut) XXX_DiscardUnknown() { + xxx_messageInfo_EventSourcedStreamOut.DiscardUnknown(m) +} + +var xxx_messageInfo_EventSourcedStreamOut proto.InternalMessageInfo + +type isEventSourcedStreamOut_Message interface { + isEventSourcedStreamOut_Message() +} + +type EventSourcedStreamOut_Reply struct { + Reply *EventSourcedReply `protobuf:"bytes,1,opt,name=reply,proto3,oneof"` +} + +type EventSourcedStreamOut_Failure struct { + Failure *Failure `protobuf:"bytes,2,opt,name=failure,proto3,oneof"` +} + +func (*EventSourcedStreamOut_Reply) isEventSourcedStreamOut_Message() {} + +func (*EventSourcedStreamOut_Failure) isEventSourcedStreamOut_Message() {} + +func (m *EventSourcedStreamOut) GetMessage() isEventSourcedStreamOut_Message { + if m != nil { + return m.Message + } + return nil +} + +func (m *EventSourcedStreamOut) GetReply() *EventSourcedReply { + if x, ok := m.GetMessage().(*EventSourcedStreamOut_Reply); ok { + return x.Reply + } + return nil +} + +func (m *EventSourcedStreamOut) GetFailure() *Failure { + if x, ok := m.GetMessage().(*EventSourcedStreamOut_Failure); ok { + return x.Failure + } + return nil +} + +// XXX_OneofWrappers is for the internal use of the proto package. +func (*EventSourcedStreamOut) XXX_OneofWrappers() []interface{} { + return []interface{}{ + (*EventSourcedStreamOut_Reply)(nil), + (*EventSourcedStreamOut_Failure)(nil), + } +} + +func init() { + proto.RegisterType((*EventSourcedInit)(nil), "cloudstate.eventsourced.EventSourcedInit") + proto.RegisterType((*EventSourcedSnapshot)(nil), "cloudstate.eventsourced.EventSourcedSnapshot") + proto.RegisterType((*EventSourcedEvent)(nil), "cloudstate.eventsourced.EventSourcedEvent") + proto.RegisterType((*EventSourcedReply)(nil), "cloudstate.eventsourced.EventSourcedReply") + proto.RegisterType((*EventSourcedStreamIn)(nil), "cloudstate.eventsourced.EventSourcedStreamIn") + proto.RegisterType((*EventSourcedStreamOut)(nil), "cloudstate.eventsourced.EventSourcedStreamOut") +} + +func init() { proto.RegisterFile("cloudstate/event_sourced.proto", fileDescriptor_e62d03156a02a758) } + +var fileDescriptor_e62d03156a02a758 = []byte{ + // 549 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0xcd, 0x6e, 0xd3, 0x4c, + 0x14, 0x8d, 0x9b, 0x36, 0x3f, 0x37, 0xf9, 0xa4, 0x76, 0xda, 0xaf, 0x35, 0x41, 0xa0, 0x90, 0x55, + 0xf8, 0xa9, 0x53, 0x85, 0x15, 0x0b, 0x84, 0x1a, 0x54, 0x94, 0x6c, 0xa8, 0xe4, 0xec, 0xd8, 0x58, + 0x53, 0xfb, 0x26, 0x1d, 0xc9, 0x9e, 0x09, 0x9e, 0x71, 0xa5, 0x2c, 0x78, 0x03, 0xf6, 0xac, 0x78, + 0x2e, 0x5e, 0x07, 0x79, 0x66, 0x1c, 0x26, 0xa5, 0xa0, 0xb0, 0x1b, 0xcf, 0x39, 0xe7, 0xde, 0x33, + 0xe7, 0xde, 0x04, 0x9e, 0xc6, 0xa9, 0x28, 0x12, 0xa9, 0xa8, 0xc2, 0x11, 0xde, 0x21, 0x57, 0x91, + 0x14, 0x45, 0x1e, 0x63, 0x12, 0xac, 0x72, 0xa1, 0x04, 0x39, 0xfb, 0x85, 0x07, 0x1a, 0xb7, 0x70, + 0xef, 0xd1, 0x52, 0x88, 0x65, 0x8a, 0x23, 0x4d, 0xbb, 0x29, 0x16, 0x23, 0xca, 0xd7, 0x46, 0xd3, + 0x3b, 0x73, 0x6b, 0x72, 0xc5, 0x94, 0x05, 0x06, 0xdf, 0x3d, 0x38, 0xbc, 0x2a, 0x8b, 0xcc, 0x4d, + 0x91, 0x19, 0x67, 0x8a, 0x3c, 0x83, 0xae, 0xc4, 0xfc, 0x8e, 0xc5, 0x18, 0x71, 0x9a, 0xa1, 0xef, + 0xf5, 0xbd, 0x61, 0x3b, 0xec, 0xd8, 0xbb, 0x8f, 0x34, 0x43, 0xf2, 0x18, 0xda, 0xa6, 0x4e, 0xc4, + 0x12, 0x7f, 0x4f, 0xe3, 0x2d, 0x73, 0x31, 0x4b, 0xc8, 0x0c, 0x5a, 0x92, 0xd3, 0x95, 0xbc, 0x15, + 0xca, 0xaf, 0xf7, 0xbd, 0x61, 0x67, 0x7c, 0x1e, 0xfc, 0xc1, 0x74, 0xe0, 0x36, 0x9f, 0x5b, 0x51, + 0xb8, 0x91, 0x0f, 0x0a, 0x38, 0x79, 0x88, 0x41, 0x5e, 0xc2, 0x51, 0xc5, 0x89, 0x24, 0x7e, 0x2e, + 0x90, 0xc7, 0xc6, 0x67, 0x3d, 0x3c, 0xac, 0x80, 0xb9, 0xbd, 0x27, 0x17, 0x8e, 0x9f, 0x3d, 0xed, + 0xe7, 0x24, 0x30, 0x59, 0x05, 0x55, 0x56, 0xc1, 0x25, 0x5f, 0x3b, 0x6d, 0x23, 0x38, 0x72, 0xdb, + 0xea, 0x33, 0xe9, 0x41, 0xeb, 0x5e, 0xab, 0xcd, 0x37, 0x09, 0xa0, 0xb9, 0xa2, 0xeb, 0x54, 0xd0, + 0xe4, 0xaf, 0x1d, 0x2a, 0xd2, 0xe0, 0xeb, 0xde, 0x76, 0x87, 0x10, 0x57, 0xe9, 0x9a, 0x3c, 0x01, + 0x88, 0x45, 0x96, 0x51, 0x9e, 0x94, 0xb1, 0x9a, 0x1e, 0x6d, 0x7b, 0x33, 0x4b, 0xc8, 0x5b, 0xf8, + 0x2f, 0x4e, 0x59, 0xb9, 0x11, 0x34, 0x56, 0x4c, 0x70, 0xdb, 0xca, 0x77, 0xc3, 0x7d, 0xaf, 0x09, + 0x97, 0x1a, 0x0f, 0xbb, 0xb1, 0xf3, 0x45, 0xde, 0x40, 0x57, 0xb2, 0x04, 0x23, 0x5c, 0x2c, 0x30, + 0x56, 0xd2, 0xaf, 0xf7, 0xeb, 0xc3, 0xce, 0xf8, 0xd4, 0x55, 0xcf, 0x59, 0x82, 0x57, 0x1a, 0x0e, + 0x3b, 0x72, 0x73, 0x96, 0xe4, 0x15, 0x34, 0xcc, 0xd4, 0xfc, 0x7d, 0x2d, 0x7a, 0xf8, 0x75, 0x96, + 0xb3, 0x95, 0xf7, 0xc1, 0x4e, 0x79, 0xff, 0xf0, 0xee, 0xcd, 0x59, 0xe5, 0x48, 0xb3, 0x19, 0x27, + 0xef, 0x60, 0x9f, 0x71, 0xa6, 0x74, 0x16, 0x9d, 0xf1, 0xf3, 0x9d, 0xd6, 0xa8, 0xdc, 0xe1, 0x69, + 0x2d, 0xd4, 0x42, 0x32, 0x81, 0x03, 0x4d, 0xb4, 0x59, 0xbd, 0xd8, 0xa9, 0x82, 0x3e, 0x4f, 0x6b, + 0xa1, 0x91, 0x92, 0x11, 0x34, 0xed, 0x10, 0xec, 0x3a, 0x1f, 0x6f, 0x25, 0x6e, 0xa0, 0x69, 0x2d, + 0xac, 0x58, 0x93, 0x36, 0x34, 0x33, 0x94, 0x92, 0x2e, 0x71, 0xf0, 0xcd, 0x83, 0xff, 0x7f, 0x7f, + 0xd9, 0x75, 0xa1, 0x9d, 0xe5, 0xe5, 0xd4, 0xed, 0xdb, 0x76, 0x73, 0xa6, 0xf7, 0xa4, 0x74, 0xa6, + 0xa5, 0xa5, 0xb3, 0x05, 0x65, 0x69, 0x91, 0xa3, 0x7d, 0xdf, 0x96, 0xb3, 0x0f, 0x06, 0x2a, 0x9d, + 0x59, 0x96, 0xe3, 0x6c, 0xfc, 0x05, 0xba, 0x6e, 0x65, 0x92, 0x41, 0xe3, 0x96, 0xf2, 0x24, 0x45, + 0xb2, 0xe3, 0xaf, 0xd5, 0xce, 0xa8, 0x17, 0xfc, 0x03, 0xfd, 0xba, 0x50, 0x83, 0xda, 0xd0, 0xbb, + 0xf0, 0x26, 0xe7, 0x70, 0xca, 0x84, 0xab, 0xd4, 0xab, 0x11, 0x8b, 0xf4, 0xd3, 0xb1, 0xf3, 0x67, + 0x55, 0x5d, 0xde, 0x34, 0xf4, 0xe9, 0xf5, 0xcf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x29, 0xfc, 0xf0, + 0xf2, 0x1e, 0x05, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// EventSourcedClient is the client API for EventSourced service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type EventSourcedClient interface { + // The stream. One stream will be established per active entity. + // Once established, the first message sent will be Init, which contains the entity ID, and, + // if the entity has previously persisted a snapshot, it will contain that snapshot. It will + // then send zero to many event messages, one for each event previously persisted. The entity + // is expected to apply these to its state in a deterministic fashion. Once all the events + // are sent, one to many commands are sent, with new commands being sent as new requests for + // the entity come in. The entity is expected to reply to each command with exactly one reply + // message. The entity should reply in order, and any events that the entity requests to be + // persisted the entity should handle itself, applying them to its own state, as if they had + // arrived as events when the event stream was being replayed on load. + Handle(ctx context.Context, opts ...grpc.CallOption) (EventSourced_HandleClient, error) +} + +type eventSourcedClient struct { + cc *grpc.ClientConn +} + +func NewEventSourcedClient(cc *grpc.ClientConn) EventSourcedClient { + return &eventSourcedClient{cc} +} + +func (c *eventSourcedClient) Handle(ctx context.Context, opts ...grpc.CallOption) (EventSourced_HandleClient, error) { + stream, err := c.cc.NewStream(ctx, &_EventSourced_serviceDesc.Streams[0], "/cloudstate.eventsourced.EventSourced/handle", opts...) + if err != nil { + return nil, err + } + x := &eventSourcedHandleClient{stream} + return x, nil +} + +type EventSourced_HandleClient interface { + Send(*EventSourcedStreamIn) error + Recv() (*EventSourcedStreamOut, error) + grpc.ClientStream +} + +type eventSourcedHandleClient struct { + grpc.ClientStream +} + +func (x *eventSourcedHandleClient) Send(m *EventSourcedStreamIn) error { + return x.ClientStream.SendMsg(m) +} + +func (x *eventSourcedHandleClient) Recv() (*EventSourcedStreamOut, error) { + m := new(EventSourcedStreamOut) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// EventSourcedServer is the server API for EventSourced service. +type EventSourcedServer interface { + // The stream. One stream will be established per active entity. + // Once established, the first message sent will be Init, which contains the entity ID, and, + // if the entity has previously persisted a snapshot, it will contain that snapshot. It will + // then send zero to many event messages, one for each event previously persisted. The entity + // is expected to apply these to its state in a deterministic fashion. Once all the events + // are sent, one to many commands are sent, with new commands being sent as new requests for + // the entity come in. The entity is expected to reply to each command with exactly one reply + // message. The entity should reply in order, and any events that the entity requests to be + // persisted the entity should handle itself, applying them to its own state, as if they had + // arrived as events when the event stream was being replayed on load. + Handle(EventSourced_HandleServer) error +} + +// UnimplementedEventSourcedServer can be embedded to have forward compatible implementations. +type UnimplementedEventSourcedServer struct { +} + +func (*UnimplementedEventSourcedServer) Handle(srv EventSourced_HandleServer) error { + return status.Errorf(codes.Unimplemented, "method Handle not implemented") +} + +func RegisterEventSourcedServer(s *grpc.Server, srv EventSourcedServer) { + s.RegisterService(&_EventSourced_serviceDesc, srv) +} + +func _EventSourced_Handle_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(EventSourcedServer).Handle(&eventSourcedHandleServer{stream}) +} + +type EventSourced_HandleServer interface { + Send(*EventSourcedStreamOut) error + Recv() (*EventSourcedStreamIn, error) + grpc.ServerStream +} + +type eventSourcedHandleServer struct { + grpc.ServerStream +} + +func (x *eventSourcedHandleServer) Send(m *EventSourcedStreamOut) error { + return x.ServerStream.SendMsg(m) +} + +func (x *eventSourcedHandleServer) Recv() (*EventSourcedStreamIn, error) { + m := new(EventSourcedStreamIn) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +var _EventSourced_serviceDesc = grpc.ServiceDesc{ + ServiceName: "cloudstate.eventsourced.EventSourced", + HandlerType: (*EventSourcedServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "handle", + Handler: _EventSourced_Handle_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "cloudstate/event_sourced.proto", +} diff --git a/cloudstate/protocol/function.pb.go b/cloudstate/protocol/function.pb.go new file mode 100644 index 0000000..02696c2 --- /dev/null +++ b/cloudstate/protocol/function.pb.go @@ -0,0 +1,489 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: cloudstate/function.proto + +package protocol + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + any "github.com/golang/protobuf/ptypes/any" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type FunctionCommand struct { + // The name of the service this function is on. + ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + // Command name + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // The command payload. + Payload *any.Any `protobuf:"bytes,4,opt,name=payload,proto3" json:"payload,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *FunctionCommand) Reset() { *m = FunctionCommand{} } +func (m *FunctionCommand) String() string { return proto.CompactTextString(m) } +func (*FunctionCommand) ProtoMessage() {} +func (*FunctionCommand) Descriptor() ([]byte, []int) { + return fileDescriptor_876d54e6158c20c4, []int{0} +} + +func (m *FunctionCommand) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_FunctionCommand.Unmarshal(m, b) +} +func (m *FunctionCommand) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_FunctionCommand.Marshal(b, m, deterministic) +} +func (m *FunctionCommand) XXX_Merge(src proto.Message) { + xxx_messageInfo_FunctionCommand.Merge(m, src) +} +func (m *FunctionCommand) XXX_Size() int { + return xxx_messageInfo_FunctionCommand.Size(m) +} +func (m *FunctionCommand) XXX_DiscardUnknown() { + xxx_messageInfo_FunctionCommand.DiscardUnknown(m) +} + +var xxx_messageInfo_FunctionCommand proto.InternalMessageInfo + +func (m *FunctionCommand) GetServiceName() string { + if m != nil { + return m.ServiceName + } + return "" +} + +func (m *FunctionCommand) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *FunctionCommand) GetPayload() *any.Any { + if m != nil { + return m.Payload + } + return nil +} + +type FunctionReply struct { + // Types that are valid to be assigned to Response: + // *FunctionReply_Reply + // *FunctionReply_Forward + Response isFunctionReply_Response `protobuf_oneof:"response"` + SideEffects []*SideEffect `protobuf:"bytes,4,rep,name=side_effects,json=sideEffects,proto3" json:"side_effects,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *FunctionReply) Reset() { *m = FunctionReply{} } +func (m *FunctionReply) String() string { return proto.CompactTextString(m) } +func (*FunctionReply) ProtoMessage() {} +func (*FunctionReply) Descriptor() ([]byte, []int) { + return fileDescriptor_876d54e6158c20c4, []int{1} +} + +func (m *FunctionReply) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_FunctionReply.Unmarshal(m, b) +} +func (m *FunctionReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_FunctionReply.Marshal(b, m, deterministic) +} +func (m *FunctionReply) XXX_Merge(src proto.Message) { + xxx_messageInfo_FunctionReply.Merge(m, src) +} +func (m *FunctionReply) XXX_Size() int { + return xxx_messageInfo_FunctionReply.Size(m) +} +func (m *FunctionReply) XXX_DiscardUnknown() { + xxx_messageInfo_FunctionReply.DiscardUnknown(m) +} + +var xxx_messageInfo_FunctionReply proto.InternalMessageInfo + +type isFunctionReply_Response interface { + isFunctionReply_Response() +} + +type FunctionReply_Reply struct { + Reply *Reply `protobuf:"bytes,2,opt,name=reply,proto3,oneof"` +} + +type FunctionReply_Forward struct { + Forward *Forward `protobuf:"bytes,3,opt,name=forward,proto3,oneof"` +} + +func (*FunctionReply_Reply) isFunctionReply_Response() {} + +func (*FunctionReply_Forward) isFunctionReply_Response() {} + +func (m *FunctionReply) GetResponse() isFunctionReply_Response { + if m != nil { + return m.Response + } + return nil +} + +func (m *FunctionReply) GetReply() *Reply { + if x, ok := m.GetResponse().(*FunctionReply_Reply); ok { + return x.Reply + } + return nil +} + +func (m *FunctionReply) GetForward() *Forward { + if x, ok := m.GetResponse().(*FunctionReply_Forward); ok { + return x.Forward + } + return nil +} + +func (m *FunctionReply) GetSideEffects() []*SideEffect { + if m != nil { + return m.SideEffects + } + return nil +} + +// XXX_OneofWrappers is for the internal use of the proto package. +func (*FunctionReply) XXX_OneofWrappers() []interface{} { + return []interface{}{ + (*FunctionReply_Reply)(nil), + (*FunctionReply_Forward)(nil), + } +} + +func init() { + proto.RegisterType((*FunctionCommand)(nil), "cloudstate.function.FunctionCommand") + proto.RegisterType((*FunctionReply)(nil), "cloudstate.function.FunctionReply") +} + +func init() { proto.RegisterFile("cloudstate/function.proto", fileDescriptor_876d54e6158c20c4) } + +var fileDescriptor_876d54e6158c20c4 = []byte{ + // 383 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x93, 0x4f, 0xaf, 0xd2, 0x40, + 0x14, 0xc5, 0xa9, 0xa0, 0xe8, 0x2d, 0xfe, 0x61, 0x30, 0x08, 0xac, 0xb0, 0x71, 0x51, 0x17, 0x4e, + 0x49, 0x5d, 0xb9, 0x14, 0x23, 0xc1, 0x8d, 0x26, 0x25, 0x2e, 0x74, 0x03, 0x43, 0xe7, 0x16, 0x9b, + 0xb4, 0x33, 0xcd, 0xcc, 0x54, 0xed, 0x07, 0xf1, 0x8b, 0xf8, 0x09, 0x5f, 0x98, 0xd2, 0xf7, 0xca, + 0xcb, 0xcb, 0x5b, 0xb1, 0x3b, 0x99, 0xf3, 0x9b, 0x7b, 0xce, 0xed, 0x1f, 0x98, 0xc6, 0x99, 0x2c, + 0xb9, 0x36, 0xcc, 0x60, 0x90, 0x94, 0x22, 0x36, 0xa9, 0x14, 0xb4, 0x50, 0xd2, 0x48, 0x32, 0xba, + 0xb1, 0x68, 0x63, 0xcd, 0xa6, 0x07, 0x29, 0x0f, 0x19, 0x06, 0x16, 0xd9, 0x97, 0x49, 0xc0, 0x44, + 0x55, 0xf3, 0xb3, 0x57, 0xad, 0x51, 0x28, 0x4c, 0x6a, 0x4e, 0x86, 0xf7, 0x17, 0x9e, 0xaf, 0x4e, + 0xf7, 0x3f, 0xc9, 0x3c, 0x67, 0x82, 0x93, 0xd7, 0x30, 0xd0, 0xa8, 0x7e, 0xa7, 0x31, 0x6e, 0x05, + 0xcb, 0x71, 0xf2, 0x60, 0xee, 0xf8, 0x4f, 0x22, 0xf7, 0x74, 0xf6, 0x95, 0xe5, 0x48, 0x08, 0xf4, + 0xac, 0xd5, 0xb5, 0x96, 0xd5, 0x84, 0x42, 0xbf, 0x60, 0x55, 0x26, 0x19, 0x9f, 0xf4, 0xe6, 0x8e, + 0xef, 0x86, 0x2f, 0x69, 0xdd, 0x87, 0x36, 0x7d, 0xe8, 0x47, 0x51, 0x45, 0x0d, 0xe4, 0xfd, 0x77, + 0xe0, 0x69, 0x13, 0x1d, 0x61, 0x91, 0x55, 0xe4, 0x2d, 0x3c, 0x54, 0x47, 0x61, 0x13, 0xdd, 0x70, + 0x48, 0x5b, 0x4b, 0x5a, 0x62, 0xdd, 0x89, 0x6a, 0x82, 0x04, 0xd0, 0x4f, 0xa4, 0xfa, 0xc3, 0x14, + 0xb7, 0x1d, 0xdc, 0x70, 0xd4, 0x86, 0x57, 0xb5, 0xb5, 0xee, 0x44, 0x0d, 0x45, 0x3e, 0xc0, 0x40, + 0xa7, 0x1c, 0xb7, 0x98, 0x24, 0x18, 0x1b, 0x3d, 0xe9, 0xcd, 0xbb, 0xbe, 0x1b, 0x8e, 0xdb, 0xb7, + 0x36, 0x29, 0xc7, 0xcf, 0xd6, 0x8e, 0x5c, 0x7d, 0xad, 0xf5, 0x12, 0xe0, 0xb1, 0x42, 0x5d, 0x48, + 0xa1, 0x31, 0xfc, 0xd7, 0x85, 0xe1, 0xe6, 0x48, 0x67, 0xa8, 0x75, 0xd3, 0x9e, 0xfc, 0x00, 0xf7, + 0x17, 0x13, 0x3c, 0xc3, 0xef, 0x82, 0xa9, 0x8a, 0xbc, 0xa1, 0x77, 0xbc, 0x1d, 0x7a, 0xeb, 0x31, + 0xcf, 0xbc, 0x7b, 0x29, 0xbb, 0xaf, 0xd7, 0x21, 0x3b, 0x78, 0x51, 0x8f, 0xde, 0x18, 0x85, 0x2c, + 0x47, 0xfe, 0x45, 0x5c, 0x72, 0xbe, 0xef, 0x10, 0x06, 0xc3, 0xf3, 0x84, 0x6f, 0xa5, 0xb9, 0x64, + 0xc4, 0xc2, 0x21, 0x3b, 0x78, 0x76, 0x1e, 0x71, 0xd9, 0x15, 0x16, 0xce, 0xf2, 0x1d, 0x8c, 0x53, + 0xd9, 0xa6, 0xed, 0x37, 0x17, 0xcb, 0xec, 0x67, 0xeb, 0x4f, 0x09, 0x9a, 0xc3, 0xfd, 0x23, 0xab, + 0xde, 0x5f, 0x05, 0x00, 0x00, 0xff, 0xff, 0x25, 0xcb, 0xb1, 0x9a, 0x62, 0x03, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// StatelessFunctionClient is the client API for StatelessFunction service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type StatelessFunctionClient interface { + HandleUnary(ctx context.Context, in *FunctionCommand, opts ...grpc.CallOption) (*FunctionReply, error) + HandleStreamedIn(ctx context.Context, opts ...grpc.CallOption) (StatelessFunction_HandleStreamedInClient, error) + HandleStreamedOut(ctx context.Context, in *FunctionCommand, opts ...grpc.CallOption) (StatelessFunction_HandleStreamedOutClient, error) + HandleStreamed(ctx context.Context, opts ...grpc.CallOption) (StatelessFunction_HandleStreamedClient, error) +} + +type statelessFunctionClient struct { + cc *grpc.ClientConn +} + +func NewStatelessFunctionClient(cc *grpc.ClientConn) StatelessFunctionClient { + return &statelessFunctionClient{cc} +} + +func (c *statelessFunctionClient) HandleUnary(ctx context.Context, in *FunctionCommand, opts ...grpc.CallOption) (*FunctionReply, error) { + out := new(FunctionReply) + err := c.cc.Invoke(ctx, "/cloudstate.function.StatelessFunction/handleUnary", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *statelessFunctionClient) HandleStreamedIn(ctx context.Context, opts ...grpc.CallOption) (StatelessFunction_HandleStreamedInClient, error) { + stream, err := c.cc.NewStream(ctx, &_StatelessFunction_serviceDesc.Streams[0], "/cloudstate.function.StatelessFunction/handleStreamedIn", opts...) + if err != nil { + return nil, err + } + x := &statelessFunctionHandleStreamedInClient{stream} + return x, nil +} + +type StatelessFunction_HandleStreamedInClient interface { + Send(*FunctionCommand) error + CloseAndRecv() (*FunctionReply, error) + grpc.ClientStream +} + +type statelessFunctionHandleStreamedInClient struct { + grpc.ClientStream +} + +func (x *statelessFunctionHandleStreamedInClient) Send(m *FunctionCommand) error { + return x.ClientStream.SendMsg(m) +} + +func (x *statelessFunctionHandleStreamedInClient) CloseAndRecv() (*FunctionReply, error) { + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + m := new(FunctionReply) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *statelessFunctionClient) HandleStreamedOut(ctx context.Context, in *FunctionCommand, opts ...grpc.CallOption) (StatelessFunction_HandleStreamedOutClient, error) { + stream, err := c.cc.NewStream(ctx, &_StatelessFunction_serviceDesc.Streams[1], "/cloudstate.function.StatelessFunction/handleStreamedOut", opts...) + if err != nil { + return nil, err + } + x := &statelessFunctionHandleStreamedOutClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type StatelessFunction_HandleStreamedOutClient interface { + Recv() (*FunctionReply, error) + grpc.ClientStream +} + +type statelessFunctionHandleStreamedOutClient struct { + grpc.ClientStream +} + +func (x *statelessFunctionHandleStreamedOutClient) Recv() (*FunctionReply, error) { + m := new(FunctionReply) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *statelessFunctionClient) HandleStreamed(ctx context.Context, opts ...grpc.CallOption) (StatelessFunction_HandleStreamedClient, error) { + stream, err := c.cc.NewStream(ctx, &_StatelessFunction_serviceDesc.Streams[2], "/cloudstate.function.StatelessFunction/handleStreamed", opts...) + if err != nil { + return nil, err + } + x := &statelessFunctionHandleStreamedClient{stream} + return x, nil +} + +type StatelessFunction_HandleStreamedClient interface { + Send(*FunctionCommand) error + Recv() (*FunctionReply, error) + grpc.ClientStream +} + +type statelessFunctionHandleStreamedClient struct { + grpc.ClientStream +} + +func (x *statelessFunctionHandleStreamedClient) Send(m *FunctionCommand) error { + return x.ClientStream.SendMsg(m) +} + +func (x *statelessFunctionHandleStreamedClient) Recv() (*FunctionReply, error) { + m := new(FunctionReply) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// StatelessFunctionServer is the server API for StatelessFunction service. +type StatelessFunctionServer interface { + HandleUnary(context.Context, *FunctionCommand) (*FunctionReply, error) + HandleStreamedIn(StatelessFunction_HandleStreamedInServer) error + HandleStreamedOut(*FunctionCommand, StatelessFunction_HandleStreamedOutServer) error + HandleStreamed(StatelessFunction_HandleStreamedServer) error +} + +// UnimplementedStatelessFunctionServer can be embedded to have forward compatible implementations. +type UnimplementedStatelessFunctionServer struct { +} + +func (*UnimplementedStatelessFunctionServer) HandleUnary(ctx context.Context, req *FunctionCommand) (*FunctionReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method HandleUnary not implemented") +} +func (*UnimplementedStatelessFunctionServer) HandleStreamedIn(srv StatelessFunction_HandleStreamedInServer) error { + return status.Errorf(codes.Unimplemented, "method HandleStreamedIn not implemented") +} +func (*UnimplementedStatelessFunctionServer) HandleStreamedOut(req *FunctionCommand, srv StatelessFunction_HandleStreamedOutServer) error { + return status.Errorf(codes.Unimplemented, "method HandleStreamedOut not implemented") +} +func (*UnimplementedStatelessFunctionServer) HandleStreamed(srv StatelessFunction_HandleStreamedServer) error { + return status.Errorf(codes.Unimplemented, "method HandleStreamed not implemented") +} + +func RegisterStatelessFunctionServer(s *grpc.Server, srv StatelessFunctionServer) { + s.RegisterService(&_StatelessFunction_serviceDesc, srv) +} + +func _StatelessFunction_HandleUnary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FunctionCommand) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StatelessFunctionServer).HandleUnary(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/cloudstate.function.StatelessFunction/HandleUnary", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatelessFunctionServer).HandleUnary(ctx, req.(*FunctionCommand)) + } + return interceptor(ctx, in, info, handler) +} + +func _StatelessFunction_HandleStreamedIn_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StatelessFunctionServer).HandleStreamedIn(&statelessFunctionHandleStreamedInServer{stream}) +} + +type StatelessFunction_HandleStreamedInServer interface { + SendAndClose(*FunctionReply) error + Recv() (*FunctionCommand, error) + grpc.ServerStream +} + +type statelessFunctionHandleStreamedInServer struct { + grpc.ServerStream +} + +func (x *statelessFunctionHandleStreamedInServer) SendAndClose(m *FunctionReply) error { + return x.ServerStream.SendMsg(m) +} + +func (x *statelessFunctionHandleStreamedInServer) Recv() (*FunctionCommand, error) { + m := new(FunctionCommand) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func _StatelessFunction_HandleStreamedOut_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(FunctionCommand) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StatelessFunctionServer).HandleStreamedOut(m, &statelessFunctionHandleStreamedOutServer{stream}) +} + +type StatelessFunction_HandleStreamedOutServer interface { + Send(*FunctionReply) error + grpc.ServerStream +} + +type statelessFunctionHandleStreamedOutServer struct { + grpc.ServerStream +} + +func (x *statelessFunctionHandleStreamedOutServer) Send(m *FunctionReply) error { + return x.ServerStream.SendMsg(m) +} + +func _StatelessFunction_HandleStreamed_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StatelessFunctionServer).HandleStreamed(&statelessFunctionHandleStreamedServer{stream}) +} + +type StatelessFunction_HandleStreamedServer interface { + Send(*FunctionReply) error + Recv() (*FunctionCommand, error) + grpc.ServerStream +} + +type statelessFunctionHandleStreamedServer struct { + grpc.ServerStream +} + +func (x *statelessFunctionHandleStreamedServer) Send(m *FunctionReply) error { + return x.ServerStream.SendMsg(m) +} + +func (x *statelessFunctionHandleStreamedServer) Recv() (*FunctionCommand, error) { + m := new(FunctionCommand) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +var _StatelessFunction_serviceDesc = grpc.ServiceDesc{ + ServiceName: "cloudstate.function.StatelessFunction", + HandlerType: (*StatelessFunctionServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "handleUnary", + Handler: _StatelessFunction_HandleUnary_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "handleStreamedIn", + Handler: _StatelessFunction_HandleStreamedIn_Handler, + ClientStreams: true, + }, + { + StreamName: "handleStreamedOut", + Handler: _StatelessFunction_HandleStreamedOut_Handler, + ServerStreams: true, + }, + { + StreamName: "handleStreamed", + Handler: _StatelessFunction_HandleStreamed_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "cloudstate/function.proto", +} diff --git a/cloudstate/protosupport.go b/cloudstate/protosupport.go new file mode 100644 index 0000000..d5c07d3 --- /dev/null +++ b/cloudstate/protosupport.go @@ -0,0 +1,44 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstate + +import ( + "bytes" + "compress/gzip" + "errors" + "github.com/golang/protobuf/proto" + filedescr "github.com/golang/protobuf/protoc-gen-go/descriptor" + "io/ioutil" +) + +const protoAnyBase = "type.googleapis.com" + +func unpackFile(gz []byte) (*filedescr.FileDescriptorProto, error) { + r, err := gzip.NewReader(bytes.NewReader(gz)) + if err != nil { + return nil, errors.New("failed to open gzip reader") + } + defer r.Close() + b, err := ioutil.ReadAll(r) + if err != nil { + return nil, errors.New("failed to uncompress descriptor") + } + fd := new(filedescr.FileDescriptorProto) + if err := proto.Unmarshal(b, fd); err != nil { + return nil, errors.New("malformed FileDescriptorProto") + } + return fd, nil +} diff --git a/cloudstate/types.go b/cloudstate/types.go new file mode 100644 index 0000000..1e53adc --- /dev/null +++ b/cloudstate/types.go @@ -0,0 +1,20 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstate + +const ( + EventSourced = "cloudstate.eventsourced.EventSourced" +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..703a9aa --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/cloudstateio/go-support + +go 1.13 + +require ( + github.com/golang/protobuf v1.3.2 + google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 + google.golang.org/grpc v1.24.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7bb2398 --- /dev/null +++ b/go.sum @@ -0,0 +1,27 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 82e554f0c9cc398f6d0cf2571eb3d7ed101a66d9 Mon Sep 17 00:00:00 2001 From: Marcel Lanz Date: Sat, 28 Sep 2019 23:38:08 +0200 Subject: [PATCH 04/10] - moved over tck shopping cart example - handleCommand cleanup - added codecov to travis build - refined snapshot handling - snapshot support without tests - implemented protobuf any serialization by CloudState conventions - added benchmarks for CloudState serialization - cleaned up documentation - travis build for other OS (win,linux, macOS) - refactored EventSourcedHandler.handleCommand - aligned and cleaned up failure handling --- .travis.yml | 14 +- README.md | 3 + build/TCK.Dockerfile | 23 + build/build-and-publish-docker-image.sh | 8 + build/compile-pb.sh | 15 + build/fetch-cloudstate-pb.sh | 30 ++ cloudstate/cloudstate.go | 7 +- cloudstate/encoding/any.go | 21 + cloudstate/encoding/any_json.go | 66 +++ cloudstate/encoding/any_json_test.go | 92 ++++ cloudstate/encoding/any_primitive.go | 188 +++++++ cloudstate/encoding/any_primitive_test.go | 163 ++++++ cloudstate/entity_key.pb.go | 10 +- cloudstate/event.go | 5 +- cloudstate/event_test.go | 31 ++ cloudstate/eventsourced.go | 442 ++++++++++------- cloudstate/eventsourced_reply.go | 78 ++- cloudstate/eventsourced_test.go | 24 +- cloudstate/marshal_proto.go | 56 +++ cloudstate/marshal_proto_test.go | 41 ++ go.sum | 1 + .../shoppingcart/persistence/domain.proto | 27 + .../example/shoppingcart/shoppingcart.proto | 60 +++ tck/cmd/tck_shoppingcart/shoppingcart.go | 194 ++++++++ tck/shoppingcart/persistence/domain.pb.go | 223 +++++++++ tck/shoppingcart/shoppingcart.pb.go | 466 ++++++++++++++++++ 26 files changed, 2088 insertions(+), 200 deletions(-) create mode 100644 build/TCK.Dockerfile create mode 100755 build/build-and-publish-docker-image.sh create mode 100755 build/compile-pb.sh create mode 100755 build/fetch-cloudstate-pb.sh create mode 100644 cloudstate/encoding/any.go create mode 100644 cloudstate/encoding/any_json.go create mode 100644 cloudstate/encoding/any_json_test.go create mode 100644 cloudstate/encoding/any_primitive.go create mode 100644 cloudstate/encoding/any_primitive_test.go create mode 100644 cloudstate/marshal_proto.go create mode 100644 cloudstate/marshal_proto_test.go create mode 100644 protobuf/example/shoppingcart/persistence/domain.proto create mode 100644 protobuf/example/shoppingcart/shoppingcart.proto create mode 100644 tck/cmd/tck_shoppingcart/shoppingcart.go create mode 100644 tck/shoppingcart/persistence/domain.pb.go create mode 100644 tck/shoppingcart/shoppingcart.pb.go diff --git a/.travis.yml b/.travis.yml index effc654..3961665 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,13 @@ language: go - +os: + - linux + - osx + - windows go: - - "1.13" - + - 1.13.x +before_install: + - go get -t -v ./... script: - - go test -v ./... \ No newline at end of file + - go test -v -race -coverprofile=coverage.txt -covermode=atomic -bench=. ./... +after_success: + - if [ "$TRAVIS_OS_NAME" = "linux" ]; then bash <(curl -s https://codecov.io/bash); fi \ No newline at end of file diff --git a/README.md b/README.md index cadc75d..7cb731e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # CloudState stateful service support in Go +[![Build Status](https://travis-ci.com/marcellanz/go-support.svg?branch=feature%2Fgo-support)](https://travis-ci.com/marcellanz/go-support) +[![codecov](https://codecov.io/gh/marcellanz/go-support/branch/master/graph/badge.svg)](https://codecov.io/gh/marcellanz/go-support) +[![GoDoc](https://godoc.org/github.com/marcellanz/go-support?status.svg)](https://godoc.org/github.com/marcellanz/go-support) This package provides support for writing [CloudState](https://github.com/cloudstateio/cloudstate) stateful functions in Go. diff --git a/build/TCK.Dockerfile b/build/TCK.Dockerfile new file mode 100644 index 0000000..7065d0c --- /dev/null +++ b/build/TCK.Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.13.1-alpine3.10 + +RUN apk --no-cache add git + +WORKDIR /go/src/app +COPY . . + +# -race and therefore CGO needs gcc, we don't want it to have in our build +RUN CGO_ENABLED=0 go build -v -o tck_shoppingcart ./tck/cmd/tck_shoppingcart +RUN go install -v ./... + +# multistage – copy over the binary +FROM alpine:latest +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ +COPY --from=0 /go/bin/tck_shoppingcart . + +EXPOSE 8080 +ENV HOST 0.0.0.0 +ENV PORT 8080 + +CMD ["./tck_shoppingcart"] \ No newline at end of file diff --git a/build/build-and-publish-docker-image.sh b/build/build-and-publish-docker-image.sh new file mode 100755 index 0000000..933b86a --- /dev/null +++ b/build/build-and-publish-docker-image.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -o nounset +set -o errexit +set -o pipefail + +docker build -t gcr.io/mrcllnz/cloudstate-go-tck:latest -f ./build/TCK.Dockerfile . +docker push gcr.io/mrcllnz/cloudstate-go-tck:latest diff --git a/build/compile-pb.sh b/build/compile-pb.sh new file mode 100755 index 0000000..93d65d0 --- /dev/null +++ b/build/compile-pb.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -o nounset +set -o errexit +set -o pipefail + +# CloudState protocol +protoc --go_out=plugins=grpc,paths=source_relative:. --proto_path=protobuf/frontend/ protobuf/frontend/cloudstate/entity_key.proto +protoc --go_out=plugins=grpc:. --proto_path=protobuf/protocol/ protobuf/protocol/cloudstate/entity.proto +protoc --go_out=plugins=grpc:. --proto_path=protobuf/protocol/ protobuf/protocol/cloudstate/event_sourced.proto +protoc --go_out=plugins=grpc:. --proto_path=protobuf/protocol/ protobuf/protocol/cloudstate/function.proto + +# TCK shopping cart sample +protoc --go_out=plugins=grpc:. --proto_path=protobuf/protocol/ --proto_path=protobuf/frontend/ --proto_path=protobuf/proxy/ --proto_path=protobuf/example/ protobuf/example/shoppingcart/shoppingcart.proto +protoc --go_out=plugins=grpc,paths=source_relative:tck/shoppingcart/persistence --proto_path=protobuf/protocol/ --proto_path=protobuf/frontend/ --proto_path=protobuf/proxy/ --proto_path=protobuf/example/shoppingcart/persistence/ protobuf/example/shoppingcart/persistence/domain.proto diff --git a/build/fetch-cloudstate-pb.sh b/build/fetch-cloudstate-pb.sh new file mode 100755 index 0000000..1644d7b --- /dev/null +++ b/build/fetch-cloudstate-pb.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -o nounset +set -o errexit +set -o pipefail + +function fetch() { + local path=$1 + mkdir -p protobuf/$(dirname $path) + curl -o protobuf/${path} https://raw.githubusercontent.com/cloudstateio/cloudstate/master/protocols/${path} + #sed 's/^option java_package.*/option go_package = "${go_package}";/' protobuf/${path} +} + +# CloudState protocol +fetch "protocol/cloudstate/entity.proto" +fetch "protocol/cloudstate/event_sourced.proto" +fetch "protocol/cloudstate/function.proto" +fetch "protocol/cloudstate/crdt.proto" + +# TCK shopping cart example +fetch "example/shoppingcart/shoppingcart.proto" +fetch "example/shoppingcart/persistence/domain.proto" + +# CloudState frontend +fetch "frontend/cloudstate/entity_key.proto" + +# dependencies +fetch "proxy/grpc/reflection/v1alpha/reflection.proto" +fetch "frontend/google/api/annotations.proto" +fetch "frontend/google/api/http.proto" diff --git a/cloudstate/cloudstate.go b/cloudstate/cloudstate.go index dadf61d..91de75d 100644 --- a/cloudstate/cloudstate.go +++ b/cloudstate/cloudstate.go @@ -32,7 +32,7 @@ import ( ) const ( - SupportLibraryVersion = "0.4.4" + SupportLibraryVersion = "0.1.0" SupportLibraryName = "cloudstate-go-support" ) @@ -85,7 +85,7 @@ func (dc DescriptorConfig) AddDomainDescriptor(filename string) DescriptorConfig // Register registers an event sourced entity for CloudState. func (cs *CloudState) Register(ese *EventSourcedEntity, config DescriptorConfig) (err error) { - ese.once.Do(func() { + ese.registerOnce.Do(func() { if err = ese.initZeroValue(); err != nil { return } @@ -172,7 +172,7 @@ func (r *EntityDiscoveryResponder) Discover(c context.Context, pi *protocol.Prox return r.entitySpec, nil } -// ReportError logs +// ReportError logs any user function error reported by the CloudState proxy. func (r *EntityDiscoveryResponder) ReportError(c context.Context, fe *protocol.UserFunctionError) (*empty.Empty, error) { log.Printf("ReportError: %v\n", fe) return &empty.Empty{}, nil @@ -227,7 +227,6 @@ func (r *EntityDiscoveryResponder) registerEntity(e *EventSourcedEntity, config ServiceName: e.ServiceName, PersistenceId: persistenceID, }) - // TODO: e.SnapshotEvery return r.updateSpec() } diff --git a/cloudstate/encoding/any.go b/cloudstate/encoding/any.go new file mode 100644 index 0000000..affc75c --- /dev/null +++ b/cloudstate/encoding/any.go @@ -0,0 +1,21 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import "errors" + +var ErrNotMarshalled = errors.New("not marshalled") +var ErrNotUnmarshalled = errors.New("not unmarshalled") diff --git a/cloudstate/encoding/any_json.go b/cloudstate/encoding/any_json.go new file mode 100644 index 0000000..014066a --- /dev/null +++ b/cloudstate/encoding/any_json.go @@ -0,0 +1,66 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import ( + "encoding/json" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes/any" + "reflect" + "strings" +) + +const ( + jsonTypeURLPrefix = "json.cloudstate.io" +) + +func MarshalJSON(value interface{}) (*any.Any, error) { + typeOf := reflect.TypeOf(value) + if typeOf.Kind() != reflect.Struct { + return nil, ErrNotMarshalled + } + buffer := proto.NewBuffer(make([]byte, 0)) + buffer.SetDeterministic(true) + typeUrl := jsonTypeURLPrefix + "/" + typeOf.PkgPath() + "." + typeOf.Name() + _ = buffer.EncodeVarint(fieldKey | proto.WireBytes) + bytes, err := json.Marshal(value) + if err != nil { + return nil, err + } + _ = buffer.EncodeRawBytes(bytes) + return &any.Any{ + TypeUrl: typeUrl, + Value: buffer.Bytes(), + }, nil +} + +// UnmarshalPrimitive decodes a CloudState Any proto message +// into its JSON value. +func UnmarshalJSON(any *any.Any, target interface{}) error { + if !strings.HasPrefix(any.GetTypeUrl(), jsonTypeURLPrefix) { + return ErrNotMarshalled + } + buffer := proto.NewBuffer(any.GetValue()) + _, err := buffer.DecodeVarint() + if err != nil { + return ErrNotUnmarshalled + } + bytes, err := buffer.DecodeRawBytes(true) + if err != nil { + return ErrNotUnmarshalled + } + return json.Unmarshal(bytes, target) +} diff --git a/cloudstate/encoding/any_json_test.go b/cloudstate/encoding/any_json_test.go new file mode 100644 index 0000000..e9e3c18 --- /dev/null +++ b/cloudstate/encoding/any_json_test.go @@ -0,0 +1,92 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import ( + "fmt" + "github.com/golang/protobuf/ptypes/any" + "reflect" + "testing" +) + +var testsJSON = []struct { + name string + value interface{} + zero interface{} + typeURL string + shouldFail bool +}{ + {jsonTypeURLPrefix + "/github.com/cloudstateio/go-support/cloudstate/encoding.a", + a{B: "29", C: 29}, a{}, jsonTypeURLPrefix + "/github.com/cloudstateio/go-support/cloudstate/encoding.a", false}, + {jsonTypeURLPrefix + "/github.com/cloudstateio/go-support/cloudstate/encoding.aDefault" + "_defaultValue", + aDefault{}, aDefault{}, jsonTypeURLPrefix + "/github.com/cloudstateio/go-support/cloudstate/encoding.aDefault", false}, +} + +func TestMarshallerJSON(t *testing.T) { + for _, test := range testsJSON { + test0 := test + t.Run(fmt.Sprintf("%v", test0.name), func(t *testing.T) { + any0, err := MarshalJSON(test0.value) + hasErr := err != nil + if hasErr && !test0.shouldFail { + t.Errorf("err: %v, got: %+v, expected: %+v", err, any0, test0) + return + } else if !hasErr && test0.shouldFail { + t.Errorf("err: %v, got: %+v, expected: %+v", err, any0, test0) + return + } + failed := any0.GetTypeUrl() != test0.typeURL + if failed && !test0.shouldFail { + t.Errorf("err: %v, got: %+v, expected: %+v", err, any0, test0) + } else if !failed && test0.shouldFail { + t.Errorf("err: %v, got: %+v, expected: %+v", err, any0, test0) + } + value := reflect.New(reflect.TypeOf(test0.value)) + err = UnmarshalJSON(any0, value.Interface()) + if err != nil { + t.Error(err) + } + if test0.value != value.Elem().Interface() { + t.Errorf("err: %v. got: %+v, expected: %+v", err, value.Elem().Interface(), test0.value) + } + }) + } +} + +func BenchmarkMarshallerJSON(b *testing.B) { + var any0 *any.Any + for _, bench := range testsJSON { + if !bench.shouldFail { + b.Run(bench.name, func(b *testing.B) { + b.ReportAllocs() + var any1 *any.Any + for i := 0; i < b.N; i++ { + any0, err := MarshalJSON(bench.value) + if err != nil { + b.Error(err) + } + value := bench.zero + err = UnmarshalJSON(any0, &value) + if err != nil { + b.Error(err) + } + } + any0 = any1 //prevent the call optimized away + }) + } + } + _ = any0 == nil //use any0 +} diff --git a/cloudstate/encoding/any_primitive.go b/cloudstate/encoding/any_primitive.go new file mode 100644 index 0000000..6333078 --- /dev/null +++ b/cloudstate/encoding/any_primitive.go @@ -0,0 +1,188 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import ( + "encoding/json" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes/any" + "math" + "reflect" +) + +const ( + primitiveTypeURLPrefix = "p.cloudstate.io" + + primitiveTypeURLPrefixInt32 = primitiveTypeURLPrefix + "/int32" + primitiveTypeURLPrefixInt64 = primitiveTypeURLPrefix + "/int64" + primitiveTypeURLPrefixString = primitiveTypeURLPrefix + "/string" + primitiveTypeURLPrefixFloat = primitiveTypeURLPrefix + "/float" + primitiveTypeURLPrefixDouble = primitiveTypeURLPrefix + "/double" + primitiveTypeURLPrefixBool = primitiveTypeURLPrefix + "/bool" + primitiveTypeURLPrefixBytes = primitiveTypeURLPrefix + "/bytes" +) + +const fieldKey = 1 << 3 + +func MarshalPrimitive(i interface{}) (*any.Any, error) { + buf := make([]byte, 0) + buffer := proto.NewBuffer(buf) + buffer.SetDeterministic(true) + // see https://developers.google.com/protocol-buffers/docs/encoding#structure + var typeUrl string + switch val := i.(type) { + case int32: + typeUrl = primitiveTypeURLPrefixInt32 + _ = buffer.EncodeVarint(fieldKey | proto.WireVarint) + _ = buffer.EncodeVarint(uint64(val)) + case int64: + typeUrl = primitiveTypeURLPrefixInt64 + _ = buffer.EncodeVarint(fieldKey | proto.WireVarint) + _ = buffer.EncodeVarint(uint64(val)) + case string: + typeUrl = primitiveTypeURLPrefixString + _ = buffer.EncodeVarint(fieldKey | proto.WireBytes) + if err := buffer.EncodeStringBytes(val); err != nil { + return nil, err + } + case float32: + typeUrl = primitiveTypeURLPrefixFloat + _ = buffer.EncodeVarint(fieldKey | proto.WireFixed32) + _ = buffer.EncodeFixed32(uint64(math.Float32bits(val))) + case float64: + typeUrl = primitiveTypeURLPrefixDouble + _ = buffer.EncodeVarint(fieldKey | proto.WireFixed64) + _ = buffer.EncodeFixed64(math.Float64bits(val)) + case bool: + typeUrl = primitiveTypeURLPrefixBool + _ = buffer.EncodeVarint(fieldKey | proto.WireVarint) + switch val { + case true: + _ = buffer.EncodeVarint(1) + case false: + _ = buffer.EncodeVarint(0) + } + case []byte: + typeUrl = primitiveTypeURLPrefixBytes + _ = buffer.EncodeVarint(fieldKey | proto.WireBytes) + if err := buffer.EncodeRawBytes(val); err != nil { + return nil, err + } + case interface{}: + typeOf := reflect.TypeOf(val) + if typeOf.Kind() == reflect.Struct { + typeUrl = jsonTypeURLPrefix + "/" + typeOf.PkgPath() + "." + typeOf.Name() + _ = buffer.EncodeVarint(fieldKey | proto.WireBytes) + bytes, err := json.Marshal(val) + if err != nil { + return nil, err + } + _ = buffer.EncodeRawBytes(bytes) + } else { + return nil, ErrNotMarshalled + } + default: + return nil, ErrNotMarshalled + } + return &any.Any{ + TypeUrl: typeUrl, + Value: buffer.Bytes(), + }, nil +} + +// UnmarshalPrimitive decodes a CloudState Any proto message +// into its primitive value. +func UnmarshalPrimitive(any *any.Any) (interface{}, error) { + buffer := proto.NewBuffer(any.GetValue()) + if any.GetTypeUrl() == primitiveTypeURLPrefixInt32 { + _, err := buffer.DecodeVarint() + if err != nil { + return nil, ErrNotUnmarshalled + } + value, err := buffer.DecodeVarint() + if err != nil { + return nil, ErrNotUnmarshalled + } + return int32(value), nil + } + if any.GetTypeUrl() == primitiveTypeURLPrefixInt64 { + _, err := buffer.DecodeVarint() + if err != nil { + return nil, ErrNotUnmarshalled + } + value, err := buffer.DecodeVarint() + if err != nil { + return nil, ErrNotUnmarshalled + } + return int64(value), nil + } + if any.GetTypeUrl() == primitiveTypeURLPrefixString { + _, err := buffer.DecodeVarint() + if err != nil { + return nil, ErrNotUnmarshalled + } + value, err := buffer.DecodeStringBytes() + if err != nil { + return nil, ErrNotUnmarshalled + } + return value, nil + } + if any.GetTypeUrl() == primitiveTypeURLPrefixFloat { + _, err := buffer.DecodeVarint() + if err != nil { + return nil, ErrNotUnmarshalled + } + value, err := buffer.DecodeFixed32() + if err != nil { + return nil, ErrNotUnmarshalled + } + return math.Float32frombits(uint32(value)), nil + } + if any.GetTypeUrl() == primitiveTypeURLPrefixDouble { + _, err := buffer.DecodeVarint() + if err != nil { + return nil, ErrNotUnmarshalled + } + value, err := buffer.DecodeFixed64() + if err != nil { + return nil, ErrNotUnmarshalled + } + return math.Float64frombits(value), nil + } + if any.GetTypeUrl() == primitiveTypeURLPrefixBool { + _, err := buffer.DecodeVarint() + if err != nil { + return nil, ErrNotUnmarshalled + } + value, err := buffer.DecodeVarint() + if err != nil { + return nil, ErrNotUnmarshalled + } + return value == 1, nil + } + if any.GetTypeUrl() == primitiveTypeURLPrefixBytes { + _, err := buffer.DecodeVarint() + if err != nil { + return nil, ErrNotUnmarshalled + } + value, err := buffer.DecodeRawBytes(true) + if err != nil { + return nil, ErrNotUnmarshalled + } + return value, nil + } + return nil, nil +} diff --git a/cloudstate/encoding/any_primitive_test.go b/cloudstate/encoding/any_primitive_test.go new file mode 100644 index 0000000..9b97298 --- /dev/null +++ b/cloudstate/encoding/any_primitive_test.go @@ -0,0 +1,163 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import ( + "bytes" + "fmt" + "github.com/golang/protobuf/ptypes/any" + "testing" +) + +type a struct { + B string `json:"b"` + C int32 `json:"c"` +} + +type aDefault struct { +} + +var tests = []struct { + name string + value interface{} + typeURL string + shouldFail bool +}{ + {primitiveTypeURLPrefixInt32, uint32(28), primitiveTypeURLPrefixInt32, true}, + {primitiveTypeURLPrefixInt32 + "_defaultValue", uint32(0), primitiveTypeURLPrefixInt32, true}, + {primitiveTypeURLPrefixInt32, int32(29), primitiveTypeURLPrefixInt32, false}, + {primitiveTypeURLPrefixInt32 + "_defaultValue", int32(0), primitiveTypeURLPrefixInt32, false}, + {primitiveTypeURLPrefixInt64, int64(29), primitiveTypeURLPrefixInt64, false}, + {primitiveTypeURLPrefixInt64 + "_defaultValue", int64(0), primitiveTypeURLPrefixInt64, false}, + {primitiveTypeURLPrefixFloat, float32(2.9), primitiveTypeURLPrefixFloat, false}, + {primitiveTypeURLPrefixFloat + "_defaultValue", float32(2.9), primitiveTypeURLPrefixFloat, false}, + {primitiveTypeURLPrefixDouble, float64(2.9), primitiveTypeURLPrefixDouble, false}, + {primitiveTypeURLPrefixDouble + "_defaultValue", float64(0), primitiveTypeURLPrefixDouble, false}, + {primitiveTypeURLPrefixString, "29", primitiveTypeURLPrefixString, false}, + {primitiveTypeURLPrefixString + "_defaultValue", "", primitiveTypeURLPrefixString, false}, + {primitiveTypeURLPrefixBool + "_true", true, primitiveTypeURLPrefixBool, false}, + {primitiveTypeURLPrefixBool + "_false", false, primitiveTypeURLPrefixBool, false}, + {primitiveTypeURLPrefixBool + "_defaultValue", false, primitiveTypeURLPrefixBool, false}, + {primitiveTypeURLPrefixBytes, make([]byte, 29), primitiveTypeURLPrefixBytes, false}, + {primitiveTypeURLPrefixBytes + "_defaultValue", make([]byte, 0), primitiveTypeURLPrefixBytes, false}, +} + +func TestMarshallerPrimitives(t *testing.T) { + for _, test := range tests { + tc := test + t.Run(fmt.Sprintf("%v", tc.name), func(t *testing.T) { + any0, err := MarshalPrimitive(tc.value) + hasErr := err != nil + if hasErr && !tc.shouldFail { + t.Errorf("err: %v, got: %+v, expected: %+v", err, any0, tc) + return + } else if !hasErr && tc.shouldFail { + t.Errorf("err: %v, got: %+v, expected: %+v", err, any0, tc) + return + } + failed := any0.GetTypeUrl() != tc.typeURL + if failed && !tc.shouldFail { + t.Errorf("err: %v, got: %+v, expected: %+v", err, any0, tc) + } else if !failed && tc.shouldFail { + t.Errorf("err: %v, got: %+v, expected: %+v", err, any0, tc) + } + }) + } +} + +func TestMarshalUnmarshalPrimitive(t *testing.T) { + for _, test := range tests { + if test.shouldFail { + continue + } + tc := test + t.Run(fmt.Sprintf("%v", tc.name), func(t *testing.T) { + a, err := MarshalPrimitive(tc.value) + if err != nil { + t.Error(err) + } + u, err := UnmarshalPrimitive(a) + if err != nil { + t.Error(err) + } + switch ut := u.(type) { + case []byte: + byt := tc.value.([]byte) + if bytes.Compare(byt, ut) != 0 { + t.Errorf("err: %v. got: %+v, expected: %+v", err, u, tc.value) + } + default: + if tc.value != u { + t.Errorf("err: %v. got: %+v, expected: %+v", err, u, tc.value) + } + } + }) + } +} + +func BenchmarkMarshallerPrimitives(b *testing.B) { + var any0 *any.Any + for _, i := range tests { + tc := i + if !tc.shouldFail { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + var any1 *any.Any + for i := 0; i < b.N; i++ { + any1, _ = MarshalPrimitive(tc.value) + } + any0 = any1 //prevent the call optimized away + }) + } + } + _ = any0 == nil //use any0 +} + +func BenchmarkMarshalUnmarshal(b *testing.B) { + var any0 *any.Any + for _, i := range tests { + tc := i + if !tc.shouldFail { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + var any1 *any.Any + for i := 0; i < b.N; i++ { + a, err := MarshalPrimitive(tc.value) + if err != nil { + b.Error(err) + } + u, err := UnmarshalPrimitive(a) + if err != nil { + b.Error(err) + } + switch ut := u.(type) { + case []byte: + byt := tc.value.([]byte) + if bytes.Compare(byt, ut) != 0 { + b.Errorf("err: %v. got: %+v, expected: %+v", err, u, tc.value) + } + default: + if tc.value != u { + b.Errorf("err: %v. got: %+v, expected: %+v", err, u, tc.value) + } + } + } + any0 = any1 //prevent the call optimized away + }) + } + } + _ = any0 == nil //use any0 +} diff --git a/cloudstate/entity_key.pb.go b/cloudstate/entity_key.pb.go index c9bbd22..345be8d 100644 --- a/cloudstate/entity_key.pb.go +++ b/cloudstate/entity_key.pb.go @@ -37,7 +37,7 @@ func init() { func init() { proto.RegisterFile("cloudstate/entity_key.proto", fileDescriptor_7bcabc3af9eb79b9) } var fileDescriptor_7bcabc3af9eb79b9 = []byte{ - // 160 bytes of a gzipped FileDescriptorProto + // 178 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4e, 0xce, 0xc9, 0x2f, 0x4d, 0x29, 0x2e, 0x49, 0x2c, 0x49, 0xd5, 0x4f, 0xcd, 0x2b, 0xc9, 0x2c, 0xa9, 0x8c, 0xcf, 0x4e, 0xad, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x48, 0x4a, 0x29, 0xa4, 0xe7, 0xe7, @@ -45,7 +45,9 @@ var fileDescriptor_7bcabc3af9eb79b9 = []byte{ 0x0b, 0x4a, 0xf2, 0x8b, 0x20, 0xaa, 0xad, 0xec, 0xb8, 0xb8, 0x10, 0x26, 0x08, 0xc9, 0xea, 0x41, 0x34, 0xe8, 0xc1, 0x34, 0xe8, 0xb9, 0x65, 0xa6, 0xe6, 0xa4, 0xf8, 0x17, 0x94, 0x64, 0xe6, 0xe7, 0x15, 0x4b, 0x5c, 0x6a, 0x63, 0x56, 0x60, 0xd4, 0xe0, 0x08, 0xe2, 0x84, 0x68, 0xf1, 0x4e, 0xad, - 0x74, 0x32, 0xe6, 0xe2, 0xcd, 0xcc, 0xd7, 0x43, 0x58, 0x19, 0xa5, 0x84, 0x60, 0xeb, 0x65, 0xe6, - 0xeb, 0xa7, 0xe7, 0x17, 0x97, 0x16, 0x14, 0xe4, 0x17, 0x95, 0xe8, 0x23, 0xc4, 0x93, 0xd8, 0xc0, - 0xc6, 0x1b, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x40, 0xb5, 0x41, 0x6c, 0xc8, 0x00, 0x00, 0x00, + 0x74, 0x0a, 0xe2, 0xe2, 0xcd, 0xcc, 0xd7, 0x43, 0x58, 0x19, 0xe5, 0x98, 0x9e, 0x59, 0x92, 0x51, + 0x9a, 0xa4, 0x97, 0x9c, 0x9f, 0xab, 0x8f, 0x10, 0xce, 0xcc, 0xd7, 0x4f, 0xcf, 0xd7, 0x2d, 0x2e, + 0x2d, 0x28, 0xc8, 0x2f, 0x2a, 0x41, 0x12, 0x87, 0xb8, 0x2c, 0x39, 0x3f, 0xc7, 0x1a, 0x21, 0x96, + 0xc4, 0x06, 0x16, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xb6, 0x70, 0x0b, 0x55, 0xe7, 0x00, + 0x00, 0x00, } diff --git a/cloudstate/event.go b/cloudstate/event.go index eaa599b..24cef4c 100644 --- a/cloudstate/event.go +++ b/cloudstate/event.go @@ -48,8 +48,8 @@ type eventEmitter struct { subscriptions []*Subscription } -// Emit will immediately invoke the associated event handler for that event - -// this both validates that the event can be applied to the current state, as well as +// Emit will immediately invoke the associated event handler for that event. +// This both validates that the event can be applied to the current state, as well as // updates the state so that subsequent processing in the command handler can use it. func (e *eventEmitter) Emit(event interface{}) { for _, subs := range e.subscriptions { @@ -63,6 +63,7 @@ func (e *eventEmitter) Emit(event interface{}) { } if err != nil && subs.OnErr != nil { subs.OnErr(err) + // TODO: we have no context here to fail to the proxy } } e.events = append(e.events, event) diff --git a/cloudstate/event_test.go b/cloudstate/event_test.go index 310c24d..bec88db 100644 --- a/cloudstate/event_test.go +++ b/cloudstate/event_test.go @@ -54,6 +54,37 @@ func TestMultipleSubscribers(t *testing.T) { } } +func TestUnsubscribe(t *testing.T) { + e := AnEntity{EventEmitter: NewEmitter()} + n := int64(0) + sub1 := &Subscription{ + OnNext: func(event interface{}) error { + atomic.AddInt64(&n, 1) + return nil + }, + } + e.Subscribe(sub1) + e.Emit(1) + if n != 1 { + t.Fail() + } + e.Emit(1) + if n != 2 { + t.Fail() + } + e.Subscribe(&Subscription{ + OnNext: func(event interface{}) error { + atomic.AddInt64(&n, 1) + return nil + }, + }) + sub1.Unsubscribe() + e.Emit(1) + if n != 3 { + t.Fail() + } +} + func TestEventEmitter(t *testing.T) { e := AnEntity{EventEmitter: NewEmitter()} s := make([]string, 0) diff --git a/cloudstate/eventsourced.go b/cloudstate/eventsourced.go index 18e684e..f7ad2e7 100644 --- a/cloudstate/eventsourced.go +++ b/cloudstate/eventsourced.go @@ -16,13 +16,13 @@ package cloudstate import ( + "context" "errors" "fmt" "github.com/cloudstateio/go-support/cloudstate/protocol" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes/any" "io" - "log" "reflect" "strings" "sync" @@ -38,11 +38,10 @@ type EntityInitializer interface { New() interface{} } -type EntityInstance struct { - EventSourcedEntity *EventSourcedEntity - Instance interface{} -} +const snapshotEveryDefault = 100 +// EventSourcedEntity captures an Entity, its ServiceName and PersistenceID. +// It is used to be registered as an event sourced entity on a CloudState instance. type EventSourcedEntity struct { // Entity is a nil or Zero-Initialized reference // to the entity to be event sourced. It has to @@ -63,11 +62,11 @@ type EventSourcedEntity struct { // so that the entity doesn't need to be recovered from the whole journal // each time it’s loaded. If left unset, it defaults to 100. // Setting it to a negative number will result in snapshots never being taken. - SnapshotEvery int + SnapshotEvery int64 // internal - entityName string - once sync.Once + entityName string + registerOnce sync.Once } // initZeroValue get its Entity type and Zero-Value it to @@ -85,39 +84,61 @@ func (e *EventSourcedEntity) initZeroValue() error { return errors.New("the Entity does not implement EntityInitializer") } e.entityName = t.Name() + e.SnapshotEvery = snapshotEveryDefault } return nil } +// The EntityInstance represents a concrete instance of +// a event sourced entity +type EntityInstance struct { + // Instance is an instance of the EventSourcedEntity.Entity + Instance interface{} + // EventSourcedEntity describes the instance + EventSourcedEntity *EventSourcedEntity + + eventSequence int64 +} + +func (e *EntityInstance) shouldSnapshot() bool { + return e.eventSequence >= e.EventSourcedEntity.SnapshotEvery +} + +func (e *EntityInstance) resetSnapshotEvery() { + e.eventSequence = 0 +} + // A EntityInstanceContext represents a event sourced entity together with its // associated service. // Commands are dispatched through this context. -type EntityInstanceContext struct { - EntityInstance EntityInstance +type EntityInstanceContext struct { // TODO: EntityInstanceContext might be actually a EntityInstance + // EntityInstance is the entity instance of this context + EntityInstance *EntityInstance + // active indicates if this context is active + active bool // TODO: inactivate a context in case of errors } // ServiceName returns the contexts service name. -func (ec *EntityInstanceContext) ServiceName() string { - return ec.EntityInstance.EventSourcedEntity.ServiceName +func (c EntityInstanceContext) ServiceName() string { + return c.EntityInstance.EventSourcedEntity.ServiceName } // EventSourcedHandler is the implementation of the EventSourcedHandler server API for EventSourced service. type EventSourcedHandler struct { // entities are indexed by their service name entities map[string]*EventSourcedEntity - // entity instance contexts for all - // event sourced entities indexed by their entity ids + // contexts are entity instance contexts indexed by their entity ids contexts map[string]*EntityInstanceContext - // method cache - methodCache map[string]reflect.Method + // cmdMethodCache is the command handler method cache + cmdMethodCache map[string]reflect.Method } // NewEventSourcedHandler returns an initialized EventSourcedHandler func NewEventSourcedHandler() *EventSourcedHandler { return &EventSourcedHandler{ - entities: make(map[string]*EventSourcedEntity), - contexts: make(map[string]*EntityInstanceContext), - methodCache: make(map[string]reflect.Method), + entities: make(map[string]*EventSourcedEntity), + contexts: make(map[string]*EntityInstanceContext), + cmdMethodCache: make(map[string]reflect.Method), } } @@ -126,30 +147,52 @@ func (esh *EventSourcedHandler) registerEntity(ese *EventSourcedEntity) error { return nil } -// see EventSourcedServer.Handle +// Handle +// from EventSourcedServer.Handle +// The stream. One stream will be established per active entity. +// Once established, the first message sent will be Init, which contains the entity ID, and, +// if the entity has previously persisted a snapshot, it will contain that snapshot. It will +// then send zero to many event messages, one for each event previously persisted. The entity +// is expected to apply these to its state in a deterministic fashion. Once all the events +// are sent, one to many commands are sent, with new commands being sent as new requests for +// the entity come in. The entity is expected to reply to each command with exactly one reply +// message. The entity should reply in order, and any events that the entity requests to be +// persisted the entity should handle itself, applying them to its own state, as if they had +// arrived as events when the event stream was being replayed on load. func (esh *EventSourcedHandler) Handle(server protocol.EventSourced_HandleServer) error { + var entityId string + var failed error for { - msg, err := server.Recv() - if err == io.EOF { + if failed != nil { + return failed + } + msg, recvErr := server.Recv() + if recvErr == io.EOF { return nil } - if err != nil { - return err + if recvErr != nil { + return recvErr } if cmd := msg.GetCommand(); cmd != nil { if err := esh.handleCommand(cmd, server); err != nil { - if errors.Is(err, ErrSendFailure) { - } - return err + // TODO: in general, what happens with the stream here if an error happens? + failed = handleFailure(err, server, cmd.GetId()) } + continue } if event := msg.GetEvent(); event != nil { - log.Fatalf("event handling is not implemented yet") + // TODO spec: Why does command carry the entityId and an event not? + if err := esh.handleEvent(entityId, event); err != nil { + failed = handleFailure(err, server, 0) + } + continue } if init := msg.GetInit(); init != nil { - if err := esh.handleInit(init, server); err != nil { // TODO: unwrap the error and see if its a server.Send error - return err + if err := esh.handleInit(init, server); err != nil { + failed = handleFailure(err, server, 0) } + entityId = init.GetEntityId() + continue } } } @@ -157,46 +200,87 @@ func (esh *EventSourcedHandler) Handle(server protocol.EventSourced_HandleServer func (esh *EventSourcedHandler) handleInit(init *protocol.EventSourcedInit, server protocol.EventSourced_HandleServer) error { eid := init.GetEntityId() if _, present := esh.contexts[eid]; present { - if err := server.Send(&protocol.EventSourcedStreamOut{ - Message: &protocol.EventSourcedStreamOut_Failure{ - Failure: &protocol.Failure{ - Description: "entity already initialized", - }}}); err != nil { - return fmt.Errorf("unable to server.Send, %w", err) - } - return nil + return NewFailureError("unable to server.Send") } entity := esh.entities[init.GetServiceName()] if initializer, ok := entity.Entity.(EntityInitializer); ok { instance := initializer.New() esh.contexts[eid] = &EntityInstanceContext{ - EntityInstance: EntityInstance{ + EntityInstance: &EntityInstance{ Instance: instance, EventSourcedEntity: entity, }, + active: true, } } else { return fmt.Errorf("unable to handle init entity.Entity does not implement EntityInitializer") } - esh.subscribeEvents(esh.contexts[eid].EntityInstance.Instance) + + if err := esh.handleInitSnapshot(init); err != nil { + return NewFailureError("unable to server.Send. %w", err) + } + esh.subscribeEvents(esh.contexts[eid].EntityInstance) return nil } -func (esh *EventSourcedHandler) subscribeEvents(inst interface{}) { - if emitter, ok := inst.(EventEmitter); ok { +func (esh *EventSourcedHandler) handleInitSnapshot(init *protocol.EventSourcedInit) error { + if init.Snapshot == nil { + return nil + } + entityId := init.GetEntityId() + if snapShotHandler, ok := esh.contexts[entityId].EntityInstance.Instance.(SnapshotHandler); ok { + msgName := strings.TrimPrefix(init.Snapshot.Snapshot.GetTypeUrl(), protoAnyBase+"/") // TODO: this might be something else than a proto message + messageType := proto.MessageType(msgName) + if messageType.Kind() == reflect.Ptr { + if message, ok := reflect.New(messageType.Elem()).Interface().(proto.Message); ok { + err := proto.Unmarshal(init.Snapshot.Snapshot.Value, message) + if err != nil { + return NewFailureError("unmarshalling snapshot failed with: %v", err) + } + handled, err := snapShotHandler.HandleSnapshot(message) + if err != nil { + return NewFailureError("handling snapshot failed with: %v", err) + } + if handled { + esh.contexts[entityId].EntityInstance.eventSequence = init.GetSnapshot().SnapshotSequence + } + } + } + } + return nil +} + +func (esh *EventSourcedHandler) subscribeEvents(instance *EntityInstance) { + if emitter, ok := instance.Instance.(EventEmitter); ok { emitter.Subscribe(&Subscription{ OnNext: func(event interface{}) error { - anyEvent, err := esh.marshalEvent(event) - if err != nil { - return err + err := esh.applyEvent(instance, event) + if err == nil { + instance.eventSequence++ } - return esh.handleEvents(reflect.ValueOf(inst), anyEvent) + return err }, - OnErr: func(err error) {}, // TODO: investigate what to report to the proxy + OnErr: func(err error) { + }, // TODO: investigate what to report to the proxy }) } } +func (esh *EventSourcedHandler) handleEvent(entityId string, event *protocol.EventSourcedEvent) error { + if entityId == "" { + return NewFailureError("no entityId was found from a previous init message for event sequence: %v", event.Sequence) + } + entityContext := esh.contexts[entityId] + if entityContext == nil { + return NewFailureError("no entity with entityId registered: %v", entityId) + } + err := esh.handleEvents(entityContext.EntityInstance, event) + if err != nil { + return NewFailureError("handle event failed: %v", err) + } + return err +} + // handleCommand handles a command received from the CloudState proxy. // // TODO: remove these following lines of comment @@ -218,168 +302,175 @@ func (esh *EventSourcedHandler) subscribeEvents(inst interface{}) { // Beside calling the service method, we have to collect "events" the service might emit. // These events afterwards have to be handled by a EventHandler to update the state of the // entity. The CloudState proxy can re-play these events at any time -// TODO: split it up +// TODO: move sendFailure to the caller func (esh *EventSourcedHandler) handleCommand(cmd *protocol.Command, server protocol.EventSourced_HandleServer) error { + // method to call + method, err := esh.methodToCall(cmd) + if err != nil { + return NewProtocolFailure(protocol.Failure{ + CommandId: cmd.GetId(), + Description: err.Error(), + }) + } entityContext := esh.contexts[cmd.GetEntityId()] - entity := esh.entities[entityContext.ServiceName()] - entityValue := reflect.ValueOf(entityContext.EntityInstance.Instance) + // build the input arguments for the method we're about to call + inputs, err := esh.buildInputs(entityContext, method, cmd, server.Context()) + if err != nil { + return NewProtocolFailure(protocol.Failure{ + CommandId: cmd.GetId(), + Description: err.Error(), + }) + } + // call it + called := method.Func.Call(inputs) + // The gRPC implementation returns the rpc return method + // and an error as a second return value. + errReturned := called[1] + if errReturned.CanInterface() && errReturned.Interface() != nil && errReturned.Type().Name() == "error" { // FIXME: looks ugly + // TCK says: TODO Expects entity.Failure, but gets lientAction.Action.Failure(Failure(commandId, msg))) + return NewProtocolFailure(protocol.Failure{ + CommandId: cmd.GetId(), + Description: errReturned.Interface().(error).Error(), + }) + } + // the reply + callReply, err := marshalAny(called[0].Interface()) + if err != nil { // this should never happen + return NewProtocolFailure(protocol.Failure{ + CommandId: cmd.GetId(), + Description: fmt.Errorf("called return value at index 0 is no proto.Message").Error(), + }) + } + // emitted events + events, err := marshalEventsAny(entityContext) + if err != nil { + return NewProtocolFailure(protocol.Failure{ + CommandId: cmd.GetId(), + Description: err.Error(), + }) + } + // snapshot + snapshot, err := esh.handleSnapshots(entityContext) + if err != nil { + return NewProtocolFailure(protocol.Failure{ + CommandId: cmd.GetId(), + Description: err.Error(), + }) + } + return sendEventSourcedReply(&protocol.EventSourcedReply{ + CommandId: cmd.GetId(), + ClientAction: &protocol.ClientAction{ + Action: &protocol.ClientAction_Reply{ + Reply: &protocol.Reply{ + Payload: callReply, + }, + }, + }, + Events: events, + Snapshot: snapshot, + }, server) +} + +func (*EventSourcedHandler) buildInputs(entityContext *EntityInstanceContext, method reflect.Method, cmd *protocol.Command, ctx context.Context) ([]reflect.Value, error) { + inputs := make([]reflect.Value, method.Type.NumIn()) + inputs[0] = reflect.ValueOf(entityContext.EntityInstance.Instance) + inputs[1] = reflect.ValueOf(ctx) + // create a zero-value for the type of the message we call the method with + arg1 := method.Type.In(2) + ptr := false + for arg1.Kind() == reflect.Ptr { + ptr = true + arg1 = arg1.Elem() + } + var msg proto.Message + if ptr { + msg = reflect.New(arg1).Interface().(proto.Message) + } else { + msg = reflect.Zero(arg1).Interface().(proto.Message) + } + if err := proto.Unmarshal(cmd.GetPayload().GetValue(), msg); err != nil { + return nil, fmt.Errorf("failed to unmarshal: %w", err) + } + inputs[2] = reflect.ValueOf(msg) + return inputs, nil +} +func (esh *EventSourcedHandler) methodToCall(cmd *protocol.Command) (reflect.Method, error) { + entityContext := esh.contexts[cmd.GetEntityId()] cacheKey := entityContext.ServiceName() + cmd.Name - method, hit := esh.methodCache[cacheKey] + method, hit := esh.cmdMethodCache[cacheKey] // as measured this cache saves us about 75% of a call // to be prepared with 4.4µs vs. 17.6µs where a typical // call by reflection like GetCart() with Func.Call() // takes ~10µs and to get return values processed somewhere 0.7µs. if !hit { + entityValue := reflect.ValueOf(entityContext.EntityInstance.Instance) // entities implement the proxied grpc service // we try to find the method we're called by name with the // received command. methodByName := entityValue.MethodByName(cmd.Name) if !methodByName.IsValid() { - return fmt.Errorf("no method named: %s found for: %v", cmd.Name, entity) + entity := esh.entities[entityContext.ServiceName()] + return reflect.Method{}, fmt.Errorf("no method named: %s found for: %v", cmd.Name, entity) } // gRPC services are unary rpc methods, always. // They have one message in and one message out. - if methodByName.Type().NumIn() != 2 { - // this should never happen - failure := &protocol.Failure{ - Description: fmt.Sprintf("non-unary method %s of entity: %v found", methodByName.String(), entityValue.String()), - } - if err := sendFailure(failure, server); err != nil { - return ErrSendFailure - } + if err := checkUnary(methodByName); err != nil { + return reflect.Method{}, err } // The first argument in the gRPC implementation // is always a context.Context. methodArg0Type := methodByName.Type().In(0) - if !reflect.TypeOf(server.Context()).Implements(methodArg0Type) { - return fmt.Errorf( + contextType := reflect.TypeOf(context.Background()) + if !contextType.Implements(methodArg0Type) { + return reflect.Method{}, fmt.Errorf( "first argument for method: %s is not of type: %s", - methodByName.String(), reflect.TypeOf(server.Context()).Name(), + methodByName.String(), contextType.Name(), ) } + // we'll find one for sure as we found one on the entityValue method, _ = reflect.TypeOf(entityContext.EntityInstance.Instance).MethodByName(cmd.Name) - esh.methodCache[cacheKey] = method + esh.cmdMethodCache[cacheKey] = method } + return method, nil +} - // build the input arguments for the method we're about to call - inputs := make([]reflect.Value, method.Type.NumIn()) - inputs[0] = entityValue - inputs[1] = reflect.ValueOf(server.Context()) - - // create a zero-value for the type of the - // message we call the method with - arg1 := method.Type.In(2) - ptr := false - for arg1.Kind() == reflect.Ptr { - ptr = true - arg1 = arg1.Elem() - } - var msg proto.Message - if ptr { - msg = reflect.New(arg1).Interface().(proto.Message) - } else { - msg = reflect.Zero(arg1).Interface().(proto.Message) +func (*EventSourcedHandler) handleSnapshots(entityContext *EntityInstanceContext) (*any.Any, error) { + if !entityContext.EntityInstance.shouldSnapshot() { + return nil, nil } - if err := proto.Unmarshal(cmd.GetPayload().GetValue(), msg); err != nil { - return fmt.Errorf("failed to unmarshal: %w", err) - } - - inputs[2] = reflect.ValueOf(msg) - // call it - called := method.Func.Call(inputs) - // The gRPC implementation returns the rpc return method - // and an error as a second return value. - errReturned := called[1] - if errReturned.CanInterface() && errReturned.Interface() != nil && errReturned.Type().Name() == "error" { // FIXME: looks ugly - // TCK says: FIXME Expects entity.Failure, but gets lientAction.Action.Failure(Failure(commandId, msg))) - failure := &protocol.Failure{ - CommandId: cmd.GetId(), - Description: errReturned.Interface().(error).Error(), + if snapshotter, canSnapshot := entityContext.EntityInstance.Instance.(Snapshotter); canSnapshot { + snap, err := snapshotter.Snapshot() + if err != nil { + return nil, fmt.Errorf("getting a snapshot has failed: %v. %w", err, ErrFailure) } - if err := sendClientActionFailure(failure, server); err != nil { - return ErrSendFailure + // TODO: we expect a proto.Message but should support other formats + snapshot, err := marshalAny(snap) + if err != nil { + return nil, err } - return nil - } - // the reply - callReply, ok := called[0].Interface().(proto.Message) - if !ok { - // this should never happen - return fmt.Errorf("called return value at index 0 is no proto.Message") - } - typeUrl := fmt.Sprintf("%s/%s", protoAnyBase, proto.MessageName(callReply)) - marshal, err := proto.Marshal(callReply) - if err != nil { - return fmt.Errorf("unable to Marshal command reply: %w", ErrMarshal) + entityContext.EntityInstance.resetSnapshotEvery() + return snapshot, nil + } else { + // TODO: every entity should implement snapshotting, right? } + return nil, nil +} - // emitted events - events, err := esh.marshalEventsTo(entityValue) - if err != nil { - return err - } - err = sendEventSourcedReply(&protocol.EventSourcedReply{ - CommandId: cmd.GetId(), - ClientAction: &protocol.ClientAction{ - Action: &protocol.ClientAction_Reply{ - Reply: &protocol.Reply{ - Payload: &any.Any{ - TypeUrl: typeUrl, - Value: marshal, - }, - }, - }, - }, - Events: events, - }, server) - if err != nil { - return fmt.Errorf("%s, %w", err, ErrSend) +func checkUnary(methodByName reflect.Value) error { + if methodByName.Type().NumIn() != 2 { + return NewFailureError("method: %s is no unary method", methodByName.String()) } return nil } -func (_ *EventSourcedHandler) marshalEvent(evt interface{}) (*any.Any, error) { - // TODO: protobufs are expected here, but CloudState supports other formats - message, ok := evt.(proto.Message) - if !ok { - return nil, fmt.Errorf("got a non-proto message as event") - } - marshal, err := proto.Marshal(message) +// applyEvent applies an event to a local entity +func (esh EventSourcedHandler) applyEvent(entityInstance *EntityInstance, event interface{}) error { + payload, err := marshalAny(event) if err != nil { - return nil, fmt.Errorf("%s, %w", err, ErrMarshal) - } - return &any.Any{ - TypeUrl: fmt.Sprintf("%s/%s", protoAnyBase, proto.MessageName(message)), - Value: marshal, - }, nil -} - -// marshalEventsTo receives the events emitted through the handling of a command -// and marshals them to the event serialized form. -func (_ *EventSourcedHandler) marshalEventsTo(entityValue reflect.Value) ([]*any.Any, error) { - events := make([]*any.Any, 0) - if emitter, ok := entityValue.Interface().(EventEmitter); ok { - for _, evt := range emitter.Events() { - // TODO: protobufs are expected here, but CloudState supports other formats - message, ok := evt.(proto.Message) - if !ok { - return nil, fmt.Errorf("got a non-proto message as event") - } - marshal, err := proto.Marshal(message) - if err != nil { - return nil, fmt.Errorf("%s, %w", err, ErrMarshal) - } - events = append(events, - &any.Any{ - TypeUrl: fmt.Sprintf("%s/%s", protoAnyBase, proto.MessageName(message)), - Value: marshal, - }, - ) - } - emitter.Clear() + return err } - return events, nil + return esh.handleEvents(entityInstance, &protocol.EventSourcedEvent{Payload: payload}) } // handleEvents handles a list of events encoded as protobuf Any messages. @@ -389,17 +480,17 @@ func (_ *EventSourcedHandler) marshalEventsTo(entityValue reflect.Value) ([]*any // and snapshots is to use protobufs. CloudState will automatically detect if // an emitted event is a protobuf, and serialize it as such. For other // serialization options, including JSON, see Serialization. -func (_ *EventSourcedHandler) handleEvents(entityValue reflect.Value, events ...*any.Any) error { - eventHandler, implementsEventHandler := entityValue.Interface().(EventHandler) +func (EventSourcedHandler) handleEvents(entityInstance *EntityInstance, events ...*protocol.EventSourcedEvent) error { + eventHandler, implementsEventHandler := entityInstance.Instance.(EventHandler) for _, event := range events { // TODO: here's the point where events can be protobufs, serialized as json or other formats - msgName := strings.TrimPrefix(event.GetTypeUrl(), protoAnyBase+"/") + msgName := strings.TrimPrefix(event.Payload.GetTypeUrl(), protoAnyBase+"/") messageType := proto.MessageType(msgName) if messageType.Kind() == reflect.Ptr { // get a zero-ed message of this type if message, ok := reflect.New(messageType.Elem()).Interface().(proto.Message); ok { // and marshal onto it what we got as an any.Any onto it - err := proto.Unmarshal(event.Value, message) + err := proto.Unmarshal(event.Payload.Value, message) if err != nil { return fmt.Errorf("%s, %w", err, ErrMarshal) } else { @@ -417,6 +508,7 @@ func (_ *EventSourcedHandler) handleEvents(entityValue reflect.Value, events ... // to the type of the message received. if !handled { // find a concrete handling method + entityValue := reflect.ValueOf(entityInstance.Instance) entityType := entityValue.Type() for tmi := 0; tmi < entityType.NumMethod(); tmi++ { method := entityType.Method(tmi) diff --git a/cloudstate/eventsourced_reply.go b/cloudstate/eventsourced_reply.go index d1053c8..2e6899f 100644 --- a/cloudstate/eventsourced_reply.go +++ b/cloudstate/eventsourced_reply.go @@ -17,6 +17,7 @@ package cloudstate import ( "errors" + "fmt" "github.com/cloudstateio/go-support/cloudstate/protocol" ) @@ -24,24 +25,91 @@ var ErrSendFailure = errors.New("unable to send a failure message") var ErrSend = errors.New("unable to send a message") var ErrMarshal = errors.New("unable to marshal a message") +var ErrFailure = errors.New("cloudstate failure") +var ErrClientActionFailure = errors.New("cloudstate client action failure") + +func NewFailureError(format string, a ...interface{}) error { + if len(a) != 0 { + errorf := fmt.Errorf(fmt.Sprintf(format, a)+". %w", ErrFailure) + return errorf + } else { + errorf := fmt.Errorf(format+". %w", ErrFailure) + return errorf + } +} + +func NewClientActionFailureError(format string, a ...interface{}) error { + errorf := fmt.Errorf(fmt.Sprintf(format, a)+". %w", ErrClientActionFailure) + return errorf +} + +type ProtocolFailure struct { + protocol.Failure + err error +} + +func (f ProtocolFailure) Error() string { + return f.err.Error() +} + +func (f ProtocolFailure) Unwrap() error { + return f.err +} + +func NewProtocolFailure(failure protocol.Failure) error { + return ProtocolFailure{ + Failure: failure, + err: ErrFailure, + } +} + +// handleFailure checks if a CloudState failure or client action failure should +// be sent to the proxy, otherwise handleFailure returns the original failure +func handleFailure(failure error, server protocol.EventSourced_HandleServer, cmdId int64) error { + if errors.Is(failure, ErrFailure) { + // TCK says: Failure was not received, or not well-formed: Failure(Failure(0,cloudstate failure)) was not reply (CloudStateTCK.scala:339) + // FIXME: why not getting the failure from the ProtocolFailure + //return sendFailure(&protocol.Failure{Description: failure.Error()}, server) + return sendClientActionFailure(&protocol.Failure{ + CommandId: cmdId, + Description: failure.Error(), + }, server) + } + if errors.Is(failure, ErrClientActionFailure) { + return sendClientActionFailure(&protocol.Failure{ + CommandId: cmdId, + Description: failure.Error(), + }, server) + } + return failure +} + func sendEventSourcedReply(reply *protocol.EventSourcedReply, server protocol.EventSourced_HandleServer) error { - return server.Send(&protocol.EventSourcedStreamOut{ + err := server.Send(&protocol.EventSourcedStreamOut{ Message: &protocol.EventSourcedStreamOut_Reply{ Reply: reply, }, }) + if err != nil { + return fmt.Errorf("%s, %w", err, ErrSend) + } + return err } func sendFailure(failure *protocol.Failure, server protocol.EventSourced_HandleServer) error { - return server.Send(&protocol.EventSourcedStreamOut{ + err := server.Send(&protocol.EventSourcedStreamOut{ Message: &protocol.EventSourcedStreamOut_Failure{ Failure: failure, }, }) + if err != nil { + err = fmt.Errorf("%s, %w", err, ErrSendFailure) + } + return err } func sendClientActionFailure(failure *protocol.Failure, server protocol.EventSourced_HandleServer) error { - return server.Send(&protocol.EventSourcedStreamOut{ + err := server.Send(&protocol.EventSourcedStreamOut{ Message: &protocol.EventSourcedStreamOut_Reply{ Reply: &protocol.EventSourcedReply{ CommandId: failure.CommandId, @@ -53,4 +121,8 @@ func sendClientActionFailure(failure *protocol.Failure, server protocol.EventSou }, }, }) + if err != nil { + err = fmt.Errorf("%s, %w", err, ErrSendFailure) + } + return err } diff --git a/cloudstate/eventsourced_test.go b/cloudstate/eventsourced_test.go index bbd36fc..48e5e60 100644 --- a/cloudstate/eventsourced_test.go +++ b/cloudstate/eventsourced_test.go @@ -34,6 +34,14 @@ type TestEntity struct { EventEmitter } +func (te *TestEntity) Snapshot() (snapshot interface{}, err error) { + panic("implement me") +} + +func (te *TestEntity) HandleSnapshot(snapshot interface{}) (handled bool, err error) { + panic("implement me") +} + func (te *TestEntity) IncrementBy(n int64) (int64, error) { te.Value += n return te.Value, nil @@ -169,7 +177,7 @@ func newHandler(t *testing.T) *EventSourcedHandler { Entity: (*TestEntity)(nil), ServiceName: "TestEventSourcedHandler-Service", SnapshotEvery: 0, - once: sync.Once{}, + registerOnce: sync.Once{}, } err := entity.initZeroValue() if err != nil { @@ -220,11 +228,11 @@ func TestErrSend(t *testing.T) { func TestEventSourcedHandlerHandlesCommandAndEvents(t *testing.T) { handler := newHandler(t) if testEntity.Value >= 0 { - t.Errorf("testEntity.Value should be <0 but was not: %+v", testEntity) + t.Fatalf("testEntity.Value should be <0 but was not: %+v", testEntity) } initHandler(handler, t) if testEntity.Value != 0 { - t.Errorf("testEntity.Value should be 0 but was not: %+v", testEntity) + t.Fatalf("testEntity.Value should be 0 but was not: %+v", testEntity) } incrementedTo := int64(7) incrCmdValue, err := marshal(&IncrementByCommand{Amount: incrementedTo}, t) @@ -239,15 +247,15 @@ func TestEventSourcedHandlerHandlesCommandAndEvents(t *testing.T) { } err = handler.handleCommand(&incrCommand, TestEventSourcedHandleServer{}) if err != nil { - t.Errorf("%v", err) + t.Fatalf("%v", err) } if testEntity.Value != incrementedTo { - t.Errorf("testEntity.Value != incrementedTo") + t.Fatalf("testEntity.Value: (%v) != incrementedTo: (%v)", testEntity.Value, incrementedTo) } decrCmdValue, err := proto.Marshal(&DecrementByCommand{Amount: incrementedTo}) if err != nil { - t.Errorf("%v", err) + t.Fatalf("%v", err) } decrCommand := protocol.Command{ EntityId: "entity-0", @@ -260,10 +268,10 @@ func TestEventSourcedHandlerHandlesCommandAndEvents(t *testing.T) { } err = handler.handleCommand(&decrCommand, TestEventSourcedHandleServer{}) if err != nil { - t.Errorf("%v", err) + t.Fatalf("%v", err) } if testEntity.Value != 0 { - t.Errorf("testEntity.Value != 0") + t.Fatalf("testEntity.Value != 0") } } diff --git a/cloudstate/marshal_proto.go b/cloudstate/marshal_proto.go new file mode 100644 index 0000000..ed790b7 --- /dev/null +++ b/cloudstate/marshal_proto.go @@ -0,0 +1,56 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstate + +import ( + "fmt" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes/any" +) + +// marshalAny marshals a proto.Message to a any.Any value +func marshalAny(pb interface{}) (*any.Any, error) { + // TODO: protobufs are expected here, but CloudState supports other formats + message, ok := pb.(proto.Message) + if !ok { + return nil, fmt.Errorf("got a non-proto message as protobuf: %v", pb) + } + bytes, err := proto.Marshal(message) + if err != nil { + return nil, fmt.Errorf("%s, %w", err, ErrMarshal) + } + return &any.Any{ + TypeUrl: fmt.Sprintf("%s/%s", protoAnyBase, proto.MessageName(message)), + Value: bytes, + }, nil +} + +// marshalEventsAny receives the events emitted through the handling of a command +// and marshals them to the event serialized form. +func marshalEventsAny(entityContext *EntityInstanceContext) ([]*any.Any, error) { + events := make([]*any.Any, 0) + if emitter, ok := entityContext.EntityInstance.Instance.(EventEmitter); ok { + for _, evt := range emitter.Events() { + event, err := marshalAny(evt) + if err != nil { + return nil, err + } + events = append(events, event) + } + emitter.Clear() + } + return events, nil +} diff --git a/cloudstate/marshal_proto_test.go b/cloudstate/marshal_proto_test.go new file mode 100644 index 0000000..2903054 --- /dev/null +++ b/cloudstate/marshal_proto_test.go @@ -0,0 +1,41 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstate + +import ( + "fmt" + "github.com/golang/protobuf/proto" + "testing" +) + +func TestMarshalAnyProto(t *testing.T) { + event := IncrementByEvent{Value: 29} + any, err := marshalAny(&event) + if err != nil { + t.Fatalf("failed to marshalAny: %v", err) + } + expected := fmt.Sprintf("%s/%s", protoAnyBase, "IncrementByEvent") + if expected != any.GetTypeUrl() { + t.Fatalf("any.GetTypeUrl: %s is not: %s", any.GetTypeUrl(), expected) + } + event2 := &IncrementByEvent{} + if err := proto.Unmarshal(any.Value, event2); err != nil { + t.Fatalf("%v", err) + } + if event2.Value != event.Value { + t.Fatalf("event2.Value: %d != event.Value: %d", event2.Value, event.Value) + } +} diff --git a/go.sum b/go.sum index 7bb2398..72cc347 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= diff --git a/protobuf/example/shoppingcart/persistence/domain.proto b/protobuf/example/shoppingcart/persistence/domain.proto new file mode 100644 index 0000000..c827b80 --- /dev/null +++ b/protobuf/example/shoppingcart/persistence/domain.proto @@ -0,0 +1,27 @@ +// These are the messages that get persisted - the events, plus the current state (Cart) for snapshots. +syntax = "proto3"; + +package com.example.shoppingcart.persistence; + +option go_package = "persistence"; + +message LineItem { + string productId = 1; + string name = 2; + int32 quantity = 3; +} + +// The item added event. +message ItemAdded { + LineItem item = 1; +} + +// The item removed event. +message ItemRemoved { + string productId = 1; +} + +// The shopping cart state. +message Cart { + repeated LineItem items = 1; +} diff --git a/protobuf/example/shoppingcart/shoppingcart.proto b/protobuf/example/shoppingcart/shoppingcart.proto new file mode 100644 index 0000000..f7b3cf6 --- /dev/null +++ b/protobuf/example/shoppingcart/shoppingcart.proto @@ -0,0 +1,60 @@ +// This is the public API offered by the shopping cart entity. +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "cloudstate/entity_key.proto"; +import "google/api/annotations.proto"; +import "google/api/http.proto"; + +package com.example.shoppingcart; + +option go_package = "tck/shoppingcart"; + +message AddLineItem { + string user_id = 1 [(.cloudstate.entity_key) = true]; + string product_id = 2; + string name = 3; + int32 quantity = 4; +} + +message RemoveLineItem { + string user_id = 1 [(.cloudstate.entity_key) = true]; + string product_id = 2; +} + +message GetShoppingCart { + string user_id = 1 [(.cloudstate.entity_key) = true]; +} + +message LineItem { + string product_id = 1; + string name = 2; + int32 quantity = 3; +} + +message Cart { + repeated LineItem items = 1; +} + +service ShoppingCart { + rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/cart/{user_id}/items/add", + body: "*", + }; + } + + rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { + option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; + } + + rpc GetCart(GetShoppingCart) returns (Cart) { + option (google.api.http) = { + get: "/carts/{user_id}", + additional_bindings: { + get: "/carts/{user_id}/items", + response_body: "items" + } + }; + } +} diff --git a/tck/cmd/tck_shoppingcart/shoppingcart.go b/tck/cmd/tck_shoppingcart/shoppingcart.go new file mode 100644 index 0000000..ce3b008 --- /dev/null +++ b/tck/cmd/tck_shoppingcart/shoppingcart.go @@ -0,0 +1,194 @@ +// +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main implements an event sourced entity shopping cart example +package main + +import ( + "context" + "errors" + "fmt" + "github.com/cloudstateio/go-support/cloudstate" + "github.com/cloudstateio/go-support/tck/shoppingcart" + domain "github.com/cloudstateio/go-support/tck/shoppingcart/persistence" + "github.com/golang/protobuf/ptypes/empty" + "log" +) + +// main creates a CloudState instance and registers the ShoppingCart +// as a event sourced entity. +func main() { + cloudState := cloudstate.NewCloudState(&cloudstate.Options{ + ServiceName: "shopping-cart", + ServiceVersion: "0.1.0", + }) + err := cloudState.Register( + &cloudstate.EventSourcedEntity{ + Entity: (*ShoppingCart)(nil), + ServiceName: "com.example.shoppingcart.ShoppingCart", + }, + cloudstate.DescriptorConfig{ + Service: "shoppingcart/shoppingcart.proto", + }.AddDomainDescriptor("domain.proto"), + ) + if err != nil { + log.Fatalf("CloudState failed to register entity: %v", err) + } + err = cloudState.Run() + if err != nil { + log.Fatalf("CloudState failed to run: %v", err) + } +} + +// A CloudState event sourced entity. +type ShoppingCart struct { + // our domain object + cart []*domain.LineItem + // as an Emitter we can emit events + cloudstate.EventEmitter +} + +// New implements EntityInitializer and returns a new +// and initialized instance of the ShoppingCart entity. +func (sc ShoppingCart) New() interface{} { + return NewShoppingCart() +} + +// NewShoppingCart returns a new and initialized +// instance of the ShoppingCart entity. +func NewShoppingCart() *ShoppingCart { + return &ShoppingCart{ + cart: make([]*domain.LineItem, 0), + EventEmitter: cloudstate.NewEmitter(), // TODO: the EventEmitter could be provided by the event sourced handler + } +} + +// ItemAdded is a event handler function for the ItemAdded event. +func (sc *ShoppingCart) ItemAdded(added *domain.ItemAdded) error { // TODO: enable handling for values + if item, _ := sc.find(added.Item.ProductId); item != nil { + item.Quantity += added.Item.Quantity + } else { + sc.cart = append(sc.cart, &domain.LineItem{ + ProductId: added.Item.ProductId, + Name: added.Item.Name, + Quantity: added.Item.Quantity, + }) + } + return nil +} + +// ItemRemoved is a event handler function for the ItemRemoved event. +func (sc *ShoppingCart) ItemRemoved(removed *domain.ItemRemoved) error { + if !sc.remove(removed.ProductId) { + // this should never happen + return errors.New("unable to remove product") + } + return nil +} + +// Handle lets us handle events by ourselves. +// +// returns handle set to true if we have handled the event +// and any error that happened during the handling +func (sc *ShoppingCart) HandleEvent(event interface{}) (handled bool, err error) { + switch e := event.(type) { + case *domain.ItemAdded: + return true, sc.ItemAdded(e) + //case *domain.ItemRemoved: + // *domain.ItemRemoved is handled by reflection + default: + return false, nil + } +} + +// AddItem implements the AddItem command handling of the shopping cart service. +func (sc *ShoppingCart) AddItem(c context.Context, li *shoppingcart.AddLineItem) (*empty.Empty, error) { + if li.GetQuantity() <= 0 { + return nil, fmt.Errorf("cannot add negative quantity of to item %s", li.GetProductId()) + } + sc.Emit(&domain.ItemAdded{ + Item: &domain.LineItem{ + ProductId: li.ProductId, + Name: li.Name, + Quantity: li.Quantity, + }}) + return &empty.Empty{}, nil +} + +// RemoveItem implements the RemoveItem command handling of the shopping cart service. +func (sc *ShoppingCart) RemoveItem(c context.Context, li *shoppingcart.RemoveLineItem) (*empty.Empty, error) { + if item, _ := sc.find(li.GetProductId()); item == nil { + return nil, fmt.Errorf("cannot remove item %s because it is not in the cart", li.GetProductId()) + } + sc.Emit(&domain.ItemRemoved{ProductId: li.ProductId}) + return &empty.Empty{}, nil +} + +// GetCart implements the GetCart command handling of the shopping cart service. +func (sc *ShoppingCart) GetCart(c context.Context, _ *shoppingcart.GetShoppingCart) (*shoppingcart.Cart, error) { + cart := &shoppingcart.Cart{} + for _, item := range sc.cart { + cart.Items = append(cart.Items, &shoppingcart.LineItem{ + ProductId: item.ProductId, + Name: item.Name, + Quantity: item.Quantity, + }) + } + return cart, nil +} + +func (sc *ShoppingCart) Snapshot() (snapshot interface{}, err error) { + return domain.Cart{ + Items: append(make([]*domain.LineItem, len(sc.cart)), sc.cart...), + }, nil +} + +func (sc *ShoppingCart) HandleSnapshot(snapshot interface{}) (handled bool, err error) { + switch value := snapshot.(type) { + case domain.Cart: + sc.cart = append(sc.cart[:0], value.Items...) + return true, nil + default: + return false, nil + } +} + +// find finds a product in the shopping cart by productId and returns it as a LineItem. +func (sc *ShoppingCart) find(productId string) (item *domain.LineItem, index int) { + for i, item := range sc.cart { + if productId == item.ProductId { + return item, i + } + } + return nil, 0 +} + +// remove removes a product from the shopping cart. +// +// A ok flag is returned to indicate that the product was present and removed. +func (sc *ShoppingCart) remove(productId string) (ok bool) { + if item, i := sc.find(productId); item != nil { + // remove and re-slice + copy(sc.cart[i:], sc.cart[i+1:]) + sc.cart = sc.cart[:len(sc.cart)-1] + return true + } else { + return false + } +} + +func init() { + log.SetFlags(log.LstdFlags | log.Lmicroseconds) +} diff --git a/tck/shoppingcart/persistence/domain.pb.go b/tck/shoppingcart/persistence/domain.pb.go new file mode 100644 index 0000000..a689beb --- /dev/null +++ b/tck/shoppingcart/persistence/domain.pb.go @@ -0,0 +1,223 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: domain.proto + +package persistence + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type LineItem struct { + ProductId string `protobuf:"bytes,1,opt,name=productId,proto3" json:"productId,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Quantity int32 `protobuf:"varint,3,opt,name=quantity,proto3" json:"quantity,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *LineItem) Reset() { *m = LineItem{} } +func (m *LineItem) String() string { return proto.CompactTextString(m) } +func (*LineItem) ProtoMessage() {} +func (*LineItem) Descriptor() ([]byte, []int) { + return fileDescriptor_73e6234e76dbdb84, []int{0} +} + +func (m *LineItem) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_LineItem.Unmarshal(m, b) +} +func (m *LineItem) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_LineItem.Marshal(b, m, deterministic) +} +func (m *LineItem) XXX_Merge(src proto.Message) { + xxx_messageInfo_LineItem.Merge(m, src) +} +func (m *LineItem) XXX_Size() int { + return xxx_messageInfo_LineItem.Size(m) +} +func (m *LineItem) XXX_DiscardUnknown() { + xxx_messageInfo_LineItem.DiscardUnknown(m) +} + +var xxx_messageInfo_LineItem proto.InternalMessageInfo + +func (m *LineItem) GetProductId() string { + if m != nil { + return m.ProductId + } + return "" +} + +func (m *LineItem) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *LineItem) GetQuantity() int32 { + if m != nil { + return m.Quantity + } + return 0 +} + +// The item added event. +type ItemAdded struct { + Item *LineItem `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ItemAdded) Reset() { *m = ItemAdded{} } +func (m *ItemAdded) String() string { return proto.CompactTextString(m) } +func (*ItemAdded) ProtoMessage() {} +func (*ItemAdded) Descriptor() ([]byte, []int) { + return fileDescriptor_73e6234e76dbdb84, []int{1} +} + +func (m *ItemAdded) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ItemAdded.Unmarshal(m, b) +} +func (m *ItemAdded) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ItemAdded.Marshal(b, m, deterministic) +} +func (m *ItemAdded) XXX_Merge(src proto.Message) { + xxx_messageInfo_ItemAdded.Merge(m, src) +} +func (m *ItemAdded) XXX_Size() int { + return xxx_messageInfo_ItemAdded.Size(m) +} +func (m *ItemAdded) XXX_DiscardUnknown() { + xxx_messageInfo_ItemAdded.DiscardUnknown(m) +} + +var xxx_messageInfo_ItemAdded proto.InternalMessageInfo + +func (m *ItemAdded) GetItem() *LineItem { + if m != nil { + return m.Item + } + return nil +} + +// The item removed event. +type ItemRemoved struct { + ProductId string `protobuf:"bytes,1,opt,name=productId,proto3" json:"productId,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ItemRemoved) Reset() { *m = ItemRemoved{} } +func (m *ItemRemoved) String() string { return proto.CompactTextString(m) } +func (*ItemRemoved) ProtoMessage() {} +func (*ItemRemoved) Descriptor() ([]byte, []int) { + return fileDescriptor_73e6234e76dbdb84, []int{2} +} + +func (m *ItemRemoved) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ItemRemoved.Unmarshal(m, b) +} +func (m *ItemRemoved) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ItemRemoved.Marshal(b, m, deterministic) +} +func (m *ItemRemoved) XXX_Merge(src proto.Message) { + xxx_messageInfo_ItemRemoved.Merge(m, src) +} +func (m *ItemRemoved) XXX_Size() int { + return xxx_messageInfo_ItemRemoved.Size(m) +} +func (m *ItemRemoved) XXX_DiscardUnknown() { + xxx_messageInfo_ItemRemoved.DiscardUnknown(m) +} + +var xxx_messageInfo_ItemRemoved proto.InternalMessageInfo + +func (m *ItemRemoved) GetProductId() string { + if m != nil { + return m.ProductId + } + return "" +} + +// The shopping cart state. +type Cart struct { + Items []*LineItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Cart) Reset() { *m = Cart{} } +func (m *Cart) String() string { return proto.CompactTextString(m) } +func (*Cart) ProtoMessage() {} +func (*Cart) Descriptor() ([]byte, []int) { + return fileDescriptor_73e6234e76dbdb84, []int{3} +} + +func (m *Cart) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Cart.Unmarshal(m, b) +} +func (m *Cart) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Cart.Marshal(b, m, deterministic) +} +func (m *Cart) XXX_Merge(src proto.Message) { + xxx_messageInfo_Cart.Merge(m, src) +} +func (m *Cart) XXX_Size() int { + return xxx_messageInfo_Cart.Size(m) +} +func (m *Cart) XXX_DiscardUnknown() { + xxx_messageInfo_Cart.DiscardUnknown(m) +} + +var xxx_messageInfo_Cart proto.InternalMessageInfo + +func (m *Cart) GetItems() []*LineItem { + if m != nil { + return m.Items + } + return nil +} + +func init() { + proto.RegisterType((*LineItem)(nil), "com.example.shoppingcart.persistence.LineItem") + proto.RegisterType((*ItemAdded)(nil), "com.example.shoppingcart.persistence.ItemAdded") + proto.RegisterType((*ItemRemoved)(nil), "com.example.shoppingcart.persistence.ItemRemoved") + proto.RegisterType((*Cart)(nil), "com.example.shoppingcart.persistence.Cart") +} + +func init() { proto.RegisterFile("domain.proto", fileDescriptor_73e6234e76dbdb84) } + +var fileDescriptor_73e6234e76dbdb84 = []byte{ + // 223 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x90, 0x3f, 0x4b, 0x04, 0x31, + 0x10, 0xc5, 0x89, 0xb7, 0x27, 0xb7, 0xb3, 0xda, 0xa4, 0x0a, 0x62, 0xb1, 0x04, 0x8b, 0x03, 0x21, + 0x85, 0x7e, 0x02, 0x4f, 0x9b, 0x85, 0x05, 0x61, 0x2b, 0xb1, 0x8b, 0xc9, 0xa0, 0x01, 0xf3, 0xc7, + 0x64, 0x56, 0xf4, 0xdb, 0xcb, 0x46, 0xfc, 0x53, 0x89, 0x5c, 0x37, 0x6f, 0x1e, 0xef, 0xcd, 0x8f, + 0x81, 0x23, 0x1b, 0xbd, 0x76, 0x41, 0xa5, 0x1c, 0x29, 0xf2, 0x33, 0x13, 0xbd, 0xc2, 0x37, 0xed, + 0xd3, 0x33, 0xaa, 0xf2, 0x14, 0x53, 0x72, 0xe1, 0xd1, 0xe8, 0x4c, 0x2a, 0x61, 0x2e, 0xae, 0x10, + 0x06, 0x83, 0xf2, 0x0e, 0x36, 0xa3, 0x0b, 0x38, 0x10, 0x7a, 0x7e, 0x0a, 0x6d, 0xca, 0xd1, 0xce, + 0x86, 0x06, 0x2b, 0x58, 0xcf, 0xb6, 0xed, 0xf4, 0xb3, 0xe0, 0x1c, 0x9a, 0xa0, 0x3d, 0x8a, 0x83, + 0x6a, 0xd4, 0x99, 0x9f, 0xc0, 0xe6, 0x65, 0xd6, 0x81, 0x1c, 0xbd, 0x8b, 0x55, 0xcf, 0xb6, 0xeb, + 0xe9, 0x5b, 0xcb, 0x5b, 0x68, 0x97, 0xd6, 0x2b, 0x6b, 0xd1, 0xf2, 0x1d, 0x34, 0x8e, 0xd0, 0xd7, + 0xd6, 0xee, 0x42, 0xa9, 0xff, 0xb0, 0xa9, 0x2f, 0xb0, 0xa9, 0x66, 0xe5, 0x39, 0x74, 0x55, 0xa1, + 0x8f, 0xaf, 0x68, 0xff, 0xa6, 0x95, 0x23, 0x34, 0xd7, 0x3a, 0x13, 0xbf, 0x81, 0xf5, 0x12, 0x2e, + 0x82, 0xf5, 0xab, 0x3d, 0x2e, 0x7f, 0x86, 0x77, 0xc7, 0xf7, 0xdd, 0x2f, 0xfb, 0xe1, 0xb0, 0x7e, + 0xf8, 0xf2, 0x23, 0x00, 0x00, 0xff, 0xff, 0xc7, 0xe9, 0xbb, 0x2b, 0x71, 0x01, 0x00, 0x00, +} diff --git a/tck/shoppingcart/shoppingcart.pb.go b/tck/shoppingcart/shoppingcart.pb.go new file mode 100644 index 0000000..1e4037d --- /dev/null +++ b/tck/shoppingcart/shoppingcart.pb.go @@ -0,0 +1,466 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: shoppingcart/shoppingcart.proto + +package shoppingcart + +import ( + context "context" + fmt "fmt" + _ "github.com/cloudstateio/go-support/cloudstate/protocol" + proto "github.com/golang/protobuf/proto" + empty "github.com/golang/protobuf/ptypes/empty" + _ "google.golang.org/genproto/googleapis/api/annotations" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type AddLineItem struct { + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ProductId string `protobuf:"bytes,2,opt,name=product_id,json=productId,proto3" json:"product_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Quantity int32 `protobuf:"varint,4,opt,name=quantity,proto3" json:"quantity,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AddLineItem) Reset() { *m = AddLineItem{} } +func (m *AddLineItem) String() string { return proto.CompactTextString(m) } +func (*AddLineItem) ProtoMessage() {} +func (*AddLineItem) Descriptor() ([]byte, []int) { + return fileDescriptor_230c614558c2d7f8, []int{0} +} + +func (m *AddLineItem) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AddLineItem.Unmarshal(m, b) +} +func (m *AddLineItem) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AddLineItem.Marshal(b, m, deterministic) +} +func (m *AddLineItem) XXX_Merge(src proto.Message) { + xxx_messageInfo_AddLineItem.Merge(m, src) +} +func (m *AddLineItem) XXX_Size() int { + return xxx_messageInfo_AddLineItem.Size(m) +} +func (m *AddLineItem) XXX_DiscardUnknown() { + xxx_messageInfo_AddLineItem.DiscardUnknown(m) +} + +var xxx_messageInfo_AddLineItem proto.InternalMessageInfo + +func (m *AddLineItem) GetUserId() string { + if m != nil { + return m.UserId + } + return "" +} + +func (m *AddLineItem) GetProductId() string { + if m != nil { + return m.ProductId + } + return "" +} + +func (m *AddLineItem) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *AddLineItem) GetQuantity() int32 { + if m != nil { + return m.Quantity + } + return 0 +} + +type RemoveLineItem struct { + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ProductId string `protobuf:"bytes,2,opt,name=product_id,json=productId,proto3" json:"product_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RemoveLineItem) Reset() { *m = RemoveLineItem{} } +func (m *RemoveLineItem) String() string { return proto.CompactTextString(m) } +func (*RemoveLineItem) ProtoMessage() {} +func (*RemoveLineItem) Descriptor() ([]byte, []int) { + return fileDescriptor_230c614558c2d7f8, []int{1} +} + +func (m *RemoveLineItem) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RemoveLineItem.Unmarshal(m, b) +} +func (m *RemoveLineItem) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RemoveLineItem.Marshal(b, m, deterministic) +} +func (m *RemoveLineItem) XXX_Merge(src proto.Message) { + xxx_messageInfo_RemoveLineItem.Merge(m, src) +} +func (m *RemoveLineItem) XXX_Size() int { + return xxx_messageInfo_RemoveLineItem.Size(m) +} +func (m *RemoveLineItem) XXX_DiscardUnknown() { + xxx_messageInfo_RemoveLineItem.DiscardUnknown(m) +} + +var xxx_messageInfo_RemoveLineItem proto.InternalMessageInfo + +func (m *RemoveLineItem) GetUserId() string { + if m != nil { + return m.UserId + } + return "" +} + +func (m *RemoveLineItem) GetProductId() string { + if m != nil { + return m.ProductId + } + return "" +} + +type GetShoppingCart struct { + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetShoppingCart) Reset() { *m = GetShoppingCart{} } +func (m *GetShoppingCart) String() string { return proto.CompactTextString(m) } +func (*GetShoppingCart) ProtoMessage() {} +func (*GetShoppingCart) Descriptor() ([]byte, []int) { + return fileDescriptor_230c614558c2d7f8, []int{2} +} + +func (m *GetShoppingCart) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetShoppingCart.Unmarshal(m, b) +} +func (m *GetShoppingCart) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetShoppingCart.Marshal(b, m, deterministic) +} +func (m *GetShoppingCart) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetShoppingCart.Merge(m, src) +} +func (m *GetShoppingCart) XXX_Size() int { + return xxx_messageInfo_GetShoppingCart.Size(m) +} +func (m *GetShoppingCart) XXX_DiscardUnknown() { + xxx_messageInfo_GetShoppingCart.DiscardUnknown(m) +} + +var xxx_messageInfo_GetShoppingCart proto.InternalMessageInfo + +func (m *GetShoppingCart) GetUserId() string { + if m != nil { + return m.UserId + } + return "" +} + +type LineItem struct { + ProductId string `protobuf:"bytes,1,opt,name=product_id,json=productId,proto3" json:"product_id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Quantity int32 `protobuf:"varint,3,opt,name=quantity,proto3" json:"quantity,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *LineItem) Reset() { *m = LineItem{} } +func (m *LineItem) String() string { return proto.CompactTextString(m) } +func (*LineItem) ProtoMessage() {} +func (*LineItem) Descriptor() ([]byte, []int) { + return fileDescriptor_230c614558c2d7f8, []int{3} +} + +func (m *LineItem) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_LineItem.Unmarshal(m, b) +} +func (m *LineItem) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_LineItem.Marshal(b, m, deterministic) +} +func (m *LineItem) XXX_Merge(src proto.Message) { + xxx_messageInfo_LineItem.Merge(m, src) +} +func (m *LineItem) XXX_Size() int { + return xxx_messageInfo_LineItem.Size(m) +} +func (m *LineItem) XXX_DiscardUnknown() { + xxx_messageInfo_LineItem.DiscardUnknown(m) +} + +var xxx_messageInfo_LineItem proto.InternalMessageInfo + +func (m *LineItem) GetProductId() string { + if m != nil { + return m.ProductId + } + return "" +} + +func (m *LineItem) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *LineItem) GetQuantity() int32 { + if m != nil { + return m.Quantity + } + return 0 +} + +type Cart struct { + Items []*LineItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Cart) Reset() { *m = Cart{} } +func (m *Cart) String() string { return proto.CompactTextString(m) } +func (*Cart) ProtoMessage() {} +func (*Cart) Descriptor() ([]byte, []int) { + return fileDescriptor_230c614558c2d7f8, []int{4} +} + +func (m *Cart) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Cart.Unmarshal(m, b) +} +func (m *Cart) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Cart.Marshal(b, m, deterministic) +} +func (m *Cart) XXX_Merge(src proto.Message) { + xxx_messageInfo_Cart.Merge(m, src) +} +func (m *Cart) XXX_Size() int { + return xxx_messageInfo_Cart.Size(m) +} +func (m *Cart) XXX_DiscardUnknown() { + xxx_messageInfo_Cart.DiscardUnknown(m) +} + +var xxx_messageInfo_Cart proto.InternalMessageInfo + +func (m *Cart) GetItems() []*LineItem { + if m != nil { + return m.Items + } + return nil +} + +func init() { + proto.RegisterType((*AddLineItem)(nil), "com.example.shoppingcart.AddLineItem") + proto.RegisterType((*RemoveLineItem)(nil), "com.example.shoppingcart.RemoveLineItem") + proto.RegisterType((*GetShoppingCart)(nil), "com.example.shoppingcart.GetShoppingCart") + proto.RegisterType((*LineItem)(nil), "com.example.shoppingcart.LineItem") + proto.RegisterType((*Cart)(nil), "com.example.shoppingcart.Cart") +} + +func init() { proto.RegisterFile("shoppingcart/shoppingcart.proto", fileDescriptor_230c614558c2d7f8) } + +var fileDescriptor_230c614558c2d7f8 = []byte{ + // 461 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0xc1, 0x6e, 0xd3, 0x40, + 0x10, 0xd5, 0x26, 0x69, 0xd3, 0x4e, 0x11, 0x54, 0x2b, 0x51, 0x19, 0x97, 0xb6, 0xd1, 0x0a, 0xa4, + 0x14, 0x24, 0x2f, 0xb4, 0x17, 0xe0, 0x44, 0x8b, 0x50, 0x15, 0x09, 0x71, 0x30, 0x27, 0x7a, 0xa9, + 0x36, 0xde, 0x21, 0xb5, 0x1a, 0x7b, 0x8d, 0x3d, 0x46, 0x54, 0x51, 0x0f, 0xf0, 0x03, 0x20, 0xc1, + 0xaf, 0xf0, 0x25, 0xfc, 0x02, 0x1f, 0x82, 0xbc, 0x76, 0xa8, 0x13, 0xb1, 0xe2, 0xc2, 0xcd, 0x3b, + 0x6f, 0x76, 0xde, 0x7b, 0xfb, 0xc6, 0xb0, 0x57, 0x9c, 0x9b, 0x2c, 0x8b, 0xd3, 0x49, 0xa4, 0x72, + 0x92, 0xed, 0x43, 0x90, 0xe5, 0x86, 0x0c, 0xf7, 0x22, 0x93, 0x04, 0xf8, 0x51, 0x25, 0xd9, 0x14, + 0x83, 0x36, 0xee, 0x6f, 0x4f, 0x8c, 0x99, 0x4c, 0x51, 0xda, 0xbe, 0x71, 0xf9, 0x4e, 0x62, 0x92, + 0xd1, 0x65, 0x7d, 0xcd, 0xdf, 0x8e, 0xa6, 0xa6, 0xd4, 0x05, 0x29, 0x42, 0x89, 0x29, 0xc5, 0x74, + 0x79, 0x76, 0x81, 0x73, 0xf0, 0x6e, 0x73, 0x53, 0x65, 0xb1, 0x54, 0x69, 0x6a, 0x48, 0x51, 0x6c, + 0xd2, 0xa2, 0x41, 0x6f, 0xb7, 0xd0, 0x73, 0xa2, 0xac, 0x2e, 0x8b, 0x19, 0x6c, 0x1c, 0x69, 0xfd, + 0x2a, 0x4e, 0x71, 0x44, 0x98, 0xf0, 0x1d, 0xe8, 0x97, 0x05, 0xe6, 0x67, 0xb1, 0xf6, 0xd8, 0x80, + 0x0d, 0xd7, 0x8f, 0x7b, 0x5f, 0x7f, 0x78, 0x2c, 0x5c, 0xad, 0x8a, 0x23, 0xcd, 0x77, 0x00, 0xb2, + 0xdc, 0xe8, 0x32, 0xa2, 0xaa, 0xa3, 0x53, 0x75, 0x84, 0xeb, 0x4d, 0x65, 0xa4, 0x39, 0x87, 0x5e, + 0xaa, 0x12, 0xf4, 0xba, 0x16, 0xb0, 0xdf, 0xdc, 0x87, 0xb5, 0xf7, 0xa5, 0xb2, 0x5a, 0xbd, 0xde, + 0x80, 0x0d, 0x57, 0xc2, 0x3f, 0x67, 0xf1, 0x1a, 0x6e, 0x86, 0x98, 0x98, 0x0f, 0xf8, 0x7f, 0xf8, + 0xc5, 0x23, 0xb8, 0x75, 0x82, 0xf4, 0xa6, 0x79, 0xce, 0x17, 0x2a, 0xa7, 0x7f, 0x0c, 0x14, 0x6f, + 0x61, 0xad, 0xc5, 0xdd, 0x1e, 0xce, 0x5c, 0xe6, 0x3a, 0x0e, 0x73, 0xdd, 0x25, 0x73, 0xcf, 0xa1, + 0x67, 0x15, 0x3c, 0x81, 0x95, 0x98, 0x30, 0x29, 0x3c, 0x36, 0xe8, 0x0e, 0x37, 0x0e, 0x44, 0xe0, + 0x8a, 0x3e, 0x98, 0x2b, 0x09, 0xeb, 0x0b, 0x07, 0xdf, 0xbb, 0x70, 0x63, 0xc1, 0x4c, 0x0a, 0xfd, + 0x23, 0xad, 0xad, 0xd8, 0xfb, 0xee, 0x31, 0xad, 0x3c, 0xfd, 0xad, 0xa0, 0x8e, 0x3d, 0x98, 0xaf, + 0x53, 0xf0, 0xb2, 0x5a, 0x27, 0x71, 0xef, 0xf3, 0xcf, 0x5f, 0xdf, 0x3a, 0xbb, 0xe2, 0x8e, 0xb4, + 0x1b, 0x3a, 0x6b, 0xde, 0xe8, 0x4a, 0x5a, 0x66, 0xa9, 0xb4, 0x7e, 0xc6, 0x1e, 0xf0, 0x4f, 0x0c, + 0xa0, 0x0e, 0xc8, 0x72, 0x0e, 0xdd, 0x9c, 0x8b, 0x31, 0x3a, 0x69, 0x1f, 0x5b, 0xda, 0x87, 0x62, + 0xff, 0xef, 0xb4, 0xb3, 0xeb, 0xf7, 0xbf, 0x92, 0xb9, 0x1d, 0xc9, 0xbf, 0x30, 0xe8, 0x9f, 0x20, + 0x59, 0xff, 0xfb, 0x6e, 0x01, 0x4b, 0xb9, 0xfb, 0xbb, 0xee, 0xd6, 0x0a, 0x17, 0x4f, 0xad, 0x92, + 0x43, 0xbe, 0x69, 0x95, 0x14, 0xd7, 0x52, 0x4e, 0xf7, 0xf8, 0xd6, 0x72, 0xad, 0x96, 0x37, 0xae, + 0x63, 0x39, 0xe6, 0xa7, 0x9b, 0x14, 0x5d, 0x2c, 0xfc, 0xd5, 0xe3, 0x55, 0x6b, 0xf4, 0xf0, 0x77, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xee, 0x63, 0x38, 0x32, 0xf9, 0x03, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// ShoppingCartClient is the client API for ShoppingCart service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type ShoppingCartClient interface { + AddItem(ctx context.Context, in *AddLineItem, opts ...grpc.CallOption) (*empty.Empty, error) + RemoveItem(ctx context.Context, in *RemoveLineItem, opts ...grpc.CallOption) (*empty.Empty, error) + GetCart(ctx context.Context, in *GetShoppingCart, opts ...grpc.CallOption) (*Cart, error) +} + +type shoppingCartClient struct { + cc *grpc.ClientConn +} + +func NewShoppingCartClient(cc *grpc.ClientConn) ShoppingCartClient { + return &shoppingCartClient{cc} +} + +func (c *shoppingCartClient) AddItem(ctx context.Context, in *AddLineItem, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/com.example.shoppingcart.ShoppingCart/AddItem", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *shoppingCartClient) RemoveItem(ctx context.Context, in *RemoveLineItem, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/com.example.shoppingcart.ShoppingCart/RemoveItem", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *shoppingCartClient) GetCart(ctx context.Context, in *GetShoppingCart, opts ...grpc.CallOption) (*Cart, error) { + out := new(Cart) + err := c.cc.Invoke(ctx, "/com.example.shoppingcart.ShoppingCart/GetCart", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ShoppingCartServer is the server API for ShoppingCart service. +type ShoppingCartServer interface { + AddItem(context.Context, *AddLineItem) (*empty.Empty, error) + RemoveItem(context.Context, *RemoveLineItem) (*empty.Empty, error) + GetCart(context.Context, *GetShoppingCart) (*Cart, error) +} + +// UnimplementedShoppingCartServer can be embedded to have forward compatible implementations. +type UnimplementedShoppingCartServer struct { +} + +func (*UnimplementedShoppingCartServer) AddItem(ctx context.Context, req *AddLineItem) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddItem not implemented") +} +func (*UnimplementedShoppingCartServer) RemoveItem(ctx context.Context, req *RemoveLineItem) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveItem not implemented") +} +func (*UnimplementedShoppingCartServer) GetCart(ctx context.Context, req *GetShoppingCart) (*Cart, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCart not implemented") +} + +func RegisterShoppingCartServer(s *grpc.Server, srv ShoppingCartServer) { + s.RegisterService(&_ShoppingCart_serviceDesc, srv) +} + +func _ShoppingCart_AddItem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddLineItem) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShoppingCartServer).AddItem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/com.example.shoppingcart.ShoppingCart/AddItem", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShoppingCartServer).AddItem(ctx, req.(*AddLineItem)) + } + return interceptor(ctx, in, info, handler) +} + +func _ShoppingCart_RemoveItem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveLineItem) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShoppingCartServer).RemoveItem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/com.example.shoppingcart.ShoppingCart/RemoveItem", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShoppingCartServer).RemoveItem(ctx, req.(*RemoveLineItem)) + } + return interceptor(ctx, in, info, handler) +} + +func _ShoppingCart_GetCart_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetShoppingCart) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShoppingCartServer).GetCart(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/com.example.shoppingcart.ShoppingCart/GetCart", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShoppingCartServer).GetCart(ctx, req.(*GetShoppingCart)) + } + return interceptor(ctx, in, info, handler) +} + +var _ShoppingCart_serviceDesc = grpc.ServiceDesc{ + ServiceName: "com.example.shoppingcart.ShoppingCart", + HandlerType: (*ShoppingCartServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "AddItem", + Handler: _ShoppingCart_AddItem_Handler, + }, + { + MethodName: "RemoveItem", + Handler: _ShoppingCart_RemoveItem_Handler, + }, + { + MethodName: "GetCart", + Handler: _ShoppingCart_GetCart_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "shoppingcart/shoppingcart.proto", +} From 83b5294d0031f82c1b60a7392b88417213e9feb1 Mon Sep 17 00:00:00 2001 From: Marcel Lanz Date: Tue, 8 Oct 2019 23:23:10 +0200 Subject: [PATCH 05/10] cleanups and added proto to the repo --- ... => build-and-publish-docker-image-tck.sh} | 2 +- build/fetch-cloudstate-pb.sh | 25 +- cloudstate/entity_key.pb.go | 9 +- cloudstate/eventsourced.go | 14 +- cloudstate/eventsourced_reply.go | 15 +- cloudstate/eventsourced_test.go | 6 +- cloudstate/marshal_proto.go | 2 +- protobuf/frontend/cloudstate/entity_key.proto | 30 ++ protobuf/protocol/cloudstate/crdt.proto | 379 ++++++++++++++++++ protobuf/protocol/cloudstate/entity.proto | 191 +++++++++ .../protocol/cloudstate/event_sourced.proto | 115 ++++++ protobuf/protocol/cloudstate/function.proto | 60 +++ .../grpc/reflection/v1alpha/reflection.proto | 136 +++++++ 13 files changed, 950 insertions(+), 34 deletions(-) rename build/{build-and-publish-docker-image.sh => build-and-publish-docker-image-tck.sh} (74%) create mode 100644 protobuf/frontend/cloudstate/entity_key.proto create mode 100644 protobuf/protocol/cloudstate/crdt.proto create mode 100644 protobuf/protocol/cloudstate/entity.proto create mode 100644 protobuf/protocol/cloudstate/event_sourced.proto create mode 100644 protobuf/protocol/cloudstate/function.proto create mode 100644 protobuf/proxy/grpc/reflection/v1alpha/reflection.proto diff --git a/build/build-and-publish-docker-image.sh b/build/build-and-publish-docker-image-tck.sh similarity index 74% rename from build/build-and-publish-docker-image.sh rename to build/build-and-publish-docker-image-tck.sh index 933b86a..ea743ab 100755 --- a/build/build-and-publish-docker-image.sh +++ b/build/build-and-publish-docker-image-tck.sh @@ -5,4 +5,4 @@ set -o errexit set -o pipefail docker build -t gcr.io/mrcllnz/cloudstate-go-tck:latest -f ./build/TCK.Dockerfile . -docker push gcr.io/mrcllnz/cloudstate-go-tck:latest +docker push gcr.io/mrcllnz/cloudstate-go-tck:latest \ No newline at end of file diff --git a/build/fetch-cloudstate-pb.sh b/build/fetch-cloudstate-pb.sh index 1644d7b..98b6deb 100755 --- a/build/fetch-cloudstate-pb.sh +++ b/build/fetch-cloudstate-pb.sh @@ -6,25 +6,28 @@ set -o pipefail function fetch() { local path=$1 + local tag=$2 mkdir -p protobuf/$(dirname $path) - curl -o protobuf/${path} https://raw.githubusercontent.com/cloudstateio/cloudstate/master/protocols/${path} + curl -o protobuf/${path} https://raw.githubusercontent.com/cloudstateio/cloudstate/${tag}/protocols/${path} #sed 's/^option java_package.*/option go_package = "${go_package}";/' protobuf/${path} } +tag=$1 + # CloudState protocol -fetch "protocol/cloudstate/entity.proto" -fetch "protocol/cloudstate/event_sourced.proto" -fetch "protocol/cloudstate/function.proto" -fetch "protocol/cloudstate/crdt.proto" +fetch "protocol/cloudstate/entity.proto" $tag +fetch "protocol/cloudstate/event_sourced.proto" $tag +fetch "protocol/cloudstate/function.proto" $tag +fetch "protocol/cloudstate/crdt.proto" $tag # TCK shopping cart example -fetch "example/shoppingcart/shoppingcart.proto" -fetch "example/shoppingcart/persistence/domain.proto" +fetch "example/shoppingcart/shoppingcart.proto" $tag +fetch "example/shoppingcart/persistence/domain.proto" $tag # CloudState frontend -fetch "frontend/cloudstate/entity_key.proto" +fetch "frontend/cloudstate/entity_key.proto" $tag # dependencies -fetch "proxy/grpc/reflection/v1alpha/reflection.proto" -fetch "frontend/google/api/annotations.proto" -fetch "frontend/google/api/http.proto" +fetch "proxy/grpc/reflection/v1alpha/reflection.proto" $tag +fetch "frontend/google/api/annotations.proto" $tag +fetch "frontend/google/api/http.proto" $tag diff --git a/cloudstate/entity_key.pb.go b/cloudstate/entity_key.pb.go index 345be8d..b0094a9 100644 --- a/cloudstate/entity_key.pb.go +++ b/cloudstate/entity_key.pb.go @@ -37,7 +37,7 @@ func init() { func init() { proto.RegisterFile("cloudstate/entity_key.proto", fileDescriptor_7bcabc3af9eb79b9) } var fileDescriptor_7bcabc3af9eb79b9 = []byte{ - // 178 bytes of a gzipped FileDescriptorProto + // 174 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4e, 0xce, 0xc9, 0x2f, 0x4d, 0x29, 0x2e, 0x49, 0x2c, 0x49, 0xd5, 0x4f, 0xcd, 0x2b, 0xc9, 0x2c, 0xa9, 0x8c, 0xcf, 0x4e, 0xad, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x48, 0x4a, 0x29, 0xa4, 0xe7, 0xe7, @@ -45,9 +45,8 @@ var fileDescriptor_7bcabc3af9eb79b9 = []byte{ 0x0b, 0x4a, 0xf2, 0x8b, 0x20, 0xaa, 0xad, 0xec, 0xb8, 0xb8, 0x10, 0x26, 0x08, 0xc9, 0xea, 0x41, 0x34, 0xe8, 0xc1, 0x34, 0xe8, 0xb9, 0x65, 0xa6, 0xe6, 0xa4, 0xf8, 0x17, 0x94, 0x64, 0xe6, 0xe7, 0x15, 0x4b, 0x5c, 0x6a, 0x63, 0x56, 0x60, 0xd4, 0xe0, 0x08, 0xe2, 0x84, 0x68, 0xf1, 0x4e, 0xad, - 0x74, 0x0a, 0xe2, 0xe2, 0xcd, 0xcc, 0xd7, 0x43, 0x58, 0x19, 0xe5, 0x98, 0x9e, 0x59, 0x92, 0x51, + 0x74, 0xf2, 0xe2, 0xe2, 0xcd, 0xcc, 0xd7, 0x43, 0x58, 0x19, 0x65, 0x99, 0x9e, 0x59, 0x92, 0x51, 0x9a, 0xa4, 0x97, 0x9c, 0x9f, 0xab, 0x8f, 0x10, 0xce, 0xcc, 0xd7, 0x4f, 0xcf, 0xd7, 0x2d, 0x2e, - 0x2d, 0x28, 0xc8, 0x2f, 0x2a, 0x41, 0x12, 0x87, 0xb8, 0x2c, 0x39, 0x3f, 0xc7, 0x1a, 0x21, 0x96, - 0xc4, 0x06, 0x16, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xb6, 0x70, 0x0b, 0x55, 0xe7, 0x00, - 0x00, 0x00, + 0x2d, 0x28, 0xc8, 0x2f, 0x2a, 0x41, 0x12, 0xd7, 0xb7, 0x46, 0xb0, 0x93, 0xd8, 0xc0, 0xb6, 0x1a, + 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xd9, 0xd8, 0x72, 0x17, 0xdf, 0x00, 0x00, 0x00, } diff --git a/cloudstate/eventsourced.go b/cloudstate/eventsourced.go index f7ad2e7..5783acc 100644 --- a/cloudstate/eventsourced.go +++ b/cloudstate/eventsourced.go @@ -228,7 +228,7 @@ func (esh *EventSourcedHandler) handleInitSnapshot(init *protocol.EventSourcedIn return nil } entityId := init.GetEntityId() - if snapShotHandler, ok := esh.contexts[entityId].EntityInstance.Instance.(SnapshotHandler); ok { + if snapshotHandler, ok := esh.contexts[entityId].EntityInstance.Instance.(SnapshotHandler); ok { msgName := strings.TrimPrefix(init.Snapshot.Snapshot.GetTypeUrl(), protoAnyBase+"/") // TODO: this might be something else than a proto message messageType := proto.MessageType(msgName) if messageType.Kind() == reflect.Ptr { @@ -237,7 +237,7 @@ func (esh *EventSourcedHandler) handleInitSnapshot(init *protocol.EventSourcedIn if err != nil { return NewFailureError("unmarshalling snapshot failed with: %v", err) } - handled, err := snapShotHandler.HandleSnapshot(message) + handled, err := snapshotHandler.HandleSnapshot(message) if err != nil { return NewFailureError("handling snapshot failed with: %v", err) } @@ -302,7 +302,6 @@ func (esh *EventSourcedHandler) handleEvent(entityId string, event *protocol.Eve // Beside calling the service method, we have to collect "events" the service might emit. // These events afterwards have to be handled by a EventHandler to update the state of the // entity. The CloudState proxy can re-play these events at any time -// TODO: move sendFailure to the caller func (esh *EventSourcedHandler) handleCommand(cmd *protocol.Command, server protocol.EventSourced_HandleServer) error { // method to call method, err := esh.methodToCall(cmd) @@ -486,6 +485,7 @@ func (EventSourcedHandler) handleEvents(entityInstance *EntityInstance, events . // TODO: here's the point where events can be protobufs, serialized as json or other formats msgName := strings.TrimPrefix(event.Payload.GetTypeUrl(), protoAnyBase+"/") messageType := proto.MessageType(msgName) + if messageType.Kind() == reflect.Ptr { // get a zero-ed message of this type if message, ok := reflect.New(messageType.Elem()).Interface().(proto.Message); ok { @@ -500,7 +500,7 @@ func (EventSourcedHandler) handleEvents(entityInstance *EntityInstance, events . if implementsEventHandler { handled, err = eventHandler.HandleEvent(message) if err != nil { - return err + return err // FIXME/TODO: is this correct? if we fail here, nothing is safe afterwards. } } // if not, we try to find one @@ -510,8 +510,8 @@ func (EventSourcedHandler) handleEvents(entityInstance *EntityInstance, events . // find a concrete handling method entityValue := reflect.ValueOf(entityInstance.Instance) entityType := entityValue.Type() - for tmi := 0; tmi < entityType.NumMethod(); tmi++ { - method := entityType.Method(tmi) + for n := 0; n < entityType.NumMethod(); n++ { + method := entityType.Method(n) // we expect one argument for now, the domain message // the first argument is the receiver itself if method.Func.Type().NumIn() == 2 { @@ -520,7 +520,7 @@ func (EventSourcedHandler) handleEvents(entityInstance *EntityInstance, events . entityValue.MethodByName(method.Name).Call([]reflect.Value{reflect.ValueOf(message)}) } } else { - // we have not found a one-argument method maching the + // we have not found a one-argument method matching the events type as an argument // TODO: what to do here? we might support more variations of possible handlers we can detect } } diff --git a/cloudstate/eventsourced_reply.go b/cloudstate/eventsourced_reply.go index 2e6899f..d2964b1 100644 --- a/cloudstate/eventsourced_reply.go +++ b/cloudstate/eventsourced_reply.go @@ -30,17 +30,18 @@ var ErrClientActionFailure = errors.New("cloudstate client action failure") func NewFailureError(format string, a ...interface{}) error { if len(a) != 0 { - errorf := fmt.Errorf(fmt.Sprintf(format, a)+". %w", ErrFailure) - return errorf + return fmt.Errorf(fmt.Sprintf(format, a...)+". %w", ErrFailure) } else { - errorf := fmt.Errorf(format+". %w", ErrFailure) - return errorf + return fmt.Errorf(format+". %w", ErrFailure) } } func NewClientActionFailureError(format string, a ...interface{}) error { - errorf := fmt.Errorf(fmt.Sprintf(format, a)+". %w", ErrClientActionFailure) - return errorf + if len(a) != 0 { + return fmt.Errorf(fmt.Sprintf(format, a...)+". %w", ErrClientActionFailure) + } else { + return fmt.Errorf(format+". %w", ErrClientActionFailure) + } } type ProtocolFailure struct { @@ -67,8 +68,8 @@ func NewProtocolFailure(failure protocol.Failure) error { // be sent to the proxy, otherwise handleFailure returns the original failure func handleFailure(failure error, server protocol.EventSourced_HandleServer, cmdId int64) error { if errors.Is(failure, ErrFailure) { - // TCK says: Failure was not received, or not well-formed: Failure(Failure(0,cloudstate failure)) was not reply (CloudStateTCK.scala:339) // FIXME: why not getting the failure from the ProtocolFailure + // TCK says: Failure was not received, or not well-formed: Failure(Failure(0,cloudstate failure)) was not reply (CloudStateTCK.scala:339) //return sendFailure(&protocol.Failure{Description: failure.Error()}, server) return sendClientActionFailure(&protocol.Failure{ CommandId: cmdId, diff --git a/cloudstate/eventsourced_test.go b/cloudstate/eventsourced_test.go index 48e5e60..97e6680 100644 --- a/cloudstate/eventsourced_test.go +++ b/cloudstate/eventsourced_test.go @@ -70,14 +70,16 @@ func (te *TestEntity) New() interface{} { return testEntity } -func (te *TestEntity) IncrementByCommand(c context.Context, ibc *IncrementByCommand) (*empty.Empty, error) { +// IncrementByCommand with value receiver +func (te TestEntity) IncrementByCommand(_ context.Context, ibc *IncrementByCommand) (*empty.Empty, error) { te.Emit(&IncrementByEvent{ Value: ibc.Amount, }) return &empty.Empty{}, nil } -func (te *TestEntity) DecrementByCommand(c context.Context, ibc *DecrementByCommand) (*empty.Empty, error) { +// DecrementByCommand with pointer receiver +func (te *TestEntity) DecrementByCommand(_ context.Context, ibc *DecrementByCommand) (*empty.Empty, error) { te.Emit(&DecrementByEvent{ Value: ibc.Amount, }) diff --git a/cloudstate/marshal_proto.go b/cloudstate/marshal_proto.go index ed790b7..5e87b7c 100644 --- a/cloudstate/marshal_proto.go +++ b/cloudstate/marshal_proto.go @@ -21,7 +21,7 @@ import ( "github.com/golang/protobuf/ptypes/any" ) -// marshalAny marshals a proto.Message to a any.Any value +// marshalAny marshals a proto.Message to a any.Any value. func marshalAny(pb interface{}) (*any.Any, error) { // TODO: protobufs are expected here, but CloudState supports other formats message, ok := pb.(proto.Message) diff --git a/protobuf/frontend/cloudstate/entity_key.proto b/protobuf/frontend/cloudstate/entity_key.proto new file mode 100644 index 0000000..d208121 --- /dev/null +++ b/protobuf/frontend/cloudstate/entity_key.proto @@ -0,0 +1,30 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Extension for specifying which field in a message is to be considered an +// entity key, for the purposes associating gRPC calls with entities and +// sharding. + +syntax = "proto3"; + +import "google/protobuf/descriptor.proto"; + +package cloudstate; + +option java_package = "io.cloudstate"; +option go_package = "github.com/cloudstateio/go-support/cloudstate/;cloudstate"; + +extend google.protobuf.FieldOptions { + bool entity_key = 50002; +} diff --git a/protobuf/protocol/cloudstate/crdt.proto b/protobuf/protocol/cloudstate/crdt.proto new file mode 100644 index 0000000..691242e --- /dev/null +++ b/protobuf/protocol/cloudstate/crdt.proto @@ -0,0 +1,379 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// gRPC interface for Event Sourced Entity user functions. + +syntax = "proto3"; + +package cloudstate.crdt; + +// Any is used so that domain events defined according to the functions business domain can be embedded inside +// the protocol. +import "google/protobuf/any.proto"; +import "cloudstate/entity.proto"; + +option java_package = "io.cloudstate.protocol"; +option go_package = "cloudstate/protocol"; + +// CRDT Protocol +// +// Note that while this protocol provides support for CRDTs, the data types sent across the protocol are not CRDTs +// themselves. It is the responsibility of the CloudState proxy to implement the CRDTs, merge functions, vector clocks +// etc, not the user function. The user function need only hold the current value in memory, and this protocol sends +// deltas to the user function to update its in memory value as necessary. These deltas have no way of dealing with +// conflicts, hence it important that the CloudState proxy always know what the state of the user functions in memory +// value is before sending a delta. If the CloudState proxy is not sure what the value is, eg because it has just sent +// an operation to the user function may have updated its value as a result, the proxy should wait until it gets the +// result of the operation back, to ensure its in memory value is in sync with the user function so that it can +// calculate deltas that won't conflict. +// +// The user function is expected to update its value both as the result of receiving deltas from the proxy, as well as +// when it sends deltas. It must not update its value in any other circumstance, updating the value in response to any +// other stimuli risks the value becoming out of sync with the CloudState proxy. The user function will not be sent +// back deltas as a result of its own changes. +// +// An invocation of handle is made for each entity being handled. It may be kept alive and used to handle multiple +// commands, and may subsequently be terminated if that entity becomes idle, or if the entity is deleted. Shutdown is +// typically done for efficiency reasons, unless the entity is explicitly deleted, a terminated handle stream does not +// mean the proxy has stopped tracking the state of the entity in its memory. +// +// Special care must be taken when working with maps and sets. The keys/values are google.protobuf.Any, which encodes +// the value as binary protobuf, however, serialized protobufs are not stable, two semantically equal objects could +// encode to different bytes. It is the responsibility of the user function to ensure that stable encodings are used. +service Crdt { + + // After invoking handle, the first message sent will always be a CrdtInit message, containing the entity ID, and, + // if it exists or is available, the current state of the entity. After that, one or more commands may be sent, + // as well as deltas as they arrive, and the entire state if either the entity is created, or the proxy wishes the + // user function to replace its entire state. + // + // The user function must respond with one reply per command in. They do not necessarily have to be sent in the same + // order that the commands were sent, the command ID is used to correlate commands to replies. + rpc handle(stream CrdtStreamIn) returns (stream CrdtStreamOut); +} + +// Message for the Crdt handle stream in. +message CrdtStreamIn { + oneof message { + + // Always sent first, and only once. + CrdtInit init = 1; + + // Sent to indicate the user function should replace its current state with this state. If the user function + // does not have a current state, either because the init function didn't send one and the user function hasn't + // updated the state itself in response to a command, or because the state was deleted, this must be sent before + // any deltas. + CrdtState state = 2; + + // A delta to be applied to the current state. May be sent at any time as long as the user function already has + // state. + CrdtDelta changed = 3; + + // Delete the entity. May be sent at any time. The user function should clear its state when it receives this. + // A proxy may decide to terminate the stream after sending this. + CrdtDelete deleted = 4; + + // A command, may be sent at any time. + Command command = 5; + + // A stream has been cancelled. + StreamCancelled stream_cancelled = 6; + } +} + +// Message for the Crdt handle stream out. +message CrdtStreamOut { + oneof message { + // A reply to an incoming command. Either one reply, or one failure, must be sent in response to each command. + CrdtReply reply = 1; + // A streamed message. + CrdtStreamedMessage streamed_message = 2; + // A stream cancelled response, may be sent in response to stream_cancelled. + CrdtStreamCancelledResponse stream_cancelled_response = 3; + // A failure. Either sent in response to a command, or sent if some other error occurs. + Failure failure = 4; + } +} + +// The CRDT state. This represents the full state of a CRDT. When received, a user function should replace the current +// state with this, not apply it as a delta. This includes both for the top level CRDT, and embedded CRDTs, such as +// the values of an ORMap. +message CrdtState { + oneof state { + // A Grow-only Counter + GCounterState gcounter = 1; + + // A Positve-Negative Counter + PNCounterState pncounter = 2; + + // A Grow-only Set + GSetState gset = 3; + + // An Observed-Removed Set + ORSetState orset = 4; + + // A Last-Write-Wins Register + LWWRegisterState lwwregister = 5; + + // A Flag + FlagState flag = 6; + + // An Observed-Removed Map + ORMapState ormap = 7; + + // A vote + VoteState vote = 8; + } +} + +// A Grow-only counter +// +// A G-Counter can only be incremented, it can't be decremented. +message GCounterState { + + // The current value of the counter. + uint64 value = 1; +} + +// A Positve-Negative Counter +// +// A PN-Counter can be both incremented and decremented. +message PNCounterState { + + // The current value of the counter. + int64 value = 1; +} + +// A Grow-only Set +// +// A G-Set can only have items added, items cannot be removed. +message GSetState { + + // The current items in the set. + repeated google.protobuf.Any items = 1; +} + +// An Observed-Removed Set +// +// An OR-Set may have items added and removed, with the condition that an item must be observed to be in the set before +// it is removed. +message ORSetState { + + // The current items in the set. + repeated google.protobuf.Any items = 1; +} + +// A Last-Write-Wins Register +// +// A LWW-Register holds a single value, with the current value being selected based on when it was last written. +// The time of the last write may either be determined using the proxies clock, or may be based on a custom, domain +// specific value. +message LWWRegisterState { + + // The current value of the register. + google.protobuf.Any value = 1; + + // The clock to use if this state needs to be merged with another one. + CrdtClock clock = 2; + + // The clock value if the clock in use is a custom clock. + int64 custom_clock_value = 3; +} + +// A Flag +// +// A Flag is a boolean value, that once set to true, stays true. +message FlagState { + + // The current value of the flag. + bool value = 1; +} + +// An Observed-Removed Map +// +// Like an OR-Set, an OR-Map may have items added and removed, with the condition that an item must be observed to be +// in the map before it is removed. The values of the map are CRDTs themselves. Different keys are allowed to use +// different CRDTs, and if an item is removed, and then replaced, the new value may be a different CRDT. +message ORMapState { + + // The entries of the map. + repeated ORMapEntry entries = 1; +} + +// An OR-Map entry. +message ORMapEntry { + + // The entry key. + google.protobuf.Any key = 1; + + // The value of the entry, a CRDT itself. + CrdtState value = 2; +} + +// A Vote. This allows nodes to vote on something. +message VoteState { + + // The number of votes for + uint32 votes_for = 1; + + // The total number of voters + uint32 total_voters = 2; + + // The vote of the current node, which is included in the above two numbers + bool self_vote = 3; +} + +// A CRDT delta +// +// Deltas only carry the change in value, not the full value (unless +message CrdtDelta { + oneof delta { + GCounterDelta gcounter = 1; + PNCounterDelta pncounter = 2; + GSetDelta gset = 3; + ORSetDelta orset = 4; + LWWRegisterDelta lwwregister = 5; + FlagDelta flag = 6; + ORMapDelta ormap = 7; + VoteDelta vote = 8; + } +} + +message GCounterDelta { + uint64 increment = 1; +} + +message PNCounterDelta { + sint64 change = 1; +} + +message GSetDelta { + repeated google.protobuf.Any added = 1; +} + +message ORSetDelta { + // If cleared is set, the set must be cleared before added is processed. + bool cleared = 1; + repeated google.protobuf.Any removed = 2; + repeated google.protobuf.Any added = 3; +} + +message LWWRegisterDelta { + google.protobuf.Any value = 1; + CrdtClock clock = 2; + int64 custom_clock_value = 3; +} + +message FlagDelta { + bool value = 1; +} + +message ORMapDelta { + bool cleared = 1; + repeated google.protobuf.Any removed = 2; + repeated ORMapEntryDelta updated = 3; + repeated ORMapEntry added = 4; +} + +message ORMapEntryDelta { + // The entry key. + google.protobuf.Any key = 1; + + CrdtDelta delta = 2; +} + +message VoteDelta { + // Only set by the user function to change the nodes current vote. + bool self_vote = 1; + + // Only set by the proxy to change the votes for and total voters. + int32 votes_for = 2; + int32 total_voters = 3; +} + +message CrdtInit { + string service_name = 1; + string entity_id = 2; + CrdtState state = 3; +} + +message CrdtDelete { +} + +message CrdtReply { + + int64 command_id = 1; + + ClientAction client_action = 2; + + repeated SideEffect side_effects = 4; + + CrdtStateAction state_action = 5; + + // If the request was streamed, setting this to true indicates that the command should + // be handled as a stream. Subsequently, the user function may send CrdtStreamedMessage, + // and a CrdtStreamCancelled message will be sent if the stream is cancelled (though + // not if the a CrdtStreamedMessage ends the stream first). + bool streamed = 6; +} + +message CrdtStateAction { + oneof action { + CrdtState create = 5; + CrdtDelta update = 6; + CrdtDelete delete = 7; + } + + CrdtWriteConsistency write_consistency = 8; +} + +// May be sent as often as liked if the first reply set streamed to true +message CrdtStreamedMessage { + + int64 command_id = 1; + + ClientAction client_action = 2; + + repeated SideEffect side_effects = 3; + + // Indicates the stream should end, no messages may be sent for this command after this. + bool end_stream = 4; +} + +message CrdtStreamCancelledResponse { + int64 command_id = 1; + + repeated SideEffect side_effects = 2; + + CrdtStateAction state_action = 3; +} + +enum CrdtWriteConsistency { + LOCAL = 0; + MAJORITY = 1; + ALL = 2; +} + +enum CrdtClock { + // Use the default clock for deciding the last write, which is the system clocks + // milliseconds since epoch. + DEFAULT = 0; + // Use the reverse semantics with the default clock, to enable first write wins. + REVERSE = 1; + // Use a custom clock value, set using custom_clock_value. + CUSTOM = 2; + // Use a custom clock value, but automatically increment it by one if the clock + // value from the current value is equal to the custom_clock_value. + CUSTOM_AUTO_INCREMENT = 3; +} diff --git a/protobuf/protocol/cloudstate/entity.proto b/protobuf/protocol/cloudstate/entity.proto new file mode 100644 index 0000000..05cd7be --- /dev/null +++ b/protobuf/protocol/cloudstate/entity.proto @@ -0,0 +1,191 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// gRPC interface for Event Sourced Entity user functions. + +syntax = "proto3"; + +package cloudstate; + +// Any is used so that domain events defined according to the functions business domain can be embedded inside +// the protocol. +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/descriptor.proto"; + +option java_package = "io.cloudstate.protocol"; +option go_package = "cloudstate/protocol"; + +// A reply to the sender. +message Reply { + // The reply payload + google.protobuf.Any payload = 1; +} + +// Forwards handling of this request to another entity. +message Forward { + // The name of the service to forward to. + string service_name = 1; + // The name of the command. + string command_name = 2; + // The payload. + google.protobuf.Any payload = 3; +} + +// An action for the client +message ClientAction { + oneof action { + + // Send a reply + Reply reply = 1; + + // Forward to another entity + Forward forward = 2; + + // Send a failure to the client + Failure failure = 3; + } +} + +// A side effect to be done after this command is handled. +message SideEffect { + + // The name of the service to perform the side effect on. + string service_name = 1; + + // The name of the command. + string command_name = 2; + + // The payload of the command. + google.protobuf.Any payload = 3; + + // Whether this side effect should be performed synchronously, ie, before the reply is eventually + // sent, or not. + bool synchronous = 4; +} + +// A command. For each command received, a reply must be sent with a matching command id. +message Command { + + // The ID of the entity. + string entity_id = 1; + + // A command id. + int64 id = 2; + + // Command name + string name = 3; + + // The command payload. + google.protobuf.Any payload = 4; + + // Whether the command is streamed or not + bool streamed = 5; +} + +message StreamCancelled { + + // The ID of the entity + string entity_id = 1; + + // The command id + int64 id = 2; +} + +// A failure reply. If this is returned, it will be translated into a gRPC unknown +// error with the corresponding description if supplied. +message Failure { + + // The id of the command being replied to. Must match the input command. + int64 command_id = 1; + + // A description of the error. + string description = 2; +} + +message EntitySpec { + // This should be the Descriptors.FileDescriptorSet in proto serialized from as generated by: + // protoc --include_imports \ + // --proto_path= \ + // --descriptor_set_out=user-function.desc \ + // + bytes proto = 1; + + // The entities being served. + repeated Entity entities = 2; + + // Optional information about the service. + ServiceInfo service_info = 3; +} + +// Information about the service that proxy is proxying to. +// All of the information in here is optional. It may be useful for debug purposes. +message ServiceInfo { + + // The name of the service, eg, "shopping-cart". + string service_name = 1; + + // The version of the service. + string service_version = 2; + + // A description of the runtime for the service. Can be anything, but examples might be: + // - node v10.15.2 + // - OpenJDK Runtime Environment 1.8.0_192-b12 + string service_runtime = 3; + + // If using a support library, the name of that library, eg "cloudstate" + string support_library_name = 4; + + // The version of the support library being used. + string support_library_version = 5; +} + +message Entity { + + // The type of entity. By convention, this should be a fully qualified entity protocol grpc + // service name, for example, cloudstate.eventsourced.EventSourced. + string entity_type = 1; + + // The name of the service to load from the protobuf file. + string service_name = 2; + + // The ID to namespace state by. How this is used depends on the type of entity, for example, + // event sourced entities will prefix this to the persistence id. + string persistence_id = 3; +} + +message UserFunctionError { + string message = 1; +} + +message ProxyInfo { + int32 protocol_major_version = 1; + int32 protocol_minor_version = 2; + string proxy_name = 3; + string proxy_version = 4; + repeated string supported_entity_types = 5; +} + +// Entity discovery service. +service EntityDiscovery { + + // Discover what entities the user function wishes to serve. + rpc discover(ProxyInfo) returns (EntitySpec) {} + + // Report an error back to the user function. This will only be invoked to tell the user function + // that it has done something wrong, eg, violated the protocol, tried to use an entity type that + // isn't supported, or attempted to forward to an entity that doesn't exist, etc. These messages + // should be logged clearly for debugging purposes. + rpc reportError(UserFunctionError) returns (google.protobuf.Empty) {} +} diff --git a/protobuf/protocol/cloudstate/event_sourced.proto b/protobuf/protocol/cloudstate/event_sourced.proto new file mode 100644 index 0000000..0417f14 --- /dev/null +++ b/protobuf/protocol/cloudstate/event_sourced.proto @@ -0,0 +1,115 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// gRPC interface for Event Sourced Entity user functions. + +syntax = "proto3"; + +package cloudstate.eventsourced; + +// Any is used so that domain events defined according to the functions business domain can be embedded inside +// the protocol. +import "google/protobuf/any.proto"; +import "cloudstate/entity.proto"; + +option java_package = "io.cloudstate.protocol"; +option go_package = "cloudstate/protocol"; + +// The init message. This will always be the first message sent to the entity when +// it is loaded. +message EventSourcedInit { + + string service_name = 1; + + // The ID of the entity. + string entity_id = 2; + + // If present the entity should initialise its state using this snapshot. + EventSourcedSnapshot snapshot = 3; +} + +// A snapshot +message EventSourcedSnapshot { + + // The sequence number when the snapshot was taken. + int64 snapshot_sequence = 1; + + // The snapshot. + google.protobuf.Any snapshot = 2; +} + +// An event. These will be sent to the entity when the entity starts up. +message EventSourcedEvent { + + // The sequence number of the event. + int64 sequence = 1; + + // The event payload. + google.protobuf.Any payload = 2; +} + +// A reply to a command. +message EventSourcedReply { + + // The id of the command being replied to. Must match the input command. + int64 command_id = 1; + + // The action to take + ClientAction client_action = 2; + + // Any side effects to perform + repeated SideEffect side_effects = 3; + + // A list of events to persist - these will be persisted before the reply + // is sent. + repeated google.protobuf.Any events = 4; + + // An optional snapshot to persist. It is assumed that this snapshot will have + // the state of any events in the events field applied to it. It is illegal to + // send a snapshot without sending any events. + google.protobuf.Any snapshot = 5; +} + +// Input message type for the gRPC stream in. +message EventSourcedStreamIn { + oneof message { + EventSourcedInit init = 1; + EventSourcedEvent event = 2; + Command command = 3; + } +} + +// Output message type for the gRPC stream out. +message EventSourcedStreamOut { + oneof message { + EventSourcedReply reply = 1; + Failure failure = 2; + } +} + +// The Entity service +service EventSourced { + + // The stream. One stream will be established per active entity. + // Once established, the first message sent will be Init, which contains the entity ID, and, + // if the entity has previously persisted a snapshot, it will contain that snapshot. It will + // then send zero to many event messages, one for each event previously persisted. The entity + // is expected to apply these to its state in a deterministic fashion. Once all the events + // are sent, one to many commands are sent, with new commands being sent as new requests for + // the entity come in. The entity is expected to reply to each command with exactly one reply + // message. The entity should reply in order, and any events that the entity requests to be + // persisted the entity should handle itself, applying them to its own state, as if they had + // arrived as events when the event stream was being replayed on load. + rpc handle(stream EventSourcedStreamIn) returns (stream EventSourcedStreamOut) {} +} diff --git a/protobuf/protocol/cloudstate/function.proto b/protobuf/protocol/cloudstate/function.proto new file mode 100644 index 0000000..3ca581b --- /dev/null +++ b/protobuf/protocol/cloudstate/function.proto @@ -0,0 +1,60 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// gRPC interface for Event Sourced Entity user functions. + +syntax = "proto3"; + +package cloudstate.function; + +// Any is used so that domain events defined according to the functions business domain can be embedded inside +// the protocol. +import "google/protobuf/any.proto"; +import "cloudstate/entity.proto"; + +option java_package = "io.cloudstate.protocol"; +option go_package = "cloudstate/protocol"; + +message FunctionCommand { + // The name of the service this function is on. + string service_name = 2; + + // Command name + string name = 3; + + // The command payload. + google.protobuf.Any payload = 4; +} + +message FunctionReply { + + oneof response { + Reply reply = 2; + Forward forward = 3; + } + + repeated SideEffect side_effects = 4; +} + +service StatelessFunction { + + rpc handleUnary(FunctionCommand) returns (FunctionReply) {} + + rpc handleStreamedIn(stream FunctionCommand) returns (FunctionReply) {} + + rpc handleStreamedOut(FunctionCommand) returns (stream FunctionReply) {} + + rpc handleStreamed(stream FunctionCommand) returns (stream FunctionReply) {} + +} diff --git a/protobuf/proxy/grpc/reflection/v1alpha/reflection.proto b/protobuf/proxy/grpc/reflection/v1alpha/reflection.proto new file mode 100644 index 0000000..816852f --- /dev/null +++ b/protobuf/proxy/grpc/reflection/v1alpha/reflection.proto @@ -0,0 +1,136 @@ +// Copyright 2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Service exported by server reflection + +syntax = "proto3"; + +package grpc.reflection.v1alpha; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server set one of the following fields accroding to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. As + // the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requst. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services request. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} From 536586169f91a083bd15d51874e0a415d13830aa Mon Sep 17 00:00:00 2001 From: Marcel Lanz Date: Fri, 11 Oct 2019 15:09:16 +0200 Subject: [PATCH 06/10] [tests] test for snapshot --- cloudstate/encoding/any_primitive.go | 16 ++++----- cloudstate/eventsourced.go | 52 ++++++++++++++++++++++------ cloudstate/eventsourced_reply.go | 3 ++ cloudstate/eventsourced_test.go | 46 ++++++++++++++++++++++-- 4 files changed, 96 insertions(+), 21 deletions(-) diff --git a/cloudstate/encoding/any_primitive.go b/cloudstate/encoding/any_primitive.go index 6333078..5b9380d 100644 --- a/cloudstate/encoding/any_primitive.go +++ b/cloudstate/encoding/any_primitive.go @@ -24,15 +24,15 @@ import ( ) const ( - primitiveTypeURLPrefix = "p.cloudstate.io" + PrimitiveTypeURLPrefix = "p.cloudstate.io" - primitiveTypeURLPrefixInt32 = primitiveTypeURLPrefix + "/int32" - primitiveTypeURLPrefixInt64 = primitiveTypeURLPrefix + "/int64" - primitiveTypeURLPrefixString = primitiveTypeURLPrefix + "/string" - primitiveTypeURLPrefixFloat = primitiveTypeURLPrefix + "/float" - primitiveTypeURLPrefixDouble = primitiveTypeURLPrefix + "/double" - primitiveTypeURLPrefixBool = primitiveTypeURLPrefix + "/bool" - primitiveTypeURLPrefixBytes = primitiveTypeURLPrefix + "/bytes" + primitiveTypeURLPrefixInt32 = PrimitiveTypeURLPrefix + "/int32" + primitiveTypeURLPrefixInt64 = PrimitiveTypeURLPrefix + "/int64" + primitiveTypeURLPrefixString = PrimitiveTypeURLPrefix + "/string" + primitiveTypeURLPrefixFloat = PrimitiveTypeURLPrefix + "/float" + primitiveTypeURLPrefixDouble = PrimitiveTypeURLPrefix + "/double" + primitiveTypeURLPrefixBool = PrimitiveTypeURLPrefix + "/bool" + primitiveTypeURLPrefixBytes = PrimitiveTypeURLPrefix + "/bytes" ) const fieldKey = 1 << 3 diff --git a/cloudstate/eventsourced.go b/cloudstate/eventsourced.go index 5783acc..0f7acd1 100644 --- a/cloudstate/eventsourced.go +++ b/cloudstate/eventsourced.go @@ -19,10 +19,12 @@ import ( "context" "errors" "fmt" + "github.com/cloudstateio/go-support/cloudstate/encoding" "github.com/cloudstateio/go-support/cloudstate/protocol" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes/any" "io" + "net/url" "reflect" "strings" "sync" @@ -229,25 +231,53 @@ func (esh *EventSourcedHandler) handleInitSnapshot(init *protocol.EventSourcedIn } entityId := init.GetEntityId() if snapshotHandler, ok := esh.contexts[entityId].EntityInstance.Instance.(SnapshotHandler); ok { + snapshot, err := esh.unmarshalSnapshot(init) + if snapshot == nil || err != nil { + return NewFailureError("handling snapshot failed with: %v", err) + } + handled, err := snapshotHandler.HandleSnapshot(snapshot) + if err != nil { + return NewFailureError("handling snapshot failed with: %v", err) + } + if handled { + esh.contexts[entityId].EntityInstance.eventSequence = init.GetSnapshot().SnapshotSequence + } + return nil + } + return nil +} + +func (EventSourcedHandler) unmarshalSnapshot(init *protocol.EventSourcedInit) (interface{}, error) { + // see: https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/any#typeurl + typeUrl := init.Snapshot.Snapshot.GetTypeUrl() + if !strings.Contains(typeUrl, "://") { + typeUrl = "https://" + typeUrl + } + typeURL, err := url.Parse(typeUrl) + if err != nil { + return nil, err + } + switch typeURL.Host { + case encoding.PrimitiveTypeURLPrefix: + snapshot, err := encoding.UnmarshalPrimitive(init.Snapshot.Snapshot) + if err != nil { + return nil, fmt.Errorf("unmarshalling snapshot failed with: %v", err) + } + return snapshot, nil + case protoAnyBase: msgName := strings.TrimPrefix(init.Snapshot.Snapshot.GetTypeUrl(), protoAnyBase+"/") // TODO: this might be something else than a proto message messageType := proto.MessageType(msgName) if messageType.Kind() == reflect.Ptr { if message, ok := reflect.New(messageType.Elem()).Interface().(proto.Message); ok { err := proto.Unmarshal(init.Snapshot.Snapshot.Value, message) if err != nil { - return NewFailureError("unmarshalling snapshot failed with: %v", err) - } - handled, err := snapshotHandler.HandleSnapshot(message) - if err != nil { - return NewFailureError("handling snapshot failed with: %v", err) - } - if handled { - esh.contexts[entityId].EntityInstance.eventSequence = init.GetSnapshot().SnapshotSequence + return nil, fmt.Errorf("unmarshalling snapshot failed with: %v", err) } + return message, nil } } } - return nil + return nil, fmt.Errorf("unmarshalling snapshot failed with: no snapshot unmarshaller found for: %v", typeURL.String()) } func (esh *EventSourcedHandler) subscribeEvents(instance *EntityInstance) { @@ -325,7 +355,7 @@ func (esh *EventSourcedHandler) handleCommand(cmd *protocol.Command, server prot // The gRPC implementation returns the rpc return method // and an error as a second return value. errReturned := called[1] - if errReturned.CanInterface() && errReturned.Interface() != nil && errReturned.Type().Name() == "error" { // FIXME: looks ugly + if errReturned.CanInterface() && errReturned.Interface() != nil && errReturned.Type().Name() == "error" { // TCK says: TODO Expects entity.Failure, but gets lientAction.Action.Failure(Failure(commandId, msg))) return NewProtocolFailure(protocol.Failure{ CommandId: cmd.GetId(), @@ -337,7 +367,7 @@ func (esh *EventSourcedHandler) handleCommand(cmd *protocol.Command, server prot if err != nil { // this should never happen return NewProtocolFailure(protocol.Failure{ CommandId: cmd.GetId(), - Description: fmt.Errorf("called return value at index 0 is no proto.Message").Error(), + Description: fmt.Errorf("called return value at index 0 is no proto.Message. %w", err).Error(), }) } // emitted events diff --git a/cloudstate/eventsourced_reply.go b/cloudstate/eventsourced_reply.go index d2964b1..0d1efe0 100644 --- a/cloudstate/eventsourced_reply.go +++ b/cloudstate/eventsourced_reply.go @@ -85,6 +85,7 @@ func handleFailure(failure error, server protocol.EventSourced_HandleServer, cmd return failure } +// sendEventSourcedReply sends a given EventSourcedReply and if it fails, handles the error wrapping func sendEventSourcedReply(reply *protocol.EventSourcedReply, server protocol.EventSourced_HandleServer) error { err := server.Send(&protocol.EventSourcedStreamOut{ Message: &protocol.EventSourcedStreamOut_Reply{ @@ -97,6 +98,7 @@ func sendEventSourcedReply(reply *protocol.EventSourcedReply, server protocol.Ev return err } +// sendFailure sends a given EventSourcedReply and if it fails, handles the error wrapping func sendFailure(failure *protocol.Failure, server protocol.EventSourced_HandleServer) error { err := server.Send(&protocol.EventSourcedStreamOut{ Message: &protocol.EventSourcedStreamOut_Failure{ @@ -109,6 +111,7 @@ func sendFailure(failure *protocol.Failure, server protocol.EventSourced_HandleS return err } +// sendClientActionFailure sends a given EventSourcedReply and if it fails, handles the error wrapping func sendClientActionFailure(failure *protocol.Failure, server protocol.EventSourced_HandleServer) error { err := server.Send(&protocol.EventSourcedStreamOut{ Message: &protocol.EventSourcedStreamOut_Reply{ diff --git a/cloudstate/eventsourced_test.go b/cloudstate/eventsourced_test.go index 97e6680..38062ff 100644 --- a/cloudstate/eventsourced_test.go +++ b/cloudstate/eventsourced_test.go @@ -19,6 +19,7 @@ import ( "context" "errors" "fmt" + "github.com/cloudstateio/go-support/cloudstate/encoding" "github.com/cloudstateio/go-support/cloudstate/protocol" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes/any" @@ -34,12 +35,26 @@ type TestEntity struct { EventEmitter } +func (inc TestEntity) String() string { + return proto.CompactTextString(inc) +} + +func (inc TestEntity) ProtoMessage() { +} + +func (inc TestEntity) Reset() { +} + func (te *TestEntity) Snapshot() (snapshot interface{}, err error) { - panic("implement me") + return encoding.MarshalPrimitive(te.Value) } func (te *TestEntity) HandleSnapshot(snapshot interface{}) (handled bool, err error) { - panic("implement me") + switch v := snapshot.(type) { + case int64: + te.Value = v + } + return true, nil } func (te *TestEntity) IncrementBy(n int64) (int64, error) { @@ -214,6 +229,7 @@ func marshal(msg proto.Message, t *testing.T) ([]byte, error) { func TestMain(m *testing.M) { proto.RegisterType((*IncrementByEvent)(nil), "IncrementByEvent") proto.RegisterType((*DecrementByEvent)(nil), "DecrementByEvent") + proto.RegisterType((*TestEntity)(nil), "TestEntity") resetTestEntity() defer resetTestEntity() os.Exit(m.Run()) @@ -226,8 +242,34 @@ func TestErrSend(t *testing.T) { t.Fatalf("err1 is no err0 but should") } } +func TestSnapshot(t *testing.T) { + resetTestEntity() + handler := newHandler(t) + if testEntity.Value >= 0 { + t.Fatalf("testEntity.Value should be <0 but was not: %+v", testEntity) + } + primitive, err := encoding.MarshalPrimitive(int64(987)) + if err != nil { + t.Fatalf("%v", err) + } + err = handler.handleInit(&protocol.EventSourcedInit{ + ServiceName: "TestEventSourcedHandler-Service", + EntityId: "entity-0", + Snapshot: &protocol.EventSourcedSnapshot{ + SnapshotSequence: 0, + Snapshot: primitive, + }, + }, nil) + if err != nil { + t.Fatalf("%v", err) + } + if testEntity.Value != 987 { + t.Fatalf("testEntity.Value should be 0 but was not: %+v", testEntity) + } +} func TestEventSourcedHandlerHandlesCommandAndEvents(t *testing.T) { + resetTestEntity() handler := newHandler(t) if testEntity.Value >= 0 { t.Fatalf("testEntity.Value should be <0 but was not: %+v", testEntity) From a8fdc373171664626d07e1dd3e6880dc187e3499 Mon Sep 17 00:00:00 2001 From: Marcel Lanz Date: Sat, 12 Oct 2019 01:55:57 +0200 Subject: [PATCH 07/10] [go-support] added missing proto files. removed old json encoding left on first implementation. --- cloudstate/encoding/any_primitive.go | 15 - .../frontend/google/api/annotations.proto | 31 ++ protobuf/frontend/google/api/http.proto | 376 ++++++++++++++++++ 3 files changed, 407 insertions(+), 15 deletions(-) create mode 100644 protobuf/frontend/google/api/annotations.proto create mode 100644 protobuf/frontend/google/api/http.proto diff --git a/cloudstate/encoding/any_primitive.go b/cloudstate/encoding/any_primitive.go index 5b9380d..8cde6d0 100644 --- a/cloudstate/encoding/any_primitive.go +++ b/cloudstate/encoding/any_primitive.go @@ -16,11 +16,9 @@ package encoding import ( - "encoding/json" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes/any" "math" - "reflect" ) const ( @@ -81,19 +79,6 @@ func MarshalPrimitive(i interface{}) (*any.Any, error) { if err := buffer.EncodeRawBytes(val); err != nil { return nil, err } - case interface{}: - typeOf := reflect.TypeOf(val) - if typeOf.Kind() == reflect.Struct { - typeUrl = jsonTypeURLPrefix + "/" + typeOf.PkgPath() + "." + typeOf.Name() - _ = buffer.EncodeVarint(fieldKey | proto.WireBytes) - bytes, err := json.Marshal(val) - if err != nil { - return nil, err - } - _ = buffer.EncodeRawBytes(bytes) - } else { - return nil, ErrNotMarshalled - } default: return nil, ErrNotMarshalled } diff --git a/protobuf/frontend/google/api/annotations.proto b/protobuf/frontend/google/api/annotations.proto new file mode 100644 index 0000000..85c361b --- /dev/null +++ b/protobuf/frontend/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright (c) 2015, Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/protobuf/frontend/google/api/http.proto b/protobuf/frontend/google/api/http.proto new file mode 100644 index 0000000..b2977f5 --- /dev/null +++ b/protobuf/frontend/google/api/http.proto @@ -0,0 +1,376 @@ +// Copyright 2019 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} From 801f67ce046019c9b49763d4b8fefe80fccf2a91 Mon Sep 17 00:00:00 2001 From: Marcel Lanz Date: Tue, 15 Oct 2019 09:16:20 +0200 Subject: [PATCH 08/10] [PR-review] added missing EOL. Removed commented code. --- .travis.yml | 2 +- build/TCK.Dockerfile | 2 +- cloudstate/eventsourced_test.go | 72 --------------------------------- 3 files changed, 2 insertions(+), 74 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3961665..7d71e7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,4 @@ before_install: script: - go test -v -race -coverprofile=coverage.txt -covermode=atomic -bench=. ./... after_success: - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then bash <(curl -s https://codecov.io/bash); fi \ No newline at end of file + - if [ "$TRAVIS_OS_NAME" = "linux" ]; then bash <(curl -s https://codecov.io/bash); fi diff --git a/build/TCK.Dockerfile b/build/TCK.Dockerfile index 7065d0c..d31efa8 100644 --- a/build/TCK.Dockerfile +++ b/build/TCK.Dockerfile @@ -20,4 +20,4 @@ EXPOSE 8080 ENV HOST 0.0.0.0 ENV PORT 8080 -CMD ["./tck_shoppingcart"] \ No newline at end of file +CMD ["./tck_shoppingcart"] diff --git a/cloudstate/eventsourced_test.go b/cloudstate/eventsourced_test.go index 38062ff..6ad30cf 100644 --- a/cloudstate/eventsourced_test.go +++ b/cloudstate/eventsourced_test.go @@ -318,75 +318,3 @@ func TestEventSourcedHandlerHandlesCommandAndEvents(t *testing.T) { t.Fatalf("testEntity.Value != 0") } } - -//type IncrementTwiceByCommand struct { -// Amount int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` -//} -// -//func (inc IncrementTwiceByCommand) String() string { -// return proto.CompactTextString(inc) -//} -// -//func (inc IncrementTwiceByCommand) ProtoMessage() { -//} -// -//func (inc IncrementTwiceByCommand) Reset() { -//} -// -//type IncrementTwiceByEvent struct { -// Value int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` -//} -// -//func (inc IncrementTwiceByEvent) String() string { -// return proto.CompactTextString(inc) -//} -// -//func (inc IncrementTwiceByEvent) ProtoMessage() { -//} -// -//func (inc IncrementTwiceByEvent) Reset() { -//} -// -//func TestEventSourcedHandlerEventsAreAppliedForEveryEmit(t *testing.T) { -// handler := newHandler(t) -// initHandler(handler, t) -// incrementedTo := int64(7) -// cmd, err := marshal(&IncrementTwiceByCommand{Amount: incrementedTo}, t) -// incrCommand := protocol.Command{ -// EntityId: "entity-0", -// Id: 1, -// Name: "IncrementTwiceByCommand", -// Payload: &any.Any{ -// TypeUrl: "type.googleapis.com/IncrementTwiceByCommand", -// Value: cmd, -// }, -// } -// err = handler.handleCommand(&incrCommand, TestEventSourcedHandleServer{}) -// if err != nil { -// t.Errorf("%v", err) -// } -// if testEntity.Value != incrementedTo { -// t.Errorf("testEntity.Value != incrementedTo") -// } -// -// decrCmdValue, err := proto.Marshal(&DecrementByCommand{Amount: incrementedTo}) -// if err != nil { -// t.Errorf("%v", err) -// } -// decrCommand := protocol.Command{ -// EntityId: "entity-0", -// Id: 1, -// Name: "DecrementByCommand", -// Payload: &any.Any{ -// TypeUrl: "type.googleapis.com/DecrementByCommand", -// Value: decrCmdValue, -// }, -// } -// err = handler.handleCommand(&decrCommand, TestEventSourcedHandleServer{}) -// if err != nil { -// t.Errorf("%v", err) -// } -// if testEntity.Value != 0 { -// t.Errorf("testEntity.Value != incrementedTo") -// } -//} From 14373dabdad775885471950d225c39df4bb200c0 Mon Sep 17 00:00:00 2001 From: Marcel Lanz Date: Wed, 16 Oct 2019 02:18:32 +0200 Subject: [PATCH 09/10] [PR Review Feedback] first batch of changes after review of https://github.com/cloudstateio/go-support/pull/5 --- build/build-and-publish-docker-image-tck.sh | 2 +- build/fetch-cloudstate-pb.sh | 25 +++++----- cloudstate/cloudstate.go | 51 ++++++++++----------- cloudstate/cloudstate_test.go | 11 +++-- tck/cmd/tck_shoppingcart/shoppingcart.go | 11 +++-- 5 files changed, 52 insertions(+), 48 deletions(-) diff --git a/build/build-and-publish-docker-image-tck.sh b/build/build-and-publish-docker-image-tck.sh index ea743ab..933b86a 100755 --- a/build/build-and-publish-docker-image-tck.sh +++ b/build/build-and-publish-docker-image-tck.sh @@ -5,4 +5,4 @@ set -o errexit set -o pipefail docker build -t gcr.io/mrcllnz/cloudstate-go-tck:latest -f ./build/TCK.Dockerfile . -docker push gcr.io/mrcllnz/cloudstate-go-tck:latest \ No newline at end of file +docker push gcr.io/mrcllnz/cloudstate-go-tck:latest diff --git a/build/fetch-cloudstate-pb.sh b/build/fetch-cloudstate-pb.sh index 98b6deb..3e8714d 100755 --- a/build/fetch-cloudstate-pb.sh +++ b/build/fetch-cloudstate-pb.sh @@ -7,27 +7,26 @@ set -o pipefail function fetch() { local path=$1 local tag=$2 - mkdir -p protobuf/$(dirname $path) - curl -o protobuf/${path} https://raw.githubusercontent.com/cloudstateio/cloudstate/${tag}/protocols/${path} - #sed 's/^option java_package.*/option go_package = "${go_package}";/' protobuf/${path} + mkdir -p "protobuf/$(dirname $path)" + curl -o "protobuf/${path}" "https://raw.githubusercontent.com/cloudstateio/cloudstate/${tag}/protocols/${path}" } tag=$1 # CloudState protocol -fetch "protocol/cloudstate/entity.proto" $tag -fetch "protocol/cloudstate/event_sourced.proto" $tag -fetch "protocol/cloudstate/function.proto" $tag -fetch "protocol/cloudstate/crdt.proto" $tag +fetch "protocol/cloudstate/entity.proto" "${tag}" +fetch "protocol/cloudstate/event_sourced.proto" "${tag}" +fetch "protocol/cloudstate/function.proto" "${tag}" +fetch "protocol/cloudstate/crdt.proto" "${tag}" # TCK shopping cart example -fetch "example/shoppingcart/shoppingcart.proto" $tag -fetch "example/shoppingcart/persistence/domain.proto" $tag +fetch "example/shoppingcart/shoppingcart.proto" "${tag}" +fetch "example/shoppingcart/persistence/domain.proto" "${tag}" # CloudState frontend -fetch "frontend/cloudstate/entity_key.proto" $tag +fetch "frontend/cloudstate/entity_key.proto" "${tag}" # dependencies -fetch "proxy/grpc/reflection/v1alpha/reflection.proto" $tag -fetch "frontend/google/api/annotations.proto" $tag -fetch "frontend/google/api/http.proto" $tag +fetch "proxy/grpc/reflection/v1alpha/reflection.proto" "${tag}" +fetch "frontend/google/api/annotations.proto" "${tag}" +fetch "frontend/google/api/http.proto" "${tag}" diff --git a/cloudstate/cloudstate.go b/cloudstate/cloudstate.go index 91de75d..662438a 100644 --- a/cloudstate/cloudstate.go +++ b/cloudstate/cloudstate.go @@ -19,16 +19,17 @@ import ( "context" "errors" "fmt" + "log" + "net" + "os" + "runtime" + "github.com/cloudstateio/go-support/cloudstate/protocol" "github.com/golang/protobuf/descriptor" "github.com/golang/protobuf/proto" filedescr "github.com/golang/protobuf/protoc-gen-go/descriptor" "github.com/golang/protobuf/ptypes/empty" "google.golang.org/grpc" - "log" - "net" - "os" - "runtime" ) const ( @@ -39,22 +40,22 @@ const ( // CloudState is an instance of a CloudState User Function type CloudState struct { server *grpc.Server - entityDiscoveryResponder *EntityDiscoveryResponder + entityDiscoveryResponder *EntityDiscoveryService eventSourcedHandler *EventSourcedHandler } -// NewCloudState returns a new CloudState instance. -func NewCloudState(options *Options) *CloudState { +// New returns a new CloudState instance. +func New(options Options) (*CloudState, error) { cs := &CloudState{ server: grpc.NewServer(), - entityDiscoveryResponder: NewEntityDiscoveryResponder(options), + entityDiscoveryResponder: newEntityDiscoveryResponder(options), eventSourcedHandler: NewEventSourcedHandler(), } protocol.RegisterEntityDiscoveryServer(cs.server, cs.entityDiscoveryResponder) log.Println("RegisterEntityDiscoveryServer") protocol.RegisterEventSourcedServer(cs.server, cs.eventSourcedHandler) log.Println("RegisterEventSourcedServer") - return cs + return cs, nil } // Options go get a CloudState instance configured. @@ -63,8 +64,6 @@ type Options struct { ServiceVersion string } -var NoOptions = Options{} - // DescriptorConfig configures service and dependent descriptors. type DescriptorConfig struct { Service string @@ -83,8 +82,8 @@ func (dc DescriptorConfig) AddDomainDescriptor(filename string) DescriptorConfig return dc } -// Register registers an event sourced entity for CloudState. -func (cs *CloudState) Register(ese *EventSourcedEntity, config DescriptorConfig) (err error) { +// RegisterEventSourcedEntity registers an event sourced entity for CloudState. +func (cs *CloudState) RegisterEventSourcedEntity(ese *EventSourcedEntity, config DescriptorConfig) (err error) { ese.registerOnce.Do(func() { if err = ese.initZeroValue(); err != nil { return @@ -111,7 +110,7 @@ func (cs *CloudState) Run() error { } lis, err := net.Listen("tcp", fmt.Sprintf("%s:%s", host, port)) if err != nil { - return fmt.Errorf("failed to listen: %v\n", err) + return fmt.Errorf("failed to listen: %v", err) } log.Printf("starting grpcServer at: %s:%s", host, port) if e := cs.server.Serve(lis); e != nil { @@ -120,16 +119,16 @@ func (cs *CloudState) Run() error { return nil } -// EntityDiscoveryResponder implements the CloudState discovery protocol. -type EntityDiscoveryResponder struct { +// EntityDiscoveryService implements the CloudState discovery protocol. +type EntityDiscoveryService struct { fileDescriptorSet *filedescr.FileDescriptorSet entitySpec *protocol.EntitySpec message *descriptor.Message } -// NewEntityDiscoveryResponder returns a new and initialized EntityDiscoveryResponder. -func NewEntityDiscoveryResponder(options *Options) *EntityDiscoveryResponder { - responder := &EntityDiscoveryResponder{} +// newEntityDiscoveryResponder returns a new and initialized EntityDiscoveryService. +func newEntityDiscoveryResponder(options Options) *EntityDiscoveryService { + responder := &EntityDiscoveryService{} responder.entitySpec = &protocol.EntitySpec{ Entities: make([]*protocol.Entity, 0), ServiceInfo: &protocol.ServiceInfo{ @@ -147,7 +146,7 @@ func NewEntityDiscoveryResponder(options *Options) *EntityDiscoveryResponder { } // Discover returns an entity spec for -func (r *EntityDiscoveryResponder) Discover(c context.Context, pi *protocol.ProxyInfo) (*protocol.EntitySpec, error) { +func (r *EntityDiscoveryService) Discover(c context.Context, pi *protocol.ProxyInfo) (*protocol.EntitySpec, error) { log.Printf("Received discovery call from sidecar [%s w%s] supporting CloudState %v.%v\n", pi.ProxyName, pi.ProxyVersion, @@ -173,12 +172,12 @@ func (r *EntityDiscoveryResponder) Discover(c context.Context, pi *protocol.Prox } // ReportError logs any user function error reported by the CloudState proxy. -func (r *EntityDiscoveryResponder) ReportError(c context.Context, fe *protocol.UserFunctionError) (*empty.Empty, error) { +func (r *EntityDiscoveryService) ReportError(c context.Context, fe *protocol.UserFunctionError) (*empty.Empty, error) { log.Printf("ReportError: %v\n", fe) return &empty.Empty{}, nil } -func (r *EntityDiscoveryResponder) updateSpec() (err error) { +func (r *EntityDiscoveryService) updateSpec() (err error) { protoBytes, err := proto.Marshal(r.fileDescriptorSet) if err != nil { return errors.New("unable to Marshal FileDescriptorSet") @@ -187,7 +186,7 @@ func (r *EntityDiscoveryResponder) updateSpec() (err error) { return nil } -func (r *EntityDiscoveryResponder) resolveFileDescriptors(dc DescriptorConfig) error { +func (r *EntityDiscoveryService) resolveFileDescriptors(dc DescriptorConfig) error { // service if dc.Service != "" { if err := r.registerFileDescriptorProto(dc.Service); err != nil { @@ -214,7 +213,7 @@ func (r *EntityDiscoveryResponder) resolveFileDescriptors(dc DescriptorConfig) e return nil } -func (r *EntityDiscoveryResponder) registerEntity(e *EventSourcedEntity, config DescriptorConfig) error { +func (r *EntityDiscoveryService) registerEntity(e *EventSourcedEntity, config DescriptorConfig) error { if err := r.resolveFileDescriptors(config); err != nil { return fmt.Errorf("failed to resolveFileDescriptor for DescriptorConfig: %+v: %w", config, err) } @@ -230,7 +229,7 @@ func (r *EntityDiscoveryResponder) registerEntity(e *EventSourcedEntity, config return r.updateSpec() } -func (r *EntityDiscoveryResponder) registerFileDescriptorProto(filename string) error { +func (r *EntityDiscoveryService) registerFileDescriptorProto(filename string) error { descriptorProto, err := unpackFile(proto.FileDescriptor(filename)) if err != nil { return fmt.Errorf("failed to registerFileDescriptorProto for filename: %s: %w", filename, err) @@ -239,7 +238,7 @@ func (r *EntityDiscoveryResponder) registerFileDescriptorProto(filename string) return r.updateSpec() } -func (r *EntityDiscoveryResponder) registerFileDescriptor(msg descriptor.Message) error { +func (r *EntityDiscoveryService) registerFileDescriptor(msg descriptor.Message) error { fd, _ := descriptor.ForMessage(msg) // this can panic if r := recover(); r != nil { return fmt.Errorf("descriptor.ForMessage panicked (%v) for: %+v", r, msg) diff --git a/cloudstate/cloudstate_test.go b/cloudstate/cloudstate_test.go index 607ea32..53b281a 100644 --- a/cloudstate/cloudstate_test.go +++ b/cloudstate/cloudstate_test.go @@ -19,16 +19,17 @@ package cloudstate import ( "bytes" "context" - "github.com/cloudstateio/go-support/cloudstate/protocol" - _ "google.golang.org/genproto/googleapis/api/annotations" "log" "os" "strings" "testing" + + "github.com/cloudstateio/go-support/cloudstate/protocol" + _ "google.golang.org/genproto/googleapis/api/annotations" ) func TestNewCloudState(t *testing.T) { - cloudState := NewCloudState(&Options{}) + cloudState, _ := New(Options{}) si := cloudState.server.GetServiceInfo() if si == nil { t.Fail() @@ -36,7 +37,7 @@ func TestNewCloudState(t *testing.T) { } func TestEntityDiscoveryResponderDiscover(t *testing.T) { - responder := NewEntityDiscoveryResponder(&Options{ + responder := newEntityDiscoveryResponder(Options{ ServiceName: "service.one", ServiceVersion: "0.0.1", }) @@ -78,7 +79,7 @@ func captureOutput(f func()) string { } func TestEntityDiscoveryResponderReportError(t *testing.T) { - responder := NewEntityDiscoveryResponder(&Options{ + responder := newEntityDiscoveryResponder(Options{ ServiceName: "service.one", ServiceVersion: "0.0.1", }) diff --git a/tck/cmd/tck_shoppingcart/shoppingcart.go b/tck/cmd/tck_shoppingcart/shoppingcart.go index ce3b008..0741b92 100644 --- a/tck/cmd/tck_shoppingcart/shoppingcart.go +++ b/tck/cmd/tck_shoppingcart/shoppingcart.go @@ -20,21 +20,25 @@ import ( "context" "errors" "fmt" + "log" + "github.com/cloudstateio/go-support/cloudstate" "github.com/cloudstateio/go-support/tck/shoppingcart" domain "github.com/cloudstateio/go-support/tck/shoppingcart/persistence" "github.com/golang/protobuf/ptypes/empty" - "log" ) // main creates a CloudState instance and registers the ShoppingCart // as a event sourced entity. func main() { - cloudState := cloudstate.NewCloudState(&cloudstate.Options{ + cloudState, err := cloudstate.New(cloudstate.Options{ ServiceName: "shopping-cart", ServiceVersion: "0.1.0", }) - err := cloudState.Register( + if err != nil { + log.Fatalf("CloudState.New failed: %v", err) + } + err = cloudState.RegisterEventSourcedEntity( &cloudstate.EventSourcedEntity{ Entity: (*ShoppingCart)(nil), ServiceName: "com.example.shoppingcart.ShoppingCart", @@ -43,6 +47,7 @@ func main() { Service: "shoppingcart/shoppingcart.proto", }.AddDomainDescriptor("domain.proto"), ) + if err != nil { log.Fatalf("CloudState failed to register entity: %v", err) } From accb0f358fbcc51a6a21793f129f00f24e853ec1 Mon Sep 17 00:00:00 2001 From: Marcel Lanz Date: Tue, 22 Oct 2019 19:29:13 +0200 Subject: [PATCH 10/10] [PR Review Feedback] second batch of changes after review of https://github.com/cloudstateio/go-support/pull/5 --- cloudstate/cloudstate.go | 49 +++++++++++++++----------------- cloudstate/eventsourced.go | 50 ++++++++++++++++----------------- cloudstate/eventsourced_test.go | 14 ++++----- 3 files changed, 55 insertions(+), 58 deletions(-) diff --git a/cloudstate/cloudstate.go b/cloudstate/cloudstate.go index 662438a..9813f2d 100644 --- a/cloudstate/cloudstate.go +++ b/cloudstate/cloudstate.go @@ -39,22 +39,20 @@ const ( // CloudState is an instance of a CloudState User Function type CloudState struct { - server *grpc.Server - entityDiscoveryResponder *EntityDiscoveryService - eventSourcedHandler *EventSourcedHandler + server *grpc.Server + entityDiscoveryServer *EntityDiscoveryServer + eventSourcedServer *EventSourcedServer } // New returns a new CloudState instance. func New(options Options) (*CloudState, error) { cs := &CloudState{ - server: grpc.NewServer(), - entityDiscoveryResponder: newEntityDiscoveryResponder(options), - eventSourcedHandler: NewEventSourcedHandler(), - } - protocol.RegisterEntityDiscoveryServer(cs.server, cs.entityDiscoveryResponder) - log.Println("RegisterEntityDiscoveryServer") - protocol.RegisterEventSourcedServer(cs.server, cs.eventSourcedHandler) - log.Println("RegisterEventSourcedServer") + server: grpc.NewServer(), + entityDiscoveryServer: newEntityDiscoveryResponder(options), + eventSourcedServer: newEventSourcedServer(), + } + protocol.RegisterEntityDiscoveryServer(cs.server, cs.entityDiscoveryServer) + protocol.RegisterEventSourcedServer(cs.server, cs.eventSourcedServer) return cs, nil } @@ -88,10 +86,10 @@ func (cs *CloudState) RegisterEventSourcedEntity(ese *EventSourcedEntity, config if err = ese.initZeroValue(); err != nil { return } - if err = cs.eventSourcedHandler.registerEntity(ese); err != nil { + if err = cs.eventSourcedServer.registerEntity(ese); err != nil { return } - if err = cs.entityDiscoveryResponder.registerEntity(ese, config); err != nil { + if err = cs.entityDiscoveryServer.registerEntity(ese, config); err != nil { return } }) @@ -112,23 +110,22 @@ func (cs *CloudState) Run() error { if err != nil { return fmt.Errorf("failed to listen: %v", err) } - log.Printf("starting grpcServer at: %s:%s", host, port) if e := cs.server.Serve(lis); e != nil { return fmt.Errorf("failed to grpcServer.Serve for: %v", lis) } return nil } -// EntityDiscoveryService implements the CloudState discovery protocol. -type EntityDiscoveryService struct { +// EntityDiscoveryServer implements the CloudState discovery protocol. +type EntityDiscoveryServer struct { fileDescriptorSet *filedescr.FileDescriptorSet entitySpec *protocol.EntitySpec message *descriptor.Message } -// newEntityDiscoveryResponder returns a new and initialized EntityDiscoveryService. -func newEntityDiscoveryResponder(options Options) *EntityDiscoveryService { - responder := &EntityDiscoveryService{} +// newEntityDiscoveryResponder returns a new and initialized EntityDiscoveryServer. +func newEntityDiscoveryResponder(options Options) *EntityDiscoveryServer { + responder := &EntityDiscoveryServer{} responder.entitySpec = &protocol.EntitySpec{ Entities: make([]*protocol.Entity, 0), ServiceInfo: &protocol.ServiceInfo{ @@ -146,7 +143,7 @@ func newEntityDiscoveryResponder(options Options) *EntityDiscoveryService { } // Discover returns an entity spec for -func (r *EntityDiscoveryService) Discover(c context.Context, pi *protocol.ProxyInfo) (*protocol.EntitySpec, error) { +func (r *EntityDiscoveryServer) Discover(c context.Context, pi *protocol.ProxyInfo) (*protocol.EntitySpec, error) { log.Printf("Received discovery call from sidecar [%s w%s] supporting CloudState %v.%v\n", pi.ProxyName, pi.ProxyVersion, @@ -172,12 +169,12 @@ func (r *EntityDiscoveryService) Discover(c context.Context, pi *protocol.ProxyI } // ReportError logs any user function error reported by the CloudState proxy. -func (r *EntityDiscoveryService) ReportError(c context.Context, fe *protocol.UserFunctionError) (*empty.Empty, error) { +func (r *EntityDiscoveryServer) ReportError(c context.Context, fe *protocol.UserFunctionError) (*empty.Empty, error) { log.Printf("ReportError: %v\n", fe) return &empty.Empty{}, nil } -func (r *EntityDiscoveryService) updateSpec() (err error) { +func (r *EntityDiscoveryServer) updateSpec() (err error) { protoBytes, err := proto.Marshal(r.fileDescriptorSet) if err != nil { return errors.New("unable to Marshal FileDescriptorSet") @@ -186,7 +183,7 @@ func (r *EntityDiscoveryService) updateSpec() (err error) { return nil } -func (r *EntityDiscoveryService) resolveFileDescriptors(dc DescriptorConfig) error { +func (r *EntityDiscoveryServer) resolveFileDescriptors(dc DescriptorConfig) error { // service if dc.Service != "" { if err := r.registerFileDescriptorProto(dc.Service); err != nil { @@ -213,7 +210,7 @@ func (r *EntityDiscoveryService) resolveFileDescriptors(dc DescriptorConfig) err return nil } -func (r *EntityDiscoveryService) registerEntity(e *EventSourcedEntity, config DescriptorConfig) error { +func (r *EntityDiscoveryServer) registerEntity(e *EventSourcedEntity, config DescriptorConfig) error { if err := r.resolveFileDescriptors(config); err != nil { return fmt.Errorf("failed to resolveFileDescriptor for DescriptorConfig: %+v: %w", config, err) } @@ -229,7 +226,7 @@ func (r *EntityDiscoveryService) registerEntity(e *EventSourcedEntity, config De return r.updateSpec() } -func (r *EntityDiscoveryService) registerFileDescriptorProto(filename string) error { +func (r *EntityDiscoveryServer) registerFileDescriptorProto(filename string) error { descriptorProto, err := unpackFile(proto.FileDescriptor(filename)) if err != nil { return fmt.Errorf("failed to registerFileDescriptorProto for filename: %s: %w", filename, err) @@ -238,7 +235,7 @@ func (r *EntityDiscoveryService) registerFileDescriptorProto(filename string) er return r.updateSpec() } -func (r *EntityDiscoveryService) registerFileDescriptor(msg descriptor.Message) error { +func (r *EntityDiscoveryServer) registerFileDescriptor(msg descriptor.Message) error { fd, _ := descriptor.ForMessage(msg) // this can panic if r := recover(); r != nil { return fmt.Errorf("descriptor.ForMessage panicked (%v) for: %+v", r, msg) diff --git a/cloudstate/eventsourced.go b/cloudstate/eventsourced.go index 0f7acd1..48b0ecc 100644 --- a/cloudstate/eventsourced.go +++ b/cloudstate/eventsourced.go @@ -125,8 +125,8 @@ func (c EntityInstanceContext) ServiceName() string { return c.EntityInstance.EventSourcedEntity.ServiceName } -// EventSourcedHandler is the implementation of the EventSourcedHandler server API for EventSourced service. -type EventSourcedHandler struct { +// EventSourcedServer is the implementation of the EventSourcedServer server API for EventSourced service. +type EventSourcedServer struct { // entities are indexed by their service name entities map[string]*EventSourcedEntity // contexts are entity instance contexts indexed by their entity ids @@ -135,22 +135,22 @@ type EventSourcedHandler struct { cmdMethodCache map[string]reflect.Method } -// NewEventSourcedHandler returns an initialized EventSourcedHandler -func NewEventSourcedHandler() *EventSourcedHandler { - return &EventSourcedHandler{ +// newEventSourcedServer returns an initialized EventSourcedServer +func newEventSourcedServer() *EventSourcedServer { + return &EventSourcedServer{ entities: make(map[string]*EventSourcedEntity), contexts: make(map[string]*EntityInstanceContext), cmdMethodCache: make(map[string]reflect.Method), } } -func (esh *EventSourcedHandler) registerEntity(ese *EventSourcedEntity) error { +func (esh *EventSourcedServer) registerEntity(ese *EventSourcedEntity) error { esh.entities[ese.ServiceName] = ese return nil } // Handle -// from EventSourcedServer.Handle +// // The stream. One stream will be established per active entity. // Once established, the first message sent will be Init, which contains the entity ID, and, // if the entity has previously persisted a snapshot, it will contain that snapshot. It will @@ -161,14 +161,14 @@ func (esh *EventSourcedHandler) registerEntity(ese *EventSourcedEntity) error { // message. The entity should reply in order, and any events that the entity requests to be // persisted the entity should handle itself, applying them to its own state, as if they had // arrived as events when the event stream was being replayed on load. -func (esh *EventSourcedHandler) Handle(server protocol.EventSourced_HandleServer) error { +func (esh *EventSourcedServer) Handle(stream protocol.EventSourced_HandleServer) error { var entityId string var failed error for { if failed != nil { return failed } - msg, recvErr := server.Recv() + msg, recvErr := stream.Recv() if recvErr == io.EOF { return nil } @@ -176,22 +176,22 @@ func (esh *EventSourcedHandler) Handle(server protocol.EventSourced_HandleServer return recvErr } if cmd := msg.GetCommand(); cmd != nil { - if err := esh.handleCommand(cmd, server); err != nil { + if err := esh.handleCommand(cmd, stream); err != nil { // TODO: in general, what happens with the stream here if an error happens? - failed = handleFailure(err, server, cmd.GetId()) + failed = handleFailure(err, stream, cmd.GetId()) } continue } if event := msg.GetEvent(); event != nil { // TODO spec: Why does command carry the entityId and an event not? if err := esh.handleEvent(entityId, event); err != nil { - failed = handleFailure(err, server, 0) + failed = handleFailure(err, stream, 0) } continue } if init := msg.GetInit(); init != nil { - if err := esh.handleInit(init, server); err != nil { - failed = handleFailure(err, server, 0) + if err := esh.handleInit(init, stream); err != nil { + failed = handleFailure(err, stream, 0) } entityId = init.GetEntityId() continue @@ -199,7 +199,7 @@ func (esh *EventSourcedHandler) Handle(server protocol.EventSourced_HandleServer } } -func (esh *EventSourcedHandler) handleInit(init *protocol.EventSourcedInit, server protocol.EventSourced_HandleServer) error { +func (esh *EventSourcedServer) handleInit(init *protocol.EventSourcedInit, server protocol.EventSourced_HandleServer) error { eid := init.GetEntityId() if _, present := esh.contexts[eid]; present { return NewFailureError("unable to server.Send") @@ -225,7 +225,7 @@ func (esh *EventSourcedHandler) handleInit(init *protocol.EventSourcedInit, serv return nil } -func (esh *EventSourcedHandler) handleInitSnapshot(init *protocol.EventSourcedInit) error { +func (esh *EventSourcedServer) handleInitSnapshot(init *protocol.EventSourcedInit) error { if init.Snapshot == nil { return nil } @@ -247,7 +247,7 @@ func (esh *EventSourcedHandler) handleInitSnapshot(init *protocol.EventSourcedIn return nil } -func (EventSourcedHandler) unmarshalSnapshot(init *protocol.EventSourcedInit) (interface{}, error) { +func (EventSourcedServer) unmarshalSnapshot(init *protocol.EventSourcedInit) (interface{}, error) { // see: https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/any#typeurl typeUrl := init.Snapshot.Snapshot.GetTypeUrl() if !strings.Contains(typeUrl, "://") { @@ -280,7 +280,7 @@ func (EventSourcedHandler) unmarshalSnapshot(init *protocol.EventSourcedInit) (i return nil, fmt.Errorf("unmarshalling snapshot failed with: no snapshot unmarshaller found for: %v", typeURL.String()) } -func (esh *EventSourcedHandler) subscribeEvents(instance *EntityInstance) { +func (esh *EventSourcedServer) subscribeEvents(instance *EntityInstance) { if emitter, ok := instance.Instance.(EventEmitter); ok { emitter.Subscribe(&Subscription{ OnNext: func(event interface{}) error { @@ -296,7 +296,7 @@ func (esh *EventSourcedHandler) subscribeEvents(instance *EntityInstance) { } } -func (esh *EventSourcedHandler) handleEvent(entityId string, event *protocol.EventSourcedEvent) error { +func (esh *EventSourcedServer) handleEvent(entityId string, event *protocol.EventSourcedEvent) error { if entityId == "" { return NewFailureError("no entityId was found from a previous init message for event sequence: %v", event.Sequence) } @@ -332,7 +332,7 @@ func (esh *EventSourcedHandler) handleEvent(entityId string, event *protocol.Eve // Beside calling the service method, we have to collect "events" the service might emit. // These events afterwards have to be handled by a EventHandler to update the state of the // entity. The CloudState proxy can re-play these events at any time -func (esh *EventSourcedHandler) handleCommand(cmd *protocol.Command, server protocol.EventSourced_HandleServer) error { +func (esh *EventSourcedServer) handleCommand(cmd *protocol.Command, server protocol.EventSourced_HandleServer) error { // method to call method, err := esh.methodToCall(cmd) if err != nil { @@ -400,7 +400,7 @@ func (esh *EventSourcedHandler) handleCommand(cmd *protocol.Command, server prot }, server) } -func (*EventSourcedHandler) buildInputs(entityContext *EntityInstanceContext, method reflect.Method, cmd *protocol.Command, ctx context.Context) ([]reflect.Value, error) { +func (*EventSourcedServer) buildInputs(entityContext *EntityInstanceContext, method reflect.Method, cmd *protocol.Command, ctx context.Context) ([]reflect.Value, error) { inputs := make([]reflect.Value, method.Type.NumIn()) inputs[0] = reflect.ValueOf(entityContext.EntityInstance.Instance) inputs[1] = reflect.ValueOf(ctx) @@ -424,7 +424,7 @@ func (*EventSourcedHandler) buildInputs(entityContext *EntityInstanceContext, me return inputs, nil } -func (esh *EventSourcedHandler) methodToCall(cmd *protocol.Command) (reflect.Method, error) { +func (esh *EventSourcedServer) methodToCall(cmd *protocol.Command) (reflect.Method, error) { entityContext := esh.contexts[cmd.GetEntityId()] cacheKey := entityContext.ServiceName() + cmd.Name method, hit := esh.cmdMethodCache[cacheKey] @@ -464,7 +464,7 @@ func (esh *EventSourcedHandler) methodToCall(cmd *protocol.Command) (reflect.Met return method, nil } -func (*EventSourcedHandler) handleSnapshots(entityContext *EntityInstanceContext) (*any.Any, error) { +func (*EventSourcedServer) handleSnapshots(entityContext *EntityInstanceContext) (*any.Any, error) { if !entityContext.EntityInstance.shouldSnapshot() { return nil, nil } @@ -494,7 +494,7 @@ func checkUnary(methodByName reflect.Value) error { } // applyEvent applies an event to a local entity -func (esh EventSourcedHandler) applyEvent(entityInstance *EntityInstance, event interface{}) error { +func (esh EventSourcedServer) applyEvent(entityInstance *EntityInstance, event interface{}) error { payload, err := marshalAny(event) if err != nil { return err @@ -509,7 +509,7 @@ func (esh EventSourcedHandler) applyEvent(entityInstance *EntityInstance, event // and snapshots is to use protobufs. CloudState will automatically detect if // an emitted event is a protobuf, and serialize it as such. For other // serialization options, including JSON, see Serialization. -func (EventSourcedHandler) handleEvents(entityInstance *EntityInstance, events ...*protocol.EventSourcedEvent) error { +func (EventSourcedServer) handleEvents(entityInstance *EntityInstance, events ...*protocol.EventSourcedEvent) error { eventHandler, implementsEventHandler := entityInstance.Instance.(EventHandler) for _, event := range events { // TODO: here's the point where events can be protobufs, serialized as json or other formats diff --git a/cloudstate/eventsourced_test.go b/cloudstate/eventsourced_test.go index 6ad30cf..5924169 100644 --- a/cloudstate/eventsourced_test.go +++ b/cloudstate/eventsourced_test.go @@ -188,11 +188,11 @@ func (t TestEventSourcedHandleServer) Recv() (*protocol.EventSourcedStreamIn, er return nil, nil } -func newHandler(t *testing.T) *EventSourcedHandler { - handler := NewEventSourcedHandler() +func newHandler(t *testing.T) *EventSourcedServer { + handler := newEventSourcedServer() entity := EventSourcedEntity{ Entity: (*TestEntity)(nil), - ServiceName: "TestEventSourcedHandler-Service", + ServiceName: "TestEventSourcedServer-Service", SnapshotEvery: 0, registerOnce: sync.Once{}, } @@ -207,9 +207,9 @@ func newHandler(t *testing.T) *EventSourcedHandler { return handler } -func initHandler(handler *EventSourcedHandler, t *testing.T) { +func initHandler(handler *EventSourcedServer, t *testing.T) { err := handler.handleInit(&protocol.EventSourcedInit{ - ServiceName: "TestEventSourcedHandler-Service", + ServiceName: "TestEventSourcedServer-Service", EntityId: "entity-0", }, nil) if err != nil { @@ -253,7 +253,7 @@ func TestSnapshot(t *testing.T) { t.Fatalf("%v", err) } err = handler.handleInit(&protocol.EventSourcedInit{ - ServiceName: "TestEventSourcedHandler-Service", + ServiceName: "TestEventSourcedServer-Service", EntityId: "entity-0", Snapshot: &protocol.EventSourcedSnapshot{ SnapshotSequence: 0, @@ -268,7 +268,7 @@ func TestSnapshot(t *testing.T) { } } -func TestEventSourcedHandlerHandlesCommandAndEvents(t *testing.T) { +func TestEventSourcedServerHandlesCommandAndEvents(t *testing.T) { resetTestEntity() handler := newHandler(t) if testEntity.Value >= 0 {