From 469887d5442323297326180b8c242918926e7593 Mon Sep 17 00:00:00 2001 From: Marco Massenzio Date: Fri, 9 Dec 2022 00:18:40 -0800 Subject: [PATCH 1/4] [Hotfix] Increased timeout to ensure test doesn't time out on slow machine --- build.settings | 2 +- pubsub/sqs_pub_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.settings b/build.settings index 465815c..f1245a2 100644 --- a/build.settings +++ b/build.settings @@ -1,3 +1,3 @@ # Build configuration -version = 0.7.0 +version = 0.7.1 diff --git a/pubsub/sqs_pub_test.go b/pubsub/sqs_pub_test.go index 1320be0..7ff8135 100644 --- a/pubsub/sqs_pub_test.go +++ b/pubsub/sqs_pub_test.go @@ -183,7 +183,7 @@ var _ = Describe("SQS Publisher", func() { } }() close(notificationsCh) - Eventually(done).Should(BeClosed()) + Eventually(done, "5s").Should(BeClosed()) }) It("will only notify error outcomes if configured to", func() { responseOk := protos.EventResponse{ From 5d525197ed3537644b98ff66800a78090af63352 Mon Sep 17 00:00:00 2001 From: Marco Massenzio Date: Fri, 9 Dec 2022 10:57:00 -0800 Subject: [PATCH 2/4] Updated clean target --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d1a8651..06e94e6 100644 --- a/Makefile +++ b/Makefile @@ -38,10 +38,10 @@ help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) .PHONY: clean +img=$(shell docker images -q --filter=reference=$(image)) clean: ## Cleans up the binary, container image and other data @rm -f $(out) - @docker-compose -f $(compose) down - @docker rmi $(shell docker images -q --filter=reference=$(image)) + @[ ! -z $(img) ] && docker rmi $(img) || true .PHONY: build test container cov clean fmt fmt: ## Formats the Go source code using 'go fmt' From ec7e483460c7be436327313494e475666b52605b Mon Sep 17 00:00:00 2001 From: Marco Massenzio <1153951+massenz@users.noreply.github.com> Date: Sat, 17 Dec 2022 21:05:19 -0800 Subject: [PATCH 3/4] [#33] Implement data model (#68) * [#33] Implement data model * Added update state tests --- docs/images/datamodel.png | Bin 0 -> 156825 bytes storage/memory_store.go | 29 ++++++-- storage/redis_sets_store.go | 72 ++++++++++++++++++++ storage/redis_store.go | 9 +-- storage/redis_store_test.go | 129 +++++++++++++++++++++++++++++++++++- storage/types.go | 32 ++++++++- 6 files changed, 256 insertions(+), 15 deletions(-) create mode 100644 docs/images/datamodel.png create mode 100644 storage/redis_sets_store.go diff --git a/docs/images/datamodel.png b/docs/images/datamodel.png new file mode 100644 index 0000000000000000000000000000000000000000..b4c49d6568b9890f1969b56cd6fd0c5eb545913f GIT binary patch literal 156825 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 literal 0 HcmV?d00001 diff --git a/storage/memory_store.go b/storage/memory_store.go index 641c585..b89ca35 100644 --- a/storage/memory_store.go +++ b/storage/memory_store.go @@ -61,12 +61,6 @@ func (csm *InMemoryStore) put(key string, value proto.Message) error { return err } -func (csm *InMemoryStore) GetAllInState(cfg string, state string) []*protos.FiniteStateMachine { - // TODO [#33] Ability to query for all machines in a given state - csm.logger.Error("Not implemented") - return nil -} - func (csm *InMemoryStore) GetEvent(id string, cfg string) (*protos.Event, bool) { key := NewKeyForEvent(id, cfg) event := &protos.Event{} @@ -144,3 +138,26 @@ func (csm *InMemoryStore) GetTimeout() time.Duration { func (csm *InMemoryStore) Health() error { return nil } + +func (csm *InMemoryStore) GetAllInState(cfg string, state string) []string { + // TODO [#33] Ability to query for all machines in a given state + csm.logger.Error(NotImplementedError("GetAllInState").Error()) + return nil +} + +func (csm *InMemoryStore) GetAllConfigs() []string { + // TODO [#33] Ability to query for all machines in a given state + csm.logger.Error(NotImplementedError("GetAllConfigs").Error()) + return nil +} + +func (csm *InMemoryStore) GetAllVersions(name string) []string { + // TODO [#33] Ability to query for all machines in a given state + csm.logger.Error(NotImplementedError("GetAllVersions").Error()) + return nil +} +func (csm *InMemoryStore) UpdateState(cfgName string, id string, oldState string, newState string) error { + // TODO [#33] Ability to query for all machines in a given state + csm.logger.Error(NotImplementedError("GetAllVersions").Error()) + return nil +} diff --git a/storage/redis_sets_store.go b/storage/redis_sets_store.go new file mode 100644 index 0000000..b7fc507 --- /dev/null +++ b/storage/redis_sets_store.go @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 AlertAvert.com. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Author: Marco Massenzio (marco@alertavert.com) + */ + +package storage + +import ( + "context" +) + +func (csm *RedisStore) UpdateState(cfgName string, id string, oldState string, newState string) error { + var key string + var err error + if newState == "" { + return IllegalStoreError("when updating state, new state cannot be empty") + } + if oldState != "" { + key = NewKeyForMachinesByState(cfgName, oldState) + err = csm.client.SRem(context.Background(), key, id).Err() + if err != nil { + csm.logger.Error("Cannot update FSM [%s#%s] from old state `%s`", cfgName, id, oldState) + return err + } + } + key = NewKeyForMachinesByState(cfgName, newState) + err = csm.client.SAdd(context.Background(), key, id).Err() + if err != nil { + csm.logger.Error("Cannot update FSM [%s#%s] to new state `%s`", cfgName, id, newState) + } + return err +} + +func (csm *RedisStore) GetAllInState(cfg string, state string) []string { + // TODO: enable splitting results with a (cursor, count) + csm.logger.Debug("Looking up all FSMs [%s] in DB with state `%s`", cfg, state) + key := NewKeyForMachinesByState(cfg, state) + fsms, err := csm.client.SMembers(context.Background(), key).Result() + if err != nil { + csm.logger.Error("Could not retrieve FSMs for state `%s`: %s", state, err) + return nil + } + csm.logger.Debug("Returning %d items", len(fsms)) + return fsms +} + +func (csm *RedisStore) GetAllConfigs() []string { + // TODO: enable splitting results with a (cursor, count) + csm.logger.Debug("Looking up all configs in DB") + configs, err := csm.client.SMembers(context.Background(), ConfigsPrefix).Result() + if err != nil { + csm.logger.Error("Could not retrieve configurations: %s", err) + return nil + } + csm.logger.Debug("Returning %d items", len(configs)) + return configs +} + +func (csm *RedisStore) GetAllVersions(name string) []string { + csm.logger.Debug("Looking up all versions for Configurations `%s` in DB", name) + configs, err := csm.client.SMembers(context.Background(), NewKeyForConfig(name)).Result() + if err != nil { + csm.logger.Error("Could not retrieve configurations: %s", err) + return nil + } + csm.logger.Debug("Returning %d items", len(configs)) + return configs +} diff --git a/storage/redis_store.go b/storage/redis_store.go index dfd827e..9113968 100644 --- a/storage/redis_store.go +++ b/storage/redis_store.go @@ -40,12 +40,6 @@ type RedisStore struct { MaxRetries int } -func (csm *RedisStore) GetAllInState(cfg string, state string) []*protos.FiniteStateMachine { - // TODO [#33] Ability to query for all machines in a given state - csm.logger.Error(NotImplementedError("GetAllInState").Error()) - return nil -} - func (csm *RedisStore) GetConfig(id string) (*protos.Configuration, bool) { key := NewKeyForConfig(id) var cfg protos.Configuration @@ -87,6 +81,9 @@ func (csm *RedisStore) PutConfig(cfg *protos.Configuration) error { if csm.client.Exists(context.Background(), key).Val() == 1 { return AlreadyExistsError(key) } + // TODO: Find out whether the client allows to batch requests, instead of sending multiple server requests + csm.client.SAdd(context.Background(), ConfigsPrefix, cfg.Name) + csm.client.SAdd(context.Background(), NewKeyForConfig(cfg.Name), api.GetVersionId(cfg)) return csm.put(key, cfg, NeverExpire) } diff --git a/storage/redis_store_test.go b/storage/redis_store_test.go index ddd49c3..c39d892 100644 --- a/storage/redis_store_test.go +++ b/storage/redis_store_test.go @@ -11,6 +11,7 @@ package storage_test import ( "context" + "fmt" . "github.com/JiaYongfei/respect/gomega" "github.com/go-redis/redis/v8" "github.com/golang/protobuf/proto" @@ -25,7 +26,7 @@ import ( var _ = Describe("RedisStore", func() { - Context("when configured locally", func() { + Context("for simple operations", func() { var store storage.StoreManager var rdb *redis.Client var cfg *protos.Configuration @@ -49,9 +50,11 @@ var _ = Describe("RedisStore", func() { Addr: container.Address, DB: storage.DefaultRedisDb, }) + }, 0.5) + AfterEach(func() { // Cleaning up the DB to prevent "dirty" store to impact test results rdb.FlushDB(context.Background()) - }) + }, 0.2) It("is healthy", func() { Expect(store.Health()).To(Succeed()) }) @@ -213,4 +216,126 @@ var _ = Describe("RedisStore", func() { storage.NeverExpire)).To(HaveOccurred()) }) }) + + When("querying for configurations", func() { + var store storage.StoreManager + var rdb *redis.Client + + BeforeEach(func() { + Expect(container).ToNot(BeNil()) + store = storage.NewRedisStoreWithDefaults(container.Address) + Expect(store).ToNot(BeNil()) + store.SetLogLevel(slf4go.NONE) + + // This is used to go "behind the back" of our StoreManager and mess with it for testing + // purposes. Do NOT do this in your code. + rdb = redis.NewClient(&redis.Options{ + Addr: container.Address, + DB: storage.DefaultRedisDb, + }) + }, 0.5) + AfterEach(func() { + // Cleaning up the DB to prevent "dirty" store to impact test results + rdb.FlushDB(context.Background()) + }, 0.2) + + It("can get all configuration names", func() { + for _, name := range []string{"orders", "devices", "users"} { + Expect(store.PutConfig(&protos.Configuration{Name: name, Version: "v3", StartingState: "start"})). + ToNot(HaveOccurred()) + } + configs := store.GetAllConfigs() + Expect(len(configs)).To(Equal(3)) + Expect(configs).To(ContainElements("orders", "devices", "users")) + }) + It("can get all versions of a configuration", func() { + for _, version := range []string{"v1alpha1", "v1beta", "v1"} { + Expect(store.PutConfig(&protos.Configuration{Name: "orders", Version: version, StartingState: "start"})). + ToNot(HaveOccurred()) + } + configs := store.GetAllVersions("orders") + Expect(len(configs)).To(Equal(3)) + Expect(configs).To(ContainElements("orders:v1alpha1", "orders:v1beta", "orders:v1")) + }) + It("returns an empty slice for a non-existent config", func() { + configs := store.GetAllVersions("fake") + Expect(len(configs)).To(Equal(0)) + }) + }) + When("querying for FSMs", func() { + var store storage.StoreManager + var rdb *redis.Client + + BeforeEach(func() { + Expect(container).ToNot(BeNil()) + store = storage.NewRedisStoreWithDefaults(container.Address) + Expect(store).ToNot(BeNil()) + store.SetLogLevel(slf4go.NONE) + + // This is used to go "behind the back" of our StoreManager and mess with it for testing + // purposes. Do NOT do this in your code. + rdb = redis.NewClient(&redis.Options{ + Addr: container.Address, + DB: storage.DefaultRedisDb, + }) + }, 0.5) + AfterEach(func() { + // Cleaning up the DB to prevent "dirty" store to impact test results + rdb.FlushDB(context.Background()) + }, 0.2) + It("finds them by state", func() { + for id := 1; id < 5; id++ { + fsm := &protos.FiniteStateMachine{ + ConfigId: "orders:v4", + State: "in_transit", + History: []*protos.Event{ + {Transition: &protos.Transition{Event: "confirmed"}, Originator: "bot"}, + {Transition: &protos.Transition{Event: "shipped"}, Originator: "bot"}, + }, + } + fsmId := fmt.Sprintf("fsm-%d", id) + Expect(store.PutStateMachine(fsmId, fsm)).ToNot(HaveOccurred()) + Expect(store.UpdateState("orders", fsmId, "", fsm.State)) + } + res := store.GetAllInState("orders", "in_transit") + Expect(len(res)).To(Equal(4)) + for id := 1; id < 5; id++ { + Expect(res).To(ContainElement(fmt.Sprintf("fsm-%d", id))) + } + }) + When("transitioning state", func() { + BeforeEach(func() { + for id := 1; id < 10; id++ { + fsm := &protos.FiniteStateMachine{ + ConfigId: "orders:v4", + State: "in_transit", + History: []*protos.Event{ + {Transition: &protos.Transition{Event: "confirmed"}, Originator: "bot"}, + {Transition: &protos.Transition{Event: "shipped"}, Originator: "bot"}, + }, + } + fsmId := fmt.Sprintf("fsm-%d", id) + Expect(store.PutStateMachine(fsmId, fsm)).ToNot(HaveOccurred()) + Expect(store.UpdateState("orders", fsmId, "", fsm.State)) + } + }) + It("finds them", func() { + for id := 3; id < 6; id++ { + fsmId := fmt.Sprintf("fsm-%d", id) + Expect(store.UpdateState("orders", fsmId, "in_transit", "shipped")) + } + res := store.GetAllInState("orders", "shipped") + Expect(len(res)).To(Equal(3)) + for id := 3; id < 6; id++ { + Expect(res).To(ContainElement(fmt.Sprintf("fsm-%d", id))) + } + res = store.GetAllInState("orders", "in_transit") + Expect(len(res)).To(Equal(6)) + }) + It("will fail for an empty newState", func() { + Expect(store.UpdateState("fake", "12345678", "in_transit", "")).ToNot(Succeed()) + }) + }) + }) + }) diff --git a/storage/types.go b/storage/types.go index 048f050..258fa8a 100644 --- a/storage/types.go +++ b/storage/types.go @@ -32,12 +32,42 @@ var ( type ConfigurationStorageManager interface { GetConfig(versionId string) (*protos.Configuration, bool) PutConfig(cfg *protos.Configuration) error + + // GetAllConfigs returns all the `Configurations` that exist in the server, regardless of + // the version, and whether are used or not by an FSM. + GetAllConfigs() []string + + // GetAllVersions returns the full `name:version` ID of all the Configurations whose + // name matches `name`. + GetAllVersions(name string) []string } type FiniteStateMachineStorageManager interface { + // GetStateMachine will find the FSM with `id and that is configured via a `Configuration` whose + // `name` matches `cfg` (without the `version`). GetStateMachine(id string, cfg string) (*protos.FiniteStateMachine, bool) + + // PutStateMachine creates or updates the FSM whose `id` is given. + // No further action is taken: no check that the referenced `Configuration` exists, and the + // `state` SETs are not updated either: it is the caller's responsibility to call the + // `UpdateState` method (possibly with an empty `oldState`, in the case of creation). PutStateMachine(id string, fsm *protos.FiniteStateMachine) error - GetAllInState(cfg string, state string) []*protos.FiniteStateMachine + + // GetAllInState looks up all the FSMs that are currently in the given `state` and + // are configured with a `Configuration` whose name matches `cfg` (regardless of the + // configuration's version). + // + // It returns the IDs for the FSMs. + GetAllInState(cfg string, state string) []string + + // UpdateState will move the FSM's `id` from/to the respective Redis SETs. + // + // When creating or updating an FSM with `PutStateMachine`, the state SETs are not + // modified; it is the responsibility of the caller to manage the FSM state appropriately + // (or not, as the case may be). + // + // `oldState` may be empty in the case of a new FSM being created. + UpdateState(cfgName string, id string, oldState string, newState string) error } type EventStorageManager interface { From 0d167f5030336f5f25f2d3821288edd0fde1a266 Mon Sep 17 00:00:00 2001 From: Marco Massenzio <1153951+massenz@users.noreply.github.com> Date: Tue, 27 Dec 2022 23:42:25 -0800 Subject: [PATCH 4/4] [#33] Update API & gRPC Implementation (#74) * Updated gRPC API to enable new Data Model * Updated response to use one_of; * Added test to retrieve all versions of configuration. * Added Streaming methods * Updated API Proto dependency version --- ...ll Tests.run.xml => Run all tests.run.xml} | 12 +- build.settings | 2 +- go.mod | 7 +- go.sum | 11 +- grpc/grpc_server.go | 149 +++++-- grpc/grpc_server_stream_test.go | 173 ++++++++ grpc/grpc_server_test.go | 380 ++++++++++++------ grpc/grpc_suite_test.go | 29 +- internals/testing/containers.go | 94 +++++ pubsub/listener.go | 60 +-- pubsub/listener_test.go | 17 +- pubsub/pubsub_suite_test.go | 60 +-- pubsub/sqs_pub_test.go | 10 +- pubsub/sqs_sub.go | 2 +- pubsub/sqs_sub_test.go | 4 +- server/event_handlers_test.go | 4 +- server/types.go | 2 +- storage/redis_sets_store.go | 23 +- storage/redis_store.go | 2 +- storage/redis_store_test.go | 11 +- storage/storage_suite_test.go | 47 +-- 21 files changed, 762 insertions(+), 337 deletions(-) rename .run/{Run All Tests.run.xml => Run all tests.run.xml} (67%) create mode 100644 grpc/grpc_server_stream_test.go create mode 100644 internals/testing/containers.go diff --git a/.run/Run All Tests.run.xml b/.run/Run all tests.run.xml similarity index 67% rename from .run/Run All Tests.run.xml rename to .run/Run all tests.run.xml index 6cf7d44..7946e6a 100644 --- a/.run/Run All Tests.run.xml +++ b/.run/Run all tests.run.xml @@ -1,16 +1,8 @@ - - + @@ -18,4 +10,4 @@ - + \ No newline at end of file diff --git a/build.settings b/build.settings index f1245a2..3f5338a 100644 --- a/build.settings +++ b/build.settings @@ -1,3 +1,3 @@ # Build configuration -version = 0.7.1 +version = 0.8.0 diff --git a/go.mod b/go.mod index 6c91679..34f1b35 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,11 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/massenz/slf4go v0.3.2-g4eb5504 - github.com/massenz/statemachine-proto/golang v0.6.0-ga901a76 + github.com/massenz/statemachine-proto/golang v1.1.0-beta-g1fc5dd8 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.18.1 github.com/testcontainers/testcontainers-go v0.16.0 - google.golang.org/grpc v1.49.0 + google.golang.org/grpc v1.51.0 google.golang.org/protobuf v1.28.1 ) @@ -132,7 +132,7 @@ require ( golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.1.0 // indirect golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/text v0.4.0 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad // indirect @@ -153,7 +153,6 @@ replace ( github.com/docker/cli => github.com/docker/cli v20.10.3-0.20221013132413-1d6c6e2367e2+incompatible // 22.06 master branch github.com/docker/docker => github.com/docker/docker v20.10.3-0.20221013203545-33ab36d6b304+incompatible // 22.06 branch github.com/moby/buildkit => github.com/moby/buildkit v0.10.1-0.20220816171719-55ba9d14360a // same as buildx - github.com/opencontainers/runc => github.com/opencontainers/runc v1.1.2 // Can be removed on next bump of containerd to > 1.6.4 // For k8s dependencies, we use a replace directive, to prevent them being diff --git a/go.sum b/go.sum index 36598d0..d86186f 100644 --- a/go.sum +++ b/go.sum @@ -380,8 +380,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/massenz/slf4go v0.3.2-g4eb5504 h1:tRrxPOKcqNKQn25eS8Dy9bW3NMNPpuK4Sla9jKfWmSs= github.com/massenz/slf4go v0.3.2-g4eb5504/go.mod h1:ZJjthXAnZMJGwXUz3Z3v5uyban00uAFFoDYODOoLFpw= -github.com/massenz/statemachine-proto/golang v0.6.0-ga901a76 h1:tik7Xn5GL+w9U5RTJZ3mieoP2sun6RDM+cUBi7WGrUU= -github.com/massenz/statemachine-proto/golang v0.6.0-ga901a76/go.mod h1:EkwQg7wD6c/cmXVxfqNaUOVSrBLlti+xYljIxaQNJqA= +github.com/massenz/statemachine-proto/golang v1.1.0-beta-g1fc5dd8 h1:Dp2yv070ogiHLwQU5LppXskUDnCoO8tDkqgszyZMNmk= +github.com/massenz/statemachine-proto/golang v1.1.0-beta-g1fc5dd8/go.mod h1:g6CkyXxfs7XF8wv6OLdMZZDUu0fn4PY6HQQ2WDbW3GU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -820,8 +820,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -954,8 +955,8 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/grpc/grpc_server.go b/grpc/grpc_server.go index a02683e..8e6770d 100644 --- a/grpc/grpc_server.go +++ b/grpc/grpc_server.go @@ -17,6 +17,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/wrapperspb" "strings" "time" @@ -32,6 +33,9 @@ type Config struct { Timeout time.Duration } +type StatemachineStream = protos.StatemachineService_StreamAllInstateServer +type ConfigurationStream = protos.StatemachineService_StreamAllConfigurationsServer + var _ protos.StatemachineServiceServer = (*grpcSubscriber)(nil) const ( @@ -43,9 +47,9 @@ type grpcSubscriber struct { *Config } -func (s *grpcSubscriber) ProcessEvent(ctx context.Context, request *protos.EventRequest) (*protos. +func (s *grpcSubscriber) SendEvent(ctx context.Context, request *protos.EventRequest) (*protos. EventResponse, error) { - if request.Dest == "" { + if request.GetId() == "" { return nil, status.Error(codes.FailedPrecondition, api.MissingDestinationError.Error()) } if request.GetEvent() == nil || request.Event.GetTransition() == nil || @@ -87,31 +91,46 @@ func (s *grpcSubscriber) PutConfiguration(ctx context.Context, cfg *protos.Confi } s.Logger.Trace("configuration stored: %s", api.GetVersionId(cfg)) return &protos.PutResponse{ - Id: api.GetVersionId(cfg), - Config: cfg, + Id: api.GetVersionId(cfg), + // Note: this is the magic incantation to use a `one_of` field in Protobuf. + EntityResponse: &protos.PutResponse_Config{Config: cfg}, }, nil } +func (s *grpcSubscriber) GetAllConfigurations(ctx context.Context, req *wrapperspb.StringValue) ( + *protos.ListResponse, error) { + cfgName := req.Value + if cfgName == "" { + s.Logger.Trace("looking up all available configurations on server") + return &protos.ListResponse{Ids: s.Store.GetAllConfigs()}, nil + } + s.Logger.Trace("looking up all version for configuration %s", cfgName) + return &protos.ListResponse{Ids: s.Store.GetAllVersions(cfgName)}, nil +} -func (s *grpcSubscriber) GetConfiguration(ctx context.Context, request *protos.GetRequest) ( +func (s *grpcSubscriber) GetConfiguration(ctx context.Context, configId *wrapperspb.StringValue) ( *protos.Configuration, error) { - s.Logger.Trace("retrieving Configuration %s", request.GetId()) - cfg, found := s.Store.GetConfig(request.GetId()) + cfgId := configId.Value + s.Logger.Trace("retrieving Configuration %s", cfgId) + cfg, found := s.Store.GetConfig(cfgId) if !found { - return nil, status.Errorf(codes.NotFound, "configuration %s not found", request.GetId()) + return nil, status.Errorf(codes.NotFound, "configuration %s not found", cfgId) } return cfg, nil } func (s *grpcSubscriber) PutFiniteStateMachine(ctx context.Context, - fsm *protos.FiniteStateMachine) (*protos.PutResponse, error) { + request *protos.PutFsmRequest) (*protos.PutResponse, error) { + fsm := request.Fsm // First check that the configuration for the FSM is valid cfg, ok := s.Store.GetConfig(fsm.ConfigId) if !ok { return nil, status.Error(codes.FailedPrecondition, storage.NotFoundError( fsm.ConfigId).Error()) } - // FIXME: we need to allow clients to specify the ID of the FSM to create - id := uuid.NewString() + var id = request.Id + if id == "" { + id = uuid.NewString() + } // If the State of the FSM is not specified, // we set it to the initial state of the configuration. if fsm.State == "" { @@ -122,38 +141,52 @@ func (s *grpcSubscriber) PutFiniteStateMachine(ctx context.Context, s.Logger.Error("could not store FSM [%v]: %v", fsm, err) return nil, status.Error(codes.Internal, err.Error()) } - return &protos.PutResponse{Id: id, Fsm: fsm}, nil + if err := s.Store.UpdateState(cfg.Name, id, "", fsm.State); err != nil { + s.Logger.Error("could not store FSM in state set [%s]: %v", fsm.State, err) + return nil, status.Error(codes.Internal, err.Error()) + } + return &protos.PutResponse{Id: id, EntityResponse: &protos.PutResponse_Fsm{Fsm: fsm}}, nil } -func (s *grpcSubscriber) GetFiniteStateMachine(ctx context.Context, request *protos.GetRequest) ( +func (s *grpcSubscriber) GetFiniteStateMachine(ctx context.Context, in *protos.GetFsmRequest) ( *protos.FiniteStateMachine, error) { - // TODO: use Context to set a timeout, and then pass it on to the Store. - // This may require a pretty large refactoring of the store interface. - s.Logger.Debug("looking up FSM %s", request.GetId()) - // The ID in the request contains the FSM ID, - // prefixed by the Config Name (which defines the "type" of FSM) - splitId := strings.Split(request.GetId(), storage.KeyPrefixIDSeparator) - if len(splitId) != 2 { - return nil, status.Errorf(codes.InvalidArgument, "invalid FSM ID: %s", request.GetId()) - } - fsm, ok := s.Store.GetStateMachine(splitId[1], splitId[0]) + cfg := in.GetConfig() + if cfg == "" { + return nil, status.Error(codes.InvalidArgument, "configuration name must always be provided when looking up statemachine") + } + fsmId := in.GetId() + if fsmId == "" { + return nil, status.Error(codes.InvalidArgument, "ID must always be provided when looking up statemachine") + } + s.Logger.Debug("looking up FSM [%s] (Configuration: %s)", fsmId, cfg) + fsm, ok := s.Store.GetStateMachine(fsmId, cfg) if !ok { - return nil, status.Error(codes.NotFound, storage.NotFoundError(request.GetId()).Error()) + return nil, status.Error(codes.NotFound, storage.NotFoundError(fsmId).Error()) } return fsm, nil } -func (s *grpcSubscriber) GetEventOutcome(ctx context.Context, request *protos.GetRequest) ( - *protos.EventResponse, error) { - - s.Logger.Debug("looking up EventOutcome %s", request.GetId()) - dest := strings.Split(request.GetId(), storage.KeyPrefixIDSeparator) - if len(dest) != 2 { - return nil, status.Error(codes.InvalidArgument, - fmt.Sprintf("invalid destination [%s] expected: #", request.GetId())) +func (s *grpcSubscriber) GetAllInState(ctx context.Context, in *protos.GetFsmRequest) ( + *protos.ListResponse, error) { + cfgName := in.GetConfig() + if cfgName == "" { + return nil, status.Errorf(codes.InvalidArgument, "configuration must always be specified") + } + state := in.GetState() + if state == "" { + // TODO: implement table scanning + return nil, status.Errorf(codes.Unimplemented, "missing state, table scan not implemented") } - smType, evtId := dest[0], dest[1] - outcome, ok := s.Store.GetOutcomeForEvent(evtId, smType) + ids := s.Store.GetAllInState(cfgName, state) + return &protos.ListResponse{Ids: ids}, nil +} + +func (s *grpcSubscriber) GetEventOutcome(ctx context.Context, in *protos.EventRequest) ( + *protos.EventResponse, error) { + evtId := in.GetId() + config := in.GetConfig() + s.Logger.Debug("looking up EventOutcome %s (%s)", evtId, config) + outcome, ok := s.Store.GetOutcomeForEvent(evtId, config) if !ok { return nil, status.Error(codes.NotFound, fmt.Sprintf("outcome for event %s not found", evtId)) } @@ -163,6 +196,48 @@ func (s *grpcSubscriber) GetEventOutcome(ctx context.Context, request *protos.Ge }, nil } +func (s *grpcSubscriber) StreamAllInstate(in *protos.GetFsmRequest, stream StatemachineStream) error { + response, err := s.GetAllInState(context.Background(), in) + if err != nil { + return err + } + cfgName := in.GetConfig() + for _, id := range response.GetIds() { + fsm, found := s.Store.GetStateMachine(id, cfgName) + if !found { + return storage.NotFoundError(id) + } + if err = stream.SendMsg(&protos.PutResponse{ + Id: id, + EntityResponse: &protos.PutResponse_Fsm{Fsm: fsm}, + }); err != nil { + s.Logger.Error("could not stream response back: %s", err) + return err + } + } + return nil +} + +func (s *grpcSubscriber) StreamAllConfigurations(in *wrapperspb.StringValue, stream ConfigurationStream) error { + if in.GetValue() == "" { + return status.Errorf(codes.InvalidArgument, "must specify the Configuration name") + } + response, err := s.GetAllConfigurations(context.Background(), in) + if err != nil { + return nil + } + for _, cfgId := range response.GetIds() { + cfg, found := s.Store.GetConfig(cfgId) + if !found { + return storage.NotFoundError(cfgId) + } + if err = stream.SendMsg(cfg); err != nil { + return err + } + } + return nil +} + // NewGrpcServer creates a new gRPC server to handle incoming events and other API calls. // The `Config` can be used to configure the backing store, a timeout and the logger. func NewGrpcServer(config *Config) (*grpc.Server, error) { @@ -170,7 +245,7 @@ func NewGrpcServer(config *Config) (*grpc.Server, error) { if config.Timeout == 0 { config.Timeout = DefaultTimeout } - gsrv := grpc.NewServer() - protos.RegisterStatemachineServiceServer(gsrv, &grpcSubscriber{Config: config}) - return gsrv, nil + server := grpc.NewServer() + protos.RegisterStatemachineServiceServer(server, &grpcSubscriber{Config: config}) + return server, nil } diff --git a/grpc/grpc_server_stream_test.go b/grpc/grpc_server_stream_test.go new file mode 100644 index 0000000..781853f --- /dev/null +++ b/grpc/grpc_server_stream_test.go @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2022 AlertAvert.com. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Author: Marco Massenzio (marco@alertavert.com) + */ + +package grpc_test + +import ( + "context" + "github.com/go-redis/redis/v8" + "github.com/massenz/slf4go/logging" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + g "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/types/known/wrapperspb" + "io" + "net" + + . "github.com/massenz/go-statemachine/api" + "github.com/massenz/go-statemachine/grpc" + "github.com/massenz/go-statemachine/storage" + "github.com/massenz/statemachine-proto/golang/api" +) + +var _ = Describe("gRPC Server Streams", func() { + When("using Redis as the backing store", func() { + var ( + listener net.Listener + client api.StatemachineServiceClient + cfg *api.Configuration + done func() + store storage.StoreManager + ) + // Server setup + BeforeEach(func() { + store = storage.NewRedisStoreWithDefaults(container.Address) + store.SetLogLevel(logging.NONE) + listener, _ = net.Listen("tcp", ":0") + cc, _ := g.Dial(listener.Addr().String(), + g.WithTransportCredentials(insecure.NewCredentials())) + client = api.NewStatemachineServiceClient(cc) + // Use this to log errors when diagnosing test failures; then set to NONE once done. + l := logging.NewLog("grpc-server-test") + l.Level = logging.NONE + server, _ := grpc.NewGrpcServer(&grpc.Config{ + Store: store, + Logger: l, + }) + go func() { + Ω(server.Serve(listener)).Should(Succeed()) + }() + done = func() { + server.Stop() + } + }) + // Server shutdown & Clean up the DB + AfterEach(func() { + done() + rdb := redis.NewClient(&redis.Options{ + Addr: container.Address, + DB: storage.DefaultRedisDb, + }) + rdb.FlushDB(context.Background()) + }) + Context("streaming Configurations", func() { + var versions []string + var name = "test-conf" + // Test data setup + BeforeEach(func() { + versions = []string{"v1", "v2", "v3"} + cfg = &api.Configuration{ + Name: name, + States: []string{"start", "stop"}, + Transitions: []*api.Transition{ + {From: "start", To: "stop", Event: "shutdown"}, + }, + StartingState: "start", + } + for _, v := range versions { + cfg.Version = v + Ω(store.PutConfig(cfg)).ToNot(HaveOccurred()) + } + }) + It("should find all configurations", func() { + stream, err := client.StreamAllConfigurations(bkgnd, + &wrapperspb.StringValue{Value: name}) + Ω(err).ShouldNot(HaveOccurred()) + count := 0 + for { + item, err := stream.Recv() + if err == io.EOF { + Ω(count).Should(Equal(len(versions))) + break + } + count++ + Ω(err).ShouldNot(HaveOccurred()) + Ω(item).ShouldNot(BeNil()) + Ω(item.Name).Should(Equal(name)) + Ω(versions).Should(ContainElement(item.Version)) + } + }) + It("should fail for empty name", func() { + data, err := client.StreamAllConfigurations(bkgnd, &wrapperspb.StringValue{Value: ""}) + Ω(err).ShouldNot(HaveOccurred()) + _, err = data.Recv() + Ω(err).Should(HaveOccurred()) + AssertStatusCode(codes.InvalidArgument, err) + }) + It("should retrieve an empty stream for valid but non-existent configuration", func() { + response, err := client.StreamAllConfigurations(bkgnd, + &wrapperspb.StringValue{Value: "fake"}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(response).ShouldNot(BeNil()) + _, err = response.Recv() + Ω(err).Should(Equal(io.EOF)) + }) + }) + Context("streaming Statemachines", func() { + var ids = []string{"1", "2", "3"} + // Test data setup + BeforeEach(func() { + cfg = &api.Configuration{ + Name: "test-conf", + Version: "v1", + States: []string{"start", "stop"}, + Transitions: []*api.Transition{ + {From: "start", To: "stop", Event: "shutdown"}, + }, + StartingState: "start", + } + Ω(store.PutConfig(cfg)).ShouldNot(HaveOccurred()) + for _, id := range ids { + Ω(store.PutStateMachine(id, &api.FiniteStateMachine{ + ConfigId: GetVersionId(cfg), + State: "start", + })).ShouldNot(HaveOccurred()) + Ω(store.UpdateState(cfg.Name, id, "", "start")). + ShouldNot(HaveOccurred()) + } + }) + It("should find all FSM", func() { + resp, err := client.StreamAllInstate(bkgnd, + &api.GetFsmRequest{ + Config: cfg.Name, + Query: &api.GetFsmRequest_State{State: "start"}, + }) + Ω(err).ShouldNot(HaveOccurred()) + Ω(resp).ShouldNot(BeNil()) + count := 0 + for { + item, err := resp.Recv() + if err == io.EOF { + Ω(count).Should(Equal(len(ids))) + break + } + count++ + Ω(err).ShouldNot(HaveOccurred()) + Ω(ids).Should(ContainElement(item.Id)) + fsm := item.GetFsm() + Ω(fsm).ShouldNot(BeNil()) + Ω(fsm.State).Should(Equal("start")) + Ω(fsm.ConfigId).Should(Equal(GetVersionId(cfg))) + } + }) + }) + }) +}) diff --git a/grpc/grpc_server_test.go b/grpc/grpc_server_test.go index 8095233..8d82a5e 100644 --- a/grpc/grpc_server_test.go +++ b/grpc/grpc_server_test.go @@ -10,46 +10,54 @@ package grpc_test import ( - . "github.com/JiaYongfei/respect/gomega" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "google.golang.org/grpc/codes" - "strings" - "context" "fmt" - "github.com/massenz/slf4go/logging" - g "google.golang.org/grpc" - "google.golang.org/grpc/status" "net" + "strings" "time" + "github.com/go-redis/redis/v8" + "github.com/massenz/slf4go/logging" + g "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/types/known/wrapperspb" + + . "github.com/JiaYongfei/respect/gomega" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/massenz/go-statemachine/api" "github.com/massenz/go-statemachine/grpc" "github.com/massenz/go-statemachine/storage" - "github.com/massenz/statemachine-proto/golang/api" + protos "github.com/massenz/statemachine-proto/golang/api" ) -var _ = Describe("GrpcServer", func() { - - Context("when processing events", func() { - var testCh chan api.EventRequest +var bkgnd = context.Background() +var _ = Describe("the gRPC Server", func() { + When("processing events", func() { + var testCh chan protos.EventRequest var listener net.Listener - var client api.StatemachineServiceClient + var client protos.StatemachineServiceClient var done func() BeforeEach(func() { var err error - testCh = make(chan api.EventRequest, 5) + testCh = make(chan protos.EventRequest, 5) listener, err = net.Listen("tcp", ":0") Ω(err).ShouldNot(HaveOccurred()) - cc, err := g.Dial(listener.Addr().String(), g.WithInsecure()) + cc, err := g.Dial(listener.Addr().String(), + g.WithTransportCredentials(insecure.NewCredentials())) Ω(err).ShouldNot(HaveOccurred()) - client = api.NewStatemachineServiceClient(cc) + client = protos.NewStatemachineServiceClient(cc) + // TODO: use GinkgoWriter for logs l := logging.NewLog("grpc-server-test") l.Level = logging.NONE + // Note the `Config` here has no store configured, because we are + // only testing events in this Context, and those are never stored + // in Redis by the gRPC server (other parts of the system do). server, err := grpc.NewGrpcServer(&grpc.Config{ EventsChannel: testCh, Logger: l, @@ -65,15 +73,16 @@ var _ = Describe("GrpcServer", func() { } }) It("should succeed for well-formed events", func() { - response, err := client.ProcessEvent(context.Background(), &api.EventRequest{ - Event: &api.Event{ + response, err := client.SendEvent(bkgnd, &protos.EventRequest{ + Event: &protos.Event{ EventId: "1", - Transition: &api.Transition{ + Transition: &protos.Transition{ Event: "test-vt", }, Originator: "test", }, - Dest: "2", + Config: "test-cfg", + Id: "2", }) Ω(err).ToNot(HaveOccurred()) Ω(response).ToNot(BeNil()) @@ -84,21 +93,22 @@ var _ = Describe("GrpcServer", func() { Ω(evt.Event.EventId).To(Equal("1")) Ω(evt.Event.Transition.Event).To(Equal("test-vt")) Ω(evt.Event.Originator).To(Equal("test")) - Ω(evt.Dest).To(Equal("2")) + Ω(evt.Id).To(Equal("2")) case <-time.After(10 * time.Millisecond): Fail("Timed out") } }) It("should create an ID for events without", func() { - response, err := client.ProcessEvent(context.Background(), &api.EventRequest{ - Event: &api.Event{ - Transition: &api.Transition{ + response, err := client.SendEvent(bkgnd, &protos.EventRequest{ + Event: &protos.Event{ + Transition: &protos.Transition{ Event: "test-vt", }, Originator: "test", }, - Dest: "123456", + Config: "test-cfg", + Id: "123456", }) Ω(err).ToNot(HaveOccurred()) Ω(response.EventId).ToNot(BeNil()) @@ -113,15 +123,15 @@ var _ = Describe("GrpcServer", func() { } }) It("should fail for missing destination", func() { - _, err := client.ProcessEvent(context.Background(), &api.EventRequest{ - Event: &api.Event{ - Transition: &api.Transition{ + _, err := client.SendEvent(bkgnd, &protos.EventRequest{ + Event: &protos.Event{ + Transition: &protos.Transition{ Event: "test-vt", }, Originator: "test", }, }) - assertStatusCode(codes.FailedPrecondition, err) + AssertStatusCode(codes.FailedPrecondition, err) done() select { case evt := <-testCh: @@ -131,16 +141,17 @@ var _ = Describe("GrpcServer", func() { } }) It("should fail for missing event", func() { - _, err := client.ProcessEvent(context.Background(), &api.EventRequest{ - Event: &api.Event{ - Transition: &api.Transition{ + _, err := client.SendEvent(bkgnd, &protos.EventRequest{ + Event: &protos.Event{ + Transition: &protos.Transition{ Event: "", }, Originator: "test", }, - Dest: "9876", + Config: "test", + Id: "9876", }) - assertStatusCode(codes.FailedPrecondition, err) + AssertStatusCode(codes.FailedPrecondition, err) done() select { case evt := <-testCh: @@ -151,24 +162,24 @@ var _ = Describe("GrpcServer", func() { }) }) - Context("when retrieving data from the store", func() { + When("using Redis as the backing store", func() { var ( listener net.Listener - client api.StatemachineServiceClient - cfg *api.Configuration - fsm *api.FiniteStateMachine + client protos.StatemachineServiceClient + cfg *protos.Configuration + fsm *protos.FiniteStateMachine done func() store storage.StoreManager ) // Server setup BeforeEach(func() { - store = storage.NewInMemoryStore() + store = storage.NewRedisStoreWithDefaults(container.Address) store.SetLogLevel(logging.NONE) - listener, _ = net.Listen("tcp", ":0") - cc, _ := g.Dial(listener.Addr().String(), g.WithInsecure()) - client = api.NewStatemachineServiceClient(cc) + cc, _ := g.Dial(listener.Addr().String(), + g.WithTransportCredentials(insecure.NewCredentials())) + client = protos.NewStatemachineServiceClient(cc) // Use this to log errors when diagnosing test failures; then set to NONE once done. l := logging.NewLog("grpc-server-test") l.Level = logging.NONE @@ -178,100 +189,213 @@ var _ = Describe("GrpcServer", func() { }) go func() { + defer GinkgoRecover() Ω(server.Serve(listener)).Should(Succeed()) }() done = func() { server.Stop() } }) - // Server shutdown + // Server shutdown & Clean up the DB AfterEach(func() { done() + rdb := redis.NewClient(&redis.Options{ + Addr: container.Address, + DB: storage.DefaultRedisDb, + }) + rdb.FlushDB(context.Background()) }) - // Test data setup - BeforeEach(func() { - cfg = &api.Configuration{ - Name: "test-conf", - Version: "v1", - States: []string{"start", "stop"}, - Transitions: []*api.Transition{ - {From: "start", To: "stop", Event: "shutdown"}, - }, - StartingState: "start", - } - fsm = &api.FiniteStateMachine{ConfigId: GetVersionId(cfg)} - }) - It("should store valid configurations", func() { - _, ok := store.GetConfig(GetVersionId(cfg)) - Ω(ok).To(BeFalse()) - response, err := client.PutConfiguration(context.Background(), cfg) - Ω(err).ToNot(HaveOccurred()) - Ω(response).ToNot(BeNil()) - Ω(response.Id).To(Equal(GetVersionId(cfg))) - found, ok := store.GetConfig(response.Id) - Ω(ok).Should(BeTrue()) - Ω(found).Should(Respect(cfg)) - }) - It("should fail for invalid configuration", func() { - invalid := &api.Configuration{ - Name: "invalid", - Version: "v1", - States: []string{}, - Transitions: nil, - StartingState: "", - } - _, err := client.PutConfiguration(context.Background(), invalid) - assertStatusCode(codes.InvalidArgument, err) - }) - It("should retrieve a valid configuration", func() { - Ω(store.PutConfig(cfg)).To(Succeed()) - response, err := client.GetConfiguration(context.Background(), - &api.GetRequest{Id: GetVersionId(cfg)}) - Ω(err).ToNot(HaveOccurred()) - Ω(response).ToNot(BeNil()) - Ω(response).Should(Respect(cfg)) - }) - It("should return an empty configuration for an invalid ID", func() { - _, err := client.GetConfiguration(context.Background(), &api.GetRequest{Id: "fake"}) - assertStatusCode(codes.NotFound, err) - }) - It("should store a valid FSM", func() { - Ω(store.PutConfig(cfg)).To(Succeed()) - resp, err := client.PutFiniteStateMachine(context.Background(), fsm) - Ω(err).ToNot(HaveOccurred()) - Ω(resp).ToNot(BeNil()) - Ω(resp.Id).ToNot(BeNil()) - Ω(resp.Fsm).Should(Respect(fsm)) - }) - It("should fail with an invalid Config ID", func() { - invalid := &api.FiniteStateMachine{ConfigId: "fake"} - _, err := client.PutFiniteStateMachine(context.Background(), invalid) - assertStatusCode(codes.FailedPrecondition, err) - }) - It("can retrieve a stored FSM", func() { - id := "123456" - Ω(store.PutConfig(cfg)) - Ω(store.PutStateMachine(id, fsm)).Should(Succeed()) - Ω(client.GetFiniteStateMachine(context.Background(), - &api.GetRequest{ - Id: strings.Join([]string{cfg.Name, id}, storage.KeyPrefixIDSeparator), - })).Should(Respect(fsm)) - }) - It("will return an Invalid error for an invalid ID", func() { - _, err := client.GetFiniteStateMachine(context.Background(), &api.GetRequest{Id: "fake"}) - assertStatusCode(codes.InvalidArgument, err) + Context("handling Configuration API requests", func() { + // Test data setup + BeforeEach(func() { + cfg = &protos.Configuration{ + Name: "test-conf", + Version: "v1", + States: []string{"start", "stop"}, + Transitions: []*protos.Transition{ + {From: "start", To: "stop", Event: "shutdown"}, + }, + StartingState: "start", + } + }) + It("should store valid configurations", func() { + _, ok := store.GetConfig(GetVersionId(cfg)) + Ω(ok).To(BeFalse()) + response, err := client.PutConfiguration(bkgnd, cfg) + Ω(err).ToNot(HaveOccurred()) + Ω(response).ToNot(BeNil()) + Ω(response.Id).To(Equal(GetVersionId(cfg))) + found, ok := store.GetConfig(response.Id) + Ω(ok).Should(BeTrue()) + Ω(found).Should(Respect(cfg)) + }) + It("should fail for invalid configuration", func() { + invalid := &protos.Configuration{ + Name: "invalid", + Version: "v1", + States: []string{}, + Transitions: nil, + StartingState: "", + } + _, err := client.PutConfiguration(bkgnd, invalid) + AssertStatusCode(codes.InvalidArgument, err) + }) + It("should retrieve a valid configuration", func() { + Ω(store.PutConfig(cfg)).To(Succeed()) + response, err := client.GetConfiguration(bkgnd, + &wrapperspb.StringValue{Value: GetVersionId(cfg)}) + Ω(err).ToNot(HaveOccurred()) + Ω(response).ToNot(BeNil()) + Ω(response).Should(Respect(cfg)) + }) + It("should return an empty configuration for an invalid ID", func() { + _, err := client.GetConfiguration(bkgnd, &wrapperspb.StringValue{Value: "fake"}) + AssertStatusCode(codes.NotFound, err) + }) + It("will find all configurations", func() { + names := []string{"orders", "devices", "users"} + for _, name := range names { + cfg = &protos.Configuration{ + Name: name, + Version: "v1", + States: []string{"start", "stop"}, + Transitions: []*protos.Transition{ + {From: "start", To: "stop", Event: "shutdown"}, + }, + StartingState: "start", + } + Ω(store.PutConfig(cfg)).Should(Succeed()) + } + found, err := client.GetAllConfigurations(bkgnd, &wrapperspb.StringValue{}) + Ω(err).Should(Succeed()) + Ω(len(found.Ids)).To(Equal(3)) + for _, value := range found.Ids { + Ω(names).To(ContainElement(value)) + } + }) + It("will find all version for a configuration", func() { + name := "store.api" + versions := []string{"v1alpha", "v1beta", "v1"} + for _, v := range versions { + cfg = &protos.Configuration{ + Name: name, + Version: v, + States: []string{"checkout", "close"}, + Transitions: []*protos.Transition{ + {From: "checkout", To: "close", Event: "payment"}, + }, + StartingState: "checkout", + } + Ω(store.PutConfig(cfg)).Should(Succeed()) + } + found, err := client.GetAllConfigurations(bkgnd, &wrapperspb.StringValue{Value: name}) + Ω(err).Should(Succeed()) + Ω(len(found.Ids)).To(Equal(3)) + for _, value := range versions { + Ω(found.Ids).To(ContainElement( + strings.Join([]string{name, value}, storage.KeyPrefixComponentsSeparator))) + } + }) }) - It("will return a NotFound error for a missing ID", func() { - _, err := client.GetFiniteStateMachine(context.Background(), - &api.GetRequest{Id: "cfg#fake"}) - assertStatusCode(codes.NotFound, err) + Context("handling Statemachine API requests", func() { + // Test data setup + BeforeEach(func() { + cfg = &protos.Configuration{ + Name: "test-conf", + Version: "v1", + States: []string{"start", "stop"}, + Transitions: []*protos.Transition{ + {From: "start", To: "stop", Event: "shutdown"}, + }, + StartingState: "start", + } + fsm = &protos.FiniteStateMachine{ConfigId: GetVersionId(cfg)} + }) + It("should store a valid FSM", func() { + Ω(store.PutConfig(cfg)).To(Succeed()) + resp, err := client.PutFiniteStateMachine(bkgnd, + &protos.PutFsmRequest{Id: "123456", Fsm: fsm}) + Ω(err).ToNot(HaveOccurred()) + Ω(resp).ToNot(BeNil()) + Ω(resp.Id).To(Equal("123456")) + Ω(resp.GetFsm()).Should(Respect(fsm)) + // As we didn't specify a state when creating the FSM, the `StartingState` + // was automatically configured. + found := store.GetAllInState(cfg.Name, cfg.StartingState) + Ω(len(found)).To(Equal(1)) + Ω(found[0]).To(Equal(resp.Id)) + }) + It("should fail with an invalid Config ID", func() { + invalid := &protos.FiniteStateMachine{ConfigId: "fake"} + _, err := client.PutFiniteStateMachine(bkgnd, + &protos.PutFsmRequest{Fsm: invalid}) + AssertStatusCode(codes.FailedPrecondition, err) + }) + It("can retrieve a stored FSM", func() { + id := "123456" + Ω(store.PutConfig(cfg)) + Ω(store.PutStateMachine(id, fsm)).Should(Succeed()) + Ω(client.GetFiniteStateMachine(bkgnd, + &protos.GetFsmRequest{ + Config: cfg.Name, + Query: &protos.GetFsmRequest_Id{Id: id}, + })).Should(Respect(fsm)) + }) + It("will return an Invalid error for missing config or ID", func() { + _, err := client.GetFiniteStateMachine(bkgnd, + &protos.GetFsmRequest{ + Query: &protos.GetFsmRequest_Id{Id: "fake"}, + }) + AssertStatusCode(codes.InvalidArgument, err) + _, err = client.GetFiniteStateMachine(bkgnd, + &protos.GetFsmRequest{ + Config: cfg.Name, + }) + AssertStatusCode(codes.InvalidArgument, err) + }) + It("will return a NotFound error for a missing ID", func() { + _, err := client.GetFiniteStateMachine(bkgnd, + &protos.GetFsmRequest{ + Config: cfg.Name, + Query: &protos.GetFsmRequest_Id{Id: "12345"}, + }) + AssertStatusCode(codes.NotFound, err) + }) + It("will find all FSMs by State", func() { + for i := 1; i <= 5; i++ { + id := fmt.Sprintf("fsm-%d", i) + Ω(store.PutStateMachine(id, + &protos.FiniteStateMachine{ + ConfigId: "test.m:v1", + State: "start", + })).Should(Succeed()) + store.UpdateState("test.m", id, "", "start") + } + for i := 10; i < 13; i++ { + id := fmt.Sprintf("fsm-%d", i) + Ω(store.PutStateMachine(id, + &protos.FiniteStateMachine{ + ConfigId: "test.m:v1", + State: "stop", + })).Should(Succeed()) + store.UpdateState("test.m", id, "", "stop") + + } + items, err := client.GetAllInState(bkgnd, &protos.GetFsmRequest{ + Config: "test.m", + Query: &protos.GetFsmRequest_State{State: "start"}, + }) + Ω(err).ShouldNot(HaveOccurred()) + Ω(len(items.GetIds())).Should(Equal(5)) + Ω(items.GetIds()).Should(ContainElements("fsm-3", "fsm-5")) + items, err = client.GetAllInState(bkgnd, &protos.GetFsmRequest{ + Config: "test.m", + Query: &protos.GetFsmRequest_State{State: "stop"}, + }) + Ω(err).ShouldNot(HaveOccurred()) + Ω(len(items.GetIds())).Should(Equal(3)) + Ω(items.GetIds()).Should(ContainElements("fsm-10", "fsm-12")) + }) }) }) }) - -func assertStatusCode(code codes.Code, err error) { - Ω(err).To(HaveOccurred()) - s, ok := status.FromError(err) - Ω(ok).To(BeTrue()) - Ω(s.Code()).To(Equal(code)) -} diff --git a/grpc/grpc_suite_test.go b/grpc/grpc_suite_test.go index b1531ca..c40b29a 100644 --- a/grpc/grpc_suite_test.go +++ b/grpc/grpc_suite_test.go @@ -10,13 +10,40 @@ package grpc_test import ( + "context" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + internals "github.com/massenz/go-statemachine/internals/testing" ) func TestGrpc(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "gRPC Suite") + RunSpecs(t, "gRPC Server") +} + +var container *internals.Container +var _ = BeforeSuite(func() { + var err error + container, err = internals.NewRedisContainer(context.Background()) + Expect(err).ToNot(HaveOccurred()) + // Note the timeout here is in seconds (and it's not a time.Duration either) +}, 5.0) + +var _ = AfterSuite(func() { + if container != nil { + Expect(container.Terminate(context.Background())).To(Succeed()) + } +}, 2.0) + +// TODO: should be an Omega Matcher +func AssertStatusCode(code codes.Code, err error) { + Ω(err).To(HaveOccurred()) + s, ok := status.FromError(err) + Ω(ok).To(BeTrue()) + Ω(s.Code()).To(Equal(code)) } diff --git a/internals/testing/containers.go b/internals/testing/containers.go new file mode 100644 index 0000000..0d23cf5 --- /dev/null +++ b/internals/testing/containers.go @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2022 AlertAvert.com. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Author: Marco Massenzio (marco@alertavert.com) + */ + +package testing + +import ( + "context" + "fmt" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + localstackImage = "localstack/localstack:1.3" + localstackEdgePort = "4566" + redisImage = "redis:6" + redisPort = "6379/tcp" + Region = "us-west-2" +) + +// Container is an internal wrapper around the `testcontainers.Container` carrying also +// the `Address` (which could be a URI) to which the server can be reached at. +type Container struct { + testcontainers.Container + Address string +} + +// NewLocalstackContainer creates a new connection to the `LocalStack` `testcontainers` +func NewLocalstackContainer(ctx context.Context) (*Container, error) { + req := testcontainers.ContainerRequest{ + Image: localstackImage, + ExposedPorts: []string{localstackEdgePort}, + WaitingFor: wait.ForLog("Ready."), + Env: map[string]string{ + "AWS_REGION": Region, + "EDGE_PORT": localstackEdgePort, + "SERVICES": "sqs", + }, + } + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return nil, err + } + + ip, err := container.Host(ctx) + if err != nil { + return nil, err + } + + mappedPort, err := container.MappedPort(ctx, localstackEdgePort) + if err != nil { + return nil, err + } + + uri := fmt.Sprintf("http://%s:%s", ip, mappedPort.Port()) + return &Container{Container: container, Address: uri}, nil +} + +func NewRedisContainer(ctx context.Context) (*Container, error) { + req := testcontainers.ContainerRequest{ + Image: redisImage, + ExposedPorts: []string{redisPort}, + WaitingFor: wait.ForLog("* Ready to accept connections"), + } + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return nil, err + } + + mappedPort, err := container.MappedPort(ctx, "6379") + if err != nil { + return nil, err + } + + hostIP, err := container.Host(ctx) + if err != nil { + return nil, err + } + + address := fmt.Sprintf("%s:%s", hostIP, mappedPort.Port()) + return &Container{Container: container, Address: address}, nil +} diff --git a/pubsub/listener.go b/pubsub/listener.go index 288327a..94bf1d3 100644 --- a/pubsub/listener.go +++ b/pubsub/listener.go @@ -15,7 +15,6 @@ import ( "github.com/massenz/go-statemachine/storage" log "github.com/massenz/slf4go/logging" protos "github.com/massenz/statemachine-proto/golang/api" - "strings" ) func NewEventsListener(options *ListenerOptions) *EventsListener { @@ -34,10 +33,11 @@ func (listener *EventsListener) SetLogLevel(level log.LogLevel) { func (listener *EventsListener) PostNotificationAndReportOutcome(eventResponse *protos.EventResponse) { if eventResponse.Outcome.Code != protos.EventOutcome_Ok { - listener.logger.Error("[Event ID: %s]: %s", eventResponse.EventId, eventResponse.GetOutcome().Details) + listener.logger.Error("event [%s]: %s", + eventResponse.GetEventId(), eventResponse.GetOutcome().Details) } if listener.notifications != nil { - listener.logger.Debug("Posting notification: %v", eventResponse.GetEventId()) + listener.logger.Debug("posting notification: %v", eventResponse.GetEventId()) listener.notifications <- *eventResponse } listener.logger.Debug("Reporting outcome: %v", eventResponse.GetEventId()) @@ -48,29 +48,25 @@ func (listener *EventsListener) ListenForMessages() { listener.logger.Info("Events message listener started") for request := range listener.events { listener.logger.Debug("Received request %s", request.Event.String()) - if request.Dest == "" { + fsmId := request.GetId() + if fsmId == "" { listener.PostNotificationAndReportOutcome(makeResponse(&request, protos.EventOutcome_MissingDestination, - fmt.Sprintf("no destination specified"))) + fmt.Sprintf("no statemachine ID specified"))) continue } - // TODO: this is an API change and needs to be documented - // Destination comprises both the type (configuration name) and ID of the statemachine, - // separated by the # character: # (e.g., `order#1234`) - dest := strings.Split(request.Dest, storage.KeyPrefixIDSeparator) - if len(dest) != 2 { + config := request.GetConfig() + if config == "" { listener.PostNotificationAndReportOutcome(makeResponse(&request, protos.EventOutcome_MissingDestination, - fmt.Sprintf("invalid destination [%s] expected #", - request.Dest))) + fmt.Sprintf("no Configuration name specified"))) continue } - smType, smId := dest[0], dest[1] - fsm, ok := listener.store.GetStateMachine(smId, smType) + fsm, ok := listener.store.GetStateMachine(fsmId, config) if !ok { listener.PostNotificationAndReportOutcome(makeResponse(&request, protos.EventOutcome_FsmNotFound, - fmt.Sprintf("statemachine [%s] could not be found", request.Dest))) + fmt.Sprintf("statemachine [%s] could not be found", fsmId))) continue } // TODO: cache the configuration locally: they are immutable anyway. @@ -86,8 +82,8 @@ func (listener *EventsListener) ListenForMessages() { Config: cfg, FSM: fsm, } - listener.logger.Debug("Preparing to send event `%s` for FSM [%s] (current state: %s)", - request.Event.Transition.Event, smId, previousState) + listener.logger.Debug("preparing to send event `%s` for FSM [%s] (current state: %s)", + request.Event.Transition.Event, fsmId, previousState) if err := cfgFsm.SendEvent(request.Event); err != nil { listener.PostNotificationAndReportOutcome(makeResponse(&request, protos.EventOutcome_TransitionNotAllowed, @@ -95,29 +91,34 @@ func (listener *EventsListener) ListenForMessages() { request.GetEvent().GetTransition().GetEvent(), err))) continue } - listener.logger.Debug("Event `%s` transitioned FSM [%s] to state `%s` from state `%s` - updating store", - request.Event.Transition.Event, smId, fsm.State, previousState) - err := listener.store.PutStateMachine(smId, fsm) - if err != nil { + if err := listener.store.PutStateMachine(fsmId, fsm); err != nil { + listener.PostNotificationAndReportOutcome(makeResponse(&request, + protos.EventOutcome_InternalError, + fmt.Sprintf("could not update statemachine [%s#%s] in store: %v", + config, fsmId, err))) + continue + } + if err := listener.store.UpdateState(config, fsmId, previousState, fsm.State); err != nil { listener.PostNotificationAndReportOutcome(makeResponse(&request, protos.EventOutcome_InternalError, - fmt.Sprintf("could not update statemachine [%s] in store: %v", - request.Dest, err))) + fmt.Sprintf("could not update statemachine state set (%s#%s): %v", + config, fsmId, err))) continue } // All good, we want to report success too. + listener.logger.Debug("Event `%s` transitioned FSM [%s] to state `%s` from state `%s` - updating store", + request.Event.Transition.Event, fsmId, fsm.State, previousState) listener.PostNotificationAndReportOutcome(makeResponse(&request, protos.EventOutcome_Ok, fmt.Sprintf("event [%s] transitioned FSM [%s] to state [%s]", - request.Event.Transition.Event, smId, fsm.State))) + request.Event.Transition.Event, fsmId, fsm.State))) } } func (listener *EventsListener) reportOutcome(response *protos.EventResponse) { - smType := strings.Split(response.Outcome.Dest, storage.KeyPrefixIDSeparator)[0] - if err := listener.store.AddEventOutcome(response.EventId, smType, response.Outcome, - storage.NeverExpire); err != nil { - listener.logger.Error("could not add outcome to store: %v", err) + if err := listener.store.AddEventOutcome(response.EventId, response.GetOutcome().GetConfig(), + response.Outcome, storage.NeverExpire); err != nil { + listener.logger.Error("could not save event outcome: %v", err) } } @@ -127,8 +128,9 @@ func makeResponse(request *protos.EventRequest, code protos.EventOutcome_StatusC EventId: request.GetEvent().GetEventId(), Outcome: &protos.EventOutcome{ Code: code, - Dest: request.Dest, Details: details, + Config: request.Config, + Id: request.Id, }, } } diff --git a/pubsub/listener_test.go b/pubsub/listener_test.go index ac06da9..e7162e9 100644 --- a/pubsub/listener_test.go +++ b/pubsub/listener_test.go @@ -66,7 +66,7 @@ var _ = Describe("A Listener", func() { case n := <-notificationsCh: Ω(n.EventId).To(Equal(msg.GetEventId())) Ω(n.Outcome).ToNot(BeNil()) - Ω(n.Outcome.Dest).To(BeEmpty()) + Ω(n.Outcome.Id).To(BeEmpty()) Ω(n.Outcome.Details).To(Equal(detail)) Ω(n.Outcome.Code).To(Equal(protos.EventOutcome_MissingDestination)) @@ -84,8 +84,9 @@ var _ = Describe("A Listener", func() { Details: "more details", } request := protos.EventRequest{ - Event: &event, - Dest: "test#12345-faa44", + Event: &event, + Config: "test", + Id: "12345-faa44", } Ω(store.PutConfig(&protos.Configuration{ Name: "test", @@ -111,7 +112,7 @@ var _ = Describe("A Listener", func() { // First we want to test that the outcome was successful Ω(notification.EventId).To(Equal(event.GetEventId())) Ω(notification.Outcome).ToNot(BeNil()) - Ω(notification.Outcome.Dest).To(Equal(request.GetDest())) + Ω(notification.Outcome.Id).To(Equal(request.GetId())) Ω(notification.Outcome.Details).To(ContainSubstring("transitioned")) Ω(notification.Outcome.Code).To(Equal(protos.EventOutcome_Ok)) @@ -136,8 +137,9 @@ var _ = Describe("A Listener", func() { Details: "more details", } request := protos.EventRequest{ - Event: &event, - Dest: "test#fake-fsm", + Event: &event, + Config: "test", + Id: "fake-fsm", } go func() { testListener.ListenForMessages() @@ -148,7 +150,7 @@ var _ = Describe("A Listener", func() { case n := <-notificationsCh: Ω(n.EventId).To(Equal(request.Event.EventId)) Ω(n.Outcome).ToNot(BeNil()) - Ω(n.Outcome.Dest).To(Equal(request.Dest)) + Ω(n.Outcome.Id).To(Equal(request.GetId())) Ω(n.Outcome.Code).To(Equal(protos.EventOutcome_FsmNotFound)) case <-time.After(timeout): Fail("the listener did not exit when the events channel was closed") @@ -159,7 +161,6 @@ var _ = Describe("A Listener", func() { Event: &protos.Event{ EventId: "feed-beef", }, - Dest: "", } go func() { testListener.ListenForMessages() }() eventsCh <- request diff --git a/pubsub/pubsub_suite_test.go b/pubsub/pubsub_suite_test.go index 9ab1238..1c15e20 100644 --- a/pubsub/pubsub_suite_test.go +++ b/pubsub/pubsub_suite_test.go @@ -13,10 +13,6 @@ import ( "context" "fmt" "github.com/golang/protobuf/proto" - "github.com/massenz/go-statemachine/pubsub" - "github.com/massenz/statemachine-proto/golang/api" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" "os" "testing" "time" @@ -27,11 +23,13 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/sqs" + + internals "github.com/massenz/go-statemachine/internals/testing" + "github.com/massenz/go-statemachine/pubsub" + "github.com/massenz/statemachine-proto/golang/api" ) const ( - localstackImage = "localstack/localstack:1.3" - localstackEdgePort = "4566" eventsQueue = "test-events" notificationsQueue = "test-notifications" acksQueue = "test-acks" @@ -44,63 +42,27 @@ func TestPubSub(t *testing.T) { RunSpecs(t, "Pub/Sub Suite") } -type LocalstackContainer struct { - testcontainers.Container - EndpointUri string -} -func SetupAwsLocal(ctx context.Context) (*LocalstackContainer, error) { - req := testcontainers.ContainerRequest{ - Image: localstackImage, - ExposedPorts: []string{localstackEdgePort}, - WaitingFor: wait.ForLog("Ready."), - Env: map[string]string{ - "AWS_REGION": "us-west-2", - "EDGE_PORT": "4566", - "SERVICES": "sqs", - }, - } - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) - if err != nil { - return nil, err - } - - ip, err := container.Host(ctx) - if err != nil { - return nil, err - } - - mappedPort, err := container.MappedPort(ctx, localstackEdgePort) - if err != nil { - return nil, err - } - - uri := fmt.Sprintf("http://%s:%s", ip, mappedPort.Port()) - - return &LocalstackContainer{Container: container, EndpointUri: uri}, nil -} // Although these are constants, we cannot take the pointers unless we declare them vars. var ( - region = "us-west-2" - awsLocal *LocalstackContainer + awsLocal *internals.Container testSqsClient *sqs.SQS ) var _ = BeforeSuite(func() { - Expect(os.Setenv("AWS_REGION", region)).ToNot(HaveOccurred()) + Expect(os.Setenv("AWS_REGION", internals.Region)).ToNot(HaveOccurred()) var err error - awsLocal, err = SetupAwsLocal(context.Background()) + awsLocal, err = internals.NewLocalstackContainer(context.Background()) Expect(err).ToNot(HaveOccurred()) + // Can't take the address of a constant. + region := internals.Region testSqsClient = sqs.New(session.Must(session.NewSessionWithOptions(session.Options{ SharedConfigState: session.SharedConfigEnable, Config: aws.Config{ - Endpoint: &awsLocal.EndpointUri, + Endpoint: &awsLocal.Address, Region: ®ion, }, }))) @@ -134,7 +96,7 @@ var _ = AfterSuite(func() { Expect(err).NotTo(HaveOccurred()) } } - awsLocal.Terminate(context.Background()) + Expect(awsLocal.Terminate(context.Background())).ToNot(HaveOccurred()) }, 2.0) // getQueueName provides a way to obtain a process-independent name for the SQS queue, diff --git a/pubsub/sqs_pub_test.go b/pubsub/sqs_pub_test.go index 7ff8135..0ce7b9b 100644 --- a/pubsub/sqs_pub_test.go +++ b/pubsub/sqs_pub_test.go @@ -34,7 +34,7 @@ var _ = Describe("SQS Publisher", func() { ) BeforeEach(func() { notificationsCh = make(chan protos.EventResponse) - testPublisher = pubsub.NewSqsPublisher(notificationsCh, &awsLocal.EndpointUri) + testPublisher = pubsub.NewSqsPublisher(notificationsCh, &awsLocal.Address) Expect(testPublisher).ToNot(BeNil()) // Set to DEBUG when diagnosing test failures testPublisher.SetLogLevel(logging.NONE) @@ -46,8 +46,9 @@ var _ = Describe("SQS Publisher", func() { EventId: "feed-beef", Outcome: &protos.EventOutcome{ Code: protos.EventOutcome_InternalError, - Dest: "me", Details: "error details", + Config: "test-cfg", + Id: "abd-456", }, } done := make(chan interface{}) @@ -157,8 +158,9 @@ var _ = Describe("SQS Publisher", func() { EventId: evt.EventId, Outcome: &protos.EventOutcome{ Code: protos.EventOutcome_InternalError, - Dest: fmt.Sprintf("test-%d", i), Details: "more details about the error", + Config: "test-cfg", + Id: fmt.Sprintf("fsm-%d", i), }, } } @@ -178,7 +180,7 @@ var _ = Describe("SQS Publisher", func() { g.Expect(receivedEvt.EventId).To(Equal(fmt.Sprintf("event-%d", i))) g.Expect(receivedEvt.Outcome.Code).To(Equal(protos.EventOutcome_InternalError)) g.Expect(receivedEvt.Outcome.Details).To(Equal("more details about the error")) - g.Expect(receivedEvt.Outcome.Dest).To(ContainSubstring("test-")) + g.Expect(receivedEvt.Outcome.Id).To(ContainSubstring("fsm-")) }).Should(Succeed()) } }() diff --git a/pubsub/sqs_sub.go b/pubsub/sqs_sub.go index aa3f6df..1a34be0 100644 --- a/pubsub/sqs_sub.go +++ b/pubsub/sqs_sub.go @@ -139,7 +139,7 @@ func (s *SqsSubscriber) ProcessMessage(msg *sqs.Message, queueUrl *string) { return } - destId := request.Dest + destId := request.GetId() if destId == "" { errDetails := fmt.Sprintf("No Destination ID in %v", request.String()) s.logger.Error(errDetails) diff --git a/pubsub/sqs_sub_test.go b/pubsub/sqs_sub_test.go index 72b32a6..670cf2e 100644 --- a/pubsub/sqs_sub_test.go +++ b/pubsub/sqs_sub_test.go @@ -32,7 +32,7 @@ var _ = Describe("SQS Subscriber", func() { BeforeEach(func() { Expect(awsLocal).ToNot(BeNil()) eventsCh = make(chan protos.EventRequest) - testSubscriber = pubsub.NewSqsSubscriber(eventsCh, &awsLocal.EndpointUri) + testSubscriber = pubsub.NewSqsSubscriber(eventsCh, &awsLocal.Address) Expect(testSubscriber).ToNot(BeNil()) // Set to DEBUG when diagnosing failing tests testSubscriber.SetLogLevel(log.NONE) @@ -43,7 +43,7 @@ var _ = Describe("SQS Subscriber", func() { It("receives events", func() { msg := protos.EventRequest{ Event: api.NewEvent("test-event"), - Dest: "some-fsm", + Id: "some-fsm", } msg.Event.EventId = "feed-beef" msg.Event.Originator = "test-subscriber" diff --git a/server/event_handlers_test.go b/server/event_handlers_test.go index f0d6c11..f4b895b 100644 --- a/server/event_handlers_test.go +++ b/server/event_handlers_test.go @@ -119,7 +119,7 @@ var _ = Describe("Event Handlers", func() { id = uuid.NewString() outcome = &protos.EventOutcome{ Code: protos.EventOutcome_Ok, - Dest: "fake-sm", + Id: "fake-sm", Details: "something happened", } Expect(store.AddEventOutcome(id, cfgName, outcome, @@ -136,7 +136,7 @@ var _ = Describe("Event Handlers", func() { Expect(json.NewDecoder(writer.Body).Decode(&result)).ToNot(HaveOccurred()) Expect(result.StatusCode).To(Equal(outcome.Code.String())) Expect(result.Message).To(Equal(outcome.Details)) - Expect(result.Destination).To(Equal(outcome.Dest)) + Expect(result.Destination).To(Equal(outcome.Id)) }) It("with an invalid ID will return Not Found", func() { endpoint := strings.Join([]string{server.ApiPrefix, diff --git a/server/types.go b/server/types.go index 9992add..8c449c5 100644 --- a/server/types.go +++ b/server/types.go @@ -71,6 +71,6 @@ func MakeOutcomeResponse(outcome *protos.EventOutcome) *OutcomeResponse { return &OutcomeResponse{ StatusCode: outcome.Code.String(), Message: outcome.Details, - Destination: outcome.Dest, + Destination: outcome.Id, } } diff --git a/storage/redis_sets_store.go b/storage/redis_sets_store.go index b7fc507..98fde88 100644 --- a/storage/redis_sets_store.go +++ b/storage/redis_sets_store.go @@ -11,28 +11,31 @@ package storage import ( "context" + "fmt" ) func (csm *RedisStore) UpdateState(cfgName string, id string, oldState string, newState string) error { var key string var err error - if newState == "" { - return IllegalStoreError("when updating state, new state cannot be empty") - } if oldState != "" { key = NewKeyForMachinesByState(cfgName, oldState) err = csm.client.SRem(context.Background(), key, id).Err() if err != nil { - csm.logger.Error("Cannot update FSM [%s#%s] from old state `%s`", cfgName, id, oldState) - return err + return fmt.Errorf( + "cannot remove FSM [%s#%s] from state set `%s`: %s", + cfgName, id, oldState, err) } } - key = NewKeyForMachinesByState(cfgName, newState) - err = csm.client.SAdd(context.Background(), key, id).Err() - if err != nil { - csm.logger.Error("Cannot update FSM [%s#%s] to new state `%s`", cfgName, id, newState) + if newState != "" { + key = NewKeyForMachinesByState(cfgName, newState) + err = csm.client.SAdd(context.Background(), key, id).Err() + if err != nil { + return fmt.Errorf( + "cannot add FSM [%s#%s] to state set `%s`: %s", + cfgName, id, newState, err) + } } - return err + return nil } func (csm *RedisStore) GetAllInState(cfg string, state string) []string { diff --git a/storage/redis_store.go b/storage/redis_store.go index 9113968..ccaa75b 100644 --- a/storage/redis_store.go +++ b/storage/redis_store.go @@ -67,7 +67,7 @@ func (csm *RedisStore) GetStateMachine(id string, cfg string) (*protos.FiniteSta var stateMachine protos.FiniteStateMachine err := csm.get(key, &stateMachine) if err != nil { - csm.logger.Error("Error retrieving state machine `%s`: %s", key, err.Error()) + csm.logger.Error("cannot access store for state machine `%s`: %s", key, err.Error()) return nil, false } return &stateMachine, true diff --git a/storage/redis_store_test.go b/storage/redis_store_test.go index c39d892..be13b5d 100644 --- a/storage/redis_store_test.go +++ b/storage/redis_store_test.go @@ -174,7 +174,8 @@ var _ = Describe("RedisStore", func() { cfg := "orders" response := &protos.EventOutcome{ Code: protos.EventOutcome_Ok, - Dest: "1234-feed-beef", + Config: "test", + Id: "1234-feed-beef", Details: "this was just a test", } Expect(store.AddEventOutcome(id, cfg, response, storage.NeverExpire)).ToNot(HaveOccurred()) @@ -191,8 +192,8 @@ var _ = Describe("RedisStore", func() { cfg := "orders" response := &protos.EventOutcome{ Code: protos.EventOutcome_Ok, - Dest: "1234-feed-beef", Details: "this was just a test", + Id: "1234-feed-beef", } key := storage.NewKeyForOutcome(id, cfg) val, _ := proto.Marshal(response) @@ -332,8 +333,10 @@ var _ = Describe("RedisStore", func() { res = store.GetAllInState("orders", "in_transit") Expect(len(res)).To(Equal(6)) }) - It("will fail for an empty newState", func() { - Expect(store.UpdateState("fake", "12345678", "in_transit", "")).ToNot(Succeed()) + It("will remove with an empty newState", func() { + Expect(store.UpdateState("orders", "fsm-1", "in_transit", "")).To(Succeed()) + res := store.GetAllInState("orders", "in_transit") + Ω(res).ToNot(ContainElement("fsm-1")) }) }) }) diff --git a/storage/storage_suite_test.go b/storage/storage_suite_test.go index 3b58d97..fe5a929 100644 --- a/storage/storage_suite_test.go +++ b/storage/storage_suite_test.go @@ -11,13 +11,13 @@ package storage_test import ( "context" - "fmt" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" "testing" "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + internals "github.com/massenz/go-statemachine/internals/testing" ) func TestStorage(t *testing.T) { @@ -25,43 +25,10 @@ func TestStorage(t *testing.T) { RunSpecs(t, "Storage Suite") } -type RedisContainer struct { - testcontainers.Container - Address string -} - -func SetupRedis(ctx context.Context) (*RedisContainer, error) { - req := testcontainers.ContainerRequest{ - Image: "redis:6", - ExposedPorts: []string{"6379/tcp"}, - WaitingFor: wait.ForLog("* Ready to accept connections"), - } - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) - if err != nil { - return nil, err - } - - mappedPort, err := container.MappedPort(ctx, "6379") - if err != nil { - return nil, err - } - - hostIP, err := container.Host(ctx) - if err != nil { - return nil, err - } - - address := fmt.Sprintf("%s:%s", hostIP, mappedPort.Port()) - return &RedisContainer{Container: container, Address: address}, nil -} - -var container *RedisContainer +var container *internals.Container var _ = BeforeSuite(func() { var err error - container, err = SetupRedis(context.Background()) + container, err = internals.NewRedisContainer(context.Background()) Expect(err).ToNot(HaveOccurred()) // Note the timeout here is in seconds (and it's not a time.Duration either) }, 5.0)