From 9b67a66a8d84dd0a0023d1a6a49808f2a28db13b Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 7 Jun 2022 08:45:40 -0400 Subject: [PATCH 01/95] Refactor FFCAPI to a local API interace, and FFTM to a utility package Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 4 + Dockerfile | 12 - Makefile | 24 +- cmd/config_docs_test.go | 46 -- cmd/fftm.go | 102 ----- cmd/fftm_test.go | 75 ---- cmd/version.go | 103 ----- cmd/version_test.go | 65 --- go.mod | 2 - go.sum | 2 - internal/confirmations/confirmations.go | 86 ++-- internal/confirmations/confirmations_test.go | 425 +++++++----------- internal/manager/api_test.go | 323 ------------- internal/tmconfig/tmconfig.go | 6 - mocks/ffcapimocks/api.go | 174 ++++--- mocks/managermocks/manager.go | 43 -- mocks/policyenginemocks/policy_engine.go | 14 +- mocks/policyenginesmocks/policy_engine.go | 45 ++ pkg/ffcapi/api.go | 130 ++++++ pkg/ffcapi/block_info_by_hash.go | 25 ++ .../ffcapi/block_info_by_number.go | 17 +- .../ffcapi/event_listener_add.go | 30 +- .../ffcapi/event_listener_remove.go | 18 +- pkg/ffcapi/gas_price_estimate.go | 26 ++ pkg/ffcapi/method_call.go | 36 ++ .../ffcapi/next_nonce_for_signer.go | 33 +- pkg/ffcapi/transaction_prepare.go | 48 ++ pkg/ffcapi/transaction_receipt.go | 33 ++ pkg/ffcapi/transaction_send.go | 34 ++ {internal/manager => pkg/fftm}/api.go | 10 +- pkg/fftm/api_test.go | 238 ++++++++++ .../manager => pkg/fftm}/changelistener.go | 2 +- .../fftm}/changelistener_test.go | 4 +- {internal/manager => pkg/fftm}/ffcore.go | 12 +- pkg/fftm/managed_tx.go | 48 -- {internal/manager => pkg/fftm}/manager.go | 21 +- .../manager => pkg/fftm}/manager_test.go | 73 ++- {internal/manager => pkg/fftm}/nonces.go | 12 +- {internal/manager => pkg/fftm}/nonces_test.go | 54 ++- {internal/manager => pkg/fftm}/policyloop.go | 18 +- .../manager => pkg/fftm}/policyloop_test.go | 73 ++- {internal/manager => pkg/fftm}/routes.go | 12 +- pkg/policyengine/managed_tx.go | 48 ++ pkg/policyengine/policyengine.go | 5 +- pkg/{fftm => policyengine}/tx_request.go | 4 +- {internal => pkg}/policyengines/registry.go | 0 .../policyengines/registry_test.go | 2 +- .../policyengines/simple/config.go | 0 .../simple/simple_policy_engine.go | 13 +- .../simple/simple_policy_engine_test.go | 72 +-- 50 files changed, 1201 insertions(+), 1501 deletions(-) delete mode 100644 Dockerfile delete mode 100644 cmd/config_docs_test.go delete mode 100644 cmd/fftm.go delete mode 100644 cmd/fftm_test.go delete mode 100644 cmd/version.go delete mode 100644 cmd/version_test.go delete mode 100644 internal/manager/api_test.go delete mode 100644 mocks/managermocks/manager.go create mode 100644 mocks/policyenginesmocks/policy_engine.go create mode 100644 pkg/ffcapi/api.go create mode 100644 pkg/ffcapi/block_info_by_hash.go rename cmd/config_test.go => pkg/ffcapi/block_info_by_number.go (74%) rename cmd/config.go => pkg/ffcapi/event_listener_add.go (57%) rename fftm/main.go => pkg/ffcapi/event_listener_remove.go (76%) create mode 100644 pkg/ffcapi/gas_price_estimate.go create mode 100644 pkg/ffcapi/method_call.go rename cmd/config_docs_generate_test.go => pkg/ffcapi/next_nonce_for_signer.go (53%) create mode 100644 pkg/ffcapi/transaction_prepare.go create mode 100644 pkg/ffcapi/transaction_receipt.go create mode 100644 pkg/ffcapi/transaction_send.go rename {internal/manager => pkg/fftm}/api.go (89%) create mode 100644 pkg/fftm/api_test.go rename {internal/manager => pkg/fftm}/changelistener.go (99%) rename {internal/manager => pkg/fftm}/changelistener_test.go (95%) rename {internal/manager => pkg/fftm}/ffcore.go (90%) delete mode 100644 pkg/fftm/managed_tx.go rename {internal/manager => pkg/fftm}/manager.go (93%) rename {internal/manager => pkg/fftm}/manager_test.go (84%) rename {internal/manager => pkg/fftm}/nonces.go (89%) rename {internal/manager => pkg/fftm}/nonces_test.go (57%) rename {internal/manager => pkg/fftm}/policyloop.go (91%) rename {internal/manager => pkg/fftm}/policyloop_test.go (84%) rename {internal/manager => pkg/fftm}/routes.go (87%) create mode 100644 pkg/policyengine/managed_tx.go rename pkg/{fftm => policyengine}/tx_request.go (93%) rename {internal => pkg}/policyengines/registry.go (100%) rename {internal => pkg}/policyengines/registry_test.go (93%) rename {internal => pkg}/policyengines/simple/config.go (100%) rename {internal => pkg}/policyengines/simple/simple_policy_engine.go (93%) rename {internal => pkg}/policyengines/simple/simple_policy_engine_test.go (89%) diff --git a/.vscode/settings.json b/.vscode/settings.json index a537eed0..25ab9174 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "cSpell.words": [ "APIID", "ccache", + "confirmationsmocks", "dataexchange", "Debugf", "devdocs", @@ -32,12 +33,15 @@ "hyperledger", "Infof", "IPFS", + "Kaleido", "mtxs", "NATS", "Nowarn", "oapispec", "optype", "policyengine", + "policyenginemocks", + "policyengines", "protocolid", "resty", "santhosh", diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 06dc8f3c..00000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM golang:1.17-buster AS builder -ARG BUILD_VERSION -ENV BUILD_VERSION=${BUILD_VERSION} -ADD . /fftm -WORKDIR /fftm -RUN make - -FROM debian:buster-slim -WORKDIR /fftm -COPY --from=builder /fftm/firefly-transaction-manager /usr/bin/fftm - -ENTRYPOINT [ "/usr/bin/fftm" ] diff --git a/Makefile b/Makefile index 051a6ecd..8d8a596d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ VGO=go -GOFILES := $(shell find cmd internal pkg -name '*.go' -print) +GOFILES := $(shell find internal pkg -name '*.go' -print) GOBIN := $(shell $(VGO) env GOPATH)/bin LINT := $(GOBIN)/golangci-lint MOCKERY := $(GOBIN)/mockery @@ -12,15 +12,13 @@ GOGC=30 all: build test go-mod-tidy test: deps lint - $(VGO) test ./internal/... ./cmd/... -cover -coverprofile=coverage.txt -covermode=atomic -timeout=30s + $(VGO) test ./internal/... ./pkg/... -cover -coverprofile=coverage.txt -covermode=atomic -timeout=30s coverage.html: $(VGO) tool cover -html=coverage.txt coverage: test coverage.html lint: ${LINT} GOGC=20 $(LINT) run -v --timeout 5m -ffcapi: - $(eval FFCAPI_PATH := $(shell $(VGO) list -f '{{.Dir}}' github.com/hyperledger/firefly-common/pkg/ffcapi)) - + ${MOCKERY}: $(VGO) install github.com/vektra/mockery/cmd/mockery@latest ${LINT}: @@ -29,26 +27,22 @@ ${LINT}: define makemock mocks: mocks-$(strip $(1))-$(strip $(2)) -mocks-$(strip $(1))-$(strip $(2)): ${MOCKERY} ffcapi +mocks-$(strip $(1))-$(strip $(2)): ${MOCKERY} ${MOCKERY} --case underscore --dir $(1) --name $(2) --outpkg $(3) --output mocks/$(strip $(3)) endef -$(eval $(call makemock, $${FFCAPI_PATH}, API, ffcapimocks)) +$(eval $(call makemock, pkg/ffcapi, API, ffcapimocks)) $(eval $(call makemock, pkg/policyengine, PolicyEngine, policyenginemocks)) $(eval $(call makemock, internal/confirmations, Manager, confirmationsmocks)) -$(eval $(call makemock, internal/manager, Manager, managermocks)) -firefly-transaction-manager: ${GOFILES} - $(VGO) build -o ./firefly-transaction-manager -ldflags "-X main.buildDate=`date -u +\"%Y-%m-%dT%H:%M:%SZ\"` -X main.buildVersion=$(BUILD_VERSION)" -tags=prod -tags=prod -v ./fftm go-mod-tidy: .ALWAYS $(VGO) mod tidy -build: firefly-transaction-manager +build: test .ALWAYS: ; clean: $(VGO) clean deps: - $(VGO) get ./fftm + $(VGO) get ./internal/... ./pkg/... + $(VGO) get -t ./internal/... ./pkg/... docs: - $(VGO) test ./cmd -timeout=10s -tags docs -docker: - docker build --build-arg BUILD_VERSION=${BUILD_VERSION} ${DOCKER_ARGS} -t hyperledger/firefly-transaction-manager . \ No newline at end of file + $(VGO) test ./internal -timeout=10s -tags docs diff --git a/cmd/config_docs_test.go b/cmd/config_docs_test.go deleted file mode 100644 index 52adc184..00000000 --- a/cmd/config_docs_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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. - -//go:build !docs -// +build !docs - -package cmd - -import ( - "context" - "crypto/sha1" - "os" - "path/filepath" - "testing" - - "github.com/hyperledger/firefly-common/pkg/config" - "github.com/stretchr/testify/assert" -) - -func TestConfigDocsUpToDate(t *testing.T) { - // Initialize config of all plugins - initConfig() - generatedConfig, err := config.GenerateConfigMarkdown(context.Background(), config.GetKnownKeys()) - assert.NoError(t, err) - configOnDisk, err := os.ReadFile(filepath.Join("..", "config.md")) - assert.NoError(t, err) - - generatedConfigHash := sha1.New() - generatedConfigHash.Write(generatedConfig) - configOnDiskHash := sha1.New() - configOnDiskHash.Write(configOnDisk) - assert.Equal(t, configOnDiskHash.Sum(nil), generatedConfigHash.Sum(nil), "The config reference docs generated by the code did not match the config.md file in git. Did you forget to run `make docs`?") -} diff --git a/cmd/fftm.go b/cmd/fftm.go deleted file mode 100644 index 54e8ba65..00000000 --- a/cmd/fftm.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 cmd - -import ( - "context" - "fmt" - "os" - "os/signal" - "syscall" - - "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/i18n" - "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-transaction-manager/internal/manager" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines/simple" - "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -var sigs = make(chan os.Signal, 1) - -var rootCmd = &cobra.Command{ - Use: "fftm", - Short: "Hyperledger FireFly Tranansaction Manager", - Long: ``, - RunE: func(cmd *cobra.Command, args []string) error { - return run() - }, -} - -var cfgFile string - -func init() { - rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file") - rootCmd.AddCommand(versionCommand()) - rootCmd.AddCommand(configCommand()) -} - -func Execute() error { - return rootCmd.Execute() -} - -func initConfig() { - // Read the configuration, and register our policy engines - tmconfig.Reset() - policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) -} - -func run() error { - - initConfig() - err := config.ReadConfig("fftm", cfgFile) - - // Setup logging after reading config (even if failed), to output header correctly - ctx, cancelCtx := context.WithCancel(context.Background()) - defer cancelCtx() - ctx = log.WithLogger(ctx, logrus.WithField("pid", fmt.Sprintf("%d", os.Getpid()))) - ctx = log.WithLogger(ctx, logrus.WithField("prefix", config.GetString(tmconfig.ManagerName))) - - config.SetupLogging(ctx) - - // Deferred error return from reading config - if err != nil { - cancelCtx() - return i18n.WrapError(ctx, err, i18n.MsgConfigFailed) - } - - // Setup signal handling to cancel the context, which shuts down the API Server - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - go func() { - sig := <-sigs - log.L(ctx).Infof("Shutting down due to %s", sig.String()) - cancelCtx() - }() - - manager, err := manager.NewManager(ctx) - if err != nil { - return err - } - err = manager.Start() - if err != nil { - return err - } - return manager.WaitStop() -} diff --git a/cmd/fftm_test.go b/cmd/fftm_test.go deleted file mode 100644 index af0ac56c..00000000 --- a/cmd/fftm_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright © 2021 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 cmd - -import ( - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -const configDir = "../test/data/config" - -func TestRunOK(t *testing.T) { - - rootCmd.SetArgs([]string{"-f", "../test/firefly.fftm.yaml"}) - defer rootCmd.SetArgs([]string{}) - - done := make(chan struct{}) - go func() { - defer close(done) - err := Execute() - assert.NoError(t, err) - }() - - time.Sleep(10 * time.Millisecond) - sigs <- os.Kill - - <-done - -} - -func TestRunMissingConfig(t *testing.T) { - - rootCmd.SetArgs([]string{"-f", "../test/does-not-exist.fftm.yaml"}) - defer rootCmd.SetArgs([]string{}) - - err := Execute() - assert.Regexp(t, "FF00101", err) - -} - -func TestRunBadConfig(t *testing.T) { - - rootCmd.SetArgs([]string{"-f", "../test/empty-config.fftm.yaml"}) - defer rootCmd.SetArgs([]string{}) - - err := Execute() - assert.Regexp(t, "FF21018", err) - -} - -func TestRunFailStartup(t *testing.T) { - rootCmd.SetArgs([]string{"-f", "../test/quick-fail.fftm.yaml"}) - defer rootCmd.SetArgs([]string{}) - - err := Execute() - assert.Regexp(t, "FF21017", err) - -} diff --git a/cmd/version.go b/cmd/version.go deleted file mode 100644 index 483baafd..00000000 --- a/cmd/version.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 cmd - -import ( - "context" - "encoding/json" - "fmt" - "runtime/debug" - - "github.com/hyperledger/firefly-common/pkg/i18n" - "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/spf13/cobra" - "gopkg.in/yaml.v2" -) - -var shortened = false -var output = "json" - -var BuildDate string // set by go-releaser -var BuildCommit string // set by go-releaser -var BuildVersionOverride string // set by go-releaser - -type Info struct { - Version string `json:"Version,omitempty" yaml:"Version,omitempty"` - Commit string `json:"Commit,omitempty" yaml:"Commit,omitempty"` - Date string `json:"Date,omitempty" yaml:"Date,omitempty"` - License string `json:"License,omitempty" yaml:"License,omitempty"` -} - -func setBuildInfo(info *Info, buildInfo *debug.BuildInfo, ok bool) { - if ok { - info.Version = buildInfo.Main.Version - } -} - -func versionCommand() *cobra.Command { - versionCmd := &cobra.Command{ - Use: "version", - Short: "Prints the version info", - Long: "", - RunE: func(cmd *cobra.Command, args []string) error { - - info := &Info{ - Version: BuildVersionOverride, - Date: BuildDate, - Commit: BuildCommit, - License: "Apache-2.0", - } - - // Where you are using go install, we will get good version information usefully from Go - // When we're in go-releaser in a Github action, we will have the version passed in explicitly - if info.Version == "" { - buildInfo, ok := debug.ReadBuildInfo() - setBuildInfo(info, buildInfo, ok) - } - - if shortened { - fmt.Println(info.Version) - } else { - var ( - bytes []byte - err error - ) - - switch output { - case "json": - bytes, err = json.MarshalIndent(info, "", " ") - case "yaml": - bytes, err = yaml.Marshal(info) - default: - err = i18n.NewError(context.Background(), tmmsgs.MsgInvalidOutputType, output) - } - - if err != nil { - return err - } - - fmt.Println(string(bytes)) - } - - return nil - }, - } - - versionCmd.Flags().BoolVarP(&shortened, "short", "s", false, "print only the version") - versionCmd.Flags().StringVarP(&output, "output", "o", "json", "output format (\"yaml\"|\"json\")") - return versionCmd -} diff --git a/cmd/version_test.go b/cmd/version_test.go deleted file mode 100644 index 44d4e5f5..00000000 --- a/cmd/version_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 cmd - -import ( - "runtime/debug" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestVersionCmdDefault(t *testing.T) { - rootCmd.SetArgs([]string{"version"}) - defer rootCmd.SetArgs([]string{}) - err := rootCmd.Execute() - assert.NoError(t, err) -} - -func TestVersionCmdYAML(t *testing.T) { - rootCmd.SetArgs([]string{"version", "-o", "yaml"}) - defer rootCmd.SetArgs([]string{}) - err := rootCmd.Execute() - assert.NoError(t, err) -} - -func TestVersionCmdJSON(t *testing.T) { - rootCmd.SetArgs([]string{"version", "-o", "json"}) - defer rootCmd.SetArgs([]string{}) - err := rootCmd.Execute() - assert.NoError(t, err) -} - -func TestVersionCmdInvalidType(t *testing.T) { - rootCmd.SetArgs([]string{"version", "-o", "wrong"}) - defer rootCmd.SetArgs([]string{}) - err := rootCmd.Execute() - assert.Regexp(t, "FF21010", err) -} - -func TestVersionCmdShorthand(t *testing.T) { - rootCmd.SetArgs([]string{"version", "-s"}) - defer rootCmd.SetArgs([]string{}) - err := rootCmd.Execute() - assert.NoError(t, err) -} - -func TestSetBuildInfoWithBI(t *testing.T) { - info := &Info{} - setBuildInfo(info, &debug.BuildInfo{Main: debug.Module{Version: "12345"}}, true) - assert.Equal(t, "12345", info.Version) -} diff --git a/go.mod b/go.mod index 6cb35195..9ddf0847 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,6 @@ require ( github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e github.com/hyperledger/firefly-common v0.1.2 github.com/sirupsen/logrus v1.8.1 - github.com/spf13/cobra v1.3.0 github.com/spf13/viper v1.11.0 github.com/stretchr/testify v1.7.1 - gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 63f5928f..b86ba540 100644 --- a/go.sum +++ b/go.sum @@ -625,7 +625,6 @@ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= @@ -994,7 +993,6 @@ github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index ea3719f8..18cdad8d 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -24,12 +24,12 @@ import ( lru "github.com/hashicorp/golang-lru" "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) // Manager listens to the blocks on the chain, and attributes confirmations to @@ -65,13 +65,13 @@ type EventInfo struct { TransactionHash string TransactionIndex uint64 LogIndex uint64 - Receipt func(receipt *ffcapi.GetReceiptResponse) + Receipt func(receipt *ffcapi.TransactionReceiptResponse) Confirmed func(confirmations []BlockInfo) } type TransactionInfo struct { TransactionHash string - Receipt func(receipt *ffcapi.GetReceiptResponse) + Receipt func(receipt *ffcapi.TransactionReceiptResponse) Confirmed func(confirmations []BlockInfo) } @@ -90,9 +90,8 @@ type BlockInfo struct { type blockConfirmationManager struct { ctx context.Context cancelFunc func() - blockListenerID string + connector ffcapi.API blockListenerStale bool - connectorAPI ffcapi.API requiredConfirmations int pollingInterval time.Duration staleReceiptTimeout time.Duration @@ -104,14 +103,14 @@ type blockConfirmationManager struct { done chan struct{} } -func NewBlockConfirmationManager(ctx context.Context, connectorAPI ffcapi.API) (Manager, error) { +func NewBlockConfirmationManager(ctx context.Context, connector ffcapi.API) (Manager, error) { var err error bcm := &blockConfirmationManager{ - connectorAPI: connectorAPI, + connector: connector, + blockListenerStale: true, requiredConfirmations: config.GetInt(tmconfig.ConfirmationsRequired), pollingInterval: config.GetDuration(tmconfig.ConfirmationsBlockPollingInterval), staleReceiptTimeout: config.GetDuration(tmconfig.ConfirmationsStaleReceiptTimeout), - blockListenerStale: true, bcmNotifications: make(chan *Notification, config.GetInt(tmconfig.ConfirmationsNotificationQueueLength)), pending: make(map[string]*pendingItem), staleReceipts: make(map[string]bool), @@ -137,8 +136,8 @@ type pendingItem struct { pType pendingType added time.Time confirmations []*BlockInfo - lastReceiptcheck time.Time - receiptCallback func(receipt *ffcapi.GetReceiptResponse) + lastReceiptCheck time.Time + receiptCallback func(receipt *ffcapi.TransactionReceiptResponse) confirmedCallback func(confirmations []BlockInfo) transactionHash string streamID string // events only @@ -195,7 +194,7 @@ func (n *Notification) eventPendingItem() *pendingItem { func (n *Notification) transactionPendingItem() *pendingItem { return &pendingItem{ pType: pendingTypeTransaction, - lastReceiptcheck: time.Now(), + lastReceiptCheck: time.Now(), transactionHash: n.Transaction.TransactionHash, receiptCallback: n.Transaction.Receipt, confirmedCallback: n.Transaction.Confirmed, @@ -251,32 +250,6 @@ func (bcm *blockConfirmationManager) Notify(n *Notification) error { return nil } -func (bcm *blockConfirmationManager) createBlockListener() error { - res, _, err := bcm.connectorAPI.CreateBlockListener(bcm.ctx, &ffcapi.CreateBlockListenerRequest{}) - if err != nil { - return err - } - bcm.blockListenerStale = false - bcm.blockListenerID = res.ListenerID - log.L(bcm.ctx).Infof("Created blockListener: %s", bcm.blockListenerID) - return err -} - -func (bcm *blockConfirmationManager) pollBlockListener() ([]string, error) { - ctx, cancel := context.WithTimeout(bcm.ctx, 30*time.Second) - defer cancel() - res, reason, err := bcm.connectorAPI.GetNewBlockHashes(ctx, &ffcapi.GetNewBlockHashesRequest{ - ListenerID: bcm.blockListenerID, - }) - if err != nil { - if reason == ffcapi.ErrorReasonNotFound { - bcm.blockListenerStale = true - } - return nil, err - } - return res.BlockHashes, nil -} - func (bcm *blockConfirmationManager) addToCache(blockInfo *BlockInfo) { bcm.blockCache.Add(blockInfo.BlockHash, blockInfo) bcm.blockCache.Add(strconv.FormatUint(blockInfo.BlockNumber, 10), blockInfo) @@ -288,7 +261,7 @@ func (bcm *blockConfirmationManager) getBlockByHash(blockHash string) (*BlockInf return cached.(*BlockInfo), nil } - res, reason, err := bcm.connectorAPI.GetBlockInfoByHash(bcm.ctx, &ffcapi.GetBlockInfoByHashRequest{ + res, reason, err := bcm.connector.BlockInfoByHash(bcm.ctx, &ffcapi.BlockInfoByHashRequest{ BlockHash: blockHash, }) if err != nil { @@ -315,7 +288,7 @@ func (bcm *blockConfirmationManager) getBlockByNumber(blockNumber uint64, expect return blockInfo, nil } } - res, reason, err := bcm.connectorAPI.GetBlockInfoByNumber(bcm.ctx, &ffcapi.GetBlockInfoByNumberRequest{ + res, reason, err := bcm.connector.BlockInfoByNumber(bcm.ctx, &ffcapi.BlockInfoByNumberRequest{ BlockNumber: fftypes.NewFFBigInt(int64(blockNumber)), }) if err != nil { @@ -339,6 +312,21 @@ func transformBlockInfo(res *ffcapi.BlockInfo) *BlockInfo { } } +func (bcm *blockConfirmationManager) getNewBlockHashes() []string { + var blockHashes []string + for { + select { + case bhe := <-bcm.connector.NewBlockHashes(): + if bhe.GapPotential { + bcm.blockListenerStale = true + } + blockHashes = append(blockHashes, bhe.BlockHashes...) + default: + return blockHashes + } + } +} + func (bcm *blockConfirmationManager) confirmationsListener() { defer close(bcm.done) pollTimer := time.NewTimer(0) @@ -364,28 +352,16 @@ func (bcm *blockConfirmationManager) confirmationsListener() { } pollTimer = time.NewTimer(bcm.pollingInterval) - // Setup a blockListener if we're missing one if bcm.blockListenerStale { - if err := bcm.createBlockListener(); err != nil { - log.L(bcm.ctx).Errorf("Failed to create blockListener: %s", err) - continue - } - if err := bcm.walkChain(); err != nil { log.L(bcm.ctx).Errorf("Failed to create walk chain after restoring blockListener: %s", err) continue } - } - - // Do the poll - blockHashes, err := bcm.pollBlockListener() - if err != nil { - log.L(bcm.ctx).Errorf("Failed to retrieve blocks from blockListener: %s", err) - continue + bcm.blockListenerStale = false } // Process each new block - bcm.processBlockHashes(blockHashes) + bcm.processBlockHashes(bcm.getNewBlockHashes()) // Process any new notifications - we do this at the end, so it can benefit // from knowing the latest highestBlockSeen @@ -414,7 +390,7 @@ func (bcm *blockConfirmationManager) confirmationsListener() { func (bcm *blockConfirmationManager) staleReceiptCheck() { now := time.Now() for _, pending := range bcm.pending { - if pending.pType == pendingTypeTransaction && now.Sub(pending.lastReceiptcheck) > bcm.staleReceiptTimeout { + if pending.pType == pendingTypeTransaction && now.Sub(pending.lastReceiptCheck) > bcm.staleReceiptTimeout { pendingKey := pending.getKey() log.L(bcm.ctx).Infof("Marking receipt check stale for %s", pendingKey) bcm.staleReceipts[pendingKey] = true @@ -450,7 +426,7 @@ func (bcm *blockConfirmationManager) processNotifications(notifications []*Notif } func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem) { - res, reason, err := bcm.connectorAPI.GetReceipt(bcm.ctx, &ffcapi.GetReceiptRequest{ + res, reason, err := bcm.connector.TransactionReceipt(bcm.ctx, &ffcapi.TransactionReceiptRequest{ TransactionHash: pending.transactionHash, }) diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index d4ba4959..61e939b8 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -22,10 +22,10 @@ import ( "time" "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -70,24 +70,15 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { confirmed <- confirmations }, } - lastBlockDetected := false - - // Establish the block filter - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() // First poll for changes gives nothing, but we load up the event at this point for the next round - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Run(func(args mock.Arguments) { + blockHashes := make(chan *ffcapi.BlockHashEvent, 1) + mca.On("NewBlockHashes").Run(func(args mock.Arguments) { bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: eventToConfirm, }) - }).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Once() + }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() // Next time round gives a block that is in the confirmation chain, but one block ahead block1003 := &BlockInfo{ @@ -95,16 +86,16 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", ParentHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295", } - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{block1003.BlockHash}, - }, ffcapi.ErrorReason(""), nil).Once() + mca.On("NewBlockHashes").Run(func(args mock.Arguments) { + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{block1003.BlockHash}, + } + }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() // The next filter gives us 1003 - which is two blocks ahead of our notified log - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)), BlockHash: block1003.BlockHash, @@ -118,9 +109,9 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { BlockHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295", ParentHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", } - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)), BlockHash: block1002.BlockHash, @@ -137,16 +128,17 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", } - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{block1004.BlockHash}, - }, ffcapi.ErrorReason(""), nil).Once() + + mca.On("NewBlockHashes").Run(func(args mock.Arguments) { + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{block1004.BlockHash}, + } + }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() // Which then gets downloaded, and should complete the confirmation - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1004.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)), BlockHash: block1004.BlockHash, @@ -154,16 +146,8 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - // Subsequent calls get nothing, and blocks until close anyway - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Run(func(args mock.Arguments) { - if lastBlockDetected { - <-bcm.ctx.Done() - } - }).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Maybe() + // Subsequent calls get nothing + mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) bcm.Start() @@ -195,12 +179,6 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { confirmed <- confirmations }, } - lastBlockDetected := false - - // Establish the block filter - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() // The next filter gives us 1002, and a first 1003 block - which will later be removed block1002 := &BlockInfo{ @@ -213,26 +191,28 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { BlockHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295", ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", } - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{ - block1002.BlockHash, - block1003a.BlockHash, - }, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + + blockHashes := make(chan *ffcapi.BlockHashEvent, 1) + mca.On("NewBlockHashes").Run(func(args mock.Arguments) { + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1002.BlockHash, + block1003a.BlockHash, + }, + } + }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1002.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)), BlockHash: block1002.BlockHash, ParentHash: block1002.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003a.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003a.BlockNumber)), BlockHash: block1003a.BlockHash, @@ -251,26 +231,26 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21", ParentHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", } - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{ - block1003b.BlockHash, - block1004.BlockHash, - }, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("NewBlockHashes").Run(func(args mock.Arguments) { + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1003b.BlockHash, + block1004.BlockHash, + }, + } + }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003b.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003b.BlockNumber)), BlockHash: block1003b.BlockHash, ParentHash: block1003b.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1004.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)), BlockHash: block1004.BlockHash, @@ -278,16 +258,8 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - // Subsequent calls get nothing, and blocks until close anyway - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Run(func(args mock.Arguments) { - if lastBlockDetected { - <-bcm.ctx.Done() - } - }).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Maybe() + // Subsequent calls get nothing + mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) bcm.Start() @@ -314,13 +286,13 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, true) confirmed := make(chan []BlockInfo, 1) - receiptReceived := make(chan *ffcapi.GetReceiptResponse, 1) + receiptReceived := make(chan *ffcapi.TransactionReceiptResponse, 1) txToConfirmForkA := &TransactionInfo{ TransactionHash: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", Confirmed: func(confirmations []BlockInfo) { confirmed <- confirmations }, - Receipt: func(receipt *ffcapi.GetReceiptResponse) { + Receipt: func(receipt *ffcapi.TransactionReceiptResponse) { receiptReceived <- receipt }, } @@ -346,52 +318,47 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", ParentHash: "0x33eb56730878a08e126f2d52b19242d3b3127dc7611447255928be91b2dda455", } - lastBlockDetected := false - - // Establish the block filter - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() // The next filter gives us 1002a, which will later be removed - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ + blockHashes := make(chan *ffcapi.BlockHashEvent, 1) + // Notify of the first confirmation for the first receipt - 1002a + blockHashes <- &ffcapi.BlockHashEvent{ BlockHashes: []string{ block1002a.BlockHash, }, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { - return r.BlockHash == block1002a.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(int64(block1002a.BlockNumber)), - BlockHash: block1002a.BlockHash, - ParentHash: block1002a.ParentHash, - }, - }, ffcapi.ErrorReason(""), nil).Once() + } + mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) // Transaction receipt is immediately available on fork A - mca.On("GetReceipt", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetReceiptRequest) bool { + mca.On("TransactionReceipt", mock.Anything, mock.MatchedBy(func(r *ffcapi.TransactionReceiptRequest) bool { return r.TransactionHash == txToConfirmForkA.TransactionHash - })).Return(&ffcapi.GetReceiptResponse{ + })).Return(&ffcapi.TransactionReceiptResponse{ BlockHash: block1002a.ParentHash, BlockNumber: fftypes.NewFFBigInt(1001), TransactionIndex: fftypes.NewFFBigInt(0), Success: true, }, ffcapi.ErrorReason(""), nil).Once() - // Next we notify of the new block 1001b - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{ - block1001b.BlockHash, + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { + return r.BlockHash == block1002a.BlockHash + })).Run(func(args mock.Arguments) { + // Next we notify of the new block 1001b + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1001b.BlockHash, + }, + } + }).Return(&ffcapi.BlockInfoByHashResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block1002a.BlockNumber)), + BlockHash: block1002a.BlockHash, + ParentHash: block1002a.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1001b.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1001b.BlockNumber)), BlockHash: block1001b.BlockHash, @@ -401,26 +368,15 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { }, ffcapi.ErrorReason(""), nil).Once() // Transaction receipt is then found on fork B via new block header notification - mca.On("GetReceipt", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetReceiptRequest) bool { + mca.On("TransactionReceipt", mock.Anything, mock.MatchedBy(func(r *ffcapi.TransactionReceiptRequest) bool { return r.TransactionHash == txToConfirmForkA.TransactionHash - })).Return(&ffcapi.GetReceiptResponse{ + })).Return(&ffcapi.TransactionReceiptResponse{ BlockHash: block1001b.BlockHash, BlockNumber: fftypes.NewFFBigInt(1001), TransactionIndex: fftypes.NewFFBigInt(0), Success: true, }, ffcapi.ErrorReason(""), nil).Once() - // We will go and ask for block 1002 again, as the hash mismatches our updated notification - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { - return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(int64(block1002b.BlockNumber)), - BlockHash: block1002b.BlockHash, - ParentHash: block1002b.ParentHash, - }, - }, ffcapi.ErrorReason(""), nil).Once() - // Then we get the final fork up to our confirmation block1003 := &BlockInfo{ BlockNumber: 1003, @@ -432,26 +388,39 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21", ParentHash: "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", } - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{ - block1003.BlockHash, - block1004.BlockHash, + + // We will go and ask for block 1002 again, as the hash mismatches our updated notification + // Give the right answer now + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1002 + })).Run(func(args mock.Arguments) { + // Notify of the new block 1003/1004 + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1003.BlockHash, + block1004.BlockHash, + }, + } + }).Return(&ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block1002b.BlockNumber)), + BlockHash: block1002b.BlockHash, + ParentHash: block1002b.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)), BlockHash: block1003.BlockHash, ParentHash: block1003.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1004.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)), BlockHash: block1004.BlockHash, @@ -459,16 +428,8 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - // Subsequent calls get nothing, and blocks until close anyway - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Run(func(args mock.Arguments) { - if lastBlockDetected { - <-bcm.ctx.Done() - } - }).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Maybe() + // Subsequent calls get nothing + mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) bcm.Start() @@ -505,17 +466,9 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { }, } - // Establish the block filter - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() - // We don't notify of any new blocks - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil) + blockHashes := make(chan *ffcapi.BlockHashEvent, 1) + mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) // Then we should walk the chain by number to fill in 1002/1003, because our HWM is 1003 block1002 := &BlockInfo{ @@ -533,27 +486,27 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", } - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)), BlockHash: block1002.BlockHash, ParentHash: block1002.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1003 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)), BlockHash: block1003.BlockHash, ParentHash: block1003.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1004 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)), BlockHash: block1004.BlockHash, @@ -599,25 +552,6 @@ func TestSortPendingEvents(t *testing.T) { }, events) } -func TestCreateBlockFilterFail(t *testing.T) { - - bcm, mca := newTestBlockConfirmationManager(t, false) - bcm.done = make(chan struct{}) - bcm.blockListenerID = "listener1" - - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return( - &ffcapi.CreateBlockListenerResponse{ListenerID: "listener1"}, - ffcapi.ErrorReason(""), - fmt.Errorf("pop"), - ).Once().Run(func(args mock.Arguments) { - bcm.cancelFunc() - }) - - bcm.confirmationsListener() - - mca.AssertExpectations(t) -} - func TestConfirmationsListenerFailWalkingChain(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) @@ -633,61 +567,30 @@ func TestConfirmationsListenerFailWalkingChain(t *testing.T) { } bcm.addOrReplaceItem(n.eventPendingItem()) - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once().Run(func(args mock.Arguments) { - bcm.cancelFunc() - }) - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 - })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() - - bcm.confirmationsListener() - - mca.AssertExpectations(t) -} - -func TestConfirmationsListenerFailPollingBlocks(t *testing.T) { - - bcm, mca := newTestBlockConfirmationManager(t, false) - bcm.done = make(chan struct{}) - - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once().Run(func(args mock.Arguments) { + })).Run(func(args mock.Arguments) { bcm.cancelFunc() - }) - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + }).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() bcm.confirmationsListener() mca.AssertExpectations(t) } -func TestConfirmationsListenerLostFilterReestablish(t *testing.T) { +func TestConfirmationsListenerStalePollingBlocks(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) bcm.done = make(chan struct{}) - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once().Twice() - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReasonNotFound, fmt.Errorf("pop")).Once() - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Run(func(args mock.Arguments) { + blockHashes := make(chan *ffcapi.BlockHashEvent, 1) + blockHashes <- &ffcapi.BlockHashEvent{ + GapPotential: true, + } + mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() + mca.On("NewBlockHashes").Run(func(args mock.Arguments) { bcm.cancelFunc() - }) + }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) bcm.confirmationsListener() @@ -716,15 +619,9 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { Event: eventToConfirm, }) - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil) - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + blockHashes := make(chan *ffcapi.BlockHashEvent, 1) + mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once().Run(func(args mock.Arguments) { bcm.cancelFunc() @@ -760,15 +657,9 @@ func TestConfirmationsListenerStopStream(t *testing.T) { }, }) - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Maybe() - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Maybe() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Maybe() + blockHashes := make(chan *ffcapi.BlockHashEvent, 1) + mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Maybe() bcm.Start() @@ -800,15 +691,9 @@ func TestConfirmationsRemoveEvent(t *testing.T) { Event: eventInfo, }) - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + blockHashes := make(chan *ffcapi.BlockHashEvent, 1) + mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) { bcm.cancelFunc() @@ -837,17 +722,11 @@ func TestConfirmationsRemoveTransaction(t *testing.T) { Transaction: txInfo, }) - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetNewBlockHashes", mock.Anything, mock.Anything).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetNewBlockHashes", mock.Anything, mock.Anything).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Once().Run(func(args mock.Arguments) { + blockHashes := make(chan *ffcapi.BlockHashEvent, 1) + mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() + mca.On("NewBlockHashes").Run(func(args mock.Arguments) { bcm.cancelFunc() - }) + }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) bcm.confirmationsListener() <-bcm.done @@ -871,9 +750,9 @@ func TestWalkChainForEventBlockNotInConfirmationChain(t *testing.T) { }, }).eventPendingItem() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(1002), BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", @@ -902,7 +781,7 @@ func TestWalkChainForEventBlockLookupFail(t *testing.T) { }, }).eventPendingItem() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() @@ -917,7 +796,7 @@ func TestProcessBlockHashesLookupFail(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) blockHash := "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8" - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == blockHash })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() @@ -940,18 +819,18 @@ func TestGetBlockByNumberForceLookupMismatchedBlockType(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(1002), BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21", ParentHash: "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(1002), BlockHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", @@ -980,9 +859,9 @@ func TestGetBlockByHashCached(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df" - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(1003), BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", @@ -1005,7 +884,7 @@ func TestGetBlockNotFound(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df" })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Once() @@ -1062,7 +941,7 @@ func TestCheckReceiptNotFound(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) - mca.On("GetReceipt", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")) + mca.On("TransactionReceipt", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")) txHash := "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" pending := &pendingItem{ @@ -1081,7 +960,7 @@ func TestCheckReceiptFail(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) - mca.On("GetReceipt", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + mca.On("TransactionReceipt", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) txHash := "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" pending := &pendingItem{ @@ -1100,12 +979,12 @@ func TestCheckReceiptWalkFail(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) - mca.On("GetReceipt", mock.Anything, mock.Anything).Return(&ffcapi.GetReceiptResponse{ + mca.On("TransactionReceipt", mock.Anything, mock.Anything).Return(&ffcapi.TransactionReceiptResponse{ BlockNumber: fftypes.NewFFBigInt(12345), BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", TransactionIndex: fftypes.NewFFBigInt(10), }, ffcapi.ErrorReason(""), nil) - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 12346 })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) @@ -1129,7 +1008,7 @@ func TestStaleReceiptCheck(t *testing.T) { txHash := "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" pending := &pendingItem{ pType: pendingTypeTransaction, - lastReceiptcheck: time.Now().Add(-1 * time.Hour), + lastReceiptCheck: time.Now().Add(-1 * time.Hour), transactionHash: txHash, } bcm.pending[pending.getKey()] = pending diff --git a/internal/manager/api_test.go b/internal/manager/api_test.go deleted file mode 100644 index 0a2f9041..00000000 --- a/internal/manager/api_test.go +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 manager - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" - "testing" - - "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/ffcapi" - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" - "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -const sampleSendTX = `{ - "headers": { - "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", - "type": "SendTransaction" - }, - "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", - "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", - "gas": 1000000, - "method": { - "inputs": [ - { - "internalType":" uint256", - "name": "x", - "type": "uint256" - } - ], - "name":"set", - "outputs":[], - "stateMutability":"nonpayable", - "type":"function" - }, - "params": [ - { - "value": 4276993775, - "type": "uint256" - } - ] -}` - -func testFFCAPIHandler(t *testing.T, fn func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int)) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var reqHeader ffcapi.RequestBase - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - err = json.Unmarshal(b, &reqHeader) - assert.NoError(t, err) - - assert.NotNil(t, reqHeader.FFCAPI.RequestID) - assert.Equal(t, ffcapi.VersionCurrent, reqHeader.FFCAPI.Version) - assert.Equal(t, ffcapi.Variant("evm"), reqHeader.FFCAPI.Variant) - - res, status := fn(reqHeader.FFCAPI.RequestType, b) - - b, err = json.Marshal(res) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - w.Write(b) - - } -} - -func TestSendTransactionE2E(t *testing.T) { - - txSent := make(chan struct{}) - - url, m, cancel := newTestManager(t, - testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) { - status = 200 - switch reqType { - - case ffcapi.RequestTypeGetNextNonce: - var nonceReq ffcapi.GetNextNonceRequest - err := json.Unmarshal(b, &nonceReq) - assert.NoError(t, err) - assert.Equal(t, "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", nonceReq.Signer) - res = ffcapi.GetNextNonceResponse{ - Nonce: fftypes.NewFFBigInt(12345), - } - - case ffcapi.RequestTypePrepareTransaction: - var prepTX ffcapi.PrepareTransactionRequest - err := json.Unmarshal(b, &prepTX) - assert.NoError(t, err) - assert.Equal(t, "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", prepTX.From) - assert.Equal(t, "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", prepTX.To) - assert.Equal(t, uint64(1000000), prepTX.Gas.Uint64()) - assert.Equal(t, "set", prepTX.Method.JSONObject().GetString("name")) - assert.Len(t, prepTX.Params, 1) - assert.Equal(t, "4276993775", prepTX.Params[0].JSONObject().GetString("value")) - res = ffcapi.PrepareTransactionResponse{ - TransactionData: "RAW_UNSIGNED_BYTES", - Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation - } - - case ffcapi.RequestTypeSendTransaction: - var sendTX ffcapi.SendTransactionRequest - err := json.Unmarshal(b, &sendTX) - assert.NoError(t, err) - assert.Equal(t, "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", sendTX.From) - assert.Equal(t, "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", sendTX.To) - assert.Equal(t, uint64(2000000), sendTX.Gas.Uint64()) - assert.Equal(t, `223344556677`, sendTX.GasPrice.String()) - assert.Equal(t, "RAW_UNSIGNED_BYTES", sendTX.TransactionData) - res = ffcapi.SendTransactionResponse{ - TransactionHash: "0x106215b9c0c9372e3f541beff0cdc3cd061a26f69f3808e28fd139a1abc9d345", - } - - // We're at end of job for this test - close(txSent) - - default: - assert.Fail(t, fmt.Sprintf("Unexpected type: %s", reqType)) - status = 500 - } - return res, status - }), - func(w http.ResponseWriter, r *http.Request) { - - }, - ) - defer cancel() - - mc := m.confirmations.(*confirmationsmocks.Manager) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - return n.NotificationType == confirmations.NewTransaction - })).Return(nil) - - m.Start() - - req := strings.NewReader(sampleSendTX) - res, err := resty.New().R(). - SetBody(req). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 200, res.StatusCode()) - - <-txSent - -} - -func TestSendInvalidRequestNoHeaders(t *testing.T) { - - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - m.Start() - - req := strings.NewReader(`{ - "noHeaders": true - }`) - var errRes fftypes.RESTError - res, err := resty.New().R(). - SetBody(req). - SetError(&errRes). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 400, res.StatusCode()) - assert.Regexp(t, "FF21022", errRes.Error) -} - -func TestSendInvalidRequestWrongType(t *testing.T) { - - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - m.Start() - - req := strings.NewReader(`{ - "headers": { - "id": "ns1:` + fftypes.NewUUID().String() + `", - "type": "wrong" - } - }`) - var errRes fftypes.RESTError - res, err := resty.New().R(). - SetBody(req). - SetError(&errRes). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 400, res.StatusCode()) - assert.Regexp(t, "FF21023", errRes.Error) -} - -func TestSendInvalidRequestFail(t *testing.T) { - - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - backendError := &fftypes.RESTError{Error: "pop"} - b, err := json.Marshal(&backendError) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(503) - w.Write(b) - }, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - m.Start() - - req := strings.NewReader(`{ - "headers": { - "id": "ns1:` + fftypes.NewUUID().String() + `", - "type": "SendTransaction" - } - }`) - var errRes fftypes.RESTError - res, err := resty.New().R(). - SetBody(req). - SetError(&errRes). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 500, res.StatusCode()) - assert.Regexp(t, "FF00157", errRes.Error) -} - -func TestSendTransactionPrepareFail(t *testing.T) { - - url, m, cancel := newTestManager(t, - testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) { - status = 200 - switch reqType { - case ffcapi.RequestTypeGetNextNonce: - res = ffcapi.GetNextNonceResponse{ - Nonce: fftypes.NewFFBigInt(12345), - } - - case ffcapi.RequestTypePrepareTransaction: - res = ffcapi.ErrorResponse{ - Error: "pop", - } - status = 500 - } - return res, status - }), - func(w http.ResponseWriter, r *http.Request) { - - }, - ) - defer cancel() - - m.Start() - - req := strings.NewReader(sampleSendTX) - res, err := resty.New().R(). - SetBody(req). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 500, res.StatusCode()) - -} - -func TestSendTransactionUpdateFireFlyFail(t *testing.T) { - - url, m, cancel := newTestManager(t, - testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) { - status = 200 - switch reqType { - case ffcapi.RequestTypeGetNextNonce: - res = ffcapi.GetNextNonceResponse{ - Nonce: fftypes.NewFFBigInt(12345), - } - - case ffcapi.RequestTypePrepareTransaction: - res = ffcapi.PrepareTransactionResponse{} - status = 200 - } - return res, status - }), - func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPatch { - errRes := fftypes.RESTError{Error: "pop"} - b, err := json.Marshal(&errRes) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - w.Write(b) - } else { - w.WriteHeader(200) - } - }, - ) - defer cancel() - - m.Start() - - req := strings.NewReader(sampleSendTX) - res, err := resty.New().R(). - SetBody(req). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 500, res.StatusCode()) - -} diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 48a25959..92a1aaa9 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -18,7 +18,6 @@ package tmconfig import ( "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/ffresty" "github.com/hyperledger/firefly-common/pkg/httpserver" "github.com/hyperledger/firefly-common/pkg/wsclient" "github.com/hyperledger/firefly/pkg/core" @@ -60,8 +59,6 @@ var ( PolicyEngineName = ffc("policyengine.name") ) -var ConnectorPrefix config.Prefix - var FFCorePrefix config.Prefix var APIPrefix config.Prefix @@ -93,9 +90,6 @@ func setDefaults() { func Reset() { config.RootConfigReset(setDefaults) - ConnectorPrefix = config.NewPluginConfig("connector") - ffresty.InitPrefix(ConnectorPrefix) - FFCorePrefix = config.NewPluginConfig("ffcore") wsclient.InitPrefix(FFCorePrefix) FFCorePrefix.SetDefault(wsclient.WSConfigKeyPath, "/admin/ws") diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index 64d37f5b..caae49dc 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -5,7 +5,7 @@ package ffcapimocks import ( context "context" - ffcapi "github.com/hyperledger/firefly-common/pkg/ffcapi" + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" mock "github.com/stretchr/testify/mock" ) @@ -14,28 +14,28 @@ type API struct { mock.Mock } -// CreateBlockListener provides a mock function with given fields: ctx, req -func (_m *API) CreateBlockListener(ctx context.Context, req *ffcapi.CreateBlockListenerRequest) (*ffcapi.CreateBlockListenerResponse, ffcapi.ErrorReason, error) { +// BlockInfoByHash provides a mock function with given fields: ctx, req +func (_m *API) BlockInfoByHash(ctx context.Context, req *ffcapi.BlockInfoByHashRequest) (*ffcapi.BlockInfoByHashResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.CreateBlockListenerResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.CreateBlockListenerRequest) *ffcapi.CreateBlockListenerResponse); ok { + var r0 *ffcapi.BlockInfoByHashResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.BlockInfoByHashRequest) *ffcapi.BlockInfoByHashResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.CreateBlockListenerResponse) + r0 = ret.Get(0).(*ffcapi.BlockInfoByHashResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.CreateBlockListenerRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.BlockInfoByHashRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.CreateBlockListenerRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.BlockInfoByHashRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -44,28 +44,28 @@ func (_m *API) CreateBlockListener(ctx context.Context, req *ffcapi.CreateBlockL return r0, r1, r2 } -// ExecQuery provides a mock function with given fields: ctx, req -func (_m *API) ExecQuery(ctx context.Context, req *ffcapi.ExecQueryRequest) (*ffcapi.ExecQueryResponse, ffcapi.ErrorReason, error) { +// BlockInfoByNumber provides a mock function with given fields: ctx, req +func (_m *API) BlockInfoByNumber(ctx context.Context, req *ffcapi.BlockInfoByNumberRequest) (*ffcapi.BlockInfoByNumberResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.ExecQueryResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.ExecQueryRequest) *ffcapi.ExecQueryResponse); ok { + var r0 *ffcapi.BlockInfoByNumberResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.BlockInfoByNumberRequest) *ffcapi.BlockInfoByNumberResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.ExecQueryResponse) + r0 = ret.Get(0).(*ffcapi.BlockInfoByNumberResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.ExecQueryRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.BlockInfoByNumberRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.ExecQueryRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.BlockInfoByNumberRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -74,28 +74,28 @@ func (_m *API) ExecQuery(ctx context.Context, req *ffcapi.ExecQueryRequest) (*ff return r0, r1, r2 } -// GetBlockInfoByHash provides a mock function with given fields: ctx, req -func (_m *API) GetBlockInfoByHash(ctx context.Context, req *ffcapi.GetBlockInfoByHashRequest) (*ffcapi.GetBlockInfoByHashResponse, ffcapi.ErrorReason, error) { +// EventListenerAdd provides a mock function with given fields: ctx, req +func (_m *API) EventListenerAdd(ctx context.Context, req *ffcapi.EventListenerAddRequest) (*ffcapi.EventListenerAddResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetBlockInfoByHashResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetBlockInfoByHashRequest) *ffcapi.GetBlockInfoByHashResponse); ok { + var r0 *ffcapi.EventListenerAddResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventListenerAddRequest) *ffcapi.EventListenerAddResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetBlockInfoByHashResponse) + r0 = ret.Get(0).(*ffcapi.EventListenerAddResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetBlockInfoByHashRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventListenerAddRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetBlockInfoByHashRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventListenerAddRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -104,28 +104,28 @@ func (_m *API) GetBlockInfoByHash(ctx context.Context, req *ffcapi.GetBlockInfoB return r0, r1, r2 } -// GetBlockInfoByNumber provides a mock function with given fields: ctx, req -func (_m *API) GetBlockInfoByNumber(ctx context.Context, req *ffcapi.GetBlockInfoByNumberRequest) (*ffcapi.GetBlockInfoByNumberResponse, ffcapi.ErrorReason, error) { +// EventListenerRemove provides a mock function with given fields: ctx, req +func (_m *API) EventListenerRemove(ctx context.Context, req *ffcapi.EventListenerRemoveRequest) (*ffcapi.EventListenerRemoveResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetBlockInfoByNumberResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetBlockInfoByNumberRequest) *ffcapi.GetBlockInfoByNumberResponse); ok { + var r0 *ffcapi.EventListenerRemoveResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventListenerRemoveRequest) *ffcapi.EventListenerRemoveResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetBlockInfoByNumberResponse) + r0 = ret.Get(0).(*ffcapi.EventListenerRemoveResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetBlockInfoByNumberRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventListenerRemoveRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetBlockInfoByNumberRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventListenerRemoveRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -134,28 +134,44 @@ func (_m *API) GetBlockInfoByNumber(ctx context.Context, req *ffcapi.GetBlockInf return r0, r1, r2 } -// GetGasPrice provides a mock function with given fields: ctx, req -func (_m *API) GetGasPrice(ctx context.Context, req *ffcapi.GetGasPriceRequest) (*ffcapi.GetGasPriceResponse, ffcapi.ErrorReason, error) { +// Events provides a mock function with given fields: +func (_m *API) Events() <-chan *ffcapi.Event { + ret := _m.Called() + + var r0 <-chan *ffcapi.Event + if rf, ok := ret.Get(0).(func() <-chan *ffcapi.Event); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan *ffcapi.Event) + } + } + + return r0 +} + +// GasPriceEstimate provides a mock function with given fields: ctx, req +func (_m *API) GasPriceEstimate(ctx context.Context, req *ffcapi.GasPriceEstimateRequest) (*ffcapi.GasPriceEstimateResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetGasPriceResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetGasPriceRequest) *ffcapi.GetGasPriceResponse); ok { + var r0 *ffcapi.GasPriceEstimateResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GasPriceEstimateRequest) *ffcapi.GasPriceEstimateResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetGasPriceResponse) + r0 = ret.Get(0).(*ffcapi.GasPriceEstimateResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetGasPriceRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GasPriceEstimateRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetGasPriceRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GasPriceEstimateRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -164,28 +180,28 @@ func (_m *API) GetGasPrice(ctx context.Context, req *ffcapi.GetGasPriceRequest) return r0, r1, r2 } -// GetNewBlockHashes provides a mock function with given fields: ctx, req -func (_m *API) GetNewBlockHashes(ctx context.Context, req *ffcapi.GetNewBlockHashesRequest) (*ffcapi.GetNewBlockHashesResponse, ffcapi.ErrorReason, error) { +// MethodCall provides a mock function with given fields: ctx, req +func (_m *API) MethodCall(ctx context.Context, req *ffcapi.MethodCallRequest) (*ffcapi.MethodCallResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetNewBlockHashesResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetNewBlockHashesRequest) *ffcapi.GetNewBlockHashesResponse); ok { + var r0 *ffcapi.MethodCallResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.MethodCallRequest) *ffcapi.MethodCallResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetNewBlockHashesResponse) + r0 = ret.Get(0).(*ffcapi.MethodCallResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetNewBlockHashesRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.MethodCallRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetNewBlockHashesRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.MethodCallRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -194,28 +210,44 @@ func (_m *API) GetNewBlockHashes(ctx context.Context, req *ffcapi.GetNewBlockHas return r0, r1, r2 } -// GetNextNonce provides a mock function with given fields: ctx, req -func (_m *API) GetNextNonce(ctx context.Context, req *ffcapi.GetNextNonceRequest) (*ffcapi.GetNextNonceResponse, ffcapi.ErrorReason, error) { +// NewBlockHashes provides a mock function with given fields: +func (_m *API) NewBlockHashes() <-chan *ffcapi.BlockHashEvent { + ret := _m.Called() + + var r0 <-chan *ffcapi.BlockHashEvent + if rf, ok := ret.Get(0).(func() <-chan *ffcapi.BlockHashEvent); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan *ffcapi.BlockHashEvent) + } + } + + return r0 +} + +// NextNonceForSigner provides a mock function with given fields: ctx, req +func (_m *API) NextNonceForSigner(ctx context.Context, req *ffcapi.NextNonceForSignerRequest) (*ffcapi.NextNonceForSignerResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetNextNonceResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetNextNonceRequest) *ffcapi.GetNextNonceResponse); ok { + var r0 *ffcapi.NextNonceForSignerResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.NextNonceForSignerRequest) *ffcapi.NextNonceForSignerResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetNextNonceResponse) + r0 = ret.Get(0).(*ffcapi.NextNonceForSignerResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetNextNonceRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.NextNonceForSignerRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetNextNonceRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.NextNonceForSignerRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -224,28 +256,28 @@ func (_m *API) GetNextNonce(ctx context.Context, req *ffcapi.GetNextNonceRequest return r0, r1, r2 } -// GetReceipt provides a mock function with given fields: ctx, req -func (_m *API) GetReceipt(ctx context.Context, req *ffcapi.GetReceiptRequest) (*ffcapi.GetReceiptResponse, ffcapi.ErrorReason, error) { +// TransactionPrepare provides a mock function with given fields: ctx, req +func (_m *API) TransactionPrepare(ctx context.Context, req *ffcapi.TransactionPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetReceiptResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetReceiptRequest) *ffcapi.GetReceiptResponse); ok { + var r0 *ffcapi.TransactionPrepareResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.TransactionPrepareRequest) *ffcapi.TransactionPrepareResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetReceiptResponse) + r0 = ret.Get(0).(*ffcapi.TransactionPrepareResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetReceiptRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.TransactionPrepareRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetReceiptRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.TransactionPrepareRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -254,28 +286,28 @@ func (_m *API) GetReceipt(ctx context.Context, req *ffcapi.GetReceiptRequest) (* return r0, r1, r2 } -// PrepareTransaction provides a mock function with given fields: ctx, req -func (_m *API) PrepareTransaction(ctx context.Context, req *ffcapi.PrepareTransactionRequest) (*ffcapi.PrepareTransactionResponse, ffcapi.ErrorReason, error) { +// TransactionReceipt provides a mock function with given fields: ctx, req +func (_m *API) TransactionReceipt(ctx context.Context, req *ffcapi.TransactionReceiptRequest) (*ffcapi.TransactionReceiptResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.PrepareTransactionResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.PrepareTransactionRequest) *ffcapi.PrepareTransactionResponse); ok { + var r0 *ffcapi.TransactionReceiptResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.TransactionReceiptRequest) *ffcapi.TransactionReceiptResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.PrepareTransactionResponse) + r0 = ret.Get(0).(*ffcapi.TransactionReceiptResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.PrepareTransactionRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.TransactionReceiptRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.PrepareTransactionRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.TransactionReceiptRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -284,28 +316,28 @@ func (_m *API) PrepareTransaction(ctx context.Context, req *ffcapi.PrepareTransa return r0, r1, r2 } -// SendTransaction provides a mock function with given fields: ctx, req -func (_m *API) SendTransaction(ctx context.Context, req *ffcapi.SendTransactionRequest) (*ffcapi.SendTransactionResponse, ffcapi.ErrorReason, error) { +// TransactionSend provides a mock function with given fields: ctx, req +func (_m *API) TransactionSend(ctx context.Context, req *ffcapi.TransactionSendRequest) (*ffcapi.TransactionSendResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.SendTransactionResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.SendTransactionRequest) *ffcapi.SendTransactionResponse); ok { + var r0 *ffcapi.TransactionSendResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.TransactionSendRequest) *ffcapi.TransactionSendResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.SendTransactionResponse) + r0 = ret.Get(0).(*ffcapi.TransactionSendResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.SendTransactionRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.TransactionSendRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.SendTransactionRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.TransactionSendRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) diff --git a/mocks/managermocks/manager.go b/mocks/managermocks/manager.go deleted file mode 100644 index e4cfd0b3..00000000 --- a/mocks/managermocks/manager.go +++ /dev/null @@ -1,43 +0,0 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. - -package managermocks - -import mock "github.com/stretchr/testify/mock" - -// Manager is an autogenerated mock type for the Manager type -type Manager struct { - mock.Mock -} - -// Start provides a mock function with given fields: -func (_m *Manager) Start() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Stop provides a mock function with given fields: -func (_m *Manager) Stop() { - _m.Called() -} - -// WaitStop provides a mock function with given fields: -func (_m *Manager) WaitStop() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/mocks/policyenginemocks/policy_engine.go b/mocks/policyenginemocks/policy_engine.go index 26fcd79e..bccac0d4 100644 --- a/mocks/policyenginemocks/policy_engine.go +++ b/mocks/policyenginemocks/policy_engine.go @@ -5,10 +5,10 @@ package policyenginemocks import ( context "context" - ffcapi "github.com/hyperledger/firefly-common/pkg/ffcapi" - fftm "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" - + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" mock "github.com/stretchr/testify/mock" + + policyengine "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) // PolicyEngine is an autogenerated mock type for the PolicyEngine type @@ -17,25 +17,25 @@ type PolicyEngine struct { } // Execute provides a mock function with given fields: ctx, cAPI, mtx -func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *fftm.ManagedTXOutput) (bool, ffcapi.ErrorReason, error) { +func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *policyengine.ManagedTXOutput) (bool, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, cAPI, mtx) var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *fftm.ManagedTXOutput) bool); ok { + if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) bool); ok { r0 = rf(ctx, cAPI, mtx) } else { r0 = ret.Get(0).(bool) } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *fftm.ManagedTXOutput) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) ffcapi.ErrorReason); ok { r1 = rf(ctx, cAPI, mtx) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, ffcapi.API, *fftm.ManagedTXOutput) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) error); ok { r2 = rf(ctx, cAPI, mtx) } else { r2 = ret.Error(2) diff --git a/mocks/policyenginesmocks/policy_engine.go b/mocks/policyenginesmocks/policy_engine.go new file mode 100644 index 00000000..7943b3bf --- /dev/null +++ b/mocks/policyenginesmocks/policy_engine.go @@ -0,0 +1,45 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package policyenginemocks + +import ( + context "context" + + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + mock "github.com/stretchr/testify/mock" + + policyengines "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" +) + +// PolicyEngine is an autogenerated mock type for the PolicyEngine type +type PolicyEngine struct { + mock.Mock +} + +// Execute provides a mock function with given fields: ctx, cAPI, mtx +func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *policyengine.ManagedTXOutput) (bool, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, cAPI, mtx) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) bool); ok { + r0 = rf(ctx, cAPI, mtx) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) ffcapi.ErrorReason); ok { + r1 = rf(ctx, cAPI, mtx) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) error); ok { + r2 = rf(ctx, cAPI, mtx) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go new file mode 100644 index 00000000..dc90071c --- /dev/null +++ b/pkg/ffcapi/api.go @@ -0,0 +1,130 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +// API is the interface to the blockchain specific connector, from the FFTM server and policy engine. +// +// The functions follow a consistent pattern of request/response objects, to allow extensibility of the +// inputs/outputs with minimal code change to existing connector implementations. +type API interface { + + // BlockInfoByHash gets block information using the hash of the block + BlockInfoByHash(ctx context.Context, req *BlockInfoByHashRequest) (*BlockInfoByHashResponse, ErrorReason, error) + + // BlockInfoByNumber gets block information from the specified position (block number/index) in the canonical chain currently known to the local node + BlockInfoByNumber(ctx context.Context, req *BlockInfoByNumberRequest) (*BlockInfoByNumberResponse, ErrorReason, error) + + // NextNonceForSigner is used when there are no outstanding transactions for a given signing identity, to determine the next nonce to use for submission of a transaction + NextNonceForSigner(ctx context.Context, req *NextNonceForSignerRequest) (*NextNonceForSignerResponse, ErrorReason, error) + + // GasPriceEstimate provides a blockchain specific gas price estimate + GasPriceEstimate(ctx context.Context, req *GasPriceEstimateRequest) (*GasPriceEstimateResponse, ErrorReason, error) + + // MethodCall executes a method on a blockchain smart contract, which might execute Smart Contract code, but does not affect the blockchain state. + MethodCall(ctx context.Context, req *MethodCallRequest) (*MethodCallResponse, ErrorReason, error) + + // TransactionReceipt queries to see if a receipt is available for a given transaction hash + TransactionReceipt(ctx context.Context, req *TransactionReceiptRequest) (*TransactionReceiptResponse, ErrorReason, error) + + // TransactionPrepare validates transaction inputs against the supplied schema/ABI and performs any binary serialization required (prior to signing) to encode a transaction from JSON into the native blockchain format + TransactionPrepare(ctx context.Context, req *TransactionPrepareRequest) (*TransactionPrepareResponse, ErrorReason, error) + + // TransactionSend combines a previously prepared encoded transaction, with a current gas price, and submits it to the transaction pool of the blockchain for mining + TransactionSend(ctx context.Context, req *TransactionSendRequest) (*TransactionSendResponse, ErrorReason, error) + + // EventListenerAdd begins/resumes listening on set of events that must be consistently ordered. Blockchain specific signatures of the events are included, along with initial conditions (initial block number etc.), and the last stored checkpoint (if any) + EventListenerAdd(ctx context.Context, req *EventListenerAddRequest) (*EventListenerAddResponse, ErrorReason, error) + + // EventListenerRemove ends listening on a set of events previous started + EventListenerRemove(ctx context.Context, req *EventListenerRemoveRequest) (*EventListenerRemoveResponse, ErrorReason, error) + + // NewBlockHashes should dynamically push the hashes of all new blocks detected from the blockchain, if confirmations are supported + NewBlockHashes() <-chan *BlockHashEvent + + // Events is the channel over which all events are delivered + Events() <-chan *Event +} + +type BlockHashEvent struct { + BlockHashes []string `json:"blockHash"` // zero or more hashes (can be nil) + GapPotential bool `json:"gapPotential,omitempty"` // when true, the caller cannot be sure if blocks have been missed (use on reconnect of a websocket for example) +} + +// Event is a blockchain event that matches one of the started listeners. +// The implementation is responsible for ensuring all events on a listener are +// ordered on to this channel in the exact sequence from the blockchain. +type Event struct { + ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this event + Data *fftypes.JSONAny `json:"data"` // the JSON data to deliver for this event + ProtocolID string `json:"protocolId"` // a protocol identifier for the event, that is string sortable per https://hyperledger.github.io/firefly/reference/types/blockchainevent.html#protocol-id + Info *fftypes.JSONAny `json:"info"` // additional blockchain specific information +} + +// ErrorReason are a set of standard error conditions that a blockchain connector can return +// from execution, that affect the action of the transaction manager to the response. +// It is important that error mapping is performed for each of these classification +type ErrorReason string + +const ( + // ErrorReasonInvalidInputs transaction inputs could not be parsed by the connector according to the interface (nothing was sent to the blockchain) + ErrorReasonInvalidInputs ErrorReason = "invalid_inputs" + // ErrorReasonTransactionReverted on-chain execution (only expected to be returned when the connector is doing gas estimation, or executing a query) + ErrorReasonTransactionReverted ErrorReason = "transaction_reverted" + // ErrorReasonNonceTooLow on transaction submission, if the nonce has already been used for a transaction that has made it into a block on the canonical chain known to the local node + ErrorReasonNonceTooLow ErrorReason = "nonce_too_low" + // ErrorReasonTransactionUnderpriced if the transaction is rejected due to too low gas price. Either because it was too low according to the minimum configured on the node, or because it's a rescue transaction without a price bump. + ErrorReasonTransactionUnderpriced ErrorReason = "transaction_underpriced" + // ErrorReasonInsufficientFunds if the transaction is rejected due to not having enough of the underlying network coin (ether etc.) in your wallet + ErrorReasonInsufficientFunds ErrorReason = "insufficient_funds" + // ErrorReasonNotFound if the requested object (block/receipt etc.) was not found + ErrorReasonNotFound ErrorReason = "not_found" + // ErrorKnownTransaction if the exact transaction is already known + ErrorKnownTransaction ErrorReason = "known_transaction" +) + +// TransactionInput is a standardized set of parameters that describe a transaction submission to a blockchain. +// For convenience, ths structure is compatible with the EthConnect `TransactionSend` structure, for the subset of usage made by FireFly core / Tokens connectors. +// - Numeric values such as nonce/gas/gasPrice, are all passed as string encoded Base 10 integers +// - From/To are passed as strings, and are pass-through for FFTM from the values it receives from FireFly core after signing key resolution +// - The interface is a structure describing the method to invoke. The `variant` in the header tells you how to decode it. For variant=evm it will be an ABI method definition +// - The supplied value is passed through for each input parameter. It could be any JSON type (simple number/boolean/string, or complex object/array). The blockchain connection is responsible for serializing these according to the rules in the interface. +type TransactionInput struct { + TransactionHeaders + Method fftypes.JSONAny `json:"method"` + Params []*fftypes.JSONAny `json:"params"` +} + +type TransactionHeaders struct { + From string `json:"from"` + To string `json:"to,omitempty"` + Nonce *fftypes.FFBigInt `json:"nonce,omitempty"` + Gas *fftypes.FFBigInt `json:"gas,omitempty"` + Value *fftypes.FFBigInt `json:"value,omitempty"` +} + +type BlockInfo struct { + BlockNumber *fftypes.FFBigInt `json:"blockNumber"` + BlockHash string `json:"blockHash"` + ParentHash string `json:"parentHash"` + TransactionHashes []string `json:"transactionHashes"` +} diff --git a/pkg/ffcapi/block_info_by_hash.go b/pkg/ffcapi/block_info_by_hash.go new file mode 100644 index 00000000..7adb02b8 --- /dev/null +++ b/pkg/ffcapi/block_info_by_hash.go @@ -0,0 +1,25 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +type BlockInfoByHashRequest struct { + BlockHash string `json:"blockHash"` +} + +type BlockInfoByHashResponse struct { + BlockInfo +} diff --git a/cmd/config_test.go b/pkg/ffcapi/block_info_by_number.go similarity index 74% rename from cmd/config_test.go rename to pkg/ffcapi/block_info_by_number.go index 8ccdd1bf..3afde7a3 100644 --- a/cmd/config_test.go +++ b/pkg/ffcapi/block_info_by_number.go @@ -14,17 +14,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cmd +package ffcapi import ( - "testing" - - "github.com/stretchr/testify/assert" + "github.com/hyperledger/firefly-common/pkg/fftypes" ) -func TestConfigMarkdown(t *testing.T) { - rootCmd.SetArgs([]string{"docs"}) - defer rootCmd.SetArgs([]string{}) - err := rootCmd.Execute() - assert.NoError(t, err) +type BlockInfoByNumberRequest struct { + BlockNumber *fftypes.FFBigInt `json:"blockNumber"` +} + +type BlockInfoByNumberResponse struct { + BlockInfo } diff --git a/cmd/config.go b/pkg/ffcapi/event_listener_add.go similarity index 57% rename from cmd/config.go rename to pkg/ffcapi/event_listener_add.go index 7cae3ea1..b297229d 100644 --- a/cmd/config.go +++ b/pkg/ffcapi/event_listener_add.go @@ -14,27 +14,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cmd +package ffcapi import ( - "context" - "fmt" - - "github.com/hyperledger/firefly-common/pkg/config" - "github.com/spf13/cobra" + "github.com/hyperledger/firefly-common/pkg/fftypes" ) -func configCommand() *cobra.Command { - versionCmd := &cobra.Command{ - Use: "docs", - Short: "Prints the config info as markdown", - Long: "", - RunE: func(cmd *cobra.Command, args []string) error { - initConfig() - b, err := config.GenerateConfigMarkdown(context.Background(), config.GetKnownKeys()) - fmt.Println(string(b)) - return err - }, - } - return versionCmd +type EventListenerAddRequest struct { + ID *fftypes.UUID `json:"uuid"` + Events []*fftypes.JSONAny `json:"events"` + Options *fftypes.JSONAny `json:"options"` + Checkpoint *fftypes.JSONAny `json:"checkpoint"` +} + +type EventListenerAddResponse struct { + Events chan<- Event } diff --git a/fftm/main.go b/pkg/ffcapi/event_listener_remove.go similarity index 76% rename from fftm/main.go rename to pkg/ffcapi/event_listener_remove.go index 80b54974..d53550c7 100644 --- a/fftm/main.go +++ b/pkg/ffcapi/event_listener_remove.go @@ -14,19 +14,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package ffcapi import ( - "fmt" - "os" - - "github.com/hyperledger/firefly-transaction-manager/cmd" + "github.com/hyperledger/firefly-common/pkg/fftypes" ) -func main() { - if err := cmd.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - os.Exit(1) - } - os.Exit(0) +type EventListenerRemoveRequest struct { + ID *fftypes.UUID `json:"uuid"` +} + +type EventListenerRemoveResponse struct { } diff --git a/pkg/ffcapi/gas_price_estimate.go b/pkg/ffcapi/gas_price_estimate.go new file mode 100644 index 00000000..06b7d59d --- /dev/null +++ b/pkg/ffcapi/gas_price_estimate.go @@ -0,0 +1,26 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import "github.com/hyperledger/firefly-common/pkg/fftypes" + +type GasPriceEstimateRequest struct { +} + +type GasPriceEstimateResponse struct { + GasPrice *fftypes.JSONAny `json:"gasPrice"` +} diff --git a/pkg/ffcapi/method_call.go b/pkg/ffcapi/method_call.go new file mode 100644 index 00000000..cd3cc12c --- /dev/null +++ b/pkg/ffcapi/method_call.go @@ -0,0 +1,36 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +// MethodCallRequest requests execution of a smart contract method in order to either: +// 1) Query state +// 2) Attempt to extract the revert reason from an on-chain failure to execute a transaction +// +// See the list of standard error reasons that should be returned for situations that can be +// detected by the back-end connector. +type MethodCallRequest struct { + TransactionInput + BlockNumber *fftypes.FFBigInt `json:"blockNumber,omitempty"` +} + +type MethodCallResponse struct { + Outputs *fftypes.JSONAny `json:"outputs"` +} diff --git a/cmd/config_docs_generate_test.go b/pkg/ffcapi/next_nonce_for_signer.go similarity index 53% rename from cmd/config_docs_generate_test.go rename to pkg/ffcapi/next_nonce_for_signer.go index 5c7098cc..9c3181e5 100644 --- a/cmd/config_docs_generate_test.go +++ b/pkg/ffcapi/next_nonce_for_signer.go @@ -14,30 +14,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build docs -// +build docs - -package cmd +package ffcapi import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/hyperledger/firefly-common/pkg/config" - "github.com/stretchr/testify/assert" + "github.com/hyperledger/firefly-common/pkg/fftypes" ) -func TestGenerateConfigDocs(t *testing.T) { - // Initialize config of all plugins - initConfig() - f, err := os.Create(filepath.Join("..", "config.md")) - assert.NoError(t, err) - generatedConfig, err := config.GenerateConfigMarkdown(context.Background(), config.GetKnownKeys()) - assert.NoError(t, err) - _, err = f.Write(generatedConfig) - assert.NoError(t, err) - err = f.Close() - assert.NoError(t, err) +// NextNonceForSignerRequest used to do a query for the next nonce to use for a +// given signing identity. This is only used when there are no pending +// operations outstanding for this signer known to the transaction manager. +type NextNonceForSignerRequest struct { + Signer string `json:"signer"` +} + +type NextNonceForSignerResponse struct { + Nonce *fftypes.FFBigInt `json:"nonce"` } diff --git a/pkg/ffcapi/transaction_prepare.go b/pkg/ffcapi/transaction_prepare.go new file mode 100644 index 00000000..1dfef35f --- /dev/null +++ b/pkg/ffcapi/transaction_prepare.go @@ -0,0 +1,48 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +// TransactionPrepareRequest is used to prepare a set of JSON formatted developer friendly +// inputs, into a raw transaction ready for submission to the blockchain. +// +// The connector is responsible for encoding the transaction ready for sumission, +// and returning the hash for the transaction as well as a string serialization of +// the pre-signed raw transaction in a format of its own choosing (hex etc.). +// The hash is expected to be a function of: +// - the method signature +// - the signing identity +// - the nonce +// - the particular blockchain the transaction is submitted to +// - the input parameters +// +// If "gas" is not supplied, the connector is expected to perform gas estimation +// prior to generating the payload. +// +// See the list of standard error reasons that should be returned for situations that can be +// detected by the back-end connector. +type TransactionPrepareRequest struct { + TransactionInput +} + +type TransactionPrepareResponse struct { + Gas *fftypes.FFBigInt `json:"gas"` + TransactionData string `json:"transactionData"` +} diff --git a/pkg/ffcapi/transaction_receipt.go b/pkg/ffcapi/transaction_receipt.go new file mode 100644 index 00000000..44ab8d87 --- /dev/null +++ b/pkg/ffcapi/transaction_receipt.go @@ -0,0 +1,33 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type TransactionReceiptRequest struct { + TransactionHash string `json:"transactionHash"` +} + +type TransactionReceiptResponse struct { + BlockNumber *fftypes.FFBigInt `json:"blockNumber"` + TransactionIndex *fftypes.FFBigInt `json:"transactionIndex"` + BlockHash string `json:"blockHash"` + Success bool `json:"success"` + ExtraInfo fftypes.JSONAny `json:"extraInfo"` +} diff --git a/pkg/ffcapi/transaction_send.go b/pkg/ffcapi/transaction_send.go new file mode 100644 index 00000000..c83f68df --- /dev/null +++ b/pkg/ffcapi/transaction_send.go @@ -0,0 +1,34 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +// TransactionSendRequest is used to send a transaction to the blockchain. +// The connector is responsible for adding it to the transaction pool of the blockchain, +// noting the transaction hash has already been calculated in the prepare step previously. +type TransactionSendRequest struct { + GasPrice *fftypes.JSONAny `json:"gasPrice,omitempty"` // can be a simple string/number, or a complex object - contract is between policy engine and blockchain connector + TransactionHeaders + TransactionData string `json:"transactionData"` +} + +type TransactionSendResponse struct { + TransactionHash string `json:"transactionHash"` +} diff --git a/internal/manager/api.go b/pkg/fftm/api.go similarity index 89% rename from internal/manager/api.go rename to pkg/fftm/api.go index de3435cb..6fbc76a4 100644 --- a/internal/manager/api.go +++ b/pkg/fftm/api.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "context" @@ -27,7 +27,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) func (m *manager) router() *mux.Router { @@ -40,7 +40,7 @@ func (m *manager) runAPIServer() { m.apiServer.ServeHTTP(m.ctx) } -func (m *manager) validateRequest(ctx context.Context, tReq *fftm.TransactionRequest) error { +func (m *manager) validateRequest(ctx context.Context, tReq *policyengine.TransactionRequest) error { if tReq == nil || tReq.Headers.ID == "" || tReq.Headers.Type == "" { log.L(ctx).Warnf("Invalid request: %+v", tReq) return i18n.NewError(ctx, tmmsgs.MsgErrorInvalidRequest) @@ -50,7 +50,7 @@ func (m *manager) validateRequest(ctx context.Context, tReq *fftm.TransactionReq func (m *manager) apiHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - var tReq *fftm.TransactionRequest + var tReq *policyengine.TransactionRequest statusCode := 200 err := json.NewDecoder(r.Body).Decode(&tReq) if err == nil { @@ -62,7 +62,7 @@ func (m *manager) apiHandler(w http.ResponseWriter, r *http.Request) { } else { ctx = log.WithLogField(ctx, "requestId", tReq.Headers.ID) switch tReq.Headers.Type { - case fftm.RequestTypeSendTransaction: + case policyengine.RequestTypeSendTransaction: resBody, err = m.sendManagedTransaction(ctx, tReq) default: err = i18n.NewError(ctx, tmmsgs.MsgUnsupportedRequestType, tReq.Headers.Type) diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go new file mode 100644 index 00000000..cc93fccb --- /dev/null +++ b/pkg/fftm/api_test.go @@ -0,0 +1,238 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const sampleSendTX = `{ + "headers": { + "id": "ns1:904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "SendTransaction" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "gas": 1000000, + "method": { + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + } + ], + "name":"set", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "params": [ + { + "value": 4276993775, + "type": "uint256" + } + ] +}` + +func TestSendTransactionE2E(t *testing.T) { + + txSent := make(chan struct{}) + + url, m, cancel := newTestManager(t, + func(w http.ResponseWriter, r *http.Request) {}, + ) + defer cancel() + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(12345), + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("TransactionPrepare", mock.Anything, mock.MatchedBy(func(prepTX *ffcapi.TransactionPrepareRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == prepTX.From && + "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771" == prepTX.To && + uint64(1000000) == prepTX.Gas.Uint64() && + "set" == prepTX.Method.JSONObject().GetString("name") && + 1 == len(prepTX.Params) && + "4276993775" == prepTX.Params[0].JSONObject().GetString("value") && + "4276993775" == prepTX.Params[0].JSONObject().GetString("value") + })).Return(&ffcapi.TransactionPrepareResponse{ + TransactionData: "RAW_UNSIGNED_BYTES", + Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("TransactionSend", mock.Anything, mock.MatchedBy(func(sendTX *ffcapi.TransactionSendRequest) bool { + matches := "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == sendTX.From && + "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771" == sendTX.To && + uint64(2000000) == sendTX.Gas.Uint64() && + `223344556677` == sendTX.GasPrice.String() && + "RAW_UNSIGNED_BYTES" == sendTX.TransactionData + if matches { + // We're at end of job for this test + close(txSent) + } + return matches + })).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: "0x106215b9c0c9372e3f541beff0cdc3cd061a26f69f3808e28fd139a1abc9d345", + }, ffcapi.ErrorReason(""), nil) + + mc := m.confirmations.(*confirmationsmocks.Manager) + mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { + return n.NotificationType == confirmations.NewTransaction + })).Return(nil) + + m.Start() + + req := strings.NewReader(sampleSendTX) + res, err := resty.New().R(). + SetBody(req). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + <-txSent + +} + +func TestSendInvalidRequestNoHeaders(t *testing.T) { + + url, m, cancel := newTestManager(t, + func(w http.ResponseWriter, r *http.Request) {}, + ) + defer cancel() + m.Start() + + req := strings.NewReader(`{ + "noHeaders": true + }`) + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetBody(req). + SetError(&errRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 400, res.StatusCode()) + assert.Regexp(t, "FF21022", errRes.Error) +} + +func TestSendInvalidRequestWrongType(t *testing.T) { + + url, m, cancel := newTestManager(t, + func(w http.ResponseWriter, r *http.Request) {}, + ) + defer cancel() + m.Start() + + req := strings.NewReader(`{ + "headers": { + "id": "ns1:` + fftypes.NewUUID().String() + `", + "type": "wrong" + } + }`) + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetBody(req). + SetError(&errRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 400, res.StatusCode()) + assert.Regexp(t, "FF21023", errRes.Error) +} + +func TestSendTransactionPrepareFail(t *testing.T) { + + url, m, cancel := newTestManager(t, + func(w http.ResponseWriter, r *http.Request) {}, + ) + defer cancel() + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(12345), + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + m.Start() + + req := strings.NewReader(sampleSendTX) + res, err := resty.New().R(). + SetBody(req). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 500, res.StatusCode()) + +} + +func TestSendTransactionUpdateFireFlyFail(t *testing.T) { + + url, m, cancel := newTestManager(t, + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPatch { + errRes := fftypes.RESTError{Error: "pop"} + b, err := json.Marshal(&errRes) + assert.NoError(t, err) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + w.Write(b) + } else { + w.WriteHeader(200) + } + }, + ) + defer cancel() + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(12345), + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{}, ffcapi.ErrorReason(""), nil) + + m.Start() + + req := strings.NewReader(sampleSendTX) + res, err := resty.New().R(). + SetBody(req). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 500, res.StatusCode()) + +} diff --git a/internal/manager/changelistener.go b/pkg/fftm/changelistener.go similarity index 99% rename from internal/manager/changelistener.go rename to pkg/fftm/changelistener.go index bb044a4f..6d9a9a45 100644 --- a/internal/manager/changelistener.go +++ b/pkg/fftm/changelistener.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "context" diff --git a/internal/manager/changelistener_test.go b/pkg/fftm/changelistener_test.go similarity index 95% rename from internal/manager/changelistener_test.go rename to pkg/fftm/changelistener_test.go index 34379914..9460f0ca 100644 --- a/internal/manager/changelistener_test.go +++ b/pkg/fftm/changelistener_test.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "encoding/json" @@ -52,7 +52,6 @@ func TestWSChangeDeliveryLookup(t *testing.T) { httpURL.Scheme = "http" _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) {}, httpURL.String(), ) @@ -87,7 +86,6 @@ func TestWSConnectFail(t *testing.T) { _, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, ) cancel() diff --git a/internal/manager/ffcore.go b/pkg/fftm/ffcore.go similarity index 90% rename from internal/manager/ffcore.go rename to pkg/fftm/ffcore.go index f8f4ad5b..f27073df 100644 --- a/internal/manager/ffcore.go +++ b/pkg/fftm/ffcore.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "context" @@ -26,18 +26,18 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" ) // opUpdate allows us to avoid JSONObject serialization to a map before we upload our managedTXOutput type opUpdate struct { - Status core.OpStatus `json:"status"` - Output *fftm.ManagedTXOutput `json:"output"` - Error string `json:"error"` + Status core.OpStatus `json:"status"` + Output *policyengine.ManagedTXOutput `json:"output"` + Error string `json:"error"` } -func (m *manager) writeManagedTX(ctx context.Context, mtx *fftm.ManagedTXOutput, status core.OpStatus, errString string) error { +func (m *manager) writeManagedTX(ctx context.Context, mtx *policyengine.ManagedTXOutput, status core.OpStatus, errString string) error { log.L(ctx).Debugf("Updating operation %s status=%s", mtx.ID, status) var errorInfo fftypes.RESTError var op core.Operation diff --git a/pkg/fftm/managed_tx.go b/pkg/fftm/managed_tx.go deleted file mode 100644 index 8b34ca1a..00000000 --- a/pkg/fftm/managed_tx.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 fftm - -import ( - "github.com/hyperledger/firefly-common/pkg/ffcapi" - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" -) - -type ManagedTXError struct { - Time *fftypes.FFTime `json:"time"` - Error string `json:"error,omitempty"` - Mapped ffcapi.ErrorReason `json:"mapped,omitempty"` -} - -// ManagedTXOutput is the structure stored into the operation in FireFly, that the policy -// engine can use to apply policy, and apply updates to -type ManagedTXOutput struct { - FFTMName string `json:"fftmName"` - ID string `json:"id"` - Nonce *fftypes.FFBigInt `json:"nonce"` - Gas *fftypes.FFBigInt `json:"gas"` - TransactionHash string `json:"transactionHash,omitempty"` - TransactionData string `json:"transactionData,omitempty"` - GasPrice *fftypes.JSONAny `json:"gasPrice"` - PolicyInfo *fftypes.JSONAny `json:"policyInfo"` - FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` - LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` - Request *TransactionRequest `json:"request,omitempty"` - Receipt *ffcapi.GetReceiptResponse `json:"receipt,omitempty"` - ErrorHistory []*ManagedTXError `json:"errorHistory"` - Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` -} diff --git a/internal/manager/manager.go b/pkg/fftm/manager.go similarity index 93% rename from internal/manager/manager.go rename to pkg/fftm/manager.go index 9bf6e98f..7c6df302 100644 --- a/internal/manager/manager.go +++ b/pkg/fftm/manager.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "context" @@ -25,7 +25,6 @@ import ( "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/ffresty" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/httpserver" @@ -33,11 +32,11 @@ import ( "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-common/pkg/wsclient" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" "github.com/hyperledger/firefly/pkg/core" ) @@ -50,7 +49,7 @@ type Manager interface { type manager struct { ctx context.Context cancelCtx func() - connectorAPI ffcapi.API + connector ffcapi.API confirmations confirmations.Manager policyEngine policyengine.PolicyEngine apiServer httpserver.HTTPServer @@ -79,10 +78,10 @@ type manager struct { enableChangeListener bool } -func NewManager(ctx context.Context) (Manager, error) { +func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { var err error m := &manager{ - connectorAPI: ffcapi.NewFFCAPIClient(ctx, tmconfig.ConnectorPrefix, ffcapi.Variant(config.GetString(tmconfig.ConnectorVariant))), + connector: connector, ffCoreClient: ffresty.New(ctx, tmconfig.FFCorePrefix), fullScanRequests: make(chan bool, 1), nextNonces: make(map[string]uint64), @@ -103,7 +102,7 @@ func NewManager(ctx context.Context) (Manager, error) { if m.name == "" { return nil, i18n.NewError(ctx, tmmsgs.MsgConfigParamNotSet, tmconfig.ManagerName) } - m.confirmations, err = confirmations.NewBlockConfirmationManager(ctx, m.connectorAPI) + m.confirmations, err = confirmations.NewBlockConfirmationManager(ctx, m.connector) if err != nil { return nil, err } @@ -124,7 +123,7 @@ func NewManager(ctx context.Context) (Manager, error) { } type pendingState struct { - mtx *fftm.ManagedTXOutput + mtx *policyengine.ManagedTXOutput confirmed bool removed bool trackingTransactionHash string @@ -214,7 +213,7 @@ func (m *manager) fullScan() error { func (m *manager) trackIfManaged(op *core.Operation) { outputJSON := []byte(op.Output.String()) - var mtx fftm.ManagedTXOutput + var mtx policyengine.ManagedTXOutput err := json.Unmarshal(outputJSON, &mtx) if err != nil { log.L(m.ctx).Warnf("Failed to parse output from operation %s", err) @@ -235,7 +234,7 @@ func (m *manager) trackIfManaged(op *core.Operation) { m.trackManaged(&mtx) } -func (m *manager) trackManaged(mtx *fftm.ManagedTXOutput) { +func (m *manager) trackManaged(mtx *policyengine.ManagedTXOutput) { m.mux.Lock() defer m.mux.Unlock() _, existing := m.pendingOpsByID[mtx.ID] diff --git a/internal/manager/manager_test.go b/pkg/fftm/manager_test.go similarity index 84% rename from internal/manager/manager_test.go rename to pkg/fftm/manager_test.go index a8dc3730..8a92dbb7 100644 --- a/internal/manager/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "context" @@ -28,28 +28,26 @@ import ( "time" "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/ffresty" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/httpserver" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines/simple" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines/simple" "github.com/hyperledger/firefly/pkg/core" "github.com/stretchr/testify/assert" ) const testManagerName = "unittest" -func newTestManager(t *testing.T, cAPIHandler http.HandlerFunc, ffCoreHandler http.HandlerFunc, wsURL ...string) (string, *manager, func()) { +func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...string) (string, *manager, func()) { tmconfig.Reset() policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) - cAPIServer := httptest.NewServer(cAPIHandler) - tmconfig.ConnectorPrefix.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", cAPIServer.Listener.Addr())) - ffCoreServer := httptest.NewServer(ffCoreHandler) tmconfig.FFCorePrefix.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", ffCoreServer.Listener.Addr())) @@ -69,7 +67,7 @@ func newTestManager(t *testing.T, cAPIHandler http.HandlerFunc, ffCoreHandler ht tmconfig.FFCorePrefix.Set(ffresty.HTTPConfigURL, wsURL[0]) } - mm, err := NewManager(context.Background()) + mm, err := NewManager(context.Background(), &ffcapimocks.API{}) assert.NoError(t, err) m := mm.(*manager) mcm := &confirmationsmocks.Manager{} @@ -79,7 +77,6 @@ func newTestManager(t *testing.T, cAPIHandler http.HandlerFunc, ffCoreHandler ht return fmt.Sprintf("http://127.0.0.1:%s", managerPort), m, func() { - cAPIServer.Close() ffCoreServer.Close() m.Stop() _ = m.WaitStop() @@ -87,7 +84,7 @@ func newTestManager(t *testing.T, cAPIHandler http.HandlerFunc, ffCoreHandler ht } -func newTestOperation(t *testing.T, mtx *fftm.ManagedTXOutput, status core.OpStatus) *core.Operation { +func newTestOperation(t *testing.T, mtx *policyengine.ManagedTXOutput, status core.OpStatus) *core.Operation { b, err := json.Marshal(&mtx) assert.NoError(t, err) op := &core.Operation{ @@ -105,7 +102,7 @@ func TestNewManagerMissingName(t *testing.T) { tmconfig.Reset() config.Set(tmconfig.ManagerName, "") - _, err := NewManager(context.Background()) + _, err := NewManager(context.Background(), nil) assert.Regexp(t, "FF21018", err) } @@ -119,7 +116,7 @@ func TestNewManagerBadHttpConfig(t *testing.T) { policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGasPrice, "223344556677") - _, err := NewManager(context.Background()) + _, err := NewManager(context.Background(), nil) assert.Regexp(t, "FF00151", err) } @@ -133,7 +130,7 @@ func TestNewManagerFireFlyURLConfig(t *testing.T) { policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGasPrice, "223344556677") - _, err := NewManager(context.Background()) + _, err := NewManager(context.Background(), nil) assert.Regexp(t, "FF00149", err) } @@ -144,7 +141,7 @@ func TestNewManagerBadConfirmationsCacheSize(t *testing.T) { config.Set(tmconfig.ManagerName, "test") config.Set(tmconfig.ConfirmationsBlockCacheSize, -1) - _, err := NewManager(context.Background()) + _, err := NewManager(context.Background(), nil) assert.Regexp(t, "FF21015", err) } @@ -155,7 +152,7 @@ func TestNewManagerBadPolicyEngine(t *testing.T) { config.Set(tmconfig.ManagerName, "test") config.Set(tmconfig.PolicyEngineName, "wrong") - _, err := NewManager(context.Background()) + _, err := NewManager(context.Background(), nil) assert.Regexp(t, "FF21019", err) } @@ -171,7 +168,6 @@ func TestChangeEventsNewBadOutput(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) @@ -206,14 +202,13 @@ func TestChangeEventsWrongName(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - b, err := json.Marshal(newTestOperation(t, &fftm.ManagedTXOutput{ + b, err := json.Marshal(newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + ce.ID.String(), FFTMName: "wrong", - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, }, core.OpStatusPending)) assert.NoError(t, err) w.Header().Set("Content-Type", "application/json") @@ -239,14 +234,13 @@ func TestChangeEventsWrongID(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - op := newTestOperation(t, &fftm.ManagedTXOutput{ + op := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + ce.ID.String(), FFTMName: testManagerName, - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, }, core.OpStatusPending) op.ID = fftypes.NewUUID() b, err := json.Marshal(&op) @@ -274,11 +268,10 @@ func TestChangeEventsNilRequest(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - op := newTestOperation(t, &fftm.ManagedTXOutput{ + op := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + ce.ID.String(), FFTMName: testManagerName, }, core.OpStatusPending) @@ -307,7 +300,6 @@ func TestChangeEventsQueryFail(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) @@ -335,15 +327,14 @@ func TestChangeEventsMarkForCleanup(t *testing.T) { Namespace: "ns1", } - op := newTestOperation(t, &fftm.ManagedTXOutput{ + op := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + ce.ID.String(), FFTMName: testManagerName, - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, }, core.OpStatusFailed) var m *manager _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) @@ -364,24 +355,24 @@ func TestChangeEventsMarkForCleanup(t *testing.T) { func TestStartupScanMultiPageOK(t *testing.T) { - op1 := newTestOperation(t, &fftm.ManagedTXOutput{ + op1 := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), FFTMName: testManagerName, - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, }, core.OpStatusPending) t1 := fftypes.FFTime(time.Now().Add(-10 * time.Minute)) op1.Created = &t1 - op2 := newTestOperation(t, &fftm.ManagedTXOutput{ + op2 := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), FFTMName: testManagerName, - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, }, core.OpStatusPending) t2 := fftypes.FFTime(time.Now().Add(-5 * time.Minute)) op2.Created = &t2 - op3 := newTestOperation(t, &fftm.ManagedTXOutput{ + op3 := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), FFTMName: testManagerName, - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, }, core.OpStatusPending) t3 := fftypes.FFTime(time.Now().Add(-1 * time.Minute)) op3.Created = &t3 @@ -390,7 +381,6 @@ func TestStartupScanMultiPageOK(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, "/spi/v1/operations", r.URL.Path) @@ -440,7 +430,6 @@ func TestStartupScanFail(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, ) cancel() // close servers m.ctx = context.Background() @@ -456,7 +445,6 @@ func TestRequestFullScanNonBlocking(t *testing.T) { _, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, ) defer cancel() @@ -470,7 +458,6 @@ func TestRequestFullScanCancelledBeforeStart(t *testing.T) { _, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, ) defer cancel() @@ -484,7 +471,6 @@ func TestStartupCancelledDuringRetry(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, ) cancel() // close servers m.startupScanMaxRetries = 2 @@ -499,7 +485,6 @@ func TestStartChangeEventListener(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, ) defer cancel() @@ -514,12 +499,11 @@ func TestAddErrorMessageMax(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, ) defer cancel() m.errorHistoryCount = 2 - mtx := &fftm.ManagedTXOutput{} + mtx := &policyengine.ManagedTXOutput{} m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("snap")) m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("crackle")) m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("pop")) @@ -534,7 +518,6 @@ func TestUnparsableOperation(t *testing.T) { var m *manager _, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, ) defer cancel() diff --git a/internal/manager/nonces.go b/pkg/fftm/nonces.go similarity index 89% rename from internal/manager/nonces.go rename to pkg/fftm/nonces.go index e5210a5d..76029433 100644 --- a/internal/manager/nonces.go +++ b/pkg/fftm/nonces.go @@ -14,14 +14,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "context" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) type lockedNonce struct { @@ -30,7 +30,7 @@ type lockedNonce struct { signer string unlocked chan struct{} nonce uint64 - spent *fftm.ManagedTXOutput + spent *policyengine.ManagedTXOutput } // complete must be called for any lockedNonce returned from a successful assignAndLockNonce call @@ -84,11 +84,11 @@ func (m *manager) assignAndLockNonce(ctx context.Context, nsOpID, signer string) } else if doLookup { // We have to ensure we either successfully return a nonce, // or otherwise we unlock when we send the error - nextNonceRes, _, err := m.connectorAPI.GetNextNonce(ctx, &ffcapi.GetNextNonceRequest{ + nextNonceRes, _, err := m.connector.NextNonceForSigner(ctx, &ffcapi.NextNonceForSignerRequest{ Signer: signer, }) if err != nil { - close(locked.unlocked) + locked.complete(ctx) return nil, err } nextNonce := nextNonceRes.Nonce.Uint64() diff --git a/internal/manager/nonces_test.go b/pkg/fftm/nonces_test.go similarity index 57% rename from internal/manager/nonces_test.go rename to pkg/fftm/nonces_test.go index 365ac88e..3327090e 100644 --- a/internal/manager/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -14,32 +14,38 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "context" + "fmt" "net/http" "testing" "time" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestNonceCached(t *testing.T) { _, m, cancel := newTestManager(t, - testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) { - return &ffcapi.GetNextNonceResponse{ - Nonce: fftypes.NewFFBigInt(1111), - }, 200 - }), func(w http.ResponseWriter, r *http.Request) {}, ) defer cancel() + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0x12345" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(1111), + }, ffcapi.ErrorReason(""), nil) + locked1 := make(chan struct{}) done1 := make(chan struct{}) done2 := make(chan struct{}) @@ -53,10 +59,10 @@ func TestNonceCached(t *testing.T) { close(locked1) time.Sleep(1 * time.Millisecond) - ln.spent = &fftm.ManagedTXOutput{ + ln.spent = &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), - Request: &fftm.TransactionRequest{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x12345", @@ -84,3 +90,31 @@ func TestNonceCached(t *testing.T) { <-done2 } + +func TestNonceError(t *testing.T) { + + _, m, cancel := newTestManager(t, + func(w http.ResponseWriter, r *http.Request) {}, + ) + defer cancel() + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + _, err := m.sendManagedTransaction(context.Background(), &policyengine.TransactionRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + }, + }) + assert.Regexp(t, "pop", err) + + m.mux.Lock() + locked, isLocked := m.lockedNonces["0x12345"] + assert.Nil(t, locked) + assert.False(t, isLocked) + m.mux.Unlock() + +} diff --git a/internal/manager/policyloop.go b/pkg/fftm/policyloop.go similarity index 91% rename from internal/manager/policyloop.go rename to pkg/fftm/policyloop.go index 81f37bc0..dc8acc26 100644 --- a/internal/manager/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -14,16 +14,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "time" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" ) @@ -62,14 +62,14 @@ func (m *manager) policyLoopCycle() { } -func (m *manager) addError(mtx *fftm.ManagedTXOutput, reason ffcapi.ErrorReason, err error) { +func (m *manager) addError(mtx *policyengine.ManagedTXOutput, reason ffcapi.ErrorReason, err error) { newLen := len(mtx.ErrorHistory) + 1 if newLen > m.errorHistoryCount { newLen = m.errorHistoryCount } oldHistory := mtx.ErrorHistory - mtx.ErrorHistory = make([]*fftm.ManagedTXError, newLen) - mtx.ErrorHistory[0] = &fftm.ManagedTXError{ + mtx.ErrorHistory = make([]*policyengine.ManagedTXError, newLen) + mtx.ErrorHistory[0] = &policyengine.ManagedTXError{ Time: fftypes.Now(), Mapped: reason, Error: err.Error(), @@ -103,7 +103,7 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { // Pass the state to the pluggable policy engine to potentially perform more actions against it, // such as submitting for the first time, or raising the gas etc. var reason ffcapi.ErrorReason - updated, reason, err = m.policyEngine.Execute(m.ctx, m.connectorAPI, pending.mtx) + updated, reason, err = m.policyEngine.Execute(m.ctx, m.connector, pending.mtx) if err != nil { log.L(m.ctx).Errorf("Policy engine returned error for operation %s reason=%s: %s", mtx.ID, reason, err) m.addError(mtx, reason, err) @@ -152,7 +152,7 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { NotificationType: confirmations.NewTransaction, Transaction: &confirmations.TransactionInfo{ TransactionHash: pending.mtx.TransactionHash, - Receipt: func(receipt *ffcapi.GetReceiptResponse) { + Receipt: func(receipt *ffcapi.TransactionReceiptResponse) { // Will be picked up on the next policy loop cycle - guaranteed to occur before Confirmed m.mux.Lock() pending.mtx.Receipt = receipt @@ -177,7 +177,7 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { } } -func (m *manager) clearConfirmationTracking(mtx *fftm.ManagedTXOutput) { +func (m *manager) clearConfirmationTracking(mtx *policyengine.ManagedTXOutput) { // The only error condition on confirmations manager is if we are exiting, which it logs _ = m.confirmations.Notify(&confirmations.Notification{ NotificationType: confirmations.RemovedTransaction, diff --git a/internal/manager/policyloop_test.go b/pkg/fftm/policyloop_test.go similarity index 84% rename from internal/manager/policyloop_test.go rename to pkg/fftm/policyloop_test.go index f756b5ec..c5f31656 100644 --- a/internal/manager/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "encoding/json" @@ -22,12 +22,13 @@ import ( "net/http" "testing" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -40,15 +41,14 @@ const ( func TestPolicyLoopE2EOk(t *testing.T) { - mtx := &fftm.ManagedTXOutput{ + mtx := &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, } _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { var op core.Operation err := json.NewDecoder(r.Body).Decode(&op) @@ -64,7 +64,7 @@ func TestPolicyLoopE2EOk(t *testing.T) { return n.NotificationType == confirmations.NewTransaction })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.GetReceiptResponse{ + n.Transaction.Receipt(&ffcapi.TransactionReceiptResponse{ BlockNumber: fftypes.NewFFBigInt(12345), TransactionIndex: fftypes.NewFFBigInt(10), BlockHash: fftypes.NewRandB32().String(), @@ -88,15 +88,14 @@ func TestPolicyLoopE2EOk(t *testing.T) { func TestPolicyLoopE2EOkReverted(t *testing.T) { - mtx := &fftm.ManagedTXOutput{ + mtx := &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, } _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { var op core.Operation err := json.NewDecoder(r.Body).Decode(&op) @@ -112,7 +111,7 @@ func TestPolicyLoopE2EOkReverted(t *testing.T) { return n.NotificationType == confirmations.NewTransaction })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.GetReceiptResponse{ + n.Transaction.Receipt(&ffcapi.TransactionReceiptResponse{ BlockNumber: fftypes.NewFFBigInt(12345), TransactionIndex: fftypes.NewFFBigInt(10), BlockHash: fftypes.NewRandB32().String(), @@ -136,15 +135,14 @@ func TestPolicyLoopE2EOkReverted(t *testing.T) { func TestPolicyLoopUpdateFFCoreWithError(t *testing.T) { - mtx := &fftm.ManagedTXOutput{ + mtx := &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, } _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { var op core.Operation err := json.NewDecoder(r.Body).Decode(&op) @@ -169,15 +167,14 @@ func TestPolicyLoopUpdateFFCoreWithError(t *testing.T) { func TestPolicyLoopUpdateOpFail(t *testing.T) { - mtx := &fftm.ManagedTXOutput{ + mtx := &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, } _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, func(w http.ResponseWriter, r *http.Request) { errRes := fftypes.RESTError{Error: "pop"} b, err := json.Marshal(&errRes) @@ -194,7 +191,7 @@ func TestPolicyLoopUpdateOpFail(t *testing.T) { return n.NotificationType == confirmations.NewTransaction })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.GetReceiptResponse{ + n.Transaction.Receipt(&ffcapi.TransactionReceiptResponse{ BlockNumber: fftypes.NewFFBigInt(12345), TransactionIndex: fftypes.NewFFBigInt(10), BlockHash: fftypes.NewRandB32().String(), @@ -215,28 +212,13 @@ func TestPolicyLoopUpdateOpFail(t *testing.T) { func TestPolicyLoopResubmitNewTXID(t *testing.T) { - mtx := &fftm.ManagedTXOutput{ + mtx := &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, } opUpdateCount := 0 _, m, cancel := newTestManager(t, - testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) { - switch reqType { - case ffcapi.RequestTypePrepareTransaction: - return &ffcapi.PrepareTransactionResponse{ - Gas: fftypes.NewFFBigInt(12345), - TransactionData: "0x12345", - }, 200 - case ffcapi.RequestTypeSendTransaction: - return &ffcapi.SendTransactionResponse{ - TransactionHash: sampleTXHash2, - }, 200 - default: - return nil, 500 - } - }), func(w http.ResponseWriter, r *http.Request) { var op core.Operation err := json.NewDecoder(r.Body).Decode(&op) @@ -252,6 +234,17 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { ) defer cancel() + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{ + Gas: fftypes.NewFFBigInt(12345), + TransactionData: "0x12345", + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("TransactionSend", mock.Anything, mock.Anything).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: sampleTXHash2, + }, ffcapi.ErrorReason(""), nil) + mc := m.confirmations.(*confirmationsmocks.Manager) mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { // First we get notified to remove the old TX hash @@ -264,7 +257,7 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { n.Transaction.TransactionHash == sampleTXHash2 })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.GetReceiptResponse{ + n.Transaction.Receipt(&ffcapi.TransactionReceiptResponse{ BlockNumber: fftypes.NewFFBigInt(12345), TransactionIndex: fftypes.NewFFBigInt(10), BlockHash: fftypes.NewRandB32().String(), @@ -293,14 +286,13 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { func TestPolicyLoopCycleCleanupRemoved(t *testing.T) { - mtx := &fftm.ManagedTXOutput{ + mtx := &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), - Request: &fftm.TransactionRequest{}, + Request: &policyengine.TransactionRequest{}, } _, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, ) defer cancel() @@ -319,7 +311,6 @@ func TestNotifyConfirmationMgrFail(t *testing.T) { _, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, ) defer cancel() @@ -327,7 +318,7 @@ func TestNotifyConfirmationMgrFail(t *testing.T) { mc.On("Notify", mock.Anything).Return(fmt.Errorf("pop")) m.trackSubmittedTransaction(&pendingState{ - mtx: &fftm.ManagedTXOutput{ + mtx: &policyengine.ManagedTXOutput{ TransactionHash: sampleSendTX, }, }) diff --git a/internal/manager/routes.go b/pkg/fftm/routes.go similarity index 87% rename from internal/manager/routes.go rename to pkg/fftm/routes.go index bd54f1ff..3fff33c1 100644 --- a/internal/manager/routes.go +++ b/pkg/fftm/routes.go @@ -14,18 +14,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "context" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" ) -func (m *manager) sendManagedTransaction(ctx context.Context, request *fftm.TransactionRequest) (*fftm.ManagedTXOutput, error) { +func (m *manager) sendManagedTransaction(ctx context.Context, request *policyengine.TransactionRequest) (*policyengine.ManagedTXOutput, error) { // First job is to assign the next nonce to this request. // We block any further sends on this nonce until we've got this one successfully into the node, or @@ -40,7 +40,7 @@ func (m *manager) sendManagedTransaction(ctx context.Context, request *fftm.Tran // Prepare the transaction, which will mean we have a transaction that should be submittable. // If we fail at this stage, we don't need to write any state as we are sure we haven't submitted // anything to the blockchain itself. - prepared, _, err := m.connectorAPI.PrepareTransaction(ctx, &ffcapi.PrepareTransactionRequest{ + prepared, _, err := m.connector.TransactionPrepare(ctx, &ffcapi.TransactionPrepareRequest{ TransactionInput: request.TransactionInput, }) if err != nil { @@ -51,7 +51,7 @@ func (m *manager) sendManagedTransaction(ctx context.Context, request *fftm.Tran // From this point on, we will guide this transaction through to submission. // We return an "ack" at this point, and dispatch the work of getting the transaction submitted // to the background worker. - mtx := &fftm.ManagedTXOutput{ + mtx := &policyengine.ManagedTXOutput{ FFTMName: m.name, ID: request.Headers.ID, // on input the request ID must be the namespaced operation ID Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), diff --git a/pkg/policyengine/managed_tx.go b/pkg/policyengine/managed_tx.go new file mode 100644 index 00000000..0cc46f61 --- /dev/null +++ b/pkg/policyengine/managed_tx.go @@ -0,0 +1,48 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 policyengine + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +type ManagedTXError struct { + Time *fftypes.FFTime `json:"time"` + Error string `json:"error,omitempty"` + Mapped ffcapi.ErrorReason `json:"mapped,omitempty"` +} + +// ManagedTXOutput is the structure stored into the operation in FireFly, that the policy +// engine can use to apply policy, and apply updates to +type ManagedTXOutput struct { + FFTMName string `json:"fftmName"` + ID string `json:"id"` + Nonce *fftypes.FFBigInt `json:"nonce"` + Gas *fftypes.FFBigInt `json:"gas"` + TransactionHash string `json:"transactionHash,omitempty"` + TransactionData string `json:"transactionData,omitempty"` + GasPrice *fftypes.JSONAny `json:"gasPrice"` + PolicyInfo *fftypes.JSONAny `json:"policyInfo"` + FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` + LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` + Request *TransactionRequest `json:"request,omitempty"` + Receipt *ffcapi.TransactionReceiptResponse `json:"receipt,omitempty"` + ErrorHistory []*ManagedTXError `json:"errorHistory"` + Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` +} diff --git a/pkg/policyengine/policyengine.go b/pkg/policyengine/policyengine.go index e5fa467e..ba1ccee0 100644 --- a/pkg/policyengine/policyengine.go +++ b/pkg/policyengine/policyengine.go @@ -19,10 +19,9 @@ package policyengine import ( "context" - "github.com/hyperledger/firefly-common/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) type PolicyEngine interface { - Execute(ctx context.Context, cAPI ffcapi.API, mtx *fftm.ManagedTXOutput) (updated bool, reason ffcapi.ErrorReason, err error) + Execute(ctx context.Context, cAPI ffcapi.API, mtx *ManagedTXOutput) (updated bool, reason ffcapi.ErrorReason, err error) } diff --git a/pkg/fftm/tx_request.go b/pkg/policyengine/tx_request.go similarity index 93% rename from pkg/fftm/tx_request.go rename to pkg/policyengine/tx_request.go index 8895571a..131d3674 100644 --- a/pkg/fftm/tx_request.go +++ b/pkg/policyengine/tx_request.go @@ -14,10 +14,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package fftm +package policyengine import ( - "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) // TransactionRequest is the external interface into sending transactions to the front-side of Transaction Manager diff --git a/internal/policyengines/registry.go b/pkg/policyengines/registry.go similarity index 100% rename from internal/policyengines/registry.go rename to pkg/policyengines/registry.go diff --git a/internal/policyengines/registry_test.go b/pkg/policyengines/registry_test.go similarity index 93% rename from internal/policyengines/registry_test.go rename to pkg/policyengines/registry_test.go index 567103d1..c99c7b23 100644 --- a/internal/policyengines/registry_test.go +++ b/pkg/policyengines/registry_test.go @@ -20,8 +20,8 @@ import ( "context" "testing" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines/simple" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines/simple" "github.com/stretchr/testify/assert" ) diff --git a/internal/policyengines/simple/config.go b/pkg/policyengines/simple/config.go similarity index 100% rename from internal/policyengines/simple/config.go rename to pkg/policyengines/simple/config.go diff --git a/internal/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go similarity index 93% rename from internal/policyengines/simple/simple_policy_engine.go rename to pkg/policyengines/simple/simple_policy_engine.go index b05a86e9..10f2143a 100644 --- a/internal/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -25,13 +25,12 @@ import ( "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/ffresty" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) @@ -94,7 +93,7 @@ type simplePolicyInfo struct { } // withPolicyInfo is a convenience helper to run some logic that accesses/updates our policy section -func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *fftm.ManagedTXOutput, fn func(info *simplePolicyInfo) (updated bool, reason ffcapi.ErrorReason, err error)) (updated bool, reason ffcapi.ErrorReason, err error) { +func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *policyengine.ManagedTXOutput, fn func(info *simplePolicyInfo) (updated bool, reason ffcapi.ErrorReason, err error)) (updated bool, reason ffcapi.ErrorReason, err error) { var info simplePolicyInfo infoBytes := []byte(mtx.PolicyInfo.String()) if len(infoBytes) > 0 { @@ -111,7 +110,7 @@ func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *fftm.Manag return updated, reason, err } -func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *fftm.ManagedTXOutput) (updated bool, reason ffcapi.ErrorReason, err error) { +func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *policyengine.ManagedTXOutput) (updated bool, reason ffcapi.ErrorReason, err error) { // Simple policy engine only submits once. if mtx.FirstSubmit == nil { @@ -119,7 +118,7 @@ func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx * if err != nil { return false, "", err } - sendTX := &ffcapi.SendTransactionRequest{ + sendTX := &ffcapi.TransactionSendRequest{ TransactionHeaders: mtx.Request.TransactionHeaders, GasPrice: mtx.GasPrice, TransactionData: mtx.TransactionData, @@ -127,7 +126,7 @@ func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx * sendTX.TransactionHeaders.Nonce = (*fftypes.FFBigInt)(mtx.Nonce.Int()) sendTX.TransactionHeaders.Gas = (*fftypes.FFBigInt)(mtx.Gas.Int()) log.L(ctx).Infof("Sending transaction: %+v", sendTX) - res, reason, err := cAPI.SendTransaction(ctx, sendTX) + res, reason, err := cAPI.TransactionSend(ctx, sendTX) if err != nil { // A more sophisticated policy engine would consider the reason here, and potentially adjust the transaction for future attempts return false, reason, err @@ -181,7 +180,7 @@ func (p *simplePolicyEngine) getGasPrice(ctx context.Context, cAPI ffcapi.API) ( return p.gasOracleQueryValue, nil case GasOracleModeConnector: // Call the connector - res, _, err := cAPI.GetGasPrice(ctx, &ffcapi.GetGasPriceRequest{}) + res, _, err := cAPI.GasPriceEstimate(ctx, &ffcapi.GasPriceEstimateRequest{}) if err != nil { return nil, err } diff --git a/internal/policyengines/simple/simple_policy_engine_test.go b/pkg/policyengines/simple/simple_policy_engine_test.go similarity index 89% rename from internal/policyengines/simple/simple_policy_engine_test.go rename to pkg/policyengines/simple/simple_policy_engine_test.go index 5313e041..51c6b59b 100644 --- a/internal/policyengines/simple/simple_policy_engine_test.go +++ b/pkg/policyengines/simple/simple_policy_engine_test.go @@ -25,12 +25,12 @@ import ( "time" "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/ffresty" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -61,8 +61,8 @@ func TestFixedGasPriceOK(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), prefix) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -74,12 +74,12 @@ func TestFixedGasPriceOK(t *testing.T) { } mockFFCAPI := &ffcapimocks.API{} - mockFFCAPI.On("SendTransaction", mock.Anything, mock.MatchedBy(func(req *ffcapi.SendTransactionRequest) bool { + mockFFCAPI.On("TransactionSend", mock.Anything, mock.MatchedBy(func(req *ffcapi.TransactionSendRequest) bool { return req.GasPrice.JSONObject().GetString("maxPriorityFee") == "32.146027800733336" && req.GasPrice.JSONObject().GetString("maxFee") == "32.14602781673334" && req.From == "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712" && req.TransactionData == "SOME_RAW_TX_BYTES" - })).Return(&ffcapi.SendTransactionResponse{ + })).Return(&ffcapi.TransactionSendResponse{ TransactionHash: "0x12345", }, ffcapi.ErrorReason(""), nil) @@ -129,8 +129,8 @@ func TestGasOracleSendOK(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), prefix) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -142,12 +142,12 @@ func TestGasOracleSendOK(t *testing.T) { } mockFFCAPI := &ffcapimocks.API{} - mockFFCAPI.On("SendTransaction", mock.Anything, mock.MatchedBy(func(req *ffcapi.SendTransactionRequest) bool { + mockFFCAPI.On("TransactionSend", mock.Anything, mock.MatchedBy(func(req *ffcapi.TransactionSendRequest) bool { return req.GasPrice.JSONObject().GetString("unit") == "gwei" && req.GasPrice.JSONObject().GetString("value") == "32.146027800733336" && req.From == "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712" && req.TransactionData == "SOME_RAW_TX_BYTES" - })).Return(&ffcapi.SendTransactionResponse{ + })).Return(&ffcapi.TransactionSendResponse{ TransactionHash: "0x12345", }, ffcapi.ErrorReason(""), nil) @@ -176,8 +176,8 @@ func TestConnectorGasOracleSendOK(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), prefix) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -189,13 +189,13 @@ func TestConnectorGasOracleSendOK(t *testing.T) { } mockFFCAPI := &ffcapimocks.API{} - mockFFCAPI.On("GetGasPrice", mock.Anything, mock.Anything).Return(&ffcapi.GetGasPriceResponse{ + mockFFCAPI.On("GasPriceEstimate", mock.Anything, mock.Anything).Return(&ffcapi.GasPriceEstimateResponse{ GasPrice: fftypes.JSONAnyPtr(`"12345"`), }, ffcapi.ErrorReason(""), nil).Once() - mockFFCAPI.On("SendTransaction", mock.Anything, mock.MatchedBy(func(req *ffcapi.SendTransactionRequest) bool { + mockFFCAPI.On("TransactionSend", mock.Anything, mock.MatchedBy(func(req *ffcapi.TransactionSendRequest) bool { return req.From == "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712" && req.TransactionData == "SOME_RAW_TX_BYTES" - })).Return(&ffcapi.SendTransactionResponse{ + })).Return(&ffcapi.TransactionSendResponse{ TransactionHash: "0x12345", }, ffcapi.ErrorReason(""), nil) @@ -223,8 +223,8 @@ func TestConnectorGasOracleFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), prefix) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -236,7 +236,7 @@ func TestConnectorGasOracleFail(t *testing.T) { } mockFFCAPI := &ffcapimocks.API{} - mockFFCAPI.On("GetGasPrice", mock.Anything, mock.Anything).Return(&ffcapi.GetGasPriceResponse{ + mockFFCAPI.On("GasPriceEstimate", mock.Anything, mock.Anything).Return(&ffcapi.GasPriceEstimateResponse{ GasPrice: fftypes.JSONAnyPtr(`"12345"`), }, ffcapi.ErrorReason(""), fmt.Errorf("pop")) @@ -264,8 +264,8 @@ func TestGasOracleSendFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), prefix) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -324,8 +324,8 @@ func TestGasOracleTemplateExecuteFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), prefix) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -354,8 +354,8 @@ func TestGasOracleNonJSON(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), prefix) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -387,8 +387,8 @@ func TestTXSendFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), prefix) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -399,7 +399,7 @@ func TestTXSendFail(t *testing.T) { } mockFFCAPI := &ffcapimocks.API{} - mockFFCAPI.On("SendTransaction", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonInvalidInputs, fmt.Errorf("pop")) + mockFFCAPI.On("TransactionSend", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonInvalidInputs, fmt.Errorf("pop")) ctx := context.Background() _, _, err = p.Execute(ctx, mockFFCAPI, mtx) assert.Regexp(t, "pop", err) @@ -413,11 +413,11 @@ func TestWarnStaleWarningCannotParse(t *testing.T) { assert.NoError(t, err) submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) - mtx := &fftm.ManagedTXOutput{ + mtx := &policyengine.ManagedTXOutput{ TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: &submitTime, PolicyInfo: fftypes.JSONAnyPtr("!not json!"), - Request: &fftm.TransactionRequest{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -445,8 +445,8 @@ func TestWarnStaleAdditionalWarning(t *testing.T) { submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.FFTime(time.Now().Add(-50 * time.Hour)) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -479,8 +479,8 @@ func TestWarnStaleNoWarning(t *testing.T) { submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.Now() - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -511,8 +511,8 @@ func TestNoOpWithReceipt(t *testing.T) { assert.NoError(t, err) submitTime := fftypes.Now() - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ + mtx := &policyengine.ManagedTXOutput{ + Request: &policyengine.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -521,7 +521,7 @@ func TestNoOpWithReceipt(t *testing.T) { }, TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: submitTime, - Receipt: &ffcapi.GetReceiptResponse{ + Receipt: &ffcapi.TransactionReceiptResponse{ BlockHash: "0x39e2664effa5ad0651c35f1fe3b4c4b90492b1955fee731c2e9fb4d6518de114", }, } From ea51536b41ba8ba8ec24921fd3c8b6b54dda6cc5 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 1 Jun 2022 22:57:10 -0400 Subject: [PATCH 02/95] Update to go 1.17 Signed-off-by: Peter Broadhurst --- go.mod | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 9ddf0847..9244902d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hyperledger/firefly-transaction-manager -go 1.16 +go 1.17 require ( github.com/go-resty/resty/v2 v2.7.0 @@ -12,3 +12,40 @@ require ( github.com/spf13/viper v1.11.0 github.com/stretchr/testify v1.7.1 ) + +require ( + github.com/aidarkhanov/nanoid v1.0.8 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/pelletier/go-toml v1.9.4 // indirect + github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/cors v1.8.2 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.2.0 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect + golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/ini.v1 v1.66.4 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) From 2c4710695d3bc49a642fbec167d5b91c12659250 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 1 Jun 2022 22:58:44 -0400 Subject: [PATCH 03/95] Remove docker build Signed-off-by: Peter Broadhurst --- .github/workflows/docker_main.yml | 43 ---------------------------- .github/workflows/docker_release.yml | 37 ------------------------ 2 files changed, 80 deletions(-) delete mode 100644 .github/workflows/docker_main.yml delete mode 100644 .github/workflows/docker_release.yml diff --git a/.github/workflows/docker_main.yml b/.github/workflows/docker_main.yml deleted file mode 100644 index e3814e3a..00000000 --- a/.github/workflows/docker_main.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Docker Main Build - -on: - push: - branches: - - main - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set build tag - id: build_tag_generator - run: | - RELEASE_TAG=$(curl https://api.github.com/repos/hyperledger/firefly-transaction-manager/releases/latest -s | jq .tag_name -r) - BUILD_TAG=$RELEASE_TAG-$(date +"%Y%m%d")-$GITHUB_RUN_NUMBER - echo ::set-output name=BUILD_TAG::$BUILD_TAG - - - name: Build - run: | - make BUILD_VERSION="${GITHUB_REF##*/}" DOCKER_ARGS="\ - --label commit=$GITHUB_SHA \ - --label build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - --label tag=${{ steps.build_tag_generator.outputs.BUILD_TAG }} \ - --tag ghcr.io/hyperledger/firefly-transaction-manager:${{ steps.build_tag_generator.outputs.BUILD_TAG }}" \ - docker - - - name: Tag release - run: docker tag ghcr.io/hyperledger/firefly-transaction-manager:${{ steps.build_tag_generator.outputs.BUILD_TAG }} ghcr.io/hyperledger/firefly-transaction-manager:head - - - name: Push docker image - run: | - echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - docker push ghcr.io/hyperledger/firefly-transaction-manager:${{ steps.build_tag_generator.outputs.BUILD_TAG }} - - - name: Push head tag - run: | - echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - docker push ghcr.io/hyperledger/firefly-transaction-manager:head diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml deleted file mode 100644 index 79632ee2..00000000 --- a/.github/workflows/docker_release.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Docker Release Build - -on: - release: - types: [released, prereleased] - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Build - run: | - make BUILD_VERSION="${GITHUB_REF##*/}" DOCKER_ARGS="\ - --label commit=$GITHUB_SHA \ - --label build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - --label tag=${GITHUB_REF##*/} \ - --tag ghcr.io/hyperledger/firefly-transaction-manager:${GITHUB_REF##*/}" \ - docker - - - name: Tag release - if: github.event.action == 'released' - run: docker tag ghcr.io/hyperledger/firefly-transaction-manager:${GITHUB_REF##*/} ghcr.io/hyperledger/firefly-transaction-manager:latest - - - name: Push docker image - run: | - echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - docker push ghcr.io/hyperledger/firefly-transaction-manager:${GITHUB_REF##*/} - - - name: Push latest tag - if: github.event.action == 'released' - run: | - echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - docker push ghcr.io/hyperledger/firefly-transaction-manager:latest From f7e7c3a62f6830519a8c1cafd3a5ca98097f21fd Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 3 Jun 2022 09:09:39 -0400 Subject: [PATCH 04/95] Update firefly-common and resolve intermittent test failure Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 3 + go.mod | 30 +- go.sum | 81 +- internal/confirmations/confirmations_test.go | 20 +- internal/events/eventstream.go | 757 ++++++++++++++++++ internal/tmconfig/tmconfig.go | 22 +- internal/tmmsgs/en_config_descriptions.go | 9 +- internal/tmmsgs/en_error_messges.go | 9 +- mocks/policyenginesmocks/policy_engine.go | 45 -- pkg/fftm/manager.go | 8 +- pkg/fftm/manager_test.go | 24 +- pkg/fftm/{routes.go => send_tx.go} | 0 pkg/policyengines/registry.go | 12 +- pkg/policyengines/registry_test.go | 8 +- pkg/policyengines/simple/config.go | 20 +- .../simple/simple_policy_engine.go | 18 +- .../simple/simple_policy_engine_test.go | 130 +-- 17 files changed, 989 insertions(+), 207 deletions(-) create mode 100644 internal/events/eventstream.go delete mode 100644 mocks/policyenginesmocks/policy_engine.go rename pkg/fftm/{routes.go => send_tx.go} (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 25ab9174..c77c1c0d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,9 @@ "Debugf", "devdocs", "Devel", + "distmode", "ethconnect", + "eventstream", "fabconnect", "ffcapi", "ffcapimocks", @@ -30,6 +32,7 @@ "fftypes", "finalizers", "GJSON", + "httpserver", "hyperledger", "Infof", "IPFS", diff --git a/go.mod b/go.mod index 9244902d..1f543621 100644 --- a/go.mod +++ b/go.mod @@ -7,17 +7,18 @@ require ( github.com/gorilla/mux v1.8.0 github.com/hashicorp/golang-lru v0.5.4 github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e - github.com/hyperledger/firefly-common v0.1.2 + github.com/hyperledger/firefly-common v0.1.8 github.com/sirupsen/logrus v1.8.1 - github.com/spf13/viper v1.11.0 + github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.1 + golang.org/x/text v0.3.7 ) require ( github.com/aidarkhanov/nanoid v1.0.8 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.4.0 // indirect - github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -25,27 +26,26 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/mitchellh/mapstructure v1.4.3 // indirect - github.com/pelletier/go-toml v1.9.4 // indirect - github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.8.2 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect github.com/spf13/afero v1.8.2 // indirect - github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.2.0 // indirect - github.com/subosito/gotenv v1.2.0 // indirect + github.com/subosito/gotenv v1.4.0 // indirect github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect - golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect - golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect - gopkg.in/ini.v1 v1.66.4 // indirect + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b86ba540..29ea0634 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7 cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= @@ -359,6 +361,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -370,16 +373,20 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getkin/kin-openapi v0.94.1-0.20220401165309-136a868a30c2/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= +github.com/getkin/kin-openapi v0.96.0/go.mod h1:w4lRPHiyOdwGbOkLIyk+P0qCwlu7TXPCHD/64nSXzgE= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= @@ -512,6 +519,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v35 v35.2.0/go.mod h1:s0515YVTI+IMrDoy9Y4pHt9ShGpzHvHO8rZ7L7acgvs= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -550,6 +559,7 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -615,8 +625,9 @@ github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e h1:QP+Yykyq7C670zb4Fs7s4lAtYmvIll4rP/y00hdOsg4= github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e/go.mod h1:434LxYn4ntyK/E0dY+2dTc55caBy6BdUMYBM2gLndAI= -github.com/hyperledger/firefly-common v0.1.2 h1:zwz7WAHrK5we8bku7FO1rGZSFY+N4sDO3fWrabsnL+E= github.com/hyperledger/firefly-common v0.1.2/go.mod h1:ytB+404+Qg00wJKx602Yi/V6Spx9d0g65lZR9y5fzlo= +github.com/hyperledger/firefly-common v0.1.8 h1:gP4waMEEkbhB+pbsJyp5VrfrT4jQad6/4TXnXTfqXI8= +github.com/hyperledger/firefly-common v0.1.8/go.mod h1:MYL6Dbj3KqM/79IkS+mCzJ7wRguNbd/PKdVu8aXo5TI= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -626,6 +637,7 @@ github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -723,8 +735,9 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= @@ -795,8 +808,9 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= @@ -879,10 +893,12 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= @@ -910,6 +926,7 @@ github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQ github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -945,6 +962,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -957,6 +976,7 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= +github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -988,8 +1008,9 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= @@ -1005,8 +1026,9 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= -github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1022,8 +1044,10 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= +github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -1076,10 +1100,14 @@ go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1128,8 +1156,9 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1243,8 +1272,11 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA= +golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1279,6 +1311,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1400,11 +1433,14 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1500,6 +1536,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= @@ -1544,6 +1581,9 @@ google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1636,6 +1676,12 @@ google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1670,6 +1716,8 @@ google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzI google.golang.org/grpc v1.42.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.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= 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= @@ -1701,8 +1749,9 @@ gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKW gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= @@ -1724,8 +1773,10 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index 61e939b8..25463e4e 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -321,18 +321,24 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { // The next filter gives us 1002a, which will later be removed blockHashes := make(chan *ffcapi.BlockHashEvent, 1) - // Notify of the first confirmation for the first receipt - 1002a - blockHashes <- &ffcapi.BlockHashEvent{ - BlockHashes: []string{ - block1002a.BlockHash, - }, - } mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) + // First check while walking the chain does not yield a block + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1002 + })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Once() + // Transaction receipt is immediately available on fork A mca.On("TransactionReceipt", mock.Anything, mock.MatchedBy(func(r *ffcapi.TransactionReceiptRequest) bool { return r.TransactionHash == txToConfirmForkA.TransactionHash - })).Return(&ffcapi.TransactionReceiptResponse{ + })).Run(func(args mock.Arguments) { + // Notify of the first confirmation for the first receipt - 1002a + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1002a.BlockHash, + }, + } + }).Return(&ffcapi.TransactionReceiptResponse{ BlockHash: block1002a.ParentHash, BlockNumber: fftypes.NewFFBigInt(1001), TransactionIndex: fftypes.NewFFBigInt(0), diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go new file mode 100644 index 00000000..8b86ce3b --- /dev/null +++ b/internal/events/eventstream.go @@ -0,0 +1,757 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 events + +// import ( +// "container/list" +// "context" +// "math/big" +// "net" +// "net/url" +// "strconv" +// "strings" +// "sync" +// "time" + +// "github.com/hyperledger/firefly-common/pkg/fftypes" +// "github.com/hyperledger/firefly-ethconnect/internal/auth" +// "github.com/hyperledger/firefly-ethconnect/internal/errors" +// "github.com/hyperledger/firefly-ethconnect/internal/ws" +// "github.com/hyperledger/firefly/pkg/fftypes" +// ethbinding "github.com/kaleido-io/ethbinding/pkg" + +// lru "github.com/hashicorp/golang-lru" +// log "github.com/sirupsen/logrus" +// ) + +// type DistributionMode fftypes.FFEnum + +// const ( +// DistributionModeBroadcast = fftypes.FFEnumValue("distmode", "broadcast") +// DistributionModeWLD = fftypes.FFEnumValue("distmode", "workloadDistribution") +// ) + +// // EventStream is the +// type EventStream struct { +// ID string `ffstruct:"eventstream" json:"id"` +// Created *fftypes.FFTime `ffstruct:"eventstream" json:"created"` +// Name string `ffstruct:"eventstream" json:"name,omitempty"` +// Path string `ffstruct:"eventstream" json:"path"` +// Suspended bool `ffstruct:"eventstream" json:"suspended"` +// Type string `ffstruct:"eventstream" json:"type,omitempty"` +// BatchSize *uint64 `ffstruct:"eventstream" json:"batchSize,omitempty"` +// BatchTimeoutMS *uint64 `ffstruct:"eventstream" json:"batchTimeoutMS,omitempty"` +// ErrorHandling string `ffstruct:"eventstream" json:"errorHandling,omitempty"` +// RetryTimeoutSec uint64 `ffstruct:"eventstream" json:"retryTimeoutSec,omitempty"` +// BlockedRetryDelaySec *uint64 `ffstruct:"eventstream" json:"blockedRetryDelaySec,omitempty"` +// Webhook *WebhookActionInfo `ffstruct:"eventstream" json:"webhook,omitempty"` +// WebSocket *WebSocketActionInfo `ffstruct:"eventstream" json:"websocket,omitempty"` +// Timestamps bool `ffstruct:"eventstream" json:"timestamps,omitempty"` +// Inputs bool `ffstruct:"eventstream" json:"inputs,omitempty"` +// } + +// type WebhookActionInfo struct { +// URL string `json:"url,omitempty"` +// Headers map[string]string `json:"headers,omitempty"` +// TLSkipHostVerify bool `json:"tlsSkipHostVerify,omitempty"` +// RequestTimeoutSec uint32 `json:"requestTimeoutSec,omitempty"` +// } + +// type WebSocketActionInfo struct { +// Topic string `json:"topic,omitempty"` +// DistributionMode DistributionMode `json:"distributionMode,omitempty"` +// } + +// type eventStream struct { +// sm subscriptionManager +// allowPrivateIPs bool +// spec *StreamInfo +// eventStream chan *eventData +// stopped bool +// pollingInterval time.Duration +// inFlight uint64 +// batchCond *sync.Cond +// batchQueue *list.List +// batchCount uint64 +// initialRetryDelay time.Duration +// backoffFactor float64 +// updateInProgress bool +// updateInterrupt chan struct{} // a zero-sized struct used only for signaling (hand rolled alternative to context) +// blockTimestampCache *lru.Cache +// action eventStreamAction +// wsChannels ws.WebSocketChannels +// decimalTransactionIndex bool + +// eventPollerDone chan struct{} +// batchProcessorDone chan struct{} +// batchDispatcherDone chan struct{} +// } + +// type eventStreamAction interface { +// attemptBatch(batchNumber, attempt uint64, events []*eventData) error +// } + +// func validateWebSocket(w *webSocketActionInfo) error { +// if w.DistributionMode != "" && w.DistributionMode != DistributionModeBroadcast && w.DistributionMode != DistributionModeWLD { +// return errors.Errorf(errors.EventStreamsInvalidDistributionMode, w.DistributionMode) +// } +// return nil +// } + +// // newEventStream constructor verifies the action is correct, kicks +// // off the event batch processor, and blockHWM will be +// // initialied to that supplied (zero on initial, or the +// // value from the checkpoint) +// func newEventStream(sm subscriptionManager, spec *StreamInfo, wsChannels ws.WebSocketChannels) (a *eventStream, err error) { +// if spec == nil || spec.GetID() == "" { +// return nil, errors.Errorf(errors.EventStreamsNoID) +// } + +// if spec.BatchSize == 0 { +// spec.BatchSize = 1 +// } else if spec.BatchSize > MaxBatchSize { +// spec.BatchSize = MaxBatchSize +// } +// if spec.BatchTimeoutMS == 0 { +// spec.BatchTimeoutMS = 5000 +// } +// if strings.ToLower(spec.ErrorHandling) == ErrorHandlingBlock { +// spec.ErrorHandling = ErrorHandlingBlock +// } else { +// spec.ErrorHandling = ErrorHandlingSkip +// } +// if spec.TimestampCacheSize == 0 { +// spec.TimestampCacheSize = DefaultTimestampCacheSize +// } + +// a = &eventStream{ +// sm: sm, +// spec: spec, +// allowPrivateIPs: sm.config().WebhooksAllowPrivateIPs, +// eventStream: make(chan *eventData), +// batchCond: sync.NewCond(&sync.Mutex{}), +// batchQueue: list.New(), +// initialRetryDelay: DefaultExponentialBackoffInitial, +// backoffFactor: DefaultExponentialBackoffFactor, +// pollingInterval: time.Duration(sm.config().EventPollingIntervalSec) * time.Second, +// wsChannels: wsChannels, +// decimalTransactionIndex: sm.config().DecimalTransactionIndex, +// } + +// if a.blockTimestampCache, err = lru.New(spec.TimestampCacheSize); err != nil { +// return nil, errors.Errorf(errors.EventStreamsCreateStreamResourceErr, err) +// } +// if a.pollingInterval == 0 { +// // Let's us do this from UTs, without exposing it +// a.pollingInterval = 10 * time.Millisecond +// } + +// spec.Type = strings.ToLower(spec.Type) +// switch spec.Type { +// case "webhook": +// if a.action, err = newWebhookAction(a, spec.Webhook); err != nil { +// return nil, err +// } +// case "websocket": + +// if spec.WebSocket != nil { +// if err := validateWebSocket(spec.WebSocket); err != nil { +// return nil, err +// } +// } + +// if a.action, err = newWebSocketAction(a, spec.WebSocket); err != nil { +// return nil, err +// } +// default: +// return nil, errors.Errorf(errors.EventStreamsInvalidActionType, spec.Type) +// } + +// a.startEventHandlers(false) +// return a, nil +// } + +// // formatTransactionIndex honors the configuration for whether transactionIndex should be an `0x` +// // hex string on the return. This was a bug in earlier version of ethconnect, and an option +// // is provided to restore the old behavior in case an application was depending on it. +// func (a *eventStream) formatTransactionIndex(txIndex ethbinding.HexUint) string { +// if a.decimalTransactionIndex { +// return strconv.FormatUint(uint64(txIndex), 10) +// } +// return txIndex.String() +// } + +// // helper to kick off go routines and any tracking entities +// func (a *eventStream) startEventHandlers(resume bool) { +// // create a context that can be used to indicate an update to the eventstream +// a.updateInterrupt = make(chan struct{}) +// a.eventPollerDone = make(chan struct{}) +// go a.eventPoller() +// a.batchProcessorDone = make(chan struct{}) +// go a.batchProcessor() +// // For a pause/resume, the batch dispatcher goroutine is not terminated, hence no need to start it +// if !resume { +// a.batchDispatcherDone = make(chan struct{}) +// go a.batchDispatcher() +// } +// } + +// // GetID returns the ID (for sorting) +// func (spec *StreamInfo) GetID() string { +// return spec.ID +// } + +// func (spec *StreamInfo) blockedRetryDelaySec() uint64 { +// if spec.BlockedRetryDelaySec == nil { +// if spec.TypoReryDelaySec > 0 { +// return spec.TypoReryDelaySec +// } else { +// return 30 +// } +// } +// return *spec.BlockedRetryDelaySec +// } + +// // preUpdateStream sets a flag to indicate updateInProgress and wakes up goroutines waiting on condition variable +// func (a *eventStream) preUpdateStream() error { +// a.batchCond.L.Lock() +// if a.updateInProgress { +// a.batchCond.L.Unlock() +// return errors.Errorf(errors.EventStreamsUpdateAlreadyInProgress) +// } +// a.updateInProgress = true +// // close the updateInterrupt channel so that the event handler go routines can be woken up +// close(a.updateInterrupt) +// a.batchCond.Broadcast() +// a.batchCond.L.Unlock() + +// a.drainBlockConfirmationManager() + +// return nil +// } + +// func (a *eventStream) drainBlockConfirmationManager() { +// bcm := a.sm.confirmationManager() +// if bcm != nil { +// n := &bcmNotification{ +// nType: bcmStopStream, +// eventStream: a, +// complete: make(chan struct{}), +// } +// bcm.notify(n) +// <-n.complete +// } +// } + +// // postUpdateStream resets flags and kicks off a fresh round of handler go routines +// func (a *eventStream) postUpdateStream() { +// a.batchCond.L.Lock() +// a.startEventHandlers(false) +// a.updateInProgress = false +// a.inFlight = 0 +// a.batchCond.L.Unlock() +// } + +// func (a *eventStream) checkUpdate(newSpec *StreamInfo) (updatedSpec *StreamInfo, err error) { + +// // setUpdated marks that there is a change, and creates a copied object +// specCopy := *a.spec +// setUpdated := func() *StreamInfo { +// if updatedSpec == nil { +// updatedSpec = &specCopy +// } +// return updatedSpec +// } + +// if newSpec.Type != "" && newSpec.Type != specCopy.Type { +// return nil, errors.Errorf(errors.EventStreamsCannotUpdateType) +// } +// if specCopy.Type == "webhook" && newSpec.Webhook != nil { +// if newSpec.Webhook.RequestTimeoutSec != 0 && newSpec.Webhook.RequestTimeoutSec != specCopy.Webhook.RequestTimeoutSec { +// setUpdated().Webhook.RequestTimeoutSec = newSpec.Webhook.RequestTimeoutSec +// } +// if newSpec.Webhook.TLSkipHostVerify != specCopy.Webhook.TLSkipHostVerify { +// setUpdated().Webhook.TLSkipHostVerify = newSpec.Webhook.TLSkipHostVerify +// } +// if newSpec.Webhook.URL != "" && newSpec.Webhook.URL != specCopy.Webhook.URL { +// if _, err = url.Parse(newSpec.Webhook.URL); err != nil { +// return nil, errors.Errorf(errors.EventStreamsWebhookInvalidURL) +// } +// setUpdated().Webhook.URL = newSpec.Webhook.URL +// } +// for k, v := range newSpec.Webhook.Headers { +// if specCopy.Webhook.Headers == nil || specCopy.Webhook.Headers[k] != v { +// setUpdated().Webhook.Headers = newSpec.Webhook.Headers +// break +// } +// } +// } +// if specCopy.Type == "websocket" && newSpec.WebSocket != nil { +// if newSpec.WebSocket.Topic != specCopy.WebSocket.Topic { +// setUpdated().WebSocket.Topic = newSpec.WebSocket.Topic +// } +// if newSpec.WebSocket.DistributionMode != specCopy.WebSocket.DistributionMode { +// setUpdated().WebSocket.DistributionMode = newSpec.WebSocket.DistributionMode +// } +// // Validate if we changed it +// if updatedSpec != nil { +// if err := validateWebSocket(newSpec.WebSocket); err != nil { +// return nil, err +// } +// } +// } + +// if specCopy.BatchSize != newSpec.BatchSize && newSpec.BatchSize != 0 && newSpec.BatchSize < MaxBatchSize { +// setUpdated().BatchSize = newSpec.BatchSize +// } +// if specCopy.BatchTimeoutMS != newSpec.BatchTimeoutMS && newSpec.BatchTimeoutMS != 0 { +// setUpdated().BatchTimeoutMS = newSpec.BatchTimeoutMS +// } +// if newSpec.BlockedRetryDelaySec != nil && specCopy.blockedRetryDelaySec() != newSpec.blockedRetryDelaySec() { +// blockedRetryDelaySec := newSpec.blockedRetryDelaySec() +// setUpdated().BlockedRetryDelaySec = &blockedRetryDelaySec +// } +// if newSpec.ErrorHandling != "" && newSpec.ErrorHandling != specCopy.ErrorHandling { +// if strings.ToLower(newSpec.ErrorHandling) == ErrorHandlingBlock { +// setUpdated().ErrorHandling = ErrorHandlingBlock +// } else { +// setUpdated().ErrorHandling = ErrorHandlingSkip +// } +// } +// if newSpec.Name != "" && specCopy.Name != newSpec.Name { +// setUpdated().Name = newSpec.Name +// } +// if specCopy.Timestamps != newSpec.Timestamps { +// setUpdated().Timestamps = newSpec.Timestamps +// } +// if specCopy.Inputs != newSpec.Inputs { +// setUpdated().Inputs = newSpec.Inputs +// } + +// // Return a non-nil object ONLY if there's a change +// return updatedSpec, nil +// } + +// // update modifies an existing eventStream +// func (a *eventStream) update(newSpec *StreamInfo) (spec *StreamInfo, err error) { +// log.Infof("%s: Update event stream", a.spec.ID) +// updatedSpec, err := a.checkUpdate(newSpec) +// if err != nil { +// return nil, err +// } +// if updatedSpec == nil { +// log.Infof("%s: No change", a.spec.ID) +// return a.spec, nil +// } + +// // set a flag to indicate updateInProgress +// // For any go routines that are Wait() ing on the eventListener, wake them up +// if err := a.preUpdateStream(); err != nil { +// return nil, err +// } +// a.spec = updatedSpec +// // wait for the poked goroutines to finish up +// <-a.eventPollerDone +// <-a.batchProcessorDone +// <-a.batchDispatcherDone +// defer a.postUpdateStream() +// return a.spec, nil +// } + +// // HandleEvent is the entry point for the stream from the event detection logic +// func (a *eventStream) handleEvent(event *eventData) { +// // Does nothing more than add it to the batch, to be picked up +// // by the batchDispatcher +// select { +// case a.eventStream <- event: +// case <-a.batchDispatcherDone: +// // If the dispatcher isn't running, then there's no problem - the HWM won't get updated +// log.Infof("Event arrived while event stream shutting down") +// } +// } + +// // stop is a lazy stop, that marks a flag for the batch goroutine to pick up +// func (a *eventStream) stop(wait bool) { +// a.batchCond.L.Lock() +// if !a.stopped { +// a.stopped = true +// if a.updateInterrupt != nil { +// close(a.updateInterrupt) +// } +// } +// a.batchCond.Broadcast() +// a.batchCond.L.Unlock() +// if wait { +// <-a.eventPollerDone +// <-a.batchProcessorDone +// <-a.batchDispatcherDone +// } +// } + +// // suspend only stops the dispatcher, pushing back as if we're in blocking mode +// func (a *eventStream) suspend() { +// a.batchCond.L.Lock() +// a.spec.Suspended = true +// a.batchCond.Broadcast() +// a.batchCond.L.Unlock() +// a.drainBlockConfirmationManager() +// <-a.eventPollerDone +// <-a.batchProcessorDone +// } + +// func isChannelDone(c chan struct{}) bool { +// var isDone bool +// select { +// case <-c: +// isDone = true +// default: +// isDone = false +// } +// return isDone +// } + +// // resume resumes the dispatcher +// func (a *eventStream) resume() error { +// a.batchCond.L.Lock() +// defer a.batchCond.L.Unlock() + +// if !isChannelDone(a.batchProcessorDone) || !isChannelDone(a.eventPollerDone) { +// return errors.Errorf(errors.EventStreamsWebhookResumeActive, a.spec.Suspended) +// } +// a.spec.Suspended = false + +// a.startEventHandlers(true) +// a.batchCond.Broadcast() +// return nil +// } + +// // isBlocked protect us from polling for more events when the stream is blocked. +// // Can happen regardless of whether the error handling is +// // block or skip. It's just with skip we eventually move onto new messages +// // after the retries etc. are complete +// func (a *eventStream) isBlocked() bool { +// a.batchCond.L.Lock() +// inFlight := a.inFlight +// isBlocked := inFlight >= a.spec.BatchSize +// a.batchCond.L.Unlock() +// if isBlocked { +// log.Warnf("%s: Is currently blocked. InFlight=%d BatchSize=%d", a.spec.ID, inFlight, a.spec.BatchSize) +// } else if inFlight > 0 { +// log.Debugf("%s: InFlight=%d BatchSize=%d", a.spec.ID, inFlight, a.spec.BatchSize) +// } +// return isBlocked +// } + +// func (a *eventStream) markAllSubscriptionsStale(ctx context.Context) { +// // Mark all subscriptions stale, so they will re-start from the checkpoint if/when we re-run the poller +// subs := a.sm.subscriptionsForStream(a.spec.ID) +// for _, sub := range subs { +// sub.markFilterStale(ctx, true) +// } +// } + +// // eventPoller checks every few seconds against the ethereum node for any +// // new events on the subscriptions that are registered for this stream +// func (a *eventStream) eventPoller() { +// defer close(a.eventPollerDone) + +// ctx := auth.NewSystemAuthContext() +// var checkpoint map[string]*big.Int +// for !a.suspendOrStop() { +// var err error +// // Load the checkpoint (should only be first time round) +// if checkpoint == nil { +// if checkpoint, err = a.sm.loadCheckpoint(a.spec.ID); err != nil { +// log.Errorf("%s: Failed to load checkpoint: %s", a.spec.ID, err) +// } +// } +// // If we're not blocked, then grab some more events +// subs := a.sm.subscriptionsForStream(a.spec.ID) +// if err == nil && !a.isBlocked() { +// for _, sub := range subs { +// // We do the reset on the event processing thread, to avoid any concurrency issue. +// // It's just an unsubscribe, which clears the resetRequested flag and sets us stale. +// if sub.resetRequested { +// _ = sub.unsubscribe(ctx, false) +// // Clear any checkpoint +// delete(checkpoint, sub.info.ID) +// } +// stale := sub.filterStale +// if stale && !sub.deleting { +// blockHeight, exists := checkpoint[sub.info.ID] +// if !exists || blockHeight.Cmp(big.NewInt(0)) <= 0 { +// blockHeight, err = sub.setInitialBlockHeight(ctx) +// } else if !sub.inCatchupMode() { +// sub.setCheckpointBlockHeight(blockHeight) +// } +// if err == nil { +// err = sub.restartFilter(ctx, blockHeight) +// } +// } +// if err == nil { +// err = sub.processNewEvents(ctx) +// } +// if err != nil { +// log.Errorf("%s: subscription error: %s", a.spec.ID, err) +// err = nil +// } +// } +// } +// // Record a new checkpoint if needed +// if checkpoint != nil { +// changed := false +// for _, sub := range subs { +// i1 := checkpoint[sub.info.ID] +// i2 := sub.blockHWM() + +// subChanged := i1 == nil || i1.Cmp(&i2) != 0 +// if subChanged { +// log.Debugf("%s: New checkpoint HWM: %s", a.spec.ID, i2.String()) +// } +// changed = changed || subChanged +// checkpoint[sub.info.ID] = new(big.Int).Set(&i2) +// } +// if changed { +// if err = a.sm.storeCheckpoint(a.spec.ID, checkpoint); err != nil { +// log.Errorf("%s: Failed to store checkpoint: %s", a.spec.ID, err) +// } +// } +// } +// // the event poller reacts to notification about a stream update, else it starts +// // another round of polling after completion of the pollingInterval +// select { +// case <-a.updateInterrupt: +// // we were notified by the caller about an ongoing update, no need to continue +// log.Infof("%s: Notified of an ongoing stream update, existing event poller", a.spec.ID) +// a.markAllSubscriptionsStale(ctx) +// return +// case <-time.After(a.pollingInterval): //fall through and continue to the next iteration +// } +// } + +// a.markAllSubscriptionsStale(ctx) + +// } + +// // batchDispatcher is the goroutine that is always available to read new +// // events and form them into batches. Because we can't be sure how many +// // events we'll be dispatched from blocks before the IsBlocked() feedback +// // loop protects us, this logic has to build a list of batches +// func (a *eventStream) batchDispatcher() { +// defer close(a.batchDispatcherDone) +// var currentBatch []*eventData +// var batchStart time.Time +// batchTimeout := time.Duration(a.spec.BatchTimeoutMS) * time.Millisecond +// for { +// // Wait for the next event - if we're in the middle of a batch, we +// // need to cope with a timeout +// log.Debugf("%s: Begin batch dispatcher loop, current batch length: %d", a.spec.ID, len(currentBatch)) +// timeout := false +// if len(currentBatch) > 0 { +// // Existing batch +// timeLeft := time.Until(batchStart.Add(batchTimeout)) +// ctx, cancel := context.WithTimeout(context.Background(), timeLeft) +// select { +// case <-ctx.Done(): +// cancel() +// timeout = true +// case event := <-a.eventStream: +// cancel() +// if event == nil { +// log.Infof("%s: Event stream stopped while waiting for in-flight batch to fill", a.spec.ID) +// return +// } +// currentBatch = append(currentBatch, event) +// case <-a.updateInterrupt: +// // we were notified by the caller about an ongoing update, cancel the timeout ctx and return +// log.Infof("%s: Notified of an ongoing stream update, will not dispatch batch", a.spec.ID) +// cancel() // cancel the ctx which was started to track timeout +// return +// } +// } else { +// // New batch - react to an update notification or process the next set of events from the stream +// select { +// case <-a.updateInterrupt: +// // we were notified by the caller about an ongoing update, return +// log.Infof("%s: Notified of an ongoing stream update, not waiting for new events", a.spec.ID) +// return +// case event := <-a.eventStream: +// if event == nil { +// log.Infof("%s: Event stream stopped", a.spec.ID) +// return +// } +// currentBatch = []*eventData{event} +// log.Infof("%s: New batch length %d", a.spec.ID, len(currentBatch)) +// batchStart = time.Now() +// } +// } +// if timeout || uint64(len(currentBatch)) == a.spec.BatchSize { +// // We are ready to dispatch the batch +// a.batchCond.L.Lock() +// if !timeout { +// a.inFlight++ +// } +// a.batchQueue.PushBack(currentBatch) +// a.batchCond.Broadcast() +// a.batchCond.L.Unlock() +// currentBatch = []*eventData{} +// } else { +// // Just increment in-flight count (batch processor decrements) +// a.batchCond.L.Lock() +// a.inFlight++ +// a.batchCond.L.Unlock() +// } +// } +// } + +// func (a *eventStream) suspendOrStop() bool { +// return a.spec.Suspended || a.stopped +// } + +// // batchProcessor picks up batches from the batchDispatcher, and performs the blocking +// // actions required to perform the action itself. +// // We use a sync.Cond rather than a channel to communicate with this goroutine, as +// // it might be blocked for very large periods of time +// func (a *eventStream) batchProcessor() { +// defer close(a.batchProcessorDone) + +// for { +// // Wait for the next batch, or to be stopped +// a.batchCond.L.Lock() +// for !a.suspendOrStop() && a.batchQueue.Len() == 0 { +// if a.updateInProgress { +// a.batchCond.L.Unlock() +// <-a.updateInterrupt +// // we were notified by the caller about an ongoing update, return +// log.Infof("%s: Notified of an ongoing stream update, existing batch processor", a.spec.ID) +// return +// } else { +// a.batchCond.Wait() +// } +// } +// if a.suspendOrStop() { +// log.Infof("%s: Suspended, returning existing batch processor", a.spec.ID) +// a.batchCond.L.Unlock() +// return +// } +// batchElem := a.batchQueue.Front() +// a.batchCount++ +// batchNumber := a.batchCount +// a.batchQueue.Remove(batchElem) +// a.batchCond.L.Unlock() +// // Process the batch - could block for a very long time, particularly if +// // ErrorHandlingBlock is configured. +// // Track this as an item in the update wait group +// a.processBatch(batchNumber, batchElem.Value.([]*eventData)) +// } +// } + +// // processBatch is the blocking function to process a batch of events +// // It never returns an error, and uses the chosen block/skip ErrorHandling +// // behavior combined with the parameters on the event itself +// func (a *eventStream) processBatch(batchNumber uint64, events []*eventData) { +// if len(events) == 0 { +// return +// } +// processed := false +// attempt := 0 +// for !a.suspendOrStop() && !processed { +// if attempt > 0 { +// select { +// case <-a.updateInterrupt: +// // we were notified by the caller about an ongoing update, no need to continue +// log.Infof("%s: Notified of an ongoing stream update, terminating process batch", a.spec.ID) +// return +// case <-time.After(time.Duration(a.spec.blockedRetryDelaySec()) * time.Second): //fall through and continue +// } +// } +// attempt++ +// log.Infof("%s: Batch %d initiated with %d events. FirstBlock=%s LastBlock=%s", a.spec.ID, batchNumber, len(events), events[0].BlockNumber, events[len(events)-1].BlockNumber) +// err := a.performActionWithRetry(batchNumber, events) +// // If we got an error after all of the internal retries within the event +// // handler failed, then the ErrorHandling strategy kicks in +// processed = (err == nil) +// if !processed { +// log.Errorf("%s: Batch %d attempt %d failed. ErrorHandling=%s BlockedRetryDelay=%ds err=%s", +// a.spec.ID, batchNumber, attempt, a.spec.ErrorHandling, a.spec.BlockedRetryDelaySec, err) +// processed = (a.spec.ErrorHandling == ErrorHandlingSkip) +// } +// } + +// // decrement the in-flight count if we've processed (wouldn't have occurred if we were suspended or stopped) +// a.batchCond.L.Lock() +// if processed { +// a.inFlight -= uint64(len(events)) +// } +// a.batchCond.L.Unlock() + +// // If we were suspended, do not ack the batch +// if a.suspendOrStop() { +// return +// } + +// // Call all the callbacks on the events, so they can update their high water marks +// // If there are multiple events from one SubID, we call it only once with the +// // last message in the batch +// cbs := make(map[string]*eventData) +// for _, event := range events { +// cbs[event.SubID] = event +// } +// for _, event := range cbs { +// event.batchComplete(event) +// } +// } + +// // performActionWithRetry performs an action, with exponential backoff retry up +// // to a given threshold +// func (a *eventStream) performActionWithRetry(batchNumber uint64, events []*eventData) (err error) { +// startTime := time.Now() +// endTime := startTime.Add(time.Duration(a.spec.RetryTimeoutSec) * time.Second) +// delay := a.initialRetryDelay +// var attempt uint64 +// complete := false + +// for !a.suspendOrStop() && !complete { +// if attempt > 0 { +// log.Infof("%s: Waiting %.2fs before re-attempting batch %d", a.spec.ID, delay.Seconds(), batchNumber) +// select { +// case <-a.updateInterrupt: +// // we were notified by the caller about an ongoing update, no need to continue +// log.Infof("%s: Notified of an ongoing stream update, terminating perform action for batch number: %d", a.spec.ID, batchNumber) +// return +// case <-time.After(delay): //fall through and continue +// } +// delay = time.Duration(float64(delay) * a.backoffFactor) +// } +// attempt++ +// err = a.action.attemptBatch(batchNumber, attempt, events) +// complete = err == nil || time.Until(endTime) < 0 +// } +// return err +// } + +// // isAddressSafe checks for local IPs +// func (a *eventStream) isAddressUnsafe(ip *net.IPAddr) bool { +// ip4 := ip.IP.To4() +// return !a.allowPrivateIPs && +// (ip4[0] == 0 || +// ip4[0] >= 224 || +// ip4[0] == 127 || +// ip4[0] == 10 || +// (ip4[0] == 172 && ip4[1] >= 16 && ip4[1] < 32) || +// (ip4[0] == 192 && ip4[1] == 168)) +// } diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 92a1aaa9..fa715684 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -59,13 +59,13 @@ var ( PolicyEngineName = ffc("policyengine.name") ) -var FFCorePrefix config.Prefix +var FFCoreConfig config.Section -var APIPrefix config.Prefix +var APIConfig config.Section -var CorsConfig config.Prefix +var CorsConfig config.Section -var PolicyEngineBasePrefix config.Prefix +var PolicyEngineBaseConfig config.Section func setDefaults() { viper.SetDefault(string(OperationsFullScanPageSize), 100) @@ -90,17 +90,17 @@ func setDefaults() { func Reset() { config.RootConfigReset(setDefaults) - FFCorePrefix = config.NewPluginConfig("ffcore") - wsclient.InitPrefix(FFCorePrefix) - FFCorePrefix.SetDefault(wsclient.WSConfigKeyPath, "/admin/ws") + FFCoreConfig = config.RootSection("ffcore") + wsclient.InitConfig(FFCoreConfig) + FFCoreConfig.SetDefault(wsclient.WSConfigKeyPath, "/admin/ws") - APIPrefix = config.NewPluginConfig("api") - httpserver.InitHTTPConfPrefix(APIPrefix, 5008) + APIConfig = config.RootSection("api") + httpserver.InitHTTPConfig(APIConfig, 5008) - CorsConfig = config.NewPluginConfig("cors") + CorsConfig = config.RootSection("cors") httpserver.InitCORSConfig(CorsConfig) - PolicyEngineBasePrefix = config.NewPluginConfig("policyengine") + PolicyEngineBaseConfig = config.RootSection("policyengine") // policy engines must be registered outside of this package } diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go index c9aab861..c6fc4d63 100644 --- a/internal/tmmsgs/en_config_descriptions.go +++ b/internal/tmmsgs/en_config_descriptions.go @@ -16,9 +16,14 @@ package tmmsgs -import "github.com/hyperledger/firefly-common/pkg/i18n" +import ( + "github.com/hyperledger/firefly-common/pkg/i18n" + "golang.org/x/text/language" +) -var ffc = i18n.FFC +var ffc = func(key, translation, fieldType string) i18n.ConfigMessageKey { + return i18n.FFC(language.AmericanEnglish, key, translation, fieldType) +} //revive:disable var ( diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 57b9426b..c2406a89 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -16,9 +16,14 @@ package tmmsgs -import "github.com/hyperledger/firefly-common/pkg/i18n" +import ( + "github.com/hyperledger/firefly-common/pkg/i18n" + "golang.org/x/text/language" +) -var ffe = i18n.FFE +var ffe = func(key, translation string, statusHint ...int) i18n.ErrorMessageKey { + return i18n.FFE(language.AmericanEnglish, key, translation, statusHint...) +} //revive:disable var ( diff --git a/mocks/policyenginesmocks/policy_engine.go b/mocks/policyenginesmocks/policy_engine.go deleted file mode 100644 index 7943b3bf..00000000 --- a/mocks/policyenginesmocks/policy_engine.go +++ /dev/null @@ -1,45 +0,0 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. - -package policyenginemocks - -import ( - context "context" - - ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - mock "github.com/stretchr/testify/mock" - - policyengines "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" -) - -// PolicyEngine is an autogenerated mock type for the PolicyEngine type -type PolicyEngine struct { - mock.Mock -} - -// Execute provides a mock function with given fields: ctx, cAPI, mtx -func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *policyengine.ManagedTXOutput) (bool, ffcapi.ErrorReason, error) { - ret := _m.Called(ctx, cAPI, mtx) - - var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) bool); ok { - r0 = rf(ctx, cAPI, mtx) - } else { - r0 = ret.Get(0).(bool) - } - - var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) ffcapi.ErrorReason); ok { - r1 = rf(ctx, cAPI, mtx) - } else { - r1 = ret.Get(1).(ffcapi.ErrorReason) - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) error); ok { - r2 = rf(ctx, cAPI, mtx) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index 7c6df302..b00b1fff 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -82,7 +82,7 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { var err error m := &manager{ connector: connector, - ffCoreClient: ffresty.New(ctx, tmconfig.FFCorePrefix), + ffCoreClient: ffresty.New(ctx, tmconfig.FFCoreConfig), fullScanRequests: make(chan bool, 1), nextNonces: make(map[string]uint64), lockedNonces: make(map[string]*lockedNonce), @@ -106,16 +106,16 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { if err != nil { return nil, err } - m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBasePrefix, config.GetString(tmconfig.PolicyEngineName)) + m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBaseConfig, config.GetString(tmconfig.PolicyEngineName)) if err != nil { return nil, err } - wsconfig := wsclient.GenerateConfigFromPrefix(tmconfig.FFCorePrefix) + wsconfig := wsclient.GenerateConfig(tmconfig.FFCoreConfig) m.wsClient, err = wsclient.New(m.ctx, wsconfig, nil, m.startChangeListener) if err != nil { return nil, err } - m.apiServer, err = httpserver.NewHTTPServer(ctx, "api", m.router(), m.apiServerDone, tmconfig.APIPrefix, tmconfig.CorsConfig) + m.apiServer, err = httpserver.NewHTTPServer(ctx, "api", m.router(), m.apiServerDone, tmconfig.APIConfig, tmconfig.CorsConfig) if err != nil { return nil, err } diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 8a92dbb7..5779c490 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -46,25 +46,25 @@ const testManagerName = "unittest" func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...string) (string, *manager, func()) { tmconfig.Reset() - policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) + policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) ffCoreServer := httptest.NewServer(ffCoreHandler) - tmconfig.FFCorePrefix.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", ffCoreServer.Listener.Addr())) + tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", ffCoreServer.Listener.Addr())) ln, err := net.Listen("tcp", "127.0.0.1:0") assert.NoError(t, err) managerPort := strings.Split(ln.Addr().String(), ":")[1] ln.Close() - tmconfig.APIPrefix.Set(httpserver.HTTPConfPort, managerPort) - tmconfig.APIPrefix.Set(httpserver.HTTPConfAddress, "127.0.0.1") + tmconfig.APIConfig.Set(httpserver.HTTPConfPort, managerPort) + tmconfig.APIConfig.Set(httpserver.HTTPConfAddress, "127.0.0.1") config.Set(tmconfig.ManagerName, testManagerName) config.Set(tmconfig.PolicyLoopInterval, "1ms") - tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGasPrice, "223344556677") + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") if len(wsURL) > 0 { config.Set(tmconfig.OperationsChangeListenerEnabled, true) - tmconfig.FFCorePrefix.Set(ffresty.HTTPConfigURL, wsURL[0]) + tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, wsURL[0]) } mm, err := NewManager(context.Background(), &ffcapimocks.API{}) @@ -111,10 +111,10 @@ func TestNewManagerBadHttpConfig(t *testing.T) { tmconfig.Reset() config.Set(tmconfig.ManagerName, "test") - tmconfig.APIPrefix.Set(httpserver.HTTPConfAddress, "::::") + tmconfig.APIConfig.Set(httpserver.HTTPConfAddress, "::::") - policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) - tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGasPrice, "223344556677") + policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") _, err := NewManager(context.Background(), nil) assert.Regexp(t, "FF00151", err) @@ -125,10 +125,10 @@ func TestNewManagerFireFlyURLConfig(t *testing.T) { tmconfig.Reset() config.Set(tmconfig.ManagerName, "test") - tmconfig.FFCorePrefix.Set(ffresty.HTTPConfigURL, ":::!badurl") + tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, ":::!badurl") - policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) - tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGasPrice, "223344556677") + policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") _, err := NewManager(context.Background(), nil) assert.Regexp(t, "FF00149", err) diff --git a/pkg/fftm/routes.go b/pkg/fftm/send_tx.go similarity index 100% rename from pkg/fftm/routes.go rename to pkg/fftm/send_tx.go diff --git a/pkg/policyengines/registry.go b/pkg/policyengines/registry.go index 8c0656f2..83b91c6e 100644 --- a/pkg/policyengines/registry.go +++ b/pkg/policyengines/registry.go @@ -27,23 +27,23 @@ import ( var policyEngines = make(map[string]Factory) -func NewPolicyEngine(ctx context.Context, basePrefix config.Prefix, name string) (policyengine.PolicyEngine, error) { +func NewPolicyEngine(ctx context.Context, baseConfig config.Section, name string) (policyengine.PolicyEngine, error) { factory, ok := policyEngines[name] if !ok { return nil, i18n.NewError(ctx, tmmsgs.MsgPolicyEngineNotRegistered, name) } - return factory.NewPolicyEngine(ctx, basePrefix.SubPrefix(name)) + return factory.NewPolicyEngine(ctx, baseConfig.SubSection(name)) } type Factory interface { Name() string - InitPrefix(prefix config.Prefix) - NewPolicyEngine(ctx context.Context, prefix config.Prefix) (policyengine.PolicyEngine, error) + InitConfig(conf config.Section) + NewPolicyEngine(ctx context.Context, conf config.Section) (policyengine.PolicyEngine, error) } -func RegisterEngine(basePrefix config.Prefix, factory Factory) string { +func RegisterEngine(baseConfig config.Section, factory Factory) string { name := factory.Name() policyEngines[name] = factory - factory.InitPrefix(basePrefix.SubPrefix(name)) + factory.InitConfig(baseConfig.SubSection(name)) return name } diff --git a/pkg/policyengines/registry_test.go b/pkg/policyengines/registry_test.go index c99c7b23..dfbfecc4 100644 --- a/pkg/policyengines/registry_test.go +++ b/pkg/policyengines/registry_test.go @@ -28,14 +28,14 @@ import ( func TestRegistry(t *testing.T) { tmconfig.Reset() - RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) + RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) - tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGasPrice, "12345") - p, err := NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBasePrefix, "simple") + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "12345") + p, err := NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBaseConfig, "simple") assert.NotNil(t, p) assert.NoError(t, err) - p, err = NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBasePrefix, "bob") + p, err = NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBaseConfig, "bob") assert.Nil(t, p) assert.Regexp(t, "FF21019", err) diff --git a/pkg/policyengines/simple/config.go b/pkg/policyengines/simple/config.go index 4dcfcbd9..d0c3f76e 100644 --- a/pkg/policyengines/simple/config.go +++ b/pkg/policyengines/simple/config.go @@ -26,7 +26,7 @@ import ( const ( FixedGasPrice = "fixedGasPrice" // when not using a gas station - will be treated as a raw JSON string, so can be numeric 123, or string "123", or object {"maxPriorityFeePerGas":123}) WarnInterval = "warnInterval" // warnings will be written to the log at this interval if mining has not occurred - GasOraclePrefix = "gasOracle" + GasOracleConfig = "gasOracle" GasOracleMode = "mode" GasOracleMethod = "method" GasOracleTemplate = "template" @@ -46,15 +46,15 @@ const ( defaultGasOracleMode = GasOracleModeDisabled ) -func (f *PolicyEngineFactory) InitPrefix(prefix config.Prefix) { - prefix.AddKnownKey(FixedGasPrice) - prefix.AddKnownKey(WarnInterval, defaultWarnInterval) +func (f *PolicyEngineFactory) InitConfig(conf config.Section) { + conf.AddKnownKey(FixedGasPrice) + conf.AddKnownKey(WarnInterval, defaultWarnInterval) - gasOraclePrefix := prefix.SubPrefix(GasOraclePrefix) - ffresty.InitPrefix(gasOraclePrefix) - gasOraclePrefix.AddKnownKey(GasOracleMethod, defaultGasOracleMethod) - gasOraclePrefix.AddKnownKey(GasOracleMode, defaultGasOracleMode) - gasOraclePrefix.AddKnownKey(GasOracleQueryInterval, defaultGasOracleQueryInterval) - gasOraclePrefix.AddKnownKey(GasOracleTemplate) + gasOracleConfig := conf.SubSection(GasOracleConfig) + ffresty.InitConfig(gasOracleConfig) + gasOracleConfig.AddKnownKey(GasOracleMethod, defaultGasOracleMethod) + gasOracleConfig.AddKnownKey(GasOracleMode, defaultGasOracleMode) + gasOracleConfig.AddKnownKey(GasOracleQueryInterval, defaultGasOracleQueryInterval) + gasOracleConfig.AddKnownKey(GasOracleTemplate) } diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index 10f2143a..abc30f82 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -44,22 +44,22 @@ func (f *PolicyEngineFactory) Name() string { // - It uses a public gas estimation // - It submits the transaction once // - It logs errors transactions breach certain configured thresholds of staleness -func (f *PolicyEngineFactory) NewPolicyEngine(ctx context.Context, prefix config.Prefix) (pe policyengine.PolicyEngine, err error) { - gasOraclePrefix := prefix.SubPrefix(GasOraclePrefix) +func (f *PolicyEngineFactory) NewPolicyEngine(ctx context.Context, conf config.Section) (pe policyengine.PolicyEngine, err error) { + gasOracleConfig := conf.SubSection(GasOracleConfig) p := &simplePolicyEngine{ - warnInterval: prefix.GetDuration(WarnInterval), - fixedGasPrice: fftypes.JSONAnyPtr(prefix.GetString(FixedGasPrice)), + warnInterval: conf.GetDuration(WarnInterval), + fixedGasPrice: fftypes.JSONAnyPtr(conf.GetString(FixedGasPrice)), - gasOracleMethod: gasOraclePrefix.GetString(GasOracleMethod), - gasOracleQueryInterval: gasOraclePrefix.GetDuration(GasOracleQueryInterval), - gasOracleMode: gasOraclePrefix.GetString(GasOracleMode), + gasOracleMethod: gasOracleConfig.GetString(GasOracleMethod), + gasOracleQueryInterval: gasOracleConfig.GetDuration(GasOracleQueryInterval), + gasOracleMode: gasOracleConfig.GetString(GasOracleMode), } switch p.gasOracleMode { case GasOracleModeConnector: // No initialization required case GasOracleModeRESTAPI: - p.gasOracleClient = ffresty.New(ctx, gasOraclePrefix) - templateString := gasOraclePrefix.GetString(GasOracleTemplate) + p.gasOracleClient = ffresty.New(ctx, gasOracleConfig) + templateString := gasOracleConfig.GetString(GasOracleTemplate) if templateString == "" { return nil, i18n.NewError(ctx, tmmsgs.MsgMissingGOTemplate) } diff --git a/pkg/policyengines/simple/simple_policy_engine_test.go b/pkg/policyengines/simple/simple_policy_engine_test.go index 51c6b59b..1589a511 100644 --- a/pkg/policyengines/simple/simple_policy_engine_test.go +++ b/pkg/policyengines/simple/simple_policy_engine_test.go @@ -35,30 +35,30 @@ import ( "github.com/stretchr/testify/mock" ) -func newTestPolicyEngineFactory(t *testing.T) (*PolicyEngineFactory, config.Prefix) { +func newTestPolicyEngineFactory(t *testing.T) (*PolicyEngineFactory, config.Section) { tmconfig.Reset() - prefix := config.NewPluginConfig("unittest.simple") + conf := config.RootSection("unittest.simple") f := &PolicyEngineFactory{} - f.InitPrefix(prefix) + f.InitConfig(conf) assert.Equal(t, "simple", f.Name()) - return f, prefix + return f, conf } func TestMissingGasConfig(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeDisabled) - _, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeDisabled) + _, err := f.NewPolicyEngine(context.Background(), conf) assert.Regexp(t, "FF21020", err) } func TestFixedGasPriceOK(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeDisabled) - prefix.Set(FixedGasPrice, `{ + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeDisabled) + conf.Set(FixedGasPrice, `{ "maxPriorityFee":32.146027800733336, "maxFee":32.14602781673334 }`) - p, err := f.NewPolicyEngine(context.Background(), prefix) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ @@ -122,11 +122,11 @@ func TestGasOracleSendOK(t *testing.T) { }`)) })) - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, `{"unit":"gwei","value":{{ .standard.maxPriorityFee }}}`) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, `{"unit":"gwei","value":{{ .standard.maxPriorityFee }}}`) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ @@ -171,9 +171,9 @@ func TestGasOracleSendOK(t *testing.T) { func TestConnectorGasOracleSendOK(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeConnector) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeConnector) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ @@ -218,9 +218,9 @@ func TestConnectorGasOracleSendOK(t *testing.T) { func TestConnectorGasOracleFail(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeConnector) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeConnector) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ @@ -257,11 +257,11 @@ func TestGasOracleSendFail(t *testing.T) { })) defer server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, "{{ . }}") - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, "{{ . }}") + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ @@ -287,10 +287,10 @@ func TestGasOracleMissingTemplate(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - _, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + _, err := f.NewPolicyEngine(context.Background(), conf) assert.Regexp(t, "FF21024", err) } @@ -300,11 +300,11 @@ func TestGasOracleBadTemplate(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, "{{ !!! wrong") - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - _, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, "{{ !!! wrong") + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + _, err := f.NewPolicyEngine(context.Background(), conf) assert.Regexp(t, "FF21025", err) } @@ -317,11 +317,11 @@ func TestGasOracleTemplateExecuteFail(t *testing.T) { })) defer server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, "{{ .wrong.thing | len }}") - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, "{{ .wrong.thing | len }}") + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ @@ -347,11 +347,11 @@ func TestGasOracleNonJSON(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, "{{ . }}") - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, "{{ . }}") + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ @@ -380,11 +380,11 @@ func TestTXSendFail(t *testing.T) { })) defer server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, "{{ . }}") - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, "{{ . }}") + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ @@ -407,9 +407,9 @@ func TestTXSendFail(t *testing.T) { } func TestWarnStaleWarningCannotParse(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.Set(FixedGasPrice, `12345`) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.Set(FixedGasPrice, `12345`) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) @@ -438,9 +438,9 @@ func TestWarnStaleWarningCannotParse(t *testing.T) { } func TestWarnStaleAdditionalWarning(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.Set(FixedGasPrice, `12345`) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.Set(FixedGasPrice, `12345`) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) @@ -471,10 +471,10 @@ func TestWarnStaleAdditionalWarning(t *testing.T) { } func TestWarnStaleNoWarning(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.Set(FixedGasPrice, `12345`) - prefix.Set(WarnInterval, "100s") - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.Set(FixedGasPrice, `12345`) + conf.Set(WarnInterval, "100s") + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) @@ -504,10 +504,10 @@ func TestWarnStaleNoWarning(t *testing.T) { } func TestNoOpWithReceipt(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.Set(FixedGasPrice, `12345`) - prefix.Set(WarnInterval, "100s") - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.Set(FixedGasPrice, `12345`) + conf.Set(WarnInterval, "100s") + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) submitTime := fftypes.Now() From e88d412774c6f3e93d76c9acd394beead41ac01e Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 3 Jun 2022 16:09:28 -0400 Subject: [PATCH 05/95] Interim commit Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 2 + internal/events/eventstream.go | 969 +++++++--------------------- internal/events/listener.go | 44 ++ internal/events/webhooks.go | 17 + internal/events/websockets.go | 17 + internal/tmmsgs/en_error_messges.go | 2 + pkg/ffcapi/api.go | 10 +- pkg/ffcapi/event_listener_add.go | 8 +- pkg/ffcapi/method_call.go | 2 +- pkg/ffcapi/transaction_receipt.go | 10 +- pkg/fftm/api_types.go | 160 +++++ 11 files changed, 487 insertions(+), 754 deletions(-) create mode 100644 internal/events/listener.go create mode 100644 internal/events/webhooks.go create mode 100644 internal/events/websockets.go create mode 100644 pkg/fftm/api_types.go diff --git a/.vscode/settings.json b/.vscode/settings.json index c77c1c0d..be2bf419 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "devdocs", "Devel", "distmode", + "estype", "ethconnect", "eventstream", "fabconnect", @@ -64,6 +65,7 @@ "upgrader", "upserts", "Warnf", + "whconfig", "wsclient", "wsconfig" ], diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 8b86ce3b..1c59d27d 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -16,742 +16,233 @@ package events -// import ( -// "container/list" -// "context" -// "math/big" -// "net" -// "net/url" -// "strconv" -// "strings" -// "sync" -// "time" - -// "github.com/hyperledger/firefly-common/pkg/fftypes" -// "github.com/hyperledger/firefly-ethconnect/internal/auth" -// "github.com/hyperledger/firefly-ethconnect/internal/errors" -// "github.com/hyperledger/firefly-ethconnect/internal/ws" -// "github.com/hyperledger/firefly/pkg/fftypes" -// ethbinding "github.com/kaleido-io/ethbinding/pkg" - -// lru "github.com/hashicorp/golang-lru" -// log "github.com/sirupsen/logrus" -// ) - -// type DistributionMode fftypes.FFEnum - -// const ( -// DistributionModeBroadcast = fftypes.FFEnumValue("distmode", "broadcast") -// DistributionModeWLD = fftypes.FFEnumValue("distmode", "workloadDistribution") -// ) - -// // EventStream is the -// type EventStream struct { -// ID string `ffstruct:"eventstream" json:"id"` -// Created *fftypes.FFTime `ffstruct:"eventstream" json:"created"` -// Name string `ffstruct:"eventstream" json:"name,omitempty"` -// Path string `ffstruct:"eventstream" json:"path"` -// Suspended bool `ffstruct:"eventstream" json:"suspended"` -// Type string `ffstruct:"eventstream" json:"type,omitempty"` -// BatchSize *uint64 `ffstruct:"eventstream" json:"batchSize,omitempty"` -// BatchTimeoutMS *uint64 `ffstruct:"eventstream" json:"batchTimeoutMS,omitempty"` -// ErrorHandling string `ffstruct:"eventstream" json:"errorHandling,omitempty"` -// RetryTimeoutSec uint64 `ffstruct:"eventstream" json:"retryTimeoutSec,omitempty"` -// BlockedRetryDelaySec *uint64 `ffstruct:"eventstream" json:"blockedRetryDelaySec,omitempty"` -// Webhook *WebhookActionInfo `ffstruct:"eventstream" json:"webhook,omitempty"` -// WebSocket *WebSocketActionInfo `ffstruct:"eventstream" json:"websocket,omitempty"` -// Timestamps bool `ffstruct:"eventstream" json:"timestamps,omitempty"` -// Inputs bool `ffstruct:"eventstream" json:"inputs,omitempty"` -// } - -// type WebhookActionInfo struct { -// URL string `json:"url,omitempty"` -// Headers map[string]string `json:"headers,omitempty"` -// TLSkipHostVerify bool `json:"tlsSkipHostVerify,omitempty"` -// RequestTimeoutSec uint32 `json:"requestTimeoutSec,omitempty"` -// } - -// type WebSocketActionInfo struct { -// Topic string `json:"topic,omitempty"` -// DistributionMode DistributionMode `json:"distributionMode,omitempty"` -// } - -// type eventStream struct { -// sm subscriptionManager -// allowPrivateIPs bool -// spec *StreamInfo -// eventStream chan *eventData -// stopped bool -// pollingInterval time.Duration -// inFlight uint64 -// batchCond *sync.Cond -// batchQueue *list.List -// batchCount uint64 -// initialRetryDelay time.Duration -// backoffFactor float64 -// updateInProgress bool -// updateInterrupt chan struct{} // a zero-sized struct used only for signaling (hand rolled alternative to context) -// blockTimestampCache *lru.Cache -// action eventStreamAction -// wsChannels ws.WebSocketChannels -// decimalTransactionIndex bool - -// eventPollerDone chan struct{} -// batchProcessorDone chan struct{} -// batchDispatcherDone chan struct{} -// } - -// type eventStreamAction interface { -// attemptBatch(batchNumber, attempt uint64, events []*eventData) error -// } - -// func validateWebSocket(w *webSocketActionInfo) error { -// if w.DistributionMode != "" && w.DistributionMode != DistributionModeBroadcast && w.DistributionMode != DistributionModeWLD { -// return errors.Errorf(errors.EventStreamsInvalidDistributionMode, w.DistributionMode) -// } -// return nil -// } - -// // newEventStream constructor verifies the action is correct, kicks -// // off the event batch processor, and blockHWM will be -// // initialied to that supplied (zero on initial, or the -// // value from the checkpoint) -// func newEventStream(sm subscriptionManager, spec *StreamInfo, wsChannels ws.WebSocketChannels) (a *eventStream, err error) { -// if spec == nil || spec.GetID() == "" { -// return nil, errors.Errorf(errors.EventStreamsNoID) -// } - -// if spec.BatchSize == 0 { -// spec.BatchSize = 1 -// } else if spec.BatchSize > MaxBatchSize { -// spec.BatchSize = MaxBatchSize -// } -// if spec.BatchTimeoutMS == 0 { -// spec.BatchTimeoutMS = 5000 -// } -// if strings.ToLower(spec.ErrorHandling) == ErrorHandlingBlock { -// spec.ErrorHandling = ErrorHandlingBlock -// } else { -// spec.ErrorHandling = ErrorHandlingSkip -// } -// if spec.TimestampCacheSize == 0 { -// spec.TimestampCacheSize = DefaultTimestampCacheSize -// } - -// a = &eventStream{ -// sm: sm, -// spec: spec, -// allowPrivateIPs: sm.config().WebhooksAllowPrivateIPs, -// eventStream: make(chan *eventData), -// batchCond: sync.NewCond(&sync.Mutex{}), -// batchQueue: list.New(), -// initialRetryDelay: DefaultExponentialBackoffInitial, -// backoffFactor: DefaultExponentialBackoffFactor, -// pollingInterval: time.Duration(sm.config().EventPollingIntervalSec) * time.Second, -// wsChannels: wsChannels, -// decimalTransactionIndex: sm.config().DecimalTransactionIndex, -// } - -// if a.blockTimestampCache, err = lru.New(spec.TimestampCacheSize); err != nil { -// return nil, errors.Errorf(errors.EventStreamsCreateStreamResourceErr, err) -// } -// if a.pollingInterval == 0 { -// // Let's us do this from UTs, without exposing it -// a.pollingInterval = 10 * time.Millisecond -// } - -// spec.Type = strings.ToLower(spec.Type) -// switch spec.Type { -// case "webhook": -// if a.action, err = newWebhookAction(a, spec.Webhook); err != nil { -// return nil, err -// } -// case "websocket": - -// if spec.WebSocket != nil { -// if err := validateWebSocket(spec.WebSocket); err != nil { -// return nil, err -// } -// } - -// if a.action, err = newWebSocketAction(a, spec.WebSocket); err != nil { -// return nil, err -// } -// default: -// return nil, errors.Errorf(errors.EventStreamsInvalidActionType, spec.Type) -// } - -// a.startEventHandlers(false) -// return a, nil -// } - -// // formatTransactionIndex honors the configuration for whether transactionIndex should be an `0x` -// // hex string on the return. This was a bug in earlier version of ethconnect, and an option -// // is provided to restore the old behavior in case an application was depending on it. -// func (a *eventStream) formatTransactionIndex(txIndex ethbinding.HexUint) string { -// if a.decimalTransactionIndex { -// return strconv.FormatUint(uint64(txIndex), 10) -// } -// return txIndex.String() -// } - -// // helper to kick off go routines and any tracking entities -// func (a *eventStream) startEventHandlers(resume bool) { -// // create a context that can be used to indicate an update to the eventstream -// a.updateInterrupt = make(chan struct{}) -// a.eventPollerDone = make(chan struct{}) -// go a.eventPoller() -// a.batchProcessorDone = make(chan struct{}) -// go a.batchProcessor() -// // For a pause/resume, the batch dispatcher goroutine is not terminated, hence no need to start it -// if !resume { -// a.batchDispatcherDone = make(chan struct{}) -// go a.batchDispatcher() -// } -// } - -// // GetID returns the ID (for sorting) -// func (spec *StreamInfo) GetID() string { -// return spec.ID -// } - -// func (spec *StreamInfo) blockedRetryDelaySec() uint64 { -// if spec.BlockedRetryDelaySec == nil { -// if spec.TypoReryDelaySec > 0 { -// return spec.TypoReryDelaySec -// } else { -// return 30 -// } -// } -// return *spec.BlockedRetryDelaySec -// } - -// // preUpdateStream sets a flag to indicate updateInProgress and wakes up goroutines waiting on condition variable -// func (a *eventStream) preUpdateStream() error { -// a.batchCond.L.Lock() -// if a.updateInProgress { -// a.batchCond.L.Unlock() -// return errors.Errorf(errors.EventStreamsUpdateAlreadyInProgress) -// } -// a.updateInProgress = true -// // close the updateInterrupt channel so that the event handler go routines can be woken up -// close(a.updateInterrupt) -// a.batchCond.Broadcast() -// a.batchCond.L.Unlock() - -// a.drainBlockConfirmationManager() - -// return nil -// } - -// func (a *eventStream) drainBlockConfirmationManager() { -// bcm := a.sm.confirmationManager() -// if bcm != nil { -// n := &bcmNotification{ -// nType: bcmStopStream, -// eventStream: a, -// complete: make(chan struct{}), -// } -// bcm.notify(n) -// <-n.complete -// } -// } - -// // postUpdateStream resets flags and kicks off a fresh round of handler go routines -// func (a *eventStream) postUpdateStream() { -// a.batchCond.L.Lock() -// a.startEventHandlers(false) -// a.updateInProgress = false -// a.inFlight = 0 -// a.batchCond.L.Unlock() -// } - -// func (a *eventStream) checkUpdate(newSpec *StreamInfo) (updatedSpec *StreamInfo, err error) { - -// // setUpdated marks that there is a change, and creates a copied object -// specCopy := *a.spec -// setUpdated := func() *StreamInfo { -// if updatedSpec == nil { -// updatedSpec = &specCopy -// } -// return updatedSpec -// } - -// if newSpec.Type != "" && newSpec.Type != specCopy.Type { -// return nil, errors.Errorf(errors.EventStreamsCannotUpdateType) -// } -// if specCopy.Type == "webhook" && newSpec.Webhook != nil { -// if newSpec.Webhook.RequestTimeoutSec != 0 && newSpec.Webhook.RequestTimeoutSec != specCopy.Webhook.RequestTimeoutSec { -// setUpdated().Webhook.RequestTimeoutSec = newSpec.Webhook.RequestTimeoutSec -// } -// if newSpec.Webhook.TLSkipHostVerify != specCopy.Webhook.TLSkipHostVerify { -// setUpdated().Webhook.TLSkipHostVerify = newSpec.Webhook.TLSkipHostVerify -// } -// if newSpec.Webhook.URL != "" && newSpec.Webhook.URL != specCopy.Webhook.URL { -// if _, err = url.Parse(newSpec.Webhook.URL); err != nil { -// return nil, errors.Errorf(errors.EventStreamsWebhookInvalidURL) -// } -// setUpdated().Webhook.URL = newSpec.Webhook.URL -// } -// for k, v := range newSpec.Webhook.Headers { -// if specCopy.Webhook.Headers == nil || specCopy.Webhook.Headers[k] != v { -// setUpdated().Webhook.Headers = newSpec.Webhook.Headers -// break -// } -// } -// } -// if specCopy.Type == "websocket" && newSpec.WebSocket != nil { -// if newSpec.WebSocket.Topic != specCopy.WebSocket.Topic { -// setUpdated().WebSocket.Topic = newSpec.WebSocket.Topic -// } -// if newSpec.WebSocket.DistributionMode != specCopy.WebSocket.DistributionMode { -// setUpdated().WebSocket.DistributionMode = newSpec.WebSocket.DistributionMode -// } -// // Validate if we changed it -// if updatedSpec != nil { -// if err := validateWebSocket(newSpec.WebSocket); err != nil { -// return nil, err -// } -// } -// } - -// if specCopy.BatchSize != newSpec.BatchSize && newSpec.BatchSize != 0 && newSpec.BatchSize < MaxBatchSize { -// setUpdated().BatchSize = newSpec.BatchSize -// } -// if specCopy.BatchTimeoutMS != newSpec.BatchTimeoutMS && newSpec.BatchTimeoutMS != 0 { -// setUpdated().BatchTimeoutMS = newSpec.BatchTimeoutMS -// } -// if newSpec.BlockedRetryDelaySec != nil && specCopy.blockedRetryDelaySec() != newSpec.blockedRetryDelaySec() { -// blockedRetryDelaySec := newSpec.blockedRetryDelaySec() -// setUpdated().BlockedRetryDelaySec = &blockedRetryDelaySec -// } -// if newSpec.ErrorHandling != "" && newSpec.ErrorHandling != specCopy.ErrorHandling { -// if strings.ToLower(newSpec.ErrorHandling) == ErrorHandlingBlock { -// setUpdated().ErrorHandling = ErrorHandlingBlock -// } else { -// setUpdated().ErrorHandling = ErrorHandlingSkip -// } -// } -// if newSpec.Name != "" && specCopy.Name != newSpec.Name { -// setUpdated().Name = newSpec.Name -// } -// if specCopy.Timestamps != newSpec.Timestamps { -// setUpdated().Timestamps = newSpec.Timestamps -// } -// if specCopy.Inputs != newSpec.Inputs { -// setUpdated().Inputs = newSpec.Inputs -// } - -// // Return a non-nil object ONLY if there's a change -// return updatedSpec, nil -// } - -// // update modifies an existing eventStream -// func (a *eventStream) update(newSpec *StreamInfo) (spec *StreamInfo, err error) { -// log.Infof("%s: Update event stream", a.spec.ID) -// updatedSpec, err := a.checkUpdate(newSpec) -// if err != nil { -// return nil, err -// } -// if updatedSpec == nil { -// log.Infof("%s: No change", a.spec.ID) -// return a.spec, nil -// } - -// // set a flag to indicate updateInProgress -// // For any go routines that are Wait() ing on the eventListener, wake them up -// if err := a.preUpdateStream(); err != nil { -// return nil, err -// } -// a.spec = updatedSpec -// // wait for the poked goroutines to finish up -// <-a.eventPollerDone -// <-a.batchProcessorDone -// <-a.batchDispatcherDone -// defer a.postUpdateStream() -// return a.spec, nil -// } - -// // HandleEvent is the entry point for the stream from the event detection logic -// func (a *eventStream) handleEvent(event *eventData) { -// // Does nothing more than add it to the batch, to be picked up -// // by the batchDispatcher -// select { -// case a.eventStream <- event: -// case <-a.batchDispatcherDone: -// // If the dispatcher isn't running, then there's no problem - the HWM won't get updated -// log.Infof("Event arrived while event stream shutting down") -// } -// } - -// // stop is a lazy stop, that marks a flag for the batch goroutine to pick up -// func (a *eventStream) stop(wait bool) { -// a.batchCond.L.Lock() -// if !a.stopped { -// a.stopped = true -// if a.updateInterrupt != nil { -// close(a.updateInterrupt) -// } -// } -// a.batchCond.Broadcast() -// a.batchCond.L.Unlock() -// if wait { -// <-a.eventPollerDone -// <-a.batchProcessorDone -// <-a.batchDispatcherDone -// } -// } - -// // suspend only stops the dispatcher, pushing back as if we're in blocking mode -// func (a *eventStream) suspend() { -// a.batchCond.L.Lock() -// a.spec.Suspended = true -// a.batchCond.Broadcast() -// a.batchCond.L.Unlock() -// a.drainBlockConfirmationManager() -// <-a.eventPollerDone -// <-a.batchProcessorDone -// } - -// func isChannelDone(c chan struct{}) bool { -// var isDone bool -// select { -// case <-c: -// isDone = true -// default: -// isDone = false -// } -// return isDone -// } - -// // resume resumes the dispatcher -// func (a *eventStream) resume() error { -// a.batchCond.L.Lock() -// defer a.batchCond.L.Unlock() - -// if !isChannelDone(a.batchProcessorDone) || !isChannelDone(a.eventPollerDone) { -// return errors.Errorf(errors.EventStreamsWebhookResumeActive, a.spec.Suspended) -// } -// a.spec.Suspended = false - -// a.startEventHandlers(true) -// a.batchCond.Broadcast() -// return nil -// } - -// // isBlocked protect us from polling for more events when the stream is blocked. -// // Can happen regardless of whether the error handling is -// // block or skip. It's just with skip we eventually move onto new messages -// // after the retries etc. are complete -// func (a *eventStream) isBlocked() bool { -// a.batchCond.L.Lock() -// inFlight := a.inFlight -// isBlocked := inFlight >= a.spec.BatchSize -// a.batchCond.L.Unlock() -// if isBlocked { -// log.Warnf("%s: Is currently blocked. InFlight=%d BatchSize=%d", a.spec.ID, inFlight, a.spec.BatchSize) -// } else if inFlight > 0 { -// log.Debugf("%s: InFlight=%d BatchSize=%d", a.spec.ID, inFlight, a.spec.BatchSize) -// } -// return isBlocked -// } - -// func (a *eventStream) markAllSubscriptionsStale(ctx context.Context) { -// // Mark all subscriptions stale, so they will re-start from the checkpoint if/when we re-run the poller -// subs := a.sm.subscriptionsForStream(a.spec.ID) -// for _, sub := range subs { -// sub.markFilterStale(ctx, true) -// } -// } - -// // eventPoller checks every few seconds against the ethereum node for any -// // new events on the subscriptions that are registered for this stream -// func (a *eventStream) eventPoller() { -// defer close(a.eventPollerDone) - -// ctx := auth.NewSystemAuthContext() -// var checkpoint map[string]*big.Int -// for !a.suspendOrStop() { -// var err error -// // Load the checkpoint (should only be first time round) -// if checkpoint == nil { -// if checkpoint, err = a.sm.loadCheckpoint(a.spec.ID); err != nil { -// log.Errorf("%s: Failed to load checkpoint: %s", a.spec.ID, err) -// } -// } -// // If we're not blocked, then grab some more events -// subs := a.sm.subscriptionsForStream(a.spec.ID) -// if err == nil && !a.isBlocked() { -// for _, sub := range subs { -// // We do the reset on the event processing thread, to avoid any concurrency issue. -// // It's just an unsubscribe, which clears the resetRequested flag and sets us stale. -// if sub.resetRequested { -// _ = sub.unsubscribe(ctx, false) -// // Clear any checkpoint -// delete(checkpoint, sub.info.ID) -// } -// stale := sub.filterStale -// if stale && !sub.deleting { -// blockHeight, exists := checkpoint[sub.info.ID] -// if !exists || blockHeight.Cmp(big.NewInt(0)) <= 0 { -// blockHeight, err = sub.setInitialBlockHeight(ctx) -// } else if !sub.inCatchupMode() { -// sub.setCheckpointBlockHeight(blockHeight) -// } -// if err == nil { -// err = sub.restartFilter(ctx, blockHeight) -// } -// } -// if err == nil { -// err = sub.processNewEvents(ctx) -// } -// if err != nil { -// log.Errorf("%s: subscription error: %s", a.spec.ID, err) -// err = nil -// } -// } -// } -// // Record a new checkpoint if needed -// if checkpoint != nil { -// changed := false -// for _, sub := range subs { -// i1 := checkpoint[sub.info.ID] -// i2 := sub.blockHWM() - -// subChanged := i1 == nil || i1.Cmp(&i2) != 0 -// if subChanged { -// log.Debugf("%s: New checkpoint HWM: %s", a.spec.ID, i2.String()) -// } -// changed = changed || subChanged -// checkpoint[sub.info.ID] = new(big.Int).Set(&i2) -// } -// if changed { -// if err = a.sm.storeCheckpoint(a.spec.ID, checkpoint); err != nil { -// log.Errorf("%s: Failed to store checkpoint: %s", a.spec.ID, err) -// } -// } -// } -// // the event poller reacts to notification about a stream update, else it starts -// // another round of polling after completion of the pollingInterval -// select { -// case <-a.updateInterrupt: -// // we were notified by the caller about an ongoing update, no need to continue -// log.Infof("%s: Notified of an ongoing stream update, existing event poller", a.spec.ID) -// a.markAllSubscriptionsStale(ctx) -// return -// case <-time.After(a.pollingInterval): //fall through and continue to the next iteration -// } -// } - -// a.markAllSubscriptionsStale(ctx) - -// } - -// // batchDispatcher is the goroutine that is always available to read new -// // events and form them into batches. Because we can't be sure how many -// // events we'll be dispatched from blocks before the IsBlocked() feedback -// // loop protects us, this logic has to build a list of batches -// func (a *eventStream) batchDispatcher() { -// defer close(a.batchDispatcherDone) -// var currentBatch []*eventData -// var batchStart time.Time -// batchTimeout := time.Duration(a.spec.BatchTimeoutMS) * time.Millisecond -// for { -// // Wait for the next event - if we're in the middle of a batch, we -// // need to cope with a timeout -// log.Debugf("%s: Begin batch dispatcher loop, current batch length: %d", a.spec.ID, len(currentBatch)) -// timeout := false -// if len(currentBatch) > 0 { -// // Existing batch -// timeLeft := time.Until(batchStart.Add(batchTimeout)) -// ctx, cancel := context.WithTimeout(context.Background(), timeLeft) -// select { -// case <-ctx.Done(): -// cancel() -// timeout = true -// case event := <-a.eventStream: -// cancel() -// if event == nil { -// log.Infof("%s: Event stream stopped while waiting for in-flight batch to fill", a.spec.ID) -// return -// } -// currentBatch = append(currentBatch, event) -// case <-a.updateInterrupt: -// // we were notified by the caller about an ongoing update, cancel the timeout ctx and return -// log.Infof("%s: Notified of an ongoing stream update, will not dispatch batch", a.spec.ID) -// cancel() // cancel the ctx which was started to track timeout -// return -// } -// } else { -// // New batch - react to an update notification or process the next set of events from the stream -// select { -// case <-a.updateInterrupt: -// // we were notified by the caller about an ongoing update, return -// log.Infof("%s: Notified of an ongoing stream update, not waiting for new events", a.spec.ID) -// return -// case event := <-a.eventStream: -// if event == nil { -// log.Infof("%s: Event stream stopped", a.spec.ID) -// return -// } -// currentBatch = []*eventData{event} -// log.Infof("%s: New batch length %d", a.spec.ID, len(currentBatch)) -// batchStart = time.Now() -// } -// } -// if timeout || uint64(len(currentBatch)) == a.spec.BatchSize { -// // We are ready to dispatch the batch -// a.batchCond.L.Lock() -// if !timeout { -// a.inFlight++ -// } -// a.batchQueue.PushBack(currentBatch) -// a.batchCond.Broadcast() -// a.batchCond.L.Unlock() -// currentBatch = []*eventData{} -// } else { -// // Just increment in-flight count (batch processor decrements) -// a.batchCond.L.Lock() -// a.inFlight++ -// a.batchCond.L.Unlock() -// } -// } -// } - -// func (a *eventStream) suspendOrStop() bool { -// return a.spec.Suspended || a.stopped -// } - -// // batchProcessor picks up batches from the batchDispatcher, and performs the blocking -// // actions required to perform the action itself. -// // We use a sync.Cond rather than a channel to communicate with this goroutine, as -// // it might be blocked for very large periods of time -// func (a *eventStream) batchProcessor() { -// defer close(a.batchProcessorDone) - -// for { -// // Wait for the next batch, or to be stopped -// a.batchCond.L.Lock() -// for !a.suspendOrStop() && a.batchQueue.Len() == 0 { -// if a.updateInProgress { -// a.batchCond.L.Unlock() -// <-a.updateInterrupt -// // we were notified by the caller about an ongoing update, return -// log.Infof("%s: Notified of an ongoing stream update, existing batch processor", a.spec.ID) -// return -// } else { -// a.batchCond.Wait() -// } -// } -// if a.suspendOrStop() { -// log.Infof("%s: Suspended, returning existing batch processor", a.spec.ID) -// a.batchCond.L.Unlock() -// return -// } -// batchElem := a.batchQueue.Front() -// a.batchCount++ -// batchNumber := a.batchCount -// a.batchQueue.Remove(batchElem) -// a.batchCond.L.Unlock() -// // Process the batch - could block for a very long time, particularly if -// // ErrorHandlingBlock is configured. -// // Track this as an item in the update wait group -// a.processBatch(batchNumber, batchElem.Value.([]*eventData)) -// } -// } - -// // processBatch is the blocking function to process a batch of events -// // It never returns an error, and uses the chosen block/skip ErrorHandling -// // behavior combined with the parameters on the event itself -// func (a *eventStream) processBatch(batchNumber uint64, events []*eventData) { -// if len(events) == 0 { -// return -// } -// processed := false -// attempt := 0 -// for !a.suspendOrStop() && !processed { -// if attempt > 0 { -// select { -// case <-a.updateInterrupt: -// // we were notified by the caller about an ongoing update, no need to continue -// log.Infof("%s: Notified of an ongoing stream update, terminating process batch", a.spec.ID) -// return -// case <-time.After(time.Duration(a.spec.blockedRetryDelaySec()) * time.Second): //fall through and continue -// } -// } -// attempt++ -// log.Infof("%s: Batch %d initiated with %d events. FirstBlock=%s LastBlock=%s", a.spec.ID, batchNumber, len(events), events[0].BlockNumber, events[len(events)-1].BlockNumber) -// err := a.performActionWithRetry(batchNumber, events) -// // If we got an error after all of the internal retries within the event -// // handler failed, then the ErrorHandling strategy kicks in -// processed = (err == nil) -// if !processed { -// log.Errorf("%s: Batch %d attempt %d failed. ErrorHandling=%s BlockedRetryDelay=%ds err=%s", -// a.spec.ID, batchNumber, attempt, a.spec.ErrorHandling, a.spec.BlockedRetryDelaySec, err) -// processed = (a.spec.ErrorHandling == ErrorHandlingSkip) -// } -// } - -// // decrement the in-flight count if we've processed (wouldn't have occurred if we were suspended or stopped) -// a.batchCond.L.Lock() -// if processed { -// a.inFlight -= uint64(len(events)) -// } -// a.batchCond.L.Unlock() - -// // If we were suspended, do not ack the batch -// if a.suspendOrStop() { -// return -// } - -// // Call all the callbacks on the events, so they can update their high water marks -// // If there are multiple events from one SubID, we call it only once with the -// // last message in the batch -// cbs := make(map[string]*eventData) -// for _, event := range events { -// cbs[event.SubID] = event -// } -// for _, event := range cbs { -// event.batchComplete(event) -// } -// } - -// // performActionWithRetry performs an action, with exponential backoff retry up -// // to a given threshold -// func (a *eventStream) performActionWithRetry(batchNumber uint64, events []*eventData) (err error) { -// startTime := time.Now() -// endTime := startTime.Add(time.Duration(a.spec.RetryTimeoutSec) * time.Second) -// delay := a.initialRetryDelay -// var attempt uint64 -// complete := false - -// for !a.suspendOrStop() && !complete { -// if attempt > 0 { -// log.Infof("%s: Waiting %.2fs before re-attempting batch %d", a.spec.ID, delay.Seconds(), batchNumber) -// select { -// case <-a.updateInterrupt: -// // we were notified by the caller about an ongoing update, no need to continue -// log.Infof("%s: Notified of an ongoing stream update, terminating perform action for batch number: %d", a.spec.ID, batchNumber) -// return -// case <-time.After(delay): //fall through and continue -// } -// delay = time.Duration(float64(delay) * a.backoffFactor) -// } -// attempt++ -// err = a.action.attemptBatch(batchNumber, attempt, events) -// complete = err == nil || time.Until(endTime) < 0 -// } -// return err -// } - -// // isAddressSafe checks for local IPs -// func (a *eventStream) isAddressUnsafe(ip *net.IPAddr) bool { -// ip4 := ip.IP.To4() -// return !a.allowPrivateIPs && -// (ip4[0] == 0 || -// ip4[0] >= 224 || -// ip4[0] == 127 || -// ip4[0] == 10 || -// (ip4[0] == 172 && ip4[1] >= 16 && ip4[1] < 32) || -// (ip4[0] == 192 && ip4[1] == 168)) -// } +import ( + "context" + "sync" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" +) + +type EventStreamPersistence interface { + StoreCheckpoint(ctx context.Context, streamID *fftypes.UUID, listenerID *fftypes.UUID, checkpoint *fftypes.JSONAny) error + ReadCheckpoint(ctx context.Context, streamID *fftypes.UUID, listenerID *fftypes.UUID) (*fftypes.JSONAny, error) + DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID, listenerID *fftypes.UUID) error +} + +type EventStream interface { + AddOrUpdateListener(ctx context.Context, s *fftm.Listener) error // Add or update a listener + RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener + UpdateDefinition(ctx context.Context, updates *fftm.EventStream) (*fftm.EventStream, error) // Apply definition updates (if there are changes) and return new object + Start(ctx context.Context) error // Start delivery + Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) + Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint +} + +type streamState string + +const ( + streamStateStarted = "started" + streamStateStopping = "stopping" + streamStateStopped = "stopped" + streamStateDeleted = "deleted" +) + +type eventStreamAction interface { + attemptBatch(batchNumber, attempt uint64, events []*ffcapi.Event) error +} + +// eventStreamDefaults are all customizable via the configuration +type eventStreamDefaults struct { +} + +type eventStream struct { + bgCtx context.Context + definition *fftm.EventStream + mux sync.Mutex + state streamState + connector ffcapi.API + persistence EventStreamPersistence + confirmations confirmations.Manager + listeners map[fftypes.UUID]*listener + action eventStreamAction + cancelEventLoop func() + eventLoopDone chan struct{} + + eventStreamDefaults +} + +func NewEventStream( + bgCtx context.Context, + definition *fftm.EventStream, + connector ffcapi.API, + persistence EventStreamPersistence, + confirmations confirmations.Manager, +) (EventStream, error) { + es := &eventStream{ + bgCtx: log.WithLogField(bgCtx, "eventstream", definition.ID.String()), + state: streamStateStopped, + definition: definition, + connector: connector, + persistence: persistence, + confirmations: confirmations, + listeners: make(map[fftypes.UUID]*listener), + } + if err := es.initAction(); err != nil { + return nil, err + } + return es, nil +} + +func (es *eventStream) initAction() error { + // TODO: Implement websocket/webhook + return nil +} + +func (es *eventStream) updateVerify(ctx context.Context, updates *fftm.EventStream) (merged *fftm.EventStream, changed bool, err error) { + + if updates == nil { + updates = &fftm.EventStream{} + } + old := es.definition + merged = &fftm.EventStream{ + Updated: fftypes.Now(), + Created: old.Created, + } + + changed = changed || fftm.CheckUpdateString(&merged.Name, old.Name, updates.Name, "") + if merged.Name == nil || *merged.Name == "" { + return nil, false, i18n.NewError(ctx, tmmsgs.MsgMissingName) + } + changed = changed || fftm.CheckUpdateBool(&merged.Suspended, old.Suspended, updates.Suspended, false) + changed = changed || fftm.CheckUpdateEnum(&merged.Type, old.Type, updates.Type, fftm.EventStreamTypeWebSocket) + changed = changed || fftm.CheckUpdateUint64(&merged.BatchSize, old.BatchSize, updates.BatchSize, fftm.EventStreamTypeWebSocket) + + return merged, changed, nil +} + +func (es *eventStream) UpdateDefinition(ctx context.Context, updates *fftm.EventStream) (*fftm.EventStream, error) { + merged, changed, err := es.updateVerify(ctx, updates) + if !changed || err != nil { + return es.definition, err + } + + es.mux.Lock() + es.definition = merged + defer es.mux.Unlock() + + return es.definition, nil +} + +func (es *eventStream) AddOrUpdateListener(ctx context.Context, s *fftm.Listener) error { + return nil +} + +func (es *eventStream) RemoveListener(ctx context.Context, id *fftypes.UUID) error { + return nil +} + +func (es *eventStream) String() string { + return es.definition.ID.String() +} + +func (es *eventStream) checkSetState(ctx context.Context, requiredState streamState, newState ...streamState) error { + es.mux.Lock() + defer es.mux.Unlock() + return es.checkSetStateLocked(ctx, requiredState, newState...) +} + +func (es *eventStream) checkSetStateLocked(ctx context.Context, requiredState streamState, newState ...streamState) error { + if es.state != requiredState { + return i18n.NewError(ctx, tmmsgs.MsgStreamStateError, es.state) + } + if len(newState) == 1 { + es.state = newState[0] + } + return nil +} + +func (es *eventStream) Start(ctx context.Context) error { + es.mux.Lock() + defer es.mux.Unlock() + if err := es.checkSetStateLocked(ctx, streamStateStopped, streamStateStarted); err != nil { + return err + } + log.L(ctx).Infof("Starting event stream %s", es) + + elCtx, cancelELCtx := context.WithCancel(es.bgCtx) + es.cancelEventLoop = cancelELCtx + es.eventLoopDone = make(chan struct{}) + go es.eventLoop(elCtx) + return nil +} + +func (es *eventStream) requestStop(ctx context.Context) ([]*listener, error) { + es.mux.Lock() + defer es.mux.Unlock() + if err := es.checkSetStateLocked(ctx, streamStateStarted, streamStateStopping); err != nil { + return nil, err + } + log.L(ctx).Infof("Stopping event stream %s", es) + + // Cancel the event loop + es.cancelEventLoop() + + // Stop all the listeners - we hold the lock during this + listeners := make([]*listener, 0, len(es.listeners)) + for _, l := range es.listeners { + l.RequestStop(ctx) + listeners = append(listeners, l) + } + return listeners, nil +} + +func (es *eventStream) Stop(ctx context.Context) error { + + // Request the stop - this phase is locked, and gives us a safe copy of the listeners array to use outside the lock + listeners, err := es.requestStop(ctx) + if err != nil { + return err + } + + // Wait for each listener to stop + for _, l := range listeners { + l.WaitStopped(ctx) + } + + // Wait for our event loop to stop + <-es.eventLoopDone + + // Transition to stopped (takes the lock again) + return es.checkSetState(ctx, streamStateStopping, streamStateStopped) +} + +func (es *eventStream) Delete(ctx context.Context) error { + // Check we are stopped + if err := es.checkSetState(ctx, streamStateStopped); err != nil { + if err := es.Stop(ctx); err != nil { + return err + } + } + log.L(ctx).Infof("Deleting event stream %s", es) + + // Hold the lock for the whole of delete, rather than transitioning into a deleting state. + // If we error out, that way the caller can retry. + es.mux.Lock() + defer es.mux.Unlock() + for _, l := range es.listeners { + if err := es.persistence.DeleteCheckpoint(ctx, es.definition.ID, l.ID()); err != nil { + return err + } + } + return es.checkSetStateLocked(ctx, streamStateStopped, streamStateDeleted) +} + +func (es *eventStream) eventLoop(ctx context.Context) { + +} diff --git a/internal/events/listener.go b/internal/events/listener.go new file mode 100644 index 00000000..bb48404b --- /dev/null +++ b/internal/events/listener.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 events + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" +) + +type listener struct { + definition *fftm.Listener +} + +func (l *listener) ID() *fftypes.UUID { + return l.definition.ID +} + +func (l *listener) Start(ctx context.Context) error { + return nil +} + +func (l *listener) RequestStop(ctx context.Context) { + +} + +func (l *listener) WaitStopped(ctx context.Context) { + +} diff --git a/internal/events/webhooks.go b/internal/events/webhooks.go new file mode 100644 index 00000000..dd070113 --- /dev/null +++ b/internal/events/webhooks.go @@ -0,0 +1,17 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 events diff --git a/internal/events/websockets.go b/internal/events/websockets.go new file mode 100644 index 00000000..dd070113 --- /dev/null +++ b/internal/events/websockets.go @@ -0,0 +1,17 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 events diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index c2406a89..43d20673 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -42,4 +42,6 @@ var ( MsgMissingGOTemplate = ffe("FF21024", "Missing template for processing response from Gas Oracle REST API") MsgBadGOTemplate = ffe("FF21025", "Invalid Go template: %s") MsgGasOracleResultError = ffe("FF21026", "Error processing result from gas station API via template") + MsgStreamStateError = ffe("FF21027", "Event stream is in %s state") + MsgMissingName = ffe("FF21028", "Name is required") ) diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index dc90071c..bc681d07 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -74,10 +74,10 @@ type BlockHashEvent struct { // The implementation is responsible for ensuring all events on a listener are // ordered on to this channel in the exact sequence from the blockchain. type Event struct { - ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this event - Data *fftypes.JSONAny `json:"data"` // the JSON data to deliver for this event - ProtocolID string `json:"protocolId"` // a protocol identifier for the event, that is string sortable per https://hyperledger.github.io/firefly/reference/types/blockchainevent.html#protocol-id - Info *fftypes.JSONAny `json:"info"` // additional blockchain specific information + ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this event + Data *fftypes.JSONAny `json:"data"` // the JSON data to deliver for this event (can be array or object structure) + ProtocolID string `json:"protocolId"` // a protocol identifier for the event, that is string sortable per https://hyperledger.github.io/firefly/reference/types/blockchainevent.html#protocol-id + Info fftypes.JSONObject `json:"info"` // additional blockchain specific information } // ErrorReason are a set of standard error conditions that a blockchain connector can return @@ -110,7 +110,7 @@ const ( // - The supplied value is passed through for each input parameter. It could be any JSON type (simple number/boolean/string, or complex object/array). The blockchain connection is responsible for serializing these according to the rules in the interface. type TransactionInput struct { TransactionHeaders - Method fftypes.JSONAny `json:"method"` + Method *fftypes.JSONAny `json:"method"` Params []*fftypes.JSONAny `json:"params"` } diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go index b297229d..6a655fe3 100644 --- a/pkg/ffcapi/event_listener_add.go +++ b/pkg/ffcapi/event_listener_add.go @@ -21,10 +21,10 @@ import ( ) type EventListenerAddRequest struct { - ID *fftypes.UUID `json:"uuid"` - Events []*fftypes.JSONAny `json:"events"` - Options *fftypes.JSONAny `json:"options"` - Checkpoint *fftypes.JSONAny `json:"checkpoint"` + ID *fftypes.UUID `json:"uuid"` + Events fftypes.JSONObjectArray `json:"events"` + Options fftypes.JSONObject `json:"options"` + Checkpoint fftypes.JSONObject `json:"checkpoint"` } type EventListenerAddResponse struct { diff --git a/pkg/ffcapi/method_call.go b/pkg/ffcapi/method_call.go index cd3cc12c..78b5ef1a 100644 --- a/pkg/ffcapi/method_call.go +++ b/pkg/ffcapi/method_call.go @@ -32,5 +32,5 @@ type MethodCallRequest struct { } type MethodCallResponse struct { - Outputs *fftypes.JSONAny `json:"outputs"` + Outputs *fftypes.JSONAny `json:"outputs"` // The data output from the method call - can be array or object structure } diff --git a/pkg/ffcapi/transaction_receipt.go b/pkg/ffcapi/transaction_receipt.go index 44ab8d87..7d385fe5 100644 --- a/pkg/ffcapi/transaction_receipt.go +++ b/pkg/ffcapi/transaction_receipt.go @@ -25,9 +25,9 @@ type TransactionReceiptRequest struct { } type TransactionReceiptResponse struct { - BlockNumber *fftypes.FFBigInt `json:"blockNumber"` - TransactionIndex *fftypes.FFBigInt `json:"transactionIndex"` - BlockHash string `json:"blockHash"` - Success bool `json:"success"` - ExtraInfo fftypes.JSONAny `json:"extraInfo"` + BlockNumber *fftypes.FFBigInt `json:"blockNumber"` + TransactionIndex *fftypes.FFBigInt `json:"transactionIndex"` + BlockHash string `json:"blockHash"` + Success bool `json:"success"` + ExtraInfo fftypes.JSONObject `json:"extraInfo"` } diff --git a/pkg/fftm/api_types.go b/pkg/fftm/api_types.go new file mode 100644 index 00000000..92c6f395 --- /dev/null +++ b/pkg/fftm/api_types.go @@ -0,0 +1,160 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "bytes" + "encoding/json" + + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type DistributionMode = fftypes.FFEnum + +var ( + DistributionModeBroadcast = fftypes.FFEnumValue("distmode", "broadcast") + DistributionModeWLD = fftypes.FFEnumValue("distmode", "workloadDistribution") +) + +type EventStreamType = fftypes.FFEnum + +var ( + EventStreamTypeWebhook = fftypes.FFEnumValue("estype", "webhook") + EventStreamTypeWebSocket = fftypes.FFEnumValue("estype", "websocket") +) + +type ErrorHandlingType = fftypes.FFEnum + +var ( + ErrorHandlingTypeBlock = fftypes.FFEnumValue("ehtype", "block") + ErrorHandlingTypeSkip = fftypes.FFEnumValue("ehtype", "skip") +) + +type EventStream struct { + ID *fftypes.UUID `ffstruct:"eventstream" json:"id"` + Created *fftypes.FFTime `ffstruct:"eventstream" json:"created"` + Updated *fftypes.FFTime `ffstruct:"eventstream" json:"updated"` + Name *string `ffstruct:"eventstream" json:"name,omitempty"` + Suspended *bool `ffstruct:"eventstream" json:"suspended,omitempty"` + Type *EventStreamType `ffstruct:"eventstream" json:"type,omitempty" ffenum:"estype"` + + BatchSize *uint64 `ffstruct:"eventstream" json:"batchSize,omitempty"` + BatchTimeout *uint64 `ffstruct:"eventstream" json:"batchTimeout,omitempty"` + ErrorHandling *ErrorHandlingType `ffstruct:"eventstream" json:"errorHandling,omitempty"` + RetryTimeout *uint64 `ffstruct:"eventstream" json:"retryTimeout,omitempty"` + BlockedRetryDelay *uint64 `ffstruct:"eventstream" json:"blockedRetryDelay,omitempty"` + + DeprecatedBatchTimeoutMS *uint64 `ffstruct:"eventstream" json:"batchTimeoutMS,omitempty"` // we now allow duration units like 100ms / 10s + DeprecatedRetryTimeoutSec *uint64 `ffstruct:"eventstream" json:"retryTimeoutSec,omitempty"` // we now allow duration units like 100ms / 10s + DeprecatedBlockedRetryDelaySec *uint64 `ffstruct:"eventstream" json:"blockedRetryDelaySec,omitempty"` // we now allow duration units like 100ms / 10s + + Webhook *WebhookConfig `ffstruct:"eventstream" json:"webhook,omitempty"` + WebSocket *WebSocketConfig `ffstruct:"eventstream" json:"websocket,omitempty"` + Options fftypes.JSONObject `ffstruct:"eventstream" json:"options,omitempty"` +} + +type WebhookConfig struct { + URL string `ffstruct:"whconfig" json:"url,omitempty"` + Headers map[string]string `ffstruct:"whconfig" json:"headers,omitempty"` + TLSkipHostVerify bool `ffstruct:"whconfig" json:"tlsSkipHostVerify,omitempty"` + RequestTimeoutSec uint32 `ffstruct:"whconfig" json:"requestTimeout,omitempty"` + DeprecatedRequestTimeoutSec uint32 `ffstruct:"whconfig" json:"requestTimeoutSec,omitempty"` // we now allow duration units like 100ms / 10s +} + +type WebSocketConfig struct { + Topic string `ffstruct:"wsconfig" json:"topic,omitempty"` + DistributionMode DistributionMode `ffstruct:"wsconfig" json:"distributionMode,omitempty"` +} + +type Listener struct { + ID *fftypes.UUID `ffstruct:"listener" json:"id,omitempty"` + Name string `ffstruct:"listener" json:"name"` + Stream string `ffstruct:"listener" json:"stream"` + Event *fftypes.JSONAny `ffstruct:"listener" json:"event"` + Options fftypes.JSONObject `ffstruct:"listener" json:"options"` + FromBlock string `ffstruct:"listener" json:"fromBlock,omitempty"` +} + +func CheckUpdateString(target **string, old *string, new *string, defValue string) bool { + if new == nil { + *target = new + } else { + *target = old + } + if *target == nil { + v := defValue + *target = &v + return true + } + return *old != *new +} + +func CheckUpdateBool(target **bool, old *bool, new *bool, defValue bool) bool { + if new == nil { + *target = new + } else { + *target = old + } + if *target == nil { + v := defValue + *target = &v + return true + } + return *old != *new +} + +func CheckUpdateUint64(target **uint64, old *uint64, new *uint64, defValue uint64) bool { + if new == nil { + *target = new + } else { + *target = old + } + if *target == nil { + v := defValue + *target = &v + return true + } + return *old != *new +} + +func CheckUpdateEnum(target **fftypes.FFEnum, old *fftypes.FFEnum, new *fftypes.FFEnum, defValue fftypes.FFEnum) bool { + if new == nil { + *target = new + } else { + *target = old + } + if *target == nil { + v := defValue + *target = &v + return true + } + return *old != *new +} + +func CheckUpdateObject(target *fftypes.JSONObject, old fftypes.JSONObject, new fftypes.JSONObject) bool { + if new == nil { + *target = old + return false + } + *target = new + if old == nil { + return true + } + jsonOld, _ := json.Marshal(old) + jsonNew, _ := json.Marshal(old) + return !bytes.Equal(jsonOld, jsonNew) +} From 5b7744397df6a6cd74ecc648e9c2354e2e044844 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 9 Jun 2022 22:13:45 -0400 Subject: [PATCH 06/95] Event stream config and actions, with initial event loop Signed-off-by: Peter Broadhurst --- .golangci.yml | 3 + .vscode/settings.json | 5 + go.mod | 2 +- internal/events/eventstream.go | 303 +++++++++++++++++++----- internal/events/eventstream_test.go | 163 +++++++++++++ internal/events/webhooks.go | 120 ++++++++++ internal/events/websockets.go | 103 ++++++++ internal/tmconfig/tmconfig.go | 70 +++--- internal/tmmsgs/en_error_messges.go | 11 + internal/ws/wsconn.go | 181 ++++++++++++++ internal/ws/wsserver.go | 229 ++++++++++++++++++ internal/ws/wsserver_test.go | 353 ++++++++++++++++++++++++++++ mocks/ffcapimocks/api.go | 25 ++ pkg/ffcapi/api.go | 14 +- pkg/ffcapi/event_listener_add.go | 11 +- pkg/ffcapi/transaction_receipt.go | 10 +- pkg/fftm/api_types.go | 124 +++++----- 17 files changed, 1569 insertions(+), 158 deletions(-) create mode 100644 internal/events/eventstream_test.go create mode 100644 internal/ws/wsconn.go create mode 100644 internal/ws/wsserver.go create mode 100644 internal/ws/wsserver_test.go diff --git a/.golangci.yml b/.golangci.yml index 6c6e82f6..39595b42 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,6 +8,9 @@ linters-settings: enabled-checks: [] disabled-checks: - regexpMust + gosec: + excludes: + - G402 goheader: values: regexp: diff --git a/.vscode/settings.json b/.vscode/settings.json index be2bf419..9756d86e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,9 +12,11 @@ "devdocs", "Devel", "distmode", + "ehtype", "estype", "ethconnect", "eventstream", + "eventstreams", "fabconnect", "ffcapi", "ffcapimocks", @@ -38,6 +40,7 @@ "Infof", "IPFS", "Kaleido", + "loadbalanced", "mtxs", "NATS", "Nowarn", @@ -46,6 +49,7 @@ "policyengine", "policyenginemocks", "policyengines", + "policyloop", "protocolid", "resty", "santhosh", @@ -66,6 +70,7 @@ "upserts", "Warnf", "whconfig", + "workloaddistribution", "wsclient", "wsconfig" ], diff --git a/go.mod b/go.mod index 1f543621..35b69a84 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.17 require ( github.com/go-resty/resty/v2 v2.7.0 github.com/gorilla/mux v1.8.0 + github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru v0.5.4 github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e github.com/hyperledger/firefly-common v0.1.8 @@ -20,7 +21,6 @@ require ( github.com/docker/go-units v0.4.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 1c59d27d..523cbc47 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -19,12 +19,17 @@ package events import ( "context" "sync" + "time" + "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-common/pkg/retry" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/internal/ws" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" ) @@ -36,12 +41,13 @@ type EventStreamPersistence interface { } type EventStream interface { - AddOrUpdateListener(ctx context.Context, s *fftm.Listener) error // Add or update a listener - RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener - UpdateDefinition(ctx context.Context, updates *fftm.EventStream) (*fftm.EventStream, error) // Apply definition updates (if there are changes) and return new object - Start(ctx context.Context) error // Start delivery - Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) - Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint + AddOrUpdateListener(ctx context.Context, s *fftm.Listener) error // Add or update a listener + RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener + UpdateDefinition(ctx context.Context, updates *fftm.EventStream) error // Apply definition updates (if there are changes) + Definition() *fftm.EventStream // Retrieve the current definition to persist + Start(ctx context.Context) error // Start delivery + Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) + Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint } type streamState string @@ -53,90 +59,202 @@ const ( streamStateDeleted = "deleted" ) +// esDefaults are the defaults for new event streams, read from the config once in InitDefaults() +var esDefaults struct { + initialized bool + batchSize int64 + batchTimeout fftypes.FFDuration + errorHandling fftm.ErrorHandlingType + retryTimeout fftypes.FFDuration + blockedRetryDelay fftypes.FFDuration + webhookRequestTimeout fftypes.FFDuration + websocketDistributionMode fftm.DistributionMode + retry *retry.Retry +} + +func InitDefaults() { + esDefaults.batchSize = config.GetInt64(tmconfig.EventStreamsDefaultsBatchSize) + esDefaults.batchTimeout = fftypes.FFDuration(config.GetDuration(tmconfig.EventStreamsDefaultsBatchTimeout)) + esDefaults.errorHandling = fftypes.FFEnum(config.GetString(tmconfig.EventStreamsDefaultsErrorHandling)) + esDefaults.retryTimeout = fftypes.FFDuration(config.GetDuration(tmconfig.EventStreamsDefaultsRetryTimeout)) + esDefaults.blockedRetryDelay = fftypes.FFDuration(config.GetDuration(tmconfig.EventStreamsDefaultsBlockedRetryDelay)) + esDefaults.webhookRequestTimeout = fftypes.FFDuration(config.GetDuration(tmconfig.EventStreamsDefaultsWebhookRequestTimeout)) + esDefaults.websocketDistributionMode = fftypes.FFEnum(config.GetString(tmconfig.EventStreamsDefaultsWebsocketDistributionMode)) + esDefaults.retry = &retry.Retry{ + InitialDelay: config.GetDuration(tmconfig.EventStreamsRetryInitDelay), + MaximumDelay: config.GetDuration(tmconfig.EventStreamsRetryMaxDelay), + Factor: config.GetFloat64(tmconfig.EventStreamsRetryFactor), + } +} + type eventStreamAction interface { - attemptBatch(batchNumber, attempt uint64, events []*ffcapi.Event) error + attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.Event) error } -// eventStreamDefaults are all customizable via the configuration -type eventStreamDefaults struct { +type eventStreamBatch struct { + number int + events []*ffcapi.Event + timeoutContext context.Context + timeoutCancel func() } type eventStream struct { - bgCtx context.Context - definition *fftm.EventStream - mux sync.Mutex - state streamState - connector ffcapi.API - persistence EventStreamPersistence - confirmations confirmations.Manager - listeners map[fftypes.UUID]*listener - action eventStreamAction - cancelEventLoop func() - eventLoopDone chan struct{} - - eventStreamDefaults + bgCtx context.Context + spec *fftm.EventStream + mux sync.Mutex + state streamState + connector ffcapi.API + persistence EventStreamPersistence + confirmations confirmations.Manager + listeners map[fftypes.UUID]*listener + wsChannels ws.WebSocketChannels + retry *retry.Retry + startedState struct { + ctx context.Context + cancelCtx func() + action eventStreamAction + eventLoopDone chan struct{} + events chan *ffcapi.Event + } } func NewEventStream( bgCtx context.Context, - definition *fftm.EventStream, + persistedSpec *fftm.EventStream, connector ffcapi.API, persistence EventStreamPersistence, confirmations confirmations.Manager, -) (EventStream, error) { + wsChannels ws.WebSocketChannels, +) (ees EventStream, err error) { es := &eventStream{ - bgCtx: log.WithLogField(bgCtx, "eventstream", definition.ID.String()), + bgCtx: log.WithLogField(bgCtx, "eventstream", persistedSpec.ID.String()), state: streamStateStopped, - definition: definition, + spec: persistedSpec, connector: connector, persistence: persistence, confirmations: confirmations, listeners: make(map[fftypes.UUID]*listener), + wsChannels: wsChannels, + retry: esDefaults.retry, } - if err := es.initAction(); err != nil { + // The configuration we have in memory, applies all the defaults to what is passed in + // to ensure there are no nil fields on the configuration object. + if es.spec, _, err = mergeValidateEsConfig(es.bgCtx, nil, persistedSpec); err != nil { return nil, err } return es, nil } -func (es *eventStream) initAction() error { - // TODO: Implement websocket/webhook - return nil +func (es *eventStream) initAction() { + ctx := es.startedState.ctx + switch *es.spec.Type { + case fftm.EventStreamTypeWebhook: + es.startedState.action = newWebhookAction(ctx, es.spec.Webhook) + case fftm.EventStreamTypeWebSocket: + es.startedState.action = newWebSocketAction(ctx, es.wsChannels, es.spec.WebSocket, *es.spec.Name) + } + // mergeValidateEsConfig always be called previous to this + panic(i18n.NewError(ctx, tmmsgs.MsgInvalidStreamType, *es.spec.Type)) } -func (es *eventStream) updateVerify(ctx context.Context, updates *fftm.EventStream) (merged *fftm.EventStream, changed bool, err error) { +func mergeValidateEsConfig(ctx context.Context, base *fftm.EventStream, updates *fftm.EventStream) (merged *fftm.EventStream, changed bool, err error) { - if updates == nil { - updates = &fftm.EventStream{} + // Merged is assured to not have any unset values (default set in all cases), or any deprecated fields + if base == nil { + base = &fftm.EventStream{} } - old := es.definition merged = &fftm.EventStream{ + ID: base.ID, + Created: base.Created, Updated: fftypes.Now(), - Created: old.Created, } - - changed = changed || fftm.CheckUpdateString(&merged.Name, old.Name, updates.Name, "") - if merged.Name == nil || *merged.Name == "" { + if merged.Created == nil || merged.ID == nil { + merged.Created = merged.Updated + merged.ID = fftypes.NewUUID() + } + // Name (no default - must be set) + // - Note we do not check for uniqueness of the name at this layer in the code, but we do require unique names. + // That's the responsibility of the calling code that manages the persistence of the configured streams. + changed = fftm.CheckUpdateString(changed, &merged.Name, base.Name, updates.Name, "") + if *merged.Name == "" { return nil, false, i18n.NewError(ctx, tmmsgs.MsgMissingName) } - changed = changed || fftm.CheckUpdateBool(&merged.Suspended, old.Suspended, updates.Suspended, false) - changed = changed || fftm.CheckUpdateEnum(&merged.Type, old.Type, updates.Type, fftm.EventStreamTypeWebSocket) - changed = changed || fftm.CheckUpdateUint64(&merged.BatchSize, old.BatchSize, updates.BatchSize, fftm.EventStreamTypeWebSocket) + + // Suspended + changed = fftm.CheckUpdateBool(changed, &merged.Suspended, base.Suspended, updates.Suspended, false) + + // Batch size + changed = fftm.CheckUpdateUint64(changed, &merged.BatchSize, base.BatchSize, updates.BatchSize, esDefaults.batchSize) + + // Error handling mode + changed = fftm.CheckUpdateEnum(changed, &merged.ErrorHandling, base.ErrorHandling, updates.ErrorHandling, esDefaults.errorHandling) + + // Batch timeout + if updates.DeprecatedBatchTimeoutMS != nil { + dv := fftypes.FFDuration(*updates.DeprecatedBatchTimeoutMS) * fftypes.FFDuration(time.Millisecond) + changed = fftm.CheckUpdateDuration(changed, &merged.BatchTimeout, base.BatchTimeout, &dv, esDefaults.batchTimeout) + } else { + changed = fftm.CheckUpdateDuration(changed, &merged.BatchTimeout, base.BatchTimeout, updates.BatchTimeout, esDefaults.batchTimeout) + } + + // Retry timeout + if updates.DeprecatedRetryTimeoutSec != nil { + dv := fftypes.FFDuration(*updates.DeprecatedRetryTimeoutSec) * fftypes.FFDuration(time.Second) + changed = fftm.CheckUpdateDuration(changed, &merged.RetryTimeout, base.RetryTimeout, &dv, esDefaults.retryTimeout) + } else { + changed = fftm.CheckUpdateDuration(changed, &merged.RetryTimeout, base.RetryTimeout, updates.RetryTimeout, esDefaults.retryTimeout) + } + + // Blocked retry delay + if updates.DeprecatedBlockedRetryDelaySec != nil { + dv := fftypes.FFDuration(*updates.DeprecatedBlockedRetryDelaySec) * fftypes.FFDuration(time.Second) + changed = fftm.CheckUpdateDuration(changed, &merged.BlockedRetryDelay, base.BlockedRetryDelay, &dv, esDefaults.blockedRetryDelay) + } else { + changed = fftm.CheckUpdateDuration(changed, &merged.BlockedRetryDelay, base.BlockedRetryDelay, updates.BlockedRetryDelay, esDefaults.blockedRetryDelay) + } + + // Type + changed = fftm.CheckUpdateEnum(changed, &merged.Type, base.Type, updates.Type, fftm.EventStreamTypeWebSocket) + switch *merged.Type { + case fftm.EventStreamTypeWebSocket: + if merged.WebSocket, changed, err = mergeValidateWsConfig(ctx, changed, base.WebSocket, updates.WebSocket); err != nil { + return nil, false, err + } + case fftm.EventStreamTypeWebhook: + if merged.Webhook, changed, err = mergeValidateWhConfig(ctx, changed, base.Webhook, updates.Webhook); err != nil { + return nil, false, err + } + default: + return nil, false, i18n.NewError(ctx, tmmsgs.MsgInvalidStreamType, *merged.Type) + } return merged, changed, nil } -func (es *eventStream) UpdateDefinition(ctx context.Context, updates *fftm.EventStream) (*fftm.EventStream, error) { - merged, changed, err := es.updateVerify(ctx, updates) - if !changed || err != nil { - return es.definition, err +func (es *eventStream) Definition() *fftm.EventStream { + return es.spec +} + +func (es *eventStream) UpdateDefinition(ctx context.Context, updates *fftm.EventStream) error { + merged, changed, err := mergeValidateEsConfig(ctx, es.spec, updates) + if err != nil { + return err } es.mux.Lock() - es.definition = merged + es.spec = merged defer es.mux.Unlock() - return es.definition, nil + if changed { + if err := es.Stop(ctx); err != nil { + return i18n.NewError(ctx, tmmsgs.MsgStopFailedUpdatingESConfig) + } + if err := es.Start(ctx); err != nil { + return i18n.NewError(ctx, tmmsgs.MsgStartFailedUpdatingESConfig) + } + } + + return nil } func (es *eventStream) AddOrUpdateListener(ctx context.Context, s *fftm.Listener) error { @@ -148,7 +266,7 @@ func (es *eventStream) RemoveListener(ctx context.Context, id *fftypes.UUID) err } func (es *eventStream) String() string { - return es.definition.ID.String() + return es.spec.ID.String() } func (es *eventStream) checkSetState(ctx context.Context, requiredState streamState, newState ...streamState) error { @@ -175,10 +293,11 @@ func (es *eventStream) Start(ctx context.Context) error { } log.L(ctx).Infof("Starting event stream %s", es) - elCtx, cancelELCtx := context.WithCancel(es.bgCtx) - es.cancelEventLoop = cancelELCtx - es.eventLoopDone = make(chan struct{}) - go es.eventLoop(elCtx) + es.startedState.ctx, es.startedState.cancelCtx = context.WithCancel(es.bgCtx) + es.initAction() + es.startedState.eventLoopDone = make(chan struct{}) + es.startedState.events = make(chan *ffcapi.Event, int(*es.spec.BatchSize)) + go es.eventLoop() return nil } @@ -190,8 +309,8 @@ func (es *eventStream) requestStop(ctx context.Context) ([]*listener, error) { } log.L(ctx).Infof("Stopping event stream %s", es) - // Cancel the event loop - es.cancelEventLoop() + // Cancel the context, stop stop the event loop, and shut down the action (WebSockets in particular) + es.startedState.cancelCtx() // Stop all the listeners - we hold the lock during this listeners := make([]*listener, 0, len(es.listeners)) @@ -216,7 +335,7 @@ func (es *eventStream) Stop(ctx context.Context) error { } // Wait for our event loop to stop - <-es.eventLoopDone + <-es.startedState.eventLoopDone // Transition to stopped (takes the lock again) return es.checkSetState(ctx, streamStateStopping, streamStateStopped) @@ -236,13 +355,83 @@ func (es *eventStream) Delete(ctx context.Context) error { es.mux.Lock() defer es.mux.Unlock() for _, l := range es.listeners { - if err := es.persistence.DeleteCheckpoint(ctx, es.definition.ID, l.ID()); err != nil { + if err := es.persistence.DeleteCheckpoint(ctx, es.spec.ID, l.ID()); err != nil { return err } } return es.checkSetStateLocked(ctx, streamStateStopped, streamStateDeleted) } -func (es *eventStream) eventLoop(ctx context.Context) { +func (es *eventStream) eventLoop() { + defer close(es.startedState.eventLoopDone) + ctx := es.startedState.ctx + batchTimeout := time.Duration(*es.spec.BatchTimeout) + maxSize := int(*es.spec.BatchSize) + batchNumber := 0 + + var batch *eventStreamBatch + for { + var timeoutContext context.Context + var timedOut bool + if batch != nil { + timeoutContext = batch.timeoutContext + } else { + timeoutContext = ctx + } + select { + case event := <-es.startedState.events: + if batch == nil { + batchNumber++ + batch = &eventStreamBatch{number: batchNumber} + batch.timeoutContext, batch.timeoutCancel = context.WithTimeout(ctx, batchTimeout) + } + batch.events = append(batch.events, event) + case <-timeoutContext.Done(): + timedOut = true + } + if batch != nil && (timedOut || len(batch.events) >= maxSize) { + batch.timeoutCancel() + err := es.performActionWithRetry(batch) + if err != nil { + log.L(ctx).Debugf("Operation update worker exiting: %s", err) + return + } + batch = nil + } + } +} + +// performActionWithRetry performs an action, with exponential back-off retry up +// to a given threshold +func (es *eventStream) performActionWithRetry(batch *eventStreamBatch) (err error) { + ctx := es.startedState.ctx + startTime := time.Now() + for { + // Short exponential back-off retry + err := es.retry.Do(ctx, "action", func(attempt int) (retry bool, err error) { + err = es.startedState.action.attemptBatch(ctx, batch.number, attempt, batch.events) + if err != nil { + log.L(ctx).Errorf("Batch %d attempt %d failed. err=%s", + batch.number, attempt, err) + return time.Since(startTime) > time.Duration(*es.spec.RetryTimeout), err + } + return false, nil + }) + if err == nil { + return nil + } + // We're in blocked retry delay + log.L(ctx).Errorf("Batch failed short retry after %.2fs secs. ErrorHandling=%s BlockedRetryDelay=%.2fs ", + time.Since(startTime), *es.spec.ErrorHandling, time.Duration(*es.spec.BlockedRetryDelay).Seconds()) + if *es.spec.ErrorHandling == fftm.ErrorHandlingTypeSkip { + return nil + } + select { + case <-time.After(time.Duration(*es.spec.BlockedRetryDelay)): + case <-ctx.Done(): + // Only way we exit with error, is if the context is cancelled + return i18n.NewError(ctx, i18n.MsgContextCanceled) + } + } } diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go new file mode 100644 index 00000000..a880bf8f --- /dev/null +++ b/internal/events/eventstream_test.go @@ -0,0 +1,163 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 events + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/stretchr/testify/assert" +) + +func testESConf(t *testing.T, j string) (es *fftm.EventStream) { + err := json.Unmarshal([]byte(j), &es) + assert.NoError(t, err) + return es +} + +func TestConfigNewDefaultsUpdate(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + es := testESConf(t, `{ + "name": "test1" + }`) + es, changed, err := mergeValidateEsConfig(context.Background(), nil, es) + assert.NoError(t, err) + assert.True(t, changed) + + b, err := json.Marshal(&es) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "id":"`+es.ID.String()+`", + "created":"`+es.Created.String()+`", + "updated":"`+es.Created.String()+`", + "batchSize": 50, + "batchTimeout": "5s", + "blockedRetryDelay": "30s", + "errorHandling":"block", + "name":"test1", + "retryTimeout":"30s", + "suspended":false, + "type":"websocket", + "websocket": { + "distributionMode":"loadbalanced" + } + }`, string(b)) + + es, changed, err = mergeValidateEsConfig(context.Background(), es, es) + assert.NoError(t, err) + assert.False(t, changed) + + es2, changed, err := mergeValidateEsConfig(context.Background(), es, testESConf(t, `{ + "id": "4023945d-ea5d-43aa-ab4f-f39f8c055c7e",`+ /* ignored */ ` + "batchSize": 111, + "batchTimeoutMS": 222, + "blockedRetryDelaySec": 333, + "errorHandling": "skip", + "name": "test2", + "retryTimeoutSec": 444, + "suspended": true, + "type": "webhook", + "webhook": { + "url": "http://test.example.com" + } + }`)) + + assert.NoError(t, err) + assert.True(t, changed) + + b, err = json.Marshal(&es2) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "id":"`+es.ID.String()+`", + "created":"`+es.Created.String()+`", + "updated":"`+es2.Updated.String()+`", + "batchSize": 111, + "batchTimeout": "222ms", + "blockedRetryDelay": "5m33s", + "errorHandling":"skip", + "name":"test2", + "retryTimeout":"7m24s", + "suspended":true, + "type":"webhook", + "webhook": { + "tlsSkipHostVerify": false, + "requestTimeout": "30s", + "url": "http://test.example.com" + } + }`, string(b)) + +} + +func TestConfigNewMissingName(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + _, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{}`)) + assert.Regexp(t, "FF21028", err) + +} + +func TestConfigNewMissingWebhookConf(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + _, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ + "name": "test", + "type": "webhook", + "websocket": {} + }`)) + assert.Regexp(t, "FF21030", err) + +} + +func TestConfigNewBadType(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + _, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ + "name": "test", + "type": "wrong" + }`)) + assert.Regexp(t, "FF21029", err) + +} + +func TestConfigNewWebhookRetryMigration(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + es, changed, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ + "name": "test", + "type": "webhook", + "webhook": { + "urL": "http://www.example.com", + "requestTimeoutSec": 5 + } + }`)) + assert.NoError(t, err) + assert.True(t, changed) + + assert.Equal(t, fftypes.FFDuration(5*time.Second), *es.Webhook.RequestTimeout) + +} diff --git a/internal/events/webhooks.go b/internal/events/webhooks.go index dd070113..13f55068 100644 --- a/internal/events/webhooks.go +++ b/internal/events/webhooks.go @@ -15,3 +15,123 @@ // limitations under the License. package events + +import ( + "context" + "crypto/tls" + "net" + "net/url" + "time" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffresty" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" +) + +func mergeValidateWhConfig(ctx context.Context, changed bool, base *fftm.WebhookConfig, updates *fftm.WebhookConfig) (*fftm.WebhookConfig, bool, error) { + + if base == nil { + base = &fftm.WebhookConfig{} + } + if updates == nil { + updates = &fftm.WebhookConfig{} + } + merged := &fftm.WebhookConfig{} + + // URL (no default - must be set) + changed = fftm.CheckUpdateString(changed, &merged.URL, base.URL, updates.URL, "") + if *merged.URL == "" { + return nil, false, i18n.NewError(ctx, tmmsgs.MsgMissingWebhookURL) + } + + // Headers + changed = fftm.CheckUpdateStringMap(changed, &merged.Headers, base.Headers, updates.Headers) + + // Skip host verify (disable TLS checking) + changed = fftm.CheckUpdateBool(changed, &merged.TLSkipHostVerify, base.TLSkipHostVerify, updates.TLSkipHostVerify, false) + + // Request timeout + if updates.DeprecatedRequestTimeoutSec != nil { + dv := fftypes.FFDuration(*updates.DeprecatedRequestTimeoutSec) * fftypes.FFDuration(time.Second) + changed = fftm.CheckUpdateDuration(changed, &merged.RequestTimeout, base.RequestTimeout, &dv, esDefaults.webhookRequestTimeout) + } else { + changed = fftm.CheckUpdateDuration(changed, &merged.RequestTimeout, base.RequestTimeout, updates.RequestTimeout, esDefaults.webhookRequestTimeout) + } + + return merged, changed, nil +} + +type webhookAction struct { + allowPrivateIPs bool + headers map[string]string + spec *fftm.WebhookConfig + client *resty.Client +} + +func newWebhookAction(ctx context.Context, spec *fftm.WebhookConfig) *webhookAction { + client := ffresty.New(ctx, tmconfig.WebhookPrefix) // majority of settings come from config + client.SetTimeout(time.Duration(*spec.RequestTimeout)) // request timeout set per stream + if *spec.TLSkipHostVerify { + client.SetTLSClientConfig(&tls.Config{ + InsecureSkipVerify: true, + }) + } + + return &webhookAction{ + spec: spec, + allowPrivateIPs: config.GetBool(tmconfig.WebhooksAllowPrivateIPs), + client: client, + } +} + +// attemptWebhookAction performs a single attempt of a webhook action +func (w *webhookAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.Event) error { + // We perform DNS resolution before each attempt, to exclude private IP address ranges from the target + u, _ := url.Parse(*w.spec.URL) + addr, err := net.ResolveIPAddr("ip4", u.Hostname()) + if err != nil { + return err + } + if w.isAddressBlocked(addr) { + return i18n.NewError(ctx, tmmsgs.MsgBlockWebhookAddress, u.Hostname()) + } + var resBody []byte + req := w.client.R(). + SetContext(ctx). + SetBody(events). + SetResult(&resBody). + SetError(&resBody) + req.Header.Set("Content-Type", "application/json") + for h, v := range w.spec.Headers { + req.Header.Set(h, v) + } + res, err := req.Post(u.String()) + if err != nil { + log.L(ctx).Errorf("Webhook %s (%s) [%d]: %s", *w.spec.URL, u, res.StatusCode(), err) + return err + } + if res.IsError() { + log.L(ctx).Errorf("Webhook %s (%s) [%d]: %s", *w.spec.URL, u, res.StatusCode(), resBody) + return i18n.NewError(ctx, tmmsgs.MsgWebhookFailed, res.StatusCode()) + } + return err +} + +// isAddressBlocked allows blocking of all of the "private" address blocks defined by IPv4 +func (w *webhookAction) isAddressBlocked(ip *net.IPAddr) bool { + ip4 := ip.IP.To4() + return !w.allowPrivateIPs && + (ip4[0] == 0 || + ip4[0] >= 224 || + ip4[0] == 127 || + ip4[0] == 10 || + (ip4[0] == 172 && ip4[1] >= 16 && ip4[1] < 32) || + (ip4[0] == 192 && ip4[1] == 168)) +} diff --git a/internal/events/websockets.go b/internal/events/websockets.go index dd070113..0b28b9cb 100644 --- a/internal/events/websockets.go +++ b/internal/events/websockets.go @@ -15,3 +15,106 @@ // limitations under the License. package events + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/internal/ws" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" +) + +func mergeValidateWsConfig(ctx context.Context, changed bool, base *fftm.WebSocketConfig, updates *fftm.WebSocketConfig) (*fftm.WebSocketConfig, bool, error) { + + if base == nil { + base = &fftm.WebSocketConfig{} + } + if updates == nil { + updates = &fftm.WebSocketConfig{} + } + merged := &fftm.WebSocketConfig{} + + // Distribution mode + changed = fftm.CheckUpdateEnum(changed, &merged.DistributionMode, base.DistributionMode, updates.DistributionMode, esDefaults.websocketDistributionMode) + switch *merged.DistributionMode { + case fftm.DistributionModeLoadBalance, fftm.DistributionMode("workloaddistribution"): + // Migrate old "workloadDistribution" enum value to more consistent with other FF enums "load_balance" + *merged.DistributionMode = fftm.DistributionModeLoadBalance + case fftm.DistributionModeBroadcast: + default: + return nil, false, i18n.NewError(ctx, tmmsgs.MsgInvalidDistributionMode, *merged.DistributionMode) + } + + return merged, changed, nil +} + +type webSocketAction struct { + ctx context.Context + topic string + spec *fftm.WebSocketConfig + wsChannels ws.WebSocketChannels +} + +func newWebSocketAction(parentCtx context.Context, wsChannels ws.WebSocketChannels, spec *fftm.WebSocketConfig, topic string) *webSocketAction { + return &webSocketAction{ + ctx: log.WithLogField(parentCtx, "action", "websocket"), + spec: spec, + wsChannels: wsChannels, + topic: topic, + } +} + +// attemptBatch attempts to deliver a batch over socket IO +func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.Event) error { + var err error + + // Get a blocking channel to send and receive on our chosen namespace + sender, broadcaster, receiver := w.wsChannels.GetChannels(w.topic) + + var channel chan<- interface{} + switch *w.spec.DistributionMode { + case fftm.DistributionModeBroadcast: + channel = broadcaster + case fftm.DistributionModeLoadBalance: + channel = sender + default: + return i18n.NewError(ctx, tmmsgs.MsgInvalidDistributionMode, *w.spec.DistributionMode) + } + + // Clear out any current ack/error + purging := true + for purging { + select { + case err1 := <-receiver: + log.L(w.ctx).Warnf("Cleared out spurious ack (could be from previous disconnect). err=%s", err1) + default: + purging = false + } + } + + // Sent the batch of events + select { + case channel <- events: + break + case <-w.ctx.Done(): + err = i18n.NewError(w.ctx, tmmsgs.MsgWebSocketInterruptedSend) + } + + // If we ever add more distribution modes, we may want to change this logic from a simple if statement + if err == nil && *w.spec.DistributionMode != fftm.DistributionModeBroadcast { + // Wait for the next ack or exception + select { + case err = <-receiver: + break + case <-w.ctx.Done(): + err = i18n.NewError(w.ctx, tmmsgs.MsgWebSocketInterruptedReceive) + } + } + + // Pass back any exception from the client + log.L(w.ctx).Infof("WebSocket event batch %d complete (len=%d). err=%v", batchNumber, len(events), err) + return err +} diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index fa715684..70c84860 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -18,6 +18,7 @@ package tmconfig import ( "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffresty" "github.com/hyperledger/firefly-common/pkg/httpserver" "github.com/hyperledger/firefly-common/pkg/wsclient" "github.com/hyperledger/firefly/pkg/core" @@ -27,36 +28,32 @@ import ( var ffc = config.AddRootKey var ( - // ManagerName is a name for this manager, that must be unique if there are multiple managers on this node - ManagerName = ffc("manager.name") - // ConnectorVariant is the variant setting to add to all requests to the backend connector - ConnectorVariant = ffc("connector.variant") - // ConfirmationsRequired is the number of confirmations required for a transaction to be considered final - ConfirmationsRequired = ffc("confirmations.required") - // ConfirmationsBlockCacheSize is the size of the block cache - ConfirmationsBlockCacheSize = ffc("confirmations.blockCacheSize") - // ConfirmationsBlockPollingInterval is the time between block polling - ConfirmationsBlockPollingInterval = ffc("confirmations.blockPollingInterval") - // ConfirmationsStaleReceiptTimeout the duration after which to force a receipt check for a pending transaction - ConfirmationsStaleReceiptTimeout = ffc("confirmations.staleReceiptTimeout") - // ConfirmationsNotificationQueueLength is the length of the internal queue to the block confirmations manager - ConfirmationsNotificationQueueLength = ffc("confirmations.notificationQueueLength") - // OperationsTypes the type of operations to monitor - only those that were submitted through the manager will have the required output format, so this is the superset - OperationsTypes = ffc("operations.types") - // OperationsFullScanStartupMaxRetries is the maximum times to try the scan on first startup, before failing startup - OperationsFullScanStartupMaxRetries = ffc("operations.fullScan.startupMaxRetries") - // OperationsPageSize page size for polling - OperationsFullScanPageSize = ffc("operations.fullScan.pageSize") - // OperationsFullScanMinimumDelay the minimum delay between full scan attempts - OperationsFullScanMinimumDelay = ffc("operations.fullScan.minimumDelay") - // OperationsErrorHistoryCount the number of errors to retain in the operation - OperationsErrorHistoryCount = ffc("operations.errorHistoryCount") - // OperationsChangeListenerEnabled whether to enable the operation change listener - OperationsChangeListenerEnabled = ffc("operations.changeListener.enabled") - // PolicyLoopInterval how often to go round the loop executing the policy engine against all pending transactions to make decisions - PolicyLoopInterval = ffc("policyloop.interval") - // PolicyEngineName the name of the policy engine to use - PolicyEngineName = ffc("policyengine.name") + ManagerName = ffc("manager.name") + ConnectorVariant = ffc("connector.variant") + ConfirmationsRequired = ffc("confirmations.required") + ConfirmationsBlockCacheSize = ffc("confirmations.blockCacheSize") + ConfirmationsBlockPollingInterval = ffc("confirmations.blockPollingInterval") + ConfirmationsStaleReceiptTimeout = ffc("confirmations.staleReceiptTimeout") + ConfirmationsNotificationQueueLength = ffc("confirmations.notificationQueueLength") + OperationsTypes = ffc("operations.types") + OperationsFullScanStartupMaxRetries = ffc("operations.fullScan.startupMaxRetries") + OperationsFullScanPageSize = ffc("operations.fullScan.pageSize") + OperationsFullScanMinimumDelay = ffc("operations.fullScan.minimumDelay") + OperationsErrorHistoryCount = ffc("operations.errorHistoryCount") + OperationsChangeListenerEnabled = ffc("operations.changeListener.enabled") + PolicyLoopInterval = ffc("policyloop.interval") + PolicyEngineName = ffc("policyengine.name") + EventStreamsDefaultsBatchSize = ffc("eventstreams.defaults.batchSize") + EventStreamsDefaultsBatchTimeout = ffc("eventstreams.defaults.batchTimeout") + EventStreamsDefaultsErrorHandling = ffc("eventstreams.defaults.errorHandling") + EventStreamsDefaultsRetryTimeout = ffc("eventstreams.defaults.retryTimeout") + EventStreamsDefaultsBlockedRetryDelay = ffc("eventstreams.defaults.blockedRetryDelay") + EventStreamsDefaultsWebhookRequestTimeout = ffc("eventstreams.defaults.webhookRequestTimeout") + EventStreamsDefaultsWebsocketDistributionMode = ffc("eventstreams.defaults.websocketDistributionMode") + EventStreamsRetryInitDelay = ffc("eventstreams.retry.initialDelay") + EventStreamsRetryMaxDelay = ffc("eventstreams.retry.maxDelay") + EventStreamsRetryFactor = ffc("eventstreams.retry.factor") + WebhooksAllowPrivateIPs = ffc("webhooks.allowPrivateIPs") ) var FFCoreConfig config.Section @@ -67,6 +64,8 @@ var CorsConfig config.Section var PolicyEngineBaseConfig config.Section +var WebhookPrefix config.Section + func setDefaults() { viper.SetDefault(string(OperationsFullScanPageSize), 100) viper.SetDefault(string(OperationsFullScanMinimumDelay), "5s") @@ -85,6 +84,14 @@ func setDefaults() { viper.SetDefault(string(OperationsErrorHistoryCount), 25) viper.SetDefault(string(PolicyLoopInterval), "1s") viper.SetDefault(string(PolicyEngineName), "simple") + + viper.SetDefault(string(EventStreamsDefaultsBatchSize), 50) + viper.SetDefault(string(EventStreamsDefaultsBatchTimeout), "5s") + viper.SetDefault(string(EventStreamsDefaultsErrorHandling), "block") + viper.SetDefault(string(EventStreamsDefaultsRetryTimeout), "30s") + viper.SetDefault(string(EventStreamsDefaultsBlockedRetryDelay), "30s") + viper.SetDefault(string(EventStreamsDefaultsWebhookRequestTimeout), "30s") + viper.SetDefault(string(EventStreamsDefaultsWebsocketDistributionMode), "loadbalanced") } func Reset() { @@ -100,6 +107,9 @@ func Reset() { CorsConfig = config.RootSection("cors") httpserver.InitCORSConfig(CorsConfig) + WebhookPrefix = config.RootSection("webhooks") + ffresty.InitConfig(WebhookPrefix) + PolicyEngineBaseConfig = config.RootSection("policyengine") // policy engines must be registered outside of this package diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 43d20673..944f9ba3 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -44,4 +44,15 @@ var ( MsgGasOracleResultError = ffe("FF21026", "Error processing result from gas station API via template") MsgStreamStateError = ffe("FF21027", "Event stream is in %s state") MsgMissingName = ffe("FF21028", "Name is required") + MsgInvalidStreamType = ffe("FF21029", "Invalid event stream type '%s'") + MsgMissingWebhookURL = ffe("FF21030", "'url' is required for webhook configuration") + MsgStopFailedUpdatingESConfig = ffe("FF21031", "Failed to stop event stream to apply updated configuration: %s") + MsgStartFailedUpdatingESConfig = ffe("FF21032", "Failed to restart event stream while applying updated configuration: %s") + MsgBlockWebhookAddress = ffe("FF21033", "Cannot send Webhook POST to address: %s") + MsgInvalidDistributionMode = ffe("FF21034", "Invalid distribution mode for WebSocket: %s") + MsgWebhookFailed = ffe("FF21035", "Webhook request failed with status %d") + MsgWSErrorFromClient = ffe("FF21036", "Error received from WebSocket client: %s") + MsgWebSocketClosed = ffe("FF21037", "WebSocket '%s' closed") + MsgWebSocketInterruptedSend = ffe("FF21038", "Interrupted waiting for WebSocket connection to send event") + MsgWebSocketInterruptedReceive = ffe("FF21039", "Interrupted waiting for WebSocket acknowledgment") ) diff --git a/internal/ws/wsconn.go b/internal/ws/wsconn.go new file mode 100644 index 00000000..2968b4a0 --- /dev/null +++ b/internal/ws/wsconn.go @@ -0,0 +1,181 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ws + +import ( + "context" + "reflect" + "strings" + "sync" + + ws "github.com/gorilla/websocket" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" +) + +type webSocketConnection struct { + ctx context.Context + id string + server *webSocketServer + conn *ws.Conn + mux sync.Mutex + closed bool + topics map[string]*webSocketTopic + broadcast chan interface{} + newTopic chan bool + receive chan error + closing chan struct{} +} + +type webSocketCommandMessage struct { + Type string `json:"type,omitempty"` + Topic string `json:"topic,omitempty"` // synonym for "topic" - from a time when we let you configure the topic separate to the stream name + Stream string `json:"stream,omitempty"` // name of the event stream + Message string `json:"message,omitempty"` +} + +func newConnection(bgCtx context.Context, server *webSocketServer, conn *ws.Conn) *webSocketConnection { + id := fftypes.NewUUID().String() + wsc := &webSocketConnection{ + ctx: log.WithLogField(bgCtx, "wsc", id), + id: id, + server: server, + conn: conn, + newTopic: make(chan bool), + topics: make(map[string]*webSocketTopic), + broadcast: make(chan interface{}), + receive: make(chan error), + closing: make(chan struct{}), + } + go wsc.listen() + go wsc.sender() + return wsc +} + +func (c *webSocketConnection) close() { + c.mux.Lock() + if !c.closed { + c.closed = true + c.conn.Close() + close(c.closing) + } + c.mux.Unlock() + + for _, t := range c.topics { + c.server.cycleTopic(c.id, t) + log.L(c.ctx).Infof("Websocket closed while active on topic '%s'", t.topic) + } + c.server.connectionClosed(c) + log.L(c.ctx).Infof("Disconnected") +} + +func (c *webSocketConnection) sender() { + defer c.close() + buildCases := func() []reflect.SelectCase { + c.mux.Lock() + defer c.mux.Unlock() + cases := make([]reflect.SelectCase, len(c.topics)+3) + i := 0 + for _, t := range c.topics { + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(t.senderChannel)} + i++ + } + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c.broadcast)} + i++ + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c.closing)} + i++ + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c.newTopic)} + return cases + } + cases := buildCases() + for { + chosen, value, ok := reflect.Select(cases) + if !ok { + log.L(c.ctx).Infof("Closing") + return + } + + if chosen == len(cases)-1 { + // Addition of a new topic + cases = buildCases() + } else { + // Message from one of the existing topics + _ = c.conn.WriteJSON(value.Interface()) + } + } +} + +func (c *webSocketConnection) listenTopic(t *webSocketTopic) { + c.mux.Lock() + c.topics[t.topic] = t + c.server.ListenOnTopic(c, t.topic) + c.mux.Unlock() + select { + case c.newTopic <- true: + case <-c.closing: + } +} + +func (c *webSocketConnection) listenReplies() { + c.server.ListenForReplies(c) +} + +func (c *webSocketConnection) listen() { + defer c.close() + log.L(c.ctx).Infof("Connected") + for { + var msg webSocketCommandMessage + err := c.conn.ReadJSON(&msg) + if err != nil { + log.L(c.ctx).Errorf("Error: %s", err) + return + } + log.L(c.ctx).Debugf("Received: %+v", msg) + + topic := msg.Stream + if topic == "" { + topic = msg.Topic + } + t := c.server.getTopic(topic) + switch strings.ToLower(msg.Type) { + case "listen": + c.listenTopic(t) + case "listenreplies": + c.listenReplies() + case "ack": + c.handleAckOrError(t, nil) + case "error": + c.handleAckOrError(t, i18n.NewError(c.ctx, tmmsgs.MsgWSErrorFromClient, msg.Message)) + default: + log.L(c.ctx).Errorf("Unexpected message type: %+v", msg) + } + } +} + +func (c *webSocketConnection) handleAckOrError(t *webSocketTopic, err error) { + isError := err != nil + select { + case t.receiverChannel <- err: + log.L(c.ctx).Debugf("response (error='%t') on topic '%s' passed on for processing", isError, t.topic) + break + default: + log.L(c.ctx).Debugf("spurious ack received (error='%t') on topic '%s'", isError, t.topic) + break + } +} diff --git a/internal/ws/wsserver.go b/internal/ws/wsserver.go new file mode 100644 index 00000000..8d0a3ca3 --- /dev/null +++ b/internal/ws/wsserver.go @@ -0,0 +1,229 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ws + +import ( + "context" + "net/http" + "reflect" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" +) + +// WebSocketChannels is provided to allow us to do a blocking send to a namespace that will complete once a client connects on it +// We also provide a channel to listen on for closing of the connection, to allow a select to wake on a blocking send +type WebSocketChannels interface { + GetChannels(topic string) (chan<- interface{}, chan<- interface{}, <-chan error) + SendReply(message interface{}) +} + +// WebSocketServer is the full server interface with the init call +type WebSocketServer interface { + WebSocketChannels + Handler(w http.ResponseWriter, r *http.Request) + Close() +} + +type webSocketServer struct { + ctx context.Context + processingTimeout time.Duration + mux sync.Mutex + topics map[string]*webSocketTopic + topicMap map[string]map[string]*webSocketConnection + replyMap map[string]*webSocketConnection + newTopic chan bool + replyChannel chan interface{} + upgrader *websocket.Upgrader + connections map[string]*webSocketConnection +} + +type webSocketTopic struct { + topic string + senderChannel chan interface{} + broadcastChannel chan interface{} + receiverChannel chan error +} + +// NewWebSocketServer create a new server with a simplified interface +func NewWebSocketServer(bgCtx context.Context) WebSocketServer { + s := &webSocketServer{ + ctx: bgCtx, + connections: make(map[string]*webSocketConnection), + topics: make(map[string]*webSocketTopic), + topicMap: make(map[string]map[string]*webSocketConnection), + replyMap: make(map[string]*webSocketConnection), + newTopic: make(chan bool), + replyChannel: make(chan interface{}), + processingTimeout: 30 * time.Second, + upgrader: &websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + }, + } + go s.processBroadcasts() + go s.processReplies() + return s +} + +func (s *webSocketServer) Handler(w http.ResponseWriter, r *http.Request) { + conn, err := s.upgrader.Upgrade(w, r, nil) + if err != nil { + log.L(s.ctx).Errorf("WebSocket upgrade failed: %s", err) + return + } + s.mux.Lock() + defer s.mux.Unlock() + c := newConnection(s.ctx, s, conn) + s.connections[c.id] = c +} + +func (s *webSocketServer) cycleTopic(connInfo string, t *webSocketTopic) { + s.mux.Lock() + defer s.mux.Unlock() + + // When a connection that was listening on a topic closes, we need to wake anyone + // that was listening for a response + select { + case t.receiverChannel <- i18n.NewError(s.ctx, tmmsgs.MsgWebSocketClosed, connInfo): + default: + } +} + +func (s *webSocketServer) connectionClosed(c *webSocketConnection) { + s.mux.Lock() + defer s.mux.Unlock() + delete(s.connections, c.id) + delete(s.replyMap, c.id) + for _, topic := range c.topics { + delete(s.topicMap[topic.topic], c.id) + } +} + +func (s *webSocketServer) Close() { + for _, c := range s.connections { + c.close() + } +} + +func (s *webSocketServer) getTopic(topic string) *webSocketTopic { + s.mux.Lock() + t, exists := s.topics[topic] + if !exists { + t = &webSocketTopic{ + topic: topic, + senderChannel: make(chan interface{}), + broadcastChannel: make(chan interface{}), + receiverChannel: make(chan error, 1), + } + s.topics[topic] = t + s.topicMap[topic] = make(map[string]*webSocketConnection) + } + s.mux.Unlock() + if !exists { + // Signal to the broadcaster that a new topic has been added + s.newTopic <- true + } + return t +} + +func (s *webSocketServer) GetChannels(topic string) (chan<- interface{}, chan<- interface{}, <-chan error) { + t := s.getTopic(topic) + return t.senderChannel, t.broadcastChannel, t.receiverChannel +} + +func (s *webSocketServer) ListenOnTopic(c *webSocketConnection, topic string) { + // Track that this connection is interested in this topic + s.topicMap[topic][c.id] = c +} + +func (s *webSocketServer) ListenForReplies(c *webSocketConnection) { + s.replyMap[c.id] = c +} + +func (s *webSocketServer) SendReply(message interface{}) { + s.replyChannel <- message +} + +func (s *webSocketServer) processBroadcasts() { + var topics []string + buildCases := func() []reflect.SelectCase { + // only hold the lock while we're building the list of cases (not while doing the select) + s.mux.Lock() + defer s.mux.Unlock() + topics = make([]string, len(s.topics)) + cases := make([]reflect.SelectCase, len(s.topics)+1) + i := 0 + for _, t := range s.topics { + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(t.broadcastChannel)} + topics[i] = t.topic + i++ + } + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(s.newTopic)} + return cases + } + cases := buildCases() + for { + chosen, value, ok := reflect.Select(cases) + if !ok { + log.L(s.ctx).Warn("An error occurred broadcasting the message") + return + } + + if chosen == len(cases)-1 { + // Addition of a new topic + cases = buildCases() + } else { + // Message on one of the existing topics + // Gather all connections interested in this topic and send to them + s.mux.Lock() + topic := topics[chosen] + wsconns := getConnListFromMap(s.topicMap[topic]) + s.mux.Unlock() + s.broadcastToConnections(wsconns, value.Interface()) + } + } +} + +// getConnListFromMap is a simple helper to snapshot a map into a list, which can be called with a short-lived lock +func getConnListFromMap(tm map[string]*webSocketConnection) []*webSocketConnection { + wsconns := make([]*webSocketConnection, 0, len(tm)) + for _, c := range tm { + wsconns = append(wsconns, c) + } + return wsconns +} + +func (s *webSocketServer) processReplies() { + for { + message := <-s.replyChannel + s.mux.Lock() + wsconns := getConnListFromMap(s.replyMap) + s.mux.Unlock() + s.broadcastToConnections(wsconns, message) + } +} + +func (s *webSocketServer) broadcastToConnections(connections []*webSocketConnection, message interface{}) { + for _, c := range connections { + c.broadcast <- message + } +} diff --git a/internal/ws/wsserver_test.go b/internal/ws/wsserver_test.go new file mode 100644 index 00000000..f2b66f0b --- /dev/null +++ b/internal/ws/wsserver_test.go @@ -0,0 +1,353 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ws + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "sync" + "testing" + "time" + + ws "github.com/gorilla/websocket" + + "github.com/stretchr/testify/assert" +) + +func newTestWebSocketServer() (*webSocketServer, *httptest.Server) { + s := NewWebSocketServer(context.Background()).(*webSocketServer) + ts := httptest.NewServer(http.HandlerFunc(s.Handler)) + return s, ts +} + +func TestConnectSendReceiveCycle(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, err := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + }) + + s, _, r := w.GetChannels("") + + s <- "Hello World" + + var val string + c.ReadJSON(&val) + assert.Equal("Hello World", val) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "ignoreme", + }) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "ack", + }) + err = <-r + assert.NoError(err) + + s <- "Don't Panic!" + + c.ReadJSON(&val) + assert.Equal("Don't Panic!", val) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "error", + Message: "Panic!", + }) + + err = <-r + assert.Regexp("Error received from WebSocket client: Panic!", err) + + w.Close() + +} + +func TestConnectTopicIsolation(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, err := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + c1, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + c2, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c1.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + Topic: "topic1", + }) + + c2.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + Topic: "topic2", + }) + + s1, _, r1 := w.GetChannels("topic1") + s2, _, r2 := w.GetChannels("topic2") + + s1 <- "Hello Number 1" + s2 <- "Hello Number 2" + + var val string + c1.ReadJSON(&val) + assert.Equal("Hello Number 1", val) + c1.WriteJSON(&webSocketCommandMessage{ + Type: "ack", + Topic: "topic1", + }) + err = <-r1 + assert.NoError(err) + + c2.ReadJSON(&val) + assert.Equal("Hello Number 2", val) + c2.WriteJSON(&webSocketCommandMessage{ + Type: "ack", + Topic: "topic2", + }) + err = <-r2 + assert.NoError(err) + + w.Close() + +} + +func TestConnectAbandonRequest(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, err := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + }) + _, _, r := w.GetChannels("") + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + select { + case <-r: + break + } + wg.Done() + }() + + // Close the client while we've got an active read stream + c.Close() + + // We whould find the read stream closes out + wg.Wait() + w.Close() + +} + +func TestSpuriousAckProcessing(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + w.processingTimeout = 1 * time.Millisecond + + u, err := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "ack", + Topic: "mytopic", + }) + c.WriteJSON(&webSocketCommandMessage{ + Type: "ack", + Topic: "mytopic", + }) + c.Close() + + for len(w.connections) > 0 { + time.Sleep(1 * time.Millisecond) + } + w.Close() +} + +func TestConnectBadWebsocketHandshake(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, _ := url.Parse(ts.URL) + u.Path = "/ws" + + res, err := http.Get(u.String()) + assert.NoError(err) + assert.Equal(400, res.StatusCode) + + w.Close() + +} + +func TestBroadcast(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, _ := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + topic := "banana" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + Topic: topic, + }) + + // Wait until the client has subscribed to the topic before proceeding + for len(w.topicMap[topic]) == 0 { + time.Sleep(10 * time.Millisecond) + } + + _, b, _ := w.GetChannels(topic) + b <- "Hello World" + + var val string + c.ReadJSON(&val) + assert.Equal("Hello World", val) + + b <- "Hello World Again" + + c.ReadJSON(&val) + assert.Equal("Hello World Again", val) + + w.Close() +} + +func TestBroadcastDefaultTopic(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, _ := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + topic := "" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + }) + + // Wait until the client has subscribed to the topic before proceeding + for len(w.topicMap[topic]) == 0 { + time.Sleep(10 * time.Millisecond) + } + + _, b, _ := w.GetChannels(topic) + b <- "Hello World" + + var val string + c.ReadJSON(&val) + assert.Equal("Hello World", val) + + b <- "Hello World Again" + + c.ReadJSON(&val) + assert.Equal("Hello World Again", val) + + w.Close() +} + +func TestRecvNotOk(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, _ := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + topic := "" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + }) + + // Wait until the client has subscribed to the topic before proceeding + for len(w.topicMap[topic]) == 0 { + time.Sleep(10 * time.Millisecond) + } + + _, b, _ := w.GetChannels(topic) + close(b) + w.Close() +} + +func TestSendReply(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, _ := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listenReplies", + }) + + // Wait until the client has subscribed to the topic before proceeding + for len(w.replyMap) == 0 { + time.Sleep(10 * time.Millisecond) + } + + w.SendReply("Hello World") + + var val string + c.ReadJSON(&val) + assert.Equal("Hello World", val) +} diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index caae49dc..e7b13f23 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -5,7 +5,9 @@ package ffcapimocks import ( context "context" + fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + mock "github.com/stretchr/testify/mock" ) @@ -134,6 +136,29 @@ func (_m *API) EventListenerRemove(ctx context.Context, req *ffcapi.EventListene return r0, r1, r2 } +// EventListenerVerifyOptions provides a mock function with given fields: ctx, options +func (_m *API) EventListenerVerifyOptions(ctx context.Context, options *fftypes.JSONAny) (*fftypes.JSONAny, error) { + ret := _m.Called(ctx, options) + + var r0 *fftypes.JSONAny + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.JSONAny) *fftypes.JSONAny); ok { + r0 = rf(ctx, options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.JSONAny) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.JSONAny) error); ok { + r1 = rf(ctx, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Events provides a mock function with given fields: func (_m *API) Events() <-chan *ffcapi.Event { ret := _m.Called() diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index bc681d07..b7ec6349 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -52,6 +52,9 @@ type API interface { // TransactionSend combines a previously prepared encoded transaction, with a current gas price, and submits it to the transaction pool of the blockchain for mining TransactionSend(ctx context.Context, req *TransactionSendRequest) (*TransactionSendResponse, ErrorReason, error) + // EventListenerVerifyOptions validates the configuration options for a listener, applying any defaults needed by the connector, and returning the update options for FFTM to persist + EventListenerVerifyOptions(ctx context.Context, options *fftypes.JSONAny) (*fftypes.JSONAny, error) + // EventListenerAdd begins/resumes listening on set of events that must be consistently ordered. Blockchain specific signatures of the events are included, along with initial conditions (initial block number etc.), and the last stored checkpoint (if any) EventListenerAdd(ctx context.Context, req *EventListenerAddRequest) (*EventListenerAddResponse, ErrorReason, error) @@ -60,9 +63,6 @@ type API interface { // NewBlockHashes should dynamically push the hashes of all new blocks detected from the blockchain, if confirmations are supported NewBlockHashes() <-chan *BlockHashEvent - - // Events is the channel over which all events are delivered - Events() <-chan *Event } type BlockHashEvent struct { @@ -74,10 +74,10 @@ type BlockHashEvent struct { // The implementation is responsible for ensuring all events on a listener are // ordered on to this channel in the exact sequence from the blockchain. type Event struct { - ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this event - Data *fftypes.JSONAny `json:"data"` // the JSON data to deliver for this event (can be array or object structure) - ProtocolID string `json:"protocolId"` // a protocol identifier for the event, that is string sortable per https://hyperledger.github.io/firefly/reference/types/blockchainevent.html#protocol-id - Info fftypes.JSONObject `json:"info"` // additional blockchain specific information + ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this event + Data *fftypes.JSONAny `json:"data"` // the JSON data to deliver for this event (can be array or object structure) + ProtocolID string `json:"protocolId"` // a protocol identifier for the event, that is string sortable per https://hyperledger.github.io/firefly/reference/types/blockchainevent.html#protocol-id + Info *fftypes.JSONAny `json:"info"` // additional blockchain specific information } // ErrorReason are a set of standard error conditions that a blockchain connector can return diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go index 6a655fe3..355464b2 100644 --- a/pkg/ffcapi/event_listener_add.go +++ b/pkg/ffcapi/event_listener_add.go @@ -21,12 +21,13 @@ import ( ) type EventListenerAddRequest struct { - ID *fftypes.UUID `json:"uuid"` - Events fftypes.JSONObjectArray `json:"events"` - Options fftypes.JSONObject `json:"options"` - Checkpoint fftypes.JSONObject `json:"checkpoint"` + ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event + Filters []*fftypes.JSONAny // The blockchain specific list of filters. The top-level array is an OR list. The semantics within each entry is defined by the blockchain + Options *fftypes.JSONAny // Blockchain specific set of options, such as the first block to detect events from + Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream + Done <-chan struct{} // Channel that will be closed when the event listener needs to stop - the event listener should stop pushing events + EventStream chan<- Event // The event stream to push events to as they are detected - remember to select on Done as well when pushing events } type EventListenerAddResponse struct { - Events chan<- Event } diff --git a/pkg/ffcapi/transaction_receipt.go b/pkg/ffcapi/transaction_receipt.go index 7d385fe5..ac64d05d 100644 --- a/pkg/ffcapi/transaction_receipt.go +++ b/pkg/ffcapi/transaction_receipt.go @@ -25,9 +25,9 @@ type TransactionReceiptRequest struct { } type TransactionReceiptResponse struct { - BlockNumber *fftypes.FFBigInt `json:"blockNumber"` - TransactionIndex *fftypes.FFBigInt `json:"transactionIndex"` - BlockHash string `json:"blockHash"` - Success bool `json:"success"` - ExtraInfo fftypes.JSONObject `json:"extraInfo"` + BlockNumber *fftypes.FFBigInt `json:"blockNumber"` + TransactionIndex *fftypes.FFBigInt `json:"transactionIndex"` + BlockHash string `json:"blockHash"` + Success bool `json:"success"` + ExtraInfo *fftypes.JSONAny `json:"extraInfo"` } diff --git a/pkg/fftm/api_types.go b/pkg/fftm/api_types.go index 92c6f395..c79c857e 100644 --- a/pkg/fftm/api_types.go +++ b/pkg/fftm/api_types.go @@ -26,8 +26,8 @@ import ( type DistributionMode = fftypes.FFEnum var ( - DistributionModeBroadcast = fftypes.FFEnumValue("distmode", "broadcast") - DistributionModeWLD = fftypes.FFEnumValue("distmode", "workloadDistribution") + DistributionModeBroadcast = fftypes.FFEnumValue("distmode", "broadcast") + DistributionModeLoadBalance = fftypes.FFEnumValue("distmode", "load_balance") ) type EventStreamType = fftypes.FFEnum @@ -52,32 +52,30 @@ type EventStream struct { Suspended *bool `ffstruct:"eventstream" json:"suspended,omitempty"` Type *EventStreamType `ffstruct:"eventstream" json:"type,omitempty" ffenum:"estype"` - BatchSize *uint64 `ffstruct:"eventstream" json:"batchSize,omitempty"` - BatchTimeout *uint64 `ffstruct:"eventstream" json:"batchTimeout,omitempty"` - ErrorHandling *ErrorHandlingType `ffstruct:"eventstream" json:"errorHandling,omitempty"` - RetryTimeout *uint64 `ffstruct:"eventstream" json:"retryTimeout,omitempty"` - BlockedRetryDelay *uint64 `ffstruct:"eventstream" json:"blockedRetryDelay,omitempty"` + ErrorHandling *ErrorHandlingType `ffstruct:"eventstream" json:"errorHandling"` + BatchSize *uint64 `ffstruct:"eventstream" json:"batchSize"` + BatchTimeout *fftypes.FFDuration `ffstruct:"eventstream" json:"batchTimeout"` + RetryTimeout *fftypes.FFDuration `ffstruct:"eventstream" json:"retryTimeout"` + BlockedRetryDelay *fftypes.FFDuration `ffstruct:"eventstream" json:"blockedRetryDelay"` - DeprecatedBatchTimeoutMS *uint64 `ffstruct:"eventstream" json:"batchTimeoutMS,omitempty"` // we now allow duration units like 100ms / 10s - DeprecatedRetryTimeoutSec *uint64 `ffstruct:"eventstream" json:"retryTimeoutSec,omitempty"` // we now allow duration units like 100ms / 10s - DeprecatedBlockedRetryDelaySec *uint64 `ffstruct:"eventstream" json:"blockedRetryDelaySec,omitempty"` // we now allow duration units like 100ms / 10s + DeprecatedBatchTimeoutMS *uint64 `ffstruct:"eventstream" json:"batchTimeoutMS,omitempty"` // input only, for backwards compatibility + DeprecatedRetryTimeoutSec *uint64 `ffstruct:"eventstream" json:"retryTimeoutSec,omitempty"` // input only, for backwards compatibility + DeprecatedBlockedRetryDelaySec *uint64 `ffstruct:"eventstream" json:"blockedRetryDelaySec,omitempty"` // input only, for backwards compatibility - Webhook *WebhookConfig `ffstruct:"eventstream" json:"webhook,omitempty"` - WebSocket *WebSocketConfig `ffstruct:"eventstream" json:"websocket,omitempty"` - Options fftypes.JSONObject `ffstruct:"eventstream" json:"options,omitempty"` + Webhook *WebhookConfig `ffstruct:"eventstream" json:"webhook,omitempty"` + WebSocket *WebSocketConfig `ffstruct:"eventstream" json:"websocket,omitempty"` } type WebhookConfig struct { - URL string `ffstruct:"whconfig" json:"url,omitempty"` - Headers map[string]string `ffstruct:"whconfig" json:"headers,omitempty"` - TLSkipHostVerify bool `ffstruct:"whconfig" json:"tlsSkipHostVerify,omitempty"` - RequestTimeoutSec uint32 `ffstruct:"whconfig" json:"requestTimeout,omitempty"` - DeprecatedRequestTimeoutSec uint32 `ffstruct:"whconfig" json:"requestTimeoutSec,omitempty"` // we now allow duration units like 100ms / 10s + URL *string `ffstruct:"whconfig" json:"url,omitempty"` + Headers map[string]string `ffstruct:"whconfig" json:"headers,omitempty"` + TLSkipHostVerify *bool `ffstruct:"whconfig" json:"tlsSkipHostVerify,omitempty"` + RequestTimeout *fftypes.FFDuration `ffstruct:"whconfig" json:"requestTimeout,omitempty"` + DeprecatedRequestTimeoutSec *int64 `ffstruct:"whconfig" json:"requestTimeoutSec,omitempty"` // input only, for backwards compatibility } type WebSocketConfig struct { - Topic string `ffstruct:"wsconfig" json:"topic,omitempty"` - DistributionMode DistributionMode `ffstruct:"wsconfig" json:"distributionMode,omitempty"` + DistributionMode *DistributionMode `ffstruct:"wsconfig" json:"distributionMode,omitempty"` } type Listener struct { @@ -89,69 +87,89 @@ type Listener struct { FromBlock string `ffstruct:"listener" json:"fromBlock,omitempty"` } -func CheckUpdateString(target **string, old *string, new *string, defValue string) bool { - if new == nil { - *target = new +// CheckUpdateString helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateString(changed bool, merged **string, old *string, new *string, defValue string) bool { + if new != nil { + *merged = new } else { - *target = old + *merged = old } - if *target == nil { + if *merged == nil { v := defValue - *target = &v + *merged = &v return true } - return *old != *new + return changed || old == nil || *old != **merged } -func CheckUpdateBool(target **bool, old *bool, new *bool, defValue bool) bool { - if new == nil { - *target = new +// CheckUpdateBool helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateBool(changed bool, merged **bool, old *bool, new *bool, defValue bool) bool { + if new != nil { + *merged = new } else { - *target = old + *merged = old } - if *target == nil { + if *merged == nil { v := defValue - *target = &v + *merged = &v return true } - return *old != *new + return changed || old == nil || *old != **merged } -func CheckUpdateUint64(target **uint64, old *uint64, new *uint64, defValue uint64) bool { - if new == nil { - *target = new +// CheckUpdateUint64 helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateUint64(changed bool, merged **uint64, old *uint64, new *uint64, defValue int64) bool { + if new != nil { + *merged = new } else { - *target = old + *merged = old } - if *target == nil { + if *merged == nil { + v := uint64(defValue) + *merged = &v + return true + } + return changed || old == nil || *old != **merged +} + +// CheckUpdateDuration helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateDuration(changed bool, merged **fftypes.FFDuration, old *fftypes.FFDuration, new *fftypes.FFDuration, defValue fftypes.FFDuration) bool { + if new != nil { + *merged = new + } else { + *merged = old + } + if *merged == nil { v := defValue - *target = &v + *merged = &v return true } - return *old != *new + return changed || old == nil || *old != **merged } -func CheckUpdateEnum(target **fftypes.FFEnum, old *fftypes.FFEnum, new *fftypes.FFEnum, defValue fftypes.FFEnum) bool { - if new == nil { - *target = new +// CheckUpdateEnum helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateEnum(changed bool, merged **fftypes.FFEnum, old *fftypes.FFEnum, new *fftypes.FFEnum, defValue fftypes.FFEnum) bool { + if new != nil { + *merged = new } else { - *target = old + *merged = old } - if *target == nil { + if *merged == nil { v := defValue - *target = &v + *merged = &v return true } - return *old != *new + return changed || old == nil || *old != **merged } -func CheckUpdateObject(target *fftypes.JSONObject, old fftypes.JSONObject, new fftypes.JSONObject) bool { - if new == nil { - *target = old +// CheckUpdateStringMap helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateStringMap(changed bool, merged *map[string]string, old map[string]string, new map[string]string) bool { + if new != nil { + *merged = old return false } - *target = new - if old == nil { + *merged = new + if old == nil || changed { return true } jsonOld, _ := json.Marshal(old) From aa14af53e889b036718b0c70fc552cfa2598b136 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 10 Jun 2022 11:58:40 -0500 Subject: [PATCH 07/95] Add framework for checkpoints and mocks Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 5 +- Makefile | 8 +- internal/events/eventstream.go | 96 ++++++++++++++----- internal/events/eventstream_test.go | 24 ++++- internal/events/listener.go | 9 +- internal/events/webhooks.go | 2 +- internal/events/websockets.go | 2 +- internal/persistence/persistence.go | 35 +++++++ mocks/ffcapimocks/api.go | 16 ---- .../event_stream_persistence.go | 68 +++++++++++++ mocks/wsmocks/web_socket_channels.go | 49 ++++++++++ pkg/ffcapi/api.go | 13 ++- pkg/ffcapi/event_listener_add.go | 12 +-- 13 files changed, 280 insertions(+), 59 deletions(-) create mode 100644 internal/persistence/persistence.go create mode 100644 mocks/persistencemocks/event_stream_persistence.go create mode 100644 mocks/wsmocks/web_socket_channels.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 9756d86e..33d13b82 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,7 @@ "ehtype", "estype", "ethconnect", + "eventsmocks", "eventstream", "eventstreams", "fabconnect", @@ -46,6 +47,7 @@ "Nowarn", "oapispec", "optype", + "persistencemocks", "policyengine", "policyenginemocks", "policyengines", @@ -72,7 +74,8 @@ "whconfig", "workloaddistribution", "wsclient", - "wsconfig" + "wsconfig", + "wsmocks" ], "go.testTimeout": "10s" } diff --git a/Makefile b/Makefile index 8d8a596d..8cc50aa7 100644 --- a/Makefile +++ b/Makefile @@ -31,9 +31,11 @@ mocks-$(strip $(1))-$(strip $(2)): ${MOCKERY} ${MOCKERY} --case underscore --dir $(1) --name $(2) --outpkg $(3) --output mocks/$(strip $(3)) endef -$(eval $(call makemock, pkg/ffcapi, API, ffcapimocks)) -$(eval $(call makemock, pkg/policyengine, PolicyEngine, policyenginemocks)) -$(eval $(call makemock, internal/confirmations, Manager, confirmationsmocks)) +$(eval $(call makemock, pkg/ffcapi, API, ffcapimocks)) +$(eval $(call makemock, pkg/policyengine, PolicyEngine, policyenginemocks)) +$(eval $(call makemock, internal/confirmations, Manager, confirmationsmocks)) +$(eval $(call makemock, internal/persistence, EventStreamPersistence, persistencemocks)) +$(eval $(call makemock, internal/ws, WebSocketChannels, wsmocks)) go-mod-tidy: .ALWAYS $(VGO) mod tidy diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 523cbc47..8220cf18 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -27,6 +27,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-common/pkg/retry" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/internal/ws" @@ -34,13 +35,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" ) -type EventStreamPersistence interface { - StoreCheckpoint(ctx context.Context, streamID *fftypes.UUID, listenerID *fftypes.UUID, checkpoint *fftypes.JSONAny) error - ReadCheckpoint(ctx context.Context, streamID *fftypes.UUID, listenerID *fftypes.UUID) (*fftypes.JSONAny, error) - DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID, listenerID *fftypes.UUID) error -} - -type EventStream interface { +type Stream interface { AddOrUpdateListener(ctx context.Context, s *fftm.Listener) error // Add or update a listener RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener UpdateDefinition(ctx context.Context, updates *fftm.EventStream) error // Apply definition updates (if there are changes) @@ -88,12 +83,13 @@ func InitDefaults() { } type eventStreamAction interface { - attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.Event) error + attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error } type eventStreamBatch struct { number int - events []*ffcapi.Event + events []*ffcapi.EventWithContext + checkpoints map[fftypes.UUID]*fftypes.JSONAny timeoutContext context.Context timeoutCancel func() } @@ -104,7 +100,7 @@ type eventStream struct { mux sync.Mutex state streamState connector ffcapi.API - persistence EventStreamPersistence + persistence persistence.EventStreamPersistence confirmations confirmations.Manager listeners map[fftypes.UUID]*listener wsChannels ws.WebSocketChannels @@ -114,7 +110,7 @@ type eventStream struct { cancelCtx func() action eventStreamAction eventLoopDone chan struct{} - events chan *ffcapi.Event + updates chan *ffcapi.ListenerUpdate } } @@ -122,10 +118,10 @@ func NewEventStream( bgCtx context.Context, persistedSpec *fftm.EventStream, connector ffcapi.API, - persistence EventStreamPersistence, + persistence persistence.EventStreamPersistence, confirmations confirmations.Manager, wsChannels ws.WebSocketChannels, -) (ees EventStream, err error) { +) (ees Stream, err error) { es := &eventStream{ bgCtx: log.WithLogField(bgCtx, "eventstream", persistedSpec.ID.String()), state: streamStateStopped, @@ -296,7 +292,7 @@ func (es *eventStream) Start(ctx context.Context) error { es.startedState.ctx, es.startedState.cancelCtx = context.WithCancel(es.bgCtx) es.initAction() es.startedState.eventLoopDone = make(chan struct{}) - es.startedState.events = make(chan *ffcapi.Event, int(*es.spec.BatchSize)) + es.startedState.updates = make(chan *ffcapi.ListenerUpdate, int(*es.spec.BatchSize)) go es.eventLoop() return nil } @@ -354,10 +350,8 @@ func (es *eventStream) Delete(ctx context.Context) error { // If we error out, that way the caller can retry. es.mux.Lock() defer es.mux.Unlock() - for _, l := range es.listeners { - if err := es.persistence.DeleteCheckpoint(ctx, es.spec.ID, l.ID()); err != nil { - return err - } + if err := es.persistence.DeleteCheckpoint(ctx, es.spec.ID); err != nil { + return err } return es.checkSetStateLocked(ctx, streamStateStopped, streamStateDeleted) } @@ -379,22 +373,43 @@ func (es *eventStream) eventLoop() { timeoutContext = ctx } select { - case event := <-es.startedState.events: + case update := <-es.startedState.updates: if batch == nil { batchNumber++ - batch = &eventStreamBatch{number: batchNumber} + batch = &eventStreamBatch{ + number: batchNumber, + checkpoints: make(map[fftypes.UUID]*fftypes.JSONAny), + } batch.timeoutContext, batch.timeoutCancel = context.WithTimeout(ctx, batchTimeout) } - batch.events = append(batch.events, event) + if update.Checkpoint != nil { + batch.checkpoints[*update.ListenerID] = update.Checkpoint + } + for _, event := range update.Events { + batch.events = append(batch.events, &ffcapi.EventWithContext{ + StreamID: es.spec.ID, + ListenerID: update.ListenerID, + Event: *event, + }) + } case <-timeoutContext.Done(): + if batch == nil { + // The started context exited, we are stopping + log.L(ctx).Debugf("Event poller exiting") + return + } + // Otherwise we timed out timedOut = true } if batch != nil && (timedOut || len(batch.events) >= maxSize) { batch.timeoutCancel() - err := es.performActionWithRetry(batch) + err := es.performActionsWithRetry(batch) + if err == nil { + err = es.writeCheckpoint(batch) + } if err != nil { - log.L(ctx).Debugf("Operation update worker exiting: %s", err) + log.L(ctx).Debugf("Event poller exiting: %s", err) return } batch = nil @@ -403,8 +418,13 @@ func (es *eventStream) eventLoop() { } // performActionWithRetry performs an action, with exponential back-off retry up -// to a given threshold -func (es *eventStream) performActionWithRetry(batch *eventStreamBatch) (err error) { +// to a given threshold. Only returns error in the case that the context is closed. +func (es *eventStream) performActionsWithRetry(batch *eventStreamBatch) (err error) { + // We may not have anything to do, if we only had checkpoints in the batch timeout cycle + if len(batch.events) == 0 { + return nil + } + ctx := es.startedState.ctx startTime := time.Now() for { @@ -425,6 +445,7 @@ func (es *eventStream) performActionWithRetry(batch *eventStreamBatch) (err erro log.L(ctx).Errorf("Batch failed short retry after %.2fs secs. ErrorHandling=%s BlockedRetryDelay=%.2fs ", time.Since(startTime), *es.spec.ErrorHandling, time.Duration(*es.spec.BlockedRetryDelay).Seconds()) if *es.spec.ErrorHandling == fftm.ErrorHandlingTypeSkip { + // Swallow the error now we have logged it return nil } select { @@ -435,3 +456,28 @@ func (es *eventStream) performActionWithRetry(batch *eventStreamBatch) (err erro } } } + +func (es *eventStream) writeCheckpoint(batch *eventStreamBatch) (err error) { + // We update the checkpoints (under lock) for all listeners with events in this batch. + // The last event for any listener in the batch wins. + es.mux.Lock() + cp := &persistence.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Time: fftypes.Now(), + Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), + } + for lID, lCP := range batch.checkpoints { + if l, ok := es.listeners[lID]; ok { + l.UpdateCheckpoint(lCP) + } + } + for lID, l := range es.listeners { + cp.Listeners[lID] = l.checkpoint + } + es.mux.Unlock() + + // We only return if the context is cancelled, or the checkpoint succeeds + return es.retry.Do(es.startedState.ctx, "action", func(attempt int) (retry bool, err error) { + return true, es.persistence.StoreCheckpoint(es.startedState.ctx, cp) + }) +} diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index a880bf8f..4a0c5da6 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -24,14 +24,32 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/wsmocks" "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" "github.com/stretchr/testify/assert" ) -func testESConf(t *testing.T, j string) (es *fftm.EventStream) { - err := json.Unmarshal([]byte(j), &es) +func testESConf(t *testing.T, j string) (spec *fftm.EventStream) { + err := json.Unmarshal([]byte(j), &spec) assert.NoError(t, err) - return es + return spec +} + +func newTestEventStream(t *testing.T) (es *eventStream) { + tmconfig.Reset() + InitDefaults() + ees, err := NewEventStream(context.Background(), testESConf(t, `{ + "name": "ut_stream" + }`), + &ffcapimocks.API{}, + &persistencemocks.EventStreamPersistence{}, + &confirmationsmocks.Manager{}, + &wsmocks.WebSocketChannels{}) + assert.NoError(t, err) + return ees.(*eventStream) } func TestConfigNewDefaultsUpdate(t *testing.T) { diff --git a/internal/events/listener.go b/internal/events/listener.go index bb48404b..9781156f 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -25,10 +25,15 @@ import ( type listener struct { definition *fftm.Listener + checkpoint *fftypes.JSONAny } -func (l *listener) ID() *fftypes.UUID { - return l.definition.ID +func (l *listener) Checkpoint() *fftypes.JSONAny { + return l.checkpoint +} + +func (l *listener) UpdateCheckpoint(cp *fftypes.JSONAny) { + l.checkpoint = cp } func (l *listener) Start(ctx context.Context) error { diff --git a/internal/events/webhooks.go b/internal/events/webhooks.go index 13f55068..e0ac87fe 100644 --- a/internal/events/webhooks.go +++ b/internal/events/webhooks.go @@ -92,7 +92,7 @@ func newWebhookAction(ctx context.Context, spec *fftm.WebhookConfig) *webhookAct } // attemptWebhookAction performs a single attempt of a webhook action -func (w *webhookAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.Event) error { +func (w *webhookAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { // We perform DNS resolution before each attempt, to exclude private IP address ranges from the target u, _ := url.Parse(*w.spec.URL) addr, err := net.ResolveIPAddr("ip4", u.Hostname()) diff --git a/internal/events/websockets.go b/internal/events/websockets.go index 0b28b9cb..bb669059 100644 --- a/internal/events/websockets.go +++ b/internal/events/websockets.go @@ -68,7 +68,7 @@ func newWebSocketAction(parentCtx context.Context, wsChannels ws.WebSocketChanne } // attemptBatch attempts to deliver a batch over socket IO -func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.Event) error { +func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { var err error // Get a blocking channel to send and receive on our chosen namespace diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go new file mode 100644 index 00000000..8ed04aa1 --- /dev/null +++ b/internal/persistence/persistence.go @@ -0,0 +1,35 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 persistence + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type EventStreamCheckpoint struct { + StreamID *fftypes.UUID `json:"streamId"` + Time *fftypes.FFTime `json:"time"` + Listeners map[fftypes.UUID]*fftypes.JSONAny `json:"listeners"` +} + +type EventStreamPersistence interface { + StoreCheckpoint(ctx context.Context, checkpoint *EventStreamCheckpoint) error + ReadCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*EventStreamCheckpoint, error) + DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error +} diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index e7b13f23..c58d28f5 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -159,22 +159,6 @@ func (_m *API) EventListenerVerifyOptions(ctx context.Context, options *fftypes. return r0, r1 } -// Events provides a mock function with given fields: -func (_m *API) Events() <-chan *ffcapi.Event { - ret := _m.Called() - - var r0 <-chan *ffcapi.Event - if rf, ok := ret.Get(0).(func() <-chan *ffcapi.Event); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(<-chan *ffcapi.Event) - } - } - - return r0 -} - // GasPriceEstimate provides a mock function with given fields: ctx, req func (_m *API) GasPriceEstimate(ctx context.Context, req *ffcapi.GasPriceEstimateRequest) (*ffcapi.GasPriceEstimateResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) diff --git a/mocks/persistencemocks/event_stream_persistence.go b/mocks/persistencemocks/event_stream_persistence.go new file mode 100644 index 00000000..3e68d879 --- /dev/null +++ b/mocks/persistencemocks/event_stream_persistence.go @@ -0,0 +1,68 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package persistencemocks + +import ( + context "context" + + fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" + mock "github.com/stretchr/testify/mock" + + persistence "github.com/hyperledger/firefly-transaction-manager/internal/persistence" +) + +// EventStreamPersistence is an autogenerated mock type for the EventStreamPersistence type +type EventStreamPersistence struct { + mock.Mock +} + +// DeleteCheckpoint provides a mock function with given fields: ctx, streamID +func (_m *EventStreamPersistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error { + ret := _m.Called(ctx, streamID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, streamID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ReadCheckpoint provides a mock function with given fields: ctx, streamID +func (_m *EventStreamPersistence) ReadCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*persistence.EventStreamCheckpoint, error) { + ret := _m.Called(ctx, streamID) + + var r0 *persistence.EventStreamCheckpoint + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *persistence.EventStreamCheckpoint); ok { + r0 = rf(ctx, streamID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*persistence.EventStreamCheckpoint) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { + r1 = rf(ctx, streamID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// StoreCheckpoint provides a mock function with given fields: ctx, checkpoint +func (_m *EventStreamPersistence) StoreCheckpoint(ctx context.Context, checkpoint *persistence.EventStreamCheckpoint) error { + ret := _m.Called(ctx, checkpoint) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *persistence.EventStreamCheckpoint) error); ok { + r0 = rf(ctx, checkpoint) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/wsmocks/web_socket_channels.go b/mocks/wsmocks/web_socket_channels.go new file mode 100644 index 00000000..593f108e --- /dev/null +++ b/mocks/wsmocks/web_socket_channels.go @@ -0,0 +1,49 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package wsmocks + +import mock "github.com/stretchr/testify/mock" + +// WebSocketChannels is an autogenerated mock type for the WebSocketChannels type +type WebSocketChannels struct { + mock.Mock +} + +// GetChannels provides a mock function with given fields: topic +func (_m *WebSocketChannels) GetChannels(topic string) (chan<- interface{}, chan<- interface{}, <-chan error) { + ret := _m.Called(topic) + + var r0 chan<- interface{} + if rf, ok := ret.Get(0).(func(string) chan<- interface{}); ok { + r0 = rf(topic) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(chan<- interface{}) + } + } + + var r1 chan<- interface{} + if rf, ok := ret.Get(1).(func(string) chan<- interface{}); ok { + r1 = rf(topic) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(chan<- interface{}) + } + } + + var r2 <-chan error + if rf, ok := ret.Get(2).(func(string) <-chan error); ok { + r2 = rf(topic) + } else { + if ret.Get(2) != nil { + r2 = ret.Get(2).(<-chan error) + } + } + + return r0, r1, r2 +} + +// SendReply provides a mock function with given fields: message +func (_m *WebSocketChannels) SendReply(message interface{}) { + _m.Called(message) +} diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index b7ec6349..fa399759 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -74,12 +74,23 @@ type BlockHashEvent struct { // The implementation is responsible for ensuring all events on a listener are // ordered on to this channel in the exact sequence from the blockchain. type Event struct { - ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this event Data *fftypes.JSONAny `json:"data"` // the JSON data to deliver for this event (can be array or object structure) ProtocolID string `json:"protocolId"` // a protocol identifier for the event, that is string sortable per https://hyperledger.github.io/firefly/reference/types/blockchainevent.html#protocol-id Info *fftypes.JSONAny `json:"info"` // additional blockchain specific information } +type EventWithContext struct { + StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event + ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this event + Event +} + +type ListenerUpdate struct { + ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this update - expected to be the same for all events in the events array + Checkpoint *fftypes.JSONAny `json:"checkpoint"` // checkpoint information for the listener. This should be supplied regularly even if there are no events, to minimize recovery time after restart + Events []*Event `json:"events,omitempty"` // zero or more events. Can be nil for checkpoint-only updates +} + // ErrorReason are a set of standard error conditions that a blockchain connector can return // from execution, that affect the action of the transaction manager to the response. // It is important that error mapping is performed for each of these classification diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go index 355464b2..156f927f 100644 --- a/pkg/ffcapi/event_listener_add.go +++ b/pkg/ffcapi/event_listener_add.go @@ -21,12 +21,12 @@ import ( ) type EventListenerAddRequest struct { - ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event - Filters []*fftypes.JSONAny // The blockchain specific list of filters. The top-level array is an OR list. The semantics within each entry is defined by the blockchain - Options *fftypes.JSONAny // Blockchain specific set of options, such as the first block to detect events from - Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream - Done <-chan struct{} // Channel that will be closed when the event listener needs to stop - the event listener should stop pushing events - EventStream chan<- Event // The event stream to push events to as they are detected - remember to select on Done as well when pushing events + ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event + Filters []*fftypes.JSONAny // The blockchain specific list of filters. The top-level array is an OR list. The semantics within each entry is defined by the blockchain + Options *fftypes.JSONAny // Blockchain specific set of options, such as the first block to detect events from + Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream + Done <-chan struct{} // Channel that will be closed when the event listener needs to stop - the event listener should stop pushing events + EventStream chan<- ListenerUpdate // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events } type EventListenerAddResponse struct { From 1d8930a885cac3900a7e70f255a614fe9f0ce9bc Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 10 Jun 2022 14:13:20 -0500 Subject: [PATCH 08/95] Tweaks to lifecycle and first UT of start/stop Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 179 ++++++++++++++++++++-------- internal/events/eventstream_test.go | 66 +++++++++- internal/events/listener.go | 39 +++--- internal/events/webhooks.go | 1 - internal/tmconfig/tmconfig.go | 2 +- mocks/ffcapimocks/api.go | 20 ++-- pkg/ffcapi/api.go | 6 +- pkg/ffcapi/event_listener_add.go | 12 +- pkg/fftm/api_types.go | 14 ++- 9 files changed, 243 insertions(+), 96 deletions(-) diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 8220cf18..5b4c6477 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -94,6 +94,15 @@ type eventStreamBatch struct { timeoutCancel func() } +type startedStreamState struct { + ctx context.Context + cancelCtx func() + startTime *fftypes.FFTime + action eventStreamAction + eventLoopDone chan struct{} + updates chan *ffcapi.ListenerUpdate +} + type eventStream struct { bgCtx context.Context spec *fftm.EventStream @@ -105,13 +114,7 @@ type eventStream struct { listeners map[fftypes.UUID]*listener wsChannels ws.WebSocketChannels retry *retry.Retry - startedState struct { - ctx context.Context - cancelCtx func() - action eventStreamAction - eventLoopDone chan struct{} - updates chan *ffcapi.ListenerUpdate - } + currentState *startedStreamState } func NewEventStream( @@ -141,16 +144,17 @@ func NewEventStream( return es, nil } -func (es *eventStream) initAction() { - ctx := es.startedState.ctx +func (es *eventStream) initAction(startedState *startedStreamState) { + ctx := startedState.ctx switch *es.spec.Type { case fftm.EventStreamTypeWebhook: - es.startedState.action = newWebhookAction(ctx, es.spec.Webhook) + startedState.action = newWebhookAction(ctx, es.spec.Webhook) case fftm.EventStreamTypeWebSocket: - es.startedState.action = newWebSocketAction(ctx, es.wsChannels, es.spec.WebSocket, *es.spec.Name) + startedState.action = newWebSocketAction(ctx, es.wsChannels, es.spec.WebSocket, *es.spec.Name) + default: + // mergeValidateEsConfig always be called previous to this + panic(i18n.NewError(ctx, tmmsgs.MsgInvalidStreamType, *es.spec.Type)) } - // mergeValidateEsConfig always be called previous to this - panic(i18n.NewError(ctx, tmmsgs.MsgInvalidStreamType, *es.spec.Type)) } func mergeValidateEsConfig(ctx context.Context, base *fftm.EventStream, updates *fftm.EventStream) (merged *fftm.EventStream, changed bool, err error) { @@ -253,8 +257,73 @@ func (es *eventStream) UpdateDefinition(ctx context.Context, updates *fftm.Event return nil } -func (es *eventStream) AddOrUpdateListener(ctx context.Context, s *fftm.Listener) error { - return nil +func safeCompareFilterList(a, b []fftypes.JSONAny) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *fftm.Listener) error { + + // Allow a single "event" object to be specified instead of a filter, with an optional "address". + // This is migrated to the new syntax: `"filters":[{"address":"0x1235","event":{...}}]` + // (only expected to work for the eth connector that supports address/event) + if spec.Filters == nil && spec.DeprecatedEvent != nil { + migrationFilter := fftypes.JSONObject{ + "event": spec.DeprecatedEvent, + } + if spec.DeprecatedAddress != nil { + migrationFilter["address"] = *spec.DeprecatedAddress + } + spec.Filters = []fftypes.JSONAny{fftypes.JSONAny(migrationFilter.String())} + } + + // The connector needs to validate the options + mergedOptions, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.ListenerOptions{ + FromBlock: spec.FromBlock, + }, spec.Options) + if err != nil { + return err + } + + // Check if this is a new listener, an update, or a no-op + es.mux.Lock() + l, exists := es.listeners[*spec.ID] + if exists { + if mergedOptions == l.options && safeCompareFilterList(spec.Filters, l.filters) { + log.L(ctx).Infof("Event listener already configured on stream") + return nil + } + l.options = mergedOptions + l.filters = spec.Filters + } else { + es.listeners[*spec.ID] = &listener{ + es: es, + id: spec.ID, + options: mergedOptions, + filters: spec.Filters, + } + } + // Take a copy of the current started state, before unlocking + startedState := es.currentState + es.mux.Unlock() + if startedState == nil { + return nil + } + + // We need to restart any streams + if exists { + if err := l.stop(startedState); err != nil { + return err + } + } + return l.start(startedState) } func (es *eventStream) RemoveListener(ctx context.Context, id *fftypes.UUID) error { @@ -289,52 +358,64 @@ func (es *eventStream) Start(ctx context.Context) error { } log.L(ctx).Infof("Starting event stream %s", es) - es.startedState.ctx, es.startedState.cancelCtx = context.WithCancel(es.bgCtx) - es.initAction() - es.startedState.eventLoopDone = make(chan struct{}) - es.startedState.updates = make(chan *ffcapi.ListenerUpdate, int(*es.spec.BatchSize)) - go es.eventLoop() + startedState := &startedStreamState{ + startTime: fftypes.Now(), + eventLoopDone: make(chan struct{}), + updates: make(chan *ffcapi.ListenerUpdate, int(*es.spec.BatchSize)), + } + startedState.ctx, startedState.cancelCtx = context.WithCancel(es.bgCtx) + es.currentState = startedState + es.initAction(startedState) + for _, l := range es.listeners { + if err := l.start(startedState); err != nil { + return err + } + } + go es.eventLoop(startedState) return nil } -func (es *eventStream) requestStop(ctx context.Context) ([]*listener, error) { +func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, error) { es.mux.Lock() + startedState := es.currentState defer es.mux.Unlock() if err := es.checkSetStateLocked(ctx, streamStateStarted, streamStateStopping); err != nil { return nil, err } log.L(ctx).Infof("Stopping event stream %s", es) + if startedState == nil { + return nil, nil + } // Cancel the context, stop stop the event loop, and shut down the action (WebSockets in particular) - es.startedState.cancelCtx() + startedState.cancelCtx() // Stop all the listeners - we hold the lock during this - listeners := make([]*listener, 0, len(es.listeners)) for _, l := range es.listeners { - l.RequestStop(ctx) - listeners = append(listeners, l) + err := l.stop(startedState) + if err != nil { + return nil, err + } } - return listeners, nil + return startedState, nil } func (es *eventStream) Stop(ctx context.Context) error { // Request the stop - this phase is locked, and gives us a safe copy of the listeners array to use outside the lock - listeners, err := es.requestStop(ctx) - if err != nil { + startedState, err := es.requestStop(ctx) + if err != nil || startedState == nil { return err } - // Wait for each listener to stop - for _, l := range listeners { - l.WaitStopped(ctx) - } - // Wait for our event loop to stop - <-es.startedState.eventLoopDone + <-startedState.eventLoopDone // Transition to stopped (takes the lock again) - return es.checkSetState(ctx, streamStateStopping, streamStateStopped) + es.mux.Lock() + es.currentState = nil + defer es.mux.Unlock() + return es.checkSetStateLocked(ctx, streamStateStopping, streamStateStopped) } func (es *eventStream) Delete(ctx context.Context) error { @@ -356,9 +437,9 @@ func (es *eventStream) Delete(ctx context.Context) error { return es.checkSetStateLocked(ctx, streamStateStopped, streamStateDeleted) } -func (es *eventStream) eventLoop() { - defer close(es.startedState.eventLoopDone) - ctx := es.startedState.ctx +func (es *eventStream) eventLoop(startedState *startedStreamState) { + defer close(startedState.eventLoopDone) + ctx := startedState.ctx batchTimeout := time.Duration(*es.spec.BatchTimeout) maxSize := int(*es.spec.BatchSize) batchNumber := 0 @@ -373,7 +454,7 @@ func (es *eventStream) eventLoop() { timeoutContext = ctx } select { - case update := <-es.startedState.updates: + case update := <-startedState.updates: if batch == nil { batchNumber++ batch = &eventStreamBatch{ @@ -404,9 +485,9 @@ func (es *eventStream) eventLoop() { if batch != nil && (timedOut || len(batch.events) >= maxSize) { batch.timeoutCancel() - err := es.performActionsWithRetry(batch) + err := es.performActionsWithRetry(startedState, batch) if err == nil { - err = es.writeCheckpoint(batch) + err = es.writeCheckpoint(startedState, batch) } if err != nil { log.L(ctx).Debugf("Event poller exiting: %s", err) @@ -419,18 +500,18 @@ func (es *eventStream) eventLoop() { // performActionWithRetry performs an action, with exponential back-off retry up // to a given threshold. Only returns error in the case that the context is closed. -func (es *eventStream) performActionsWithRetry(batch *eventStreamBatch) (err error) { +func (es *eventStream) performActionsWithRetry(startedState *startedStreamState, batch *eventStreamBatch) (err error) { // We may not have anything to do, if we only had checkpoints in the batch timeout cycle if len(batch.events) == 0 { return nil } - ctx := es.startedState.ctx + ctx := startedState.ctx startTime := time.Now() for { // Short exponential back-off retry err := es.retry.Do(ctx, "action", func(attempt int) (retry bool, err error) { - err = es.startedState.action.attemptBatch(ctx, batch.number, attempt, batch.events) + err = startedState.action.attemptBatch(ctx, batch.number, attempt, batch.events) if err != nil { log.L(ctx).Errorf("Batch %d attempt %d failed. err=%s", batch.number, attempt, err) @@ -443,7 +524,7 @@ func (es *eventStream) performActionsWithRetry(batch *eventStreamBatch) (err err } // We're in blocked retry delay log.L(ctx).Errorf("Batch failed short retry after %.2fs secs. ErrorHandling=%s BlockedRetryDelay=%.2fs ", - time.Since(startTime), *es.spec.ErrorHandling, time.Duration(*es.spec.BlockedRetryDelay).Seconds()) + time.Since(startTime).Seconds(), *es.spec.ErrorHandling, time.Duration(*es.spec.BlockedRetryDelay).Seconds()) if *es.spec.ErrorHandling == fftm.ErrorHandlingTypeSkip { // Swallow the error now we have logged it return nil @@ -457,7 +538,7 @@ func (es *eventStream) performActionsWithRetry(batch *eventStreamBatch) (err err } } -func (es *eventStream) writeCheckpoint(batch *eventStreamBatch) (err error) { +func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch *eventStreamBatch) (err error) { // We update the checkpoints (under lock) for all listeners with events in this batch. // The last event for any listener in the batch wins. es.mux.Lock() @@ -468,7 +549,7 @@ func (es *eventStream) writeCheckpoint(batch *eventStreamBatch) (err error) { } for lID, lCP := range batch.checkpoints { if l, ok := es.listeners[lID]; ok { - l.UpdateCheckpoint(lCP) + l.checkpoint = lCP } } for lID, l := range es.listeners { @@ -477,7 +558,7 @@ func (es *eventStream) writeCheckpoint(batch *eventStreamBatch) (err error) { es.mux.Unlock() // We only return if the context is cancelled, or the checkpoint succeeds - return es.retry.Do(es.startedState.ctx, "action", func(attempt int) (retry bool, err error) { - return true, es.persistence.StoreCheckpoint(es.startedState.ctx, cp) + return es.retry.Do(startedState.ctx, "action", func(attempt int) (retry bool, err error) { + return true, es.persistence.StoreCheckpoint(startedState.ctx, cp) }) } diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 4a0c5da6..bea2f6cc 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -28,8 +28,10 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" "github.com/hyperledger/firefly-transaction-manager/mocks/wsmocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func testESConf(t *testing.T, j string) (spec *fftm.EventStream) { @@ -78,7 +80,7 @@ func TestConfigNewDefaultsUpdate(t *testing.T) { "suspended":false, "type":"websocket", "websocket": { - "distributionMode":"loadbalanced" + "distributionMode":"load_balance" } }`, string(b)) @@ -179,3 +181,65 @@ func TestConfigNewWebhookRetryMigration(t *testing.T) { assert.Equal(t, fftypes.FFDuration(5*time.Second), *es.Webhook.RequestTimeout) } + +func TestEventStreamsE2EMigrationThenStart(t *testing.T) { + + es := newTestEventStream(t) + + addr := "0x12345" + l := &fftm.Listener{ + ID: fftypes.NewUUID(), + Name: "ut_listener", + DeprecatedAddress: &addr, + DeprecatedEvent: fftypes.JSONAnyPtr(`{"event":"definition"}`), + Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), + FromBlock: "12345", + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.MatchedBy(func(standard *ffcapi.ListenerOptions) bool { + return standard.FromBlock == "12345" + }), mock.MatchedBy(func(customOptions *fftypes.JSONAny) bool { + return customOptions.JSONObject().GetString("option1") == "value1" + })).Return(*fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), nil) + + started := make(chan struct{}) + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + assert.NotNil(t, r.Done) + assert.NotNil(t, r.EventStream) + assert.JSONEq(t, `{ + "event": {"event":"definition"}, + "address": "0x12345" + }`, r.Filters[0].String()) + assert.JSONEq(t, `{ + "option1":"value1", + "option2":"value2" + }`, r.Options.String()) + return r.ID.Equals(l.ID) + })).Run(func(args mock.Arguments) { + close(started) + }).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + + stopped := make(chan struct{}) + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l.ID) + })).Run(func(args mock.Arguments) { + close(stopped) + }).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + + err := es.AddOrUpdateListener(es.bgCtx, l) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + <-started + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-stopped + + mfc.AssertExpectations(t) +} diff --git a/internal/events/listener.go b/internal/events/listener.go index 9781156f..30fb6457 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -17,33 +17,32 @@ package events import ( - "context" - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) type listener struct { - definition *fftm.Listener + es *eventStream + id *fftypes.UUID + filters []fftypes.JSONAny + options fftypes.JSONAny checkpoint *fftypes.JSONAny } -func (l *listener) Checkpoint() *fftypes.JSONAny { - return l.checkpoint -} - -func (l *listener) UpdateCheckpoint(cp *fftypes.JSONAny) { - l.checkpoint = cp -} - -func (l *listener) Start(ctx context.Context) error { - return nil +func (l *listener) stop(startedState *startedStreamState) error { + _, _, err := l.es.connector.EventListenerRemove(startedState.ctx, &ffcapi.EventListenerRemoveRequest{ + ID: l.id, + }) + return err } -func (l *listener) RequestStop(ctx context.Context) { - -} - -func (l *listener) WaitStopped(ctx context.Context) { - +func (l *listener) start(startedState *startedStreamState) error { + _, _, err := l.es.connector.EventListenerAdd(startedState.ctx, &ffcapi.EventListenerAddRequest{ + ID: l.id, + Filters: l.filters, + Options: l.options, + EventStream: startedState.updates, + Done: startedState.ctx.Done(), + }) + return err } diff --git a/internal/events/webhooks.go b/internal/events/webhooks.go index e0ac87fe..dc48b8ab 100644 --- a/internal/events/webhooks.go +++ b/internal/events/webhooks.go @@ -70,7 +70,6 @@ func mergeValidateWhConfig(ctx context.Context, changed bool, base *fftm.Webhook type webhookAction struct { allowPrivateIPs bool - headers map[string]string spec *fftm.WebhookConfig client *resty.Client } diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 70c84860..22e119e3 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -91,7 +91,7 @@ func setDefaults() { viper.SetDefault(string(EventStreamsDefaultsRetryTimeout), "30s") viper.SetDefault(string(EventStreamsDefaultsBlockedRetryDelay), "30s") viper.SetDefault(string(EventStreamsDefaultsWebhookRequestTimeout), "30s") - viper.SetDefault(string(EventStreamsDefaultsWebsocketDistributionMode), "loadbalanced") + viper.SetDefault(string(EventStreamsDefaultsWebsocketDistributionMode), "load_balance") } func Reset() { diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index c58d28f5..a91794e4 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -136,22 +136,20 @@ func (_m *API) EventListenerRemove(ctx context.Context, req *ffcapi.EventListene return r0, r1, r2 } -// EventListenerVerifyOptions provides a mock function with given fields: ctx, options -func (_m *API) EventListenerVerifyOptions(ctx context.Context, options *fftypes.JSONAny) (*fftypes.JSONAny, error) { - ret := _m.Called(ctx, options) +// EventListenerVerifyOptions provides a mock function with given fields: ctx, standardOptions, customOptions +func (_m *API) EventListenerVerifyOptions(ctx context.Context, standardOptions *ffcapi.ListenerOptions, customOptions *fftypes.JSONAny) (fftypes.JSONAny, error) { + ret := _m.Called(ctx, standardOptions, customOptions) - var r0 *fftypes.JSONAny - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.JSONAny) *fftypes.JSONAny); ok { - r0 = rf(ctx, options) + var r0 fftypes.JSONAny + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.ListenerOptions, *fftypes.JSONAny) fftypes.JSONAny); ok { + r0 = rf(ctx, standardOptions, customOptions) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*fftypes.JSONAny) - } + r0 = ret.Get(0).(fftypes.JSONAny) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.JSONAny) error); ok { - r1 = rf(ctx, options) + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.ListenerOptions, *fftypes.JSONAny) error); ok { + r1 = rf(ctx, standardOptions, customOptions) } else { r1 = ret.Error(1) } diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index fa399759..c8c829a3 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -53,7 +53,7 @@ type API interface { TransactionSend(ctx context.Context, req *TransactionSendRequest) (*TransactionSendResponse, ErrorReason, error) // EventListenerVerifyOptions validates the configuration options for a listener, applying any defaults needed by the connector, and returning the update options for FFTM to persist - EventListenerVerifyOptions(ctx context.Context, options *fftypes.JSONAny) (*fftypes.JSONAny, error) + EventListenerVerifyOptions(ctx context.Context, standardOptions *ListenerOptions, customOptions *fftypes.JSONAny) (fftypes.JSONAny, error) // EventListenerAdd begins/resumes listening on set of events that must be consistently ordered. Blockchain specific signatures of the events are included, along with initial conditions (initial block number etc.), and the last stored checkpoint (if any) EventListenerAdd(ctx context.Context, req *EventListenerAddRequest) (*EventListenerAddResponse, ErrorReason, error) @@ -65,6 +65,10 @@ type API interface { NewBlockHashes() <-chan *BlockHashEvent } +type ListenerOptions struct { + FromBlock string `json:"fromBlock"` +} + type BlockHashEvent struct { BlockHashes []string `json:"blockHash"` // zero or more hashes (can be nil) GapPotential bool `json:"gapPotential,omitempty"` // when true, the caller cannot be sure if blocks have been missed (use on reconnect of a websocket for example) diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go index 156f927f..221c1b2e 100644 --- a/pkg/ffcapi/event_listener_add.go +++ b/pkg/ffcapi/event_listener_add.go @@ -21,12 +21,12 @@ import ( ) type EventListenerAddRequest struct { - ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event - Filters []*fftypes.JSONAny // The blockchain specific list of filters. The top-level array is an OR list. The semantics within each entry is defined by the blockchain - Options *fftypes.JSONAny // Blockchain specific set of options, such as the first block to detect events from - Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream - Done <-chan struct{} // Channel that will be closed when the event listener needs to stop - the event listener should stop pushing events - EventStream chan<- ListenerUpdate // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events + ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event + Filters []fftypes.JSONAny // The blockchain specific list of filters. The top-level array is an OR list. The semantics within each entry is defined by the blockchain + Options fftypes.JSONAny // Blockchain specific set of options, such as the first block to detect events from + Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream + Done <-chan struct{} // Channel that will be closed when the event listener needs to stop - the event listener should stop pushing events + EventStream chan<- *ListenerUpdate // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events } type EventListenerAddResponse struct { diff --git a/pkg/fftm/api_types.go b/pkg/fftm/api_types.go index c79c857e..32043d8c 100644 --- a/pkg/fftm/api_types.go +++ b/pkg/fftm/api_types.go @@ -79,12 +79,14 @@ type WebSocketConfig struct { } type Listener struct { - ID *fftypes.UUID `ffstruct:"listener" json:"id,omitempty"` - Name string `ffstruct:"listener" json:"name"` - Stream string `ffstruct:"listener" json:"stream"` - Event *fftypes.JSONAny `ffstruct:"listener" json:"event"` - Options fftypes.JSONObject `ffstruct:"listener" json:"options"` - FromBlock string `ffstruct:"listener" json:"fromBlock,omitempty"` + ID *fftypes.UUID `ffstruct:"listener" json:"id,omitempty"` + Name string `ffstruct:"listener" json:"name"` + Stream string `ffstruct:"listener" json:"stream" ffexcludeoutput:"true"` + DeprecatedAddress *string `ffstruct:"listener" json:"address,omitempty"` + DeprecatedEvent *fftypes.JSONAny `ffstruct:"listener" json:"event,omitempty"` + Filters []fftypes.JSONAny `ffstruct:"listener" json:"filters"` + Options *fftypes.JSONAny `ffstruct:"listener" json:"options"` + FromBlock string `ffstruct:"listener" json:"fromBlock,omitempty"` } // CheckUpdateString helper merges supplied configuration, with a base, and applies a default if unset From 837a4a9315d02e3620e4c97fbfcb535336bb7b69 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 10 Jun 2022 18:02:44 -0500 Subject: [PATCH 09/95] Additional tests and tweaks Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 23 +- internal/events/eventstream_test.go | 562 ++++++++++++++++++++++++++-- internal/tmconfig/tmconfig.go | 1 + internal/tmmsgs/en_error_messges.go | 15 +- internal/ws/wsserver.go | 2 +- 5 files changed, 557 insertions(+), 46 deletions(-) diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 5b4c6477..e22a097e 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -39,7 +39,7 @@ type Stream interface { AddOrUpdateListener(ctx context.Context, s *fftm.Listener) error // Add or update a listener RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener UpdateDefinition(ctx context.Context, updates *fftm.EventStream) error // Apply definition updates (if there are changes) - Definition() *fftm.EventStream // Retrieve the current definition to persist + Definition() *fftm.EventStream // Retrieve the merged definition to persist Start(ctx context.Context) error // Start delivery Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint @@ -243,14 +243,14 @@ func (es *eventStream) UpdateDefinition(ctx context.Context, updates *fftm.Event es.mux.Lock() es.spec = merged - defer es.mux.Unlock() + es.mux.Unlock() if changed { if err := es.Stop(ctx); err != nil { - return i18n.NewError(ctx, tmmsgs.MsgStopFailedUpdatingESConfig) + return i18n.NewError(ctx, tmmsgs.MsgStopFailedUpdatingESConfig, err) } if err := es.Start(ctx); err != nil { - return i18n.NewError(ctx, tmmsgs.MsgStartFailedUpdatingESConfig) + return i18n.NewError(ctx, tmmsgs.MsgStartFailedUpdatingESConfig, err) } } @@ -289,7 +289,7 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *fftm.Liste FromBlock: spec.FromBlock, }, spec.Options) if err != nil { - return err + return i18n.NewError(ctx, tmmsgs.MsgBadListenerOptions, err) } // Check if this is a new listener, an update, or a no-op @@ -298,6 +298,7 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *fftm.Liste if exists { if mergedOptions == l.options && safeCompareFilterList(spec.Filters, l.filters) { log.L(ctx).Infof("Event listener already configured on stream") + es.mux.Unlock() return nil } l.options = mergedOptions @@ -366,13 +367,15 @@ func (es *eventStream) Start(ctx context.Context) error { startedState.ctx, startedState.cancelCtx = context.WithCancel(es.bgCtx) es.currentState = startedState es.initAction(startedState) + go es.eventLoop(startedState) + var lastErr error for _, l := range es.listeners { if err := l.start(startedState); err != nil { - return err + log.L(ctx).Errorf("Failed to start event listener %s: %s", l.id, err) + lastErr = err } } - go es.eventLoop(startedState) - return nil + return lastErr } func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, error) { @@ -384,9 +387,6 @@ func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, er } log.L(ctx).Infof("Stopping event stream %s", es) - if startedState == nil { - return nil, nil - } // Cancel the context, stop stop the event loop, and shut down the action (WebSockets in particular) startedState.cancelCtx() @@ -394,6 +394,7 @@ func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, er for _, l := range es.listeners { err := l.stop(startedState) if err != nil { + _ = es.checkSetStateLocked(ctx, streamStateStopping, streamStateStarted) // restore started state return nil, err } } diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index bea2f6cc..f3f8f745 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -19,10 +19,15 @@ package events import ( "context" "encoding/json" + "fmt" + "net/http" + "net/http/httptest" "testing" "time" + "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" @@ -40,12 +45,11 @@ func testESConf(t *testing.T, j string) (spec *fftm.EventStream) { return spec } -func newTestEventStream(t *testing.T) (es *eventStream) { +func newTestEventStream(t *testing.T, conf string) (es *eventStream) { tmconfig.Reset() + config.Set(tmconfig.EventStreamsDefaultsBatchTimeout, "1us") InitDefaults() - ees, err := NewEventStream(context.Background(), testESConf(t, `{ - "name": "ut_stream" - }`), + ees, err := NewEventStream(context.Background(), testESConf(t, conf), &ffcapimocks.API{}, &persistencemocks.EventStreamPersistence{}, &confirmationsmocks.Manager{}, @@ -54,6 +58,26 @@ func newTestEventStream(t *testing.T) (es *eventStream) { return ees.(*eventStream) } +func mockWSChannels(es *eventStream) (chan interface{}, chan interface{}, chan error) { + wsc := es.wsChannels.(*wsmocks.WebSocketChannels) + senderChannel := make(chan interface{}, 1) + broadcastChannel := make(chan interface{}, 1) + receiverChannel := make(chan error, 1) + wsc.On("GetChannels", "ut_stream").Return((chan<- interface{})(senderChannel), (chan<- interface{})(broadcastChannel), (<-chan error)(receiverChannel)) + return senderChannel, broadcastChannel, receiverChannel +} + +func TestNewTestEventStreamBadConfig(t *testing.T) { + tmconfig.Reset() + InitDefaults() + _, err := NewEventStream(context.Background(), testESConf(t, `{}`), + &ffcapimocks.API{}, + &persistencemocks.EventStreamPersistence{}, + &confirmationsmocks.Manager{}, + &wsmocks.WebSocketChannels{}) + assert.Regexp(t, "FF21028", err) +} + func TestConfigNewDefaultsUpdate(t *testing.T) { tmconfig.Reset() InitDefaults() @@ -129,25 +153,47 @@ func TestConfigNewDefaultsUpdate(t *testing.T) { } -func TestConfigNewMissingName(t *testing.T) { +func TestConfigNewMissingWebhookConf(t *testing.T) { tmconfig.Reset() InitDefaults() - _, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{}`)) - assert.Regexp(t, "FF21028", err) + _, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ + "name": "test", + "type": "webhook", + "websocket": {} + }`)) + assert.Regexp(t, "FF21030", err) } -func TestConfigNewMissingWebhookConf(t *testing.T) { +func TestConfigBadWebSocketDistModeConf(t *testing.T) { tmconfig.Reset() InitDefaults() _, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ "name": "test", - "type": "webhook", - "websocket": {} + "type": "websocket", + "websocket": { + "distributionMode":"wrong" + } }`)) - assert.Regexp(t, "FF21030", err) + assert.Regexp(t, "FF21034", err) + +} + +func TestConfigWebSocketBroadcast(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + es, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ + "name": "test", + "type": "websocket", + "websocket": { + "distributionMode":"broadcast" + } + }`)) + assert.NoError(t, err) + assert.Equal(t, fftm.DistributionModeBroadcast, *es.WebSocket.DistributionMode) } @@ -182,9 +228,24 @@ func TestConfigNewWebhookRetryMigration(t *testing.T) { } -func TestEventStreamsE2EMigrationThenStart(t *testing.T) { +func TestInitActionBadAction(t *testing.T) { + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + badType := fftm.EventStreamType("wrong") + es.spec.Type = &badType + assert.Panics(t, func() { + es.initAction(&startedStreamState{ + ctx: context.Background(), + }) + }) +} + +func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { - es := newTestEventStream(t) + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) addr := "0x12345" l := &fftm.Listener{ @@ -204,29 +265,400 @@ func TestEventStreamsE2EMigrationThenStart(t *testing.T) { return customOptions.JSONObject().GetString("option1") == "value1" })).Return(*fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), nil) - started := make(chan struct{}) + started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { - assert.NotNil(t, r.Done) - assert.NotNil(t, r.EventStream) - assert.JSONEq(t, `{ + started <- r + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.EventStreamPersistence) + msp.On("StoreCheckpoint", mock.Anything, mock.MatchedBy(func(cp *persistence.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" + })).Return(nil) + + senderChannel, _, receiverChannel := mockWSChannels(es) + + err := es.AddOrUpdateListener(es.bgCtx, l) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + assert.JSONEq(t, `{ "event": {"event":"definition"}, "address": "0x12345" }`, r.Filters[0].String()) - assert.JSONEq(t, `{ + assert.JSONEq(t, `{ "option1":"value1", "option2":"value2" }`, r.Options.String()) + + r.EventStream <- &ffcapi.ListenerUpdate{ + ListenerID: l.ID, + Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), + Events: []*ffcapi.Event{ + { + Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), + ProtocolID: "000000000042/000013/000001", + Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), + }, + }, + } + + batch1 := (<-senderChannel).([]*ffcapi.EventWithContext) + assert.Len(t, batch1, 1) + assert.Equal(t, "v1", batch1[0].Data.JSONObject().GetString("k1")) + + receiverChannel <- nil // ack + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.Done + + mfc.AssertExpectations(t) +} + +func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { + + receivedWebhook := make(chan []*ffcapi.EventWithContext, 1) + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/test/path", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "application/json", r.Header.Get("content-type")) + var events []*ffcapi.EventWithContext + err := json.NewDecoder(r.Body).Decode(&events) + assert.NoError(t, err) + receivedWebhook <- events + })) + defer s.Close() + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "type": "webhook", + "webhook": { + "url": "`+fmt.Sprintf("http://%s/test/path", s.Listener.Addr())+`" + } + }`) + + l := &fftm.Listener{ + ID: fftypes.NewUUID(), + Name: "ut_listener", + Filters: []fftypes.JSONAny{ + `{"event":"definition1"}`, + `{"event":"definition2"}`, + }, + Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), + FromBlock: "12345", + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.MatchedBy(func(standard *ffcapi.ListenerOptions) bool { + return standard.FromBlock == "12345" + }), mock.MatchedBy(func(customOptions *fftypes.JSONAny) bool { + return customOptions.JSONObject().GetString("option1") == "value1" + })).Return(*fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), nil) + + started := make(chan *ffcapi.EventListenerAddRequest, 1) + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started <- r + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.EventStreamPersistence) + msp.On("StoreCheckpoint", mock.Anything, mock.MatchedBy(func(cp *persistence.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" + })).Return(nil) + + err := es.AddOrUpdateListener(es.bgCtx, l) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + assert.JSONEq(t, `{"event":"definition1"}`, r.Filters[0].String()) + assert.JSONEq(t, `{"event":"definition2"}`, r.Filters[1].String()) + assert.JSONEq(t, `{ + "option1":"value1", + "option2":"value2" + }`, r.Options.String()) + + r.EventStream <- &ffcapi.ListenerUpdate{ + ListenerID: l.ID, + Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), + Events: []*ffcapi.Event{ + { + Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), + ProtocolID: "000000000042/000013/000001", + Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), + }, + }, + } + + batch1 := <-receivedWebhook + assert.Len(t, batch1, 1) + assert.Equal(t, "v1", batch1[0].Data.JSONObject().GetString("k1")) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.Done + + mfc.AssertExpectations(t) +} + +func TestConnectorRejectListener(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &fftm.Listener{ + ID: fftypes.NewUUID(), + Name: "ut_listener", + Filters: []fftypes.JSONAny{`badness`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), fmt.Errorf("pop")) + + err := es.AddOrUpdateListener(es.bgCtx, l) + assert.Regexp(t, "FF21040.*pop", err) + + mfc.AssertExpectations(t) +} + +func TestUpdateStreamStarted(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &fftm.Listener{ + ID: fftypes.NewUUID(), + Name: "ut_listener", + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil) + + started := make(chan *ffcapi.EventListenerAddRequest, 1) + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started <- r + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Twice() + + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() + + err := es.AddOrUpdateListener(es.bgCtx, l) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + defNoChange := testESConf(t, `{ + "name": "ut_stream" + }`) + err = es.UpdateDefinition(context.Background(), defNoChange) + assert.NoError(t, err) + + defChanged := testESConf(t, `{ + "name": "ut_stream2" + }`) + err = es.UpdateDefinition(context.Background(), defChanged) + assert.NoError(t, err) + + assert.Equal(t, "ut_stream2", *es.Definition().Name) + + <-r.Done + r = <-started + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.Done + + mfc.AssertExpectations(t) +} + +func TestUpdateListenerStarted(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l1 := &fftm.Listener{ + ID: fftypes.NewUUID(), + Name: "ut_listener", + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil).Times(3) + + started := make(chan *ffcapi.EventListenerAddRequest, 1) + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started <- r + return r.ID.Equals(l1.ID) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Twice() + + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l1.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() + + err := es.AddOrUpdateListener(es.bgCtx, l1) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + // Double add the same + err = es.AddOrUpdateListener(es.bgCtx, l1) + assert.NoError(t, err) + + l2 := &fftm.Listener{ + ID: l1.ID, + Name: "ut_listener", + Filters: []fftypes.JSONAny{`{"event":"definition2"}`}, + } + + // Change the event definition + err = es.AddOrUpdateListener(es.bgCtx, l2) + assert.NoError(t, err) + + r = <-started + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.Done + + mfc.AssertExpectations(t) +} + +func TestUpdateListenerFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l1 := &fftm.Listener{ + ID: fftypes.NewUUID(), + Name: "ut_listener", + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil).Times(3) + + started := make(chan *ffcapi.EventListenerAddRequest, 1) + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started <- r + return r.ID.Equals(l1.ID) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l1.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + + err := es.AddOrUpdateListener(es.bgCtx, l1) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + // Double add the same + err = es.AddOrUpdateListener(es.bgCtx, l1) + assert.NoError(t, err) + + l2 := &fftm.Listener{ + ID: l1.ID, + Name: "ut_listener", + Filters: []fftypes.JSONAny{`{"event":"definition2"}`}, + } + + err = es.AddOrUpdateListener(es.bgCtx, l2) + assert.Regexp(t, "pop", err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.Done + + mfc.AssertExpectations(t) +} + +func TestUpdateEventStreamBad(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "old_name" + }`) + + defNoChange := testESConf(t, `{ + "name": "new_name", + "type": "wrong" + }`) + err := es.UpdateDefinition(context.Background(), defNoChange) + assert.Regexp(t, "FF21029", err) + + assert.Equal(t, "old_name", *es.Definition().Name) + +} + +func TestUpdateStreamRestartFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &fftm.Listener{ + ID: fftypes.NewUUID(), + Name: "ut_listener", + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil) + + started := make(chan *ffcapi.EventListenerAddRequest, 1) + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started <- r return r.ID.Equals(l.ID) - })).Run(func(args mock.Arguments) { - close(started) - }).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() - stopped := make(chan struct{}) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l.ID) - })).Run(func(args mock.Arguments) { - close(stopped) - }).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() err := es.AddOrUpdateListener(es.bgCtx, l) assert.NoError(t, err) @@ -234,12 +666,88 @@ func TestEventStreamsE2EMigrationThenStart(t *testing.T) { err = es.Start(es.bgCtx) assert.NoError(t, err) - <-started + r := <-started + + defChanged := testESConf(t, `{ + "name": "ut_stream2" + }`) + err = es.UpdateDefinition(context.Background(), defChanged) + assert.Regexp(t, "FF21032.*pop", err) + + <-r.Done + r = <-started err = es.Stop(es.bgCtx) assert.NoError(t, err) - <-stopped + <-r.Done mfc.AssertExpectations(t) } + +func TestUpdateStreamStopFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &fftm.Listener{ + ID: fftypes.NewUUID(), + Name: "ut_listener", + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil) + + started := make(chan *ffcapi.EventListenerAddRequest, 1) + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started <- r + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() + + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + + err := es.AddOrUpdateListener(es.bgCtx, l) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + defChanged := testESConf(t, `{ + "name": "ut_stream2" + }`) + err = es.UpdateDefinition(context.Background(), defChanged) + assert.Regexp(t, "FF21031.*pop", err) + + <-r.Done + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.Done + + mfc.AssertExpectations(t) +} + +func TestStopWhenNotStarted(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + err := es.Stop(es.bgCtx) + assert.Regexp(t, "FF21027", err) + +} + +func TestSafeCompareFilterListDiffLen(t *testing.T) { + assert.False(t, safeCompareFilterList([]fftypes.JSONAny{}, []fftypes.JSONAny{`{}`})) +} diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 22e119e3..df6e1b31 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -92,6 +92,7 @@ func setDefaults() { viper.SetDefault(string(EventStreamsDefaultsBlockedRetryDelay), "30s") viper.SetDefault(string(EventStreamsDefaultsWebhookRequestTimeout), "30s") viper.SetDefault(string(EventStreamsDefaultsWebsocketDistributionMode), "load_balance") + viper.SetDefault(string(WebhooksAllowPrivateIPs), true) } func Reset() { diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 944f9ba3..2ec65a79 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -37,22 +37,23 @@ var ( MsgPolicyEngineNotRegistered = ffe("FF21019", "No policy engine registered with name '%s'") MsgNoGasConfigSetForPolicyEngine = ffe("FF21020", "A fixed gas price must be set when not using a gas oracle") MsgErrorQueryingGasOracleAPI = ffe("FF21021", "Error from gas station API [%d]: %s") - MsgErrorInvalidRequest = ffe("FF21022", "Invalid request") - MsgUnsupportedRequestType = ffe("FF21023", "Unsupported request type: %s") + MsgErrorInvalidRequest = ffe("FF21022", "Invalid request", 400) + MsgUnsupportedRequestType = ffe("FF21023", "Unsupported request type: %s", 400) MsgMissingGOTemplate = ffe("FF21024", "Missing template for processing response from Gas Oracle REST API") MsgBadGOTemplate = ffe("FF21025", "Invalid Go template: %s") MsgGasOracleResultError = ffe("FF21026", "Error processing result from gas station API via template") - MsgStreamStateError = ffe("FF21027", "Event stream is in %s state") - MsgMissingName = ffe("FF21028", "Name is required") - MsgInvalidStreamType = ffe("FF21029", "Invalid event stream type '%s'") - MsgMissingWebhookURL = ffe("FF21030", "'url' is required for webhook configuration") + MsgStreamStateError = ffe("FF21027", "Event stream is in %s state", 409) + MsgMissingName = ffe("FF21028", "Name is required", 400) + MsgInvalidStreamType = ffe("FF21029", "Invalid event stream type '%s'", 400) + MsgMissingWebhookURL = ffe("FF21030", "'url' is required for webhook configuration", 400) MsgStopFailedUpdatingESConfig = ffe("FF21031", "Failed to stop event stream to apply updated configuration: %s") MsgStartFailedUpdatingESConfig = ffe("FF21032", "Failed to restart event stream while applying updated configuration: %s") MsgBlockWebhookAddress = ffe("FF21033", "Cannot send Webhook POST to address: %s") - MsgInvalidDistributionMode = ffe("FF21034", "Invalid distribution mode for WebSocket: %s") + MsgInvalidDistributionMode = ffe("FF21034", "Invalid distribution mode for WebSocket: %s", 400) MsgWebhookFailed = ffe("FF21035", "Webhook request failed with status %d") MsgWSErrorFromClient = ffe("FF21036", "Error received from WebSocket client: %s") MsgWebSocketClosed = ffe("FF21037", "WebSocket '%s' closed") MsgWebSocketInterruptedSend = ffe("FF21038", "Interrupted waiting for WebSocket connection to send event") MsgWebSocketInterruptedReceive = ffe("FF21039", "Interrupted waiting for WebSocket acknowledgment") + MsgBadListenerOptions = ffe("FF21040", "Invalid listener options: %s", 400) ) diff --git a/internal/ws/wsserver.go b/internal/ws/wsserver.go index 8d0a3ca3..0201ae2d 100644 --- a/internal/ws/wsserver.go +++ b/internal/ws/wsserver.go @@ -32,7 +32,7 @@ import ( // WebSocketChannels is provided to allow us to do a blocking send to a namespace that will complete once a client connects on it // We also provide a channel to listen on for closing of the connection, to allow a select to wake on a blocking send type WebSocketChannels interface { - GetChannels(topic string) (chan<- interface{}, chan<- interface{}, <-chan error) + GetChannels(topic string) (senderChannel chan<- interface{}, broadcastChannel chan<- interface{}, receiverChannel <-chan error) SendReply(message interface{}) } From fb8bed6a85b4f8502df116fd14dc422a2b218b5d Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 13 Jun 2022 17:29:19 -0400 Subject: [PATCH 10/95] Work through coverage on events Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 50 +++--- internal/events/eventstream_test.go | 256 +++++++++++++++++++++++++++- internal/events/webhooks.go | 14 +- internal/events/webhooks_test.go | 107 ++++++++++++ internal/events/websockets.go | 31 ++-- internal/events/websockets_test.go | 98 +++++++++++ internal/tmmsgs/en_error_messges.go | 6 +- 7 files changed, 511 insertions(+), 51 deletions(-) create mode 100644 internal/events/webhooks_test.go create mode 100644 internal/events/websockets_test.go diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index e22a097e..01ed63ed 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -82,9 +82,7 @@ func InitDefaults() { } } -type eventStreamAction interface { - attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error -} +type eventStreamAction func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error type eventStreamBatch struct { number int @@ -148,9 +146,9 @@ func (es *eventStream) initAction(startedState *startedStreamState) { ctx := startedState.ctx switch *es.spec.Type { case fftm.EventStreamTypeWebhook: - startedState.action = newWebhookAction(ctx, es.spec.Webhook) + startedState.action = newWebhookAction(ctx, es.spec.Webhook).attemptBatch case fftm.EventStreamTypeWebSocket: - startedState.action = newWebSocketAction(ctx, es.wsChannels, es.spec.WebSocket, *es.spec.Name) + startedState.action = newWebSocketAction(es.wsChannels, es.spec.WebSocket, *es.spec.Name).attemptBatch default: // mergeValidateEsConfig always be called previous to this panic(i18n.NewError(ctx, tmmsgs.MsgInvalidStreamType, *es.spec.Type)) @@ -327,21 +325,31 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *fftm.Liste return l.start(startedState) } -func (es *eventStream) RemoveListener(ctx context.Context, id *fftypes.UUID) error { - return nil +func (es *eventStream) RemoveListener(ctx context.Context, id *fftypes.UUID) (err error) { + es.mux.Lock() + l, exists := es.listeners[*id] + if !exists { + log.L(ctx).Warnf("Removing listener not in map: %s", id) + es.mux.Unlock() + return nil + } + startedState := es.currentState + delete(es.listeners, *id) + es.mux.Unlock() + + log.L(ctx).Warnf("Removing listener: %s", id) + if startedState != nil { + err = l.stop(startedState) + } + return err } func (es *eventStream) String() string { return es.spec.ID.String() } +// checkSetState - caller must have locked the mux when calling this func (es *eventStream) checkSetState(ctx context.Context, requiredState streamState, newState ...streamState) error { - es.mux.Lock() - defer es.mux.Unlock() - return es.checkSetStateLocked(ctx, requiredState, newState...) -} - -func (es *eventStream) checkSetStateLocked(ctx context.Context, requiredState streamState, newState ...streamState) error { if es.state != requiredState { return i18n.NewError(ctx, tmmsgs.MsgStreamStateError, es.state) } @@ -354,7 +362,7 @@ func (es *eventStream) checkSetStateLocked(ctx context.Context, requiredState st func (es *eventStream) Start(ctx context.Context) error { es.mux.Lock() defer es.mux.Unlock() - if err := es.checkSetStateLocked(ctx, streamStateStopped, streamStateStarted); err != nil { + if err := es.checkSetState(ctx, streamStateStopped, streamStateStarted); err != nil { return err } log.L(ctx).Infof("Starting event stream %s", es) @@ -382,7 +390,7 @@ func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, er es.mux.Lock() startedState := es.currentState defer es.mux.Unlock() - if err := es.checkSetStateLocked(ctx, streamStateStarted, streamStateStopping); err != nil { + if err := es.checkSetState(ctx, streamStateStarted, streamStateStopping); err != nil { return nil, err } log.L(ctx).Infof("Stopping event stream %s", es) @@ -394,7 +402,7 @@ func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, er for _, l := range es.listeners { err := l.stop(startedState) if err != nil { - _ = es.checkSetStateLocked(ctx, streamStateStopping, streamStateStarted) // restore started state + _ = es.checkSetState(ctx, streamStateStopping, streamStateStarted) // restore started state return nil, err } } @@ -416,7 +424,7 @@ func (es *eventStream) Stop(ctx context.Context) error { es.mux.Lock() es.currentState = nil defer es.mux.Unlock() - return es.checkSetStateLocked(ctx, streamStateStopping, streamStateStopped) + return es.checkSetState(ctx, streamStateStopping, streamStateStopped) } func (es *eventStream) Delete(ctx context.Context) error { @@ -435,7 +443,7 @@ func (es *eventStream) Delete(ctx context.Context) error { if err := es.persistence.DeleteCheckpoint(ctx, es.spec.ID); err != nil { return err } - return es.checkSetStateLocked(ctx, streamStateStopped, streamStateDeleted) + return es.checkSetState(ctx, streamStateStopped, streamStateDeleted) } func (es *eventStream) eventLoop(startedState *startedStreamState) { @@ -512,11 +520,11 @@ func (es *eventStream) performActionsWithRetry(startedState *startedStreamState, for { // Short exponential back-off retry err := es.retry.Do(ctx, "action", func(attempt int) (retry bool, err error) { - err = startedState.action.attemptBatch(ctx, batch.number, attempt, batch.events) + err = startedState.action(ctx, batch.number, attempt, batch.events) if err != nil { log.L(ctx).Errorf("Batch %d attempt %d failed. err=%s", batch.number, attempt, err) - return time.Since(startTime) > time.Duration(*es.spec.RetryTimeout), err + return time.Since(startTime) < time.Duration(*es.spec.RetryTimeout), err } return false, nil }) @@ -559,7 +567,7 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * es.mux.Unlock() // We only return if the context is cancelled, or the checkpoint succeeds - return es.retry.Do(startedState.ctx, "action", func(attempt int) (retry bool, err error) { + return es.retry.Do(startedState.ctx, "checkpoint", func(attempt int) (retry bool, err error) { return true, es.persistence.StoreCheckpoint(startedState.ctx, cp) }) } diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index f3f8f745..f778e014 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -58,8 +58,7 @@ func newTestEventStream(t *testing.T, conf string) (es *eventStream) { return ees.(*eventStream) } -func mockWSChannels(es *eventStream) (chan interface{}, chan interface{}, chan error) { - wsc := es.wsChannels.(*wsmocks.WebSocketChannels) +func mockWSChannels(wsc *wsmocks.WebSocketChannels) (chan interface{}, chan interface{}, chan error) { senderChannel := make(chan interface{}, 1) broadcastChannel := make(chan interface{}, 1) receiverChannel := make(chan error, 1) @@ -280,7 +279,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" })).Return(nil) - senderChannel, _, receiverChannel := mockWSChannels(es) + senderChannel, _, receiverChannel := mockWSChannels(es.wsChannels.(*wsmocks.WebSocketChannels)) err := es.AddOrUpdateListener(es.bgCtx, l) assert.NoError(t, err) @@ -288,6 +287,9 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { err = es.Start(es.bgCtx) assert.NoError(t, err) + err = es.Start(es.bgCtx) // double start is error + assert.Regexp(t, "FF21027", err) + r := <-started assert.JSONEq(t, `{ @@ -501,7 +503,55 @@ func TestUpdateStreamStarted(t *testing.T) { mfc.AssertExpectations(t) } -func TestUpdateListenerStarted(t *testing.T) { +func TestAddRemoveListener(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &fftm.Listener{ + ID: fftypes.NewUUID(), + Name: "ut_listener", + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil) + + started := make(chan *ffcapi.EventListenerAddRequest, 1) + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started <- r + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + + err := es.AddOrUpdateListener(es.bgCtx, l) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + err = es.RemoveListener(es.bgCtx, l.ID) + assert.NoError(t, err) + + err = es.RemoveListener(es.bgCtx, l.ID) + assert.NoError(t, err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.Done + + mfc.AssertExpectations(t) +} + +func TestUpdateListenerAndDeleteStarted(t *testing.T) { es := newTestEventStream(t, `{ "name": "ut_stream" @@ -527,6 +577,10 @@ func TestUpdateListenerStarted(t *testing.T) { return r.ID.Equals(l1.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() + msp := es.persistence.(*persistencemocks.EventStreamPersistence) + msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(fmt.Errorf("pop")).Once() + msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) + err := es.AddOrUpdateListener(es.bgCtx, l1) assert.NoError(t, err) @@ -551,7 +605,10 @@ func TestUpdateListenerStarted(t *testing.T) { r = <-started - err = es.Stop(es.bgCtx) + err = es.Delete(es.bgCtx) + assert.Regexp(t, "pop", err) + + err = es.Delete(es.bgCtx) assert.NoError(t, err) <-r.Done @@ -707,7 +764,7 @@ func TestUpdateStreamStopFail(t *testing.T) { return r.ID.Equals(l.ID) })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() - mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Twice() mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l.ID) @@ -727,7 +784,8 @@ func TestUpdateStreamStopFail(t *testing.T) { err = es.UpdateDefinition(context.Background(), defChanged) assert.Regexp(t, "FF21031.*pop", err) - <-r.Done + err = es.Delete(context.Background()) + assert.Regexp(t, "pop", err) err = es.Stop(es.bgCtx) assert.NoError(t, err) @@ -751,3 +809,187 @@ func TestStopWhenNotStarted(t *testing.T) { func TestSafeCompareFilterListDiffLen(t *testing.T) { assert.False(t, safeCompareFilterList([]fftypes.JSONAny{}, []fftypes.JSONAny{`{}`})) } + +func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "websocket": { + "distributionMode": "broadcast" + } + }`) + + l := &fftm.Listener{ + ID: fftypes.NewUUID(), + Name: "ut_listener", + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), + FromBlock: "12345", + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil) + + started := make(chan *ffcapi.EventListenerAddRequest, 1) + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started <- r + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + + first := true + done := make(chan struct{}) + msp := es.persistence.(*persistencemocks.EventStreamPersistence) + msp.On("StoreCheckpoint", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")).Run(func(args mock.Arguments) { + if first { + go func() { + // Close here so we exit the loop + err := es.Stop(es.bgCtx) + assert.NoError(t, err) + close(done) + }() + first = false + } + }) + + _, broadcastChannel, _ := mockWSChannels(es.wsChannels.(*wsmocks.WebSocketChannels)) + + err := es.AddOrUpdateListener(es.bgCtx, l) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + r.EventStream <- &ffcapi.ListenerUpdate{ + ListenerID: l.ID, + Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), + Events: []*ffcapi.Event{ + { + Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), + ProtocolID: "000000000042/000013/000001", + Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), + }, + }, + } + batch1 := (<-broadcastChannel).([]*ffcapi.EventWithContext) + assert.Len(t, batch1, 1) + assert.Equal(t, "v1", batch1[0].Data.JSONObject().GetString("k1")) + + <-r.Done + <-done + + mfc.AssertExpectations(t) +} + +func TestActionRetryOk(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "errorHandling": "skip", + "retryTimeout": "1s" + }`) + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + es.mux.Lock() + callCount := 0 + es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { + callCount++ + if callCount > 1 { + return nil + } + return fmt.Errorf("pop") + } + es.mux.Unlock() + + // No-op + err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{}) + assert.NoError(t, err) + + // retry then ok + err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ + events: []*ffcapi.EventWithContext{ + {StreamID: es.spec.ID, ListenerID: fftypes.NewUUID()}, + }, + }) + assert.NoError(t, err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + +} + +func TestActionRetrySkip(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "errorHandling": "skip", + "blockedRetryDelay": "0s", + "retryTimeout": "0s" + }`) + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + es.mux.Lock() + es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { + return fmt.Errorf("pop") + } + es.mux.Unlock() + + // Skip behavior + err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ + events: []*ffcapi.EventWithContext{ + {StreamID: es.spec.ID, ListenerID: fftypes.NewUUID()}, + }, + }) + assert.NoError(t, err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + +} + +func TestActionRetryBlock(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "errorHandling": "block", + "blockedRetryDelay": "0s", + "retryTimeout": "0s" + }`) + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + es.mux.Lock() + callCount := 0 + done := make(chan struct{}) + es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { + callCount++ + if callCount == 1 { + go func() { + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + close(done) + }() + } + return fmt.Errorf("pop") + } + es.mux.Unlock() + + // Skip behavior + err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ + events: []*ffcapi.EventWithContext{ + {StreamID: es.spec.ID, ListenerID: fftypes.NewUUID()}, + }, + }) + assert.Regexp(t, "FF00154", err) + + <-done + assert.Greater(t, callCount, 1) +} diff --git a/internal/events/webhooks.go b/internal/events/webhooks.go index dc48b8ab..f48c1fe0 100644 --- a/internal/events/webhooks.go +++ b/internal/events/webhooks.go @@ -74,8 +74,8 @@ type webhookAction struct { client *resty.Client } -func newWebhookAction(ctx context.Context, spec *fftm.WebhookConfig) *webhookAction { - client := ffresty.New(ctx, tmconfig.WebhookPrefix) // majority of settings come from config +func newWebhookAction(bgCtx context.Context, spec *fftm.WebhookConfig) *webhookAction { + client := ffresty.New(bgCtx, tmconfig.WebhookPrefix) // majority of settings come from config client.SetTimeout(time.Duration(*spec.RequestTimeout)) // request timeout set per stream if *spec.TLSkipHostVerify { client.SetTLSClientConfig(&tls.Config{ @@ -96,10 +96,10 @@ func (w *webhookAction) attemptBatch(ctx context.Context, batchNumber, attempt i u, _ := url.Parse(*w.spec.URL) addr, err := net.ResolveIPAddr("ip4", u.Hostname()) if err != nil { - return err + return i18n.NewError(ctx, tmmsgs.MsgInvalidHost, u.Hostname()) } if w.isAddressBlocked(addr) { - return i18n.NewError(ctx, tmmsgs.MsgBlockWebhookAddress, u.Hostname()) + return i18n.NewError(ctx, tmmsgs.MsgBlockWebhookAddress, addr, u.Hostname()) } var resBody []byte req := w.client.R(). @@ -113,12 +113,12 @@ func (w *webhookAction) attemptBatch(ctx context.Context, batchNumber, attempt i } res, err := req.Post(u.String()) if err != nil { - log.L(ctx).Errorf("Webhook %s (%s) [%d]: %s", *w.spec.URL, u, res.StatusCode(), err) - return err + log.L(ctx).Errorf("Webhook %s (%s): %s", *w.spec.URL, u, err) + return i18n.NewError(ctx, tmmsgs.MsgWebhookErr, err) } if res.IsError() { log.L(ctx).Errorf("Webhook %s (%s) [%d]: %s", *w.spec.URL, u, res.StatusCode(), resBody) - return i18n.NewError(ctx, tmmsgs.MsgWebhookFailed, res.StatusCode()) + err = i18n.NewError(ctx, tmmsgs.MsgWebhookFailedStatus, res.StatusCode()) } return err } diff --git a/internal/events/webhooks_test.go b/internal/events/webhooks_test.go new file mode 100644 index 00000000..f84164c9 --- /dev/null +++ b/internal/events/webhooks_test.go @@ -0,0 +1,107 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 events + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/stretchr/testify/assert" +) + +func newTestWebhooks(url string) *webhookAction { + tmconfig.Reset() + truthy := true + oneSec := 1 * time.Second + return newWebhookAction(context.Background(), &fftm.WebhookConfig{ + TLSkipHostVerify: &truthy, + URL: &url, + RequestTimeout: (*fftypes.FFDuration)(&oneSec), + }) +} + +func TestWebhooksBadHost(t *testing.T) { + tmconfig.Reset() + ws := newTestWebhooks("http://www.sample.invalid/guaranteed-to-fail") + + err := ws.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + assert.Regexp(t, "FF21041", err) +} + +func TestWebhooksPrivateBlocked(t *testing.T) { + tmconfig.Reset() + ws := newTestWebhooks("http://10.0.0.1/one-of-the-private-ranges") + falsy := false + ws.allowPrivateIPs = falsy + + err := ws.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + assert.Regexp(t, "FF21033", err) +} + +func TestWebhooksCustomHeaders403(t *testing.T) { + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/test/path", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "test-value", r.Header.Get("test-header")) + var events []*ffcapi.EventWithContext + err := json.NewDecoder(r.Body).Decode(&events) + assert.NoError(t, err) + w.WriteHeader(403) + })) + defer s.Close() + + tmconfig.Reset() + ws := newTestWebhooks(fmt.Sprintf("http://%s/test/path", s.Listener.Addr())) + ws.spec.Headers = map[string]string{ + "test-header": "test-value", + } + + done := make(chan struct{}) + go func() { + err := ws.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + assert.Regexp(t, "FF21035.*403", err) + close(done) + }() + <-done +} + +func TestWebhooksCustomHeadersConnectFail(t *testing.T) { + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + s.Close() + + tmconfig.Reset() + ws := newTestWebhooks(fmt.Sprintf("http://%s/test/path", s.Listener.Addr())) + + done := make(chan struct{}) + go func() { + err := ws.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + assert.Regexp(t, "FF21042", err) + close(done) + }() + <-done +} diff --git a/internal/events/websockets.go b/internal/events/websockets.go index bb669059..d876bef6 100644 --- a/internal/events/websockets.go +++ b/internal/events/websockets.go @@ -52,15 +52,13 @@ func mergeValidateWsConfig(ctx context.Context, changed bool, base *fftm.WebSock } type webSocketAction struct { - ctx context.Context topic string spec *fftm.WebSocketConfig wsChannels ws.WebSocketChannels } -func newWebSocketAction(parentCtx context.Context, wsChannels ws.WebSocketChannels, spec *fftm.WebSocketConfig, topic string) *webSocketAction { +func newWebSocketAction(wsChannels ws.WebSocketChannels, spec *fftm.WebSocketConfig, topic string) *webSocketAction { return &webSocketAction{ - ctx: log.WithLogField(parentCtx, "action", "websocket"), spec: spec, wsChannels: wsChannels, topic: topic, @@ -89,7 +87,7 @@ func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt for purging { select { case err1 := <-receiver: - log.L(w.ctx).Warnf("Cleared out spurious ack (could be from previous disconnect). err=%s", err1) + log.L(ctx).Warnf("Cleared out spurious ack (could be from previous disconnect). err=%v", err1) default: purging = false } @@ -99,22 +97,27 @@ func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt select { case channel <- events: break - case <-w.ctx.Done(): - err = i18n.NewError(w.ctx, tmmsgs.MsgWebSocketInterruptedSend) + case <-ctx.Done(): + err = i18n.NewError(ctx, tmmsgs.MsgWebSocketInterruptedSend) } // If we ever add more distribution modes, we may want to change this logic from a simple if statement if err == nil && *w.spec.DistributionMode != fftm.DistributionModeBroadcast { - // Wait for the next ack or exception - select { - case err = <-receiver: - break - case <-w.ctx.Done(): - err = i18n.NewError(w.ctx, tmmsgs.MsgWebSocketInterruptedReceive) - } + err = w.waitForAck(ctx, receiver) } // Pass back any exception from the client - log.L(w.ctx).Infof("WebSocket event batch %d complete (len=%d). err=%v", batchNumber, len(events), err) + log.L(ctx).Infof("WebSocket event batch %d complete (len=%d). err=%v", batchNumber, len(events), err) + return err +} + +func (w *webSocketAction) waitForAck(ctx context.Context, receiver <-chan error) (err error) { + // Wait for the next ack or exception + select { + case err = <-receiver: + break + case <-ctx.Done(): + err = i18n.NewError(ctx, tmmsgs.MsgWebSocketInterruptedReceive) + } return err } diff --git a/internal/events/websockets_test.go b/internal/events/websockets_test.go new file mode 100644 index 00000000..8cbf9ba6 --- /dev/null +++ b/internal/events/websockets_test.go @@ -0,0 +1,98 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 events + +import ( + "context" + "testing" + + "github.com/hyperledger/firefly-transaction-manager/mocks/wsmocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/stretchr/testify/assert" +) + +func TestWSAttemptBatchBadDistMode(t *testing.T) { + + mws := &wsmocks.WebSocketChannels{} + mockWSChannels(mws) + + dmw := fftm.DistributionMode("wrong") + wsa := newWebSocketAction(mws, &fftm.WebSocketConfig{ + DistributionMode: &dmw, + }, "ut_stream") + + err := wsa.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + assert.Regexp(t, "FF21034", err) + +} + +func TestWSAttemptBatchPurge(t *testing.T) { + + mws := &wsmocks.WebSocketChannels{} + _, _, rc := mockWSChannels(mws) + rc <- nil + + dmw := fftm.DistributionModeBroadcast + wsa := newWebSocketAction(mws, &fftm.WebSocketConfig{ + DistributionMode: &dmw, + }, "ut_stream") + + err := wsa.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + assert.NoError(t, err) + + select { + case <-rc: + assert.Fail(t, "Should not be anything left on the ack channel - should have been purged before send") + default: + } +} + +func TestWSAttemptBatchExitPushingEvent(t *testing.T) { + + mws := &wsmocks.WebSocketChannels{} + _, bc, _ := mockWSChannels(mws) + bc <- []*ffcapi.EventWithContext{} // block the broadcast channel + + dmw := fftm.DistributionModeBroadcast + wsa := newWebSocketAction(mws, &fftm.WebSocketConfig{ + DistributionMode: &dmw, + }, "ut_stream") + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := wsa.attemptBatch(ctx, 0, 0, []*ffcapi.EventWithContext{}) + assert.Regexp(t, "FF21038", err) + +} + +func TestWSAttemptBatchExitReceivingReply(t *testing.T) { + + mws := &wsmocks.WebSocketChannels{} + _, _, rc := mockWSChannels(mws) + + dmw := fftm.DistributionModeBroadcast + wsa := newWebSocketAction(mws, &fftm.WebSocketConfig{ + DistributionMode: &dmw, + }, "ut_stream") + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := wsa.waitForAck(ctx, rc) + assert.Regexp(t, "FF21039", err) + +} diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 2ec65a79..b69df52d 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -48,12 +48,14 @@ var ( MsgMissingWebhookURL = ffe("FF21030", "'url' is required for webhook configuration", 400) MsgStopFailedUpdatingESConfig = ffe("FF21031", "Failed to stop event stream to apply updated configuration: %s") MsgStartFailedUpdatingESConfig = ffe("FF21032", "Failed to restart event stream while applying updated configuration: %s") - MsgBlockWebhookAddress = ffe("FF21033", "Cannot send Webhook POST to address: %s") + MsgBlockWebhookAddress = ffe("FF21033", "Cannot send Webhook POST to address '%s' for host '%s'") MsgInvalidDistributionMode = ffe("FF21034", "Invalid distribution mode for WebSocket: %s", 400) - MsgWebhookFailed = ffe("FF21035", "Webhook request failed with status %d") + MsgWebhookFailedStatus = ffe("FF21035", "Webhook request failed with status %d") MsgWSErrorFromClient = ffe("FF21036", "Error received from WebSocket client: %s") MsgWebSocketClosed = ffe("FF21037", "WebSocket '%s' closed") MsgWebSocketInterruptedSend = ffe("FF21038", "Interrupted waiting for WebSocket connection to send event") MsgWebSocketInterruptedReceive = ffe("FF21039", "Interrupted waiting for WebSocket acknowledgment") MsgBadListenerOptions = ffe("FF21040", "Invalid listener options: %s", 400) + MsgInvalidHost = ffe("FF21041", "Cannot send Webhook POST to host '%s': %s") + MsgWebhookErr = ffe("FF21042", "Webhook request failed: %s") ) From 05e3e10a23a50cc869a17d0f702b2321ecdd045a Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 13 Jun 2022 18:08:27 -0400 Subject: [PATCH 11/95] Coverage of struct-change utilty functions Signed-off-by: Peter Broadhurst --- pkg/fftm/api_types.go | 11 ++- pkg/fftm/api_types_test.go | 177 +++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 pkg/fftm/api_types_test.go diff --git a/pkg/fftm/api_types.go b/pkg/fftm/api_types.go index 32043d8c..84c67349 100644 --- a/pkg/fftm/api_types.go +++ b/pkg/fftm/api_types.go @@ -167,14 +167,17 @@ func CheckUpdateEnum(changed bool, merged **fftypes.FFEnum, old *fftypes.FFEnum, // CheckUpdateStringMap helper merges supplied configuration, with a base, and applies a default if unset func CheckUpdateStringMap(changed bool, merged *map[string]string, old map[string]string, new map[string]string) bool { if new != nil { + *merged = new + changed = changed || (old == nil) + } else { *merged = old - return false + return false // new was nil, we cannot have changed } - *merged = new - if old == nil || changed { + if changed { return true } + // We need to compare otherwise jsonOld, _ := json.Marshal(old) - jsonNew, _ := json.Marshal(old) + jsonNew, _ := json.Marshal(new) return !bytes.Equal(jsonOld, jsonNew) } diff --git a/pkg/fftm/api_types_test.go b/pkg/fftm/api_types_test.go new file mode 100644 index 00000000..d8608ed0 --- /dev/null +++ b/pkg/fftm/api_types_test.go @@ -0,0 +1,177 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "testing" + "time" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/stretchr/testify/assert" +) + +func TestCheckUpdateString(t *testing.T) { + var val1 = "val1" + var val2 = "val2" + var pVal3 *string + + changed := CheckUpdateString(false, &pVal3, nil, nil, "defVal") + assert.Equal(t, "defVal", *pVal3) // the default won + assert.True(t, changed) + + changed = CheckUpdateString(false, &pVal3, &val1, &val2, "differentDefault") + assert.Equal(t, "val2", *pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateString(true, &pVal3, &val2, &val2, "differentDefault") + assert.Equal(t, "val2", *pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateString(false, &pVal3, &val2, &val2, "differentDefault") + assert.Equal(t, "val2", *pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateString(false, &pVal3, &val1, nil, "differentDefault") + assert.Equal(t, "val1", *pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestCheckUpdateBool(t *testing.T) { + var val1 = true + var val2 = false + var pVal3 *bool + + changed := CheckUpdateBool(false, &pVal3, nil, nil, true) + assert.Equal(t, true, *pVal3) // the default won + assert.True(t, changed) + + changed = CheckUpdateBool(false, &pVal3, &val1, &val2, false) + assert.Equal(t, false, *pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateBool(true, &pVal3, &val2, &val2, false) + assert.Equal(t, false, *pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateBool(false, &pVal3, &val2, &val2, false) + assert.Equal(t, false, *pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateBool(false, &pVal3, &val1, nil, false) + assert.Equal(t, true, *pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestCheckUpdateInt64(t *testing.T) { + var val1 uint64 = 1111 + var val2 uint64 = 2222 + var pVal3 *uint64 + + changed := CheckUpdateUint64(false, &pVal3, nil, nil, 3333) + assert.Equal(t, uint64(3333), *pVal3) // the default won + assert.True(t, changed) + + changed = CheckUpdateUint64(false, &pVal3, &val1, &val2, 4444) + assert.Equal(t, uint64(2222), *pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateUint64(true, &pVal3, &val2, &val2, 4444) + assert.Equal(t, uint64(2222), *pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateUint64(false, &pVal3, &val2, &val2, 4444) + assert.Equal(t, uint64(2222), *pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateUint64(false, &pVal3, &val1, nil, 4444) + assert.Equal(t, uint64(1111), *pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestCheckUpdateDuration(t *testing.T) { + var val1 fftypes.FFDuration = fftypes.FFDuration(1111 * time.Second) + var val2 fftypes.FFDuration = fftypes.FFDuration(2222 * time.Second) + var pVal3 *fftypes.FFDuration + + changed := CheckUpdateDuration(false, &pVal3, nil, nil, fftypes.FFDuration(3333*time.Second)) + assert.Equal(t, fftypes.FFDuration(3333*time.Second), *pVal3) // the default won + assert.True(t, changed) + + changed = CheckUpdateDuration(false, &pVal3, &val1, &val2, fftypes.FFDuration(4444*time.Second)) + assert.Equal(t, fftypes.FFDuration(2222*time.Second), *pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateDuration(true, &pVal3, &val2, &val2, fftypes.FFDuration(4444*time.Second)) + assert.Equal(t, fftypes.FFDuration(2222*time.Second), *pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateDuration(false, &pVal3, &val2, &val2, fftypes.FFDuration(4444*time.Second)) + assert.Equal(t, fftypes.FFDuration(2222*time.Second), *pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateDuration(false, &pVal3, &val1, nil, fftypes.FFDuration(4444*time.Second)) + assert.Equal(t, fftypes.FFDuration(1111*time.Second), *pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestCheckUpdateEnum(t *testing.T) { + var val1 fftypes.FFEnum = fftypes.FFEnum("val1") + var val2 fftypes.FFEnum = fftypes.FFEnum("val2") + var pVal3 *fftypes.FFEnum + + changed := CheckUpdateEnum(false, &pVal3, nil, nil, fftypes.FFEnum("def1")) + assert.Equal(t, fftypes.FFEnum("def1"), *pVal3) // the default won + assert.True(t, changed) + + changed = CheckUpdateEnum(false, &pVal3, &val1, &val2, fftypes.FFEnum("def2")) + assert.Equal(t, fftypes.FFEnum("val2"), *pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateEnum(true, &pVal3, &val2, &val2, fftypes.FFEnum("def2")) + assert.Equal(t, fftypes.FFEnum("val2"), *pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateEnum(false, &pVal3, &val2, &val2, fftypes.FFEnum("def2")) + assert.Equal(t, fftypes.FFEnum("val2"), *pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateEnum(false, &pVal3, &val1, nil, fftypes.FFEnum("def2")) + assert.Equal(t, fftypes.FFEnum("val1"), *pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestCheckUpdateStringMap(t *testing.T) { + val1 := map[string]string{"key1": "val1"} + val2 := map[string]string{"key2": "val2"} + var pVal3 map[string]string + + changed := CheckUpdateStringMap(false, &pVal3, val1, val2) + assert.Equal(t, map[string]string{"key2": "val2"}, pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateStringMap(true, &pVal3, val2, val2) + assert.Equal(t, map[string]string{"key2": "val2"}, pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateStringMap(false, &pVal3, val2, val2) + assert.Equal(t, map[string]string{"key2": "val2"}, pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateStringMap(false, &pVal3, val1, nil) + assert.Equal(t, map[string]string{"key1": "val1"}, pVal3) // val1 won + assert.False(t, changed) // which was the current value +} From 916eff32f7f1e15f608a357a3915c4834129c8b3 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 13 Jun 2022 18:31:03 -0400 Subject: [PATCH 12/95] Close coverage gap Signed-off-by: Peter Broadhurst --- internal/ws/wsserver_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/ws/wsserver_test.go b/internal/ws/wsserver_test.go index f2b66f0b..91827be0 100644 --- a/internal/ws/wsserver_test.go +++ b/internal/ws/wsserver_test.go @@ -351,3 +351,21 @@ func TestSendReply(t *testing.T) { c.ReadJSON(&val) assert.Equal("Hello World", val) } + +func TestListenTopicClosing(t *testing.T) { + + w, ts := newTestWebSocketServer() + defer ts.Close() + w.getTopic("test") + + c := &webSocketConnection{ + server: w, + topics: make(map[string]*webSocketTopic), + closing: make(chan struct{}), + newTopic: make(chan bool), + } + close(c.closing) + c.listenTopic(&webSocketTopic{ + topic: "test", + }) +} From e9340c204c70cab8abcfc0f1f74dae6fb22c16a5 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 14 Jun 2022 19:28:03 -0400 Subject: [PATCH 13/95] LevelDB Persistence Implementation Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 6 + Makefile | 2 +- go.mod | 4 +- go.sum | 7 + internal/events/eventstream.go | 8 +- internal/events/eventstream_test.go | 19 +- internal/persistence/leveldb_persistence.go | 216 ++++++++++++ .../persistence/leveldb_persistence_test.go | 321 ++++++++++++++++++ internal/persistence/persistence.go | 27 +- internal/tmconfig/tmconfig.go | 8 + .../event_stream_persistence.go | 68 ---- mocks/persistencemocks/persistence.go | 239 +++++++++++++ pkg/fftm/api_types.go | 8 +- 13 files changed, 841 insertions(+), 92 deletions(-) create mode 100644 internal/persistence/leveldb_persistence.go create mode 100644 internal/persistence/leveldb_persistence_test.go delete mode 100644 mocks/persistencemocks/event_stream_persistence.go create mode 100644 mocks/persistencemocks/persistence.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 33d13b82..48158ef8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "devdocs", "Devel", "distmode", + "Dont", "ehtype", "estype", "ethconnect", @@ -36,11 +37,13 @@ "fftypes", "finalizers", "GJSON", + "goleveldb", "httpserver", "hyperledger", "Infof", "IPFS", "Kaleido", + "leveldb", "loadbalanced", "mtxs", "NATS", @@ -58,6 +61,7 @@ "secp", "sigs", "stretchr", + "syndtr", "sysmessaging", "tekuri", "tmconfig", @@ -68,6 +72,8 @@ "txid", "txtype", "unflushed", + "unmarshalled", + "unmarshalling", "upgrader", "upserts", "Warnf", diff --git a/Makefile b/Makefile index 8cc50aa7..fed1698b 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ endef $(eval $(call makemock, pkg/ffcapi, API, ffcapimocks)) $(eval $(call makemock, pkg/policyengine, PolicyEngine, policyenginemocks)) $(eval $(call makemock, internal/confirmations, Manager, confirmationsmocks)) -$(eval $(call makemock, internal/persistence, EventStreamPersistence, persistencemocks)) +$(eval $(call makemock, internal/persistence, Persistence, persistencemocks)) $(eval $(call makemock, internal/ws, WebSocketChannels, wsmocks)) go-mod-tidy: .ALWAYS diff --git a/go.mod b/go.mod index 35b69a84..d8b4371e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.17 require ( github.com/go-resty/resty/v2 v2.7.0 + github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru v0.5.4 @@ -12,6 +13,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.1 + github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 golang.org/x/text v0.3.7 ) @@ -20,7 +22,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect diff --git a/go.sum b/go.sum index 29ea0634..11cee6ae 100644 --- a/go.sum +++ b/go.sum @@ -502,6 +502,7 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -850,6 +851,7 @@ github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= @@ -1051,6 +1053,8 @@ github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -1248,6 +1252,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1373,12 +1378,14 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 01ed63ed..ad5660c7 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -107,7 +107,7 @@ type eventStream struct { mux sync.Mutex state streamState connector ffcapi.API - persistence persistence.EventStreamPersistence + persistence persistence.Persistence confirmations confirmations.Manager listeners map[fftypes.UUID]*listener wsChannels ws.WebSocketChannels @@ -119,7 +119,7 @@ func NewEventStream( bgCtx context.Context, persistedSpec *fftm.EventStream, connector ffcapi.API, - persistence persistence.EventStreamPersistence, + persistence persistence.Persistence, confirmations confirmations.Manager, wsChannels ws.WebSocketChannels, ) (ees Stream, err error) { @@ -551,7 +551,7 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * // We update the checkpoints (under lock) for all listeners with events in this batch. // The last event for any listener in the batch wins. es.mux.Lock() - cp := &persistence.EventStreamCheckpoint{ + cp := &fftm.EventStreamCheckpoint{ StreamID: es.spec.ID, Time: fftypes.Now(), Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), @@ -568,6 +568,6 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * // We only return if the context is cancelled, or the checkpoint succeeds return es.retry.Do(startedState.ctx, "checkpoint", func(attempt int) (retry bool, err error) { - return true, es.persistence.StoreCheckpoint(startedState.ctx, cp) + return true, es.persistence.WriteCheckpoint(startedState.ctx, cp) }) } diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index f778e014..a16cd466 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -27,7 +27,6 @@ import ( "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" @@ -51,7 +50,7 @@ func newTestEventStream(t *testing.T, conf string) (es *eventStream) { InitDefaults() ees, err := NewEventStream(context.Background(), testESConf(t, conf), &ffcapimocks.API{}, - &persistencemocks.EventStreamPersistence{}, + &persistencemocks.Persistence{}, &confirmationsmocks.Manager{}, &wsmocks.WebSocketChannels{}) assert.NoError(t, err) @@ -71,7 +70,7 @@ func TestNewTestEventStreamBadConfig(t *testing.T) { InitDefaults() _, err := NewEventStream(context.Background(), testESConf(t, `{}`), &ffcapimocks.API{}, - &persistencemocks.EventStreamPersistence{}, + &persistencemocks.Persistence{}, &confirmationsmocks.Manager{}, &wsmocks.WebSocketChannels{}) assert.Regexp(t, "FF21028", err) @@ -274,8 +273,8 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { return r.ID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) - msp := es.persistence.(*persistencemocks.EventStreamPersistence) - msp.On("StoreCheckpoint", mock.Anything, mock.MatchedBy(func(cp *persistence.EventStreamCheckpoint) bool { + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *fftm.EventStreamCheckpoint) bool { return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" })).Return(nil) @@ -378,8 +377,8 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { return r.ID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) - msp := es.persistence.(*persistencemocks.EventStreamPersistence) - msp.On("StoreCheckpoint", mock.Anything, mock.MatchedBy(func(cp *persistence.EventStreamCheckpoint) bool { + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *fftm.EventStreamCheckpoint) bool { return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" })).Return(nil) @@ -577,7 +576,7 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { return r.ID.Equals(l1.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() - msp := es.persistence.(*persistencemocks.EventStreamPersistence) + msp := es.persistence.(*persistencemocks.Persistence) msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(fmt.Errorf("pop")).Once() msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) @@ -840,8 +839,8 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { first := true done := make(chan struct{}) - msp := es.persistence.(*persistencemocks.EventStreamPersistence) - msp.On("StoreCheckpoint", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")).Run(func(args mock.Arguments) { + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")).Run(func(args mock.Arguments) { if first { go func() { // Close here so we exit the loop diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go new file mode 100644 index 00000000..38a917fc --- /dev/null +++ b/internal/persistence/leveldb_persistence.go @@ -0,0 +1,216 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 persistence + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/opt" + "github.com/syndtr/goleveldb/leveldb/util" +) + +type leveldbPersistence struct { + db *leveldb.DB + syncWrites bool +} + +func NewLevelDBPersistence(ctx context.Context) (Persistence, error) { + dbPath := config.GetString(tmconfig.PersistenceLevelDBPath) + db, err := leveldb.OpenFile(dbPath, &opt.Options{ + OpenFilesCacheCapacity: config.GetInt(tmconfig.PersistenceLevelDBMaxHandles), + }) + if err != nil { + return nil, err + } + return &leveldbPersistence{ + db: db, + syncWrites: config.GetBool(tmconfig.PersistenceLevelDBSyncWrites), + }, nil +} + +func (p *leveldbPersistence) checkpointKey(streamID *fftypes.UUID) []byte { + return []byte(fmt.Sprintf("checkpoints/%s", streamID)) +} + +func (p *leveldbPersistence) streamKey(streamID *fftypes.UUID) []byte { + return []byte(fmt.Sprintf("eventstreams/%s", streamID)) +} + +func (p *leveldbPersistence) listenerKey(listenerID *fftypes.UUID) []byte { + return []byte(fmt.Sprintf("listeners/%s", listenerID)) +} + +func (p *leveldbPersistence) writeJSON(ctx context.Context, key []byte, target interface{}) error { + b, err := json.Marshal(target) + if err != nil { + return err + } + log.L(ctx).Debugf("Wrote %s", key) + return p.db.Put(key, b, &opt.WriteOptions{Sync: p.syncWrites}) +} + +func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target interface{}) error { + b, err := p.db.Get(key, &opt.ReadOptions{}) + if err != nil { + if err == leveldb.ErrNotFound { + return nil + } + return err + } + err = json.Unmarshal(b, target) + if err != nil { + return err + } + log.L(ctx).Debugf("Read %s", key) + return nil +} + +func (p *leveldbPersistence) listJSON(ctx context.Context, prefix, after string, limit int, + val func() interface{}, // return a pointer to a pointer variable, of the type to unmarshal + add func(interface{}), // passes back the val() for adding to the list, if the filters match + filters ...func(interface{}) bool, // filters to apply to the val() after unmarshalling +) error { + rangeStart := &util.Range{Start: []byte(prefix)} + if after != "" { + rangeStart.Start = []byte(prefix + after) + } + it := p.db.NewIterator(rangeStart, &opt.ReadOptions{DontFillCache: true}) + defer it.Release() + count := 0 + skippedAfter := false +itLoop: + for it.Next() { + if after != "" && !skippedAfter && bytes.Equal(it.Key(), rangeStart.Start) { + skippedAfter = true // need to skip the first one, as the range is inclusive + continue + } + if !strings.HasPrefix(string(it.Key()), prefix) { + break itLoop + } + v := val() + err := json.Unmarshal(it.Value(), v) + if err != nil { + return err + } + for _, f := range filters { + if !f(v) { + continue itLoop + } + } + add(v) + count++ + if limit > 0 && count >= limit { + return nil + } + } + log.L(ctx).Debugf("Listed %d items", count) + return nil +} + +func (p *leveldbPersistence) deleteKeys(ctx context.Context, keys ...[]byte) error { + for _, key := range keys { + err := p.db.Delete(key, &opt.WriteOptions{Sync: p.syncWrites}) + if err != nil { + return err + } + log.L(ctx).Debugf("Deleted %s", key) + } + return nil +} + +func (p *leveldbPersistence) WriteCheckpoint(ctx context.Context, checkpoint *fftm.EventStreamCheckpoint) error { + return p.writeJSON(ctx, p.checkpointKey(checkpoint.StreamID), checkpoint) +} + +func (p *leveldbPersistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (cp *fftm.EventStreamCheckpoint, err error) { + err = p.readJSON(ctx, p.checkpointKey(streamID), &cp) + return cp, err +} + +func (p *leveldbPersistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error { + return p.deleteKeys(ctx, p.checkpointKey(streamID)) +} + +func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.EventStream, error) { + streams := make([]*fftm.EventStream, 0) + if err := p.listJSON(ctx, "eventstreams/", after.String(), limit, + func() interface{} { var v *fftm.EventStream; return &v }, + func(v interface{}) { streams = append(streams, *(v.(**fftm.EventStream))) }, + ); err != nil { + return nil, err + } + return streams, nil +} + +func (p *leveldbPersistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (es *fftm.EventStream, err error) { + err = p.readJSON(ctx, p.streamKey(streamID), &es) + return es, err +} + +func (p *leveldbPersistence) WriteStream(ctx context.Context, spec *fftm.EventStream) error { + return p.writeJSON(ctx, p.streamKey(spec.ID), spec) +} + +func (p *leveldbPersistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) error { + return p.deleteKeys(ctx, p.streamKey(streamID)) +} + +func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.Listener, error) { + listeners := make([]*fftm.Listener, 0) + if err := p.listJSON(ctx, "listeners/", after.String(), limit, + func() interface{} { var v *fftm.Listener; return &v }, + func(v interface{}) { listeners = append(listeners, *(v.(**fftm.Listener))) }, + ); err != nil { + return nil, err + } + return listeners, nil +} + +func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*fftm.Listener, error) { + listeners := make([]*fftm.Listener, 0) + if err := p.listJSON(ctx, "listeners/", after.String(), limit, + func() interface{} { var v *fftm.Listener; return &v }, + func(v interface{}) { listeners = append(listeners, *(v.(**fftm.Listener))) }, + func(v interface{}) bool { return (*(v.(**fftm.Listener))).StreamID.Equals(streamID) }, + ); err != nil { + return nil, err + } + return listeners, nil +} + +func (p *leveldbPersistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (l *fftm.Listener, err error) { + err = p.readJSON(ctx, p.listenerKey(listenerID), &l) + return l, err +} + +func (p *leveldbPersistence) WriteListener(ctx context.Context, spec *fftm.Listener) error { + return p.writeJSON(ctx, p.listenerKey(spec.ID), spec) +} + +func (p *leveldbPersistence) DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error { + return p.deleteKeys(ctx, p.listenerKey(listenerID)) +} diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go new file mode 100644 index 00000000..69bb5650 --- /dev/null +++ b/internal/persistence/leveldb_persistence_test.go @@ -0,0 +1,321 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 persistence + +import ( + "context" + "io/ioutil" + "os" + "testing" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/stretchr/testify/assert" + "github.com/syndtr/goleveldb/leveldb/opt" +) + +func newTestLevelDBPersistence(t *testing.T) (*leveldbPersistence, func()) { + + dir, err := ioutil.TempDir("", "ldb_*") + assert.NoError(t, err) + + tmconfig.Reset() + config.Set(tmconfig.PersistenceLevelDBPath, dir) + + pp, err := NewLevelDBPersistence(context.Background()) + assert.NoError(t, err) + + // Write some random stuff to the DB + p := pp.(*leveldbPersistence) + for i := 0; i < 26; i++ { + letter := (byte)('a' + i) + key := make([]byte, 10) + for i := range key { + key[i] = letter + } + err := p.db.Put(key, key, &opt.WriteOptions{}) + assert.NoError(t, err) + } + + return p, func() { + os.RemoveAll(dir) + } + +} + +func strPtr(s string) *string { return &s } + +func TestLevelDBInitFail(t *testing.T) { + file, err := ioutil.TempFile("", "ldb_*") + assert.NoError(t, err) + ioutil.WriteFile(file.Name(), []byte("not a leveldb"), 0777) + defer os.Remove(file.Name()) + + tmconfig.Reset() + config.Set(tmconfig.PersistenceLevelDBPath, file.Name()) + + _, err = NewLevelDBPersistence(context.Background()) + assert.Error(t, err) + +} + +func TestReadWriteStreams(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + s1 := &fftm.EventStream{ + ID: UUIDVersion1(), // ensure we get sequentially ascending IDs + Name: strPtr("stream1"), + } + p.WriteStream(ctx, s1) + s2 := &fftm.EventStream{ + ID: UUIDVersion1(), + Name: strPtr("stream2"), + } + p.WriteStream(ctx, s2) + s3 := &fftm.EventStream{ + ID: UUIDVersion1(), + Name: strPtr("stream3"), + } + p.WriteStream(ctx, s3) + + streams, err := p.ListStreams(ctx, nil, 0) + assert.NoError(t, err) + assert.Len(t, streams, 3) + + assert.Equal(t, s1.ID, streams[0].ID) + assert.Equal(t, s2.ID, streams[1].ID) + assert.Equal(t, s3.ID, streams[2].ID) + + // Test pagination + + streams, err = p.ListStreams(ctx, nil, 2) + assert.NoError(t, err) + assert.Len(t, streams, 2) + assert.Equal(t, s1.ID, streams[0].ID) + assert.Equal(t, s2.ID, streams[1].ID) + + streams, err = p.ListStreams(ctx, streams[1].ID, 2) + assert.NoError(t, err) + assert.Len(t, streams, 1) + assert.Equal(t, s3.ID, streams[0].ID) + + // Test delete + + err = p.DeleteStream(ctx, s2.ID) + assert.NoError(t, err) + streams, err = p.ListStreams(ctx, nil, 2) + assert.NoError(t, err) + assert.Len(t, streams, 2) + assert.Equal(t, s1.ID, streams[0].ID) + assert.Equal(t, s3.ID, streams[1].ID) + + // Test get direct + + s, err := p.GetStream(ctx, s3.ID) + assert.NoError(t, err) + assert.Equal(t, s3.ID, s.ID) + assert.Equal(t, s3.Name, s.Name) + + s, err = p.GetStream(ctx, s2.ID) + assert.NoError(t, err) + assert.Nil(t, s) +} + +func TestReadWriteListeners(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + + sID1 := UUIDVersion1() + sID2 := UUIDVersion1() + + s1l1 := &fftm.Listener{ + ID: UUIDVersion1(), + StreamID: sID1, + } + err := p.WriteListener(ctx, s1l1) + assert.NoError(t, err) + + s2l1 := &fftm.Listener{ + ID: UUIDVersion1(), + StreamID: sID2, + } + err = p.WriteListener(ctx, s2l1) + assert.NoError(t, err) + + s1l2 := &fftm.Listener{ + ID: UUIDVersion1(), + StreamID: sID1, + } + err = p.WriteListener(ctx, s1l2) + assert.NoError(t, err) + + listeners, err := p.ListListeners(ctx, nil, 0) + assert.NoError(t, err) + assert.Len(t, listeners, 3) + + assert.Equal(t, s1l1.ID, listeners[0].ID) + assert.Equal(t, s2l1.ID, listeners[1].ID) + assert.Equal(t, s1l2.ID, listeners[2].ID) + + // Test stream filter + + listeners, err = p.ListStreamListeners(ctx, nil, 0, sID1) + assert.NoError(t, err) + assert.Len(t, listeners, 2) + assert.Equal(t, s1l1.ID, listeners[0].ID) + assert.Equal(t, s1l2.ID, listeners[1].ID) + + // Test delete + + err = p.DeleteListener(ctx, s2l1.ID) + assert.NoError(t, err) + listeners, err = p.ListStreamListeners(ctx, nil, 0, sID2) + assert.NoError(t, err) + assert.Len(t, listeners, 0) + + // Test get direct + + l, err := p.GetListener(ctx, s1l2.ID) + assert.NoError(t, err) + assert.Equal(t, s1l2.ID, l.ID) + + l, err = p.GetListener(ctx, s2l1.ID) + assert.NoError(t, err) + assert.Nil(t, l) +} + +func TestReadWriteCheckpoints(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + cp1 := &fftm.EventStreamCheckpoint{ + StreamID: UUIDVersion1(), + } + cp2 := &fftm.EventStreamCheckpoint{ + StreamID: UUIDVersion1(), + } + + err := p.WriteCheckpoint(ctx, cp1) + assert.NoError(t, err) + + err = p.WriteCheckpoint(ctx, cp2) + assert.NoError(t, err) + + err = p.DeleteCheckpoint(ctx, cp1.StreamID) + assert.NoError(t, err) + + err = p.DeleteCheckpoint(ctx, cp1.StreamID) + assert.NoError(t, err) // No-op + + cp, err := p.GetCheckpoint(ctx, cp1.StreamID) + assert.NoError(t, err) + assert.Nil(t, cp) + + cp, err = p.GetCheckpoint(ctx, cp2.StreamID) + assert.NoError(t, err) + assert.Equal(t, cp2.StreamID, cp.StreamID) +} + +func TestListStreamsBadJSON(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + sID := UUIDVersion1() + err := p.db.Put(p.streamKey(sID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListStreams(context.Background(), nil, 0) + assert.Error(t, err) + +} + +func TestListListenersBadJSON(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + lID := UUIDVersion1() + err := p.db.Put(p.listenerKey(lID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListListeners(context.Background(), nil, 0) + assert.Error(t, err) + + _, err = p.ListStreamListeners(context.Background(), nil, 0, UUIDVersion1()) + assert.Error(t, err) + +} + +func TestDeleteStreamFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + err := p.DeleteStream(context.Background(), UUIDVersion1()) + assert.Error(t, err) + +} + +func TestWriteCheckpointFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + id1 := UUIDVersion1() + err := p.WriteCheckpoint(context.Background(), &fftm.EventStreamCheckpoint{ + Listeners: map[fftypes.UUID]*fftypes.JSONAny{ + *id1: fftypes.JSONAnyPtr(`{!!! bad json`), + }, + }) + assert.Error(t, err) + +} + +func TestReadListenerFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + _, err := p.GetListener(context.Background(), UUIDVersion1()) + assert.Error(t, err) + +} + +func TestReadCheckpointFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + sID := UUIDVersion1() + err := p.db.Put(p.checkpointKey(sID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.GetCheckpoint(context.Background(), sID) + assert.Error(t, err) + +} diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go index 8ed04aa1..058315c6 100644 --- a/internal/persistence/persistence.go +++ b/internal/persistence/persistence.go @@ -19,17 +19,30 @@ package persistence import ( "context" + "github.com/google/uuid" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" ) -type EventStreamCheckpoint struct { - StreamID *fftypes.UUID `json:"streamId"` - Time *fftypes.FFTime `json:"time"` - Listeners map[fftypes.UUID]*fftypes.JSONAny `json:"listeners"` +// UUIDVersion1 returns a version 1 UUID - where the alphanumeric sequence is assured to be ascending based on the order of generation +func UUIDVersion1() *fftypes.UUID { + u, _ := uuid.NewUUID() + return (*fftypes.UUID)(&u) } -type EventStreamPersistence interface { - StoreCheckpoint(ctx context.Context, checkpoint *EventStreamCheckpoint) error - ReadCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*EventStreamCheckpoint, error) +type Persistence interface { + WriteCheckpoint(ctx context.Context, checkpoint *fftm.EventStreamCheckpoint) error + GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*fftm.EventStreamCheckpoint, error) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error + + ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.EventStream, error) + GetStream(ctx context.Context, streamID *fftypes.UUID) (*fftm.EventStream, error) + WriteStream(ctx context.Context, spec *fftm.EventStream) error + DeleteStream(ctx context.Context, streamID *fftypes.UUID) error + + ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.Listener, error) + ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*fftm.Listener, error) + GetListener(ctx context.Context, listenerID *fftypes.UUID) (*fftm.Listener, error) + WriteListener(ctx context.Context, spec *fftm.Listener) error + DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error } diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index df6e1b31..2f88a5ce 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -54,6 +54,10 @@ var ( EventStreamsRetryMaxDelay = ffc("eventstreams.retry.maxDelay") EventStreamsRetryFactor = ffc("eventstreams.retry.factor") WebhooksAllowPrivateIPs = ffc("webhooks.allowPrivateIPs") + PersistenceType = ffc("persistence.type") + PersistenceLevelDBPath = ffc("persistence.leveldb.path") + PersistenceLevelDBMaxHandles = ffc("persistence.leveldb.maxHandles") + PersistenceLevelDBSyncWrites = ffc("persistence.leveldb.syncWrites") ) var FFCoreConfig config.Section @@ -93,6 +97,10 @@ func setDefaults() { viper.SetDefault(string(EventStreamsDefaultsWebhookRequestTimeout), "30s") viper.SetDefault(string(EventStreamsDefaultsWebsocketDistributionMode), "load_balance") viper.SetDefault(string(WebhooksAllowPrivateIPs), true) + + viper.SetDefault(string(PersistenceType), "leveldb") + viper.SetDefault(string(PersistenceLevelDBMaxHandles), 100) + viper.SetDefault(string(PersistenceLevelDBSyncWrites), true) } func Reset() { diff --git a/mocks/persistencemocks/event_stream_persistence.go b/mocks/persistencemocks/event_stream_persistence.go deleted file mode 100644 index 3e68d879..00000000 --- a/mocks/persistencemocks/event_stream_persistence.go +++ /dev/null @@ -1,68 +0,0 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. - -package persistencemocks - -import ( - context "context" - - fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" - mock "github.com/stretchr/testify/mock" - - persistence "github.com/hyperledger/firefly-transaction-manager/internal/persistence" -) - -// EventStreamPersistence is an autogenerated mock type for the EventStreamPersistence type -type EventStreamPersistence struct { - mock.Mock -} - -// DeleteCheckpoint provides a mock function with given fields: ctx, streamID -func (_m *EventStreamPersistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error { - ret := _m.Called(ctx, streamID) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { - r0 = rf(ctx, streamID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// ReadCheckpoint provides a mock function with given fields: ctx, streamID -func (_m *EventStreamPersistence) ReadCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*persistence.EventStreamCheckpoint, error) { - ret := _m.Called(ctx, streamID) - - var r0 *persistence.EventStreamCheckpoint - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *persistence.EventStreamCheckpoint); ok { - r0 = rf(ctx, streamID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*persistence.EventStreamCheckpoint) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { - r1 = rf(ctx, streamID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// StoreCheckpoint provides a mock function with given fields: ctx, checkpoint -func (_m *EventStreamPersistence) StoreCheckpoint(ctx context.Context, checkpoint *persistence.EventStreamCheckpoint) error { - ret := _m.Called(ctx, checkpoint) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *persistence.EventStreamCheckpoint) error); ok { - r0 = rf(ctx, checkpoint) - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go new file mode 100644 index 00000000..824e4b02 --- /dev/null +++ b/mocks/persistencemocks/persistence.go @@ -0,0 +1,239 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package persistencemocks + +import ( + context "context" + + fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" + fftm "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + + mock "github.com/stretchr/testify/mock" +) + +// Persistence is an autogenerated mock type for the Persistence type +type Persistence struct { + mock.Mock +} + +// DeleteCheckpoint provides a mock function with given fields: ctx, streamID +func (_m *Persistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error { + ret := _m.Called(ctx, streamID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, streamID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteListener provides a mock function with given fields: ctx, listenerID +func (_m *Persistence) DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error { + ret := _m.Called(ctx, listenerID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, listenerID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteStream provides a mock function with given fields: ctx, streamID +func (_m *Persistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) error { + ret := _m.Called(ctx, streamID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, streamID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetListener provides a mock function with given fields: ctx, listenerID +func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*fftm.Listener, error) { + ret := _m.Called(ctx, listenerID) + + var r0 *fftm.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *fftm.Listener); ok { + r0 = rf(ctx, listenerID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftm.Listener) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { + r1 = rf(ctx, listenerID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStream provides a mock function with given fields: ctx, streamID +func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (*fftm.EventStream, error) { + ret := _m.Called(ctx, streamID) + + var r0 *fftm.EventStream + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *fftm.EventStream); ok { + r0 = rf(ctx, streamID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftm.EventStream) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { + r1 = rf(ctx, streamID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListListeners provides a mock function with given fields: ctx, after, limit +func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.Listener, error) { + ret := _m.Called(ctx, after, limit) + + var r0 []*fftm.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*fftm.Listener); ok { + r0 = rf(ctx, after, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*fftm.Listener) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int) error); ok { + r1 = rf(ctx, after, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListStreamListeners provides a mock function with given fields: ctx, after, limit, streamID +func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*fftm.Listener, error) { + ret := _m.Called(ctx, after, limit, streamID) + + var r0 []*fftm.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, *fftypes.UUID) []*fftm.Listener); ok { + r0 = rf(ctx, after, limit, streamID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*fftm.Listener) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, *fftypes.UUID) error); ok { + r1 = rf(ctx, after, limit, streamID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListStreams provides a mock function with given fields: ctx, after, limit +func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.EventStream, error) { + ret := _m.Called(ctx, after, limit) + + var r0 []*fftm.EventStream + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*fftm.EventStream); ok { + r0 = rf(ctx, after, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*fftm.EventStream) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int) error); ok { + r1 = rf(ctx, after, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetCheckpoint provides a mock function with given fields: ctx, streamID +func (_m *Persistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*fftm.EventStreamCheckpoint, error) { + ret := _m.Called(ctx, streamID) + + var r0 *fftm.EventStreamCheckpoint + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *fftm.EventStreamCheckpoint); ok { + r0 = rf(ctx, streamID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftm.EventStreamCheckpoint) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { + r1 = rf(ctx, streamID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// WriteCheckpoint provides a mock function with given fields: ctx, checkpoint +func (_m *Persistence) WriteCheckpoint(ctx context.Context, checkpoint *fftm.EventStreamCheckpoint) error { + ret := _m.Called(ctx, checkpoint) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftm.EventStreamCheckpoint) error); ok { + r0 = rf(ctx, checkpoint) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// WriteListener provides a mock function with given fields: ctx, spec +func (_m *Persistence) WriteListener(ctx context.Context, spec *fftm.Listener) error { + ret := _m.Called(ctx, spec) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftm.Listener) error); ok { + r0 = rf(ctx, spec) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// WriteStream provides a mock function with given fields: ctx, spec +func (_m *Persistence) WriteStream(ctx context.Context, spec *fftm.EventStream) error { + ret := _m.Called(ctx, spec) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftm.EventStream) error); ok { + r0 = rf(ctx, spec) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/fftm/api_types.go b/pkg/fftm/api_types.go index 84c67349..29c5332f 100644 --- a/pkg/fftm/api_types.go +++ b/pkg/fftm/api_types.go @@ -66,6 +66,12 @@ type EventStream struct { WebSocket *WebSocketConfig `ffstruct:"eventstream" json:"websocket,omitempty"` } +type EventStreamCheckpoint struct { + StreamID *fftypes.UUID `json:"streamId"` + Time *fftypes.FFTime `json:"time"` + Listeners map[fftypes.UUID]*fftypes.JSONAny `json:"listeners"` +} + type WebhookConfig struct { URL *string `ffstruct:"whconfig" json:"url,omitempty"` Headers map[string]string `ffstruct:"whconfig" json:"headers,omitempty"` @@ -81,7 +87,7 @@ type WebSocketConfig struct { type Listener struct { ID *fftypes.UUID `ffstruct:"listener" json:"id,omitempty"` Name string `ffstruct:"listener" json:"name"` - Stream string `ffstruct:"listener" json:"stream" ffexcludeoutput:"true"` + StreamID *fftypes.UUID `ffstruct:"listener" json:"stream" ffexcludeoutput:"true"` DeprecatedAddress *string `ffstruct:"listener" json:"address,omitempty"` DeprecatedEvent *fftypes.JSONAny `ffstruct:"listener" json:"event,omitempty"` Filters []fftypes.JSONAny `ffstruct:"listener" json:"filters"` From 31a7a0035392c502a347735344e049c892c271f1 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 15 Jun 2022 13:08:21 -0400 Subject: [PATCH 14/95] Refactor to avoid circular dependency Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 1 + internal/events/eventstream.go | 70 ++++++------ internal/events/eventstream_test.go | 36 +++---- internal/events/webhooks.go | 24 ++--- internal/events/webhooks_test.go | 4 +- internal/events/websockets.go | 28 ++--- internal/events/websockets_test.go | 18 ++-- internal/persistence/leveldb_persistence.go | 40 +++---- .../persistence/leveldb_persistence_test.go | 20 ++-- internal/persistence/persistence.go | 20 ++-- mocks/persistencemocks/persistence.go | 101 +++++++++--------- pkg/{fftm => apitypes}/api_types.go | 2 +- pkg/{fftm => apitypes}/api_types_test.go | 2 +- 13 files changed, 184 insertions(+), 182 deletions(-) rename pkg/{fftm => apitypes}/api_types.go (99%) rename pkg/{fftm => apitypes}/api_types_test.go (99%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 48158ef8..cda8d766 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "go.lintTool": "golangci-lint", "cSpell.words": [ "APIID", + "apitypes", "ccache", "confirmationsmocks", "dataexchange", diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index ad5660c7..1d8a63b1 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -31,18 +31,18 @@ import ( "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/internal/ws" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" ) type Stream interface { - AddOrUpdateListener(ctx context.Context, s *fftm.Listener) error // Add or update a listener - RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener - UpdateDefinition(ctx context.Context, updates *fftm.EventStream) error // Apply definition updates (if there are changes) - Definition() *fftm.EventStream // Retrieve the merged definition to persist - Start(ctx context.Context) error // Start delivery - Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) - Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint + AddOrUpdateListener(ctx context.Context, s *apitypes.Listener) error // Add or update a listener + RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener + UpdateDefinition(ctx context.Context, updates *apitypes.EventStream) error // Apply definition updates (if there are changes) + Definition() *apitypes.EventStream // Retrieve the merged definition to persist + Start(ctx context.Context) error // Start delivery + Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) + Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint } type streamState string @@ -59,11 +59,11 @@ var esDefaults struct { initialized bool batchSize int64 batchTimeout fftypes.FFDuration - errorHandling fftm.ErrorHandlingType + errorHandling apitypes.ErrorHandlingType retryTimeout fftypes.FFDuration blockedRetryDelay fftypes.FFDuration webhookRequestTimeout fftypes.FFDuration - websocketDistributionMode fftm.DistributionMode + websocketDistributionMode apitypes.DistributionMode retry *retry.Retry } @@ -103,7 +103,7 @@ type startedStreamState struct { type eventStream struct { bgCtx context.Context - spec *fftm.EventStream + spec *apitypes.EventStream mux sync.Mutex state streamState connector ffcapi.API @@ -117,7 +117,7 @@ type eventStream struct { func NewEventStream( bgCtx context.Context, - persistedSpec *fftm.EventStream, + persistedSpec *apitypes.EventStream, connector ffcapi.API, persistence persistence.Persistence, confirmations confirmations.Manager, @@ -145,9 +145,9 @@ func NewEventStream( func (es *eventStream) initAction(startedState *startedStreamState) { ctx := startedState.ctx switch *es.spec.Type { - case fftm.EventStreamTypeWebhook: + case apitypes.EventStreamTypeWebhook: startedState.action = newWebhookAction(ctx, es.spec.Webhook).attemptBatch - case fftm.EventStreamTypeWebSocket: + case apitypes.EventStreamTypeWebSocket: startedState.action = newWebSocketAction(es.wsChannels, es.spec.WebSocket, *es.spec.Name).attemptBatch default: // mergeValidateEsConfig always be called previous to this @@ -155,13 +155,13 @@ func (es *eventStream) initAction(startedState *startedStreamState) { } } -func mergeValidateEsConfig(ctx context.Context, base *fftm.EventStream, updates *fftm.EventStream) (merged *fftm.EventStream, changed bool, err error) { +func mergeValidateEsConfig(ctx context.Context, base *apitypes.EventStream, updates *apitypes.EventStream) (merged *apitypes.EventStream, changed bool, err error) { // Merged is assured to not have any unset values (default set in all cases), or any deprecated fields if base == nil { - base = &fftm.EventStream{} + base = &apitypes.EventStream{} } - merged = &fftm.EventStream{ + merged = &apitypes.EventStream{ ID: base.ID, Created: base.Created, Updated: fftypes.Now(), @@ -173,52 +173,52 @@ func mergeValidateEsConfig(ctx context.Context, base *fftm.EventStream, updates // Name (no default - must be set) // - Note we do not check for uniqueness of the name at this layer in the code, but we do require unique names. // That's the responsibility of the calling code that manages the persistence of the configured streams. - changed = fftm.CheckUpdateString(changed, &merged.Name, base.Name, updates.Name, "") + changed = apitypes.CheckUpdateString(changed, &merged.Name, base.Name, updates.Name, "") if *merged.Name == "" { return nil, false, i18n.NewError(ctx, tmmsgs.MsgMissingName) } // Suspended - changed = fftm.CheckUpdateBool(changed, &merged.Suspended, base.Suspended, updates.Suspended, false) + changed = apitypes.CheckUpdateBool(changed, &merged.Suspended, base.Suspended, updates.Suspended, false) // Batch size - changed = fftm.CheckUpdateUint64(changed, &merged.BatchSize, base.BatchSize, updates.BatchSize, esDefaults.batchSize) + changed = apitypes.CheckUpdateUint64(changed, &merged.BatchSize, base.BatchSize, updates.BatchSize, esDefaults.batchSize) // Error handling mode - changed = fftm.CheckUpdateEnum(changed, &merged.ErrorHandling, base.ErrorHandling, updates.ErrorHandling, esDefaults.errorHandling) + changed = apitypes.CheckUpdateEnum(changed, &merged.ErrorHandling, base.ErrorHandling, updates.ErrorHandling, esDefaults.errorHandling) // Batch timeout if updates.DeprecatedBatchTimeoutMS != nil { dv := fftypes.FFDuration(*updates.DeprecatedBatchTimeoutMS) * fftypes.FFDuration(time.Millisecond) - changed = fftm.CheckUpdateDuration(changed, &merged.BatchTimeout, base.BatchTimeout, &dv, esDefaults.batchTimeout) + changed = apitypes.CheckUpdateDuration(changed, &merged.BatchTimeout, base.BatchTimeout, &dv, esDefaults.batchTimeout) } else { - changed = fftm.CheckUpdateDuration(changed, &merged.BatchTimeout, base.BatchTimeout, updates.BatchTimeout, esDefaults.batchTimeout) + changed = apitypes.CheckUpdateDuration(changed, &merged.BatchTimeout, base.BatchTimeout, updates.BatchTimeout, esDefaults.batchTimeout) } // Retry timeout if updates.DeprecatedRetryTimeoutSec != nil { dv := fftypes.FFDuration(*updates.DeprecatedRetryTimeoutSec) * fftypes.FFDuration(time.Second) - changed = fftm.CheckUpdateDuration(changed, &merged.RetryTimeout, base.RetryTimeout, &dv, esDefaults.retryTimeout) + changed = apitypes.CheckUpdateDuration(changed, &merged.RetryTimeout, base.RetryTimeout, &dv, esDefaults.retryTimeout) } else { - changed = fftm.CheckUpdateDuration(changed, &merged.RetryTimeout, base.RetryTimeout, updates.RetryTimeout, esDefaults.retryTimeout) + changed = apitypes.CheckUpdateDuration(changed, &merged.RetryTimeout, base.RetryTimeout, updates.RetryTimeout, esDefaults.retryTimeout) } // Blocked retry delay if updates.DeprecatedBlockedRetryDelaySec != nil { dv := fftypes.FFDuration(*updates.DeprecatedBlockedRetryDelaySec) * fftypes.FFDuration(time.Second) - changed = fftm.CheckUpdateDuration(changed, &merged.BlockedRetryDelay, base.BlockedRetryDelay, &dv, esDefaults.blockedRetryDelay) + changed = apitypes.CheckUpdateDuration(changed, &merged.BlockedRetryDelay, base.BlockedRetryDelay, &dv, esDefaults.blockedRetryDelay) } else { - changed = fftm.CheckUpdateDuration(changed, &merged.BlockedRetryDelay, base.BlockedRetryDelay, updates.BlockedRetryDelay, esDefaults.blockedRetryDelay) + changed = apitypes.CheckUpdateDuration(changed, &merged.BlockedRetryDelay, base.BlockedRetryDelay, updates.BlockedRetryDelay, esDefaults.blockedRetryDelay) } // Type - changed = fftm.CheckUpdateEnum(changed, &merged.Type, base.Type, updates.Type, fftm.EventStreamTypeWebSocket) + changed = apitypes.CheckUpdateEnum(changed, &merged.Type, base.Type, updates.Type, apitypes.EventStreamTypeWebSocket) switch *merged.Type { - case fftm.EventStreamTypeWebSocket: + case apitypes.EventStreamTypeWebSocket: if merged.WebSocket, changed, err = mergeValidateWsConfig(ctx, changed, base.WebSocket, updates.WebSocket); err != nil { return nil, false, err } - case fftm.EventStreamTypeWebhook: + case apitypes.EventStreamTypeWebhook: if merged.Webhook, changed, err = mergeValidateWhConfig(ctx, changed, base.Webhook, updates.Webhook); err != nil { return nil, false, err } @@ -229,11 +229,11 @@ func mergeValidateEsConfig(ctx context.Context, base *fftm.EventStream, updates return merged, changed, nil } -func (es *eventStream) Definition() *fftm.EventStream { +func (es *eventStream) Definition() *apitypes.EventStream { return es.spec } -func (es *eventStream) UpdateDefinition(ctx context.Context, updates *fftm.EventStream) error { +func (es *eventStream) UpdateDefinition(ctx context.Context, updates *apitypes.EventStream) error { merged, changed, err := mergeValidateEsConfig(ctx, es.spec, updates) if err != nil { return err @@ -267,7 +267,7 @@ func safeCompareFilterList(a, b []fftypes.JSONAny) bool { return true } -func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *fftm.Listener) error { +func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.Listener) error { // Allow a single "event" object to be specified instead of a filter, with an optional "address". // This is migrated to the new syntax: `"filters":[{"address":"0x1235","event":{...}}]` @@ -534,7 +534,7 @@ func (es *eventStream) performActionsWithRetry(startedState *startedStreamState, // We're in blocked retry delay log.L(ctx).Errorf("Batch failed short retry after %.2fs secs. ErrorHandling=%s BlockedRetryDelay=%.2fs ", time.Since(startTime).Seconds(), *es.spec.ErrorHandling, time.Duration(*es.spec.BlockedRetryDelay).Seconds()) - if *es.spec.ErrorHandling == fftm.ErrorHandlingTypeSkip { + if *es.spec.ErrorHandling == apitypes.ErrorHandlingTypeSkip { // Swallow the error now we have logged it return nil } @@ -551,7 +551,7 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * // We update the checkpoints (under lock) for all listeners with events in this batch. // The last event for any listener in the batch wins. es.mux.Lock() - cp := &fftm.EventStreamCheckpoint{ + cp := &apitypes.EventStreamCheckpoint{ StreamID: es.spec.ID, Time: fftypes.Now(), Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index a16cd466..6353422a 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -32,13 +32,13 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" "github.com/hyperledger/firefly-transaction-manager/mocks/wsmocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) -func testESConf(t *testing.T, j string) (spec *fftm.EventStream) { +func testESConf(t *testing.T, j string) (spec *apitypes.EventStream) { err := json.Unmarshal([]byte(j), &spec) assert.NoError(t, err) return spec @@ -191,7 +191,7 @@ func TestConfigWebSocketBroadcast(t *testing.T) { } }`)) assert.NoError(t, err) - assert.Equal(t, fftm.DistributionModeBroadcast, *es.WebSocket.DistributionMode) + assert.Equal(t, apitypes.DistributionModeBroadcast, *es.WebSocket.DistributionMode) } @@ -230,7 +230,7 @@ func TestInitActionBadAction(t *testing.T) { es := newTestEventStream(t, `{ "name": "ut_stream" }`) - badType := fftm.EventStreamType("wrong") + badType := apitypes.EventStreamType("wrong") es.spec.Type = &badType assert.Panics(t, func() { es.initAction(&startedStreamState{ @@ -246,7 +246,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { }`) addr := "0x12345" - l := &fftm.Listener{ + l := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: "ut_listener", DeprecatedAddress: &addr, @@ -274,7 +274,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) - msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *fftm.EventStreamCheckpoint) bool { + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" })).Return(nil) @@ -348,7 +348,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { } }`) - l := &fftm.Listener{ + l := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: "ut_listener", Filters: []fftypes.JSONAny{ @@ -378,7 +378,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) - msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *fftm.EventStreamCheckpoint) bool { + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" })).Return(nil) @@ -427,7 +427,7 @@ func TestConnectorRejectListener(t *testing.T) { "name": "ut_stream" }`) - l := &fftm.Listener{ + l := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: "ut_listener", Filters: []fftypes.JSONAny{`badness`}, @@ -449,7 +449,7 @@ func TestUpdateStreamStarted(t *testing.T) { "name": "ut_stream" }`) - l := &fftm.Listener{ + l := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: "ut_listener", Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, @@ -508,7 +508,7 @@ func TestAddRemoveListener(t *testing.T) { "name": "ut_stream" }`) - l := &fftm.Listener{ + l := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: "ut_listener", Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, @@ -556,7 +556,7 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { "name": "ut_stream" }`) - l1 := &fftm.Listener{ + l1 := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: "ut_listener", Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, @@ -592,7 +592,7 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { err = es.AddOrUpdateListener(es.bgCtx, l1) assert.NoError(t, err) - l2 := &fftm.Listener{ + l2 := &apitypes.Listener{ ID: l1.ID, Name: "ut_listener", Filters: []fftypes.JSONAny{`{"event":"definition2"}`}, @@ -621,7 +621,7 @@ func TestUpdateListenerFail(t *testing.T) { "name": "ut_stream" }`) - l1 := &fftm.Listener{ + l1 := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: "ut_listener", Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, @@ -654,7 +654,7 @@ func TestUpdateListenerFail(t *testing.T) { err = es.AddOrUpdateListener(es.bgCtx, l1) assert.NoError(t, err) - l2 := &fftm.Listener{ + l2 := &apitypes.Listener{ ID: l1.ID, Name: "ut_listener", Filters: []fftypes.JSONAny{`{"event":"definition2"}`}, @@ -694,7 +694,7 @@ func TestUpdateStreamRestartFail(t *testing.T) { "name": "ut_stream" }`) - l := &fftm.Listener{ + l := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: "ut_listener", Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, @@ -747,7 +747,7 @@ func TestUpdateStreamStopFail(t *testing.T) { "name": "ut_stream" }`) - l := &fftm.Listener{ + l := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: "ut_listener", Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, @@ -818,7 +818,7 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { } }`) - l := &fftm.Listener{ + l := &apitypes.Listener{ ID: fftypes.NewUUID(), Name: "ut_listener", Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, diff --git a/internal/events/webhooks.go b/internal/events/webhooks.go index f48c1fe0..2bb788ae 100644 --- a/internal/events/webhooks.go +++ b/internal/events/webhooks.go @@ -31,38 +31,38 @@ import ( "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" ) -func mergeValidateWhConfig(ctx context.Context, changed bool, base *fftm.WebhookConfig, updates *fftm.WebhookConfig) (*fftm.WebhookConfig, bool, error) { +func mergeValidateWhConfig(ctx context.Context, changed bool, base *apitypes.WebhookConfig, updates *apitypes.WebhookConfig) (*apitypes.WebhookConfig, bool, error) { if base == nil { - base = &fftm.WebhookConfig{} + base = &apitypes.WebhookConfig{} } if updates == nil { - updates = &fftm.WebhookConfig{} + updates = &apitypes.WebhookConfig{} } - merged := &fftm.WebhookConfig{} + merged := &apitypes.WebhookConfig{} // URL (no default - must be set) - changed = fftm.CheckUpdateString(changed, &merged.URL, base.URL, updates.URL, "") + changed = apitypes.CheckUpdateString(changed, &merged.URL, base.URL, updates.URL, "") if *merged.URL == "" { return nil, false, i18n.NewError(ctx, tmmsgs.MsgMissingWebhookURL) } // Headers - changed = fftm.CheckUpdateStringMap(changed, &merged.Headers, base.Headers, updates.Headers) + changed = apitypes.CheckUpdateStringMap(changed, &merged.Headers, base.Headers, updates.Headers) // Skip host verify (disable TLS checking) - changed = fftm.CheckUpdateBool(changed, &merged.TLSkipHostVerify, base.TLSkipHostVerify, updates.TLSkipHostVerify, false) + changed = apitypes.CheckUpdateBool(changed, &merged.TLSkipHostVerify, base.TLSkipHostVerify, updates.TLSkipHostVerify, false) // Request timeout if updates.DeprecatedRequestTimeoutSec != nil { dv := fftypes.FFDuration(*updates.DeprecatedRequestTimeoutSec) * fftypes.FFDuration(time.Second) - changed = fftm.CheckUpdateDuration(changed, &merged.RequestTimeout, base.RequestTimeout, &dv, esDefaults.webhookRequestTimeout) + changed = apitypes.CheckUpdateDuration(changed, &merged.RequestTimeout, base.RequestTimeout, &dv, esDefaults.webhookRequestTimeout) } else { - changed = fftm.CheckUpdateDuration(changed, &merged.RequestTimeout, base.RequestTimeout, updates.RequestTimeout, esDefaults.webhookRequestTimeout) + changed = apitypes.CheckUpdateDuration(changed, &merged.RequestTimeout, base.RequestTimeout, updates.RequestTimeout, esDefaults.webhookRequestTimeout) } return merged, changed, nil @@ -70,11 +70,11 @@ func mergeValidateWhConfig(ctx context.Context, changed bool, base *fftm.Webhook type webhookAction struct { allowPrivateIPs bool - spec *fftm.WebhookConfig + spec *apitypes.WebhookConfig client *resty.Client } -func newWebhookAction(bgCtx context.Context, spec *fftm.WebhookConfig) *webhookAction { +func newWebhookAction(bgCtx context.Context, spec *apitypes.WebhookConfig) *webhookAction { client := ffresty.New(bgCtx, tmconfig.WebhookPrefix) // majority of settings come from config client.SetTimeout(time.Duration(*spec.RequestTimeout)) // request timeout set per stream if *spec.TLSkipHostVerify { diff --git a/internal/events/webhooks_test.go b/internal/events/webhooks_test.go index f84164c9..4948f4eb 100644 --- a/internal/events/webhooks_test.go +++ b/internal/events/webhooks_test.go @@ -27,8 +27,8 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" "github.com/stretchr/testify/assert" ) @@ -36,7 +36,7 @@ func newTestWebhooks(url string) *webhookAction { tmconfig.Reset() truthy := true oneSec := 1 * time.Second - return newWebhookAction(context.Background(), &fftm.WebhookConfig{ + return newWebhookAction(context.Background(), &apitypes.WebhookConfig{ TLSkipHostVerify: &truthy, URL: &url, RequestTimeout: (*fftypes.FFDuration)(&oneSec), diff --git a/internal/events/websockets.go b/internal/events/websockets.go index d876bef6..45351831 100644 --- a/internal/events/websockets.go +++ b/internal/events/websockets.go @@ -23,27 +23,27 @@ import ( "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/internal/ws" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" ) -func mergeValidateWsConfig(ctx context.Context, changed bool, base *fftm.WebSocketConfig, updates *fftm.WebSocketConfig) (*fftm.WebSocketConfig, bool, error) { +func mergeValidateWsConfig(ctx context.Context, changed bool, base *apitypes.WebSocketConfig, updates *apitypes.WebSocketConfig) (*apitypes.WebSocketConfig, bool, error) { if base == nil { - base = &fftm.WebSocketConfig{} + base = &apitypes.WebSocketConfig{} } if updates == nil { - updates = &fftm.WebSocketConfig{} + updates = &apitypes.WebSocketConfig{} } - merged := &fftm.WebSocketConfig{} + merged := &apitypes.WebSocketConfig{} // Distribution mode - changed = fftm.CheckUpdateEnum(changed, &merged.DistributionMode, base.DistributionMode, updates.DistributionMode, esDefaults.websocketDistributionMode) + changed = apitypes.CheckUpdateEnum(changed, &merged.DistributionMode, base.DistributionMode, updates.DistributionMode, esDefaults.websocketDistributionMode) switch *merged.DistributionMode { - case fftm.DistributionModeLoadBalance, fftm.DistributionMode("workloaddistribution"): + case apitypes.DistributionModeLoadBalance, apitypes.DistributionMode("workloaddistribution"): // Migrate old "workloadDistribution" enum value to more consistent with other FF enums "load_balance" - *merged.DistributionMode = fftm.DistributionModeLoadBalance - case fftm.DistributionModeBroadcast: + *merged.DistributionMode = apitypes.DistributionModeLoadBalance + case apitypes.DistributionModeBroadcast: default: return nil, false, i18n.NewError(ctx, tmmsgs.MsgInvalidDistributionMode, *merged.DistributionMode) } @@ -53,11 +53,11 @@ func mergeValidateWsConfig(ctx context.Context, changed bool, base *fftm.WebSock type webSocketAction struct { topic string - spec *fftm.WebSocketConfig + spec *apitypes.WebSocketConfig wsChannels ws.WebSocketChannels } -func newWebSocketAction(wsChannels ws.WebSocketChannels, spec *fftm.WebSocketConfig, topic string) *webSocketAction { +func newWebSocketAction(wsChannels ws.WebSocketChannels, spec *apitypes.WebSocketConfig, topic string) *webSocketAction { return &webSocketAction{ spec: spec, wsChannels: wsChannels, @@ -74,9 +74,9 @@ func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt var channel chan<- interface{} switch *w.spec.DistributionMode { - case fftm.DistributionModeBroadcast: + case apitypes.DistributionModeBroadcast: channel = broadcaster - case fftm.DistributionModeLoadBalance: + case apitypes.DistributionModeLoadBalance: channel = sender default: return i18n.NewError(ctx, tmmsgs.MsgInvalidDistributionMode, *w.spec.DistributionMode) @@ -102,7 +102,7 @@ func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt } // If we ever add more distribution modes, we may want to change this logic from a simple if statement - if err == nil && *w.spec.DistributionMode != fftm.DistributionModeBroadcast { + if err == nil && *w.spec.DistributionMode != apitypes.DistributionModeBroadcast { err = w.waitForAck(ctx, receiver) } diff --git a/internal/events/websockets_test.go b/internal/events/websockets_test.go index 8cbf9ba6..4eb9ffd2 100644 --- a/internal/events/websockets_test.go +++ b/internal/events/websockets_test.go @@ -21,8 +21,8 @@ import ( "testing" "github.com/hyperledger/firefly-transaction-manager/mocks/wsmocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" "github.com/stretchr/testify/assert" ) @@ -31,8 +31,8 @@ func TestWSAttemptBatchBadDistMode(t *testing.T) { mws := &wsmocks.WebSocketChannels{} mockWSChannels(mws) - dmw := fftm.DistributionMode("wrong") - wsa := newWebSocketAction(mws, &fftm.WebSocketConfig{ + dmw := apitypes.DistributionMode("wrong") + wsa := newWebSocketAction(mws, &apitypes.WebSocketConfig{ DistributionMode: &dmw, }, "ut_stream") @@ -47,8 +47,8 @@ func TestWSAttemptBatchPurge(t *testing.T) { _, _, rc := mockWSChannels(mws) rc <- nil - dmw := fftm.DistributionModeBroadcast - wsa := newWebSocketAction(mws, &fftm.WebSocketConfig{ + dmw := apitypes.DistributionModeBroadcast + wsa := newWebSocketAction(mws, &apitypes.WebSocketConfig{ DistributionMode: &dmw, }, "ut_stream") @@ -68,8 +68,8 @@ func TestWSAttemptBatchExitPushingEvent(t *testing.T) { _, bc, _ := mockWSChannels(mws) bc <- []*ffcapi.EventWithContext{} // block the broadcast channel - dmw := fftm.DistributionModeBroadcast - wsa := newWebSocketAction(mws, &fftm.WebSocketConfig{ + dmw := apitypes.DistributionModeBroadcast + wsa := newWebSocketAction(mws, &apitypes.WebSocketConfig{ DistributionMode: &dmw, }, "ut_stream") @@ -85,8 +85,8 @@ func TestWSAttemptBatchExitReceivingReply(t *testing.T) { mws := &wsmocks.WebSocketChannels{} _, _, rc := mockWSChannels(mws) - dmw := fftm.DistributionModeBroadcast - wsa := newWebSocketAction(mws, &fftm.WebSocketConfig{ + dmw := apitypes.DistributionModeBroadcast + wsa := newWebSocketAction(mws, &apitypes.WebSocketConfig{ DistributionMode: &dmw, }, "ut_stream") diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index 38a917fc..c441049d 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -27,7 +27,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb/opt" "github.com/syndtr/goleveldb/leveldb/util" @@ -142,11 +142,11 @@ func (p *leveldbPersistence) deleteKeys(ctx context.Context, keys ...[]byte) err return nil } -func (p *leveldbPersistence) WriteCheckpoint(ctx context.Context, checkpoint *fftm.EventStreamCheckpoint) error { +func (p *leveldbPersistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error { return p.writeJSON(ctx, p.checkpointKey(checkpoint.StreamID), checkpoint) } -func (p *leveldbPersistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (cp *fftm.EventStreamCheckpoint, err error) { +func (p *leveldbPersistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (cp *apitypes.EventStreamCheckpoint, err error) { err = p.readJSON(ctx, p.checkpointKey(streamID), &cp) return cp, err } @@ -155,23 +155,23 @@ func (p *leveldbPersistence) DeleteCheckpoint(ctx context.Context, streamID *fft return p.deleteKeys(ctx, p.checkpointKey(streamID)) } -func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.EventStream, error) { - streams := make([]*fftm.EventStream, 0) +func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) { + streams := make([]*apitypes.EventStream, 0) if err := p.listJSON(ctx, "eventstreams/", after.String(), limit, - func() interface{} { var v *fftm.EventStream; return &v }, - func(v interface{}) { streams = append(streams, *(v.(**fftm.EventStream))) }, + func() interface{} { var v *apitypes.EventStream; return &v }, + func(v interface{}) { streams = append(streams, *(v.(**apitypes.EventStream))) }, ); err != nil { return nil, err } return streams, nil } -func (p *leveldbPersistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (es *fftm.EventStream, err error) { +func (p *leveldbPersistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (es *apitypes.EventStream, err error) { err = p.readJSON(ctx, p.streamKey(streamID), &es) return es, err } -func (p *leveldbPersistence) WriteStream(ctx context.Context, spec *fftm.EventStream) error { +func (p *leveldbPersistence) WriteStream(ctx context.Context, spec *apitypes.EventStream) error { return p.writeJSON(ctx, p.streamKey(spec.ID), spec) } @@ -179,35 +179,35 @@ func (p *leveldbPersistence) DeleteStream(ctx context.Context, streamID *fftypes return p.deleteKeys(ctx, p.streamKey(streamID)) } -func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.Listener, error) { - listeners := make([]*fftm.Listener, 0) +func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { + listeners := make([]*apitypes.Listener, 0) if err := p.listJSON(ctx, "listeners/", after.String(), limit, - func() interface{} { var v *fftm.Listener; return &v }, - func(v interface{}) { listeners = append(listeners, *(v.(**fftm.Listener))) }, + func() interface{} { var v *apitypes.Listener; return &v }, + func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, ); err != nil { return nil, err } return listeners, nil } -func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*fftm.Listener, error) { - listeners := make([]*fftm.Listener, 0) +func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { + listeners := make([]*apitypes.Listener, 0) if err := p.listJSON(ctx, "listeners/", after.String(), limit, - func() interface{} { var v *fftm.Listener; return &v }, - func(v interface{}) { listeners = append(listeners, *(v.(**fftm.Listener))) }, - func(v interface{}) bool { return (*(v.(**fftm.Listener))).StreamID.Equals(streamID) }, + func() interface{} { var v *apitypes.Listener; return &v }, + func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, + func(v interface{}) bool { return (*(v.(**apitypes.Listener))).StreamID.Equals(streamID) }, ); err != nil { return nil, err } return listeners, nil } -func (p *leveldbPersistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (l *fftm.Listener, err error) { +func (p *leveldbPersistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (l *apitypes.Listener, err error) { err = p.readJSON(ctx, p.listenerKey(listenerID), &l) return l, err } -func (p *leveldbPersistence) WriteListener(ctx context.Context, spec *fftm.Listener) error { +func (p *leveldbPersistence) WriteListener(ctx context.Context, spec *apitypes.Listener) error { return p.writeJSON(ctx, p.listenerKey(spec.ID), spec) } diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index 69bb5650..a566b4f6 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -25,7 +25,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/stretchr/testify/assert" "github.com/syndtr/goleveldb/leveldb/opt" ) @@ -81,17 +81,17 @@ func TestReadWriteStreams(t *testing.T) { defer done() ctx := context.Background() - s1 := &fftm.EventStream{ + s1 := &apitypes.EventStream{ ID: UUIDVersion1(), // ensure we get sequentially ascending IDs Name: strPtr("stream1"), } p.WriteStream(ctx, s1) - s2 := &fftm.EventStream{ + s2 := &apitypes.EventStream{ ID: UUIDVersion1(), Name: strPtr("stream2"), } p.WriteStream(ctx, s2) - s3 := &fftm.EventStream{ + s3 := &apitypes.EventStream{ ID: UUIDVersion1(), Name: strPtr("stream3"), } @@ -150,21 +150,21 @@ func TestReadWriteListeners(t *testing.T) { sID1 := UUIDVersion1() sID2 := UUIDVersion1() - s1l1 := &fftm.Listener{ + s1l1 := &apitypes.Listener{ ID: UUIDVersion1(), StreamID: sID1, } err := p.WriteListener(ctx, s1l1) assert.NoError(t, err) - s2l1 := &fftm.Listener{ + s2l1 := &apitypes.Listener{ ID: UUIDVersion1(), StreamID: sID2, } err = p.WriteListener(ctx, s2l1) assert.NoError(t, err) - s1l2 := &fftm.Listener{ + s1l2 := &apitypes.Listener{ ID: UUIDVersion1(), StreamID: sID1, } @@ -212,10 +212,10 @@ func TestReadWriteCheckpoints(t *testing.T) { defer done() ctx := context.Background() - cp1 := &fftm.EventStreamCheckpoint{ + cp1 := &apitypes.EventStreamCheckpoint{ StreamID: UUIDVersion1(), } - cp2 := &fftm.EventStreamCheckpoint{ + cp2 := &apitypes.EventStreamCheckpoint{ StreamID: UUIDVersion1(), } @@ -287,7 +287,7 @@ func TestWriteCheckpointFail(t *testing.T) { p.db.Close() id1 := UUIDVersion1() - err := p.WriteCheckpoint(context.Background(), &fftm.EventStreamCheckpoint{ + err := p.WriteCheckpoint(context.Background(), &apitypes.EventStreamCheckpoint{ Listeners: map[fftypes.UUID]*fftypes.JSONAny{ *id1: fftypes.JSONAnyPtr(`{!!! bad json`), }, diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go index 058315c6..b1043927 100644 --- a/internal/persistence/persistence.go +++ b/internal/persistence/persistence.go @@ -21,7 +21,7 @@ import ( "github.com/google/uuid" "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" ) // UUIDVersion1 returns a version 1 UUID - where the alphanumeric sequence is assured to be ascending based on the order of generation @@ -31,18 +31,18 @@ func UUIDVersion1() *fftypes.UUID { } type Persistence interface { - WriteCheckpoint(ctx context.Context, checkpoint *fftm.EventStreamCheckpoint) error - GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*fftm.EventStreamCheckpoint, error) + WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error + GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error - ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.EventStream, error) - GetStream(ctx context.Context, streamID *fftypes.UUID) (*fftm.EventStream, error) - WriteStream(ctx context.Context, spec *fftm.EventStream) error + ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) + GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) + WriteStream(ctx context.Context, spec *apitypes.EventStream) error DeleteStream(ctx context.Context, streamID *fftypes.UUID) error - ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.Listener, error) - ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*fftm.Listener, error) - GetListener(ctx context.Context, listenerID *fftypes.UUID) (*fftm.Listener, error) - WriteListener(ctx context.Context, spec *fftm.Listener) error + ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) + ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) + GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) + WriteListener(ctx context.Context, spec *apitypes.Listener) error DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error } diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go index 824e4b02..4be5fc1b 100644 --- a/mocks/persistencemocks/persistence.go +++ b/mocks/persistencemocks/persistence.go @@ -5,8 +5,9 @@ package persistencemocks import ( context "context" + apitypes "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" - fftm "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" mock "github.com/stretchr/testify/mock" ) @@ -58,16 +59,39 @@ func (_m *Persistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) return r0 } +// GetCheckpoint provides a mock function with given fields: ctx, streamID +func (_m *Persistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error) { + ret := _m.Called(ctx, streamID) + + var r0 *apitypes.EventStreamCheckpoint + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.EventStreamCheckpoint); ok { + r0 = rf(ctx, streamID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.EventStreamCheckpoint) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { + r1 = rf(ctx, streamID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetListener provides a mock function with given fields: ctx, listenerID -func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*fftm.Listener, error) { +func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) { ret := _m.Called(ctx, listenerID) - var r0 *fftm.Listener - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *fftm.Listener); ok { + var r0 *apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.Listener); ok { r0 = rf(ctx, listenerID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*fftm.Listener) + r0 = ret.Get(0).(*apitypes.Listener) } } @@ -82,15 +106,15 @@ func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID } // GetStream provides a mock function with given fields: ctx, streamID -func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (*fftm.EventStream, error) { +func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) { ret := _m.Called(ctx, streamID) - var r0 *fftm.EventStream - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *fftm.EventStream); ok { + var r0 *apitypes.EventStream + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.EventStream); ok { r0 = rf(ctx, streamID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*fftm.EventStream) + r0 = ret.Get(0).(*apitypes.EventStream) } } @@ -105,15 +129,15 @@ func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (* } // ListListeners provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.Listener, error) { +func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { ret := _m.Called(ctx, after, limit) - var r0 []*fftm.Listener - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*fftm.Listener); ok { + var r0 []*apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.Listener); ok { r0 = rf(ctx, after, limit) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*fftm.Listener) + r0 = ret.Get(0).([]*apitypes.Listener) } } @@ -128,15 +152,15 @@ func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, l } // ListStreamListeners provides a mock function with given fields: ctx, after, limit, streamID -func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*fftm.Listener, error) { +func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { ret := _m.Called(ctx, after, limit, streamID) - var r0 []*fftm.Listener - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, *fftypes.UUID) []*fftm.Listener); ok { + var r0 []*apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, *fftypes.UUID) []*apitypes.Listener); ok { r0 = rf(ctx, after, limit, streamID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*fftm.Listener) + r0 = ret.Get(0).([]*apitypes.Listener) } } @@ -151,15 +175,15 @@ func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.U } // ListStreams provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*fftm.EventStream, error) { +func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) { ret := _m.Called(ctx, after, limit) - var r0 []*fftm.EventStream - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*fftm.EventStream); ok { + var r0 []*apitypes.EventStream + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.EventStream); ok { r0 = rf(ctx, after, limit) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*fftm.EventStream) + r0 = ret.Get(0).([]*apitypes.EventStream) } } @@ -173,35 +197,12 @@ func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, lim return r0, r1 } -// GetCheckpoint provides a mock function with given fields: ctx, streamID -func (_m *Persistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*fftm.EventStreamCheckpoint, error) { - ret := _m.Called(ctx, streamID) - - var r0 *fftm.EventStreamCheckpoint - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *fftm.EventStreamCheckpoint); ok { - r0 = rf(ctx, streamID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*fftm.EventStreamCheckpoint) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { - r1 = rf(ctx, streamID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // WriteCheckpoint provides a mock function with given fields: ctx, checkpoint -func (_m *Persistence) WriteCheckpoint(ctx context.Context, checkpoint *fftm.EventStreamCheckpoint) error { +func (_m *Persistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error { ret := _m.Called(ctx, checkpoint) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftm.EventStreamCheckpoint) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStreamCheckpoint) error); ok { r0 = rf(ctx, checkpoint) } else { r0 = ret.Error(0) @@ -211,11 +212,11 @@ func (_m *Persistence) WriteCheckpoint(ctx context.Context, checkpoint *fftm.Eve } // WriteListener provides a mock function with given fields: ctx, spec -func (_m *Persistence) WriteListener(ctx context.Context, spec *fftm.Listener) error { +func (_m *Persistence) WriteListener(ctx context.Context, spec *apitypes.Listener) error { ret := _m.Called(ctx, spec) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftm.Listener) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.Listener) error); ok { r0 = rf(ctx, spec) } else { r0 = ret.Error(0) @@ -225,11 +226,11 @@ func (_m *Persistence) WriteListener(ctx context.Context, spec *fftm.Listener) e } // WriteStream provides a mock function with given fields: ctx, spec -func (_m *Persistence) WriteStream(ctx context.Context, spec *fftm.EventStream) error { +func (_m *Persistence) WriteStream(ctx context.Context, spec *apitypes.EventStream) error { ret := _m.Called(ctx, spec) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftm.EventStream) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStream) error); ok { r0 = rf(ctx, spec) } else { r0 = ret.Error(0) diff --git a/pkg/fftm/api_types.go b/pkg/apitypes/api_types.go similarity index 99% rename from pkg/fftm/api_types.go rename to pkg/apitypes/api_types.go index 29c5332f..289d1017 100644 --- a/pkg/fftm/api_types.go +++ b/pkg/apitypes/api_types.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package fftm +package apitypes import ( "bytes" diff --git a/pkg/fftm/api_types_test.go b/pkg/apitypes/api_types_test.go similarity index 99% rename from pkg/fftm/api_types_test.go rename to pkg/apitypes/api_types_test.go index d8608ed0..f2061095 100644 --- a/pkg/fftm/api_types_test.go +++ b/pkg/apitypes/api_types_test.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package fftm +package apitypes import ( "testing" From 06e5b9c053291e56ab23b2e3de14612572724d5a Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 15 Jun 2022 17:06:04 -0400 Subject: [PATCH 15/95] API/Swagger route framework and stream init Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 1 + go.mod | 7 ++ go.sum | 7 ++ internal/events/eventstream.go | 32 +++++--- internal/events/eventstream_test.go | 20 ++--- internal/events/listener.go | 1 + internal/persistence/leveldb_persistence.go | 7 ++ .../persistence/leveldb_persistence_test.go | 35 ++++---- internal/persistence/persistence.go | 9 +- internal/tmconfig/tmconfig.go | 5 ++ internal/tmmsgs/en_api_descriptions.go | 31 +++++++ internal/tmmsgs/en_error_messges.go | 1 + mocks/ffcapimocks/api.go | 23 ++++-- mocks/persistencemocks/persistence.go | 5 ++ pkg/apitypes/api_types.go | 7 ++ pkg/apitypes/api_types_test.go | 7 ++ pkg/ffcapi/api.go | 2 +- pkg/fftm/api.go | 39 +++++++++ pkg/fftm/api_test.go | 21 +++++ pkg/fftm/changelistener_test.go | 2 +- pkg/fftm/manager.go | 40 +++++++-- pkg/fftm/manager_test.go | 23 +++++- pkg/fftm/route_patch_new_eventstream.go | 42 ++++++++++ pkg/fftm/routes.go | 25 ++++++ pkg/fftm/stream_management.go | 82 +++++++++++++++++++ 25 files changed, 409 insertions(+), 65 deletions(-) create mode 100644 internal/tmmsgs/en_api_descriptions.go create mode 100644 pkg/fftm/route_patch_new_eventstream.go create mode 100644 pkg/fftm/routes.go create mode 100644 pkg/fftm/stream_management.go diff --git a/.vscode/settings.json b/.vscode/settings.json index cda8d766..54ec9168 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,6 +21,7 @@ "eventstream", "eventstreams", "fabconnect", + "ffapi", "ffcapi", "ffcapimocks", "ffcore", diff --git a/go.mod b/go.mod index d8b4371e..4f6cd673 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/hyperledger/firefly-transaction-manager go 1.17 require ( + github.com/ghodss/yaml v1.0.0 github.com/go-resty/resty/v2 v2.7.0 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 @@ -22,9 +23,15 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/getkin/kin-openapi v0.96.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/invopop/yaml v0.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect diff --git a/go.sum b/go.sum index 11cee6ae..8e7ef378 100644 --- a/go.sum +++ b/go.sum @@ -386,8 +386,10 @@ github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmx github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getkin/kin-openapi v0.94.1-0.20220401165309-136a868a30c2/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= +github.com/getkin/kin-openapi v0.96.0 h1:VVbcSdQAJzfc5kCLU7z2ezw84czu3rbC6UG1BGGzahY= github.com/getkin/kin-openapi v0.96.0/go.mod h1:w4lRPHiyOdwGbOkLIyk+P0qCwlu7TXPCHD/64nSXzgE= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= @@ -408,12 +410,14 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= @@ -638,6 +642,7 @@ github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= @@ -696,6 +701,7 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -764,6 +770,7 @@ 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/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 1d8a63b1..faac4255 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -139,6 +139,7 @@ func NewEventStream( if es.spec, _, err = mergeValidateEsConfig(es.bgCtx, nil, persistedSpec); err != nil { return nil, err } + log.L(es.bgCtx).Infof("Initialized Event Stream") return es, nil } @@ -268,6 +269,7 @@ func safeCompareFilterList(a, b []fftypes.JSONAny) bool { } func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.Listener) error { + log.L(ctx).Warnf("Initializing listener %s", spec.ID) // Allow a single "event" object to be specified instead of a filter, with an optional "address". // This is migrated to the new syntax: `"filters":[{"address":"0x1235","event":{...}}]` @@ -283,7 +285,7 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.L } // The connector needs to validate the options - mergedOptions, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.ListenerOptions{ + signature, mergedOptions, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.ListenerOptions{ FromBlock: spec.FromBlock, }, spec.Options) if err != nil { @@ -301,12 +303,14 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.L } l.options = mergedOptions l.filters = spec.Filters + l.signature = signature } else { es.listeners[*spec.ID] = &listener{ - es: es, - id: spec.ID, - options: mergedOptions, - filters: spec.Filters, + es: es, + id: spec.ID, + options: mergedOptions, + filters: spec.Filters, + signature: signature, } } // Take a copy of the current started state, before unlocking @@ -476,11 +480,18 @@ func (es *eventStream) eventLoop(startedState *startedStreamState) { batch.checkpoints[*update.ListenerID] = update.Checkpoint } for _, event := range update.Events { - batch.events = append(batch.events, &ffcapi.EventWithContext{ - StreamID: es.spec.ID, - ListenerID: update.ListenerID, - Event: *event, - }) + var l *listener + if update.ListenerID != nil { + l = es.listeners[*update.ListenerID] + if l != nil { + log.L(es.bgCtx).Debugf("%s (%s) event: %s", l.signature, l.id, event.ProtocolID) + batch.events = append(batch.events, &ffcapi.EventWithContext{ + StreamID: es.spec.ID, + ListenerID: update.ListenerID, + Event: *event, + }) + } + } } case <-timeoutContext.Done(): if batch == nil { @@ -559,6 +570,7 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * for lID, lCP := range batch.checkpoints { if l, ok := es.listeners[lID]; ok { l.checkpoint = lCP + log.L(es.bgCtx).Tracef("%s (%s) checkpoint: %s", l.signature, l.id, lCP) } } for lID, l := range es.listeners { diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 6353422a..69f1d1d3 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -261,7 +261,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { return standard.FromBlock == "12345" }), mock.MatchedBy(func(customOptions *fftypes.JSONAny) bool { return customOptions.JSONObject().GetString("option1") == "value1" - })).Return(*fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), nil) + })).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -365,7 +365,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { return standard.FromBlock == "12345" }), mock.MatchedBy(func(customOptions *fftypes.JSONAny) bool { return customOptions.JSONObject().GetString("option1") == "value1" - })).Return(*fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), nil) + })).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -435,7 +435,7 @@ func TestConnectorRejectListener(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), fmt.Errorf("pop")) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("", *fftypes.JSONAnyPtr(`{}`), fmt.Errorf("pop")) err := es.AddOrUpdateListener(es.bgCtx, l) assert.Regexp(t, "FF21040.*pop", err) @@ -457,7 +457,7 @@ func TestUpdateStreamStarted(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -516,7 +516,7 @@ func TestAddRemoveListener(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -564,7 +564,7 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil).Times(3) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil).Times(3) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -629,7 +629,7 @@ func TestUpdateListenerFail(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil).Times(3) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil).Times(3) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -702,7 +702,7 @@ func TestUpdateStreamRestartFail(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -755,7 +755,7 @@ func TestUpdateStreamStopFail(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -828,7 +828,7 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return(*fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { diff --git a/internal/events/listener.go b/internal/events/listener.go index 30fb6457..a3827a9e 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -27,6 +27,7 @@ type listener struct { filters []fftypes.JSONAny options fftypes.JSONAny checkpoint *fftypes.JSONAny + signature string } func (l *listener) stop(startedState *startedStreamState) error { diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index c441049d..974296bf 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -214,3 +214,10 @@ func (p *leveldbPersistence) WriteListener(ctx context.Context, spec *apitypes.L func (p *leveldbPersistence) DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error { return p.deleteKeys(ctx, p.listenerKey(listenerID)) } + +func (p *leveldbPersistence) Close(ctx context.Context) { + err := p.db.Close() + if err != nil { + log.L(ctx).Warnf("Error closing leveldb: %s", err) + } +} diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index a566b4f6..19749802 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -54,6 +54,7 @@ func newTestLevelDBPersistence(t *testing.T) (*leveldbPersistence, func()) { } return p, func() { + p.Close(context.Background()) os.RemoveAll(dir) } @@ -82,17 +83,17 @@ func TestReadWriteStreams(t *testing.T) { ctx := context.Background() s1 := &apitypes.EventStream{ - ID: UUIDVersion1(), // ensure we get sequentially ascending IDs + ID: apitypes.UUIDVersion1(), // ensure we get sequentially ascending IDs Name: strPtr("stream1"), } p.WriteStream(ctx, s1) s2 := &apitypes.EventStream{ - ID: UUIDVersion1(), + ID: apitypes.UUIDVersion1(), Name: strPtr("stream2"), } p.WriteStream(ctx, s2) s3 := &apitypes.EventStream{ - ID: UUIDVersion1(), + ID: apitypes.UUIDVersion1(), Name: strPtr("stream3"), } p.WriteStream(ctx, s3) @@ -147,25 +148,25 @@ func TestReadWriteListeners(t *testing.T) { ctx := context.Background() - sID1 := UUIDVersion1() - sID2 := UUIDVersion1() + sID1 := apitypes.UUIDVersion1() + sID2 := apitypes.UUIDVersion1() s1l1 := &apitypes.Listener{ - ID: UUIDVersion1(), + ID: apitypes.UUIDVersion1(), StreamID: sID1, } err := p.WriteListener(ctx, s1l1) assert.NoError(t, err) s2l1 := &apitypes.Listener{ - ID: UUIDVersion1(), + ID: apitypes.UUIDVersion1(), StreamID: sID2, } err = p.WriteListener(ctx, s2l1) assert.NoError(t, err) s1l2 := &apitypes.Listener{ - ID: UUIDVersion1(), + ID: apitypes.UUIDVersion1(), StreamID: sID1, } err = p.WriteListener(ctx, s1l2) @@ -213,10 +214,10 @@ func TestReadWriteCheckpoints(t *testing.T) { ctx := context.Background() cp1 := &apitypes.EventStreamCheckpoint{ - StreamID: UUIDVersion1(), + StreamID: apitypes.UUIDVersion1(), } cp2 := &apitypes.EventStreamCheckpoint{ - StreamID: UUIDVersion1(), + StreamID: apitypes.UUIDVersion1(), } err := p.WriteCheckpoint(ctx, cp1) @@ -244,7 +245,7 @@ func TestListStreamsBadJSON(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - sID := UUIDVersion1() + sID := apitypes.UUIDVersion1() err := p.db.Put(p.streamKey(sID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) @@ -257,14 +258,14 @@ func TestListListenersBadJSON(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - lID := UUIDVersion1() + lID := apitypes.UUIDVersion1() err := p.db.Put(p.listenerKey(lID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) _, err = p.ListListeners(context.Background(), nil, 0) assert.Error(t, err) - _, err = p.ListStreamListeners(context.Background(), nil, 0, UUIDVersion1()) + _, err = p.ListStreamListeners(context.Background(), nil, 0, apitypes.UUIDVersion1()) assert.Error(t, err) } @@ -275,7 +276,7 @@ func TestDeleteStreamFail(t *testing.T) { p.db.Close() - err := p.DeleteStream(context.Background(), UUIDVersion1()) + err := p.DeleteStream(context.Background(), apitypes.UUIDVersion1()) assert.Error(t, err) } @@ -286,7 +287,7 @@ func TestWriteCheckpointFail(t *testing.T) { p.db.Close() - id1 := UUIDVersion1() + id1 := apitypes.UUIDVersion1() err := p.WriteCheckpoint(context.Background(), &apitypes.EventStreamCheckpoint{ Listeners: map[fftypes.UUID]*fftypes.JSONAny{ *id1: fftypes.JSONAnyPtr(`{!!! bad json`), @@ -302,7 +303,7 @@ func TestReadListenerFail(t *testing.T) { p.db.Close() - _, err := p.GetListener(context.Background(), UUIDVersion1()) + _, err := p.GetListener(context.Background(), apitypes.UUIDVersion1()) assert.Error(t, err) } @@ -311,7 +312,7 @@ func TestReadCheckpointFail(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - sID := UUIDVersion1() + sID := apitypes.UUIDVersion1() err := p.db.Put(p.checkpointKey(sID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go index b1043927..18d26599 100644 --- a/internal/persistence/persistence.go +++ b/internal/persistence/persistence.go @@ -19,17 +19,10 @@ package persistence import ( "context" - "github.com/google/uuid" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" ) -// UUIDVersion1 returns a version 1 UUID - where the alphanumeric sequence is assured to be ascending based on the order of generation -func UUIDVersion1() *fftypes.UUID { - u, _ := uuid.NewUUID() - return (*fftypes.UUID)(&u) -} - type Persistence interface { WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error) @@ -45,4 +38,6 @@ type Persistence interface { GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) WriteListener(ctx context.Context, spec *apitypes.Listener) error DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error + + Close(ctx context.Context) } diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 2f88a5ce..13807bf4 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -58,6 +58,8 @@ var ( PersistenceLevelDBPath = ffc("persistence.leveldb.path") PersistenceLevelDBMaxHandles = ffc("persistence.leveldb.maxHandles") PersistenceLevelDBSyncWrites = ffc("persistence.leveldb.syncWrites") + APIDefaultRequestTimeout = ffc("api.defaultRequestTimeout") + APIMaxRequestTimeout = ffc("api.maxRequestTimeout") ) var FFCoreConfig config.Section @@ -101,6 +103,9 @@ func setDefaults() { viper.SetDefault(string(PersistenceType), "leveldb") viper.SetDefault(string(PersistenceLevelDBMaxHandles), 100) viper.SetDefault(string(PersistenceLevelDBSyncWrites), true) + + viper.SetDefault(string(APIDefaultRequestTimeout), "30s") + viper.SetDefault(string(APIMaxRequestTimeout), "10m") } func Reset() { diff --git a/internal/tmmsgs/en_api_descriptions.go b/internal/tmmsgs/en_api_descriptions.go new file mode 100644 index 00000000..70ee4cba --- /dev/null +++ b/internal/tmmsgs/en_api_descriptions.go @@ -0,0 +1,31 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 tmmsgs + +import ( + "github.com/hyperledger/firefly-common/pkg/i18n" + "golang.org/x/text/language" +) + +var ffm = func(key, translation string) i18n.MessageKey { + return i18n.FFM(language.AmericanEnglish, key, translation) +} + +//revive:disable +var ( + APIEndpointPatchEventStream = ffm("api.endpoints.post.eventstreams", "Create a new event stream") +) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index b69df52d..389513fa 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -58,4 +58,5 @@ var ( MsgBadListenerOptions = ffe("FF21040", "Invalid listener options: %s", 400) MsgInvalidHost = ffe("FF21041", "Cannot send Webhook POST to host '%s': %s") MsgWebhookErr = ffe("FF21042", "Webhook request failed: %s") + MsgUnknownPersistence = ffe("FF21043", "Unknown persistence type '%s'") ) diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index a91794e4..d8875266 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -137,24 +137,31 @@ func (_m *API) EventListenerRemove(ctx context.Context, req *ffcapi.EventListene } // EventListenerVerifyOptions provides a mock function with given fields: ctx, standardOptions, customOptions -func (_m *API) EventListenerVerifyOptions(ctx context.Context, standardOptions *ffcapi.ListenerOptions, customOptions *fftypes.JSONAny) (fftypes.JSONAny, error) { +func (_m *API) EventListenerVerifyOptions(ctx context.Context, standardOptions *ffcapi.ListenerOptions, customOptions *fftypes.JSONAny) (string, fftypes.JSONAny, error) { ret := _m.Called(ctx, standardOptions, customOptions) - var r0 fftypes.JSONAny - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.ListenerOptions, *fftypes.JSONAny) fftypes.JSONAny); ok { + var r0 string + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.ListenerOptions, *fftypes.JSONAny) string); ok { r0 = rf(ctx, standardOptions, customOptions) } else { - r0 = ret.Get(0).(fftypes.JSONAny) + r0 = ret.Get(0).(string) } - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.ListenerOptions, *fftypes.JSONAny) error); ok { + var r1 fftypes.JSONAny + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.ListenerOptions, *fftypes.JSONAny) fftypes.JSONAny); ok { r1 = rf(ctx, standardOptions, customOptions) } else { - r1 = ret.Error(1) + r1 = ret.Get(1).(fftypes.JSONAny) } - return r0, r1 + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.ListenerOptions, *fftypes.JSONAny) error); ok { + r2 = rf(ctx, standardOptions, customOptions) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 } // GasPriceEstimate provides a mock function with given fields: ctx, req diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go index 4be5fc1b..14bea4c1 100644 --- a/mocks/persistencemocks/persistence.go +++ b/mocks/persistencemocks/persistence.go @@ -17,6 +17,11 @@ type Persistence struct { mock.Mock } +// Close provides a mock function with given fields: ctx +func (_m *Persistence) Close(ctx context.Context) { + _m.Called(ctx) +} + // DeleteCheckpoint provides a mock function with given fields: ctx, streamID func (_m *Persistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error { ret := _m.Called(ctx, streamID) diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 289d1017..0a868c7d 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -20,6 +20,7 @@ import ( "bytes" "encoding/json" + "github.com/google/uuid" "github.com/hyperledger/firefly-common/pkg/fftypes" ) @@ -95,6 +96,12 @@ type Listener struct { FromBlock string `ffstruct:"listener" json:"fromBlock,omitempty"` } +// UUIDVersion1 returns a version 1 UUID - where the alphanumeric sequence is assured to be ascending based on the order of generation +func UUIDVersion1() *fftypes.UUID { + u, _ := uuid.NewUUID() + return (*fftypes.UUID)(&u) +} + // CheckUpdateString helper merges supplied configuration, with a base, and applies a default if unset func CheckUpdateString(changed bool, merged **string, old *string, new *string, defValue string) bool { if new != nil { diff --git a/pkg/apitypes/api_types_test.go b/pkg/apitypes/api_types_test.go index f2061095..8f566702 100644 --- a/pkg/apitypes/api_types_test.go +++ b/pkg/apitypes/api_types_test.go @@ -17,6 +17,7 @@ package apitypes import ( + "strings" "testing" "time" @@ -24,6 +25,12 @@ import ( "github.com/stretchr/testify/assert" ) +func TestV1UUID(t *testing.T) { + u1 := UUIDVersion1() + u2 := UUIDVersion1() + assert.Negative(t, strings.Compare(u1.String(), u2.String())) +} + func TestCheckUpdateString(t *testing.T) { var val1 = "val1" var val2 = "val2" diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index c8c829a3..50d9bc41 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -53,7 +53,7 @@ type API interface { TransactionSend(ctx context.Context, req *TransactionSendRequest) (*TransactionSendResponse, ErrorReason, error) // EventListenerVerifyOptions validates the configuration options for a listener, applying any defaults needed by the connector, and returning the update options for FFTM to persist - EventListenerVerifyOptions(ctx context.Context, standardOptions *ListenerOptions, customOptions *fftypes.JSONAny) (fftypes.JSONAny, error) + EventListenerVerifyOptions(ctx context.Context, standardOptions *ListenerOptions, customOptions *fftypes.JSONAny) (signature string, options fftypes.JSONAny, err error) // EventListenerAdd begins/resumes listening on set of events that must be consistently ordered. Blockchain specific signatures of the events are included, along with initial conditions (initial block number etc.), and the last stored checkpoint (if any) EventListenerAdd(ctx context.Context, req *EventListenerAddRequest) (*EventListenerAddResponse, ErrorReason, error) diff --git a/pkg/fftm/api.go b/pkg/fftm/api.go index 6fbc76a4..97ed8e36 100644 --- a/pkg/fftm/api.go +++ b/pkg/fftm/api.go @@ -22,10 +22,14 @@ import ( "net/http" "strconv" + "github.com/ghodss/yaml" "github.com/gorilla/mux" + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffapi" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) @@ -33,6 +37,41 @@ import ( func (m *manager) router() *mux.Router { mux := mux.NewRouter() mux.Path("/").Methods(http.MethodPost).Handler(http.HandlerFunc(m.apiHandler)) + hf := ffapi.HandlerFactory{ + DefaultRequestTimeout: config.GetDuration(tmconfig.APIDefaultRequestTimeout), + MaxTimeout: config.GetDuration(tmconfig.APIMaxRequestTimeout), + } + routes := m.routes() + for _, r := range routes { + mux.Path(r.Path).Methods(r.Method).Handler(hf.RouteHandler(r)) + } + mux.Path("/api").Methods(http.MethodGet).Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + url := req.URL.String() + "/spec.yaml" + handler := hf.APIWrapper(hf.SwaggerUIHandler(url)) + handler(res, req) + })) + mux.Path("/api/spec.yaml").Methods(http.MethodGet).Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + u := req.URL + u.Path = "" + swaggerGen := ffapi.NewSwaggerGen(&ffapi.Options{ + BaseURL: u.String(), + }) + doc := swaggerGen.Generate(req.Context(), routes) + res.Header().Add("Content-Type", "application/x-yaml") + b, _ := yaml.Marshal(&doc) + _, _ = res.Write(b) + })) + mux.Path("/api/spec.json").Methods(http.MethodGet).Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + u := req.URL + u.Path = "" + swaggerGen := ffapi.NewSwaggerGen(&ffapi.Options{ + BaseURL: u.String(), + }) + doc := swaggerGen.Generate(req.Context(), routes) + res.Header().Add("Content-Type", "application/json") + b, _ := json.Marshal(&doc) + _, _ = res.Write(b) + })) return mux } diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go index cc93fccb..f00e6edc 100644 --- a/pkg/fftm/api_test.go +++ b/pkg/fftm/api_test.go @@ -146,6 +146,27 @@ func TestSendInvalidRequestNoHeaders(t *testing.T) { assert.Regexp(t, "FF21022", errRes.Error) } +func TestSwaggerEndpoints(t *testing.T) { + + url, m, cancel := newTestManager(t, + func(w http.ResponseWriter, r *http.Request) {}, + ) + defer cancel() + m.Start() + + res, err := resty.New().R().SetDoNotParseResponse(true).Get(url + "/api/spec.json") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + res, err = resty.New().R().SetDoNotParseResponse(true).Get(url + "/api/spec.yaml") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + res, err = resty.New().R().SetDoNotParseResponse(true).Get(url + "/api") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) +} + func TestSendInvalidRequestWrongType(t *testing.T) { url, m, cancel := newTestManager(t, diff --git a/pkg/fftm/changelistener_test.go b/pkg/fftm/changelistener_test.go index 9460f0ca..666db636 100644 --- a/pkg/fftm/changelistener_test.go +++ b/pkg/fftm/changelistener_test.go @@ -91,6 +91,6 @@ func TestWSConnectFail(t *testing.T) { m.enableChangeListener = true err := m.startWS() - assert.Regexp(t, "FF00154", err) + assert.Error(t, err) } diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index b00b1fff..7c5663fc 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -32,8 +32,11 @@ import ( "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-common/pkg/wsclient" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/events" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/internal/ws" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" @@ -42,8 +45,7 @@ import ( type Manager interface { Start() error - Stop() - WaitStop() error + Close() } type manager struct { @@ -55,11 +57,14 @@ type manager struct { apiServer httpserver.HTTPServer ffCoreClient *resty.Client wsClient wsclient.WSClient + wsChannels ws.WebSocketChannels + persistence persistence.Persistence mux sync.Mutex nextNonces map[string]uint64 lockedNonces map[string]*lockedNonce pendingOpsByID map[string]*pendingState + eventStreams map[fftypes.UUID]events.Stream changeEventLoopDone chan struct{} firstFullScanDone chan error policyLoopDone chan struct{} @@ -119,6 +124,9 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { if err != nil { return nil, err } + if err = m.initPersistence(ctx); err != nil { + return nil, err + } return m, nil } @@ -138,6 +146,19 @@ func (m *manager) requestFullScan() { } } +func (m *manager) initPersistence(ctx context.Context) (err error) { + pType := config.GetString(tmconfig.PersistenceType) + switch pType { + case "leveldb": + if m.persistence, err = persistence.NewLevelDBPersistence(ctx); err != nil { + return err + } + return nil + default: + return i18n.NewError(ctx, tmmsgs.MsgUnknownPersistence, pType) + } +} + func (m *manager) waitScanDelay(lastFullScan *fftypes.FFTime) { scanDelay := m.fullScanMinDelay - time.Since(*lastFullScan.Time()) log.L(m.ctx).Debugf("Delaying %dms before next full scan", scanDelay.Milliseconds()) @@ -266,7 +287,11 @@ func (m *manager) Start() error { m.firstFullScanDone = make(chan error) m.fullScanLoopDone = make(chan struct{}) go m.fullScanLoop() - return m.waitForFirstScanAndStart() + err := m.waitForFirstScanAndStart() + if err != nil { + return err + } + return m.restoreStreams() } func (m *manager) waitForFirstScanAndStart() error { @@ -292,17 +317,14 @@ func (m *manager) waitForFirstScanAndStart() error { return err } -func (m *manager) Stop() { +func (m *manager) Close() { m.cancelCtx() -} - -func (m *manager) WaitStop() (err error) { if m.started { m.started = false - err = <-m.apiServerDone + <-m.apiServerDone <-m.fullScanLoopDone <-m.policyLoopDone m.waitWSStop() } - return err + m.persistence.Close(m.ctx) } diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 5779c490..589121f9 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -20,9 +20,11 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" "net" "net/http" "net/http/httptest" + "os" "strings" "testing" "time" @@ -47,6 +49,9 @@ const testManagerName = "unittest" func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...string) (string, *manager, func()) { tmconfig.Reset() policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + dir, err := ioutil.TempDir("", "ldb_*") + assert.NoError(t, err) + config.Set(tmconfig.PersistenceLevelDBPath, dir) ffCoreServer := httptest.NewServer(ffCoreHandler) tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", ffCoreServer.Listener.Addr())) @@ -78,8 +83,8 @@ func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...strin m, func() { ffCoreServer.Close() - m.Stop() - _ = m.WaitStop() + m.Close() + os.RemoveAll(dir) } } @@ -121,6 +126,20 @@ func TestNewManagerBadHttpConfig(t *testing.T) { } +func TestNewManagerBadPersistenceConfig(t *testing.T) { + + tmconfig.Reset() + config.Set(tmconfig.ManagerName, "test") + config.Set(tmconfig.PersistenceType, "wrong") + + policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") + + _, err := NewManager(context.Background(), nil) + assert.Regexp(t, "FF21043", err) + +} + func TestNewManagerFireFlyURLConfig(t *testing.T) { tmconfig.Reset() diff --git a/pkg/fftm/route_patch_new_eventstream.go b/pkg/fftm/route_patch_new_eventstream.go new file mode 100644 index 00000000..e40b3010 --- /dev/null +++ b/pkg/fftm/route_patch_new_eventstream.go @@ -0,0 +1,42 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postEventStream = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postEventStream", + Path: "eventstreams", + Method: http.MethodPost, + PathParams: nil, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchEventStream, + JSONInputValue: func() interface{} { return &apitypes.EventStream{} }, + JSONOutputValue: func() interface{} { return &apitypes.EventStream{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.newStream(r.Req.Context(), r.Input.(*apitypes.EventStream)) + }, + } +} diff --git a/pkg/fftm/routes.go b/pkg/fftm/routes.go new file mode 100644 index 00000000..322075b9 --- /dev/null +++ b/pkg/fftm/routes.go @@ -0,0 +1,25 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import "github.com/hyperledger/firefly-common/pkg/ffapi" + +func (m *manager) routes() []*ffapi.Route { + return []*ffapi.Route{ + postEventStream(m), + } +} diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go new file mode 100644 index 00000000..0c081648 --- /dev/null +++ b/pkg/fftm/stream_management.go @@ -0,0 +1,82 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/events" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +const ( + startupPaginationLimit = 25 +) + +func (m *manager) restoreStreams() error { + var lastInPage *fftypes.UUID + for { + streamDefs, err := m.persistence.ListStreams(m.ctx, lastInPage, startupPaginationLimit) + if err != nil { + return err + } + if len(streamDefs) == 0 { + break + } + for _, def := range streamDefs { + lastInPage = def.ID + s, err := events.NewEventStream(m.ctx, def, m.connector, m.persistence, m.confirmations, m.wsChannels) + if err != nil { + return err + } + m.mux.Lock() + m.eventStreams[*s.Definition().ID] = s + m.mux.Unlock() + } + } + return m.restoreListeners() +} + +func (m *manager) restoreListeners() error { + var lastInPage *fftypes.UUID + for { + listenerDefs, err := m.persistence.ListListeners(m.ctx, lastInPage, startupPaginationLimit) + if err != nil { + return err + } + if len(listenerDefs) == 0 { + break + } + for _, def := range listenerDefs { + lastInPage = def.ID + m.mux.Lock() + s := m.eventStreams[*def.StreamID] + m.mux.Unlock() + if s != nil { + if err := s.AddOrUpdateListener(m.ctx, def); err != nil { + return err + } + } + } + } + return nil +} + +func (m *manager) newStream(ctx context.Context, es *apitypes.EventStream) (*apitypes.EventStream, error) { + return nil, nil +} From 965294963cf78c7c2d9285246c570a2bcc689afd Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 16 Jun 2022 13:27:18 -0400 Subject: [PATCH 16/95] CRUD ops on streams Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 70 +++++---- internal/events/eventstream_test.go | 18 ++- internal/tmmsgs/en_api_descriptions.go | 7 +- pkg/apitypes/api_types.go | 3 + pkg/fftm/api.go | 5 +- pkg/fftm/manager.go | 12 ++ pkg/fftm/manager_test.go | 2 + pkg/fftm/route_delete_eventstream.go | 44 ++++++ pkg/fftm/route_delete_eventstream_test.go | 56 +++++++ pkg/fftm/route_patch_eventstream.go | 44 ++++++ pkg/fftm/route_patch_eventstream_test.go | 61 ++++++++ ...entstream.go => route_post_eventstream.go} | 6 +- pkg/fftm/route_post_eventstream_resume.go | 48 ++++++ .../route_post_eventstream_resume_test.go | 60 ++++++++ pkg/fftm/route_post_eventstream_suspend.go | 48 ++++++ .../route_post_eventstream_suspend_test.go | 58 ++++++++ pkg/fftm/route_post_eventstream_test.go | 49 +++++++ pkg/fftm/routes.go | 4 + pkg/fftm/stream_management.go | 138 ++++++++++++++++-- 19 files changed, 680 insertions(+), 53 deletions(-) create mode 100644 pkg/fftm/route_delete_eventstream.go create mode 100644 pkg/fftm/route_delete_eventstream_test.go create mode 100644 pkg/fftm/route_patch_eventstream.go create mode 100644 pkg/fftm/route_patch_eventstream_test.go rename pkg/fftm/{route_patch_new_eventstream.go => route_post_eventstream.go} (88%) create mode 100644 pkg/fftm/route_post_eventstream_resume.go create mode 100644 pkg/fftm/route_post_eventstream_resume_test.go create mode 100644 pkg/fftm/route_post_eventstream_suspend.go create mode 100644 pkg/fftm/route_post_eventstream_suspend_test.go create mode 100644 pkg/fftm/route_post_eventstream_test.go diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index faac4255..d3ea5924 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -36,22 +36,23 @@ import ( ) type Stream interface { - AddOrUpdateListener(ctx context.Context, s *apitypes.Listener) error // Add or update a listener - RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener - UpdateDefinition(ctx context.Context, updates *apitypes.EventStream) error // Apply definition updates (if there are changes) - Definition() *apitypes.EventStream // Retrieve the merged definition to persist - Start(ctx context.Context) error // Start delivery - Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) - Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint + AddOrUpdateListener(ctx context.Context, s *apitypes.Listener) error // Add or update a listener + RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener + UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error // Apply definition updates (if there are changes) + Spec() *apitypes.EventStream // Retrieve the merged definition to persist + State() StreamState // Get the current state + Start(ctx context.Context) error // Start delivery + Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) + Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint } -type streamState string +type StreamState string const ( - streamStateStarted = "started" - streamStateStopping = "stopping" - streamStateStopped = "stopped" - streamStateDeleted = "deleted" + StreamStateStarted StreamState = "started" + StreamStateStopping StreamState = "stopping" + StreamStateStopped StreamState = "stopped" + StreamStateDeleted StreamState = "deleted" ) // esDefaults are the defaults for new event streams, read from the config once in InitDefaults() @@ -105,7 +106,7 @@ type eventStream struct { bgCtx context.Context spec *apitypes.EventStream mux sync.Mutex - state streamState + state StreamState connector ffcapi.API persistence persistence.Persistence confirmations confirmations.Manager @@ -125,7 +126,7 @@ func NewEventStream( ) (ees Stream, err error) { es := &eventStream{ bgCtx: log.WithLogField(bgCtx, "eventstream", persistedSpec.ID.String()), - state: streamStateStopped, + state: StreamStateStopped, spec: persistedSpec, connector: connector, persistence: persistence, @@ -169,7 +170,7 @@ func mergeValidateEsConfig(ctx context.Context, base *apitypes.EventStream, upda } if merged.Created == nil || merged.ID == nil { merged.Created = merged.Updated - merged.ID = fftypes.NewUUID() + merged.ID = apitypes.UUIDVersion1() } // Name (no default - must be set) // - Note we do not check for uniqueness of the name at this layer in the code, but we do require unique names. @@ -230,11 +231,11 @@ func mergeValidateEsConfig(ctx context.Context, base *apitypes.EventStream, upda return merged, changed, nil } -func (es *eventStream) Definition() *apitypes.EventStream { +func (es *eventStream) Spec() *apitypes.EventStream { return es.spec } -func (es *eventStream) UpdateDefinition(ctx context.Context, updates *apitypes.EventStream) error { +func (es *eventStream) UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error { merged, changed, err := mergeValidateEsConfig(ctx, es.spec, updates) if err != nil { return err @@ -242,9 +243,10 @@ func (es *eventStream) UpdateDefinition(ctx context.Context, updates *apitypes.E es.mux.Lock() es.spec = merged + isStarted := es.state == StreamStateStarted es.mux.Unlock() - if changed { + if changed && isStarted { if err := es.Stop(ctx); err != nil { return i18n.NewError(ctx, tmmsgs.MsgStopFailedUpdatingESConfig, err) } @@ -268,9 +270,15 @@ func safeCompareFilterList(a, b []fftypes.JSONAny) bool { return true } -func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.Listener) error { +func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.Listener) (err error) { log.L(ctx).Warnf("Initializing listener %s", spec.ID) + if spec.ID == nil { + spec.ID = apitypes.UUIDVersion1() + spec.Created = fftypes.Now() + } + spec.Updated = fftypes.Now() + // Allow a single "event" object to be specified instead of a filter, with an optional "address". // This is migrated to the new syntax: `"filters":[{"address":"0x1235","event":{...}}]` // (only expected to work for the eth connector that supports address/event) @@ -292,6 +300,10 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.L return i18n.NewError(ctx, tmmsgs.MsgBadListenerOptions, err) } + // We update the spec object in-place for the signature and resolved options + spec.Signature = signature + spec.Options = &mergedOptions + // Check if this is a new listener, an update, or a no-op es.mux.Lock() l, exists := es.listeners[*spec.ID] @@ -353,7 +365,7 @@ func (es *eventStream) String() string { } // checkSetState - caller must have locked the mux when calling this -func (es *eventStream) checkSetState(ctx context.Context, requiredState streamState, newState ...streamState) error { +func (es *eventStream) checkSetState(ctx context.Context, requiredState StreamState, newState ...StreamState) error { if es.state != requiredState { return i18n.NewError(ctx, tmmsgs.MsgStreamStateError, es.state) } @@ -366,7 +378,7 @@ func (es *eventStream) checkSetState(ctx context.Context, requiredState streamSt func (es *eventStream) Start(ctx context.Context) error { es.mux.Lock() defer es.mux.Unlock() - if err := es.checkSetState(ctx, streamStateStopped, streamStateStarted); err != nil { + if err := es.checkSetState(ctx, StreamStateStopped, StreamStateStarted); err != nil { return err } log.L(ctx).Infof("Starting event stream %s", es) @@ -394,7 +406,7 @@ func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, er es.mux.Lock() startedState := es.currentState defer es.mux.Unlock() - if err := es.checkSetState(ctx, streamStateStarted, streamStateStopping); err != nil { + if err := es.checkSetState(ctx, StreamStateStarted, StreamStateStopping); err != nil { return nil, err } log.L(ctx).Infof("Stopping event stream %s", es) @@ -406,13 +418,19 @@ func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, er for _, l := range es.listeners { err := l.stop(startedState) if err != nil { - _ = es.checkSetState(ctx, streamStateStopping, streamStateStarted) // restore started state + _ = es.checkSetState(ctx, StreamStateStopping, StreamStateStarted) // restore started state return nil, err } } return startedState, nil } +func (es *eventStream) State() StreamState { + es.mux.Lock() + defer es.mux.Unlock() + return es.state +} + func (es *eventStream) Stop(ctx context.Context) error { // Request the stop - this phase is locked, and gives us a safe copy of the listeners array to use outside the lock @@ -428,12 +446,12 @@ func (es *eventStream) Stop(ctx context.Context) error { es.mux.Lock() es.currentState = nil defer es.mux.Unlock() - return es.checkSetState(ctx, streamStateStopping, streamStateStopped) + return es.checkSetState(ctx, StreamStateStopping, StreamStateStopped) } func (es *eventStream) Delete(ctx context.Context) error { // Check we are stopped - if err := es.checkSetState(ctx, streamStateStopped); err != nil { + if err := es.checkSetState(ctx, StreamStateStopped); err != nil { if err := es.Stop(ctx); err != nil { return err } @@ -447,7 +465,7 @@ func (es *eventStream) Delete(ctx context.Context) error { if err := es.persistence.DeleteCheckpoint(ctx, es.spec.ID); err != nil { return err } - return es.checkSetState(ctx, streamStateStopped, streamStateDeleted) + return es.checkSetState(ctx, StreamStateStopped, StreamStateDeleted) } func (es *eventStream) eventLoop(startedState *startedStreamState) { diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 69f1d1d3..5f311bc5 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -247,7 +247,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { addr := "0x12345" l := &apitypes.Listener{ - ID: fftypes.NewUUID(), + // ID will be allocated in AddOrUpdateListener Name: "ut_listener", DeprecatedAddress: &addr, DeprecatedEvent: fftypes.JSONAnyPtr(`{"event":"definition"}`), @@ -286,6 +286,8 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { err = es.Start(es.bgCtx) assert.NoError(t, err) + assert.Equal(t, StreamStateStarted, es.State()) + err = es.Start(es.bgCtx) // double start is error assert.Regexp(t, "FF21027", err) @@ -480,16 +482,16 @@ func TestUpdateStreamStarted(t *testing.T) { defNoChange := testESConf(t, `{ "name": "ut_stream" }`) - err = es.UpdateDefinition(context.Background(), defNoChange) + err = es.UpdateSpec(context.Background(), defNoChange) assert.NoError(t, err) defChanged := testESConf(t, `{ "name": "ut_stream2" }`) - err = es.UpdateDefinition(context.Background(), defChanged) + err = es.UpdateSpec(context.Background(), defChanged) assert.NoError(t, err) - assert.Equal(t, "ut_stream2", *es.Definition().Name) + assert.Equal(t, "ut_stream2", *es.Spec().Name) <-r.Done r = <-started @@ -681,10 +683,10 @@ func TestUpdateEventStreamBad(t *testing.T) { "name": "new_name", "type": "wrong" }`) - err := es.UpdateDefinition(context.Background(), defNoChange) + err := es.UpdateSpec(context.Background(), defNoChange) assert.Regexp(t, "FF21029", err) - assert.Equal(t, "old_name", *es.Definition().Name) + assert.Equal(t, "old_name", *es.Spec().Name) } @@ -727,7 +729,7 @@ func TestUpdateStreamRestartFail(t *testing.T) { defChanged := testESConf(t, `{ "name": "ut_stream2" }`) - err = es.UpdateDefinition(context.Background(), defChanged) + err = es.UpdateSpec(context.Background(), defChanged) assert.Regexp(t, "FF21032.*pop", err) <-r.Done @@ -780,7 +782,7 @@ func TestUpdateStreamStopFail(t *testing.T) { defChanged := testESConf(t, `{ "name": "ut_stream2" }`) - err = es.UpdateDefinition(context.Background(), defChanged) + err = es.UpdateSpec(context.Background(), defChanged) assert.Regexp(t, "FF21031.*pop", err) err = es.Delete(context.Background()) diff --git a/internal/tmmsgs/en_api_descriptions.go b/internal/tmmsgs/en_api_descriptions.go index 70ee4cba..92ab2a63 100644 --- a/internal/tmmsgs/en_api_descriptions.go +++ b/internal/tmmsgs/en_api_descriptions.go @@ -27,5 +27,10 @@ var ffm = func(key, translation string) i18n.MessageKey { //revive:disable var ( - APIEndpointPatchEventStream = ffm("api.endpoints.post.eventstreams", "Create a new event stream") + APIEndpointPostEventStream = ffm("api.endpoints.post.eventstreams", "Create a new event stream") + APIEndpointPatchEventStream = ffm("api.endpoints.patch.eventstreams", "Update an existing event stream") + APIEndpointPostEventStreamSuspend = ffm("api.endpoints.post.eventstream.suspend", "Suspend an event stream") + APIEndpointPostEventStreamResume = ffm("api.endpoints.post.eventstream.resume", "Resume an event stream") + + APIParamStreamID = ffm("api.params.streamId", "Event Stream ID") ) diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 0a868c7d..87c6f189 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -87,12 +87,15 @@ type WebSocketConfig struct { type Listener struct { ID *fftypes.UUID `ffstruct:"listener" json:"id,omitempty"` + Created *fftypes.FFTime `ffstruct:"listener" json:"created"` + Updated *fftypes.FFTime `ffstruct:"listener" json:"updated"` Name string `ffstruct:"listener" json:"name"` StreamID *fftypes.UUID `ffstruct:"listener" json:"stream" ffexcludeoutput:"true"` DeprecatedAddress *string `ffstruct:"listener" json:"address,omitempty"` DeprecatedEvent *fftypes.JSONAny `ffstruct:"listener" json:"event,omitempty"` Filters []fftypes.JSONAny `ffstruct:"listener" json:"filters"` Options *fftypes.JSONAny `ffstruct:"listener" json:"options"` + Signature string `ffstruct:"listener" json:"signature,omitempty" ffexcludeinput:"true"` FromBlock string `ffstruct:"listener" json:"fromBlock,omitempty"` } diff --git a/pkg/fftm/api.go b/pkg/fftm/api.go index 97ed8e36..6b71d39e 100644 --- a/pkg/fftm/api.go +++ b/pkg/fftm/api.go @@ -36,7 +36,6 @@ import ( func (m *manager) router() *mux.Router { mux := mux.NewRouter() - mux.Path("/").Methods(http.MethodPost).Handler(http.HandlerFunc(m.apiHandler)) hf := ffapi.HandlerFactory{ DefaultRequestTimeout: config.GetDuration(tmconfig.APIDefaultRequestTimeout), MaxTimeout: config.GetDuration(tmconfig.APIMaxRequestTimeout), @@ -72,6 +71,10 @@ func (m *manager) router() *mux.Router { b, _ := json.Marshal(&doc) _, _ = res.Write(b) })) + mux.Path("/").Methods(http.MethodPost).Handler(http.HandlerFunc(m.apiHandler)) + mux.NotFoundHandler = hf.APIWrapper(func(res http.ResponseWriter, req *http.Request) (status int, err error) { + return 404, i18n.NewError(req.Context(), i18n.Msg404NotFound) + }) return mux } diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index 7c5663fc..fbcb9ce2 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -85,6 +85,7 @@ type manager struct { func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { var err error + events.InitDefaults() m := &manager{ connector: connector, ffCoreClient: ffresty.New(ctx, tmconfig.FFCoreConfig), @@ -93,6 +94,7 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { lockedNonces: make(map[string]*lockedNonce), apiServerDone: make(chan error), pendingOpsByID: make(map[string]*pendingState), + eventStreams: make(map[fftypes.UUID]events.Stream), name: config.GetString(tmconfig.ManagerName), opTypes: config.GetStringSlice(tmconfig.OperationsTypes), @@ -325,6 +327,16 @@ func (m *manager) Close() { <-m.fullScanLoopDone <-m.policyLoopDone m.waitWSStop() + + streams := []events.Stream{} + m.mux.Lock() + for _, s := range m.eventStreams { + streams = append(streams, s) + } + m.mux.Unlock() + for _, s := range streams { + _ = s.Stop(m.ctx) + } } m.persistence.Close(m.ctx) } diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 589121f9..d1ce16fd 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -46,6 +46,8 @@ import ( const testManagerName = "unittest" +func strPtr(s string) *string { return &s } + func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...string) (string, *manager, func()) { tmconfig.Reset() policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) diff --git a/pkg/fftm/route_delete_eventstream.go b/pkg/fftm/route_delete_eventstream.go new file mode 100644 index 00000000..cbc3c374 --- /dev/null +++ b/pkg/fftm/route_delete_eventstream.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" +) + +var deleteEventStream = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "deleteEventStream", + Path: "/eventstreams/{streamId}", + Method: http.MethodDelete, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchEventStream, + JSONInputValue: func() interface{} { return struct{}{} }, // empty input + JSONOutputValue: func() interface{} { return struct{}{} }, // empty output + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + err = m.deleteStream(r.Req.Context(), r.PP["streamId"]) + return &struct{}{}, err + }, + } +} diff --git a/pkg/fftm/route_delete_eventstream_test.go b/pkg/fftm/route_delete_eventstream_test.go new file mode 100644 index 00000000..b792c6fe --- /dev/null +++ b/pkg/fftm/route_delete_eventstream_test.go @@ -0,0 +1,56 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestDeleteEventStream(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + // Create stream + var es apitypes.EventStream + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + // Then delete it + res, err = resty.New().R(). + SetResult(&es). + Delete(url + "/eventstreams/" + es.ID.String()) + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + assert.Nil(t, m.eventStreams[(*es.ID)]) + +} diff --git a/pkg/fftm/route_patch_eventstream.go b/pkg/fftm/route_patch_eventstream.go new file mode 100644 index 00000000..8ebb395e --- /dev/null +++ b/pkg/fftm/route_patch_eventstream.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var patchEventStream = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "patchEventStream", + Path: "/eventstreams/{streamId}", + Method: http.MethodPatch, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchEventStream, + JSONInputValue: func() interface{} { return &apitypes.EventStream{} }, + JSONOutputValue: func() interface{} { return &apitypes.EventStream{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.updateStream(r.Req.Context(), r.PP["streamId"], r.Input.(*apitypes.EventStream)) + }, + } +} diff --git a/pkg/fftm/route_patch_eventstream_test.go b/pkg/fftm/route_patch_eventstream_test.go new file mode 100644 index 00000000..13adc50c --- /dev/null +++ b/pkg/fftm/route_patch_eventstream_test.go @@ -0,0 +1,61 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestPatchEventStream(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + // Create stream + var es apitypes.EventStream + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + // Then update it + res, err = resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my renamed event stream"), + }). + SetResult(&es). + Patch(url + "/eventstreams/" + es.ID.String()) + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + assert.NotNil(t, es.ID) + assert.NotNil(t, es.Created) + assert.NotEqual(t, es.Created, es.Updated) + assert.Equal(t, "my renamed event stream", *es.Name) + +} diff --git a/pkg/fftm/route_patch_new_eventstream.go b/pkg/fftm/route_post_eventstream.go similarity index 88% rename from pkg/fftm/route_patch_new_eventstream.go rename to pkg/fftm/route_post_eventstream.go index e40b3010..c41d70cb 100644 --- a/pkg/fftm/route_patch_new_eventstream.go +++ b/pkg/fftm/route_post_eventstream.go @@ -27,16 +27,16 @@ import ( var postEventStream = func(m *manager) *ffapi.Route { return &ffapi.Route{ Name: "postEventStream", - Path: "eventstreams", + Path: "/eventstreams", Method: http.MethodPost, PathParams: nil, QueryParams: nil, - Description: tmmsgs.APIEndpointPatchEventStream, + Description: tmmsgs.APIEndpointPostEventStream, JSONInputValue: func() interface{} { return &apitypes.EventStream{} }, JSONOutputValue: func() interface{} { return &apitypes.EventStream{} }, JSONOutputCodes: []int{http.StatusOK}, JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { - return m.newStream(r.Req.Context(), r.Input.(*apitypes.EventStream)) + return m.createAndStoreNewStream(r.Req.Context(), r.Input.(*apitypes.EventStream)) }, } } diff --git a/pkg/fftm/route_post_eventstream_resume.go b/pkg/fftm/route_post_eventstream_resume.go new file mode 100644 index 00000000..ce9c2931 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_resume.go @@ -0,0 +1,48 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postEventStreamResume = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postEventStreamResume", + Path: "/eventstreams/{streamId}/resume", + Method: http.MethodPost, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostEventStreamResume, + JSONInputValue: func() interface{} { return struct{}{} }, // empty input + JSONOutputValue: func() interface{} { return struct{}{} }, // empty output + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + falsy := false + _, err = m.updateStream(r.Req.Context(), r.PP["streamId"], &apitypes.EventStream{ + Suspended: &falsy, + }) + return &struct{}{}, err + }, + } +} diff --git a/pkg/fftm/route_post_eventstream_resume_test.go b/pkg/fftm/route_post_eventstream_resume_test.go new file mode 100644 index 00000000..e076a5a3 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_resume_test.go @@ -0,0 +1,60 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/internal/events" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestPostEventStreamResume(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + // Create stream + var es apitypes.EventStream + truthy := true + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + Suspended: &truthy, + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + // Then suspend it + res, err = resty.New().R(). + SetBody(&struct{}{}). + SetResult(&es). + Post(url + "/eventstreams/" + es.ID.String() + "/resume") + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + assert.Equal(t, events.StreamStateStarted, m.eventStreams[(*es.ID)].State()) + +} diff --git a/pkg/fftm/route_post_eventstream_suspend.go b/pkg/fftm/route_post_eventstream_suspend.go new file mode 100644 index 00000000..3551ee42 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_suspend.go @@ -0,0 +1,48 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postEventStreamSuspend = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postEventStreamSuspend", + Path: "/eventstreams/{streamId}/suspend", + Method: http.MethodPost, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostEventStreamSuspend, + JSONInputValue: func() interface{} { return struct{}{} }, // empty input + JSONOutputValue: func() interface{} { return struct{}{} }, // empty output + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + truthy := true + _, err = m.updateStream(r.Req.Context(), r.PP["streamId"], &apitypes.EventStream{ + Suspended: &truthy, + }) + return &struct{}{}, err + }, + } +} diff --git a/pkg/fftm/route_post_eventstream_suspend_test.go b/pkg/fftm/route_post_eventstream_suspend_test.go new file mode 100644 index 00000000..858c87ee --- /dev/null +++ b/pkg/fftm/route_post_eventstream_suspend_test.go @@ -0,0 +1,58 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/internal/events" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestPostEventStreamSuspend(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + // Create stream + var es apitypes.EventStream + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + // Then suspend it + res, err = resty.New().R(). + SetBody(&struct{}{}). + SetResult(&es). + Post(url + "/eventstreams/" + es.ID.String() + "/suspend") + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + assert.Equal(t, events.StreamStateStopped, m.eventStreams[(*es.ID)].State()) + +} diff --git a/pkg/fftm/route_post_eventstream_test.go b/pkg/fftm/route_post_eventstream_test.go new file mode 100644 index 00000000..56f4247a --- /dev/null +++ b/pkg/fftm/route_post_eventstream_test.go @@ -0,0 +1,49 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestPostNewEventStream(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + var es apitypes.EventStream + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + assert.NotNil(t, es.ID) + assert.NotNil(t, es.Created) + assert.Equal(t, es.Created, es.Updated) + +} diff --git a/pkg/fftm/routes.go b/pkg/fftm/routes.go index 322075b9..e80e3913 100644 --- a/pkg/fftm/routes.go +++ b/pkg/fftm/routes.go @@ -21,5 +21,9 @@ import "github.com/hyperledger/firefly-common/pkg/ffapi" func (m *manager) routes() []*ffapi.Route { return []*ffapi.Route{ postEventStream(m), + patchEventStream(m), + postEventStreamSuspend(m), + postEventStreamResume(m), + deleteEventStream(m), } } diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index 0c081648..5e7f9d0c 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -20,6 +20,8 @@ import ( "context" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/events" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" ) @@ -40,13 +42,9 @@ func (m *manager) restoreStreams() error { } for _, def := range streamDefs { lastInPage = def.ID - s, err := events.NewEventStream(m.ctx, def, m.connector, m.persistence, m.confirmations, m.wsChannels) - if err != nil { + if _, err := m.addRuntimeStream(def); err != nil { return err } - m.mux.Lock() - m.eventStreams[*s.Definition().ID] = s - m.mux.Unlock() } } return m.restoreListeners() @@ -64,19 +62,131 @@ func (m *manager) restoreListeners() error { } for _, def := range listenerDefs { lastInPage = def.ID - m.mux.Lock() - s := m.eventStreams[*def.StreamID] - m.mux.Unlock() - if s != nil { - if err := s.AddOrUpdateListener(m.ctx, def); err != nil { - return err - } + if _, err := m.addRuntimeListener(m.ctx, def); err != nil { + return err + } + } + } + return nil +} + +func (m *manager) deleteAllStreamListeners(ctx context.Context, streamID *fftypes.UUID) error { + var lastInPage *fftypes.UUID + for { + listenerDefs, err := m.persistence.ListStreamListeners(ctx, lastInPage, startupPaginationLimit, streamID) + if err != nil { + return err + } + if len(listenerDefs) == 0 { + break + } + for _, def := range listenerDefs { + lastInPage = def.ID + if err := m.persistence.DeleteListener(ctx, def.ID); err != nil { + return err } } } return nil } -func (m *manager) newStream(ctx context.Context, es *apitypes.EventStream) (*apitypes.EventStream, error) { - return nil, nil +func (m *manager) addRuntimeListener(ctx context.Context, def *apitypes.Listener) (*apitypes.Listener, error) { + m.mux.Lock() + s := m.eventStreams[*def.StreamID] + m.mux.Unlock() + if s != nil { + // The definition is updated in-place by the event stream code + if err := s.AddOrUpdateListener(ctx, def); err != nil { + return nil, err + } + } + return def, nil +} + +func (m *manager) addRuntimeStream(def *apitypes.EventStream) (events.Stream, error) { + s, err := events.NewEventStream(m.ctx, def, m.connector, m.persistence, m.confirmations, m.wsChannels) + if err != nil { + return nil, err + } + spec := s.Spec() + m.mux.Lock() + m.eventStreams[*spec.ID] = s + m.mux.Unlock() + return s, nil +} + +func (m *manager) deleteStream(ctx context.Context, idStr string) error { + id, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return err + } + m.mux.Lock() + s := m.eventStreams[*id] + delete(m.eventStreams, *id) + m.mux.Unlock() + if s != nil { + if err := m.deleteAllStreamListeners(ctx, id); err != nil { + return err + } + if err := m.persistence.DeleteStream(ctx, id); err != nil { + return err + } + return s.Delete(ctx) + } + return nil +} + +func (m *manager) createAndStoreNewStream(ctx context.Context, def *apitypes.EventStream) (*apitypes.EventStream, error) { + def.ID = nil // set by addRuntimeStream + def.Created = nil + s, err := m.addRuntimeStream(def) + if err != nil { + return nil, err + } + spec := s.Spec() + err = m.persistence.WriteStream(ctx, spec) + if err != nil { + err1 := m.deleteStream(ctx, spec.ID.String()) + log.L(ctx).Infof("Cleaned up runtime stream after write failed (err?=%v)", err1) + return nil, err + } + if !*spec.Suspended { + if err = s.Start(ctx); err != nil { + return nil, err + } + } + return spec, nil +} + +func (m *manager) updateStream(ctx context.Context, idStr string, updates *apitypes.EventStream) (*apitypes.EventStream, error) { + id, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return nil, err + } + m.mux.Lock() + s := m.eventStreams[*id] + m.mux.Unlock() + if s == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) + } + err = s.UpdateSpec(ctx, updates) + if err != nil { + return nil, err + } + spec := s.Spec() + err = m.persistence.WriteStream(ctx, spec) + if err != nil { + return nil, err + } + // We might need to start or stop + if *spec.Suspended && s.State() != events.StreamStateStopped { + if err = s.Stop(ctx); err != nil { + return nil, err + } + } else if !*spec.Suspended && s.State() != events.StreamStateStarted { + if err = s.Start(ctx); err != nil { + return nil, err + } + } + return spec, nil } From 9045baf26dedfedd39315f8168186169a64ceb69 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 16 Jun 2022 17:02:05 -0400 Subject: [PATCH 17/95] More routes Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 47 ++++++-------- internal/events/eventstream_test.go | 2 +- internal/tmmsgs/en_api_descriptions.go | 3 + internal/tmmsgs/en_error_messges.go | 1 + pkg/apitypes/api_types.go | 14 ++++ pkg/fftm/route_delete_eventstream.go | 8 +-- pkg/fftm/route_get_eventstream.go | 44 +++++++++++++ pkg/fftm/route_get_eventstream_test.go | 58 +++++++++++++++++ pkg/fftm/route_get_eventstreams copy.go | 46 +++++++++++++ pkg/fftm/route_get_eventstreams.go | 45 +++++++++++++ pkg/fftm/route_get_eventstreams_test.go | 56 ++++++++++++++++ pkg/fftm/route_get_subscriptions_test.go | 56 ++++++++++++++++ .../route_post_eventstream_resume_test.go | 3 +- .../route_post_eventstream_suspend_test.go | 3 +- pkg/fftm/routes.go | 8 ++- pkg/fftm/stream_management.go | 65 ++++++++++++++++++- 16 files changed, 417 insertions(+), 42 deletions(-) create mode 100644 pkg/fftm/route_get_eventstream.go create mode 100644 pkg/fftm/route_get_eventstream_test.go create mode 100644 pkg/fftm/route_get_eventstreams copy.go create mode 100644 pkg/fftm/route_get_eventstreams.go create mode 100644 pkg/fftm/route_get_eventstreams_test.go create mode 100644 pkg/fftm/route_get_subscriptions_test.go diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index d3ea5924..383ba718 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -40,21 +40,12 @@ type Stream interface { RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error // Apply definition updates (if there are changes) Spec() *apitypes.EventStream // Retrieve the merged definition to persist - State() StreamState // Get the current state + Status() apitypes.EventStreamStatus // Get the current status Start(ctx context.Context) error // Start delivery Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint } -type StreamState string - -const ( - StreamStateStarted StreamState = "started" - StreamStateStopping StreamState = "stopping" - StreamStateStopped StreamState = "stopped" - StreamStateDeleted StreamState = "deleted" -) - // esDefaults are the defaults for new event streams, read from the config once in InitDefaults() var esDefaults struct { initialized bool @@ -106,7 +97,7 @@ type eventStream struct { bgCtx context.Context spec *apitypes.EventStream mux sync.Mutex - state StreamState + status apitypes.EventStreamStatus connector ffcapi.API persistence persistence.Persistence confirmations confirmations.Manager @@ -126,7 +117,7 @@ func NewEventStream( ) (ees Stream, err error) { es := &eventStream{ bgCtx: log.WithLogField(bgCtx, "eventstream", persistedSpec.ID.String()), - state: StreamStateStopped, + status: apitypes.EventStreamStatusStopped, spec: persistedSpec, connector: connector, persistence: persistence, @@ -243,7 +234,7 @@ func (es *eventStream) UpdateSpec(ctx context.Context, updates *apitypes.EventSt es.mux.Lock() es.spec = merged - isStarted := es.state == StreamStateStarted + isStarted := es.status == apitypes.EventStreamStatusStarted es.mux.Unlock() if changed && isStarted { @@ -325,7 +316,7 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.L signature: signature, } } - // Take a copy of the current started state, before unlocking + // Take a copy of the current started status, before unlocking startedState := es.currentState es.mux.Unlock() if startedState == nil { @@ -364,13 +355,13 @@ func (es *eventStream) String() string { return es.spec.ID.String() } -// checkSetState - caller must have locked the mux when calling this -func (es *eventStream) checkSetState(ctx context.Context, requiredState StreamState, newState ...StreamState) error { - if es.state != requiredState { - return i18n.NewError(ctx, tmmsgs.MsgStreamStateError, es.state) +// checkSetStatus - caller must have locked the mux when calling this +func (es *eventStream) checkSetStatus(ctx context.Context, requiredState apitypes.EventStreamStatus, newState ...apitypes.EventStreamStatus) error { + if es.status != requiredState { + return i18n.NewError(ctx, tmmsgs.MsgStreamStateError, es.status) } if len(newState) == 1 { - es.state = newState[0] + es.status = newState[0] } return nil } @@ -378,7 +369,7 @@ func (es *eventStream) checkSetState(ctx context.Context, requiredState StreamSt func (es *eventStream) Start(ctx context.Context) error { es.mux.Lock() defer es.mux.Unlock() - if err := es.checkSetState(ctx, StreamStateStopped, StreamStateStarted); err != nil { + if err := es.checkSetStatus(ctx, apitypes.EventStreamStatusStopped, apitypes.EventStreamStatusStarted); err != nil { return err } log.L(ctx).Infof("Starting event stream %s", es) @@ -406,7 +397,7 @@ func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, er es.mux.Lock() startedState := es.currentState defer es.mux.Unlock() - if err := es.checkSetState(ctx, StreamStateStarted, StreamStateStopping); err != nil { + if err := es.checkSetStatus(ctx, apitypes.EventStreamStatusStarted, apitypes.EventStreamStatusStopping); err != nil { return nil, err } log.L(ctx).Infof("Stopping event stream %s", es) @@ -418,17 +409,17 @@ func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, er for _, l := range es.listeners { err := l.stop(startedState) if err != nil { - _ = es.checkSetState(ctx, StreamStateStopping, StreamStateStarted) // restore started state + _ = es.checkSetStatus(ctx, apitypes.EventStreamStatusStopping, apitypes.EventStreamStatusStarted) // restore started status return nil, err } } return startedState, nil } -func (es *eventStream) State() StreamState { +func (es *eventStream) Status() apitypes.EventStreamStatus { es.mux.Lock() defer es.mux.Unlock() - return es.state + return es.status } func (es *eventStream) Stop(ctx context.Context) error { @@ -446,26 +437,26 @@ func (es *eventStream) Stop(ctx context.Context) error { es.mux.Lock() es.currentState = nil defer es.mux.Unlock() - return es.checkSetState(ctx, StreamStateStopping, StreamStateStopped) + return es.checkSetStatus(ctx, apitypes.EventStreamStatusStopping, apitypes.EventStreamStatusStopped) } func (es *eventStream) Delete(ctx context.Context) error { // Check we are stopped - if err := es.checkSetState(ctx, StreamStateStopped); err != nil { + if err := es.checkSetStatus(ctx, apitypes.EventStreamStatusStopped); err != nil { if err := es.Stop(ctx); err != nil { return err } } log.L(ctx).Infof("Deleting event stream %s", es) - // Hold the lock for the whole of delete, rather than transitioning into a deleting state. + // Hold the lock for the whole of delete, rather than transitioning into a deleting status. // If we error out, that way the caller can retry. es.mux.Lock() defer es.mux.Unlock() if err := es.persistence.DeleteCheckpoint(ctx, es.spec.ID); err != nil { return err } - return es.checkSetState(ctx, StreamStateStopped, StreamStateDeleted) + return es.checkSetStatus(ctx, apitypes.EventStreamStatusStopped, apitypes.EventStreamStatusDeleted) } func (es *eventStream) eventLoop(startedState *startedStreamState) { diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 5f311bc5..d40739c6 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -286,7 +286,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { err = es.Start(es.bgCtx) assert.NoError(t, err) - assert.Equal(t, StreamStateStarted, es.State()) + assert.Equal(t, apitypes.EventStreamStatusStarted, es.Status()) err = es.Start(es.bgCtx) // double start is error assert.Regexp(t, "FF21027", err) diff --git a/internal/tmmsgs/en_api_descriptions.go b/internal/tmmsgs/en_api_descriptions.go index 92ab2a63..2fa77460 100644 --- a/internal/tmmsgs/en_api_descriptions.go +++ b/internal/tmmsgs/en_api_descriptions.go @@ -31,6 +31,9 @@ var ( APIEndpointPatchEventStream = ffm("api.endpoints.patch.eventstreams", "Update an existing event stream") APIEndpointPostEventStreamSuspend = ffm("api.endpoints.post.eventstream.suspend", "Suspend an event stream") APIEndpointPostEventStreamResume = ffm("api.endpoints.post.eventstream.resume", "Resume an event stream") + APIEndpointGetEventStreams = ffm("api.endpoints.get.eventstreams", "List event streams") APIParamStreamID = ffm("api.params.streamId", "Event Stream ID") + APIParamLimit = ffm("api.params.limit", "Maximum number of entries to return") + APIParamAfter = ffm("api.params.after", "Return entries after this ID - for pagination (non-inclusive)") ) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 389513fa..51a2641e 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -59,4 +59,5 @@ var ( MsgInvalidHost = ffe("FF21041", "Cannot send Webhook POST to host '%s': %s") MsgWebhookErr = ffe("FF21042", "Webhook request failed: %s") MsgUnknownPersistence = ffe("FF21043", "Unknown persistence type '%s'") + MsgInvalidLimit = ffe("FF21044", "Invalid limit string '%s': %s") ) diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 87c6f189..c80293e9 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -67,6 +67,20 @@ type EventStream struct { WebSocket *WebSocketConfig `ffstruct:"eventstream" json:"websocket,omitempty"` } +type EventStreamStatus string + +const ( + EventStreamStatusStarted EventStreamStatus = "started" + EventStreamStatusStopping EventStreamStatus = "stopping" + EventStreamStatusStopped EventStreamStatus = "stopped" + EventStreamStatusDeleted EventStreamStatus = "deleted" +) + +type EventStreamWithStatus struct { + EventStream + Status EventStreamStatus `ffstruct:"eventstream" json:"status"` +} + type EventStreamCheckpoint struct { StreamID *fftypes.UUID `json:"streamId"` Time *fftypes.FFTime `json:"time"` diff --git a/pkg/fftm/route_delete_eventstream.go b/pkg/fftm/route_delete_eventstream.go index cbc3c374..d3c03bae 100644 --- a/pkg/fftm/route_delete_eventstream.go +++ b/pkg/fftm/route_delete_eventstream.go @@ -33,12 +33,12 @@ var deleteEventStream = func(m *manager) *ffapi.Route { }, QueryParams: nil, Description: tmmsgs.APIEndpointPatchEventStream, - JSONInputValue: func() interface{} { return struct{}{} }, // empty input - JSONOutputValue: func() interface{} { return struct{}{} }, // empty output - JSONOutputCodes: []int{http.StatusOK}, + JSONInputValue: nil, + JSONOutputValue: nil, + JSONOutputCodes: []int{http.StatusNoContent}, JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { err = m.deleteStream(r.Req.Context(), r.PP["streamId"]) - return &struct{}{}, err + return nil, err }, } } diff --git a/pkg/fftm/route_get_eventstream.go b/pkg/fftm/route_get_eventstream.go new file mode 100644 index 00000000..ed07830c --- /dev/null +++ b/pkg/fftm/route_get_eventstream.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getEventStream = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getEventStream", + Path: "/eventstreams/{streamId}", + Method: http.MethodGet, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchEventStream, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.EventStreamWithStatus{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getStream(r.Req.Context(), r.PP["streamId"]) + }, + } +} diff --git a/pkg/fftm/route_get_eventstream_test.go b/pkg/fftm/route_get_eventstream_test.go new file mode 100644 index 00000000..24f7cc8f --- /dev/null +++ b/pkg/fftm/route_get_eventstream_test.go @@ -0,0 +1,58 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestGetEventStream(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + // Create stream + var es apitypes.EventStream + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + // Then get it + var ess apitypes.EventStreamWithStatus + res, err = resty.New().R(). + SetResult(&ess). + Get(url + "/eventstreams/" + es.ID.String()) + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + assert.Equal(t, es.ID, ess.ID) + assert.Equal(t, apitypes.EventStreamStatusStarted, ess.Status) + +} diff --git a/pkg/fftm/route_get_eventstreams copy.go b/pkg/fftm/route_get_eventstreams copy.go new file mode 100644 index 00000000..ee1beb25 --- /dev/null +++ b/pkg/fftm/route_get_eventstreams copy.go @@ -0,0 +1,46 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getSubscriptions = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getSubscriptions", + Path: "/subscriptions", + Deprecated: true, // in favor of "/eventstreams/{id}/listeners" + Method: http.MethodGet, + PathParams: nil, + QueryParams: []*ffapi.QueryParam{ + {Name: "limit", Description: tmmsgs.APIParamLimit}, + {Name: "after", Description: tmmsgs.APIParamAfter}, + }, + Description: tmmsgs.APIEndpointPatchEventStream, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return []*apitypes.EventStream{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getStreams(r.Req.Context(), r.QP["after"], r.QP["limit"]) + }, + } +} diff --git a/pkg/fftm/route_get_eventstreams.go b/pkg/fftm/route_get_eventstreams.go new file mode 100644 index 00000000..49e8f098 --- /dev/null +++ b/pkg/fftm/route_get_eventstreams.go @@ -0,0 +1,45 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getEventStreams = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getEventStreams", + Path: "/eventstreams", + Method: http.MethodGet, + PathParams: nil, + QueryParams: []*ffapi.QueryParam{ + {Name: "limit", Description: tmmsgs.APIParamLimit}, + {Name: "after", Description: tmmsgs.APIParamAfter}, + }, + Description: tmmsgs.APIEndpointPatchEventStream, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return []*apitypes.EventStream{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getStreams(r.Req.Context(), r.QP["after"], r.QP["limit"]) + }, + } +} diff --git a/pkg/fftm/route_get_eventstreams_test.go b/pkg/fftm/route_get_eventstreams_test.go new file mode 100644 index 00000000..9633fae1 --- /dev/null +++ b/pkg/fftm/route_get_eventstreams_test.go @@ -0,0 +1,56 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestGetEventStreams(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + // Create 3 streams + var es1, es2, es3 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream2")}).SetResult(&es2).Post(url + "/eventstreams") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream3")}).SetResult(&es3).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Then get it + var ess []*apitypes.EventStream + res, err = resty.New().R(). + SetResult(&ess). + Get(url + "/eventstreams?limit=1&after=" + es1.ID.String()) + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + assert.Len(t, ess, 1) + assert.Equal(t, es2.ID, ess[0].ID) + +} diff --git a/pkg/fftm/route_get_subscriptions_test.go b/pkg/fftm/route_get_subscriptions_test.go new file mode 100644 index 00000000..5a04834a --- /dev/null +++ b/pkg/fftm/route_get_subscriptions_test.go @@ -0,0 +1,56 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestGetSubscriptions(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + // Create 3 streams + var es1, es2, es3 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream2")}).SetResult(&es2).Post(url + "/eventstreams") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream3")}).SetResult(&es3).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Then get it + var ess []*apitypes.EventStream + res, err = resty.New().R(). + SetResult(&ess). + Get(url + "/subscriptions?limit=1&after=" + es1.ID.String()) + assert.NoError(t, err) + assert.True(t, res.IsSuccess()) + + assert.Len(t, ess, 1) + assert.Equal(t, es2.ID, ess[0].ID) + +} diff --git a/pkg/fftm/route_post_eventstream_resume_test.go b/pkg/fftm/route_post_eventstream_resume_test.go index e076a5a3..d8467f21 100644 --- a/pkg/fftm/route_post_eventstream_resume_test.go +++ b/pkg/fftm/route_post_eventstream_resume_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-transaction-manager/internal/events" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/stretchr/testify/assert" ) @@ -55,6 +54,6 @@ func TestPostEventStreamResume(t *testing.T) { assert.NoError(t, err) assert.True(t, res.IsSuccess()) - assert.Equal(t, events.StreamStateStarted, m.eventStreams[(*es.ID)].State()) + assert.Equal(t, apitypes.EventStreamStatusStarted, m.eventStreams[(*es.ID)].Status()) } diff --git a/pkg/fftm/route_post_eventstream_suspend_test.go b/pkg/fftm/route_post_eventstream_suspend_test.go index 858c87ee..05a704e7 100644 --- a/pkg/fftm/route_post_eventstream_suspend_test.go +++ b/pkg/fftm/route_post_eventstream_suspend_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-transaction-manager/internal/events" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/stretchr/testify/assert" ) @@ -53,6 +52,6 @@ func TestPostEventStreamSuspend(t *testing.T) { assert.NoError(t, err) assert.True(t, res.IsSuccess()) - assert.Equal(t, events.StreamStateStopped, m.eventStreams[(*es.ID)].State()) + assert.Equal(t, apitypes.EventStreamStatusStopped, m.eventStreams[(*es.ID)].Status()) } diff --git a/pkg/fftm/routes.go b/pkg/fftm/routes.go index e80e3913..cee231b6 100644 --- a/pkg/fftm/routes.go +++ b/pkg/fftm/routes.go @@ -20,10 +20,12 @@ import "github.com/hyperledger/firefly-common/pkg/ffapi" func (m *manager) routes() []*ffapi.Route { return []*ffapi.Route{ - postEventStream(m), + deleteEventStream(m), + getEventStream(m), + getEventStreams(m), patchEventStream(m), - postEventStreamSuspend(m), + postEventStream(m), postEventStreamResume(m), - deleteEventStream(m), + postEventStreamSuspend(m), } } diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index 5e7f9d0c..774fbb27 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -18,11 +18,13 @@ package fftm import ( "context" + "strconv" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/events" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" ) @@ -179,14 +181,73 @@ func (m *manager) updateStream(ctx context.Context, idStr string, updates *apity return nil, err } // We might need to start or stop - if *spec.Suspended && s.State() != events.StreamStateStopped { + if *spec.Suspended && s.Status() != apitypes.EventStreamStatusStopped { if err = s.Stop(ctx); err != nil { return nil, err } - } else if !*spec.Suspended && s.State() != events.StreamStateStarted { + } else if !*spec.Suspended && s.Status() != apitypes.EventStreamStatusStarted { if err = s.Start(ctx); err != nil { return nil, err } } return spec, nil } + +func (m *manager) getStream(ctx context.Context, idStr string) (*apitypes.EventStreamWithStatus, error) { + id, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return nil, err + } + m.mux.Lock() + s := m.eventStreams[*id] + m.mux.Unlock() + if s == nil { + return nil, nil + } + return &apitypes.EventStreamWithStatus{ + EventStream: *s.Spec(), + Status: s.Status(), + }, nil +} + +func (m *manager) parseAfterAndLimit(ctx context.Context, afterStr, limitStr string) (after *fftypes.UUID, limit int, err error) { + if limitStr != "" { + if limit, err = strconv.Atoi(limitStr); err != nil { + return nil, -1, i18n.NewError(ctx, tmmsgs.MsgInvalidLimit, limitStr, err) + } + } + if afterStr != "" { + if after, err = fftypes.ParseUUID(ctx, afterStr); err != nil { + return nil, -1, err + } + } + return after, limit, nil +} + +func (m *manager) getStreams(ctx context.Context, afterStr, limitStr string) (streams []*apitypes.EventStream, err error) { + after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) + if err != nil { + return nil, err + } + return m.persistence.ListStreams(ctx, after, limit) +} + +func (m *manager) getListeners(ctx context.Context, afterStr, limitStr string) (streams []*apitypes.Listener, err error) { + after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) + if err != nil { + return nil, err + } + return m.persistence.ListListeners(ctx, after, limit) +} + +func (m *manager) getStreamListeners(ctx context.Context, afterStr, limitStr, idStr string) (streams []*apitypes.Listener, err error) { + after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) + if err != nil { + return nil, err + } + id, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return nil, err + } + return m.persistence.ListStreamListeners(ctx, after, limit, id) +} From 37727621b7731b339743936b8e6e1f6e98ff58b4 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 17 Jun 2022 19:24:21 -0400 Subject: [PATCH 18/95] Deprecated /subscriptions routes Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 1 + internal/events/eventstream.go | 19 +- internal/events/eventstream_test.go | 16 +- internal/tmmsgs/en_api_descriptions.go | 14 +- internal/tmmsgs/en_error_messges.go | 4 + pkg/fftm/manager.go | 2 + pkg/fftm/route_delete_eventstream.go | 2 +- pkg/fftm/route_delete_eventstream_test.go | 4 +- pkg/fftm/route_delete_subscription.go | 45 +++++ pkg/fftm/route_delete_subscription_test.go | 62 +++++++ pkg/fftm/route_get_eventstream.go | 2 +- pkg/fftm/route_get_eventstream_test.go | 4 +- pkg/fftm/route_get_eventstreams.go | 2 +- pkg/fftm/route_get_eventstreams_test.go | 2 +- pkg/fftm/route_get_subscription.go | 45 +++++ pkg/fftm/route_get_subscription_test.go | 67 +++++++ ...ams copy.go => route_get_subscriptions.go} | 6 +- pkg/fftm/route_get_subscriptions_test.go | 35 ++-- pkg/fftm/route_patch_eventstream_test.go | 4 +- pkg/fftm/route_patch_subscription.go | 45 +++++ pkg/fftm/route_patch_subscription_test.go | 71 ++++++++ .../route_post_eventstream_resume_test.go | 4 +- .../route_post_eventstream_suspend_test.go | 4 +- pkg/fftm/route_post_eventstream_test.go | 2 +- pkg/fftm/route_post_subscriptions.go | 46 +++++ pkg/fftm/route_post_subscriptions_test.go | 59 +++++++ pkg/fftm/routes.go | 5 + pkg/fftm/stream_management.go | 164 ++++++++++++++++-- 28 files changed, 687 insertions(+), 49 deletions(-) create mode 100644 pkg/fftm/route_delete_subscription.go create mode 100644 pkg/fftm/route_delete_subscription_test.go create mode 100644 pkg/fftm/route_get_subscription.go create mode 100644 pkg/fftm/route_get_subscription_test.go rename pkg/fftm/{route_get_eventstreams copy.go => route_get_subscriptions.go} (87%) create mode 100644 pkg/fftm/route_patch_subscription.go create mode 100644 pkg/fftm/route_patch_subscription_test.go create mode 100644 pkg/fftm/route_post_subscriptions.go create mode 100644 pkg/fftm/route_post_subscriptions_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 54ec9168..533da0c2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -77,6 +77,7 @@ "unmarshalled", "unmarshalling", "upgrader", + "upsert", "upserts", "Warnf", "whconfig", diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 383ba718..91e017c6 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -160,8 +160,14 @@ func mergeValidateEsConfig(ctx context.Context, base *apitypes.EventStream, upda Updated: fftypes.Now(), } if merged.Created == nil || merged.ID == nil { - merged.Created = merged.Updated - merged.ID = apitypes.UUIDVersion1() + merged.Created = updates.Created + merged.ID = updates.ID + if merged.Created == nil { + merged.Created = merged.Updated + } + if merged.ID == nil { + return nil, false, i18n.NewError(ctx, tmmsgs.MsgMissingID) + } } // Name (no default - must be set) // - Note we do not check for uniqueness of the name at this layer in the code, but we do require unique names. @@ -294,27 +300,32 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.L // We update the spec object in-place for the signature and resolved options spec.Signature = signature spec.Options = &mergedOptions + if spec.Name == "" { + spec.Name = signature + } // Check if this is a new listener, an update, or a no-op es.mux.Lock() l, exists := es.listeners[*spec.ID] if exists { if mergedOptions == l.options && safeCompareFilterList(spec.Filters, l.filters) { - log.L(ctx).Infof("Event listener already configured on stream") + log.L(ctx).Infof("Event listener '%s' already configured on stream, with no changes", l.id) es.mux.Unlock() return nil } + log.L(ctx).Infof("Listener '%s' configuration updated, it will be restarted", l.id) l.options = mergedOptions l.filters = spec.Filters l.signature = signature } else { - es.listeners[*spec.ID] = &listener{ + l = &listener{ es: es, id: spec.ID, options: mergedOptions, filters: spec.Filters, signature: signature, } + es.listeners[*spec.ID] = l } // Take a copy of the current started status, before unlocking startedState := es.currentState diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index d40739c6..e06ac3c2 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -41,6 +41,7 @@ import ( func testESConf(t *testing.T, j string) (spec *apitypes.EventStream) { err := json.Unmarshal([]byte(j), &spec) assert.NoError(t, err) + spec.ID = apitypes.UUIDVersion1() return spec } @@ -65,6 +66,17 @@ func mockWSChannels(wsc *wsmocks.WebSocketChannels) (chan interface{}, chan inte return senderChannel, broadcastChannel, receiverChannel } +func TestNewTestEventStreamMissingID(t *testing.T) { + tmconfig.Reset() + InitDefaults() + _, err := NewEventStream(context.Background(), &apitypes.EventStream{}, + &ffcapimocks.API{}, + &persistencemocks.Persistence{}, + &confirmationsmocks.Manager{}, + &wsmocks.WebSocketChannels{}) + assert.Regexp(t, "FF21048", err) +} + func TestNewTestEventStreamBadConfig(t *testing.T) { tmconfig.Reset() InitDefaults() @@ -351,8 +363,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { }`) l := &apitypes.Listener{ - ID: fftypes.NewUUID(), - Name: "ut_listener", + ID: fftypes.NewUUID(), Filters: []fftypes.JSONAny{ `{"event":"definition1"}`, `{"event":"definition2"}`, @@ -386,6 +397,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { err := es.AddOrUpdateListener(es.bgCtx, l) assert.NoError(t, err) + assert.Equal(t, "EventSig(uint256)", l.Name) // Defaulted err = es.Start(es.bgCtx) assert.NoError(t, err) diff --git a/internal/tmmsgs/en_api_descriptions.go b/internal/tmmsgs/en_api_descriptions.go index 2fa77460..c383fdd9 100644 --- a/internal/tmmsgs/en_api_descriptions.go +++ b/internal/tmmsgs/en_api_descriptions.go @@ -32,8 +32,16 @@ var ( APIEndpointPostEventStreamSuspend = ffm("api.endpoints.post.eventstream.suspend", "Suspend an event stream") APIEndpointPostEventStreamResume = ffm("api.endpoints.post.eventstream.resume", "Resume an event stream") APIEndpointGetEventStreams = ffm("api.endpoints.get.eventstreams", "List event streams") + APIEndpointGetEventStream = ffm("api.endpoints.get.eventstream", "Get an event stream with status") + APIEndpointDeleteEventStream = ffm("api.endpoints.delete.eventstream", "Delete an event stream") + APIEndpointGetSubscriptions = ffm("api.endpoints.get.subscriptions", "Get listeners - route deprecated in favor of /eventstreams/{streamId}/listeners") + APIEndpointGetSubscription = ffm("api.endpoints.get.subscription", "Get listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointPostSubscriptions = ffm("api.endpoints.post.subscriptions", "Create new listener - route deprecated in favor of /eventstreams/{streamId}/listeners") + APIEndpointPatchSubscription = ffm("api.endpoints.patch.subscription", "Update listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointDeleteSubscription = ffm("api.endpoints.delete.subscription", "Delete listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") - APIParamStreamID = ffm("api.params.streamId", "Event Stream ID") - APIParamLimit = ffm("api.params.limit", "Maximum number of entries to return") - APIParamAfter = ffm("api.params.after", "Return entries after this ID - for pagination (non-inclusive)") + APIParamStreamID = ffm("api.params.streamId", "Event Stream ID") + APIParamListenerID = ffm("api.params.listenerId", "Listener ID") + APIParamLimit = ffm("api.params.limit", "Maximum number of entries to return") + APIParamAfter = ffm("api.params.after", "Return entries after this ID - for pagination (non-inclusive)") ) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 51a2641e..0b779e66 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -60,4 +60,8 @@ var ( MsgWebhookErr = ffe("FF21042", "Webhook request failed: %s") MsgUnknownPersistence = ffe("FF21043", "Unknown persistence type '%s'") MsgInvalidLimit = ffe("FF21044", "Invalid limit string '%s': %s") + MsgStreamNotFound = ffe("FF21045", "Event stream '%v' not found") + MsgListenerNotFound = ffe("FF21046", "Event listener '%v' not found") + MsgDuplicateStreamName = ffe("FF21047", "Duplicate event stream name '%s' used by stream '%s'", 409) + MsgMissingID = ffe("FF21048", "ID is required", 400) ) diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index fbcb9ce2..65caba2a 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -65,6 +65,7 @@ type manager struct { lockedNonces map[string]*lockedNonce pendingOpsByID map[string]*pendingState eventStreams map[fftypes.UUID]events.Stream + streamsByName map[string]*fftypes.UUID changeEventLoopDone chan struct{} firstFullScanDone chan error policyLoopDone chan struct{} @@ -95,6 +96,7 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { apiServerDone: make(chan error), pendingOpsByID: make(map[string]*pendingState), eventStreams: make(map[fftypes.UUID]events.Stream), + streamsByName: make(map[string]*fftypes.UUID), name: config.GetString(tmconfig.ManagerName), opTypes: config.GetStringSlice(tmconfig.OperationsTypes), diff --git a/pkg/fftm/route_delete_eventstream.go b/pkg/fftm/route_delete_eventstream.go index d3c03bae..841a037c 100644 --- a/pkg/fftm/route_delete_eventstream.go +++ b/pkg/fftm/route_delete_eventstream.go @@ -32,7 +32,7 @@ var deleteEventStream = func(m *manager) *ffapi.Route { {Name: "streamId", Description: tmmsgs.APIParamStreamID}, }, QueryParams: nil, - Description: tmmsgs.APIEndpointPatchEventStream, + Description: tmmsgs.APIEndpointDeleteEventStream, JSONInputValue: nil, JSONOutputValue: nil, JSONOutputCodes: []int{http.StatusNoContent}, diff --git a/pkg/fftm/route_delete_eventstream_test.go b/pkg/fftm/route_delete_eventstream_test.go index b792c6fe..e6d94d89 100644 --- a/pkg/fftm/route_delete_eventstream_test.go +++ b/pkg/fftm/route_delete_eventstream_test.go @@ -42,14 +42,14 @@ func TestDeleteEventStream(t *testing.T) { SetResult(&es). Post(url + "/eventstreams") assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) // Then delete it res, err = resty.New().R(). SetResult(&es). Delete(url + "/eventstreams/" + es.ID.String()) assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 204, res.StatusCode()) assert.Nil(t, m.eventStreams[(*es.ID)]) diff --git a/pkg/fftm/route_delete_subscription.go b/pkg/fftm/route_delete_subscription.go new file mode 100644 index 00000000..fd097996 --- /dev/null +++ b/pkg/fftm/route_delete_subscription.go @@ -0,0 +1,45 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var deleteSubscription = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "deleteSubscription", + Path: "/subscriptions/{listenerId}", + Deprecated: true, // in favor of "/eventstreams/{streamId}/listeners/{listenerId}" + Method: http.MethodDelete, + PathParams: []*ffapi.PathParam{ + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointDeleteSubscription, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusNoContent}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return nil, m.deleteListener(r.Req.Context(), "" /* no streamId on this path */, r.PP["listenerId"]) + }, + } +} diff --git a/pkg/fftm/route_delete_subscription_test.go b/pkg/fftm/route_delete_subscription_test.go new file mode 100644 index 00000000..213b3692 --- /dev/null +++ b/pkg/fftm/route_delete_subscription_test.go @@ -0,0 +1,62 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestDeleteSubscription(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + res, err = resty.New().R().Delete(url + "/subscriptions/" + l1.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 204, res.StatusCode()) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_get_eventstream.go b/pkg/fftm/route_get_eventstream.go index ed07830c..19694c98 100644 --- a/pkg/fftm/route_get_eventstream.go +++ b/pkg/fftm/route_get_eventstream.go @@ -33,7 +33,7 @@ var getEventStream = func(m *manager) *ffapi.Route { {Name: "streamId", Description: tmmsgs.APIParamStreamID}, }, QueryParams: nil, - Description: tmmsgs.APIEndpointPatchEventStream, + Description: tmmsgs.APIEndpointGetEventStream, JSONInputValue: nil, JSONOutputValue: func() interface{} { return &apitypes.EventStreamWithStatus{} }, JSONOutputCodes: []int{http.StatusOK}, diff --git a/pkg/fftm/route_get_eventstream_test.go b/pkg/fftm/route_get_eventstream_test.go index 24f7cc8f..a646807d 100644 --- a/pkg/fftm/route_get_eventstream_test.go +++ b/pkg/fftm/route_get_eventstream_test.go @@ -42,7 +42,7 @@ func TestGetEventStream(t *testing.T) { SetResult(&es). Post(url + "/eventstreams") assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) // Then get it var ess apitypes.EventStreamWithStatus @@ -50,7 +50,7 @@ func TestGetEventStream(t *testing.T) { SetResult(&ess). Get(url + "/eventstreams/" + es.ID.String()) assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) assert.Equal(t, es.ID, ess.ID) assert.Equal(t, apitypes.EventStreamStatusStarted, ess.Status) diff --git a/pkg/fftm/route_get_eventstreams.go b/pkg/fftm/route_get_eventstreams.go index 49e8f098..0cb27503 100644 --- a/pkg/fftm/route_get_eventstreams.go +++ b/pkg/fftm/route_get_eventstreams.go @@ -34,7 +34,7 @@ var getEventStreams = func(m *manager) *ffapi.Route { {Name: "limit", Description: tmmsgs.APIParamLimit}, {Name: "after", Description: tmmsgs.APIParamAfter}, }, - Description: tmmsgs.APIEndpointPatchEventStream, + Description: tmmsgs.APIEndpointGetEventStreams, JSONInputValue: nil, JSONOutputValue: func() interface{} { return []*apitypes.EventStream{} }, JSONOutputCodes: []int{http.StatusOK}, diff --git a/pkg/fftm/route_get_eventstreams_test.go b/pkg/fftm/route_get_eventstreams_test.go index 9633fae1..106d00dc 100644 --- a/pkg/fftm/route_get_eventstreams_test.go +++ b/pkg/fftm/route_get_eventstreams_test.go @@ -48,7 +48,7 @@ func TestGetEventStreams(t *testing.T) { SetResult(&ess). Get(url + "/eventstreams?limit=1&after=" + es1.ID.String()) assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) assert.Len(t, ess, 1) assert.Equal(t, es2.ID, ess[0].ID) diff --git a/pkg/fftm/route_get_subscription.go b/pkg/fftm/route_get_subscription.go new file mode 100644 index 00000000..68d47e05 --- /dev/null +++ b/pkg/fftm/route_get_subscription.go @@ -0,0 +1,45 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getSubscription = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getSubscription", + Path: "/subscriptions/{listenerId}", + Deprecated: true, // in favor of "/eventstreams/{streamId}/listeners/{listenerId}" + Method: http.MethodGet, + PathParams: []*ffapi.PathParam{ + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointGetSubscription, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getListener(r.Req.Context(), "" /* no streamId on this path */, r.PP["listenerId"]) + }, + } +} diff --git a/pkg/fftm/route_get_subscription_test.go b/pkg/fftm/route_get_subscription_test.go new file mode 100644 index 00000000..e2ac4ab7 --- /dev/null +++ b/pkg/fftm/route_get_subscription_test.go @@ -0,0 +1,67 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetSubscription(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetResult(&listener). + Get(url + "/subscriptions/" + l1.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_get_eventstreams copy.go b/pkg/fftm/route_get_subscriptions.go similarity index 87% rename from pkg/fftm/route_get_eventstreams copy.go rename to pkg/fftm/route_get_subscriptions.go index ee1beb25..b3bdca3f 100644 --- a/pkg/fftm/route_get_eventstreams copy.go +++ b/pkg/fftm/route_get_subscriptions.go @@ -35,12 +35,12 @@ var getSubscriptions = func(m *manager) *ffapi.Route { {Name: "limit", Description: tmmsgs.APIParamLimit}, {Name: "after", Description: tmmsgs.APIParamAfter}, }, - Description: tmmsgs.APIEndpointPatchEventStream, + Description: tmmsgs.APIEndpointGetSubscriptions, JSONInputValue: nil, - JSONOutputValue: func() interface{} { return []*apitypes.EventStream{} }, + JSONOutputValue: func() interface{} { return []*apitypes.Listener{} }, JSONOutputCodes: []int{http.StatusOK}, JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { - return m.getStreams(r.Req.Context(), r.QP["after"], r.QP["limit"]) + return m.getListeners(r.Req.Context(), r.QP["after"], r.QP["limit"]) }, } } diff --git a/pkg/fftm/route_get_subscriptions_test.go b/pkg/fftm/route_get_subscriptions_test.go index 5a04834a..0dcafc46 100644 --- a/pkg/fftm/route_get_subscriptions_test.go +++ b/pkg/fftm/route_get_subscriptions_test.go @@ -21,8 +21,12 @@ import ( "testing" "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestGetSubscriptions(t *testing.T) { @@ -33,24 +37,35 @@ func TestGetSubscriptions(t *testing.T) { err := m.Start() assert.NoError(t, err) - // Create 3 streams - var es1, es2, es3 apitypes.EventStream + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") assert.NoError(t, err) - res, err = resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream2")}).SetResult(&es2).Post(url + "/eventstreams") + + // Create some listeners + var l1, l2 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") assert.NoError(t, err) - res, err = resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream3")}).SetResult(&es3).Post(url + "/eventstreams") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener2", StreamID: es1.ID}).SetResult(&l2).Post(url + "/subscriptions") assert.NoError(t, err) // Then get it - var ess []*apitypes.EventStream + var listeners []*apitypes.Listener res, err = resty.New().R(). - SetResult(&ess). - Get(url + "/subscriptions?limit=1&after=" + es1.ID.String()) + SetResult(&listeners). + Get(url + "/subscriptions?limit=1&after=" + l1.ID.String()) assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) + + assert.Len(t, listeners, 1) + assert.Equal(t, l2.ID, listeners[0].ID) + assert.Equal(t, es1.ID, listeners[0].StreamID) - assert.Len(t, ess, 1) - assert.Equal(t, es2.ID, ess[0].ID) + mfc.AssertExpectations(t) } diff --git a/pkg/fftm/route_patch_eventstream_test.go b/pkg/fftm/route_patch_eventstream_test.go index 13adc50c..9633924e 100644 --- a/pkg/fftm/route_patch_eventstream_test.go +++ b/pkg/fftm/route_patch_eventstream_test.go @@ -42,7 +42,7 @@ func TestPatchEventStream(t *testing.T) { SetResult(&es). Post(url + "/eventstreams") assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) // Then update it res, err = resty.New().R(). @@ -52,7 +52,7 @@ func TestPatchEventStream(t *testing.T) { SetResult(&es). Patch(url + "/eventstreams/" + es.ID.String()) assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) assert.NotNil(t, es.ID) assert.NotNil(t, es.Created) assert.NotEqual(t, es.Created, es.Updated) diff --git a/pkg/fftm/route_patch_subscription.go b/pkg/fftm/route_patch_subscription.go new file mode 100644 index 00000000..3c8e5b61 --- /dev/null +++ b/pkg/fftm/route_patch_subscription.go @@ -0,0 +1,45 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var patchSubscription = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "patchSubscription", + Path: "/subscriptions/{listenerId}", + Deprecated: true, // in favor of "/eventstreams/{streamId}/listeners/{listenerId}" + Method: http.MethodPatch, + PathParams: []*ffapi.PathParam{ + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchSubscription, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.updateExistingListener(r.Req.Context(), "" /* no streamId on this path */, r.PP["listenerId"], r.Input.(*apitypes.Listener)) + }, + } +} diff --git a/pkg/fftm/route_patch_subscription_test.go b/pkg/fftm/route_patch_subscription_test.go new file mode 100644 index 00000000..fd33363c --- /dev/null +++ b/pkg/fftm/route_patch_subscription_test.go @@ -0,0 +1,71 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPatchSubscription(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetBody(&apitypes.Listener{ + Name: "listener1a", + }). + SetResult(&listener). + Patch(url + "/subscriptions/" + l1.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + assert.Equal(t, "listener1a", listener.Name) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_post_eventstream_resume_test.go b/pkg/fftm/route_post_eventstream_resume_test.go index d8467f21..50ad56a3 100644 --- a/pkg/fftm/route_post_eventstream_resume_test.go +++ b/pkg/fftm/route_post_eventstream_resume_test.go @@ -44,7 +44,7 @@ func TestPostEventStreamResume(t *testing.T) { SetResult(&es). Post(url + "/eventstreams") assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) // Then suspend it res, err = resty.New().R(). @@ -52,7 +52,7 @@ func TestPostEventStreamResume(t *testing.T) { SetResult(&es). Post(url + "/eventstreams/" + es.ID.String() + "/resume") assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) assert.Equal(t, apitypes.EventStreamStatusStarted, m.eventStreams[(*es.ID)].Status()) diff --git a/pkg/fftm/route_post_eventstream_suspend_test.go b/pkg/fftm/route_post_eventstream_suspend_test.go index 05a704e7..3ab8a739 100644 --- a/pkg/fftm/route_post_eventstream_suspend_test.go +++ b/pkg/fftm/route_post_eventstream_suspend_test.go @@ -42,7 +42,7 @@ func TestPostEventStreamSuspend(t *testing.T) { SetResult(&es). Post(url + "/eventstreams") assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) // Then suspend it res, err = resty.New().R(). @@ -50,7 +50,7 @@ func TestPostEventStreamSuspend(t *testing.T) { SetResult(&es). Post(url + "/eventstreams/" + es.ID.String() + "/suspend") assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) assert.Equal(t, apitypes.EventStreamStatusStopped, m.eventStreams[(*es.ID)].Status()) diff --git a/pkg/fftm/route_post_eventstream_test.go b/pkg/fftm/route_post_eventstream_test.go index 56f4247a..43ede403 100644 --- a/pkg/fftm/route_post_eventstream_test.go +++ b/pkg/fftm/route_post_eventstream_test.go @@ -41,7 +41,7 @@ func TestPostNewEventStream(t *testing.T) { SetResult(&es). Post(url + "/eventstreams") assert.NoError(t, err) - assert.True(t, res.IsSuccess()) + assert.Equal(t, 200, res.StatusCode()) assert.NotNil(t, es.ID) assert.NotNil(t, es.Created) assert.Equal(t, es.Created, es.Updated) diff --git a/pkg/fftm/route_post_subscriptions.go b/pkg/fftm/route_post_subscriptions.go new file mode 100644 index 00000000..acf6bc5f --- /dev/null +++ b/pkg/fftm/route_post_subscriptions.go @@ -0,0 +1,46 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postSubscriptions = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "posSubscriptions", + Path: "/subscriptions", + Deprecated: true, // in favor of "/eventstreams/{id}/listeners" + Method: http.MethodPost, + PathParams: nil, + QueryParams: []*ffapi.QueryParam{ + {Name: "limit", Description: tmmsgs.APIParamLimit}, + {Name: "after", Description: tmmsgs.APIParamAfter}, + }, + Description: tmmsgs.APIEndpointPostSubscriptions, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.createAndStoreNewListener(r.Req.Context(), r.Input.(*apitypes.Listener)) + }, + } +} diff --git a/pkg/fftm/route_post_subscriptions_test.go b/pkg/fftm/route_post_subscriptions_test.go new file mode 100644 index 00000000..c193fd05 --- /dev/null +++ b/pkg/fftm/route_post_subscriptions_test.go @@ -0,0 +1,59 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostSubscriptions(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + // Create a listener + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/routes.go b/pkg/fftm/routes.go index cee231b6..e2f14234 100644 --- a/pkg/fftm/routes.go +++ b/pkg/fftm/routes.go @@ -21,11 +21,16 @@ import "github.com/hyperledger/firefly-common/pkg/ffapi" func (m *manager) routes() []*ffapi.Route { return []*ffapi.Route{ deleteEventStream(m), + deleteSubscription(m), getEventStream(m), getEventStreams(m), + getSubscription(m), + getSubscriptions(m), patchEventStream(m), + patchSubscription(m), postEventStream(m), postEventStreamResume(m), postEventStreamSuspend(m), + postSubscriptions(m), } } diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index 774fbb27..ad0d53fa 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -125,6 +125,9 @@ func (m *manager) deleteStream(ctx context.Context, idStr string) error { m.mux.Lock() s := m.eventStreams[*id] delete(m.eventStreams, *id) + if s != nil { + delete(m.streamsByName, *s.Spec().Name) + } m.mux.Unlock() if s != nil { if err := m.deleteAllStreamListeners(ctx, id); err != nil { @@ -138,9 +141,51 @@ func (m *manager) deleteStream(ctx context.Context, idStr string) error { return nil } +func (m *manager) reserveStreamName(ctx context.Context, name string, id *fftypes.UUID) (func(bool), error) { + + m.mux.Lock() + defer m.mux.Unlock() + + oldName := "" + s := m.eventStreams[*id] + if s != nil { + oldName = *s.Spec().Name + } + existing := m.streamsByName[name] + if existing != nil { + if !existing.Equals(id) { + return nil, i18n.NewError(ctx, tmmsgs.MsgDuplicateStreamName, name, existing) + } + } + m.streamsByName[name] = id + + return func(succeeded bool) { + // Release the name on failure, but only if it wasn't existing + if !succeeded && (existing == nil) { + m.mux.Lock() + delete(m.streamsByName, name) + m.mux.Unlock() + } else if succeeded && oldName != name { + // Delete the old name on success + delete(m.streamsByName, oldName) + } + }, nil +} + func (m *manager) createAndStoreNewStream(ctx context.Context, def *apitypes.EventStream) (*apitypes.EventStream, error) { - def.ID = nil // set by addRuntimeStream - def.Created = nil + def.ID = apitypes.UUIDVersion1() + def.Created = nil // set to updated time by events.NewEventStream + if def.Name == nil || *def.Name == "" { + return nil, i18n.NewError(ctx, tmmsgs.MsgMissingName) + } + + stored := false + closeoutName, err := m.reserveStreamName(ctx, *def.Name, def.ID) + if err != nil { + return nil, err + } + defer func() { closeoutName(stored) }() + s, err := m.addRuntimeStream(def) if err != nil { return nil, err @@ -152,6 +197,7 @@ func (m *manager) createAndStoreNewStream(ctx context.Context, def *apitypes.Eve log.L(ctx).Infof("Cleaned up runtime stream after write failed (err?=%v)", err1) return nil, err } + stored = true if !*spec.Suspended { if err = s.Start(ctx); err != nil { return nil, err @@ -160,6 +206,64 @@ func (m *manager) createAndStoreNewStream(ctx context.Context, def *apitypes.Eve return spec, nil } +func (m *manager) createAndStoreNewListener(ctx context.Context, def *apitypes.Listener) (*apitypes.Listener, error) { + def.ID = nil // set by AddOrUpdateListener + def.Created = nil + return m.createOrUpdateListener(ctx, def) +} + +func (m *manager) updateExistingListener(ctx context.Context, streamIDStr, listenerIDStr string, updates *apitypes.Listener) (*apitypes.Listener, error) { + l, err := m.getListener(ctx, streamIDStr, listenerIDStr) // Verify the listener exists in storage + if err != nil { + return nil, err + } + updates.ID = l.ID + updates.StreamID = l.StreamID + l, err = m.createOrUpdateListener(ctx, updates) + if err != nil { + return nil, err + } + return l, err +} + +func (m *manager) createOrUpdateListener(ctx context.Context, def *apitypes.Listener) (*apitypes.Listener, error) { + var s events.Stream + if def.StreamID != nil { + m.mux.Lock() + s = m.eventStreams[*def.StreamID] + m.mux.Unlock() + } + if s == nil { + return nil, i18n.NewError(ctx, tmmsgs.MsgStreamNotFound, def.StreamID) + } + if err := s.AddOrUpdateListener(ctx, def); err != nil { + return nil, err + } + if err := m.persistence.WriteListener(ctx, def); err != nil { + err1 := s.RemoveListener(ctx, def.ID) + log.L(ctx).Infof("Cleaned up runtime listener after write failed (err?=%v)", err1) + return nil, err + } + return def, nil +} + +func (m *manager) deleteListener(ctx context.Context, streamIDStr, listenerIDStr string) error { + spec, err := m.getListener(ctx, streamIDStr, listenerIDStr) // Verify the listener exists in storage + if err != nil { + return err + } + m.mux.Lock() + s := m.eventStreams[*spec.StreamID] + m.mux.Unlock() + if s == nil { + return i18n.NewError(ctx, tmmsgs.MsgStreamNotFound, spec.StreamID) + } + if err := s.RemoveListener(ctx, spec.ID); err != nil { + return err + } + return m.persistence.DeleteListener(ctx, spec.ID) +} + func (m *manager) updateStream(ctx context.Context, idStr string, updates *apitypes.EventStream) (*apitypes.EventStream, error) { id, err := fftypes.ParseUUID(ctx, idStr) if err != nil { @@ -169,8 +273,18 @@ func (m *manager) updateStream(ctx context.Context, idStr string, updates *apity s := m.eventStreams[*id] m.mux.Unlock() if s == nil { - return nil, i18n.NewError(ctx, i18n.Msg404NotFound) + return nil, i18n.NewError(ctx, tmmsgs.MsgStreamNotFound, id) + } + + nameChanged := false + if updates.Name != nil && *updates.Name != "" { + closeoutName, err := m.reserveStreamName(ctx, *updates.Name, id) + if err != nil { + return nil, err + } + defer func() { closeoutName(nameChanged) }() } + err = s.UpdateSpec(ctx, updates) if err != nil { return nil, err @@ -180,6 +294,8 @@ func (m *manager) updateStream(ctx context.Context, idStr string, updates *apity if err != nil { return nil, err } + nameChanged = true + // We might need to start or stop if *spec.Suspended && s.Status() != apitypes.EventStreamStatusStopped { if err = s.Stop(ctx); err != nil { @@ -232,22 +348,46 @@ func (m *manager) getStreams(ctx context.Context, afterStr, limitStr string) (st return m.persistence.ListStreams(ctx, after, limit) } -func (m *manager) getListeners(ctx context.Context, afterStr, limitStr string) (streams []*apitypes.Listener, err error) { - after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) +func (m *manager) getListener(ctx context.Context, streamIDStr, listenerIDStr string) (spec *apitypes.Listener, err error) { + var streamID *fftypes.UUID + if streamIDStr != "" { + streamID, err = fftypes.ParseUUID(ctx, streamIDStr) + if err != nil { + return nil, err + } + + } + listenerID, err := fftypes.ParseUUID(ctx, listenerIDStr) if err != nil { return nil, err } - return m.persistence.ListListeners(ctx, after, limit) -} - -func (m *manager) getStreamListeners(ctx context.Context, afterStr, limitStr, idStr string) (streams []*apitypes.Listener, err error) { - after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) + spec, err = m.persistence.GetListener(ctx, listenerID) if err != nil { return nil, err } - id, err := fftypes.ParseUUID(ctx, idStr) + // Check we found the listener, and it's owned by the correct stream ID (if we're on a path that specifies a stream ID) + if spec == nil || (streamID != nil && !streamID.Equals(spec.StreamID)) { + return nil, i18n.NewError(ctx, tmmsgs.MsgListenerNotFound, listenerID) + } + return spec, nil +} + +func (m *manager) getListeners(ctx context.Context, afterStr, limitStr string) (streams []*apitypes.Listener, err error) { + after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) if err != nil { return nil, err } - return m.persistence.ListStreamListeners(ctx, after, limit, id) + return m.persistence.ListListeners(ctx, after, limit) } + +// func (m *manager) getStreamListeners(ctx context.Context, afterStr, limitStr, idStr string) (streams []*apitypes.Listener, err error) { +// after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) +// if err != nil { +// return nil, err +// } +// id, err := fftypes.ParseUUID(ctx, idStr) +// if err != nil { +// return nil, err +// } +// return m.persistence.ListStreamListeners(ctx, after, limit, id) +// } From 57cdfd08bfab249dc6ca635ef9fc4fe7d71be4e0 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sat, 18 Jun 2022 13:55:50 -0400 Subject: [PATCH 19/95] Listener routes, and full Swagger gen for base commands Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 2 + go.mod | 2 +- go.sum | 2 + internal/tmmsgs/en_api_descriptions.go | 30 +++++--- internal/tmmsgs/en_error_messges.go | 5 +- pkg/apitypes/base_request.go | 38 ++++++++++ pkg/apitypes/base_request_test.go | 58 ++++++++++++++ pkg/{policyengine => apitypes}/tx_request.go | 8 +- pkg/fftm/api.go | 50 ------------ pkg/fftm/api_test.go | 26 ++++++- pkg/fftm/manager.go | 2 +- pkg/fftm/manager_test.go | 33 ++++++-- pkg/fftm/nonces_test.go | 5 +- pkg/fftm/policyloop_test.go | 13 ++-- pkg/fftm/route__root_command.go | 67 ++++++++++++++++ pkg/fftm/route_delete_eventstream_listener.go | 45 +++++++++++ .../route_delete_eventstream_listener_test.go | 63 +++++++++++++++ pkg/fftm/route_get_eventstream_listener.go | 45 +++++++++++ .../route_get_eventstream_listener_test.go | 68 +++++++++++++++++ pkg/fftm/route_get_eventstream_listeners.go | 47 ++++++++++++ .../route_get_eventstream_listeners_test.go | 76 +++++++++++++++++++ pkg/fftm/route_patch_eventstream_listener.go | 45 +++++++++++ .../route_patch_eventstream_listener_test.go | 72 ++++++++++++++++++ pkg/fftm/route_post_eventstream_listeners.go | 47 ++++++++++++ .../route_post_eventstream_listeners_test.go | 60 +++++++++++++++ pkg/fftm/route_post_subscriptions.go | 2 +- pkg/fftm/routes.go | 6 ++ pkg/fftm/send_tx.go | 3 +- pkg/fftm/stream_management.go | 31 +++++--- pkg/policyengine/managed_tx.go | 3 +- .../simple/simple_policy_engine_test.go | 25 +++--- 31 files changed, 867 insertions(+), 112 deletions(-) create mode 100644 pkg/apitypes/base_request.go create mode 100644 pkg/apitypes/base_request_test.go rename pkg/{policyengine => apitypes}/tx_request.go (86%) create mode 100644 pkg/fftm/route__root_command.go create mode 100644 pkg/fftm/route_delete_eventstream_listener.go create mode 100644 pkg/fftm/route_delete_eventstream_listener_test.go create mode 100644 pkg/fftm/route_get_eventstream_listener.go create mode 100644 pkg/fftm/route_get_eventstream_listener_test.go create mode 100644 pkg/fftm/route_get_eventstream_listeners.go create mode 100644 pkg/fftm/route_get_eventstream_listeners_test.go create mode 100644 pkg/fftm/route_patch_eventstream_listener.go create mode 100644 pkg/fftm/route_patch_eventstream_listener_test.go create mode 100644 pkg/fftm/route_post_eventstream_listeners.go create mode 100644 pkg/fftm/route_post_eventstream_listeners_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 533da0c2..36a6df06 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,6 +36,7 @@ "ffresty", "ffstruct", "fftm", + "fftmrequest", "fftypes", "finalizers", "GJSON", @@ -51,6 +52,7 @@ "NATS", "Nowarn", "oapispec", + "openapi", "optype", "persistencemocks", "policyengine", diff --git a/go.mod b/go.mod index 4f6cd673..52ae4102 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru v0.5.4 github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e - github.com/hyperledger/firefly-common v0.1.8 + github.com/hyperledger/firefly-common v0.1.11-0.20220618173558-a825551beb7a github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.1 diff --git a/go.sum b/go.sum index 8e7ef378..14e80762 100644 --- a/go.sum +++ b/go.sum @@ -633,6 +633,8 @@ github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e/go.mod h1:43 github.com/hyperledger/firefly-common v0.1.2/go.mod h1:ytB+404+Qg00wJKx602Yi/V6Spx9d0g65lZR9y5fzlo= github.com/hyperledger/firefly-common v0.1.8 h1:gP4waMEEkbhB+pbsJyp5VrfrT4jQad6/4TXnXTfqXI8= github.com/hyperledger/firefly-common v0.1.8/go.mod h1:MYL6Dbj3KqM/79IkS+mCzJ7wRguNbd/PKdVu8aXo5TI= +github.com/hyperledger/firefly-common v0.1.11-0.20220618173558-a825551beb7a h1:JmwLgtI++IyC8piQEMcT5/efp7Pwl4s5/8KXnXLltpA= +github.com/hyperledger/firefly-common v0.1.11-0.20220618173558-a825551beb7a/go.mod h1:MYL6Dbj3KqM/79IkS+mCzJ7wRguNbd/PKdVu8aXo5TI= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= diff --git a/internal/tmmsgs/en_api_descriptions.go b/internal/tmmsgs/en_api_descriptions.go index c383fdd9..71231071 100644 --- a/internal/tmmsgs/en_api_descriptions.go +++ b/internal/tmmsgs/en_api_descriptions.go @@ -27,18 +27,24 @@ var ffm = func(key, translation string) i18n.MessageKey { //revive:disable var ( - APIEndpointPostEventStream = ffm("api.endpoints.post.eventstreams", "Create a new event stream") - APIEndpointPatchEventStream = ffm("api.endpoints.patch.eventstreams", "Update an existing event stream") - APIEndpointPostEventStreamSuspend = ffm("api.endpoints.post.eventstream.suspend", "Suspend an event stream") - APIEndpointPostEventStreamResume = ffm("api.endpoints.post.eventstream.resume", "Resume an event stream") - APIEndpointGetEventStreams = ffm("api.endpoints.get.eventstreams", "List event streams") - APIEndpointGetEventStream = ffm("api.endpoints.get.eventstream", "Get an event stream with status") - APIEndpointDeleteEventStream = ffm("api.endpoints.delete.eventstream", "Delete an event stream") - APIEndpointGetSubscriptions = ffm("api.endpoints.get.subscriptions", "Get listeners - route deprecated in favor of /eventstreams/{streamId}/listeners") - APIEndpointGetSubscription = ffm("api.endpoints.get.subscription", "Get listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") - APIEndpointPostSubscriptions = ffm("api.endpoints.post.subscriptions", "Create new listener - route deprecated in favor of /eventstreams/{streamId}/listeners") - APIEndpointPatchSubscription = ffm("api.endpoints.patch.subscription", "Update listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") - APIEndpointDeleteSubscription = ffm("api.endpoints.delete.subscription", "Delete listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointPostRoot = ffm("api.endpoints.post.root", "") + APIEndpointPostEventStream = ffm("api.endpoints.post.eventstreams", "Create a new event stream") + APIEndpointPatchEventStream = ffm("api.endpoints.patch.eventstreams", "Update an existing event stream") + APIEndpointPostEventStreamSuspend = ffm("api.endpoints.post.eventstream.suspend", "Suspend an event stream") + APIEndpointPostEventStreamResume = ffm("api.endpoints.post.eventstream.resume", "Resume an event stream") + APIEndpointGetEventStreams = ffm("api.endpoints.get.eventstreams", "List event streams") + APIEndpointGetEventStream = ffm("api.endpoints.get.eventstream", "Get an event stream with status") + APIEndpointDeleteEventStream = ffm("api.endpoints.delete.eventstream", "Delete an event stream") + APIEndpointGetSubscriptions = ffm("api.endpoints.get.subscriptions", "Get listeners - route deprecated in favor of /eventstreams/{streamId}/listeners") + APIEndpointGetSubscription = ffm("api.endpoints.get.subscription", "Get listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointPostSubscriptions = ffm("api.endpoints.post.subscriptions", "Create new listener - route deprecated in favor of /eventstreams/{streamId}/listeners") + APIEndpointPatchSubscription = ffm("api.endpoints.patch.subscription", "Update listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointDeleteSubscription = ffm("api.endpoints.delete.subscription", "Delete listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointGetEventStreamListeners = ffm("api.endpoints.get.eventstream.listeners", "List event stream listeners") + APIEndpointGetEventStreamListener = ffm("api.endpoints.get.eventstream.listener", "Get event stream listener") + APIEndpointPostEventStreamListener = ffm("api.endpoints.post.eventstream.listener", "Create event stream listener") + APIEndpointPatchEventStreamListener = ffm("api.endpoints.patch.eventstream.listener", "Update event stream listener") + APIEndpointDeleteEventStreamListener = ffm("api.endpoints.delete.eventstream.listener", "Delete event stream listener") APIParamStreamID = ffm("api.params.streamId", "Event Stream ID") APIParamListenerID = ffm("api.params.listenerId", "Listener ID") diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 0b779e66..04312ad2 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -29,7 +29,7 @@ var ffe = func(key, translation string, statusHint ...int) i18n.ErrorMessageKey var ( MsgInvalidOutputType = ffe("FF21010", "Invalid output type: %s") MsgConnectorError = ffe("FF21012", "Connector failed request. requestId=%s reason=%s error: %s") - MsgConnectorInvalidConentType = ffe("FF21013", "Connector failed request. requestId=%s invalid response content type: %s") + MsgConnectorInvalidContentType = ffe("FF21013", "Connector failed request. requestId=%s invalid response content type: %s") MsgCacheInitFail = ffe("FF21015", "Failed to initialize cache") MsgInvalidConfirmationRequest = ffe("FF21016", "Invalid confirmation request %+v") MsgCoreError = ffe("FF21017", "Error from core status=%d: %s") @@ -37,7 +37,7 @@ var ( MsgPolicyEngineNotRegistered = ffe("FF21019", "No policy engine registered with name '%s'") MsgNoGasConfigSetForPolicyEngine = ffe("FF21020", "A fixed gas price must be set when not using a gas oracle") MsgErrorQueryingGasOracleAPI = ffe("FF21021", "Error from gas station API [%d]: %s") - MsgErrorInvalidRequest = ffe("FF21022", "Invalid request", 400) + MsgInvalidRequestErr = ffe("FF21022", "Invalid '%s' request: %s", 400) MsgUnsupportedRequestType = ffe("FF21023", "Unsupported request type: %s", 400) MsgMissingGOTemplate = ffe("FF21024", "Missing template for processing response from Gas Oracle REST API") MsgBadGOTemplate = ffe("FF21025", "Invalid Go template: %s") @@ -64,4 +64,5 @@ var ( MsgListenerNotFound = ffe("FF21046", "Event listener '%v' not found") MsgDuplicateStreamName = ffe("FF21047", "Duplicate event stream name '%s' used by stream '%s'", 409) MsgMissingID = ffe("FF21048", "ID is required", 400) + MsgPersistenceInitFail = ffe("FF21049", "Failed to initialize '%s' persistence: %s") ) diff --git a/pkg/apitypes/base_request.go b/pkg/apitypes/base_request.go new file mode 100644 index 00000000..2474939b --- /dev/null +++ b/pkg/apitypes/base_request.go @@ -0,0 +1,38 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 apitypes + +import "encoding/json" + +// BaseRequest is the common headers to all requests, and captures the full input payload for later decoding to a specific type +type BaseRequest struct { + headerDecoder + fullPayload []byte +} + +type headerDecoder struct { + Headers RequestHeaders `json:"headers"` +} + +func (br *BaseRequest) UnmarshalJSON(data []byte) error { + br.fullPayload = data + return json.Unmarshal(data, &br.headerDecoder) +} + +func (br *BaseRequest) UnmarshalTo(o interface{}) error { + return json.Unmarshal(br.fullPayload, &o) +} diff --git a/pkg/apitypes/base_request_test.go b/pkg/apitypes/base_request_test.go new file mode 100644 index 00000000..05e4713a --- /dev/null +++ b/pkg/apitypes/base_request_test.go @@ -0,0 +1,58 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 apitypes + +import ( + "encoding/json" + "testing" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" +) + +func TestBaseRequestDecoding(t *testing.T) { + + sampleRequest := &TransactionRequest{ + Headers: RequestHeaders{ + Type: RequestTypeSendTransaction, + ID: fftypes.NewUUID().String(), + }, + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + }, + } + + j, err := json.Marshal(&sampleRequest) + assert.NoError(t, err) + + var br BaseRequest + err = json.Unmarshal(j, &br) + assert.NoError(t, err) + + assert.Equal(t, RequestTypeSendTransaction, br.Headers.Type) + assert.Equal(t, sampleRequest.Headers.ID, br.Headers.ID) + + var receivedRequest TransactionRequest + err = br.UnmarshalTo(&receivedRequest) + assert.NoError(t, err) + + assert.Equal(t, "0x12345", receivedRequest.TransactionInput.From) + +} diff --git a/pkg/policyengine/tx_request.go b/pkg/apitypes/tx_request.go similarity index 86% rename from pkg/policyengine/tx_request.go rename to pkg/apitypes/tx_request.go index 131d3674..50bb9537 100644 --- a/pkg/policyengine/tx_request.go +++ b/pkg/apitypes/tx_request.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package policyengine +package apitypes import ( "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -28,13 +28,13 @@ type TransactionRequest struct { } type RequestHeaders struct { - ID string `json:"id"` + ID string `ffstruct:"fftmrequest" json:"id"` Type RequestType `json:"type"` } type RequestType string const ( - RequestTypeSendTransaction = "SendTransaction" - RequestTypeQuery = "Query" + RequestTypeSendTransaction RequestType = "SendTransaction" + RequestTypeQuery RequestType = "Query" ) diff --git a/pkg/fftm/api.go b/pkg/fftm/api.go index 6b71d39e..5f492175 100644 --- a/pkg/fftm/api.go +++ b/pkg/fftm/api.go @@ -17,21 +17,15 @@ package fftm import ( - "context" "encoding/json" "net/http" - "strconv" "github.com/ghodss/yaml" "github.com/gorilla/mux" "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/ffapi" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" - "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" - "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) func (m *manager) router() *mux.Router { @@ -71,7 +65,6 @@ func (m *manager) router() *mux.Router { b, _ := json.Marshal(&doc) _, _ = res.Write(b) })) - mux.Path("/").Methods(http.MethodPost).Handler(http.HandlerFunc(m.apiHandler)) mux.NotFoundHandler = hf.APIWrapper(func(res http.ResponseWriter, req *http.Request) (status int, err error) { return 404, i18n.NewError(req.Context(), i18n.Msg404NotFound) }) @@ -81,46 +74,3 @@ func (m *manager) router() *mux.Router { func (m *manager) runAPIServer() { m.apiServer.ServeHTTP(m.ctx) } - -func (m *manager) validateRequest(ctx context.Context, tReq *policyengine.TransactionRequest) error { - if tReq == nil || tReq.Headers.ID == "" || tReq.Headers.Type == "" { - log.L(ctx).Warnf("Invalid request: %+v", tReq) - return i18n.NewError(ctx, tmmsgs.MsgErrorInvalidRequest) - } - return nil -} - -func (m *manager) apiHandler(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - var tReq *policyengine.TransactionRequest - statusCode := 200 - err := json.NewDecoder(r.Body).Decode(&tReq) - if err == nil { - err = m.validateRequest(ctx, tReq) - } - var resBody interface{} - if err != nil { - statusCode = 400 - } else { - ctx = log.WithLogField(ctx, "requestId", tReq.Headers.ID) - switch tReq.Headers.Type { - case policyengine.RequestTypeSendTransaction: - resBody, err = m.sendManagedTransaction(ctx, tReq) - default: - err = i18n.NewError(ctx, tmmsgs.MsgUnsupportedRequestType, tReq.Headers.Type) - statusCode = 400 - } - } - if err != nil { - log.L(ctx).Errorf("Request failed: %s", err) - resBody = &fftypes.RESTError{Error: err.Error()} - if statusCode < 400 { - statusCode = 500 - } - } - w.Header().Set("Content-Type", "application/json") - resBytes, _ := json.Marshal(&resBody) - w.Header().Set("Content-Length", strconv.FormatInt(int64(len(resBytes)), 10)) - w.WriteHeader(statusCode) - _, _ = w.Write(resBytes) -} diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go index f00e6edc..3d9d1ea6 100644 --- a/pkg/fftm/api_test.go +++ b/pkg/fftm/api_test.go @@ -125,7 +125,7 @@ func TestSendTransactionE2E(t *testing.T) { } -func TestSendInvalidRequestNoHeaders(t *testing.T) { +func TestSendInvalidRequestBadTXType(t *testing.T) { url, m, cancel := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}, @@ -134,7 +134,12 @@ func TestSendInvalidRequestNoHeaders(t *testing.T) { m.Start() req := strings.NewReader(`{ - "noHeaders": true + "headers": { + "type": "SendTransaction" + }, + "from": { + "Not": "a string" + } }`) var errRes fftypes.RESTError res, err := resty.New().R(). @@ -257,3 +262,20 @@ func TestSendTransactionUpdateFireFlyFail(t *testing.T) { assert.Equal(t, 500, res.StatusCode()) } + +func TestNotFound(t *testing.T) { + + url, m, cancel := newTestManager(t, + func(w http.ResponseWriter, r *http.Request) {}, + ) + defer cancel() + m.Start() + + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetError(&errRes). + Post(url + "/not found") + assert.NoError(t, err) + assert.Equal(t, 404, res.StatusCode()) + assert.Regexp(t, "FF00167", errRes.Error) +} diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index 65caba2a..5df0e677 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -155,7 +155,7 @@ func (m *manager) initPersistence(ctx context.Context) (err error) { switch pType { case "leveldb": if m.persistence, err = persistence.NewLevelDBPersistence(ctx); err != nil { - return err + return i18n.NewError(ctx, tmmsgs.MsgPersistenceInitFail, pType, err) } return nil default: diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index d1ce16fd..5f8a623c 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -36,6 +36,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" @@ -128,11 +129,31 @@ func TestNewManagerBadHttpConfig(t *testing.T) { } +func TestNewManagerBadLevelDBConfig(t *testing.T) { + + tmpFile, err := ioutil.TempFile("", "ut-*") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + tmconfig.Reset() + config.Set(tmconfig.ManagerName, "test") + config.Set(tmconfig.PersistenceLevelDBPath, tmpFile.Name) + tmconfig.APIConfig.Set(httpserver.HTTPConfPort, "0") + + policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") + + _, err = NewManager(context.Background(), nil) + assert.Regexp(t, "FF21049", err) + +} + func TestNewManagerBadPersistenceConfig(t *testing.T) { tmconfig.Reset() config.Set(tmconfig.ManagerName, "test") config.Set(tmconfig.PersistenceType, "wrong") + tmconfig.APIConfig.Set(httpserver.HTTPConfPort, "0") policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") @@ -229,7 +250,7 @@ func TestChangeEventsWrongName(t *testing.T) { b, err := json.Marshal(newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + ce.ID.String(), FFTMName: "wrong", - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending)) assert.NoError(t, err) w.Header().Set("Content-Type", "application/json") @@ -261,7 +282,7 @@ func TestChangeEventsWrongID(t *testing.T) { op := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + ce.ID.String(), FFTMName: testManagerName, - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) op.ID = fftypes.NewUUID() b, err := json.Marshal(&op) @@ -351,7 +372,7 @@ func TestChangeEventsMarkForCleanup(t *testing.T) { op := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + ce.ID.String(), FFTMName: testManagerName, - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, }, core.OpStatusFailed) var m *manager @@ -379,21 +400,21 @@ func TestStartupScanMultiPageOK(t *testing.T) { op1 := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), FFTMName: testManagerName, - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) t1 := fftypes.FFTime(time.Now().Add(-10 * time.Minute)) op1.Created = &t1 op2 := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), FFTMName: testManagerName, - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) t2 := fftypes.FFTime(time.Now().Add(-5 * time.Minute)) op2.Created = &t2 op3 := newTestOperation(t, &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), FFTMName: testManagerName, - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) t3 := fftypes.FFTime(time.Now().Add(-1 * time.Minute)) op3.Created = &t3 diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go index 3327090e..8a33fc98 100644 --- a/pkg/fftm/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -25,6 +25,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/stretchr/testify/assert" @@ -62,7 +63,7 @@ func TestNonceCached(t *testing.T) { ln.spent = &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x12345", @@ -102,7 +103,7 @@ func TestNonceError(t *testing.T) { mFFC.On("NextNonceForSigner", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) - _, err := m.sendManagedTransaction(context.Background(), &policyengine.TransactionRequest{ + _, err := m.sendManagedTransaction(context.Background(), &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x12345", diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index c5f31656..ed43dbd1 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -27,6 +27,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" @@ -45,7 +46,7 @@ func TestPolicyLoopE2EOk(t *testing.T) { ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, } _, m, cancel := newTestManager(t, @@ -92,7 +93,7 @@ func TestPolicyLoopE2EOkReverted(t *testing.T) { ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, } _, m, cancel := newTestManager(t, @@ -139,7 +140,7 @@ func TestPolicyLoopUpdateFFCoreWithError(t *testing.T) { ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, } _, m, cancel := newTestManager(t, @@ -171,7 +172,7 @@ func TestPolicyLoopUpdateOpFail(t *testing.T) { ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, } _, m, cancel := newTestManager(t, @@ -214,7 +215,7 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { mtx := &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, } opUpdateCount := 0 @@ -288,7 +289,7 @@ func TestPolicyLoopCycleCleanupRemoved(t *testing.T) { mtx := &policyengine.ManagedTXOutput{ ID: "ns1:" + fftypes.NewUUID().String(), - Request: &policyengine.TransactionRequest{}, + Request: &apitypes.TransactionRequest{}, } _, m, cancel := newTestManager(t, diff --git a/pkg/fftm/route__root_command.go b/pkg/fftm/route__root_command.go new file mode 100644 index 00000000..ffd49530 --- /dev/null +++ b/pkg/fftm/route__root_command.go @@ -0,0 +1,67 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "context" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postRootCommand = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postRootCommand", + Path: "/", + Method: http.MethodPost, + PathParams: nil, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostSubscriptions, + JSONInputValue: func() interface{} { return &apitypes.BaseRequest{} }, + JSONOutputValue: func() interface{} { return map[string]interface{}{} }, + JSONInputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) { + schemas := []*openapi3.SchemaRef{} + sendTxSchema, err := schemaGen(&apitypes.TransactionRequest{}) + if err == nil { + schemas = append(schemas, sendTxSchema) + } + return &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + AnyOf: schemas, + }, + }, err + }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + baseReq := r.Input.(*apitypes.BaseRequest) + switch baseReq.Headers.Type { + case apitypes.RequestTypeSendTransaction: + var tReq apitypes.TransactionRequest + if err = baseReq.UnmarshalTo(&tReq); err != nil { + return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) + } + return m.sendManagedTransaction(r.Req.Context(), &tReq) + default: + return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgUnsupportedRequestType, baseReq.Headers.Type) + } + }, + } +} diff --git a/pkg/fftm/route_delete_eventstream_listener.go b/pkg/fftm/route_delete_eventstream_listener.go new file mode 100644 index 00000000..7dc890d8 --- /dev/null +++ b/pkg/fftm/route_delete_eventstream_listener.go @@ -0,0 +1,45 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var deleteEventStreamListener = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "deleteEventStreamListener", + Path: "/eventstreams/{streamId}/listeners/{listenerId}", + Method: http.MethodDelete, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointDeleteEventStreamListener, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusNoContent}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return nil, m.deleteListener(r.Req.Context(), r.PP["streamId"], r.PP["listenerId"]) + }, + } +} diff --git a/pkg/fftm/route_delete_eventstream_listener_test.go b/pkg/fftm/route_delete_eventstream_listener_test.go new file mode 100644 index 00000000..39133161 --- /dev/null +++ b/pkg/fftm/route_delete_eventstream_listener_test.go @@ -0,0 +1,63 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestDeleteEventStreamListener(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + res, err = resty.New().R().Delete(fmt.Sprintf("%s/eventstreams/%s/listeners/%s", url, es1.ID, l1.ID)) + assert.NoError(t, err) + assert.Equal(t, 204, res.StatusCode()) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_get_eventstream_listener.go b/pkg/fftm/route_get_eventstream_listener.go new file mode 100644 index 00000000..2e80ea76 --- /dev/null +++ b/pkg/fftm/route_get_eventstream_listener.go @@ -0,0 +1,45 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getEventStreamListener = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getEventStreamListener", + Path: "/eventstreams/{streamId}/listeners/{listenerId}", + Method: http.MethodGet, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointGetEventStreamListener, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getListener(r.Req.Context(), r.PP["streamId"], r.PP["listenerId"]) + }, + } +} diff --git a/pkg/fftm/route_get_eventstream_listener_test.go b/pkg/fftm/route_get_eventstream_listener_test.go new file mode 100644 index 00000000..a4c7e4d3 --- /dev/null +++ b/pkg/fftm/route_get_eventstream_listener_test.go @@ -0,0 +1,68 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetEventStreamsListener(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetResult(&listener). + Get(fmt.Sprintf("%s/eventstreams/%s/listeners/%s", url, es1.ID, l1.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_get_eventstream_listeners.go b/pkg/fftm/route_get_eventstream_listeners.go new file mode 100644 index 00000000..52c94c78 --- /dev/null +++ b/pkg/fftm/route_get_eventstream_listeners.go @@ -0,0 +1,47 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getEventStreamListeners = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getEventStreamListeners", + Path: "/eventstreams/{streamId}/listeners", + Method: http.MethodGet, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: []*ffapi.QueryParam{ + {Name: "limit", Description: tmmsgs.APIParamLimit}, + {Name: "after", Description: tmmsgs.APIParamAfter}, + }, + Description: tmmsgs.APIEndpointGetEventStreamListeners, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return []*apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getStreamListeners(r.Req.Context(), r.QP["after"], r.QP["limit"], r.PP["streamId"]) + }, + } +} diff --git a/pkg/fftm/route_get_eventstream_listeners_test.go b/pkg/fftm/route_get_eventstream_listeners_test.go new file mode 100644 index 00000000..88691e09 --- /dev/null +++ b/pkg/fftm/route_get_eventstream_listeners_test.go @@ -0,0 +1,76 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetEventStreamListeners(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1, es2 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream2")}).SetResult(&es2).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1, l2, l3 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener2", StreamID: es2.ID}).SetResult(&l2).Post(url + "/subscriptions") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener3", StreamID: es1.ID}).SetResult(&l3).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listeners []*apitypes.Listener + res, err = resty.New().R(). + SetResult(&listeners). + Get(fmt.Sprintf("%s/eventstreams/%s/listeners?limit=1&after=%s", url, es1.ID, l1.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Len(t, listeners, 1) + assert.Equal(t, l3.ID, listeners[0].ID) + assert.Equal(t, es1.ID, listeners[0].StreamID) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_patch_eventstream_listener.go b/pkg/fftm/route_patch_eventstream_listener.go new file mode 100644 index 00000000..794db7a6 --- /dev/null +++ b/pkg/fftm/route_patch_eventstream_listener.go @@ -0,0 +1,45 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var patchEventStreamListener = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "patchEventStreamListener", + Path: "/eventstreams/{streamId}/listeners/{listenerId}", + Method: http.MethodPatch, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchEventStreamListener, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.updateExistingListener(r.Req.Context(), r.PP["streamId"], r.PP["listenerId"], r.Input.(*apitypes.Listener)) + }, + } +} diff --git a/pkg/fftm/route_patch_eventstream_listener_test.go b/pkg/fftm/route_patch_eventstream_listener_test.go new file mode 100644 index 00000000..d0a7e340 --- /dev/null +++ b/pkg/fftm/route_patch_eventstream_listener_test.go @@ -0,0 +1,72 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPatchEventStreamListener(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetBody(&apitypes.Listener{ + Name: "listener1a", + }). + SetResult(&listener). + Patch(fmt.Sprintf("%s/eventstreams/%s/listeners/%s", url, es1.ID, l1.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + assert.Equal(t, "listener1a", listener.Name) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_post_eventstream_listeners.go b/pkg/fftm/route_post_eventstream_listeners.go new file mode 100644 index 00000000..aac77479 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_listeners.go @@ -0,0 +1,47 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postEventStreamListeners = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postEventStreamListeners", + Path: "/eventstreams/{streamId}/listeners", + Method: http.MethodPost, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: []*ffapi.QueryParam{ + {Name: "limit", Description: tmmsgs.APIParamLimit}, + {Name: "after", Description: tmmsgs.APIParamAfter}, + }, + Description: tmmsgs.APIEndpointPostEventStreamListener, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.createAndStoreNewStreamListener(r.Req.Context(), r.PP["streamId"], r.Input.(*apitypes.Listener)) + }, + } +} diff --git a/pkg/fftm/route_post_eventstream_listeners_test.go b/pkg/fftm/route_post_eventstream_listeners_test.go new file mode 100644 index 00000000..0a060f78 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_listeners_test.go @@ -0,0 +1,60 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostEventStreamListeners(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + // Create a listener + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(fmt.Sprintf("%s/eventstreams/%s/listeners", url, es1.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_post_subscriptions.go b/pkg/fftm/route_post_subscriptions.go index acf6bc5f..f3e31186 100644 --- a/pkg/fftm/route_post_subscriptions.go +++ b/pkg/fftm/route_post_subscriptions.go @@ -26,7 +26,7 @@ import ( var postSubscriptions = func(m *manager) *ffapi.Route { return &ffapi.Route{ - Name: "posSubscriptions", + Name: "postSubscriptions", Path: "/subscriptions", Deprecated: true, // in favor of "/eventstreams/{id}/listeners" Method: http.MethodPost, diff --git a/pkg/fftm/routes.go b/pkg/fftm/routes.go index e2f14234..79a5e232 100644 --- a/pkg/fftm/routes.go +++ b/pkg/fftm/routes.go @@ -21,16 +21,22 @@ import "github.com/hyperledger/firefly-common/pkg/ffapi" func (m *manager) routes() []*ffapi.Route { return []*ffapi.Route{ deleteEventStream(m), + deleteEventStreamListener(m), deleteSubscription(m), getEventStream(m), + getEventStreamListener(m), + getEventStreamListeners(m), getEventStreams(m), getSubscription(m), getSubscriptions(m), patchEventStream(m), + patchEventStreamListener(m), patchSubscription(m), postEventStream(m), + postEventStreamListeners(m), postEventStreamResume(m), postEventStreamSuspend(m), + postRootCommand(m), postSubscriptions(m), } } diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index 3fff33c1..7e2ae6ef 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -20,12 +20,13 @@ import ( "context" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" ) -func (m *manager) sendManagedTransaction(ctx context.Context, request *policyengine.TransactionRequest) (*policyengine.ManagedTXOutput, error) { +func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (*policyengine.ManagedTXOutput, error) { // First job is to assign the next nonce to this request. // We block any further sends on this nonce until we've got this one successfully into the node, or diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index ad0d53fa..ce9947d7 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -206,6 +206,15 @@ func (m *manager) createAndStoreNewStream(ctx context.Context, def *apitypes.Eve return spec, nil } +func (m *manager) createAndStoreNewStreamListener(ctx context.Context, idStr string, def *apitypes.Listener) (*apitypes.Listener, error) { + streamID, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return nil, err + } + def.StreamID = streamID + return m.createAndStoreNewListener(ctx, def) +} + func (m *manager) createAndStoreNewListener(ctx context.Context, def *apitypes.Listener) (*apitypes.Listener, error) { def.ID = nil // set by AddOrUpdateListener def.Created = nil @@ -380,14 +389,14 @@ func (m *manager) getListeners(ctx context.Context, afterStr, limitStr string) ( return m.persistence.ListListeners(ctx, after, limit) } -// func (m *manager) getStreamListeners(ctx context.Context, afterStr, limitStr, idStr string) (streams []*apitypes.Listener, err error) { -// after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) -// if err != nil { -// return nil, err -// } -// id, err := fftypes.ParseUUID(ctx, idStr) -// if err != nil { -// return nil, err -// } -// return m.persistence.ListStreamListeners(ctx, after, limit, id) -// } +func (m *manager) getStreamListeners(ctx context.Context, afterStr, limitStr, idStr string) (streams []*apitypes.Listener, err error) { + after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) + if err != nil { + return nil, err + } + id, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return nil, err + } + return m.persistence.ListStreamListeners(ctx, after, limit, id) +} diff --git a/pkg/policyengine/managed_tx.go b/pkg/policyengine/managed_tx.go index 0cc46f61..64ebac61 100644 --- a/pkg/policyengine/managed_tx.go +++ b/pkg/policyengine/managed_tx.go @@ -19,6 +19,7 @@ package policyengine import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -41,7 +42,7 @@ type ManagedTXOutput struct { PolicyInfo *fftypes.JSONAny `json:"policyInfo"` FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` - Request *TransactionRequest `json:"request,omitempty"` + Request *apitypes.TransactionRequest `json:"request,omitempty"` Receipt *ffcapi.TransactionReceiptResponse `json:"receipt,omitempty"` ErrorHistory []*ManagedTXError `json:"errorHistory"` Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` diff --git a/pkg/policyengines/simple/simple_policy_engine_test.go b/pkg/policyengines/simple/simple_policy_engine_test.go index 1589a511..1c159a1f 100644 --- a/pkg/policyengines/simple/simple_policy_engine_test.go +++ b/pkg/policyengines/simple/simple_policy_engine_test.go @@ -29,6 +29,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/stretchr/testify/assert" @@ -62,7 +63,7 @@ func TestFixedGasPriceOK(t *testing.T) { assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -130,7 +131,7 @@ func TestGasOracleSendOK(t *testing.T) { assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -177,7 +178,7 @@ func TestConnectorGasOracleSendOK(t *testing.T) { assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -224,7 +225,7 @@ func TestConnectorGasOracleFail(t *testing.T) { assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -265,7 +266,7 @@ func TestGasOracleSendFail(t *testing.T) { assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -325,7 +326,7 @@ func TestGasOracleTemplateExecuteFail(t *testing.T) { assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -355,7 +356,7 @@ func TestGasOracleNonJSON(t *testing.T) { assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -388,7 +389,7 @@ func TestTXSendFail(t *testing.T) { assert.NoError(t, err) mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -417,7 +418,7 @@ func TestWarnStaleWarningCannotParse(t *testing.T) { TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: &submitTime, PolicyInfo: fftypes.JSONAnyPtr("!not json!"), - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -446,7 +447,7 @@ func TestWarnStaleAdditionalWarning(t *testing.T) { submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.FFTime(time.Now().Add(-50 * time.Hour)) mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -480,7 +481,7 @@ func TestWarnStaleNoWarning(t *testing.T) { submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.Now() mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", @@ -512,7 +513,7 @@ func TestNoOpWithReceipt(t *testing.T) { submitTime := fftypes.Now() mtx := &policyengine.ManagedTXOutput{ - Request: &policyengine.TransactionRequest{ + Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", From 80a08ccf653a3205286faae92b31514eb60a982a Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sat, 18 Jun 2022 17:24:53 -0400 Subject: [PATCH 20/95] Round out API CRUD APIs/tests on streams & listeners Signed-off-by: Peter Broadhurst --- Makefile | 3 +- go.mod | 2 +- go.sum | 2 - mocks/eventsmocks/stream.go | 132 ++++++++ pkg/fftm/manager.go | 49 +-- pkg/fftm/manager_test.go | 10 + pkg/fftm/stream_management.go | 48 +-- pkg/fftm/stream_management_test.go | 521 +++++++++++++++++++++++++++++ 8 files changed, 718 insertions(+), 49 deletions(-) create mode 100644 mocks/eventsmocks/stream.go create mode 100644 pkg/fftm/stream_management_test.go diff --git a/Makefile b/Makefile index fed1698b..b5eeea1e 100644 --- a/Makefile +++ b/Makefile @@ -34,8 +34,9 @@ endef $(eval $(call makemock, pkg/ffcapi, API, ffcapimocks)) $(eval $(call makemock, pkg/policyengine, PolicyEngine, policyenginemocks)) $(eval $(call makemock, internal/confirmations, Manager, confirmationsmocks)) -$(eval $(call makemock, internal/persistence, Persistence, persistencemocks)) +$(eval $(call makemock, internal/persistence, Persistence, persistencemocks)) $(eval $(call makemock, internal/ws, WebSocketChannels, wsmocks)) +$(eval $(call makemock, internal/events, Stream, eventsmocks)) go-mod-tidy: .ALWAYS $(VGO) mod tidy diff --git a/go.mod b/go.mod index 52ae4102..82238497 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/hyperledger/firefly-transaction-manager go 1.17 require ( + github.com/getkin/kin-openapi v0.96.0 github.com/ghodss/yaml v1.0.0 github.com/go-resty/resty/v2 v2.7.0 github.com/google/uuid v1.3.0 @@ -23,7 +24,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/getkin/kin-openapi v0.96.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.19.15 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/go.sum b/go.sum index 14e80762..256ad7a1 100644 --- a/go.sum +++ b/go.sum @@ -631,8 +631,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e h1:QP+Yykyq7C670zb4Fs7s4lAtYmvIll4rP/y00hdOsg4= github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e/go.mod h1:434LxYn4ntyK/E0dY+2dTc55caBy6BdUMYBM2gLndAI= github.com/hyperledger/firefly-common v0.1.2/go.mod h1:ytB+404+Qg00wJKx602Yi/V6Spx9d0g65lZR9y5fzlo= -github.com/hyperledger/firefly-common v0.1.8 h1:gP4waMEEkbhB+pbsJyp5VrfrT4jQad6/4TXnXTfqXI8= -github.com/hyperledger/firefly-common v0.1.8/go.mod h1:MYL6Dbj3KqM/79IkS+mCzJ7wRguNbd/PKdVu8aXo5TI= github.com/hyperledger/firefly-common v0.1.11-0.20220618173558-a825551beb7a h1:JmwLgtI++IyC8piQEMcT5/efp7Pwl4s5/8KXnXLltpA= github.com/hyperledger/firefly-common v0.1.11-0.20220618173558-a825551beb7a/go.mod h1:MYL6Dbj3KqM/79IkS+mCzJ7wRguNbd/PKdVu8aXo5TI= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= diff --git a/mocks/eventsmocks/stream.go b/mocks/eventsmocks/stream.go new file mode 100644 index 00000000..76095e86 --- /dev/null +++ b/mocks/eventsmocks/stream.go @@ -0,0 +1,132 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package eventsmocks + +import ( + context "context" + + apitypes "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + + fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" + + mock "github.com/stretchr/testify/mock" +) + +// Stream is an autogenerated mock type for the Stream type +type Stream struct { + mock.Mock +} + +// AddOrUpdateListener provides a mock function with given fields: ctx, s +func (_m *Stream) AddOrUpdateListener(ctx context.Context, s *apitypes.Listener) error { + ret := _m.Called(ctx, s) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.Listener) error); ok { + r0 = rf(ctx, s) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Delete provides a mock function with given fields: ctx +func (_m *Stream) Delete(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveListener provides a mock function with given fields: ctx, id +func (_m *Stream) RemoveListener(ctx context.Context, id *fftypes.UUID) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Spec provides a mock function with given fields: +func (_m *Stream) Spec() *apitypes.EventStream { + ret := _m.Called() + + var r0 *apitypes.EventStream + if rf, ok := ret.Get(0).(func() *apitypes.EventStream); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.EventStream) + } + } + + return r0 +} + +// Start provides a mock function with given fields: ctx +func (_m *Stream) Start(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Status provides a mock function with given fields: +func (_m *Stream) Status() apitypes.EventStreamStatus { + ret := _m.Called() + + var r0 apitypes.EventStreamStatus + if rf, ok := ret.Get(0).(func() apitypes.EventStreamStatus); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(apitypes.EventStreamStatus) + } + + return r0 +} + +// Stop provides a mock function with given fields: ctx +func (_m *Stream) Stop(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateSpec provides a mock function with given fields: ctx, updates +func (_m *Stream) UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error { + ret := _m.Called(ctx, updates) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStream) error); ok { + r0 = rf(ctx, updates) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index 5df0e677..6a438dfc 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -86,28 +86,7 @@ type manager struct { func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { var err error - events.InitDefaults() - m := &manager{ - connector: connector, - ffCoreClient: ffresty.New(ctx, tmconfig.FFCoreConfig), - fullScanRequests: make(chan bool, 1), - nextNonces: make(map[string]uint64), - lockedNonces: make(map[string]*lockedNonce), - apiServerDone: make(chan error), - pendingOpsByID: make(map[string]*pendingState), - eventStreams: make(map[fftypes.UUID]events.Stream), - streamsByName: make(map[string]*fftypes.UUID), - - name: config.GetString(tmconfig.ManagerName), - opTypes: config.GetStringSlice(tmconfig.OperationsTypes), - startupScanMaxRetries: config.GetInt(tmconfig.OperationsFullScanStartupMaxRetries), - fullScanPageSize: config.GetInt64(tmconfig.OperationsFullScanPageSize), - fullScanMinDelay: config.GetDuration(tmconfig.OperationsFullScanMinimumDelay), - policyLoopInterval: config.GetDuration(tmconfig.PolicyLoopInterval), - errorHistoryCount: config.GetInt(tmconfig.OperationsErrorHistoryCount), - enableChangeListener: config.GetBool(tmconfig.OperationsChangeListenerEnabled), - } - m.ctx, m.cancelCtx = context.WithCancel(ctx) + m := newManager(ctx, connector) if m.name == "" { return nil, i18n.NewError(ctx, tmmsgs.MsgConfigParamNotSet, tmconfig.ManagerName) } @@ -134,6 +113,32 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { return m, nil } +func newManager(ctx context.Context, connector ffcapi.API) *manager { + events.InitDefaults() + m := &manager{ + connector: connector, + ffCoreClient: ffresty.New(ctx, tmconfig.FFCoreConfig), + fullScanRequests: make(chan bool, 1), + nextNonces: make(map[string]uint64), + lockedNonces: make(map[string]*lockedNonce), + apiServerDone: make(chan error), + pendingOpsByID: make(map[string]*pendingState), + eventStreams: make(map[fftypes.UUID]events.Stream), + streamsByName: make(map[string]*fftypes.UUID), + + name: config.GetString(tmconfig.ManagerName), + opTypes: config.GetStringSlice(tmconfig.OperationsTypes), + startupScanMaxRetries: config.GetInt(tmconfig.OperationsFullScanStartupMaxRetries), + fullScanPageSize: config.GetInt64(tmconfig.OperationsFullScanPageSize), + fullScanMinDelay: config.GetDuration(tmconfig.OperationsFullScanMinimumDelay), + policyLoopInterval: config.GetDuration(tmconfig.PolicyLoopInterval), + errorHistoryCount: config.GetInt(tmconfig.OperationsErrorHistoryCount), + enableChangeListener: config.GetBool(tmconfig.OperationsChangeListenerEnabled), + } + m.ctx, m.cancelCtx = context.WithCancel(ctx) + return m +} + type pendingState struct { mtx *policyengine.ManagedTXOutput confirmed bool diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 5f8a623c..62f8be47 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -36,6 +36,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" @@ -92,6 +93,15 @@ func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...strin } +func newMockPersistenceManager(t *testing.T) (*persistencemocks.Persistence, *ffcapimocks.API, *manager) { + tmconfig.Reset() + mca := &ffcapimocks.API{} + mps := &persistencemocks.Persistence{} + m := newManager(context.Background(), mca) + m.persistence = mps + return mps, mca, m +} + func newTestOperation(t *testing.T, mtx *policyengine.ManagedTXOutput, status core.OpStatus) *core.Operation { b, err := json.Marshal(&mtx) assert.NoError(t, err) diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index ce9947d7..a28a27b6 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -44,7 +44,16 @@ func (m *manager) restoreStreams() error { } for _, def := range streamDefs { lastInPage = def.ID - if _, err := m.addRuntimeStream(def); err != nil { + closeoutName, err := m.reserveStreamName(m.ctx, *def.Name, def.ID) + var s events.Stream + if err == nil { + s, err = m.addRuntimeStream(def) + } + if err == nil && !*def.Suspended { + err = s.Start(m.ctx) + } + closeoutName(err == nil) + if err != nil { return err } } @@ -129,13 +138,13 @@ func (m *manager) deleteStream(ctx context.Context, idStr string) error { delete(m.streamsByName, *s.Spec().Name) } m.mux.Unlock() + if err := m.deleteAllStreamListeners(ctx, id); err != nil { + return err + } + if err := m.persistence.DeleteStream(ctx, id); err != nil { + return err + } if s != nil { - if err := m.deleteAllStreamListeners(ctx, id); err != nil { - return err - } - if err := m.persistence.DeleteStream(ctx, id); err != nil { - return err - } return s.Delete(ctx) } return nil @@ -193,15 +202,16 @@ func (m *manager) createAndStoreNewStream(ctx context.Context, def *apitypes.Eve spec := s.Spec() err = m.persistence.WriteStream(ctx, spec) if err != nil { - err1 := m.deleteStream(ctx, spec.ID.String()) + m.mux.Lock() + delete(m.eventStreams, *def.ID) + m.mux.Unlock() + err1 := s.Delete(ctx) log.L(ctx).Infof("Cleaned up runtime stream after write failed (err?=%v)", err1) return nil, err } stored = true if !*spec.Suspended { - if err = s.Start(ctx); err != nil { - return nil, err - } + return spec, s.Start(ctx) } return spec, nil } @@ -228,11 +238,7 @@ func (m *manager) updateExistingListener(ctx context.Context, streamIDStr, liste } updates.ID = l.ID updates.StreamID = l.StreamID - l, err = m.createOrUpdateListener(ctx, updates) - if err != nil { - return nil, err - } - return l, err + return m.createOrUpdateListener(ctx, updates) } func (m *manager) createOrUpdateListener(ctx context.Context, def *apitypes.Listener) (*apitypes.Listener, error) { @@ -307,13 +313,9 @@ func (m *manager) updateStream(ctx context.Context, idStr string, updates *apity // We might need to start or stop if *spec.Suspended && s.Status() != apitypes.EventStreamStatusStopped { - if err = s.Stop(ctx); err != nil { - return nil, err - } + return nil, s.Stop(ctx) } else if !*spec.Suspended && s.Status() != apitypes.EventStreamStatusStarted { - if err = s.Start(ctx); err != nil { - return nil, err - } + return nil, s.Start(ctx) } return spec, nil } @@ -327,7 +329,7 @@ func (m *manager) getStream(ctx context.Context, idStr string) (*apitypes.EventS s := m.eventStreams[*id] m.mux.Unlock() if s == nil { - return nil, nil + return nil, i18n.NewError(ctx, tmmsgs.MsgStreamNotFound, idStr) } return &apitypes.EventStreamWithStatus{ EventStream: *s.Spec(), diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go new file mode 100644 index 00000000..fd4d065c --- /dev/null +++ b/pkg/fftm/stream_management_test.go @@ -0,0 +1,521 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestRestoreStreamsAndListenersOK(t *testing.T) { + + _, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + falsy := false + + es1 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr("stream1"), Suspended: &falsy} + err := m.persistence.WriteStream(m.ctx, es1) + assert.NoError(t, err) + + e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: "listener1", StreamID: es1.ID} + err = m.persistence.WriteListener(m.ctx, e1l1) + assert.NoError(t, err) + + e1l2 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: "listener2", StreamID: es1.ID} + err = m.persistence.WriteListener(m.ctx, e1l2) + assert.NoError(t, err) + + es2 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr("stream2"), Suspended: &falsy} + err = m.persistence.WriteStream(m.ctx, es2) + assert.NoError(t, err) + + e2l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: "listener3", StreamID: es2.ID} + err = m.persistence.WriteListener(m.ctx, e2l1) + assert.NoError(t, err) + + err = m.Start() + assert.NoError(t, err) + + assert.Equal(t, es1.ID, m.streamsByName["stream1"]) + assert.Equal(t, es2.ID, m.streamsByName["stream2"]) + + mfc.AssertExpectations(t) + +} + +func TestRestoreStreamsReadFailed(t *testing.T) { + + mp, _, m := newMockPersistenceManager(t) + + mp.On("ListStreams", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit).Return(nil, fmt.Errorf("pop")) + + err := m.restoreStreams() + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestRestoreListenersReadFailed(t *testing.T) { + + mp, _, m := newMockPersistenceManager(t) + + mp.On("ListListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit).Return(nil, fmt.Errorf("pop")) + + err := m.restoreListeners() + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestRestoreStreamsValidateFail(t *testing.T) { + + _, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + falsy := false + es1 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr(""), Suspended: &falsy} + err := m.persistence.WriteStream(m.ctx, es1) + assert.NoError(t, err) + + err = m.restoreStreams() + assert.Regexp(t, "FF21028", err) + +} + +func TestRestoreListenersStartFail(t *testing.T) { + + _, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + falsy := false + es1 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr("stream1"), Suspended: &falsy} + err := m.persistence.WriteStream(m.ctx, es1) + assert.NoError(t, err) + + e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: "listener1", StreamID: es1.ID} + err = m.persistence.WriteListener(m.ctx, e1l1) + assert.NoError(t, err) + + err = m.restoreStreams() + assert.Regexp(t, "pop", err) + + mfc.AssertExpectations(t) + +} + +func TestDeleteStartedListener(t *testing.T) { + + _, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + + falsy := false + es1 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr("stream1"), Suspended: &falsy} + err := m.persistence.WriteStream(m.ctx, es1) + assert.NoError(t, err) + + e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: "listener1", StreamID: es1.ID} + err = m.persistence.WriteListener(m.ctx, e1l1) + assert.NoError(t, err) + + err = m.Start() + assert.NoError(t, err) + + err = m.deleteStream(m.ctx, es1.ID.String()) + assert.NoError(t, err) + + mfc.AssertExpectations(t) + +} + +func TestDeleteStartedListenerFail(t *testing.T) { + + mp, _, m := newMockPersistenceManager(t) + + esID := apitypes.UUIDVersion1() + lID := apitypes.UUIDVersion1() + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, esID).Return([]*apitypes.Listener{ + {ID: lID, StreamID: esID}, + }, nil) + mp.On("DeleteListener", m.ctx, lID).Return(fmt.Errorf("pop")) + + err := m.deleteAllStreamListeners(m.ctx, esID) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestDeleteStreamBadID(t *testing.T) { + + _, _, m := newMockPersistenceManager(t) + + err := m.deleteStream(m.ctx, "Bad ID") + assert.Regexp(t, "FF00138", err) + +} + +func TestDeleteStreamListenerPersistenceFail(t *testing.T) { + + mp, _, m := newMockPersistenceManager(t) + + esID := apitypes.UUIDVersion1() + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, esID).Return(nil, fmt.Errorf("pop")) + + err := m.deleteStream(m.ctx, esID.String()) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestDeleteStreamPersistenceFail(t *testing.T) { + + mp, _, m := newMockPersistenceManager(t) + + esID := apitypes.UUIDVersion1() + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, esID).Return([]*apitypes.Listener{}, nil) + mp.On("DeleteStream", m.ctx, esID).Return(fmt.Errorf("pop")) + + err := m.deleteStream(m.ctx, esID.String()) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestDeleteStreamNotInitialized(t *testing.T) { + + mp, _, m := newMockPersistenceManager(t) + + esID := apitypes.UUIDVersion1() + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, esID).Return([]*apitypes.Listener{}, nil) + mp.On("DeleteStream", m.ctx, esID).Return(nil) + + err := m.deleteStream(m.ctx, esID.String()) + assert.NoError(t, err) + + mp.AssertExpectations(t) +} + +func TestCreateRenameStreamNameReservation(t *testing.T) { + + mp, _, m := newMockPersistenceManager(t) + + mp.On("WriteStream", m.ctx, mock.Anything).Return(fmt.Errorf("temporary")).Once() + mp.On("DeleteCheckpoint", m.ctx, mock.Anything).Return(fmt.Errorf("temporary")).Once() + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + + // Reject missing name + _, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{}) + assert.Regexp(t, "FF21028", err) + + // Attempt to start and encounter a temporary error + _, err = m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("Name1")}) + assert.Regexp(t, "temporary", err) + + // Ensure we still allow use of the name after the glitch is fixed + es1, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("Name1")}) + assert.NoError(t, err) + + // Ensure we can't create another stream of same name + _, err = m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("Name1")}) + assert.Regexp(t, "FF21047", err) + + // Create a second stream to test clash on rename + es2, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("Name2")}) + assert.NoError(t, err) + + // Check for clash + _, err = m.updateStream(m.ctx, es1.ID.String(), &apitypes.EventStream{Name: strPtr("Name2")}) + assert.Regexp(t, "FF21047", err) + + // Check for no-op rename to self + _, err = m.updateStream(m.ctx, es2.ID.String(), &apitypes.EventStream{Name: strPtr("Name2")}) + assert.NoError(t, err) + + mp.AssertExpectations(t) +} + +func TestCreateStreamValidateFail(t *testing.T) { + + _, _, m := newMockPersistenceManager(t) + + wrongType := apitypes.DistributionMode("wrong") + _, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1"), Type: &wrongType}) + assert.Regexp(t, "FF21029", err) + +} + +func TestCreateAndStoreNewStreamListenerBadID(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.createAndStoreNewStreamListener(m.ctx, "bad", nil) + assert.Regexp(t, "FF00138", err) +} + +func TestUpdateExistingListenerNotFound(t *testing.T) { + mp, _, m := newMockPersistenceManager(t) + + mp.On("GetListener", m.ctx, mock.Anything).Return(nil, nil) + + _, err := m.updateExistingListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String(), &apitypes.Listener{}) + assert.Regexp(t, "FF21046", err) + + mp.AssertExpectations(t) +} + +func TestCreateOrUpdateListenerNotFound(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.createOrUpdateListener(m.ctx, &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: apitypes.UUIDVersion1()}) + assert.Regexp(t, "FF21045", err) + +} + +func TestCreateOrUpdateListenerFail(t *testing.T) { + mp, _, m := newMockPersistenceManager(t) + + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + _, err = m.createOrUpdateListener(m.ctx, &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: es.ID}) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestCreateOrUpdateListenerWriteFail(t *testing.T) { + mp, _, m := newMockPersistenceManager(t) + + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("WriteListener", m.ctx, mock.Anything).Return(fmt.Errorf("pop")) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + _, err = m.createOrUpdateListener(m.ctx, &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: es.ID}) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestDeleteListenerBadID(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + err := m.deleteListener(m.ctx, "bad ID", "bad ID") + assert.Regexp(t, "FF00138", err) + +} + +func TestDeleteListenerStreamNotFound(t *testing.T) { + mp, _, m := newMockPersistenceManager(t) + + l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: apitypes.UUIDVersion1()} + mp.On("GetListener", m.ctx, mock.Anything).Return(l1, nil) + + err := m.deleteListener(m.ctx, l1.StreamID.String(), l1.ID.String()) + assert.Regexp(t, "FF21045", err) + + mp.AssertExpectations(t) + +} + +func TestDeleteListenerFail(t *testing.T) { + mp, _, m := newMockPersistenceManager(t) + + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("WriteListener", m.ctx, mock.Anything).Return(nil) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + l1, err := m.createOrUpdateListener(m.ctx, &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: es.ID}) + assert.NoError(t, err) + + mp.On("GetListener", m.ctx, mock.Anything).Return(l1, nil) + + err = m.deleteListener(m.ctx, l1.StreamID.String(), l1.ID.String()) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) + +} + +func TestUpdateStreamBadID(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.updateStream(m.ctx, "bad ID", &apitypes.EventStream{}) + assert.Regexp(t, "FF00138", err) + +} + +func TestUpdateStreamNotFound(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.updateStream(m.ctx, apitypes.UUIDVersion1().String(), &apitypes.EventStream{}) + assert.Regexp(t, "FF21045", err) + +} + +func TestUpdateStreamBadChanges(t *testing.T) { + mp, _, m := newMockPersistenceManager(t) + + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + wrongType := apitypes.DistributionMode("wrong") + _, err = m.updateStream(m.ctx, es.ID.String(), &apitypes.EventStream{Type: &wrongType}) + assert.Regexp(t, "FF21029", err) + +} + +func TestUpdateStreamWriteFail(t *testing.T) { + mp, _, m := newMockPersistenceManager(t) + + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil).Once() + mp.On("WriteStream", m.ctx, mock.Anything).Return(fmt.Errorf("pop")) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + _, err = m.updateStream(m.ctx, es.ID.String(), &apitypes.EventStream{}) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) + +} + +func TestGetStreamBadID(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.getStream(m.ctx, "bad ID") + assert.Regexp(t, "FF00138", err) + +} + +func TestGetStreamNotFound(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.getStream(m.ctx, apitypes.UUIDVersion1().String()) + assert.Regexp(t, "FF21045", err) + +} + +func TestGetStreamsBadLimit(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.getStreams(m.ctx, "", "wrong") + assert.Regexp(t, "FF21044", err) + +} + +func TestGetListenerBadAfter(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.getListeners(m.ctx, "!bad UUID", "") + assert.Regexp(t, "FF00138", err) + +} + +func TestGetListenerBadStreamID(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.getListener(m.ctx, "bad ID", apitypes.UUIDVersion1().String()) + assert.Regexp(t, "FF00138", err) + +} + +func TestGetListenerBadListenerID(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.getListener(m.ctx, apitypes.UUIDVersion1().String(), "bad ID") + assert.Regexp(t, "FF00138", err) + +} + +func TestGetListenerLookupErr(t *testing.T) { + mp, _, m := newMockPersistenceManager(t) + + mp.On("GetListener", m.ctx, mock.Anything).Return(nil, fmt.Errorf("pop")) + + _, err := m.getListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String()) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) + +} + +func TestGetListenerNotFound(t *testing.T) { + mp, _, m := newMockPersistenceManager(t) + + mp.On("GetListener", m.ctx, mock.Anything).Return(nil, nil) + + _, err := m.getListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String()) + assert.Regexp(t, "FF21046", err) + + mp.AssertExpectations(t) + +} + +func TestGetStreamListenersBadLimit(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.getStreamListeners(m.ctx, "", "!bad limit", apitypes.UUIDVersion1().String()) + assert.Regexp(t, "FF21044", err) + +} + +func TestGetStreamListenersBadStreamID(t *testing.T) { + _, _, m := newMockPersistenceManager(t) + + _, err := m.getStreamListeners(m.ctx, "", "", "bad ID") + assert.Regexp(t, "FF00138", err) + +} From 99d39053555117c8fdb2694d35a6fcf006cb2662 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sun, 19 Jun 2022 12:54:44 -0400 Subject: [PATCH 21/95] Add Query support to API Signed-off-by: Peter Broadhurst --- mocks/ffcapimocks/api.go | 60 +++++++++++++++--------------- pkg/apitypes/base_request.go | 12 ++++++ pkg/apitypes/query_request.go | 30 +++++++++++++++ pkg/apitypes/tx_request.go | 15 +------- pkg/ffcapi/api.go | 4 +- pkg/ffcapi/method_call.go | 6 +-- pkg/fftm/api_test.go | 65 +++++++++++++++++++++++++++++++++ pkg/fftm/route__root_command.go | 18 ++++++++- 8 files changed, 159 insertions(+), 51 deletions(-) create mode 100644 pkg/apitypes/query_request.go diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index d8875266..1e8055ab 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -194,36 +194,6 @@ func (_m *API) GasPriceEstimate(ctx context.Context, req *ffcapi.GasPriceEstimat return r0, r1, r2 } -// MethodCall provides a mock function with given fields: ctx, req -func (_m *API) MethodCall(ctx context.Context, req *ffcapi.MethodCallRequest) (*ffcapi.MethodCallResponse, ffcapi.ErrorReason, error) { - ret := _m.Called(ctx, req) - - var r0 *ffcapi.MethodCallResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.MethodCallRequest) *ffcapi.MethodCallResponse); ok { - r0 = rf(ctx, req) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.MethodCallResponse) - } - } - - var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.MethodCallRequest) ffcapi.ErrorReason); ok { - r1 = rf(ctx, req) - } else { - r1 = ret.Get(1).(ffcapi.ErrorReason) - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.MethodCallRequest) error); ok { - r2 = rf(ctx, req) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - // NewBlockHashes provides a mock function with given fields: func (_m *API) NewBlockHashes() <-chan *ffcapi.BlockHashEvent { ret := _m.Called() @@ -270,6 +240,36 @@ func (_m *API) NextNonceForSigner(ctx context.Context, req *ffcapi.NextNonceForS return r0, r1, r2 } +// QueryInvoke provides a mock function with given fields: ctx, req +func (_m *API) QueryInvoke(ctx context.Context, req *ffcapi.QueryInvokeRequest) (*ffcapi.QueryInvokeResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.QueryInvokeResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.QueryInvokeRequest) *ffcapi.QueryInvokeResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.QueryInvokeResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.QueryInvokeRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.QueryInvokeRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // TransactionPrepare provides a mock function with given fields: ctx, req func (_m *API) TransactionPrepare(ctx context.Context, req *ffcapi.TransactionPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) diff --git a/pkg/apitypes/base_request.go b/pkg/apitypes/base_request.go index 2474939b..86618d98 100644 --- a/pkg/apitypes/base_request.go +++ b/pkg/apitypes/base_request.go @@ -36,3 +36,15 @@ func (br *BaseRequest) UnmarshalJSON(data []byte) error { func (br *BaseRequest) UnmarshalTo(o interface{}) error { return json.Unmarshal(br.fullPayload, &o) } + +type RequestHeaders struct { + ID string `ffstruct:"fftmrequest" json:"id"` + Type RequestType `json:"type"` +} + +type RequestType string + +const ( + RequestTypeSendTransaction RequestType = "SendTransaction" + RequestTypeQuery RequestType = "Query" +) diff --git a/pkg/apitypes/query_request.go b/pkg/apitypes/query_request.go new file mode 100644 index 00000000..50668df8 --- /dev/null +++ b/pkg/apitypes/query_request.go @@ -0,0 +1,30 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 apitypes + +import ( + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +// QueryRequest is the request payload to send to perform a synchronous query against the blockchain state +type QueryRequest struct { + Headers RequestHeaders `json:"headers"` + ffcapi.TransactionInput +} + +// QueryResponse is the response payload for a query +type QueryResponse ffcapi.QueryInvokeResponse diff --git a/pkg/apitypes/tx_request.go b/pkg/apitypes/tx_request.go index 50bb9537..e1917892 100644 --- a/pkg/apitypes/tx_request.go +++ b/pkg/apitypes/tx_request.go @@ -20,21 +20,8 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) -// TransactionRequest is the external interface into sending transactions to the front-side of Transaction Manager -// Note this is a deliberate match for the EthConnect subset that is supported by FireFly core +// TransactionRequest is the payload sent to initiate a new transaction type TransactionRequest struct { Headers RequestHeaders `json:"headers"` ffcapi.TransactionInput } - -type RequestHeaders struct { - ID string `ffstruct:"fftmrequest" json:"id"` - Type RequestType `json:"type"` -} - -type RequestType string - -const ( - RequestTypeSendTransaction RequestType = "SendTransaction" - RequestTypeQuery RequestType = "Query" -) diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 50d9bc41..c1cf4cc0 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -40,8 +40,8 @@ type API interface { // GasPriceEstimate provides a blockchain specific gas price estimate GasPriceEstimate(ctx context.Context, req *GasPriceEstimateRequest) (*GasPriceEstimateResponse, ErrorReason, error) - // MethodCall executes a method on a blockchain smart contract, which might execute Smart Contract code, but does not affect the blockchain state. - MethodCall(ctx context.Context, req *MethodCallRequest) (*MethodCallResponse, ErrorReason, error) + // QueryInvoke executes a method on a blockchain smart contract, which might execute Smart Contract code, but does not affect the blockchain state. + QueryInvoke(ctx context.Context, req *QueryInvokeRequest) (*QueryInvokeResponse, ErrorReason, error) // TransactionReceipt queries to see if a receipt is available for a given transaction hash TransactionReceipt(ctx context.Context, req *TransactionReceiptRequest) (*TransactionReceiptResponse, ErrorReason, error) diff --git a/pkg/ffcapi/method_call.go b/pkg/ffcapi/method_call.go index 78b5ef1a..613496f3 100644 --- a/pkg/ffcapi/method_call.go +++ b/pkg/ffcapi/method_call.go @@ -20,17 +20,17 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" ) -// MethodCallRequest requests execution of a smart contract method in order to either: +// QueryInvokeRequest requests execution of a smart contract method in order to either: // 1) Query state // 2) Attempt to extract the revert reason from an on-chain failure to execute a transaction // // See the list of standard error reasons that should be returned for situations that can be // detected by the back-end connector. -type MethodCallRequest struct { +type QueryInvokeRequest struct { TransactionInput BlockNumber *fftypes.FFBigInt `json:"blockNumber,omitempty"` } -type MethodCallResponse struct { +type QueryInvokeResponse struct { Outputs *fftypes.JSONAny `json:"outputs"` // The data output from the method call - can be array or object structure } diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go index 3d9d1ea6..30685266 100644 --- a/pkg/fftm/api_test.go +++ b/pkg/fftm/api_test.go @@ -28,6 +28,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -263,6 +264,70 @@ func TestSendTransactionUpdateFireFlyFail(t *testing.T) { } +func TestQueryOK(t *testing.T) { + + url, m, cancel := newTestManager(t, + func(w http.ResponseWriter, r *http.Request) {}, + ) + defer cancel() + m.Start() + + mca := m.connector.(*ffcapimocks.API) + mca.On("QueryInvoke", mock.Anything, mock.MatchedBy(func(req *ffcapi.QueryInvokeRequest) bool { + return req.Method.String() == `"some method details"` + })).Return(&ffcapi.QueryInvokeResponse{ + Outputs: fftypes.JSONAnyPtr(`"some output data"`), + }, ffcapi.ErrorReason(""), nil) + + var queryRes apitypes.QueryResponse + res, err := resty.New().R(). + SetBody(&apitypes.QueryRequest{ + Headers: apitypes.RequestHeaders{ + ID: fftypes.NewUUID().String(), + Type: apitypes.RequestTypeQuery, + }, + TransactionInput: ffcapi.TransactionInput{ + Method: fftypes.JSONAnyPtr(`"some method details"`), + }, + }). + SetResult(&queryRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, `"some output data"`, queryRes.Outputs.String()) + + mca.AssertExpectations(t) + +} + +func TestQueryBadRequest(t *testing.T) { + + url, m, cancel := newTestManager(t, + func(w http.ResponseWriter, r *http.Request) {}, + ) + defer cancel() + m.Start() + + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetBody(`{ + "headers": { + "id": "`+fftypes.NewUUID().String()+`", + "type": "Query" + }, + "params": "not an array" + }`, + ). + SetHeader("content-type", "application/json"). + SetError(&errRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 400, res.StatusCode()) + assert.Regexp(t, "FF21022", errRes.Error) + +} + func TestNotFound(t *testing.T) { url, m, cancel := newTestManager(t, diff --git a/pkg/fftm/route__root_command.go b/pkg/fftm/route__root_command.go index ffd49530..526d09c8 100644 --- a/pkg/fftm/route__root_command.go +++ b/pkg/fftm/route__root_command.go @@ -25,6 +25,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) var postRootCommand = func(m *manager) *ffapi.Route { @@ -39,9 +40,13 @@ var postRootCommand = func(m *manager) *ffapi.Route { JSONOutputValue: func() interface{} { return map[string]interface{}{} }, JSONInputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) { schemas := []*openapi3.SchemaRef{} - sendTxSchema, err := schemaGen(&apitypes.TransactionRequest{}) + txRequest, err := schemaGen(&apitypes.TransactionRequest{}) if err == nil { - schemas = append(schemas, sendTxSchema) + schemas = append(schemas, txRequest) + } + queryRequest, err := schemaGen(&apitypes.QueryRequest{}) + if err == nil { + schemas = append(schemas, queryRequest) } return &openapi3.SchemaRef{ Value: &openapi3.Schema{ @@ -59,6 +64,15 @@ var postRootCommand = func(m *manager) *ffapi.Route { return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) } return m.sendManagedTransaction(r.Req.Context(), &tReq) + case apitypes.RequestTypeQuery: + var tReq apitypes.QueryRequest + if err = baseReq.UnmarshalTo(&tReq); err != nil { + return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) + } + res, _, err := m.connector.QueryInvoke(r.Req.Context(), &ffcapi.QueryInvokeRequest{ + TransactionInput: tReq.TransactionInput, + }) + return res, err default: return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgUnsupportedRequestType, baseReq.Headers.Type) } From c45c520300679b7ef674e89bc7775a6f13014dbf Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 20 Jun 2022 12:30:46 -0400 Subject: [PATCH 22/95] Update config generation and fill in description gaps Signed-off-by: Peter Broadhurst --- Makefile | 2 +- config.md | 135 ++++++++++++++-------- internal/events/eventstream_test.go | 2 +- internal/tmmsgs/en_config_descriptions.go | 34 +++++- pkg/fftm/config_docs_generate_test.go | 43 +++++++ pkg/fftm/config_docs_test.go | 46 ++++++++ pkg/fftm/manager.go | 6 +- pkg/fftm/manager_test.go | 4 +- 8 files changed, 214 insertions(+), 58 deletions(-) create mode 100644 pkg/fftm/config_docs_generate_test.go create mode 100644 pkg/fftm/config_docs_test.go diff --git a/Makefile b/Makefile index b5eeea1e..732e49d1 100644 --- a/Makefile +++ b/Makefile @@ -48,4 +48,4 @@ deps: $(VGO) get ./internal/... ./pkg/... $(VGO) get -t ./internal/... ./pkg/... docs: - $(VGO) test ./internal -timeout=10s -tags docs + $(VGO) test ./pkg/fftm -timeout=10s -tags docs diff --git a/config.md b/config.md index 744b4396..894699b0 100644 --- a/config.md +++ b/config.md @@ -1,8 +1,8 @@ --- layout: default -title: Configuration Reference +title: pages.reference parent: Reference -nav_order: 3 +nav_order: 2 --- # Configuration Reference @@ -22,6 +22,8 @@ nav_order: 3 |Key|Description|Type|Default Value| |---|-----------|----|-------------| |address|Listener address for API|`string`|`127.0.0.1` +|defaultRequestTimeout|Default server-side request timeout for API calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|maxRequestTimeout|Maximum server-side request timeout a caller can request with a Request-Timeout header|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10m` |port|Listener port for API|`int`|`5008` |publicURL|External address callers should access API over|`string`|`` |readTimeout|The maximum time to wait when reading from an HTTP connection|[`time.Duration`](https://pkg.go.dev/time#Duration)|`15s` @@ -52,38 +54,8 @@ nav_order: 3 |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` -|expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` -|headers|Adds custom headers to HTTP requests|`map[string]string`|`` -|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms` -|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100` -|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` -|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` -|url|The URL of the blockchain connector|`string`|`` |variant|The variant is the overall category of blockchain connector, defining things like how input/output definitions are passed|`string`|`evm` -## connector.auth - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|password|Password|`string`|`` -|username|Username|`string`|`` - -## connector.proxy - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|url|Optional HTTP proxy URL to use for the blockchain connector|`string`|`` - -## connector.retry - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|count|The maximum number of times to retry|`int`|`5` -|enabled|Enables retries|`boolean`|`false` -|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` -|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` - ## cors |Key|Description|Type|Default Value| @@ -96,6 +68,26 @@ nav_order: 3 |methods| CORS setting to control the allowed methods|`string`|`[GET POST PUT PATCH DELETE]` |origins|CORS setting to control the allowed origins|`string`|`[*]` +## eventstreams.defaults + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|batchSize|Default batch size for newly created event streams|`int`|`50` +|batchTimeout|Default batch timeout for newly created event streams|[`time.Duration`](https://pkg.go.dev/time#Duration)|`5s` +|blockedRetryDelay|Default blocked retry delay for newly created event streams|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|errorHandling|Default error handling for newly created event streams|'skip' or 'block'|`block` +|retryTimeout|Default retry timeout for newly created event streams|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|webhookRequestTimeout|Default WebHook request timeout for newly created event streams|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|websocketDistributionMode|Default WebSocket distribution mode for newly created event streams|'load_balance' or 'broadcast'|`load_balance` + +## eventstreams.retry + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|factor|Factor to increase the delay by, between each retry|`boolean`|`` +|initialDelay|Initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|maxDelay|Maximum delay between retries|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` + ## ffcore |Key|Description|Type|Default Value| @@ -200,6 +192,20 @@ nav_order: 3 |pageSize|The page size to use when performing a full scan of the ForeFly core API on startup, or recovery|`int`|`100` |startupMaxRetries|The page size to use when performing a full scan of the ForeFly core API on startup, or recovery|`int`|`10` +## persistence + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|type|The type of persistence to use|Only 'leveldb' currently supported|`leveldb` + +## persistence.leveldb + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|maxHandles|The maximum number of cached file handles LevelDB should keep open|`int`|`100` +|path|The path for the LevelDB persistence directory|`string`|`` +|syncWrites|Whether to synchronously perform writes to the storage|`boolean`|`true` + ## policyengine |Key|Description|Type|Default Value| @@ -211,23 +217,23 @@ nav_order: 3 |Key|Description|Type|Default Value| |---|-----------|----|-------------| |fixedGasPrice|A fixed gasPrice value/structure to pass to the connector|Raw JSON|`` -|warnInterval|The time between warnings when a blockchain transaction has not been allocated a receipt|[`time.Duration`](https://pkg.go.dev/time#Duration)|`15m` +|warnInterval|The time between warnings when a blockchain transaction has not been allocated a receipt|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` ## policyengine.simple.gasOracle |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` -|expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` +|connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` |headers|Adds custom headers to HTTP requests|`map[string]string`|`` -|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms` -|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100` -|method|The HTTP Method to use when invoking the Gas Oracle REST API|`string`|`GET` -|mode|The gas oracle mode|connector | restapi | disabled|`disabled` -|queryInterval|The minimum interval between queries to the Gas Oracle|[`time.Duration`](https://pkg.go.dev/time#Duration)|`5m` -|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|maxIdleConns|The max number of idle connections to hold pooled|`int`|`` +|method|The HTTP Method to use when invoking the Gas Oracle REST API|`string`|`` +|mode|The gas oracle mode|connector | restapi | disabled|`` +|queryInterval|The minimum interval between queries to the Gas Oracle|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` |template|REST API Gas Oracle: A go template to execute against the result from the Gas Oracle, to create a JSON block that will be passed as the gas price to the connector|[Go Template](https://pkg.go.dev/text/template) `string`|`` -|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` +|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` |url|REST API Gas Oracle: The URL of a Gas Oracle REST API to call|`string`|`` ## policyengine.simple.gasOracle.auth @@ -247,13 +253,48 @@ nav_order: 3 |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|count|The maximum number of times to retry|`int`|`5` -|enabled|Enables retries|`boolean`|`false` -|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` -|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|count|The maximum number of times to retry|`int`|`` +|enabled|Enables retries|`boolean`|`` +|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` ## policyloop |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|interval|Interval at which to invoke the policy engine to evaluate outstanding transactions|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` \ No newline at end of file +|interval|Interval at which to invoke the policy engine to evaluate outstanding transactions|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` + +## webhooks + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|allowPrivateIPs|Whether to allow WebHook URLs that resolve to Private IP address ranges (vs. internet addresses)|`boolean`|`true` +|connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` +|headers|Adds custom headers to HTTP requests|`map[string]string`|`` +|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms` +|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100` +|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` + +## webhooks.auth + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|password|Password|`string`|`` +|username|Username|`string`|`` + +## webhooks.proxy + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|url|Optional HTTP proxy to use when invoking WebHooks|`string`|`` + +## webhooks.retry + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|count|The maximum number of times to retry|`int`|`5` +|enabled|Enables retries|`boolean`|`false` +|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` +|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` \ No newline at end of file diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index e06ac3c2..b037d410 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -1004,5 +1004,5 @@ func TestActionRetryBlock(t *testing.T) { assert.Regexp(t, "FF00154", err) <-done - assert.Greater(t, callCount, 1) + assert.Greater(t, callCount, 0) } diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go index c6fc4d63..960bca6d 100644 --- a/internal/tmmsgs/en_config_descriptions.go +++ b/internal/tmmsgs/en_config_descriptions.go @@ -27,12 +27,14 @@ var ffc = func(key, translation, fieldType string) i18n.ConfigMessageKey { //revive:disable var ( - ConfigAPIAddress = ffc("config.api.address", "Listener address for API", i18n.StringType) - ConfigAPIPort = ffc("config.api.port", "Listener port for API", i18n.IntType) - ConfigAPIPublicURL = ffc("config.api.publicURL", "External address callers should access API over", i18n.StringType) - ConfigAPIReadTimeout = ffc("config.api.readTimeout", "The maximum time to wait when reading from an HTTP connection", i18n.TimeDurationType) - ConfigAPIWriteTimeout = ffc("config.api.writeTimeout", "The maximum time to wait when writing to a HTTP connection", i18n.TimeDurationType) - ConfigAPIShutdownTimeout = ffc("config.api.shutdownTimeout", "The maximum amount of time to wait for any open HTTP requests to finish before shutting down the HTTP server", i18n.TimeDurationType) + ConfigAPIDefaultRequestTimeout = ffc("config.api.defaultRequestTimeout", "Default server-side request timeout for API calls", i18n.TimeDurationType) + ConfigAPIMaxRequestTimeout = ffc("config.api.maxRequestTimeout", "Maximum server-side request timeout a caller can request with a Request-Timeout header", i18n.TimeDurationType) + ConfigAPIAddress = ffc("config.api.address", "Listener address for API", i18n.StringType) + ConfigAPIPort = ffc("config.api.port", "Listener port for API", i18n.IntType) + ConfigAPIPublicURL = ffc("config.api.publicURL", "External address callers should access API over", i18n.StringType) + ConfigAPIReadTimeout = ffc("config.api.readTimeout", "The maximum time to wait when reading from an HTTP connection", i18n.TimeDurationType) + ConfigAPIWriteTimeout = ffc("config.api.writeTimeout", "The maximum time to wait when writing to a HTTP connection", i18n.TimeDurationType) + ConfigAPIShutdownTimeout = ffc("config.api.shutdownTimeout", "The maximum amount of time to wait for any open HTTP requests to finish before shutting down the HTTP server", i18n.TimeDurationType) ConfigConfirmationsBlockCacheSize = ffc("config.confirmations.blockCacheSize", "The maximum number of block headers to keep in the cache", i18n.IntType) ConfigConfirmationsBlockPollingInterval = ffc("config.confirmations.blockPollingInterval", "How often to poll for new block headers", i18n.TimeDurationType) @@ -68,4 +70,24 @@ var ( ConfigPolicyEngineSimpleGasOracleProxyURL = ffc("config.policyengine.simple.gasOracle.proxy.url", "Optional HTTP proxy URL to use for the Gas Oracle REST API", i18n.StringType) ConfigPolicyEngineSimpleGasOracleMethod = ffc("config.policyengine.simple.gasOracle.method", "The HTTP Method to use when invoking the Gas Oracle REST API", i18n.StringType) ConfigPolicyEngineSimpleGasOracleQueryInterval = ffc("config.policyengine.simple.gasOracle.queryInterval", "The minimum interval between queries to the Gas Oracle", i18n.TimeDurationType) + + ConfigEventStreamsDefaultsBatchSize = ffc("config.eventstreams.defaults.batchSize", "Default batch size for newly created event streams", i18n.IntType) + ConfigEventStreamsDefaultsBatchTimeout = ffc("config.eventstreams.defaults.batchTimeout", "Default batch timeout for newly created event streams", i18n.TimeDurationType) + ConfigEventStreamsDefaultsErrorHandling = ffc("config.eventstreams.defaults.errorHandling", "Default error handling for newly created event streams", "'skip' or 'block'") + ConfigEventStreamsDefaultsRetryTimeout = ffc("config.eventstreams.defaults.retryTimeout", "Default retry timeout for newly created event streams", i18n.TimeDurationType) + ConfigEventStreamsDefaultsBlockedRetryDelay = ffc("config.eventstreams.defaults.blockedRetryDelay", "Default blocked retry delay for newly created event streams", i18n.TimeDurationType) + ConfigEventStreamsDefaultsWebhookRequestTimeout = ffc("config.eventstreams.defaults.webhookRequestTimeout", "Default WebHook request timeout for newly created event streams", i18n.TimeDurationType) + ConfigEventStreamsDefaultsWebsocketDistributionMode = ffc("config.eventstreams.defaults.websocketDistributionMode", "Default WebSocket distribution mode for newly created event streams", "'load_balance' or 'broadcast'") + ConfigEventStreamsRetryInitDelay = ffc("config.eventstreams.retry.initialDelay", "Initial retry delay", i18n.TimeDurationType) + ConfigEventStreamsRetryMaxDelay = ffc("config.eventstreams.retry.maxDelay", "Maximum delay between retries", i18n.TimeDurationType) + ConfigEventStreamsRetryFactor = ffc("config.eventstreams.retry.factor", "Factor to increase the delay by, between each retry", i18n.FloatType) + + ConfigPersistenceType = ffc("config.persistence.type", "The type of persistence to use", "Only 'leveldb' currently supported") + ConfigPersistenceLevelDBPath = ffc("config.persistence.leveldb.path", "The path for the LevelDB persistence directory", i18n.StringType) + ConfigPersistenceLevelDBMaxHandles = ffc("config.persistence.leveldb.maxHandles", "The maximum number of cached file handles LevelDB should keep open", i18n.IntType) + ConfigPersistenceLevelDBSyncWrites = ffc("config.persistence.leveldb.syncWrites", "Whether to synchronously perform writes to the storage", i18n.BooleanType) + + ConfigWebhooksAllowPrivateIPs = ffc("config.webhooks.allowPrivateIPs", "Whether to allow WebHook URLs that resolve to Private IP address ranges (vs. internet addresses)", i18n.BooleanType) + ConfigWebhooksURL = ffc("config.webhooks.url", "Unused (overridden by the WebHook configuration of an individual event stream)", i18n.IgnoredType) + ConfigWebhooksProxyURL = ffc("config.webhooks.proxy.url", "Optional HTTP proxy to use when invoking WebHooks", i18n.StringType) ) diff --git a/pkg/fftm/config_docs_generate_test.go b/pkg/fftm/config_docs_generate_test.go new file mode 100644 index 00000000..0a9f0f95 --- /dev/null +++ b/pkg/fftm/config_docs_generate_test.go @@ -0,0 +1,43 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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. + +//go:build docs +// +build docs + +package fftm + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/stretchr/testify/assert" +) + +func TestGenerateConfigDocs(t *testing.T) { + // Initialize config of all plugins + InitConfig() + f, err := os.Create(filepath.Join("..", "..", "config.md")) + assert.NoError(t, err) + generatedConfig, err := config.GenerateConfigMarkdown(context.Background(), config.GetKnownKeys()) + assert.NoError(t, err) + _, err = f.Write(generatedConfig) + assert.NoError(t, err) + err = f.Close() + assert.NoError(t, err) +} diff --git a/pkg/fftm/config_docs_test.go b/pkg/fftm/config_docs_test.go new file mode 100644 index 00000000..8b3fb763 --- /dev/null +++ b/pkg/fftm/config_docs_test.go @@ -0,0 +1,46 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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. + +//go:build !docs +// +build !docs + +package fftm + +import ( + "context" + "crypto/sha1" + "os" + "path/filepath" + "testing" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/stretchr/testify/assert" +) + +func TestConfigDocsUpToDate(t *testing.T) { + // Initialize config of all plugins + InitConfig() + generatedConfig, err := config.GenerateConfigMarkdown(context.Background(), config.GetKnownKeys()) + assert.NoError(t, err) + configOnDisk, err := os.ReadFile(filepath.Join("..", "..", "config.md")) + assert.NoError(t, err) + + generatedConfigHash := sha1.New() + generatedConfigHash.Write(generatedConfig) + configOnDiskHash := sha1.New() + configOnDiskHash.Write(configOnDisk) + assert.Equal(t, configOnDiskHash.Sum(nil), generatedConfigHash.Sum(nil), "The config reference docs generated by the code did not match the config.md file in git. Did you forget to run `make docs`?") +} diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index 6a438dfc..13790b86 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -84,6 +84,11 @@ type manager struct { enableChangeListener bool } +func InitConfig() { + tmconfig.Reset() + events.InitDefaults() +} + func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { var err error m := newManager(ctx, connector) @@ -114,7 +119,6 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { } func newManager(ctx context.Context, connector ffcapi.API) *manager { - events.InitDefaults() m := &manager{ connector: connector, ffCoreClient: ffresty.New(ctx, tmconfig.FFCoreConfig), diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 62f8be47..1acd395c 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -51,7 +51,7 @@ const testManagerName = "unittest" func strPtr(s string) *string { return &s } func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...string) (string, *manager, func()) { - tmconfig.Reset() + InitConfig() policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) dir, err := ioutil.TempDir("", "ldb_*") assert.NoError(t, err) @@ -94,7 +94,7 @@ func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...strin } func newMockPersistenceManager(t *testing.T) (*persistencemocks.Persistence, *ffcapimocks.API, *manager) { - tmconfig.Reset() + InitConfig() mca := &ffcapimocks.API{} mps := &persistencemocks.Persistence{} m := newManager(context.Background(), mca) From a96f657aed87a4097ec80fbd5e706ffcb6dbc33b Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 20 Jun 2022 12:53:51 -0400 Subject: [PATCH 23/95] Remove unused config Signed-off-by: Peter Broadhurst --- config.md | 6 ------ internal/tmconfig/tmconfig.go | 2 -- internal/tmmsgs/en_config_descriptions.go | 4 ---- 3 files changed, 12 deletions(-) diff --git a/config.md b/config.md index 894699b0..fee4ee8f 100644 --- a/config.md +++ b/config.md @@ -50,12 +50,6 @@ nav_order: 2 |required|Number of confirmations required to consider a transaction/event final|`int`|`20` |staleReceiptTimeout|Duration after which to force a receipt check for a pending transaction|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1m` -## connector - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|variant|The variant is the overall category of blockchain connector, defining things like how input/output definitions are passed|`string`|`evm` - ## cors |Key|Description|Type|Default Value| diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 13807bf4..6b41d08f 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -29,7 +29,6 @@ var ffc = config.AddRootKey var ( ManagerName = ffc("manager.name") - ConnectorVariant = ffc("connector.variant") ConfirmationsRequired = ffc("confirmations.required") ConfirmationsBlockCacheSize = ffc("confirmations.blockCacheSize") ConfirmationsBlockPollingInterval = ffc("confirmations.blockPollingInterval") @@ -81,7 +80,6 @@ func setDefaults() { core.OpTypeTokenCreatePool.String(), }) viper.SetDefault(string(OperationsFullScanStartupMaxRetries), 10) - viper.SetDefault(string(ConnectorVariant), "evm") viper.SetDefault(string(ConfirmationsRequired), 20) viper.SetDefault(string(ConfirmationsBlockCacheSize), 1000) viper.SetDefault(string(ConfirmationsBlockPollingInterval), "3s") diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go index 960bca6d..3305e92e 100644 --- a/internal/tmmsgs/en_config_descriptions.go +++ b/internal/tmmsgs/en_config_descriptions.go @@ -42,10 +42,6 @@ var ( ConfigConfirmationsRequired = ffc("config.confirmations.required", "Number of confirmations required to consider a transaction/event final", i18n.IntType) ConfigConfirmationsStaleReceiptTimeout = ffc("config.confirmations.staleReceiptTimeout", "Duration after which to force a receipt check for a pending transaction", i18n.TimeDurationType) - ConfigConnectorURL = ffc("config.connector.url", "The URL of the blockchain connector", i18n.StringType) - ConfigConnectorVariant = ffc("config.connector.variant", "The variant is the overall category of blockchain connector, defining things like how input/output definitions are passed", i18n.StringType) - ConfigConnectorProxyURL = ffc("config.connector.proxy.url", "Optional HTTP proxy URL to use for the blockchain connector", i18n.StringType) - ConfigFFCoreURL = ffc("config.ffcore.url", "The URL of the FireFly core admin API server to connect to", i18n.StringType) ConfigFFCoreProxyURL = ffc("config.ffcore.proxy.url", "Optional HTTP proxy URL to use for the FireFly core admin API server", i18n.StringType) From 3c065973df055b1ee3670f559231c440f37b5922 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 20 Jun 2022 12:59:05 -0400 Subject: [PATCH 24/95] Make policy engine base config fixed Signed-off-by: Peter Broadhurst --- pkg/fftm/manager_test.go | 10 +++++----- pkg/policyengines/registry.go | 5 +++-- pkg/policyengines/registry_test.go | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 1acd395c..86afd768 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -52,7 +52,7 @@ func strPtr(s string) *string { return &s } func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...string) (string, *manager, func()) { InitConfig() - policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) dir, err := ioutil.TempDir("", "ldb_*") assert.NoError(t, err) config.Set(tmconfig.PersistenceLevelDBPath, dir) @@ -131,7 +131,7 @@ func TestNewManagerBadHttpConfig(t *testing.T) { config.Set(tmconfig.ManagerName, "test") tmconfig.APIConfig.Set(httpserver.HTTPConfAddress, "::::") - policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") _, err := NewManager(context.Background(), nil) @@ -150,7 +150,7 @@ func TestNewManagerBadLevelDBConfig(t *testing.T) { config.Set(tmconfig.PersistenceLevelDBPath, tmpFile.Name) tmconfig.APIConfig.Set(httpserver.HTTPConfPort, "0") - policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") _, err = NewManager(context.Background(), nil) @@ -165,7 +165,7 @@ func TestNewManagerBadPersistenceConfig(t *testing.T) { config.Set(tmconfig.PersistenceType, "wrong") tmconfig.APIConfig.Set(httpserver.HTTPConfPort, "0") - policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") _, err := NewManager(context.Background(), nil) @@ -179,7 +179,7 @@ func TestNewManagerFireFlyURLConfig(t *testing.T) { config.Set(tmconfig.ManagerName, "test") tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, ":::!badurl") - policyengines.RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") _, err := NewManager(context.Background(), nil) diff --git a/pkg/policyengines/registry.go b/pkg/policyengines/registry.go index 83b91c6e..f375f9e2 100644 --- a/pkg/policyengines/registry.go +++ b/pkg/policyengines/registry.go @@ -21,6 +21,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) @@ -41,9 +42,9 @@ type Factory interface { NewPolicyEngine(ctx context.Context, conf config.Section) (policyengine.PolicyEngine, error) } -func RegisterEngine(baseConfig config.Section, factory Factory) string { +func RegisterEngine(factory Factory) string { name := factory.Name() policyEngines[name] = factory - factory.InitConfig(baseConfig.SubSection(name)) + factory.InitConfig(tmconfig.PolicyEngineBaseConfig.SubSection(name)) return name } diff --git a/pkg/policyengines/registry_test.go b/pkg/policyengines/registry_test.go index dfbfecc4..e50d82d2 100644 --- a/pkg/policyengines/registry_test.go +++ b/pkg/policyengines/registry_test.go @@ -28,7 +28,7 @@ import ( func TestRegistry(t *testing.T) { tmconfig.Reset() - RegisterEngine(tmconfig.PolicyEngineBaseConfig, &simple.PolicyEngineFactory{}) + RegisterEngine(&simple.PolicyEngineFactory{}) tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "12345") p, err := NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBaseConfig, "simple") From 23a5385927e1b93259651392fcb3d57867ff6145 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 20 Jun 2022 13:07:48 -0400 Subject: [PATCH 25/95] Default gas estimation to the connector in simple policy Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 1 + pkg/fftm/manager_test.go | 1 + pkg/policyengines/simple/config.go | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 36a6df06..93f5ae67 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,6 +60,7 @@ "policyengines", "policyloop", "protocolid", + "restapi", "resty", "santhosh", "secp", diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 86afd768..50f5beac 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -56,6 +56,7 @@ func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...strin dir, err := ioutil.TempDir("", "ldb_*") assert.NoError(t, err) config.Set(tmconfig.PersistenceLevelDBPath, dir) + tmconfig.PolicyEngineBaseConfig.SubSection("simple").SubSection(simple.GasOracleConfig).Set(simple.GasOracleMode, simple.GasOracleModeDisabled) ffCoreServer := httptest.NewServer(ffCoreHandler) tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", ffCoreServer.Listener.Addr())) diff --git a/pkg/policyengines/simple/config.go b/pkg/policyengines/simple/config.go index d0c3f76e..bf636d12 100644 --- a/pkg/policyengines/simple/config.go +++ b/pkg/policyengines/simple/config.go @@ -43,7 +43,7 @@ const ( defaultWarnInterval = "15m" defaultGasOracleQueryInterval = "5m" defaultGasOracleMethod = http.MethodGet - defaultGasOracleMode = GasOracleModeDisabled + defaultGasOracleMode = GasOracleModeConnector ) func (f *PolicyEngineFactory) InitConfig(conf config.Section) { From b21277ba304401d9bfc0c9c2a83d4c145db348b3 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 20 Jun 2022 13:11:16 -0400 Subject: [PATCH 26/95] Explicit error when missing LevelDB path Signed-off-by: Peter Broadhurst --- internal/persistence/leveldb_persistence.go | 5 +++++ internal/persistence/leveldb_persistence_test.go | 9 +++++++++ internal/tmmsgs/en_error_messges.go | 1 + 3 files changed, 15 insertions(+) diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index 974296bf..a324cfd7 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -25,8 +25,10 @@ import ( "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb/opt" @@ -40,6 +42,9 @@ type leveldbPersistence struct { func NewLevelDBPersistence(ctx context.Context) (Persistence, error) { dbPath := config.GetString(tmconfig.PersistenceLevelDBPath) + if dbPath == "" { + return nil, i18n.NewError(ctx, tmmsgs.MsgLevelDBPathMissing) + } db, err := leveldb.OpenFile(dbPath, &opt.Options{ OpenFilesCacheCapacity: config.GetInt(tmconfig.PersistenceLevelDBMaxHandles), }) diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index 19749802..67f9d33e 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -62,6 +62,15 @@ func newTestLevelDBPersistence(t *testing.T) (*leveldbPersistence, func()) { func strPtr(s string) *string { return &s } +func TestLevelDBInitMissingPath(t *testing.T) { + + tmconfig.Reset() + + _, err := NewLevelDBPersistence(context.Background()) + assert.Regexp(t, "FF21050", err) + +} + func TestLevelDBInitFail(t *testing.T) { file, err := ioutil.TempFile("", "ldb_*") assert.NoError(t, err) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 04312ad2..e92f98f5 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -65,4 +65,5 @@ var ( MsgDuplicateStreamName = ffe("FF21047", "Duplicate event stream name '%s' used by stream '%s'", 409) MsgMissingID = ffe("FF21048", "ID is required", 400) MsgPersistenceInitFail = ffe("FF21049", "Failed to initialize '%s' persistence: %s") + MsgLevelDBPathMissing = ffe("FF21050", "Path must be supplied for LevelDB persistence") ) From e1f12c0badf1f6f3cef65335c0ebbffca5d0165b Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 20 Jun 2022 16:29:10 -0400 Subject: [PATCH 27/95] In/out structs for verify options like all other FFCAPI functions Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 28 ++++++----- internal/events/eventstream_test.go | 46 ++++++++++--------- mocks/ffcapimocks/api.go | 30 ++++++------ pkg/apitypes/api_types.go | 2 +- pkg/ffcapi/api.go | 6 +-- pkg/ffcapi/event_listener_add.go | 15 ++++++ .../route_delete_eventstream_listener_test.go | 3 +- pkg/fftm/route_delete_subscription_test.go | 3 +- .../route_get_eventstream_listener_test.go | 3 +- .../route_get_eventstream_listeners_test.go | 3 +- pkg/fftm/route_get_subscription_test.go | 3 +- pkg/fftm/route_get_subscriptions_test.go | 3 +- .../route_patch_eventstream_listener_test.go | 3 +- pkg/fftm/route_patch_subscription_test.go | 3 +- .../route_post_eventstream_listeners_test.go | 3 +- pkg/fftm/route_post_subscriptions_test.go | 3 +- pkg/fftm/stream_management_test.go | 12 ++--- 17 files changed, 90 insertions(+), 79 deletions(-) diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 91e017c6..69ac0501 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -290,40 +290,46 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.L } // The connector needs to validate the options - signature, mergedOptions, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.ListenerOptions{ - FromBlock: spec.FromBlock, - }, spec.Options) + fromBlock := ffcapi.FromBlockLatest + if spec.FromBlock != nil { + fromBlock = *spec.FromBlock + } + res, _, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.EventListenerVerifyOptionsRequest{ + FromBlock: fromBlock, + Filters: spec.Filters, + Options: spec.Options, + }) if err != nil { return i18n.NewError(ctx, tmmsgs.MsgBadListenerOptions, err) } // We update the spec object in-place for the signature and resolved options - spec.Signature = signature - spec.Options = &mergedOptions + spec.Signature = res.ResolvedSignature + spec.Options = &res.ResolvedOptions if spec.Name == "" { - spec.Name = signature + spec.Name = spec.Signature } // Check if this is a new listener, an update, or a no-op es.mux.Lock() l, exists := es.listeners[*spec.ID] if exists { - if mergedOptions == l.options && safeCompareFilterList(spec.Filters, l.filters) { + if res.ResolvedOptions == l.options && safeCompareFilterList(spec.Filters, l.filters) { log.L(ctx).Infof("Event listener '%s' already configured on stream, with no changes", l.id) es.mux.Unlock() return nil } log.L(ctx).Infof("Listener '%s' configuration updated, it will be restarted", l.id) - l.options = mergedOptions + l.options = res.ResolvedOptions + l.signature = res.ResolvedSignature l.filters = spec.Filters - l.signature = signature } else { l = &listener{ es: es, id: spec.ID, - options: mergedOptions, + options: res.ResolvedOptions, filters: spec.Filters, - signature: signature, + signature: res.ResolvedSignature, } es.listeners[*spec.ID] = l } diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index b037d410..cb4fff50 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -38,6 +38,8 @@ import ( "github.com/stretchr/testify/mock" ) +func strPtr(s string) *string { return &s } + func testESConf(t *testing.T, j string) (spec *apitypes.EventStream) { err := json.Unmarshal([]byte(j), &spec) assert.NoError(t, err) @@ -264,16 +266,17 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { DeprecatedAddress: &addr, DeprecatedEvent: fftypes.JSONAnyPtr(`{"event":"definition"}`), Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), - FromBlock: "12345", + FromBlock: strPtr("12345"), } mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.MatchedBy(func(standard *ffcapi.ListenerOptions) bool { - return standard.FromBlock == "12345" - }), mock.MatchedBy(func(customOptions *fftypes.JSONAny) bool { - return customOptions.JSONObject().GetString("option1") == "value1" - })).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventListenerVerifyOptionsRequest) bool { + return req.FromBlock == "12345" && req.Options.JSONObject().GetString("option1") == "value1" + })).Return(&ffcapi.EventListenerVerifyOptionsResponse{ + ResolvedSignature: "EventSig(uint256)", + ResolvedOptions: *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), + }, ffcapi.ErrorReason(""), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -369,16 +372,17 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { `{"event":"definition2"}`, }, Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), - FromBlock: "12345", + FromBlock: strPtr("12345"), } mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.MatchedBy(func(standard *ffcapi.ListenerOptions) bool { - return standard.FromBlock == "12345" - }), mock.MatchedBy(func(customOptions *fftypes.JSONAny) bool { - return customOptions.JSONObject().GetString("option1") == "value1" - })).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventListenerVerifyOptionsRequest) bool { + return req.FromBlock == "12345" && req.Options.JSONObject().GetString("option1") == "value1" + })).Return(&ffcapi.EventListenerVerifyOptionsResponse{ + ResolvedSignature: "EventSig(uint256)", + ResolvedOptions: *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), + }, ffcapi.ErrorReason(""), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -449,7 +453,7 @@ func TestConnectorRejectListener(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("", *fftypes.JSONAnyPtr(`{}`), fmt.Errorf("pop")) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) err := es.AddOrUpdateListener(es.bgCtx, l) assert.Regexp(t, "FF21040.*pop", err) @@ -471,7 +475,7 @@ func TestUpdateStreamStarted(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -530,7 +534,7 @@ func TestAddRemoveListener(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -578,7 +582,7 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil).Times(3) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -643,7 +647,7 @@ func TestUpdateListenerFail(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil).Times(3) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -716,7 +720,7 @@ func TestUpdateStreamRestartFail(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -769,7 +773,7 @@ func TestUpdateStreamStopFail(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { @@ -837,12 +841,12 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { Name: "ut_listener", Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), - FromBlock: "12345", + FromBlock: strPtr("12345"), } mfc := es.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) started := make(chan *ffcapi.EventListenerAddRequest, 1) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index 1e8055ab..4054b026 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -5,9 +5,7 @@ package ffcapimocks import ( context "context" - fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - mock "github.com/stretchr/testify/mock" ) @@ -136,27 +134,29 @@ func (_m *API) EventListenerRemove(ctx context.Context, req *ffcapi.EventListene return r0, r1, r2 } -// EventListenerVerifyOptions provides a mock function with given fields: ctx, standardOptions, customOptions -func (_m *API) EventListenerVerifyOptions(ctx context.Context, standardOptions *ffcapi.ListenerOptions, customOptions *fftypes.JSONAny) (string, fftypes.JSONAny, error) { - ret := _m.Called(ctx, standardOptions, customOptions) +// EventListenerVerifyOptions provides a mock function with given fields: ctx, req +func (_m *API) EventListenerVerifyOptions(ctx context.Context, req *ffcapi.EventListenerVerifyOptionsRequest) (*ffcapi.EventListenerVerifyOptionsResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) - var r0 string - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.ListenerOptions, *fftypes.JSONAny) string); ok { - r0 = rf(ctx, standardOptions, customOptions) + var r0 *ffcapi.EventListenerVerifyOptionsResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventListenerVerifyOptionsRequest) *ffcapi.EventListenerVerifyOptionsResponse); ok { + r0 = rf(ctx, req) } else { - r0 = ret.Get(0).(string) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.EventListenerVerifyOptionsResponse) + } } - var r1 fftypes.JSONAny - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.ListenerOptions, *fftypes.JSONAny) fftypes.JSONAny); ok { - r1 = rf(ctx, standardOptions, customOptions) + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventListenerVerifyOptionsRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) } else { - r1 = ret.Get(1).(fftypes.JSONAny) + r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.ListenerOptions, *fftypes.JSONAny) error); ok { - r2 = rf(ctx, standardOptions, customOptions) + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventListenerVerifyOptionsRequest) error); ok { + r2 = rf(ctx, req) } else { r2 = ret.Error(2) } diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index c80293e9..5e57f92d 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -110,7 +110,7 @@ type Listener struct { Filters []fftypes.JSONAny `ffstruct:"listener" json:"filters"` Options *fftypes.JSONAny `ffstruct:"listener" json:"options"` Signature string `ffstruct:"listener" json:"signature,omitempty" ffexcludeinput:"true"` - FromBlock string `ffstruct:"listener" json:"fromBlock,omitempty"` + FromBlock *string `ffstruct:"listener" json:"fromBlock,omitempty"` } // UUIDVersion1 returns a version 1 UUID - where the alphanumeric sequence is assured to be ascending based on the order of generation diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index c1cf4cc0..b6444ee0 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -53,7 +53,7 @@ type API interface { TransactionSend(ctx context.Context, req *TransactionSendRequest) (*TransactionSendResponse, ErrorReason, error) // EventListenerVerifyOptions validates the configuration options for a listener, applying any defaults needed by the connector, and returning the update options for FFTM to persist - EventListenerVerifyOptions(ctx context.Context, standardOptions *ListenerOptions, customOptions *fftypes.JSONAny) (signature string, options fftypes.JSONAny, err error) + EventListenerVerifyOptions(ctx context.Context, req *EventListenerVerifyOptionsRequest) (*EventListenerVerifyOptionsResponse, ErrorReason, error) // EventListenerAdd begins/resumes listening on set of events that must be consistently ordered. Blockchain specific signatures of the events are included, along with initial conditions (initial block number etc.), and the last stored checkpoint (if any) EventListenerAdd(ctx context.Context, req *EventListenerAddRequest) (*EventListenerAddResponse, ErrorReason, error) @@ -65,10 +65,6 @@ type API interface { NewBlockHashes() <-chan *BlockHashEvent } -type ListenerOptions struct { - FromBlock string `json:"fromBlock"` -} - type BlockHashEvent struct { BlockHashes []string `json:"blockHash"` // zero or more hashes (can be nil) GapPotential bool `json:"gapPotential,omitempty"` // when true, the caller cannot be sure if blocks have been missed (use on reconnect of a websocket for example) diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go index 221c1b2e..bf36c9a4 100644 --- a/pkg/ffcapi/event_listener_add.go +++ b/pkg/ffcapi/event_listener_add.go @@ -20,6 +20,21 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" ) +const ( + FromBlockLatest = "latest" +) + +type EventListenerVerifyOptionsRequest struct { + FromBlock string + Filters []fftypes.JSONAny + Options *fftypes.JSONAny +} + +type EventListenerVerifyOptionsResponse struct { + ResolvedSignature string + ResolvedOptions fftypes.JSONAny +} + type EventListenerAddRequest struct { ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event Filters []fftypes.JSONAny // The blockchain specific list of filters. The top-level array is an OR list. The semantics within each entry is defined by the blockchain diff --git a/pkg/fftm/route_delete_eventstream_listener_test.go b/pkg/fftm/route_delete_eventstream_listener_test.go index 39133161..362b2d80 100644 --- a/pkg/fftm/route_delete_eventstream_listener_test.go +++ b/pkg/fftm/route_delete_eventstream_listener_test.go @@ -22,7 +22,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -39,7 +38,7 @@ func TestDeleteEventStreamListener(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) diff --git a/pkg/fftm/route_delete_subscription_test.go b/pkg/fftm/route_delete_subscription_test.go index 213b3692..660f216b 100644 --- a/pkg/fftm/route_delete_subscription_test.go +++ b/pkg/fftm/route_delete_subscription_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -38,7 +37,7 @@ func TestDeleteSubscription(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) diff --git a/pkg/fftm/route_get_eventstream_listener_test.go b/pkg/fftm/route_get_eventstream_listener_test.go index a4c7e4d3..f7d1be35 100644 --- a/pkg/fftm/route_get_eventstream_listener_test.go +++ b/pkg/fftm/route_get_eventstream_listener_test.go @@ -22,7 +22,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -39,7 +38,7 @@ func TestGetEventStreamsListener(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_get_eventstream_listeners_test.go b/pkg/fftm/route_get_eventstream_listeners_test.go index 88691e09..8a0508cb 100644 --- a/pkg/fftm/route_get_eventstream_listeners_test.go +++ b/pkg/fftm/route_get_eventstream_listeners_test.go @@ -22,7 +22,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -39,7 +38,7 @@ func TestGetEventStreamListeners(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_get_subscription_test.go b/pkg/fftm/route_get_subscription_test.go index e2ac4ab7..9b99354f 100644 --- a/pkg/fftm/route_get_subscription_test.go +++ b/pkg/fftm/route_get_subscription_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -38,7 +37,7 @@ func TestGetSubscription(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_get_subscriptions_test.go b/pkg/fftm/route_get_subscriptions_test.go index 0dcafc46..20b14f59 100644 --- a/pkg/fftm/route_get_subscriptions_test.go +++ b/pkg/fftm/route_get_subscriptions_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -38,7 +37,7 @@ func TestGetSubscriptions(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_patch_eventstream_listener_test.go b/pkg/fftm/route_patch_eventstream_listener_test.go index d0a7e340..00ccf228 100644 --- a/pkg/fftm/route_patch_eventstream_listener_test.go +++ b/pkg/fftm/route_patch_eventstream_listener_test.go @@ -22,7 +22,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -39,7 +38,7 @@ func TestPatchEventStreamListener(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_patch_subscription_test.go b/pkg/fftm/route_patch_subscription_test.go index fd33363c..f5378c6c 100644 --- a/pkg/fftm/route_patch_subscription_test.go +++ b/pkg/fftm/route_patch_subscription_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -38,7 +37,7 @@ func TestPatchSubscription(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_post_eventstream_listeners_test.go b/pkg/fftm/route_post_eventstream_listeners_test.go index 0a060f78..89b973b9 100644 --- a/pkg/fftm/route_post_eventstream_listeners_test.go +++ b/pkg/fftm/route_post_eventstream_listeners_test.go @@ -22,7 +22,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -39,7 +38,7 @@ func TestPostEventStreamListeners(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_post_subscriptions_test.go b/pkg/fftm/route_post_subscriptions_test.go index c193fd05..f6e2a861 100644 --- a/pkg/fftm/route_post_subscriptions_test.go +++ b/pkg/fftm/route_post_subscriptions_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -38,7 +37,7 @@ func TestPostSubscriptions(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go index fd4d065c..98eb0b45 100644 --- a/pkg/fftm/stream_management_test.go +++ b/pkg/fftm/stream_management_test.go @@ -35,7 +35,7 @@ func TestRestoreStreamsAndListenersOK(t *testing.T) { defer done() mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() @@ -116,7 +116,7 @@ func TestRestoreListenersStartFail(t *testing.T) { defer done() mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) falsy := false @@ -141,7 +141,7 @@ func TestDeleteStartedListener(t *testing.T) { defer done() mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) @@ -312,7 +312,7 @@ func TestCreateOrUpdateListenerFail(t *testing.T) { mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) @@ -330,7 +330,7 @@ func TestCreateOrUpdateListenerWriteFail(t *testing.T) { mp.On("WriteListener", m.ctx, mock.Anything).Return(fmt.Errorf("pop")) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) @@ -370,7 +370,7 @@ func TestDeleteListenerFail(t *testing.T) { mp.On("WriteListener", m.ctx, mock.Anything).Return(nil) mfc := m.connector.(*ffcapimocks.API) - mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything, mock.Anything).Return("EventSig(uint256)", *fftypes.JSONAnyPtr(`{}`), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) From df21dce9f7ec206284fb017126e8b4321b1f0dbb Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 22 Jun 2022 10:33:42 -0400 Subject: [PATCH 28/95] Add reset functionality and ensure correct PATCH semantics for listeners Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 200 ++++++++++------ internal/events/eventstream_test.go | 226 +++++++++++++++--- internal/events/listener.go | 14 +- internal/tmmsgs/en_error_messges.go | 6 +- mocks/eventsmocks/stream.go | 10 +- pkg/apitypes/api_types.go | 2 +- .../route_delete_eventstream_listener_test.go | 2 +- pkg/fftm/route_delete_subscription_test.go | 2 +- .../route_get_eventstream_listener_test.go | 2 +- .../route_get_eventstream_listeners_test.go | 6 +- pkg/fftm/route_get_subscription_test.go | 2 +- pkg/fftm/route_get_subscriptions_test.go | 4 +- pkg/fftm/route_patch_eventstream_listener.go | 2 +- .../route_patch_eventstream_listener_test.go | 6 +- pkg/fftm/route_patch_subscription.go | 2 +- pkg/fftm/route_patch_subscription_test.go | 6 +- .../route_post_eventstream_listener_reset.go | 45 ++++ ...te_post_eventstream_listener_reset_test.go | 71 ++++++ .../route_post_eventstream_listeners_test.go | 2 +- pkg/fftm/route_post_subscription_reset.go | 45 ++++ .../route_post_subscription_reset_test.go | 70 ++++++ pkg/fftm/route_post_subscriptions_test.go | 2 +- pkg/fftm/routes.go | 2 + pkg/fftm/stream_management.go | 22 +- pkg/fftm/stream_management_test.go | 20 +- 25 files changed, 601 insertions(+), 170 deletions(-) create mode 100644 pkg/fftm/route_post_eventstream_listener_reset.go create mode 100644 pkg/fftm/route_post_eventstream_listener_reset_test.go create mode 100644 pkg/fftm/route_post_subscription_reset.go create mode 100644 pkg/fftm/route_post_subscription_reset_test.go diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 69ac0501..9152bbc3 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -36,14 +36,14 @@ import ( ) type Stream interface { - AddOrUpdateListener(ctx context.Context, s *apitypes.Listener) error // Add or update a listener - RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener - UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error // Apply definition updates (if there are changes) - Spec() *apitypes.EventStream // Retrieve the merged definition to persist - Status() apitypes.EventStreamStatus // Get the current status - Start(ctx context.Context) error // Start delivery - Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) - Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint + AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, updates *apitypes.Listener, reset bool) (*apitypes.Listener, error) // Add or update a listener + RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener + UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error // Apply definition updates (if there are changes) + Spec() *apitypes.EventStream // Retrieve the merged definition to persist + Status() apitypes.EventStreamStatus // Get the current status + Start(ctx context.Context) error // Start delivery + Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) + Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint } // esDefaults are the defaults for new event streams, read from the config once in InitDefaults() @@ -255,98 +255,150 @@ func (es *eventStream) UpdateSpec(ctx context.Context, updates *apitypes.EventSt return nil } -func safeCompareFilterList(a, b []fftypes.JSONAny) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false +func (es *eventStream) mergeListenerOptions(id *fftypes.UUID, updates *apitypes.Listener) *apitypes.Listener { + + es.mux.Lock() + l := es.listeners[*id] + var base *apitypes.Listener + now := fftypes.Now() + if l != nil { + base = l.spec + } else { + latest := ffcapi.FromBlockLatest + base = &apitypes.Listener{ + ID: id, + Created: now, + FromBlock: &latest, + StreamID: es.spec.ID, } } - return true -} + es.mux.Unlock() -func (es *eventStream) AddOrUpdateListener(ctx context.Context, spec *apitypes.Listener) (err error) { - log.L(ctx).Warnf("Initializing listener %s", spec.ID) + merged := *base - if spec.ID == nil { - spec.ID = apitypes.UUIDVersion1() - spec.Created = fftypes.Now() + if updates.Name != nil { + merged.Name = updates.Name } - spec.Updated = fftypes.Now() - // Allow a single "event" object to be specified instead of a filter, with an optional "address". - // This is migrated to the new syntax: `"filters":[{"address":"0x1235","event":{...}}]` - // (only expected to work for the eth connector that supports address/event) - if spec.Filters == nil && spec.DeprecatedEvent != nil { - migrationFilter := fftypes.JSONObject{ - "event": spec.DeprecatedEvent, - } - if spec.DeprecatedAddress != nil { - migrationFilter["address"] = *spec.DeprecatedAddress - } - spec.Filters = []fftypes.JSONAny{fftypes.JSONAny(migrationFilter.String())} + if updates.FromBlock != nil { + merged.FromBlock = updates.FromBlock + } + + if updates.Options != nil { + merged.Options = updates.Options + } else { + merged.Options = base.Options } - // The connector needs to validate the options - fromBlock := ffcapi.FromBlockLatest - if spec.FromBlock != nil { - fromBlock = *spec.FromBlock + if updates.Filters != nil { + merged.Filters = updates.Filters + } else { + // Allow a single "event" object to be specified instead of a filter, with an optional "address". + // This is migrated to the new syntax: `"filters":[{"address":"0x1235","event":{...}}]` + // (only expected to work for the eth connector that supports address/event) + if updates.DeprecatedEvent != nil { + migrationFilter := fftypes.JSONObject{ + "event": updates.DeprecatedEvent, + } + if updates.DeprecatedAddress != nil { + migrationFilter["address"] = *updates.DeprecatedAddress + } + merged.Filters = []fftypes.JSONAny{fftypes.JSONAny(migrationFilter.String())} + } else { + merged.Filters = base.Filters + } } + + return &merged + +} + +func (es *eventStream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, updates *apitypes.Listener, reset bool) (merged *apitypes.Listener, err error) { + log.L(ctx).Warnf("Initializing listener %s", id) + + // Merge the supplied options with defaults and any existing config. + spec := es.mergeListenerOptions(id, updates) + + // The connector needs to validate the options, building a set of options that are assured to be non-nil res, _, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.EventListenerVerifyOptionsRequest{ - FromBlock: fromBlock, + FromBlock: *spec.FromBlock, Filters: spec.Filters, Options: spec.Options, }) if err != nil { - return i18n.NewError(ctx, tmmsgs.MsgBadListenerOptions, err) + return nil, i18n.NewError(ctx, tmmsgs.MsgBadListenerOptions, err) } // We update the spec object in-place for the signature and resolved options spec.Signature = res.ResolvedSignature spec.Options = &res.ResolvedOptions - if spec.Name == "" { - spec.Name = spec.Signature + if spec.Name == nil || *spec.Name == "" { + sig := spec.Signature + spec.Name = &sig } - // Check if this is a new listener, an update, or a no-op + // Do the locked part - which checks if this is a new listener, or just an update to the options. + new, l, startedState, err := es.lockedListenerUpdate(ctx, spec, res, reset) + if err != nil { + return nil, err + } + if reset { + // Only safe to do the reset with the event stream stopped + if startedState != nil { + if err := es.Stop(ctx); err != nil { + return nil, err + } + } + // Clear out the checkpoint for this listener + if err := es.resetListenerCheckpoint(ctx, l); err != nil { + return nil, err + } + // Restart if we were started + if startedState != nil { + if err := es.Start(ctx); err != nil { + return nil, err + } + } + } else if new && startedState != nil { + // Start the new listener + return spec, l.start(startedState) + } + return spec, nil +} + +func (es *eventStream) resetListenerCheckpoint(ctx context.Context, l *listener) error { + cp, err := es.persistence.GetCheckpoint(ctx, es.spec.ID) + if err != nil || cp == nil { + return err + } + delete(cp.Listeners, *l.spec.ID) + return es.persistence.WriteCheckpoint(ctx, cp) +} + +func (es *eventStream) lockedListenerUpdate(ctx context.Context, spec *apitypes.Listener, res *ffcapi.EventListenerVerifyOptionsResponse, reset bool) (bool, *listener, *startedStreamState, error) { es.mux.Lock() + defer es.mux.Unlock() + l, exists := es.listeners[*spec.ID] - if exists { - if res.ResolvedOptions == l.options && safeCompareFilterList(spec.Filters, l.filters) { - log.L(ctx).Infof("Event listener '%s' already configured on stream, with no changes", l.id) - es.mux.Unlock() - return nil + switch { + case exists: + if res.ResolvedSignature != l.spec.Signature { + // We do not allow the filters to be updated, because that would lead to a confusing situation + // where the previously emitted events are a subset/mismatch to the filters configured now. + return false, nil, nil, i18n.NewError(ctx, tmmsgs.MsgFilterUpdateNotAllowed, l.spec.Signature, res.ResolvedSignature) } - log.L(ctx).Infof("Listener '%s' configuration updated, it will be restarted", l.id) - l.options = res.ResolvedOptions - l.signature = res.ResolvedSignature - l.filters = spec.Filters - } else { + l.spec = spec + case reset: + return false, nil, nil, i18n.NewError(ctx, tmmsgs.MsgResetStreamNotFound, spec.ID, es.spec.ID) + default: l = &listener{ - es: es, - id: spec.ID, - options: res.ResolvedOptions, - filters: spec.Filters, - signature: res.ResolvedSignature, + es: es, + spec: spec, } es.listeners[*spec.ID] = l } // Take a copy of the current started status, before unlocking - startedState := es.currentState - es.mux.Unlock() - if startedState == nil { - return nil - } - - // We need to restart any streams - if exists { - if err := l.stop(startedState); err != nil { - return err - } - } - return l.start(startedState) + return !exists, l, es.currentState, nil } func (es *eventStream) RemoveListener(ctx context.Context, id *fftypes.UUID) (err error) { @@ -403,7 +455,7 @@ func (es *eventStream) Start(ctx context.Context) error { var lastErr error for _, l := range es.listeners { if err := l.start(startedState); err != nil { - log.L(ctx).Errorf("Failed to start event listener %s: %s", l.id, err) + log.L(ctx).Errorf("Failed to start event listener %s: %s", l.spec.ID, err) lastErr = err } } @@ -510,7 +562,7 @@ func (es *eventStream) eventLoop(startedState *startedStreamState) { if update.ListenerID != nil { l = es.listeners[*update.ListenerID] if l != nil { - log.L(es.bgCtx).Debugf("%s (%s) event: %s", l.signature, l.id, event.ProtocolID) + log.L(es.bgCtx).Debugf("%s (%s) event: %s", l.spec.Signature, l.spec.ID, event.ProtocolID) batch.events = append(batch.events, &ffcapi.EventWithContext{ StreamID: es.spec.ID, ListenerID: update.ListenerID, @@ -596,7 +648,7 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * for lID, lCP := range batch.checkpoints { if l, ok := es.listeners[lID]; ok { l.checkpoint = lCP - log.L(es.bgCtx).Tracef("%s (%s) checkpoint: %s", l.signature, l.id, lCP) + log.L(es.bgCtx).Tracef("%s (%s) checkpoint: %s", l.spec.Signature, l.spec.ID, lCP) } } for lID, l := range es.listeners { diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index cb4fff50..bfd736cf 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -261,8 +261,8 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { addr := "0x12345" l := &apitypes.Listener{ - // ID will be allocated in AddOrUpdateListener - Name: "ut_listener", + ID: apitypes.UUIDVersion1(), + Name: strPtr("ut_listener"), DeprecatedAddress: &addr, DeprecatedEvent: fftypes.JSONAnyPtr(`{"event":"definition"}`), Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), @@ -295,7 +295,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { senderChannel, _, receiverChannel := mockWSChannels(es.wsChannels.(*wsmocks.WebSocketChannels)) - err := es.AddOrUpdateListener(es.bgCtx, l) + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) err = es.Start(es.bgCtx) @@ -399,9 +399,9 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" })).Return(nil) - err := es.AddOrUpdateListener(es.bgCtx, l) + l, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) - assert.Equal(t, "EventSig(uint256)", l.Name) // Defaulted + assert.Equal(t, "EventSig(uint256)", *l.Name) // Defaulted err = es.Start(es.bgCtx) assert.NoError(t, err) @@ -447,7 +447,7 @@ func TestConnectorRejectListener(t *testing.T) { l := &apitypes.Listener{ ID: fftypes.NewUUID(), - Name: "ut_listener", + Name: strPtr("ut_listener"), Filters: []fftypes.JSONAny{`badness`}, } @@ -455,7 +455,7 @@ func TestConnectorRejectListener(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) - err := es.AddOrUpdateListener(es.bgCtx, l) + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.Regexp(t, "FF21040.*pop", err) mfc.AssertExpectations(t) @@ -469,7 +469,7 @@ func TestUpdateStreamStarted(t *testing.T) { l := &apitypes.Listener{ ID: fftypes.NewUUID(), - Name: "ut_listener", + Name: strPtr("ut_listener"), Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, } @@ -487,7 +487,7 @@ func TestUpdateStreamStarted(t *testing.T) { return r.ID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() - err := es.AddOrUpdateListener(es.bgCtx, l) + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) err = es.Start(es.bgCtx) @@ -528,7 +528,7 @@ func TestAddRemoveListener(t *testing.T) { l := &apitypes.Listener{ ID: fftypes.NewUUID(), - Name: "ut_listener", + Name: strPtr("ut_listener"), Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, } @@ -546,7 +546,7 @@ func TestAddRemoveListener(t *testing.T) { return r.ID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() - err := es.AddOrUpdateListener(es.bgCtx, l) + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) err = es.Start(es.bgCtx) @@ -576,7 +576,7 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { l1 := &apitypes.Listener{ ID: fftypes.NewUUID(), - Name: "ut_listener", + Name: strPtr("ut_listener"), Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, } @@ -595,10 +595,11 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(nil, nil) msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(fmt.Errorf("pop")).Once() msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) - err := es.AddOrUpdateListener(es.bgCtx, l1) + _, err := es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) assert.NoError(t, err) err = es.Start(es.bgCtx) @@ -606,18 +607,17 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { r := <-started - // Double add the same - err = es.AddOrUpdateListener(es.bgCtx, l1) + // Double add the same - no change, already started + _, err = es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) assert.NoError(t, err) - l2 := &apitypes.Listener{ - ID: l1.ID, - Name: "ut_listener", + updates := &apitypes.Listener{ + Name: strPtr("ut_listener"), Filters: []fftypes.JSONAny{`{"event":"definition2"}`}, } - // Change the event definition - err = es.AddOrUpdateListener(es.bgCtx, l2) + // Change the event definition, with reset + _, err = es.AddOrUpdateListener(es.bgCtx, l1.ID, updates, true) assert.NoError(t, err) r = <-started @@ -641,7 +641,7 @@ func TestUpdateListenerFail(t *testing.T) { l1 := &apitypes.Listener{ ID: fftypes.NewUUID(), - Name: "ut_listener", + Name: strPtr("ut_listener"), Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, } @@ -660,7 +660,7 @@ func TestUpdateListenerFail(t *testing.T) { return r.ID.Equals(l1.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() - err := es.AddOrUpdateListener(es.bgCtx, l1) + _, err := es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) assert.NoError(t, err) err = es.Start(es.bgCtx) @@ -669,16 +669,16 @@ func TestUpdateListenerFail(t *testing.T) { r := <-started // Double add the same - err = es.AddOrUpdateListener(es.bgCtx, l1) + _, err = es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) assert.NoError(t, err) - l2 := &apitypes.Listener{ - ID: l1.ID, - Name: "ut_listener", - Filters: []fftypes.JSONAny{`{"event":"definition2"}`}, + updates := &apitypes.Listener{ + Name: strPtr("ut_listener"), + FromBlock: strPtr("0"), } - err = es.AddOrUpdateListener(es.bgCtx, l2) + // Update and reset + _, err = es.AddOrUpdateListener(es.bgCtx, l1.ID, updates, true) assert.Regexp(t, "pop", err) err = es.Stop(es.bgCtx) @@ -714,7 +714,7 @@ func TestUpdateStreamRestartFail(t *testing.T) { l := &apitypes.Listener{ ID: fftypes.NewUUID(), - Name: "ut_listener", + Name: strPtr("ut_listener"), Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, } @@ -734,10 +734,10 @@ func TestUpdateStreamRestartFail(t *testing.T) { return r.ID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() - err := es.AddOrUpdateListener(es.bgCtx, l) + err := es.Start(es.bgCtx) assert.NoError(t, err) - err = es.Start(es.bgCtx) + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) r := <-started @@ -759,6 +759,82 @@ func TestUpdateStreamRestartFail(t *testing.T) { mfc.AssertExpectations(t) } +func TestUpdateAttemptChangeSignature(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{ + ResolvedSignature: "sig1", + }, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{ + ResolvedSignature: "sig2", + }, ffcapi.ErrorReason(""), nil).Once() + + started := make(chan *ffcapi.EventListenerAddRequest, 1) + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started <- r + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + // Attempt to update filters + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, &apitypes.Listener{ + Filters: []fftypes.JSONAny{*fftypes.JSONAnyPtr(`{"new":"filter"}`)}, + }, false) + assert.Regexp(t, "FF21051", err) + + r := <-started + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.Done + + mfc.AssertExpectations(t) +} + +func TestAttemptResetNonExistentListener(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, true) + assert.Regexp(t, "FF21052", err) + + mfc.AssertExpectations(t) + +} + func TestUpdateStreamStopFail(t *testing.T) { es := newTestEventStream(t, `{ @@ -767,7 +843,7 @@ func TestUpdateStreamStopFail(t *testing.T) { l := &apitypes.Listener{ ID: fftypes.NewUUID(), - Name: "ut_listener", + Name: strPtr("ut_listener"), Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, } @@ -787,7 +863,7 @@ func TestUpdateStreamStopFail(t *testing.T) { return r.ID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() - err := es.AddOrUpdateListener(es.bgCtx, l) + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) err = es.Start(es.bgCtx) @@ -812,6 +888,82 @@ func TestUpdateStreamStopFail(t *testing.T) { mfc.AssertExpectations(t) } +func TestResetListenerWriteCheckpointFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), + }, nil) + msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, true) + assert.Regexp(t, "pop", err) + + mfc.AssertExpectations(t) +} + +func TestResetListenerRestartFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), + }, nil) + msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(nil) + msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, true) + assert.Regexp(t, "pop", err) + + err = es.Delete(es.bgCtx) + assert.NoError(t, err) + + mfc.AssertExpectations(t) +} + func TestStopWhenNotStarted(t *testing.T) { es := newTestEventStream(t, `{ @@ -823,10 +975,6 @@ func TestStopWhenNotStarted(t *testing.T) { } -func TestSafeCompareFilterListDiffLen(t *testing.T) { - assert.False(t, safeCompareFilterList([]fftypes.JSONAny{}, []fftypes.JSONAny{`{}`})) -} - func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { es := newTestEventStream(t, `{ @@ -838,7 +986,7 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { l := &apitypes.Listener{ ID: fftypes.NewUUID(), - Name: "ut_listener", + Name: strPtr("ut_listener"), Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), FromBlock: strPtr("12345"), @@ -872,7 +1020,7 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { _, broadcastChannel, _ := mockWSChannels(es.wsChannels.(*wsmocks.WebSocketChannels)) - err := es.AddOrUpdateListener(es.bgCtx, l) + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) err = es.Start(es.bgCtx) diff --git a/internal/events/listener.go b/internal/events/listener.go index a3827a9e..26fd2ec1 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -18,30 +18,28 @@ package events import ( "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) type listener struct { es *eventStream - id *fftypes.UUID - filters []fftypes.JSONAny - options fftypes.JSONAny + spec *apitypes.Listener checkpoint *fftypes.JSONAny - signature string } func (l *listener) stop(startedState *startedStreamState) error { _, _, err := l.es.connector.EventListenerRemove(startedState.ctx, &ffcapi.EventListenerRemoveRequest{ - ID: l.id, + ID: l.spec.ID, }) return err } func (l *listener) start(startedState *startedStreamState) error { _, _, err := l.es.connector.EventListenerAdd(startedState.ctx, &ffcapi.EventListenerAddRequest{ - ID: l.id, - Filters: l.filters, - Options: l.options, + ID: l.spec.ID, + Filters: l.spec.Filters, + Options: *l.spec.Options, EventStream: startedState.updates, Done: startedState.ctx.Done(), }) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index e92f98f5..130d18b8 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -60,10 +60,12 @@ var ( MsgWebhookErr = ffe("FF21042", "Webhook request failed: %s") MsgUnknownPersistence = ffe("FF21043", "Unknown persistence type '%s'") MsgInvalidLimit = ffe("FF21044", "Invalid limit string '%s': %s") - MsgStreamNotFound = ffe("FF21045", "Event stream '%v' not found") - MsgListenerNotFound = ffe("FF21046", "Event listener '%v' not found") + MsgStreamNotFound = ffe("FF21045", "Event stream '%v' not found", 404) + MsgListenerNotFound = ffe("FF21046", "Event listener '%v' not found", 404) MsgDuplicateStreamName = ffe("FF21047", "Duplicate event stream name '%s' used by stream '%s'", 409) MsgMissingID = ffe("FF21048", "ID is required", 400) MsgPersistenceInitFail = ffe("FF21049", "Failed to initialize '%s' persistence: %s") MsgLevelDBPathMissing = ffe("FF21050", "Path must be supplied for LevelDB persistence") + MsgFilterUpdateNotAllowed = ffe("FF21051", "Event filters cannot be updated after a listener is created. Previous signature: '%s'. New signature: '%s'") + MsgResetStreamNotFound = ffe("FF21052", "Attempt to reset listener '%s', which is not currently registered on stream '%s'", 404) ) diff --git a/mocks/eventsmocks/stream.go b/mocks/eventsmocks/stream.go index 76095e86..3049a26f 100644 --- a/mocks/eventsmocks/stream.go +++ b/mocks/eventsmocks/stream.go @@ -17,13 +17,13 @@ type Stream struct { mock.Mock } -// AddOrUpdateListener provides a mock function with given fields: ctx, s -func (_m *Stream) AddOrUpdateListener(ctx context.Context, s *apitypes.Listener) error { - ret := _m.Called(ctx, s) +// AddOrUpdateListener provides a mock function with given fields: ctx, s, reset +func (_m *Stream) AddOrUpdateListener(ctx context.Context, s *apitypes.Listener, reset bool) error { + ret := _m.Called(ctx, s, reset) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *apitypes.Listener) error); ok { - r0 = rf(ctx, s) + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.Listener, bool) error); ok { + r0 = rf(ctx, s, reset) } else { r0 = ret.Error(0) } diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 5e57f92d..3bf3d2bb 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -103,7 +103,7 @@ type Listener struct { ID *fftypes.UUID `ffstruct:"listener" json:"id,omitempty"` Created *fftypes.FFTime `ffstruct:"listener" json:"created"` Updated *fftypes.FFTime `ffstruct:"listener" json:"updated"` - Name string `ffstruct:"listener" json:"name"` + Name *string `ffstruct:"listener" json:"name"` StreamID *fftypes.UUID `ffstruct:"listener" json:"stream" ffexcludeoutput:"true"` DeprecatedAddress *string `ffstruct:"listener" json:"address,omitempty"` DeprecatedEvent *fftypes.JSONAny `ffstruct:"listener" json:"event,omitempty"` diff --git a/pkg/fftm/route_delete_eventstream_listener_test.go b/pkg/fftm/route_delete_eventstream_listener_test.go index 362b2d80..a81121d2 100644 --- a/pkg/fftm/route_delete_eventstream_listener_test.go +++ b/pkg/fftm/route_delete_eventstream_listener_test.go @@ -49,7 +49,7 @@ func TestDeleteEventStreamListener(t *testing.T) { // Create some listeners var l1 apitypes.Listener - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") assert.NoError(t, err) // Then get it diff --git a/pkg/fftm/route_delete_subscription_test.go b/pkg/fftm/route_delete_subscription_test.go index 660f216b..6f798457 100644 --- a/pkg/fftm/route_delete_subscription_test.go +++ b/pkg/fftm/route_delete_subscription_test.go @@ -48,7 +48,7 @@ func TestDeleteSubscription(t *testing.T) { // Create some listeners var l1 apitypes.Listener - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") assert.NoError(t, err) // Then get it diff --git a/pkg/fftm/route_get_eventstream_listener_test.go b/pkg/fftm/route_get_eventstream_listener_test.go index f7d1be35..538fbb0f 100644 --- a/pkg/fftm/route_get_eventstream_listener_test.go +++ b/pkg/fftm/route_get_eventstream_listener_test.go @@ -49,7 +49,7 @@ func TestGetEventStreamsListener(t *testing.T) { // Create some listeners var l1 apitypes.Listener - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") assert.NoError(t, err) // Then get it diff --git a/pkg/fftm/route_get_eventstream_listeners_test.go b/pkg/fftm/route_get_eventstream_listeners_test.go index 8a0508cb..31c71008 100644 --- a/pkg/fftm/route_get_eventstream_listeners_test.go +++ b/pkg/fftm/route_get_eventstream_listeners_test.go @@ -51,11 +51,11 @@ func TestGetEventStreamListeners(t *testing.T) { // Create some listeners var l1, l2, l3 apitypes.Listener - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") assert.NoError(t, err) - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener2", StreamID: es2.ID}).SetResult(&l2).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener2"), StreamID: es2.ID}).SetResult(&l2).Post(url + "/subscriptions") assert.NoError(t, err) - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener3", StreamID: es1.ID}).SetResult(&l3).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener3"), StreamID: es1.ID}).SetResult(&l3).Post(url + "/subscriptions") assert.NoError(t, err) // Then get it diff --git a/pkg/fftm/route_get_subscription_test.go b/pkg/fftm/route_get_subscription_test.go index 9b99354f..c3bba766 100644 --- a/pkg/fftm/route_get_subscription_test.go +++ b/pkg/fftm/route_get_subscription_test.go @@ -48,7 +48,7 @@ func TestGetSubscription(t *testing.T) { // Create some listeners var l1 apitypes.Listener - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") assert.NoError(t, err) // Then get it diff --git a/pkg/fftm/route_get_subscriptions_test.go b/pkg/fftm/route_get_subscriptions_test.go index 20b14f59..76364500 100644 --- a/pkg/fftm/route_get_subscriptions_test.go +++ b/pkg/fftm/route_get_subscriptions_test.go @@ -48,9 +48,9 @@ func TestGetSubscriptions(t *testing.T) { // Create some listeners var l1, l2 apitypes.Listener - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") assert.NoError(t, err) - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener2", StreamID: es1.ID}).SetResult(&l2).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener2"), StreamID: es1.ID}).SetResult(&l2).Post(url + "/subscriptions") assert.NoError(t, err) // Then get it diff --git a/pkg/fftm/route_patch_eventstream_listener.go b/pkg/fftm/route_patch_eventstream_listener.go index 794db7a6..76302cc4 100644 --- a/pkg/fftm/route_patch_eventstream_listener.go +++ b/pkg/fftm/route_patch_eventstream_listener.go @@ -39,7 +39,7 @@ var patchEventStreamListener = func(m *manager) *ffapi.Route { JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, JSONOutputCodes: []int{http.StatusOK}, JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { - return m.updateExistingListener(r.Req.Context(), r.PP["streamId"], r.PP["listenerId"], r.Input.(*apitypes.Listener)) + return m.updateExistingListener(r.Req.Context(), r.PP["streamId"], r.PP["listenerId"], r.Input.(*apitypes.Listener), false) }, } } diff --git a/pkg/fftm/route_patch_eventstream_listener_test.go b/pkg/fftm/route_patch_eventstream_listener_test.go index 00ccf228..69cfb6d5 100644 --- a/pkg/fftm/route_patch_eventstream_listener_test.go +++ b/pkg/fftm/route_patch_eventstream_listener_test.go @@ -49,14 +49,14 @@ func TestPatchEventStreamListener(t *testing.T) { // Create some listeners var l1 apitypes.Listener - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") assert.NoError(t, err) // Then get it var listener apitypes.Listener res, err = resty.New().R(). SetBody(&apitypes.Listener{ - Name: "listener1a", + Name: strPtr("listener1a"), }). SetResult(&listener). Patch(fmt.Sprintf("%s/eventstreams/%s/listeners/%s", url, es1.ID, l1.ID)) @@ -64,7 +64,7 @@ func TestPatchEventStreamListener(t *testing.T) { assert.Equal(t, 200, res.StatusCode()) assert.Equal(t, l1.ID, listener.ID) - assert.Equal(t, "listener1a", listener.Name) + assert.Equal(t, "listener1a", *listener.Name) mfc.AssertExpectations(t) diff --git a/pkg/fftm/route_patch_subscription.go b/pkg/fftm/route_patch_subscription.go index 3c8e5b61..9ae7ce22 100644 --- a/pkg/fftm/route_patch_subscription.go +++ b/pkg/fftm/route_patch_subscription.go @@ -39,7 +39,7 @@ var patchSubscription = func(m *manager) *ffapi.Route { JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, JSONOutputCodes: []int{http.StatusOK}, JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { - return m.updateExistingListener(r.Req.Context(), "" /* no streamId on this path */, r.PP["listenerId"], r.Input.(*apitypes.Listener)) + return m.updateExistingListener(r.Req.Context(), "" /* no streamId on this path */, r.PP["listenerId"], r.Input.(*apitypes.Listener), false) }, } } diff --git a/pkg/fftm/route_patch_subscription_test.go b/pkg/fftm/route_patch_subscription_test.go index f5378c6c..694099d7 100644 --- a/pkg/fftm/route_patch_subscription_test.go +++ b/pkg/fftm/route_patch_subscription_test.go @@ -48,14 +48,14 @@ func TestPatchSubscription(t *testing.T) { // Create some listeners var l1 apitypes.Listener - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") assert.NoError(t, err) // Then get it var listener apitypes.Listener res, err = resty.New().R(). SetBody(&apitypes.Listener{ - Name: "listener1a", + Name: strPtr("listener1a"), }). SetResult(&listener). Patch(url + "/subscriptions/" + l1.ID.String()) @@ -63,7 +63,7 @@ func TestPatchSubscription(t *testing.T) { assert.Equal(t, 200, res.StatusCode()) assert.Equal(t, l1.ID, listener.ID) - assert.Equal(t, "listener1a", listener.Name) + assert.Equal(t, "listener1a", *listener.Name) mfc.AssertExpectations(t) diff --git a/pkg/fftm/route_post_eventstream_listener_reset.go b/pkg/fftm/route_post_eventstream_listener_reset.go new file mode 100644 index 00000000..d8f1fe19 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_listener_reset.go @@ -0,0 +1,45 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postEventStreamListenerReset = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postEventStreamListenerReset", + Path: "/eventstreams/{streamId}/listeners/{listenerId}/reset", + Method: http.MethodPost, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchEventStreamListener, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.updateExistingListener(r.Req.Context(), r.PP["streamId"], r.PP["listenerId"], r.Input.(*apitypes.Listener), true) + }, + } +} diff --git a/pkg/fftm/route_post_eventstream_listener_reset_test.go b/pkg/fftm/route_post_eventstream_listener_reset_test.go new file mode 100644 index 00000000..b81d17b6 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_listener_reset_test.go @@ -0,0 +1,71 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostEventStreamListenerReset(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetBody(&apitypes.Listener{ + Name: strPtr("listener1a"), + }). + SetResult(&listener). + Post(fmt.Sprintf("%s/eventstreams/%s/listeners/%s/reset", url, es1.ID, l1.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + assert.Equal(t, "listener1a", *listener.Name) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_post_eventstream_listeners_test.go b/pkg/fftm/route_post_eventstream_listeners_test.go index 89b973b9..67993e12 100644 --- a/pkg/fftm/route_post_eventstream_listeners_test.go +++ b/pkg/fftm/route_post_eventstream_listeners_test.go @@ -50,7 +50,7 @@ func TestPostEventStreamListeners(t *testing.T) { // Create a listener var l1 apitypes.Listener - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(fmt.Sprintf("%s/eventstreams/%s/listeners", url, es1.ID)) + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(fmt.Sprintf("%s/eventstreams/%s/listeners", url, es1.ID)) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) diff --git a/pkg/fftm/route_post_subscription_reset.go b/pkg/fftm/route_post_subscription_reset.go new file mode 100644 index 00000000..dba74721 --- /dev/null +++ b/pkg/fftm/route_post_subscription_reset.go @@ -0,0 +1,45 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postSubscriptionReset = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postSubscriptionReset", + Path: "/subscriptions/{listenerId}/reset", + Deprecated: true, // in favor of "/eventstreams/{streamId}/listeners/{listenerId}" + Method: http.MethodPost, + PathParams: []*ffapi.PathParam{ + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchSubscription, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.updateExistingListener(r.Req.Context(), "" /* no streamId on this path */, r.PP["listenerId"], r.Input.(*apitypes.Listener), true) + }, + } +} diff --git a/pkg/fftm/route_post_subscription_reset_test.go b/pkg/fftm/route_post_subscription_reset_test.go new file mode 100644 index 00000000..67d1c2d1 --- /dev/null +++ b/pkg/fftm/route_post_subscription_reset_test.go @@ -0,0 +1,70 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostSubscriptionReset(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetBody(&apitypes.Listener{ + Name: strPtr("listener1a"), + }). + SetResult(&listener). + Post(url + "/subscriptions/" + l1.ID.String() + "/reset") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + assert.Equal(t, "listener1a", *listener.Name) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_post_subscriptions_test.go b/pkg/fftm/route_post_subscriptions_test.go index f6e2a861..622bb9a0 100644 --- a/pkg/fftm/route_post_subscriptions_test.go +++ b/pkg/fftm/route_post_subscriptions_test.go @@ -49,7 +49,7 @@ func TestPostSubscriptions(t *testing.T) { // Create a listener var l1 apitypes.Listener - res, err = resty.New().R().SetBody(&apitypes.Listener{Name: "listener1", StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) diff --git a/pkg/fftm/routes.go b/pkg/fftm/routes.go index 79a5e232..9d8eee77 100644 --- a/pkg/fftm/routes.go +++ b/pkg/fftm/routes.go @@ -33,10 +33,12 @@ func (m *manager) routes() []*ffapi.Route { patchEventStreamListener(m), patchSubscription(m), postEventStream(m), + postEventStreamListenerReset(m), postEventStreamListeners(m), postEventStreamResume(m), postEventStreamSuspend(m), postRootCommand(m), + postSubscriptionReset(m), postSubscriptions(m), } } diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index a28a27b6..8d86aa93 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -107,7 +107,7 @@ func (m *manager) addRuntimeListener(ctx context.Context, def *apitypes.Listener m.mux.Unlock() if s != nil { // The definition is updated in-place by the event stream code - if err := s.AddOrUpdateListener(ctx, def); err != nil { + if _, err := s.AddOrUpdateListener(ctx, def.ID, def, false); err != nil { return nil, err } } @@ -226,32 +226,30 @@ func (m *manager) createAndStoreNewStreamListener(ctx context.Context, idStr str } func (m *manager) createAndStoreNewListener(ctx context.Context, def *apitypes.Listener) (*apitypes.Listener, error) { - def.ID = nil // set by AddOrUpdateListener - def.Created = nil - return m.createOrUpdateListener(ctx, def) + return m.createOrUpdateListener(ctx, apitypes.UUIDVersion1(), def, false) } -func (m *manager) updateExistingListener(ctx context.Context, streamIDStr, listenerIDStr string, updates *apitypes.Listener) (*apitypes.Listener, error) { +func (m *manager) updateExistingListener(ctx context.Context, streamIDStr, listenerIDStr string, updates *apitypes.Listener, reset bool) (*apitypes.Listener, error) { l, err := m.getListener(ctx, streamIDStr, listenerIDStr) // Verify the listener exists in storage if err != nil { return nil, err } - updates.ID = l.ID updates.StreamID = l.StreamID - return m.createOrUpdateListener(ctx, updates) + return m.createOrUpdateListener(ctx, l.ID, updates, reset) } -func (m *manager) createOrUpdateListener(ctx context.Context, def *apitypes.Listener) (*apitypes.Listener, error) { +func (m *manager) createOrUpdateListener(ctx context.Context, id *fftypes.UUID, newOrUpdates *apitypes.Listener, reset bool) (*apitypes.Listener, error) { var s events.Stream - if def.StreamID != nil { + if newOrUpdates.StreamID != nil { m.mux.Lock() - s = m.eventStreams[*def.StreamID] + s = m.eventStreams[*newOrUpdates.StreamID] m.mux.Unlock() } if s == nil { - return nil, i18n.NewError(ctx, tmmsgs.MsgStreamNotFound, def.StreamID) + return nil, i18n.NewError(ctx, tmmsgs.MsgStreamNotFound, newOrUpdates.StreamID) } - if err := s.AddOrUpdateListener(ctx, def); err != nil { + def, err := s.AddOrUpdateListener(ctx, id, newOrUpdates, reset) + if err != nil { return nil, err } if err := m.persistence.WriteListener(ctx, def); err != nil { diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go index 98eb0b45..6ea835b7 100644 --- a/pkg/fftm/stream_management_test.go +++ b/pkg/fftm/stream_management_test.go @@ -45,11 +45,11 @@ func TestRestoreStreamsAndListenersOK(t *testing.T) { err := m.persistence.WriteStream(m.ctx, es1) assert.NoError(t, err) - e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: "listener1", StreamID: es1.ID} + e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: strPtr("listener1"), StreamID: es1.ID} err = m.persistence.WriteListener(m.ctx, e1l1) assert.NoError(t, err) - e1l2 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: "listener2", StreamID: es1.ID} + e1l2 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: strPtr("listener2"), StreamID: es1.ID} err = m.persistence.WriteListener(m.ctx, e1l2) assert.NoError(t, err) @@ -57,7 +57,7 @@ func TestRestoreStreamsAndListenersOK(t *testing.T) { err = m.persistence.WriteStream(m.ctx, es2) assert.NoError(t, err) - e2l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: "listener3", StreamID: es2.ID} + e2l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: strPtr("listener3"), StreamID: es2.ID} err = m.persistence.WriteListener(m.ctx, e2l1) assert.NoError(t, err) @@ -124,7 +124,7 @@ func TestRestoreListenersStartFail(t *testing.T) { err := m.persistence.WriteStream(m.ctx, es1) assert.NoError(t, err) - e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: "listener1", StreamID: es1.ID} + e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: strPtr("listener1"), StreamID: es1.ID} err = m.persistence.WriteListener(m.ctx, e1l1) assert.NoError(t, err) @@ -150,7 +150,7 @@ func TestDeleteStartedListener(t *testing.T) { err := m.persistence.WriteStream(m.ctx, es1) assert.NoError(t, err) - e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: "listener1", StreamID: es1.ID} + e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: strPtr("listener1"), StreamID: es1.ID} err = m.persistence.WriteListener(m.ctx, e1l1) assert.NoError(t, err) @@ -292,7 +292,7 @@ func TestUpdateExistingListenerNotFound(t *testing.T) { mp.On("GetListener", m.ctx, mock.Anything).Return(nil, nil) - _, err := m.updateExistingListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String(), &apitypes.Listener{}) + _, err := m.updateExistingListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String(), &apitypes.Listener{}, false) assert.Regexp(t, "FF21046", err) mp.AssertExpectations(t) @@ -301,7 +301,7 @@ func TestUpdateExistingListenerNotFound(t *testing.T) { func TestCreateOrUpdateListenerNotFound(t *testing.T) { _, _, m := newMockPersistenceManager(t) - _, err := m.createOrUpdateListener(m.ctx, &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: apitypes.UUIDVersion1()}) + _, err := m.createOrUpdateListener(m.ctx, apitypes.UUIDVersion1(), &apitypes.Listener{StreamID: apitypes.UUIDVersion1()}, false) assert.Regexp(t, "FF21045", err) } @@ -317,7 +317,7 @@ func TestCreateOrUpdateListenerFail(t *testing.T) { es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) - _, err = m.createOrUpdateListener(m.ctx, &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: es.ID}) + _, err = m.createOrUpdateListener(m.ctx, apitypes.UUIDVersion1(), &apitypes.Listener{StreamID: es.ID}, false) assert.Regexp(t, "pop", err) mp.AssertExpectations(t) @@ -336,7 +336,7 @@ func TestCreateOrUpdateListenerWriteFail(t *testing.T) { es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) - _, err = m.createOrUpdateListener(m.ctx, &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: es.ID}) + _, err = m.createOrUpdateListener(m.ctx, apitypes.UUIDVersion1(), &apitypes.Listener{StreamID: es.ID}, false) assert.Regexp(t, "pop", err) mp.AssertExpectations(t) @@ -376,7 +376,7 @@ func TestDeleteListenerFail(t *testing.T) { es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) - l1, err := m.createOrUpdateListener(m.ctx, &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: es.ID}) + l1, err := m.createOrUpdateListener(m.ctx, apitypes.UUIDVersion1(), &apitypes.Listener{StreamID: es.ID}, false) assert.NoError(t, err) mp.On("GetListener", m.ctx, mock.Anything).Return(l1, nil) From 31ba62defcf9fd2952414100edd050cd5de9b9e6 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 23 Jun 2022 10:30:27 -0400 Subject: [PATCH 29/95] Add sortable events interface Signed-off-by: Peter Broadhurst --- pkg/ffcapi/api.go | 10 +++++++++- pkg/ffcapi/api_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 pkg/ffcapi/api_test.go diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index b6444ee0..ca1c4b43 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -18,6 +18,7 @@ package ffcapi import ( "context" + "strings" "github.com/hyperledger/firefly-common/pkg/fftypes" ) @@ -79,6 +80,13 @@ type Event struct { Info *fftypes.JSONAny `json:"info"` // additional blockchain specific information } +// Events array has a natural sort order of the protocol ID +type Events []*Event + +func (es Events) Len() int { return len(es) } +func (es Events) Swap(i, j int) { es[i], es[j] = es[j], es[i] } +func (es Events) Less(i, j int) bool { return strings.Compare(es[i].ProtocolID, es[j].ProtocolID) < 0 } + type EventWithContext struct { StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this event @@ -88,7 +96,7 @@ type EventWithContext struct { type ListenerUpdate struct { ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this update - expected to be the same for all events in the events array Checkpoint *fftypes.JSONAny `json:"checkpoint"` // checkpoint information for the listener. This should be supplied regularly even if there are no events, to minimize recovery time after restart - Events []*Event `json:"events,omitempty"` // zero or more events. Can be nil for checkpoint-only updates + Events Events `json:"events,omitempty"` // zero or more events. Can be nil for checkpoint-only updates } // ErrorReason are a set of standard error conditions that a blockchain connector can return diff --git a/pkg/ffcapi/api_test.go b/pkg/ffcapi/api_test.go new file mode 100644 index 00000000..48e39870 --- /dev/null +++ b/pkg/ffcapi/api_test.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "crypto/rand" + "fmt" + "math/big" + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSortEvents(t *testing.T) { + + events := make(Events, 1000) + for i := 0; i < 1000; i++ { + v, _ := rand.Int(rand.Reader, big.NewInt(100000000)) + events[i] = &Event{ + ProtocolID: fmt.Sprintf("%.9d", v.Int64()), + } + } + sort.Sort(events) + + for i := 1; i < 1000; i++ { + assert.Negative(t, strings.Compare(events[i-1].ProtocolID, events[i].ProtocolID)) + } +} From d6b42ec478930390674bad7f35c3b5d1c4485dc0 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 24 Jun 2022 09:26:01 -0400 Subject: [PATCH 30/95] Pass FromBlock separate to options and allow options to be null Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 8 +++++--- internal/events/listener.go | 7 +++++-- pkg/ffcapi/event_listener_add.go | 16 ++++++++++------ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 9152bbc3..38837d8c 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -321,9 +321,11 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID // The connector needs to validate the options, building a set of options that are assured to be non-nil res, _, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.EventListenerVerifyOptionsRequest{ - FromBlock: *spec.FromBlock, - Filters: spec.Filters, - Options: spec.Options, + EventListenerOptions: ffcapi.EventListenerOptions{ + FromBlock: *spec.FromBlock, + Filters: spec.Filters, + Options: spec.Options, + }, }) if err != nil { return nil, i18n.NewError(ctx, tmmsgs.MsgBadListenerOptions, err) diff --git a/internal/events/listener.go b/internal/events/listener.go index 26fd2ec1..b0902747 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -37,9 +37,12 @@ func (l *listener) stop(startedState *startedStreamState) error { func (l *listener) start(startedState *startedStreamState) error { _, _, err := l.es.connector.EventListenerAdd(startedState.ctx, &ffcapi.EventListenerAddRequest{ + EventListenerOptions: ffcapi.EventListenerOptions{ + FromBlock: *l.spec.FromBlock, + Filters: l.spec.Filters, + Options: l.spec.Options, + }, ID: l.spec.ID, - Filters: l.spec.Filters, - Options: *l.spec.Options, EventStream: startedState.updates, Done: startedState.ctx.Done(), }) diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go index bf36c9a4..169a89c0 100644 --- a/pkg/ffcapi/event_listener_add.go +++ b/pkg/ffcapi/event_listener_add.go @@ -21,13 +21,18 @@ import ( ) const ( - FromBlockLatest = "latest" + FromBlockEarliest = "earliest" + FromBlockLatest = "latest" ) +type EventListenerOptions struct { + FromBlock string // The instruction for the first block to index from (when there is no previous checkpoint). Special "earliest" and "latest" strings should be supported as well as blockchain specific block ID (like a decimal number etc.) + Filters []fftypes.JSONAny // The blockchain specific list of filters. The top-level array is an OR list. The semantics within each entry is defined by the blockchain + Options *fftypes.JSONAny // Blockchain specific set of options, such as the first block to detect events from (can be null) +} + type EventListenerVerifyOptionsRequest struct { - FromBlock string - Filters []fftypes.JSONAny - Options *fftypes.JSONAny + EventListenerOptions } type EventListenerVerifyOptionsResponse struct { @@ -36,9 +41,8 @@ type EventListenerVerifyOptionsResponse struct { } type EventListenerAddRequest struct { + EventListenerOptions ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event - Filters []fftypes.JSONAny // The blockchain specific list of filters. The top-level array is an OR list. The semantics within each entry is defined by the blockchain - Options fftypes.JSONAny // Blockchain specific set of options, such as the first block to detect events from Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream Done <-chan struct{} // Channel that will be closed when the event listener needs to stop - the event listener should stop pushing events EventStream chan<- *ListenerUpdate // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events From 318227063adecee95b462e81a00506dafbdb115d Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sat, 25 Jun 2022 16:00:36 -0400 Subject: [PATCH 31/95] Pass name through to listener for inclusion in events Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 6 +----- internal/events/listener.go | 21 +++++++++++++-------- pkg/ffcapi/event_listener_add.go | 1 + 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 38837d8c..f25daf0b 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -321,11 +321,7 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID // The connector needs to validate the options, building a set of options that are assured to be non-nil res, _, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.EventListenerVerifyOptionsRequest{ - EventListenerOptions: ffcapi.EventListenerOptions{ - FromBlock: *spec.FromBlock, - Filters: spec.Filters, - Options: spec.Options, - }, + EventListenerOptions: listenerSpecToOptions(spec), }) if err != nil { return nil, i18n.NewError(ctx, tmmsgs.MsgBadListenerOptions, err) diff --git a/internal/events/listener.go b/internal/events/listener.go index b0902747..fc50d782 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -28,6 +28,14 @@ type listener struct { checkpoint *fftypes.JSONAny } +func listenerSpecToOptions(spec *apitypes.Listener) ffcapi.EventListenerOptions { + return ffcapi.EventListenerOptions{ + FromBlock: *spec.FromBlock, + Filters: spec.Filters, + Options: spec.Options, + } +} + func (l *listener) stop(startedState *startedStreamState) error { _, _, err := l.es.connector.EventListenerRemove(startedState.ctx, &ffcapi.EventListenerRemoveRequest{ ID: l.spec.ID, @@ -37,14 +45,11 @@ func (l *listener) stop(startedState *startedStreamState) error { func (l *listener) start(startedState *startedStreamState) error { _, _, err := l.es.connector.EventListenerAdd(startedState.ctx, &ffcapi.EventListenerAddRequest{ - EventListenerOptions: ffcapi.EventListenerOptions{ - FromBlock: *l.spec.FromBlock, - Filters: l.spec.Filters, - Options: l.spec.Options, - }, - ID: l.spec.ID, - EventStream: startedState.updates, - Done: startedState.ctx.Done(), + EventListenerOptions: listenerSpecToOptions(l.spec), + Name: *l.spec.Name, + ID: l.spec.ID, + EventStream: startedState.updates, + Done: startedState.ctx.Done(), }) return err } diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go index 169a89c0..be6900e1 100644 --- a/pkg/ffcapi/event_listener_add.go +++ b/pkg/ffcapi/event_listener_add.go @@ -43,6 +43,7 @@ type EventListenerVerifyOptionsResponse struct { type EventListenerAddRequest struct { EventListenerOptions ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event + Name string // Descriptive name of the listener, provided by the user, or defaulted to the signature. Not guaranteed to be unique. Should be included in the event info Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream Done <-chan struct{} // Channel that will be closed when the event listener needs to stop - the event listener should stop pushing events EventStream chan<- *ListenerUpdate // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events From 005fc319fa5be75ff370a6eb77df5c7969154199 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sat, 25 Jun 2022 16:02:19 -0400 Subject: [PATCH 32/95] Upgrade common Signed-off-by: Peter Broadhurst --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 82238497..57a560b1 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru v0.5.4 github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e - github.com/hyperledger/firefly-common v0.1.11-0.20220618173558-a825551beb7a + github.com/hyperledger/firefly-common v0.1.13 github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.1 diff --git a/go.sum b/go.sum index 256ad7a1..4672d0c7 100644 --- a/go.sum +++ b/go.sum @@ -633,6 +633,8 @@ github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e/go.mod h1:43 github.com/hyperledger/firefly-common v0.1.2/go.mod h1:ytB+404+Qg00wJKx602Yi/V6Spx9d0g65lZR9y5fzlo= github.com/hyperledger/firefly-common v0.1.11-0.20220618173558-a825551beb7a h1:JmwLgtI++IyC8piQEMcT5/efp7Pwl4s5/8KXnXLltpA= github.com/hyperledger/firefly-common v0.1.11-0.20220618173558-a825551beb7a/go.mod h1:MYL6Dbj3KqM/79IkS+mCzJ7wRguNbd/PKdVu8aXo5TI= +github.com/hyperledger/firefly-common v0.1.13 h1:eNK99U9FV43u1F46MM0mPuXT4Xn++orghpoTIIPsmwo= +github.com/hyperledger/firefly-common v0.1.13/go.mod h1:2NqPi5Ud9H6rSlZXkLbotxW7z4EAD89p3/8oNOpm9Gs= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= From 71354ddbbb0fdf1214fa4a38660ff0b966e6ed8a Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sat, 25 Jun 2022 20:20:50 -0400 Subject: [PATCH 33/95] Remove block cache from confirmations manager - moving to connector Signed-off-by: Peter Broadhurst --- config.md | 1 - go.mod | 1 - go.sum | 3 - internal/confirmations/confirmations.go | 37 +----- internal/confirmations/confirmations_test.go | 124 +++++++------------ internal/tmconfig/tmconfig.go | 2 - internal/tmmsgs/en_error_messges.go | 1 - pkg/ffcapi/block_info_by_number.go | 3 +- pkg/fftm/manager.go | 5 +- pkg/fftm/manager_test.go | 11 -- 10 files changed, 49 insertions(+), 139 deletions(-) diff --git a/config.md b/config.md index fee4ee8f..4e8da5e9 100644 --- a/config.md +++ b/config.md @@ -44,7 +44,6 @@ nav_order: 2 |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|blockCacheSize|The maximum number of block headers to keep in the cache|`int`|`1000` |blockPollingInterval|How often to poll for new block headers|[`time.Duration`](https://pkg.go.dev/time#Duration)|`3s` |notificationQueueLength|Internal queue length for notifying the confirmations manager of new transactions/events|`int`|`50` |required|Number of confirmations required to consider a transaction/event final|`int`|`20` diff --git a/go.mod b/go.mod index 57a560b1..c088fba4 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 - github.com/hashicorp/golang-lru v0.5.4 github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e github.com/hyperledger/firefly-common v0.1.13 github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index 4672d0c7..eedc5890 100644 --- a/go.sum +++ b/go.sum @@ -615,7 +615,6 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -631,8 +630,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e h1:QP+Yykyq7C670zb4Fs7s4lAtYmvIll4rP/y00hdOsg4= github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e/go.mod h1:434LxYn4ntyK/E0dY+2dTc55caBy6BdUMYBM2gLndAI= github.com/hyperledger/firefly-common v0.1.2/go.mod h1:ytB+404+Qg00wJKx602Yi/V6Spx9d0g65lZR9y5fzlo= -github.com/hyperledger/firefly-common v0.1.11-0.20220618173558-a825551beb7a h1:JmwLgtI++IyC8piQEMcT5/efp7Pwl4s5/8KXnXLltpA= -github.com/hyperledger/firefly-common v0.1.11-0.20220618173558-a825551beb7a/go.mod h1:MYL6Dbj3KqM/79IkS+mCzJ7wRguNbd/PKdVu8aXo5TI= github.com/hyperledger/firefly-common v0.1.13 h1:eNK99U9FV43u1F46MM0mPuXT4Xn++orghpoTIIPsmwo= github.com/hyperledger/firefly-common v0.1.13/go.mod h1:2NqPi5Ud9H6rSlZXkLbotxW7z4EAD89p3/8oNOpm9Gs= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index 18cdad8d..2dec8fa4 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -19,10 +19,8 @@ import ( "context" "fmt" "sort" - "strconv" "time" - lru "github.com/hashicorp/golang-lru" "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" @@ -95,7 +93,6 @@ type blockConfirmationManager struct { requiredConfirmations int pollingInterval time.Duration staleReceiptTimeout time.Duration - blockCache *lru.Cache bcmNotifications chan *Notification highestBlockSeen uint64 pending map[string]*pendingItem @@ -103,8 +100,7 @@ type blockConfirmationManager struct { done chan struct{} } -func NewBlockConfirmationManager(ctx context.Context, connector ffcapi.API) (Manager, error) { - var err error +func NewBlockConfirmationManager(ctx context.Context, connector ffcapi.API) Manager { bcm := &blockConfirmationManager{ connector: connector, blockListenerStale: true, @@ -116,11 +112,7 @@ func NewBlockConfirmationManager(ctx context.Context, connector ffcapi.API) (Man staleReceipts: make(map[string]bool), } bcm.ctx, bcm.cancelFunc = context.WithCancel(ctx) - bcm.blockCache, err = lru.New(config.GetInt(tmconfig.ConfirmationsBlockCacheSize)) - if err != nil { - return nil, i18n.WrapError(bcm.ctx, err, tmmsgs.MsgCacheInitFail) - } - return bcm, nil + return bcm } type pendingType int @@ -250,17 +242,7 @@ func (bcm *blockConfirmationManager) Notify(n *Notification) error { return nil } -func (bcm *blockConfirmationManager) addToCache(blockInfo *BlockInfo) { - bcm.blockCache.Add(blockInfo.BlockHash, blockInfo) - bcm.blockCache.Add(strconv.FormatUint(blockInfo.BlockNumber, 10), blockInfo) -} - func (bcm *blockConfirmationManager) getBlockByHash(blockHash string) (*BlockInfo, error) { - cached, ok := bcm.blockCache.Get(blockHash) - if ok { - return cached.(*BlockInfo), nil - } - res, reason, err := bcm.connector.BlockInfoByHash(bcm.ctx, &ffcapi.BlockInfoByHashRequest{ BlockHash: blockHash, }) @@ -273,23 +255,13 @@ func (bcm *blockConfirmationManager) getBlockByHash(blockHash string) (*BlockInf blockInfo := transformBlockInfo(&res.BlockInfo) log.L(bcm.ctx).Debugf("Downloaded block header by hash: %d / %s parent=%s", blockInfo.BlockNumber, blockInfo.BlockHash, blockInfo.ParentHash) - bcm.addToCache(blockInfo) return blockInfo, nil } func (bcm *blockConfirmationManager) getBlockByNumber(blockNumber uint64, expectedParentHash string) (*BlockInfo, error) { - cached, ok := bcm.blockCache.Get(strconv.FormatUint(blockNumber, 10)) - if ok { - blockInfo := cached.(*BlockInfo) - if blockInfo.ParentHash != expectedParentHash { - // Treat a missing block, or a mismatched block, both as a cache miss and query the node - log.L(bcm.ctx).Debugf("Block cache miss due to parent hash mismatch: %d / %s parent=%s required=%s ", blockInfo.BlockNumber, blockInfo.BlockHash, blockInfo.ParentHash, expectedParentHash) - } else { - return blockInfo, nil - } - } res, reason, err := bcm.connector.BlockInfoByNumber(bcm.ctx, &ffcapi.BlockInfoByNumberRequest{ - BlockNumber: fftypes.NewFFBigInt(int64(blockNumber)), + BlockNumber: fftypes.NewFFBigInt(int64(blockNumber)), + ExpectedParentHash: expectedParentHash, }) if err != nil { if reason == ffcapi.ErrorReasonNotFound { @@ -299,7 +271,6 @@ func (bcm *blockConfirmationManager) getBlockByNumber(blockNumber uint64, expect } blockInfo := transformBlockInfo(&res.BlockInfo) log.L(bcm.ctx).Debugf("Downloaded block header by number: %d / %s parent=%s", blockInfo.BlockNumber, blockInfo.BlockHash, blockInfo.ParentHash) - bcm.addToCache(blockInfo) return blockInfo, nil } diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index 25463e4e..46823419 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -42,19 +42,10 @@ func newTestBlockConfirmationManager(t *testing.T, enabled bool) (*blockConfirma func newTestBlockConfirmationManagerCustomConfig(t *testing.T) (*blockConfirmationManager, *ffcapimocks.API) { logrus.SetLevel(logrus.DebugLevel) mca := &ffcapimocks.API{} - bcm, err := NewBlockConfirmationManager(context.Background(), mca) - assert.NoError(t, err) + bcm := NewBlockConfirmationManager(context.Background(), mca) return bcm.(*blockConfirmationManager), mca } -func TestBCMInitError(t *testing.T) { - tmconfig.Reset() - config.Set(tmconfig.ConfirmationsBlockCacheSize, -1) - mca := &ffcapimocks.API{} - _, err := NewBlockConfirmationManager(context.Background(), mca) - assert.Regexp(t, "FF21015", err) -} - func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, true) @@ -117,23 +108,29 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { BlockHash: block1002.BlockHash, ParentHash: block1002.ParentHash, }, - }, ffcapi.ErrorReason(""), nil).Once() - - // Then we should walk the chain by number to fill in 1002, because our HWM is 1003. - // Note this doesn't result in any RPC calls, as we just cached the block and it matches + }, ffcapi.ErrorReason(""), nil) - // Then we get notified of 1004 to complete the last confirmation + // Notify of 1004 after we download 1003 block1004 := &BlockInfo{ BlockNumber: 1004, BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", } - mca.On("NewBlockHashes").Run(func(args mock.Arguments) { + // Then we should walk the chain by number to fill in 1003, because our HWM is 1003. + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1003 + })).Run(func(args mock.Arguments) { blockHashes <- &ffcapi.BlockHashEvent{ BlockHashes: []string{block1004.BlockHash}, } - }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() + }).Return(&ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)), + BlockHash: block1003.BlockHash, + ParentHash: block1003.ParentHash, + }, + }, ffcapi.ErrorReason(""), nil) // Which then gets downloaded, and should complete the confirmation mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { @@ -261,6 +258,34 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { // Subsequent calls get nothing mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1002 + })).Return(&ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)), + BlockHash: block1002.BlockHash, + ParentHash: block1002.ParentHash, + }, + }, ffcapi.ErrorReason(""), nil) + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1003 + })).Return(&ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block1003b.BlockNumber)), + BlockHash: block1003b.BlockHash, + ParentHash: block1003b.ParentHash, + }, + }, ffcapi.ErrorReason(""), nil) + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1004 + })).Return(&ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)), + BlockHash: block1004.BlockHash, + ParentHash: block1004.ParentHash, + }, + }, ffcapi.ErrorReason(""), nil) + bcm.Start() bcm.Notify(&Notification{ @@ -821,71 +846,6 @@ func TestProcessNotificationsSwallowsUnknownType(t *testing.T) { }) } -func TestGetBlockByNumberForceLookupMismatchedBlockType(t *testing.T) { - - bcm, mca := newTestBlockConfirmationManager(t, false) - - mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { - return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.BlockInfoByNumberResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(1002), - BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21", - ParentHash: "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", - }, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { - return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.BlockInfoByNumberResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(1002), - BlockHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - ParentHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - }, - }, ffcapi.ErrorReason(""), nil).Once() - - // Make the first call that caches - blockInfo, err := bcm.getBlockByNumber(1002, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e") - assert.NoError(t, err) - assert.Equal(t, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", blockInfo.ParentHash) - - // Make second call that is cached as parent matches - blockInfo, err = bcm.getBlockByNumber(1002, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e") - assert.NoError(t, err) - assert.Equal(t, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", blockInfo.ParentHash) - - // Make third call that does not as parent mismatched - blockInfo, err = bcm.getBlockByNumber(1002, "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542") - assert.NoError(t, err) - assert.Equal(t, "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", blockInfo.ParentHash) - -} - -func TestGetBlockByHashCached(t *testing.T) { - - bcm, mca := newTestBlockConfirmationManager(t, false) - - mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { - return r.BlockHash == "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df" - })).Return(&ffcapi.BlockInfoByHashResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(1003), - BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", - ParentHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295", - }, - }, ffcapi.ErrorReason(""), nil).Once() - - blockInfo, err := bcm.getBlockByHash("0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df") - assert.NoError(t, err) - assert.Equal(t, "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", blockInfo.BlockHash) - - // Get again cached - blockInfo, err = bcm.getBlockByHash("0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df") - assert.NoError(t, err) - assert.Equal(t, "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", blockInfo.BlockHash) - -} - func TestGetBlockNotFound(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 6b41d08f..a6bcb70b 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -30,7 +30,6 @@ var ffc = config.AddRootKey var ( ManagerName = ffc("manager.name") ConfirmationsRequired = ffc("confirmations.required") - ConfirmationsBlockCacheSize = ffc("confirmations.blockCacheSize") ConfirmationsBlockPollingInterval = ffc("confirmations.blockPollingInterval") ConfirmationsStaleReceiptTimeout = ffc("confirmations.staleReceiptTimeout") ConfirmationsNotificationQueueLength = ffc("confirmations.notificationQueueLength") @@ -81,7 +80,6 @@ func setDefaults() { }) viper.SetDefault(string(OperationsFullScanStartupMaxRetries), 10) viper.SetDefault(string(ConfirmationsRequired), 20) - viper.SetDefault(string(ConfirmationsBlockCacheSize), 1000) viper.SetDefault(string(ConfirmationsBlockPollingInterval), "3s") viper.SetDefault(string(ConfirmationsNotificationQueueLength), 50) viper.SetDefault(string(ConfirmationsStaleReceiptTimeout), "1m") diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 130d18b8..1cf6b370 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -30,7 +30,6 @@ var ( MsgInvalidOutputType = ffe("FF21010", "Invalid output type: %s") MsgConnectorError = ffe("FF21012", "Connector failed request. requestId=%s reason=%s error: %s") MsgConnectorInvalidContentType = ffe("FF21013", "Connector failed request. requestId=%s invalid response content type: %s") - MsgCacheInitFail = ffe("FF21015", "Failed to initialize cache") MsgInvalidConfirmationRequest = ffe("FF21016", "Invalid confirmation request %+v") MsgCoreError = ffe("FF21017", "Error from core status=%d: %s") MsgConfigParamNotSet = ffe("FF21018", "Configuration parameter '%s' must be set") diff --git a/pkg/ffcapi/block_info_by_number.go b/pkg/ffcapi/block_info_by_number.go index 3afde7a3..d9c26112 100644 --- a/pkg/ffcapi/block_info_by_number.go +++ b/pkg/ffcapi/block_info_by_number.go @@ -21,7 +21,8 @@ import ( ) type BlockInfoByNumberRequest struct { - BlockNumber *fftypes.FFBigInt `json:"blockNumber"` + BlockNumber *fftypes.FFBigInt `json:"blockNumber"` + ExpectedParentHash string `json:"expectedParentHash"` // If set then a mismatched parent hash should be considered a cache miss (if the connector does caching) } type BlockInfoByNumberResponse struct { diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index 13790b86..e047f263 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -95,10 +95,7 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { if m.name == "" { return nil, i18n.NewError(ctx, tmmsgs.MsgConfigParamNotSet, tmconfig.ManagerName) } - m.confirmations, err = confirmations.NewBlockConfirmationManager(ctx, m.connector) - if err != nil { - return nil, err - } + m.confirmations = confirmations.NewBlockConfirmationManager(ctx, m.connector) m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBaseConfig, config.GetString(tmconfig.PolicyEngineName)) if err != nil { return nil, err diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 50f5beac..bc79323e 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -188,17 +188,6 @@ func TestNewManagerFireFlyURLConfig(t *testing.T) { } -func TestNewManagerBadConfirmationsCacheSize(t *testing.T) { - - tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") - config.Set(tmconfig.ConfirmationsBlockCacheSize, -1) - - _, err := NewManager(context.Background(), nil) - assert.Regexp(t, "FF21015", err) - -} - func TestNewManagerBadPolicyEngine(t *testing.T) { tmconfig.Reset() From 453dfc3e0e679187e5ba630ed7604682285a56fa Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 28 Jun 2022 16:38:23 -0400 Subject: [PATCH 34/95] Explicitly start streams so connectors can have separate routines per stream Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 53 ++- internal/events/eventstream_test.go | 311 ++++++++++++------ internal/events/listener.go | 13 +- mocks/eventsmocks/stream.go | 25 +- mocks/ffcapimocks/api.go | 30 ++ pkg/ffcapi/api.go | 3 + pkg/ffcapi/event_listener_add.go | 9 +- pkg/ffcapi/event_stream_start.go | 33 ++ .../route_delete_eventstream_listener_test.go | 1 + pkg/fftm/route_delete_eventstream_test.go | 6 + pkg/fftm/route_delete_subscription_test.go | 1 + .../route_get_eventstream_listener_test.go | 1 + .../route_get_eventstream_listeners_test.go | 1 + pkg/fftm/route_get_eventstream_test.go | 6 + pkg/fftm/route_get_eventstreams_test.go | 6 + pkg/fftm/route_get_subscription_test.go | 1 + pkg/fftm/route_get_subscriptions_test.go | 1 + .../route_patch_eventstream_listener_test.go | 1 + pkg/fftm/route_patch_eventstream_test.go | 6 + pkg/fftm/route_patch_subscription_test.go | 1 + ...te_post_eventstream_listener_reset_test.go | 1 + .../route_post_eventstream_listeners_test.go | 1 + .../route_post_eventstream_resume_test.go | 6 + .../route_post_eventstream_suspend_test.go | 6 + pkg/fftm/route_post_eventstream_test.go | 6 + .../route_post_subscription_reset_test.go | 1 + pkg/fftm/route_post_subscriptions_test.go | 1 + pkg/fftm/stream_management.go | 45 +-- pkg/fftm/stream_management_test.go | 35 +- 29 files changed, 426 insertions(+), 185 deletions(-) create mode 100644 pkg/ffcapi/event_stream_start.go diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index f25daf0b..ee9487f2 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -114,6 +114,7 @@ func NewEventStream( persistence persistence.Persistence, confirmations confirmations.Manager, wsChannels ws.WebSocketChannels, + initialListeners []*apitypes.Listener, ) (ees Stream, err error) { es := &eventStream{ bgCtx: log.WithLogField(bgCtx, "eventstream", persistedSpec.ID.String()), @@ -131,6 +132,16 @@ func NewEventStream( if es.spec, _, err = mergeValidateEsConfig(es.bgCtx, nil, persistedSpec); err != nil { return nil, err } + for _, existing := range initialListeners { + spec, err := es.verifyListenerOptions(es.bgCtx, existing.ID, existing) + if err != nil { + return nil, err + } + es.listeners[*spec.ID] = &listener{ + es: es, + spec: spec, + } + } log.L(es.bgCtx).Infof("Initialized Event Stream") return es, nil } @@ -313,11 +324,9 @@ func (es *eventStream) mergeListenerOptions(id *fftypes.UUID, updates *apitypes. } -func (es *eventStream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, updates *apitypes.Listener, reset bool) (merged *apitypes.Listener, err error) { - log.L(ctx).Warnf("Initializing listener %s", id) - +func (es *eventStream) verifyListenerOptions(ctx context.Context, id *fftypes.UUID, updatesOrNew *apitypes.Listener) (*apitypes.Listener, error) { // Merge the supplied options with defaults and any existing config. - spec := es.mergeListenerOptions(id, updates) + spec := es.mergeListenerOptions(id, updatesOrNew) // The connector needs to validate the options, building a set of options that are assured to be non-nil res, _, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.EventListenerVerifyOptionsRequest{ @@ -334,9 +343,21 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID sig := spec.Signature spec.Name = &sig } + log.L(ctx).Infof("Listener %s signature: %s", spec.ID, spec.Signature) + return spec, nil +} + +func (es *eventStream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, updates *apitypes.Listener, reset bool) (merged *apitypes.Listener, err error) { + log.L(ctx).Warnf("Adding/updating listener %s", id) + + // Ask the connector to verify the options, and apply defaults + spec, err := es.verifyListenerOptions(ctx, id, updates) + if err != nil { + return nil, err + } // Do the locked part - which checks if this is a new listener, or just an update to the options. - new, l, startedState, err := es.lockedListenerUpdate(ctx, spec, res, reset) + new, l, startedState, err := es.lockedListenerUpdate(ctx, spec, reset) if err != nil { return nil, err } @@ -373,17 +394,17 @@ func (es *eventStream) resetListenerCheckpoint(ctx context.Context, l *listener) return es.persistence.WriteCheckpoint(ctx, cp) } -func (es *eventStream) lockedListenerUpdate(ctx context.Context, spec *apitypes.Listener, res *ffcapi.EventListenerVerifyOptionsResponse, reset bool) (bool, *listener, *startedStreamState, error) { +func (es *eventStream) lockedListenerUpdate(ctx context.Context, spec *apitypes.Listener, reset bool) (bool, *listener, *startedStreamState, error) { es.mux.Lock() defer es.mux.Unlock() l, exists := es.listeners[*spec.ID] switch { case exists: - if res.ResolvedSignature != l.spec.Signature { + if spec.Signature != l.spec.Signature { // We do not allow the filters to be updated, because that would lead to a confusing situation // where the previously emitted events are a subset/mismatch to the filters configured now. - return false, nil, nil, i18n.NewError(ctx, tmmsgs.MsgFilterUpdateNotAllowed, l.spec.Signature, res.ResolvedSignature) + return false, nil, nil, i18n.NewError(ctx, tmmsgs.MsgFilterUpdateNotAllowed, l.spec.Signature, spec.Signature) } l.spec = spec case reset: @@ -450,14 +471,18 @@ func (es *eventStream) Start(ctx context.Context) error { es.currentState = startedState es.initAction(startedState) go es.eventLoop(startedState) - var lastErr error + + initialListeners := make([]*ffcapi.EventListenerAddRequest, 0) for _, l := range es.listeners { - if err := l.start(startedState); err != nil { - log.L(ctx).Errorf("Failed to start event listener %s: %s", l.spec.ID, err) - lastErr = err - } + initialListeners = append(initialListeners, l.buildAddRequest()) } - return lastErr + _, _, err := es.connector.EventStreamStart(startedState.ctx, &ffcapi.EventStreamStartRequest{ + ID: es.spec.ID, + EventStream: startedState.updates, + StreamContext: startedState.ctx, + InitialListeners: initialListeners, + }) + return err } func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, error) { diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index bfd736cf..26210626 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -48,16 +48,26 @@ func testESConf(t *testing.T, j string) (spec *apitypes.EventStream) { } func newTestEventStream(t *testing.T, conf string) (es *eventStream) { + es, err := newTestEventStreamWithListener(t, &ffcapimocks.API{}, conf) + assert.NoError(t, err) + return es +} + +func newTestEventStreamWithListener(t *testing.T, mfc *ffcapimocks.API, conf string, listeners ...*apitypes.Listener) (es *eventStream, err error) { tmconfig.Reset() config.Set(tmconfig.EventStreamsDefaultsBatchTimeout, "1us") InitDefaults() ees, err := NewEventStream(context.Background(), testESConf(t, conf), - &ffcapimocks.API{}, + mfc, &persistencemocks.Persistence{}, &confirmationsmocks.Manager{}, - &wsmocks.WebSocketChannels{}) - assert.NoError(t, err) - return ees.(*eventStream) + &wsmocks.WebSocketChannels{}, + listeners, + ) + if err != nil { + return nil, err + } + return ees.(*eventStream), err } func mockWSChannels(wsc *wsmocks.WebSocketChannels) (chan interface{}, chan interface{}, chan error) { @@ -75,7 +85,9 @@ func TestNewTestEventStreamMissingID(t *testing.T) { &ffcapimocks.API{}, &persistencemocks.Persistence{}, &confirmationsmocks.Manager{}, - &wsmocks.WebSocketChannels{}) + &wsmocks.WebSocketChannels{}, + []*apitypes.Listener{}, + ) assert.Regexp(t, "FF21048", err) } @@ -86,7 +98,9 @@ func TestNewTestEventStreamBadConfig(t *testing.T) { &ffcapimocks.API{}, &persistencemocks.Persistence{}, &confirmationsmocks.Manager{}, - &wsmocks.WebSocketChannels{}) + &wsmocks.WebSocketChannels{}, + []*apitypes.Listener{}, + ) assert.Regexp(t, "FF21028", err) } @@ -278,11 +292,22 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { ResolvedOptions: *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), }, ffcapi.ErrorReason(""), nil) - started := make(chan *ffcapi.EventListenerAddRequest, 1) - mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) started <- r - return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + assert.Len(t, r.InitialListeners, 1) + assert.JSONEq(t, `{ + "event": {"event":"definition"}, + "address": "0x12345" + }`, r.InitialListeners[0].Filters[0].String()) + assert.JSONEq(t, `{ + "option1":"value1", + "option2":"value2" + }`, r.InitialListeners[0].Options.String()) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l.ID) @@ -308,15 +333,6 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { r := <-started - assert.JSONEq(t, `{ - "event": {"event":"definition"}, - "address": "0x12345" - }`, r.Filters[0].String()) - assert.JSONEq(t, `{ - "option1":"value1", - "option2":"value2" - }`, r.Options.String()) - r.EventStream <- &ffcapi.ListenerUpdate{ ListenerID: l.ID, Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), @@ -338,7 +354,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { err = es.Stop(es.bgCtx) assert.NoError(t, err) - <-r.Done + <-r.StreamContext.Done() mfc.AssertExpectations(t) } @@ -384,11 +400,25 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { ResolvedOptions: *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), }, ffcapi.ErrorReason(""), nil) - started := make(chan *ffcapi.EventListenerAddRequest, 1) - mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) started <- r + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventListenerAddRequest) + assert.JSONEq(t, `{"event":"definition1"}`, r.Filters[0].String()) + assert.JSONEq(t, `{"event":"definition2"}`, r.Filters[1].String()) + assert.JSONEq(t, `{ + "option1":"value1", + "option2":"value2" + }`, r.Options.String()) + }).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l.ID) @@ -399,22 +429,15 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" })).Return(nil) - l, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + err := es.Start(es.bgCtx) assert.NoError(t, err) - assert.Equal(t, "EventSig(uint256)", *l.Name) // Defaulted - err = es.Start(es.bgCtx) + l, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) + assert.Equal(t, "EventSig(uint256)", *l.Name) // Defaulted r := <-started - assert.JSONEq(t, `{"event":"definition1"}`, r.Filters[0].String()) - assert.JSONEq(t, `{"event":"definition2"}`, r.Filters[1].String()) - assert.JSONEq(t, `{ - "option1":"value1", - "option2":"value2" - }`, r.Options.String()) - r.EventStream <- &ffcapi.ListenerUpdate{ ListenerID: l.ID, Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), @@ -434,7 +457,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { err = es.Stop(es.bgCtx) assert.NoError(t, err) - <-r.Done + <-r.StreamContext.Done() mfc.AssertExpectations(t) } @@ -461,6 +484,46 @@ func TestConnectorRejectListener(t *testing.T) { mfc.AssertExpectations(t) } +func TestStartWithExistingStreamOk(t *testing.T) { + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := &ffcapimocks.API{} + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + _, err := newTestEventStreamWithListener(t, mfc, `{ + "name": "ut_stream" + }`, l) + assert.NoError(t, err) + + mfc.AssertExpectations(t) +} + +func TestStartWithExistingStreamFail(t *testing.T) { + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := &ffcapimocks.API{} + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + _, err := newTestEventStreamWithListener(t, mfc, `{ + "name": "ut_stream" + }`, l) + assert.Regexp(t, "pop", err) + + mfc.AssertExpectations(t) +} + func TestUpdateStreamStarted(t *testing.T) { es := newTestEventStream(t, `{ @@ -477,11 +540,13 @@ func TestUpdateStreamStarted(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - started := make(chan *ffcapi.EventListenerAddRequest, 1) - mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) started <- r - return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Twice() + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l.ID) @@ -509,13 +574,13 @@ func TestUpdateStreamStarted(t *testing.T) { assert.Equal(t, "ut_stream2", *es.Spec().Name) - <-r.Done + <-r.StreamContext.Done() r = <-started err = es.Stop(es.bgCtx) assert.NoError(t, err) - <-r.Done + <-r.StreamContext.Done() mfc.AssertExpectations(t) } @@ -536,11 +601,13 @@ func TestAddRemoveListener(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - started := make(chan *ffcapi.EventListenerAddRequest, 1) - mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) started <- r - return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l.ID) @@ -563,7 +630,7 @@ func TestAddRemoveListener(t *testing.T) { err = es.Stop(es.bgCtx) assert.NoError(t, err) - <-r.Done + <-r.StreamContext.Done() mfc.AssertExpectations(t) } @@ -584,11 +651,17 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - started := make(chan *ffcapi.EventListenerAddRequest, 1) - mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) started <- r + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { return r.ID.Equals(l1.ID) - })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Twice() + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l1.ID) @@ -599,10 +672,10 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(fmt.Errorf("pop")).Once() msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) - _, err := es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) + err := es.Start(es.bgCtx) assert.NoError(t, err) - err = es.Start(es.bgCtx) + _, err = es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) assert.NoError(t, err) r := <-started @@ -628,7 +701,7 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { err = es.Delete(es.bgCtx) assert.NoError(t, err) - <-r.Done + <-r.StreamContext.Done() mfc.AssertExpectations(t) } @@ -649,11 +722,13 @@ func TestUpdateListenerFail(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - started := make(chan *ffcapi.EventListenerAddRequest, 1) - mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) started <- r - return r.ID.Equals(l1.ID) - })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { @@ -684,7 +759,7 @@ func TestUpdateListenerFail(t *testing.T) { err = es.Stop(es.bgCtx) assert.NoError(t, err) - <-r.Done + <-r.StreamContext.Done() mfc.AssertExpectations(t) } @@ -722,13 +797,20 @@ func TestUpdateStreamRestartFail(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - started := make(chan *ffcapi.EventListenerAddRequest, 1) - mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { - started <- r - return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() - mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil).Once() mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l.ID) @@ -748,13 +830,13 @@ func TestUpdateStreamRestartFail(t *testing.T) { err = es.UpdateSpec(context.Background(), defChanged) assert.Regexp(t, "FF21032.*pop", err) - <-r.Done + <-r.StreamContext.Done() r = <-started err = es.Stop(es.bgCtx) assert.NoError(t, err) - <-r.Done + <-r.StreamContext.Done() mfc.AssertExpectations(t) } @@ -780,11 +862,12 @@ func TestUpdateAttemptChangeSignature(t *testing.T) { ResolvedSignature: "sig2", }, ffcapi.ErrorReason(""), nil).Once() - started := make(chan *ffcapi.EventListenerAddRequest, 1) - mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { - started <- r - return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l.ID) @@ -807,7 +890,7 @@ func TestUpdateAttemptChangeSignature(t *testing.T) { err = es.Stop(es.bgCtx) assert.NoError(t, err) - <-r.Done + <-r.StreamContext.Done() mfc.AssertExpectations(t) } @@ -851,11 +934,12 @@ func TestUpdateStreamStopFail(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - started := make(chan *ffcapi.EventListenerAddRequest, 1) - mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { - started <- r - return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Twice() @@ -883,12 +967,12 @@ func TestUpdateStreamStopFail(t *testing.T) { err = es.Stop(es.bgCtx) assert.NoError(t, err) - <-r.Done + <-r.StreamContext.Done() mfc.AssertExpectations(t) } -func TestResetListenerWriteCheckpointFail(t *testing.T) { +func TestResetListenerRestartFail(t *testing.T) { es := newTestEventStream(t, `{ "name": "ut_stream" @@ -903,23 +987,44 @@ func TestResetListenerWriteCheckpointFail(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() + + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ID.Equals(l.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() + msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ StreamID: es.spec.ID, Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), }, nil) - msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(nil) + msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) - _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, true) assert.Regexp(t, "pop", err) + err = es.Delete(es.bgCtx) + assert.NoError(t, err) + mfc.AssertExpectations(t) } -func TestResetListenerRestartFail(t *testing.T) { +func TestResetListenerWriteCheckpointFail(t *testing.T) { es := newTestEventStream(t, `{ "name": "ut_stream" @@ -934,33 +1039,19 @@ func TestResetListenerRestartFail(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() - mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) - - mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() - msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ StreamID: es.spec.ID, Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), }, nil) - msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(nil) - msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) - - err := es.Start(es.bgCtx) - assert.NoError(t, err) + msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) - _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, true) assert.Regexp(t, "pop", err) - err = es.Delete(es.bgCtx) - assert.NoError(t, err) - mfc.AssertExpectations(t) } @@ -996,11 +1087,12 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - started := make(chan *ffcapi.EventListenerAddRequest, 1) - mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { - started <- r - return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) first := true @@ -1043,7 +1135,7 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { assert.Len(t, batch1, 1) assert.Equal(t, "v1", batch1[0].Data.JSONObject().GetString("k1")) - <-r.Done + <-r.StreamContext.Done() <-done mfc.AssertExpectations(t) @@ -1057,6 +1149,11 @@ func TestActionRetryOk(t *testing.T) { "retryTimeout": "1s" }`) + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + err := es.Start(es.bgCtx) assert.NoError(t, err) @@ -1097,6 +1194,11 @@ func TestActionRetrySkip(t *testing.T) { "retryTimeout": "0s" }`) + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + err := es.Start(es.bgCtx) assert.NoError(t, err) @@ -1128,6 +1230,11 @@ func TestActionRetryBlock(t *testing.T) { "retryTimeout": "0s" }`) + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + err := es.Start(es.bgCtx) assert.NoError(t, err) diff --git a/internal/events/listener.go b/internal/events/listener.go index fc50d782..2124e52c 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -43,13 +43,16 @@ func (l *listener) stop(startedState *startedStreamState) error { return err } -func (l *listener) start(startedState *startedStreamState) error { - _, _, err := l.es.connector.EventListenerAdd(startedState.ctx, &ffcapi.EventListenerAddRequest{ +func (l *listener) buildAddRequest() *ffcapi.EventListenerAddRequest { + return &ffcapi.EventListenerAddRequest{ EventListenerOptions: listenerSpecToOptions(l.spec), Name: *l.spec.Name, ID: l.spec.ID, - EventStream: startedState.updates, - Done: startedState.ctx.Done(), - }) + StreamID: l.spec.StreamID, + } +} + +func (l *listener) start(startedState *startedStreamState) error { + _, _, err := l.es.connector.EventListenerAdd(startedState.ctx, l.buildAddRequest()) return err } diff --git a/mocks/eventsmocks/stream.go b/mocks/eventsmocks/stream.go index 3049a26f..85b2bc05 100644 --- a/mocks/eventsmocks/stream.go +++ b/mocks/eventsmocks/stream.go @@ -17,18 +17,27 @@ type Stream struct { mock.Mock } -// AddOrUpdateListener provides a mock function with given fields: ctx, s, reset -func (_m *Stream) AddOrUpdateListener(ctx context.Context, s *apitypes.Listener, reset bool) error { - ret := _m.Called(ctx, s, reset) +// AddOrUpdateListener provides a mock function with given fields: ctx, id, updates, reset +func (_m *Stream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, updates *apitypes.Listener, reset bool) (*apitypes.Listener, error) { + ret := _m.Called(ctx, id, updates, reset) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *apitypes.Listener, bool) error); ok { - r0 = rf(ctx, s, reset) + var r0 *apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, *apitypes.Listener, bool) *apitypes.Listener); ok { + r0 = rf(ctx, id, updates, reset) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.Listener) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, *apitypes.Listener, bool) error); ok { + r1 = rf(ctx, id, updates, reset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // Delete provides a mock function with given fields: ctx diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index 4054b026..ad0cda74 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -164,6 +164,36 @@ func (_m *API) EventListenerVerifyOptions(ctx context.Context, req *ffcapi.Event return r0, r1, r2 } +// EventStreamStart provides a mock function with given fields: ctx, req +func (_m *API) EventStreamStart(ctx context.Context, req *ffcapi.EventStreamStartRequest) (*ffcapi.EventStreamStartResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.EventStreamStartResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventStreamStartRequest) *ffcapi.EventStreamStartResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.EventStreamStartResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventStreamStartRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventStreamStartRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // GasPriceEstimate provides a mock function with given fields: ctx, req func (_m *API) GasPriceEstimate(ctx context.Context, req *ffcapi.GasPriceEstimateRequest) (*ffcapi.GasPriceEstimateResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index ca1c4b43..7853b104 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -53,6 +53,9 @@ type API interface { // TransactionSend combines a previously prepared encoded transaction, with a current gas price, and submits it to the transaction pool of the blockchain for mining TransactionSend(ctx context.Context, req *TransactionSendRequest) (*TransactionSendResponse, ErrorReason, error) + // EventStreamStart starts an event stream with an initial set of listeners (which might be empty), a channel to deliver events, and a context that will close to stop the stream + EventStreamStart(ctx context.Context, req *EventStreamStartRequest) (*EventStreamStartResponse, ErrorReason, error) + // EventListenerVerifyOptions validates the configuration options for a listener, applying any defaults needed by the connector, and returning the update options for FFTM to persist EventListenerVerifyOptions(ctx context.Context, req *EventListenerVerifyOptionsRequest) (*EventListenerVerifyOptionsResponse, ErrorReason, error) diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go index be6900e1..63c4e908 100644 --- a/pkg/ffcapi/event_listener_add.go +++ b/pkg/ffcapi/event_listener_add.go @@ -42,11 +42,10 @@ type EventListenerVerifyOptionsResponse struct { type EventListenerAddRequest struct { EventListenerOptions - ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event - Name string // Descriptive name of the listener, provided by the user, or defaulted to the signature. Not guaranteed to be unique. Should be included in the event info - Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream - Done <-chan struct{} // Channel that will be closed when the event listener needs to stop - the event listener should stop pushing events - EventStream chan<- *ListenerUpdate // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events + ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event + StreamID *fftypes.UUID // The event stream (previously started) to which events should be delivered + Name string // Descriptive name of the listener, provided by the user, or defaulted to the signature. Not guaranteed to be unique. Should be included in the event info + Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream } type EventListenerAddResponse struct { diff --git a/pkg/ffcapi/event_stream_start.go b/pkg/ffcapi/event_stream_start.go new file mode 100644 index 00000000..38b137a9 --- /dev/null +++ b/pkg/ffcapi/event_stream_start.go @@ -0,0 +1,33 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type EventStreamStartRequest struct { + ID *fftypes.UUID // UUID of the stream, which we be referenced in any future add/remove listener requests + StreamContext context.Context // Context that will be cancelled when the event stream needs to stop - no further events will be consumed after this, so all pushes to the stream should select on the done channel too + EventStream chan<- *ListenerUpdate // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events + InitialListeners []*EventListenerAddRequest // Initial list of event listeners to start with the stream - allows these to be started concurrently +} + +type EventStreamStartResponse struct { +} diff --git a/pkg/fftm/route_delete_eventstream_listener_test.go b/pkg/fftm/route_delete_eventstream_listener_test.go index a81121d2..27dbf885 100644 --- a/pkg/fftm/route_delete_eventstream_listener_test.go +++ b/pkg/fftm/route_delete_eventstream_listener_test.go @@ -38,6 +38,7 @@ func TestDeleteEventStreamListener(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) diff --git a/pkg/fftm/route_delete_eventstream_test.go b/pkg/fftm/route_delete_eventstream_test.go index e6d94d89..50b33def 100644 --- a/pkg/fftm/route_delete_eventstream_test.go +++ b/pkg/fftm/route_delete_eventstream_test.go @@ -21,8 +21,11 @@ import ( "testing" "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestDeleteEventStream(t *testing.T) { @@ -30,6 +33,9 @@ func TestDeleteEventStream(t *testing.T) { url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) defer done() + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_delete_subscription_test.go b/pkg/fftm/route_delete_subscription_test.go index 6f798457..41694d95 100644 --- a/pkg/fftm/route_delete_subscription_test.go +++ b/pkg/fftm/route_delete_subscription_test.go @@ -37,6 +37,7 @@ func TestDeleteSubscription(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) diff --git a/pkg/fftm/route_get_eventstream_listener_test.go b/pkg/fftm/route_get_eventstream_listener_test.go index 538fbb0f..b5fce9f5 100644 --- a/pkg/fftm/route_get_eventstream_listener_test.go +++ b/pkg/fftm/route_get_eventstream_listener_test.go @@ -38,6 +38,7 @@ func TestGetEventStreamsListener(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_get_eventstream_listeners_test.go b/pkg/fftm/route_get_eventstream_listeners_test.go index 31c71008..ccecf800 100644 --- a/pkg/fftm/route_get_eventstream_listeners_test.go +++ b/pkg/fftm/route_get_eventstream_listeners_test.go @@ -38,6 +38,7 @@ func TestGetEventStreamListeners(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_get_eventstream_test.go b/pkg/fftm/route_get_eventstream_test.go index a646807d..ebca73eb 100644 --- a/pkg/fftm/route_get_eventstream_test.go +++ b/pkg/fftm/route_get_eventstream_test.go @@ -21,8 +21,11 @@ import ( "testing" "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestGetEventStream(t *testing.T) { @@ -30,6 +33,9 @@ func TestGetEventStream(t *testing.T) { url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) defer done() + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_get_eventstreams_test.go b/pkg/fftm/route_get_eventstreams_test.go index 106d00dc..c57fd9f8 100644 --- a/pkg/fftm/route_get_eventstreams_test.go +++ b/pkg/fftm/route_get_eventstreams_test.go @@ -21,8 +21,11 @@ import ( "testing" "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestGetEventStreams(t *testing.T) { @@ -30,6 +33,9 @@ func TestGetEventStreams(t *testing.T) { url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) defer done() + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_get_subscription_test.go b/pkg/fftm/route_get_subscription_test.go index c3bba766..bef5cf26 100644 --- a/pkg/fftm/route_get_subscription_test.go +++ b/pkg/fftm/route_get_subscription_test.go @@ -37,6 +37,7 @@ func TestGetSubscription(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_get_subscriptions_test.go b/pkg/fftm/route_get_subscriptions_test.go index 76364500..8944de1b 100644 --- a/pkg/fftm/route_get_subscriptions_test.go +++ b/pkg/fftm/route_get_subscriptions_test.go @@ -37,6 +37,7 @@ func TestGetSubscriptions(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_patch_eventstream_listener_test.go b/pkg/fftm/route_patch_eventstream_listener_test.go index 69cfb6d5..3053ccac 100644 --- a/pkg/fftm/route_patch_eventstream_listener_test.go +++ b/pkg/fftm/route_patch_eventstream_listener_test.go @@ -38,6 +38,7 @@ func TestPatchEventStreamListener(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_patch_eventstream_test.go b/pkg/fftm/route_patch_eventstream_test.go index 9633924e..cb184ef9 100644 --- a/pkg/fftm/route_patch_eventstream_test.go +++ b/pkg/fftm/route_patch_eventstream_test.go @@ -21,8 +21,11 @@ import ( "testing" "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestPatchEventStream(t *testing.T) { @@ -30,6 +33,9 @@ func TestPatchEventStream(t *testing.T) { url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) defer done() + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_patch_subscription_test.go b/pkg/fftm/route_patch_subscription_test.go index 694099d7..0e0ff95c 100644 --- a/pkg/fftm/route_patch_subscription_test.go +++ b/pkg/fftm/route_patch_subscription_test.go @@ -37,6 +37,7 @@ func TestPatchSubscription(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_post_eventstream_listener_reset_test.go b/pkg/fftm/route_post_eventstream_listener_reset_test.go index b81d17b6..07c4ca27 100644 --- a/pkg/fftm/route_post_eventstream_listener_reset_test.go +++ b/pkg/fftm/route_post_eventstream_listener_reset_test.go @@ -38,6 +38,7 @@ func TestPostEventStreamListenerReset(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_post_eventstream_listeners_test.go b/pkg/fftm/route_post_eventstream_listeners_test.go index 67993e12..0406151b 100644 --- a/pkg/fftm/route_post_eventstream_listeners_test.go +++ b/pkg/fftm/route_post_eventstream_listeners_test.go @@ -38,6 +38,7 @@ func TestPostEventStreamListeners(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_post_eventstream_resume_test.go b/pkg/fftm/route_post_eventstream_resume_test.go index 50ad56a3..a80963ab 100644 --- a/pkg/fftm/route_post_eventstream_resume_test.go +++ b/pkg/fftm/route_post_eventstream_resume_test.go @@ -21,8 +21,11 @@ import ( "testing" "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestPostEventStreamResume(t *testing.T) { @@ -30,6 +33,9 @@ func TestPostEventStreamResume(t *testing.T) { url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) defer done() + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_post_eventstream_suspend_test.go b/pkg/fftm/route_post_eventstream_suspend_test.go index 3ab8a739..9d241a3c 100644 --- a/pkg/fftm/route_post_eventstream_suspend_test.go +++ b/pkg/fftm/route_post_eventstream_suspend_test.go @@ -21,8 +21,11 @@ import ( "testing" "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestPostEventStreamSuspend(t *testing.T) { @@ -30,6 +33,9 @@ func TestPostEventStreamSuspend(t *testing.T) { url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) defer done() + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_post_eventstream_test.go b/pkg/fftm/route_post_eventstream_test.go index 43ede403..66befe0a 100644 --- a/pkg/fftm/route_post_eventstream_test.go +++ b/pkg/fftm/route_post_eventstream_test.go @@ -21,8 +21,11 @@ import ( "testing" "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestPostNewEventStream(t *testing.T) { @@ -30,6 +33,9 @@ func TestPostNewEventStream(t *testing.T) { url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) defer done() + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_post_subscription_reset_test.go b/pkg/fftm/route_post_subscription_reset_test.go index 67d1c2d1..501efa03 100644 --- a/pkg/fftm/route_post_subscription_reset_test.go +++ b/pkg/fftm/route_post_subscription_reset_test.go @@ -37,6 +37,7 @@ func TestPostSubscriptionReset(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/route_post_subscriptions_test.go b/pkg/fftm/route_post_subscriptions_test.go index 622bb9a0..84629efa 100644 --- a/pkg/fftm/route_post_subscriptions_test.go +++ b/pkg/fftm/route_post_subscriptions_test.go @@ -37,6 +37,7 @@ func TestPostSubscriptions(t *testing.T) { assert.NoError(t, err) mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index 8d86aa93..b1c22334 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -44,10 +44,14 @@ func (m *manager) restoreStreams() error { } for _, def := range streamDefs { lastInPage = def.ID + streamListeners, err := m.persistence.ListStreamListeners(m.ctx, nil, 0, def.ID) + if err != nil { + return err + } closeoutName, err := m.reserveStreamName(m.ctx, *def.Name, def.ID) var s events.Stream if err == nil { - s, err = m.addRuntimeStream(def) + s, err = m.addRuntimeStream(def, streamListeners) } if err == nil && !*def.Suspended { err = s.Start(m.ctx) @@ -58,26 +62,6 @@ func (m *manager) restoreStreams() error { } } } - return m.restoreListeners() -} - -func (m *manager) restoreListeners() error { - var lastInPage *fftypes.UUID - for { - listenerDefs, err := m.persistence.ListListeners(m.ctx, lastInPage, startupPaginationLimit) - if err != nil { - return err - } - if len(listenerDefs) == 0 { - break - } - for _, def := range listenerDefs { - lastInPage = def.ID - if _, err := m.addRuntimeListener(m.ctx, def); err != nil { - return err - } - } - } return nil } @@ -101,21 +85,8 @@ func (m *manager) deleteAllStreamListeners(ctx context.Context, streamID *fftype return nil } -func (m *manager) addRuntimeListener(ctx context.Context, def *apitypes.Listener) (*apitypes.Listener, error) { - m.mux.Lock() - s := m.eventStreams[*def.StreamID] - m.mux.Unlock() - if s != nil { - // The definition is updated in-place by the event stream code - if _, err := s.AddOrUpdateListener(ctx, def.ID, def, false); err != nil { - return nil, err - } - } - return def, nil -} - -func (m *manager) addRuntimeStream(def *apitypes.EventStream) (events.Stream, error) { - s, err := events.NewEventStream(m.ctx, def, m.connector, m.persistence, m.confirmations, m.wsChannels) +func (m *manager) addRuntimeStream(def *apitypes.EventStream, listeners []*apitypes.Listener) (events.Stream, error) { + s, err := events.NewEventStream(m.ctx, def, m.connector, m.persistence, m.confirmations, m.wsChannels, listeners) if err != nil { return nil, err } @@ -195,7 +166,7 @@ func (m *manager) createAndStoreNewStream(ctx context.Context, def *apitypes.Eve } defer func() { closeoutName(stored) }() - s, err := m.addRuntimeStream(def) + s, err := m.addRuntimeStream(def, nil /* no listeners when a new stream is first created */) if err != nil { return nil, err } diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go index 6ea835b7..a703ebac 100644 --- a/pkg/fftm/stream_management_test.go +++ b/pkg/fftm/stream_management_test.go @@ -35,8 +35,8 @@ func TestRestoreStreamsAndListenersOK(t *testing.T) { defer done() mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() falsy := false @@ -87,9 +87,12 @@ func TestRestoreListenersReadFailed(t *testing.T) { mp, _, m := newMockPersistenceManager(t) - mp.On("ListListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit).Return(nil, fmt.Errorf("pop")) + mp.On("ListStreams", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit).Return([]*apitypes.EventStream{ + {ID: fftypes.NewUUID()}, + }, nil) + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), 0, mock.Anything).Return(nil, fmt.Errorf("pop")) - err := m.restoreListeners() + err := m.restoreStreams() assert.Regexp(t, "pop", err) mp.AssertExpectations(t) @@ -117,7 +120,7 @@ func TestRestoreListenersStartFail(t *testing.T) { mfc := m.connector.(*ffcapimocks.API) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")) falsy := false es1 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr("stream1"), Suspended: &falsy} @@ -142,7 +145,7 @@ func TestDeleteStartedListener(t *testing.T) { mfc := m.connector.(*ffcapimocks.API) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) falsy := false @@ -233,7 +236,9 @@ func TestDeleteStreamNotInitialized(t *testing.T) { func TestCreateRenameStreamNameReservation(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + mp, mfc, m := newMockPersistenceManager(t) + + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mp.On("WriteStream", m.ctx, mock.Anything).Return(fmt.Errorf("temporary")).Once() mp.On("DeleteCheckpoint", m.ctx, mock.Anything).Return(fmt.Errorf("temporary")).Once() @@ -307,11 +312,11 @@ func TestCreateOrUpdateListenerNotFound(t *testing.T) { } func TestCreateOrUpdateListenerFail(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + mp, mfc, m := newMockPersistenceManager(t) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) - mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) @@ -324,12 +329,12 @@ func TestCreateOrUpdateListenerFail(t *testing.T) { } func TestCreateOrUpdateListenerWriteFail(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + mp, mfc, m := newMockPersistenceManager(t) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) mp.On("WriteListener", m.ctx, mock.Anything).Return(fmt.Errorf("pop")) - mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) @@ -364,12 +369,12 @@ func TestDeleteListenerStreamNotFound(t *testing.T) { } func TestDeleteListenerFail(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + mp, mfc, m := newMockPersistenceManager(t) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) mp.On("WriteListener", m.ctx, mock.Anything).Return(nil) - mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) @@ -405,8 +410,9 @@ func TestUpdateStreamNotFound(t *testing.T) { } func TestUpdateStreamBadChanges(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + mp, mfc, m := newMockPersistenceManager(t) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) @@ -418,8 +424,9 @@ func TestUpdateStreamBadChanges(t *testing.T) { } func TestUpdateStreamWriteFail(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + mp, mfc, m := newMockPersistenceManager(t) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil).Once() mp.On("WriteStream", m.ctx, mock.Anything).Return(fmt.Errorf("pop")) From 176459a6324714d1d964e5c3a1788b1ba27bbe61 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 1 Jul 2022 15:15:01 -0400 Subject: [PATCH 35/95] Refactor to separate stream go routines and confirmation managers Signed-off-by: Peter Broadhurst --- config.md | 2 +- internal/confirmations/confirmations.go | 103 ++---- internal/confirmations/confirmations_test.go | 335 ++++++++----------- internal/events/eventstream.go | 126 +++++-- internal/events/eventstream_test.go | 121 +++++-- internal/tmconfig/tmconfig.go | 4 +- internal/tmmsgs/en_config_descriptions.go | 2 +- mocks/confirmationsmocks/manager.go | 18 + mocks/ffcapimocks/api.go | 16 - pkg/ffcapi/api.go | 53 ++- pkg/ffcapi/api_test.go | 20 +- pkg/ffcapi/event_stream_start.go | 1 + pkg/fftm/manager_test.go | 2 +- pkg/fftm/stream_management.go | 2 +- 14 files changed, 453 insertions(+), 352 deletions(-) diff --git a/config.md b/config.md index 4e8da5e9..d8b6913c 100644 --- a/config.md +++ b/config.md @@ -44,7 +44,7 @@ nav_order: 2 |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|blockPollingInterval|How often to poll for new block headers|[`time.Duration`](https://pkg.go.dev/time#Duration)|`3s` +|blockQueueLength|Internal queue length for notifying the confirmations manager of new blocks|`int`|`50` |notificationQueueLength|Internal queue length for notifying the confirmations manager of new transactions/events|`int`|`50` |required|Number of confirmations required to consider a transaction/event final|`int`|`20` |staleReceiptTimeout|Duration after which to force a receipt check for a pending transaction|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1m` diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index 2dec8fa4..b05d78ae 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -37,6 +37,7 @@ type Manager interface { Notify(n *Notification) error Start() Stop() + NewBlockHashes() chan<- *ffcapi.BlockHashEvent } type NotificationType int @@ -46,25 +47,17 @@ const ( RemovedEventLog NewTransaction RemovedTransaction - StopStream ) type Notification struct { NotificationType NotificationType Event *EventInfo Transaction *TransactionInfo - StoppedStream *StoppedStreamInfo } type EventInfo struct { - StreamID string - BlockHash string - BlockNumber uint64 - TransactionHash string - TransactionIndex uint64 - LogIndex uint64 - Receipt func(receipt *ffcapi.TransactionReceiptResponse) - Confirmed func(confirmations []BlockInfo) + ffcapi.EventID + Confirmed func(confirmations []BlockInfo) } type TransactionInfo struct { @@ -86,12 +79,13 @@ type BlockInfo struct { } type blockConfirmationManager struct { + baseContext context.Context ctx context.Context cancelFunc func() + newBlockHashes chan *ffcapi.BlockHashEvent connector ffcapi.API blockListenerStale bool requiredConfirmations int - pollingInterval time.Duration staleReceiptTimeout time.Duration bcmNotifications chan *Notification highestBlockSeen uint64 @@ -100,18 +94,19 @@ type blockConfirmationManager struct { done chan struct{} } -func NewBlockConfirmationManager(ctx context.Context, connector ffcapi.API) Manager { +func NewBlockConfirmationManager(baseContext context.Context, connector ffcapi.API) Manager { bcm := &blockConfirmationManager{ + baseContext: baseContext, connector: connector, blockListenerStale: true, requiredConfirmations: config.GetInt(tmconfig.ConfirmationsRequired), - pollingInterval: config.GetDuration(tmconfig.ConfirmationsBlockPollingInterval), staleReceiptTimeout: config.GetDuration(tmconfig.ConfirmationsStaleReceiptTimeout), bcmNotifications: make(chan *Notification, config.GetInt(tmconfig.ConfirmationsNotificationQueueLength)), pending: make(map[string]*pendingItem), staleReceipts: make(map[string]bool), + newBlockHashes: make(chan *ffcapi.BlockHashEvent, config.GetInt(tmconfig.ConfirmationsBlockQueueLength)), } - bcm.ctx, bcm.cancelFunc = context.WithCancel(ctx) + bcm.ctx, bcm.cancelFunc = context.WithCancel(baseContext) return bcm } @@ -132,7 +127,6 @@ type pendingItem struct { receiptCallback func(receipt *ffcapi.TransactionReceiptResponse) confirmedCallback func(confirmations []BlockInfo) transactionHash string - streamID string // events only blockHash string // can be notified of changes to this for receipts blockNumber uint64 // known at creation time for event logs transactionIndex uint64 // known at creation time for event logs @@ -148,7 +142,7 @@ func (pi *pendingItem) getKey() string { case pendingTypeEvent: // For events they are identified by their hash, blockNumber, transactionIndex and logIndex // If any of those change, it's a new new event - and as such we should get informed of it separately by the blockchain connector. - return fmt.Sprintf("Event[%s]:th=%s,bh=%s,bn=%d,ti=%d,li=%d", pi.streamID, pi.transactionHash, pi.blockHash, pi.blockNumber, pi.transactionIndex, pi.logIndex) + return fmt.Sprintf("Event:th=%s,bh=%s,bn=%d,ti=%d,li=%d", pi.transactionHash, pi.blockHash, pi.blockNumber, pi.transactionIndex, pi.logIndex) case pendingTypeTransaction: // For transactions, it's simply the transaction hash that identifies it. It can go into any block return pendingKeyForTX(pi.transactionHash) @@ -175,7 +169,6 @@ func (n *Notification) eventPendingItem() *pendingItem { pType: pendingTypeEvent, blockNumber: n.Event.BlockNumber, blockHash: n.Event.BlockHash, - streamID: n.Event.StreamID, transactionHash: n.Event.TransactionHash, transactionIndex: n.Event.TransactionIndex, logIndex: n.Event.LogIndex, @@ -213,25 +206,30 @@ func (bcm *blockConfirmationManager) Start() { } func (bcm *blockConfirmationManager) Stop() { - bcm.cancelFunc() - <-bcm.done + if bcm.done != nil { + bcm.cancelFunc() + <-bcm.done + bcm.done = nil + // Reset context ready for restart + bcm.ctx, bcm.cancelFunc = context.WithCancel(bcm.baseContext) + } +} + +func (bcm *blockConfirmationManager) NewBlockHashes() chan<- *ffcapi.BlockHashEvent { + return bcm.newBlockHashes } // Notify is used to notify the confirmation manager of detection of a new logEntry addition or removal func (bcm *blockConfirmationManager) Notify(n *Notification) error { switch n.NotificationType { case NewEventLog, RemovedEventLog: - if n.Event == nil || n.Event.StreamID == "" || n.Event.TransactionHash == "" || n.Event.BlockHash == "" { + if n.Event == nil || n.Event.TransactionHash == "" || n.Event.BlockHash == "" { return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n) } case NewTransaction, RemovedTransaction: if n.Transaction == nil || n.Transaction.TransactionHash == "" { return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n) } - case StopStream: - if n.StoppedStream == nil || n.StoppedStream.Completed == nil { - return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n) - } } select { case bcm.bcmNotifications <- n: @@ -283,45 +281,24 @@ func transformBlockInfo(res *ffcapi.BlockInfo) *BlockInfo { } } -func (bcm *blockConfirmationManager) getNewBlockHashes() []string { - var blockHashes []string +func (bcm *blockConfirmationManager) confirmationsListener() { + defer close(bcm.done) + notifications := make([]*Notification, 0) + blockHashes := make([]string, 0) for { select { - case bhe := <-bcm.connector.NewBlockHashes(): + case bhe := <-bcm.newBlockHashes: if bhe.GapPotential { bcm.blockListenerStale = true } blockHashes = append(blockHashes, bhe.BlockHashes...) - default: - return blockHashes - } - } -} - -func (bcm *blockConfirmationManager) confirmationsListener() { - defer close(bcm.done) - pollTimer := time.NewTimer(0) - notifications := make([]*Notification, 0) - for { - popped := false - for !popped { - select { - case <-pollTimer.C: - popped = true - case <-bcm.ctx.Done(): - log.L(bcm.ctx).Debugf("Block confirmation listener stopping") - return - case notification := <-bcm.bcmNotifications: - if notification.NotificationType == StopStream { - // Handle stream notifications immediately - bcm.streamStopped(notification) - } else { - // Defer until after we've got new logs - notifications = append(notifications, notification) - } - } + case <-bcm.ctx.Done(): + log.L(bcm.ctx).Debugf("Block confirmation listener stopping") + return + case notification := <-bcm.bcmNotifications: + // Defer until after we've got new logs + notifications = append(notifications, notification) } - pollTimer = time.NewTimer(bcm.pollingInterval) if bcm.blockListenerStale { if err := bcm.walkChain(); err != nil { @@ -332,7 +309,9 @@ func (bcm *blockConfirmationManager) confirmationsListener() { } // Process each new block - bcm.processBlockHashes(bcm.getNewBlockHashes()) + bcm.processBlockHashes(blockHashes) + // Truncate the block hashes now we've processed them + blockHashes = blockHashes[:0] // Process any new notifications - we do this at the end, so it can benefit // from knowing the latest highestBlockSeen @@ -427,16 +406,6 @@ func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem) { delete(bcm.staleReceipts, pending.getKey()) } -// streamStopped removes all pending work for a given stream, and notifies once done -func (bcm *blockConfirmationManager) streamStopped(notification *Notification) { - for pendingKey, pending := range bcm.pending { - if pending.streamID == notification.StoppedStream.StreamID { - delete(bcm.pending, pendingKey) - } - } - close(notification.StoppedStream.Completed) -} - // addEvent is called by the goroutine on receipt of a new event/transaction notification func (bcm *blockConfirmationManager) addOrReplaceItem(pending *pendingItem) { pending.added = time.Now() diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index 46823419..879db24a 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -34,7 +34,6 @@ import ( func newTestBlockConfirmationManager(t *testing.T, enabled bool) (*blockConfirmationManager, *ffcapimocks.API) { tmconfig.Reset() config.Set(tmconfig.ConfirmationsRequired, 3) - config.Set(tmconfig.ConfirmationsBlockPollingInterval, "10ms") config.Set(tmconfig.ConfirmationsNotificationQueueLength, 1) return newTestBlockConfirmationManagerCustomConfig(t) } @@ -51,25 +50,20 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + EventID: ffcapi.EventID{ + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, Confirmed: func(confirmations []BlockInfo) { confirmed <- confirmations }, } // First poll for changes gives nothing, but we load up the event at this point for the next round - blockHashes := make(chan *ffcapi.BlockHashEvent, 1) - mca.On("NewBlockHashes").Run(func(args mock.Arguments) { - bcm.Notify(&Notification{ - NotificationType: NewEventLog, - Event: eventToConfirm, - }) - }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() + blockHashes := bcm.NewBlockHashes() // Next time round gives a block that is in the confirmation chain, but one block ahead block1003 := &BlockInfo{ @@ -77,16 +71,19 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", ParentHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295", } - mca.On("NewBlockHashes").Run(func(args mock.Arguments) { - blockHashes <- &ffcapi.BlockHashEvent{ - BlockHashes: []string{block1003.BlockHash}, - } - }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{block1003.BlockHash}, + } // The next filter gives us 1003 - which is two blocks ahead of our notified log mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003.BlockHash - })).Return(&ffcapi.BlockInfoByHashResponse{ + })).Run(func(args mock.Arguments) { + bcm.Notify(&Notification{ + NotificationType: NewEventLog, + Event: eventToConfirm, + }) + }).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)), BlockHash: block1003.BlockHash, @@ -143,9 +140,6 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - // Subsequent calls get nothing - mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) - bcm.Start() dispatched := <-confirmed @@ -156,7 +150,6 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { }, dispatched) bcm.Stop() - <-bcm.done mca.AssertExpectations(t) } @@ -166,12 +159,13 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + EventID: ffcapi.EventID{ + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, Confirmed: func(confirmations []BlockInfo) { confirmed <- confirmations }, @@ -189,15 +183,14 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", } - blockHashes := make(chan *ffcapi.BlockHashEvent, 1) - mca.On("NewBlockHashes").Run(func(args mock.Arguments) { - blockHashes <- &ffcapi.BlockHashEvent{ - BlockHashes: []string{ - block1002.BlockHash, - block1003a.BlockHash, - }, - } - }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() + blockHashes := bcm.NewBlockHashes() + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1002.BlockHash, + block1003a.BlockHash, + }, + } + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1002.BlockHash })).Return(&ffcapi.BlockInfoByHashResponse{ @@ -209,7 +202,13 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { }, ffcapi.ErrorReason(""), nil).Once() mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003a.BlockHash - })).Return(&ffcapi.BlockInfoByHashResponse{ + })).Run(func(args mock.Arguments) { + // Notify of event after we've downloaded the 1002/1003a + bcm.Notify(&Notification{ + NotificationType: NewEventLog, + Event: eventToConfirm, + }) + }).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003a.BlockNumber)), BlockHash: block1003a.BlockHash, @@ -228,14 +227,6 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21", ParentHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", } - mca.On("NewBlockHashes").Run(func(args mock.Arguments) { - blockHashes <- &ffcapi.BlockHashEvent{ - BlockHashes: []string{ - block1003b.BlockHash, - block1004.BlockHash, - }, - } - }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003b.BlockHash })).Return(&ffcapi.BlockInfoByHashResponse{ @@ -255,9 +246,6 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - // Subsequent calls get nothing - mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) - mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 })).Return(&ffcapi.BlockInfoByNumberResponse{ @@ -268,31 +256,20 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil) mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + // Simulate 1003 disappearing from the chain return r.BlockNumber.Uint64() == 1003 - })).Return(&ffcapi.BlockInfoByNumberResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(int64(block1003b.BlockNumber)), - BlockHash: block1003b.BlockHash, - ParentHash: block1003b.ParentHash, - }, - }, ffcapi.ErrorReason(""), nil) - mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { - return r.BlockNumber.Uint64() == 1004 - })).Return(&ffcapi.BlockInfoByNumberResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)), - BlockHash: block1004.BlockHash, - ParentHash: block1004.ParentHash, - }, - }, ffcapi.ErrorReason(""), nil) + })).Run(func(args mock.Arguments) { + // Then notify about a new 1003 which matches the event, and a 1004 + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1003b.BlockHash, + block1004.BlockHash, + }, + } + }).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")) bcm.Start() - bcm.Notify(&Notification{ - NotificationType: NewEventLog, - Event: eventToConfirm, - }) - dispatched := <-confirmed assert.Equal(t, []BlockInfo{ *block1002, @@ -301,7 +278,6 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { }, dispatched) bcm.Stop() - <-bcm.done mca.AssertExpectations(t) @@ -345,8 +321,7 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { } // The next filter gives us 1002a, which will later be removed - blockHashes := make(chan *ffcapi.BlockHashEvent, 1) - mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) + blockHashes := bcm.NewBlockHashes() // First check while walking the chain does not yield a block mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { @@ -459,9 +434,6 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - // Subsequent calls get nothing - mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) - bcm.Start() receipt := <-receiptReceived @@ -475,7 +447,6 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { }, dispatched) bcm.Stop() - <-bcm.done mca.AssertExpectations(t) @@ -486,21 +457,18 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + EventID: ffcapi.EventID{ + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, Confirmed: func(confirmations []BlockInfo) { confirmed <- confirmations }, } - // We don't notify of any new blocks - blockHashes := make(chan *ffcapi.BlockHashEvent, 1) - mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) - // Then we should walk the chain by number to fill in 1002/1003, because our HWM is 1003 block1002 := &BlockInfo{ BlockNumber: 1002, @@ -560,7 +528,6 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { }, dispatched) bcm.Stop() - <-bcm.done mca.AssertExpectations(t) } @@ -588,15 +555,16 @@ func TestConfirmationsListenerFailWalkingChain(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) bcm.done = make(chan struct{}) - n := &Notification{ + bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: &EventInfo{ - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, + EventID: ffcapi.EventID{ + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + }, }, - } - bcm.addOrReplaceItem(n.eventPendingItem()) + }) mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 @@ -609,25 +577,6 @@ func TestConfirmationsListenerFailWalkingChain(t *testing.T) { mca.AssertExpectations(t) } -func TestConfirmationsListenerStalePollingBlocks(t *testing.T) { - - bcm, mca := newTestBlockConfirmationManager(t, false) - bcm.done = make(chan struct{}) - - blockHashes := make(chan *ffcapi.BlockHashEvent, 1) - blockHashes <- &ffcapi.BlockHashEvent{ - GapPotential: true, - } - mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() - mca.On("NewBlockHashes").Run(func(args mock.Arguments) { - bcm.cancelFunc() - }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) - - bcm.confirmationsListener() - - mca.AssertExpectations(t) -} - func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) @@ -635,12 +584,13 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + EventID: ffcapi.EventID{ + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, Confirmed: func(confirmations []BlockInfo) { confirmed <- confirmations }, @@ -650,8 +600,6 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { Event: eventToConfirm, }) - blockHashes := make(chan *ffcapi.BlockHashEvent, 1) - mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once().Run(func(args mock.Arguments) { @@ -663,14 +611,13 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { mca.AssertExpectations(t) } -func TestConfirmationsListenerStopStream(t *testing.T) { +func TestConfirmationsRemoveEvent(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) bcm.done = make(chan struct{}) - n := &Notification{ - Event: &EventInfo{ - StreamID: "stream1", + eventInfo := &EventInfo{ + EventID: ffcapi.EventID{ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, @@ -678,62 +625,61 @@ func TestConfirmationsListenerStopStream(t *testing.T) { LogIndex: 10, }, } - bcm.addOrReplaceItem(n.eventPendingItem()) - completed := make(chan struct{}) + bcm.addOrReplaceItem((&Notification{ + Event: eventInfo, + }).eventPendingItem()) bcm.Notify(&Notification{ - NotificationType: StopStream, - StoppedStream: &StoppedStreamInfo{ - StreamID: "stream1", - Completed: completed, - }, + NotificationType: RemovedEventLog, + Event: eventInfo, }) - blockHashes := make(chan *ffcapi.BlockHashEvent, 1) - mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) - mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Maybe() + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1002 + })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) { + bcm.cancelFunc() + }) - bcm.Start() + bcm.confirmationsListener() + <-bcm.done - <-completed assert.Empty(t, bcm.pending) - - bcm.Stop() mca.AssertExpectations(t) } -func TestConfirmationsRemoveEvent(t *testing.T) { +func TestConfirmationsFailWalkChainAfterBlockGap(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) bcm.done = make(chan struct{}) - eventInfo := &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + eventNotification := &Notification{ + NotificationType: NewEventLog, + Event: &EventInfo{ + EventID: ffcapi.EventID{ + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, + }, } - bcm.addOrReplaceItem((&Notification{ - Event: eventInfo, - }).eventPendingItem()) - bcm.Notify(&Notification{ - NotificationType: RemovedEventLog, - Event: eventInfo, - }) + bcm.Notify(eventNotification) - blockHashes := make(chan *ffcapi.BlockHashEvent, 1) - mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) - mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { - return r.BlockNumber.Uint64() == 1002 - })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) { + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) { + bcm.NewBlockHashes() <- &ffcapi.BlockHashEvent{ + GapPotential: true, + } + }).Once() + + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Run(func(args mock.Arguments) { bcm.cancelFunc() }) bcm.confirmationsListener() <-bcm.done - assert.Empty(t, bcm.pending) + assert.Len(t, bcm.pending, 1) + assert.NotNil(t, eventNotification.eventPendingItem().getKey()) // should be the event in there, the TX should be removed mca.AssertExpectations(t) } @@ -745,24 +691,40 @@ func TestConfirmationsRemoveTransaction(t *testing.T) { txInfo := &TransactionInfo{ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", } + eventNotification := &Notification{ + NotificationType: NewEventLog, + Event: &EventInfo{ + EventID: ffcapi.EventID{ + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, + }, + } bcm.addOrReplaceItem((&Notification{ Transaction: txInfo, }).transactionPendingItem()) - bcm.Notify(&Notification{ - NotificationType: RemovedTransaction, - Transaction: txInfo, - }) + go func() { + // The notification we want to test + bcm.Notify(&Notification{ + NotificationType: RemovedTransaction, + Transaction: txInfo, + }) + // Another notification that causes BlockInfoByNumber, so we can break the loop + bcm.Notify(eventNotification) + }() - blockHashes := make(chan *ffcapi.BlockHashEvent, 1) - mca.On("NewBlockHashes").Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)).Once() - mca.On("NewBlockHashes").Run(func(args mock.Arguments) { + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) { bcm.cancelFunc() - }).Return((<-chan *ffcapi.BlockHashEvent)(blockHashes)) + }) bcm.confirmationsListener() <-bcm.done - assert.Empty(t, bcm.pending) + assert.Len(t, bcm.pending, 1) + assert.NotNil(t, eventNotification.eventPendingItem().getKey()) // should be the event in there, the TX should be removed mca.AssertExpectations(t) } @@ -772,12 +734,13 @@ func TestWalkChainForEventBlockNotInConfirmationChain(t *testing.T) { pending := (&Notification{ Event: &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + EventID: ffcapi.EventID{ + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, }, }).eventPendingItem() @@ -803,12 +766,13 @@ func TestWalkChainForEventBlockLookupFail(t *testing.T) { pending := (&Notification{ Event: &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + EventID: ffcapi.EventID{ + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, }, }).eventPendingItem() @@ -886,11 +850,6 @@ func TestNotificationValidation(t *testing.T) { }) assert.Regexp(t, "FF21016", err) - err = bcm.Notify(&Notification{ - NotificationType: StopStream, - }) - assert.Regexp(t, "FF21016", err) - bcm.cancelFunc() err = bcm.Notify(&Notification{ NotificationType: NewTransaction, diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index ee9487f2..60c4c59c 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -90,6 +90,7 @@ type startedStreamState struct { startTime *fftypes.FFTime action eventStreamAction eventLoopDone chan struct{} + batchLoopDone chan struct{} updates chan *ffcapi.ListenerUpdate } @@ -105,6 +106,7 @@ type eventStream struct { wsChannels ws.WebSocketChannels retry *retry.Retry currentState *startedStreamState + batchChannel chan *ffcapi.ListenerUpdate } func NewEventStream( @@ -112,28 +114,31 @@ func NewEventStream( persistedSpec *apitypes.EventStream, connector ffcapi.API, persistence persistence.Persistence, - confirmations confirmations.Manager, wsChannels ws.WebSocketChannels, initialListeners []*apitypes.Listener, ) (ees Stream, err error) { + esCtx := log.WithLogField(bgCtx, "eventstream", persistedSpec.ID.String()) es := &eventStream{ - bgCtx: log.WithLogField(bgCtx, "eventstream", persistedSpec.ID.String()), - status: apitypes.EventStreamStatusStopped, - spec: persistedSpec, - connector: connector, - persistence: persistence, - confirmations: confirmations, - listeners: make(map[fftypes.UUID]*listener), - wsChannels: wsChannels, - retry: esDefaults.retry, + bgCtx: esCtx, + status: apitypes.EventStreamStatusStopped, + spec: persistedSpec, + connector: connector, + persistence: persistence, + listeners: make(map[fftypes.UUID]*listener), + wsChannels: wsChannels, + retry: esDefaults.retry, + } + if config.GetInt(tmconfig.ConfirmationsRequired) > 0 { + es.confirmations = confirmations.NewBlockConfirmationManager(esCtx, connector) } // The configuration we have in memory, applies all the defaults to what is passed in // to ensure there are no nil fields on the configuration object. - if es.spec, _, err = mergeValidateEsConfig(es.bgCtx, nil, persistedSpec); err != nil { + if es.spec, _, err = mergeValidateEsConfig(esCtx, nil, persistedSpec); err != nil { return nil, err } + es.batchChannel = make(chan *ffcapi.ListenerUpdate, *es.spec.BatchSize) for _, existing := range initialListeners { - spec, err := es.verifyListenerOptions(es.bgCtx, existing.ID, existing) + spec, err := es.verifyListenerOptions(esCtx, existing.ID, existing) if err != nil { return nil, err } @@ -142,7 +147,7 @@ func NewEventStream( spec: spec, } } - log.L(es.bgCtx).Infof("Initialized Event Stream") + log.L(esCtx).Infof("Initialized Event Stream") return es, nil } @@ -465,12 +470,12 @@ func (es *eventStream) Start(ctx context.Context) error { startedState := &startedStreamState{ startTime: fftypes.Now(), eventLoopDone: make(chan struct{}), + batchLoopDone: make(chan struct{}), updates: make(chan *ffcapi.ListenerUpdate, int(*es.spec.BatchSize)), } startedState.ctx, startedState.cancelCtx = context.WithCancel(es.bgCtx) es.currentState = startedState es.initAction(startedState) - go es.eventLoop(startedState) initialListeners := make([]*ffcapi.EventListenerAddRequest, 0) for _, l := range es.listeners { @@ -482,6 +487,18 @@ func (es *eventStream) Start(ctx context.Context) error { StreamContext: startedState.ctx, InitialListeners: initialListeners, }) + if err != nil { + _ = es.checkSetStatus(ctx, apitypes.EventStreamStatusStarted, apitypes.EventStreamStatusStopped) + return err + } + + // Kick off the loops + go es.eventLoop(startedState) + go es.batchLoop(startedState) + + // Start the confirmations manager + es.confirmations.Start() + return err } @@ -522,9 +539,15 @@ func (es *eventStream) Stop(ctx context.Context) error { return err } + // Stop the confirmations manager + es.confirmations.Stop() + // Wait for our event loop to stop <-startedState.eventLoopDone + // Wait for our batch loop to stop + <-startedState.batchLoopDone + // Transition to stopped (takes the lock again) es.mux.Lock() es.currentState = nil @@ -554,6 +577,52 @@ func (es *eventStream) Delete(ctx context.Context) error { func (es *eventStream) eventLoop(startedState *startedStreamState) { defer close(startedState.eventLoopDone) ctx := startedState.ctx + + for { + select { + case update := <-startedState.updates: + event := update.Event + es.mux.Lock() + l := es.listeners[*update.ListenerID] + es.mux.Unlock() + if l != nil { + log.L(es.bgCtx).Debugf("%s event detected: %s", update.ListenerID, update.Event) + if event == nil || es.confirmations == nil { + // Updates that are just a checkpoint update, go straight to the batch loop. + // Or if the confirmation manager is disabled. + // - Note this will block the eventLoop when the event stream is blocked + es.batchChannel <- update + } else { + // Notify will block, when the confirmation manager is blocked, which per below + // will flow back from when the event stream is blocked + err := es.confirmations.Notify(&confirmations.Notification{ + NotificationType: confirmations.NewEventLog, + Event: &confirmations.EventInfo{ + EventID: event.EventID, + Confirmed: func(confirmations []confirmations.BlockInfo) { + // Push it to the batch when confirmed + // - Note this will block the confirmation manager when the event stream is blocked + es.batchChannel <- update + }, + }, + }) + if err != nil { + log.L(es.bgCtx).Warnf("Failed to notify confirmation manager for event '%s': %s", update.Event, err) + } + } + } + case <-ctx.Done(): + log.L(ctx).Debugf("Event loop exiting") + return + } + } +} + +// batchLoop receives confirmed events from the confirmation manager, +// batches them together, and drives the actions. +func (es *eventStream) batchLoop(startedState *startedStreamState) { + defer close(startedState.batchLoopDone) + ctx := startedState.ctx batchTimeout := time.Duration(*es.spec.BatchTimeout) maxSize := int(*es.spec.BatchSize) batchNumber := 0 @@ -568,7 +637,7 @@ func (es *eventStream) eventLoop(startedState *startedStreamState) { timeoutContext = ctx } select { - case update := <-startedState.updates: + case update := <-es.batchChannel: if batch == nil { batchNumber++ batch = &eventStreamBatch{ @@ -580,24 +649,23 @@ func (es *eventStream) eventLoop(startedState *startedStreamState) { if update.Checkpoint != nil { batch.checkpoints[*update.ListenerID] = update.Checkpoint } - for _, event := range update.Events { - var l *listener - if update.ListenerID != nil { - l = es.listeners[*update.ListenerID] - if l != nil { - log.L(es.bgCtx).Debugf("%s (%s) event: %s", l.spec.Signature, l.spec.ID, event.ProtocolID) - batch.events = append(batch.events, &ffcapi.EventWithContext{ - StreamID: es.spec.ID, - ListenerID: update.ListenerID, - Event: *event, - }) - } + if update.Event != nil { + es.mux.Lock() + l := es.listeners[*update.ListenerID] + es.mux.Unlock() + if l != nil { + log.L(es.bgCtx).Debugf("%s '%s' event confirmed: %s", l.spec.ID, l.spec.Signature, update.Event) + batch.events = append(batch.events, &ffcapi.EventWithContext{ + StreamID: es.spec.ID, + ListenerID: update.ListenerID, + Event: *update.Event, + }) } } case <-timeoutContext.Done(): if batch == nil { // The started context exited, we are stopping - log.L(ctx).Debugf("Event poller exiting") + log.L(ctx).Debugf("Batch loop exiting") return } // Otherwise we timed out @@ -611,7 +679,7 @@ func (es *eventStream) eventLoop(startedState *startedStreamState) { err = es.writeCheckpoint(startedState, batch) } if err != nil { - log.L(ctx).Debugf("Event poller exiting: %s", err) + log.L(ctx).Debugf("Batch loop exiting: %s", err) return } batch = nil diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 26210626..eb10614a 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -27,6 +27,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" @@ -60,14 +61,24 @@ func newTestEventStreamWithListener(t *testing.T, mfc *ffcapimocks.API, conf str ees, err := NewEventStream(context.Background(), testESConf(t, conf), mfc, &persistencemocks.Persistence{}, - &confirmationsmocks.Manager{}, &wsmocks.WebSocketChannels{}, listeners, ) if err != nil { return nil, err } - return ees.(*eventStream), err + es = ees.(*eventStream) + mcm := &confirmationsmocks.Manager{} + es.confirmations = mcm + mcm.On("Start").Return(nil).Maybe() + mcm.On("Stop").Return(nil).Maybe() + mcm.On("Notify", mock.Anything).Run(func(args mock.Arguments) { + n := args[0].(*confirmations.Notification) + if n.Event != nil { + go n.Event.Confirmed([]confirmations.BlockInfo{}) + } + }).Return(nil).Maybe() + return es, err } func mockWSChannels(wsc *wsmocks.WebSocketChannels) (chan interface{}, chan interface{}, chan error) { @@ -84,7 +95,6 @@ func TestNewTestEventStreamMissingID(t *testing.T) { _, err := NewEventStream(context.Background(), &apitypes.EventStream{}, &ffcapimocks.API{}, &persistencemocks.Persistence{}, - &confirmationsmocks.Manager{}, &wsmocks.WebSocketChannels{}, []*apitypes.Listener{}, ) @@ -97,7 +107,6 @@ func TestNewTestEventStreamBadConfig(t *testing.T) { _, err := NewEventStream(context.Background(), testESConf(t, `{}`), &ffcapimocks.API{}, &persistencemocks.Persistence{}, - &confirmationsmocks.Manager{}, &wsmocks.WebSocketChannels{}, []*apitypes.Listener{}, ) @@ -336,12 +345,14 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { r.EventStream <- &ffcapi.ListenerUpdate{ ListenerID: l.ID, Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), - Events: []*ffcapi.Event{ - { - Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), - ProtocolID: "000000000042/000013/000001", - Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), + Event: &ffcapi.Event{ + EventID: ffcapi.EventID{ + BlockNumber: 42, + TransactionIndex: 13, + LogIndex: 1, }, + Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), + Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), }, } @@ -441,12 +452,14 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { r.EventStream <- &ffcapi.ListenerUpdate{ ListenerID: l.ID, Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), - Events: []*ffcapi.Event{ - { - Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), - ProtocolID: "000000000042/000013/000001", - Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), + Event: &ffcapi.Event{ + EventID: ffcapi.EventID{ + BlockNumber: 42, + TransactionIndex: 13, + LogIndex: 1, }, + Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), + Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), }, } @@ -814,7 +827,7 @@ func TestUpdateStreamRestartFail(t *testing.T) { mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) err := es.Start(es.bgCtx) assert.NoError(t, err) @@ -831,12 +844,8 @@ func TestUpdateStreamRestartFail(t *testing.T) { assert.Regexp(t, "FF21032.*pop", err) <-r.StreamContext.Done() - r = <-started - err = es.Stop(es.bgCtx) - assert.NoError(t, err) - - <-r.StreamContext.Done() + assert.Equal(t, apitypes.EventStreamStatusStopped, es.status) mfc.AssertExpectations(t) } @@ -999,7 +1008,7 @@ func TestResetListenerRestartFail(t *testing.T) { mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { return r.ID.Equals(l.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ @@ -1123,12 +1132,14 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { r.EventStream <- &ffcapi.ListenerUpdate{ ListenerID: l.ID, Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), - Events: []*ffcapi.Event{ - { - Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), - ProtocolID: "000000000042/000013/000001", - Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), + Event: &ffcapi.Event{ + EventID: ffcapi.EventID{ + BlockNumber: 42, + TransactionIndex: 13, + LogIndex: 1, }, + Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), + Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), }, } batch1 := (<-broadcastChannel).([]*ffcapi.EventWithContext) @@ -1265,3 +1276,61 @@ func TestActionRetryBlock(t *testing.T) { <-done assert.Greater(t, callCount, 0) } + +func TestEventLoopConfirmationsManagerBypass(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerUpdate, 1), + eventLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + u1 := &ffcapi.ListenerUpdate{ + ListenerID: fftypes.NewUUID(), + } + es.confirmations = nil + es.listeners[*u1.ListenerID] = &listener{} + + go func() { + ss.updates <- u1 + u2 := <-es.batchChannel + assert.Equal(t, u1, u2) + ss.cancelCtx() + }() + + es.eventLoop(ss) +} + +func TestEventLoopConfirmationsManagerFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerUpdate, 1), + eventLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + u1 := &ffcapi.ListenerUpdate{ + ListenerID: fftypes.NewUUID(), + Event: &ffcapi.Event{}, + } + mcm := &confirmationsmocks.Manager{} + mcm.On("Notify", mock.Anything).Return(fmt.Errorf("pop")).Run(func(args mock.Arguments) { + ss.cancelCtx() + }) + es.confirmations = mcm + es.listeners[*u1.ListenerID] = &listener{} + + go func() { + ss.updates <- u1 + }() + + es.eventLoop(ss) +} diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index a6bcb70b..aa4f7942 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -30,7 +30,7 @@ var ffc = config.AddRootKey var ( ManagerName = ffc("manager.name") ConfirmationsRequired = ffc("confirmations.required") - ConfirmationsBlockPollingInterval = ffc("confirmations.blockPollingInterval") + ConfirmationsBlockQueueLength = ffc("confirmations.blockQueueLength") ConfirmationsStaleReceiptTimeout = ffc("confirmations.staleReceiptTimeout") ConfirmationsNotificationQueueLength = ffc("confirmations.notificationQueueLength") OperationsTypes = ffc("operations.types") @@ -80,7 +80,7 @@ func setDefaults() { }) viper.SetDefault(string(OperationsFullScanStartupMaxRetries), 10) viper.SetDefault(string(ConfirmationsRequired), 20) - viper.SetDefault(string(ConfirmationsBlockPollingInterval), "3s") + viper.SetDefault(string(ConfirmationsBlockQueueLength), 50) viper.SetDefault(string(ConfirmationsNotificationQueueLength), 50) viper.SetDefault(string(ConfirmationsStaleReceiptTimeout), "1m") viper.SetDefault(string(OperationsErrorHistoryCount), 25) diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go index 3305e92e..c4a92c4f 100644 --- a/internal/tmmsgs/en_config_descriptions.go +++ b/internal/tmmsgs/en_config_descriptions.go @@ -37,7 +37,7 @@ var ( ConfigAPIShutdownTimeout = ffc("config.api.shutdownTimeout", "The maximum amount of time to wait for any open HTTP requests to finish before shutting down the HTTP server", i18n.TimeDurationType) ConfigConfirmationsBlockCacheSize = ffc("config.confirmations.blockCacheSize", "The maximum number of block headers to keep in the cache", i18n.IntType) - ConfigConfirmationsBlockPollingInterval = ffc("config.confirmations.blockPollingInterval", "How often to poll for new block headers", i18n.TimeDurationType) + ConfigConfirmationsBlockQueueLength = ffc("config.confirmations.blockQueueLength", "Internal queue length for notifying the confirmations manager of new blocks", i18n.IntType) ConfigConfirmationsNotificationsQueueLength = ffc("config.confirmations.notificationQueueLength", "Internal queue length for notifying the confirmations manager of new transactions/events", i18n.IntType) ConfigConfirmationsRequired = ffc("config.confirmations.required", "Number of confirmations required to consider a transaction/event final", i18n.IntType) ConfigConfirmationsStaleReceiptTimeout = ffc("config.confirmations.staleReceiptTimeout", "Duration after which to force a receipt check for a pending transaction", i18n.TimeDurationType) diff --git a/mocks/confirmationsmocks/manager.go b/mocks/confirmationsmocks/manager.go index 68cf5f7d..0c274d68 100644 --- a/mocks/confirmationsmocks/manager.go +++ b/mocks/confirmationsmocks/manager.go @@ -4,6 +4,8 @@ package confirmationsmocks import ( confirmations "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + mock "github.com/stretchr/testify/mock" ) @@ -12,6 +14,22 @@ type Manager struct { mock.Mock } +// NewBlockHashes provides a mock function with given fields: +func (_m *Manager) NewBlockHashes() chan<- *ffcapi.BlockHashEvent { + ret := _m.Called() + + var r0 chan<- *ffcapi.BlockHashEvent + if rf, ok := ret.Get(0).(func() chan<- *ffcapi.BlockHashEvent); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(chan<- *ffcapi.BlockHashEvent) + } + } + + return r0 +} + // Notify provides a mock function with given fields: n func (_m *Manager) Notify(n *confirmations.Notification) error { ret := _m.Called(n) diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index ad0cda74..d460b285 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -224,22 +224,6 @@ func (_m *API) GasPriceEstimate(ctx context.Context, req *ffcapi.GasPriceEstimat return r0, r1, r2 } -// NewBlockHashes provides a mock function with given fields: -func (_m *API) NewBlockHashes() <-chan *ffcapi.BlockHashEvent { - ret := _m.Called() - - var r0 <-chan *ffcapi.BlockHashEvent - if rf, ok := ret.Get(0).(func() <-chan *ffcapi.BlockHashEvent); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(<-chan *ffcapi.BlockHashEvent) - } - } - - return r0 -} - // NextNonceForSigner provides a mock function with given fields: ctx, req func (_m *API) NextNonceForSigner(ctx context.Context, req *ffcapi.NextNonceForSignerRequest) (*ffcapi.NextNonceForSignerResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 7853b104..766d94a2 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -18,7 +18,7 @@ package ffcapi import ( "context" - "strings" + "fmt" "github.com/hyperledger/firefly-common/pkg/fftypes" ) @@ -64,9 +64,6 @@ type API interface { // EventListenerRemove ends listening on a set of events previous started EventListenerRemove(ctx context.Context, req *EventListenerRemoveRequest) (*EventListenerRemoveResponse, ErrorReason, error) - - // NewBlockHashes should dynamically push the hashes of all new blocks detected from the blockchain, if confirmations are supported - NewBlockHashes() <-chan *BlockHashEvent } type BlockHashEvent struct { @@ -74,21 +71,45 @@ type BlockHashEvent struct { GapPotential bool `json:"gapPotential,omitempty"` // when true, the caller cannot be sure if blocks have been missed (use on reconnect of a websocket for example) } +// EventID are the set of required fields an FFCAPI compatible connector needs to map to the underlying blockchain constructs, to uniquely identify an event +type EventID struct { + BlockHash string // String representation of the block, which will change if any transaction info in the block changes + BlockNumber uint64 // A numeric identifier for the block + TransactionHash string // The transaction + TransactionIndex uint64 // Index within the block of the transaction that emitted the event + LogIndex uint64 // Index within the transaction of this emitted event log +} + // Event is a blockchain event that matches one of the started listeners. // The implementation is responsible for ensuring all events on a listener are // ordered on to this channel in the exact sequence from the blockchain. type Event struct { - Data *fftypes.JSONAny `json:"data"` // the JSON data to deliver for this event (can be array or object structure) - ProtocolID string `json:"protocolId"` // a protocol identifier for the event, that is string sortable per https://hyperledger.github.io/firefly/reference/types/blockchainevent.html#protocol-id - Info *fftypes.JSONAny `json:"info"` // additional blockchain specific information + EventID + Data *fftypes.JSONAny `json:"data"` // the JSON data to deliver for this event (can be array or object structure) + Info *fftypes.JSONAny `json:"info"` // additional blockchain specific information +} + +// String is unique in all cases for an event, by combining the protocol ID with the block hash +func (eid *EventID) String() string { + return fmt.Sprintf("%s/B=%s", eid.ProtocolID(), eid.BlockHash) +} + +// ProtocolID represents the unique (once finality is reached) sortable position within the blockchain +func (eid *EventID) ProtocolID() string { + return fmt.Sprintf("%.12d/%.6d/%.6d", eid.BlockNumber, eid.TransactionIndex, eid.LogIndex) } // Events array has a natural sort order of the protocol ID type Events []*Event -func (es Events) Len() int { return len(es) } -func (es Events) Swap(i, j int) { es[i], es[j] = es[j], es[i] } -func (es Events) Less(i, j int) bool { return strings.Compare(es[i].ProtocolID, es[j].ProtocolID) < 0 } +func (es Events) Len() int { return len(es) } +func (es Events) Swap(i, j int) { es[i], es[j] = es[j], es[i] } +func (es Events) Less(i, j int) bool { + return es[i].BlockNumber < es[j].BlockNumber || + ((es[i].BlockNumber == es[j].BlockNumber) && + ((es[i].TransactionIndex < es[j].TransactionIndex) || + ((es[i].TransactionIndex == es[j].TransactionIndex) && (es[i].LogIndex < es[j].LogIndex)))) +} type EventWithContext struct { StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event @@ -96,10 +117,16 @@ type EventWithContext struct { Event } +// A listener update contains a checkpoint, plus zero or one events. +// - If only a checkpoint, then that will be stored immediately. Use this to notify when there has been a period of inactivity +// where no events have arrived - to minimize reprocessing of the chain after a restart. +// If there are any events in-flight for this listener in the confirmation manager, then the checkpoint will be ignored. +// - If an event is included, then this will be passed to the confirmation manager. The checkpoint will only be stored after +// the event is confirmed and successfully processed by the listener. type ListenerUpdate struct { - ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this update - expected to be the same for all events in the events array - Checkpoint *fftypes.JSONAny `json:"checkpoint"` // checkpoint information for the listener. This should be supplied regularly even if there are no events, to minimize recovery time after restart - Events Events `json:"events,omitempty"` // zero or more events. Can be nil for checkpoint-only updates + ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this update - expected to be the same for all events in the events array + Checkpoint *fftypes.JSONAny `json:"checkpoint"` // checkpoint information for the listener. This should be supplied regularly even if there are no events, to minimize recovery time after restart + Event *Event `json:"event,omitempty"` // An event an be nil for checkpoint-only updates } // ErrorReason are a set of standard error conditions that a blockchain connector can return diff --git a/pkg/ffcapi/api_test.go b/pkg/ffcapi/api_test.go index 48e39870..58a8ed62 100644 --- a/pkg/ffcapi/api_test.go +++ b/pkg/ffcapi/api_test.go @@ -18,7 +18,6 @@ package ffcapi import ( "crypto/rand" - "fmt" "math/big" "sort" "strings" @@ -29,16 +28,23 @@ import ( func TestSortEvents(t *testing.T) { - events := make(Events, 1000) - for i := 0; i < 1000; i++ { - v, _ := rand.Int(rand.Reader, big.NewInt(100000000)) + events := make(Events, 10000) + for i := 0; i < 10000; i++ { + b, _ := rand.Int(rand.Reader, big.NewInt(1000)) + t, _ := rand.Int(rand.Reader, big.NewInt(10)) + l, _ := rand.Int(rand.Reader, big.NewInt(10)) events[i] = &Event{ - ProtocolID: fmt.Sprintf("%.9d", v.Int64()), + EventID: EventID{ + BlockNumber: b.Uint64(), + TransactionIndex: t.Uint64(), + LogIndex: l.Uint64(), + }, } } sort.Sort(events) - for i := 1; i < 1000; i++ { - assert.Negative(t, strings.Compare(events[i-1].ProtocolID, events[i].ProtocolID)) + for i := 1; i < len(events); i++ { + assert.LessOrEqual(t, strings.Compare(events[i-1].ProtocolID(), events[i].ProtocolID()), 0) + assert.LessOrEqual(t, strings.Compare(events[i-1].String(), events[i].String()), 0) } } diff --git a/pkg/ffcapi/event_stream_start.go b/pkg/ffcapi/event_stream_start.go index 38b137a9..0475da5e 100644 --- a/pkg/ffcapi/event_stream_start.go +++ b/pkg/ffcapi/event_stream_start.go @@ -26,6 +26,7 @@ type EventStreamStartRequest struct { ID *fftypes.UUID // UUID of the stream, which we be referenced in any future add/remove listener requests StreamContext context.Context // Context that will be cancelled when the event stream needs to stop - no further events will be consumed after this, so all pushes to the stream should select on the done channel too EventStream chan<- *ListenerUpdate // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events + BlockListener chan<- *BlockHashEvent // The connector should push new blocks to every stream, marking if it's possible blocks were missed (due to reconnect). The stream guarantees to always consume from this channel, until the stream context closes. InitialListeners []*EventListenerAddRequest // Initial list of event listeners to start with the stream - allows these to be started concurrently } diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index bc79323e..3bbaf9d8 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -81,8 +81,8 @@ func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...strin assert.NoError(t, err) m := mm.(*manager) mcm := &confirmationsmocks.Manager{} - m.confirmations = mcm mcm.On("Start").Return().Maybe() + m.confirmations = mcm return fmt.Sprintf("http://127.0.0.1:%s", managerPort), m, diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index b1c22334..7d0a5cb7 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -86,7 +86,7 @@ func (m *manager) deleteAllStreamListeners(ctx context.Context, streamID *fftype } func (m *manager) addRuntimeStream(def *apitypes.EventStream, listeners []*apitypes.Listener) (events.Stream, error) { - s, err := events.NewEventStream(m.ctx, def, m.connector, m.persistence, m.confirmations, m.wsChannels, listeners) + s, err := events.NewEventStream(m.ctx, def, m.connector, m.persistence, m.wsChannels, listeners) if err != nil { return nil, err } From 30974cf190b1f6273ab8098550927973b23876a9 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 1 Jul 2022 22:25:00 -0400 Subject: [PATCH 36/95] Create block update listener on ES that never blocks Signed-off-by: Peter Broadhurst --- internal/events/blocklistener.go | 74 +++++++++++++++++++++++++++ internal/events/blocklistener_test.go | 68 ++++++++++++++++++++++++ internal/events/eventstream.go | 48 ++++++++++------- 3 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 internal/events/blocklistener.go create mode 100644 internal/events/blocklistener_test.go diff --git a/internal/events/blocklistener.go b/internal/events/blocklistener.go new file mode 100644 index 00000000..d33b93cb --- /dev/null +++ b/internal/events/blocklistener.go @@ -0,0 +1,74 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 events + +import ( + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +// blockListener ensures it always pulls blocks from the channel passed to the connector +// for new block events, regardless of whether the downstream confirmations update queue +// is full blocked (likely because the event stream is blocked). +// This is critical to avoid the situation where one blocked stream, stops another stream +// from receiving block events. +// We use the same "GapPotential" flag that the connector can mark on a reconnect, to mark +// when we've had to discard events for a blocked event listener (event listeners could stay +// blocked indefinitely, so we can't leak memory by storing up an indefinite number of new +// block events). +func (es *eventStream) blockListener(startedState *startedStreamState) { + defer close(startedState.blockListenerDone) + var blockedUpdate *ffcapi.BlockHashEvent + for { + if blockedUpdate != nil { + select { + case blockUpdate := <-startedState.blocks: + // Have to discard this + blockedUpdate.GapPotential = true // there is a gap for sure at this point + log.L(startedState.ctx).Debugf("Blocked event stream missed new block event: %v", blockUpdate.BlockHashes) + case es.confirmations.NewBlockHashes() <- blockedUpdate: + // We're not blocked any more + log.L(startedState.ctx).Infof("Event stream block-listener unblocked") + blockedUpdate = nil + case <-startedState.ctx.Done(): + log.L(startedState.ctx).Debugf("Block listener exiting (previously blocked)") + return + } + } else { + select { + case blockUpdate := <-startedState.blocks: + log.L(startedState.ctx).Debugf("Received block event: %v", blockUpdate.BlockHashes) + // Nothing to do unless we have confirmations turned on + if es.confirmations != nil { + select { + case es.confirmations.NewBlockHashes() <- blockUpdate: + // all good, we passed it on + default: + // we can't deliver it immediately, we switch to blocked mode + log.L(startedState.ctx).Infof("Event stream block-listener became blocked") + // Take a copy of the block update, so we can modify (to mark a gap) without affecting other streams + var bu = *blockUpdate + blockedUpdate = &bu + } + } + case <-startedState.ctx.Done(): + log.L(startedState.ctx).Debugf("Block listener exiting") + return + } + } + } +} diff --git a/internal/events/blocklistener_test.go b/internal/events/blocklistener_test.go new file mode 100644 index 00000000..06765712 --- /dev/null +++ b/internal/events/blocklistener_test.go @@ -0,0 +1,68 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 events + +import ( + "context" + "sync" + "testing" + + "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" +) + +func TestBlockListenerDoesNotBlock(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + blocks: make(chan *ffcapi.BlockHashEvent, 1), + blockListenerDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + blockIt := make(chan *ffcapi.BlockHashEvent) + mcm := &confirmationsmocks.Manager{} + mcm.On("NewBlockHashes").Return((chan<- *ffcapi.BlockHashEvent)(blockIt)) + es.confirmations = mcm + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + es.blockListener(ss) + wg.Done() + }() + + for i := 0; i < 100; i++ { + ss.blocks <- &ffcapi.BlockHashEvent{} + } + + // Now when re unblock, we're told about the gap + bhe := <-blockIt + assert.True(t, bhe.GapPotential) + + // Block it again + ss.blocks <- &ffcapi.BlockHashEvent{} + + // And check we can exit + ss.cancelCtx() + wg.Wait() + +} diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 60c4c59c..05c14022 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -36,14 +36,15 @@ import ( ) type Stream interface { - AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, updates *apitypes.Listener, reset bool) (*apitypes.Listener, error) // Add or update a listener - RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener - UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error // Apply definition updates (if there are changes) - Spec() *apitypes.EventStream // Retrieve the merged definition to persist - Status() apitypes.EventStreamStatus // Get the current status - Start(ctx context.Context) error // Start delivery - Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) - Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint + AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, + updates *apitypes.Listener, reset bool) (*apitypes.Listener, error) // Add or update a listener + RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener + UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error // Apply definition updates (if there are changes) + Spec() *apitypes.EventStream // Retrieve the merged definition to persist + Status() apitypes.EventStreamStatus // Get the current status + Start(ctx context.Context) error // Start delivery + Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) + Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint } // esDefaults are the defaults for new event streams, read from the config once in InitDefaults() @@ -85,13 +86,15 @@ type eventStreamBatch struct { } type startedStreamState struct { - ctx context.Context - cancelCtx func() - startTime *fftypes.FFTime - action eventStreamAction - eventLoopDone chan struct{} - batchLoopDone chan struct{} - updates chan *ffcapi.ListenerUpdate + ctx context.Context + cancelCtx func() + startTime *fftypes.FFTime + action eventStreamAction + eventLoopDone chan struct{} + batchLoopDone chan struct{} + blockListenerDone chan struct{} + updates chan *ffcapi.ListenerUpdate + blocks chan *ffcapi.BlockHashEvent } type eventStream struct { @@ -468,10 +471,12 @@ func (es *eventStream) Start(ctx context.Context) error { log.L(ctx).Infof("Starting event stream %s", es) startedState := &startedStreamState{ - startTime: fftypes.Now(), - eventLoopDone: make(chan struct{}), - batchLoopDone: make(chan struct{}), - updates: make(chan *ffcapi.ListenerUpdate, int(*es.spec.BatchSize)), + startTime: fftypes.Now(), + eventLoopDone: make(chan struct{}), + batchLoopDone: make(chan struct{}), + blockListenerDone: make(chan struct{}), + updates: make(chan *ffcapi.ListenerUpdate, int(*es.spec.BatchSize)), + blocks: make(chan *ffcapi.BlockHashEvent), // we promise to consume immediately } startedState.ctx, startedState.cancelCtx = context.WithCancel(es.bgCtx) es.currentState = startedState @@ -485,6 +490,7 @@ func (es *eventStream) Start(ctx context.Context) error { ID: es.spec.ID, EventStream: startedState.updates, StreamContext: startedState.ctx, + BlockListener: startedState.blocks, InitialListeners: initialListeners, }) if err != nil { @@ -495,6 +501,7 @@ func (es *eventStream) Start(ctx context.Context) error { // Kick off the loops go es.eventLoop(startedState) go es.batchLoop(startedState) + go es.blockListener(startedState) // Start the confirmations manager es.confirmations.Start() @@ -548,6 +555,9 @@ func (es *eventStream) Stop(ctx context.Context) error { // Wait for our batch loop to stop <-startedState.batchLoopDone + // Wait for our block listener to stop + <-startedState.blockListenerDone + // Transition to stopped (takes the lock again) es.mux.Lock() es.currentState = nil From af4f2611761702562039e9c3929377a5d148a89e Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 1 Jul 2022 22:42:11 -0400 Subject: [PATCH 37/95] Enhance test Signed-off-by: Peter Broadhurst --- internal/events/blocklistener_test.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/events/blocklistener_test.go b/internal/events/blocklistener_test.go index 06765712..8f69ce32 100644 --- a/internal/events/blocklistener_test.go +++ b/internal/events/blocklistener_test.go @@ -38,7 +38,7 @@ func TestBlockListenerDoesNotBlock(t *testing.T) { } ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) - blockIt := make(chan *ffcapi.BlockHashEvent) + blockIt := make(chan *ffcapi.BlockHashEvent, 1) mcm := &confirmationsmocks.Manager{} mcm.On("NewBlockHashes").Return((chan<- *ffcapi.BlockHashEvent)(blockIt)) es.confirmations = mcm @@ -54,12 +54,17 @@ func TestBlockListenerDoesNotBlock(t *testing.T) { ss.blocks <- &ffcapi.BlockHashEvent{} } - // Now when re unblock, we're told about the gap + // Get the one that was stuck in the pipe bhe := <-blockIt + assert.False(t, bhe.GapPotential) + + // We should get the unblocking one too, with GapPotential set + bhe = <-blockIt assert.True(t, bhe.GapPotential) // Block it again ss.blocks <- &ffcapi.BlockHashEvent{} + ss.blocks <- &ffcapi.BlockHashEvent{} // And check we can exit ss.cancelCtx() From 362802556cc745fb578b8ac6dba8b2736b2d9b40 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 5 Jul 2022 09:30:00 -0400 Subject: [PATCH 38/95] Extend sortable collection to listener updates Signed-off-by: Peter Broadhurst --- pkg/ffcapi/api.go | 25 +++++++++++++++++-------- pkg/ffcapi/api_test.go | 6 ++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 766d94a2..90e470df 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -99,16 +99,25 @@ func (eid *EventID) ProtocolID() string { return fmt.Sprintf("%.12d/%.6d/%.6d", eid.BlockNumber, eid.TransactionIndex, eid.LogIndex) } -// Events array has a natural sort order of the protocol ID +// Events array has a natural sort order of the block/txIndex/logIndex type Events []*Event -func (es Events) Len() int { return len(es) } -func (es Events) Swap(i, j int) { es[i], es[j] = es[j], es[i] } -func (es Events) Less(i, j int) bool { - return es[i].BlockNumber < es[j].BlockNumber || - ((es[i].BlockNumber == es[j].BlockNumber) && - ((es[i].TransactionIndex < es[j].TransactionIndex) || - ((es[i].TransactionIndex == es[j].TransactionIndex) && (es[i].LogIndex < es[j].LogIndex)))) +func (es Events) Len() int { return len(es) } +func (es Events) Swap(i, j int) { es[i], es[j] = es[j], es[i] } +func (es Events) Less(i, j int) bool { return evLess(es[i], es[j]) } + +// ListenerUpdates array has a natural sort order of the event +type ListenerUpdates []*ListenerUpdate + +func (lu ListenerUpdates) Len() int { return len(lu) } +func (lu ListenerUpdates) Swap(i, j int) { lu[i], lu[j] = lu[j], lu[i] } +func (lu ListenerUpdates) Less(i, j int) bool { return evLess(lu[i].Event, lu[j].Event) } + +func evLess(eI *Event, eJ *Event) bool { + return eI.BlockNumber < eJ.BlockNumber || + ((eI.BlockNumber == eJ.BlockNumber) && + ((eI.TransactionIndex < eJ.TransactionIndex) || + ((eI.TransactionIndex == eJ.TransactionIndex) && (eI.LogIndex < eJ.LogIndex)))) } type EventWithContext struct { diff --git a/pkg/ffcapi/api_test.go b/pkg/ffcapi/api_test.go index 58a8ed62..868be567 100644 --- a/pkg/ffcapi/api_test.go +++ b/pkg/ffcapi/api_test.go @@ -29,6 +29,7 @@ import ( func TestSortEvents(t *testing.T) { events := make(Events, 10000) + listenerUpdates := make(ListenerUpdates, len(events)) for i := 0; i < 10000; i++ { b, _ := rand.Int(rand.Reader, big.NewInt(1000)) t, _ := rand.Int(rand.Reader, big.NewInt(10)) @@ -40,11 +41,16 @@ func TestSortEvents(t *testing.T) { LogIndex: l.Uint64(), }, } + listenerUpdates[i] = &ListenerUpdate{ + Event: events[i], + } } sort.Sort(events) + sort.Sort(listenerUpdates) for i := 1; i < len(events); i++ { assert.LessOrEqual(t, strings.Compare(events[i-1].ProtocolID(), events[i].ProtocolID()), 0) assert.LessOrEqual(t, strings.Compare(events[i-1].String(), events[i].String()), 0) + assert.LessOrEqual(t, strings.Compare(listenerUpdates[i-1].Event.ProtocolID(), listenerUpdates[i].Event.ProtocolID()), 0) } } From 36f9f52953e10ee9ebc05f8064a1cdd89ac9d1e0 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 5 Jul 2022 14:56:02 -0400 Subject: [PATCH 39/95] Support remvoed events, and tracking/removing in-flight events by listener id Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 1 + internal/confirmations/confirmations.go | 45 +++++-- internal/confirmations/confirmations_test.go | 86 ++++++++++-- internal/events/eventstream.go | 111 ++++++++++------ internal/events/eventstream_test.go | 130 ++++++++++++++++--- mocks/ffcapimocks/api.go | 30 +++++ pkg/ffcapi/api.go | 39 +++--- pkg/ffcapi/api_test.go | 2 +- pkg/ffcapi/event_listener_hwm.go | 29 +++++ pkg/ffcapi/event_stream_start.go | 2 +- 10 files changed, 371 insertions(+), 104 deletions(-) create mode 100644 pkg/ffcapi/event_listener_hwm.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 93f5ae67..2d4050fc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -48,6 +48,7 @@ "Kaleido", "leveldb", "loadbalanced", + "logrus", "mtxs", "NATS", "Nowarn", diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index b05d78ae..21741513 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -47,12 +47,14 @@ const ( RemovedEventLog NewTransaction RemovedTransaction + ListenerRemoved ) type Notification struct { NotificationType NotificationType Event *EventInfo Transaction *TransactionInfo + RemovedListener *RemovedListenerInfo } type EventInfo struct { @@ -66,9 +68,9 @@ type TransactionInfo struct { Confirmed func(confirmations []BlockInfo) } -type StoppedStreamInfo struct { - StreamID string - Completed chan struct{} +type RemovedListenerInfo struct { + ListenerID *fftypes.UUID + Completed chan struct{} } type BlockInfo struct { @@ -127,10 +129,11 @@ type pendingItem struct { receiptCallback func(receipt *ffcapi.TransactionReceiptResponse) confirmedCallback func(confirmations []BlockInfo) transactionHash string - blockHash string // can be notified of changes to this for receipts - blockNumber uint64 // known at creation time for event logs - transactionIndex uint64 // known at creation time for event logs - logIndex uint64 // events only + blockHash string // can be notified of changes to this for receipts + blockNumber uint64 // known at creation time for event logs + transactionIndex uint64 // known at creation time for event logs + logIndex uint64 // events only + listenerID *fftypes.UUID // events only } func pendingKeyForTX(txHash string) string { @@ -142,7 +145,7 @@ func (pi *pendingItem) getKey() string { case pendingTypeEvent: // For events they are identified by their hash, blockNumber, transactionIndex and logIndex // If any of those change, it's a new new event - and as such we should get informed of it separately by the blockchain connector. - return fmt.Sprintf("Event:th=%s,bh=%s,bn=%d,ti=%d,li=%d", pi.transactionHash, pi.blockHash, pi.blockNumber, pi.transactionIndex, pi.logIndex) + return fmt.Sprintf("Event:l=%s,th=%s,bh=%s,bn=%d,ti=%d,li=%d", pi.listenerID, pi.transactionHash, pi.blockHash, pi.blockNumber, pi.transactionIndex, pi.logIndex) case pendingTypeTransaction: // For transactions, it's simply the transaction hash that identifies it. It can go into any block return pendingKeyForTX(pi.transactionHash) @@ -167,6 +170,7 @@ func (pi *pendingItem) copyConfirmations() []BlockInfo { func (n *Notification) eventPendingItem() *pendingItem { return &pendingItem{ pType: pendingTypeEvent, + listenerID: n.Event.ListenerID, blockNumber: n.Event.BlockNumber, blockHash: n.Event.BlockHash, transactionHash: n.Event.TransactionHash, @@ -223,13 +227,17 @@ func (bcm *blockConfirmationManager) NewBlockHashes() chan<- *ffcapi.BlockHashEv func (bcm *blockConfirmationManager) Notify(n *Notification) error { switch n.NotificationType { case NewEventLog, RemovedEventLog: - if n.Event == nil || n.Event.TransactionHash == "" || n.Event.BlockHash == "" { + if n.Event == nil || n.Event.ListenerID == nil || n.Event.TransactionHash == "" || n.Event.BlockHash == "" { return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n) } case NewTransaction, RemovedTransaction: if n.Transaction == nil || n.Transaction.TransactionHash == "" { return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n) } + case ListenerRemoved: + if n.RemovedListener == nil || n.RemovedListener.Completed == nil { + return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n) + } } select { case bcm.bcmNotifications <- n: @@ -296,8 +304,13 @@ func (bcm *blockConfirmationManager) confirmationsListener() { log.L(bcm.ctx).Debugf("Block confirmation listener stopping") return case notification := <-bcm.bcmNotifications: - // Defer until after we've got new logs - notifications = append(notifications, notification) + if notification.NotificationType == ListenerRemoved { + // Handle listener notifications immediately + bcm.listenerRemoved(notification) + } else { + // Defer until after we've got new logs + notifications = append(notifications, notification) + } } if bcm.blockListenerStale { @@ -406,6 +419,16 @@ func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem) { delete(bcm.staleReceipts, pending.getKey()) } +// listenerRemoved removes all pending work for a given listener, and notifies once done +func (bcm *blockConfirmationManager) listenerRemoved(notification *Notification) { + for pendingKey, pending := range bcm.pending { + if notification.RemovedListener.ListenerID.Equals(pending.listenerID) { + delete(bcm.pending, pendingKey) + } + } + close(notification.RemovedListener.Completed) +} + // addEvent is called by the goroutine on receipt of a new event/transaction notification func (bcm *blockConfirmationManager) addOrReplaceItem(pending *pendingItem) { pending.added = time.Now() diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index 879db24a..6b6a5f56 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -51,6 +51,7 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, @@ -79,10 +80,11 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003.BlockHash })).Run(func(args mock.Arguments) { - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: eventToConfirm, }) + assert.NoError(t, err) }).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)), @@ -160,6 +162,7 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, @@ -204,10 +207,11 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { return r.BlockHash == block1003a.BlockHash })).Run(func(args mock.Arguments) { // Notify of event after we've downloaded the 1002/1003a - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: eventToConfirm, }) + assert.NoError(t, err) }).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003a.BlockNumber)), @@ -303,10 +307,11 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { ParentHash: "0xea681fadcf56ee6254a0d30b255c56636ee9199c73c45f0dd5823759b2ad1ef8", } // We start with a notification for this one - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: NewTransaction, Transaction: txToConfirmForkA, }) + assert.NoError(t, err) block1001b := &BlockInfo{ BlockNumber: 1001, @@ -458,6 +463,7 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, @@ -513,10 +519,11 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: eventToConfirm, }) + assert.NoError(t, err) bcm.Start() @@ -555,16 +562,18 @@ func TestConfirmationsListenerFailWalkingChain(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) bcm.done = make(chan struct{}) - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: &EventInfo{ EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, }, }, }) + assert.NoError(t, err) mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 @@ -585,6 +594,7 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, @@ -595,10 +605,11 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { confirmed <- confirmations }, } - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: eventToConfirm, }) + assert.NoError(t, err) mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 @@ -611,6 +622,47 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { mca.AssertExpectations(t) } +func TestConfirmationsListenerRemoved(t *testing.T) { + + bcm, mca := newTestBlockConfirmationManager(t, false) + bcm.done = make(chan struct{}) + + lid := fftypes.NewUUID() + n := &Notification{ + Event: &EventInfo{ + EventID: ffcapi.EventID{ + ListenerID: lid, + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, + }, + } + bcm.addOrReplaceItem(n.eventPendingItem()) + completed := make(chan struct{}) + err := bcm.Notify(&Notification{ + NotificationType: ListenerRemoved, + RemovedListener: &RemovedListenerInfo{ + ListenerID: lid, + Completed: completed, + }, + }) + assert.NoError(t, err) + + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Maybe() + mca.On("GetBlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Maybe() + + bcm.Start() + + <-completed + assert.Empty(t, bcm.pending) + + bcm.Stop() + mca.AssertExpectations(t) +} + func TestConfirmationsRemoveEvent(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) @@ -618,6 +670,7 @@ func TestConfirmationsRemoveEvent(t *testing.T) { eventInfo := &EventInfo{ EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, @@ -628,10 +681,11 @@ func TestConfirmationsRemoveEvent(t *testing.T) { bcm.addOrReplaceItem((&Notification{ Event: eventInfo, }).eventPendingItem()) - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: RemovedEventLog, Event: eventInfo, }) + assert.NoError(t, err) mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 @@ -655,6 +709,7 @@ func TestConfirmationsFailWalkChainAfterBlockGap(t *testing.T) { NotificationType: NewEventLog, Event: &EventInfo{ EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, @@ -663,7 +718,8 @@ func TestConfirmationsFailWalkChainAfterBlockGap(t *testing.T) { }, }, } - bcm.Notify(eventNotification) + err := bcm.Notify(eventNotification) + assert.NoError(t, err) mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) { bcm.NewBlockHashes() <- &ffcapi.BlockHashEvent{ @@ -695,6 +751,7 @@ func TestConfirmationsRemoveTransaction(t *testing.T) { NotificationType: NewEventLog, Event: &EventInfo{ EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, @@ -708,12 +765,14 @@ func TestConfirmationsRemoveTransaction(t *testing.T) { }).transactionPendingItem()) go func() { // The notification we want to test - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: RemovedTransaction, Transaction: txInfo, }) + assert.NoError(t, err) // Another notification that causes BlockInfoByNumber, so we can break the loop - bcm.Notify(eventNotification) + err = bcm.Notify(eventNotification) + assert.NoError(t, err) }() mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) { @@ -735,6 +794,7 @@ func TestWalkChainForEventBlockNotInConfirmationChain(t *testing.T) { pending := (&Notification{ Event: &EventInfo{ EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, @@ -767,6 +827,7 @@ func TestWalkChainForEventBlockLookupFail(t *testing.T) { pending := (&Notification{ Event: &EventInfo{ EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", BlockNumber: 1001, @@ -850,6 +911,11 @@ func TestNotificationValidation(t *testing.T) { }) assert.Regexp(t, "FF21016", err) + err = bcm.Notify(&Notification{ + NotificationType: ListenerRemoved, + }) + assert.Regexp(t, "FF21016", err) + bcm.cancelFunc() err = bcm.Notify(&Notification{ NotificationType: NewTransaction, diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 05c14022..49ba8fdd 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -93,7 +93,7 @@ type startedStreamState struct { eventLoopDone chan struct{} batchLoopDone chan struct{} blockListenerDone chan struct{} - updates chan *ffcapi.ListenerUpdate + updates chan *ffcapi.ListenerEvent blocks chan *ffcapi.BlockHashEvent } @@ -109,7 +109,7 @@ type eventStream struct { wsChannels ws.WebSocketChannels retry *retry.Retry currentState *startedStreamState - batchChannel chan *ffcapi.ListenerUpdate + batchChannel chan *ffcapi.ListenerEvent } func NewEventStream( @@ -139,7 +139,7 @@ func NewEventStream( if es.spec, _, err = mergeValidateEsConfig(esCtx, nil, persistedSpec); err != nil { return nil, err } - es.batchChannel = make(chan *ffcapi.ListenerUpdate, *es.spec.BatchSize) + es.batchChannel = make(chan *ffcapi.ListenerEvent, *es.spec.BatchSize) for _, existing := range initialListeners { spec, err := es.verifyListenerOptions(esCtx, existing.ID, existing) if err != nil { @@ -475,7 +475,7 @@ func (es *eventStream) Start(ctx context.Context) error { eventLoopDone: make(chan struct{}), batchLoopDone: make(chan struct{}), blockListenerDone: make(chan struct{}), - updates: make(chan *ffcapi.ListenerUpdate, int(*es.spec.BatchSize)), + updates: make(chan *ffcapi.ListenerEvent, int(*es.spec.BatchSize)), blocks: make(chan *ffcapi.BlockHashEvent), // we promise to consume immediately } startedState.ctx, startedState.cancelCtx = context.WithCancel(es.bgCtx) @@ -584,42 +584,68 @@ func (es *eventStream) Delete(ctx context.Context) error { return es.checkSetStatus(ctx, apitypes.EventStreamStatusStopped, apitypes.EventStreamStatusDeleted) } +func (es *eventStream) processNewEvent(ctx context.Context, fev *ffcapi.ListenerEvent) { + event := fev.Event + if event == nil || event.ListenerID == nil || fev.Checkpoint == nil { + log.L(ctx).Warnf("Invalid event from connector: %+v", fev) + return + } + es.mux.Lock() + l := es.listeners[*fev.Event.ListenerID] + es.mux.Unlock() + if l != nil { + log.L(ctx).Debugf("%s event detected: %s", l.spec.ID, event) + if es.confirmations == nil { + // Updates that are just a checkpoint update, go straight to the batch loop. + // Or if the confirmation manager is disabled. + // - Note this will block the eventLoop when the event stream is blocked + es.batchChannel <- fev + } else { + // Notify will block, when the confirmation manager is blocked, which per below + // will flow back from when the event stream is blocked + err := es.confirmations.Notify(&confirmations.Notification{ + NotificationType: confirmations.NewEventLog, + Event: &confirmations.EventInfo{ + EventID: event.EventID, + Confirmed: func(confirmations []confirmations.BlockInfo) { + // Push it to the batch when confirmed + // - Note this will block the confirmation manager when the event stream is blocked + es.batchChannel <- fev + }, + }, + }) + if err != nil { + log.L(ctx).Warnf("Failed to notify confirmation manager for event '%s': %s", event, err) + } + } + } +} + +func (es *eventStream) processRemovedEvent(ctx context.Context, fev *ffcapi.ListenerEvent) { + if fev.Event != nil && fev.Event.ListenerID != nil && es.confirmations != nil { + err := es.confirmations.Notify(&confirmations.Notification{ + NotificationType: confirmations.RemovedEventLog, + Event: &confirmations.EventInfo{ + EventID: fev.Event.EventID, + }, + }) + if err != nil { + log.L(ctx).Warnf("Failed to notify confirmation manager for removed event '%s': %s", fev.Event, err) + } + } +} + func (es *eventStream) eventLoop(startedState *startedStreamState) { defer close(startedState.eventLoopDone) ctx := startedState.ctx for { select { - case update := <-startedState.updates: - event := update.Event - es.mux.Lock() - l := es.listeners[*update.ListenerID] - es.mux.Unlock() - if l != nil { - log.L(es.bgCtx).Debugf("%s event detected: %s", update.ListenerID, update.Event) - if event == nil || es.confirmations == nil { - // Updates that are just a checkpoint update, go straight to the batch loop. - // Or if the confirmation manager is disabled. - // - Note this will block the eventLoop when the event stream is blocked - es.batchChannel <- update - } else { - // Notify will block, when the confirmation manager is blocked, which per below - // will flow back from when the event stream is blocked - err := es.confirmations.Notify(&confirmations.Notification{ - NotificationType: confirmations.NewEventLog, - Event: &confirmations.EventInfo{ - EventID: event.EventID, - Confirmed: func(confirmations []confirmations.BlockInfo) { - // Push it to the batch when confirmed - // - Note this will block the confirmation manager when the event stream is blocked - es.batchChannel <- update - }, - }, - }) - if err != nil { - log.L(es.bgCtx).Warnf("Failed to notify confirmation manager for event '%s': %s", update.Event, err) - } - } + case lev := <-startedState.updates: + if lev.Removed { + es.processRemovedEvent(ctx, lev) + } else { + es.processNewEvent(ctx, lev) } case <-ctx.Done(): log.L(ctx).Debugf("Event loop exiting") @@ -647,7 +673,7 @@ func (es *eventStream) batchLoop(startedState *startedStreamState) { timeoutContext = ctx } select { - case update := <-es.batchChannel: + case fev := <-es.batchChannel: if batch == nil { batchNumber++ batch = &eventStreamBatch{ @@ -656,19 +682,18 @@ func (es *eventStream) batchLoop(startedState *startedStreamState) { } batch.timeoutContext, batch.timeoutCancel = context.WithTimeout(ctx, batchTimeout) } - if update.Checkpoint != nil { - batch.checkpoints[*update.ListenerID] = update.Checkpoint + if fev.Checkpoint != nil { + batch.checkpoints[*fev.Event.ListenerID] = fev.Checkpoint } - if update.Event != nil { + if fev.Event != nil { es.mux.Lock() - l := es.listeners[*update.ListenerID] + l := es.listeners[*fev.Event.ListenerID] es.mux.Unlock() if l != nil { - log.L(es.bgCtx).Debugf("%s '%s' event confirmed: %s", l.spec.ID, l.spec.Signature, update.Event) + log.L(es.bgCtx).Debugf("%s '%s' event confirmed: %s", l.spec.ID, l.spec.Signature, fev.Event) batch.events = append(batch.events, &ffcapi.EventWithContext{ - StreamID: es.spec.ID, - ListenerID: update.ListenerID, - Event: *update.Event, + StreamID: es.spec.ID, + Event: *fev.Event, }) } } diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index eb10614a..194ab8c3 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -342,11 +342,11 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { r := <-started - r.EventStream <- &ffcapi.ListenerUpdate{ - ListenerID: l.ID, + r.EventStream <- &ffcapi.ListenerEvent{ Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), Event: &ffcapi.Event{ EventID: ffcapi.EventID{ + ListenerID: l.ID, BlockNumber: 42, TransactionIndex: 13, LogIndex: 1, @@ -449,11 +449,11 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { r := <-started - r.EventStream <- &ffcapi.ListenerUpdate{ - ListenerID: l.ID, + r.EventStream <- &ffcapi.ListenerEvent{ Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), Event: &ffcapi.Event{ EventID: ffcapi.EventID{ + ListenerID: l.ID, BlockNumber: 42, TransactionIndex: 13, LogIndex: 1, @@ -1129,11 +1129,11 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { r := <-started - r.EventStream <- &ffcapi.ListenerUpdate{ - ListenerID: l.ID, + r.EventStream <- &ffcapi.ListenerEvent{ Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), Event: &ffcapi.Event{ EventID: ffcapi.EventID{ + ListenerID: l.ID, BlockNumber: 42, TransactionIndex: 13, LogIndex: 1, @@ -1186,7 +1186,7 @@ func TestActionRetryOk(t *testing.T) { // retry then ok err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ events: []*ffcapi.EventWithContext{ - {StreamID: es.spec.ID, ListenerID: fftypes.NewUUID()}, + {StreamID: es.spec.ID}, }, }) assert.NoError(t, err) @@ -1222,7 +1222,7 @@ func TestActionRetrySkip(t *testing.T) { // Skip behavior err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ events: []*ffcapi.EventWithContext{ - {StreamID: es.spec.ID, ListenerID: fftypes.NewUUID()}, + {StreamID: es.spec.ID}, }, }) assert.NoError(t, err) @@ -1268,7 +1268,7 @@ func TestActionRetryBlock(t *testing.T) { // Skip behavior err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ events: []*ffcapi.EventWithContext{ - {StreamID: es.spec.ID, ListenerID: fftypes.NewUUID()}, + {StreamID: es.spec.ID}, }, }) assert.Regexp(t, "FF00154", err) @@ -1277,6 +1277,78 @@ func TestActionRetryBlock(t *testing.T) { assert.Greater(t, callCount, 0) } +func TestEventLoopProcessRemovedEvent(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + eventLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + u1 := &ffcapi.ListenerEvent{ + Removed: true, + Event: &ffcapi.Event{ + EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + }, + }, + } + mcm := &confirmationsmocks.Manager{} + mcm.On("Notify", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + ss.cancelCtx() + }) + es.confirmations = mcm + es.listeners[*u1.Event.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ListenerID}, + } + + go func() { + ss.updates <- u1 + }() + + es.eventLoop(ss) +} + +func TestEventLoopProcessRemovedEventFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + eventLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + u1 := &ffcapi.ListenerEvent{ + Removed: true, + Event: &ffcapi.Event{ + EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + }, + }, + } + mcm := &confirmationsmocks.Manager{} + mcm.On("Notify", mock.Anything).Return(fmt.Errorf("pop")).Run(func(args mock.Arguments) { + ss.cancelCtx() + }) + es.confirmations = mcm + es.listeners[*u1.Event.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ListenerID}, + } + + go func() { + ss.updates <- u1 + }() + + es.eventLoop(ss) +} + func TestEventLoopConfirmationsManagerBypass(t *testing.T) { es := newTestEventStream(t, `{ @@ -1284,16 +1356,23 @@ func TestEventLoopConfirmationsManagerBypass(t *testing.T) { }`) ss := &startedStreamState{ - updates: make(chan *ffcapi.ListenerUpdate, 1), + updates: make(chan *ffcapi.ListenerEvent, 1), eventLoopDone: make(chan struct{}), } ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) - u1 := &ffcapi.ListenerUpdate{ - ListenerID: fftypes.NewUUID(), + u1 := &ffcapi.ListenerEvent{ + Checkpoint: fftypes.JSONAnyPtr("{}"), + Event: &ffcapi.Event{ + EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + }, + }, } es.confirmations = nil - es.listeners[*u1.ListenerID] = &listener{} + es.listeners[*u1.Event.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ListenerID}, + } go func() { ss.updates <- u1 @@ -1312,21 +1391,27 @@ func TestEventLoopConfirmationsManagerFail(t *testing.T) { }`) ss := &startedStreamState{ - updates: make(chan *ffcapi.ListenerUpdate, 1), + updates: make(chan *ffcapi.ListenerEvent, 1), eventLoopDone: make(chan struct{}), } ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) - u1 := &ffcapi.ListenerUpdate{ - ListenerID: fftypes.NewUUID(), - Event: &ffcapi.Event{}, + u1 := &ffcapi.ListenerEvent{ + Checkpoint: fftypes.JSONAnyPtr("{}"), + Event: &ffcapi.Event{ + EventID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + }, + }, } mcm := &confirmationsmocks.Manager{} mcm.On("Notify", mock.Anything).Return(fmt.Errorf("pop")).Run(func(args mock.Arguments) { ss.cancelCtx() }) es.confirmations = mcm - es.listeners[*u1.ListenerID] = &listener{} + es.listeners[*u1.Event.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ListenerID}, + } go func() { ss.updates <- u1 @@ -1334,3 +1419,12 @@ func TestEventLoopConfirmationsManagerFail(t *testing.T) { es.eventLoop(ss) } + +func TestEventLoopIgnoreBadEvent(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + es.processNewEvent(context.Background(), &ffcapi.ListenerEvent{}) +} diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index d460b285..e2bf7ff1 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -104,6 +104,36 @@ func (_m *API) EventListenerAdd(ctx context.Context, req *ffcapi.EventListenerAd return r0, r1, r2 } +// EventListenerHWM provides a mock function with given fields: ctx, req +func (_m *API) EventListenerHWM(ctx context.Context, req *ffcapi.EventListenerHWMRequest) (*ffcapi.EventListenerHWMResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.EventListenerHWMResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventListenerHWMRequest) *ffcapi.EventListenerHWMResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.EventListenerHWMResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventListenerHWMRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventListenerHWMRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // EventListenerRemove provides a mock function with given fields: ctx, req func (_m *API) EventListenerRemove(ctx context.Context, req *ffcapi.EventListenerRemoveRequest) (*ffcapi.EventListenerRemoveResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 90e470df..e5aa39aa 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -64,6 +64,9 @@ type API interface { // EventListenerRemove ends listening on a set of events previous started EventListenerRemove(ctx context.Context, req *EventListenerRemoveRequest) (*EventListenerRemoveResponse, ErrorReason, error) + + // EventListenerHWM queries the current high water mark checkpoint for a listener. Called at regular intervals when there are no events in flight for a listener, to ensure checkpoint are written regularly even when there is no activity + EventListenerHWM(ctx context.Context, req *EventListenerHWMRequest) (*EventListenerHWMResponse, ErrorReason, error) } type BlockHashEvent struct { @@ -73,11 +76,12 @@ type BlockHashEvent struct { // EventID are the set of required fields an FFCAPI compatible connector needs to map to the underlying blockchain constructs, to uniquely identify an event type EventID struct { - BlockHash string // String representation of the block, which will change if any transaction info in the block changes - BlockNumber uint64 // A numeric identifier for the block - TransactionHash string // The transaction - TransactionIndex uint64 // Index within the block of the transaction that emitted the event - LogIndex uint64 // Index within the transaction of this emitted event log + ListenerID *fftypes.UUID // The listener for the event + BlockHash string // String representation of the block, which will change if any transaction info in the block changes + BlockNumber uint64 // A numeric identifier for the block + TransactionHash string // The transaction + TransactionIndex uint64 // Index within the block of the transaction that emitted the event + LogIndex uint64 // Index within the transaction of this emitted event log } // Event is a blockchain event that matches one of the started listeners. @@ -89,9 +93,9 @@ type Event struct { Info *fftypes.JSONAny `json:"info"` // additional blockchain specific information } -// String is unique in all cases for an event, by combining the protocol ID with the block hash +// String is unique in all cases for an event, by combining the protocol ID with the listener ID and block hash func (eid *EventID) String() string { - return fmt.Sprintf("%s/B=%s", eid.ProtocolID(), eid.BlockHash) + return fmt.Sprintf("%s/B=%s/L=%s", eid.ProtocolID(), eid.BlockHash, eid.ListenerID) } // ProtocolID represents the unique (once finality is reached) sortable position within the blockchain @@ -107,7 +111,7 @@ func (es Events) Swap(i, j int) { es[i], es[j] = es[j], es[i] } func (es Events) Less(i, j int) bool { return evLess(es[i], es[j]) } // ListenerUpdates array has a natural sort order of the event -type ListenerUpdates []*ListenerUpdate +type ListenerUpdates []*ListenerEvent func (lu ListenerUpdates) Len() int { return len(lu) } func (lu ListenerUpdates) Swap(i, j int) { lu[i], lu[j] = lu[j], lu[i] } @@ -121,21 +125,16 @@ func evLess(eI *Event, eJ *Event) bool { } type EventWithContext struct { - StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event - ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this event + StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event Event } -// A listener update contains a checkpoint, plus zero or one events. -// - If only a checkpoint, then that will be stored immediately. Use this to notify when there has been a period of inactivity -// where no events have arrived - to minimize reprocessing of the chain after a restart. -// If there are any events in-flight for this listener in the confirmation manager, then the checkpoint will be ignored. -// - If an event is included, then this will be passed to the confirmation manager. The checkpoint will only be stored after -// the event is confirmed and successfully processed by the listener. -type ListenerUpdate struct { - ListenerID *fftypes.UUID `json:"listenerId"` // the ID of the event listener for this update - expected to be the same for all events in the events array - Checkpoint *fftypes.JSONAny `json:"checkpoint"` // checkpoint information for the listener. This should be supplied regularly even if there are no events, to minimize recovery time after restart - Event *Event `json:"event,omitempty"` // An event an be nil for checkpoint-only updates +// ListenerEvent is an event+checkpoint for a particular listener, and is the object delivered over the event stream channel when +// a new event is detected for delivery to the confirmation manager. +type ListenerEvent struct { + Checkpoint *fftypes.JSONAny `json:"checkpoint"` // the checkpoint information associated with the event, must be non-nil if the event is not removed + Event *Event `json:"event"` // the event - for removed events, can only have the EventID fields set (to generate the protocol ID) + Removed bool `json:"removed,omitempty"` // when true, this is an explicit cancellation of a previous event } // ErrorReason are a set of standard error conditions that a blockchain connector can return diff --git a/pkg/ffcapi/api_test.go b/pkg/ffcapi/api_test.go index 868be567..20491ce8 100644 --- a/pkg/ffcapi/api_test.go +++ b/pkg/ffcapi/api_test.go @@ -41,7 +41,7 @@ func TestSortEvents(t *testing.T) { LogIndex: l.Uint64(), }, } - listenerUpdates[i] = &ListenerUpdate{ + listenerUpdates[i] = &ListenerEvent{ Event: events[i], } } diff --git a/pkg/ffcapi/event_listener_hwm.go b/pkg/ffcapi/event_listener_hwm.go new file mode 100644 index 00000000..16a945da --- /dev/null +++ b/pkg/ffcapi/event_listener_hwm.go @@ -0,0 +1,29 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type EventListenerHWMRequest struct { + ID *fftypes.UUID `json:"uuid"` +} + +type EventListenerHWMResponse struct { + Checkpoint fftypes.JSONAny `json:"checkpoint"` +} diff --git a/pkg/ffcapi/event_stream_start.go b/pkg/ffcapi/event_stream_start.go index 0475da5e..96f0590f 100644 --- a/pkg/ffcapi/event_stream_start.go +++ b/pkg/ffcapi/event_stream_start.go @@ -25,7 +25,7 @@ import ( type EventStreamStartRequest struct { ID *fftypes.UUID // UUID of the stream, which we be referenced in any future add/remove listener requests StreamContext context.Context // Context that will be cancelled when the event stream needs to stop - no further events will be consumed after this, so all pushes to the stream should select on the done channel too - EventStream chan<- *ListenerUpdate // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events + EventStream chan<- *ListenerEvent // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events BlockListener chan<- *BlockHashEvent // The connector should push new blocks to every stream, marking if it's possible blocks were missed (due to reconnect). The stream guarantees to always consume from this channel, until the stream context closes. InitialListeners []*EventListenerAddRequest // Initial list of event listeners to start with the stream - allows these to be started concurrently } From 5b0fec25b1b02ed4aaf6ad4d5aaf2529b140f616 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 5 Jul 2022 23:02:26 -0400 Subject: [PATCH 40/95] Regular checkpoints for listeners not actively delivering events Signed-off-by: Peter Broadhurst --- config.md | 6 + internal/confirmations/confirmations.go | 26 ++++ internal/confirmations/confirmations_test.go | 2 + internal/events/eventstream.go | 145 +++++++++++++------ internal/events/eventstream_test.go | 120 +++++++++++++++ internal/events/listener.go | 7 +- internal/tmconfig/tmconfig.go | 2 + internal/tmmsgs/en_config_descriptions.go | 1 + mocks/confirmationsmocks/manager.go | 16 ++ 9 files changed, 278 insertions(+), 47 deletions(-) diff --git a/config.md b/config.md index d8b6913c..27579fc5 100644 --- a/config.md +++ b/config.md @@ -61,6 +61,12 @@ nav_order: 2 |methods| CORS setting to control the allowed methods|`string`|`[GET POST PUT PATCH DELETE]` |origins|CORS setting to control the allowed origins|`string`|`[*]` +## eventstreams + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|checkpointInterval|Regular interval to write checkpoints for an event stream listener that is not actively detecting/delivering events|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1m` + ## eventstreams.defaults |Key|Description|Type|Default Value| diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index 21741513..980396eb 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -19,6 +19,7 @@ import ( "context" "fmt" "sort" + "sync" "time" "github.com/hyperledger/firefly-common/pkg/config" @@ -38,6 +39,7 @@ type Manager interface { Start() Stop() NewBlockHashes() chan<- *ffcapi.BlockHashEvent + CheckInFlight(listenerID *fftypes.UUID) bool } type NotificationType int @@ -92,6 +94,7 @@ type blockConfirmationManager struct { bcmNotifications chan *Notification highestBlockSeen uint64 pending map[string]*pendingItem + pendingMux sync.Mutex staleReceipts map[string]bool done chan struct{} } @@ -248,6 +251,17 @@ func (bcm *blockConfirmationManager) Notify(n *Notification) error { return nil } +func (bcm *blockConfirmationManager) CheckInFlight(listenerID *fftypes.UUID) bool { + bcm.pendingMux.Lock() + defer bcm.pendingMux.Unlock() + for _, p := range bcm.pending { + if listenerID.Equals(p.listenerID) { + return true + } + } + return false +} + func (bcm *blockConfirmationManager) getBlockByHash(blockHash string) (*BlockInfo, error) { res, reason, err := bcm.connector.BlockInfoByHash(bcm.ctx, &ffcapi.BlockInfoByHashRequest{ BlockHash: blockHash, @@ -421,6 +435,8 @@ func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem) { // listenerRemoved removes all pending work for a given listener, and notifies once done func (bcm *blockConfirmationManager) listenerRemoved(notification *Notification) { + bcm.pendingMux.Lock() + defer bcm.pendingMux.Unlock() for pendingKey, pending := range bcm.pending { if notification.RemovedListener.ListenerID.Equals(pending.listenerID) { delete(bcm.pending, pendingKey) @@ -431,6 +447,8 @@ func (bcm *blockConfirmationManager) listenerRemoved(notification *Notification) // addEvent is called by the goroutine on receipt of a new event/transaction notification func (bcm *blockConfirmationManager) addOrReplaceItem(pending *pendingItem) { + bcm.pendingMux.Lock() + defer bcm.pendingMux.Unlock() pending.added = time.Now() pending.confirmations = make([]*BlockInfo, 0, bcm.requiredConfirmations) pendingKey := pending.getKey() @@ -440,6 +458,8 @@ func (bcm *blockConfirmationManager) addOrReplaceItem(pending *pendingItem) { // removeEvent is called by the goroutine on receipt of a remove event notification func (bcm *blockConfirmationManager) removeItem(pending *pendingItem) { + bcm.pendingMux.Lock() + defer bcm.pendingMux.Unlock() pendingKey := pending.getKey() log.L(bcm.ctx).Infof("Removing stale item %s", pendingKey) delete(bcm.pending, pendingKey) @@ -473,6 +493,7 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { // For any transactions in the block that are known to us, we need to mark them // stale to go query the receipt + bcm.pendingMux.Lock() for _, txHash := range block.TransactionHashes { txKey := pendingKeyForTX(txHash) if pending, ok := bcm.pending[txKey]; ok { @@ -482,6 +503,7 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { } } } + bcm.pendingMux.Unlock() // Go through all the events, adding in the confirmations, and popping any out // that have reached their threshold. Then drop the log before logging/processing them. @@ -507,7 +529,9 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { expectedBlockNumber++ } if len(pending.confirmations) >= bcm.requiredConfirmations { + bcm.pendingMux.Lock() delete(bcm.pending, pendingKey) + bcm.pendingMux.Unlock() confirmed = append(confirmed, pending) } @@ -535,10 +559,12 @@ func (bcm *blockConfirmationManager) dispatchConfirmed(item *pendingItem) { func (bcm *blockConfirmationManager) walkChain() error { // Grab a copy of all the pending in order + bcm.pendingMux.Lock() pendingItems := make(pendingItems, 0, len(bcm.pending)) for _, pending := range bcm.pending { pendingItems = append(pendingItems, pending) } + bcm.pendingMux.Unlock() sort.Sort(pendingItems) // Go through them in order - using the cache for efficiency diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index 6b6a5f56..93500ce4 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -697,6 +697,7 @@ func TestConfirmationsRemoveEvent(t *testing.T) { <-bcm.done assert.Empty(t, bcm.pending) + assert.False(t, bcm.CheckInFlight(eventInfo.ListenerID)) mca.AssertExpectations(t) } @@ -735,6 +736,7 @@ func TestConfirmationsFailWalkChainAfterBlockGap(t *testing.T) { <-bcm.done assert.Len(t, bcm.pending, 1) + assert.True(t, bcm.CheckInFlight(eventNotification.Event.ListenerID)) assert.NotNil(t, eventNotification.eventPendingItem().getKey()) // should be the event in there, the TX should be removed mca.AssertExpectations(t) } diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 49ba8fdd..83a1b402 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -78,11 +78,10 @@ func InitDefaults() { type eventStreamAction func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error type eventStreamBatch struct { - number int - events []*ffcapi.EventWithContext - checkpoints map[fftypes.UUID]*fftypes.JSONAny - timeoutContext context.Context - timeoutCancel func() + number int + events []*ffcapi.EventWithContext + checkpoints map[fftypes.UUID]*fftypes.JSONAny + timeout *time.Timer } type startedStreamState struct { @@ -98,18 +97,19 @@ type startedStreamState struct { } type eventStream struct { - bgCtx context.Context - spec *apitypes.EventStream - mux sync.Mutex - status apitypes.EventStreamStatus - connector ffcapi.API - persistence persistence.Persistence - confirmations confirmations.Manager - listeners map[fftypes.UUID]*listener - wsChannels ws.WebSocketChannels - retry *retry.Retry - currentState *startedStreamState - batchChannel chan *ffcapi.ListenerEvent + bgCtx context.Context + spec *apitypes.EventStream + mux sync.Mutex + status apitypes.EventStreamStatus + connector ffcapi.API + persistence persistence.Persistence + confirmations confirmations.Manager + listeners map[fftypes.UUID]*listener + wsChannels ws.WebSocketChannels + retry *retry.Retry + currentState *startedStreamState + checkpointInterval time.Duration + batchChannel chan *ffcapi.ListenerEvent } func NewEventStream( @@ -122,14 +122,15 @@ func NewEventStream( ) (ees Stream, err error) { esCtx := log.WithLogField(bgCtx, "eventstream", persistedSpec.ID.String()) es := &eventStream{ - bgCtx: esCtx, - status: apitypes.EventStreamStatusStopped, - spec: persistedSpec, - connector: connector, - persistence: persistence, - listeners: make(map[fftypes.UUID]*listener), - wsChannels: wsChannels, - retry: esDefaults.retry, + bgCtx: esCtx, + status: apitypes.EventStreamStatusStopped, + spec: persistedSpec, + connector: connector, + persistence: persistence, + listeners: make(map[fftypes.UUID]*listener), + wsChannels: wsChannels, + retry: esDefaults.retry, + checkpointInterval: config.GetDuration(tmconfig.EventStreamsCheckpointInterval), } if config.GetInt(tmconfig.ConfirmationsRequired) > 0 { es.confirmations = confirmations.NewBlockConfirmationManager(esCtx, connector) @@ -659,28 +660,30 @@ func (es *eventStream) eventLoop(startedState *startedStreamState) { func (es *eventStream) batchLoop(startedState *startedStreamState) { defer close(startedState.batchLoopDone) ctx := startedState.ctx - batchTimeout := time.Duration(*es.spec.BatchTimeout) maxSize := int(*es.spec.BatchSize) batchNumber := 0 var batch *eventStreamBatch + var checkpointTimer = time.NewTimer(es.checkpointInterval) for { - var timeoutContext context.Context - var timedOut bool + var timeoutChannel <-chan time.Time if batch != nil { - timeoutContext = batch.timeoutContext + // Once a batch has started, the batch timeout always takes precedence (even if it slows down the checkpoint slightly) + timeoutChannel = batch.timeout.C } else { - timeoutContext = ctx + // If we don't have a batch in-flight, then the (longer) checkpoint timer is used + timeoutChannel = checkpointTimer.C } + timedOut := false select { case fev := <-es.batchChannel: if batch == nil { batchNumber++ batch = &eventStreamBatch{ number: batchNumber, + timeout: time.NewTimer(time.Duration(*es.spec.BatchTimeout)), checkpoints: make(map[fftypes.UUID]*fftypes.JSONAny), } - batch.timeoutContext, batch.timeoutCancel = context.WithTimeout(ctx, batchTimeout) } if fev.Checkpoint != nil { batch.checkpoints[*fev.Event.ListenerID] = fev.Checkpoint @@ -697,20 +700,28 @@ func (es *eventStream) batchLoop(startedState *startedStreamState) { }) } } - case <-timeoutContext.Done(): + case <-timeoutChannel: + timedOut = true if batch == nil { - // The started context exited, we are stopping - log.L(ctx).Debugf("Batch loop exiting") - return + checkpointTimer = time.NewTimer(es.checkpointInterval) } - // Otherwise we timed out - timedOut = true + case <-ctx.Done(): + // The started context exited, we are stopping + if checkpointTimer != nil { + checkpointTimer.Stop() + } + log.L(ctx).Debugf("Batch loop exiting") + return } - if batch != nil && (timedOut || len(batch.events) >= maxSize) { - batch.timeoutCancel() - err := es.performActionsWithRetry(startedState, batch) + if timedOut || len(batch.events) >= maxSize { + var err error + if batch != nil { + batch.timeout.Stop() + err = es.performActionsWithRetry(startedState, batch) + } if err == nil { + checkpointTimer = time.NewTimer(es.checkpointInterval) // Reset the checkpoint timeout err = es.writeCheckpoint(startedState, batch) } if err != nil { @@ -762,6 +773,40 @@ func (es *eventStream) performActionsWithRetry(startedState *startedStreamState, } } +func (es *eventStream) checkUpdateHWMCheckpoint(ctx context.Context, l *listener) *fftypes.JSONAny { + + checkpoint := l.checkpoint + + inFlight := false + if es.confirmations != nil { + inFlight = es.confirmations.CheckInFlight(l.spec.ID) + } + + // If there's in-flight messages in the confirmation manager, we wait for these to be confirmed or purged before + // writing a checkpoint. + if inFlight { + log.L(ctx).Infof("Stale checkpoint for listener '%s' will not be updated as events are in-flight", l.spec.ID) + } else { + res, _, err := es.connector.EventListenerHWM(ctx, &ffcapi.EventListenerHWMRequest{ + ID: l.spec.ID, + }) + if err != nil { + log.L(ctx).Errorf("Failed to obtain high watermark checkpoint for listener '%s': %s", l.spec.ID, err) + return checkpoint + } + es.mux.Lock() + if l.checkpoint == checkpoint /* double check it hasn't changed */ { + checkpoint = &res.Checkpoint + l.checkpoint = checkpoint + l.lastCheckpoint = fftypes.Now() + } + es.mux.Unlock() + } + + return checkpoint + +} + func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch *eventStreamBatch) (err error) { // We update the checkpoints (under lock) for all listeners with events in this batch. // The last event for any listener in the batch wins. @@ -771,17 +816,29 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * Time: fftypes.Now(), Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), } - for lID, lCP := range batch.checkpoints { - if l, ok := es.listeners[lID]; ok { - l.checkpoint = lCP - log.L(es.bgCtx).Tracef("%s (%s) checkpoint: %s", l.spec.Signature, l.spec.ID, lCP) + if batch != nil { + for lID, lCP := range batch.checkpoints { + if l, ok := es.listeners[lID]; ok { + l.checkpoint = lCP + l.lastCheckpoint = fftypes.Now() + log.L(es.bgCtx).Tracef("%s (%s) checkpoint: %s", l.spec.Signature, l.spec.ID, lCP) + } } } + staleCheckpoints := make([]*listener, 0) for lID, l := range es.listeners { cp.Listeners[lID] = l.checkpoint + if l.checkpoint == nil || l.lastCheckpoint == nil || time.Since(*l.lastCheckpoint.Time()) > es.checkpointInterval { + staleCheckpoints = append(staleCheckpoints, l) + } } es.mux.Unlock() + // Ask the connector for any updated high watermark checkpoints - checking we don't have any in-flight confirmations + for _, l := range staleCheckpoints { + cp.Listeners[*l.spec.ID] = es.checkUpdateHWMCheckpoint(startedState.ctx, l) + } + // We only return if the context is cancelled, or the checkpoint succeeds return es.retry.Do(startedState.ctx, "checkpoint", func(attempt int) (retry bool, err error) { return true, es.persistence.WriteCheckpoint(startedState.ctx, cp) diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 194ab8c3..9c4df866 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -1428,3 +1428,123 @@ func TestEventLoopIgnoreBadEvent(t *testing.T) { es.processNewEvent(context.Background(), &ffcapi.ListenerEvent{}) } + +func TestHWMCheckpointAfterInactivity(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + batchLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + li := &listener{ + spec: &apitypes.Listener{ID: fftypes.NewUUID()}, + } + + mcm := &confirmationsmocks.Manager{} + mcm.On("CheckInFlight", li.spec.ID).Return(false) + es.confirmations = mcm + es.listeners[*li.spec.ID] = li + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventListenerHWM", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventListenerHWMRequest) bool { + return req.ID.Equals(li.spec.ID) + })).Run(func(args mock.Arguments) { + ss.cancelCtx() + }).Return(&ffcapi.EventListenerHWMResponse{Checkpoint: *fftypes.JSONAnyPtr(`{"cp1data":"stuff"}`)}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*li.spec.ID].JSONObject().GetString("cp1data") == "stuff" + })).Return(nil) + + es.checkpointInterval = 1 * time.Microsecond + + es.batchLoop(ss) + + mfc.AssertExpectations(t) + msp.AssertExpectations(t) + mcm.AssertExpectations(t) +} + +func TestHWMCheckpointInFlightSkip(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + batchLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + li := &listener{ + spec: &apitypes.Listener{ID: fftypes.NewUUID()}, + } + + mcm := &confirmationsmocks.Manager{} + mcm.On("CheckInFlight", li.spec.ID).Run(func(args mock.Arguments) { + ss.cancelCtx() + }).Return(true) + es.confirmations = mcm + es.listeners[*li.spec.ID] = li + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*li.spec.ID] == nil + })).Return(nil) + + es.checkpointInterval = 1 * time.Microsecond + + es.batchLoop(ss) + + msp.AssertExpectations(t) + mcm.AssertExpectations(t) +} + +func TestHWMCheckpointFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + batchLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + li := &listener{ + spec: &apitypes.Listener{ID: fftypes.NewUUID()}, + } + + mcm := &confirmationsmocks.Manager{} + mcm.On("CheckInFlight", li.spec.ID).Return(false) + es.confirmations = mcm + es.listeners[*li.spec.ID] = li + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventListenerHWM", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventListenerHWMRequest) bool { + return req.ID.Equals(li.spec.ID) + })).Run(func(args mock.Arguments) { + ss.cancelCtx() + }).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*li.spec.ID] == nil + })).Return(nil) + + es.checkpointInterval = 1 * time.Microsecond + + es.batchLoop(ss) + + mfc.AssertExpectations(t) + msp.AssertExpectations(t) + mcm.AssertExpectations(t) +} diff --git a/internal/events/listener.go b/internal/events/listener.go index 2124e52c..55b100c5 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -23,9 +23,10 @@ import ( ) type listener struct { - es *eventStream - spec *apitypes.Listener - checkpoint *fftypes.JSONAny + es *eventStream + spec *apitypes.Listener + lastCheckpoint *fftypes.FFTime + checkpoint *fftypes.JSONAny } func listenerSpecToOptions(spec *apitypes.Listener) ffcapi.EventListenerOptions { diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index aa4f7942..d326d2ce 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -48,6 +48,7 @@ var ( EventStreamsDefaultsBlockedRetryDelay = ffc("eventstreams.defaults.blockedRetryDelay") EventStreamsDefaultsWebhookRequestTimeout = ffc("eventstreams.defaults.webhookRequestTimeout") EventStreamsDefaultsWebsocketDistributionMode = ffc("eventstreams.defaults.websocketDistributionMode") + EventStreamsCheckpointInterval = ffc("eventstreams.checkpointInterval") EventStreamsRetryInitDelay = ffc("eventstreams.retry.initialDelay") EventStreamsRetryMaxDelay = ffc("eventstreams.retry.maxDelay") EventStreamsRetryFactor = ffc("eventstreams.retry.factor") @@ -94,6 +95,7 @@ func setDefaults() { viper.SetDefault(string(EventStreamsDefaultsBlockedRetryDelay), "30s") viper.SetDefault(string(EventStreamsDefaultsWebhookRequestTimeout), "30s") viper.SetDefault(string(EventStreamsDefaultsWebsocketDistributionMode), "load_balance") + viper.SetDefault(string(EventStreamsCheckpointInterval), "1m") viper.SetDefault(string(WebhooksAllowPrivateIPs), true) viper.SetDefault(string(PersistenceType), "leveldb") diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go index c4a92c4f..12ed7f52 100644 --- a/internal/tmmsgs/en_config_descriptions.go +++ b/internal/tmmsgs/en_config_descriptions.go @@ -74,6 +74,7 @@ var ( ConfigEventStreamsDefaultsBlockedRetryDelay = ffc("config.eventstreams.defaults.blockedRetryDelay", "Default blocked retry delay for newly created event streams", i18n.TimeDurationType) ConfigEventStreamsDefaultsWebhookRequestTimeout = ffc("config.eventstreams.defaults.webhookRequestTimeout", "Default WebHook request timeout for newly created event streams", i18n.TimeDurationType) ConfigEventStreamsDefaultsWebsocketDistributionMode = ffc("config.eventstreams.defaults.websocketDistributionMode", "Default WebSocket distribution mode for newly created event streams", "'load_balance' or 'broadcast'") + ConfigEventStreamsCheckpointInterval = ffc("config.eventstreams.checkpointInterval", "Regular interval to write checkpoints for an event stream listener that is not actively detecting/delivering events", i18n.TimeDurationType) ConfigEventStreamsRetryInitDelay = ffc("config.eventstreams.retry.initialDelay", "Initial retry delay", i18n.TimeDurationType) ConfigEventStreamsRetryMaxDelay = ffc("config.eventstreams.retry.maxDelay", "Maximum delay between retries", i18n.TimeDurationType) ConfigEventStreamsRetryFactor = ffc("config.eventstreams.retry.factor", "Factor to increase the delay by, between each retry", i18n.FloatType) diff --git a/mocks/confirmationsmocks/manager.go b/mocks/confirmationsmocks/manager.go index 0c274d68..059e3894 100644 --- a/mocks/confirmationsmocks/manager.go +++ b/mocks/confirmationsmocks/manager.go @@ -6,6 +6,8 @@ import ( confirmations "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" + mock "github.com/stretchr/testify/mock" ) @@ -14,6 +16,20 @@ type Manager struct { mock.Mock } +// CheckInFlight provides a mock function with given fields: listenerID +func (_m *Manager) CheckInFlight(listenerID *fftypes.UUID) bool { + ret := _m.Called(listenerID) + + var r0 bool + if rf, ok := ret.Get(0).(func(*fftypes.UUID) bool); ok { + r0 = rf(listenerID) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // NewBlockHashes provides a mock function with given fields: func (_m *Manager) NewBlockHashes() chan<- *ffcapi.BlockHashEvent { ret := _m.Called() From afa136645bf8fb8437daa1dcbe69303986ce281b Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 6 Jul 2022 13:43:12 -0400 Subject: [PATCH 41/95] Rename ListenerUpdates to ListenerEvents Signed-off-by: Peter Broadhurst --- pkg/ffcapi/api.go | 10 +++++----- pkg/ffcapi/api_test.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index e5aa39aa..7445a564 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -110,12 +110,12 @@ func (es Events) Len() int { return len(es) } func (es Events) Swap(i, j int) { es[i], es[j] = es[j], es[i] } func (es Events) Less(i, j int) bool { return evLess(es[i], es[j]) } -// ListenerUpdates array has a natural sort order of the event -type ListenerUpdates []*ListenerEvent +// ListenerEvents array has a natural sort order of the event +type ListenerEvents []*ListenerEvent -func (lu ListenerUpdates) Len() int { return len(lu) } -func (lu ListenerUpdates) Swap(i, j int) { lu[i], lu[j] = lu[j], lu[i] } -func (lu ListenerUpdates) Less(i, j int) bool { return evLess(lu[i].Event, lu[j].Event) } +func (lu ListenerEvents) Len() int { return len(lu) } +func (lu ListenerEvents) Swap(i, j int) { lu[i], lu[j] = lu[j], lu[i] } +func (lu ListenerEvents) Less(i, j int) bool { return evLess(lu[i].Event, lu[j].Event) } func evLess(eI *Event, eJ *Event) bool { return eI.BlockNumber < eJ.BlockNumber || diff --git a/pkg/ffcapi/api_test.go b/pkg/ffcapi/api_test.go index 20491ce8..cf381e13 100644 --- a/pkg/ffcapi/api_test.go +++ b/pkg/ffcapi/api_test.go @@ -29,7 +29,7 @@ import ( func TestSortEvents(t *testing.T) { events := make(Events, 10000) - listenerUpdates := make(ListenerUpdates, len(events)) + listenerUpdates := make(ListenerEvents, len(events)) for i := 0; i < 10000; i++ { b, _ := rand.Int(rand.Reader, big.NewInt(1000)) t, _ := rand.Int(rand.Reader, big.NewInt(10)) From 9e5921ad47f9135d5ed93bb843140a730d6a8b3f Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 6 Jul 2022 13:55:44 -0400 Subject: [PATCH 42/95] Include stream and listener IDs in HWM call Signed-off-by: Peter Broadhurst --- internal/events/blocklistener_test.go | 8 +++++++- internal/events/eventstream.go | 3 ++- internal/events/eventstream_test.go | 4 ++-- pkg/ffcapi/event_listener_hwm.go | 3 ++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/events/blocklistener_test.go b/internal/events/blocklistener_test.go index 8f69ce32..3a7513c3 100644 --- a/internal/events/blocklistener_test.go +++ b/internal/events/blocklistener_test.go @@ -66,7 +66,13 @@ func TestBlockListenerDoesNotBlock(t *testing.T) { ss.blocks <- &ffcapi.BlockHashEvent{} ss.blocks <- &ffcapi.BlockHashEvent{} - // And check we can exit + // Unblock it again + bhe = <-blockIt + + // Block it again + ss.blocks <- &ffcapi.BlockHashEvent{} + + // And check we can exit while blocked ss.cancelCtx() wg.Wait() diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 83a1b402..5bae1a02 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -788,7 +788,8 @@ func (es *eventStream) checkUpdateHWMCheckpoint(ctx context.Context, l *listener log.L(ctx).Infof("Stale checkpoint for listener '%s' will not be updated as events are in-flight", l.spec.ID) } else { res, _, err := es.connector.EventListenerHWM(ctx, &ffcapi.EventListenerHWMRequest{ - ID: l.spec.ID, + StreamID: es.spec.ID, + ListenerID: l.spec.ID, }) if err != nil { log.L(ctx).Errorf("Failed to obtain high watermark checkpoint for listener '%s': %s", l.spec.ID, err) diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 9c4df866..4d13d6b8 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -1452,7 +1452,7 @@ func TestHWMCheckpointAfterInactivity(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) mfc.On("EventListenerHWM", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventListenerHWMRequest) bool { - return req.ID.Equals(li.spec.ID) + return req.StreamID.Equals(es.spec.ID) && req.ListenerID.Equals(li.spec.ID) })).Run(func(args mock.Arguments) { ss.cancelCtx() }).Return(&ffcapi.EventListenerHWMResponse{Checkpoint: *fftypes.JSONAnyPtr(`{"cp1data":"stuff"}`)}, ffcapi.ErrorReason(""), nil) @@ -1530,7 +1530,7 @@ func TestHWMCheckpointFail(t *testing.T) { mfc := es.connector.(*ffcapimocks.API) mfc.On("EventListenerHWM", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventListenerHWMRequest) bool { - return req.ID.Equals(li.spec.ID) + return req.StreamID.Equals(es.spec.ID) && req.ListenerID.Equals(li.spec.ID) })).Run(func(args mock.Arguments) { ss.cancelCtx() }).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) diff --git a/pkg/ffcapi/event_listener_hwm.go b/pkg/ffcapi/event_listener_hwm.go index 16a945da..2fdb2aa3 100644 --- a/pkg/ffcapi/event_listener_hwm.go +++ b/pkg/ffcapi/event_listener_hwm.go @@ -21,7 +21,8 @@ import ( ) type EventListenerHWMRequest struct { - ID *fftypes.UUID `json:"uuid"` + StreamID *fftypes.UUID `json:"streamId"` + ListenerID *fftypes.UUID `json:"listenerId"` } type EventListenerHWMResponse struct { From a0356eb341b1a2260870cdc1a31c261862ba0ebe Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sun, 10 Jul 2022 13:07:58 -0400 Subject: [PATCH 43/95] Consistency in params on FFCAPI for listener ops Signed-off-by: Peter Broadhurst --- internal/events/blocklistener_test.go | 1 + internal/events/eventstream_test.go | 24 ++++++++++++------------ internal/events/listener.go | 5 +++-- pkg/ffcapi/event_listener_add.go | 2 +- pkg/ffcapi/event_listener_remove.go | 3 ++- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/internal/events/blocklistener_test.go b/internal/events/blocklistener_test.go index 3a7513c3..b99c8e81 100644 --- a/internal/events/blocklistener_test.go +++ b/internal/events/blocklistener_test.go @@ -65,6 +65,7 @@ func TestBlockListenerDoesNotBlock(t *testing.T) { // Block it again ss.blocks <- &ffcapi.BlockHashEvent{} ss.blocks <- &ffcapi.BlockHashEvent{} + ss.blocks <- &ffcapi.BlockHashEvent{} // Unblock it again bhe = <-blockIt diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 4d13d6b8..e28c882e 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -319,7 +319,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l.ID) + return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) @@ -420,7 +420,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { - return r.ID.Equals(l.ID) + return r.ListenerID.Equals(l.ID) })).Run(func(args mock.Arguments) { r := args[1].(*ffcapi.EventListenerAddRequest) assert.JSONEq(t, `{"event":"definition1"}`, r.Filters[0].String()) @@ -432,7 +432,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { }).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l.ID) + return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) @@ -562,7 +562,7 @@ func TestUpdateStreamStarted(t *testing.T) { }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l.ID) + return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) @@ -623,7 +623,7 @@ func TestAddRemoveListener(t *testing.T) { }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l.ID) + return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) @@ -673,11 +673,11 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { - return r.ID.Equals(l1.ID) + return r.ListenerID.Equals(l1.ID) })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l1.ID) + return r.ListenerID.Equals(l1.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() msp := es.persistence.(*persistencemocks.Persistence) @@ -745,7 +745,7 @@ func TestUpdateListenerFail(t *testing.T) { mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l1.ID) + return r.ListenerID.Equals(l1.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() _, err := es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) @@ -826,7 +826,7 @@ func TestUpdateStreamRestartFail(t *testing.T) { mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil).Once() mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l.ID) + return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) err := es.Start(es.bgCtx) @@ -879,7 +879,7 @@ func TestUpdateAttemptChangeSignature(t *testing.T) { }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l.ID) + return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) @@ -953,7 +953,7 @@ func TestUpdateStreamStopFail(t *testing.T) { mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Twice() mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l.ID) + return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) @@ -1007,7 +1007,7 @@ func TestResetListenerRestartFail(t *testing.T) { mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ID.Equals(l.ID) + return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) diff --git a/internal/events/listener.go b/internal/events/listener.go index 55b100c5..beaf8367 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -39,7 +39,8 @@ func listenerSpecToOptions(spec *apitypes.Listener) ffcapi.EventListenerOptions func (l *listener) stop(startedState *startedStreamState) error { _, _, err := l.es.connector.EventListenerRemove(startedState.ctx, &ffcapi.EventListenerRemoveRequest{ - ID: l.spec.ID, + StreamID: l.spec.StreamID, + ListenerID: l.spec.ID, }) return err } @@ -48,7 +49,7 @@ func (l *listener) buildAddRequest() *ffcapi.EventListenerAddRequest { return &ffcapi.EventListenerAddRequest{ EventListenerOptions: listenerSpecToOptions(l.spec), Name: *l.spec.Name, - ID: l.spec.ID, + ListenerID: l.spec.ID, StreamID: l.spec.StreamID, } } diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go index 63c4e908..0ad2ee69 100644 --- a/pkg/ffcapi/event_listener_add.go +++ b/pkg/ffcapi/event_listener_add.go @@ -42,7 +42,7 @@ type EventListenerVerifyOptionsResponse struct { type EventListenerAddRequest struct { EventListenerOptions - ID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event + ListenerID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event StreamID *fftypes.UUID // The event stream (previously started) to which events should be delivered Name string // Descriptive name of the listener, provided by the user, or defaulted to the signature. Not guaranteed to be unique. Should be included in the event info Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream diff --git a/pkg/ffcapi/event_listener_remove.go b/pkg/ffcapi/event_listener_remove.go index d53550c7..dc26298d 100644 --- a/pkg/ffcapi/event_listener_remove.go +++ b/pkg/ffcapi/event_listener_remove.go @@ -21,7 +21,8 @@ import ( ) type EventListenerRemoveRequest struct { - ID *fftypes.UUID `json:"uuid"` + StreamID *fftypes.UUID `json:"streamId"` + ListenerID *fftypes.UUID `json:"listenerId"` } type EventListenerRemoveResponse struct { From 3b5c4d8663971a9d93924d9a74c7138571c1dd66 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sun, 10 Jul 2022 20:22:13 -0400 Subject: [PATCH 44/95] Update FFCAPI interface to detect re-delivery at the batch assembly point Signed-off-by: Peter Broadhurst --- internal/events/blocklistener_test.go | 12 +-- internal/events/eventstream.go | 43 +++++---- internal/events/eventstream_test.go | 87 ++++++++++++++++--- internal/events/listener.go | 2 +- .../persistence/leveldb_persistence_test.go | 11 ++- pkg/apitypes/api_types.go | 7 +- pkg/ffcapi/api.go | 12 ++- pkg/ffcapi/event_listener_add.go | 8 +- pkg/ffcapi/event_listener_hwm.go | 2 +- 9 files changed, 135 insertions(+), 49 deletions(-) diff --git a/internal/events/blocklistener_test.go b/internal/events/blocklistener_test.go index b99c8e81..e51a97e0 100644 --- a/internal/events/blocklistener_test.go +++ b/internal/events/blocklistener_test.go @@ -63,15 +63,9 @@ func TestBlockListenerDoesNotBlock(t *testing.T) { assert.True(t, bhe.GapPotential) // Block it again - ss.blocks <- &ffcapi.BlockHashEvent{} - ss.blocks <- &ffcapi.BlockHashEvent{} - ss.blocks <- &ffcapi.BlockHashEvent{} - - // Unblock it again - bhe = <-blockIt - - // Block it again - ss.blocks <- &ffcapi.BlockHashEvent{} + for i := 0; i < 100; i++ { + ss.blocks <- &ffcapi.BlockHashEvent{} + } // And check we can exit while blocked ss.cancelCtx() diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 5bae1a02..b065e2b3 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -80,7 +80,7 @@ type eventStreamAction func(ctx context.Context, batchNumber, attempt int, event type eventStreamBatch struct { number int events []*ffcapi.EventWithContext - checkpoints map[fftypes.UUID]*fftypes.JSONAny + checkpoints map[fftypes.UUID]ffcapi.EventListenerCheckpoint timeout *time.Timer } @@ -677,22 +677,35 @@ func (es *eventStream) batchLoop(startedState *startedStreamState) { timedOut := false select { case fev := <-es.batchChannel: - if batch == nil { - batchNumber++ - batch = &eventStreamBatch{ - number: batchNumber, - timeout: time.NewTimer(time.Duration(*es.spec.BatchTimeout)), - checkpoints: make(map[fftypes.UUID]*fftypes.JSONAny), - } - } - if fev.Checkpoint != nil { - batch.checkpoints[*fev.Event.ListenerID] = fev.Checkpoint - } if fev.Event != nil { es.mux.Lock() l := es.listeners[*fev.Event.ListenerID] es.mux.Unlock() if l != nil { + currentCheckpoint := l.checkpoint + if currentCheckpoint != nil && !currentCheckpoint.LessThan(fev.Checkpoint) { + // This event is behind the current checkpoint - this is a re-detection. + // We're perfectly happy to accept re-detections from the connector, as it can be + // very efficient to batch operations between listeners that cause re-detections. + // However, we need to protect the application from receiving the re-detections. + // This loop is the right place for this check, as we are responsible for writing the checkpoints and + // delivering to the application. So we are the one source of truth. + log.L(es.bgCtx).Debugf("%s '%s' event re-detected behind checkpoint: %s", l.spec.ID, l.spec.Signature, fev.Event) + continue + } + + if batch == nil { + batchNumber++ + batch = &eventStreamBatch{ + number: batchNumber, + timeout: time.NewTimer(time.Duration(*es.spec.BatchTimeout)), + checkpoints: make(map[fftypes.UUID]ffcapi.EventListenerCheckpoint), + } + } + if fev.Checkpoint != nil { + batch.checkpoints[*fev.Event.ListenerID] = fev.Checkpoint + } + log.L(es.bgCtx).Debugf("%s '%s' event confirmed: %s", l.spec.ID, l.spec.Signature, fev.Event) batch.events = append(batch.events, &ffcapi.EventWithContext{ StreamID: es.spec.ID, @@ -773,7 +786,7 @@ func (es *eventStream) performActionsWithRetry(startedState *startedStreamState, } } -func (es *eventStream) checkUpdateHWMCheckpoint(ctx context.Context, l *listener) *fftypes.JSONAny { +func (es *eventStream) checkUpdateHWMCheckpoint(ctx context.Context, l *listener) ffcapi.EventListenerCheckpoint { checkpoint := l.checkpoint @@ -797,7 +810,7 @@ func (es *eventStream) checkUpdateHWMCheckpoint(ctx context.Context, l *listener } es.mux.Lock() if l.checkpoint == checkpoint /* double check it hasn't changed */ { - checkpoint = &res.Checkpoint + checkpoint = res.Checkpoint l.checkpoint = checkpoint l.lastCheckpoint = fftypes.Now() } @@ -815,7 +828,7 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * cp := &apitypes.EventStreamCheckpoint{ StreamID: es.spec.ID, Time: fftypes.Now(), - Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), + Listeners: make(map[fftypes.UUID]ffcapi.EventListenerCheckpoint), } if batch != nil { for lID, lCP := range batch.checkpoints { diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index e28c882e..769c26d6 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -22,6 +22,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "sync" "testing" "time" @@ -41,6 +42,14 @@ import ( func strPtr(s string) *string { return &s } +type utCheckpointType struct { + SomeSequenceNumber int64 `json:"block"` +} + +func (cp *utCheckpointType) LessThan(b ffcapi.EventListenerCheckpoint) bool { + return cp.SomeSequenceNumber < b.(*utCheckpointType).SomeSequenceNumber +} + func testESConf(t *testing.T, j string) (spec *apitypes.EventStream) { err := json.Unmarshal([]byte(j), &spec) assert.NoError(t, err) @@ -324,7 +333,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { msp := es.persistence.(*persistencemocks.Persistence) msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { - return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" + return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].(*utCheckpointType).SomeSequenceNumber == 12345 })).Return(nil) senderChannel, _, receiverChannel := mockWSChannels(es.wsChannels.(*wsmocks.WebSocketChannels)) @@ -343,7 +352,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { r := <-started r.EventStream <- &ffcapi.ListenerEvent{ - Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, Event: &ffcapi.Event{ EventID: ffcapi.EventID{ ListenerID: l.ID, @@ -437,7 +446,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { msp := es.persistence.(*persistencemocks.Persistence) msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { - return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].JSONObject().GetString("cp1data") == "stuff" + return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].(*utCheckpointType).SomeSequenceNumber == 12345 })).Return(nil) err := es.Start(es.bgCtx) @@ -450,7 +459,7 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { r := <-started r.EventStream <- &ffcapi.ListenerEvent{ - Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, Event: &ffcapi.Event{ EventID: ffcapi.EventID{ ListenerID: l.ID, @@ -1013,7 +1022,7 @@ func TestResetListenerRestartFail(t *testing.T) { msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ StreamID: es.spec.ID, - Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), + Listeners: make(map[fftypes.UUID]ffcapi.EventListenerCheckpoint), }, nil) msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(nil) msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) @@ -1051,7 +1060,7 @@ func TestResetListenerWriteCheckpointFail(t *testing.T) { msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ StreamID: es.spec.ID, - Listeners: make(map[fftypes.UUID]*fftypes.JSONAny), + Listeners: make(map[fftypes.UUID]ffcapi.EventListenerCheckpoint), }, nil) msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) @@ -1130,7 +1139,7 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { r := <-started r.EventStream <- &ffcapi.ListenerEvent{ - Checkpoint: fftypes.JSONAnyPtr(`{"cp1data": "stuff"}`), + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, Event: &ffcapi.Event{ EventID: ffcapi.EventID{ ListenerID: l.ID, @@ -1362,7 +1371,7 @@ func TestEventLoopConfirmationsManagerBypass(t *testing.T) { ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) u1 := &ffcapi.ListenerEvent{ - Checkpoint: fftypes.JSONAnyPtr("{}"), + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, Event: &ffcapi.Event{ EventID: ffcapi.EventID{ ListenerID: fftypes.NewUUID(), @@ -1397,7 +1406,7 @@ func TestEventLoopConfirmationsManagerFail(t *testing.T) { ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) u1 := &ffcapi.ListenerEvent{ - Checkpoint: fftypes.JSONAnyPtr("{}"), + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, Event: &ffcapi.Event{ EventID: ffcapi.EventID{ ListenerID: fftypes.NewUUID(), @@ -1429,6 +1438,60 @@ func TestEventLoopIgnoreBadEvent(t *testing.T) { es.processNewEvent(context.Background(), &ffcapi.ListenerEvent{}) } +func TestSkipEventsBehindCheckpoint(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + batchLoopDone: make(chan struct{}), + action: func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { + assert.Len(t, events, 1) + assert.Equal(t, events[0].BlockNumber, uint64(2001)) + return nil + }, + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + listenerID := fftypes.NewUUID() + li := &listener{ + spec: &apitypes.Listener{ID: listenerID}, + checkpoint: &utCheckpointType{SomeSequenceNumber: 2000}, + } + es.listeners[*li.spec.ID] = li + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*li.spec.ID].(*utCheckpointType).SomeSequenceNumber == 2001 + })).Return(nil).Run(func(args mock.Arguments) { + ss.cancelCtx() + }) + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + es.batchLoop(ss) + wg.Done() + }() + es.batchChannel <- &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 1999}, // before checkpoint - redelivery + Event: &ffcapi.Event{EventID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 1999}}, + } + es.batchChannel <- &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 2000}, // on checkpoint - redelivery + Event: &ffcapi.Event{EventID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 2000}}, + } + es.batchChannel <- &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 2001}, // this is a new event + Event: &ffcapi.Event{EventID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 2001}}, + } + wg.Wait() + + msp.AssertExpectations(t) +} + func TestHWMCheckpointAfterInactivity(t *testing.T) { es := newTestEventStream(t, `{ @@ -1455,11 +1518,13 @@ func TestHWMCheckpointAfterInactivity(t *testing.T) { return req.StreamID.Equals(es.spec.ID) && req.ListenerID.Equals(li.spec.ID) })).Run(func(args mock.Arguments) { ss.cancelCtx() - }).Return(&ffcapi.EventListenerHWMResponse{Checkpoint: *fftypes.JSONAnyPtr(`{"cp1data":"stuff"}`)}, ffcapi.ErrorReason(""), nil) + }).Return(&ffcapi.EventListenerHWMResponse{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, + }, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { - return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*li.spec.ID].JSONObject().GetString("cp1data") == "stuff" + return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*li.spec.ID].(*utCheckpointType).SomeSequenceNumber == 12345 })).Return(nil) es.checkpointInterval = 1 * time.Microsecond diff --git a/internal/events/listener.go b/internal/events/listener.go index beaf8367..e0482fd6 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -26,7 +26,7 @@ type listener struct { es *eventStream spec *apitypes.Listener lastCheckpoint *fftypes.FFTime - checkpoint *fftypes.JSONAny + checkpoint ffcapi.EventListenerCheckpoint } func listenerSpecToOptions(spec *apitypes.Listener) ffcapi.EventListenerOptions { diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index 67f9d33e..b868f6f7 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -26,6 +26,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" "github.com/syndtr/goleveldb/leveldb/opt" ) @@ -62,6 +63,12 @@ func newTestLevelDBPersistence(t *testing.T) (*leveldbPersistence, func()) { func strPtr(s string) *string { return &s } +type badJSONCheckpointType map[bool]bool + +func (cp *badJSONCheckpointType) LessThan(b ffcapi.EventListenerCheckpoint) bool { + return false +} + func TestLevelDBInitMissingPath(t *testing.T) { tmconfig.Reset() @@ -298,8 +305,8 @@ func TestWriteCheckpointFail(t *testing.T) { id1 := apitypes.UUIDVersion1() err := p.WriteCheckpoint(context.Background(), &apitypes.EventStreamCheckpoint{ - Listeners: map[fftypes.UUID]*fftypes.JSONAny{ - *id1: fftypes.JSONAnyPtr(`{!!! bad json`), + Listeners: map[fftypes.UUID]ffcapi.EventListenerCheckpoint{ + *id1: &badJSONCheckpointType{false: true}, }, }) assert.Error(t, err) diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 3bf3d2bb..39e168ee 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -22,6 +22,7 @@ import ( "github.com/google/uuid" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) type DistributionMode = fftypes.FFEnum @@ -82,9 +83,9 @@ type EventStreamWithStatus struct { } type EventStreamCheckpoint struct { - StreamID *fftypes.UUID `json:"streamId"` - Time *fftypes.FFTime `json:"time"` - Listeners map[fftypes.UUID]*fftypes.JSONAny `json:"listeners"` + StreamID *fftypes.UUID `json:"streamId"` + Time *fftypes.FFTime `json:"time"` + Listeners map[fftypes.UUID]ffcapi.EventListenerCheckpoint `json:"listeners"` } type WebhookConfig struct { diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 7445a564..78bf5768 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -93,6 +93,12 @@ type Event struct { Info *fftypes.JSONAny `json:"info"` // additional blockchain specific information } +// EventListenerCheckpoint is the interface that a checkpoint must implement, basically to make it sortable. +// The checkpoint must also be JSON serializable +type EventListenerCheckpoint interface { + LessThan(b EventListenerCheckpoint) bool +} + // String is unique in all cases for an event, by combining the protocol ID with the listener ID and block hash func (eid *EventID) String() string { return fmt.Sprintf("%s/B=%s/L=%s", eid.ProtocolID(), eid.BlockHash, eid.ListenerID) @@ -132,9 +138,9 @@ type EventWithContext struct { // ListenerEvent is an event+checkpoint for a particular listener, and is the object delivered over the event stream channel when // a new event is detected for delivery to the confirmation manager. type ListenerEvent struct { - Checkpoint *fftypes.JSONAny `json:"checkpoint"` // the checkpoint information associated with the event, must be non-nil if the event is not removed - Event *Event `json:"event"` // the event - for removed events, can only have the EventID fields set (to generate the protocol ID) - Removed bool `json:"removed,omitempty"` // when true, this is an explicit cancellation of a previous event + Checkpoint EventListenerCheckpoint `json:"checkpoint"` // the checkpoint information associated with the event, must be non-nil if the event is not removed + Event *Event `json:"event"` // the event - for removed events, can only have the EventID fields set (to generate the protocol ID) + Removed bool `json:"removed,omitempty"` // when true, this is an explicit cancellation of a previous event } // ErrorReason are a set of standard error conditions that a blockchain connector can return diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go index 0ad2ee69..dea671d2 100644 --- a/pkg/ffcapi/event_listener_add.go +++ b/pkg/ffcapi/event_listener_add.go @@ -42,10 +42,10 @@ type EventListenerVerifyOptionsResponse struct { type EventListenerAddRequest struct { EventListenerOptions - ListenerID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event - StreamID *fftypes.UUID // The event stream (previously started) to which events should be delivered - Name string // Descriptive name of the listener, provided by the user, or defaulted to the signature. Not guaranteed to be unique. Should be included in the event info - Checkpoint *fftypes.JSONAny // The last persisted checkpoint for this event stream + ListenerID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event + StreamID *fftypes.UUID // The event stream (previously started) to which events should be delivered + Name string // Descriptive name of the listener, provided by the user, or defaulted to the signature. Not guaranteed to be unique. Should be included in the event info + Checkpoint EventListenerCheckpoint // The last persisted checkpoint for this event stream } type EventListenerAddResponse struct { diff --git a/pkg/ffcapi/event_listener_hwm.go b/pkg/ffcapi/event_listener_hwm.go index 2fdb2aa3..2a0c8d2f 100644 --- a/pkg/ffcapi/event_listener_hwm.go +++ b/pkg/ffcapi/event_listener_hwm.go @@ -26,5 +26,5 @@ type EventListenerHWMRequest struct { } type EventListenerHWMResponse struct { - Checkpoint fftypes.JSONAny `json:"checkpoint"` + Checkpoint EventListenerCheckpoint `json:"checkpoint"` } From be0a1b12f98e6788c6e89f998a12abc364b5939b Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sun, 10 Jul 2022 21:18:53 -0400 Subject: [PATCH 45/95] Add FFCAPI function for restore of checkpoint Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 21 ++-- internal/events/eventstream_test.go | 113 ++++++++++++++++-- internal/events/listener.go | 25 +++- .../persistence/leveldb_persistence_test.go | 5 +- mocks/ffcapimocks/api.go | 16 +++ pkg/apitypes/api_types.go | 7 +- pkg/ffcapi/api.go | 3 + pkg/fftm/stream_management_test.go | 6 + 8 files changed, 170 insertions(+), 26 deletions(-) diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index b065e2b3..9dbc55a5 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -18,6 +18,7 @@ package events import ( "context" + "encoding/json" "sync" "time" @@ -388,8 +389,8 @@ func (es *eventStream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID } } } else if new && startedState != nil { - // Start the new listener - return spec, l.start(startedState) + // Start the new listener - no checkpoint needed here + return spec, l.start(startedState, nil) } return spec, nil } @@ -483,11 +484,16 @@ func (es *eventStream) Start(ctx context.Context) error { es.currentState = startedState es.initAction(startedState) + cp, err := es.persistence.GetCheckpoint(ctx, es.spec.ID) + if err != nil { + return err + } + initialListeners := make([]*ffcapi.EventListenerAddRequest, 0) for _, l := range es.listeners { - initialListeners = append(initialListeners, l.buildAddRequest()) + initialListeners = append(initialListeners, l.buildAddRequest(ctx, cp)) } - _, _, err := es.connector.EventStreamStart(startedState.ctx, &ffcapi.EventStreamStartRequest{ + _, _, err = es.connector.EventStreamStart(startedState.ctx, &ffcapi.EventStreamStartRequest{ ID: es.spec.ID, EventStream: startedState.updates, StreamContext: startedState.ctx, @@ -828,7 +834,7 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * cp := &apitypes.EventStreamCheckpoint{ StreamID: es.spec.ID, Time: fftypes.Now(), - Listeners: make(map[fftypes.UUID]ffcapi.EventListenerCheckpoint), + Listeners: make(map[fftypes.UUID]json.RawMessage), } if batch != nil { for lID, lCP := range batch.checkpoints { @@ -841,7 +847,7 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * } staleCheckpoints := make([]*listener, 0) for lID, l := range es.listeners { - cp.Listeners[lID] = l.checkpoint + cp.Listeners[lID], _ = json.Marshal(l.checkpoint) if l.checkpoint == nil || l.lastCheckpoint == nil || time.Since(*l.lastCheckpoint.Time()) > es.checkpointInterval { staleCheckpoints = append(staleCheckpoints, l) } @@ -850,7 +856,8 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * // Ask the connector for any updated high watermark checkpoints - checking we don't have any in-flight confirmations for _, l := range staleCheckpoints { - cp.Listeners[*l.spec.ID] = es.checkUpdateHWMCheckpoint(startedState.ctx, l) + cpb, _ := json.Marshal(es.checkUpdateHWMCheckpoint(startedState.ctx, l)) + cp.Listeners[*l.spec.ID] = cpb } // We only return if the context is cancelled, or the checkpoint succeeds diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 769c26d6..25104bdf 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -17,6 +17,7 @@ package events import ( + "bytes" "context" "encoding/json" "fmt" @@ -43,7 +44,7 @@ import ( func strPtr(s string) *string { return &s } type utCheckpointType struct { - SomeSequenceNumber int64 `json:"block"` + SomeSequenceNumber int64 `json:"someSequenceNumber"` } func (cp *utCheckpointType) LessThan(b ffcapi.EventListenerCheckpoint) bool { @@ -73,6 +74,7 @@ func newTestEventStreamWithListener(t *testing.T, mfc *ffcapimocks.API, conf str &wsmocks.WebSocketChannels{}, listeners, ) + mfc.On("EventStreamNewCheckpointStruct").Return(&utCheckpointType{}).Maybe() if err != nil { return nil, err } @@ -325,6 +327,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { "option1":"value1", "option2":"value2" }`, r.InitialListeners[0].Options.String()) + assert.Equal(t, int64(12000), r.InitialListeners[0].Checkpoint.(*utCheckpointType).SomeSequenceNumber) }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { @@ -332,8 +335,15 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(&apitypes.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Time: fftypes.Now(), + Listeners: map[fftypes.UUID]json.RawMessage{ + *l.ID: []byte(`{"someSequenceNumber":12000}`), + }, + }, nil) // existing checkpoint msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { - return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].(*utCheckpointType).SomeSequenceNumber == 12345 + return cp.StreamID.Equals(es.spec.ID) && string(cp.Listeners[*l.ID]) == `{"someSequenceNumber":12345}` })).Return(nil) senderChannel, _, receiverChannel := mockWSChannels(es.wsChannels.(*wsmocks.WebSocketChannels)) @@ -379,6 +389,62 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { mfc.AssertExpectations(t) } +func TestStartEventStreamCheckpointReadFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("pop")) + + err := es.Start(es.bgCtx) + assert.Regexp(t, "pop", err) +} + +func TestStartEventStreamCheckpointInvalid(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: apitypes.UUIDVersion1(), + Name: strPtr("ut_listener"), + Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), + FromBlock: strPtr("12345"), + } + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(&apitypes.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Time: fftypes.Now(), + Listeners: map[fftypes.UUID]json.RawMessage{ + *l.ID: []byte(`{"bad": JSON!`), + }, + }, nil) + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{ + ResolvedSignature: "EventSig(uint256)", + ResolvedOptions: *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), + }, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventStreamStartRequest) bool { + return req.InitialListeners[0].Checkpoint == nil + })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + mfc.AssertExpectations(t) +} + func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { receivedWebhook := make(chan []*ffcapi.EventWithContext, 1) @@ -445,8 +511,9 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { - return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*l.ID].(*utCheckpointType).SomeSequenceNumber == 12345 + return cp.StreamID.Equals(es.spec.ID) && bytes.Equal(cp.Listeners[*l.ID], json.RawMessage(`{"someSequenceNumber":12345}`)) })).Return(nil) err := es.Start(es.bgCtx) @@ -574,6 +641,9 @@ func TestUpdateStreamStarted(t *testing.T) { return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) @@ -635,6 +705,9 @@ func TestAddRemoveListener(t *testing.T) { return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) @@ -757,6 +830,9 @@ func TestUpdateListenerFail(t *testing.T) { return r.ListenerID.Equals(l1.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + _, err := es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) assert.NoError(t, err) @@ -838,6 +914,9 @@ func TestUpdateStreamRestartFail(t *testing.T) { return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + err := es.Start(es.bgCtx) assert.NoError(t, err) @@ -891,6 +970,9 @@ func TestUpdateAttemptChangeSignature(t *testing.T) { return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) @@ -965,6 +1047,9 @@ func TestUpdateStreamStopFail(t *testing.T) { return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) assert.NoError(t, err) @@ -1022,7 +1107,7 @@ func TestResetListenerRestartFail(t *testing.T) { msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ StreamID: es.spec.ID, - Listeners: make(map[fftypes.UUID]ffcapi.EventListenerCheckpoint), + Listeners: make(map[fftypes.UUID]json.RawMessage), }, nil) msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(nil) msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) @@ -1060,7 +1145,7 @@ func TestResetListenerWriteCheckpointFail(t *testing.T) { msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ StreamID: es.spec.ID, - Listeners: make(map[fftypes.UUID]ffcapi.EventListenerCheckpoint), + Listeners: make(map[fftypes.UUID]json.RawMessage), }, nil) msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) @@ -1127,6 +1212,7 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { first = false } }) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint _, broadcastChannel, _ := mockWSChannels(es.wsChannels.(*wsmocks.WebSocketChannels)) @@ -1174,6 +1260,9 @@ func TestActionRetryOk(t *testing.T) { return r.ID.Equals(es.spec.ID) })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + err := es.Start(es.bgCtx) assert.NoError(t, err) @@ -1219,6 +1308,9 @@ func TestActionRetrySkip(t *testing.T) { return r.ID.Equals(es.spec.ID) })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + err := es.Start(es.bgCtx) assert.NoError(t, err) @@ -1255,6 +1347,9 @@ func TestActionRetryBlock(t *testing.T) { return r.ID.Equals(es.spec.ID) })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + err := es.Start(es.bgCtx) assert.NoError(t, err) @@ -1464,7 +1559,7 @@ func TestSkipEventsBehindCheckpoint(t *testing.T) { msp := es.persistence.(*persistencemocks.Persistence) msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { - return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*li.spec.ID].(*utCheckpointType).SomeSequenceNumber == 2001 + return cp.StreamID.Equals(es.spec.ID) && bytes.Equal(cp.Listeners[*li.spec.ID], json.RawMessage(`{"someSequenceNumber":2001}`)) })).Return(nil).Run(func(args mock.Arguments) { ss.cancelCtx() }) @@ -1524,7 +1619,7 @@ func TestHWMCheckpointAfterInactivity(t *testing.T) { msp := es.persistence.(*persistencemocks.Persistence) msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { - return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*li.spec.ID].(*utCheckpointType).SomeSequenceNumber == 12345 + return cp.StreamID.Equals(es.spec.ID) && bytes.Equal(cp.Listeners[*li.spec.ID], json.RawMessage(`{"someSequenceNumber":12345}`)) })).Return(nil) es.checkpointInterval = 1 * time.Microsecond @@ -1561,7 +1656,7 @@ func TestHWMCheckpointInFlightSkip(t *testing.T) { msp := es.persistence.(*persistencemocks.Persistence) msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { - return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*li.spec.ID] == nil + return cp.StreamID.Equals(es.spec.ID) && string(cp.Listeners[*li.spec.ID]) == `null` })).Return(nil) es.checkpointInterval = 1 * time.Microsecond @@ -1602,7 +1697,7 @@ func TestHWMCheckpointFail(t *testing.T) { msp := es.persistence.(*persistencemocks.Persistence) msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { - return cp.StreamID.Equals(es.spec.ID) && cp.Listeners[*li.spec.ID] == nil + return cp.StreamID.Equals(es.spec.ID) && string(cp.Listeners[*li.spec.ID]) == `null` })).Return(nil) es.checkpointInterval = 1 * time.Microsecond diff --git a/internal/events/listener.go b/internal/events/listener.go index e0482fd6..6e4aa75f 100644 --- a/internal/events/listener.go +++ b/internal/events/listener.go @@ -17,7 +17,11 @@ package events import ( + "context" + "encoding/json" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -45,16 +49,29 @@ func (l *listener) stop(startedState *startedStreamState) error { return err } -func (l *listener) buildAddRequest() *ffcapi.EventListenerAddRequest { - return &ffcapi.EventListenerAddRequest{ +func (l *listener) buildAddRequest(ctx context.Context, cp *apitypes.EventStreamCheckpoint) *ffcapi.EventListenerAddRequest { + req := &ffcapi.EventListenerAddRequest{ EventListenerOptions: listenerSpecToOptions(l.spec), Name: *l.spec.Name, ListenerID: l.spec.ID, StreamID: l.spec.StreamID, } + if cp != nil { + jsonCP := cp.Listeners[*l.spec.ID] + if jsonCP != nil { + listenerCheckpoint := l.es.connector.EventStreamNewCheckpointStruct() + err := json.Unmarshal(jsonCP, &listenerCheckpoint) + if err != nil { + log.L(ctx).Errorf("Failed to restore checkpoint for listener '%s': %s", l.spec.ID, err) + } else { + req.Checkpoint = listenerCheckpoint + } + } + } + return req } -func (l *listener) start(startedState *startedStreamState) error { - _, _, err := l.es.connector.EventListenerAdd(startedState.ctx, l.buildAddRequest()) +func (l *listener) start(startedState *startedStreamState, cp *apitypes.EventStreamCheckpoint) error { + _, _, err := l.es.connector.EventListenerAdd(startedState.ctx, l.buildAddRequest(startedState.ctx, cp)) return err } diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index b868f6f7..2699970f 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -18,6 +18,7 @@ package persistence import ( "context" + "encoding/json" "io/ioutil" "os" "testing" @@ -305,8 +306,8 @@ func TestWriteCheckpointFail(t *testing.T) { id1 := apitypes.UUIDVersion1() err := p.WriteCheckpoint(context.Background(), &apitypes.EventStreamCheckpoint{ - Listeners: map[fftypes.UUID]ffcapi.EventListenerCheckpoint{ - *id1: &badJSONCheckpointType{false: true}, + Listeners: map[fftypes.UUID]json.RawMessage{ + *id1: json.RawMessage([]byte(`{"bad": "json"!`)), }, }) assert.Error(t, err) diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index e2bf7ff1..32ff1a85 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -194,6 +194,22 @@ func (_m *API) EventListenerVerifyOptions(ctx context.Context, req *ffcapi.Event return r0, r1, r2 } +// EventStreamNewCheckpointStruct provides a mock function with given fields: +func (_m *API) EventStreamNewCheckpointStruct() ffcapi.EventListenerCheckpoint { + ret := _m.Called() + + var r0 ffcapi.EventListenerCheckpoint + if rf, ok := ret.Get(0).(func() ffcapi.EventListenerCheckpoint); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ffcapi.EventListenerCheckpoint) + } + } + + return r0 +} + // EventStreamStart provides a mock function with given fields: ctx, req func (_m *API) EventStreamStart(ctx context.Context, req *ffcapi.EventStreamStartRequest) (*ffcapi.EventStreamStartResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 39e168ee..748b256e 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -22,7 +22,6 @@ import ( "github.com/google/uuid" "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) type DistributionMode = fftypes.FFEnum @@ -83,9 +82,9 @@ type EventStreamWithStatus struct { } type EventStreamCheckpoint struct { - StreamID *fftypes.UUID `json:"streamId"` - Time *fftypes.FFTime `json:"time"` - Listeners map[fftypes.UUID]ffcapi.EventListenerCheckpoint `json:"listeners"` + StreamID *fftypes.UUID `json:"streamId"` + Time *fftypes.FFTime `json:"time"` + Listeners map[fftypes.UUID]json.RawMessage `json:"listeners"` } type WebhookConfig struct { diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 78bf5768..238d8e45 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -67,6 +67,9 @@ type API interface { // EventListenerHWM queries the current high water mark checkpoint for a listener. Called at regular intervals when there are no events in flight for a listener, to ensure checkpoint are written regularly even when there is no activity EventListenerHWM(ctx context.Context, req *EventListenerHWMRequest) (*EventListenerHWMResponse, ErrorReason, error) + + // EventStreamNewCheckpointStruct used during checkpoint restore, to get the specific into which to restore the JSON bytes + EventStreamNewCheckpointStruct() EventListenerCheckpoint } type BlockHashEvent struct { diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go index a703ebac..26c34080 100644 --- a/pkg/fftm/stream_management_test.go +++ b/pkg/fftm/stream_management_test.go @@ -243,6 +243,7 @@ func TestCreateRenameStreamNameReservation(t *testing.T) { mp.On("WriteStream", m.ctx, mock.Anything).Return(fmt.Errorf("temporary")).Once() mp.On("DeleteCheckpoint", m.ctx, mock.Anything).Return(fmt.Errorf("temporary")).Once() mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) // Reject missing name _, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{}) @@ -315,6 +316,7 @@ func TestCreateOrUpdateListenerFail(t *testing.T) { mp, mfc, m := newMockPersistenceManager(t) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) @@ -333,6 +335,7 @@ func TestCreateOrUpdateListenerWriteFail(t *testing.T) { mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) mp.On("WriteListener", m.ctx, mock.Anything).Return(fmt.Errorf("pop")) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) @@ -373,6 +376,7 @@ func TestDeleteListenerFail(t *testing.T) { mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) mp.On("WriteListener", m.ctx, mock.Anything).Return(nil) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) @@ -414,6 +418,7 @@ func TestUpdateStreamBadChanges(t *testing.T) { mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) @@ -429,6 +434,7 @@ func TestUpdateStreamWriteFail(t *testing.T) { mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil).Once() mp.On("WriteStream", m.ctx, mock.Anything).Return(fmt.Errorf("pop")) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) From 8ade5d62b15e563dcdf5089a03200ccb79193c78 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sun, 10 Jul 2022 21:39:17 -0400 Subject: [PATCH 46/95] Consistently "make reference" rather than "make docs" Signed-off-by: Peter Broadhurst --- Makefile | 2 +- pkg/fftm/config_docs_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 732e49d1..09bad3a6 100644 --- a/Makefile +++ b/Makefile @@ -47,5 +47,5 @@ clean: deps: $(VGO) get ./internal/... ./pkg/... $(VGO) get -t ./internal/... ./pkg/... -docs: +reference: $(VGO) test ./pkg/fftm -timeout=10s -tags docs diff --git a/pkg/fftm/config_docs_test.go b/pkg/fftm/config_docs_test.go index 8b3fb763..4299413d 100644 --- a/pkg/fftm/config_docs_test.go +++ b/pkg/fftm/config_docs_test.go @@ -42,5 +42,5 @@ func TestConfigDocsUpToDate(t *testing.T) { generatedConfigHash.Write(generatedConfig) configOnDiskHash := sha1.New() configOnDiskHash.Write(configOnDisk) - assert.Equal(t, configOnDiskHash.Sum(nil), generatedConfigHash.Sum(nil), "The config reference docs generated by the code did not match the config.md file in git. Did you forget to run `make docs`?") + assert.Equal(t, configOnDiskHash.Sum(nil), generatedConfigHash.Sum(nil), "The config reference docs generated by the code did not match the config.md file in git. Did you forget to run `make reference`?") } From 41864b2cad5bc55380623f91fe010608c4c96800 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 11 Jul 2022 09:05:25 -0400 Subject: [PATCH 47/95] Explicit stop API for EventStream to clean up state in connector Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 23 ++-- internal/events/eventstream_test.go | 120 ++++++++++++------ mocks/ffcapimocks/api.go | 30 +++++ pkg/ffcapi/api.go | 3 + pkg/ffcapi/event_stream_stopped.go | 28 ++++ .../route_delete_eventstream_listener_test.go | 1 + pkg/fftm/route_delete_eventstream_test.go | 1 + pkg/fftm/route_delete_subscription_test.go | 1 + .../route_get_eventstream_listener_test.go | 1 + .../route_get_eventstream_listeners_test.go | 1 + pkg/fftm/route_get_eventstream_test.go | 1 + pkg/fftm/route_get_eventstreams_test.go | 1 + pkg/fftm/route_get_subscription_test.go | 1 + pkg/fftm/route_get_subscriptions_test.go | 1 + .../route_patch_eventstream_listener_test.go | 1 + pkg/fftm/route_patch_eventstream_test.go | 1 + pkg/fftm/route_patch_subscription_test.go | 1 + ...te_post_eventstream_listener_reset_test.go | 1 + .../route_post_eventstream_listeners_test.go | 1 + .../route_post_eventstream_resume_test.go | 1 + .../route_post_eventstream_suspend_test.go | 1 + pkg/fftm/route_post_eventstream_test.go | 1 + .../route_post_subscription_reset_test.go | 1 + pkg/fftm/route_post_subscriptions_test.go | 1 + pkg/fftm/stream_management_test.go | 3 +- 25 files changed, 180 insertions(+), 46 deletions(-) create mode 100644 pkg/ffcapi/event_stream_stopped.go diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 9dbc55a5..3fa47ae7 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -518,8 +518,12 @@ func (es *eventStream) Start(ctx context.Context) error { func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, error) { es.mux.Lock() - startedState := es.currentState defer es.mux.Unlock() + startedState := es.currentState + if es.status == apitypes.EventStreamStatusStopping { + // Already stopping, just return + return startedState, nil + } if err := es.checkSetStatus(ctx, apitypes.EventStreamStatusStarted, apitypes.EventStreamStatusStopping); err != nil { return nil, err } @@ -528,14 +532,6 @@ func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, er // Cancel the context, stop stop the event loop, and shut down the action (WebSockets in particular) startedState.cancelCtx() - // Stop all the listeners - we hold the lock during this - for _, l := range es.listeners { - err := l.stop(startedState) - if err != nil { - _ = es.checkSetStatus(ctx, apitypes.EventStreamStatusStopping, apitypes.EventStreamStatusStarted) // restore started status - return nil, err - } - } return startedState, nil } @@ -553,6 +549,15 @@ func (es *eventStream) Stop(ctx context.Context) error { return err } + // Inform the connector explicitly of the stream stop (it should be shutting down all it's listeners anyway + // due to the cancelled context) + if _, _, err = es.connector.EventStreamStopped(ctx, &ffcapi.EventStreamStoppedRequest{ + ID: es.spec.ID, + }); err != nil { + log.L(ctx).Errorf("Connector returned error when notified of stopped stream: %s", err) + return err + } + // Stop the confirmations manager es.confirmations.Stop() diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 25104bdf..4a98e9a2 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -330,9 +330,9 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { assert.Equal(t, int64(12000), r.InitialListeners[0].Checkpoint.(*utCheckpointType).SomeSequenceNumber) }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ListenerID.Equals(l.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(&apitypes.EventStreamCheckpoint{ @@ -432,7 +432,9 @@ func TestStartEventStreamCheckpointInvalid(t *testing.T) { mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventStreamStartRequest) bool { return req.InitialListeners[0].Checkpoint == nil })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) @@ -506,9 +508,9 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { }`, r.Options.String()) }).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ListenerID.Equals(l.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint @@ -637,9 +639,9 @@ func TestUpdateStreamStarted(t *testing.T) { started <- r }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ListenerID.Equals(l.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint @@ -705,6 +707,10 @@ func TestAddRemoveListener(t *testing.T) { return r.ListenerID.Equals(l.ID) })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint @@ -758,9 +764,9 @@ func TestUpdateListenerAndDeleteStarted(t *testing.T) { return r.ListenerID.Equals(l1.ID) })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ListenerID.Equals(l1.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Twice() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(nil, nil) @@ -825,10 +831,12 @@ func TestUpdateListenerFail(t *testing.T) { started <- r }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() - mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ListenerID.Equals(l1.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint @@ -910,9 +918,9 @@ func TestUpdateStreamRestartFail(t *testing.T) { mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil).Once() - mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ListenerID.Equals(l.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint @@ -966,9 +974,9 @@ func TestUpdateAttemptChangeSignature(t *testing.T) { started <- args[1].(*ffcapi.EventStreamStartRequest) }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() - mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ListenerID.Equals(l.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint @@ -1021,8 +1029,8 @@ func TestAttemptResetNonExistentListener(t *testing.T) { func TestUpdateStreamStopFail(t *testing.T) { es := newTestEventStream(t, `{ - "name": "ut_stream" - }`) + "name": "ut_stream" + }`) l := &apitypes.Listener{ ID: fftypes.NewUUID(), @@ -1041,11 +1049,12 @@ func TestUpdateStreamStopFail(t *testing.T) { started <- args[1].(*ffcapi.EventStreamStartRequest) }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() - mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Twice() - - mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ListenerID.Equals(l.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Twice() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint @@ -1059,8 +1068,8 @@ func TestUpdateStreamStopFail(t *testing.T) { r := <-started defChanged := testESConf(t, `{ - "name": "ut_stream2" - }`) + "name": "ut_stream2" + }`) err = es.UpdateSpec(context.Background(), defChanged) assert.Regexp(t, "FF21031.*pop", err) @@ -1100,9 +1109,9 @@ func TestResetListenerRestartFail(t *testing.T) { mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() - mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { - return r.ListenerID.Equals(l.ID) - })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ @@ -1196,7 +1205,9 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { })).Run(func(args mock.Arguments) { started <- args[1].(*ffcapi.EventStreamStartRequest) }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() - mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) first := true done := make(chan struct{}) @@ -1259,6 +1270,9 @@ func TestActionRetryOk(t *testing.T) { mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { return r.ID.Equals(es.spec.ID) })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint @@ -1307,6 +1321,9 @@ func TestActionRetrySkip(t *testing.T) { mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { return r.ID.Equals(es.spec.ID) })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint @@ -1346,6 +1363,9 @@ func TestActionRetryBlock(t *testing.T) { mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { return r.ID.Equals(es.spec.ID) })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) msp := es.persistence.(*persistencemocks.Persistence) msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint @@ -1381,6 +1401,34 @@ func TestActionRetryBlock(t *testing.T) { assert.Greater(t, callCount, 0) } +func TestDeleteFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "errorHandling": "block", + "blockedRetryDelay": "0s", + "retryTimeout": "0s" + }`) + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + err = es.Delete(es.bgCtx) + assert.Regexp(t, "pop", err) + +} + func TestEventLoopProcessRemovedEvent(t *testing.T) { es := newTestEventStream(t, `{ diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index 32ff1a85..5a1ff9c0 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -240,6 +240,36 @@ func (_m *API) EventStreamStart(ctx context.Context, req *ffcapi.EventStreamStar return r0, r1, r2 } +// EventStreamStopped provides a mock function with given fields: ctx, req +func (_m *API) EventStreamStopped(ctx context.Context, req *ffcapi.EventStreamStoppedRequest) (*ffcapi.EventStreamStoppedResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.EventStreamStoppedResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventStreamStoppedRequest) *ffcapi.EventStreamStoppedResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.EventStreamStoppedResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventStreamStoppedRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventStreamStoppedRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // GasPriceEstimate provides a mock function with given fields: ctx, req func (_m *API) GasPriceEstimate(ctx context.Context, req *ffcapi.GasPriceEstimateRequest) (*ffcapi.GasPriceEstimateResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 238d8e45..08353fee 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -56,6 +56,9 @@ type API interface { // EventStreamStart starts an event stream with an initial set of listeners (which might be empty), a channel to deliver events, and a context that will close to stop the stream EventStreamStart(ctx context.Context, req *EventStreamStartRequest) (*EventStreamStartResponse, ErrorReason, error) + // EventStreamStopped informs a connector that an event stream has been requested to stop, and the context has been cancelled. So the state associated with it can be removed (and a future start of the same ID can be performed) + EventStreamStopped(ctx context.Context, req *EventStreamStoppedRequest) (*EventStreamStoppedResponse, ErrorReason, error) + // EventListenerVerifyOptions validates the configuration options for a listener, applying any defaults needed by the connector, and returning the update options for FFTM to persist EventListenerVerifyOptions(ctx context.Context, req *EventListenerVerifyOptionsRequest) (*EventListenerVerifyOptionsResponse, ErrorReason, error) diff --git a/pkg/ffcapi/event_stream_stopped.go b/pkg/ffcapi/event_stream_stopped.go new file mode 100644 index 00000000..1165e80b --- /dev/null +++ b/pkg/ffcapi/event_stream_stopped.go @@ -0,0 +1,28 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type EventStreamStoppedRequest struct { + ID *fftypes.UUID // UUID of the stream, which we be referenced in any future add/remove listener requests +} + +type EventStreamStoppedResponse struct { +} diff --git a/pkg/fftm/route_delete_eventstream_listener_test.go b/pkg/fftm/route_delete_eventstream_listener_test.go index 27dbf885..572051f4 100644 --- a/pkg/fftm/route_delete_eventstream_listener_test.go +++ b/pkg/fftm/route_delete_eventstream_listener_test.go @@ -42,6 +42,7 @@ func TestDeleteEventStreamListener(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/route_delete_eventstream_test.go b/pkg/fftm/route_delete_eventstream_test.go index 50b33def..d4e45d0a 100644 --- a/pkg/fftm/route_delete_eventstream_test.go +++ b/pkg/fftm/route_delete_eventstream_test.go @@ -35,6 +35,7 @@ func TestDeleteEventStream(t *testing.T) { mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_delete_subscription_test.go b/pkg/fftm/route_delete_subscription_test.go index 41694d95..fdc4fa96 100644 --- a/pkg/fftm/route_delete_subscription_test.go +++ b/pkg/fftm/route_delete_subscription_test.go @@ -41,6 +41,7 @@ func TestDeleteSubscription(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/route_get_eventstream_listener_test.go b/pkg/fftm/route_get_eventstream_listener_test.go index b5fce9f5..1355b163 100644 --- a/pkg/fftm/route_get_eventstream_listener_test.go +++ b/pkg/fftm/route_get_eventstream_listener_test.go @@ -42,6 +42,7 @@ func TestGetEventStreamsListener(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/route_get_eventstream_listeners_test.go b/pkg/fftm/route_get_eventstream_listeners_test.go index ccecf800..6eca9d61 100644 --- a/pkg/fftm/route_get_eventstream_listeners_test.go +++ b/pkg/fftm/route_get_eventstream_listeners_test.go @@ -42,6 +42,7 @@ func TestGetEventStreamListeners(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1, es2 apitypes.EventStream diff --git a/pkg/fftm/route_get_eventstream_test.go b/pkg/fftm/route_get_eventstream_test.go index ebca73eb..d27d8fdc 100644 --- a/pkg/fftm/route_get_eventstream_test.go +++ b/pkg/fftm/route_get_eventstream_test.go @@ -35,6 +35,7 @@ func TestGetEventStream(t *testing.T) { mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_get_eventstreams_test.go b/pkg/fftm/route_get_eventstreams_test.go index c57fd9f8..ac57db16 100644 --- a/pkg/fftm/route_get_eventstreams_test.go +++ b/pkg/fftm/route_get_eventstreams_test.go @@ -35,6 +35,7 @@ func TestGetEventStreams(t *testing.T) { mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_get_subscription_test.go b/pkg/fftm/route_get_subscription_test.go index bef5cf26..e3cfc467 100644 --- a/pkg/fftm/route_get_subscription_test.go +++ b/pkg/fftm/route_get_subscription_test.go @@ -41,6 +41,7 @@ func TestGetSubscription(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/route_get_subscriptions_test.go b/pkg/fftm/route_get_subscriptions_test.go index 8944de1b..7d742dc3 100644 --- a/pkg/fftm/route_get_subscriptions_test.go +++ b/pkg/fftm/route_get_subscriptions_test.go @@ -41,6 +41,7 @@ func TestGetSubscriptions(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/route_patch_eventstream_listener_test.go b/pkg/fftm/route_patch_eventstream_listener_test.go index 3053ccac..8ffd77fc 100644 --- a/pkg/fftm/route_patch_eventstream_listener_test.go +++ b/pkg/fftm/route_patch_eventstream_listener_test.go @@ -42,6 +42,7 @@ func TestPatchEventStreamListener(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/route_patch_eventstream_test.go b/pkg/fftm/route_patch_eventstream_test.go index cb184ef9..5703b41a 100644 --- a/pkg/fftm/route_patch_eventstream_test.go +++ b/pkg/fftm/route_patch_eventstream_test.go @@ -35,6 +35,7 @@ func TestPatchEventStream(t *testing.T) { mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_patch_subscription_test.go b/pkg/fftm/route_patch_subscription_test.go index 0e0ff95c..e731bdf0 100644 --- a/pkg/fftm/route_patch_subscription_test.go +++ b/pkg/fftm/route_patch_subscription_test.go @@ -41,6 +41,7 @@ func TestPatchSubscription(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/route_post_eventstream_listener_reset_test.go b/pkg/fftm/route_post_eventstream_listener_reset_test.go index 07c4ca27..7fe9a2a9 100644 --- a/pkg/fftm/route_post_eventstream_listener_reset_test.go +++ b/pkg/fftm/route_post_eventstream_listener_reset_test.go @@ -42,6 +42,7 @@ func TestPostEventStreamListenerReset(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/route_post_eventstream_listeners_test.go b/pkg/fftm/route_post_eventstream_listeners_test.go index 0406151b..8d497a24 100644 --- a/pkg/fftm/route_post_eventstream_listeners_test.go +++ b/pkg/fftm/route_post_eventstream_listeners_test.go @@ -42,6 +42,7 @@ func TestPostEventStreamListeners(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/route_post_eventstream_resume_test.go b/pkg/fftm/route_post_eventstream_resume_test.go index a80963ab..980d5351 100644 --- a/pkg/fftm/route_post_eventstream_resume_test.go +++ b/pkg/fftm/route_post_eventstream_resume_test.go @@ -35,6 +35,7 @@ func TestPostEventStreamResume(t *testing.T) { mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_post_eventstream_suspend_test.go b/pkg/fftm/route_post_eventstream_suspend_test.go index 9d241a3c..9067ae23 100644 --- a/pkg/fftm/route_post_eventstream_suspend_test.go +++ b/pkg/fftm/route_post_eventstream_suspend_test.go @@ -35,6 +35,7 @@ func TestPostEventStreamSuspend(t *testing.T) { mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_post_eventstream_test.go b/pkg/fftm/route_post_eventstream_test.go index 66befe0a..a16f63e9 100644 --- a/pkg/fftm/route_post_eventstream_test.go +++ b/pkg/fftm/route_post_eventstream_test.go @@ -35,6 +35,7 @@ func TestPostNewEventStream(t *testing.T) { mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_post_subscription_reset_test.go b/pkg/fftm/route_post_subscription_reset_test.go index 501efa03..84c064ce 100644 --- a/pkg/fftm/route_post_subscription_reset_test.go +++ b/pkg/fftm/route_post_subscription_reset_test.go @@ -41,6 +41,7 @@ func TestPostSubscriptionReset(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/route_post_subscriptions_test.go b/pkg/fftm/route_post_subscriptions_test.go index 84629efa..1bb32a9f 100644 --- a/pkg/fftm/route_post_subscriptions_test.go +++ b/pkg/fftm/route_post_subscriptions_test.go @@ -41,6 +41,7 @@ func TestPostSubscriptions(t *testing.T) { mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() // Create a stream var es1 apitypes.EventStream diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go index 26c34080..1a0a6091 100644 --- a/pkg/fftm/stream_management_test.go +++ b/pkg/fftm/stream_management_test.go @@ -38,6 +38,7 @@ func TestRestoreStreamsAndListenersOK(t *testing.T) { mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() falsy := false @@ -146,7 +147,7 @@ func TestDeleteStartedListener(t *testing.T) { mfc := m.connector.(*ffcapimocks.API) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) - mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() falsy := false es1 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr("stream1"), Suspended: &falsy} From f296909282dd9da7fd0a8fee8ffe7586694d7bcc Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 13 Jul 2022 09:23:33 -0400 Subject: [PATCH 48/95] Tweaks - including translated errors for persistence Signed-off-by: Peter Broadhurst --- internal/events/blocklistener.go | 8 ++++---- internal/persistence/leveldb_persistence.go | 16 ++++++++-------- internal/tmmsgs/en_error_messges.go | 5 +++++ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/internal/events/blocklistener.go b/internal/events/blocklistener.go index d33b93cb..4840deee 100644 --- a/internal/events/blocklistener.go +++ b/internal/events/blocklistener.go @@ -50,18 +50,18 @@ func (es *eventStream) blockListener(startedState *startedStreamState) { } } else { select { - case blockUpdate := <-startedState.blocks: - log.L(startedState.ctx).Debugf("Received block event: %v", blockUpdate.BlockHashes) + case update := <-startedState.blocks: + log.L(startedState.ctx).Debugf("Received block event: %v", update.BlockHashes) // Nothing to do unless we have confirmations turned on if es.confirmations != nil { select { - case es.confirmations.NewBlockHashes() <- blockUpdate: + case es.confirmations.NewBlockHashes() <- update: // all good, we passed it on default: // we can't deliver it immediately, we switch to blocked mode log.L(startedState.ctx).Infof("Event stream block-listener became blocked") // Take a copy of the block update, so we can modify (to mark a gap) without affecting other streams - var bu = *blockUpdate + var bu = *update blockedUpdate = &bu } } diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index a324cfd7..f8022f87 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -49,7 +49,7 @@ func NewLevelDBPersistence(ctx context.Context) (Persistence, error) { OpenFilesCacheCapacity: config.GetInt(tmconfig.PersistenceLevelDBMaxHandles), }) if err != nil { - return nil, err + return nil, i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceInitFailed, dbPath) } return &leveldbPersistence{ db: db, @@ -69,10 +69,10 @@ func (p *leveldbPersistence) listenerKey(listenerID *fftypes.UUID) []byte { return []byte(fmt.Sprintf("listeners/%s", listenerID)) } -func (p *leveldbPersistence) writeJSON(ctx context.Context, key []byte, target interface{}) error { - b, err := json.Marshal(target) +func (p *leveldbPersistence) writeJSON(ctx context.Context, key []byte, value interface{}) error { + b, err := json.Marshal(value) if err != nil { - return err + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceMarshalFailed) } log.L(ctx).Debugf("Wrote %s", key) return p.db.Put(key, b, &opt.WriteOptions{Sync: p.syncWrites}) @@ -84,11 +84,11 @@ func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target in if err == leveldb.ErrNotFound { return nil } - return err + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceReadFailed) } err = json.Unmarshal(b, target) if err != nil { - return err + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceUnmarshalFailed) } log.L(ctx).Debugf("Read %s", key) return nil @@ -119,7 +119,7 @@ itLoop: v := val() err := json.Unmarshal(it.Value(), v) if err != nil { - return err + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceUnmarshalFailed) } for _, f := range filters { if !f(v) { @@ -140,7 +140,7 @@ func (p *leveldbPersistence) deleteKeys(ctx context.Context, keys ...[]byte) err for _, key := range keys { err := p.db.Delete(key, &opt.WriteOptions{Sync: p.syncWrites}) if err != nil { - return err + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceDeleteFailed) } log.L(ctx).Debugf("Deleted %s", key) } diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 1cf6b370..93976e92 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -67,4 +67,9 @@ var ( MsgLevelDBPathMissing = ffe("FF21050", "Path must be supplied for LevelDB persistence") MsgFilterUpdateNotAllowed = ffe("FF21051", "Event filters cannot be updated after a listener is created. Previous signature: '%s'. New signature: '%s'") MsgResetStreamNotFound = ffe("FF21052", "Attempt to reset listener '%s', which is not currently registered on stream '%s'", 404) + MsgPersistenceMarshalFailed = ffe("FF21053", "JSON serialization failed while writing to persistence") + MsgPersistenceUnmarshalFailed = ffe("FF21054", "JSON parsing failed while reading from persistence") + MsgPersistenceReadFailed = ffe("FF21055", "Failed to read key '%s' from persistence") + MsgPersistenceDeleteFailed = ffe("FF21056", "Failed to delete key '%s' from persistence") + MsgPersistenceInitFailed = ffe("FF21057", "Failed to initialize persistence at path '%s'") ) From e4fdd3d454b0f526da22c081e9718cba3f1c294a Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 18 Jul 2022 23:14:30 -0400 Subject: [PATCH 49/95] Reverse sort order of all collections and add TX/Nonce/InFlight collections Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 1 + config.md | 6 - internal/persistence/leveldb_persistence.go | 156 ++++++++--- .../persistence/leveldb_persistence_test.go | 242 +++++++++++++++++- internal/persistence/persistence.go | 19 +- internal/tmconfig/tmconfig.go | 1 - internal/tmmsgs/en_config_descriptions.go | 2 - mocks/persistencemocks/persistence.go | 222 ++++++++++++++++ mocks/policyenginemocks/policy_engine.go | 13 +- pkg/apitypes/managed_tx.go | 91 +++++++ pkg/fftm/ffcore.go | 10 +- pkg/fftm/manager.go | 16 +- pkg/fftm/manager_test.go | 93 ++----- pkg/fftm/nonces.go | 4 +- pkg/fftm/nonces_test.go | 3 +- pkg/fftm/policyloop.go | 10 +- pkg/fftm/policyloop_test.go | 17 +- pkg/fftm/route__root_command.go | 2 +- .../route_get_eventstream_listeners_test.go | 4 +- pkg/fftm/route_get_eventstreams_test.go | 4 +- pkg/fftm/route_get_subscriptions_test.go | 4 +- pkg/fftm/send_tx.go | 6 +- pkg/policyengine/managed_tx.go | 49 ---- pkg/policyengine/policyengine.go | 3 +- .../simple/simple_policy_engine.go | 5 +- .../simple/simple_policy_engine_test.go | 25 +- 26 files changed, 760 insertions(+), 248 deletions(-) create mode 100644 pkg/apitypes/managed_tx.go delete mode 100644 pkg/policyengine/managed_tx.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 2d4050fc..07d61c95 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,6 +76,7 @@ "txcommon", "txcommonmocks", "txid", + "txns", "txtype", "unflushed", "unmarshalled", diff --git a/config.md b/config.md index 27579fc5..1ba2ea54 100644 --- a/config.md +++ b/config.md @@ -164,12 +164,6 @@ nav_order: 2 |message|Configures the JSON key containing the log message|`string`|`message` |timestamp|Configures the JSON key containing the timestamp of the log|`string`|`@timestamp` -## manager - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|name|The name of this Transaction Manager, used in operation metadata to track which operations are to be updated|`string`|`` - ## operations |Key|Description|Type|Default Value| diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index f8022f87..033c44fe 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -17,11 +17,9 @@ package persistence import ( - "bytes" "context" "encoding/json" "fmt" - "strings" "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -57,16 +55,35 @@ func NewLevelDBPersistence(ctx context.Context) (Persistence, error) { }, nil } -func (p *leveldbPersistence) checkpointKey(streamID *fftypes.UUID) []byte { - return []byte(fmt.Sprintf("checkpoints/%s", streamID)) +const checkpointsPrefix = "checkpoints_0/" +const eventstreamsPrefix = "eventstreams_0/" +const eventstreamsEnd = "eventstreams_1" +const listenersPrefix = "listeners_0/" +const listenersEnd = "listeners_1" +const transactionsPrefix = "transactions_0/" +const transactionsEnd = "transactions_1" +const nonceAllocPrefix = "nonces_0/" +const inflightPrefix = "inflight_0/" +const inflightEnd = "inflight_1" + +func signerNonceAllocPrefix(signer string) string { + return fmt.Sprintf("%s%s_0/", nonceAllocPrefix, signer) +} + +func signerNonceKey(signer string, n int64) []byte { + return []byte(fmt.Sprintf("%s%s_0/%.12d", nonceAllocPrefix, signer, n)) } -func (p *leveldbPersistence) streamKey(streamID *fftypes.UUID) []byte { - return []byte(fmt.Sprintf("eventstreams/%s", streamID)) +func signerNonceAllocEnd(signer string) string { + return fmt.Sprintf("%s%s_1", nonceAllocPrefix, signer) } -func (p *leveldbPersistence) listenerKey(listenerID *fftypes.UUID) []byte { - return []byte(fmt.Sprintf("listeners/%s", listenerID)) +func prefixedKey(prefix string, id fmt.Stringer) []byte { + return []byte(fmt.Sprintf("%s%s", prefix, id)) +} + +func prefixedStrKey(prefix string, k string) []byte { + return []byte(fmt.Sprintf("%s%s", prefix, k)) } func (p *leveldbPersistence) writeJSON(ctx context.Context, key []byte, value interface{}) error { @@ -94,28 +111,25 @@ func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target in return nil } -func (p *leveldbPersistence) listJSON(ctx context.Context, prefix, after string, limit int, +func (p *leveldbPersistence) listJSON(ctx context.Context, collectionPrefix, collectionEnd, after string, limit int, val func() interface{}, // return a pointer to a pointer variable, of the type to unmarshal add func(interface{}), // passes back the val() for adding to the list, if the filters match filters ...func(interface{}) bool, // filters to apply to the val() after unmarshalling ) error { - rangeStart := &util.Range{Start: []byte(prefix)} + collectionRange := &util.Range{ + Start: []byte(collectionPrefix), + Limit: []byte(collectionEnd), + } if after != "" { - rangeStart.Start = []byte(prefix + after) + collectionRange.Limit = []byte(collectionPrefix + after) } - it := p.db.NewIterator(rangeStart, &opt.ReadOptions{DontFillCache: true}) + it := p.db.NewIterator(collectionRange, &opt.ReadOptions{DontFillCache: true}) defer it.Release() count := 0 - skippedAfter := false + next := it.Last // First iteration of the loop goes to the end itLoop: - for it.Next() { - if after != "" && !skippedAfter && bytes.Equal(it.Key(), rangeStart.Start) { - skippedAfter = true // need to skip the first one, as the range is inclusive - continue - } - if !strings.HasPrefix(string(it.Key()), prefix) { - break itLoop - } + for next() { + next = it.Prev // Future iterations call prev (note reverse sort order moving backwards through the selection) v := val() err := json.Unmarshal(it.Value(), v) if err != nil { @@ -148,21 +162,21 @@ func (p *leveldbPersistence) deleteKeys(ctx context.Context, keys ...[]byte) err } func (p *leveldbPersistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error { - return p.writeJSON(ctx, p.checkpointKey(checkpoint.StreamID), checkpoint) + return p.writeJSON(ctx, prefixedKey(checkpointsPrefix, checkpoint.StreamID), checkpoint) } func (p *leveldbPersistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (cp *apitypes.EventStreamCheckpoint, err error) { - err = p.readJSON(ctx, p.checkpointKey(streamID), &cp) + err = p.readJSON(ctx, prefixedKey(checkpointsPrefix, streamID), &cp) return cp, err } func (p *leveldbPersistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error { - return p.deleteKeys(ctx, p.checkpointKey(streamID)) + return p.deleteKeys(ctx, prefixedKey(checkpointsPrefix, streamID)) } func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) { streams := make([]*apitypes.EventStream, 0) - if err := p.listJSON(ctx, "eventstreams/", after.String(), limit, + if err := p.listJSON(ctx, eventstreamsPrefix, eventstreamsEnd, after.String(), limit, func() interface{} { var v *apitypes.EventStream; return &v }, func(v interface{}) { streams = append(streams, *(v.(**apitypes.EventStream))) }, ); err != nil { @@ -172,21 +186,21 @@ func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUI } func (p *leveldbPersistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (es *apitypes.EventStream, err error) { - err = p.readJSON(ctx, p.streamKey(streamID), &es) + err = p.readJSON(ctx, prefixedKey(eventstreamsPrefix, streamID), &es) return es, err } func (p *leveldbPersistence) WriteStream(ctx context.Context, spec *apitypes.EventStream) error { - return p.writeJSON(ctx, p.streamKey(spec.ID), spec) + return p.writeJSON(ctx, prefixedKey(eventstreamsPrefix, spec.ID), spec) } func (p *leveldbPersistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) error { - return p.deleteKeys(ctx, p.streamKey(streamID)) + return p.deleteKeys(ctx, prefixedKey(eventstreamsPrefix, streamID)) } func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { listeners := make([]*apitypes.Listener, 0) - if err := p.listJSON(ctx, "listeners/", after.String(), limit, + if err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, func() interface{} { var v *apitypes.Listener; return &v }, func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, ); err != nil { @@ -197,7 +211,7 @@ func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.U func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { listeners := make([]*apitypes.Listener, 0) - if err := p.listJSON(ctx, "listeners/", after.String(), limit, + if err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, func() interface{} { var v *apitypes.Listener; return &v }, func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, func(v interface{}) bool { return (*(v.(**apitypes.Listener))).StreamID.Equals(streamID) }, @@ -208,16 +222,92 @@ func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fft } func (p *leveldbPersistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (l *apitypes.Listener, err error) { - err = p.readJSON(ctx, p.listenerKey(listenerID), &l) + err = p.readJSON(ctx, prefixedKey(listenersPrefix, listenerID), &l) return l, err } func (p *leveldbPersistence) WriteListener(ctx context.Context, spec *apitypes.Listener) error { - return p.writeJSON(ctx, p.listenerKey(spec.ID), spec) + return p.writeJSON(ctx, prefixedKey(listenersPrefix, spec.ID), spec) } func (p *leveldbPersistence) DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error { - return p.deleteKeys(ctx, p.listenerKey(listenerID)) + return p.deleteKeys(ctx, prefixedKey(listenersPrefix, listenerID)) +} + +func (p *leveldbPersistence) ListManagedTransactions(ctx context.Context, after string, limit int) ([]*apitypes.ManagedTX, error) { + transactions := make([]*apitypes.ManagedTX, 0) + if err := p.listJSON(ctx, transactionsPrefix, transactionsEnd, after, limit, + func() interface{} { var v *apitypes.ManagedTX; return &v }, + func(v interface{}) { transactions = append(transactions, *(v.(**apitypes.ManagedTX))) }, + ); err != nil { + return nil, err + } + return transactions, nil +} + +func (p *leveldbPersistence) GetManagedTransaction(ctx context.Context, txID string) (tx *apitypes.ManagedTX, err error) { + err = p.readJSON(ctx, prefixedStrKey(transactionsPrefix, txID), &tx) + return tx, err +} + +func (p *leveldbPersistence) WriteManagedTransaction(ctx context.Context, tx *apitypes.ManagedTX) error { + return p.writeJSON(ctx, prefixedStrKey(transactionsPrefix, tx.ID), tx) +} + +func (p *leveldbPersistence) DeleteManagedTransaction(ctx context.Context, txID string) error { + return p.deleteKeys(ctx, prefixedStrKey(transactionsPrefix, txID)) +} + +func (p *leveldbPersistence) ListNonceAllocations(ctx context.Context, signer string, after *int64, limit int) ([]*apitypes.NonceAllocation, error) { + nonceAllocations := make([]*apitypes.NonceAllocation, 0) + afterStr := "" + if after != nil { + afterStr = fmt.Sprintf("%.12d", *after) + } + if err := p.listJSON(ctx, signerNonceAllocPrefix(signer), signerNonceAllocEnd(signer), afterStr, limit, + func() interface{} { var v *apitypes.NonceAllocation; return &v }, + func(v interface{}) { nonceAllocations = append(nonceAllocations, *(v.(**apitypes.NonceAllocation))) }, + ); err != nil { + return nil, err + } + return nonceAllocations, nil +} + +func (p *leveldbPersistence) GetNonceAllocation(ctx context.Context, signer string, nonce int64) (alloc *apitypes.NonceAllocation, err error) { + err = p.readJSON(ctx, signerNonceKey(signer, nonce), &alloc) + return alloc, err +} + +func (p *leveldbPersistence) WriteNonceAllocation(ctx context.Context, alloc *apitypes.NonceAllocation) error { + return p.writeJSON(ctx, signerNonceKey(alloc.Signer, alloc.Nonce), alloc) +} + +func (p *leveldbPersistence) DeleteNonceAllocation(ctx context.Context, signer string, nonce int64) error { + return p.deleteKeys(ctx, signerNonceKey(signer, nonce)) +} + +func (p *leveldbPersistence) ListInflightTransactions(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.InflightTX, error) { + inflight := make([]*apitypes.InflightTX, 0) + if err := p.listJSON(ctx, inflightPrefix, inflightEnd, after.String(), limit, + func() interface{} { var v *apitypes.InflightTX; return &v }, + func(v interface{}) { inflight = append(inflight, *(v.(**apitypes.InflightTX))) }, + ); err != nil { + return nil, err + } + return inflight, nil +} + +func (p *leveldbPersistence) GetInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) (inflight *apitypes.InflightTX, err error) { + err = p.readJSON(ctx, prefixedKey(inflightPrefix, inflightID), &inflight) + return inflight, err +} + +func (p *leveldbPersistence) WriteInflightTransaction(ctx context.Context, inflight *apitypes.InflightTX) error { + return p.writeJSON(ctx, prefixedKey(inflightPrefix, inflight.ID), inflight) +} + +func (p *leveldbPersistence) DeleteInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) error { + return p.deleteKeys(ctx, prefixedKey(inflightPrefix, inflightID)) } func (p *leveldbPersistence) Close(ctx context.Context) { diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index 2699970f..d0858422 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -19,6 +19,7 @@ package persistence import ( "context" "encoding/json" + "fmt" "io/ioutil" "os" "testing" @@ -119,22 +120,22 @@ func TestReadWriteStreams(t *testing.T) { assert.NoError(t, err) assert.Len(t, streams, 3) - assert.Equal(t, s1.ID, streams[0].ID) + assert.Equal(t, s3.ID, streams[0].ID) assert.Equal(t, s2.ID, streams[1].ID) - assert.Equal(t, s3.ID, streams[2].ID) + assert.Equal(t, s1.ID, streams[2].ID) // Test pagination streams, err = p.ListStreams(ctx, nil, 2) assert.NoError(t, err) assert.Len(t, streams, 2) - assert.Equal(t, s1.ID, streams[0].ID) + assert.Equal(t, s3.ID, streams[0].ID) assert.Equal(t, s2.ID, streams[1].ID) streams, err = p.ListStreams(ctx, streams[1].ID, 2) assert.NoError(t, err) assert.Len(t, streams, 1) - assert.Equal(t, s3.ID, streams[0].ID) + assert.Equal(t, s1.ID, streams[0].ID) // Test delete @@ -143,8 +144,8 @@ func TestReadWriteStreams(t *testing.T) { streams, err = p.ListStreams(ctx, nil, 2) assert.NoError(t, err) assert.Len(t, streams, 2) - assert.Equal(t, s1.ID, streams[0].ID) - assert.Equal(t, s3.ID, streams[1].ID) + assert.Equal(t, s3.ID, streams[0].ID) + assert.Equal(t, s1.ID, streams[1].ID) // Test get direct @@ -193,17 +194,17 @@ func TestReadWriteListeners(t *testing.T) { assert.NoError(t, err) assert.Len(t, listeners, 3) - assert.Equal(t, s1l1.ID, listeners[0].ID) + assert.Equal(t, s1l2.ID, listeners[0].ID) assert.Equal(t, s2l1.ID, listeners[1].ID) - assert.Equal(t, s1l2.ID, listeners[2].ID) + assert.Equal(t, s1l1.ID, listeners[2].ID) // Test stream filter listeners, err = p.ListStreamListeners(ctx, nil, 0, sID1) assert.NoError(t, err) assert.Len(t, listeners, 2) - assert.Equal(t, s1l1.ID, listeners[0].ID) - assert.Equal(t, s1l2.ID, listeners[1].ID) + assert.Equal(t, s1l2.ID, listeners[0].ID) + assert.Equal(t, s1l1.ID, listeners[1].ID) // Test delete @@ -258,12 +259,189 @@ func TestReadWriteCheckpoints(t *testing.T) { assert.Equal(t, cp2.StreamID, cp.StreamID) } +func TestReadWriteManagedTransactions(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + t1 := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1/%s", apitypes.UUIDVersion1()), // ensure we get sequentially ascending IDs (not true in actual TXNs) + Nonce: fftypes.NewFFBigInt(10001), + } + p.WriteManagedTransaction(ctx, t1) + t2 := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1/%s", apitypes.UUIDVersion1()), + Nonce: fftypes.NewFFBigInt(10002), + } + p.WriteManagedTransaction(ctx, t2) + t3 := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1/%s", apitypes.UUIDVersion1()), + Nonce: fftypes.NewFFBigInt(10003), + } + p.WriteManagedTransaction(ctx, t3) + + txns, err := p.ListManagedTransactions(ctx, "", 0) + assert.NoError(t, err) + assert.Len(t, txns, 3) + + assert.Equal(t, t3.ID, txns[0].ID) + assert.Equal(t, t2.ID, txns[1].ID) + assert.Equal(t, t1.ID, txns[2].ID) + + // Test delete + + err = p.DeleteManagedTransaction(ctx, t2.ID) + assert.NoError(t, err) + txns, err = p.ListManagedTransactions(ctx, "", 2) + assert.NoError(t, err) + assert.Len(t, txns, 2) + assert.Equal(t, t3.ID, txns[0].ID) + assert.Equal(t, t1.ID, txns[1].ID) + + // Test get direct + + v, err := p.GetManagedTransaction(ctx, t3.ID) + assert.NoError(t, err) + assert.Equal(t, t3.ID, v.ID) + assert.Equal(t, t3.Nonce, v.Nonce) + + v, err = p.GetManagedTransaction(ctx, t2.ID) + assert.NoError(t, err) + assert.Nil(t, v) +} + +func TestReadWriteNonceAllocations(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + s1n1 := &apitypes.NonceAllocation{ + Signer: "0xaaaaaaa", + Nonce: 1001, + TX: "tx1", + } + p.WriteNonceAllocation(ctx, s1n1) + s2n1 := &apitypes.NonceAllocation{ + Signer: "0xbbbbbbb", + Nonce: 1001, + TX: "tx2", + } + p.WriteNonceAllocation(ctx, s2n1) + s1n2 := &apitypes.NonceAllocation{ + Signer: "0xaaaaaaa", + Nonce: 1002, + TX: "tx3", + } + p.WriteNonceAllocation(ctx, s1n2) + s1n3 := &apitypes.NonceAllocation{ + Signer: "0xaaaaaaa", + Nonce: 1003, + TX: "tx4", + } + p.WriteNonceAllocation(ctx, s1n3) + + nonces, err := p.ListNonceAllocations(ctx, "0xaaaaaaa", nil, 0) + assert.NoError(t, err) + assert.Len(t, nonces, 3) + assert.Equal(t, s1n3.TX, nonces[0].TX) + assert.Equal(t, s1n2.TX, nonces[1].TX) + assert.Equal(t, s1n1.TX, nonces[2].TX) + + nonces, err = p.ListNonceAllocations(ctx, "0xbbbbbbb", nil, 0) + assert.NoError(t, err) + assert.Len(t, nonces, 1) + assert.Equal(t, s2n1.TX, nonces[0].TX) + + // Test pagination + + after := int64(1003) + nonces, err = p.ListNonceAllocations(ctx, "0xaaaaaaa", &after, 1) + assert.NoError(t, err) + assert.Len(t, nonces, 1) + assert.Equal(t, s1n2.TX, nonces[0].TX) + + // Test delete + + err = p.DeleteNonceAllocation(ctx, "0xaaaaaaa", s1n2.Nonce) + assert.NoError(t, err) + nonces, err = p.ListNonceAllocations(ctx, "0xaaaaaaa", nil, 0) + assert.NoError(t, err) + assert.Len(t, nonces, 2) + assert.Equal(t, s1n3.TX, nonces[0].TX) + assert.Equal(t, s1n1.TX, nonces[1].TX) + + // Test get direct + + v, err := p.GetNonceAllocation(ctx, "0xaaaaaaa", s1n3.Nonce) + assert.NoError(t, err) + assert.Equal(t, s1n3.Nonce, v.Nonce) + assert.Equal(t, s1n3.TX, v.TX) + + v, err = p.GetNonceAllocation(ctx, "0xaaaaaaa", s1n2.Nonce) + assert.NoError(t, err) + assert.Nil(t, v) +} + +func TestReadWriteInflightTransactions(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + t1 := &apitypes.InflightTX{ + ID: apitypes.UUIDVersion1(), // ensure we get sequentially ascending IDs + TX: "tx1", + } + p.WriteInflightTransaction(ctx, t1) + t2 := &apitypes.InflightTX{ + ID: apitypes.UUIDVersion1(), // ensure we get sequentially ascending IDs + TX: "tx2", + } + p.WriteInflightTransaction(ctx, t2) + t3 := &apitypes.InflightTX{ + ID: apitypes.UUIDVersion1(), // ensure we get sequentially ascending IDs + TX: "tx3", + } + p.WriteInflightTransaction(ctx, t3) + + txns, err := p.ListInflightTransactions(ctx, nil, 0) + assert.NoError(t, err) + assert.Len(t, txns, 3) + + assert.Equal(t, t3.ID, txns[0].ID) + assert.Equal(t, t2.ID, txns[1].ID) + assert.Equal(t, t1.ID, txns[2].ID) + + // Test delete + + err = p.DeleteInflightTransaction(ctx, t2.ID) + assert.NoError(t, err) + txns, err = p.ListInflightTransactions(ctx, nil, 2) + assert.NoError(t, err) + assert.Len(t, txns, 2) + assert.Equal(t, t3.ID, txns[0].ID) + assert.Equal(t, t1.ID, txns[1].ID) + + // Test get direct + + v, err := p.GetInflightTransaction(ctx, t3.ID) + assert.NoError(t, err) + assert.Equal(t, t3.ID, v.ID) + assert.Equal(t, t3.TX, v.TX) + + v, err = p.GetInflightTransaction(ctx, t2.ID) + assert.NoError(t, err) + assert.Nil(t, v) +} + func TestListStreamsBadJSON(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() sID := apitypes.UUIDVersion1() - err := p.db.Put(p.streamKey(sID), []byte("{! not json"), &opt.WriteOptions{}) + err := p.db.Put(prefixedKey(eventstreamsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) _, err = p.ListStreams(context.Background(), nil, 0) @@ -276,7 +454,7 @@ func TestListListenersBadJSON(t *testing.T) { defer done() lID := apitypes.UUIDVersion1() - err := p.db.Put(p.listenerKey(lID), []byte("{! not json"), &opt.WriteOptions{}) + err := p.db.Put(prefixedKey(listenersPrefix, lID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) _, err = p.ListListeners(context.Background(), nil, 0) @@ -330,10 +508,48 @@ func TestReadCheckpointFail(t *testing.T) { defer done() sID := apitypes.UUIDVersion1() - err := p.db.Put(p.checkpointKey(sID), []byte("{! not json"), &opt.WriteOptions{}) + err := p.db.Put(prefixedKey(checkpointsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) _, err = p.GetCheckpoint(context.Background(), sID) assert.Error(t, err) } + +func TestListManagedTransactionFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + sID := apitypes.UUIDVersion1() + err := p.db.Put(prefixedKey(transactionsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListManagedTransactions(context.Background(), "", 0) + assert.Error(t, err) + +} + +func TestListNonceAllocationsFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.db.Put(signerNonceKey("0xaaa", 12345), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListNonceAllocations(context.Background(), "0xaaa", nil, 0) + assert.Error(t, err) + +} + +func TestListInflightTransactionFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + sID := apitypes.UUIDVersion1() + err := p.db.Put(prefixedKey(inflightPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListInflightTransactions(context.Background(), nil, 0) + assert.Error(t, err) + +} diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go index 18d26599..4436599e 100644 --- a/internal/persistence/persistence.go +++ b/internal/persistence/persistence.go @@ -28,16 +28,31 @@ type Persistence interface { GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error - ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) + ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) // reverse UUIDv1 order GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) WriteStream(ctx context.Context, spec *apitypes.EventStream) error DeleteStream(ctx context.Context, streamID *fftypes.UUID) error - ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) + ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) // reverse UUIDv1 order ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) WriteListener(ctx context.Context, spec *apitypes.Listener) error DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error + ListManagedTransactions(ctx context.Context, after string, limit int) ([]*apitypes.ManagedTX, error) // note there is no order to these, so list isn't very useful + GetManagedTransaction(ctx context.Context, txID string) (*apitypes.ManagedTX, error) + WriteManagedTransaction(ctx context.Context, tx *apitypes.ManagedTX) error + DeleteManagedTransaction(ctx context.Context, txID string) error + + ListNonceAllocations(ctx context.Context, signer string, after *int64, limit int) ([]*apitypes.NonceAllocation, error) // reverse nonce order + GetNonceAllocation(ctx context.Context, signer string, nonce int64) (*apitypes.NonceAllocation, error) + WriteNonceAllocation(ctx context.Context, alloc *apitypes.NonceAllocation) error + DeleteNonceAllocation(ctx context.Context, signer string, nonce int64) error + + ListInflightTransactions(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.InflightTX, error) // reverse UUIDv1 order + GetInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) (*apitypes.InflightTX, error) + WriteInflightTransaction(ctx context.Context, inflight *apitypes.InflightTX) error + DeleteInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) error + Close(ctx context.Context) } diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index d326d2ce..fd883e42 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -28,7 +28,6 @@ import ( var ffc = config.AddRootKey var ( - ManagerName = ffc("manager.name") ConfirmationsRequired = ffc("confirmations.required") ConfirmationsBlockQueueLength = ffc("confirmations.blockQueueLength") ConfirmationsStaleReceiptTimeout = ffc("confirmations.staleReceiptTimeout") diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go index 12ed7f52..9b195c03 100644 --- a/internal/tmmsgs/en_config_descriptions.go +++ b/internal/tmmsgs/en_config_descriptions.go @@ -45,8 +45,6 @@ var ( ConfigFFCoreURL = ffc("config.ffcore.url", "The URL of the FireFly core admin API server to connect to", i18n.StringType) ConfigFFCoreProxyURL = ffc("config.ffcore.proxy.url", "Optional HTTP proxy URL to use for the FireFly core admin API server", i18n.StringType) - ConfigManagerName = ffc("config.manager.name", "The name of this Transaction Manager, used in operation metadata to track which operations are to be updated", i18n.StringType) - ConfigOperationsTypes = ffc("config.operations.types", "The operation types to query in FireFly core, that might have been submitted via this Transaction Manager", "string[]") ConfigOperationsFullScanMinimumDelay = ffc("config.operations.fullScan.minimumDelay", "The minimum delay between full scans of the FireFly core API, when reconnecting, or recovering from missed events / errors", i18n.TimeDurationType) ConfigOperationsFullScanPageSize = ffc("config.operations.fullScan.pageSize", "The page size to use when performing a full scan of the ForeFly core API on startup, or recovery", i18n.IntType) diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go index 14bea4c1..3f9aa206 100644 --- a/mocks/persistencemocks/persistence.go +++ b/mocks/persistencemocks/persistence.go @@ -36,6 +36,20 @@ func (_m *Persistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.U return r0 } +// DeleteInflightTransaction provides a mock function with given fields: ctx, inflightID +func (_m *Persistence) DeleteInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) error { + ret := _m.Called(ctx, inflightID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, inflightID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteListener provides a mock function with given fields: ctx, listenerID func (_m *Persistence) DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error { ret := _m.Called(ctx, listenerID) @@ -50,6 +64,34 @@ func (_m *Persistence) DeleteListener(ctx context.Context, listenerID *fftypes.U return r0 } +// DeleteManagedTransaction provides a mock function with given fields: ctx, txID +func (_m *Persistence) DeleteManagedTransaction(ctx context.Context, txID string) error { + ret := _m.Called(ctx, txID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, txID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteNonceAllocation provides a mock function with given fields: ctx, signer, nonce +func (_m *Persistence) DeleteNonceAllocation(ctx context.Context, signer string, nonce int64) error { + ret := _m.Called(ctx, signer, nonce) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64) error); ok { + r0 = rf(ctx, signer, nonce) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteStream provides a mock function with given fields: ctx, streamID func (_m *Persistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) error { ret := _m.Called(ctx, streamID) @@ -87,6 +129,29 @@ func (_m *Persistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID return r0, r1 } +// GetInflightTransaction provides a mock function with given fields: ctx, inflightID +func (_m *Persistence) GetInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) (*apitypes.InflightTX, error) { + ret := _m.Called(ctx, inflightID) + + var r0 *apitypes.InflightTX + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.InflightTX); ok { + r0 = rf(ctx, inflightID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.InflightTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { + r1 = rf(ctx, inflightID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetListener provides a mock function with given fields: ctx, listenerID func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) { ret := _m.Called(ctx, listenerID) @@ -110,6 +175,52 @@ func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID return r0, r1 } +// GetManagedTransaction provides a mock function with given fields: ctx, txID +func (_m *Persistence) GetManagedTransaction(ctx context.Context, txID string) (*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, txID) + + var r0 *apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, string) *apitypes.ManagedTX); ok { + r0 = rf(ctx, txID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, txID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetNonceAllocation provides a mock function with given fields: ctx, signer, nonce +func (_m *Persistence) GetNonceAllocation(ctx context.Context, signer string, nonce int64) (*apitypes.NonceAllocation, error) { + ret := _m.Called(ctx, signer, nonce) + + var r0 *apitypes.NonceAllocation + if rf, ok := ret.Get(0).(func(context.Context, string, int64) *apitypes.NonceAllocation); ok { + r0 = rf(ctx, signer, nonce) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.NonceAllocation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { + r1 = rf(ctx, signer, nonce) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetStream provides a mock function with given fields: ctx, streamID func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) { ret := _m.Called(ctx, streamID) @@ -133,6 +244,29 @@ func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (* return r0, r1 } +// ListInflightTransactions provides a mock function with given fields: ctx, after, limit +func (_m *Persistence) ListInflightTransactions(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.InflightTX, error) { + ret := _m.Called(ctx, after, limit) + + var r0 []*apitypes.InflightTX + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.InflightTX); ok { + r0 = rf(ctx, after, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.InflightTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int) error); ok { + r1 = rf(ctx, after, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ListListeners provides a mock function with given fields: ctx, after, limit func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { ret := _m.Called(ctx, after, limit) @@ -156,6 +290,52 @@ func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, l return r0, r1 } +// ListManagedTransactions provides a mock function with given fields: ctx, after, limit +func (_m *Persistence) ListManagedTransactions(ctx context.Context, after string, limit int) ([]*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, after, limit) + + var r0 []*apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, string, int) []*apitypes.ManagedTX); ok { + r0 = rf(ctx, after, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, int) error); ok { + r1 = rf(ctx, after, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListNonceAllocations provides a mock function with given fields: ctx, signer, after, limit +func (_m *Persistence) ListNonceAllocations(ctx context.Context, signer string, after *int64, limit int) ([]*apitypes.NonceAllocation, error) { + ret := _m.Called(ctx, signer, after, limit) + + var r0 []*apitypes.NonceAllocation + if rf, ok := ret.Get(0).(func(context.Context, string, *int64, int) []*apitypes.NonceAllocation); ok { + r0 = rf(ctx, signer, after, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.NonceAllocation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *int64, int) error); ok { + r1 = rf(ctx, signer, after, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ListStreamListeners provides a mock function with given fields: ctx, after, limit, streamID func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { ret := _m.Called(ctx, after, limit, streamID) @@ -216,6 +396,20 @@ func (_m *Persistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes return r0 } +// WriteInflightTransaction provides a mock function with given fields: ctx, inflight +func (_m *Persistence) WriteInflightTransaction(ctx context.Context, inflight *apitypes.InflightTX) error { + ret := _m.Called(ctx, inflight) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.InflightTX) error); ok { + r0 = rf(ctx, inflight) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // WriteListener provides a mock function with given fields: ctx, spec func (_m *Persistence) WriteListener(ctx context.Context, spec *apitypes.Listener) error { ret := _m.Called(ctx, spec) @@ -230,6 +424,34 @@ func (_m *Persistence) WriteListener(ctx context.Context, spec *apitypes.Listene return r0 } +// WriteManagedTransaction provides a mock function with given fields: ctx, tx +func (_m *Persistence) WriteManagedTransaction(ctx context.Context, tx *apitypes.ManagedTX) error { + ret := _m.Called(ctx, tx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX) error); ok { + r0 = rf(ctx, tx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// WriteNonceAllocation provides a mock function with given fields: ctx, alloc +func (_m *Persistence) WriteNonceAllocation(ctx context.Context, alloc *apitypes.NonceAllocation) error { + ret := _m.Called(ctx, alloc) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.NonceAllocation) error); ok { + r0 = rf(ctx, alloc) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // WriteStream provides a mock function with given fields: ctx, spec func (_m *Persistence) WriteStream(ctx context.Context, spec *apitypes.EventStream) error { ret := _m.Called(ctx, spec) diff --git a/mocks/policyenginemocks/policy_engine.go b/mocks/policyenginemocks/policy_engine.go index bccac0d4..063bb917 100644 --- a/mocks/policyenginemocks/policy_engine.go +++ b/mocks/policyenginemocks/policy_engine.go @@ -5,10 +5,11 @@ package policyenginemocks import ( context "context" + apitypes "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - mock "github.com/stretchr/testify/mock" - policyengine "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" + mock "github.com/stretchr/testify/mock" ) // PolicyEngine is an autogenerated mock type for the PolicyEngine type @@ -17,25 +18,25 @@ type PolicyEngine struct { } // Execute provides a mock function with given fields: ctx, cAPI, mtx -func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *policyengine.ManagedTXOutput) (bool, ffcapi.ErrorReason, error) { +func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (bool, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, cAPI, mtx) var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) bool); ok { + if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *apitypes.ManagedTX) bool); ok { r0 = rf(ctx, cAPI, mtx) } else { r0 = ret.Get(0).(bool) } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *apitypes.ManagedTX) ffcapi.ErrorReason); ok { r1 = rf(ctx, cAPI, mtx) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, ffcapi.API, *apitypes.ManagedTX) error); ok { r2 = rf(ctx, cAPI, mtx) } else { r2 = ret.Error(2) diff --git a/pkg/apitypes/managed_tx.go b/pkg/apitypes/managed_tx.go new file mode 100644 index 00000000..cd61ead0 --- /dev/null +++ b/pkg/apitypes/managed_tx.go @@ -0,0 +1,91 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 apitypes + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +// TxStatus is the current status of a transaction +type TxStatus string + +const ( + // TxStatusPending indicates the operation has been submitted, but is not yet confirmed as successful or failed + TxStatusPending TxStatus = "Pending" + // TxStatusSucceeded the infrastructure runtime has returned success for the operation + TxStatusSucceeded TxStatus = "Succeeded" + // TxStatusFailed happens when an error is reported by the infrastructure runtime + TxStatusFailed TxStatus = "Failed" +) + +type ManagedTXError struct { + Time *fftypes.FFTime `json:"time"` + Error string `json:"error,omitempty"` + Mapped ffcapi.ErrorReason `json:"mapped,omitempty"` +} + +// NonceAllocation is a mapping from an address + nonce, to a managed transaction. +// These are stored such that we can easily find the next nonce to assign to a managed transaction +// +// ** Stored first ** +// Because there's a non-zero chance we crash after writing this, and before writing the ManagedTX +// record. The nonce-allocation code must read the most recently written nonce allocation for the +// signer and check the TX object has been written. If not, it will re-allocate (cleaning up the +// in-flight record if it exists) +// +type NonceAllocation struct { + Signer string `json:"signer"` + Nonce int64 `json:"nonce"` + TX string `json:"tx"` +} + +// InflightTX is a UUIDv1 (so ordered) entry, that refers to an in-flight transaction that needs to be tracked. +// These are deleted when the transaction is complete. +// +// ** Stored second ** +// This means we might have a nonce+inflight record, but not have written the managed TX. +// The code that reads the in-flight TX list looks for this scenario, and clean up the orphaned in-flight record +// (if it gets to it before) +type InflightTX struct { + ID *fftypes.UUID `json:"id"` + TX string `json:"tx"` + Created *fftypes.FFTime `json:"created"` +} + +// ManagedTX is the structure stored for each new transaction request, using the external ID of the operation +// +// ** Stored last ** +// This is persisted (along with the two objects above) before we reply to the API call to initiate a transaction, +// and is updated as the transaction progresses onto the chain. +type ManagedTX struct { + ID string `json:"id"` + Status TxStatus `json:"status"` + Nonce *fftypes.FFBigInt `json:"nonce"` // persisted separately + Gas *fftypes.FFBigInt `json:"gas"` + TransactionHash string `json:"transactionHash,omitempty"` + TransactionData string `json:"transactionData,omitempty"` + GasPrice *fftypes.JSONAny `json:"gasPrice"` + PolicyInfo *fftypes.JSONAny `json:"policyInfo"` + FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` + LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` + Request *TransactionRequest `json:"request,omitempty"` + Receipt *ffcapi.TransactionReceiptResponse `json:"receipt,omitempty"` + ErrorHistory []*ManagedTXError `json:"errorHistory"` + Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` +} diff --git a/pkg/fftm/ffcore.go b/pkg/fftm/ffcore.go index f27073df..381ec72f 100644 --- a/pkg/fftm/ffcore.go +++ b/pkg/fftm/ffcore.go @@ -26,18 +26,18 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly/pkg/core" ) // opUpdate allows us to avoid JSONObject serialization to a map before we upload our managedTXOutput type opUpdate struct { - Status core.OpStatus `json:"status"` - Output *policyengine.ManagedTXOutput `json:"output"` - Error string `json:"error"` + Status core.OpStatus `json:"status"` + Output *apitypes.ManagedTX `json:"output"` + Error string `json:"error"` } -func (m *manager) writeManagedTX(ctx context.Context, mtx *policyengine.ManagedTXOutput, status core.OpStatus, errString string) error { +func (m *manager) writeManagedTX(ctx context.Context, mtx *apitypes.ManagedTX, status core.OpStatus, errString string) error { log.L(ctx).Debugf("Updating operation %s status=%s", mtx.ID, status) var errorInfo fftypes.RESTError var op core.Operation diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index e047f263..92d1b2c7 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -37,6 +37,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/internal/ws" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" @@ -74,7 +75,6 @@ type manager struct { started bool apiServerDone chan error - name string opTypes []string startupScanMaxRetries int fullScanPageSize int64 @@ -92,9 +92,6 @@ func InitConfig() { func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { var err error m := newManager(ctx, connector) - if m.name == "" { - return nil, i18n.NewError(ctx, tmmsgs.MsgConfigParamNotSet, tmconfig.ManagerName) - } m.confirmations = confirmations.NewBlockConfirmationManager(ctx, m.connector) m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBaseConfig, config.GetString(tmconfig.PolicyEngineName)) if err != nil { @@ -127,7 +124,6 @@ func newManager(ctx context.Context, connector ffcapi.API) *manager { eventStreams: make(map[fftypes.UUID]events.Stream), streamsByName: make(map[string]*fftypes.UUID), - name: config.GetString(tmconfig.ManagerName), opTypes: config.GetStringSlice(tmconfig.OperationsTypes), startupScanMaxRetries: config.GetInt(tmconfig.OperationsFullScanStartupMaxRetries), fullScanPageSize: config.GetInt64(tmconfig.OperationsFullScanPageSize), @@ -141,7 +137,7 @@ func newManager(ctx context.Context, connector ffcapi.API) *manager { } type pendingState struct { - mtx *policyengine.ManagedTXOutput + mtx *apitypes.ManagedTX confirmed bool removed bool trackingTransactionHash string @@ -244,16 +240,12 @@ func (m *manager) fullScan() error { func (m *manager) trackIfManaged(op *core.Operation) { outputJSON := []byte(op.Output.String()) - var mtx policyengine.ManagedTXOutput + var mtx apitypes.ManagedTX err := json.Unmarshal(outputJSON, &mtx) if err != nil { log.L(m.ctx).Warnf("Failed to parse output from operation %s", err) return } - if mtx.FFTMName != m.name { - log.L(m.ctx).Debugf("Operation %s is not managed by us (fftm=%s)", op.ID, mtx.FFTMName) - return - } if fmt.Sprintf("%s:%s", op.Namespace, op.ID) != mtx.ID { log.L(m.ctx).Warnf("Operation %s contains an invalid ID %s in the output", op.ID, mtx.ID) return @@ -265,7 +257,7 @@ func (m *manager) trackIfManaged(op *core.Operation) { m.trackManaged(&mtx) } -func (m *manager) trackManaged(mtx *policyengine.ManagedTXOutput) { +func (m *manager) trackManaged(mtx *apitypes.ManagedTX) { m.mux.Lock() defer m.mux.Unlock() _, existing := m.pendingOpsByID[mtx.ID] diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 3bbaf9d8..f21f311e 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -39,7 +39,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines/simple" "github.com/hyperledger/firefly/pkg/core" @@ -68,7 +67,6 @@ func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...strin tmconfig.APIConfig.Set(httpserver.HTTPConfPort, managerPort) tmconfig.APIConfig.Set(httpserver.HTTPConfAddress, "127.0.0.1") - config.Set(tmconfig.ManagerName, testManagerName) config.Set(tmconfig.PolicyLoopInterval, "1ms") tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") @@ -103,7 +101,7 @@ func newMockPersistenceManager(t *testing.T) (*persistencemocks.Persistence, *ff return mps, mca, m } -func newTestOperation(t *testing.T, mtx *policyengine.ManagedTXOutput, status core.OpStatus) *core.Operation { +func newTestOperation(t *testing.T, mtx *apitypes.ManagedTX, status core.OpStatus) *core.Operation { b, err := json.Marshal(&mtx) assert.NoError(t, err) op := &core.Operation{ @@ -116,20 +114,9 @@ func newTestOperation(t *testing.T, mtx *policyengine.ManagedTXOutput, status co return op } -func TestNewManagerMissingName(t *testing.T) { - - tmconfig.Reset() - config.Set(tmconfig.ManagerName, "") - - _, err := NewManager(context.Background(), nil) - assert.Regexp(t, "FF21018", err) - -} - func TestNewManagerBadHttpConfig(t *testing.T) { tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") tmconfig.APIConfig.Set(httpserver.HTTPConfAddress, "::::") policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) @@ -147,7 +134,6 @@ func TestNewManagerBadLevelDBConfig(t *testing.T) { defer os.Remove(tmpFile.Name()) tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") config.Set(tmconfig.PersistenceLevelDBPath, tmpFile.Name) tmconfig.APIConfig.Set(httpserver.HTTPConfPort, "0") @@ -162,7 +148,6 @@ func TestNewManagerBadLevelDBConfig(t *testing.T) { func TestNewManagerBadPersistenceConfig(t *testing.T) { tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") config.Set(tmconfig.PersistenceType, "wrong") tmconfig.APIConfig.Set(httpserver.HTTPConfPort, "0") @@ -177,7 +162,6 @@ func TestNewManagerBadPersistenceConfig(t *testing.T) { func TestNewManagerFireFlyURLConfig(t *testing.T) { tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, ":::!badurl") policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) @@ -191,7 +175,6 @@ func TestNewManagerFireFlyURLConfig(t *testing.T) { func TestNewManagerBadPolicyEngine(t *testing.T) { tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") config.Set(tmconfig.PolicyEngineName, "wrong") _, err := NewManager(context.Background(), nil) @@ -233,38 +216,6 @@ func TestChangeEventsNewBadOutput(t *testing.T) { } -func TestChangeEventsWrongName(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - b, err := json.Marshal(newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: "wrong", - Request: &apitypes.TransactionRequest{}, - }, core.OpStatusPending)) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - -} - func TestChangeEventsWrongID(t *testing.T) { ce := &core.ChangeEvent{ @@ -279,10 +230,9 @@ func TestChangeEventsWrongID(t *testing.T) { func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - op := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: testManagerName, - Request: &apitypes.TransactionRequest{}, + op := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + ce.ID.String(), + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) op.ID = fftypes.NewUUID() b, err := json.Marshal(&op) @@ -313,9 +263,8 @@ func TestChangeEventsNilRequest(t *testing.T) { func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - op := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: testManagerName, + op := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + ce.ID.String(), }, core.OpStatusPending) b, err := json.Marshal(&op) assert.NoError(t, err) @@ -369,10 +318,9 @@ func TestChangeEventsMarkForCleanup(t *testing.T) { Namespace: "ns1", } - op := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: testManagerName, - Request: &apitypes.TransactionRequest{}, + op := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + ce.ID.String(), + Request: &apitypes.TransactionRequest{}, }, core.OpStatusFailed) var m *manager @@ -397,24 +345,21 @@ func TestChangeEventsMarkForCleanup(t *testing.T) { func TestStartupScanMultiPageOK(t *testing.T) { - op1 := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FFTMName: testManagerName, - Request: &apitypes.TransactionRequest{}, + op1 := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + fftypes.NewUUID().String(), + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) t1 := fftypes.FFTime(time.Now().Add(-10 * time.Minute)) op1.Created = &t1 - op2 := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FFTMName: testManagerName, - Request: &apitypes.TransactionRequest{}, + op2 := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + fftypes.NewUUID().String(), + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) t2 := fftypes.FFTime(time.Now().Add(-5 * time.Minute)) op2.Created = &t2 - op3 := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FFTMName: testManagerName, - Request: &apitypes.TransactionRequest{}, + op3 := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + fftypes.NewUUID().String(), + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) t3 := fftypes.FFTime(time.Now().Add(-1 * time.Minute)) op3.Created = &t3 @@ -545,7 +490,7 @@ func TestAddErrorMessageMax(t *testing.T) { defer cancel() m.errorHistoryCount = 2 - mtx := &policyengine.ManagedTXOutput{} + mtx := &apitypes.ManagedTX{} m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("snap")) m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("crackle")) m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("pop")) diff --git a/pkg/fftm/nonces.go b/pkg/fftm/nonces.go index 76029433..e41db85b 100644 --- a/pkg/fftm/nonces.go +++ b/pkg/fftm/nonces.go @@ -20,8 +20,8 @@ import ( "context" "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) type lockedNonce struct { @@ -30,7 +30,7 @@ type lockedNonce struct { signer string unlocked chan struct{} nonce uint64 - spent *policyengine.ManagedTXOutput + spent *apitypes.ManagedTX } // complete must be called for any lockedNonce returned from a successful assignAndLockNonce call diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go index 8a33fc98..22620add 100644 --- a/pkg/fftm/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -27,7 +27,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -60,7 +59,7 @@ func TestNonceCached(t *testing.T) { close(locked1) time.Sleep(1 * time.Millisecond) - ln.spent = &policyengine.ManagedTXOutput{ + ln.spent = &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), Request: &apitypes.TransactionRequest{ diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index dc8acc26..e0ceef0e 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -22,8 +22,8 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" ) @@ -62,14 +62,14 @@ func (m *manager) policyLoopCycle() { } -func (m *manager) addError(mtx *policyengine.ManagedTXOutput, reason ffcapi.ErrorReason, err error) { +func (m *manager) addError(mtx *apitypes.ManagedTX, reason ffcapi.ErrorReason, err error) { newLen := len(mtx.ErrorHistory) + 1 if newLen > m.errorHistoryCount { newLen = m.errorHistoryCount } oldHistory := mtx.ErrorHistory - mtx.ErrorHistory = make([]*policyengine.ManagedTXError, newLen) - mtx.ErrorHistory[0] = &policyengine.ManagedTXError{ + mtx.ErrorHistory = make([]*apitypes.ManagedTXError, newLen) + mtx.ErrorHistory[0] = &apitypes.ManagedTXError{ Time: fftypes.Now(), Mapped: reason, Error: err.Error(), @@ -177,7 +177,7 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { } } -func (m *manager) clearConfirmationTracking(mtx *policyengine.ManagedTXOutput) { +func (m *manager) clearConfirmationTracking(mtx *apitypes.ManagedTX) { // The only error condition on confirmations manager is if we are exiting, which it logs _ = m.confirmations.Notify(&confirmations.Notification{ NotificationType: confirmations.RemovedTransaction, diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index ed43dbd1..51b056d7 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -29,7 +29,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -42,7 +41,7 @@ const ( func TestPolicyLoopE2EOk(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, @@ -89,7 +88,7 @@ func TestPolicyLoopE2EOk(t *testing.T) { func TestPolicyLoopE2EOkReverted(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, @@ -136,7 +135,7 @@ func TestPolicyLoopE2EOkReverted(t *testing.T) { func TestPolicyLoopUpdateFFCoreWithError(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, @@ -168,7 +167,7 @@ func TestPolicyLoopUpdateFFCoreWithError(t *testing.T) { func TestPolicyLoopUpdateOpFail(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, @@ -176,7 +175,7 @@ func TestPolicyLoopUpdateOpFail(t *testing.T) { } _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { + func(w http.ResponseWriter, _ *http.Request) { errRes := fftypes.RESTError{Error: "pop"} b, err := json.Marshal(&errRes) assert.NoError(t, err) @@ -213,7 +212,7 @@ func TestPolicyLoopUpdateOpFail(t *testing.T) { func TestPolicyLoopResubmitNewTXID(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), Request: &apitypes.TransactionRequest{}, } @@ -287,7 +286,7 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { func TestPolicyLoopCycleCleanupRemoved(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), Request: &apitypes.TransactionRequest{}, } @@ -319,7 +318,7 @@ func TestNotifyConfirmationMgrFail(t *testing.T) { mc.On("Notify", mock.Anything).Return(fmt.Errorf("pop")) m.trackSubmittedTransaction(&pendingState{ - mtx: &policyengine.ManagedTXOutput{ + mtx: &apitypes.ManagedTX{ TransactionHash: sampleSendTX, }, }) diff --git a/pkg/fftm/route__root_command.go b/pkg/fftm/route__root_command.go index 526d09c8..7fc10000 100644 --- a/pkg/fftm/route__root_command.go +++ b/pkg/fftm/route__root_command.go @@ -38,7 +38,7 @@ var postRootCommand = func(m *manager) *ffapi.Route { Description: tmmsgs.APIEndpointPostSubscriptions, JSONInputValue: func() interface{} { return &apitypes.BaseRequest{} }, JSONOutputValue: func() interface{} { return map[string]interface{}{} }, - JSONInputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) { + JSONInputSchema: func(_ context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) { schemas := []*openapi3.SchemaRef{} txRequest, err := schemaGen(&apitypes.TransactionRequest{}) if err == nil { diff --git a/pkg/fftm/route_get_eventstream_listeners_test.go b/pkg/fftm/route_get_eventstream_listeners_test.go index 6eca9d61..40018cac 100644 --- a/pkg/fftm/route_get_eventstream_listeners_test.go +++ b/pkg/fftm/route_get_eventstream_listeners_test.go @@ -64,12 +64,12 @@ func TestGetEventStreamListeners(t *testing.T) { var listeners []*apitypes.Listener res, err = resty.New().R(). SetResult(&listeners). - Get(fmt.Sprintf("%s/eventstreams/%s/listeners?limit=1&after=%s", url, es1.ID, l1.ID)) + Get(fmt.Sprintf("%s/eventstreams/%s/listeners?limit=1&after=%s", url, es1.ID, l2.ID)) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, listeners, 1) - assert.Equal(t, l3.ID, listeners[0].ID) + assert.Equal(t, l1.ID, listeners[0].ID) assert.Equal(t, es1.ID, listeners[0].StreamID) mfc.AssertExpectations(t) diff --git a/pkg/fftm/route_get_eventstreams_test.go b/pkg/fftm/route_get_eventstreams_test.go index ac57db16..f29cb851 100644 --- a/pkg/fftm/route_get_eventstreams_test.go +++ b/pkg/fftm/route_get_eventstreams_test.go @@ -53,11 +53,11 @@ func TestGetEventStreams(t *testing.T) { var ess []*apitypes.EventStream res, err = resty.New().R(). SetResult(&ess). - Get(url + "/eventstreams?limit=1&after=" + es1.ID.String()) + Get(url + "/eventstreams?limit=1&after=" + es2.ID.String()) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, ess, 1) - assert.Equal(t, es2.ID, ess[0].ID) + assert.Equal(t, es1.ID, ess[0].ID) } diff --git a/pkg/fftm/route_get_subscriptions_test.go b/pkg/fftm/route_get_subscriptions_test.go index 7d742dc3..0ed4b82a 100644 --- a/pkg/fftm/route_get_subscriptions_test.go +++ b/pkg/fftm/route_get_subscriptions_test.go @@ -59,12 +59,12 @@ func TestGetSubscriptions(t *testing.T) { var listeners []*apitypes.Listener res, err = resty.New().R(). SetResult(&listeners). - Get(url + "/subscriptions?limit=1&after=" + l1.ID.String()) + Get(url + "/subscriptions?limit=1&after=" + l2.ID.String()) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, listeners, 1) - assert.Equal(t, l2.ID, listeners[0].ID) + assert.Equal(t, l1.ID, listeners[0].ID) assert.Equal(t, es1.ID, listeners[0].StreamID) mfc.AssertExpectations(t) diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index 7e2ae6ef..deb34d4b 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -22,11 +22,10 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" ) -func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (*policyengine.ManagedTXOutput, error) { +func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (*apitypes.ManagedTX, error) { // First job is to assign the next nonce to this request. // We block any further sends on this nonce until we've got this one successfully into the node, or @@ -52,8 +51,7 @@ func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes. // From this point on, we will guide this transaction through to submission. // We return an "ack" at this point, and dispatch the work of getting the transaction submitted // to the background worker. - mtx := &policyengine.ManagedTXOutput{ - FFTMName: m.name, + mtx := &apitypes.ManagedTX{ ID: request.Headers.ID, // on input the request ID must be the namespaced operation ID Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), Gas: prepared.Gas, diff --git a/pkg/policyengine/managed_tx.go b/pkg/policyengine/managed_tx.go deleted file mode 100644 index 64ebac61..00000000 --- a/pkg/policyengine/managed_tx.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 policyengine - -import ( - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" - "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" -) - -type ManagedTXError struct { - Time *fftypes.FFTime `json:"time"` - Error string `json:"error,omitempty"` - Mapped ffcapi.ErrorReason `json:"mapped,omitempty"` -} - -// ManagedTXOutput is the structure stored into the operation in FireFly, that the policy -// engine can use to apply policy, and apply updates to -type ManagedTXOutput struct { - FFTMName string `json:"fftmName"` - ID string `json:"id"` - Nonce *fftypes.FFBigInt `json:"nonce"` - Gas *fftypes.FFBigInt `json:"gas"` - TransactionHash string `json:"transactionHash,omitempty"` - TransactionData string `json:"transactionData,omitempty"` - GasPrice *fftypes.JSONAny `json:"gasPrice"` - PolicyInfo *fftypes.JSONAny `json:"policyInfo"` - FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` - LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` - Request *apitypes.TransactionRequest `json:"request,omitempty"` - Receipt *ffcapi.TransactionReceiptResponse `json:"receipt,omitempty"` - ErrorHistory []*ManagedTXError `json:"errorHistory"` - Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` -} diff --git a/pkg/policyengine/policyengine.go b/pkg/policyengine/policyengine.go index ba1ccee0..96478358 100644 --- a/pkg/policyengine/policyengine.go +++ b/pkg/policyengine/policyengine.go @@ -19,9 +19,10 @@ package policyengine import ( "context" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) type PolicyEngine interface { - Execute(ctx context.Context, cAPI ffcapi.API, mtx *ManagedTXOutput) (updated bool, reason ffcapi.ErrorReason, err error) + Execute(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (updated bool, reason ffcapi.ErrorReason, err error) } diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index abc30f82..78cabaed 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -30,6 +30,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) @@ -93,7 +94,7 @@ type simplePolicyInfo struct { } // withPolicyInfo is a convenience helper to run some logic that accesses/updates our policy section -func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *policyengine.ManagedTXOutput, fn func(info *simplePolicyInfo) (updated bool, reason ffcapi.ErrorReason, err error)) (updated bool, reason ffcapi.ErrorReason, err error) { +func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *apitypes.ManagedTX, fn func(info *simplePolicyInfo) (updated bool, reason ffcapi.ErrorReason, err error)) (updated bool, reason ffcapi.ErrorReason, err error) { var info simplePolicyInfo infoBytes := []byte(mtx.PolicyInfo.String()) if len(infoBytes) > 0 { @@ -110,7 +111,7 @@ func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *policyengi return updated, reason, err } -func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *policyengine.ManagedTXOutput) (updated bool, reason ffcapi.ErrorReason, err error) { +func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (updated bool, reason ffcapi.ErrorReason, err error) { // Simple policy engine only submits once. if mtx.FirstSubmit == nil { diff --git a/pkg/policyengines/simple/simple_policy_engine_test.go b/pkg/policyengines/simple/simple_policy_engine_test.go index 1c159a1f..351e0bf3 100644 --- a/pkg/policyengines/simple/simple_policy_engine_test.go +++ b/pkg/policyengines/simple/simple_policy_engine_test.go @@ -31,7 +31,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -62,7 +61,7 @@ func TestFixedGasPriceOK(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -130,7 +129,7 @@ func TestGasOracleSendOK(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -177,7 +176,7 @@ func TestConnectorGasOracleSendOK(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -224,7 +223,7 @@ func TestConnectorGasOracleFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -265,7 +264,7 @@ func TestGasOracleSendFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -325,7 +324,7 @@ func TestGasOracleTemplateExecuteFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -355,7 +354,7 @@ func TestGasOracleNonJSON(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -388,7 +387,7 @@ func TestTXSendFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -414,7 +413,7 @@ func TestWarnStaleWarningCannotParse(t *testing.T) { assert.NoError(t, err) submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: &submitTime, PolicyInfo: fftypes.JSONAnyPtr("!not json!"), @@ -446,7 +445,7 @@ func TestWarnStaleAdditionalWarning(t *testing.T) { submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.FFTime(time.Now().Add(-50 * time.Hour)) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -480,7 +479,7 @@ func TestWarnStaleNoWarning(t *testing.T) { submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.Now() - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -512,7 +511,7 @@ func TestNoOpWithReceipt(t *testing.T) { assert.NoError(t, err) submitTime := fftypes.Now() - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From 78e9b0d7b1986768e43cc2a521a7e84fd874d441 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 19 Jul 2022 23:51:42 -0400 Subject: [PATCH 50/95] Update persistence with r/w lock and index appraoch Signed-off-by: Peter Broadhurst --- internal/persistence/leveldb_persistence.go | 261 +++++++++---- .../persistence/leveldb_persistence_test.go | 363 ++++++++++-------- internal/persistence/persistence.go | 21 +- internal/tmmsgs/en_error_messges.go | 6 +- mocks/persistencemocks/persistence.go | 243 ++++-------- pkg/apitypes/managed_tx.go | 47 +-- 6 files changed, 504 insertions(+), 437 deletions(-) diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index 033c44fe..50bae1a2 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "sync" "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -29,6 +30,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/iterator" "github.com/syndtr/goleveldb/leveldb/opt" "github.com/syndtr/goleveldb/leveldb/util" ) @@ -36,6 +38,7 @@ import ( type leveldbPersistence struct { db *leveldb.DB syncWrites bool + txMux sync.RWMutex // allows us to draw conclusions on the cleanup of indexes } func NewLevelDBPersistence(ctx context.Context) (Persistence, error) { @@ -60,30 +63,47 @@ const eventstreamsPrefix = "eventstreams_0/" const eventstreamsEnd = "eventstreams_1" const listenersPrefix = "listeners_0/" const listenersEnd = "listeners_1" -const transactionsPrefix = "transactions_0/" -const transactionsEnd = "transactions_1" -const nonceAllocPrefix = "nonces_0/" -const inflightPrefix = "inflight_0/" -const inflightEnd = "inflight_1" +const transactionsPrefix = "tx_0/" +const nonceAllocationPrefix = "nonce_0/" +const txPendingIndexPrefix = "tx_inflight_0/" +const txPendingIndexEnd = "tx_inflight_1" +const txCreatedIndexPrefix = "tx_created_0/" +const txCreatedIndexEnd = "tx_created_1" -func signerNonceAllocPrefix(signer string) string { - return fmt.Sprintf("%s%s_0/", nonceAllocPrefix, signer) +func signerNoncePrefix(signer string) string { + return fmt.Sprintf("%s%s_0/", nonceAllocationPrefix, signer) } -func signerNonceKey(signer string, n int64) []byte { - return []byte(fmt.Sprintf("%s%s_0/%.12d", nonceAllocPrefix, signer, n)) +func signerNonceEnd(signer string) string { + return fmt.Sprintf("%s%s_1", nonceAllocationPrefix, signer) } -func signerNonceAllocEnd(signer string) string { - return fmt.Sprintf("%s%s_1", nonceAllocPrefix, signer) +func txNonceAllocationKey(signer string, nonce *fftypes.FFBigInt) []byte { + return []byte(fmt.Sprintf("%s%s_0/%.24d", nonceAllocationPrefix, signer, nonce.Int())) +} + +func txPendingIndexKey(sequenceID *fftypes.UUID) []byte { + return []byte(fmt.Sprintf("%s%s", txPendingIndexPrefix, sequenceID)) +} + +func txCreatedIndexKey(tx *apitypes.ManagedTX) []byte { + return []byte(fmt.Sprintf("%s%.19d/%s", txCreatedIndexPrefix, tx.Created.UnixNano(), tx.SequenceID)) +} + +func txDataKey(k string) []byte { + return []byte(fmt.Sprintf("%s%s", transactionsPrefix, k)) } func prefixedKey(prefix string, id fmt.Stringer) []byte { return []byte(fmt.Sprintf("%s%s", prefix, id)) } -func prefixedStrKey(prefix string, k string) []byte { - return []byte(fmt.Sprintf("%s%s", prefix, k)) +func (p *leveldbPersistence) writeKeyValue(ctx context.Context, key, value []byte) error { + err := p.db.Put(key, value, &opt.WriteOptions{Sync: p.syncWrites}) + if err != nil { + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceWriteFailed) + } + return nil } func (p *leveldbPersistence) writeJSON(ctx context.Context, key []byte, value interface{}) error { @@ -92,16 +112,32 @@ func (p *leveldbPersistence) writeJSON(ctx context.Context, key []byte, value in return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceMarshalFailed) } log.L(ctx).Debugf("Wrote %s", key) - return p.db.Put(key, b, &opt.WriteOptions{Sync: p.syncWrites}) + return p.writeKeyValue(ctx, key, b) } -func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target interface{}) error { +func (p *leveldbPersistence) getKeyValue(ctx context.Context, key []byte) ([]byte, error) { b, err := p.db.Get(key, &opt.ReadOptions{}) if err != nil { if err == leveldb.ErrNotFound { - return nil + return nil, nil } - return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceReadFailed) + return nil, i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceReadFailed, key) + } + return b, err +} + +func (p *leveldbPersistence) readJSONByIndex(ctx context.Context, idxKey []byte, target interface{}) error { + valKey, err := p.getKeyValue(ctx, idxKey) + if err != nil || valKey == nil { + return err + } + return p.readJSON(ctx, valKey, target) +} + +func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target interface{}) error { + b, err := p.getKeyValue(ctx, key) + if err != nil || b == nil { + return err } err = json.Unmarshal(b, target) if err != nil { @@ -114,8 +150,9 @@ func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target in func (p *leveldbPersistence) listJSON(ctx context.Context, collectionPrefix, collectionEnd, after string, limit int, val func() interface{}, // return a pointer to a pointer variable, of the type to unmarshal add func(interface{}), // passes back the val() for adding to the list, if the filters match + indexResolver func(ctx context.Context, k []byte) ([]byte, error), // if non-nil then the initial lookup will be passed to this, to lookup the target bytes. Nil skips item filters ...func(interface{}) bool, // filters to apply to the val() after unmarshalling -) error { +) ([][]byte, error) { collectionRange := &util.Range{ Start: []byte(collectionPrefix), Limit: []byte(collectionEnd), @@ -125,15 +162,34 @@ func (p *leveldbPersistence) listJSON(ctx context.Context, collectionPrefix, col } it := p.db.NewIterator(collectionRange, &opt.ReadOptions{DontFillCache: true}) defer it.Release() + return p.iterateReverseJSON(ctx, it, limit, val, add, indexResolver, filters...) +} + +func (p *leveldbPersistence) iterateReverseJSON(ctx context.Context, it iterator.Iterator, limit int, + val func() interface{}, add func(interface{}), indexResolver func(ctx context.Context, k []byte) ([]byte, error), filters ...func(interface{}) bool, +) (orphanedIdxKeys [][]byte, err error) { count := 0 next := it.Last // First iteration of the loop goes to the end itLoop: for next() { next = it.Prev // Future iterations call prev (note reverse sort order moving backwards through the selection) v := val() - err := json.Unmarshal(it.Value(), v) + b := it.Value() + if indexResolver != nil { + valKey := b + b, err = indexResolver(ctx, valKey) + if err != nil { + return nil, err + } + if b == nil { + log.L(ctx).Warnf("Skipping orphaned index key '%s' pointing to '%s'", it.Key(), valKey) + orphanedIdxKeys = append(orphanedIdxKeys, it.Key()) + continue + } + } + err := json.Unmarshal(b, v) if err != nil { - return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceUnmarshalFailed) + return nil, i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceUnmarshalFailed) } for _, f := range filters { if !f(v) { @@ -143,17 +199,17 @@ itLoop: add(v) count++ if limit > 0 && count >= limit { - return nil + break } } log.L(ctx).Debugf("Listed %d items", count) - return nil + return orphanedIdxKeys, nil } func (p *leveldbPersistence) deleteKeys(ctx context.Context, keys ...[]byte) error { for _, key := range keys { err := p.db.Delete(key, &opt.WriteOptions{Sync: p.syncWrites}) - if err != nil { + if err != nil && err != leveldb.ErrNotFound { return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceDeleteFailed) } log.L(ctx).Debugf("Deleted %s", key) @@ -176,9 +232,10 @@ func (p *leveldbPersistence) DeleteCheckpoint(ctx context.Context, streamID *fft func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) { streams := make([]*apitypes.EventStream, 0) - if err := p.listJSON(ctx, eventstreamsPrefix, eventstreamsEnd, after.String(), limit, + if _, err := p.listJSON(ctx, eventstreamsPrefix, eventstreamsEnd, after.String(), limit, func() interface{} { var v *apitypes.EventStream; return &v }, func(v interface{}) { streams = append(streams, *(v.(**apitypes.EventStream))) }, + nil, ); err != nil { return nil, err } @@ -200,9 +257,10 @@ func (p *leveldbPersistence) DeleteStream(ctx context.Context, streamID *fftypes func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { listeners := make([]*apitypes.Listener, 0) - if err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, + if _, err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, func() interface{} { var v *apitypes.Listener; return &v }, func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, + nil, ); err != nil { return nil, err } @@ -211,9 +269,10 @@ func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.U func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { listeners := make([]*apitypes.Listener, 0) - if err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, + if _, err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, func() interface{} { var v *apitypes.Listener; return &v }, func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, + nil, func(v interface{}) bool { return (*(v.(**apitypes.Listener))).StreamID.Equals(streamID) }, ); err != nil { return nil, err @@ -234,80 +293,130 @@ func (p *leveldbPersistence) DeleteListener(ctx context.Context, listenerID *fft return p.deleteKeys(ctx, prefixedKey(listenersPrefix, listenerID)) } -func (p *leveldbPersistence) ListManagedTransactions(ctx context.Context, after string, limit int) ([]*apitypes.ManagedTX, error) { - transactions := make([]*apitypes.ManagedTX, 0) - if err := p.listJSON(ctx, transactionsPrefix, transactionsEnd, after, limit, - func() interface{} { var v *apitypes.ManagedTX; return &v }, - func(v interface{}) { transactions = append(transactions, *(v.(**apitypes.ManagedTX))) }, - ); err != nil { +func (p *leveldbPersistence) indexLookupCallback(ctx context.Context, key []byte) ([]byte, error) { + b, err := p.getKeyValue(ctx, key) + switch { + case err != nil: return nil, err + case b == nil: + return nil, nil } - return transactions, nil + return b, err } -func (p *leveldbPersistence) GetManagedTransaction(ctx context.Context, txID string) (tx *apitypes.ManagedTX, err error) { - err = p.readJSON(ctx, prefixedStrKey(transactionsPrefix, txID), &tx) - return tx, err +func (p *leveldbPersistence) cleanupOrphanedTXIdxKeys(ctx context.Context, orphanedIdxKeys [][]byte) { + p.txMux.Lock() + defer p.txMux.Unlock() + err := p.deleteKeys(ctx, orphanedIdxKeys...) + if err != nil { + log.L(ctx).Warnf("Failed to clean up orphaned index keys: %s", err) + } } -func (p *leveldbPersistence) WriteManagedTransaction(ctx context.Context, tx *apitypes.ManagedTX) error { - return p.writeJSON(ctx, prefixedStrKey(transactionsPrefix, tx.ID), tx) -} +func (p *leveldbPersistence) listTransactionsByIndex(ctx context.Context, collectionPrefix, collectionEnd, afterStr string, limit int) ([]*apitypes.ManagedTX, error) { -func (p *leveldbPersistence) DeleteManagedTransaction(ctx context.Context, txID string) error { - return p.deleteKeys(ctx, prefixedStrKey(transactionsPrefix, txID)) + p.txMux.RLock() + transactions := make([]*apitypes.ManagedTX, 0) + orphanedIdxKeys, err := p.listJSON(ctx, collectionPrefix, collectionEnd, afterStr, limit, + func() interface{} { var v *apitypes.ManagedTX; return &v }, + func(v interface{}) { transactions = append(transactions, *(v.(**apitypes.ManagedTX))) }, + p.indexLookupCallback, + ) + p.txMux.RUnlock() + if err != nil { + return nil, err + } + // If we find orphaned index keys we clean them up - which requires the write lock (hence dropping read-lock first) + if len(orphanedIdxKeys) > 0 { + p.cleanupOrphanedTXIdxKeys(ctx, orphanedIdxKeys) + } + return transactions, nil } -func (p *leveldbPersistence) ListNonceAllocations(ctx context.Context, signer string, after *int64, limit int) ([]*apitypes.NonceAllocation, error) { - nonceAllocations := make([]*apitypes.NonceAllocation, 0) +func (p *leveldbPersistence) ListTransactionsByCreateTime(ctx context.Context, after *fftypes.FFTime, limit int) ([]*apitypes.ManagedTX, error) { afterStr := "" if after != nil { - afterStr = fmt.Sprintf("%.12d", *after) + afterStr = fmt.Sprintf("%.19d", after.UnixNano()) } - if err := p.listJSON(ctx, signerNonceAllocPrefix(signer), signerNonceAllocEnd(signer), afterStr, limit, - func() interface{} { var v *apitypes.NonceAllocation; return &v }, - func(v interface{}) { nonceAllocations = append(nonceAllocations, *(v.(**apitypes.NonceAllocation))) }, - ); err != nil { - return nil, err - } - return nonceAllocations, nil -} - -func (p *leveldbPersistence) GetNonceAllocation(ctx context.Context, signer string, nonce int64) (alloc *apitypes.NonceAllocation, err error) { - err = p.readJSON(ctx, signerNonceKey(signer, nonce), &alloc) - return alloc, err + return p.listTransactionsByIndex(ctx, txCreatedIndexPrefix, txCreatedIndexEnd, afterStr, limit) } -func (p *leveldbPersistence) WriteNonceAllocation(ctx context.Context, alloc *apitypes.NonceAllocation) error { - return p.writeJSON(ctx, signerNonceKey(alloc.Signer, alloc.Nonce), alloc) +func (p *leveldbPersistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int) ([]*apitypes.ManagedTX, error) { + afterStr := "" + if after != nil { + afterStr = fmt.Sprintf("%.24d", after.Int()) + } + return p.listTransactionsByIndex(ctx, signerNoncePrefix(signer), signerNonceEnd(signer), afterStr, limit) } -func (p *leveldbPersistence) DeleteNonceAllocation(ctx context.Context, signer string, nonce int64) error { - return p.deleteKeys(ctx, signerNonceKey(signer, nonce)) +func (p *leveldbPersistence) ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.ManagedTX, error) { + return p.listTransactionsByIndex(ctx, txPendingIndexPrefix, txPendingIndexEnd, after.String(), limit) } -func (p *leveldbPersistence) ListInflightTransactions(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.InflightTX, error) { - inflight := make([]*apitypes.InflightTX, 0) - if err := p.listJSON(ctx, inflightPrefix, inflightEnd, after.String(), limit, - func() interface{} { var v *apitypes.InflightTX; return &v }, - func(v interface{}) { inflight = append(inflight, *(v.(**apitypes.InflightTX))) }, - ); err != nil { - return nil, err - } - return inflight, nil +func (p *leveldbPersistence) GetTransactionByID(ctx context.Context, txID string) (tx *apitypes.ManagedTX, err error) { + p.txMux.RLock() + defer p.txMux.RUnlock() + err = p.readJSON(ctx, txDataKey(txID), &tx) + return tx, err } -func (p *leveldbPersistence) GetInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) (inflight *apitypes.InflightTX, err error) { - err = p.readJSON(ctx, prefixedKey(inflightPrefix, inflightID), &inflight) - return inflight, err +func (p *leveldbPersistence) GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (tx *apitypes.ManagedTX, err error) { + p.txMux.RLock() + defer p.txMux.RUnlock() + err = p.readJSONByIndex(ctx, txNonceAllocationKey(signer, nonce), &tx) + return tx, err } -func (p *leveldbPersistence) WriteInflightTransaction(ctx context.Context, inflight *apitypes.InflightTX) error { - return p.writeJSON(ctx, prefixedKey(inflightPrefix, inflight.ID), inflight) +func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, possiblyNew bool) (err error) { + // We take a write-lock here, because we are writing multiple values (the indexes), and anybody + // attempting to read the critical nonce allocation index must know the difference between a partial write + // (we crashed before we completed all the writes) and an incomplete write that's in process. + // The reading code detects partial writes and cleans them up if it finds them. + p.txMux.Lock() + defer p.txMux.Unlock() + + if tx.Request == nil || + tx.Request.From == "" || + tx.Nonce == nil || + tx.SequenceID == nil || + tx.Created == nil { + return i18n.NewError(ctx, tmmsgs.MsgPersistenceTXIncomplete) + } + idKey := txDataKey(tx.ID) + if possiblyNew { + // We write the index records first - because if we crash, we need to be able to know if the + // index records are valid or not. When reading, if there is an index key that does not have a + // corresponding + err = p.writeKeyValue(ctx, txCreatedIndexKey(tx), idKey) + if err == nil && tx.Status == apitypes.TxStatusPending { + err = p.writeKeyValue(ctx, txPendingIndexKey(tx.SequenceID), idKey) + } + if err == nil { + err = p.writeKeyValue(ctx, txNonceAllocationKey(tx.Request.From, tx.Nonce), idKey) + } + } + // If we are creating/updating a record that is not pending, we need to ensure there is no pending index associated with it + if err == nil && tx.Status != apitypes.TxStatusPending { + err = p.deleteKeys(ctx) + } + if err == nil { + err = p.writeJSON(ctx, idKey, tx) + } + return err } -func (p *leveldbPersistence) DeleteInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) error { - return p.deleteKeys(ctx, prefixedKey(inflightPrefix, inflightID)) +func (p *leveldbPersistence) DeleteTransaction(ctx context.Context, txID string) error { + var tx *apitypes.ManagedTX + err := p.readJSON(ctx, txDataKey(txID), &tx) + if err != nil || tx == nil { + return err + } + return p.deleteKeys(ctx, + txDataKey(txID), + txCreatedIndexKey(tx), + txPendingIndexKey(tx.SequenceID), + txNonceAllocationKey(tx.Request.From, tx.Nonce), + ) } func (p *leveldbPersistence) Close(ctx context.Context) { diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index d0858422..c3149b2c 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -265,173 +265,79 @@ func TestReadWriteManagedTransactions(t *testing.T) { defer done() ctx := context.Background() - t1 := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1/%s", apitypes.UUIDVersion1()), // ensure we get sequentially ascending IDs (not true in actual TXNs) - Nonce: fftypes.NewFFBigInt(10001), - } - p.WriteManagedTransaction(ctx, t1) - t2 := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1/%s", apitypes.UUIDVersion1()), - Nonce: fftypes.NewFFBigInt(10002), - } - p.WriteManagedTransaction(ctx, t2) - t3 := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1/%s", apitypes.UUIDVersion1()), - Nonce: fftypes.NewFFBigInt(10003), + textTX := func(signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { + tx := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + SequenceID: apitypes.UUIDVersion1(), + Nonce: fftypes.NewFFBigInt(nonce), + Created: fftypes.Now(), + Request: &apitypes.TransactionRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: signer, + }, + }, + }, + Status: status, + } + err := p.WriteTransaction(ctx, tx, true) + assert.NoError(t, err) + return tx } - p.WriteManagedTransaction(ctx, t3) - - txns, err := p.ListManagedTransactions(ctx, "", 0) - assert.NoError(t, err) - assert.Len(t, txns, 3) - - assert.Equal(t, t3.ID, txns[0].ID) - assert.Equal(t, t2.ID, txns[1].ID) - assert.Equal(t, t1.ID, txns[2].ID) - - // Test delete - err = p.DeleteManagedTransaction(ctx, t2.ID) - assert.NoError(t, err) - txns, err = p.ListManagedTransactions(ctx, "", 2) - assert.NoError(t, err) - assert.Len(t, txns, 2) - assert.Equal(t, t3.ID, txns[0].ID) - assert.Equal(t, t1.ID, txns[1].ID) - - // Test get direct + s1t1 := textTX("0xaaaaa", 10001, apitypes.TxStatusSucceeded) + s2t1 := textTX("0xbbbbb", 10001, apitypes.TxStatusFailed) + s1t2 := textTX("0xaaaaa", 10002, apitypes.TxStatusPending) + s1t3 := textTX("0xaaaaa", 10003, apitypes.TxStatusPending) - v, err := p.GetManagedTransaction(ctx, t3.ID) + txns, err := p.ListTransactionsByCreateTime(ctx, nil, 0) assert.NoError(t, err) - assert.Equal(t, t3.ID, v.ID) - assert.Equal(t, t3.Nonce, v.Nonce) - - v, err = p.GetManagedTransaction(ctx, t2.ID) - assert.NoError(t, err) - assert.Nil(t, v) -} + assert.Len(t, txns, 4) -func TestReadWriteNonceAllocations(t *testing.T) { + assert.Equal(t, s1t3.ID, txns[0].ID) + assert.Equal(t, s1t2.ID, txns[1].ID) + assert.Equal(t, s2t1.ID, txns[2].ID) + assert.Equal(t, s1t1.ID, txns[3].ID) - p, done := newTestLevelDBPersistence(t) - defer done() + // Only list pending - ctx := context.Background() - s1n1 := &apitypes.NonceAllocation{ - Signer: "0xaaaaaaa", - Nonce: 1001, - TX: "tx1", - } - p.WriteNonceAllocation(ctx, s1n1) - s2n1 := &apitypes.NonceAllocation{ - Signer: "0xbbbbbbb", - Nonce: 1001, - TX: "tx2", - } - p.WriteNonceAllocation(ctx, s2n1) - s1n2 := &apitypes.NonceAllocation{ - Signer: "0xaaaaaaa", - Nonce: 1002, - TX: "tx3", - } - p.WriteNonceAllocation(ctx, s1n2) - s1n3 := &apitypes.NonceAllocation{ - Signer: "0xaaaaaaa", - Nonce: 1003, - TX: "tx4", - } - p.WriteNonceAllocation(ctx, s1n3) - - nonces, err := p.ListNonceAllocations(ctx, "0xaaaaaaa", nil, 0) + txns, err = p.ListTransactionsPending(ctx, nil, 0) assert.NoError(t, err) - assert.Len(t, nonces, 3) - assert.Equal(t, s1n3.TX, nonces[0].TX) - assert.Equal(t, s1n2.TX, nonces[1].TX) - assert.Equal(t, s1n1.TX, nonces[2].TX) + assert.Len(t, txns, 2) - nonces, err = p.ListNonceAllocations(ctx, "0xbbbbbbb", nil, 0) - assert.NoError(t, err) - assert.Len(t, nonces, 1) - assert.Equal(t, s2n1.TX, nonces[0].TX) + assert.Equal(t, s1t3.ID, txns[0].ID) + assert.Equal(t, s1t2.ID, txns[1].ID) - // Test pagination + // List with time range - after := int64(1003) - nonces, err = p.ListNonceAllocations(ctx, "0xaaaaaaa", &after, 1) + txns, err = p.ListTransactionsByCreateTime(ctx, s1t2.Created, 0) assert.NoError(t, err) - assert.Len(t, nonces, 1) - assert.Equal(t, s1n2.TX, nonces[0].TX) + assert.Len(t, txns, 2) + assert.Equal(t, s2t1.ID, txns[0].ID) + assert.Equal(t, s1t1.ID, txns[1].ID) - // Test delete + // Test delete, and querying by nonce to limit TX returned - err = p.DeleteNonceAllocation(ctx, "0xaaaaaaa", s1n2.Nonce) + err = p.DeleteTransaction(ctx, s1t2.ID) assert.NoError(t, err) - nonces, err = p.ListNonceAllocations(ctx, "0xaaaaaaa", nil, 0) + txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t3.Nonce, 0) assert.NoError(t, err) - assert.Len(t, nonces, 2) - assert.Equal(t, s1n3.TX, nonces[0].TX) - assert.Equal(t, s1n1.TX, nonces[1].TX) + assert.Len(t, txns, 1) + assert.Equal(t, s1t1.ID, txns[0].ID) // Test get direct - v, err := p.GetNonceAllocation(ctx, "0xaaaaaaa", s1n3.Nonce) - assert.NoError(t, err) - assert.Equal(t, s1n3.Nonce, v.Nonce) - assert.Equal(t, s1n3.TX, v.TX) - - v, err = p.GetNonceAllocation(ctx, "0xaaaaaaa", s1n2.Nonce) - assert.NoError(t, err) - assert.Nil(t, v) -} - -func TestReadWriteInflightTransactions(t *testing.T) { - - p, done := newTestLevelDBPersistence(t) - defer done() - - ctx := context.Background() - t1 := &apitypes.InflightTX{ - ID: apitypes.UUIDVersion1(), // ensure we get sequentially ascending IDs - TX: "tx1", - } - p.WriteInflightTransaction(ctx, t1) - t2 := &apitypes.InflightTX{ - ID: apitypes.UUIDVersion1(), // ensure we get sequentially ascending IDs - TX: "tx2", - } - p.WriteInflightTransaction(ctx, t2) - t3 := &apitypes.InflightTX{ - ID: apitypes.UUIDVersion1(), // ensure we get sequentially ascending IDs - TX: "tx3", - } - p.WriteInflightTransaction(ctx, t3) - - txns, err := p.ListInflightTransactions(ctx, nil, 0) + v, err := p.GetTransactionByID(ctx, s1t3.ID) assert.NoError(t, err) - assert.Len(t, txns, 3) - - assert.Equal(t, t3.ID, txns[0].ID) - assert.Equal(t, t2.ID, txns[1].ID) - assert.Equal(t, t1.ID, txns[2].ID) - - // Test delete - - err = p.DeleteInflightTransaction(ctx, t2.ID) - assert.NoError(t, err) - txns, err = p.ListInflightTransactions(ctx, nil, 2) - assert.NoError(t, err) - assert.Len(t, txns, 2) - assert.Equal(t, t3.ID, txns[0].ID) - assert.Equal(t, t1.ID, txns[1].ID) - - // Test get direct + assert.Equal(t, s1t3.ID, v.ID) + assert.Equal(t, s1t3.Nonce, v.Nonce) - v, err := p.GetInflightTransaction(ctx, t3.ID) + v, err = p.GetTransactionByNonce(ctx, "0xbbbbb", s2t1.Nonce) assert.NoError(t, err) - assert.Equal(t, t3.ID, v.ID) - assert.Equal(t, t3.TX, v.TX) + assert.Equal(t, s2t1.ID, v.ID) + assert.Equal(t, s2t1.Nonce, v.Nonce) - v, err = p.GetInflightTransaction(ctx, t2.ID) + v, err = p.GetTransactionByID(ctx, s1t2.ID) assert.NoError(t, err) assert.Nil(t, v) } @@ -476,7 +382,7 @@ func TestDeleteStreamFail(t *testing.T) { } -func TestWriteCheckpointFail(t *testing.T) { +func TestWriteCheckpointFailMarshal(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() @@ -492,6 +398,22 @@ func TestWriteCheckpointFail(t *testing.T) { } +func TestWriteCheckpointFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + id1 := apitypes.UUIDVersion1() + err := p.WriteCheckpoint(context.Background(), &apitypes.EventStreamCheckpoint{ + Listeners: map[fftypes.UUID]json.RawMessage{ + *id1: json.RawMessage([]byte(`{}`)), + }, + }) + assert.Error(t, err) + +} + func TestReadListenerFail(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() @@ -520,23 +442,54 @@ func TestListManagedTransactionFail(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - sID := apitypes.UUIDVersion1() - err := p.db.Put(prefixedKey(transactionsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) + tx := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + Created: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), + } + err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) + assert.NoError(t, err) + err = p.db.Put(txDataKey(tx.ID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) - _, err = p.ListManagedTransactions(context.Background(), "", 0) + _, err = p.ListTransactionsByCreateTime(context.Background(), nil, 0) assert.Error(t, err) } +func TestListManagedTransactionCleanupOrphans(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + tx := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + Created: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), + } + err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) + assert.NoError(t, err) + + txns, err := p.ListTransactionsByCreateTime(context.Background(), nil, 0) + assert.NoError(t, err) + assert.Empty(t, txns) + + cleanedUpIndex, err := p.getKeyValue(context.Background(), txCreatedIndexKey(tx)) + assert.NoError(t, err) + assert.Nil(t, cleanedUpIndex) + +} + func TestListNonceAllocationsFail(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - err := p.db.Put(signerNonceKey("0xaaa", 12345), []byte("{! not json"), &opt.WriteOptions{}) + txID := fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()) + err := p.writeKeyValue(context.Background(), txNonceAllocationKey("0xaaa", fftypes.NewFFBigInt(12345)), txDataKey(txID)) + assert.NoError(t, err) + err = p.db.Put(txDataKey(txID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) - _, err = p.ListNonceAllocations(context.Background(), "0xaaa", nil, 0) + _, err = p.ListTransactionsByNonce(context.Background(), "0xaaa", nil, 0) assert.Error(t, err) } @@ -545,11 +498,115 @@ func TestListInflightTransactionFail(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - sID := apitypes.UUIDVersion1() - err := p.db.Put(prefixedKey(inflightPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) + txID := fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()) + err := p.writeKeyValue(context.Background(), txPendingIndexKey(apitypes.UUIDVersion1()), txDataKey(txID)) + assert.NoError(t, err) + err = p.db.Put(txDataKey(txID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) - _, err = p.ListInflightTransactions(context.Background(), nil, 0) + _, err = p.ListTransactionsPending(context.Background(), nil, 0) assert.Error(t, err) } + +func TestIndexLookupCallbackErr(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + p.Close(context.Background()) + + _, err := p.indexLookupCallback(context.Background(), ([]byte("any key"))) + assert.NotNil(t, err) + +} + +func TestIndexLookupCallbackNotFound(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + b, err := p.indexLookupCallback(context.Background(), ([]byte("any key"))) + assert.Nil(t, err) + assert.Nil(t, b) + +} + +func TestGetTransactionByNonceFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + p.Close(context.Background()) + + _, err := p.GetTransactionByNonce(context.Background(), "0xaaa", fftypes.NewFFBigInt(12345)) + assert.Regexp(t, "FF21055", err) + +} + +func TestIterateReverseJSONFailIdxResolve(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.writeKeyValue(context.Background(), []byte(`test_0/key`), []byte(`test/value`)) + assert.NoError(t, err) + _, err = p.listJSON(context.Background(), + "test_0/", + "test_1", + "", + 0, + func() interface{} { return make(map[string]interface{}) }, + func(i interface{}) {}, + func(ctx context.Context, k []byte) ([]byte, error) { + return nil, fmt.Errorf("pop") + }, + ) + assert.Regexp(t, "pop", err) + +} + +func TestIterateReverseJSONSkipIdxResolve(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.writeKeyValue(context.Background(), []byte(`test_0/key`), []byte(`test/value`)) + assert.NoError(t, err) + orphans, err := p.listJSON(context.Background(), + "test_0/", + "test_1", + "", + 0, + func() interface{} { return make(map[string]interface{}) }, + func(_ interface{}) { + assert.Fail(t, "Should not be called") + }, + func(ctx context.Context, k []byte) ([]byte, error) { + return nil, nil + }, + ) + assert.NoError(t, err) + assert.Len(t, orphans, 1) + +} + +func TestCleanupOrphanedTXIdxKeysSwallowError(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + p.Close(context.Background()) + + p.cleanupOrphanedTXIdxKeys(context.Background(), [][]byte{[]byte("test")}) + +} + +func TestWriteTransactionIncomplete(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.WriteTransaction(context.Background(), &apitypes.ManagedTX{}, true) + assert.Regexp(t, "FF21059", err) + +} + +func TestDeleteTransactionMissing(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.DeleteTransaction(context.Background(), "missing") + assert.NoError(t, err) + +} diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go index 4436599e..35822aac 100644 --- a/internal/persistence/persistence.go +++ b/internal/persistence/persistence.go @@ -39,20 +39,13 @@ type Persistence interface { WriteListener(ctx context.Context, spec *apitypes.Listener) error DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error - ListManagedTransactions(ctx context.Context, after string, limit int) ([]*apitypes.ManagedTX, error) // note there is no order to these, so list isn't very useful - GetManagedTransaction(ctx context.Context, txID string) (*apitypes.ManagedTX, error) - WriteManagedTransaction(ctx context.Context, tx *apitypes.ManagedTX) error - DeleteManagedTransaction(ctx context.Context, txID string) error - - ListNonceAllocations(ctx context.Context, signer string, after *int64, limit int) ([]*apitypes.NonceAllocation, error) // reverse nonce order - GetNonceAllocation(ctx context.Context, signer string, nonce int64) (*apitypes.NonceAllocation, error) - WriteNonceAllocation(ctx context.Context, alloc *apitypes.NonceAllocation) error - DeleteNonceAllocation(ctx context.Context, signer string, nonce int64) error - - ListInflightTransactions(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.InflightTX, error) // reverse UUIDv1 order - GetInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) (*apitypes.InflightTX, error) - WriteInflightTransaction(ctx context.Context, inflight *apitypes.InflightTX) error - DeleteInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) error + ListTransactionsByCreateTime(ctx context.Context, after *fftypes.FFTime, limit int) ([]*apitypes.ManagedTX, error) // reverse create time order + ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int) ([]*apitypes.ManagedTX, error) // reverse nonce order within signer + ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.ManagedTX, error) // reverse UUIDv1 order, only those in pending state + GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) + GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (*apitypes.ManagedTX, error) + WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, possiblyNew bool) error + DeleteTransaction(ctx context.Context, txID string) error Close(ctx context.Context) } diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 93976e92..cd97a65c 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -70,6 +70,8 @@ var ( MsgPersistenceMarshalFailed = ffe("FF21053", "JSON serialization failed while writing to persistence") MsgPersistenceUnmarshalFailed = ffe("FF21054", "JSON parsing failed while reading from persistence") MsgPersistenceReadFailed = ffe("FF21055", "Failed to read key '%s' from persistence") - MsgPersistenceDeleteFailed = ffe("FF21056", "Failed to delete key '%s' from persistence") - MsgPersistenceInitFailed = ffe("FF21057", "Failed to initialize persistence at path '%s'") + MsgPersistenceWriteFailed = ffe("FF21056", "Failed to read key '%s' from persistence") + MsgPersistenceDeleteFailed = ffe("FF21057", "Failed to delete key '%s' from persistence") + MsgPersistenceInitFailed = ffe("FF21058", "Failed to initialize persistence at path '%s'") + MsgPersistenceTXIncomplete = ffe("FF21059", "Transaction is missing indexed fields") ) diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go index 3f9aa206..b10311c2 100644 --- a/mocks/persistencemocks/persistence.go +++ b/mocks/persistencemocks/persistence.go @@ -36,20 +36,6 @@ func (_m *Persistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.U return r0 } -// DeleteInflightTransaction provides a mock function with given fields: ctx, inflightID -func (_m *Persistence) DeleteInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) error { - ret := _m.Called(ctx, inflightID) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { - r0 = rf(ctx, inflightID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // DeleteListener provides a mock function with given fields: ctx, listenerID func (_m *Persistence) DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error { ret := _m.Called(ctx, listenerID) @@ -64,27 +50,13 @@ func (_m *Persistence) DeleteListener(ctx context.Context, listenerID *fftypes.U return r0 } -// DeleteManagedTransaction provides a mock function with given fields: ctx, txID -func (_m *Persistence) DeleteManagedTransaction(ctx context.Context, txID string) error { - ret := _m.Called(ctx, txID) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, txID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// DeleteNonceAllocation provides a mock function with given fields: ctx, signer, nonce -func (_m *Persistence) DeleteNonceAllocation(ctx context.Context, signer string, nonce int64) error { - ret := _m.Called(ctx, signer, nonce) +// DeleteStream provides a mock function with given fields: ctx, streamID +func (_m *Persistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) error { + ret := _m.Called(ctx, streamID) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, int64) error); ok { - r0 = rf(ctx, signer, nonce) + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, streamID) } else { r0 = ret.Error(0) } @@ -92,13 +64,13 @@ func (_m *Persistence) DeleteNonceAllocation(ctx context.Context, signer string, return r0 } -// DeleteStream provides a mock function with given fields: ctx, streamID -func (_m *Persistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) error { - ret := _m.Called(ctx, streamID) +// DeleteTransaction provides a mock function with given fields: ctx, txID +func (_m *Persistence) DeleteTransaction(ctx context.Context, txID string) error { + ret := _m.Called(ctx, txID) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { - r0 = rf(ctx, streamID) + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, txID) } else { r0 = ret.Error(0) } @@ -129,22 +101,22 @@ func (_m *Persistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID return r0, r1 } -// GetInflightTransaction provides a mock function with given fields: ctx, inflightID -func (_m *Persistence) GetInflightTransaction(ctx context.Context, inflightID *fftypes.UUID) (*apitypes.InflightTX, error) { - ret := _m.Called(ctx, inflightID) +// GetListener provides a mock function with given fields: ctx, listenerID +func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) { + ret := _m.Called(ctx, listenerID) - var r0 *apitypes.InflightTX - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.InflightTX); ok { - r0 = rf(ctx, inflightID) + var r0 *apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.Listener); ok { + r0 = rf(ctx, listenerID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*apitypes.InflightTX) + r0 = ret.Get(0).(*apitypes.Listener) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { - r1 = rf(ctx, inflightID) + r1 = rf(ctx, listenerID) } else { r1 = ret.Error(1) } @@ -152,22 +124,22 @@ func (_m *Persistence) GetInflightTransaction(ctx context.Context, inflightID *f return r0, r1 } -// GetListener provides a mock function with given fields: ctx, listenerID -func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) { - ret := _m.Called(ctx, listenerID) +// GetStream provides a mock function with given fields: ctx, streamID +func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) { + ret := _m.Called(ctx, streamID) - var r0 *apitypes.Listener - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.Listener); ok { - r0 = rf(ctx, listenerID) + var r0 *apitypes.EventStream + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.EventStream); ok { + r0 = rf(ctx, streamID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*apitypes.Listener) + r0 = ret.Get(0).(*apitypes.EventStream) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { - r1 = rf(ctx, listenerID) + r1 = rf(ctx, streamID) } else { r1 = ret.Error(1) } @@ -175,8 +147,8 @@ func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID return r0, r1 } -// GetManagedTransaction provides a mock function with given fields: ctx, txID -func (_m *Persistence) GetManagedTransaction(ctx context.Context, txID string) (*apitypes.ManagedTX, error) { +// GetTransactionByID provides a mock function with given fields: ctx, txID +func (_m *Persistence) GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) { ret := _m.Called(ctx, txID) var r0 *apitypes.ManagedTX @@ -198,21 +170,21 @@ func (_m *Persistence) GetManagedTransaction(ctx context.Context, txID string) ( return r0, r1 } -// GetNonceAllocation provides a mock function with given fields: ctx, signer, nonce -func (_m *Persistence) GetNonceAllocation(ctx context.Context, signer string, nonce int64) (*apitypes.NonceAllocation, error) { +// GetTransactionByNonce provides a mock function with given fields: ctx, signer, nonce +func (_m *Persistence) GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (*apitypes.ManagedTX, error) { ret := _m.Called(ctx, signer, nonce) - var r0 *apitypes.NonceAllocation - if rf, ok := ret.Get(0).(func(context.Context, string, int64) *apitypes.NonceAllocation); ok { + var r0 *apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt) *apitypes.ManagedTX); ok { r0 = rf(ctx, signer, nonce) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*apitypes.NonceAllocation) + r0 = ret.Get(0).(*apitypes.ManagedTX) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.FFBigInt) error); ok { r1 = rf(ctx, signer, nonce) } else { r1 = ret.Error(1) @@ -221,22 +193,22 @@ func (_m *Persistence) GetNonceAllocation(ctx context.Context, signer string, no return r0, r1 } -// GetStream provides a mock function with given fields: ctx, streamID -func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) { - ret := _m.Called(ctx, streamID) +// ListListeners provides a mock function with given fields: ctx, after, limit +func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { + ret := _m.Called(ctx, after, limit) - var r0 *apitypes.EventStream - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.EventStream); ok { - r0 = rf(ctx, streamID) + var r0 []*apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.Listener); ok { + r0 = rf(ctx, after, limit) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*apitypes.EventStream) + r0 = ret.Get(0).([]*apitypes.Listener) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { - r1 = rf(ctx, streamID) + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int) error); ok { + r1 = rf(ctx, after, limit) } else { r1 = ret.Error(1) } @@ -244,22 +216,22 @@ func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (* return r0, r1 } -// ListInflightTransactions provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListInflightTransactions(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.InflightTX, error) { - ret := _m.Called(ctx, after, limit) +// ListStreamListeners provides a mock function with given fields: ctx, after, limit, streamID +func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { + ret := _m.Called(ctx, after, limit, streamID) - var r0 []*apitypes.InflightTX - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.InflightTX); ok { - r0 = rf(ctx, after, limit) + var r0 []*apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, *fftypes.UUID) []*apitypes.Listener); ok { + r0 = rf(ctx, after, limit, streamID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*apitypes.InflightTX) + r0 = ret.Get(0).([]*apitypes.Listener) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int) error); ok { - r1 = rf(ctx, after, limit) + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, *fftypes.UUID) error); ok { + r1 = rf(ctx, after, limit, streamID) } else { r1 = ret.Error(1) } @@ -267,16 +239,16 @@ func (_m *Persistence) ListInflightTransactions(ctx context.Context, after *ffty return r0, r1 } -// ListListeners provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { +// ListStreams provides a mock function with given fields: ctx, after, limit +func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) { ret := _m.Called(ctx, after, limit) - var r0 []*apitypes.Listener - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.Listener); ok { + var r0 []*apitypes.EventStream + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.EventStream); ok { r0 = rf(ctx, after, limit) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*apitypes.Listener) + r0 = ret.Get(0).([]*apitypes.EventStream) } } @@ -290,12 +262,12 @@ func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, l return r0, r1 } -// ListManagedTransactions provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListManagedTransactions(ctx context.Context, after string, limit int) ([]*apitypes.ManagedTX, error) { +// ListTransactionsByCreateTime provides a mock function with given fields: ctx, after, limit +func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after *fftypes.FFTime, limit int) ([]*apitypes.ManagedTX, error) { ret := _m.Called(ctx, after, limit) var r0 []*apitypes.ManagedTX - if rf, ok := ret.Get(0).(func(context.Context, string, int) []*apitypes.ManagedTX); ok { + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.FFTime, int) []*apitypes.ManagedTX); ok { r0 = rf(ctx, after, limit) } else { if ret.Get(0) != nil { @@ -304,7 +276,7 @@ func (_m *Persistence) ListManagedTransactions(ctx context.Context, after string } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, int) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.FFTime, int) error); ok { r1 = rf(ctx, after, limit) } else { r1 = ret.Error(1) @@ -313,21 +285,21 @@ func (_m *Persistence) ListManagedTransactions(ctx context.Context, after string return r0, r1 } -// ListNonceAllocations provides a mock function with given fields: ctx, signer, after, limit -func (_m *Persistence) ListNonceAllocations(ctx context.Context, signer string, after *int64, limit int) ([]*apitypes.NonceAllocation, error) { +// ListTransactionsByNonce provides a mock function with given fields: ctx, signer, after, limit +func (_m *Persistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int) ([]*apitypes.ManagedTX, error) { ret := _m.Called(ctx, signer, after, limit) - var r0 []*apitypes.NonceAllocation - if rf, ok := ret.Get(0).(func(context.Context, string, *int64, int) []*apitypes.NonceAllocation); ok { + var r0 []*apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt, int) []*apitypes.ManagedTX); ok { r0 = rf(ctx, signer, after, limit) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*apitypes.NonceAllocation) + r0 = ret.Get(0).([]*apitypes.ManagedTX) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, *int64, int) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.FFBigInt, int) error); ok { r1 = rf(ctx, signer, after, limit) } else { r1 = ret.Error(1) @@ -336,39 +308,16 @@ func (_m *Persistence) ListNonceAllocations(ctx context.Context, signer string, return r0, r1 } -// ListStreamListeners provides a mock function with given fields: ctx, after, limit, streamID -func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { - ret := _m.Called(ctx, after, limit, streamID) - - var r0 []*apitypes.Listener - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, *fftypes.UUID) []*apitypes.Listener); ok { - r0 = rf(ctx, after, limit, streamID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*apitypes.Listener) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, *fftypes.UUID) error); ok { - r1 = rf(ctx, after, limit, streamID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ListStreams provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) { +// ListTransactionsPending provides a mock function with given fields: ctx, after, limit +func (_m *Persistence) ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.ManagedTX, error) { ret := _m.Called(ctx, after, limit) - var r0 []*apitypes.EventStream - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.EventStream); ok { + var r0 []*apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.ManagedTX); ok { r0 = rf(ctx, after, limit) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*apitypes.EventStream) + r0 = ret.Get(0).([]*apitypes.ManagedTX) } } @@ -396,20 +345,6 @@ func (_m *Persistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes return r0 } -// WriteInflightTransaction provides a mock function with given fields: ctx, inflight -func (_m *Persistence) WriteInflightTransaction(ctx context.Context, inflight *apitypes.InflightTX) error { - ret := _m.Called(ctx, inflight) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *apitypes.InflightTX) error); ok { - r0 = rf(ctx, inflight) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // WriteListener provides a mock function with given fields: ctx, spec func (_m *Persistence) WriteListener(ctx context.Context, spec *apitypes.Listener) error { ret := _m.Called(ctx, spec) @@ -424,27 +359,13 @@ func (_m *Persistence) WriteListener(ctx context.Context, spec *apitypes.Listene return r0 } -// WriteManagedTransaction provides a mock function with given fields: ctx, tx -func (_m *Persistence) WriteManagedTransaction(ctx context.Context, tx *apitypes.ManagedTX) error { - ret := _m.Called(ctx, tx) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX) error); ok { - r0 = rf(ctx, tx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// WriteNonceAllocation provides a mock function with given fields: ctx, alloc -func (_m *Persistence) WriteNonceAllocation(ctx context.Context, alloc *apitypes.NonceAllocation) error { - ret := _m.Called(ctx, alloc) +// WriteStream provides a mock function with given fields: ctx, spec +func (_m *Persistence) WriteStream(ctx context.Context, spec *apitypes.EventStream) error { + ret := _m.Called(ctx, spec) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *apitypes.NonceAllocation) error); ok { - r0 = rf(ctx, alloc) + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStream) error); ok { + r0 = rf(ctx, spec) } else { r0 = ret.Error(0) } @@ -452,13 +373,13 @@ func (_m *Persistence) WriteNonceAllocation(ctx context.Context, alloc *apitypes return r0 } -// WriteStream provides a mock function with given fields: ctx, spec -func (_m *Persistence) WriteStream(ctx context.Context, spec *apitypes.EventStream) error { - ret := _m.Called(ctx, spec) +// WriteTransaction provides a mock function with given fields: ctx, tx, possiblyNew +func (_m *Persistence) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, possiblyNew bool) error { + ret := _m.Called(ctx, tx, possiblyNew) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStream) error); ok { - r0 = rf(ctx, spec) + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, bool) error); ok { + r0 = rf(ctx, tx, possiblyNew) } else { r0 = ret.Error(0) } diff --git a/pkg/apitypes/managed_tx.go b/pkg/apitypes/managed_tx.go index cd61ead0..1baccc3b 100644 --- a/pkg/apitypes/managed_tx.go +++ b/pkg/apitypes/managed_tx.go @@ -40,48 +40,33 @@ type ManagedTXError struct { Mapped ffcapi.ErrorReason `json:"mapped,omitempty"` } -// NonceAllocation is a mapping from an address + nonce, to a managed transaction. -// These are stored such that we can easily find the next nonce to assign to a managed transaction -// -// ** Stored first ** -// Because there's a non-zero chance we crash after writing this, and before writing the ManagedTX -// record. The nonce-allocation code must read the most recently written nonce allocation for the -// signer and check the TX object has been written. If not, it will re-allocate (cleaning up the -// in-flight record if it exists) +// ManagedTX is the structure stored for each new transaction request, using the external ID of the operation // -type NonceAllocation struct { - Signer string `json:"signer"` - Nonce int64 `json:"nonce"` - TX string `json:"tx"` -} - -// InflightTX is a UUIDv1 (so ordered) entry, that refers to an in-flight transaction that needs to be tracked. -// These are deleted when the transaction is complete. +// Indexing: +// Multiple index collection are stored for the managed transactions, to allow them to be managed including: // -// ** Stored second ** -// This means we might have a nonce+inflight record, but not have written the managed TX. -// The code that reads the in-flight TX list looks for this scenario, and clean up the orphaned in-flight record -// (if it gets to it before) -type InflightTX struct { - ID *fftypes.UUID `json:"id"` - TX string `json:"tx"` - Created *fftypes.FFTime `json:"created"` -} - -// ManagedTX is the structure stored for each new transaction request, using the external ID of the operation +// - Nonce allocation: this is a critical index, and why cleanup is so important (mentioned below). +// We use this index to determine the next nonce to assign to a given signing key. +// - Created time: a timestamp ordered index for the transactions for convenient ordering. +// the key includes the ID of the TX for uniqueness. +// - Pending sequence: An entry in this index only exists while the transaction is pending, and is +// ordered by a UUIDv1 sequence allocated to each entry. // -// ** Stored last ** -// This is persisted (along with the two objects above) before we reply to the API call to initiate a transaction, -// and is updated as the transaction progresses onto the chain. +// Index cleanup after partial write: +// - All indexes are stored before the TX itself. +// - When listing back entries, the persistence layer will automatically clean up indexes if the underlying +// TX they refer to is not available. For this reason the index records are written first. type ManagedTX struct { ID string `json:"id"` Status TxStatus `json:"status"` - Nonce *fftypes.FFBigInt `json:"nonce"` // persisted separately + SequenceID *fftypes.UUID `json:"sequenceId"` + Nonce *fftypes.FFBigInt `json:"nonce"` Gas *fftypes.FFBigInt `json:"gas"` TransactionHash string `json:"transactionHash,omitempty"` TransactionData string `json:"transactionData,omitempty"` GasPrice *fftypes.JSONAny `json:"gasPrice"` PolicyInfo *fftypes.JSONAny `json:"policyInfo"` + Created *fftypes.FFTime `json:"created"` FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` Request *TransactionRequest `json:"request,omitempty"` From ec91cc692fbb38e919991ef1a7c0dad0a4342a74 Mon Sep 17 00:00:00 2001 From: Nicko Guyer Date: Wed, 20 Jul 2022 12:23:19 -0400 Subject: [PATCH 51/95] Various fixes for fftm to work with core Signed-off-by: Nicko Guyer --- go.mod | 2 +- internal/confirmations/confirmations.go | 2 +- internal/events/eventstream.go | 4 +-- internal/tmconfig/tmconfig.go | 3 ++ internal/tmmsgs/en_error_messges.go | 2 ++ internal/ws/wsconn.go | 3 +- pkg/ffcapi/api.go | 2 +- pkg/fftm/api.go | 3 ++ pkg/fftm/ffcore.go | 4 +-- pkg/fftm/manager.go | 45 +++++++++++++++---------- pkg/fftm/route__root_command.go | 2 +- pkg/fftm/stream_management.go | 29 +++++++++------- 12 files changed, 61 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index c088fba4..84f68e10 100644 --- a/go.mod +++ b/go.mod @@ -56,4 +56,4 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) +) \ No newline at end of file diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index 980396eb..092fa8c1 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -514,7 +514,7 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { // The block might appear at any point in the confirmation list expectedParentHash := pending.blockHash - expectedBlockNumber := pending.blockNumber + 1 + expectedBlockNumber := pending.blockNumber for i := 0; i < (len(pending.confirmations) + 1); i++ { log.L(bcm.ctx).Tracef("Comparing block number=%d parent=%s to %d / %s for %s", blockNumber, block.ParentHash, expectedBlockNumber, expectedParentHash, pendingKey) if block.ParentHash == expectedParentHash && blockNumber == expectedBlockNumber { diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 3fa47ae7..3fb6d803 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -132,9 +132,7 @@ func NewEventStream( wsChannels: wsChannels, retry: esDefaults.retry, checkpointInterval: config.GetDuration(tmconfig.EventStreamsCheckpointInterval), - } - if config.GetInt(tmconfig.ConfirmationsRequired) > 0 { - es.confirmations = confirmations.NewBlockConfirmationManager(esCtx, connector) + confirmations: confirmations.NewBlockConfirmationManager(esCtx, connector), } // The configuration we have in memory, applies all the defaults to what is passed in // to ensure there are no nil fields on the configuration object. diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index d326d2ce..bb3ffa9a 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -59,6 +59,7 @@ var ( PersistenceLevelDBSyncWrites = ffc("persistence.leveldb.syncWrites") APIDefaultRequestTimeout = ffc("api.defaultRequestTimeout") APIMaxRequestTimeout = ffc("api.maxRequestTimeout") + FFCoreNamespaces = ffc("ffcore.namespaces") ) var FFCoreConfig config.Section @@ -104,6 +105,8 @@ func setDefaults() { viper.SetDefault(string(APIDefaultRequestTimeout), "30s") viper.SetDefault(string(APIMaxRequestTimeout), "10m") + + viper.SetDefault(string(FFCoreNamespaces), []string{}) } func Reset() { diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 93976e92..be2be439 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -72,4 +72,6 @@ var ( MsgPersistenceReadFailed = ffe("FF21055", "Failed to read key '%s' from persistence") MsgPersistenceDeleteFailed = ffe("FF21056", "Failed to delete key '%s' from persistence") MsgPersistenceInitFailed = ffe("FF21057", "Failed to initialize persistence at path '%s'") + MsgNamespacesEmpty = ffe("FF21058", "ffcore.namespaces must contain a list of namespaces") + MsgNotStarted = ffe("FF21059", "Connector has not fully started yet", 503) ) diff --git a/internal/ws/wsconn.go b/internal/ws/wsconn.go index 2968b4a0..bcf4b222 100644 --- a/internal/ws/wsconn.go +++ b/internal/ws/wsconn.go @@ -133,7 +133,8 @@ func (c *webSocketConnection) listenTopic(t *webSocketTopic) { } func (c *webSocketConnection) listenReplies() { - c.server.ListenForReplies(c) + // At this point, transaction manager does not send replies on the websocket + // Instead, operations are updated in Core via the SPI } func (c *webSocketConnection) listen() { diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 08353fee..07021a72 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -184,7 +184,7 @@ type TransactionInput struct { } type TransactionHeaders struct { - From string `json:"from"` + From string `json:"from,omitempty"` To string `json:"to,omitempty"` Nonce *fftypes.FFBigInt `json:"nonce,omitempty"` Gas *fftypes.FFBigInt `json:"gas,omitempty"` diff --git a/pkg/fftm/api.go b/pkg/fftm/api.go index 5f492175..7c453ffd 100644 --- a/pkg/fftm/api.go +++ b/pkg/fftm/api.go @@ -65,6 +65,9 @@ func (m *manager) router() *mux.Router { b, _ := json.Marshal(&doc) _, _ = res.Write(b) })) + + mux.HandleFunc("/ws", m.wsServer.Handler) + mux.NotFoundHandler = hf.APIWrapper(func(res http.ResponseWriter, req *http.Request) (status int, err error) { return 404, i18n.NewError(req.Context(), i18n.Msg404NotFound) }) diff --git a/pkg/fftm/ffcore.go b/pkg/fftm/ffcore.go index f27073df..99d4b8d5 100644 --- a/pkg/fftm/ffcore.go +++ b/pkg/fftm/ffcore.go @@ -89,7 +89,7 @@ func (m *manager) queryAndAddPending(nsOpID string) { } } -func (m *manager) readOperationPage(lastOp *core.Operation) ([]*core.Operation, error) { +func (m *manager) readOperationPage(ns string, lastOp *core.Operation) ([]*core.Operation, error) { var errorInfo fftypes.RESTError var ops []*core.Operation query := url.Values{ @@ -110,7 +110,7 @@ func (m *manager) readOperationPage(lastOp *core.Operation) ([]*core.Operation, SetQueryParamsFromValues(query). SetResult(&ops). SetError(&errorInfo). - Get("/spi/v1/operations") + Get(fmt.Sprintf("/spi/v1/namespaces/%s/operations", ns)) if err != nil { return nil, i18n.WrapError(m.ctx, err, tmmsgs.MsgCoreError, -1, err) } diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index e047f263..f1e59c97 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -57,7 +57,7 @@ type manager struct { apiServer httpserver.HTTPServer ffCoreClient *resty.Client wsClient wsclient.WSClient - wsChannels ws.WebSocketChannels + wsServer ws.WebSocketServer persistence persistence.Persistence mux sync.Mutex @@ -82,6 +82,7 @@ type manager struct { policyLoopInterval time.Duration errorHistoryCount int enableChangeListener bool + namespaces []string } func InitConfig() { @@ -105,6 +106,7 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { if err != nil { return nil, err } + m.wsServer = ws.NewWebSocketServer(ctx) m.apiServer, err = httpserver.NewHTTPServer(ctx, "api", m.router(), m.apiServerDone, tmconfig.APIConfig, tmconfig.CorsConfig) if err != nil { return nil, err @@ -112,6 +114,11 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { if err = m.initPersistence(ctx); err != nil { return nil, err } + if namespaces := config.GetStringSlice(tmconfig.FFCoreNamespaces); len(namespaces) > 0 { + m.namespaces = namespaces + } else { + return nil, i18n.NewError(ctx, tmmsgs.MsgNamespacesEmpty) + } return m, nil } @@ -223,23 +230,27 @@ func (m *manager) fullScan() error { var page int64 var read, added int var lastOp *core.Operation - for { - ops, err := m.readOperationPage(lastOp) - if err != nil { - return err - } - if len(ops) == 0 { - log.L(m.ctx).Debugf("Finished reading all operations - %d read, %d added", read, added) - return nil - } - lastOp = ops[len(ops)-1] - read += len(ops) - for _, op := range ops { - added++ - m.trackIfManaged(op) + for _, ns := range m.namespaces { + page = 0 + for { + ops, err := m.readOperationPage(ns, lastOp) + if err != nil { + return err + } + if len(ops) == 0 { + log.L(m.ctx).Debugf("Finished reading all operations for namespace '%s' - %d read, %d added", ns, read, added) + break + } + lastOp = ops[len(ops)-1] + read += len(ops) + for _, op := range ops { + added++ + m.trackIfManaged(op) + } + page++ } - page++ } + return nil } func (m *manager) trackIfManaged(op *core.Operation) { @@ -297,6 +308,7 @@ func (m *manager) Start() error { m.firstFullScanDone = make(chan error) m.fullScanLoopDone = make(chan struct{}) go m.fullScanLoop() + go m.runAPIServer() err := m.waitForFirstScanAndStart() if err != nil { return err @@ -318,7 +330,6 @@ func (m *manager) waitForFirstScanAndStart() error { log.L(m.ctx).Infof("Scan complete. Completing startup") m.policyLoopDone = make(chan struct{}) go m.receiptPollingLoop() - go m.runAPIServer() go m.confirmations.Start() err := m.startWS() if err == nil { diff --git a/pkg/fftm/route__root_command.go b/pkg/fftm/route__root_command.go index 526d09c8..fc771edd 100644 --- a/pkg/fftm/route__root_command.go +++ b/pkg/fftm/route__root_command.go @@ -72,7 +72,7 @@ var postRootCommand = func(m *manager) *ffapi.Route { res, _, err := m.connector.QueryInvoke(r.Req.Context(), &ffcapi.QueryInvokeRequest{ TransactionInput: tReq.TransactionInput, }) - return res, err + return res.Outputs, err default: return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgUnsupportedRequestType, baseReq.Headers.Type) } diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index 7d0a5cb7..08af226d 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -48,17 +48,21 @@ func (m *manager) restoreStreams() error { if err != nil { return err } - closeoutName, err := m.reserveStreamName(m.ctx, *def.Name, def.ID) - var s events.Stream - if err == nil { - s, err = m.addRuntimeStream(def, streamListeners) - } - if err == nil && !*def.Suspended { - err = s.Start(m.ctx) - } - closeoutName(err == nil) - if err != nil { - return err + // check to see if it's already started + if m.eventStreams[*def.ID] == nil { + closeoutName, err := m.reserveStreamName(m.ctx, *def.Name, def.ID) + var s events.Stream + if err == nil { + s, err = m.addRuntimeStream(def, streamListeners) + } + if err == nil && !*def.Suspended { + + err = s.Start(m.ctx) + } + closeoutName(err == nil) + if err != nil { + return err + } } } } @@ -86,7 +90,7 @@ func (m *manager) deleteAllStreamListeners(ctx context.Context, streamID *fftype } func (m *manager) addRuntimeStream(def *apitypes.EventStream, listeners []*apitypes.Listener) (events.Stream, error) { - s, err := events.NewEventStream(m.ctx, def, m.connector, m.persistence, m.wsChannels, listeners) + s, err := events.NewEventStream(m.ctx, def, m.connector, m.persistence, m.wsServer, listeners) if err != nil { return nil, err } @@ -122,7 +126,6 @@ func (m *manager) deleteStream(ctx context.Context, idStr string) error { } func (m *manager) reserveStreamName(ctx context.Context, name string, id *fftypes.UUID) (func(bool), error) { - m.mux.Lock() defer m.mux.Unlock() From a5ff6ef5ee642a2586a4b0fedd8343358cce2e48 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 20 Jul 2022 15:51:45 -0400 Subject: [PATCH 52/95] Complete the Signed-off-by: Peter Broadhurst --- internal/persistence/leveldb_persistence.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index 50bae1a2..d59b37d4 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -385,8 +385,10 @@ func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes. idKey := txDataKey(tx.ID) if possiblyNew { // We write the index records first - because if we crash, we need to be able to know if the - // index records are valid or not. When reading, if there is an index key that does not have a - // corresponding + // index records are valid or not. When reading under the read lock, if there is an index key + // that does not have a corresponding managed TX available, we will clean up the + // orphaned index (after swapping the read lock for the write lock) + // See listTransactionsByIndex() for the other half of this logic. err = p.writeKeyValue(ctx, txCreatedIndexKey(tx), idKey) if err == nil && tx.Status == apitypes.TxStatusPending { err = p.writeKeyValue(ctx, txPendingIndexKey(tx.SequenceID), idKey) From 064586b91fd9a7728350668d863ba1eeb6601093 Mon Sep 17 00:00:00 2001 From: Nicko Guyer Date: Wed, 20 Jul 2022 16:37:59 -0400 Subject: [PATCH 53/95] Additional fixes for fftm to work with core Signed-off-by: Nicko Guyer --- internal/confirmations/confirmations.go | 2 +- internal/events/eventstream.go | 9 +++++++-- internal/events/websockets.go | 3 +++ pkg/apitypes/api_types.go | 1 + pkg/fftm/manager.go | 7 ++----- pkg/fftm/stream_management.go | 3 +-- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index 092fa8c1..980396eb 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -514,7 +514,7 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { // The block might appear at any point in the confirmation list expectedParentHash := pending.blockHash - expectedBlockNumber := pending.blockNumber + expectedBlockNumber := pending.blockNumber + 1 for i := 0; i < (len(pending.confirmations) + 1); i++ { log.L(bcm.ctx).Tracef("Comparing block number=%d parent=%s to %d / %s for %s", blockNumber, block.ParentHash, expectedBlockNumber, expectedParentHash, pendingKey) if block.ParentHash == expectedParentHash && blockNumber == expectedBlockNumber { diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 3fb6d803..d17061ed 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -58,6 +58,7 @@ var esDefaults struct { blockedRetryDelay fftypes.FFDuration webhookRequestTimeout fftypes.FFDuration websocketDistributionMode apitypes.DistributionMode + topic string retry *retry.Retry } @@ -132,7 +133,9 @@ func NewEventStream( wsChannels: wsChannels, retry: esDefaults.retry, checkpointInterval: config.GetDuration(tmconfig.EventStreamsCheckpointInterval), - confirmations: confirmations.NewBlockConfirmationManager(esCtx, connector), + } + if config.GetInt(tmconfig.ConfirmationsRequired) > 0 { + es.confirmations = confirmations.NewBlockConfirmationManager(esCtx, connector) } // The configuration we have in memory, applies all the defaults to what is passed in // to ensure there are no nil fields on the configuration object. @@ -509,7 +512,9 @@ func (es *eventStream) Start(ctx context.Context) error { go es.blockListener(startedState) // Start the confirmations manager - es.confirmations.Start() + if es.confirmations != nil { + es.confirmations.Start() + } return err } diff --git a/internal/events/websockets.go b/internal/events/websockets.go index 45351831..7a7e9368 100644 --- a/internal/events/websockets.go +++ b/internal/events/websockets.go @@ -48,6 +48,9 @@ func mergeValidateWsConfig(ctx context.Context, changed bool, base *apitypes.Web return nil, false, i18n.NewError(ctx, tmmsgs.MsgInvalidDistributionMode, *merged.DistributionMode) } + // Topic + changed = apitypes.CheckUpdateString(changed, &merged.Topic, base.Topic, updates.Topic, esDefaults.topic) + return merged, changed, nil } diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 748b256e..ae4a2a6c 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -97,6 +97,7 @@ type WebhookConfig struct { type WebSocketConfig struct { DistributionMode *DistributionMode `ffstruct:"wsconfig" json:"distributionMode,omitempty"` + Topic *string `ffstruct:"wsconfig" json:"topic,omitempty"` } type Listener struct { diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index f1e59c97..d46b60e0 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -309,11 +309,8 @@ func (m *manager) Start() error { m.fullScanLoopDone = make(chan struct{}) go m.fullScanLoop() go m.runAPIServer() - err := m.waitForFirstScanAndStart() - if err != nil { - return err - } - return m.restoreStreams() + go m.restoreStreams() + return m.waitForFirstScanAndStart() } func (m *manager) waitForFirstScanAndStart() error { diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index 08af226d..3da77910 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -56,13 +56,12 @@ func (m *manager) restoreStreams() error { s, err = m.addRuntimeStream(def, streamListeners) } if err == nil && !*def.Suspended { - err = s.Start(m.ctx) } - closeoutName(err == nil) if err != nil { return err } + closeoutName(err == nil) } } } From 1526ec81132b3c264dc7b0fdc6bd819c955dc051 Mon Sep 17 00:00:00 2001 From: Nicko Guyer Date: Wed, 20 Jul 2022 16:40:25 -0400 Subject: [PATCH 54/95] Resolve conflicting error codes Signed-off-by: Nicko Guyer --- internal/tmmsgs/en_error_messges.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index be2be439..eaf4befc 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -73,5 +73,6 @@ var ( MsgPersistenceDeleteFailed = ffe("FF21056", "Failed to delete key '%s' from persistence") MsgPersistenceInitFailed = ffe("FF21057", "Failed to initialize persistence at path '%s'") MsgNamespacesEmpty = ffe("FF21058", "ffcore.namespaces must contain a list of namespaces") - MsgNotStarted = ffe("FF21059", "Connector has not fully started yet", 503) + MsgPersistenceTXIncomplete = ffe("FF21059", "Transaction is missing indexed fields") + MsgNotStarted = ffe("FF21060", "Connector has not fully started yet", 503) ) From 0e40381ada6448f86b2b6661362985870a550a08 Mon Sep 17 00:00:00 2001 From: Nicko Guyer Date: Wed, 20 Jul 2022 16:49:02 -0400 Subject: [PATCH 55/95] Merge remote-tracking branch 'origin/events' into events-fixes Signed-off-by: Nicko Guyer --- .vscode/settings.json | 1 + config.md | 6 - internal/persistence/leveldb_persistence.go | 287 ++++++++++++++--- .../persistence/leveldb_persistence_test.go | 301 +++++++++++++++++- internal/persistence/persistence.go | 12 +- internal/tmconfig/tmconfig.go | 1 - internal/tmmsgs/en_config_descriptions.go | 2 - internal/tmmsgs/en_error_messges.go | 7 +- mocks/persistencemocks/persistence.go | 143 +++++++++ mocks/policyenginemocks/policy_engine.go | 13 +- pkg/{policyengine => apitypes}/managed_tx.go | 41 ++- pkg/fftm/ffcore.go | 10 +- pkg/fftm/manager.go | 16 +- pkg/fftm/manager_test.go | 93 ++---- pkg/fftm/nonces.go | 4 +- pkg/fftm/nonces_test.go | 3 +- pkg/fftm/policyloop.go | 10 +- pkg/fftm/policyloop_test.go | 17 +- pkg/fftm/route__root_command.go | 2 +- .../route_get_eventstream_listeners_test.go | 4 +- pkg/fftm/route_get_eventstreams_test.go | 4 +- pkg/fftm/route_get_subscriptions_test.go | 4 +- pkg/fftm/send_tx.go | 6 +- pkg/policyengine/policyengine.go | 3 +- .../simple/simple_policy_engine.go | 5 +- .../simple/simple_policy_engine_test.go | 25 +- 26 files changed, 800 insertions(+), 220 deletions(-) rename pkg/{policyengine => apitypes}/managed_tx.go (51%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2d4050fc..07d61c95 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,6 +76,7 @@ "txcommon", "txcommonmocks", "txid", + "txns", "txtype", "unflushed", "unmarshalled", diff --git a/config.md b/config.md index 27579fc5..1ba2ea54 100644 --- a/config.md +++ b/config.md @@ -164,12 +164,6 @@ nav_order: 2 |message|Configures the JSON key containing the log message|`string`|`message` |timestamp|Configures the JSON key containing the timestamp of the log|`string`|`@timestamp` -## manager - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|name|The name of this Transaction Manager, used in operation metadata to track which operations are to be updated|`string`|`` - ## operations |Key|Description|Type|Default Value| diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index f8022f87..d59b37d4 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -17,11 +17,10 @@ package persistence import ( - "bytes" "context" "encoding/json" "fmt" - "strings" + "sync" "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -31,6 +30,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/iterator" "github.com/syndtr/goleveldb/leveldb/opt" "github.com/syndtr/goleveldb/leveldb/util" ) @@ -38,6 +38,7 @@ import ( type leveldbPersistence struct { db *leveldb.DB syncWrites bool + txMux sync.RWMutex // allows us to draw conclusions on the cleanup of indexes } func NewLevelDBPersistence(ctx context.Context) (Persistence, error) { @@ -57,16 +58,52 @@ func NewLevelDBPersistence(ctx context.Context) (Persistence, error) { }, nil } -func (p *leveldbPersistence) checkpointKey(streamID *fftypes.UUID) []byte { - return []byte(fmt.Sprintf("checkpoints/%s", streamID)) +const checkpointsPrefix = "checkpoints_0/" +const eventstreamsPrefix = "eventstreams_0/" +const eventstreamsEnd = "eventstreams_1" +const listenersPrefix = "listeners_0/" +const listenersEnd = "listeners_1" +const transactionsPrefix = "tx_0/" +const nonceAllocationPrefix = "nonce_0/" +const txPendingIndexPrefix = "tx_inflight_0/" +const txPendingIndexEnd = "tx_inflight_1" +const txCreatedIndexPrefix = "tx_created_0/" +const txCreatedIndexEnd = "tx_created_1" + +func signerNoncePrefix(signer string) string { + return fmt.Sprintf("%s%s_0/", nonceAllocationPrefix, signer) +} + +func signerNonceEnd(signer string) string { + return fmt.Sprintf("%s%s_1", nonceAllocationPrefix, signer) +} + +func txNonceAllocationKey(signer string, nonce *fftypes.FFBigInt) []byte { + return []byte(fmt.Sprintf("%s%s_0/%.24d", nonceAllocationPrefix, signer, nonce.Int())) +} + +func txPendingIndexKey(sequenceID *fftypes.UUID) []byte { + return []byte(fmt.Sprintf("%s%s", txPendingIndexPrefix, sequenceID)) +} + +func txCreatedIndexKey(tx *apitypes.ManagedTX) []byte { + return []byte(fmt.Sprintf("%s%.19d/%s", txCreatedIndexPrefix, tx.Created.UnixNano(), tx.SequenceID)) } -func (p *leveldbPersistence) streamKey(streamID *fftypes.UUID) []byte { - return []byte(fmt.Sprintf("eventstreams/%s", streamID)) +func txDataKey(k string) []byte { + return []byte(fmt.Sprintf("%s%s", transactionsPrefix, k)) } -func (p *leveldbPersistence) listenerKey(listenerID *fftypes.UUID) []byte { - return []byte(fmt.Sprintf("listeners/%s", listenerID)) +func prefixedKey(prefix string, id fmt.Stringer) []byte { + return []byte(fmt.Sprintf("%s%s", prefix, id)) +} + +func (p *leveldbPersistence) writeKeyValue(ctx context.Context, key, value []byte) error { + err := p.db.Put(key, value, &opt.WriteOptions{Sync: p.syncWrites}) + if err != nil { + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceWriteFailed) + } + return nil } func (p *leveldbPersistence) writeJSON(ctx context.Context, key []byte, value interface{}) error { @@ -75,16 +112,32 @@ func (p *leveldbPersistence) writeJSON(ctx context.Context, key []byte, value in return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceMarshalFailed) } log.L(ctx).Debugf("Wrote %s", key) - return p.db.Put(key, b, &opt.WriteOptions{Sync: p.syncWrites}) + return p.writeKeyValue(ctx, key, b) } -func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target interface{}) error { +func (p *leveldbPersistence) getKeyValue(ctx context.Context, key []byte) ([]byte, error) { b, err := p.db.Get(key, &opt.ReadOptions{}) if err != nil { if err == leveldb.ErrNotFound { - return nil + return nil, nil } - return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceReadFailed) + return nil, i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceReadFailed, key) + } + return b, err +} + +func (p *leveldbPersistence) readJSONByIndex(ctx context.Context, idxKey []byte, target interface{}) error { + valKey, err := p.getKeyValue(ctx, idxKey) + if err != nil || valKey == nil { + return err + } + return p.readJSON(ctx, valKey, target) +} + +func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target interface{}) error { + b, err := p.getKeyValue(ctx, key) + if err != nil || b == nil { + return err } err = json.Unmarshal(b, target) if err != nil { @@ -94,32 +147,49 @@ func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target in return nil } -func (p *leveldbPersistence) listJSON(ctx context.Context, prefix, after string, limit int, +func (p *leveldbPersistence) listJSON(ctx context.Context, collectionPrefix, collectionEnd, after string, limit int, val func() interface{}, // return a pointer to a pointer variable, of the type to unmarshal add func(interface{}), // passes back the val() for adding to the list, if the filters match + indexResolver func(ctx context.Context, k []byte) ([]byte, error), // if non-nil then the initial lookup will be passed to this, to lookup the target bytes. Nil skips item filters ...func(interface{}) bool, // filters to apply to the val() after unmarshalling -) error { - rangeStart := &util.Range{Start: []byte(prefix)} +) ([][]byte, error) { + collectionRange := &util.Range{ + Start: []byte(collectionPrefix), + Limit: []byte(collectionEnd), + } if after != "" { - rangeStart.Start = []byte(prefix + after) + collectionRange.Limit = []byte(collectionPrefix + after) } - it := p.db.NewIterator(rangeStart, &opt.ReadOptions{DontFillCache: true}) + it := p.db.NewIterator(collectionRange, &opt.ReadOptions{DontFillCache: true}) defer it.Release() + return p.iterateReverseJSON(ctx, it, limit, val, add, indexResolver, filters...) +} + +func (p *leveldbPersistence) iterateReverseJSON(ctx context.Context, it iterator.Iterator, limit int, + val func() interface{}, add func(interface{}), indexResolver func(ctx context.Context, k []byte) ([]byte, error), filters ...func(interface{}) bool, +) (orphanedIdxKeys [][]byte, err error) { count := 0 - skippedAfter := false + next := it.Last // First iteration of the loop goes to the end itLoop: - for it.Next() { - if after != "" && !skippedAfter && bytes.Equal(it.Key(), rangeStart.Start) { - skippedAfter = true // need to skip the first one, as the range is inclusive - continue - } - if !strings.HasPrefix(string(it.Key()), prefix) { - break itLoop - } + for next() { + next = it.Prev // Future iterations call prev (note reverse sort order moving backwards through the selection) v := val() - err := json.Unmarshal(it.Value(), v) + b := it.Value() + if indexResolver != nil { + valKey := b + b, err = indexResolver(ctx, valKey) + if err != nil { + return nil, err + } + if b == nil { + log.L(ctx).Warnf("Skipping orphaned index key '%s' pointing to '%s'", it.Key(), valKey) + orphanedIdxKeys = append(orphanedIdxKeys, it.Key()) + continue + } + } + err := json.Unmarshal(b, v) if err != nil { - return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceUnmarshalFailed) + return nil, i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceUnmarshalFailed) } for _, f := range filters { if !f(v) { @@ -129,17 +199,17 @@ itLoop: add(v) count++ if limit > 0 && count >= limit { - return nil + break } } log.L(ctx).Debugf("Listed %d items", count) - return nil + return orphanedIdxKeys, nil } func (p *leveldbPersistence) deleteKeys(ctx context.Context, keys ...[]byte) error { for _, key := range keys { err := p.db.Delete(key, &opt.WriteOptions{Sync: p.syncWrites}) - if err != nil { + if err != nil && err != leveldb.ErrNotFound { return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceDeleteFailed) } log.L(ctx).Debugf("Deleted %s", key) @@ -148,23 +218,24 @@ func (p *leveldbPersistence) deleteKeys(ctx context.Context, keys ...[]byte) err } func (p *leveldbPersistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error { - return p.writeJSON(ctx, p.checkpointKey(checkpoint.StreamID), checkpoint) + return p.writeJSON(ctx, prefixedKey(checkpointsPrefix, checkpoint.StreamID), checkpoint) } func (p *leveldbPersistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (cp *apitypes.EventStreamCheckpoint, err error) { - err = p.readJSON(ctx, p.checkpointKey(streamID), &cp) + err = p.readJSON(ctx, prefixedKey(checkpointsPrefix, streamID), &cp) return cp, err } func (p *leveldbPersistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error { - return p.deleteKeys(ctx, p.checkpointKey(streamID)) + return p.deleteKeys(ctx, prefixedKey(checkpointsPrefix, streamID)) } func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) { streams := make([]*apitypes.EventStream, 0) - if err := p.listJSON(ctx, "eventstreams/", after.String(), limit, + if _, err := p.listJSON(ctx, eventstreamsPrefix, eventstreamsEnd, after.String(), limit, func() interface{} { var v *apitypes.EventStream; return &v }, func(v interface{}) { streams = append(streams, *(v.(**apitypes.EventStream))) }, + nil, ); err != nil { return nil, err } @@ -172,23 +243,24 @@ func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUI } func (p *leveldbPersistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (es *apitypes.EventStream, err error) { - err = p.readJSON(ctx, p.streamKey(streamID), &es) + err = p.readJSON(ctx, prefixedKey(eventstreamsPrefix, streamID), &es) return es, err } func (p *leveldbPersistence) WriteStream(ctx context.Context, spec *apitypes.EventStream) error { - return p.writeJSON(ctx, p.streamKey(spec.ID), spec) + return p.writeJSON(ctx, prefixedKey(eventstreamsPrefix, spec.ID), spec) } func (p *leveldbPersistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) error { - return p.deleteKeys(ctx, p.streamKey(streamID)) + return p.deleteKeys(ctx, prefixedKey(eventstreamsPrefix, streamID)) } func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { listeners := make([]*apitypes.Listener, 0) - if err := p.listJSON(ctx, "listeners/", after.String(), limit, + if _, err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, func() interface{} { var v *apitypes.Listener; return &v }, func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, + nil, ); err != nil { return nil, err } @@ -197,9 +269,10 @@ func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.U func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { listeners := make([]*apitypes.Listener, 0) - if err := p.listJSON(ctx, "listeners/", after.String(), limit, + if _, err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, func() interface{} { var v *apitypes.Listener; return &v }, func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, + nil, func(v interface{}) bool { return (*(v.(**apitypes.Listener))).StreamID.Equals(streamID) }, ); err != nil { return nil, err @@ -208,16 +281,144 @@ func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fft } func (p *leveldbPersistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (l *apitypes.Listener, err error) { - err = p.readJSON(ctx, p.listenerKey(listenerID), &l) + err = p.readJSON(ctx, prefixedKey(listenersPrefix, listenerID), &l) return l, err } func (p *leveldbPersistence) WriteListener(ctx context.Context, spec *apitypes.Listener) error { - return p.writeJSON(ctx, p.listenerKey(spec.ID), spec) + return p.writeJSON(ctx, prefixedKey(listenersPrefix, spec.ID), spec) } func (p *leveldbPersistence) DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error { - return p.deleteKeys(ctx, p.listenerKey(listenerID)) + return p.deleteKeys(ctx, prefixedKey(listenersPrefix, listenerID)) +} + +func (p *leveldbPersistence) indexLookupCallback(ctx context.Context, key []byte) ([]byte, error) { + b, err := p.getKeyValue(ctx, key) + switch { + case err != nil: + return nil, err + case b == nil: + return nil, nil + } + return b, err +} + +func (p *leveldbPersistence) cleanupOrphanedTXIdxKeys(ctx context.Context, orphanedIdxKeys [][]byte) { + p.txMux.Lock() + defer p.txMux.Unlock() + err := p.deleteKeys(ctx, orphanedIdxKeys...) + if err != nil { + log.L(ctx).Warnf("Failed to clean up orphaned index keys: %s", err) + } +} + +func (p *leveldbPersistence) listTransactionsByIndex(ctx context.Context, collectionPrefix, collectionEnd, afterStr string, limit int) ([]*apitypes.ManagedTX, error) { + + p.txMux.RLock() + transactions := make([]*apitypes.ManagedTX, 0) + orphanedIdxKeys, err := p.listJSON(ctx, collectionPrefix, collectionEnd, afterStr, limit, + func() interface{} { var v *apitypes.ManagedTX; return &v }, + func(v interface{}) { transactions = append(transactions, *(v.(**apitypes.ManagedTX))) }, + p.indexLookupCallback, + ) + p.txMux.RUnlock() + if err != nil { + return nil, err + } + // If we find orphaned index keys we clean them up - which requires the write lock (hence dropping read-lock first) + if len(orphanedIdxKeys) > 0 { + p.cleanupOrphanedTXIdxKeys(ctx, orphanedIdxKeys) + } + return transactions, nil +} + +func (p *leveldbPersistence) ListTransactionsByCreateTime(ctx context.Context, after *fftypes.FFTime, limit int) ([]*apitypes.ManagedTX, error) { + afterStr := "" + if after != nil { + afterStr = fmt.Sprintf("%.19d", after.UnixNano()) + } + return p.listTransactionsByIndex(ctx, txCreatedIndexPrefix, txCreatedIndexEnd, afterStr, limit) +} + +func (p *leveldbPersistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int) ([]*apitypes.ManagedTX, error) { + afterStr := "" + if after != nil { + afterStr = fmt.Sprintf("%.24d", after.Int()) + } + return p.listTransactionsByIndex(ctx, signerNoncePrefix(signer), signerNonceEnd(signer), afterStr, limit) +} + +func (p *leveldbPersistence) ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.ManagedTX, error) { + return p.listTransactionsByIndex(ctx, txPendingIndexPrefix, txPendingIndexEnd, after.String(), limit) +} + +func (p *leveldbPersistence) GetTransactionByID(ctx context.Context, txID string) (tx *apitypes.ManagedTX, err error) { + p.txMux.RLock() + defer p.txMux.RUnlock() + err = p.readJSON(ctx, txDataKey(txID), &tx) + return tx, err +} + +func (p *leveldbPersistence) GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (tx *apitypes.ManagedTX, err error) { + p.txMux.RLock() + defer p.txMux.RUnlock() + err = p.readJSONByIndex(ctx, txNonceAllocationKey(signer, nonce), &tx) + return tx, err +} + +func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, possiblyNew bool) (err error) { + // We take a write-lock here, because we are writing multiple values (the indexes), and anybody + // attempting to read the critical nonce allocation index must know the difference between a partial write + // (we crashed before we completed all the writes) and an incomplete write that's in process. + // The reading code detects partial writes and cleans them up if it finds them. + p.txMux.Lock() + defer p.txMux.Unlock() + + if tx.Request == nil || + tx.Request.From == "" || + tx.Nonce == nil || + tx.SequenceID == nil || + tx.Created == nil { + return i18n.NewError(ctx, tmmsgs.MsgPersistenceTXIncomplete) + } + idKey := txDataKey(tx.ID) + if possiblyNew { + // We write the index records first - because if we crash, we need to be able to know if the + // index records are valid or not. When reading under the read lock, if there is an index key + // that does not have a corresponding managed TX available, we will clean up the + // orphaned index (after swapping the read lock for the write lock) + // See listTransactionsByIndex() for the other half of this logic. + err = p.writeKeyValue(ctx, txCreatedIndexKey(tx), idKey) + if err == nil && tx.Status == apitypes.TxStatusPending { + err = p.writeKeyValue(ctx, txPendingIndexKey(tx.SequenceID), idKey) + } + if err == nil { + err = p.writeKeyValue(ctx, txNonceAllocationKey(tx.Request.From, tx.Nonce), idKey) + } + } + // If we are creating/updating a record that is not pending, we need to ensure there is no pending index associated with it + if err == nil && tx.Status != apitypes.TxStatusPending { + err = p.deleteKeys(ctx) + } + if err == nil { + err = p.writeJSON(ctx, idKey, tx) + } + return err +} + +func (p *leveldbPersistence) DeleteTransaction(ctx context.Context, txID string) error { + var tx *apitypes.ManagedTX + err := p.readJSON(ctx, txDataKey(txID), &tx) + if err != nil || tx == nil { + return err + } + return p.deleteKeys(ctx, + txDataKey(txID), + txCreatedIndexKey(tx), + txPendingIndexKey(tx.SequenceID), + txNonceAllocationKey(tx.Request.From, tx.Nonce), + ) } func (p *leveldbPersistence) Close(ctx context.Context) { diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index 2699970f..c3149b2c 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -19,6 +19,7 @@ package persistence import ( "context" "encoding/json" + "fmt" "io/ioutil" "os" "testing" @@ -119,22 +120,22 @@ func TestReadWriteStreams(t *testing.T) { assert.NoError(t, err) assert.Len(t, streams, 3) - assert.Equal(t, s1.ID, streams[0].ID) + assert.Equal(t, s3.ID, streams[0].ID) assert.Equal(t, s2.ID, streams[1].ID) - assert.Equal(t, s3.ID, streams[2].ID) + assert.Equal(t, s1.ID, streams[2].ID) // Test pagination streams, err = p.ListStreams(ctx, nil, 2) assert.NoError(t, err) assert.Len(t, streams, 2) - assert.Equal(t, s1.ID, streams[0].ID) + assert.Equal(t, s3.ID, streams[0].ID) assert.Equal(t, s2.ID, streams[1].ID) streams, err = p.ListStreams(ctx, streams[1].ID, 2) assert.NoError(t, err) assert.Len(t, streams, 1) - assert.Equal(t, s3.ID, streams[0].ID) + assert.Equal(t, s1.ID, streams[0].ID) // Test delete @@ -143,8 +144,8 @@ func TestReadWriteStreams(t *testing.T) { streams, err = p.ListStreams(ctx, nil, 2) assert.NoError(t, err) assert.Len(t, streams, 2) - assert.Equal(t, s1.ID, streams[0].ID) - assert.Equal(t, s3.ID, streams[1].ID) + assert.Equal(t, s3.ID, streams[0].ID) + assert.Equal(t, s1.ID, streams[1].ID) // Test get direct @@ -193,17 +194,17 @@ func TestReadWriteListeners(t *testing.T) { assert.NoError(t, err) assert.Len(t, listeners, 3) - assert.Equal(t, s1l1.ID, listeners[0].ID) + assert.Equal(t, s1l2.ID, listeners[0].ID) assert.Equal(t, s2l1.ID, listeners[1].ID) - assert.Equal(t, s1l2.ID, listeners[2].ID) + assert.Equal(t, s1l1.ID, listeners[2].ID) // Test stream filter listeners, err = p.ListStreamListeners(ctx, nil, 0, sID1) assert.NoError(t, err) assert.Len(t, listeners, 2) - assert.Equal(t, s1l1.ID, listeners[0].ID) - assert.Equal(t, s1l2.ID, listeners[1].ID) + assert.Equal(t, s1l2.ID, listeners[0].ID) + assert.Equal(t, s1l1.ID, listeners[1].ID) // Test delete @@ -258,12 +259,95 @@ func TestReadWriteCheckpoints(t *testing.T) { assert.Equal(t, cp2.StreamID, cp.StreamID) } +func TestReadWriteManagedTransactions(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + textTX := func(signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { + tx := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + SequenceID: apitypes.UUIDVersion1(), + Nonce: fftypes.NewFFBigInt(nonce), + Created: fftypes.Now(), + Request: &apitypes.TransactionRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: signer, + }, + }, + }, + Status: status, + } + err := p.WriteTransaction(ctx, tx, true) + assert.NoError(t, err) + return tx + } + + s1t1 := textTX("0xaaaaa", 10001, apitypes.TxStatusSucceeded) + s2t1 := textTX("0xbbbbb", 10001, apitypes.TxStatusFailed) + s1t2 := textTX("0xaaaaa", 10002, apitypes.TxStatusPending) + s1t3 := textTX("0xaaaaa", 10003, apitypes.TxStatusPending) + + txns, err := p.ListTransactionsByCreateTime(ctx, nil, 0) + assert.NoError(t, err) + assert.Len(t, txns, 4) + + assert.Equal(t, s1t3.ID, txns[0].ID) + assert.Equal(t, s1t2.ID, txns[1].ID) + assert.Equal(t, s2t1.ID, txns[2].ID) + assert.Equal(t, s1t1.ID, txns[3].ID) + + // Only list pending + + txns, err = p.ListTransactionsPending(ctx, nil, 0) + assert.NoError(t, err) + assert.Len(t, txns, 2) + + assert.Equal(t, s1t3.ID, txns[0].ID) + assert.Equal(t, s1t2.ID, txns[1].ID) + + // List with time range + + txns, err = p.ListTransactionsByCreateTime(ctx, s1t2.Created, 0) + assert.NoError(t, err) + assert.Len(t, txns, 2) + assert.Equal(t, s2t1.ID, txns[0].ID) + assert.Equal(t, s1t1.ID, txns[1].ID) + + // Test delete, and querying by nonce to limit TX returned + + err = p.DeleteTransaction(ctx, s1t2.ID) + assert.NoError(t, err) + txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t3.Nonce, 0) + assert.NoError(t, err) + assert.Len(t, txns, 1) + assert.Equal(t, s1t1.ID, txns[0].ID) + + // Test get direct + + v, err := p.GetTransactionByID(ctx, s1t3.ID) + assert.NoError(t, err) + assert.Equal(t, s1t3.ID, v.ID) + assert.Equal(t, s1t3.Nonce, v.Nonce) + + v, err = p.GetTransactionByNonce(ctx, "0xbbbbb", s2t1.Nonce) + assert.NoError(t, err) + assert.Equal(t, s2t1.ID, v.ID) + assert.Equal(t, s2t1.Nonce, v.Nonce) + + v, err = p.GetTransactionByID(ctx, s1t2.ID) + assert.NoError(t, err) + assert.Nil(t, v) +} + func TestListStreamsBadJSON(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() sID := apitypes.UUIDVersion1() - err := p.db.Put(p.streamKey(sID), []byte("{! not json"), &opt.WriteOptions{}) + err := p.db.Put(prefixedKey(eventstreamsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) _, err = p.ListStreams(context.Background(), nil, 0) @@ -276,7 +360,7 @@ func TestListListenersBadJSON(t *testing.T) { defer done() lID := apitypes.UUIDVersion1() - err := p.db.Put(p.listenerKey(lID), []byte("{! not json"), &opt.WriteOptions{}) + err := p.db.Put(prefixedKey(listenersPrefix, lID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) _, err = p.ListListeners(context.Background(), nil, 0) @@ -298,7 +382,7 @@ func TestDeleteStreamFail(t *testing.T) { } -func TestWriteCheckpointFail(t *testing.T) { +func TestWriteCheckpointFailMarshal(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() @@ -314,6 +398,22 @@ func TestWriteCheckpointFail(t *testing.T) { } +func TestWriteCheckpointFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + id1 := apitypes.UUIDVersion1() + err := p.WriteCheckpoint(context.Background(), &apitypes.EventStreamCheckpoint{ + Listeners: map[fftypes.UUID]json.RawMessage{ + *id1: json.RawMessage([]byte(`{}`)), + }, + }) + assert.Error(t, err) + +} + func TestReadListenerFail(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() @@ -330,10 +430,183 @@ func TestReadCheckpointFail(t *testing.T) { defer done() sID := apitypes.UUIDVersion1() - err := p.db.Put(p.checkpointKey(sID), []byte("{! not json"), &opt.WriteOptions{}) + err := p.db.Put(prefixedKey(checkpointsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) _, err = p.GetCheckpoint(context.Background(), sID) assert.Error(t, err) } + +func TestListManagedTransactionFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + tx := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + Created: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), + } + err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) + assert.NoError(t, err) + err = p.db.Put(txDataKey(tx.ID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListTransactionsByCreateTime(context.Background(), nil, 0) + assert.Error(t, err) + +} + +func TestListManagedTransactionCleanupOrphans(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + tx := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + Created: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), + } + err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) + assert.NoError(t, err) + + txns, err := p.ListTransactionsByCreateTime(context.Background(), nil, 0) + assert.NoError(t, err) + assert.Empty(t, txns) + + cleanedUpIndex, err := p.getKeyValue(context.Background(), txCreatedIndexKey(tx)) + assert.NoError(t, err) + assert.Nil(t, cleanedUpIndex) + +} + +func TestListNonceAllocationsFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + txID := fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()) + err := p.writeKeyValue(context.Background(), txNonceAllocationKey("0xaaa", fftypes.NewFFBigInt(12345)), txDataKey(txID)) + assert.NoError(t, err) + err = p.db.Put(txDataKey(txID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListTransactionsByNonce(context.Background(), "0xaaa", nil, 0) + assert.Error(t, err) + +} + +func TestListInflightTransactionFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + txID := fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()) + err := p.writeKeyValue(context.Background(), txPendingIndexKey(apitypes.UUIDVersion1()), txDataKey(txID)) + assert.NoError(t, err) + err = p.db.Put(txDataKey(txID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListTransactionsPending(context.Background(), nil, 0) + assert.Error(t, err) + +} + +func TestIndexLookupCallbackErr(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + p.Close(context.Background()) + + _, err := p.indexLookupCallback(context.Background(), ([]byte("any key"))) + assert.NotNil(t, err) + +} + +func TestIndexLookupCallbackNotFound(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + b, err := p.indexLookupCallback(context.Background(), ([]byte("any key"))) + assert.Nil(t, err) + assert.Nil(t, b) + +} + +func TestGetTransactionByNonceFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + p.Close(context.Background()) + + _, err := p.GetTransactionByNonce(context.Background(), "0xaaa", fftypes.NewFFBigInt(12345)) + assert.Regexp(t, "FF21055", err) + +} + +func TestIterateReverseJSONFailIdxResolve(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.writeKeyValue(context.Background(), []byte(`test_0/key`), []byte(`test/value`)) + assert.NoError(t, err) + _, err = p.listJSON(context.Background(), + "test_0/", + "test_1", + "", + 0, + func() interface{} { return make(map[string]interface{}) }, + func(i interface{}) {}, + func(ctx context.Context, k []byte) ([]byte, error) { + return nil, fmt.Errorf("pop") + }, + ) + assert.Regexp(t, "pop", err) + +} + +func TestIterateReverseJSONSkipIdxResolve(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.writeKeyValue(context.Background(), []byte(`test_0/key`), []byte(`test/value`)) + assert.NoError(t, err) + orphans, err := p.listJSON(context.Background(), + "test_0/", + "test_1", + "", + 0, + func() interface{} { return make(map[string]interface{}) }, + func(_ interface{}) { + assert.Fail(t, "Should not be called") + }, + func(ctx context.Context, k []byte) ([]byte, error) { + return nil, nil + }, + ) + assert.NoError(t, err) + assert.Len(t, orphans, 1) + +} + +func TestCleanupOrphanedTXIdxKeysSwallowError(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + p.Close(context.Background()) + + p.cleanupOrphanedTXIdxKeys(context.Background(), [][]byte{[]byte("test")}) + +} + +func TestWriteTransactionIncomplete(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.WriteTransaction(context.Background(), &apitypes.ManagedTX{}, true) + assert.Regexp(t, "FF21059", err) + +} + +func TestDeleteTransactionMissing(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.DeleteTransaction(context.Background(), "missing") + assert.NoError(t, err) + +} diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go index 18d26599..35822aac 100644 --- a/internal/persistence/persistence.go +++ b/internal/persistence/persistence.go @@ -28,16 +28,24 @@ type Persistence interface { GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error - ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) + ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) // reverse UUIDv1 order GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) WriteStream(ctx context.Context, spec *apitypes.EventStream) error DeleteStream(ctx context.Context, streamID *fftypes.UUID) error - ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) + ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) // reverse UUIDv1 order ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) WriteListener(ctx context.Context, spec *apitypes.Listener) error DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error + ListTransactionsByCreateTime(ctx context.Context, after *fftypes.FFTime, limit int) ([]*apitypes.ManagedTX, error) // reverse create time order + ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int) ([]*apitypes.ManagedTX, error) // reverse nonce order within signer + ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.ManagedTX, error) // reverse UUIDv1 order, only those in pending state + GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) + GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (*apitypes.ManagedTX, error) + WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, possiblyNew bool) error + DeleteTransaction(ctx context.Context, txID string) error + Close(ctx context.Context) } diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index bb3ffa9a..19760d4a 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -28,7 +28,6 @@ import ( var ffc = config.AddRootKey var ( - ManagerName = ffc("manager.name") ConfirmationsRequired = ffc("confirmations.required") ConfirmationsBlockQueueLength = ffc("confirmations.blockQueueLength") ConfirmationsStaleReceiptTimeout = ffc("confirmations.staleReceiptTimeout") diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go index 12ed7f52..9b195c03 100644 --- a/internal/tmmsgs/en_config_descriptions.go +++ b/internal/tmmsgs/en_config_descriptions.go @@ -45,8 +45,6 @@ var ( ConfigFFCoreURL = ffc("config.ffcore.url", "The URL of the FireFly core admin API server to connect to", i18n.StringType) ConfigFFCoreProxyURL = ffc("config.ffcore.proxy.url", "Optional HTTP proxy URL to use for the FireFly core admin API server", i18n.StringType) - ConfigManagerName = ffc("config.manager.name", "The name of this Transaction Manager, used in operation metadata to track which operations are to be updated", i18n.StringType) - ConfigOperationsTypes = ffc("config.operations.types", "The operation types to query in FireFly core, that might have been submitted via this Transaction Manager", "string[]") ConfigOperationsFullScanMinimumDelay = ffc("config.operations.fullScan.minimumDelay", "The minimum delay between full scans of the FireFly core API, when reconnecting, or recovering from missed events / errors", i18n.TimeDurationType) ConfigOperationsFullScanPageSize = ffc("config.operations.fullScan.pageSize", "The page size to use when performing a full scan of the ForeFly core API on startup, or recovery", i18n.IntType) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index eaf4befc..3dbd68dc 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -70,9 +70,10 @@ var ( MsgPersistenceMarshalFailed = ffe("FF21053", "JSON serialization failed while writing to persistence") MsgPersistenceUnmarshalFailed = ffe("FF21054", "JSON parsing failed while reading from persistence") MsgPersistenceReadFailed = ffe("FF21055", "Failed to read key '%s' from persistence") - MsgPersistenceDeleteFailed = ffe("FF21056", "Failed to delete key '%s' from persistence") - MsgPersistenceInitFailed = ffe("FF21057", "Failed to initialize persistence at path '%s'") - MsgNamespacesEmpty = ffe("FF21058", "ffcore.namespaces must contain a list of namespaces") + MsgPersistenceWriteFailed = ffe("FF21056", "Failed to read key '%s' from persistence") + MsgPersistenceDeleteFailed = ffe("FF21057", "Failed to delete key '%s' from persistence") + MsgPersistenceInitFailed = ffe("FF21058", "Failed to initialize persistence at path '%s'") MsgPersistenceTXIncomplete = ffe("FF21059", "Transaction is missing indexed fields") MsgNotStarted = ffe("FF21060", "Connector has not fully started yet", 503) + MsgNamespacesEmpty = ffe("FF21061", "ffcore.namespaces must contain a list of namespaces") ) diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go index 14bea4c1..b10311c2 100644 --- a/mocks/persistencemocks/persistence.go +++ b/mocks/persistencemocks/persistence.go @@ -64,6 +64,20 @@ func (_m *Persistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) return r0 } +// DeleteTransaction provides a mock function with given fields: ctx, txID +func (_m *Persistence) DeleteTransaction(ctx context.Context, txID string) error { + ret := _m.Called(ctx, txID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, txID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetCheckpoint provides a mock function with given fields: ctx, streamID func (_m *Persistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error) { ret := _m.Called(ctx, streamID) @@ -133,6 +147,52 @@ func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (* return r0, r1 } +// GetTransactionByID provides a mock function with given fields: ctx, txID +func (_m *Persistence) GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, txID) + + var r0 *apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, string) *apitypes.ManagedTX); ok { + r0 = rf(ctx, txID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, txID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransactionByNonce provides a mock function with given fields: ctx, signer, nonce +func (_m *Persistence) GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, signer, nonce) + + var r0 *apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt) *apitypes.ManagedTX); ok { + r0 = rf(ctx, signer, nonce) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.FFBigInt) error); ok { + r1 = rf(ctx, signer, nonce) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ListListeners provides a mock function with given fields: ctx, after, limit func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { ret := _m.Called(ctx, after, limit) @@ -202,6 +262,75 @@ func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, lim return r0, r1 } +// ListTransactionsByCreateTime provides a mock function with given fields: ctx, after, limit +func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after *fftypes.FFTime, limit int) ([]*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, after, limit) + + var r0 []*apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.FFTime, int) []*apitypes.ManagedTX); ok { + r0 = rf(ctx, after, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.FFTime, int) error); ok { + r1 = rf(ctx, after, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListTransactionsByNonce provides a mock function with given fields: ctx, signer, after, limit +func (_m *Persistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int) ([]*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, signer, after, limit) + + var r0 []*apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt, int) []*apitypes.ManagedTX); ok { + r0 = rf(ctx, signer, after, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.FFBigInt, int) error); ok { + r1 = rf(ctx, signer, after, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListTransactionsPending provides a mock function with given fields: ctx, after, limit +func (_m *Persistence) ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, after, limit) + + var r0 []*apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.ManagedTX); ok { + r0 = rf(ctx, after, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int) error); ok { + r1 = rf(ctx, after, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // WriteCheckpoint provides a mock function with given fields: ctx, checkpoint func (_m *Persistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error { ret := _m.Called(ctx, checkpoint) @@ -243,3 +372,17 @@ func (_m *Persistence) WriteStream(ctx context.Context, spec *apitypes.EventStre return r0 } + +// WriteTransaction provides a mock function with given fields: ctx, tx, possiblyNew +func (_m *Persistence) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, possiblyNew bool) error { + ret := _m.Called(ctx, tx, possiblyNew) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, bool) error); ok { + r0 = rf(ctx, tx, possiblyNew) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/policyenginemocks/policy_engine.go b/mocks/policyenginemocks/policy_engine.go index bccac0d4..063bb917 100644 --- a/mocks/policyenginemocks/policy_engine.go +++ b/mocks/policyenginemocks/policy_engine.go @@ -5,10 +5,11 @@ package policyenginemocks import ( context "context" + apitypes "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - mock "github.com/stretchr/testify/mock" - policyengine "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" + mock "github.com/stretchr/testify/mock" ) // PolicyEngine is an autogenerated mock type for the PolicyEngine type @@ -17,25 +18,25 @@ type PolicyEngine struct { } // Execute provides a mock function with given fields: ctx, cAPI, mtx -func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *policyengine.ManagedTXOutput) (bool, ffcapi.ErrorReason, error) { +func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (bool, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, cAPI, mtx) var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) bool); ok { + if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *apitypes.ManagedTX) bool); ok { r0 = rf(ctx, cAPI, mtx) } else { r0 = ret.Get(0).(bool) } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *apitypes.ManagedTX) ffcapi.ErrorReason); ok { r1 = rf(ctx, cAPI, mtx) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, ffcapi.API, *policyengine.ManagedTXOutput) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, ffcapi.API, *apitypes.ManagedTX) error); ok { r2 = rf(ctx, cAPI, mtx) } else { r2 = ret.Error(2) diff --git a/pkg/policyengine/managed_tx.go b/pkg/apitypes/managed_tx.go similarity index 51% rename from pkg/policyengine/managed_tx.go rename to pkg/apitypes/managed_tx.go index 64ebac61..1baccc3b 100644 --- a/pkg/policyengine/managed_tx.go +++ b/pkg/apitypes/managed_tx.go @@ -14,35 +14,62 @@ // See the License for the specific language governing permissions and // limitations under the License. -package policyengine +package apitypes import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" - "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) +// TxStatus is the current status of a transaction +type TxStatus string + +const ( + // TxStatusPending indicates the operation has been submitted, but is not yet confirmed as successful or failed + TxStatusPending TxStatus = "Pending" + // TxStatusSucceeded the infrastructure runtime has returned success for the operation + TxStatusSucceeded TxStatus = "Succeeded" + // TxStatusFailed happens when an error is reported by the infrastructure runtime + TxStatusFailed TxStatus = "Failed" +) + type ManagedTXError struct { Time *fftypes.FFTime `json:"time"` Error string `json:"error,omitempty"` Mapped ffcapi.ErrorReason `json:"mapped,omitempty"` } -// ManagedTXOutput is the structure stored into the operation in FireFly, that the policy -// engine can use to apply policy, and apply updates to -type ManagedTXOutput struct { - FFTMName string `json:"fftmName"` +// ManagedTX is the structure stored for each new transaction request, using the external ID of the operation +// +// Indexing: +// Multiple index collection are stored for the managed transactions, to allow them to be managed including: +// +// - Nonce allocation: this is a critical index, and why cleanup is so important (mentioned below). +// We use this index to determine the next nonce to assign to a given signing key. +// - Created time: a timestamp ordered index for the transactions for convenient ordering. +// the key includes the ID of the TX for uniqueness. +// - Pending sequence: An entry in this index only exists while the transaction is pending, and is +// ordered by a UUIDv1 sequence allocated to each entry. +// +// Index cleanup after partial write: +// - All indexes are stored before the TX itself. +// - When listing back entries, the persistence layer will automatically clean up indexes if the underlying +// TX they refer to is not available. For this reason the index records are written first. +type ManagedTX struct { ID string `json:"id"` + Status TxStatus `json:"status"` + SequenceID *fftypes.UUID `json:"sequenceId"` Nonce *fftypes.FFBigInt `json:"nonce"` Gas *fftypes.FFBigInt `json:"gas"` TransactionHash string `json:"transactionHash,omitempty"` TransactionData string `json:"transactionData,omitempty"` GasPrice *fftypes.JSONAny `json:"gasPrice"` PolicyInfo *fftypes.JSONAny `json:"policyInfo"` + Created *fftypes.FFTime `json:"created"` FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` - Request *apitypes.TransactionRequest `json:"request,omitempty"` + Request *TransactionRequest `json:"request,omitempty"` Receipt *ffcapi.TransactionReceiptResponse `json:"receipt,omitempty"` ErrorHistory []*ManagedTXError `json:"errorHistory"` Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` diff --git a/pkg/fftm/ffcore.go b/pkg/fftm/ffcore.go index 99d4b8d5..bbe81716 100644 --- a/pkg/fftm/ffcore.go +++ b/pkg/fftm/ffcore.go @@ -26,18 +26,18 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly/pkg/core" ) // opUpdate allows us to avoid JSONObject serialization to a map before we upload our managedTXOutput type opUpdate struct { - Status core.OpStatus `json:"status"` - Output *policyengine.ManagedTXOutput `json:"output"` - Error string `json:"error"` + Status core.OpStatus `json:"status"` + Output *apitypes.ManagedTX `json:"output"` + Error string `json:"error"` } -func (m *manager) writeManagedTX(ctx context.Context, mtx *policyengine.ManagedTXOutput, status core.OpStatus, errString string) error { +func (m *manager) writeManagedTX(ctx context.Context, mtx *apitypes.ManagedTX, status core.OpStatus, errString string) error { log.L(ctx).Debugf("Updating operation %s status=%s", mtx.ID, status) var errorInfo fftypes.RESTError var op core.Operation diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index d46b60e0..7c45d2a7 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -37,6 +37,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/internal/ws" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" @@ -74,7 +75,6 @@ type manager struct { started bool apiServerDone chan error - name string opTypes []string startupScanMaxRetries int fullScanPageSize int64 @@ -93,9 +93,6 @@ func InitConfig() { func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { var err error m := newManager(ctx, connector) - if m.name == "" { - return nil, i18n.NewError(ctx, tmmsgs.MsgConfigParamNotSet, tmconfig.ManagerName) - } m.confirmations = confirmations.NewBlockConfirmationManager(ctx, m.connector) m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBaseConfig, config.GetString(tmconfig.PolicyEngineName)) if err != nil { @@ -134,7 +131,6 @@ func newManager(ctx context.Context, connector ffcapi.API) *manager { eventStreams: make(map[fftypes.UUID]events.Stream), streamsByName: make(map[string]*fftypes.UUID), - name: config.GetString(tmconfig.ManagerName), opTypes: config.GetStringSlice(tmconfig.OperationsTypes), startupScanMaxRetries: config.GetInt(tmconfig.OperationsFullScanStartupMaxRetries), fullScanPageSize: config.GetInt64(tmconfig.OperationsFullScanPageSize), @@ -148,7 +144,7 @@ func newManager(ctx context.Context, connector ffcapi.API) *manager { } type pendingState struct { - mtx *policyengine.ManagedTXOutput + mtx *apitypes.ManagedTX confirmed bool removed bool trackingTransactionHash string @@ -255,16 +251,12 @@ func (m *manager) fullScan() error { func (m *manager) trackIfManaged(op *core.Operation) { outputJSON := []byte(op.Output.String()) - var mtx policyengine.ManagedTXOutput + var mtx apitypes.ManagedTX err := json.Unmarshal(outputJSON, &mtx) if err != nil { log.L(m.ctx).Warnf("Failed to parse output from operation %s", err) return } - if mtx.FFTMName != m.name { - log.L(m.ctx).Debugf("Operation %s is not managed by us (fftm=%s)", op.ID, mtx.FFTMName) - return - } if fmt.Sprintf("%s:%s", op.Namespace, op.ID) != mtx.ID { log.L(m.ctx).Warnf("Operation %s contains an invalid ID %s in the output", op.ID, mtx.ID) return @@ -276,7 +268,7 @@ func (m *manager) trackIfManaged(op *core.Operation) { m.trackManaged(&mtx) } -func (m *manager) trackManaged(mtx *policyengine.ManagedTXOutput) { +func (m *manager) trackManaged(mtx *apitypes.ManagedTX) { m.mux.Lock() defer m.mux.Unlock() _, existing := m.pendingOpsByID[mtx.ID] diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 3bbaf9d8..f21f311e 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -39,7 +39,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines/simple" "github.com/hyperledger/firefly/pkg/core" @@ -68,7 +67,6 @@ func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...strin tmconfig.APIConfig.Set(httpserver.HTTPConfPort, managerPort) tmconfig.APIConfig.Set(httpserver.HTTPConfAddress, "127.0.0.1") - config.Set(tmconfig.ManagerName, testManagerName) config.Set(tmconfig.PolicyLoopInterval, "1ms") tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") @@ -103,7 +101,7 @@ func newMockPersistenceManager(t *testing.T) (*persistencemocks.Persistence, *ff return mps, mca, m } -func newTestOperation(t *testing.T, mtx *policyengine.ManagedTXOutput, status core.OpStatus) *core.Operation { +func newTestOperation(t *testing.T, mtx *apitypes.ManagedTX, status core.OpStatus) *core.Operation { b, err := json.Marshal(&mtx) assert.NoError(t, err) op := &core.Operation{ @@ -116,20 +114,9 @@ func newTestOperation(t *testing.T, mtx *policyengine.ManagedTXOutput, status co return op } -func TestNewManagerMissingName(t *testing.T) { - - tmconfig.Reset() - config.Set(tmconfig.ManagerName, "") - - _, err := NewManager(context.Background(), nil) - assert.Regexp(t, "FF21018", err) - -} - func TestNewManagerBadHttpConfig(t *testing.T) { tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") tmconfig.APIConfig.Set(httpserver.HTTPConfAddress, "::::") policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) @@ -147,7 +134,6 @@ func TestNewManagerBadLevelDBConfig(t *testing.T) { defer os.Remove(tmpFile.Name()) tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") config.Set(tmconfig.PersistenceLevelDBPath, tmpFile.Name) tmconfig.APIConfig.Set(httpserver.HTTPConfPort, "0") @@ -162,7 +148,6 @@ func TestNewManagerBadLevelDBConfig(t *testing.T) { func TestNewManagerBadPersistenceConfig(t *testing.T) { tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") config.Set(tmconfig.PersistenceType, "wrong") tmconfig.APIConfig.Set(httpserver.HTTPConfPort, "0") @@ -177,7 +162,6 @@ func TestNewManagerBadPersistenceConfig(t *testing.T) { func TestNewManagerFireFlyURLConfig(t *testing.T) { tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, ":::!badurl") policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) @@ -191,7 +175,6 @@ func TestNewManagerFireFlyURLConfig(t *testing.T) { func TestNewManagerBadPolicyEngine(t *testing.T) { tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") config.Set(tmconfig.PolicyEngineName, "wrong") _, err := NewManager(context.Background(), nil) @@ -233,38 +216,6 @@ func TestChangeEventsNewBadOutput(t *testing.T) { } -func TestChangeEventsWrongName(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - b, err := json.Marshal(newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: "wrong", - Request: &apitypes.TransactionRequest{}, - }, core.OpStatusPending)) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - -} - func TestChangeEventsWrongID(t *testing.T) { ce := &core.ChangeEvent{ @@ -279,10 +230,9 @@ func TestChangeEventsWrongID(t *testing.T) { func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - op := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: testManagerName, - Request: &apitypes.TransactionRequest{}, + op := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + ce.ID.String(), + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) op.ID = fftypes.NewUUID() b, err := json.Marshal(&op) @@ -313,9 +263,8 @@ func TestChangeEventsNilRequest(t *testing.T) { func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - op := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: testManagerName, + op := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + ce.ID.String(), }, core.OpStatusPending) b, err := json.Marshal(&op) assert.NoError(t, err) @@ -369,10 +318,9 @@ func TestChangeEventsMarkForCleanup(t *testing.T) { Namespace: "ns1", } - op := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: testManagerName, - Request: &apitypes.TransactionRequest{}, + op := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + ce.ID.String(), + Request: &apitypes.TransactionRequest{}, }, core.OpStatusFailed) var m *manager @@ -397,24 +345,21 @@ func TestChangeEventsMarkForCleanup(t *testing.T) { func TestStartupScanMultiPageOK(t *testing.T) { - op1 := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FFTMName: testManagerName, - Request: &apitypes.TransactionRequest{}, + op1 := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + fftypes.NewUUID().String(), + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) t1 := fftypes.FFTime(time.Now().Add(-10 * time.Minute)) op1.Created = &t1 - op2 := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FFTMName: testManagerName, - Request: &apitypes.TransactionRequest{}, + op2 := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + fftypes.NewUUID().String(), + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) t2 := fftypes.FFTime(time.Now().Add(-5 * time.Minute)) op2.Created = &t2 - op3 := newTestOperation(t, &policyengine.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FFTMName: testManagerName, - Request: &apitypes.TransactionRequest{}, + op3 := newTestOperation(t, &apitypes.ManagedTX{ + ID: "ns1:" + fftypes.NewUUID().String(), + Request: &apitypes.TransactionRequest{}, }, core.OpStatusPending) t3 := fftypes.FFTime(time.Now().Add(-1 * time.Minute)) op3.Created = &t3 @@ -545,7 +490,7 @@ func TestAddErrorMessageMax(t *testing.T) { defer cancel() m.errorHistoryCount = 2 - mtx := &policyengine.ManagedTXOutput{} + mtx := &apitypes.ManagedTX{} m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("snap")) m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("crackle")) m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("pop")) diff --git a/pkg/fftm/nonces.go b/pkg/fftm/nonces.go index 76029433..e41db85b 100644 --- a/pkg/fftm/nonces.go +++ b/pkg/fftm/nonces.go @@ -20,8 +20,8 @@ import ( "context" "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) type lockedNonce struct { @@ -30,7 +30,7 @@ type lockedNonce struct { signer string unlocked chan struct{} nonce uint64 - spent *policyengine.ManagedTXOutput + spent *apitypes.ManagedTX } // complete must be called for any lockedNonce returned from a successful assignAndLockNonce call diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go index 8a33fc98..22620add 100644 --- a/pkg/fftm/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -27,7 +27,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -60,7 +59,7 @@ func TestNonceCached(t *testing.T) { close(locked1) time.Sleep(1 * time.Millisecond) - ln.spent = &policyengine.ManagedTXOutput{ + ln.spent = &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), Request: &apitypes.TransactionRequest{ diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index dc8acc26..e0ceef0e 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -22,8 +22,8 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" ) @@ -62,14 +62,14 @@ func (m *manager) policyLoopCycle() { } -func (m *manager) addError(mtx *policyengine.ManagedTXOutput, reason ffcapi.ErrorReason, err error) { +func (m *manager) addError(mtx *apitypes.ManagedTX, reason ffcapi.ErrorReason, err error) { newLen := len(mtx.ErrorHistory) + 1 if newLen > m.errorHistoryCount { newLen = m.errorHistoryCount } oldHistory := mtx.ErrorHistory - mtx.ErrorHistory = make([]*policyengine.ManagedTXError, newLen) - mtx.ErrorHistory[0] = &policyengine.ManagedTXError{ + mtx.ErrorHistory = make([]*apitypes.ManagedTXError, newLen) + mtx.ErrorHistory[0] = &apitypes.ManagedTXError{ Time: fftypes.Now(), Mapped: reason, Error: err.Error(), @@ -177,7 +177,7 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { } } -func (m *manager) clearConfirmationTracking(mtx *policyengine.ManagedTXOutput) { +func (m *manager) clearConfirmationTracking(mtx *apitypes.ManagedTX) { // The only error condition on confirmations manager is if we are exiting, which it logs _ = m.confirmations.Notify(&confirmations.Notification{ NotificationType: confirmations.RemovedTransaction, diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index ed43dbd1..51b056d7 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -29,7 +29,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -42,7 +41,7 @@ const ( func TestPolicyLoopE2EOk(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, @@ -89,7 +88,7 @@ func TestPolicyLoopE2EOk(t *testing.T) { func TestPolicyLoopE2EOkReverted(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, @@ -136,7 +135,7 @@ func TestPolicyLoopE2EOkReverted(t *testing.T) { func TestPolicyLoopUpdateFFCoreWithError(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, @@ -168,7 +167,7 @@ func TestPolicyLoopUpdateFFCoreWithError(t *testing.T) { func TestPolicyLoopUpdateOpFail(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), FirstSubmit: fftypes.Now(), TransactionHash: sampleTXHash, @@ -176,7 +175,7 @@ func TestPolicyLoopUpdateOpFail(t *testing.T) { } _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { + func(w http.ResponseWriter, _ *http.Request) { errRes := fftypes.RESTError{Error: "pop"} b, err := json.Marshal(&errRes) assert.NoError(t, err) @@ -213,7 +212,7 @@ func TestPolicyLoopUpdateOpFail(t *testing.T) { func TestPolicyLoopResubmitNewTXID(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), Request: &apitypes.TransactionRequest{}, } @@ -287,7 +286,7 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { func TestPolicyLoopCycleCleanupRemoved(t *testing.T) { - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ ID: "ns1:" + fftypes.NewUUID().String(), Request: &apitypes.TransactionRequest{}, } @@ -319,7 +318,7 @@ func TestNotifyConfirmationMgrFail(t *testing.T) { mc.On("Notify", mock.Anything).Return(fmt.Errorf("pop")) m.trackSubmittedTransaction(&pendingState{ - mtx: &policyengine.ManagedTXOutput{ + mtx: &apitypes.ManagedTX{ TransactionHash: sampleSendTX, }, }) diff --git a/pkg/fftm/route__root_command.go b/pkg/fftm/route__root_command.go index fc771edd..0052b2af 100644 --- a/pkg/fftm/route__root_command.go +++ b/pkg/fftm/route__root_command.go @@ -38,7 +38,7 @@ var postRootCommand = func(m *manager) *ffapi.Route { Description: tmmsgs.APIEndpointPostSubscriptions, JSONInputValue: func() interface{} { return &apitypes.BaseRequest{} }, JSONOutputValue: func() interface{} { return map[string]interface{}{} }, - JSONInputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) { + JSONInputSchema: func(_ context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) { schemas := []*openapi3.SchemaRef{} txRequest, err := schemaGen(&apitypes.TransactionRequest{}) if err == nil { diff --git a/pkg/fftm/route_get_eventstream_listeners_test.go b/pkg/fftm/route_get_eventstream_listeners_test.go index 6eca9d61..40018cac 100644 --- a/pkg/fftm/route_get_eventstream_listeners_test.go +++ b/pkg/fftm/route_get_eventstream_listeners_test.go @@ -64,12 +64,12 @@ func TestGetEventStreamListeners(t *testing.T) { var listeners []*apitypes.Listener res, err = resty.New().R(). SetResult(&listeners). - Get(fmt.Sprintf("%s/eventstreams/%s/listeners?limit=1&after=%s", url, es1.ID, l1.ID)) + Get(fmt.Sprintf("%s/eventstreams/%s/listeners?limit=1&after=%s", url, es1.ID, l2.ID)) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, listeners, 1) - assert.Equal(t, l3.ID, listeners[0].ID) + assert.Equal(t, l1.ID, listeners[0].ID) assert.Equal(t, es1.ID, listeners[0].StreamID) mfc.AssertExpectations(t) diff --git a/pkg/fftm/route_get_eventstreams_test.go b/pkg/fftm/route_get_eventstreams_test.go index ac57db16..f29cb851 100644 --- a/pkg/fftm/route_get_eventstreams_test.go +++ b/pkg/fftm/route_get_eventstreams_test.go @@ -53,11 +53,11 @@ func TestGetEventStreams(t *testing.T) { var ess []*apitypes.EventStream res, err = resty.New().R(). SetResult(&ess). - Get(url + "/eventstreams?limit=1&after=" + es1.ID.String()) + Get(url + "/eventstreams?limit=1&after=" + es2.ID.String()) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, ess, 1) - assert.Equal(t, es2.ID, ess[0].ID) + assert.Equal(t, es1.ID, ess[0].ID) } diff --git a/pkg/fftm/route_get_subscriptions_test.go b/pkg/fftm/route_get_subscriptions_test.go index 7d742dc3..0ed4b82a 100644 --- a/pkg/fftm/route_get_subscriptions_test.go +++ b/pkg/fftm/route_get_subscriptions_test.go @@ -59,12 +59,12 @@ func TestGetSubscriptions(t *testing.T) { var listeners []*apitypes.Listener res, err = resty.New().R(). SetResult(&listeners). - Get(url + "/subscriptions?limit=1&after=" + l1.ID.String()) + Get(url + "/subscriptions?limit=1&after=" + l2.ID.String()) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, listeners, 1) - assert.Equal(t, l2.ID, listeners[0].ID) + assert.Equal(t, l1.ID, listeners[0].ID) assert.Equal(t, es1.ID, listeners[0].StreamID) mfc.AssertExpectations(t) diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index 7e2ae6ef..deb34d4b 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -22,11 +22,10 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly/pkg/core" ) -func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (*policyengine.ManagedTXOutput, error) { +func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (*apitypes.ManagedTX, error) { // First job is to assign the next nonce to this request. // We block any further sends on this nonce until we've got this one successfully into the node, or @@ -52,8 +51,7 @@ func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes. // From this point on, we will guide this transaction through to submission. // We return an "ack" at this point, and dispatch the work of getting the transaction submitted // to the background worker. - mtx := &policyengine.ManagedTXOutput{ - FFTMName: m.name, + mtx := &apitypes.ManagedTX{ ID: request.Headers.ID, // on input the request ID must be the namespaced operation ID Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), Gas: prepared.Gas, diff --git a/pkg/policyengine/policyengine.go b/pkg/policyengine/policyengine.go index ba1ccee0..96478358 100644 --- a/pkg/policyengine/policyengine.go +++ b/pkg/policyengine/policyengine.go @@ -19,9 +19,10 @@ package policyengine import ( "context" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) type PolicyEngine interface { - Execute(ctx context.Context, cAPI ffcapi.API, mtx *ManagedTXOutput) (updated bool, reason ffcapi.ErrorReason, err error) + Execute(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (updated bool, reason ffcapi.ErrorReason, err error) } diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index abc30f82..78cabaed 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -30,6 +30,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) @@ -93,7 +94,7 @@ type simplePolicyInfo struct { } // withPolicyInfo is a convenience helper to run some logic that accesses/updates our policy section -func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *policyengine.ManagedTXOutput, fn func(info *simplePolicyInfo) (updated bool, reason ffcapi.ErrorReason, err error)) (updated bool, reason ffcapi.ErrorReason, err error) { +func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *apitypes.ManagedTX, fn func(info *simplePolicyInfo) (updated bool, reason ffcapi.ErrorReason, err error)) (updated bool, reason ffcapi.ErrorReason, err error) { var info simplePolicyInfo infoBytes := []byte(mtx.PolicyInfo.String()) if len(infoBytes) > 0 { @@ -110,7 +111,7 @@ func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *policyengi return updated, reason, err } -func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *policyengine.ManagedTXOutput) (updated bool, reason ffcapi.ErrorReason, err error) { +func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (updated bool, reason ffcapi.ErrorReason, err error) { // Simple policy engine only submits once. if mtx.FirstSubmit == nil { diff --git a/pkg/policyengines/simple/simple_policy_engine_test.go b/pkg/policyengines/simple/simple_policy_engine_test.go index 1c159a1f..351e0bf3 100644 --- a/pkg/policyengines/simple/simple_policy_engine_test.go +++ b/pkg/policyengines/simple/simple_policy_engine_test.go @@ -31,7 +31,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -62,7 +61,7 @@ func TestFixedGasPriceOK(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -130,7 +129,7 @@ func TestGasOracleSendOK(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -177,7 +176,7 @@ func TestConnectorGasOracleSendOK(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -224,7 +223,7 @@ func TestConnectorGasOracleFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -265,7 +264,7 @@ func TestGasOracleSendFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -325,7 +324,7 @@ func TestGasOracleTemplateExecuteFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -355,7 +354,7 @@ func TestGasOracleNonJSON(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -388,7 +387,7 @@ func TestTXSendFail(t *testing.T) { p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -414,7 +413,7 @@ func TestWarnStaleWarningCannotParse(t *testing.T) { assert.NoError(t, err) submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: &submitTime, PolicyInfo: fftypes.JSONAnyPtr("!not json!"), @@ -446,7 +445,7 @@ func TestWarnStaleAdditionalWarning(t *testing.T) { submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.FFTime(time.Now().Add(-50 * time.Hour)) - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -480,7 +479,7 @@ func TestWarnStaleNoWarning(t *testing.T) { submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.Now() - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -512,7 +511,7 @@ func TestNoOpWithReceipt(t *testing.T) { assert.NoError(t, err) submitTime := fftypes.Now() - mtx := &policyengine.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ From 07cf8cc14683507d3cf987cc209a786520640950 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 20 Jul 2022 22:58:50 -0400 Subject: [PATCH 56/95] Add GET route for transactions Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 2 + internal/persistence/leveldb_persistence.go | 4 +- .../persistence/leveldb_persistence_test.go | 2 +- internal/persistence/persistence.go | 2 +- internal/tmmsgs/en_api_descriptions.go | 2 + internal/tmmsgs/en_error_messges.go | 2 + mocks/persistencemocks/persistence.go | 6 +- pkg/fftm/manager.go | 4 +- pkg/fftm/manager_test.go | 1 + pkg/fftm/route_get_transactions.go | 48 ++++++++ pkg/fftm/route_get_transactions_test.go | 106 ++++++++++++++++++ pkg/fftm/routes.go | 1 + pkg/fftm/stream_management.go | 11 +- pkg/fftm/transaction_management.go | 63 +++++++++++ 14 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 pkg/fftm/route_get_transactions.go create mode 100644 pkg/fftm/route_get_transactions_test.go create mode 100644 pkg/fftm/transaction_management.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 07d61c95..7b768ad9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "cSpell.words": [ "APIID", "apitypes", + "badurl", "ccache", "confirmationsmocks", "dataexchange", @@ -39,6 +40,7 @@ "fftmrequest", "fftypes", "finalizers", + "getkin", "GJSON", "goleveldb", "httpserver", diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index d59b37d4..4c8a6a45 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -333,10 +333,10 @@ func (p *leveldbPersistence) listTransactionsByIndex(ctx context.Context, collec return transactions, nil } -func (p *leveldbPersistence) ListTransactionsByCreateTime(ctx context.Context, after *fftypes.FFTime, limit int) ([]*apitypes.ManagedTX, error) { +func (p *leveldbPersistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int) ([]*apitypes.ManagedTX, error) { afterStr := "" if after != nil { - afterStr = fmt.Sprintf("%.19d", after.UnixNano()) + afterStr = fmt.Sprintf("%.19d/%s", after.Created.UnixNano(), after.SequenceID) } return p.listTransactionsByIndex(ctx, txCreatedIndexPrefix, txCreatedIndexEnd, afterStr, limit) } diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index c3149b2c..71b17c1d 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -310,7 +310,7 @@ func TestReadWriteManagedTransactions(t *testing.T) { // List with time range - txns, err = p.ListTransactionsByCreateTime(ctx, s1t2.Created, 0) + txns, err = p.ListTransactionsByCreateTime(ctx, s1t2, 0) assert.NoError(t, err) assert.Len(t, txns, 2) assert.Equal(t, s2t1.ID, txns[0].ID) diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go index 35822aac..c1de71fa 100644 --- a/internal/persistence/persistence.go +++ b/internal/persistence/persistence.go @@ -39,7 +39,7 @@ type Persistence interface { WriteListener(ctx context.Context, spec *apitypes.Listener) error DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error - ListTransactionsByCreateTime(ctx context.Context, after *fftypes.FFTime, limit int) ([]*apitypes.ManagedTX, error) // reverse create time order + ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int) ([]*apitypes.ManagedTX, error) // reverse create time order ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int) ([]*apitypes.ManagedTX, error) // reverse nonce order within signer ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.ManagedTX, error) // reverse UUIDv1 order, only those in pending state GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) diff --git a/internal/tmmsgs/en_api_descriptions.go b/internal/tmmsgs/en_api_descriptions.go index 71231071..77b7c373 100644 --- a/internal/tmmsgs/en_api_descriptions.go +++ b/internal/tmmsgs/en_api_descriptions.go @@ -50,4 +50,6 @@ var ( APIParamListenerID = ffm("api.params.listenerId", "Listener ID") APIParamLimit = ffm("api.params.limit", "Maximum number of entries to return") APIParamAfter = ffm("api.params.after", "Return entries after this ID - for pagination (non-inclusive)") + APIParamTXSigner = ffm("api.params.txSigner", "Return only transactions for a specific signing address, in reverse nonce order") + APIParamTXPending = ffm("api.params.txPending", "Return only pending transactions, in reverse submission sequence (a 'sequenceId' is assigned to each transaction to determine its sequence") ) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 3dbd68dc..9c90d28a 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -76,4 +76,6 @@ var ( MsgPersistenceTXIncomplete = ffe("FF21059", "Transaction is missing indexed fields") MsgNotStarted = ffe("FF21060", "Connector has not fully started yet", 503) MsgNamespacesEmpty = ffe("FF21061", "ffcore.namespaces must contain a list of namespaces") + MsgPaginationErrTxNotFound = ffe("FF21062", "The ID specified in the 'after' option (for pagination) must match an existing transaction: '%s'", 404) + MsgTXConflictSignerPending = ffe("FF21063", "Only one of 'signer' and 'pending' can be supplied when querying transactions", 400) ) diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go index b10311c2..62463d7a 100644 --- a/mocks/persistencemocks/persistence.go +++ b/mocks/persistencemocks/persistence.go @@ -263,11 +263,11 @@ func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, lim } // ListTransactionsByCreateTime provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after *fftypes.FFTime, limit int) ([]*apitypes.ManagedTX, error) { +func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int) ([]*apitypes.ManagedTX, error) { ret := _m.Called(ctx, after, limit) var r0 []*apitypes.ManagedTX - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.FFTime, int) []*apitypes.ManagedTX); ok { + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, int) []*apitypes.ManagedTX); ok { r0 = rf(ctx, after, limit) } else { if ret.Get(0) != nil { @@ -276,7 +276,7 @@ func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after * } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.FFTime, int) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *apitypes.ManagedTX, int) error); ok { r1 = rf(ctx, after, limit) } else { r1 = ret.Error(1) diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index 7c45d2a7..c847cbdd 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -299,9 +299,11 @@ func (m *manager) Start() error { m.fullScanRequests <- true m.firstFullScanDone = make(chan error) m.fullScanLoopDone = make(chan struct{}) + if err := m.restoreStreams(); err != nil { + return err + } go m.fullScanLoop() go m.runAPIServer() - go m.restoreStreams() return m.waitForFirstScanAndStart() } diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index f21f311e..d9417274 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -54,6 +54,7 @@ func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...strin policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) dir, err := ioutil.TempDir("", "ldb_*") assert.NoError(t, err) + config.Set(tmconfig.FFCoreNamespaces, []string{"ns1"}) config.Set(tmconfig.PersistenceLevelDBPath, dir) tmconfig.PolicyEngineBaseConfig.SubSection("simple").SubSection(simple.GasOracleConfig).Set(simple.GasOracleMode, simple.GasOracleModeDisabled) diff --git a/pkg/fftm/route_get_transactions.go b/pkg/fftm/route_get_transactions.go new file mode 100644 index 00000000..dda1f9ad --- /dev/null +++ b/pkg/fftm/route_get_transactions.go @@ -0,0 +1,48 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + "strings" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getTransactions = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getTransactions", + Path: "/transactions", + Method: http.MethodGet, + PathParams: nil, + QueryParams: []*ffapi.QueryParam{ + {Name: "limit", Description: tmmsgs.APIParamLimit}, + {Name: "after", Description: tmmsgs.APIParamAfter}, + {Name: "signer", Description: tmmsgs.APIParamTXSigner}, + {Name: "pending", Description: tmmsgs.APIParamTXPending, IsBool: true}, + }, + Description: tmmsgs.APIEndpointGetSubscriptions, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return []*apitypes.ManagedTX{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getTransactions(r.Req.Context(), r.QP["after"], r.QP["limit"], r.QP["signer"], strings.EqualFold(r.QP["pending"], "true")) + }, + } +} diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go new file mode 100644 index 00000000..d883f4fd --- /dev/null +++ b/pkg/fftm/route_get_transactions_test.go @@ -0,0 +1,106 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" +) + +func newTestTxn(t *testing.T, m *manager, signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { + tx := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + SequenceID: apitypes.UUIDVersion1(), + Nonce: fftypes.NewFFBigInt(nonce), + Created: fftypes.Now(), + Request: &apitypes.TransactionRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: signer, + }, + }, + }, + Status: status, + } + err := m.persistence.WriteTransaction(context.Background(), tx, true) + assert.NoError(t, err) + return tx +} + +func TestGetTransactions(t *testing.T) { + + url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + defer done() + + err := m.Start() + assert.NoError(t, err) + + // Create a few persisted transaction directly in the persistence + s1t1 := newTestTxn(t, m, "0xaaaaa", 10001, apitypes.TxStatusSucceeded) + s2t1 := newTestTxn(t, m, "0xbbbbb", 10001, apitypes.TxStatusPending) + s1t2 := newTestTxn(t, m, "0xaaaaa", 10002, apitypes.TxStatusFailed) + s1t3 := newTestTxn(t, m, "0xaaaaa", 10003, apitypes.TxStatusPending) + + // Get with no filtering (not reverse order) + var transactions []*apitypes.ManagedTX + res, err := resty.New().R(). + SetResult(&transactions). + Get(url + "/transactions") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.Len(t, transactions, 4) + assert.Equal(t, s1t3.ID, transactions[0].ID) + assert.Equal(t, s1t2.ID, transactions[1].ID) + assert.Equal(t, s2t1.ID, transactions[2].ID) + assert.Equal(t, s1t1.ID, transactions[3].ID) + + // Test pagination on default sort/filter + res, err = resty.New().R(). + SetResult(&transactions). + Get(url + "/transactions?limit=1&after=" + s1t2.ID) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.Len(t, transactions, 1) + assert.Equal(t, s2t1.ID, transactions[0].ID) + + // Test pagination on nonce filter + res, err = resty.New().R(). + SetResult(&transactions). + Get(url + "/transactions?signer=0xaaaaa&after=" + s1t2.ID) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.Len(t, transactions, 1) + assert.Equal(t, s1t1.ID, transactions[0].ID) + + // Test pagination on pending filter + res, err = resty.New().R(). + SetResult(&transactions). + Get(url + "/transactions?pending&after=" + s1t3.ID) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.Len(t, transactions, 1) + assert.Equal(t, s2t1.ID, transactions[0].ID) + +} diff --git a/pkg/fftm/routes.go b/pkg/fftm/routes.go index 9d8eee77..c78fafdd 100644 --- a/pkg/fftm/routes.go +++ b/pkg/fftm/routes.go @@ -29,6 +29,7 @@ func (m *manager) routes() []*ffapi.Route { getEventStreams(m), getSubscription(m), getSubscriptions(m), + getTransactions(m), patchEventStream(m), patchEventStreamListener(m), patchSubscription(m), diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index 3da77910..b758f652 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -308,12 +308,19 @@ func (m *manager) getStream(ctx context.Context, idStr string) (*apitypes.EventS }, nil } -func (m *manager) parseAfterAndLimit(ctx context.Context, afterStr, limitStr string) (after *fftypes.UUID, limit int, err error) { +func (m *manager) parseLimit(ctx context.Context, limitStr string) (limit int, err error) { if limitStr != "" { if limit, err = strconv.Atoi(limitStr); err != nil { - return nil, -1, i18n.NewError(ctx, tmmsgs.MsgInvalidLimit, limitStr, err) + return -1, i18n.NewError(ctx, tmmsgs.MsgInvalidLimit, limitStr, err) } } + return limit, nil +} + +func (m *manager) parseAfterAndLimit(ctx context.Context, afterStr, limitStr string) (after *fftypes.UUID, limit int, err error) { + if limit, err = m.parseLimit(ctx, limitStr); err != nil { + return nil, -1, i18n.NewError(ctx, tmmsgs.MsgInvalidLimit, limitStr, err) + } if afterStr != "" { if after, err = fftypes.ParseUUID(ctx, afterStr); err != nil { return nil, -1, err diff --git a/pkg/fftm/transaction_management.go b/pkg/fftm/transaction_management.go new file mode 100644 index 00000000..109013ed --- /dev/null +++ b/pkg/fftm/transaction_management.go @@ -0,0 +1,63 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +func (m *manager) getTransactions(ctx context.Context, afterStr, limitStr, signer string, pending bool) (transactions []*apitypes.ManagedTX, err error) { + limit, err := m.parseLimit(ctx, limitStr) + if err != nil { + return nil, err + } + var afterTx *apitypes.ManagedTX + if afterStr != "" { + // Get the transaction, as we need this to exist to pick the right field depending on the index that's been chosen + afterTx, err = m.persistence.GetTransactionByID(ctx, afterStr) + if err != nil { + return nil, err + } + if afterTx == nil { + return nil, i18n.NewError(ctx, tmmsgs.MsgPaginationErrTxNotFound, afterStr) + } + } + switch { + case signer != "" && pending: + return nil, i18n.NewError(ctx, tmmsgs.MsgTXConflictSignerPending) + case signer != "": + var afterNonce *fftypes.FFBigInt + if afterTx != nil { + afterNonce = afterTx.Nonce + } + return m.persistence.ListTransactionsByNonce(ctx, signer, afterNonce, limit) + case pending: + var afterSequence *fftypes.UUID + if afterTx != nil { + afterSequence = afterTx.SequenceID + } + return m.persistence.ListTransactionsPending(ctx, afterSequence, limit) + default: + return m.persistence.ListTransactionsByCreateTime(ctx, afterTx, limit) + } + +} From 9f347f3c72460f55d14dd487b903815025ddbfea Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 22 Jul 2022 12:36:01 -0400 Subject: [PATCH 57/95] Move to internal persistence of the in-flight transactions Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 2 + config.md | 88 +---- go.mod | 2 +- internal/events/eventstream_test.go | 3 +- internal/persistence/leveldb_persistence.go | 82 +++-- .../persistence/leveldb_persistence_test.go | 46 ++- internal/persistence/persistence.go | 12 +- internal/tmconfig/tmconfig.go | 40 +- internal/tmconfig/tmconfig_test.go | 2 +- internal/tmmsgs/en_api_descriptions.go | 13 +- internal/tmmsgs/en_config_descriptions.go | 12 +- internal/tmmsgs/en_error_messges.go | 2 +- internal/ws/wsconn.go | 3 +- mocks/persistencemocks/persistence.go | 86 ++--- pkg/fftm/api_test.go | 77 +--- pkg/fftm/changelistener.go | 89 ----- pkg/fftm/changelistener_test.go | 96 ----- pkg/fftm/ffcore.go | 124 ------- pkg/fftm/manager.go | 252 ++----------- pkg/fftm/manager_test.go | 348 +----------------- pkg/fftm/nonces.go | 62 +++- pkg/fftm/nonces_test.go | 38 +- pkg/fftm/policyloop.go | 117 +++--- pkg/fftm/policyloop_test.go | 323 +++++++--------- .../route_delete_eventstream_listener_test.go | 3 +- pkg/fftm/route_delete_eventstream_test.go | 3 +- pkg/fftm/route_delete_subscription_test.go | 3 +- .../route_get_eventstream_listener_test.go | 3 +- .../route_get_eventstream_listeners_test.go | 3 +- pkg/fftm/route_get_eventstream_test.go | 3 +- pkg/fftm/route_get_eventstreams_test.go | 3 +- pkg/fftm/route_get_subscription_test.go | 3 +- pkg/fftm/route_get_subscriptions_test.go | 3 +- pkg/fftm/route_get_transactions.go | 3 +- pkg/fftm/route_get_transactions_test.go | 3 +- .../route_patch_eventstream_listener_test.go | 3 +- pkg/fftm/route_patch_eventstream_test.go | 3 +- pkg/fftm/route_patch_subscription_test.go | 3 +- ...te_post_eventstream_listener_reset_test.go | 3 +- .../route_post_eventstream_listeners_test.go | 3 +- .../route_post_eventstream_resume_test.go | 3 +- .../route_post_eventstream_suspend_test.go | 3 +- pkg/fftm/route_post_eventstream_test.go | 3 +- .../route_post_subscription_reset_test.go | 3 +- pkg/fftm/route_post_subscriptions_test.go | 3 +- pkg/fftm/send_tx.go | 7 +- pkg/fftm/stream_management.go | 13 +- pkg/fftm/stream_management_test.go | 24 +- pkg/fftm/transaction_management.go | 19 +- .../simple/simple_policy_engine.go | 2 +- 50 files changed, 553 insertions(+), 1494 deletions(-) delete mode 100644 pkg/fftm/changelistener.go delete mode 100644 pkg/fftm/changelistener_test.go delete mode 100644 pkg/fftm/ffcore.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 7b768ad9..47eab490 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,7 @@ "ehtype", "estype", "ethconnect", + "ethtypes", "eventsmocks", "eventstream", "eventstreams", @@ -58,6 +59,7 @@ "openapi", "optype", "persistencemocks", + "pluggable", "policyengine", "policyenginemocks", "policyengines", diff --git a/config.md b/config.md index 1ba2ea54..37503792 100644 --- a/config.md +++ b/config.md @@ -83,54 +83,9 @@ nav_order: 2 |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|factor|Factor to increase the delay by, between each retry|`boolean`|`` -|initialDelay|Initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` -|maxDelay|Maximum delay between retries|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` - -## ffcore - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` -|expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` -|headers|Adds custom headers to HTTP requests|`map[string]string`|`` -|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms` -|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100` -|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` -|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` -|url|The URL of the FireFly core admin API server to connect to|`string`|`` - -## ffcore.auth - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|password|Password|`string`|`` -|username|Username|`string`|`` - -## ffcore.proxy - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|url|Optional HTTP proxy URL to use for the FireFly core admin API server|`string`|`` - -## ffcore.retry - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|count|The maximum number of times to retry|`int`|`5` -|enabled|Enables retries|`boolean`|`false` -|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` -|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` - -## ffcore.ws - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|heartbeatInterval|The amount of time to wait between heartbeat signals on the WebSocket connection|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` -|initialConnectAttempts|The number of attempts FireFly will make to connect to the WebSocket when starting up, before failing|`int`|`5` -|path|The WebSocket sever URL to which FireFly should connect|WebSocket URL `string`|`/admin/ws` -|readBufferSize|The size in bytes of the read buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb` -|writeBufferSize|The size in bytes of the write buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb` +|factor|Factor to increase the delay by, between each retry|`boolean`|`2` +|initialDelay|Initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` +|maxDelay|Maximum delay between retries|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` ## log @@ -164,27 +119,6 @@ nav_order: 2 |message|Configures the JSON key containing the log message|`string`|`message` |timestamp|Configures the JSON key containing the timestamp of the log|`string`|`@timestamp` -## operations - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|errorHistoryCount|The number of historical errors to retain in the operation|`int`|`25` -|types|The operation types to query in FireFly core, that might have been submitted via this Transaction Manager|string[]|`[blockchain_invoke blockchain_pin_batch token_create_pool]` - -## operations.changeListener - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|enabled|Whether to enable the change event listener to detect updates made to operations outside of the FFTM|`boolean`|`` - -## operations.fullScan - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|minimumDelay|The minimum delay between full scans of the FireFly core API, when reconnecting, or recovering from missed events / errors|[`time.Duration`](https://pkg.go.dev/time#Duration)|`5s` -|pageSize|The page size to use when performing a full scan of the ForeFly core API on startup, or recovery|`int`|`100` -|startupMaxRetries|The page size to use when performing a full scan of the ForeFly core API on startup, or recovery|`int`|`10` - ## persistence |Key|Description|Type|Default Value| @@ -257,6 +191,22 @@ nav_order: 2 |---|-----------|----|-------------| |interval|Interval at which to invoke the policy engine to evaluate outstanding transactions|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` +## policyloop.retry + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|factor|The retry backoff factor|`boolean`|`2` +|initialDelay|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` +|maxDelay|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` + +## transactions + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|errorHistoryCount|The number of historical errors to retain in the operation|`int`|`25` +|maxInFlight|The maximum number of transactions to have in-flight with the policy engine / blockchain transaction pool|`int`|`100` +|nonceStateTimeout|How old the most recently submitted transaction record in our local state needs to be, before we make a request to the node to query the next nonce for a signing address|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1h` + ## webhooks |Key|Description|Type|Default Value| diff --git a/go.mod b/go.mod index 84f68e10..c088fba4 100644 --- a/go.mod +++ b/go.mod @@ -56,4 +56,4 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) \ No newline at end of file +) diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 4a98e9a2..27b629dc 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -150,7 +150,8 @@ func TestConfigNewDefaultsUpdate(t *testing.T) { "suspended":false, "type":"websocket", "websocket": { - "distributionMode":"load_balance" + "distributionMode":"load_balance", + "topic":"" } }`, string(b)) diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index 4c8a6a45..655c7248 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "sync" "github.com/hyperledger/firefly-common/pkg/config" @@ -58,6 +59,13 @@ func NewLevelDBPersistence(ctx context.Context) (Persistence, error) { }, nil } +type SortDirection int + +const ( + SortDirectionAscending SortDirection = iota + SortDirectionDescending +) + const checkpointsPrefix = "checkpoints_0/" const eventstreamsPrefix = "eventstreams_0/" const eventstreamsEnd = "eventstreams_1" @@ -148,6 +156,7 @@ func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target in } func (p *leveldbPersistence) listJSON(ctx context.Context, collectionPrefix, collectionEnd, after string, limit int, + dir SortDirection, val func() interface{}, // return a pointer to a pointer variable, of the type to unmarshal add func(interface{}), // passes back the val() for adding to the list, if the filters match indexResolver func(ctx context.Context, k []byte) ([]byte, error), // if non-nil then the initial lookup will be passed to this, to lookup the target bytes. Nil skips item @@ -157,22 +166,44 @@ func (p *leveldbPersistence) listJSON(ctx context.Context, collectionPrefix, col Start: []byte(collectionPrefix), Limit: []byte(collectionEnd), } - if after != "" { - collectionRange.Limit = []byte(collectionPrefix + after) + var it iterator.Iterator + switch dir { + case SortDirectionAscending: + afterKey := collectionPrefix + after + if after != "" { + collectionRange.Start = []byte(afterKey) + } + it = p.db.NewIterator(collectionRange, &opt.ReadOptions{DontFillCache: true}) + if after != "" && it.Next() { + if !strings.HasPrefix(string(it.Key()), afterKey) { + it.Prev() // skip back, as the first key was already after the "after" key + } + } + default: + if after != "" { + collectionRange.Limit = []byte(collectionPrefix + after) // exclusive for limit, so no need to fiddle here + } + it = p.db.NewIterator(collectionRange, &opt.ReadOptions{DontFillCache: true}) } - it := p.db.NewIterator(collectionRange, &opt.ReadOptions{DontFillCache: true}) defer it.Release() - return p.iterateReverseJSON(ctx, it, limit, val, add, indexResolver, filters...) + return p.iterateJSON(ctx, it, limit, dir, val, add, indexResolver, filters...) } -func (p *leveldbPersistence) iterateReverseJSON(ctx context.Context, it iterator.Iterator, limit int, - val func() interface{}, add func(interface{}), indexResolver func(ctx context.Context, k []byte) ([]byte, error), filters ...func(interface{}) bool, +func (p *leveldbPersistence) iterateJSON(ctx context.Context, it iterator.Iterator, limit int, + dir SortDirection, val func() interface{}, add func(interface{}), indexResolver func(ctx context.Context, k []byte) ([]byte, error), filters ...func(interface{}) bool, ) (orphanedIdxKeys [][]byte, err error) { count := 0 - next := it.Last // First iteration of the loop goes to the end + next := it.Next // forwards we enter this function before the first key + if dir == SortDirectionDescending { + next = it.Last // reverse we enter this function + } itLoop: for next() { - next = it.Prev // Future iterations call prev (note reverse sort order moving backwards through the selection) + if dir == SortDirectionDescending { + next = it.Prev + } else { + next = it.Next + } v := val() b := it.Value() if indexResolver != nil { @@ -184,7 +215,7 @@ itLoop: if b == nil { log.L(ctx).Warnf("Skipping orphaned index key '%s' pointing to '%s'", it.Key(), valKey) orphanedIdxKeys = append(orphanedIdxKeys, it.Key()) - continue + continue itLoop } } err := json.Unmarshal(b, v) @@ -230,9 +261,9 @@ func (p *leveldbPersistence) DeleteCheckpoint(ctx context.Context, streamID *fft return p.deleteKeys(ctx, prefixedKey(checkpointsPrefix, streamID)) } -func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) { +func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.EventStream, error) { streams := make([]*apitypes.EventStream, 0) - if _, err := p.listJSON(ctx, eventstreamsPrefix, eventstreamsEnd, after.String(), limit, + if _, err := p.listJSON(ctx, eventstreamsPrefix, eventstreamsEnd, after.String(), limit, dir, func() interface{} { var v *apitypes.EventStream; return &v }, func(v interface{}) { streams = append(streams, *(v.(**apitypes.EventStream))) }, nil, @@ -255,9 +286,9 @@ func (p *leveldbPersistence) DeleteStream(ctx context.Context, streamID *fftypes return p.deleteKeys(ctx, prefixedKey(eventstreamsPrefix, streamID)) } -func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { +func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.Listener, error) { listeners := make([]*apitypes.Listener, 0) - if _, err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, + if _, err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, dir, func() interface{} { var v *apitypes.Listener; return &v }, func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, nil, @@ -267,9 +298,9 @@ func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.U return listeners, nil } -func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { +func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { listeners := make([]*apitypes.Listener, 0) - if _, err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, + if _, err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, dir, func() interface{} { var v *apitypes.Listener; return &v }, func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, nil, @@ -313,11 +344,11 @@ func (p *leveldbPersistence) cleanupOrphanedTXIdxKeys(ctx context.Context, orpha } } -func (p *leveldbPersistence) listTransactionsByIndex(ctx context.Context, collectionPrefix, collectionEnd, afterStr string, limit int) ([]*apitypes.ManagedTX, error) { +func (p *leveldbPersistence) listTransactionsByIndex(ctx context.Context, collectionPrefix, collectionEnd, afterStr string, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { p.txMux.RLock() transactions := make([]*apitypes.ManagedTX, 0) - orphanedIdxKeys, err := p.listJSON(ctx, collectionPrefix, collectionEnd, afterStr, limit, + orphanedIdxKeys, err := p.listJSON(ctx, collectionPrefix, collectionEnd, afterStr, limit, dir, func() interface{} { var v *apitypes.ManagedTX; return &v }, func(v interface{}) { transactions = append(transactions, *(v.(**apitypes.ManagedTX))) }, p.indexLookupCallback, @@ -333,24 +364,24 @@ func (p *leveldbPersistence) listTransactionsByIndex(ctx context.Context, collec return transactions, nil } -func (p *leveldbPersistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int) ([]*apitypes.ManagedTX, error) { +func (p *leveldbPersistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { afterStr := "" if after != nil { afterStr = fmt.Sprintf("%.19d/%s", after.Created.UnixNano(), after.SequenceID) } - return p.listTransactionsByIndex(ctx, txCreatedIndexPrefix, txCreatedIndexEnd, afterStr, limit) + return p.listTransactionsByIndex(ctx, txCreatedIndexPrefix, txCreatedIndexEnd, afterStr, limit, dir) } -func (p *leveldbPersistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int) ([]*apitypes.ManagedTX, error) { +func (p *leveldbPersistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { afterStr := "" if after != nil { afterStr = fmt.Sprintf("%.24d", after.Int()) } - return p.listTransactionsByIndex(ctx, signerNoncePrefix(signer), signerNonceEnd(signer), afterStr, limit) + return p.listTransactionsByIndex(ctx, signerNoncePrefix(signer), signerNonceEnd(signer), afterStr, limit, dir) } -func (p *leveldbPersistence) ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.ManagedTX, error) { - return p.listTransactionsByIndex(ctx, txPendingIndexPrefix, txPendingIndexEnd, after.String(), limit) +func (p *leveldbPersistence) ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { + return p.listTransactionsByIndex(ctx, txPendingIndexPrefix, txPendingIndexEnd, after.String(), limit, dir) } func (p *leveldbPersistence) GetTransactionByID(ctx context.Context, txID string) (tx *apitypes.ManagedTX, err error) { @@ -379,7 +410,8 @@ func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes. tx.Request.From == "" || tx.Nonce == nil || tx.SequenceID == nil || - tx.Created == nil { + tx.Created == nil || + tx.Status == "" { return i18n.NewError(ctx, tmmsgs.MsgPersistenceTXIncomplete) } idKey := txDataKey(tx.ID) @@ -399,7 +431,7 @@ func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes. } // If we are creating/updating a record that is not pending, we need to ensure there is no pending index associated with it if err == nil && tx.Status != apitypes.TxStatusPending { - err = p.deleteKeys(ctx) + err = p.deleteKeys(ctx, txPendingIndexKey(tx.SequenceID)) } if err == nil { err = p.writeJSON(ctx, idKey, tx) diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index 71b17c1d..7832d4dc 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -116,7 +116,7 @@ func TestReadWriteStreams(t *testing.T) { } p.WriteStream(ctx, s3) - streams, err := p.ListStreams(ctx, nil, 0) + streams, err := p.ListStreams(ctx, nil, 0, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, streams, 3) @@ -126,13 +126,13 @@ func TestReadWriteStreams(t *testing.T) { // Test pagination - streams, err = p.ListStreams(ctx, nil, 2) + streams, err = p.ListStreams(ctx, nil, 2, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, streams, 2) assert.Equal(t, s3.ID, streams[0].ID) assert.Equal(t, s2.ID, streams[1].ID) - streams, err = p.ListStreams(ctx, streams[1].ID, 2) + streams, err = p.ListStreams(ctx, streams[1].ID, 2, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, streams, 1) assert.Equal(t, s1.ID, streams[0].ID) @@ -141,7 +141,7 @@ func TestReadWriteStreams(t *testing.T) { err = p.DeleteStream(ctx, s2.ID) assert.NoError(t, err) - streams, err = p.ListStreams(ctx, nil, 2) + streams, err = p.ListStreams(ctx, nil, 2, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, streams, 2) assert.Equal(t, s3.ID, streams[0].ID) @@ -190,7 +190,7 @@ func TestReadWriteListeners(t *testing.T) { err = p.WriteListener(ctx, s1l2) assert.NoError(t, err) - listeners, err := p.ListListeners(ctx, nil, 0) + listeners, err := p.ListListeners(ctx, nil, 0, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, listeners, 3) @@ -200,7 +200,7 @@ func TestReadWriteListeners(t *testing.T) { // Test stream filter - listeners, err = p.ListStreamListeners(ctx, nil, 0, sID1) + listeners, err = p.ListStreamListeners(ctx, nil, 0, SortDirectionDescending, sID1) assert.NoError(t, err) assert.Len(t, listeners, 2) assert.Equal(t, s1l2.ID, listeners[0].ID) @@ -210,7 +210,7 @@ func TestReadWriteListeners(t *testing.T) { err = p.DeleteListener(ctx, s2l1.ID) assert.NoError(t, err) - listeners, err = p.ListStreamListeners(ctx, nil, 0, sID2) + listeners, err = p.ListStreamListeners(ctx, nil, 0, SortDirectionDescending, sID2) assert.NoError(t, err) assert.Len(t, listeners, 0) @@ -290,7 +290,7 @@ func TestReadWriteManagedTransactions(t *testing.T) { s1t2 := textTX("0xaaaaa", 10002, apitypes.TxStatusPending) s1t3 := textTX("0xaaaaa", 10003, apitypes.TxStatusPending) - txns, err := p.ListTransactionsByCreateTime(ctx, nil, 0) + txns, err := p.ListTransactionsByCreateTime(ctx, nil, 0, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, txns, 4) @@ -301,7 +301,7 @@ func TestReadWriteManagedTransactions(t *testing.T) { // Only list pending - txns, err = p.ListTransactionsPending(ctx, nil, 0) + txns, err = p.ListTransactionsPending(ctx, nil, 0, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, txns, 2) @@ -310,7 +310,7 @@ func TestReadWriteManagedTransactions(t *testing.T) { // List with time range - txns, err = p.ListTransactionsByCreateTime(ctx, s1t2, 0) + txns, err = p.ListTransactionsByCreateTime(ctx, s1t2, 0, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, txns, 2) assert.Equal(t, s2t1.ID, txns[0].ID) @@ -320,10 +320,16 @@ func TestReadWriteManagedTransactions(t *testing.T) { err = p.DeleteTransaction(ctx, s1t2.ID) assert.NoError(t, err) - txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t3.Nonce, 0) + txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t1.Nonce, 0, SortDirectionAscending) assert.NoError(t, err) assert.Len(t, txns, 1) - assert.Equal(t, s1t1.ID, txns[0].ID) + assert.Equal(t, s1t3.ID, txns[0].ID) + + // Check we can use after with the deleted nonce, and not skip the one after + txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t2.Nonce, 0, SortDirectionAscending) + assert.NoError(t, err) + assert.Len(t, txns, 1) + assert.Equal(t, s1t3.ID, txns[0].ID) // Test get direct @@ -350,7 +356,7 @@ func TestListStreamsBadJSON(t *testing.T) { err := p.db.Put(prefixedKey(eventstreamsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) - _, err = p.ListStreams(context.Background(), nil, 0) + _, err = p.ListStreams(context.Background(), nil, 0, SortDirectionDescending) assert.Error(t, err) } @@ -363,10 +369,10 @@ func TestListListenersBadJSON(t *testing.T) { err := p.db.Put(prefixedKey(listenersPrefix, lID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) - _, err = p.ListListeners(context.Background(), nil, 0) + _, err = p.ListListeners(context.Background(), nil, 0, SortDirectionDescending) assert.Error(t, err) - _, err = p.ListStreamListeners(context.Background(), nil, 0, apitypes.UUIDVersion1()) + _, err = p.ListStreamListeners(context.Background(), nil, 0, SortDirectionDescending, apitypes.UUIDVersion1()) assert.Error(t, err) } @@ -452,7 +458,7 @@ func TestListManagedTransactionFail(t *testing.T) { err = p.db.Put(txDataKey(tx.ID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) - _, err = p.ListTransactionsByCreateTime(context.Background(), nil, 0) + _, err = p.ListTransactionsByCreateTime(context.Background(), nil, 0, SortDirectionDescending) assert.Error(t, err) } @@ -469,7 +475,7 @@ func TestListManagedTransactionCleanupOrphans(t *testing.T) { err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) assert.NoError(t, err) - txns, err := p.ListTransactionsByCreateTime(context.Background(), nil, 0) + txns, err := p.ListTransactionsByCreateTime(context.Background(), nil, 0, SortDirectionDescending) assert.NoError(t, err) assert.Empty(t, txns) @@ -489,7 +495,7 @@ func TestListNonceAllocationsFail(t *testing.T) { err = p.db.Put(txDataKey(txID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) - _, err = p.ListTransactionsByNonce(context.Background(), "0xaaa", nil, 0) + _, err = p.ListTransactionsByNonce(context.Background(), "0xaaa", nil, 0, SortDirectionDescending) assert.Error(t, err) } @@ -504,7 +510,7 @@ func TestListInflightTransactionFail(t *testing.T) { err = p.db.Put(txDataKey(txID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) - _, err = p.ListTransactionsPending(context.Background(), nil, 0) + _, err = p.ListTransactionsPending(context.Background(), nil, 0, SortDirectionDescending) assert.Error(t, err) } @@ -550,6 +556,7 @@ func TestIterateReverseJSONFailIdxResolve(t *testing.T) { "test_1", "", 0, + SortDirectionAscending, func() interface{} { return make(map[string]interface{}) }, func(i interface{}) {}, func(ctx context.Context, k []byte) ([]byte, error) { @@ -571,6 +578,7 @@ func TestIterateReverseJSONSkipIdxResolve(t *testing.T) { "test_1", "", 0, + SortDirectionAscending, func() interface{} { return make(map[string]interface{}) }, func(_ interface{}) { assert.Fail(t, "Should not be called") diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go index c1de71fa..6f1273c1 100644 --- a/internal/persistence/persistence.go +++ b/internal/persistence/persistence.go @@ -28,20 +28,20 @@ type Persistence interface { GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error - ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) // reverse UUIDv1 order + ListStreams(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.EventStream, error) // reverse UUIDv1 order GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) WriteStream(ctx context.Context, spec *apitypes.EventStream) error DeleteStream(ctx context.Context, streamID *fftypes.UUID) error - ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) // reverse UUIDv1 order - ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) + ListListeners(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.Listener, error) // reverse UUIDv1 order + ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection, streamID *fftypes.UUID) ([]*apitypes.Listener, error) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) WriteListener(ctx context.Context, spec *apitypes.Listener) error DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error - ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int) ([]*apitypes.ManagedTX, error) // reverse create time order - ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int) ([]*apitypes.ManagedTX, error) // reverse nonce order within signer - ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.ManagedTX, error) // reverse UUIDv1 order, only those in pending state + ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) // reverse create time order + ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) // reverse nonce order within signer + ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) // reverse UUIDv1 order, only those in pending state GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (*apitypes.ManagedTX, error) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, possiblyNew bool) error diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 19760d4a..317ba181 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -20,8 +20,6 @@ import ( "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/ffresty" "github.com/hyperledger/firefly-common/pkg/httpserver" - "github.com/hyperledger/firefly-common/pkg/wsclient" - "github.com/hyperledger/firefly/pkg/core" "github.com/spf13/viper" ) @@ -32,13 +30,13 @@ var ( ConfirmationsBlockQueueLength = ffc("confirmations.blockQueueLength") ConfirmationsStaleReceiptTimeout = ffc("confirmations.staleReceiptTimeout") ConfirmationsNotificationQueueLength = ffc("confirmations.notificationQueueLength") - OperationsTypes = ffc("operations.types") - OperationsFullScanStartupMaxRetries = ffc("operations.fullScan.startupMaxRetries") - OperationsFullScanPageSize = ffc("operations.fullScan.pageSize") - OperationsFullScanMinimumDelay = ffc("operations.fullScan.minimumDelay") - OperationsErrorHistoryCount = ffc("operations.errorHistoryCount") - OperationsChangeListenerEnabled = ffc("operations.changeListener.enabled") + TransactionsErrorHistoryCount = ffc("transactions.errorHistoryCount") + TransactionsMaxInFlight = ffc("transactions.maxInFlight") + TransactionsNonceStateTimeout = ffc("transactions.nonceStateTimeout") PolicyLoopInterval = ffc("policyloop.interval") + PolicyLoopRetryInitDelay = ffc("policyloop.retry.initialDelay") + PolicyLoopRetryMaxDelay = ffc("policyloop.retry.maxDelay") + PolicyLoopRetryFactor = ffc("policyloop.retry.factor") PolicyEngineName = ffc("policyengine.name") EventStreamsDefaultsBatchSize = ffc("eventstreams.defaults.batchSize") EventStreamsDefaultsBatchTimeout = ffc("eventstreams.defaults.batchTimeout") @@ -58,11 +56,8 @@ var ( PersistenceLevelDBSyncWrites = ffc("persistence.leveldb.syncWrites") APIDefaultRequestTimeout = ffc("api.defaultRequestTimeout") APIMaxRequestTimeout = ffc("api.maxRequestTimeout") - FFCoreNamespaces = ffc("ffcore.namespaces") ) -var FFCoreConfig config.Section - var APIConfig config.Section var CorsConfig config.Section @@ -72,19 +67,13 @@ var PolicyEngineBaseConfig config.Section var WebhookPrefix config.Section func setDefaults() { - viper.SetDefault(string(OperationsFullScanPageSize), 100) - viper.SetDefault(string(OperationsFullScanMinimumDelay), "5s") - viper.SetDefault(string(OperationsTypes), []string{ - core.OpTypeBlockchainInvoke.String(), - core.OpTypeBlockchainPinBatch.String(), - core.OpTypeTokenCreatePool.String(), - }) - viper.SetDefault(string(OperationsFullScanStartupMaxRetries), 10) + viper.SetDefault(string(TransactionsMaxInFlight), 100) + viper.SetDefault(string(TransactionsErrorHistoryCount), 25) + viper.SetDefault(string(TransactionsNonceStateTimeout), "1h") viper.SetDefault(string(ConfirmationsRequired), 20) viper.SetDefault(string(ConfirmationsBlockQueueLength), 50) viper.SetDefault(string(ConfirmationsNotificationQueueLength), 50) viper.SetDefault(string(ConfirmationsStaleReceiptTimeout), "1m") - viper.SetDefault(string(OperationsErrorHistoryCount), 25) viper.SetDefault(string(PolicyLoopInterval), "1s") viper.SetDefault(string(PolicyEngineName), "simple") @@ -105,16 +94,17 @@ func setDefaults() { viper.SetDefault(string(APIDefaultRequestTimeout), "30s") viper.SetDefault(string(APIMaxRequestTimeout), "10m") - viper.SetDefault(string(FFCoreNamespaces), []string{}) + viper.SetDefault(string(PolicyLoopRetryInitDelay), "250ms") + viper.SetDefault(string(PolicyLoopRetryMaxDelay), "30s") + viper.SetDefault(string(PolicyLoopRetryFactor), 2.0) + viper.SetDefault(string(EventStreamsRetryInitDelay), "250ms") + viper.SetDefault(string(EventStreamsRetryMaxDelay), "30s") + viper.SetDefault(string(EventStreamsRetryFactor), 2.0) } func Reset() { config.RootConfigReset(setDefaults) - FFCoreConfig = config.RootSection("ffcore") - wsclient.InitConfig(FFCoreConfig) - FFCoreConfig.SetDefault(wsclient.WSConfigKeyPath, "/admin/ws") - APIConfig = config.RootSection("api") httpserver.InitHTTPConfig(APIConfig, 5008) diff --git a/internal/tmconfig/tmconfig_test.go b/internal/tmconfig/tmconfig_test.go index 2919e982..3a2413e4 100644 --- a/internal/tmconfig/tmconfig_test.go +++ b/internal/tmconfig/tmconfig_test.go @@ -28,5 +28,5 @@ const configDir = "../../test/data/config" func TestInitConfigOK(t *testing.T) { Reset() - assert.Equal(t, 100, config.GetInt(OperationsFullScanPageSize)) + assert.Equal(t, 50, config.GetInt(EventStreamsDefaultsBatchSize)) } diff --git a/internal/tmmsgs/en_api_descriptions.go b/internal/tmmsgs/en_api_descriptions.go index 77b7c373..ee004763 100644 --- a/internal/tmmsgs/en_api_descriptions.go +++ b/internal/tmmsgs/en_api_descriptions.go @@ -46,10 +46,11 @@ var ( APIEndpointPatchEventStreamListener = ffm("api.endpoints.patch.eventstream.listener", "Update event stream listener") APIEndpointDeleteEventStreamListener = ffm("api.endpoints.delete.eventstream.listener", "Delete event stream listener") - APIParamStreamID = ffm("api.params.streamId", "Event Stream ID") - APIParamListenerID = ffm("api.params.listenerId", "Listener ID") - APIParamLimit = ffm("api.params.limit", "Maximum number of entries to return") - APIParamAfter = ffm("api.params.after", "Return entries after this ID - for pagination (non-inclusive)") - APIParamTXSigner = ffm("api.params.txSigner", "Return only transactions for a specific signing address, in reverse nonce order") - APIParamTXPending = ffm("api.params.txPending", "Return only pending transactions, in reverse submission sequence (a 'sequenceId' is assigned to each transaction to determine its sequence") + APIParamStreamID = ffm("api.params.streamId", "Event Stream ID") + APIParamListenerID = ffm("api.params.listenerId", "Listener ID") + APIParamLimit = ffm("api.params.limit", "Maximum number of entries to return") + APIParamAfter = ffm("api.params.after", "Return entries after this ID - for pagination (non-inclusive)") + APIParamTXSigner = ffm("api.params.txSigner", "Return only transactions for a specific signing address, in reverse nonce order") + APIParamTXPending = ffm("api.params.txPending", "Return only pending transactions, in reverse submission sequence (a 'sequenceId' is assigned to each transaction to determine its sequence") + APIParamSortDirection = ffm("api.params.sortDirection", "Sort direction: 'asc'/'ascending' or 'desc'/'descending'") ) diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go index 9b195c03..cbbeac7a 100644 --- a/internal/tmmsgs/en_config_descriptions.go +++ b/internal/tmmsgs/en_config_descriptions.go @@ -42,15 +42,9 @@ var ( ConfigConfirmationsRequired = ffc("config.confirmations.required", "Number of confirmations required to consider a transaction/event final", i18n.IntType) ConfigConfirmationsStaleReceiptTimeout = ffc("config.confirmations.staleReceiptTimeout", "Duration after which to force a receipt check for a pending transaction", i18n.TimeDurationType) - ConfigFFCoreURL = ffc("config.ffcore.url", "The URL of the FireFly core admin API server to connect to", i18n.StringType) - ConfigFFCoreProxyURL = ffc("config.ffcore.proxy.url", "Optional HTTP proxy URL to use for the FireFly core admin API server", i18n.StringType) - - ConfigOperationsTypes = ffc("config.operations.types", "The operation types to query in FireFly core, that might have been submitted via this Transaction Manager", "string[]") - ConfigOperationsFullScanMinimumDelay = ffc("config.operations.fullScan.minimumDelay", "The minimum delay between full scans of the FireFly core API, when reconnecting, or recovering from missed events / errors", i18n.TimeDurationType) - ConfigOperationsFullScanPageSize = ffc("config.operations.fullScan.pageSize", "The page size to use when performing a full scan of the ForeFly core API on startup, or recovery", i18n.IntType) - ConfigOperationsFullScanStartupMaxRetries = ffc("config.operations.fullScan.startupMaxRetries", "The page size to use when performing a full scan of the ForeFly core API on startup, or recovery", i18n.IntType) - ConfigOperationsErrorHistoryCount = ffc("config.operations.errorHistoryCount", "The number of historical errors to retain in the operation", i18n.IntType) - ConfigOperationsChangeListenerEnabled = ffc("config.operations.changeListener.enabled", "Whether to enable the change event listener to detect updates made to operations outside of the FFTM", i18n.BooleanType) + ConfigTransactionsErrorHistoryCount = ffc("config.transactions.errorHistoryCount", "The number of historical errors to retain in the operation", i18n.IntType) + ConfigTransactionsMaxInflight = ffc("config.transactions.maxInFlight", "The maximum number of transactions to have in-flight with the policy engine / blockchain transaction pool", i18n.IntType) + ConfigTransactionsNonceStateTimeout = ffc("config.transactions.nonceStateTimeout", "How old the most recently submitted transaction record in our local state needs to be, before we make a request to the node to query the next nonce for a signing address", i18n.TimeDurationType) ConfigPolicyEngineName = ffc("config.policyengine.name", "The name of the policy engine to use", i18n.StringType) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 9c90d28a..88504e24 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -75,7 +75,7 @@ var ( MsgPersistenceInitFailed = ffe("FF21058", "Failed to initialize persistence at path '%s'") MsgPersistenceTXIncomplete = ffe("FF21059", "Transaction is missing indexed fields") MsgNotStarted = ffe("FF21060", "Connector has not fully started yet", 503) - MsgNamespacesEmpty = ffe("FF21061", "ffcore.namespaces must contain a list of namespaces") MsgPaginationErrTxNotFound = ffe("FF21062", "The ID specified in the 'after' option (for pagination) must match an existing transaction: '%s'", 404) MsgTXConflictSignerPending = ffe("FF21063", "Only one of 'signer' and 'pending' can be supplied when querying transactions", 400) + MsgInvalidSortDirection = ffe("FF21064", "Sort direction must be 'asc'/'ascending' or 'desc'/'descending': '%s'", 400) ) diff --git a/internal/ws/wsconn.go b/internal/ws/wsconn.go index bcf4b222..2968b4a0 100644 --- a/internal/ws/wsconn.go +++ b/internal/ws/wsconn.go @@ -133,8 +133,7 @@ func (c *webSocketConnection) listenTopic(t *webSocketTopic) { } func (c *webSocketConnection) listenReplies() { - // At this point, transaction manager does not send replies on the websocket - // Instead, operations are updated in Core via the SPI + c.server.ListenForReplies(c) } func (c *webSocketConnection) listen() { diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go index 62463d7a..4153cf39 100644 --- a/mocks/persistencemocks/persistence.go +++ b/mocks/persistencemocks/persistence.go @@ -10,6 +10,8 @@ import ( fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" mock "github.com/stretchr/testify/mock" + + persistence "github.com/hyperledger/firefly-transaction-manager/internal/persistence" ) // Persistence is an autogenerated mock type for the Persistence type @@ -193,13 +195,13 @@ func (_m *Persistence) GetTransactionByNonce(ctx context.Context, signer string, return r0, r1 } -// ListListeners provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.Listener, error) { - ret := _m.Called(ctx, after, limit) +// ListListeners provides a mock function with given fields: ctx, after, limit, dir +func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int, dir persistence.SortDirection) ([]*apitypes.Listener, error) { + ret := _m.Called(ctx, after, limit, dir) var r0 []*apitypes.Listener - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.Listener); ok { - r0 = rf(ctx, after, limit) + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) []*apitypes.Listener); ok { + r0 = rf(ctx, after, limit, dir) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*apitypes.Listener) @@ -207,8 +209,8 @@ func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, l } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int) error); ok { - r1 = rf(ctx, after, limit) + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) error); ok { + r1 = rf(ctx, after, limit, dir) } else { r1 = ret.Error(1) } @@ -216,13 +218,13 @@ func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, l return r0, r1 } -// ListStreamListeners provides a mock function with given fields: ctx, after, limit, streamID -func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { - ret := _m.Called(ctx, after, limit, streamID) +// ListStreamListeners provides a mock function with given fields: ctx, after, limit, dir, streamID +func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, dir persistence.SortDirection, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { + ret := _m.Called(ctx, after, limit, dir, streamID) var r0 []*apitypes.Listener - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, *fftypes.UUID) []*apitypes.Listener); ok { - r0 = rf(ctx, after, limit, streamID) + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection, *fftypes.UUID) []*apitypes.Listener); ok { + r0 = rf(ctx, after, limit, dir, streamID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*apitypes.Listener) @@ -230,8 +232,8 @@ func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.U } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, *fftypes.UUID) error); ok { - r1 = rf(ctx, after, limit, streamID) + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection, *fftypes.UUID) error); ok { + r1 = rf(ctx, after, limit, dir, streamID) } else { r1 = ret.Error(1) } @@ -239,13 +241,13 @@ func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.U return r0, r1 } -// ListStreams provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.EventStream, error) { - ret := _m.Called(ctx, after, limit) +// ListStreams provides a mock function with given fields: ctx, after, limit, dir +func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int, dir persistence.SortDirection) ([]*apitypes.EventStream, error) { + ret := _m.Called(ctx, after, limit, dir) var r0 []*apitypes.EventStream - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.EventStream); ok { - r0 = rf(ctx, after, limit) + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) []*apitypes.EventStream); ok { + r0 = rf(ctx, after, limit, dir) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*apitypes.EventStream) @@ -253,8 +255,8 @@ func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, lim } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int) error); ok { - r1 = rf(ctx, after, limit) + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) error); ok { + r1 = rf(ctx, after, limit, dir) } else { r1 = ret.Error(1) } @@ -262,13 +264,13 @@ func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, lim return r0, r1 } -// ListTransactionsByCreateTime provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int) ([]*apitypes.ManagedTX, error) { - ret := _m.Called(ctx, after, limit) +// ListTransactionsByCreateTime provides a mock function with given fields: ctx, after, limit, dir +func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir persistence.SortDirection) ([]*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, after, limit, dir) var r0 []*apitypes.ManagedTX - if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, int) []*apitypes.ManagedTX); ok { - r0 = rf(ctx, after, limit) + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, int, persistence.SortDirection) []*apitypes.ManagedTX); ok { + r0 = rf(ctx, after, limit, dir) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*apitypes.ManagedTX) @@ -276,8 +278,8 @@ func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after * } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *apitypes.ManagedTX, int) error); ok { - r1 = rf(ctx, after, limit) + if rf, ok := ret.Get(1).(func(context.Context, *apitypes.ManagedTX, int, persistence.SortDirection) error); ok { + r1 = rf(ctx, after, limit, dir) } else { r1 = ret.Error(1) } @@ -285,13 +287,13 @@ func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after * return r0, r1 } -// ListTransactionsByNonce provides a mock function with given fields: ctx, signer, after, limit -func (_m *Persistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int) ([]*apitypes.ManagedTX, error) { - ret := _m.Called(ctx, signer, after, limit) +// ListTransactionsByNonce provides a mock function with given fields: ctx, signer, after, limit, dir +func (_m *Persistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int, dir persistence.SortDirection) ([]*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, signer, after, limit, dir) var r0 []*apitypes.ManagedTX - if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt, int) []*apitypes.ManagedTX); ok { - r0 = rf(ctx, signer, after, limit) + if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt, int, persistence.SortDirection) []*apitypes.ManagedTX); ok { + r0 = rf(ctx, signer, after, limit, dir) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*apitypes.ManagedTX) @@ -299,8 +301,8 @@ func (_m *Persistence) ListTransactionsByNonce(ctx context.Context, signer strin } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.FFBigInt, int) error); ok { - r1 = rf(ctx, signer, after, limit) + if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.FFBigInt, int, persistence.SortDirection) error); ok { + r1 = rf(ctx, signer, after, limit, dir) } else { r1 = ret.Error(1) } @@ -308,13 +310,13 @@ func (_m *Persistence) ListTransactionsByNonce(ctx context.Context, signer strin return r0, r1 } -// ListTransactionsPending provides a mock function with given fields: ctx, after, limit -func (_m *Persistence) ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int) ([]*apitypes.ManagedTX, error) { - ret := _m.Called(ctx, after, limit) +// ListTransactionsPending provides a mock function with given fields: ctx, after, limit, dir +func (_m *Persistence) ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int, dir persistence.SortDirection) ([]*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, after, limit, dir) var r0 []*apitypes.ManagedTX - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int) []*apitypes.ManagedTX); ok { - r0 = rf(ctx, after, limit) + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) []*apitypes.ManagedTX); ok { + r0 = rf(ctx, after, limit, dir) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*apitypes.ManagedTX) @@ -322,8 +324,8 @@ func (_m *Persistence) ListTransactionsPending(ctx context.Context, after *fftyp } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int) error); ok { - r1 = rf(ctx, after, limit) + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) error); ok { + r1 = rf(ctx, after, limit, dir) } else { r1 = ret.Error(1) } diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go index 30685266..90a43acd 100644 --- a/pkg/fftm/api_test.go +++ b/pkg/fftm/api_test.go @@ -17,9 +17,7 @@ package fftm import ( - "encoding/json" "fmt" - "net/http" "strings" "testing" @@ -67,9 +65,7 @@ func TestSendTransactionE2E(t *testing.T) { txSent := make(chan struct{}) - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + url, m, cancel := newTestManager(t) defer cancel() mFFC := m.connector.(*ffcapimocks.API) @@ -128,9 +124,7 @@ func TestSendTransactionE2E(t *testing.T) { func TestSendInvalidRequestBadTXType(t *testing.T) { - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + url, m, cancel := newTestManager(t) defer cancel() m.Start() @@ -154,9 +148,7 @@ func TestSendInvalidRequestBadTXType(t *testing.T) { func TestSwaggerEndpoints(t *testing.T) { - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + url, m, cancel := newTestManager(t) defer cancel() m.Start() @@ -175,9 +167,7 @@ func TestSwaggerEndpoints(t *testing.T) { func TestSendInvalidRequestWrongType(t *testing.T) { - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + url, m, cancel := newTestManager(t) defer cancel() m.Start() @@ -199,9 +189,7 @@ func TestSendInvalidRequestWrongType(t *testing.T) { func TestSendTransactionPrepareFail(t *testing.T) { - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + url, m, cancel := newTestManager(t) defer cancel() mFFC := m.connector.(*ffcapimocks.API) @@ -225,50 +213,9 @@ func TestSendTransactionPrepareFail(t *testing.T) { } -func TestSendTransactionUpdateFireFlyFail(t *testing.T) { - - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPatch { - errRes := fftypes.RESTError{Error: "pop"} - b, err := json.Marshal(&errRes) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - w.Write(b) - } else { - w.WriteHeader(200) - } - }, - ) - defer cancel() - - mFFC := m.connector.(*ffcapimocks.API) - - mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { - return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == nonceReq.Signer - })).Return(&ffcapi.NextNonceForSignerResponse{ - Nonce: fftypes.NewFFBigInt(12345), - }, ffcapi.ErrorReason(""), nil) - - mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{}, ffcapi.ErrorReason(""), nil) - - m.Start() - - req := strings.NewReader(sampleSendTX) - res, err := resty.New().R(). - SetBody(req). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 500, res.StatusCode()) - -} - func TestQueryOK(t *testing.T) { - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + url, m, cancel := newTestManager(t) defer cancel() m.Start() @@ -279,7 +226,7 @@ func TestQueryOK(t *testing.T) { Outputs: fftypes.JSONAnyPtr(`"some output data"`), }, ffcapi.ErrorReason(""), nil) - var queryRes apitypes.QueryResponse + var queryRes string res, err := resty.New().R(). SetBody(&apitypes.QueryRequest{ Headers: apitypes.RequestHeaders{ @@ -295,7 +242,7 @@ func TestQueryOK(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) - assert.Equal(t, `"some output data"`, queryRes.Outputs.String()) + assert.Equal(t, `some output data`, queryRes) mca.AssertExpectations(t) @@ -303,9 +250,7 @@ func TestQueryOK(t *testing.T) { func TestQueryBadRequest(t *testing.T) { - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + url, m, cancel := newTestManager(t) defer cancel() m.Start() @@ -330,9 +275,7 @@ func TestQueryBadRequest(t *testing.T) { func TestNotFound(t *testing.T) { - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + url, m, cancel := newTestManager(t) defer cancel() m.Start() diff --git a/pkg/fftm/changelistener.go b/pkg/fftm/changelistener.go deleted file mode 100644 index 6d9a9a45..00000000 --- a/pkg/fftm/changelistener.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 fftm - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-common/pkg/wsclient" - "github.com/hyperledger/firefly/pkg/core" -) - -func (m *manager) startChangeListener(ctx context.Context, w wsclient.WSClient) error { - cmd := core.WSChangeEventCommand{ - Type: core.WSChangeEventCommandTypeStart, - Collections: []string{"operations"}, - Filter: core.ChangeEventFilter{ - Types: []core.ChangeEventType{core.ChangeEventTypeUpdated}, - }, - } - b, _ := json.Marshal(&cmd) - log.L(m.ctx).Infof("Change listener connected. Sent: %s", b) - return w.Send(ctx, b) -} - -func (m *manager) handleEvent(ce *core.ChangeEvent) { - log.L(m.ctx).Debugf("%s:%s/%s operation change event received", ce.Namespace, ce.ID, ce.Type) - if ce.Collection == "operations" && ce.Type == core.ChangeEventTypeUpdated { - nsOpID := fmt.Sprintf("%s:%s", ce.Namespace, ce.ID) - m.mux.Lock() - _, knownID := m.pendingOpsByID[nsOpID] - m.mux.Unlock() - if !knownID { - // Currently the only action taken for change events, is to check we are - // tracking the transaction. However, as only transactions we submitted are - // valid and we do a full query on startup - the change listener is a little - // redundant (and disabled by default) - m.queryAndAddPending(nsOpID) - } - } -} - -func (m *manager) changeEventLoop() { - defer close(m.changeEventLoopDone) - for { - select { - case b := <-m.wsClient.Receive(): - var ce *core.ChangeEvent - _ = json.Unmarshal(b, &ce) - m.handleEvent(ce) - case <-m.ctx.Done(): - log.L(m.ctx).Infof("Change event loop exiting") - return - } - } -} - -func (m *manager) startWS() error { - if m.enableChangeListener { - m.changeEventLoopDone = make(chan struct{}) - if err := m.wsClient.Connect(); err != nil { - return err - } - go m.changeEventLoop() - } - return nil -} - -func (m *manager) waitWSStop() { - if m.changeEventLoopDone != nil { - <-m.changeEventLoopDone - } -} diff --git a/pkg/fftm/changelistener_test.go b/pkg/fftm/changelistener_test.go deleted file mode 100644 index 666db636..00000000 --- a/pkg/fftm/changelistener_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 fftm - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "testing" - - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-common/pkg/wsclient" - "github.com/hyperledger/firefly/pkg/core" - "github.com/stretchr/testify/assert" -) - -func TestWSChangeDeliveryLookup(t *testing.T) { - - opID := fftypes.NewUUID() - lookedUp := make(chan struct{}) - toServer, fromServer, wsURL, done := wsclient.NewTestWSServer( - func(req *http.Request) { - switch req.URL.Path { - case `/admin/ws`: - return - case fmt.Sprintf("/spi/v1/operations/ns1:%s", opID): - close(lookedUp) - default: - assert.Fail(t, fmt.Sprintf("Unexpected path: %s", req.URL.Path)) - } - }, - ) - defer done() - - httpURL, err := url.Parse(wsURL) - assert.NoError(t, err) - httpURL.Scheme = "http" - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - httpURL.String(), - ) - defer cancel() - - m.startWS() - - cmdJSON := <-toServer - var startCmd core.WSChangeEventCommand - err = json.Unmarshal([]byte(cmdJSON), &startCmd) - assert.NoError(t, err) - assert.Equal(t, core.WSChangeEventCommandTypeStart, startCmd.Type) - - change := &core.ChangeEvent{ - Collection: "operations", - Type: core.ChangeEventTypeUpdated, - Namespace: "ns1", - ID: opID, - } - changeJSON, err := json.Marshal(&change) - assert.NoError(t, err) - fromServer <- string(changeJSON) - - <-lookedUp - - m.cancelCtx() - m.waitWSStop() - -} - -func TestWSConnectFail(t *testing.T) { - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) - cancel() - - m.enableChangeListener = true - err := m.startWS() - assert.Error(t, err) - -} diff --git a/pkg/fftm/ffcore.go b/pkg/fftm/ffcore.go deleted file mode 100644 index bbe81716..00000000 --- a/pkg/fftm/ffcore.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 fftm - -import ( - "context" - "fmt" - "net/url" - "strconv" - - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-common/pkg/i18n" - "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" - "github.com/hyperledger/firefly/pkg/core" -) - -// opUpdate allows us to avoid JSONObject serialization to a map before we upload our managedTXOutput -type opUpdate struct { - Status core.OpStatus `json:"status"` - Output *apitypes.ManagedTX `json:"output"` - Error string `json:"error"` -} - -func (m *manager) writeManagedTX(ctx context.Context, mtx *apitypes.ManagedTX, status core.OpStatus, errString string) error { - log.L(ctx).Debugf("Updating operation %s status=%s", mtx.ID, status) - var errorInfo fftypes.RESTError - var op core.Operation - res, err := m.ffCoreClient.R(). - SetResult(&op). - SetError(&errorInfo). - SetBody(&opUpdate{ - Output: mtx, - Status: status, - Error: errString, - }). - SetContext(ctx). - Patch(fmt.Sprintf("/spi/v1/operations/%s", mtx.ID)) - if err != nil { - return err - } - if res.IsError() { - return i18n.NewError(m.ctx, tmmsgs.MsgCoreError, res.StatusCode(), errorInfo.Error) - } - return nil -} - -func (m *manager) queryAndAddPending(nsOpID string) { - var errorInfo fftypes.RESTError - var op *core.Operation - res, err := m.ffCoreClient.R(). - SetResult(&op). - SetError(&errorInfo). - Get(fmt.Sprintf("/spi/v1/operations/%s", nsOpID)) - if err == nil { - // Operations are not deleted, so we consider not found the same as any other error - if res.IsError() { - err = i18n.NewError(m.ctx, tmmsgs.MsgCoreError, res.StatusCode(), errorInfo.Error) - } - } - if err != nil { - // We logo the error, then schedule a full poll (rather than retrying here) - log.L(m.ctx).Errorf("Scheduling full poll due to error from core: %s", err) - m.requestFullScan() - return - } - // If the operation has been marked as success (by us or otherwise), or failed, then - // we can remove it. If we resolved it, then we would have cleared it up on the . - switch op.Status { - case core.OpStatusSucceeded, core.OpStatusFailed: - m.markCancelledIfTracked(nsOpID) - case core.OpStatusPending: - m.trackIfManaged(op) - } -} - -func (m *manager) readOperationPage(ns string, lastOp *core.Operation) ([]*core.Operation, error) { - var errorInfo fftypes.RESTError - var ops []*core.Operation - query := url.Values{ - "sort": []string{"created"}, - "type": m.opTypes, - "status": []string{string(core.OpStatusPending)}, - } - if lastOp != nil { - // For all but the 1st page, we use the last operation as the reference point. - // Extremely unlikely to get multiple ops withe same creation date, but not impossible - // so >= check, and removal of the duplicate at the end of the function. - query.Set("created", fmt.Sprintf(">=%d", lastOp.Created.UnixNano())) - query.Set("limit", strconv.FormatInt(m.fullScanPageSize+1, 10)) - } else { - query.Set("limit", strconv.FormatInt(m.fullScanPageSize, 10)) - } - res, err := m.ffCoreClient.R(). - SetQueryParamsFromValues(query). - SetResult(&ops). - SetError(&errorInfo). - Get(fmt.Sprintf("/spi/v1/namespaces/%s/operations", ns)) - if err != nil { - return nil, i18n.WrapError(m.ctx, err, tmmsgs.MsgCoreError, -1, err) - } - if res.IsError() { - return nil, i18n.NewError(m.ctx, tmmsgs.MsgCoreError, res.StatusCode(), errorInfo.Error) - } - if lastOp != nil && len(ops) > 0 && ops[0].ID.Equals(lastOp.ID) { - ops = ops[1:] - } - return ops, nil -} diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index c847cbdd..ed583ec4 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -18,19 +18,14 @@ package fftm import ( "context" - "encoding/json" - "fmt" "sync" "time" - "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/ffresty" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/httpserver" "github.com/hyperledger/firefly-common/pkg/i18n" - "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-common/pkg/wsclient" + "github.com/hyperledger/firefly-common/pkg/retry" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" "github.com/hyperledger/firefly-transaction-manager/internal/events" "github.com/hyperledger/firefly-transaction-manager/internal/persistence" @@ -41,7 +36,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" - "github.com/hyperledger/firefly/pkg/core" ) type Manager interface { @@ -52,37 +46,28 @@ type Manager interface { type manager struct { ctx context.Context cancelCtx func() + retry *retry.Retry connector ffcapi.API confirmations confirmations.Manager policyEngine policyengine.PolicyEngine apiServer httpserver.HTTPServer - ffCoreClient *resty.Client - wsClient wsclient.WSClient wsServer ws.WebSocketServer persistence persistence.Persistence + inflightStale chan bool + inflight []*pendingState - mux sync.Mutex - nextNonces map[string]uint64 - lockedNonces map[string]*lockedNonce - pendingOpsByID map[string]*pendingState - eventStreams map[fftypes.UUID]events.Stream - streamsByName map[string]*fftypes.UUID - changeEventLoopDone chan struct{} - firstFullScanDone chan error - policyLoopDone chan struct{} - fullScanLoopDone chan struct{} - fullScanRequests chan bool - started bool - apiServerDone chan error + mux sync.Mutex + lockedNonces map[string]*lockedNonce + eventStreams map[fftypes.UUID]events.Stream + streamsByName map[string]*fftypes.UUID + policyLoopDone chan struct{} + started bool + apiServerDone chan error - opTypes []string - startupScanMaxRetries int - fullScanPageSize int64 - fullScanMinDelay time.Duration - policyLoopInterval time.Duration - errorHistoryCount int - enableChangeListener bool - namespaces []string + policyLoopInterval time.Duration + nonceStateTimeout time.Duration + errorHistoryCount int + maxInFlight int } func InitConfig() { @@ -98,11 +83,6 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { if err != nil { return nil, err } - wsconfig := wsclient.GenerateConfig(tmconfig.FFCoreConfig) - m.wsClient, err = wsclient.New(m.ctx, wsconfig, nil, m.startChangeListener) - if err != nil { - return nil, err - } m.wsServer = ws.NewWebSocketServer(ctx) m.apiServer, err = httpserver.NewHTTPServer(ctx, "api", m.router(), m.apiServerDone, tmconfig.APIConfig, tmconfig.CorsConfig) if err != nil { @@ -111,33 +91,26 @@ func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { if err = m.initPersistence(ctx); err != nil { return nil, err } - if namespaces := config.GetStringSlice(tmconfig.FFCoreNamespaces); len(namespaces) > 0 { - m.namespaces = namespaces - } else { - return nil, i18n.NewError(ctx, tmmsgs.MsgNamespacesEmpty) - } return m, nil } func newManager(ctx context.Context, connector ffcapi.API) *manager { m := &manager{ - connector: connector, - ffCoreClient: ffresty.New(ctx, tmconfig.FFCoreConfig), - fullScanRequests: make(chan bool, 1), - nextNonces: make(map[string]uint64), - lockedNonces: make(map[string]*lockedNonce), - apiServerDone: make(chan error), - pendingOpsByID: make(map[string]*pendingState), - eventStreams: make(map[fftypes.UUID]events.Stream), - streamsByName: make(map[string]*fftypes.UUID), - - opTypes: config.GetStringSlice(tmconfig.OperationsTypes), - startupScanMaxRetries: config.GetInt(tmconfig.OperationsFullScanStartupMaxRetries), - fullScanPageSize: config.GetInt64(tmconfig.OperationsFullScanPageSize), - fullScanMinDelay: config.GetDuration(tmconfig.OperationsFullScanMinimumDelay), - policyLoopInterval: config.GetDuration(tmconfig.PolicyLoopInterval), - errorHistoryCount: config.GetInt(tmconfig.OperationsErrorHistoryCount), - enableChangeListener: config.GetBool(tmconfig.OperationsChangeListenerEnabled), + connector: connector, + lockedNonces: make(map[string]*lockedNonce), + apiServerDone: make(chan error), + eventStreams: make(map[fftypes.UUID]events.Stream), + streamsByName: make(map[string]*fftypes.UUID), + + policyLoopInterval: config.GetDuration(tmconfig.PolicyLoopInterval), + errorHistoryCount: config.GetInt(tmconfig.TransactionsErrorHistoryCount), + maxInFlight: config.GetInt(tmconfig.TransactionsMaxInFlight), + inflightStale: make(chan bool, 1), + retry: &retry.Retry{ + InitialDelay: config.GetDuration(tmconfig.PolicyLoopRetryInitDelay), + MaximumDelay: config.GetDuration(tmconfig.PolicyLoopRetryMaxDelay), + Factor: config.GetFloat64(tmconfig.PolicyLoopRetryFactor), + }, } m.ctx, m.cancelCtx = context.WithCancel(ctx) return m @@ -146,19 +119,10 @@ func newManager(ctx context.Context, connector ffcapi.API) *manager { type pendingState struct { mtx *apitypes.ManagedTX confirmed bool - removed bool + remove bool trackingTransactionHash string } -func (m *manager) requestFullScan() { - select { - case m.fullScanRequests <- true: - log.L(m.ctx).Debugf("Full scan of pending ops requested") - default: - log.L(m.ctx).Debugf("Full scan of pending ops already queued") - } -} - func (m *manager) initPersistence(ctx context.Context) (err error) { pType := config.GetString(tmconfig.PersistenceType) switch pType { @@ -172,161 +136,17 @@ func (m *manager) initPersistence(ctx context.Context) (err error) { } } -func (m *manager) waitScanDelay(lastFullScan *fftypes.FFTime) { - scanDelay := m.fullScanMinDelay - time.Since(*lastFullScan.Time()) - log.L(m.ctx).Debugf("Delaying %dms before next full scan", scanDelay.Milliseconds()) - timer := time.NewTimer(scanDelay) - select { - case <-timer.C: - case <-m.ctx.Done(): - log.L(m.ctx).Infof("Full scan loop exiting waiting for retry") - return - } -} - -func (m *manager) fullScanLoop() { - defer close(m.fullScanLoopDone) - firstFullScanDone := m.firstFullScanDone - var lastFullScan *fftypes.FFTime - errorCount := 0 - for { - select { - case <-m.fullScanRequests: - if lastFullScan != nil { - m.waitScanDelay(lastFullScan) - } - lastFullScan = fftypes.Now() - err := m.fullScan() - if err != nil { - errorCount++ - if firstFullScanDone != nil && errorCount > m.startupScanMaxRetries { - firstFullScanDone <- err - return - } - log.L(m.ctx).Errorf("Full scan failed (will be retried) count=%d: %s", errorCount, err) - m.requestFullScan() - continue - } - errorCount = 0 - // On startup we need to know the first scan has completed to populate the nonces, - // before we complete startup - if firstFullScanDone != nil { - firstFullScanDone <- nil - firstFullScanDone = nil - } - case <-m.ctx.Done(): - log.L(m.ctx).Infof("Full scan loop exiting") - return - } - } -} - -func (m *manager) fullScan() error { - log.L(m.ctx).Debugf("Reading all operations after connect") - var page int64 - var read, added int - var lastOp *core.Operation - for _, ns := range m.namespaces { - page = 0 - for { - ops, err := m.readOperationPage(ns, lastOp) - if err != nil { - return err - } - if len(ops) == 0 { - log.L(m.ctx).Debugf("Finished reading all operations for namespace '%s' - %d read, %d added", ns, read, added) - break - } - lastOp = ops[len(ops)-1] - read += len(ops) - for _, op := range ops { - added++ - m.trackIfManaged(op) - } - page++ - } - } - return nil -} - -func (m *manager) trackIfManaged(op *core.Operation) { - outputJSON := []byte(op.Output.String()) - var mtx apitypes.ManagedTX - err := json.Unmarshal(outputJSON, &mtx) - if err != nil { - log.L(m.ctx).Warnf("Failed to parse output from operation %s", err) - return - } - if fmt.Sprintf("%s:%s", op.Namespace, op.ID) != mtx.ID { - log.L(m.ctx).Warnf("Operation %s contains an invalid ID %s in the output", op.ID, mtx.ID) - return - } - if mtx.Request == nil { - log.L(m.ctx).Warnf("Operation %s contains a nil request in the output", op.ID) - return - } - m.trackManaged(&mtx) -} - -func (m *manager) trackManaged(mtx *apitypes.ManagedTX) { - m.mux.Lock() - defer m.mux.Unlock() - _, existing := m.pendingOpsByID[mtx.ID] - if !existing { - nextNonce, ok := m.nextNonces[mtx.Request.From] - nonce := mtx.Nonce.Uint64() - if !ok || nextNonce <= nonce { - log.L(m.ctx).Debugf("Nonce %d in-flight. Next nonce: %d", nonce, nonce+1) - m.nextNonces[mtx.Request.From] = nonce + 1 - } - m.pendingOpsByID[mtx.ID] = &pendingState{ - mtx: mtx, - } - } -} - -func (m *manager) markCancelledIfTracked(nsOpID string) { - m.mux.Lock() - pending, existing := m.pendingOpsByID[nsOpID] - if existing { - pending.removed = true - } - m.mux.Unlock() - -} - func (m *manager) Start() error { - m.fullScanRequests <- true - m.firstFullScanDone = make(chan error) - m.fullScanLoopDone = make(chan struct{}) if err := m.restoreStreams(); err != nil { return err } - go m.fullScanLoop() go m.runAPIServer() - return m.waitForFirstScanAndStart() -} - -func (m *manager) waitForFirstScanAndStart() error { - log.L(m.ctx).Infof("Waiting for first full scan of operations to build state") - select { - case err := <-m.firstFullScanDone: - if err != nil { - return err - } - case <-m.ctx.Done(): - log.L(m.ctx).Infof("Cancelled before startup completed") - return nil - } - log.L(m.ctx).Infof("Scan complete. Completing startup") m.policyLoopDone = make(chan struct{}) - go m.receiptPollingLoop() + m.markInflightStale() + go m.policyLoop() go m.confirmations.Start() - err := m.startWS() - if err == nil { - m.started = true - } - return err + m.started = true + return nil } func (m *manager) Close() { @@ -334,9 +154,7 @@ func (m *manager) Close() { if m.started { m.started = false <-m.apiServerDone - <-m.fullScanLoopDone <-m.policyLoopDone - m.waitWSStop() streams := []events.Stream{} m.mux.Lock() diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index d9417274..1e0d0931 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -22,15 +22,11 @@ import ( "fmt" "io/ioutil" "net" - "net/http" - "net/http/httptest" "os" "strings" "testing" - "time" "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/ffresty" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/httpserver" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" @@ -49,18 +45,14 @@ const testManagerName = "unittest" func strPtr(s string) *string { return &s } -func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...string) (string, *manager, func()) { +func newTestManager(t *testing.T) (string, *manager, func()) { InitConfig() policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) dir, err := ioutil.TempDir("", "ldb_*") assert.NoError(t, err) - config.Set(tmconfig.FFCoreNamespaces, []string{"ns1"}) config.Set(tmconfig.PersistenceLevelDBPath, dir) tmconfig.PolicyEngineBaseConfig.SubSection("simple").SubSection(simple.GasOracleConfig).Set(simple.GasOracleMode, simple.GasOracleModeDisabled) - ffCoreServer := httptest.NewServer(ffCoreHandler) - tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", ffCoreServer.Listener.Addr())) - ln, err := net.Listen("tcp", "127.0.0.1:0") assert.NoError(t, err) managerPort := strings.Split(ln.Addr().String(), ":")[1] @@ -71,11 +63,6 @@ func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...strin config.Set(tmconfig.PolicyLoopInterval, "1ms") tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") - if len(wsURL) > 0 { - config.Set(tmconfig.OperationsChangeListenerEnabled, true) - tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, wsURL[0]) - } - mm, err := NewManager(context.Background(), &ffcapimocks.API{}) assert.NoError(t, err) m := mm.(*manager) @@ -86,7 +73,6 @@ func newTestManager(t *testing.T, ffCoreHandler http.HandlerFunc, wsURL ...strin return fmt.Sprintf("http://127.0.0.1:%s", managerPort), m, func() { - ffCoreServer.Close() m.Close() os.RemoveAll(dir) } @@ -160,19 +146,6 @@ func TestNewManagerBadPersistenceConfig(t *testing.T) { } -func TestNewManagerFireFlyURLConfig(t *testing.T) { - - tmconfig.Reset() - tmconfig.FFCoreConfig.Set(ffresty.HTTPConfigURL, ":::!badurl") - - policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) - tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") - - _, err := NewManager(context.Background(), nil) - assert.Regexp(t, "FF00149", err) - -} - func TestNewManagerBadPolicyEngine(t *testing.T) { tmconfig.Reset() @@ -183,311 +156,10 @@ func TestNewManagerBadPolicyEngine(t *testing.T) { } -func TestChangeEventsNewBadOutput(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - b, err := json.Marshal(&core.Operation{ - ID: ce.ID, - Status: core.OpStatusPending, - Output: fftypes.JSONObject{ - "id": "!not a UUID", - }, - }) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - -} - -func TestChangeEventsWrongID(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - op := newTestOperation(t, &apitypes.ManagedTX{ - ID: "ns1:" + ce.ID.String(), - Request: &apitypes.TransactionRequest{}, - }, core.OpStatusPending) - op.ID = fftypes.NewUUID() - b, err := json.Marshal(&op) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - -} - -func TestChangeEventsNilRequest(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - op := newTestOperation(t, &apitypes.ManagedTX{ - ID: "ns1:" + ce.ID.String(), - }, core.OpStatusPending) - b, err := json.Marshal(&op) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - -} - -func TestChangeEventsQueryFail(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - w.WriteHeader(404) - }, - ) - defer cancel() - - m.fullScanRequests = make(chan bool, 1) - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - - // Full scan should have been requested after this failure - <-m.fullScanRequests - -} - -func TestChangeEventsMarkForCleanup(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - op := newTestOperation(t, &apitypes.ManagedTX{ - ID: "ns1:" + ce.ID.String(), - Request: &apitypes.TransactionRequest{}, - }, core.OpStatusFailed) - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - b, err := json.Marshal(&op) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.trackIfManaged(op) - m.queryAndAddPending(fmt.Sprintf("%s:%s", ce.Namespace, ce.ID)) - assert.True(t, m.pendingOpsByID[fmt.Sprintf("%s:%s", ce.Namespace, ce.ID)].removed) - -} - -func TestStartupScanMultiPageOK(t *testing.T) { - - op1 := newTestOperation(t, &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - Request: &apitypes.TransactionRequest{}, - }, core.OpStatusPending) - t1 := fftypes.FFTime(time.Now().Add(-10 * time.Minute)) - op1.Created = &t1 - op2 := newTestOperation(t, &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - Request: &apitypes.TransactionRequest{}, - }, core.OpStatusPending) - t2 := fftypes.FFTime(time.Now().Add(-5 * time.Minute)) - op2.Created = &t2 - op3 := newTestOperation(t, &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - Request: &apitypes.TransactionRequest{}, - }, core.OpStatusPending) - t3 := fftypes.FFTime(time.Now().Add(-1 * time.Minute)) - op3.Created = &t3 - - call := 0 - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, "/spi/v1/operations", r.URL.Path) - status := 200 - var res interface{} - switch call { - case 0: - res = &fftypes.RESTError{Error: "not ready yet"} - status = 500 - case 1: - res = []*core.Operation{op1, op2} - assert.Equal(t, "", r.URL.Query().Get("created")) - case 2: - res = []*core.Operation{op2 /* simulate overlap */, op3} - assert.Equal(t, fmt.Sprintf(">=%d", op2.Created.Time().UnixNano()), r.URL.Query().Get("created")) - case 3: - res = []*core.Operation{} - assert.Equal(t, fmt.Sprintf(">=%d", op3.Created.Time().UnixNano()), r.URL.Query().Get("created")) - default: - assert.Fail(t, "should have stopped after empty page") - } - call++ - b, err := json.Marshal(res) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - w.Write(b) - }, - ) - m.fullScanMinDelay = 1 * time.Microsecond - - m.fullScanRequests <- true - m.firstFullScanDone = make(chan error) - m.fullScanLoopDone = make(chan struct{}) - go m.fullScanLoop() - - <-m.firstFullScanDone - assert.Len(t, m.pendingOpsByID, 3) - - cancel() - <-m.fullScanLoopDone - -} - -func TestStartupScanFail(t *testing.T) { - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) - cancel() // close servers - m.ctx = context.Background() - m.startupScanMaxRetries = 2 - m.fullScanMinDelay = 1 * time.Microsecond - - err := m.Start() - assert.Regexp(t, "FF21017", err) - -} - -func TestRequestFullScanNonBlocking(t *testing.T) { - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - m.requestFullScan() - m.requestFullScan() - m.requestFullScan() - -} - -func TestRequestFullScanCancelledBeforeStart(t *testing.T) { - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - m.cancelCtx() - m.waitForFirstScanAndStart() - -} - -func TestStartupCancelledDuringRetry(t *testing.T) { - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) - cancel() // close servers - m.startupScanMaxRetries = 2 - m.fullScanMinDelay = 1 * time.Second - - m.waitScanDelay(fftypes.Now()) - -} - -func TestStartChangeEventListener(t *testing.T) { - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - m.wsClient.Close() - err := m.startChangeListener(m.ctx, m.wsClient) - assert.Regexp(t, "FF00147", err) - -} - func TestAddErrorMessageMax(t *testing.T) { var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + _, m, cancel := newTestManager(t) defer cancel() m.errorHistoryCount = 2 @@ -500,19 +172,3 @@ func TestAddErrorMessageMax(t *testing.T) { assert.Equal(t, "crackle", mtx.ErrorHistory[1].Error) } - -func TestUnparsableOperation(t *testing.T) { - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - m.trackIfManaged(&core.Operation{ - Output: fftypes.JSONObject{ - "test": map[bool]bool{false: true}, - }, - }) - -} diff --git a/pkg/fftm/nonces.go b/pkg/fftm/nonces.go index e41db85b..2f379117 100644 --- a/pkg/fftm/nonces.go +++ b/pkg/fftm/nonces.go @@ -18,8 +18,10 @@ package fftm import ( "context" + "time" "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -37,10 +39,8 @@ type lockedNonce struct { func (ln *lockedNonce) complete(ctx context.Context) { if ln.spent != nil { log.L(ctx).Debugf("Next nonce %d for signer %s spent", ln.nonce, ln.signer) - ln.m.trackManaged(ln.spent) } else { log.L(ctx).Debugf("Returning next nonce %d for signer %s unspent", ln.nonce, ln.signer) - // Do not } ln.m.mux.Lock() delete(ln.m.lockedNonces, ln.signer) @@ -63,16 +63,6 @@ func (m *manager) assignAndLockNonce(ctx context.Context, nsOpID, signer string) unlocked: make(chan struct{}), } m.lockedNonces[signer] = locked - // We might know the highest nonce straight away - nextNonce, nonceCached := m.nextNonces[signer] - if nonceCached { - locked.nonce = nextNonce - log.L(ctx).Debugf("Locking next nonce %d from cache for signer %s", locked.nonce, signer) - // We can return the nonce to use without any query - m.mux.Unlock() - return locked, nil - } - // Otherwise, defer a lookup to outside of the mutex doLookup = true } m.mux.Unlock() @@ -84,20 +74,54 @@ func (m *manager) assignAndLockNonce(ctx context.Context, nsOpID, signer string) } else if doLookup { // We have to ensure we either successfully return a nonce, // or otherwise we unlock when we send the error - nextNonceRes, _, err := m.connector.NextNonceForSigner(ctx, &ffcapi.NextNonceForSignerRequest{ - Signer: signer, - }) + nextNonce, err := m.calcNextNonce(ctx, signer) if err != nil { locked.complete(ctx) return nil, err } - nextNonce := nextNonceRes.Nonce.Uint64() - m.mux.Lock() - m.nextNonces[signer] = nextNonce locked.nonce = nextNonce - m.mux.Unlock() return locked, nil } } } + +func (m *manager) calcNextNonce(ctx context.Context, signer string) (uint64, error) { + + // First we check our DB to find the last nonce we used for this address. + // Note we are within the nonce-lock in assignAndLockNonce for this signer, so we can be sure we're the + // only routine attempting this right now. + var lastTxn *apitypes.ManagedTX + txns, err := m.persistence.ListTransactionsByNonce(ctx, signer, nil, 1, persistence.SortDirectionDescending) + if err != nil { + return 0, err + } + if len(txns) > 0 { + lastTxn = txns[0] + if time.Since(*lastTxn.Created.Time()) < m.nonceStateTimeout { + nextNonce := lastTxn.Nonce.Uint64() + 1 + log.L(ctx).Debugf("Allocating next nonce '%s' / '%d' after TX '%s' (status=%s)", signer, nextNonce, lastTxn.ID, lastTxn.Status) + return nextNonce, nil + } + } + + // If we don't have a fresh answer in our state store, then ask the node. + nextNonceRes, _, err := m.connector.NextNonceForSigner(ctx, &ffcapi.NextNonceForSignerRequest{ + Signer: signer, + }) + if err != nil { + return 0, err + } + nextNonce := nextNonceRes.Nonce.Uint64() + + // If we had a stale answer in our state store, make sure this isn't re-used. + // This is important in case we have transactions that have expired from the TX pool of nodes, but we still have them + // in our state store. So basically whichever is further forwards of our state store and the node answer wins. + if lastTxn != nil && nextNonce <= lastTxn.Nonce.Uint64() { + log.L(ctx).Debugf("Node TX pool next nonce '%s' / '%d' is not ahead of '%d' in TX '%s' (status=%s)", signer, nextNonce, lastTxn.Nonce.Uint64(), lastTxn.ID, lastTxn.Status) + nextNonce = lastTxn.Nonce.Uint64() + 1 + } + + return nextNonce, nil + +} diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go index 22620add..6d792e6a 100644 --- a/pkg/fftm/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -19,7 +19,6 @@ package fftm import ( "context" "fmt" - "net/http" "testing" "time" @@ -31,13 +30,29 @@ import ( "github.com/stretchr/testify/mock" ) -func TestNonceCached(t *testing.T) { +func TestNonceStaleStateContention(t *testing.T) { - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + _, m, cancel := newTestManager(t) defer cancel() + // Write a stale record to persistence + oldTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) + err := m.persistence.WriteTransaction(m.ctx, &apitypes.ManagedTX{ + ID: "stale1", + Status: apitypes.TxStatusSucceeded, + SequenceID: apitypes.UUIDVersion1(), + Nonce: fftypes.NewFFBigInt(1000), // old nonce + Created: &oldTime, // old record + Request: &apitypes.TransactionRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + }, + }, + }, true) + assert.NoError(t, err) + mFFC := m.connector.(*ffcapimocks.API) mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { @@ -60,8 +75,11 @@ func TestNonceCached(t *testing.T) { time.Sleep(1 * time.Millisecond) ln.spent = &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), + ID: "ns1:" + fftypes.NewUUID().String(), + Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), + Status: apitypes.TxStatusPending, + SequenceID: apitypes.UUIDVersion1(), + Created: &oldTime, // old record Request: &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -70,6 +88,8 @@ func TestNonceCached(t *testing.T) { }, }, } + err = m.persistence.WriteTransaction(m.ctx, ln.spent, true) + assert.NoError(t, err) ln.complete(context.Background()) }() @@ -93,9 +113,7 @@ func TestNonceCached(t *testing.T) { func TestNonceError(t *testing.T) { - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + _, m, cancel := newTestManager(t) defer cancel() mFFC := m.connector.(*ffcapimocks.API) diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index e0ceef0e..525340c0 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -22,19 +22,21 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly/pkg/core" ) -func (m *manager) receiptPollingLoop() { +func (m *manager) policyLoop() { defer close(m.policyLoopDone) for { timer := time.NewTimer(m.policyLoopInterval) select { + case <-m.inflightStale: + m.policyLoopCycle(true) case <-timer.C: - m.policyLoopCycle() + m.policyLoopCycle(false) case <-m.ctx.Done(): log.L(m.ctx).Infof("Receipt poller exiting") return @@ -42,18 +44,60 @@ func (m *manager) receiptPollingLoop() { } } -func (m *manager) policyLoopCycle() { +func (m *manager) markInflightStale() { + select { + case m.inflightStale <- true: + default: + } +} + +func (m *manager) updateInflightSet() bool { - // Grab the lock to build a list of things to check - m.mux.Lock() - allPending := make([]*pendingState, 0, len(m.pendingOpsByID)) - for _, pending := range m.pendingOpsByID { - allPending = append(allPending, pending) + oldInflight := m.inflight + m.inflight = make([]*pendingState, 0, len(oldInflight)) + + // Run through removing those that are removed + for _, p := range oldInflight { + if !p.remove { + m.inflight = append(m.inflight, p) + } } - m.mux.Unlock() - // Go through trying to query all of them - for _, pending := range allPending { + // If we are not at maximum, then query if there are more candidates now + spaces := m.maxInFlight - len(m.inflight) + if spaces > 0 { + var after *fftypes.UUID + if len(m.inflight) > 0 { + after = m.inflight[len(m.inflight)-1].mtx.SequenceID + } + var additional []*apitypes.ManagedTX + // We retry the get from persistence indefinitely (until the context cancels) + err := m.retry.Do(m.ctx, "get pending transactions", func(attempt int) (retry bool, err error) { + additional, err = m.persistence.ListTransactionsPending(m.ctx, after, spaces, persistence.SortDirectionAscending) + return true, err + }) + if err != nil { + log.L(m.ctx).Infof("Policy loop context cancelled while retrying") + return false + } + for _, mtx := range additional { + m.inflight = append(m.inflight, &pendingState{mtx: mtx}) + } + } + return true + +} + +func (m *manager) policyLoopCycle(inflightStale bool) { + + if inflightStale { + if !m.updateInflightSet() { + return + } + } + + // Go through executing the policy engine against them + for _, pending := range m.inflight { err := m.execPolicy(pending) if err != nil { log.L(m.ctx).Errorf("Failed policy cycle transaction=%s operation=%s: %s", pending.mtx.TransactionHash, pending.mtx.ID, err) @@ -79,26 +123,21 @@ func (m *manager) addError(mtx *apitypes.ManagedTX, reason ffcapi.ErrorReason, e } } -// checkReceiptCycle runs against each pending item, on each cycle, and is the one place responsible -// for state updates - to avoid those happening in parallel. func (m *manager) execPolicy(pending *pendingState) (err error) { - updated := true + var updated bool completed := false - newStatus := core.OpStatusPending mtx := pending.mtx switch { case pending.confirmed: updated = true completed = true if mtx.Receipt.Success { - newStatus = core.OpStatusSucceeded + mtx.Status = apitypes.TxStatusSucceeded } else { - newStatus = core.OpStatusFailed + mtx.Status = apitypes.TxStatusFailed } - case pending.removed: - // Remove from our state - m.removeIfTracked(mtx.ID) + default: // Pass the state to the pluggable policy engine to potentially perform more actions against it, // such as submitting for the first time, or raising the gas etc. @@ -114,19 +153,14 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { } if updated || err != nil { - errorString := "" - if err != nil { - // In the case of errors, we keep the record updated with the latest error - but leave it in Pending - errorString = err.Error() - } - err := m.writeManagedTX(m.ctx, mtx, newStatus, errorString) + err := m.persistence.WriteTransaction(m.ctx, mtx, false) if err != nil { - log.L(m.ctx).Errorf("Failed to update operation %s (status=%s): %s", mtx.ID, newStatus, err) + log.L(m.ctx).Errorf("Failed to update operation %s (status=%s): %s", mtx.ID, mtx.Status, err) return err } if completed { - // We can remove it now - m.removeIfTracked(mtx.ID) + pending.remove = true + m.markInflightStale() } } @@ -176,26 +210,3 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { pending.trackingTransactionHash = pending.mtx.TransactionHash } } - -func (m *manager) clearConfirmationTracking(mtx *apitypes.ManagedTX) { - // The only error condition on confirmations manager is if we are exiting, which it logs - _ = m.confirmations.Notify(&confirmations.Notification{ - NotificationType: confirmations.RemovedTransaction, - Transaction: &confirmations.TransactionInfo{ - TransactionHash: mtx.TransactionHash, - }, - }) -} - -func (m *manager) removeIfTracked(nsOpID string) { - m.mux.Lock() - pending, existing := m.pendingOpsByID[nsOpID] - if existing { - delete(m.pendingOpsByID, nsOpID) - } - m.mux.Unlock() - // Outside the lock tap the confirmation manager on the shoulder so it can clean up too - if existing { - m.clearConfirmationTracking(pending.mtx) - } -} diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index 51b056d7..09bd688f 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -17,95 +17,62 @@ package fftm import ( - "encoding/json" "fmt" - "net/http" "testing" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" - "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) -const ( - sampleTXHash = "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - sampleTXHash2 = "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df" -) - -func TestPolicyLoopE2EOk(t *testing.T) { +func sendSampleTX(t *testing.T, m *manager, signer string, nonce int64) *apitypes.ManagedTX { - mtx := &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - FirstSubmit: fftypes.Now(), - TransactionHash: sampleTXHash, - Request: &apitypes.TransactionRequest{}, - } - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - var op core.Operation - err := json.NewDecoder(r.Body).Decode(&op) - assert.NoError(t, err) - assert.Equal(t, core.OpStatusSucceeded, op.Status) - w.WriteHeader(200) + txInput := ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: signer, }, - ) - defer cancel() - - mc := m.confirmations.(*confirmationsmocks.Manager) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - return n.NotificationType == confirmations.NewTransaction - })).Run(func(args mock.Arguments) { - n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.TransactionReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - Success: true, - }) - n.Transaction.Confirmed([]confirmations.BlockInfo{}) - }).Return(nil) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - return n.NotificationType == confirmations.RemovedTransaction - })).Return(nil) - - m.trackManaged(mtx) - m.policyLoopCycle() + } - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) + mfc := m.connector.(*ffcapimocks.API) + mfc.On("NextNonceForSigner", m.ctx, &ffcapi.NextNonceForSignerRequest{ + Signer: signer, + }).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(nonce), + }, ffcapi.ErrorReason(""), nil).Once() + mfc.On("TransactionPrepare", m.ctx, &ffcapi.TransactionPrepareRequest{ + TransactionInput: txInput, + }).Return(&ffcapi.TransactionPrepareResponse{ + Gas: fftypes.NewFFBigInt(100000), + TransactionData: "0xabce1234", + }, ffcapi.ErrorReason(""), nil).Once() + + mtx, err := m.sendManagedTransaction(m.ctx, &apitypes.TransactionRequest{ + TransactionInput: txInput, + }) assert.NoError(t, err) - assert.Empty(t, m.pendingOpsByID) - - mc.AssertExpectations(t) + return mtx } -func TestPolicyLoopE2EOkReverted(t *testing.T) { - - mtx := &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - FirstSubmit: fftypes.Now(), - TransactionHash: sampleTXHash, - Request: &apitypes.TransactionRequest{}, - } +func TestPolicyLoopE2EOk(t *testing.T) { - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - var op core.Operation - err := json.NewDecoder(r.Body).Decode(&op) - assert.NoError(t, err) - assert.Equal(t, core.OpStatusFailed, op.Status) - w.WriteHeader(200) - }, - ) + _, m, cancel := newTestManager(t) defer cancel() + mtx := sendSampleTX(t, m, "0xaaaaa", 12345) + txHash := "0x" + fftypes.NewRandB32().String() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("TransactionSend", m.ctx, mock.MatchedBy(func(r *ffcapi.TransactionSendRequest) bool { + return r.Nonce.Equals(fftypes.NewFFBigInt(12345)) + })).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: txHash, + }, ffcapi.ErrorReason(""), nil) + mc := m.confirmations.(*confirmationsmocks.Manager) mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { return n.NotificationType == confirmations.NewTransaction @@ -115,76 +82,47 @@ func TestPolicyLoopE2EOkReverted(t *testing.T) { BlockNumber: fftypes.NewFFBigInt(12345), TransactionIndex: fftypes.NewFFBigInt(10), BlockHash: fftypes.NewRandB32().String(), - Success: false, + Success: true, }) n.Transaction.Confirmed([]confirmations.BlockInfo{}) }).Return(nil) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - return n.NotificationType == confirmations.RemovedTransaction - })).Return(nil) - m.trackManaged(mtx) - m.policyLoopCycle() + // Run the policy once to do the send + <-m.inflightStale // from sending the TX + m.policyLoopCycle(true) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) + // A second time will mark it complete for flush + m.policyLoopCycle(false) + + <-m.inflightStale // policy loop should have marked us stale, to clean up the TX + m.policyLoopCycle(true) + assert.Empty(t, m.inflight) + + // Check the update is persisted + rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.ID) assert.NoError(t, err) - assert.Empty(t, m.pendingOpsByID) + assert.Equal(t, apitypes.TxStatusSucceeded, rtx.Status) mc.AssertExpectations(t) + mfc.AssertExpectations(t) } -func TestPolicyLoopUpdateFFCoreWithError(t *testing.T) { +func TestPolicyLoopE2EReverted(t *testing.T) { - mtx := &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - FirstSubmit: fftypes.Now(), - TransactionHash: sampleTXHash, - Request: &apitypes.TransactionRequest{}, - } - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - var op core.Operation - err := json.NewDecoder(r.Body).Decode(&op) - assert.NoError(t, err) - assert.Equal(t, core.OpStatusPending, op.Status) - w.WriteHeader(200) - }, - ) + _, m, cancel := newTestManager(t) defer cancel() - m.policyEngine = &policyenginemocks.PolicyEngine{} - pc := m.policyEngine.(*policyenginemocks.PolicyEngine) - pc.On("Execute", mock.Anything, mock.Anything, mtx).Return(false, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + mtx := sendSampleTX(t, m, "0xaaaaa", 12345) + txHash := "0x" + fftypes.NewRandB32().String() - m.trackManaged(mtx) - m.policyLoopCycle() - - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) - assert.NoError(t, err) - assert.NotEmpty(t, m.pendingOpsByID) -} - -func TestPolicyLoopUpdateOpFail(t *testing.T) { - - mtx := &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - FirstSubmit: fftypes.Now(), - TransactionHash: sampleTXHash, - Request: &apitypes.TransactionRequest{}, - } - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, _ *http.Request) { - errRes := fftypes.RESTError{Error: "pop"} - b, err := json.Marshal(&errRes) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - w.Write(b) - }, - ) - defer cancel() + mfc := m.connector.(*ffcapimocks.API) + mfc.On("TransactionSend", m.ctx, mock.MatchedBy(func(r *ffcapi.TransactionSendRequest) bool { + return r.Nonce.Equals(fftypes.NewFFBigInt(12345)) + })).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: txHash, + }, ffcapi.ErrorReason(""), nil) mc := m.confirmations.(*confirmationsmocks.Manager) mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { @@ -195,66 +133,71 @@ func TestPolicyLoopUpdateOpFail(t *testing.T) { BlockNumber: fftypes.NewFFBigInt(12345), TransactionIndex: fftypes.NewFFBigInt(10), BlockHash: fftypes.NewRandB32().String(), - Success: true, + Success: false, }) n.Transaction.Confirmed([]confirmations.BlockInfo{}) }).Return(nil) - m.trackManaged(mtx) - m.policyLoopCycle() + // Run the policy once to do the send + <-m.inflightStale // from sending the TX + m.policyLoopCycle(true) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) + + // A second time will mark it complete for flush + m.policyLoopCycle(false) + + <-m.inflightStale // policy loop should have marked us stale, to clean up the TX + m.policyLoopCycle(true) + assert.Empty(t, m.inflight) - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) - assert.Regexp(t, "FF21017.*pop", err) - assert.NotEmpty(t, m.pendingOpsByID) + // Check the update is persisted + rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.ID) + assert.NoError(t, err) + assert.Equal(t, apitypes.TxStatusFailed, rtx.Status) mc.AssertExpectations(t) + mfc.AssertExpectations(t) } func TestPolicyLoopResubmitNewTXID(t *testing.T) { - mtx := &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - Request: &apitypes.TransactionRequest{}, - } - - opUpdateCount := 0 - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - var op core.Operation - err := json.NewDecoder(r.Body).Decode(&op) - assert.NoError(t, err) - opUpdateCount++ - if opUpdateCount == 1 { - assert.Equal(t, core.OpStatusPending, op.Status) - } else { - assert.Equal(t, core.OpStatusSucceeded, op.Status) - } - w.WriteHeader(200) - }, - ) + _, m, cancel := newTestManager(t) defer cancel() - mFFC := m.connector.(*ffcapimocks.API) + mtx := sendSampleTX(t, m, "0xaaaaa", 12345) + txHash1 := "0x" + fftypes.NewRandB32().String() + txHash2 := "0x" + fftypes.NewRandB32().String() + + mfc := m.connector.(*ffcapimocks.API) - mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{ + mfc.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{ Gas: fftypes.NewFFBigInt(12345), TransactionData: "0x12345", }, ffcapi.ErrorReason(""), nil) - mFFC.On("TransactionSend", mock.Anything, mock.Anything).Return(&ffcapi.TransactionSendResponse{ - TransactionHash: sampleTXHash2, - }, ffcapi.ErrorReason(""), nil) + mfc.On("TransactionSend", mock.Anything, mock.Anything).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: txHash1, + }, ffcapi.ErrorReason(""), nil).Once() + mfc.On("TransactionSend", mock.Anything, mock.Anything).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: txHash2, + }, ffcapi.ErrorReason(""), nil).Once() mc := m.confirmations.(*confirmationsmocks.Manager) mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - // First we get notified to remove the old TX hash + // First we get notified to add the old TX hash + return n.NotificationType == confirmations.NewTransaction && + n.Transaction.TransactionHash == txHash1 + })).Return(nil) + mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { + // Then we get notified to remove the old TX hash return n.NotificationType == confirmations.RemovedTransaction && - n.Transaction.TransactionHash == sampleTXHash + n.Transaction.TransactionHash == txHash1 })).Return(nil) mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { // Then we get the new TX hash, which we confirm return n.NotificationType == confirmations.NewTransaction && - n.Transaction.TransactionHash == sampleTXHash2 + n.Transaction.TransactionHash == txHash2 })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) n.Transaction.Receipt(&ffcapi.TransactionReceiptResponse{ @@ -265,62 +208,42 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { }) n.Transaction.Confirmed([]confirmations.BlockInfo{}) }).Return(nil) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - // Then we're done - return n.NotificationType == confirmations.RemovedTransaction && - n.Transaction.TransactionHash == sampleTXHash2 - })).Return(nil) - - m.trackManaged(mtx) - pending := m.pendingOpsByID[mtx.ID] - pending.trackingTransactionHash = sampleTXHash - m.policyLoopCycle() + // Run the policy once to do the send with the first hash + <-m.inflightStale // from sending the TX + m.policyLoopCycle(true) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) - assert.NoError(t, err) - assert.Empty(t, m.pendingOpsByID) + // Reset the transaction so the policy manager resubmits it + m.inflight[0].mtx.FirstSubmit = nil + m.policyLoopCycle(false) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) mc.AssertExpectations(t) + mfc.AssertExpectations(t) } -func TestPolicyLoopCycleCleanupRemoved(t *testing.T) { - - mtx := &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - Request: &apitypes.TransactionRequest{}, - } +func TestNotifyConfirmationMgrFail(t *testing.T) { - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) + _, m, cancel := newTestManager(t) defer cancel() - mc := m.confirmations.(*confirmationsmocks.Manager) - mc.On("Notify", mock.Anything).Return(nil).Once() + _ = sendSampleTX(t, m, "0xaaaaa", 12345) + txHash := "0x" + fftypes.NewRandB32().String() - m.trackManaged(mtx) - m.markCancelledIfTracked(mtx.ID) - - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) - assert.NoError(t, err) - assert.Empty(t, m.pendingOpsByID) -} - -func TestNotifyConfirmationMgrFail(t *testing.T) { - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() + mfc := m.connector.(*ffcapimocks.API) + mfc.On("TransactionSend", mock.Anything, mock.Anything).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: txHash, + }, ffcapi.ErrorReason(""), nil).Once() mc := m.confirmations.(*confirmationsmocks.Manager) mc.On("Notify", mock.Anything).Return(fmt.Errorf("pop")) - m.trackSubmittedTransaction(&pendingState{ - mtx: &apitypes.ManagedTX{ - TransactionHash: sampleSendTX, - }, - }) + m.policyLoopCycle(true) + + mc.AssertExpectations(t) + mfc.AssertExpectations(t) } diff --git a/pkg/fftm/route_delete_eventstream_listener_test.go b/pkg/fftm/route_delete_eventstream_listener_test.go index 572051f4..2b630c64 100644 --- a/pkg/fftm/route_delete_eventstream_listener_test.go +++ b/pkg/fftm/route_delete_eventstream_listener_test.go @@ -18,7 +18,6 @@ package fftm import ( "fmt" - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -31,7 +30,7 @@ import ( func TestDeleteEventStreamListener(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_delete_eventstream_test.go b/pkg/fftm/route_delete_eventstream_test.go index d4e45d0a..c2200703 100644 --- a/pkg/fftm/route_delete_eventstream_test.go +++ b/pkg/fftm/route_delete_eventstream_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestDeleteEventStream(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() mfc := m.connector.(*ffcapimocks.API) diff --git a/pkg/fftm/route_delete_subscription_test.go b/pkg/fftm/route_delete_subscription_test.go index fdc4fa96..9214b85f 100644 --- a/pkg/fftm/route_delete_subscription_test.go +++ b/pkg/fftm/route_delete_subscription_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestDeleteSubscription(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_get_eventstream_listener_test.go b/pkg/fftm/route_get_eventstream_listener_test.go index 1355b163..a750b8dc 100644 --- a/pkg/fftm/route_get_eventstream_listener_test.go +++ b/pkg/fftm/route_get_eventstream_listener_test.go @@ -18,7 +18,6 @@ package fftm import ( "fmt" - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -31,7 +30,7 @@ import ( func TestGetEventStreamsListener(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_get_eventstream_listeners_test.go b/pkg/fftm/route_get_eventstream_listeners_test.go index 40018cac..5e2eaf34 100644 --- a/pkg/fftm/route_get_eventstream_listeners_test.go +++ b/pkg/fftm/route_get_eventstream_listeners_test.go @@ -18,7 +18,6 @@ package fftm import ( "fmt" - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -31,7 +30,7 @@ import ( func TestGetEventStreamListeners(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_get_eventstream_test.go b/pkg/fftm/route_get_eventstream_test.go index d27d8fdc..580465df 100644 --- a/pkg/fftm/route_get_eventstream_test.go +++ b/pkg/fftm/route_get_eventstream_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestGetEventStream(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() mfc := m.connector.(*ffcapimocks.API) diff --git a/pkg/fftm/route_get_eventstreams_test.go b/pkg/fftm/route_get_eventstreams_test.go index f29cb851..7f10a3b0 100644 --- a/pkg/fftm/route_get_eventstreams_test.go +++ b/pkg/fftm/route_get_eventstreams_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestGetEventStreams(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() mfc := m.connector.(*ffcapimocks.API) diff --git a/pkg/fftm/route_get_subscription_test.go b/pkg/fftm/route_get_subscription_test.go index e3cfc467..88fd8a98 100644 --- a/pkg/fftm/route_get_subscription_test.go +++ b/pkg/fftm/route_get_subscription_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestGetSubscription(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_get_subscriptions_test.go b/pkg/fftm/route_get_subscriptions_test.go index 0ed4b82a..fdb85f97 100644 --- a/pkg/fftm/route_get_subscriptions_test.go +++ b/pkg/fftm/route_get_subscriptions_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestGetSubscriptions(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_get_transactions.go b/pkg/fftm/route_get_transactions.go index dda1f9ad..e85b1839 100644 --- a/pkg/fftm/route_get_transactions.go +++ b/pkg/fftm/route_get_transactions.go @@ -36,13 +36,14 @@ var getTransactions = func(m *manager) *ffapi.Route { {Name: "after", Description: tmmsgs.APIParamAfter}, {Name: "signer", Description: tmmsgs.APIParamTXSigner}, {Name: "pending", Description: tmmsgs.APIParamTXPending, IsBool: true}, + {Name: "direction", Description: tmmsgs.APIParamSortDirection}, }, Description: tmmsgs.APIEndpointGetSubscriptions, JSONInputValue: nil, JSONOutputValue: func() interface{} { return []*apitypes.ManagedTX{} }, JSONOutputCodes: []int{http.StatusOK}, JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { - return m.getTransactions(r.Req.Context(), r.QP["after"], r.QP["limit"], r.QP["signer"], strings.EqualFold(r.QP["pending"], "true")) + return m.getTransactions(r.Req.Context(), r.QP["after"], r.QP["limit"], r.QP["signer"], strings.EqualFold(r.QP["pending"], "true"), r.QP["direction"]) }, } } diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go index d883f4fd..8b446718 100644 --- a/pkg/fftm/route_get_transactions_test.go +++ b/pkg/fftm/route_get_transactions_test.go @@ -19,7 +19,6 @@ package fftm import ( "context" "fmt" - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -51,7 +50,7 @@ func newTestTxn(t *testing.T, m *manager, signer string, nonce int64, status api func TestGetTransactions(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_patch_eventstream_listener_test.go b/pkg/fftm/route_patch_eventstream_listener_test.go index 8ffd77fc..74076573 100644 --- a/pkg/fftm/route_patch_eventstream_listener_test.go +++ b/pkg/fftm/route_patch_eventstream_listener_test.go @@ -18,7 +18,6 @@ package fftm import ( "fmt" - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -31,7 +30,7 @@ import ( func TestPatchEventStreamListener(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_patch_eventstream_test.go b/pkg/fftm/route_patch_eventstream_test.go index 5703b41a..7d5dc2f1 100644 --- a/pkg/fftm/route_patch_eventstream_test.go +++ b/pkg/fftm/route_patch_eventstream_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestPatchEventStream(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() mfc := m.connector.(*ffcapimocks.API) diff --git a/pkg/fftm/route_patch_subscription_test.go b/pkg/fftm/route_patch_subscription_test.go index e731bdf0..47b56146 100644 --- a/pkg/fftm/route_patch_subscription_test.go +++ b/pkg/fftm/route_patch_subscription_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestPatchSubscription(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_post_eventstream_listener_reset_test.go b/pkg/fftm/route_post_eventstream_listener_reset_test.go index 7fe9a2a9..77273ea9 100644 --- a/pkg/fftm/route_post_eventstream_listener_reset_test.go +++ b/pkg/fftm/route_post_eventstream_listener_reset_test.go @@ -18,7 +18,6 @@ package fftm import ( "fmt" - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -31,7 +30,7 @@ import ( func TestPostEventStreamListenerReset(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_post_eventstream_listeners_test.go b/pkg/fftm/route_post_eventstream_listeners_test.go index 8d497a24..f40343c5 100644 --- a/pkg/fftm/route_post_eventstream_listeners_test.go +++ b/pkg/fftm/route_post_eventstream_listeners_test.go @@ -18,7 +18,6 @@ package fftm import ( "fmt" - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -31,7 +30,7 @@ import ( func TestPostEventStreamListeners(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_post_eventstream_resume_test.go b/pkg/fftm/route_post_eventstream_resume_test.go index 980d5351..886161bd 100644 --- a/pkg/fftm/route_post_eventstream_resume_test.go +++ b/pkg/fftm/route_post_eventstream_resume_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestPostEventStreamResume(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() mfc := m.connector.(*ffcapimocks.API) diff --git a/pkg/fftm/route_post_eventstream_suspend_test.go b/pkg/fftm/route_post_eventstream_suspend_test.go index 9067ae23..dc70c438 100644 --- a/pkg/fftm/route_post_eventstream_suspend_test.go +++ b/pkg/fftm/route_post_eventstream_suspend_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestPostEventStreamSuspend(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() mfc := m.connector.(*ffcapimocks.API) diff --git a/pkg/fftm/route_post_eventstream_test.go b/pkg/fftm/route_post_eventstream_test.go index a16f63e9..24c24863 100644 --- a/pkg/fftm/route_post_eventstream_test.go +++ b/pkg/fftm/route_post_eventstream_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestPostNewEventStream(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() mfc := m.connector.(*ffcapimocks.API) diff --git a/pkg/fftm/route_post_subscription_reset_test.go b/pkg/fftm/route_post_subscription_reset_test.go index 84c064ce..cf764071 100644 --- a/pkg/fftm/route_post_subscription_reset_test.go +++ b/pkg/fftm/route_post_subscription_reset_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestPostSubscriptionReset(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/route_post_subscriptions_test.go b/pkg/fftm/route_post_subscriptions_test.go index 1bb32a9f..eca6b0a5 100644 --- a/pkg/fftm/route_post_subscriptions_test.go +++ b/pkg/fftm/route_post_subscriptions_test.go @@ -17,7 +17,6 @@ package fftm import ( - "net/http" "testing" "github.com/go-resty/resty/v2" @@ -30,7 +29,7 @@ import ( func TestPostSubscriptions(t *testing.T) { - url, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + url, m, done := newTestManager(t) defer done() err := m.Start() diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index deb34d4b..bb18adef 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -22,7 +22,6 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - "github.com/hyperledger/firefly/pkg/core" ) func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (*apitypes.ManagedTX, error) { @@ -53,14 +52,18 @@ func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes. // to the background worker. mtx := &apitypes.ManagedTX{ ID: request.Headers.ID, // on input the request ID must be the namespaced operation ID + SequenceID: apitypes.UUIDVersion1(), + Created: fftypes.Now(), Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), Gas: prepared.Gas, TransactionData: prepared.TransactionData, Request: request, + Status: apitypes.TxStatusPending, } - if err := m.writeManagedTX(m.ctx, mtx, core.OpStatusPending, ""); err != nil { + if err := m.persistence.WriteTransaction(m.ctx, mtx, true); err != nil { return nil, err } + m.markInflightStale() // Ok - we've spent it. The rest of the processing will be triggered off of lockedNonce // completion adding this transaction to the pool (and/or the change event that comes in from diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index b758f652..f08333e1 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -24,6 +24,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/events" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" ) @@ -35,7 +36,7 @@ const ( func (m *manager) restoreStreams() error { var lastInPage *fftypes.UUID for { - streamDefs, err := m.persistence.ListStreams(m.ctx, lastInPage, startupPaginationLimit) + streamDefs, err := m.persistence.ListStreams(m.ctx, lastInPage, startupPaginationLimit, persistence.SortDirectionAscending) if err != nil { return err } @@ -44,7 +45,7 @@ func (m *manager) restoreStreams() error { } for _, def := range streamDefs { lastInPage = def.ID - streamListeners, err := m.persistence.ListStreamListeners(m.ctx, nil, 0, def.ID) + streamListeners, err := m.persistence.ListStreamListeners(m.ctx, nil, 0, persistence.SortDirectionAscending, def.ID) if err != nil { return err } @@ -71,7 +72,7 @@ func (m *manager) restoreStreams() error { func (m *manager) deleteAllStreamListeners(ctx context.Context, streamID *fftypes.UUID) error { var lastInPage *fftypes.UUID for { - listenerDefs, err := m.persistence.ListStreamListeners(ctx, lastInPage, startupPaginationLimit, streamID) + listenerDefs, err := m.persistence.ListStreamListeners(ctx, lastInPage, startupPaginationLimit, persistence.SortDirectionAscending, streamID) if err != nil { return err } @@ -334,7 +335,7 @@ func (m *manager) getStreams(ctx context.Context, afterStr, limitStr string) (st if err != nil { return nil, err } - return m.persistence.ListStreams(ctx, after, limit) + return m.persistence.ListStreams(ctx, after, limit, persistence.SortDirectionDescending) } func (m *manager) getListener(ctx context.Context, streamIDStr, listenerIDStr string) (spec *apitypes.Listener, err error) { @@ -366,7 +367,7 @@ func (m *manager) getListeners(ctx context.Context, afterStr, limitStr string) ( if err != nil { return nil, err } - return m.persistence.ListListeners(ctx, after, limit) + return m.persistence.ListListeners(ctx, after, limit, persistence.SortDirectionDescending) } func (m *manager) getStreamListeners(ctx context.Context, afterStr, limitStr, idStr string) (streams []*apitypes.Listener, err error) { @@ -378,5 +379,5 @@ func (m *manager) getStreamListeners(ctx context.Context, afterStr, limitStr, id if err != nil { return nil, err } - return m.persistence.ListStreamListeners(ctx, after, limit, id) + return m.persistence.ListStreamListeners(ctx, after, limit, persistence.SortDirectionDescending, id) } diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go index 1a0a6091..2623cde8 100644 --- a/pkg/fftm/stream_management_test.go +++ b/pkg/fftm/stream_management_test.go @@ -18,10 +18,10 @@ package fftm import ( "fmt" - "net/http" "testing" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -31,7 +31,7 @@ import ( func TestRestoreStreamsAndListenersOK(t *testing.T) { - _, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + _, m, done := newTestManager(t) defer done() mfc := m.connector.(*ffcapimocks.API) @@ -76,7 +76,7 @@ func TestRestoreStreamsReadFailed(t *testing.T) { mp, _, m := newMockPersistenceManager(t) - mp.On("ListStreams", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit).Return(nil, fmt.Errorf("pop")) + mp.On("ListStreams", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending).Return(nil, fmt.Errorf("pop")) err := m.restoreStreams() assert.Regexp(t, "pop", err) @@ -88,10 +88,10 @@ func TestRestoreListenersReadFailed(t *testing.T) { mp, _, m := newMockPersistenceManager(t) - mp.On("ListStreams", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit).Return([]*apitypes.EventStream{ + mp.On("ListStreams", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending).Return([]*apitypes.EventStream{ {ID: fftypes.NewUUID()}, }, nil) - mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), 0, mock.Anything).Return(nil, fmt.Errorf("pop")) + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), 0, persistence.SortDirectionAscending, mock.Anything).Return(nil, fmt.Errorf("pop")) err := m.restoreStreams() assert.Regexp(t, "pop", err) @@ -101,7 +101,7 @@ func TestRestoreListenersReadFailed(t *testing.T) { func TestRestoreStreamsValidateFail(t *testing.T) { - _, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + _, m, done := newTestManager(t) defer done() falsy := false @@ -116,7 +116,7 @@ func TestRestoreStreamsValidateFail(t *testing.T) { func TestRestoreListenersStartFail(t *testing.T) { - _, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + _, m, done := newTestManager(t) defer done() mfc := m.connector.(*ffcapimocks.API) @@ -141,7 +141,7 @@ func TestRestoreListenersStartFail(t *testing.T) { func TestDeleteStartedListener(t *testing.T) { - _, m, done := newTestManager(t, func(w http.ResponseWriter, r *http.Request) {}) + _, m, done := newTestManager(t) defer done() mfc := m.connector.(*ffcapimocks.API) @@ -174,7 +174,7 @@ func TestDeleteStartedListenerFail(t *testing.T) { esID := apitypes.UUIDVersion1() lID := apitypes.UUIDVersion1() - mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, esID).Return([]*apitypes.Listener{ + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{ {ID: lID, StreamID: esID}, }, nil) mp.On("DeleteListener", m.ctx, lID).Return(fmt.Errorf("pop")) @@ -199,7 +199,7 @@ func TestDeleteStreamListenerPersistenceFail(t *testing.T) { mp, _, m := newMockPersistenceManager(t) esID := apitypes.UUIDVersion1() - mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, esID).Return(nil, fmt.Errorf("pop")) + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return(nil, fmt.Errorf("pop")) err := m.deleteStream(m.ctx, esID.String()) assert.Regexp(t, "pop", err) @@ -212,7 +212,7 @@ func TestDeleteStreamPersistenceFail(t *testing.T) { mp, _, m := newMockPersistenceManager(t) esID := apitypes.UUIDVersion1() - mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, esID).Return([]*apitypes.Listener{}, nil) + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{}, nil) mp.On("DeleteStream", m.ctx, esID).Return(fmt.Errorf("pop")) err := m.deleteStream(m.ctx, esID.String()) @@ -226,7 +226,7 @@ func TestDeleteStreamNotInitialized(t *testing.T) { mp, _, m := newMockPersistenceManager(t) esID := apitypes.UUIDVersion1() - mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, esID).Return([]*apitypes.Listener{}, nil) + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{}, nil) mp.On("DeleteStream", m.ctx, esID).Return(nil) err := m.deleteStream(m.ctx, esID.String()) diff --git a/pkg/fftm/transaction_management.go b/pkg/fftm/transaction_management.go index 109013ed..c1f2671f 100644 --- a/pkg/fftm/transaction_management.go +++ b/pkg/fftm/transaction_management.go @@ -18,18 +18,29 @@ package fftm import ( "context" + "strings" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" ) -func (m *manager) getTransactions(ctx context.Context, afterStr, limitStr, signer string, pending bool) (transactions []*apitypes.ManagedTX, err error) { +func (m *manager) getTransactions(ctx context.Context, afterStr, limitStr, signer string, pending bool, dirString string) (transactions []*apitypes.ManagedTX, err error) { limit, err := m.parseLimit(ctx, limitStr) if err != nil { return nil, err } + var dir persistence.SortDirection + switch strings.ToLower(dirString) { + case "", "desc", "descending": + dir = persistence.SortDirectionDescending // descending is default + case "asc", "ascending": + dir = persistence.SortDirectionAscending + default: + return nil, i18n.NewError(ctx, tmmsgs.MsgInvalidSortDirection, dirString) + } var afterTx *apitypes.ManagedTX if afterStr != "" { // Get the transaction, as we need this to exist to pick the right field depending on the index that's been chosen @@ -49,15 +60,15 @@ func (m *manager) getTransactions(ctx context.Context, afterStr, limitStr, signe if afterTx != nil { afterNonce = afterTx.Nonce } - return m.persistence.ListTransactionsByNonce(ctx, signer, afterNonce, limit) + return m.persistence.ListTransactionsByNonce(ctx, signer, afterNonce, limit, dir) case pending: var afterSequence *fftypes.UUID if afterTx != nil { afterSequence = afterTx.SequenceID } - return m.persistence.ListTransactionsPending(ctx, afterSequence, limit) + return m.persistence.ListTransactionsPending(ctx, afterSequence, limit, dir) default: - return m.persistence.ListTransactionsByCreateTime(ctx, afterTx, limit) + return m.persistence.ListTransactionsByCreateTime(ctx, afterTx, limit, dir) } } diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index 78cabaed..b00635ad 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -126,7 +126,7 @@ func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx * } sendTX.TransactionHeaders.Nonce = (*fftypes.FFBigInt)(mtx.Nonce.Int()) sendTX.TransactionHeaders.Gas = (*fftypes.FFBigInt)(mtx.Gas.Int()) - log.L(ctx).Infof("Sending transaction: %+v", sendTX) + log.L(ctx).Debugf("Sending transaction: %+v", sendTX) res, reason, err := cAPI.TransactionSend(ctx, sendTX) if err != nil { // A more sophisticated policy engine would consider the reason here, and potentially adjust the transaction for future attempts From e7838d40e1c60760e3811f7bd544b0eb751afef7 Mon Sep 17 00:00:00 2001 From: Nicko Guyer Date: Tue, 26 Jul 2022 08:45:13 -0400 Subject: [PATCH 58/95] Fixes for event delivery Signed-off-by: Nicko Guyer --- internal/events/websockets.go | 18 ++++++++++++++++-- pkg/ffcapi/api.go | 12 ++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/internal/events/websockets.go b/internal/events/websockets.go index 7a7e9368..41194fa2 100644 --- a/internal/events/websockets.go +++ b/internal/events/websockets.go @@ -19,6 +19,7 @@ package events import ( "context" + "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" @@ -96,9 +97,11 @@ func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt } } - // Sent the batch of events + preparedEvents := prepareEventsForSending(events) + + // Send the batch of events select { - case channel <- events: + case channel <- preparedEvents: break case <-ctx.Done(): err = i18n.NewError(ctx, tmmsgs.MsgWebSocketInterruptedSend) @@ -124,3 +127,14 @@ func (w *webSocketAction) waitForAck(ctx context.Context, receiver <-chan error) } return err } + +// Unwrap the event info and add data for sending to websocket clients +func prepareEventsForSending(events []*ffcapi.EventWithContext) []*fftypes.JSONObject { + a := make([]*fftypes.JSONObject, len(events)) + for i, event := range events { + o := event.Info.JSONObject() + o["data"] = event.Data + a[i] = &o + } + return a +} diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 07021a72..9c7f605d 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -82,12 +82,12 @@ type BlockHashEvent struct { // EventID are the set of required fields an FFCAPI compatible connector needs to map to the underlying blockchain constructs, to uniquely identify an event type EventID struct { - ListenerID *fftypes.UUID // The listener for the event - BlockHash string // String representation of the block, which will change if any transaction info in the block changes - BlockNumber uint64 // A numeric identifier for the block - TransactionHash string // The transaction - TransactionIndex uint64 // Index within the block of the transaction that emitted the event - LogIndex uint64 // Index within the transaction of this emitted event log + ListenerID *fftypes.UUID `json:"listenerId"` // The listener for the event + BlockHash string `json:"blockHash"` // String representation of the block, which will change if any transaction info in the block changes + BlockNumber uint64 `json:"blockNumber"` // A numeric identifier for the block + TransactionHash string `json:"transactionHash"` // The transaction + TransactionIndex uint64 `json:"transactionIndex"` // Index within the block of the transaction that emitted the event + LogIndex uint64 `json:"logIndex"` // Index within the transaction of this emitted event log } // Event is a blockchain event that matches one of the started listeners. From 189e097651e70465463aada61965a1edfe03b8c2 Mon Sep 17 00:00:00 2001 From: Nicko Guyer Date: Fri, 29 Jul 2022 10:07:52 -0400 Subject: [PATCH 59/95] Fixes for sending receipts on websockets Signed-off-by: Nicko Guyer --- internal/confirmations/confirmations.go | 14 +++++++--- pkg/fftm/policyloop.go | 37 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index 980396eb..b065eba2 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -423,10 +423,16 @@ func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem) { if pending.receiptCallback != nil { pending.receiptCallback(res) } - // Need to walk the chain for this new receipt - if err = bcm.walkChainForItem(pending); err != nil { - log.L(bcm.ctx).Debugf("Failed to walk chain for transaction %s: %s", pending.transactionHash, err) - return + + if bcm.requiredConfirmations == 0 { + delete(bcm.pending, pending.getKey()) + bcm.dispatchConfirmed(pending) + } else { + // Need to walk the chain for this new receipt + if err = bcm.walkChainForItem(pending); err != nil { + log.L(bcm.ctx).Debugf("Failed to walk chain for transaction %s: %s", pending.transactionHash, err) + return + } } } // No need to keep polling - either we now have a receipt, or normal block header monitoring will pick this one up diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index 525340c0..738ac1fe 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -190,6 +190,10 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { // Will be picked up on the next policy loop cycle - guaranteed to occur before Confirmed m.mux.Lock() pending.mtx.Receipt = receipt + // TODO: This may not be the right spot to do this, + // but since it's part of the manager, it has a pointer + // to the wsServer to be able to send a reply + m.wsServer.SendReply(wsTransactionReceipt(pending)) m.mux.Unlock() }, Confirmed: func(confirmations []confirmations.BlockInfo) { @@ -210,3 +214,36 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { pending.trackingTransactionHash = pending.mtx.TransactionHash } } + +type WsTransactionReceipt struct { + Headers *WsTransactionReceiptHeaders `json:"headers"` + TransactionHash string `json:"transactionHash"` + ErrorMessage string `json:"errorMessage"` +} + +type WsTransactionReceiptHeaders struct { + RequestID string `json:"requestId"` + ReplyType string `json:"type"` +} + +const ( + ReplyTypeTransactionSuccess = "TransactionSuccess" + ReplyTypeTransactionFailure = "TransactionFailure" +) + +// This function is used to transform the pendingState into the format +// that FireFly Core is expecting, preserving backward compatibility +// with the original Ethconnect implementation +func wsTransactionReceipt(pending *pendingState) *WsTransactionReceipt { + // TODO: Set this status correctly. Always report success for now, for initial testing + replyType := ReplyTypeTransactionSuccess + + return &WsTransactionReceipt{ + Headers: &WsTransactionReceiptHeaders{ + RequestID: pending.mtx.ID, + ReplyType: replyType, + }, + TransactionHash: pending.mtx.TransactionHash, + ErrorMessage: "", + } +} From 9e9fccdaf9fa6c2834782acae0ae27ffc1dc3196 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 1 Aug 2022 08:42:43 -0400 Subject: [PATCH 60/95] Flatten event delivery JSON payload Signed-off-by: Peter Broadhurst --- internal/confirmations/confirmations.go | 31 ++++--- internal/confirmations/confirmations_test.go | 26 +++--- internal/events/eventstream.go | 29 ++++--- internal/events/eventstream_test.go | 86 +++++++++++--------- internal/events/webhooks.go | 3 +- internal/events/webhooks_test.go | 11 ++- internal/events/websockets.go | 19 +---- internal/events/websockets_test.go | 9 +- pkg/apitypes/api_types.go | 49 +++++++++++ pkg/apitypes/api_types_test.go | 72 ++++++++++++++++ pkg/apitypes/flatten.go | 74 +++++++++++++++++ pkg/apitypes/flatten_test.go | 70 ++++++++++++++++ pkg/ffcapi/api.go | 39 ++++----- pkg/ffcapi/api_test.go | 6 +- pkg/fftm/manager_test.go | 15 ++++ pkg/fftm/nonces_test.go | 62 ++++++++++++-- 16 files changed, 463 insertions(+), 138 deletions(-) create mode 100644 pkg/apitypes/flatten.go create mode 100644 pkg/apitypes/flatten_test.go diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index b065eba2..97bfc737 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -60,7 +60,7 @@ type Notification struct { } type EventInfo struct { - ffcapi.EventID + ID *ffcapi.EventID Confirmed func(confirmations []BlockInfo) } @@ -173,12 +173,12 @@ func (pi *pendingItem) copyConfirmations() []BlockInfo { func (n *Notification) eventPendingItem() *pendingItem { return &pendingItem{ pType: pendingTypeEvent, - listenerID: n.Event.ListenerID, - blockNumber: n.Event.BlockNumber, - blockHash: n.Event.BlockHash, - transactionHash: n.Event.TransactionHash, - transactionIndex: n.Event.TransactionIndex, - logIndex: n.Event.LogIndex, + listenerID: n.Event.ID.ListenerID, + blockNumber: n.Event.ID.BlockNumber, + blockHash: n.Event.ID.BlockHash, + transactionHash: n.Event.ID.TransactionHash, + transactionIndex: n.Event.ID.TransactionIndex, + logIndex: n.Event.ID.LogIndex, confirmedCallback: n.Event.Confirmed, } } @@ -230,7 +230,7 @@ func (bcm *blockConfirmationManager) NewBlockHashes() chan<- *ffcapi.BlockHashEv func (bcm *blockConfirmationManager) Notify(n *Notification) error { switch n.NotificationType { case NewEventLog, RemovedEventLog: - if n.Event == nil || n.Event.ListenerID == nil || n.Event.TransactionHash == "" || n.Event.BlockHash == "" { + if n.Event == nil || n.Event.ID.ListenerID == nil || n.Event.ID.TransactionHash == "" || n.Event.ID.BlockHash == "" { return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n) } case NewTransaction, RemovedTransaction: @@ -390,9 +390,9 @@ func (bcm *blockConfirmationManager) processNotifications(notifications []*Notif bcm.addOrReplaceItem(newItem) bcm.staleReceipts[newItem.getKey()] = true case RemovedEventLog: - bcm.removeItem(n.eventPendingItem()) + bcm.removeItem(n.eventPendingItem().getKey(), true) case RemovedTransaction: - bcm.removeItem(n.transactionPendingItem()) + bcm.removeItem(n.transactionPendingItem().getKey(), true) default: // Note that streamStopped is handled in the polling loop directly log.L(bcm.ctx).Warnf("Unexpected notification type: %d", n.NotificationType) @@ -425,7 +425,6 @@ func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem) { } if bcm.requiredConfirmations == 0 { - delete(bcm.pending, pending.getKey()) bcm.dispatchConfirmed(pending) } else { // Need to walk the chain for this new receipt @@ -463,11 +462,10 @@ func (bcm *blockConfirmationManager) addOrReplaceItem(pending *pendingItem) { } // removeEvent is called by the goroutine on receipt of a remove event notification -func (bcm *blockConfirmationManager) removeItem(pending *pendingItem) { +func (bcm *blockConfirmationManager) removeItem(pendingKey string, stale bool) { bcm.pendingMux.Lock() defer bcm.pendingMux.Unlock() - pendingKey := pending.getKey() - log.L(bcm.ctx).Infof("Removing stale item %s", pendingKey) + log.L(bcm.ctx).Infof("Removing item %s (stale=%t)", pendingKey, stale) delete(bcm.pending, pendingKey) delete(bcm.staleReceipts, pendingKey) } @@ -535,9 +533,6 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { expectedBlockNumber++ } if len(pending.confirmations) >= bcm.requiredConfirmations { - bcm.pendingMux.Lock() - delete(bcm.pending, pendingKey) - bcm.pendingMux.Unlock() confirmed = append(confirmed, pending) } @@ -555,6 +550,8 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { // dispatchConfirmed drive the event stream for any events that are confirmed, and prunes the state func (bcm *blockConfirmationManager) dispatchConfirmed(item *pendingItem) { pendingKey := item.getKey() + bcm.removeItem(pendingKey, false) + log.L(bcm.ctx).Infof("Confirmed with %d confirmations event=%s", len(item.confirmations), pendingKey) item.confirmedCallback(item.copyConfirmations() /* a safe copy outside of our cache */) } diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index 93500ce4..fe351806 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -50,7 +50,7 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", @@ -161,7 +161,7 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", @@ -462,7 +462,7 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", @@ -565,7 +565,7 @@ func TestConfirmationsListenerFailWalkingChain(t *testing.T) { err := bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", @@ -593,7 +593,7 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", @@ -630,7 +630,7 @@ func TestConfirmationsListenerRemoved(t *testing.T) { lid := fftypes.NewUUID() n := &Notification{ Event: &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: lid, TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", @@ -669,7 +669,7 @@ func TestConfirmationsRemoveEvent(t *testing.T) { bcm.done = make(chan struct{}) eventInfo := &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", @@ -697,7 +697,7 @@ func TestConfirmationsRemoveEvent(t *testing.T) { <-bcm.done assert.Empty(t, bcm.pending) - assert.False(t, bcm.CheckInFlight(eventInfo.ListenerID)) + assert.False(t, bcm.CheckInFlight(eventInfo.ID.ListenerID)) mca.AssertExpectations(t) } @@ -709,7 +709,7 @@ func TestConfirmationsFailWalkChainAfterBlockGap(t *testing.T) { eventNotification := &Notification{ NotificationType: NewEventLog, Event: &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", @@ -736,7 +736,7 @@ func TestConfirmationsFailWalkChainAfterBlockGap(t *testing.T) { <-bcm.done assert.Len(t, bcm.pending, 1) - assert.True(t, bcm.CheckInFlight(eventNotification.Event.ListenerID)) + assert.True(t, bcm.CheckInFlight(eventNotification.Event.ID.ListenerID)) assert.NotNil(t, eventNotification.eventPendingItem().getKey()) // should be the event in there, the TX should be removed mca.AssertExpectations(t) } @@ -752,7 +752,7 @@ func TestConfirmationsRemoveTransaction(t *testing.T) { eventNotification := &Notification{ NotificationType: NewEventLog, Event: &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", @@ -795,7 +795,7 @@ func TestWalkChainForEventBlockNotInConfirmationChain(t *testing.T) { pending := (&Notification{ Event: &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", @@ -828,7 +828,7 @@ func TestWalkChainForEventBlockLookupFail(t *testing.T) { pending := (&Notification{ Event: &EventInfo{ - EventID: ffcapi.EventID{ + ID: &ffcapi.EventID{ ListenerID: fftypes.NewUUID(), TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index d17061ed..9150da7e 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -77,11 +77,11 @@ func InitDefaults() { } } -type eventStreamAction func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error +type eventStreamAction func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error type eventStreamBatch struct { number int - events []*ffcapi.EventWithContext + events []*apitypes.EventWithContext checkpoints map[fftypes.UUID]ffcapi.EventListenerCheckpoint timeout *time.Timer } @@ -601,12 +601,12 @@ func (es *eventStream) Delete(ctx context.Context) error { func (es *eventStream) processNewEvent(ctx context.Context, fev *ffcapi.ListenerEvent) { event := fev.Event - if event == nil || event.ListenerID == nil || fev.Checkpoint == nil { + if event == nil || event.ID.ListenerID == nil || fev.Checkpoint == nil { log.L(ctx).Warnf("Invalid event from connector: %+v", fev) return } es.mux.Lock() - l := es.listeners[*fev.Event.ListenerID] + l := es.listeners[*fev.Event.ID.ListenerID] es.mux.Unlock() if l != nil { log.L(ctx).Debugf("%s event detected: %s", l.spec.ID, event) @@ -621,7 +621,7 @@ func (es *eventStream) processNewEvent(ctx context.Context, fev *ffcapi.Listener err := es.confirmations.Notify(&confirmations.Notification{ NotificationType: confirmations.NewEventLog, Event: &confirmations.EventInfo{ - EventID: event.EventID, + ID: &event.ID, Confirmed: func(confirmations []confirmations.BlockInfo) { // Push it to the batch when confirmed // - Note this will block the confirmation manager when the event stream is blocked @@ -637,11 +637,11 @@ func (es *eventStream) processNewEvent(ctx context.Context, fev *ffcapi.Listener } func (es *eventStream) processRemovedEvent(ctx context.Context, fev *ffcapi.ListenerEvent) { - if fev.Event != nil && fev.Event.ListenerID != nil && es.confirmations != nil { + if fev.Event != nil && fev.Event.ID.ListenerID != nil && es.confirmations != nil { err := es.confirmations.Notify(&confirmations.Notification{ NotificationType: confirmations.RemovedEventLog, Event: &confirmations.EventInfo{ - EventID: fev.Event.EventID, + ID: &fev.Event.ID, }, }) if err != nil { @@ -693,7 +693,7 @@ func (es *eventStream) batchLoop(startedState *startedStreamState) { case fev := <-es.batchChannel: if fev.Event != nil { es.mux.Lock() - l := es.listeners[*fev.Event.ListenerID] + l := es.listeners[*fev.Event.ID.ListenerID] es.mux.Unlock() if l != nil { currentCheckpoint := l.checkpoint @@ -717,13 +717,18 @@ func (es *eventStream) batchLoop(startedState *startedStreamState) { } } if fev.Checkpoint != nil { - batch.checkpoints[*fev.Event.ListenerID] = fev.Checkpoint + batch.checkpoints[*fev.Event.ID.ListenerID] = fev.Checkpoint } log.L(es.bgCtx).Debugf("%s '%s' event confirmed: %s", l.spec.ID, l.spec.Signature, fev.Event) - batch.events = append(batch.events, &ffcapi.EventWithContext{ - StreamID: es.spec.ID, - Event: *fev.Event, + batch.events = append(batch.events, &apitypes.EventWithContext{ + StandardContext: apitypes.EventContext{ + StreamID: es.spec.ID, + DeprecatedSubID: l.spec.ID, + ListenerName: *l.spec.Name, + Signature: l.spec.Signature, + }, + Event: *fev.Event, }) } } diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 27b629dc..bbd53c32 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -365,18 +365,22 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { r.EventStream <- &ffcapi.ListenerEvent{ Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, Event: &ffcapi.Event{ - EventID: ffcapi.EventID{ + ID: ffcapi.EventID{ ListenerID: l.ID, BlockNumber: 42, TransactionIndex: 13, LogIndex: 1, }, Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), - Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), + Info: fftypes.JSONObject{ + "blockNumber": "42", + "transactionIndex": "13", + "logIndex": "1", + }, }, } - batch1 := (<-senderChannel).([]*ffcapi.EventWithContext) + batch1 := (<-senderChannel).([]*apitypes.EventWithContext) assert.Len(t, batch1, 1) assert.Equal(t, "v1", batch1[0].Data.JSONObject().GetString("k1")) @@ -450,12 +454,12 @@ func TestStartEventStreamCheckpointInvalid(t *testing.T) { func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { - receivedWebhook := make(chan []*ffcapi.EventWithContext, 1) + receivedWebhook := make(chan []*apitypes.EventWithContext, 1) s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/test/path", r.URL.Path) assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "application/json", r.Header.Get("content-type")) - var events []*ffcapi.EventWithContext + var events []*apitypes.EventWithContext err := json.NewDecoder(r.Body).Decode(&events) assert.NoError(t, err) receivedWebhook <- events @@ -531,14 +535,18 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { r.EventStream <- &ffcapi.ListenerEvent{ Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, Event: &ffcapi.Event{ - EventID: ffcapi.EventID{ + ID: ffcapi.EventID{ ListenerID: l.ID, BlockNumber: 42, TransactionIndex: 13, LogIndex: 1, }, Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), - Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), + Info: fftypes.JSONObject{ + "blockNumber": "42", + "transactionIndex": "13", + "logIndex": "1", + }, }, } @@ -1239,17 +1247,21 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { r.EventStream <- &ffcapi.ListenerEvent{ Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, Event: &ffcapi.Event{ - EventID: ffcapi.EventID{ + ID: ffcapi.EventID{ ListenerID: l.ID, BlockNumber: 42, TransactionIndex: 13, LogIndex: 1, }, Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), - Info: fftypes.JSONAnyPtr(`{"blockNumber":"42","transactionIndex":"13","logIndex":"1"}`), + Info: fftypes.JSONObject{ + "blockNumber": "42", + "transactionIndex": "13", + "logIndex": "1", + }, }, } - batch1 := (<-broadcastChannel).([]*ffcapi.EventWithContext) + batch1 := (<-broadcastChannel).([]*apitypes.EventWithContext) assert.Len(t, batch1, 1) assert.Equal(t, "v1", batch1[0].Data.JSONObject().GetString("k1")) @@ -1283,7 +1295,7 @@ func TestActionRetryOk(t *testing.T) { es.mux.Lock() callCount := 0 - es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { + es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { callCount++ if callCount > 1 { return nil @@ -1298,8 +1310,8 @@ func TestActionRetryOk(t *testing.T) { // retry then ok err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ - events: []*ffcapi.EventWithContext{ - {StreamID: es.spec.ID}, + events: []*apitypes.EventWithContext{ + {StandardContext: apitypes.EventContext{StreamID: es.spec.ID}}, }, }) assert.NoError(t, err) @@ -1333,15 +1345,15 @@ func TestActionRetrySkip(t *testing.T) { assert.NoError(t, err) es.mux.Lock() - es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { + es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { return fmt.Errorf("pop") } es.mux.Unlock() // Skip behavior err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ - events: []*ffcapi.EventWithContext{ - {StreamID: es.spec.ID}, + events: []*apitypes.EventWithContext{ + {StandardContext: apitypes.EventContext{StreamID: es.spec.ID}}, }, }) assert.NoError(t, err) @@ -1377,7 +1389,7 @@ func TestActionRetryBlock(t *testing.T) { es.mux.Lock() callCount := 0 done := make(chan struct{}) - es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { + es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { callCount++ if callCount == 1 { go func() { @@ -1392,8 +1404,8 @@ func TestActionRetryBlock(t *testing.T) { // Skip behavior err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ - events: []*ffcapi.EventWithContext{ - {StreamID: es.spec.ID}, + events: []*apitypes.EventWithContext{ + {StandardContext: apitypes.EventContext{StreamID: es.spec.ID}}, }, }) assert.Regexp(t, "FF00154", err) @@ -1445,7 +1457,7 @@ func TestEventLoopProcessRemovedEvent(t *testing.T) { u1 := &ffcapi.ListenerEvent{ Removed: true, Event: &ffcapi.Event{ - EventID: ffcapi.EventID{ + ID: ffcapi.EventID{ ListenerID: fftypes.NewUUID(), }, }, @@ -1455,8 +1467,8 @@ func TestEventLoopProcessRemovedEvent(t *testing.T) { ss.cancelCtx() }) es.confirmations = mcm - es.listeners[*u1.Event.ListenerID] = &listener{ - spec: &apitypes.Listener{ID: u1.Event.ListenerID}, + es.listeners[*u1.Event.ID.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, } go func() { @@ -1481,7 +1493,7 @@ func TestEventLoopProcessRemovedEventFail(t *testing.T) { u1 := &ffcapi.ListenerEvent{ Removed: true, Event: &ffcapi.Event{ - EventID: ffcapi.EventID{ + ID: ffcapi.EventID{ ListenerID: fftypes.NewUUID(), }, }, @@ -1491,8 +1503,8 @@ func TestEventLoopProcessRemovedEventFail(t *testing.T) { ss.cancelCtx() }) es.confirmations = mcm - es.listeners[*u1.Event.ListenerID] = &listener{ - spec: &apitypes.Listener{ID: u1.Event.ListenerID}, + es.listeners[*u1.Event.ID.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, } go func() { @@ -1517,14 +1529,14 @@ func TestEventLoopConfirmationsManagerBypass(t *testing.T) { u1 := &ffcapi.ListenerEvent{ Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, Event: &ffcapi.Event{ - EventID: ffcapi.EventID{ + ID: ffcapi.EventID{ ListenerID: fftypes.NewUUID(), }, }, } es.confirmations = nil - es.listeners[*u1.Event.ListenerID] = &listener{ - spec: &apitypes.Listener{ID: u1.Event.ListenerID}, + es.listeners[*u1.Event.ID.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, } go func() { @@ -1552,7 +1564,7 @@ func TestEventLoopConfirmationsManagerFail(t *testing.T) { u1 := &ffcapi.ListenerEvent{ Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, Event: &ffcapi.Event{ - EventID: ffcapi.EventID{ + ID: ffcapi.EventID{ ListenerID: fftypes.NewUUID(), }, }, @@ -1562,8 +1574,8 @@ func TestEventLoopConfirmationsManagerFail(t *testing.T) { ss.cancelCtx() }) es.confirmations = mcm - es.listeners[*u1.Event.ListenerID] = &listener{ - spec: &apitypes.Listener{ID: u1.Event.ListenerID}, + es.listeners[*u1.Event.ID.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, } go func() { @@ -1591,9 +1603,9 @@ func TestSkipEventsBehindCheckpoint(t *testing.T) { ss := &startedStreamState{ updates: make(chan *ffcapi.ListenerEvent, 1), batchLoopDone: make(chan struct{}), - action: func(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { + action: func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { assert.Len(t, events, 1) - assert.Equal(t, events[0].BlockNumber, uint64(2001)) + assert.Equal(t, events[0].ID.BlockNumber, uint64(2001)) return nil }, } @@ -1601,7 +1613,7 @@ func TestSkipEventsBehindCheckpoint(t *testing.T) { listenerID := fftypes.NewUUID() li := &listener{ - spec: &apitypes.Listener{ID: listenerID}, + spec: &apitypes.Listener{ID: listenerID, Name: strPtr("listener1")}, checkpoint: &utCheckpointType{SomeSequenceNumber: 2000}, } es.listeners[*li.spec.ID] = li @@ -1621,15 +1633,15 @@ func TestSkipEventsBehindCheckpoint(t *testing.T) { }() es.batchChannel <- &ffcapi.ListenerEvent{ Checkpoint: &utCheckpointType{SomeSequenceNumber: 1999}, // before checkpoint - redelivery - Event: &ffcapi.Event{EventID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 1999}}, + Event: &ffcapi.Event{ID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 1999}}, } es.batchChannel <- &ffcapi.ListenerEvent{ Checkpoint: &utCheckpointType{SomeSequenceNumber: 2000}, // on checkpoint - redelivery - Event: &ffcapi.Event{EventID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 2000}}, + Event: &ffcapi.Event{ID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 2000}}, } es.batchChannel <- &ffcapi.ListenerEvent{ Checkpoint: &utCheckpointType{SomeSequenceNumber: 2001}, // this is a new event - Event: &ffcapi.Event{EventID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 2001}}, + Event: &ffcapi.Event{ID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 2001}}, } wg.Wait() diff --git a/internal/events/webhooks.go b/internal/events/webhooks.go index 2bb788ae..bd3eaece 100644 --- a/internal/events/webhooks.go +++ b/internal/events/webhooks.go @@ -32,7 +32,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) func mergeValidateWhConfig(ctx context.Context, changed bool, base *apitypes.WebhookConfig, updates *apitypes.WebhookConfig) (*apitypes.WebhookConfig, bool, error) { @@ -91,7 +90,7 @@ func newWebhookAction(bgCtx context.Context, spec *apitypes.WebhookConfig) *webh } // attemptWebhookAction performs a single attempt of a webhook action -func (w *webhookAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { +func (w *webhookAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { // We perform DNS resolution before each attempt, to exclude private IP address ranges from the target u, _ := url.Parse(*w.spec.URL) addr, err := net.ResolveIPAddr("ip4", u.Hostname()) diff --git a/internal/events/webhooks_test.go b/internal/events/webhooks_test.go index 4948f4eb..8b32e653 100644 --- a/internal/events/webhooks_test.go +++ b/internal/events/webhooks_test.go @@ -28,7 +28,6 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" ) @@ -47,7 +46,7 @@ func TestWebhooksBadHost(t *testing.T) { tmconfig.Reset() ws := newTestWebhooks("http://www.sample.invalid/guaranteed-to-fail") - err := ws.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + err := ws.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) assert.Regexp(t, "FF21041", err) } @@ -57,7 +56,7 @@ func TestWebhooksPrivateBlocked(t *testing.T) { falsy := false ws.allowPrivateIPs = falsy - err := ws.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + err := ws.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) assert.Regexp(t, "FF21033", err) } @@ -67,7 +66,7 @@ func TestWebhooksCustomHeaders403(t *testing.T) { assert.Equal(t, "/test/path", r.URL.Path) assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "test-value", r.Header.Get("test-header")) - var events []*ffcapi.EventWithContext + var events []*apitypes.EventWithContext err := json.NewDecoder(r.Body).Decode(&events) assert.NoError(t, err) w.WriteHeader(403) @@ -82,7 +81,7 @@ func TestWebhooksCustomHeaders403(t *testing.T) { done := make(chan struct{}) go func() { - err := ws.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + err := ws.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) assert.Regexp(t, "FF21035.*403", err) close(done) }() @@ -99,7 +98,7 @@ func TestWebhooksCustomHeadersConnectFail(t *testing.T) { done := make(chan struct{}) go func() { - err := ws.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + err := ws.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) assert.Regexp(t, "FF21042", err) close(done) }() diff --git a/internal/events/websockets.go b/internal/events/websockets.go index 41194fa2..71e889b4 100644 --- a/internal/events/websockets.go +++ b/internal/events/websockets.go @@ -19,13 +19,11 @@ package events import ( "context" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/internal/ws" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) func mergeValidateWsConfig(ctx context.Context, changed bool, base *apitypes.WebSocketConfig, updates *apitypes.WebSocketConfig) (*apitypes.WebSocketConfig, bool, error) { @@ -70,7 +68,7 @@ func newWebSocketAction(wsChannels ws.WebSocketChannels, spec *apitypes.WebSocke } // attemptBatch attempts to deliver a batch over socket IO -func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*ffcapi.EventWithContext) error { +func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { var err error // Get a blocking channel to send and receive on our chosen namespace @@ -97,11 +95,9 @@ func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt } } - preparedEvents := prepareEventsForSending(events) - // Send the batch of events select { - case channel <- preparedEvents: + case channel <- events: break case <-ctx.Done(): err = i18n.NewError(ctx, tmmsgs.MsgWebSocketInterruptedSend) @@ -127,14 +123,3 @@ func (w *webSocketAction) waitForAck(ctx context.Context, receiver <-chan error) } return err } - -// Unwrap the event info and add data for sending to websocket clients -func prepareEventsForSending(events []*ffcapi.EventWithContext) []*fftypes.JSONObject { - a := make([]*fftypes.JSONObject, len(events)) - for i, event := range events { - o := event.Info.JSONObject() - o["data"] = event.Data - a[i] = &o - } - return a -} diff --git a/internal/events/websockets_test.go b/internal/events/websockets_test.go index 4eb9ffd2..b8eabcfe 100644 --- a/internal/events/websockets_test.go +++ b/internal/events/websockets_test.go @@ -22,7 +22,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/wsmocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" ) @@ -36,7 +35,7 @@ func TestWSAttemptBatchBadDistMode(t *testing.T) { DistributionMode: &dmw, }, "ut_stream") - err := wsa.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + err := wsa.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) assert.Regexp(t, "FF21034", err) } @@ -52,7 +51,7 @@ func TestWSAttemptBatchPurge(t *testing.T) { DistributionMode: &dmw, }, "ut_stream") - err := wsa.attemptBatch(context.Background(), 0, 0, []*ffcapi.EventWithContext{}) + err := wsa.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) assert.NoError(t, err) select { @@ -66,7 +65,7 @@ func TestWSAttemptBatchExitPushingEvent(t *testing.T) { mws := &wsmocks.WebSocketChannels{} _, bc, _ := mockWSChannels(mws) - bc <- []*ffcapi.EventWithContext{} // block the broadcast channel + bc <- []*apitypes.EventWithContext{} // block the broadcast channel dmw := apitypes.DistributionModeBroadcast wsa := newWebSocketAction(mws, &apitypes.WebSocketConfig{ @@ -75,7 +74,7 @@ func TestWSAttemptBatchExitPushingEvent(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - err := wsa.attemptBatch(ctx, 0, 0, []*ffcapi.EventWithContext{}) + err := wsa.attemptBatch(ctx, 0, 0, []*apitypes.EventWithContext{}) assert.Regexp(t, "FF21038", err) } diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index ae4a2a6c..378479a6 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -19,9 +19,11 @@ package apitypes import ( "bytes" "encoding/json" + "reflect" "github.com/google/uuid" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) type DistributionMode = fftypes.FFEnum @@ -212,3 +214,50 @@ func CheckUpdateStringMap(changed bool, merged *map[string]string, old map[strin jsonNew, _ := json.Marshal(new) return !bytes.Equal(jsonOld, jsonNew) } + +type EventContext struct { + StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event + DeprecatedSubID *fftypes.UUID `json:"subId"` // ID of the listener - deprecated "subscription" naming + ListenerName string `json:"listenerName"` // name of the listener + Signature string `json:"signature"` // event signature string +} + +// EventWithContext is what is delivered +// There is custom serialization to flatten the whole structure, so all the custom `info` fields from the +// connector are alongside the required context fields. +// The `data` is kep separate +type EventWithContext struct { + StandardContext EventContext + ffcapi.Event +} + +func (e *EventWithContext) MarshalJSON() ([]byte, error) { + base := e.Info + if base == nil { + base = fftypes.JSONObject{} + } + addJSONFieldsToMap(reflect.ValueOf(&e.ID), base) + addJSONFieldsToMap(reflect.ValueOf(&e.StandardContext), base) + base["data"] = e.Data + return json.Marshal(base) +} + +func (e *EventWithContext) UnmarshalJSON(b []byte) error { + // Note on unmarshal the info will have all the id+context fields + e.Info = make(fftypes.JSONObject) + err := json.Unmarshal(b, &e.Info) + if err == nil { + // ... but not the data + data := e.Info["data"] + delete(e.Info, "data") + if data != nil { + b, _ := json.Marshal(&data) + e.Data = fftypes.JSONAnyPtrBytes(b) + } + err = json.Unmarshal(b, &e.ID) + if err == nil { + err = json.Unmarshal(b, &e.StandardContext) + } + } + return err +} diff --git a/pkg/apitypes/api_types_test.go b/pkg/apitypes/api_types_test.go index 8f566702..44b50061 100644 --- a/pkg/apitypes/api_types_test.go +++ b/pkg/apitypes/api_types_test.go @@ -17,11 +17,13 @@ package apitypes import ( + "encoding/json" "strings" "testing" "time" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" ) @@ -182,3 +184,73 @@ func TestCheckUpdateStringMap(t *testing.T) { assert.Equal(t, map[string]string{"key1": "val1"}, pVal3) // val1 won assert.False(t, changed) // which was the current value } + +func TestMarshalUnmarshalEventOK(t *testing.T) { + + e := &EventWithContext{ + StandardContext: EventContext{ + StreamID: UUIDVersion1(), + ListenerName: "listener1", + Signature: "ev()", + DeprecatedSubID: UUIDVersion1(), + }, + Event: ffcapi.Event{ + ID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + BlockHash: "0x12345", + BlockNumber: 12345, + TransactionHash: "0x23456", + TransactionIndex: 10, + LogIndex: 1, + }, + Info: fftypes.JSONObject{ + "key1": "val1", + }, + Data: fftypes.JSONAnyPtr(`{"dk1":"dv1"}`), + }, + } + + b, err := json.Marshal(&e) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "blockHash":"0x12345", + "blockNumber":12345, + "data": {"dk1":"dv1"}, + "key1":"val1", + "listenerId":"`+e.ID.ListenerID.String()+`", + "listenerName":"listener1", + "logIndex":1, "signature":"ev()", + "subId":"`+e.StandardContext.DeprecatedSubID.String()+`", + "streamId":"`+e.StandardContext.StreamID.String()+`", + "transactionHash":"0x23456", + "transactionIndex":10 + }`, string(b)) + + var e2 *EventWithContext + err = json.Unmarshal(b, &e2) + assert.NoError(t, err) + + assert.Equal(t, e.ID.ListenerID, e2.ID.ListenerID) + assert.Equal(t, e.StandardContext.StreamID, e2.StandardContext.StreamID) + assert.Equal(t, e.Data, e2.Data) + assert.Equal(t, "val1", e2.Info.GetString("key1")) + +} + +func TestMarshalUnmarshalEmptyInfoOk(t *testing.T) { + + e := &EventWithContext{} + + _, err := json.Marshal(&e) + assert.NoError(t, err) + +} + +func TestUnmarshalFail(t *testing.T) { + + e := &EventWithContext{} + + err := json.Unmarshal([]byte(`!bad JSON`), &e) + assert.Error(t, err) + +} diff --git a/pkg/apitypes/flatten.go b/pkg/apitypes/flatten.go new file mode 100644 index 00000000..dd73a214 --- /dev/null +++ b/pkg/apitypes/flatten.go @@ -0,0 +1,74 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 apitypes + +import ( + "reflect" + "strings" +) + +// addJSONFieldsToMap is a helper for marshalling struct fields down into a map +// +// Note: Does not currently respect the `omitempty` JSON flag semantics +func addJSONFieldsToMap(val reflect.Value, data map[string]interface{}) { + varType := val.Type() + if varType.Kind() == reflect.Ptr { + addJSONFieldsToMap(val.Elem(), data) + return + } + for i := 0; i < varType.NumField(); i++ { + f := val.Field(i) + fType := varType.Field(i) + if fType.Anonymous { + addJSONFieldsToMap(f, data) + continue + } + if !f.CanInterface() { + continue + } + tag, ok := varType.Field(i).Tag.Lookup(`json`) + var fieldName string + if ok && len(tag) > 0 { + if tag == "-" || strings.Contains(tag, ",omitempty") && isEmptyValue(f) { + continue + } + fieldName = tag + } else { + fieldName = fType.Name + } + data[fieldName] = f.Interface() + } +} + +// had to copy these rules over from json as not exposed +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} diff --git a/pkg/apitypes/flatten_test.go b/pkg/apitypes/flatten_test.go new file mode 100644 index 00000000..061d7b94 --- /dev/null +++ b/pkg/apitypes/flatten_test.go @@ -0,0 +1,70 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 apitypes + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFlattenStructHelper(t *testing.T) { + + type testType1 struct { + Field1 string `json:"f1"` + Field2 int64 `json:"f2"` + } + + type testType2 struct { + testType1 + Field3 string `json:"f3"` + IgnoreMe string `json:"-"` + DefaultName string + internalField string + EmptyOne *string `json:"e1,omitempty"` + EmptyTwo string `json:"e2,omitempty"` + EmptyThree int64 `json:"e3,omitempty"` + EmptyFour uint64 `json:"e4,omitempty"` + EmptyFive float64 `json:"e5,omitempty"` + EmptySix bool `json:"e6,omitempty"` + EmptySeven []string `json:"e7,omitempty"` + } + + t1 := &testType2{ + testType1: testType1{ + Field1: "val1", + Field2: 2222, + }, + Field3: "val3", + DefaultName: "def111", + internalField: "internal", + EmptySeven: []string{}, + } + + m := make(map[string]interface{}) + addJSONFieldsToMap(reflect.ValueOf(t1), m) + b, err := json.Marshal(m) + assert.NoError(t, err) + assert.Equal(t, `{"DefaultName":"def111","f1":"val1","f2":2222,"f3":"val3"}`, string(b)) + +} + +func TestUnknownEmpty(t *testing.T) { + assert.False(t, isEmptyValue(reflect.ValueOf(make(chan struct{})))) +} diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 9c7f605d..275d15ee 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -82,21 +82,27 @@ type BlockHashEvent struct { // EventID are the set of required fields an FFCAPI compatible connector needs to map to the underlying blockchain constructs, to uniquely identify an event type EventID struct { - ListenerID *fftypes.UUID `json:"listenerId"` // The listener for the event - BlockHash string `json:"blockHash"` // String representation of the block, which will change if any transaction info in the block changes - BlockNumber uint64 `json:"blockNumber"` // A numeric identifier for the block - TransactionHash string `json:"transactionHash"` // The transaction - TransactionIndex uint64 `json:"transactionIndex"` // Index within the block of the transaction that emitted the event - LogIndex uint64 `json:"logIndex"` // Index within the transaction of this emitted event log + ListenerID *fftypes.UUID `json:"listenerId"` // The listener for the event + BlockHash string `json:"blockHash"` // String representation of the block, which will change if any transaction info in the block changes + BlockNumber uint64 `json:"blockNumber"` // A numeric identifier for the block + TransactionHash string `json:"transactionHash"` // The transaction + TransactionIndex uint64 `json:"transactionIndex"` // Index within the block of the transaction that emitted the event + LogIndex uint64 `json:"logIndex"` // Index within the transaction of this emitted event log + Timestamp *fftypes.FFTime `json:"timestamp,omitempty"` // The on-chain timestamp } -// Event is a blockchain event that matches one of the started listeners. +// Event is a blockchain event that matches one of the started listeners, +// and is the structure passed from the connector to FFTM // The implementation is responsible for ensuring all events on a listener are // ordered on to this channel in the exact sequence from the blockchain. type Event struct { - EventID - Data *fftypes.JSONAny `json:"data"` // the JSON data to deliver for this event (can be array or object structure) - Info *fftypes.JSONAny `json:"info"` // additional blockchain specific information + ID EventID // standard fields provided by the connector + Info fftypes.JSONObject // extra custom fields from the connector + Data *fftypes.JSONAny // data +} + +func (e *Event) String() string { + return e.ID.String() } // EventListenerCheckpoint is the interface that a checkpoint must implement, basically to make it sortable. @@ -130,15 +136,10 @@ func (lu ListenerEvents) Swap(i, j int) { lu[i], lu[j] = lu[j], lu[i] } func (lu ListenerEvents) Less(i, j int) bool { return evLess(lu[i].Event, lu[j].Event) } func evLess(eI *Event, eJ *Event) bool { - return eI.BlockNumber < eJ.BlockNumber || - ((eI.BlockNumber == eJ.BlockNumber) && - ((eI.TransactionIndex < eJ.TransactionIndex) || - ((eI.TransactionIndex == eJ.TransactionIndex) && (eI.LogIndex < eJ.LogIndex)))) -} - -type EventWithContext struct { - StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event - Event + return eI.ID.BlockNumber < eJ.ID.BlockNumber || + ((eI.ID.BlockNumber == eJ.ID.BlockNumber) && + ((eI.ID.TransactionIndex < eJ.ID.TransactionIndex) || + ((eI.ID.TransactionIndex == eJ.ID.TransactionIndex) && (eI.ID.LogIndex < eJ.ID.LogIndex)))) } // ListenerEvent is an event+checkpoint for a particular listener, and is the object delivered over the event stream channel when diff --git a/pkg/ffcapi/api_test.go b/pkg/ffcapi/api_test.go index cf381e13..55d27a8f 100644 --- a/pkg/ffcapi/api_test.go +++ b/pkg/ffcapi/api_test.go @@ -35,7 +35,7 @@ func TestSortEvents(t *testing.T) { t, _ := rand.Int(rand.Reader, big.NewInt(10)) l, _ := rand.Int(rand.Reader, big.NewInt(10)) events[i] = &Event{ - EventID: EventID{ + ID: EventID{ BlockNumber: b.Uint64(), TransactionIndex: t.Uint64(), LogIndex: l.Uint64(), @@ -49,8 +49,8 @@ func TestSortEvents(t *testing.T) { sort.Sort(listenerUpdates) for i := 1; i < len(events); i++ { - assert.LessOrEqual(t, strings.Compare(events[i-1].ProtocolID(), events[i].ProtocolID()), 0) + assert.LessOrEqual(t, strings.Compare(events[i-1].ID.ProtocolID(), events[i].ID.ProtocolID()), 0) assert.LessOrEqual(t, strings.Compare(events[i-1].String(), events[i].String()), 0) - assert.LessOrEqual(t, strings.Compare(listenerUpdates[i-1].Event.ProtocolID(), listenerUpdates[i].Event.ProtocolID()), 0) + assert.LessOrEqual(t, strings.Compare(listenerUpdates[i-1].Event.ID.ProtocolID(), listenerUpdates[i].Event.ID.ProtocolID()), 0) } } diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 1e0d0931..05b63229 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -29,6 +29,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/httpserver" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" @@ -39,6 +40,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines/simple" "github.com/hyperledger/firefly/pkg/core" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) const testManagerName = "unittest" @@ -172,3 +174,16 @@ func TestAddErrorMessageMax(t *testing.T) { assert.Equal(t, "crackle", mtx.ErrorHistory[1].Error) } + +func TestStartRestoreFail(t *testing.T) { + _, m, cancel := newTestManager(t) + cancel() + + mp := &persistencemocks.Persistence{} + m.persistence = mp + mp.On("ListStreams", mock.Anything, mock.Anything, startupPaginationLimit, persistence.SortDirectionAscending). + Return(nil, fmt.Errorf("pop")) + + err := m.Start() + assert.Regexp(t, "pop", err) +} diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go index 6d792e6a..7a0eac6c 100644 --- a/pkg/fftm/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -24,6 +24,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" @@ -111,13 +112,43 @@ func TestNonceStaleStateContention(t *testing.T) { } -func TestNonceError(t *testing.T) { +func TestNonceListError(t *testing.T) { _, m, cancel := newTestManager(t) defer cancel() - mFFC := m.connector.(*ffcapimocks.API) + mp := &persistencemocks.Persistence{} + m.persistence = mp + mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, fmt.Errorf("pop")) + mp.On("Close", mock.Anything).Return(nil) + + _, err := m.sendManagedTransaction(context.Background(), &apitypes.TransactionRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + }, + }) + assert.Regexp(t, "pop", err) +} + +func TestNonceListStaleThenQueryFail(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + mp := &persistencemocks.Persistence{} + m.persistence = mp + old := fftypes.FFTime(time.Now().Add(-10000 * time.Hour)) + mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]*apitypes.ManagedTX{ + {ID: "id12345", Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000), Created: &old}, + }, nil) + mp.On("Close", mock.Anything).Return(nil).Maybe() + + mFFC := m.connector.(*ffcapimocks.API) mFFC.On("NextNonceForSigner", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) _, err := m.sendManagedTransaction(context.Background(), &apitypes.TransactionRequest{ @@ -129,10 +160,27 @@ func TestNonceError(t *testing.T) { }) assert.Regexp(t, "pop", err) - m.mux.Lock() - locked, isLocked := m.lockedNonces["0x12345"] - assert.Nil(t, locked) - assert.False(t, isLocked) - m.mux.Unlock() + mp.AssertExpectations(t) + mFFC.AssertExpectations(t) + +} + +func TestNonceListNotStale(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + m.nonceStateTimeout = 1 * time.Hour + + mp := &persistencemocks.Persistence{} + m.persistence = mp + mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]*apitypes.ManagedTX{ + {ID: "id12345", Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000), Created: fftypes.Now()}, + }, nil) + mp.On("Close", mock.Anything).Return(nil).Maybe() + + n, err := m.calcNextNonce(context.Background(), "0x12345") + assert.NoError(t, err) + assert.Equal(t, uint64(1001), n) } From 7e15d0853f636cc02dacaffe322b4738846870ea Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 1 Aug 2022 09:22:38 -0400 Subject: [PATCH 61/95] Update to use common package, and let Info be a interface{} Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 1 + go.mod | 2 +- go.sum | 2 + internal/events/eventstream_test.go | 30 +++++++----- pkg/apitypes/api_types.go | 29 +++++------ pkg/apitypes/api_types_test.go | 10 ++-- pkg/apitypes/flatten.go | 74 ----------------------------- pkg/apitypes/flatten_test.go | 70 --------------------------- pkg/ffcapi/api.go | 6 +-- 9 files changed, 47 insertions(+), 177 deletions(-) delete mode 100644 pkg/apitypes/flatten.go delete mode 100644 pkg/apitypes/flatten_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 47eab490..e158d17c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -48,6 +48,7 @@ "hyperledger", "Infof", "IPFS", + "jsonmap", "Kaleido", "leveldb", "loadbalanced", diff --git a/go.mod b/go.mod index c088fba4..26679635 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e - github.com/hyperledger/firefly-common v0.1.13 + github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51 github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.1 diff --git a/go.sum b/go.sum index eedc5890..17ce40dd 100644 --- a/go.sum +++ b/go.sum @@ -632,6 +632,8 @@ github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e/go.mod h1:43 github.com/hyperledger/firefly-common v0.1.2/go.mod h1:ytB+404+Qg00wJKx602Yi/V6Spx9d0g65lZR9y5fzlo= github.com/hyperledger/firefly-common v0.1.13 h1:eNK99U9FV43u1F46MM0mPuXT4Xn++orghpoTIIPsmwo= github.com/hyperledger/firefly-common v0.1.13/go.mod h1:2NqPi5Ud9H6rSlZXkLbotxW7z4EAD89p3/8oNOpm9Gs= +github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51 h1:Izw9pK53kPGliZv12hCwJfP7rYILhXPx3hMNsCNv7YQ= +github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index bbd53c32..31cf9408 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -43,6 +43,12 @@ import ( func strPtr(s string) *string { return &s } +type testInfo struct { + BlockNumber string `json:"blockNumber"` + TransactionIndex string `json:"transactionIndex"` + LogIndex string `json:"logIndex"` +} + type utCheckpointType struct { SomeSequenceNumber int64 `json:"someSequenceNumber"` } @@ -372,10 +378,10 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { LogIndex: 1, }, Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), - Info: fftypes.JSONObject{ - "blockNumber": "42", - "transactionIndex": "13", - "logIndex": "1", + Info: &testInfo{ + BlockNumber: "42", + TransactionIndex: "13", + LogIndex: "1", }, }, } @@ -542,10 +548,10 @@ func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { LogIndex: 1, }, Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), - Info: fftypes.JSONObject{ - "blockNumber": "42", - "transactionIndex": "13", - "logIndex": "1", + Info: &testInfo{ + BlockNumber: "42", + TransactionIndex: "13", + LogIndex: "1", }, }, } @@ -1254,10 +1260,10 @@ func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { LogIndex: 1, }, Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), - Info: fftypes.JSONObject{ - "blockNumber": "42", - "transactionIndex": "13", - "logIndex": "1", + Info: &testInfo{ + BlockNumber: "42", + TransactionIndex: "13", + LogIndex: "1", }, }, } diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 378479a6..09afb888 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -23,6 +23,7 @@ import ( "github.com/google/uuid" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/jsonmap" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -232,24 +233,24 @@ type EventWithContext struct { } func (e *EventWithContext) MarshalJSON() ([]byte, error) { - base := e.Info - if base == nil { - base = fftypes.JSONObject{} + m := make(map[string]interface{}) + if e.Info != nil { + jsonmap.AddJSONFieldsToMap(reflect.ValueOf(e.Info), m) } - addJSONFieldsToMap(reflect.ValueOf(&e.ID), base) - addJSONFieldsToMap(reflect.ValueOf(&e.StandardContext), base) - base["data"] = e.Data - return json.Marshal(base) + jsonmap.AddJSONFieldsToMap(reflect.ValueOf(&e.ID), m) + jsonmap.AddJSONFieldsToMap(reflect.ValueOf(&e.StandardContext), m) + m["data"] = e.Data + return json.Marshal(m) } +// Note on unmarshal info will be a map with all the fields (except "data") func (e *EventWithContext) UnmarshalJSON(b []byte) error { - // Note on unmarshal the info will have all the id+context fields - e.Info = make(fftypes.JSONObject) - err := json.Unmarshal(b, &e.Info) - if err == nil { - // ... but not the data - data := e.Info["data"] - delete(e.Info, "data") + var m fftypes.JSONObject + err := json.Unmarshal(b, &m) + if err == nil && m != nil { + e.Info = m + data := m["data"] + delete(m, "data") if data != nil { b, _ := json.Marshal(&data) e.Data = fftypes.JSONAnyPtrBytes(b) diff --git a/pkg/apitypes/api_types_test.go b/pkg/apitypes/api_types_test.go index 44b50061..d80895cf 100644 --- a/pkg/apitypes/api_types_test.go +++ b/pkg/apitypes/api_types_test.go @@ -187,6 +187,10 @@ func TestCheckUpdateStringMap(t *testing.T) { func TestMarshalUnmarshalEventOK(t *testing.T) { + type customInfo struct { + InfoKey1 string `json:"key1"` + } + e := &EventWithContext{ StandardContext: EventContext{ StreamID: UUIDVersion1(), @@ -203,8 +207,8 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { TransactionIndex: 10, LogIndex: 1, }, - Info: fftypes.JSONObject{ - "key1": "val1", + Info: &customInfo{ + InfoKey1: "val1", }, Data: fftypes.JSONAnyPtr(`{"dk1":"dv1"}`), }, @@ -233,7 +237,7 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { assert.Equal(t, e.ID.ListenerID, e2.ID.ListenerID) assert.Equal(t, e.StandardContext.StreamID, e2.StandardContext.StreamID) assert.Equal(t, e.Data, e2.Data) - assert.Equal(t, "val1", e2.Info.GetString("key1")) + assert.Equal(t, "val1", e2.Info.(fftypes.JSONObject).GetString("key1")) } diff --git a/pkg/apitypes/flatten.go b/pkg/apitypes/flatten.go deleted file mode 100644 index dd73a214..00000000 --- a/pkg/apitypes/flatten.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 apitypes - -import ( - "reflect" - "strings" -) - -// addJSONFieldsToMap is a helper for marshalling struct fields down into a map -// -// Note: Does not currently respect the `omitempty` JSON flag semantics -func addJSONFieldsToMap(val reflect.Value, data map[string]interface{}) { - varType := val.Type() - if varType.Kind() == reflect.Ptr { - addJSONFieldsToMap(val.Elem(), data) - return - } - for i := 0; i < varType.NumField(); i++ { - f := val.Field(i) - fType := varType.Field(i) - if fType.Anonymous { - addJSONFieldsToMap(f, data) - continue - } - if !f.CanInterface() { - continue - } - tag, ok := varType.Field(i).Tag.Lookup(`json`) - var fieldName string - if ok && len(tag) > 0 { - if tag == "-" || strings.Contains(tag, ",omitempty") && isEmptyValue(f) { - continue - } - fieldName = tag - } else { - fieldName = fType.Name - } - data[fieldName] = f.Interface() - } -} - -// had to copy these rules over from json as not exposed -func isEmptyValue(v reflect.Value) bool { - switch v.Kind() { - case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - return v.Len() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Interface, reflect.Ptr: - return v.IsNil() - } - return false -} diff --git a/pkg/apitypes/flatten_test.go b/pkg/apitypes/flatten_test.go deleted file mode 100644 index 061d7b94..00000000 --- a/pkg/apitypes/flatten_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 apitypes - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFlattenStructHelper(t *testing.T) { - - type testType1 struct { - Field1 string `json:"f1"` - Field2 int64 `json:"f2"` - } - - type testType2 struct { - testType1 - Field3 string `json:"f3"` - IgnoreMe string `json:"-"` - DefaultName string - internalField string - EmptyOne *string `json:"e1,omitempty"` - EmptyTwo string `json:"e2,omitempty"` - EmptyThree int64 `json:"e3,omitempty"` - EmptyFour uint64 `json:"e4,omitempty"` - EmptyFive float64 `json:"e5,omitempty"` - EmptySix bool `json:"e6,omitempty"` - EmptySeven []string `json:"e7,omitempty"` - } - - t1 := &testType2{ - testType1: testType1{ - Field1: "val1", - Field2: 2222, - }, - Field3: "val3", - DefaultName: "def111", - internalField: "internal", - EmptySeven: []string{}, - } - - m := make(map[string]interface{}) - addJSONFieldsToMap(reflect.ValueOf(t1), m) - b, err := json.Marshal(m) - assert.NoError(t, err) - assert.Equal(t, `{"DefaultName":"def111","f1":"val1","f2":2222,"f3":"val3"}`, string(b)) - -} - -func TestUnknownEmpty(t *testing.T) { - assert.False(t, isEmptyValue(reflect.ValueOf(make(chan struct{})))) -} diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 275d15ee..1b9f3fe6 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -96,9 +96,9 @@ type EventID struct { // The implementation is responsible for ensuring all events on a listener are // ordered on to this channel in the exact sequence from the blockchain. type Event struct { - ID EventID // standard fields provided by the connector - Info fftypes.JSONObject // extra custom fields from the connector - Data *fftypes.JSONAny // data + ID EventID // standard fields provided by the connector + Info interface{} // extra custom fields from the connector - can be any JSON serializable struct + Data *fftypes.JSONAny // data } func (e *Event) String() string { From 8b64969622fbf80d685c149e20d1b50d991c946a Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 1 Aug 2022 21:50:00 -0400 Subject: [PATCH 62/95] Correct typo Signed-off-by: Peter Broadhurst --- pkg/apitypes/api_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 09afb888..5d0901ed 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -226,7 +226,7 @@ type EventContext struct { // EventWithContext is what is delivered // There is custom serialization to flatten the whole structure, so all the custom `info` fields from the // connector are alongside the required context fields. -// The `data` is kep separate +// The `data` is kept separate type EventWithContext struct { StandardContext EventContext ffcapi.Event From 1d94c78dfdf25c495ca64d3299ab5bf5970111af Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 2 Aug 2022 09:03:58 -0400 Subject: [PATCH 63/95] Ensure consistent block state throughout cycle, to prevent out-of-order delivery Signed-off-by: Peter Broadhurst --- config.md | 12 +++ go.mod | 2 +- go.sum | 6 +- internal/confirmations/confirmations.go | 77 +++++++++++++++--- internal/confirmations/confirmations_test.go | 84 ++++++++++++++++++-- 5 files changed, 158 insertions(+), 23 deletions(-) diff --git a/config.md b/config.md index 37503792..be2a1c54 100644 --- a/config.md +++ b/config.md @@ -30,6 +30,18 @@ nav_order: 2 |shutdownTimeout|The maximum amount of time to wait for any open HTTP requests to finish before shutting down the HTTP server|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` |writeTimeout|The maximum time to wait when writing to a HTTP connection|[`time.Duration`](https://pkg.go.dev/time#Duration)|`15s` +## api.auth + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|type|The auth plugin to use for server side authentication of requests|`string`|`` + +## api.auth.basic + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|passwordfile|The path to a .htpasswd file to use for authenticating requests. Passwords should be hashed with bcrypt.|`string`|`` + ## api.tls |Key|Description|Type|Default Value| diff --git a/go.mod b/go.mod index 26679635..0fb41643 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e - github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51 + github.com/hyperledger/firefly-common v0.1.16 github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.1 diff --git a/go.sum b/go.sum index 17ce40dd..cd43b067 100644 --- a/go.sum +++ b/go.sum @@ -630,10 +630,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e h1:QP+Yykyq7C670zb4Fs7s4lAtYmvIll4rP/y00hdOsg4= github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e/go.mod h1:434LxYn4ntyK/E0dY+2dTc55caBy6BdUMYBM2gLndAI= github.com/hyperledger/firefly-common v0.1.2/go.mod h1:ytB+404+Qg00wJKx602Yi/V6Spx9d0g65lZR9y5fzlo= -github.com/hyperledger/firefly-common v0.1.13 h1:eNK99U9FV43u1F46MM0mPuXT4Xn++orghpoTIIPsmwo= -github.com/hyperledger/firefly-common v0.1.13/go.mod h1:2NqPi5Ud9H6rSlZXkLbotxW7z4EAD89p3/8oNOpm9Gs= -github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51 h1:Izw9pK53kPGliZv12hCwJfP7rYILhXPx3hMNsCNv7YQ= -github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= +github.com/hyperledger/firefly-common v0.1.16 h1:21xidDEKrJhtGdBSRqHN4PfDi7aYxF0HOFuAa04V1AE= +github.com/hyperledger/firefly-common v0.1.16/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index 97bfc737..d2248d6d 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -207,6 +207,12 @@ func (pi pendingItems) Less(i, j int) bool { (pi[i].transactionIndex == pi[j].transactionIndex && pi[i].logIndex < pi[j].logIndex))) } +type blockState struct { + bcm *blockConfirmationManager + blocks map[uint64]*BlockInfo + lowestNil uint64 +} + func (bcm *blockConfirmationManager) Start() { bcm.done = make(chan struct{}) go bcm.confirmationsListener() @@ -327,8 +333,13 @@ func (bcm *blockConfirmationManager) confirmationsListener() { } } + // Each time round the loop we need to have a consistent view of the chain. + // This view must not add later blocks (by number) in, or change the hash of blocks, + // otherwise we could potentially deliver things out of order. + blocks := bcm.newBlockState() + if bcm.blockListenerStale { - if err := bcm.walkChain(); err != nil { + if err := bcm.walkChain(blocks); err != nil { log.L(bcm.ctx).Errorf("Failed to create walk chain after restoring blockListener: %s", err) continue } @@ -342,7 +353,7 @@ func (bcm *blockConfirmationManager) confirmationsListener() { // Process any new notifications - we do this at the end, so it can benefit // from knowing the latest highestBlockSeen - if err := bcm.processNotifications(notifications); err != nil { + if err := bcm.processNotifications(notifications, blocks); err != nil { log.L(bcm.ctx).Errorf("Failed processing notifications: %s", err) continue } @@ -356,7 +367,7 @@ func (bcm *blockConfirmationManager) confirmationsListener() { // receipt checks, or processing block headers for pendingKey := range bcm.staleReceipts { if pending, ok := bcm.pending[pendingKey]; ok { - bcm.checkReceipt(pending) + bcm.checkReceipt(pending, blocks) } } @@ -375,14 +386,14 @@ func (bcm *blockConfirmationManager) staleReceiptCheck() { } } -func (bcm *blockConfirmationManager) processNotifications(notifications []*Notification) error { +func (bcm *blockConfirmationManager) processNotifications(notifications []*Notification, blocks *blockState) error { for _, n := range notifications { switch n.NotificationType { case NewEventLog: newItem := n.eventPendingItem() bcm.addOrReplaceItem(newItem) - if err := bcm.walkChainForItem(newItem); err != nil { + if err := bcm.walkChainForItem(newItem, blocks); err != nil { return err } case NewTransaction: @@ -402,7 +413,7 @@ func (bcm *blockConfirmationManager) processNotifications(notifications []*Notif return nil } -func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem) { +func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem, blocks *blockState) { res, reason, err := bcm.connector.TransactionReceipt(bcm.ctx, &ffcapi.TransactionReceiptRequest{ TransactionHash: pending.transactionHash, }) @@ -428,7 +439,7 @@ func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem) { bcm.dispatchConfirmed(pending) } else { // Need to walk the chain for this new receipt - if err = bcm.walkChainForItem(pending); err != nil { + if err = bcm.walkChainForItem(pending, blocks); err != nil { log.L(bcm.ctx).Debugf("Failed to walk chain for transaction %s: %s", pending.transactionHash, err) return } @@ -559,7 +570,7 @@ func (bcm *blockConfirmationManager) dispatchConfirmed(item *pendingItem) { // walkChain goes through each event and sees whether it's valid, // purging any stale confirmations - or whole events if the blockListener is invalid // We do this each time our blockListener is invalidated -func (bcm *blockConfirmationManager) walkChain() error { +func (bcm *blockConfirmationManager) walkChain(blocks *blockState) error { // Grab a copy of all the pending in order bcm.pendingMux.Lock() @@ -570,9 +581,14 @@ func (bcm *blockConfirmationManager) walkChain() error { bcm.pendingMux.Unlock() sort.Sort(pendingItems) - // Go through them in order - using the cache for efficiency + // Go through them in order, as we must deliver them in the order on the chain. + // For the same reason we use a map _including misses_ of blocks: + // Without this map we could deliver out of order: + // If a new block were to be mined+detected while we were traversing a long list, + // then only walking the chain for later events in the list would find the block. + // This means those later events would be delivered, but the earlier ones would not. for _, pending := range pendingItems { - if err := bcm.walkChainForItem(pending); err != nil { + if err := bcm.walkChainForItem(pending, blocks); err != nil { return err } } @@ -581,7 +597,44 @@ func (bcm *blockConfirmationManager) walkChain() error { } -func (bcm *blockConfirmationManager) walkChainForItem(pending *pendingItem) (err error) { +func (bcm *blockConfirmationManager) newBlockState() *blockState { + return &blockState{ + bcm: bcm, + blocks: make(map[uint64]*BlockInfo), + } +} + +func (bs *blockState) getByNumber(blockNumber uint64, expectedParentHash string) (*BlockInfo, error) { + // blockState gives a consistent view of the chain throughout a cycle, where we perform a carefully ordered + // set of actions against our pending items. + // - We never return newer blocks after a query has been made that found a nil result at a lower block number + // - We never change the hash of a block + // If these changes happen during a cycle, we will pick them up on the next cycle rather than risk out-of-order + // delivery of events by detecting them half way through. + if bs.lowestNil > 0 && blockNumber >= bs.lowestNil { + log.L(bs.bcm.ctx).Debugf("Block %d is after chain head (cached)", blockNumber) + return nil, nil + } + block := bs.blocks[blockNumber] + if block != nil { + return block, nil + } + block, err := bs.bcm.getBlockByNumber(blockNumber, expectedParentHash) + if err != nil { + return nil, err + } + if block == nil { + if bs.lowestNil == 0 || blockNumber <= bs.lowestNil { + log.L(bs.bcm.ctx).Debugf("Block %d is after chain head", blockNumber) + bs.lowestNil = blockNumber + } + return nil, nil + } + bs.blocks[blockNumber] = block + return block, nil +} + +func (bcm *blockConfirmationManager) walkChainForItem(pending *pendingItem, blocks *blockState) (err error) { if pending.blockHash == "" { // This is a transaction that we don't yet have the receipt for @@ -600,7 +653,7 @@ func (bcm *blockConfirmationManager) walkChainForItem(pending *pendingItem) (err log.L(bcm.ctx).Debugf("Waiting for confirmation after block %d event=%s", bcm.highestBlockSeen, pendingKey) return nil } - block, err := bcm.getBlockByNumber(blockNumber, expectedParentHash) + block, err := blocks.getByNumber(blockNumber, expectedParentHash) if err != nil { return err } diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index fe351806..0366a3ef 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -816,7 +816,8 @@ func TestWalkChainForEventBlockNotInConfirmationChain(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - err := bcm.walkChainForItem(pending) + blocks := bcm.newBlockState() + err := bcm.walkChainForItem(pending, blocks) assert.NoError(t, err) mca.AssertExpectations(t) @@ -843,7 +844,8 @@ func TestWalkChainForEventBlockLookupFail(t *testing.T) { return r.BlockNumber.Uint64() == 1002 })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() - err := bcm.walkChainForItem(pending) + blocks := bcm.newBlockState() + err := bcm.walkChainForItem(pending, blocks) assert.Regexp(t, "pop", err) mca.AssertExpectations(t) @@ -868,9 +870,10 @@ func TestProcessBlockHashesLookupFail(t *testing.T) { func TestProcessNotificationsSwallowsUnknownType(t *testing.T) { bcm, _ := newTestBlockConfirmationManager(t, false) + blocks := bcm.newBlockState() bcm.processNotifications([]*Notification{ {NotificationType: NotificationType(999)}, - }) + }, blocks) } func TestGetBlockNotFound(t *testing.T) { @@ -943,12 +946,41 @@ func TestCheckReceiptNotFound(t *testing.T) { } bcm.pending[pending.getKey()] = pending bcm.staleReceipts[pendingKeyForTX(txHash)] = true - bcm.checkReceipt(pending) + blocks := bcm.newBlockState() + bcm.checkReceipt(pending, blocks) assert.False(t, bcm.staleReceipts[pendingKeyForTX(txHash)]) } +func TestCheckReceiptImmediateConfirm(t *testing.T) { + + bcm, mca := newTestBlockConfirmationManager(t, false) + bcm.requiredConfirmations = 0 + + mca.On("TransactionReceipt", mock.Anything, mock.Anything).Return(&ffcapi.TransactionReceiptResponse{ + BlockHash: fftypes.NewRandB32().String(), + BlockNumber: fftypes.NewFFBigInt(1001), + TransactionIndex: fftypes.NewFFBigInt(0), + Success: true, + }, ffcapi.ErrorReasonNotFound, nil) + + done := make(chan struct{}) + txHash := "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + pending := &pendingItem{ + pType: pendingTypeTransaction, + transactionHash: txHash, + confirmedCallback: func(confirmations []BlockInfo) { + close(done) + }, + } + bcm.pending[pending.getKey()] = pending + blocks := bcm.newBlockState() + go bcm.checkReceipt(pending, blocks) + + <-done +} + func TestCheckReceiptFail(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) @@ -962,7 +994,8 @@ func TestCheckReceiptFail(t *testing.T) { } bcm.pending[pending.getKey()] = pending bcm.staleReceipts[pendingKeyForTX(txHash)] = true - bcm.checkReceipt(pending) + blocks := bcm.newBlockState() + bcm.checkReceipt(pending, blocks) assert.True(t, bcm.staleReceipts[pendingKeyForTX(txHash)]) @@ -988,7 +1021,8 @@ func TestCheckReceiptWalkFail(t *testing.T) { } bcm.pending[pending.getKey()] = pending bcm.staleReceipts[pendingKeyForTX(txHash)] = true - bcm.checkReceipt(pending) + blocks := bcm.newBlockState() + bcm.checkReceipt(pending, blocks) assert.True(t, bcm.staleReceipts[pendingKeyForTX(txHash)]) @@ -1010,3 +1044,41 @@ func TestStaleReceiptCheck(t *testing.T) { assert.True(t, bcm.staleReceipts[pendingKeyForTX(txHash)]) } + +func TestBlockState(t *testing.T) { + + bcm, mca := newTestBlockConfirmationManager(t, false) + + block1002 := &ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(1002), + BlockHash: fftypes.NewRandB32().String(), + }, + } + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1002 + })).Return(block1002, ffcapi.ErrorReason(""), nil).Once() + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1003 + })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Once() + + blocks := bcm.newBlockState() + + block, err := blocks.getByNumber(1002, "") + assert.NoError(t, err) + assert.Equal(t, block1002.BlockHash, block.BlockHash) + + block, err = blocks.getByNumber(1002, "") + assert.NoError(t, err) + assert.Equal(t, block1002.BlockHash, block.BlockHash) // cached + + block, err = blocks.getByNumber(1003, "") + assert.NoError(t, err) + assert.Nil(t, block) + + block, err = blocks.getByNumber(1004, "") + assert.NoError(t, err) + assert.Nil(t, block) // above high water mark + + mca.AssertExpectations(t) +} From a22a34e946cba6d9dc7774a5ff3045476f9289cc Mon Sep 17 00:00:00 2001 From: Nicko Guyer Date: Tue, 2 Aug 2022 14:35:34 -0400 Subject: [PATCH 64/95] Add support for deploying contracts Signed-off-by: Nicko Guyer --- config.md | 12 +++++++ go.mod | 2 +- go.sum | 2 ++ mocks/ffcapimocks/api.go | 30 ++++++++++++++++ pkg/apitypes/base_request.go | 1 + pkg/apitypes/tx_request.go | 6 ++++ pkg/ffcapi/api.go | 10 ++++++ pkg/ffcapi/transaction_prepare.go | 6 +++- pkg/fftm/route__root_command.go | 6 ++++ pkg/fftm/send_tx.go | 60 ++++++++++++++++++++++++++++--- 10 files changed, 129 insertions(+), 6 deletions(-) diff --git a/config.md b/config.md index 37503792..be2a1c54 100644 --- a/config.md +++ b/config.md @@ -30,6 +30,18 @@ nav_order: 2 |shutdownTimeout|The maximum amount of time to wait for any open HTTP requests to finish before shutting down the HTTP server|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` |writeTimeout|The maximum time to wait when writing to a HTTP connection|[`time.Duration`](https://pkg.go.dev/time#Duration)|`15s` +## api.auth + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|type|The auth plugin to use for server side authentication of requests|`string`|`` + +## api.auth.basic + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|passwordfile|The path to a .htpasswd file to use for authenticating requests. Passwords should be hashed with bcrypt.|`string`|`` + ## api.tls |Key|Description|Type|Default Value| diff --git a/go.mod b/go.mod index 26679635..0fb41643 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e - github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51 + github.com/hyperledger/firefly-common v0.1.16 github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.1 diff --git a/go.sum b/go.sum index 17ce40dd..b18ac955 100644 --- a/go.sum +++ b/go.sum @@ -634,6 +634,8 @@ github.com/hyperledger/firefly-common v0.1.13 h1:eNK99U9FV43u1F46MM0mPuXT4Xn++or github.com/hyperledger/firefly-common v0.1.13/go.mod h1:2NqPi5Ud9H6rSlZXkLbotxW7z4EAD89p3/8oNOpm9Gs= github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51 h1:Izw9pK53kPGliZv12hCwJfP7rYILhXPx3hMNsCNv7YQ= github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= +github.com/hyperledger/firefly-common v0.1.16 h1:21xidDEKrJhtGdBSRqHN4PfDi7aYxF0HOFuAa04V1AE= +github.com/hyperledger/firefly-common v0.1.16/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index 5a1ff9c0..cdc9d4c0 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -74,6 +74,36 @@ func (_m *API) BlockInfoByNumber(ctx context.Context, req *ffcapi.BlockInfoByNum return r0, r1, r2 } +// DeployContractPrepare provides a mock function with given fields: ctx, req +func (_m *API) DeployContractPrepare(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.TransactionPrepareResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.ContractDeployPrepareRequest) *ffcapi.TransactionPrepareResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.TransactionPrepareResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.ContractDeployPrepareRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.ContractDeployPrepareRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // EventListenerAdd provides a mock function with given fields: ctx, req func (_m *API) EventListenerAdd(ctx context.Context, req *ffcapi.EventListenerAddRequest) (*ffcapi.EventListenerAddResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) diff --git a/pkg/apitypes/base_request.go b/pkg/apitypes/base_request.go index 86618d98..20c0f5d4 100644 --- a/pkg/apitypes/base_request.go +++ b/pkg/apitypes/base_request.go @@ -47,4 +47,5 @@ type RequestType string const ( RequestTypeSendTransaction RequestType = "SendTransaction" RequestTypeQuery RequestType = "Query" + RequestTypeDeploy RequestType = "DeployContract" ) diff --git a/pkg/apitypes/tx_request.go b/pkg/apitypes/tx_request.go index e1917892..2bd153a7 100644 --- a/pkg/apitypes/tx_request.go +++ b/pkg/apitypes/tx_request.go @@ -25,3 +25,9 @@ type TransactionRequest struct { Headers RequestHeaders `json:"headers"` ffcapi.TransactionInput } + +// ContractDeployRequest is the payload sent to initiate a new transaction +type ContractDeployRequest struct { + Headers RequestHeaders `json:"headers"` + ffcapi.ContractDeployInput +} diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 1b9f3fe6..a9004728 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -53,6 +53,9 @@ type API interface { // TransactionSend combines a previously prepared encoded transaction, with a current gas price, and submits it to the transaction pool of the blockchain for mining TransactionSend(ctx context.Context, req *TransactionSendRequest) (*TransactionSendResponse, ErrorReason, error) + // DeployContractPrepare + DeployContractPrepare(ctx context.Context, req *ContractDeployPrepareRequest) (*TransactionPrepareResponse, ErrorReason, error) + // EventStreamStart starts an event stream with an initial set of listeners (which might be empty), a channel to deliver events, and a context that will close to stop the stream EventStreamStart(ctx context.Context, req *EventStreamStartRequest) (*EventStreamStartResponse, ErrorReason, error) @@ -184,6 +187,13 @@ type TransactionInput struct { Params []*fftypes.JSONAny `json:"params"` } +type ContractDeployInput struct { + TransactionHeaders + ABI *fftypes.JSONAny `json:"abi"` + Bytecode string `json:"bytecode"` + Params []*fftypes.JSONAny `json:"params"` +} + type TransactionHeaders struct { From string `json:"from,omitempty"` To string `json:"to,omitempty"` diff --git a/pkg/ffcapi/transaction_prepare.go b/pkg/ffcapi/transaction_prepare.go index 1dfef35f..0e7c2c01 100644 --- a/pkg/ffcapi/transaction_prepare.go +++ b/pkg/ffcapi/transaction_prepare.go @@ -23,7 +23,7 @@ import ( // TransactionPrepareRequest is used to prepare a set of JSON formatted developer friendly // inputs, into a raw transaction ready for submission to the blockchain. // -// The connector is responsible for encoding the transaction ready for sumission, +// The connector is responsible for encoding the transaction ready for submission, // and returning the hash for the transaction as well as a string serialization of // the pre-signed raw transaction in a format of its own choosing (hex etc.). // The hash is expected to be a function of: @@ -42,6 +42,10 @@ type TransactionPrepareRequest struct { TransactionInput } +type ContractDeployPrepareRequest struct { + ContractDeployInput +} + type TransactionPrepareResponse struct { Gas *fftypes.FFBigInt `json:"gas"` TransactionData string `json:"transactionData"` diff --git a/pkg/fftm/route__root_command.go b/pkg/fftm/route__root_command.go index 0052b2af..61873029 100644 --- a/pkg/fftm/route__root_command.go +++ b/pkg/fftm/route__root_command.go @@ -73,6 +73,12 @@ var postRootCommand = func(m *manager) *ffapi.Route { TransactionInput: tReq.TransactionInput, }) return res.Outputs, err + case apitypes.RequestTypeDeploy: + var tReq apitypes.ContractDeployRequest + if err = baseReq.UnmarshalTo(&tReq); err != nil { + return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) + } + return m.sendManagedContractDeployment(r.Req.Context(), &tReq) default: return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgUnsupportedRequestType, baseReq.Headers.Type) } diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index bb18adef..52140249 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -24,8 +24,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) -func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (*apitypes.ManagedTX, error) { - +func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (mtx *apitypes.ManagedTX, err error) { // First job is to assign the next nonce to this request. // We block any further sends on this nonce until we've got this one successfully into the node, or // fail deterministically in a way that allows us to return it. @@ -50,7 +49,7 @@ func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes. // From this point on, we will guide this transaction through to submission. // We return an "ack" at this point, and dispatch the work of getting the transaction submitted // to the background worker. - mtx := &apitypes.ManagedTX{ + mtx = &apitypes.ManagedTX{ ID: request.Headers.ID, // on input the request ID must be the namespaced operation ID SequenceID: apitypes.UUIDVersion1(), Created: fftypes.Now(), @@ -60,7 +59,60 @@ func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes. Request: request, Status: apitypes.TxStatusPending, } - if err := m.persistence.WriteTransaction(m.ctx, mtx, true); err != nil { + if err = m.persistence.WriteTransaction(m.ctx, mtx, true); err != nil { + return nil, err + } + m.markInflightStale() + + // Ok - we've spent it. The rest of the processing will be triggered off of lockedNonce + // completion adding this transaction to the pool (and/or the change event that comes in from + // FireFly core from the update to the transaction) + lockedNonce.spent = mtx + return mtx, nil +} + +func (m *manager) sendManagedContractDeployment(ctx context.Context, request *apitypes.ContractDeployRequest) (mtx *apitypes.ManagedTX, err error) { + // First job is to assign the next nonce to this request. + // We block any further sends on this nonce until we've got this one successfully into the node, or + // fail deterministically in a way that allows us to return it. + lockedNonce, err := m.assignAndLockNonce(ctx, request.Headers.ID, request.From) + if err != nil { + return nil, err + } + // We will call markSpent() once we reach the point the nonce has been used + defer lockedNonce.complete(ctx) + + // Prepare the transaction, which will mean we have a transaction that should be submittable. + // If we fail at this stage, we don't need to write any state as we are sure we haven't submitted + // anything to the blockchain itself. + prepared, _, err := m.connector.DeployContractPrepare(ctx, &ffcapi.ContractDeployPrepareRequest{ + ContractDeployInput: request.ContractDeployInput, + }) + if err != nil { + return nil, err + } + + // Next we update FireFly core with the pre-submitted record pending record, with the allocated nonce. + // From this point on, we will guide this transaction through to submission. + // We return an "ack" at this point, and dispatch the work of getting the transaction submitted + // to the background worker. + mtx = &apitypes.ManagedTX{ + ID: request.Headers.ID, // on input the request ID must be the namespaced operation ID + SequenceID: apitypes.UUIDVersion1(), + Created: fftypes.Now(), + Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), + Gas: prepared.Gas, + TransactionData: prepared.TransactionData, + Request: &apitypes.TransactionRequest{ + Headers: request.Headers, + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: request.TransactionHeaders, + Params: request.Params, + }, + }, + Status: apitypes.TxStatusPending, + } + if err = m.persistence.WriteTransaction(m.ctx, mtx, true); err != nil { return nil, err } m.markInflightStale() From 8033f679e11088ce73cb425a8b87131028e1f8c4 Mon Sep 17 00:00:00 2001 From: Nicko Guyer Date: Tue, 2 Aug 2022 17:18:13 -0400 Subject: [PATCH 65/95] Update bytecode field to JSONAny Signed-off-by: Nicko Guyer --- go.sum | 4 ---- pkg/ffcapi/api.go | 2 +- pkg/policyengines/simple/simple_policy_engine.go | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/go.sum b/go.sum index b18ac955..cd43b067 100644 --- a/go.sum +++ b/go.sum @@ -630,10 +630,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e h1:QP+Yykyq7C670zb4Fs7s4lAtYmvIll4rP/y00hdOsg4= github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e/go.mod h1:434LxYn4ntyK/E0dY+2dTc55caBy6BdUMYBM2gLndAI= github.com/hyperledger/firefly-common v0.1.2/go.mod h1:ytB+404+Qg00wJKx602Yi/V6Spx9d0g65lZR9y5fzlo= -github.com/hyperledger/firefly-common v0.1.13 h1:eNK99U9FV43u1F46MM0mPuXT4Xn++orghpoTIIPsmwo= -github.com/hyperledger/firefly-common v0.1.13/go.mod h1:2NqPi5Ud9H6rSlZXkLbotxW7z4EAD89p3/8oNOpm9Gs= -github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51 h1:Izw9pK53kPGliZv12hCwJfP7rYILhXPx3hMNsCNv7YQ= -github.com/hyperledger/firefly-common v0.1.16-0.20220801130311-02fc08199d51/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= github.com/hyperledger/firefly-common v0.1.16 h1:21xidDEKrJhtGdBSRqHN4PfDi7aYxF0HOFuAa04V1AE= github.com/hyperledger/firefly-common v0.1.16/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index a9004728..28b9c493 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -190,7 +190,7 @@ type TransactionInput struct { type ContractDeployInput struct { TransactionHeaders ABI *fftypes.JSONAny `json:"abi"` - Bytecode string `json:"bytecode"` + Bytecode *fftypes.JSONAny `json:"bytecode"` Params []*fftypes.JSONAny `json:"params"` } diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index b00635ad..6ed33125 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -189,7 +189,7 @@ func (p *simplePolicyEngine) getGasPrice(ctx context.Context, cAPI ffcapi.API) ( p.gasOracleLastQueryTime = fftypes.Now() return p.gasOracleQueryValue, nil default: - // Disabled - rust a fixed value + // Disabled - just a fixed value return p.fixedGasPrice, nil } } From baadce15e4cef7c713972d97ea2a16dfe46be7cb Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 2 Aug 2022 18:11:07 -0400 Subject: [PATCH 66/95] Restructure TX input for consistency with ethconnect and idempotence Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 1 + go.mod | 3 +- go.sum | 897 ------------------ internal/persistence/leveldb_persistence.go | 28 +- .../persistence/leveldb_persistence_test.go | 105 +- internal/persistence/persistence.go | 2 +- internal/tmmsgs/en_error_messges.go | 37 +- pkg/apitypes/managed_tx.go | 37 +- pkg/fftm/manager_test.go | 16 - pkg/fftm/nonces.go | 6 +- pkg/fftm/nonces_test.go | 47 +- pkg/fftm/policyloop.go | 8 +- pkg/fftm/policyloop_test.go | 12 +- pkg/fftm/route_get_transactions_test.go | 36 +- pkg/fftm/send_tx.go | 95 +- .../simple/simple_policy_engine.go | 4 +- .../simple/simple_policy_engine_test.go | 96 +- 17 files changed, 249 insertions(+), 1181 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e158d17c..cd5f120b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,6 +46,7 @@ "goleveldb", "httpserver", "hyperledger", + "idempotence", "Infof", "IPFS", "jsonmap", diff --git a/go.mod b/go.mod index 0fb41643..053f8f47 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 - github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e github.com/hyperledger/firefly-common v0.1.16 github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.12.0 @@ -35,6 +34,7 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/nxadm/tail v1.4.8 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -52,6 +52,7 @@ require ( golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index cd43b067..2e355b9b 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -24,12 +23,10 @@ cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.88.0/go.mod h1:dnKwfYbP9hQhefiUvpbcAyoGSHUrOxR20JVElLiUvEY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -50,7 +47,6 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/spanner v1.24.0/go.mod h1:EZI0yH1D/PrXK0XH9Ba5LGXTXWeqZv0ClOD/19a0Z58= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -58,144 +54,37 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= -github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= -github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= -github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= -github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= -github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= -github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= -github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= -github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= -github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= -github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/aidarkhanov/nanoid v1.0.8 h1:yxyJkgsEDFXP7+97vc6JevMcjyb03Zw+/9fqhlVXBXA= github.com/aidarkhanov/nanoid v1.0.8/go.mod h1:vadfZHT+m4uDhttg0yY4wW3GKtl2T6i4d2Age+45pYk= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64/go.mod h1:2qMFB56yOP3KzkB3PbYZ4AlUFg3a88F67TIx5lB/WwY= -github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v1.8.0/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0= -github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2/config v1.6.0/go.mod h1:TNtBVmka80lRPk5+S9ZqVfFszOQAGJJ9KbT3EM3CHNU= -github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= -github.com/aws/aws-sdk-go-v2/credentials v1.3.2/go.mod h1:PACKuTJdt6AlXvEq8rFI4eDmoqDFC5DpVKQbWysaDgM= -github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0/go.mod h1:Mj/U8OpDbcVcoctrYwA2bak8k/HFPdcLzI/vaiXMwuM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0/go.mod h1:eHwXu2+uE/T6gpnYWwBwqoeqRf9IXyCcolyOWDRAErQ= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4/go.mod h1:Ex7XQmbFmgFHrjUX6TN3mApKW5Hglyga+F7wZHTtYhA= -github.com/aws/aws-sdk-go-v2/internal/ini v1.2.0/go.mod h1:Q5jATQc+f1MfZp3PDMhn6ry18hGvE0i8yvbXoKbnZaE= -github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.2/go.mod h1:EASdTcM1lGhUe1/p4gkojHwlGJkeoRjjr1sRCzup3Is= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.2/go.mod h1:NXmNI41bdEsJMrD0v9rUvbGCB5GwdBEpKvUvIY3vTFg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.2/go.mod h1:QuL2Ym8BkrLmN4lUofXYq6000/i5jPjosCNK//t6gak= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJNT83O0oDOSF8i4dF3dvGqA6hPYYo6YYkzgRA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0/go.mod h1:6J++A5xpo7QDsIeSqPK4UHqMSyPOCopa+zKtqAMhqVQ= -github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI= -github.com/aws/aws-sdk-go-v2/service/sso v1.3.2/go.mod h1:J21I6kF+d/6XHVk7kp/cx9YVD2TMD2TbLwtRGVcinXo= -github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= -github.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNEdvJ2PP0MgOxcmv9EBJ4xs= -github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= -github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= -github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= -github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= -github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= -github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -205,153 +94,15 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= -github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= -github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= -github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= -github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= -github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= -github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= -github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= -github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= -github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= -github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= -github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= -github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= -github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= -github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= -github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= -github.com/containerd/containerd v1.5.10/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= -github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= -github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= -github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= -github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= -github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= -github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= -github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= -github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= -github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= -github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= -github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= -github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= -github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= -github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= -github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= -github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= -github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= -github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= -github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= -github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= -github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= -github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= -github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= -github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= -github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= -github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= -github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= -github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dhui/dktest v0.3.7/go.mod h1:nYMOkafiA07WchSwKnKFUSbGMb2hMm5DrCGiXYG6gwM= -github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= -github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -360,118 +111,43 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= -github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= -github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/getkin/kin-openapi v0.94.1-0.20220401165309-136a868a30c2/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= github.com/getkin/kin-openapi v0.96.0 h1:VVbcSdQAJzfc5kCLU7z2ezw84czu3rbC6UG1BGGzahY= github.com/getkin/kin-openapi v0.96.0/go.mod h1:w4lRPHiyOdwGbOkLIyk+P0qCwlu7TXPCHD/64nSXzgE= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= -github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= -github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= -github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= -github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= -github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= -github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= -github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= -github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= -github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= -github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= -github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= -github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= -github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= -github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= -github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= -github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= -github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= -github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= -github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= -github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-migrate/migrate/v4 v4.15.1/go.mod h1:/CrBenUbcDqsW29jGTR/XFqCfVi/Y6mHXlooCcSOJMQ= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -485,7 +161,6 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -503,14 +178,11 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -526,10 +198,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v35 v35.2.0/go.mod h1:s0515YVTI+IMrDoy9Y4pHt9ShGpzHvHO8rZ7L7acgvs= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -549,13 +218,9 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -565,48 +230,26 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0 github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= @@ -619,123 +262,34 @@ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e h1:QP+Yykyq7C670zb4Fs7s4lAtYmvIll4rP/y00hdOsg4= -github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e/go.mod h1:434LxYn4ntyK/E0dY+2dTc55caBy6BdUMYBM2gLndAI= -github.com/hyperledger/firefly-common v0.1.2/go.mod h1:ytB+404+Qg00wJKx602Yi/V6Spx9d0g65lZR9y5fzlo= github.com/hyperledger/firefly-common v0.1.16 h1:21xidDEKrJhtGdBSRqHN4PfDi7aYxF0HOFuAa04V1AE= github.com/hyperledger/firefly-common v0.1.16/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= -github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= -github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= -github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= -github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= -github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= -github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= -github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= -github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE= github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/karlseguin/ccache v2.0.3+incompatible/go.mod h1:CM9tNPzT6EdRh14+jiW8mEF9mkNZuuE51qmgGYUB93w= -github.com/karlseguin/expect v1.0.8/go.mod h1:lXdI8iGiQhmzpnnmU/EGA60vqKs8NbRNFnhhrJGoD5g= -github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= -github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -745,305 +299,129 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= -github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= -github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= -github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= -github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= -github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= -github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= -github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= -github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= -github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/qeesung/image2ascii v1.0.1/go.mod h1:kZKhyX0h2g/YXa/zdJR3JnLnJ8avHjZ3LrvEKSYyAyU= -github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= -github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= -github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= -github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= -github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1052,74 +430,23 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= -github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/wayneashleyberry/terminal-dimensions v1.0.0/go.mod h1:PW2XrtV6KmKOPhuf7wbtcmw1/IFnC39mryRET2XbxeE= -github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= -github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -gitlab.com/hfuss/mux-prometheus v0.0.4/go.mod h1:4dALqvZzJisEAII64a6zhtdDEfvs+BjemTynBDWuRK0= -gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= -go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= -go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= -go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -1128,70 +455,33 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1215,35 +505,22 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1260,7 +537,6 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1273,11 +549,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1288,9 +559,7 @@ golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA= golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1314,7 +583,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1323,60 +591,38 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1387,24 +633,13 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1412,34 +647,26 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1464,43 +691,25 @@ 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/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1529,7 +738,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -1542,21 +750,12 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1588,7 +787,6 @@ google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqiv google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= @@ -1597,22 +795,18 @@ google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRR google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= -google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -1621,7 +815,6 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -1641,12 +834,10 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -1658,11 +849,8 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210721163202-f1cecdd8b78a/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210726143408-b02e89920bf0/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= @@ -1673,11 +861,8 @@ google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211013025323-ce878158c4d4/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= @@ -1695,14 +880,10 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -1725,8 +906,6 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc v1.42.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.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= @@ -1746,40 +925,27 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -1790,12 +956,6 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= -gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1803,64 +963,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= -k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= -k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= -k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= -k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= -k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= -k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= -k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= -k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= -k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= -k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= -k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= -k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= -k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= -k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= -modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= -modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo= -modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= -modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= -modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= -modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= -modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= -modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= -modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= -modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= -modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= -modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs= -modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= -modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= -modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index 655c7248..a74f0ef4 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -95,7 +95,7 @@ func txPendingIndexKey(sequenceID *fftypes.UUID) []byte { } func txCreatedIndexKey(tx *apitypes.ManagedTX) []byte { - return []byte(fmt.Sprintf("%s%.19d/%s", txCreatedIndexPrefix, tx.Created.UnixNano(), tx.SequenceID)) + return []byte(fmt.Sprintf("%s%.19d/%s", txCreatedIndexPrefix, tx.Headers.TimeReceived.UnixNano(), tx.SequenceID)) } func txDataKey(k string) []byte { @@ -367,7 +367,7 @@ func (p *leveldbPersistence) listTransactionsByIndex(ctx context.Context, collec func (p *leveldbPersistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { afterStr := "" if after != nil { - afterStr = fmt.Sprintf("%.19d/%s", after.Created.UnixNano(), after.SequenceID) + afterStr = fmt.Sprintf("%.19d/%s", after.Headers.TimeReceived.UnixNano(), after.SequenceID) } return p.listTransactionsByIndex(ctx, txCreatedIndexPrefix, txCreatedIndexEnd, afterStr, limit, dir) } @@ -398,7 +398,7 @@ func (p *leveldbPersistence) GetTransactionByNonce(ctx context.Context, signer s return tx, err } -func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, possiblyNew bool) (err error) { +func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, new bool) (err error) { // We take a write-lock here, because we are writing multiple values (the indexes), and anybody // attempting to read the critical nonce allocation index must know the difference between a partial write // (we crashed before we completed all the writes) and an incomplete write that's in process. @@ -406,16 +406,24 @@ func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes. p.txMux.Lock() defer p.txMux.Unlock() - if tx.Request == nil || - tx.Request.From == "" || + if tx.TransactionHeaders.From == "" || tx.Nonce == nil || tx.SequenceID == nil || - tx.Created == nil || + tx.Headers.TimeReceived == nil || + tx.Headers.RequestID == "" || tx.Status == "" { return i18n.NewError(ctx, tmmsgs.MsgPersistenceTXIncomplete) } - idKey := txDataKey(tx.ID) - if possiblyNew { + idKey := txDataKey(tx.Headers.RequestID) + if new { + // This must be a unique ID, otherwise we return a conflict. + // Note we use the final record we write at the end for the conflict check, and also that we're write locked here + if existing, err := p.getKeyValue(ctx, idKey); err != nil { + return err + } else if existing != nil { + return i18n.NewError(ctx, tmmsgs.MsgDuplicateID, idKey) + } + // We write the index records first - because if we crash, we need to be able to know if the // index records are valid or not. When reading under the read lock, if there is an index key // that does not have a corresponding managed TX available, we will clean up the @@ -426,7 +434,7 @@ func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes. err = p.writeKeyValue(ctx, txPendingIndexKey(tx.SequenceID), idKey) } if err == nil { - err = p.writeKeyValue(ctx, txNonceAllocationKey(tx.Request.From, tx.Nonce), idKey) + err = p.writeKeyValue(ctx, txNonceAllocationKey(tx.TransactionHeaders.From, tx.Nonce), idKey) } } // If we are creating/updating a record that is not pending, we need to ensure there is no pending index associated with it @@ -449,7 +457,7 @@ func (p *leveldbPersistence) DeleteTransaction(ctx context.Context, txID string) txDataKey(txID), txCreatedIndexKey(tx), txPendingIndexKey(tx.SequenceID), - txNonceAllocationKey(tx.Request.From, tx.Nonce), + txNonceAllocationKey(tx.TransactionHeaders.From, tx.Nonce), ) } diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index 7832d4dc..af148c19 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -259,45 +259,51 @@ func TestReadWriteCheckpoints(t *testing.T) { assert.Equal(t, cp2.StreamID, cp.StreamID) } +func newTestTX(signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { + return &apitypes.ManagedTX{ + Headers: apitypes.ManagedTXHeaders{ + RequestID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + TimeReceived: fftypes.Now(), + }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: signer, + }, + SequenceID: apitypes.UUIDVersion1(), + Nonce: fftypes.NewFFBigInt(nonce), + Status: status, + } +} + func TestReadWriteManagedTransactions(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() ctx := context.Background() - textTX := func(signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { - tx := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), - SequenceID: apitypes.UUIDVersion1(), - Nonce: fftypes.NewFFBigInt(nonce), - Created: fftypes.Now(), - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: signer, - }, - }, - }, - Status: status, - } + submitNewTX := func(signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { + tx := newTestTX(signer, nonce, status) err := p.WriteTransaction(ctx, tx, true) assert.NoError(t, err) return tx } - s1t1 := textTX("0xaaaaa", 10001, apitypes.TxStatusSucceeded) - s2t1 := textTX("0xbbbbb", 10001, apitypes.TxStatusFailed) - s1t2 := textTX("0xaaaaa", 10002, apitypes.TxStatusPending) - s1t3 := textTX("0xaaaaa", 10003, apitypes.TxStatusPending) + s1t1 := submitNewTX("0xaaaaa", 10001, apitypes.TxStatusSucceeded) + s2t1 := submitNewTX("0xbbbbb", 10001, apitypes.TxStatusFailed) + s1t2 := submitNewTX("0xaaaaa", 10002, apitypes.TxStatusPending) + s1t3 := submitNewTX("0xaaaaa", 10003, apitypes.TxStatusPending) + + // Check dup + err := p.WriteTransaction(ctx, s1t1, true) + assert.Regexp(t, "FF21065", err) txns, err := p.ListTransactionsByCreateTime(ctx, nil, 0, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, txns, 4) - assert.Equal(t, s1t3.ID, txns[0].ID) - assert.Equal(t, s1t2.ID, txns[1].ID) - assert.Equal(t, s2t1.ID, txns[2].ID) - assert.Equal(t, s1t1.ID, txns[3].ID) + assert.Equal(t, s1t3.Headers.RequestID, txns[0].Headers.RequestID) + assert.Equal(t, s1t2.Headers.RequestID, txns[1].Headers.RequestID) + assert.Equal(t, s2t1.Headers.RequestID, txns[2].Headers.RequestID) + assert.Equal(t, s1t1.Headers.RequestID, txns[3].Headers.RequestID) // Only list pending @@ -305,45 +311,45 @@ func TestReadWriteManagedTransactions(t *testing.T) { assert.NoError(t, err) assert.Len(t, txns, 2) - assert.Equal(t, s1t3.ID, txns[0].ID) - assert.Equal(t, s1t2.ID, txns[1].ID) + assert.Equal(t, s1t3.Headers.RequestID, txns[0].Headers.RequestID) + assert.Equal(t, s1t2.Headers.RequestID, txns[1].Headers.RequestID) // List with time range txns, err = p.ListTransactionsByCreateTime(ctx, s1t2, 0, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, txns, 2) - assert.Equal(t, s2t1.ID, txns[0].ID) - assert.Equal(t, s1t1.ID, txns[1].ID) + assert.Equal(t, s2t1.Headers.RequestID, txns[0].Headers.RequestID) + assert.Equal(t, s1t1.Headers.RequestID, txns[1].Headers.RequestID) // Test delete, and querying by nonce to limit TX returned - err = p.DeleteTransaction(ctx, s1t2.ID) + err = p.DeleteTransaction(ctx, s1t2.Headers.RequestID) assert.NoError(t, err) txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t1.Nonce, 0, SortDirectionAscending) assert.NoError(t, err) assert.Len(t, txns, 1) - assert.Equal(t, s1t3.ID, txns[0].ID) + assert.Equal(t, s1t3.Headers.RequestID, txns[0].Headers.RequestID) // Check we can use after with the deleted nonce, and not skip the one after txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t2.Nonce, 0, SortDirectionAscending) assert.NoError(t, err) assert.Len(t, txns, 1) - assert.Equal(t, s1t3.ID, txns[0].ID) + assert.Equal(t, s1t3.Headers.RequestID, txns[0].Headers.RequestID) // Test get direct - v, err := p.GetTransactionByID(ctx, s1t3.ID) + v, err := p.GetTransactionByID(ctx, s1t3.Headers.RequestID) assert.NoError(t, err) - assert.Equal(t, s1t3.ID, v.ID) + assert.Equal(t, s1t3.Headers.RequestID, v.Headers.RequestID) assert.Equal(t, s1t3.Nonce, v.Nonce) v, err = p.GetTransactionByNonce(ctx, "0xbbbbb", s2t1.Nonce) assert.NoError(t, err) - assert.Equal(t, s2t1.ID, v.ID) + assert.Equal(t, s2t1.Headers.RequestID, v.Headers.RequestID) assert.Equal(t, s2t1.Nonce, v.Nonce) - v, err = p.GetTransactionByID(ctx, s1t2.ID) + v, err = p.GetTransactionByID(ctx, s1t2.Headers.RequestID) assert.NoError(t, err) assert.Nil(t, v) } @@ -388,6 +394,19 @@ func TestDeleteStreamFail(t *testing.T) { } +func TestWriteTXFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + tx := newTestTX("0x1234", 1000, apitypes.TxStatusPending) + + err := p.WriteTransaction(context.Background(), tx, true) + assert.Error(t, err) + +} + func TestWriteCheckpointFailMarshal(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() @@ -449,13 +468,15 @@ func TestListManagedTransactionFail(t *testing.T) { defer done() tx := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), - Created: fftypes.Now(), + Headers: apitypes.ManagedTXHeaders{ + RequestID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + TimeReceived: fftypes.Now(), + }, SequenceID: apitypes.UUIDVersion1(), } - err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) + err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.Headers.RequestID)) assert.NoError(t, err) - err = p.db.Put(txDataKey(tx.ID), []byte("{! not json"), &opt.WriteOptions{}) + err = p.db.Put(txDataKey(tx.Headers.RequestID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) _, err = p.ListTransactionsByCreateTime(context.Background(), nil, 0, SortDirectionDescending) @@ -468,11 +489,13 @@ func TestListManagedTransactionCleanupOrphans(t *testing.T) { defer done() tx := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), - Created: fftypes.Now(), + Headers: apitypes.ManagedTXHeaders{ + RequestID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + TimeReceived: fftypes.Now(), + }, SequenceID: apitypes.UUIDVersion1(), } - err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) + err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.Headers.RequestID)) assert.NoError(t, err) txns, err := p.ListTransactionsByCreateTime(context.Background(), nil, 0, SortDirectionDescending) diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go index 6f1273c1..6eb34819 100644 --- a/internal/persistence/persistence.go +++ b/internal/persistence/persistence.go @@ -44,7 +44,7 @@ type Persistence interface { ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) // reverse UUIDv1 order, only those in pending state GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (*apitypes.ManagedTX, error) - WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, possiblyNew bool) error + WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, new bool) error // must reject if new is true, and the request ID is no DeleteTransaction(ctx context.Context, txID string) error Close(ctx context.Context) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 88504e24..0c72e997 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -17,6 +17,8 @@ package tmmsgs import ( + "net/http" + "github.com/hyperledger/firefly-common/pkg/i18n" "golang.org/x/text/language" ) @@ -36,37 +38,37 @@ var ( MsgPolicyEngineNotRegistered = ffe("FF21019", "No policy engine registered with name '%s'") MsgNoGasConfigSetForPolicyEngine = ffe("FF21020", "A fixed gas price must be set when not using a gas oracle") MsgErrorQueryingGasOracleAPI = ffe("FF21021", "Error from gas station API [%d]: %s") - MsgInvalidRequestErr = ffe("FF21022", "Invalid '%s' request: %s", 400) - MsgUnsupportedRequestType = ffe("FF21023", "Unsupported request type: %s", 400) + MsgInvalidRequestErr = ffe("FF21022", "Invalid '%s' request: %s", http.StatusBadRequest) + MsgUnsupportedRequestType = ffe("FF21023", "Unsupported request type: %s", http.StatusBadRequest) MsgMissingGOTemplate = ffe("FF21024", "Missing template for processing response from Gas Oracle REST API") MsgBadGOTemplate = ffe("FF21025", "Invalid Go template: %s") MsgGasOracleResultError = ffe("FF21026", "Error processing result from gas station API via template") - MsgStreamStateError = ffe("FF21027", "Event stream is in %s state", 409) - MsgMissingName = ffe("FF21028", "Name is required", 400) - MsgInvalidStreamType = ffe("FF21029", "Invalid event stream type '%s'", 400) - MsgMissingWebhookURL = ffe("FF21030", "'url' is required for webhook configuration", 400) + MsgStreamStateError = ffe("FF21027", "Event stream is in %s state", http.StatusConflict) + MsgMissingName = ffe("FF21028", "Name is required", http.StatusBadRequest) + MsgInvalidStreamType = ffe("FF21029", "Invalid event stream type '%s'", http.StatusBadRequest) + MsgMissingWebhookURL = ffe("FF21030", "'url' is required for webhook configuration", http.StatusBadRequest) MsgStopFailedUpdatingESConfig = ffe("FF21031", "Failed to stop event stream to apply updated configuration: %s") MsgStartFailedUpdatingESConfig = ffe("FF21032", "Failed to restart event stream while applying updated configuration: %s") MsgBlockWebhookAddress = ffe("FF21033", "Cannot send Webhook POST to address '%s' for host '%s'") - MsgInvalidDistributionMode = ffe("FF21034", "Invalid distribution mode for WebSocket: %s", 400) + MsgInvalidDistributionMode = ffe("FF21034", "Invalid distribution mode for WebSocket: %s", http.StatusBadRequest) MsgWebhookFailedStatus = ffe("FF21035", "Webhook request failed with status %d") MsgWSErrorFromClient = ffe("FF21036", "Error received from WebSocket client: %s") MsgWebSocketClosed = ffe("FF21037", "WebSocket '%s' closed") MsgWebSocketInterruptedSend = ffe("FF21038", "Interrupted waiting for WebSocket connection to send event") MsgWebSocketInterruptedReceive = ffe("FF21039", "Interrupted waiting for WebSocket acknowledgment") - MsgBadListenerOptions = ffe("FF21040", "Invalid listener options: %s", 400) + MsgBadListenerOptions = ffe("FF21040", "Invalid listener options: %s", http.StatusBadRequest) MsgInvalidHost = ffe("FF21041", "Cannot send Webhook POST to host '%s': %s") MsgWebhookErr = ffe("FF21042", "Webhook request failed: %s") MsgUnknownPersistence = ffe("FF21043", "Unknown persistence type '%s'") MsgInvalidLimit = ffe("FF21044", "Invalid limit string '%s': %s") - MsgStreamNotFound = ffe("FF21045", "Event stream '%v' not found", 404) - MsgListenerNotFound = ffe("FF21046", "Event listener '%v' not found", 404) - MsgDuplicateStreamName = ffe("FF21047", "Duplicate event stream name '%s' used by stream '%s'", 409) - MsgMissingID = ffe("FF21048", "ID is required", 400) + MsgStreamNotFound = ffe("FF21045", "Event stream '%v' not found", http.StatusNotFound) + MsgListenerNotFound = ffe("FF21046", "Event listener '%v' not found", http.StatusNotFound) + MsgDuplicateStreamName = ffe("FF21047", "Duplicate event stream name '%s' used by stream '%s'", http.StatusConflict) + MsgMissingID = ffe("FF21048", "ID is required", http.StatusBadRequest) MsgPersistenceInitFail = ffe("FF21049", "Failed to initialize '%s' persistence: %s") MsgLevelDBPathMissing = ffe("FF21050", "Path must be supplied for LevelDB persistence") MsgFilterUpdateNotAllowed = ffe("FF21051", "Event filters cannot be updated after a listener is created. Previous signature: '%s'. New signature: '%s'") - MsgResetStreamNotFound = ffe("FF21052", "Attempt to reset listener '%s', which is not currently registered on stream '%s'", 404) + MsgResetStreamNotFound = ffe("FF21052", "Attempt to reset listener '%s', which is not currently registered on stream '%s'", http.StatusNotFound) MsgPersistenceMarshalFailed = ffe("FF21053", "JSON serialization failed while writing to persistence") MsgPersistenceUnmarshalFailed = ffe("FF21054", "JSON parsing failed while reading from persistence") MsgPersistenceReadFailed = ffe("FF21055", "Failed to read key '%s' from persistence") @@ -74,8 +76,9 @@ var ( MsgPersistenceDeleteFailed = ffe("FF21057", "Failed to delete key '%s' from persistence") MsgPersistenceInitFailed = ffe("FF21058", "Failed to initialize persistence at path '%s'") MsgPersistenceTXIncomplete = ffe("FF21059", "Transaction is missing indexed fields") - MsgNotStarted = ffe("FF21060", "Connector has not fully started yet", 503) - MsgPaginationErrTxNotFound = ffe("FF21062", "The ID specified in the 'after' option (for pagination) must match an existing transaction: '%s'", 404) - MsgTXConflictSignerPending = ffe("FF21063", "Only one of 'signer' and 'pending' can be supplied when querying transactions", 400) - MsgInvalidSortDirection = ffe("FF21064", "Sort direction must be 'asc'/'ascending' or 'desc'/'descending': '%s'", 400) + MsgNotStarted = ffe("FF21060", "Connector has not fully started yet", http.StatusServiceUnavailable) + MsgPaginationErrTxNotFound = ffe("FF21062", "The ID specified in the 'after' option (for pagination) must match an existing transaction: '%s'", http.StatusNotFound) + MsgTXConflictSignerPending = ffe("FF21063", "Only one of 'signer' and 'pending' can be supplied when querying transactions", http.StatusBadRequest) + MsgInvalidSortDirection = ffe("FF21064", "Sort direction must be 'asc'/'ascending' or 'desc'/'descending': '%s'", http.StatusBadRequest) + MsgDuplicateID = ffe("FF21065", "ID '%s' is not unique", http.StatusConflict) ) diff --git a/pkg/apitypes/managed_tx.go b/pkg/apitypes/managed_tx.go index 1baccc3b..662c3069 100644 --- a/pkg/apitypes/managed_tx.go +++ b/pkg/apitypes/managed_tx.go @@ -40,6 +40,12 @@ type ManagedTXError struct { Mapped ffcapi.ErrorReason `json:"mapped,omitempty"` } +type ManagedTXHeaders struct { + RequestID string `json:"requestId"` + TimeReceived *fftypes.FFTime `json:"timeReceived"` + LastUpdate *fftypes.FFTime `json:"lastUpdate"` +} + // ManagedTX is the structure stored for each new transaction request, using the external ID of the operation // // Indexing: @@ -57,20 +63,19 @@ type ManagedTXError struct { // - When listing back entries, the persistence layer will automatically clean up indexes if the underlying // TX they refer to is not available. For this reason the index records are written first. type ManagedTX struct { - ID string `json:"id"` - Status TxStatus `json:"status"` - SequenceID *fftypes.UUID `json:"sequenceId"` - Nonce *fftypes.FFBigInt `json:"nonce"` - Gas *fftypes.FFBigInt `json:"gas"` - TransactionHash string `json:"transactionHash,omitempty"` - TransactionData string `json:"transactionData,omitempty"` - GasPrice *fftypes.JSONAny `json:"gasPrice"` - PolicyInfo *fftypes.JSONAny `json:"policyInfo"` - Created *fftypes.FFTime `json:"created"` - FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` - LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` - Request *TransactionRequest `json:"request,omitempty"` - Receipt *ffcapi.TransactionReceiptResponse `json:"receipt,omitempty"` - ErrorHistory []*ManagedTXError `json:"errorHistory"` - Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` + Headers ManagedTXHeaders `json:"headers"` + Status TxStatus `json:"status"` + SequenceID *fftypes.UUID `json:"sequenceId"` + Nonce *fftypes.FFBigInt `json:"nonce"` + Gas *fftypes.FFBigInt `json:"gas"` + TransactionHeaders ffcapi.TransactionHeaders `json:"transactionHeaders"` + TransactionData string `json:"transactionData"` + TransactionHash string `json:"transactionHash,omitempty"` + GasPrice *fftypes.JSONAny `json:"gasPrice"` + PolicyInfo *fftypes.JSONAny `json:"policyInfo"` + FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` + LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` + Receipt *ffcapi.TransactionReceiptResponse `json:"receipt,omitempty"` + ErrorHistory []*ManagedTXError `json:"errorHistory"` + Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` } diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 05b63229..df543240 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -18,7 +18,6 @@ package fftm import ( "context" - "encoding/json" "fmt" "io/ioutil" "net" @@ -27,7 +26,6 @@ import ( "testing" "github.com/hyperledger/firefly-common/pkg/config" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/httpserver" "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" @@ -38,7 +36,6 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines/simple" - "github.com/hyperledger/firefly/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -90,19 +87,6 @@ func newMockPersistenceManager(t *testing.T) (*persistencemocks.Persistence, *ff return mps, mca, m } -func newTestOperation(t *testing.T, mtx *apitypes.ManagedTX, status core.OpStatus) *core.Operation { - b, err := json.Marshal(&mtx) - assert.NoError(t, err) - op := &core.Operation{ - Namespace: strings.Split(mtx.ID, ":")[0], - ID: fftypes.MustParseUUID(strings.Split(mtx.ID, ":")[1]), - Status: status, - } - err = json.Unmarshal(b, &op.Output) - assert.NoError(t, err) - return op -} - func TestNewManagerBadHttpConfig(t *testing.T) { tmconfig.Reset() diff --git a/pkg/fftm/nonces.go b/pkg/fftm/nonces.go index 2f379117..f73d3e64 100644 --- a/pkg/fftm/nonces.go +++ b/pkg/fftm/nonces.go @@ -98,9 +98,9 @@ func (m *manager) calcNextNonce(ctx context.Context, signer string) (uint64, err } if len(txns) > 0 { lastTxn = txns[0] - if time.Since(*lastTxn.Created.Time()) < m.nonceStateTimeout { + if time.Since(*lastTxn.Headers.TimeReceived.Time()) < m.nonceStateTimeout { nextNonce := lastTxn.Nonce.Uint64() + 1 - log.L(ctx).Debugf("Allocating next nonce '%s' / '%d' after TX '%s' (status=%s)", signer, nextNonce, lastTxn.ID, lastTxn.Status) + log.L(ctx).Debugf("Allocating next nonce '%s' / '%d' after TX '%s' (status=%s)", signer, nextNonce, lastTxn.Headers.RequestID, lastTxn.Status) return nextNonce, nil } } @@ -118,7 +118,7 @@ func (m *manager) calcNextNonce(ctx context.Context, signer string) (uint64, err // This is important in case we have transactions that have expired from the TX pool of nodes, but we still have them // in our state store. So basically whichever is further forwards of our state store and the node answer wins. if lastTxn != nil && nextNonce <= lastTxn.Nonce.Uint64() { - log.L(ctx).Debugf("Node TX pool next nonce '%s' / '%d' is not ahead of '%d' in TX '%s' (status=%s)", signer, nextNonce, lastTxn.Nonce.Uint64(), lastTxn.ID, lastTxn.Status) + log.L(ctx).Debugf("Node TX pool next nonce '%s' / '%d' is not ahead of '%d' in TX '%s' (status=%s)", signer, nextNonce, lastTxn.Nonce.Uint64(), lastTxn.Headers.RequestID, lastTxn.Status) nextNonce = lastTxn.Nonce.Uint64() + 1 } diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go index 7a0eac6c..b9b3b58f 100644 --- a/pkg/fftm/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -39,17 +39,15 @@ func TestNonceStaleStateContention(t *testing.T) { // Write a stale record to persistence oldTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) err := m.persistence.WriteTransaction(m.ctx, &apitypes.ManagedTX{ - ID: "stale1", + Headers: apitypes.ManagedTXHeaders{ + RequestID: "stale1", + TimeReceived: &oldTime, + }, Status: apitypes.TxStatusSucceeded, SequenceID: apitypes.UUIDVersion1(), Nonce: fftypes.NewFFBigInt(1000), // old nonce - Created: &oldTime, // old record - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x12345", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", }, }, true) assert.NoError(t, err) @@ -76,17 +74,15 @@ func TestNonceStaleStateContention(t *testing.T) { time.Sleep(1 * time.Millisecond) ln.spent = &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), + Headers: apitypes.ManagedTXHeaders{ + RequestID: "ns1:" + fftypes.NewUUID().String(), + TimeReceived: &oldTime, + }, Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), Status: apitypes.TxStatusPending, SequenceID: apitypes.UUIDVersion1(), - Created: &oldTime, // old record - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x12345", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", }, } err = m.persistence.WriteTransaction(m.ctx, ln.spent, true) @@ -117,11 +113,17 @@ func TestNonceListError(t *testing.T) { _, m, cancel := newTestManager(t) defer cancel() + mFFC := m.connector.(*ffcapimocks.API) + mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{ + TransactionData: "RAW_UNSIGNED_BYTES", + Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation + }, ffcapi.ErrorReason(""), nil) + mp := &persistencemocks.Persistence{} m.persistence = mp mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(nil, fmt.Errorf("pop")) - mp.On("Close", mock.Anything).Return(nil) + mp.On("Close", mock.Anything).Return(nil).Maybe() _, err := m.sendManagedTransaction(context.Background(), &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ @@ -132,6 +134,9 @@ func TestNonceListError(t *testing.T) { }) assert.Regexp(t, "pop", err) + mp.AssertExpectations(t) + mFFC.AssertExpectations(t) + } func TestNonceListStaleThenQueryFail(t *testing.T) { @@ -144,11 +149,15 @@ func TestNonceListStaleThenQueryFail(t *testing.T) { old := fftypes.FFTime(time.Now().Add(-10000 * time.Hour)) mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return([]*apitypes.ManagedTX{ - {ID: "id12345", Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000), Created: &old}, + {Headers: apitypes.ManagedTXHeaders{RequestID: "id12345", TimeReceived: &old}, Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, }, nil) mp.On("Close", mock.Anything).Return(nil).Maybe() mFFC := m.connector.(*ffcapimocks.API) + mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{ + TransactionData: "RAW_UNSIGNED_BYTES", + Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation + }, ffcapi.ErrorReason(""), nil) mFFC.On("NextNonceForSigner", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) _, err := m.sendManagedTransaction(context.Background(), &apitypes.TransactionRequest{ @@ -175,7 +184,7 @@ func TestNonceListNotStale(t *testing.T) { m.persistence = mp mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return([]*apitypes.ManagedTX{ - {ID: "id12345", Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000), Created: fftypes.Now()}, + {Headers: apitypes.ManagedTXHeaders{RequestID: "id12345", TimeReceived: fftypes.Now()}, Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, }, nil) mp.On("Close", mock.Anything).Return(nil).Maybe() diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index 738ac1fe..71869f34 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -100,7 +100,7 @@ func (m *manager) policyLoopCycle(inflightStale bool) { for _, pending := range m.inflight { err := m.execPolicy(pending) if err != nil { - log.L(m.ctx).Errorf("Failed policy cycle transaction=%s operation=%s: %s", pending.mtx.TransactionHash, pending.mtx.ID, err) + log.L(m.ctx).Errorf("Failed policy cycle transaction=%s operation=%s: %s", pending.mtx.TransactionHash, pending.mtx.Headers.RequestID, err) } } @@ -144,7 +144,7 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { var reason ffcapi.ErrorReason updated, reason, err = m.policyEngine.Execute(m.ctx, m.connector, pending.mtx) if err != nil { - log.L(m.ctx).Errorf("Policy engine returned error for operation %s reason=%s: %s", mtx.ID, reason, err) + log.L(m.ctx).Errorf("Policy engine returned error for operation %s reason=%s: %s", mtx.Headers.RequestID, reason, err) m.addError(mtx, reason, err) } else if mtx.FirstSubmit != nil && pending.trackingTransactionHash != mtx.TransactionHash { // If now submitted, add to confirmations manager for receipt checking @@ -155,7 +155,7 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { if updated || err != nil { err := m.persistence.WriteTransaction(m.ctx, mtx, false) if err != nil { - log.L(m.ctx).Errorf("Failed to update operation %s (status=%s): %s", mtx.ID, mtx.Status, err) + log.L(m.ctx).Errorf("Failed to update operation %s (status=%s): %s", mtx.Headers.RequestID, mtx.Status, err) return err } if completed { @@ -240,7 +240,7 @@ func wsTransactionReceipt(pending *pendingState) *WsTransactionReceipt { return &WsTransactionReceipt{ Headers: &WsTransactionReceiptHeaders{ - RequestID: pending.mtx.ID, + RequestID: pending.mtx.Headers.RequestID, ReplyType: replyType, }, TransactionHash: pending.mtx.TransactionHash, diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index 09bd688f..343bb41a 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -90,7 +90,7 @@ func TestPolicyLoopE2EOk(t *testing.T) { // Run the policy once to do the send <-m.inflightStale // from sending the TX m.policyLoopCycle(true) - assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, mtx.Headers.RequestID, m.inflight[0].mtx.Headers.RequestID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) // A second time will mark it complete for flush @@ -101,7 +101,7 @@ func TestPolicyLoopE2EOk(t *testing.T) { assert.Empty(t, m.inflight) // Check the update is persisted - rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.ID) + rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.Headers.RequestID) assert.NoError(t, err) assert.Equal(t, apitypes.TxStatusSucceeded, rtx.Status) @@ -141,7 +141,7 @@ func TestPolicyLoopE2EReverted(t *testing.T) { // Run the policy once to do the send <-m.inflightStale // from sending the TX m.policyLoopCycle(true) - assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, mtx.Headers.RequestID, m.inflight[0].mtx.Headers.RequestID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) // A second time will mark it complete for flush @@ -152,7 +152,7 @@ func TestPolicyLoopE2EReverted(t *testing.T) { assert.Empty(t, m.inflight) // Check the update is persisted - rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.ID) + rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.Headers.RequestID) assert.NoError(t, err) assert.Equal(t, apitypes.TxStatusFailed, rtx.Status) @@ -212,13 +212,13 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { // Run the policy once to do the send with the first hash <-m.inflightStale // from sending the TX m.policyLoopCycle(true) - assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, mtx.Headers.RequestID, m.inflight[0].mtx.Headers.RequestID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) // Reset the transaction so the policy manager resubmits it m.inflight[0].mtx.FirstSubmit = nil m.policyLoopCycle(false) - assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, mtx.Headers.RequestID, m.inflight[0].mtx.Headers.RequestID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) mc.AssertExpectations(t) diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go index 8b446718..118a6571 100644 --- a/pkg/fftm/route_get_transactions_test.go +++ b/pkg/fftm/route_get_transactions_test.go @@ -30,18 +30,16 @@ import ( func newTestTxn(t *testing.T, m *manager, signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { tx := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + Headers: apitypes.ManagedTXHeaders{ + RequestID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + TimeReceived: fftypes.Now(), + }, SequenceID: apitypes.UUIDVersion1(), Nonce: fftypes.NewFFBigInt(nonce), - Created: fftypes.Now(), - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: signer, - }, - }, + Status: status, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: signer, }, - Status: status, } err := m.persistence.WriteTransaction(context.Background(), tx, true) assert.NoError(t, err) @@ -70,36 +68,36 @@ func TestGetTransactions(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, transactions, 4) - assert.Equal(t, s1t3.ID, transactions[0].ID) - assert.Equal(t, s1t2.ID, transactions[1].ID) - assert.Equal(t, s2t1.ID, transactions[2].ID) - assert.Equal(t, s1t1.ID, transactions[3].ID) + assert.Equal(t, s1t3.Headers.RequestID, transactions[0].Headers.RequestID) + assert.Equal(t, s1t2.Headers.RequestID, transactions[1].Headers.RequestID) + assert.Equal(t, s2t1.Headers.RequestID, transactions[2].Headers.RequestID) + assert.Equal(t, s1t1.Headers.RequestID, transactions[3].Headers.RequestID) // Test pagination on default sort/filter res, err = resty.New().R(). SetResult(&transactions). - Get(url + "/transactions?limit=1&after=" + s1t2.ID) + Get(url + "/transactions?limit=1&after=" + s1t2.Headers.RequestID) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, transactions, 1) - assert.Equal(t, s2t1.ID, transactions[0].ID) + assert.Equal(t, s2t1.Headers.RequestID, transactions[0].Headers.RequestID) // Test pagination on nonce filter res, err = resty.New().R(). SetResult(&transactions). - Get(url + "/transactions?signer=0xaaaaa&after=" + s1t2.ID) + Get(url + "/transactions?signer=0xaaaaa&after=" + s1t2.Headers.RequestID) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, transactions, 1) - assert.Equal(t, s1t1.ID, transactions[0].ID) + assert.Equal(t, s1t1.Headers.RequestID, transactions[0].Headers.RequestID) // Test pagination on pending filter res, err = resty.New().R(). SetResult(&transactions). - Get(url + "/transactions?pending&after=" + s1t3.ID) + Get(url + "/transactions?pending&after=" + s1t3.Headers.RequestID) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, transactions, 1) - assert.Equal(t, s2t1.ID, transactions[0].ID) + assert.Equal(t, s2t1.Headers.RequestID, transactions[0].Headers.RequestID) } diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index 52140249..71c746da 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -24,16 +24,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) -func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (mtx *apitypes.ManagedTX, err error) { - // First job is to assign the next nonce to this request. - // We block any further sends on this nonce until we've got this one successfully into the node, or - // fail deterministically in a way that allows us to return it. - lockedNonce, err := m.assignAndLockNonce(ctx, request.Headers.ID, request.From) - if err != nil { - return nil, err - } - // We will call markSpent() once we reach the point the nonce has been used - defer lockedNonce.complete(ctx) +func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (*apitypes.ManagedTX, error) { // Prepare the transaction, which will mean we have a transaction that should be submittable. // If we fail at this stage, we don't need to write any state as we are sure we haven't submitted @@ -45,73 +36,63 @@ func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes. return nil, err } - // Next we update FireFly core with the pre-submitted record pending record, with the allocated nonce. - // From this point on, we will guide this transaction through to submission. - // We return an "ack" at this point, and dispatch the work of getting the transaction submitted - // to the background worker. - mtx = &apitypes.ManagedTX{ - ID: request.Headers.ID, // on input the request ID must be the namespaced operation ID - SequenceID: apitypes.UUIDVersion1(), - Created: fftypes.Now(), - Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), - Gas: prepared.Gas, - TransactionData: prepared.TransactionData, - Request: request, - Status: apitypes.TxStatusPending, - } - if err = m.persistence.WriteTransaction(m.ctx, mtx, true); err != nil { + return m.submitPreparedTX(ctx, request.Headers.ID, &request.TransactionHeaders, prepared.Gas, prepared.TransactionData) +} + +func (m *manager) sendManagedContractDeployment(ctx context.Context, request *apitypes.ContractDeployRequest) (*apitypes.ManagedTX, error) { + + // Prepare the transaction, which will mean we have a transaction that should be submittable. + // If we fail at this stage, we don't need to write any state as we are sure we haven't submitted + // anything to the blockchain itself. + prepared, _, err := m.connector.DeployContractPrepare(ctx, &ffcapi.ContractDeployPrepareRequest{ + ContractDeployInput: request.ContractDeployInput, + }) + if err != nil { return nil, err } - m.markInflightStale() - // Ok - we've spent it. The rest of the processing will be triggered off of lockedNonce - // completion adding this transaction to the pool (and/or the change event that comes in from - // FireFly core from the update to the transaction) - lockedNonce.spent = mtx - return mtx, nil + return m.submitPreparedTX(ctx, request.Headers.ID, &request.TransactionHeaders, prepared.Gas, prepared.TransactionData) } -func (m *manager) sendManagedContractDeployment(ctx context.Context, request *apitypes.ContractDeployRequest) (mtx *apitypes.ManagedTX, err error) { +func (m *manager) submitPreparedTX(ctx context.Context, reqID string, txHeaders *ffcapi.TransactionHeaders, gas *fftypes.FFBigInt, transactionData string) (*apitypes.ManagedTX, error) { + + // Sequencing ID is always generated by us - so we have a deterministic order of transactions + seqID := apitypes.UUIDVersion1() + + // The request ID is the primary ID, and should be supplied by the user for idempotence + if reqID == "" { + reqID = seqID.String() + } + // First job is to assign the next nonce to this request. // We block any further sends on this nonce until we've got this one successfully into the node, or // fail deterministically in a way that allows us to return it. - lockedNonce, err := m.assignAndLockNonce(ctx, request.Headers.ID, request.From) + lockedNonce, err := m.assignAndLockNonce(ctx, reqID, txHeaders.From) if err != nil { return nil, err } // We will call markSpent() once we reach the point the nonce has been used defer lockedNonce.complete(ctx) - // Prepare the transaction, which will mean we have a transaction that should be submittable. - // If we fail at this stage, we don't need to write any state as we are sure we haven't submitted - // anything to the blockchain itself. - prepared, _, err := m.connector.DeployContractPrepare(ctx, &ffcapi.ContractDeployPrepareRequest{ - ContractDeployInput: request.ContractDeployInput, - }) - if err != nil { - return nil, err - } - // Next we update FireFly core with the pre-submitted record pending record, with the allocated nonce. // From this point on, we will guide this transaction through to submission. // We return an "ack" at this point, and dispatch the work of getting the transaction submitted // to the background worker. - mtx = &apitypes.ManagedTX{ - ID: request.Headers.ID, // on input the request ID must be the namespaced operation ID - SequenceID: apitypes.UUIDVersion1(), - Created: fftypes.Now(), - Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), - Gas: prepared.Gas, - TransactionData: prepared.TransactionData, - Request: &apitypes.TransactionRequest{ - Headers: request.Headers, - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: request.TransactionHeaders, - Params: request.Params, - }, + now := fftypes.Now() + mtx := &apitypes.ManagedTX{ + Headers: apitypes.ManagedTXHeaders{ + RequestID: reqID, // on input the request ID must be the namespaced operation ID + TimeReceived: now, + LastUpdate: now, }, - Status: apitypes.TxStatusPending, + SequenceID: seqID, + Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), + Gas: gas, + TransactionHeaders: *txHeaders, + TransactionData: transactionData, + Status: apitypes.TxStatusPending, } + if err = m.persistence.WriteTransaction(m.ctx, mtx, true); err != nil { return nil, err } diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index 6ed33125..673370bd 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -120,7 +120,7 @@ func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx * return false, "", err } sendTX := &ffcapi.TransactionSendRequest{ - TransactionHeaders: mtx.Request.TransactionHeaders, + TransactionHeaders: mtx.TransactionHeaders, GasPrice: mtx.GasPrice, TransactionData: mtx.TransactionData, } @@ -151,7 +151,7 @@ func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx * now := fftypes.Now() if now.Time().Sub(*lastWarnTime.Time()) > p.warnInterval { secsSinceSubmit := float64(now.Time().Sub(*mtx.FirstSubmit.Time())) / float64(time.Second) - log.L(ctx).Warnf("Transaction %s (op=%s) has not been mined after %.2fs", mtx.TransactionHash, mtx.ID, secsSinceSubmit) + log.L(ctx).Warnf("Transaction %s (op=%s) has not been mined after %.2fs", mtx.TransactionHash, mtx.Headers.RequestID, secsSinceSubmit) info.LastWarnTime = now return true, "", nil } diff --git a/pkg/policyengines/simple/simple_policy_engine_test.go b/pkg/policyengines/simple/simple_policy_engine_test.go index 351e0bf3..a4414f23 100644 --- a/pkg/policyengines/simple/simple_policy_engine_test.go +++ b/pkg/policyengines/simple/simple_policy_engine_test.go @@ -62,12 +62,8 @@ func TestFixedGasPriceOK(t *testing.T) { assert.NoError(t, err) mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionHash: "0x12345", TransactionData: "SOME_RAW_TX_BYTES", @@ -130,12 +126,8 @@ func TestGasOracleSendOK(t *testing.T) { assert.NoError(t, err) mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionHash: "0x12345", TransactionData: "SOME_RAW_TX_BYTES", @@ -177,12 +169,8 @@ func TestConnectorGasOracleSendOK(t *testing.T) { assert.NoError(t, err) mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionHash: "0x12345", TransactionData: "SOME_RAW_TX_BYTES", @@ -224,12 +212,8 @@ func TestConnectorGasOracleFail(t *testing.T) { assert.NoError(t, err) mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionHash: "0x12345", TransactionData: "SOME_RAW_TX_BYTES", @@ -265,12 +249,8 @@ func TestGasOracleSendFail(t *testing.T) { assert.NoError(t, err) mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", } @@ -325,12 +305,8 @@ func TestGasOracleTemplateExecuteFail(t *testing.T) { assert.NoError(t, err) mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", } @@ -355,12 +331,8 @@ func TestGasOracleNonJSON(t *testing.T) { assert.NoError(t, err) mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", } @@ -388,12 +360,8 @@ func TestTXSendFail(t *testing.T) { assert.NoError(t, err) mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", } @@ -417,12 +385,8 @@ func TestWarnStaleWarningCannotParse(t *testing.T) { TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: &submitTime, PolicyInfo: fftypes.JSONAnyPtr("!not json!"), - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, } @@ -446,12 +410,8 @@ func TestWarnStaleAdditionalWarning(t *testing.T) { submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.FFTime(time.Now().Add(-50 * time.Hour)) mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: &submitTime, @@ -480,12 +440,8 @@ func TestWarnStaleNoWarning(t *testing.T) { submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.Now() mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: &submitTime, @@ -512,12 +468,8 @@ func TestNoOpWithReceipt(t *testing.T) { submitTime := fftypes.Now() mtx := &apitypes.ManagedTX{ - Request: &apitypes.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: submitTime, From ca9c4211f1e3d1ec8fd13752e5f87ab257c6e97d Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 2 Aug 2022 22:53:42 -0400 Subject: [PATCH 67/95] Emit replies and improve consistency Signed-off-by: Peter Broadhurst --- internal/persistence/leveldb_persistence.go | 10 +-- .../persistence/leveldb_persistence_test.go | 58 +++++++-------- internal/tmmsgs/en_error_messges.go | 1 + pkg/apitypes/managed_tx.go | 32 +++++++-- pkg/fftm/nonces.go | 6 +- pkg/fftm/nonces_test.go | 28 ++++---- pkg/fftm/policyloop.go | 71 ++++++++----------- pkg/fftm/policyloop_test.go | 47 ++++++++++-- pkg/fftm/route_get_transactions_test.go | 32 ++++----- pkg/fftm/send_tx.go | 16 ++--- .../simple/simple_policy_engine.go | 2 +- 11 files changed, 166 insertions(+), 137 deletions(-) diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index a74f0ef4..d7dfe57f 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -95,7 +95,7 @@ func txPendingIndexKey(sequenceID *fftypes.UUID) []byte { } func txCreatedIndexKey(tx *apitypes.ManagedTX) []byte { - return []byte(fmt.Sprintf("%s%.19d/%s", txCreatedIndexPrefix, tx.Headers.TimeReceived.UnixNano(), tx.SequenceID)) + return []byte(fmt.Sprintf("%s%.19d/%s", txCreatedIndexPrefix, tx.TimeReceived.UnixNano(), tx.SequenceID)) } func txDataKey(k string) []byte { @@ -367,7 +367,7 @@ func (p *leveldbPersistence) listTransactionsByIndex(ctx context.Context, collec func (p *leveldbPersistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { afterStr := "" if after != nil { - afterStr = fmt.Sprintf("%.19d/%s", after.Headers.TimeReceived.UnixNano(), after.SequenceID) + afterStr = fmt.Sprintf("%.19d/%s", after.TimeReceived.UnixNano(), after.SequenceID) } return p.listTransactionsByIndex(ctx, txCreatedIndexPrefix, txCreatedIndexEnd, afterStr, limit, dir) } @@ -409,12 +409,12 @@ func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes. if tx.TransactionHeaders.From == "" || tx.Nonce == nil || tx.SequenceID == nil || - tx.Headers.TimeReceived == nil || - tx.Headers.RequestID == "" || + tx.TimeReceived == nil || + tx.ID == "" || tx.Status == "" { return i18n.NewError(ctx, tmmsgs.MsgPersistenceTXIncomplete) } - idKey := txDataKey(tx.Headers.RequestID) + idKey := txDataKey(tx.ID) if new { // This must be a unique ID, otherwise we return a conflict. // Note we use the final record we write at the end for the conflict check, and also that we're write locked here diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index af148c19..2b5513d9 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -261,10 +261,8 @@ func TestReadWriteCheckpoints(t *testing.T) { func newTestTX(signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { return &apitypes.ManagedTX{ - Headers: apitypes.ManagedTXHeaders{ - RequestID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), - TimeReceived: fftypes.Now(), - }, + ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + TimeReceived: fftypes.Now(), TransactionHeaders: ffcapi.TransactionHeaders{ From: signer, }, @@ -300,10 +298,10 @@ func TestReadWriteManagedTransactions(t *testing.T) { assert.NoError(t, err) assert.Len(t, txns, 4) - assert.Equal(t, s1t3.Headers.RequestID, txns[0].Headers.RequestID) - assert.Equal(t, s1t2.Headers.RequestID, txns[1].Headers.RequestID) - assert.Equal(t, s2t1.Headers.RequestID, txns[2].Headers.RequestID) - assert.Equal(t, s1t1.Headers.RequestID, txns[3].Headers.RequestID) + assert.Equal(t, s1t3.ID, txns[0].ID) + assert.Equal(t, s1t2.ID, txns[1].ID) + assert.Equal(t, s2t1.ID, txns[2].ID) + assert.Equal(t, s1t1.ID, txns[3].ID) // Only list pending @@ -311,45 +309,45 @@ func TestReadWriteManagedTransactions(t *testing.T) { assert.NoError(t, err) assert.Len(t, txns, 2) - assert.Equal(t, s1t3.Headers.RequestID, txns[0].Headers.RequestID) - assert.Equal(t, s1t2.Headers.RequestID, txns[1].Headers.RequestID) + assert.Equal(t, s1t3.ID, txns[0].ID) + assert.Equal(t, s1t2.ID, txns[1].ID) // List with time range txns, err = p.ListTransactionsByCreateTime(ctx, s1t2, 0, SortDirectionDescending) assert.NoError(t, err) assert.Len(t, txns, 2) - assert.Equal(t, s2t1.Headers.RequestID, txns[0].Headers.RequestID) - assert.Equal(t, s1t1.Headers.RequestID, txns[1].Headers.RequestID) + assert.Equal(t, s2t1.ID, txns[0].ID) + assert.Equal(t, s1t1.ID, txns[1].ID) // Test delete, and querying by nonce to limit TX returned - err = p.DeleteTransaction(ctx, s1t2.Headers.RequestID) + err = p.DeleteTransaction(ctx, s1t2.ID) assert.NoError(t, err) txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t1.Nonce, 0, SortDirectionAscending) assert.NoError(t, err) assert.Len(t, txns, 1) - assert.Equal(t, s1t3.Headers.RequestID, txns[0].Headers.RequestID) + assert.Equal(t, s1t3.ID, txns[0].ID) // Check we can use after with the deleted nonce, and not skip the one after txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t2.Nonce, 0, SortDirectionAscending) assert.NoError(t, err) assert.Len(t, txns, 1) - assert.Equal(t, s1t3.Headers.RequestID, txns[0].Headers.RequestID) + assert.Equal(t, s1t3.ID, txns[0].ID) // Test get direct - v, err := p.GetTransactionByID(ctx, s1t3.Headers.RequestID) + v, err := p.GetTransactionByID(ctx, s1t3.ID) assert.NoError(t, err) - assert.Equal(t, s1t3.Headers.RequestID, v.Headers.RequestID) + assert.Equal(t, s1t3.ID, v.ID) assert.Equal(t, s1t3.Nonce, v.Nonce) v, err = p.GetTransactionByNonce(ctx, "0xbbbbb", s2t1.Nonce) assert.NoError(t, err) - assert.Equal(t, s2t1.Headers.RequestID, v.Headers.RequestID) + assert.Equal(t, s2t1.ID, v.ID) assert.Equal(t, s2t1.Nonce, v.Nonce) - v, err = p.GetTransactionByID(ctx, s1t2.Headers.RequestID) + v, err = p.GetTransactionByID(ctx, s1t2.ID) assert.NoError(t, err) assert.Nil(t, v) } @@ -468,15 +466,13 @@ func TestListManagedTransactionFail(t *testing.T) { defer done() tx := &apitypes.ManagedTX{ - Headers: apitypes.ManagedTXHeaders{ - RequestID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), - TimeReceived: fftypes.Now(), - }, - SequenceID: apitypes.UUIDVersion1(), + ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + TimeReceived: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), } - err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.Headers.RequestID)) + err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) assert.NoError(t, err) - err = p.db.Put(txDataKey(tx.Headers.RequestID), []byte("{! not json"), &opt.WriteOptions{}) + err = p.db.Put(txDataKey(tx.ID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) _, err = p.ListTransactionsByCreateTime(context.Background(), nil, 0, SortDirectionDescending) @@ -489,13 +485,11 @@ func TestListManagedTransactionCleanupOrphans(t *testing.T) { defer done() tx := &apitypes.ManagedTX{ - Headers: apitypes.ManagedTXHeaders{ - RequestID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), - TimeReceived: fftypes.Now(), - }, - SequenceID: apitypes.UUIDVersion1(), + ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + TimeReceived: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), } - err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.Headers.RequestID)) + err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) assert.NoError(t, err) txns, err := p.ListTransactionsByCreateTime(context.Background(), nil, 0, SortDirectionDescending) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 0c72e997..96e45597 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -81,4 +81,5 @@ var ( MsgTXConflictSignerPending = ffe("FF21063", "Only one of 'signer' and 'pending' can be supplied when querying transactions", http.StatusBadRequest) MsgInvalidSortDirection = ffe("FF21064", "Sort direction must be 'asc'/'ascending' or 'desc'/'descending': '%s'", http.StatusBadRequest) MsgDuplicateID = ffe("FF21065", "ID '%s' is not unique", http.StatusConflict) + MsgTransactionFailed = ffe("FF21066", "Transaction execution failed") ) diff --git a/pkg/apitypes/managed_tx.go b/pkg/apitypes/managed_tx.go index 662c3069..5f39a014 100644 --- a/pkg/apitypes/managed_tx.go +++ b/pkg/apitypes/managed_tx.go @@ -40,12 +40,6 @@ type ManagedTXError struct { Mapped ffcapi.ErrorReason `json:"mapped,omitempty"` } -type ManagedTXHeaders struct { - RequestID string `json:"requestId"` - TimeReceived *fftypes.FFTime `json:"timeReceived"` - LastUpdate *fftypes.FFTime `json:"lastUpdate"` -} - // ManagedTX is the structure stored for each new transaction request, using the external ID of the operation // // Indexing: @@ -63,7 +57,9 @@ type ManagedTXHeaders struct { // - When listing back entries, the persistence layer will automatically clean up indexes if the underlying // TX they refer to is not available. For this reason the index records are written first. type ManagedTX struct { - Headers ManagedTXHeaders `json:"headers"` + ID string `json:"id"` + TimeReceived *fftypes.FFTime `json:"timeReceived"` + LastUpdate *fftypes.FFTime `json:"lastUpdate"` Status TxStatus `json:"status"` SequenceID *fftypes.UUID `json:"sequenceId"` Nonce *fftypes.FFBigInt `json:"nonce"` @@ -76,6 +72,28 @@ type ManagedTX struct { FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` Receipt *ffcapi.TransactionReceiptResponse `json:"receipt,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` ErrorHistory []*ManagedTXError `json:"errorHistory"` Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` } + +type ReplyType string + +const ( + TransactionUpdate ReplyType = "TransactionUpdate" + TransactionUpdateSuccess ReplyType = "TransactionSuccess" + TransactionUpdateFailure ReplyType = "TransactionFailure" +) + +type ReplyHeaders struct { + RequestID string `json:"requestId"` + Type ReplyType +} + +// TransactionUpdateReply add a "headers" structure that allows a processor of websocket +// replies/updates to filter on a standard structure to know how to process the message. +// Extensible to update update types in the future. +type TransactionUpdateReply struct { + Headers ReplyHeaders + ManagedTX +} diff --git a/pkg/fftm/nonces.go b/pkg/fftm/nonces.go index f73d3e64..420d53ce 100644 --- a/pkg/fftm/nonces.go +++ b/pkg/fftm/nonces.go @@ -98,9 +98,9 @@ func (m *manager) calcNextNonce(ctx context.Context, signer string) (uint64, err } if len(txns) > 0 { lastTxn = txns[0] - if time.Since(*lastTxn.Headers.TimeReceived.Time()) < m.nonceStateTimeout { + if time.Since(*lastTxn.TimeReceived.Time()) < m.nonceStateTimeout { nextNonce := lastTxn.Nonce.Uint64() + 1 - log.L(ctx).Debugf("Allocating next nonce '%s' / '%d' after TX '%s' (status=%s)", signer, nextNonce, lastTxn.Headers.RequestID, lastTxn.Status) + log.L(ctx).Debugf("Allocating next nonce '%s' / '%d' after TX '%s' (status=%s)", signer, nextNonce, lastTxn.ID, lastTxn.Status) return nextNonce, nil } } @@ -118,7 +118,7 @@ func (m *manager) calcNextNonce(ctx context.Context, signer string) (uint64, err // This is important in case we have transactions that have expired from the TX pool of nodes, but we still have them // in our state store. So basically whichever is further forwards of our state store and the node answer wins. if lastTxn != nil && nextNonce <= lastTxn.Nonce.Uint64() { - log.L(ctx).Debugf("Node TX pool next nonce '%s' / '%d' is not ahead of '%d' in TX '%s' (status=%s)", signer, nextNonce, lastTxn.Nonce.Uint64(), lastTxn.Headers.RequestID, lastTxn.Status) + log.L(ctx).Debugf("Node TX pool next nonce '%s' / '%d' is not ahead of '%d' in TX '%s' (status=%s)", signer, nextNonce, lastTxn.Nonce.Uint64(), lastTxn.ID, lastTxn.Status) nextNonce = lastTxn.Nonce.Uint64() + 1 } diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go index b9b3b58f..404ef1a5 100644 --- a/pkg/fftm/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -39,13 +39,11 @@ func TestNonceStaleStateContention(t *testing.T) { // Write a stale record to persistence oldTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) err := m.persistence.WriteTransaction(m.ctx, &apitypes.ManagedTX{ - Headers: apitypes.ManagedTXHeaders{ - RequestID: "stale1", - TimeReceived: &oldTime, - }, - Status: apitypes.TxStatusSucceeded, - SequenceID: apitypes.UUIDVersion1(), - Nonce: fftypes.NewFFBigInt(1000), // old nonce + ID: "stale1", + TimeReceived: &oldTime, + Status: apitypes.TxStatusSucceeded, + SequenceID: apitypes.UUIDVersion1(), + Nonce: fftypes.NewFFBigInt(1000), // old nonce TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x12345", }, @@ -74,13 +72,11 @@ func TestNonceStaleStateContention(t *testing.T) { time.Sleep(1 * time.Millisecond) ln.spent = &apitypes.ManagedTX{ - Headers: apitypes.ManagedTXHeaders{ - RequestID: "ns1:" + fftypes.NewUUID().String(), - TimeReceived: &oldTime, - }, - Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), - Status: apitypes.TxStatusPending, - SequenceID: apitypes.UUIDVersion1(), + ID: "ns1:" + fftypes.NewUUID().String(), + TimeReceived: &oldTime, + Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), + Status: apitypes.TxStatusPending, + SequenceID: apitypes.UUIDVersion1(), TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x12345", }, @@ -149,7 +145,7 @@ func TestNonceListStaleThenQueryFail(t *testing.T) { old := fftypes.FFTime(time.Now().Add(-10000 * time.Hour)) mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return([]*apitypes.ManagedTX{ - {Headers: apitypes.ManagedTXHeaders{RequestID: "id12345", TimeReceived: &old}, Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, + {ID: "id12345", TimeReceived: &old, Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, }, nil) mp.On("Close", mock.Anything).Return(nil).Maybe() @@ -184,7 +180,7 @@ func TestNonceListNotStale(t *testing.T) { m.persistence = mp mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return([]*apitypes.ManagedTX{ - {Headers: apitypes.ManagedTXHeaders{RequestID: "id12345", TimeReceived: fftypes.Now()}, Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, + {ID: "id12345", TimeReceived: fftypes.Now(), Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, }, nil) mp.On("Close", mock.Anything).Return(nil).Maybe() diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index 71869f34..4c44adb7 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -20,9 +20,11 @@ import ( "time" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" "github.com/hyperledger/firefly-transaction-manager/internal/persistence" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -100,7 +102,7 @@ func (m *manager) policyLoopCycle(inflightStale bool) { for _, pending := range m.inflight { err := m.execPolicy(pending) if err != nil { - log.L(m.ctx).Errorf("Failed policy cycle transaction=%s operation=%s: %s", pending.mtx.TransactionHash, pending.mtx.Headers.RequestID, err) + log.L(m.ctx).Errorf("Failed policy cycle transaction=%s operation=%s: %s", pending.mtx.TransactionHash, pending.mtx.ID, err) } } @@ -113,11 +115,13 @@ func (m *manager) addError(mtx *apitypes.ManagedTX, reason ffcapi.ErrorReason, e } oldHistory := mtx.ErrorHistory mtx.ErrorHistory = make([]*apitypes.ManagedTXError, newLen) - mtx.ErrorHistory[0] = &apitypes.ManagedTXError{ + latestError := &apitypes.ManagedTXError{ Time: fftypes.Now(), Mapped: reason, Error: err.Error(), } + mtx.ErrorMessage = latestError.Error + mtx.ErrorHistory[0] = latestError for i := 1; i < newLen; i++ { mtx.ErrorHistory[i] = oldHistory[i-1] } @@ -134,8 +138,10 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { completed = true if mtx.Receipt.Success { mtx.Status = apitypes.TxStatusSucceeded + mtx.ErrorMessage = "" } else { mtx.Status = apitypes.TxStatusFailed + mtx.ErrorMessage = i18n.NewError(m.ctx, tmmsgs.MsgTransactionFailed).Error() } default: @@ -144,7 +150,7 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { var reason ffcapi.ErrorReason updated, reason, err = m.policyEngine.Execute(m.ctx, m.connector, pending.mtx) if err != nil { - log.L(m.ctx).Errorf("Policy engine returned error for operation %s reason=%s: %s", mtx.Headers.RequestID, reason, err) + log.L(m.ctx).Errorf("Policy engine returned error for operation %s reason=%s: %s", mtx.ID, reason, err) m.addError(mtx, reason, err) } else if mtx.FirstSubmit != nil && pending.trackingTransactionHash != mtx.TransactionHash { // If now submitted, add to confirmations manager for receipt checking @@ -155,18 +161,38 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { if updated || err != nil { err := m.persistence.WriteTransaction(m.ctx, mtx, false) if err != nil { - log.L(m.ctx).Errorf("Failed to update operation %s (status=%s): %s", mtx.Headers.RequestID, mtx.Status, err) + log.L(m.ctx).Errorf("Failed to update operation %s (status=%s): %s", mtx.ID, mtx.Status, err) return err } if completed { pending.remove = true m.markInflightStale() } + m.sendWSReply(mtx) } return nil } +func (m *manager) sendWSReply(mtx *apitypes.ManagedTX) { + wsr := &apitypes.TransactionUpdateReply{ + ManagedTX: *mtx, + Headers: apitypes.ReplyHeaders{ + RequestID: mtx.ID, + }, + } + switch mtx.Status { + case apitypes.TxStatusSucceeded: + wsr.Headers.Type = apitypes.TransactionUpdateSuccess + case apitypes.TxStatusFailed: + wsr.Headers.Type = apitypes.TransactionUpdateFailure + default: + wsr.Headers.Type = apitypes.TransactionUpdate + } + // Notify on the websocket - this is best-effort (there is no subscription/acknowledgement) + m.wsServer.SendReply(wsr) +} + func (m *manager) trackSubmittedTransaction(pending *pendingState) { var err error @@ -190,10 +216,6 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { // Will be picked up on the next policy loop cycle - guaranteed to occur before Confirmed m.mux.Lock() pending.mtx.Receipt = receipt - // TODO: This may not be the right spot to do this, - // but since it's part of the manager, it has a pointer - // to the wsServer to be able to send a reply - m.wsServer.SendReply(wsTransactionReceipt(pending)) m.mux.Unlock() }, Confirmed: func(confirmations []confirmations.BlockInfo) { @@ -214,36 +236,3 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { pending.trackingTransactionHash = pending.mtx.TransactionHash } } - -type WsTransactionReceipt struct { - Headers *WsTransactionReceiptHeaders `json:"headers"` - TransactionHash string `json:"transactionHash"` - ErrorMessage string `json:"errorMessage"` -} - -type WsTransactionReceiptHeaders struct { - RequestID string `json:"requestId"` - ReplyType string `json:"type"` -} - -const ( - ReplyTypeTransactionSuccess = "TransactionSuccess" - ReplyTypeTransactionFailure = "TransactionFailure" -) - -// This function is used to transform the pendingState into the format -// that FireFly Core is expecting, preserving backward compatibility -// with the original Ethconnect implementation -func wsTransactionReceipt(pending *pendingState) *WsTransactionReceipt { - // TODO: Set this status correctly. Always report success for now, for initial testing - replyType := ReplyTypeTransactionSuccess - - return &WsTransactionReceipt{ - Headers: &WsTransactionReceiptHeaders{ - RequestID: pending.mtx.Headers.RequestID, - ReplyType: replyType, - }, - TransactionHash: pending.mtx.TransactionHash, - ErrorMessage: "", - } -} diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index 343bb41a..4c7b6630 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -22,8 +22,10 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" @@ -90,7 +92,7 @@ func TestPolicyLoopE2EOk(t *testing.T) { // Run the policy once to do the send <-m.inflightStale // from sending the TX m.policyLoopCycle(true) - assert.Equal(t, mtx.Headers.RequestID, m.inflight[0].mtx.Headers.RequestID) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) // A second time will mark it complete for flush @@ -101,7 +103,7 @@ func TestPolicyLoopE2EOk(t *testing.T) { assert.Empty(t, m.inflight) // Check the update is persisted - rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.Headers.RequestID) + rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.ID) assert.NoError(t, err) assert.Equal(t, apitypes.TxStatusSucceeded, rtx.Status) @@ -141,7 +143,7 @@ func TestPolicyLoopE2EReverted(t *testing.T) { // Run the policy once to do the send <-m.inflightStale // from sending the TX m.policyLoopCycle(true) - assert.Equal(t, mtx.Headers.RequestID, m.inflight[0].mtx.Headers.RequestID) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) // A second time will mark it complete for flush @@ -152,7 +154,7 @@ func TestPolicyLoopE2EReverted(t *testing.T) { assert.Empty(t, m.inflight) // Check the update is persisted - rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.Headers.RequestID) + rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.ID) assert.NoError(t, err) assert.Equal(t, apitypes.TxStatusFailed, rtx.Status) @@ -212,13 +214,20 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { // Run the policy once to do the send with the first hash <-m.inflightStale // from sending the TX m.policyLoopCycle(true) - assert.Equal(t, mtx.Headers.RequestID, m.inflight[0].mtx.Headers.RequestID) + assert.Len(t, m.inflight, 1) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) + + // Run again to confirm it does not change anything, when the state is the same + m.policyLoopCycle(true) + assert.Len(t, m.inflight, 1) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) // Reset the transaction so the policy manager resubmits it m.inflight[0].mtx.FirstSubmit = nil m.policyLoopCycle(false) - assert.Equal(t, mtx.Headers.RequestID, m.inflight[0].mtx.Headers.RequestID) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) mc.AssertExpectations(t) @@ -247,3 +256,29 @@ func TestNotifyConfirmationMgrFail(t *testing.T) { mfc.AssertExpectations(t) } + +func TestInflightSetListFailCancel(t *testing.T) { + + _, m, cancel := newTestManager(t) + cancel() + + mp := &persistencemocks.Persistence{} + m.persistence = mp + mp.On("ListTransactionsPending", m.ctx, (*fftypes.UUID)(nil), m.maxInFlight, persistence.SortDirectionAscending). + Return(nil, fmt.Errorf("pop")) + + m.policyLoopCycle(true) + + mp.AssertExpectations(t) + +} + +func TestMarkInflightStaleDoesNotBlock(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + m.markInflightStale() + m.markInflightStale() + +} diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go index 118a6571..4f8c0015 100644 --- a/pkg/fftm/route_get_transactions_test.go +++ b/pkg/fftm/route_get_transactions_test.go @@ -30,13 +30,11 @@ import ( func newTestTxn(t *testing.T, m *manager, signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { tx := &apitypes.ManagedTX{ - Headers: apitypes.ManagedTXHeaders{ - RequestID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), - TimeReceived: fftypes.Now(), - }, - SequenceID: apitypes.UUIDVersion1(), - Nonce: fftypes.NewFFBigInt(nonce), - Status: status, + ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + TimeReceived: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), + Nonce: fftypes.NewFFBigInt(nonce), + Status: status, TransactionHeaders: ffcapi.TransactionHeaders{ From: signer, }, @@ -68,36 +66,36 @@ func TestGetTransactions(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, transactions, 4) - assert.Equal(t, s1t3.Headers.RequestID, transactions[0].Headers.RequestID) - assert.Equal(t, s1t2.Headers.RequestID, transactions[1].Headers.RequestID) - assert.Equal(t, s2t1.Headers.RequestID, transactions[2].Headers.RequestID) - assert.Equal(t, s1t1.Headers.RequestID, transactions[3].Headers.RequestID) + assert.Equal(t, s1t3.ID, transactions[0].ID) + assert.Equal(t, s1t2.ID, transactions[1].ID) + assert.Equal(t, s2t1.ID, transactions[2].ID) + assert.Equal(t, s1t1.ID, transactions[3].ID) // Test pagination on default sort/filter res, err = resty.New().R(). SetResult(&transactions). - Get(url + "/transactions?limit=1&after=" + s1t2.Headers.RequestID) + Get(url + "/transactions?limit=1&after=" + s1t2.ID) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, transactions, 1) - assert.Equal(t, s2t1.Headers.RequestID, transactions[0].Headers.RequestID) + assert.Equal(t, s2t1.ID, transactions[0].ID) // Test pagination on nonce filter res, err = resty.New().R(). SetResult(&transactions). - Get(url + "/transactions?signer=0xaaaaa&after=" + s1t2.Headers.RequestID) + Get(url + "/transactions?signer=0xaaaaa&after=" + s1t2.ID) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, transactions, 1) - assert.Equal(t, s1t1.Headers.RequestID, transactions[0].Headers.RequestID) + assert.Equal(t, s1t1.ID, transactions[0].ID) // Test pagination on pending filter res, err = resty.New().R(). SetResult(&transactions). - Get(url + "/transactions?pending&after=" + s1t3.Headers.RequestID) + Get(url + "/transactions?pending&after=" + s1t3.ID) assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, transactions, 1) - assert.Equal(t, s2t1.Headers.RequestID, transactions[0].Headers.RequestID) + assert.Equal(t, s2t1.ID, transactions[0].ID) } diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index 71c746da..08f3db5e 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -54,20 +54,20 @@ func (m *manager) sendManagedContractDeployment(ctx context.Context, request *ap return m.submitPreparedTX(ctx, request.Headers.ID, &request.TransactionHeaders, prepared.Gas, prepared.TransactionData) } -func (m *manager) submitPreparedTX(ctx context.Context, reqID string, txHeaders *ffcapi.TransactionHeaders, gas *fftypes.FFBigInt, transactionData string) (*apitypes.ManagedTX, error) { +func (m *manager) submitPreparedTX(ctx context.Context, txID string, txHeaders *ffcapi.TransactionHeaders, gas *fftypes.FFBigInt, transactionData string) (*apitypes.ManagedTX, error) { // Sequencing ID is always generated by us - so we have a deterministic order of transactions seqID := apitypes.UUIDVersion1() // The request ID is the primary ID, and should be supplied by the user for idempotence - if reqID == "" { - reqID = seqID.String() + if txID == "" { + txID = seqID.String() } // First job is to assign the next nonce to this request. // We block any further sends on this nonce until we've got this one successfully into the node, or // fail deterministically in a way that allows us to return it. - lockedNonce, err := m.assignAndLockNonce(ctx, reqID, txHeaders.From) + lockedNonce, err := m.assignAndLockNonce(ctx, txID, txHeaders.From) if err != nil { return nil, err } @@ -80,11 +80,9 @@ func (m *manager) submitPreparedTX(ctx context.Context, reqID string, txHeaders // to the background worker. now := fftypes.Now() mtx := &apitypes.ManagedTX{ - Headers: apitypes.ManagedTXHeaders{ - RequestID: reqID, // on input the request ID must be the namespaced operation ID - TimeReceived: now, - LastUpdate: now, - }, + ID: txID, // on input the request ID must be the namespaced operation ID + TimeReceived: now, + LastUpdate: now, SequenceID: seqID, Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), Gas: gas, diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index 673370bd..ae467cba 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -151,7 +151,7 @@ func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx * now := fftypes.Now() if now.Time().Sub(*lastWarnTime.Time()) > p.warnInterval { secsSinceSubmit := float64(now.Time().Sub(*mtx.FirstSubmit.Time())) / float64(time.Second) - log.L(ctx).Warnf("Transaction %s (op=%s) has not been mined after %.2fs", mtx.TransactionHash, mtx.Headers.RequestID, secsSinceSubmit) + log.L(ctx).Warnf("Transaction %s (op=%s) has not been mined after %.2fs", mtx.TransactionHash, mtx.ID, secsSinceSubmit) info.LastWarnTime = now return true, "", nil } From 0b41a52ef1c9b929827ff1460236f751cf7a14b6 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 2 Aug 2022 23:14:31 -0400 Subject: [PATCH 68/95] Additional policy tests Signed-off-by: Peter Broadhurst --- internal/persistence/leveldb_persistence.go | 6 +-- .../persistence/leveldb_persistence_test.go | 16 +++--- pkg/apitypes/managed_tx.go | 4 +- pkg/fftm/nonces.go | 2 +- pkg/fftm/nonces_test.go | 24 ++++----- pkg/fftm/policyloop.go | 1 + pkg/fftm/policyloop_test.go | 54 +++++++++++++++++++ pkg/fftm/route_get_transactions_test.go | 10 ++-- pkg/fftm/send_tx.go | 4 +- 9 files changed, 88 insertions(+), 33 deletions(-) diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go index d7dfe57f..1e832648 100644 --- a/internal/persistence/leveldb_persistence.go +++ b/internal/persistence/leveldb_persistence.go @@ -95,7 +95,7 @@ func txPendingIndexKey(sequenceID *fftypes.UUID) []byte { } func txCreatedIndexKey(tx *apitypes.ManagedTX) []byte { - return []byte(fmt.Sprintf("%s%.19d/%s", txCreatedIndexPrefix, tx.TimeReceived.UnixNano(), tx.SequenceID)) + return []byte(fmt.Sprintf("%s%.19d/%s", txCreatedIndexPrefix, tx.Created.UnixNano(), tx.SequenceID)) } func txDataKey(k string) []byte { @@ -367,7 +367,7 @@ func (p *leveldbPersistence) listTransactionsByIndex(ctx context.Context, collec func (p *leveldbPersistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { afterStr := "" if after != nil { - afterStr = fmt.Sprintf("%.19d/%s", after.TimeReceived.UnixNano(), after.SequenceID) + afterStr = fmt.Sprintf("%.19d/%s", after.Created.UnixNano(), after.SequenceID) } return p.listTransactionsByIndex(ctx, txCreatedIndexPrefix, txCreatedIndexEnd, afterStr, limit, dir) } @@ -409,7 +409,7 @@ func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes. if tx.TransactionHeaders.From == "" || tx.Nonce == nil || tx.SequenceID == nil || - tx.TimeReceived == nil || + tx.Created == nil || tx.ID == "" || tx.Status == "" { return i18n.NewError(ctx, tmmsgs.MsgPersistenceTXIncomplete) diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index 2b5513d9..fca9bede 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -261,8 +261,8 @@ func TestReadWriteCheckpoints(t *testing.T) { func newTestTX(signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { return &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), - TimeReceived: fftypes.Now(), + ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + Created: fftypes.Now(), TransactionHeaders: ffcapi.TransactionHeaders{ From: signer, }, @@ -466,9 +466,9 @@ func TestListManagedTransactionFail(t *testing.T) { defer done() tx := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), - TimeReceived: fftypes.Now(), - SequenceID: apitypes.UUIDVersion1(), + ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + Created: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), } err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) assert.NoError(t, err) @@ -485,9 +485,9 @@ func TestListManagedTransactionCleanupOrphans(t *testing.T) { defer done() tx := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), - TimeReceived: fftypes.Now(), - SequenceID: apitypes.UUIDVersion1(), + ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + Created: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), } err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) assert.NoError(t, err) diff --git a/pkg/apitypes/managed_tx.go b/pkg/apitypes/managed_tx.go index 5f39a014..22fd3606 100644 --- a/pkg/apitypes/managed_tx.go +++ b/pkg/apitypes/managed_tx.go @@ -58,8 +58,8 @@ type ManagedTXError struct { // TX they refer to is not available. For this reason the index records are written first. type ManagedTX struct { ID string `json:"id"` - TimeReceived *fftypes.FFTime `json:"timeReceived"` - LastUpdate *fftypes.FFTime `json:"lastUpdate"` + Created *fftypes.FFTime `json:"created"` + Updated *fftypes.FFTime `json:"updated"` Status TxStatus `json:"status"` SequenceID *fftypes.UUID `json:"sequenceId"` Nonce *fftypes.FFBigInt `json:"nonce"` diff --git a/pkg/fftm/nonces.go b/pkg/fftm/nonces.go index 420d53ce..2f379117 100644 --- a/pkg/fftm/nonces.go +++ b/pkg/fftm/nonces.go @@ -98,7 +98,7 @@ func (m *manager) calcNextNonce(ctx context.Context, signer string) (uint64, err } if len(txns) > 0 { lastTxn = txns[0] - if time.Since(*lastTxn.TimeReceived.Time()) < m.nonceStateTimeout { + if time.Since(*lastTxn.Created.Time()) < m.nonceStateTimeout { nextNonce := lastTxn.Nonce.Uint64() + 1 log.L(ctx).Debugf("Allocating next nonce '%s' / '%d' after TX '%s' (status=%s)", signer, nextNonce, lastTxn.ID, lastTxn.Status) return nextNonce, nil diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go index 404ef1a5..e44bc6c0 100644 --- a/pkg/fftm/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -39,11 +39,11 @@ func TestNonceStaleStateContention(t *testing.T) { // Write a stale record to persistence oldTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) err := m.persistence.WriteTransaction(m.ctx, &apitypes.ManagedTX{ - ID: "stale1", - TimeReceived: &oldTime, - Status: apitypes.TxStatusSucceeded, - SequenceID: apitypes.UUIDVersion1(), - Nonce: fftypes.NewFFBigInt(1000), // old nonce + ID: "stale1", + Created: &oldTime, + Status: apitypes.TxStatusSucceeded, + SequenceID: apitypes.UUIDVersion1(), + Nonce: fftypes.NewFFBigInt(1000), // old nonce TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x12345", }, @@ -72,11 +72,11 @@ func TestNonceStaleStateContention(t *testing.T) { time.Sleep(1 * time.Millisecond) ln.spent = &apitypes.ManagedTX{ - ID: "ns1:" + fftypes.NewUUID().String(), - TimeReceived: &oldTime, - Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), - Status: apitypes.TxStatusPending, - SequenceID: apitypes.UUIDVersion1(), + ID: "ns1:" + fftypes.NewUUID().String(), + Created: &oldTime, + Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), + Status: apitypes.TxStatusPending, + SequenceID: apitypes.UUIDVersion1(), TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x12345", }, @@ -145,7 +145,7 @@ func TestNonceListStaleThenQueryFail(t *testing.T) { old := fftypes.FFTime(time.Now().Add(-10000 * time.Hour)) mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return([]*apitypes.ManagedTX{ - {ID: "id12345", TimeReceived: &old, Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, + {ID: "id12345", Created: &old, Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, }, nil) mp.On("Close", mock.Anything).Return(nil).Maybe() @@ -180,7 +180,7 @@ func TestNonceListNotStale(t *testing.T) { m.persistence = mp mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return([]*apitypes.ManagedTX{ - {ID: "id12345", TimeReceived: fftypes.Now(), Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, + {ID: "id12345", Created: fftypes.Now(), Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, }, nil) mp.On("Close", mock.Anything).Return(nil).Maybe() diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index 4c44adb7..efc53141 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -159,6 +159,7 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { } if updated || err != nil { + mtx.Updated = fftypes.Now() err := m.persistence.WriteTransaction(m.ctx, mtx, false) if err != nil { log.L(m.ctx).Errorf("Failed to update operation %s (status=%s): %s", mtx.ID, mtx.Status, err) diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index 4c7b6630..63bfe6b3 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -26,6 +26,7 @@ import ( "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" @@ -266,6 +267,7 @@ func TestInflightSetListFailCancel(t *testing.T) { m.persistence = mp mp.On("ListTransactionsPending", m.ctx, (*fftypes.UUID)(nil), m.maxInFlight, persistence.SortDirectionAscending). Return(nil, fmt.Errorf("pop")) + mp.On("Close", mock.Anything).Return(nil).Maybe() m.policyLoopCycle(true) @@ -273,6 +275,58 @@ func TestInflightSetListFailCancel(t *testing.T) { } +func TestPolicyLoopUpdateFail(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + m.inflight = []*pendingState{ + { + confirmed: true, + mtx: &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + Created: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), + Nonce: fftypes.NewFFBigInt(1000), + Status: apitypes.TxStatusSucceeded, + FirstSubmit: fftypes.Now(), + Receipt: &ffcapi.TransactionReceiptResponse{}, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + }, + }, + } + + mp := &persistencemocks.Persistence{} + m.persistence = mp + mp.On("WriteTransaction", m.ctx, mock.Anything, false).Return(fmt.Errorf("pop")) + mp.On("Close", mock.Anything).Return(nil).Maybe() + + m.policyLoopCycle(false) + + mp.AssertExpectations(t) + +} + +func TestPolicyEngineFail(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + mpe := &policyenginemocks.PolicyEngine{} + m.policyEngine = mpe + mpe.On("Execute", mock.Anything, mock.Anything, mock.Anything). + Return(false, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + _ = sendSampleTX(t, m, "0xaaaaa", 12345) + + m.policyLoopCycle(true) + + mpe.AssertExpectations(t) + +} + func TestMarkInflightStaleDoesNotBlock(t *testing.T) { _, m, cancel := newTestManager(t) diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go index 4f8c0015..e0c97605 100644 --- a/pkg/fftm/route_get_transactions_test.go +++ b/pkg/fftm/route_get_transactions_test.go @@ -30,11 +30,11 @@ import ( func newTestTxn(t *testing.T, m *manager, signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { tx := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), - TimeReceived: fftypes.Now(), - SequenceID: apitypes.UUIDVersion1(), - Nonce: fftypes.NewFFBigInt(nonce), - Status: status, + ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + Created: fftypes.Now(), + SequenceID: apitypes.UUIDVersion1(), + Nonce: fftypes.NewFFBigInt(nonce), + Status: status, TransactionHeaders: ffcapi.TransactionHeaders{ From: signer, }, diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index 08f3db5e..833b321d 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -81,8 +81,8 @@ func (m *manager) submitPreparedTX(ctx context.Context, txID string, txHeaders * now := fftypes.Now() mtx := &apitypes.ManagedTX{ ID: txID, // on input the request ID must be the namespaced operation ID - TimeReceived: now, - LastUpdate: now, + Created: now, + Updated: now, SequenceID: seqID, Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), Gas: gas, From ca7f5403e0c90f1a6d316177cb66f968966a49dc Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 3 Aug 2022 11:07:04 -0400 Subject: [PATCH 69/95] Fix missing tag Signed-off-by: Peter Broadhurst --- pkg/apitypes/managed_tx.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/apitypes/managed_tx.go b/pkg/apitypes/managed_tx.go index 22fd3606..e37dd7d8 100644 --- a/pkg/apitypes/managed_tx.go +++ b/pkg/apitypes/managed_tx.go @@ -86,8 +86,8 @@ const ( ) type ReplyHeaders struct { - RequestID string `json:"requestId"` - Type ReplyType + RequestID string `json:"requestId"` + Type ReplyType `json:"type"` } // TransactionUpdateReply add a "headers" structure that allows a processor of websocket From 111a5f82387532b4afed3119a95d01ec10a551d0 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 3 Aug 2022 14:22:39 -0400 Subject: [PATCH 70/95] Avoid nil panic on query error Signed-off-by: Peter Broadhurst --- pkg/fftm/api_test.go | 27 +++++++++++++++++++++++++++ pkg/fftm/route__root_command.go | 5 ++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go index 90a43acd..1ebd0380 100644 --- a/pkg/fftm/api_test.go +++ b/pkg/fftm/api_test.go @@ -248,6 +248,33 @@ func TestQueryOK(t *testing.T) { } +func TestQueryFail(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + mca := m.connector.(*ffcapimocks.API) + mca.On("QueryInvoke", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + res, err := resty.New().R(). + SetBody(&apitypes.QueryRequest{ + Headers: apitypes.RequestHeaders{ + ID: fftypes.NewUUID().String(), + Type: apitypes.RequestTypeQuery, + }, + TransactionInput: ffcapi.TransactionInput{ + Method: fftypes.JSONAnyPtr(`"some method details"`), + }, + }). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 500, res.StatusCode()) + + mca.AssertExpectations(t) + +} + func TestQueryBadRequest(t *testing.T) { url, m, cancel := newTestManager(t) diff --git a/pkg/fftm/route__root_command.go b/pkg/fftm/route__root_command.go index 61873029..57b8f3f8 100644 --- a/pkg/fftm/route__root_command.go +++ b/pkg/fftm/route__root_command.go @@ -72,7 +72,10 @@ var postRootCommand = func(m *manager) *ffapi.Route { res, _, err := m.connector.QueryInvoke(r.Req.Context(), &ffcapi.QueryInvokeRequest{ TransactionInput: tReq.TransactionInput, }) - return res.Outputs, err + if err != nil { + return nil, err + } + return res.Outputs, nil case apitypes.RequestTypeDeploy: var tReq apitypes.ContractDeployRequest if err = baseReq.UnmarshalTo(&tReq); err != nil { From 0be295c8fca7021d72b4610dddae4eefca201c0c Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 3 Aug 2022 14:53:12 -0400 Subject: [PATCH 71/95] Fix JSON tag Signed-off-by: Peter Broadhurst --- pkg/apitypes/managed_tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apitypes/managed_tx.go b/pkg/apitypes/managed_tx.go index e37dd7d8..88d69f04 100644 --- a/pkg/apitypes/managed_tx.go +++ b/pkg/apitypes/managed_tx.go @@ -94,6 +94,6 @@ type ReplyHeaders struct { // replies/updates to filter on a standard structure to know how to process the message. // Extensible to update update types in the future. type TransactionUpdateReply struct { - Headers ReplyHeaders + Headers ReplyHeaders `json:"headers"` ManagedTX } From 4cc57d4e0ff5fc3722edb617a7424713c0bb78a7 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Wed, 3 Aug 2022 17:00:53 -0400 Subject: [PATCH 72/95] Pass through the signature from the connector - as it's event specific Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 1 - pkg/apitypes/api_types.go | 1 - pkg/apitypes/api_types_test.go | 2 +- pkg/ffcapi/api.go | 1 + 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 9150da7e..d0be00ae 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -726,7 +726,6 @@ func (es *eventStream) batchLoop(startedState *startedStreamState) { StreamID: es.spec.ID, DeprecatedSubID: l.spec.ID, ListenerName: *l.spec.Name, - Signature: l.spec.Signature, }, Event: *fev.Event, }) diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 5d0901ed..472742bd 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -220,7 +220,6 @@ type EventContext struct { StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event DeprecatedSubID *fftypes.UUID `json:"subId"` // ID of the listener - deprecated "subscription" naming ListenerName string `json:"listenerName"` // name of the listener - Signature string `json:"signature"` // event signature string } // EventWithContext is what is delivered diff --git a/pkg/apitypes/api_types_test.go b/pkg/apitypes/api_types_test.go index d80895cf..59737f41 100644 --- a/pkg/apitypes/api_types_test.go +++ b/pkg/apitypes/api_types_test.go @@ -195,7 +195,6 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { StandardContext: EventContext{ StreamID: UUIDVersion1(), ListenerName: "listener1", - Signature: "ev()", DeprecatedSubID: UUIDVersion1(), }, Event: ffcapi.Event{ @@ -206,6 +205,7 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { TransactionHash: "0x23456", TransactionIndex: 10, LogIndex: 1, + Signature: "ev()", }, Info: &customInfo{ InfoKey1: "val1", diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 28b9c493..4d8a63aa 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -86,6 +86,7 @@ type BlockHashEvent struct { // EventID are the set of required fields an FFCAPI compatible connector needs to map to the underlying blockchain constructs, to uniquely identify an event type EventID struct { ListenerID *fftypes.UUID `json:"listenerId"` // The listener for the event + Signature string `json:"signature"` // The signature of this specific event (noting a listener might filter on multiple events) BlockHash string `json:"blockHash"` // String representation of the block, which will change if any transaction info in the block changes BlockNumber uint64 `json:"blockNumber"` // A numeric identifier for the block TransactionHash string `json:"transactionHash"` // The transaction From 5ff6702d4559cf36253690ea4021b9e4db1c6170 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 4 Aug 2022 09:11:39 -0400 Subject: [PATCH 73/95] Address review comments Signed-off-by: Peter Broadhurst --- internal/tmmsgs/en_api_descriptions.go | 39 ++++++++-------- pkg/fftm/api_test.go | 4 +- pkg/fftm/route__root_command.go | 46 +++++++++++++------ .../route_post_eventstream_listener_reset.go | 2 +- pkg/fftm/route_post_eventstream_listeners.go | 5 +- pkg/fftm/route_post_subscription_reset.go | 2 +- pkg/fftm/route_post_subscriptions.go | 15 +++--- 7 files changed, 63 insertions(+), 50 deletions(-) diff --git a/internal/tmmsgs/en_api_descriptions.go b/internal/tmmsgs/en_api_descriptions.go index ee004763..3ab420f9 100644 --- a/internal/tmmsgs/en_api_descriptions.go +++ b/internal/tmmsgs/en_api_descriptions.go @@ -27,24 +27,27 @@ var ffm = func(key, translation string) i18n.MessageKey { //revive:disable var ( - APIEndpointPostRoot = ffm("api.endpoints.post.root", "") - APIEndpointPostEventStream = ffm("api.endpoints.post.eventstreams", "Create a new event stream") - APIEndpointPatchEventStream = ffm("api.endpoints.patch.eventstreams", "Update an existing event stream") - APIEndpointPostEventStreamSuspend = ffm("api.endpoints.post.eventstream.suspend", "Suspend an event stream") - APIEndpointPostEventStreamResume = ffm("api.endpoints.post.eventstream.resume", "Resume an event stream") - APIEndpointGetEventStreams = ffm("api.endpoints.get.eventstreams", "List event streams") - APIEndpointGetEventStream = ffm("api.endpoints.get.eventstream", "Get an event stream with status") - APIEndpointDeleteEventStream = ffm("api.endpoints.delete.eventstream", "Delete an event stream") - APIEndpointGetSubscriptions = ffm("api.endpoints.get.subscriptions", "Get listeners - route deprecated in favor of /eventstreams/{streamId}/listeners") - APIEndpointGetSubscription = ffm("api.endpoints.get.subscription", "Get listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") - APIEndpointPostSubscriptions = ffm("api.endpoints.post.subscriptions", "Create new listener - route deprecated in favor of /eventstreams/{streamId}/listeners") - APIEndpointPatchSubscription = ffm("api.endpoints.patch.subscription", "Update listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") - APIEndpointDeleteSubscription = ffm("api.endpoints.delete.subscription", "Delete listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") - APIEndpointGetEventStreamListeners = ffm("api.endpoints.get.eventstream.listeners", "List event stream listeners") - APIEndpointGetEventStreamListener = ffm("api.endpoints.get.eventstream.listener", "Get event stream listener") - APIEndpointPostEventStreamListener = ffm("api.endpoints.post.eventstream.listener", "Create event stream listener") - APIEndpointPatchEventStreamListener = ffm("api.endpoints.patch.eventstream.listener", "Update event stream listener") - APIEndpointDeleteEventStreamListener = ffm("api.endpoints.delete.eventstream.listener", "Delete event stream listener") + APIEndpointPostRoot = ffm("api.endpoints.post.root", "RPC/webhook style interface initiate a submit transactions, and execute queries") + APIEndpointPostRootQueryOutput = ffm("api.endpoints.post.root.query.output", "The data result of a query against a smart contract") + APIEndpointPostEventStream = ffm("api.endpoints.post.eventstreams", "Create a new event stream") + APIEndpointPatchEventStream = ffm("api.endpoints.patch.eventstreams", "Update an existing event stream") + APIEndpointPostEventStreamSuspend = ffm("api.endpoints.post.eventstream.suspend", "Suspend an event stream") + APIEndpointPostEventStreamResume = ffm("api.endpoints.post.eventstream.resume", "Resume an event stream") + APIEndpointGetEventStreams = ffm("api.endpoints.get.eventstreams", "List event streams") + APIEndpointGetEventStream = ffm("api.endpoints.get.eventstream", "Get an event stream with status") + APIEndpointDeleteEventStream = ffm("api.endpoints.delete.eventstream", "Delete an event stream") + APIEndpointGetSubscriptions = ffm("api.endpoints.get.subscriptions", "Get listeners - route deprecated in favor of /eventstreams/{streamId}/listeners") + APIEndpointGetSubscription = ffm("api.endpoints.get.subscription", "Get listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointPostSubscriptions = ffm("api.endpoints.post.subscriptions", "Create new listener - route deprecated in favor of /eventstreams/{streamId}/listeners") + APIEndpointPostSubscriptionReset = ffm("api.endpoints.post.subscription.reset", "Reset listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}/reset") + APIEndpointPatchSubscription = ffm("api.endpoints.patch.subscription", "Update listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointDeleteSubscription = ffm("api.endpoints.delete.subscription", "Delete listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointGetEventStreamListeners = ffm("api.endpoints.get.eventstream.listeners", "List event stream listeners") + APIEndpointGetEventStreamListener = ffm("api.endpoints.get.eventstream.listener", "Get event stream listener") + APIEndpointPostEventStreamListener = ffm("api.endpoints.post.eventstream.listener", "Create event stream listener") + APIEndpointPostEventStreamListenerReset = ffm("api.endpoints.post.eventstream.listener.reset", "Reset an event stream listener, to redeliver all events since the specified block") + APIEndpointPatchEventStreamListener = ffm("api.endpoints.patch.eventstream.listener", "Update event stream listener") + APIEndpointDeleteEventStreamListener = ffm("api.endpoints.delete.eventstream.listener", "Delete event stream listener") APIParamStreamID = ffm("api.params.streamId", "Event Stream ID") APIParamListenerID = ffm("api.params.listenerId", "Listener ID") diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go index 1ebd0380..0a3ffa0b 100644 --- a/pkg/fftm/api_test.go +++ b/pkg/fftm/api_test.go @@ -116,7 +116,7 @@ func TestSendTransactionE2E(t *testing.T) { SetBody(req). Post(url) assert.NoError(t, err) - assert.Equal(t, 200, res.StatusCode()) + assert.Equal(t, 202, res.StatusCode()) <-txSent @@ -240,7 +240,7 @@ func TestQueryOK(t *testing.T) { SetResult(&queryRes). Post(url) assert.NoError(t, err) - assert.Equal(t, 200, res.StatusCode()) + assert.Equal(t, 202, res.StatusCode()) assert.Equal(t, `some output data`, queryRes) diff --git a/pkg/fftm/route__root_command.go b/pkg/fftm/route__root_command.go index 57b8f3f8..d4d910f1 100644 --- a/pkg/fftm/route__root_command.go +++ b/pkg/fftm/route__root_command.go @@ -30,20 +30,23 @@ import ( var postRootCommand = func(m *manager) *ffapi.Route { return &ffapi.Route{ - Name: "postRootCommand", - Path: "/", - Method: http.MethodPost, - PathParams: nil, - QueryParams: nil, - Description: tmmsgs.APIEndpointPostSubscriptions, - JSONInputValue: func() interface{} { return &apitypes.BaseRequest{} }, - JSONOutputValue: func() interface{} { return map[string]interface{}{} }, + Name: "postRootCommand", + Path: "/", + Method: http.MethodPost, + PathParams: nil, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostRoot, + JSONInputValue: func() interface{} { return &apitypes.BaseRequest{} }, JSONInputSchema: func(_ context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) { schemas := []*openapi3.SchemaRef{} txRequest, err := schemaGen(&apitypes.TransactionRequest{}) if err == nil { schemas = append(schemas, txRequest) } + deployRequest, err := schemaGen(&apitypes.ContractDeployRequest{}) + if err == nil { + schemas = append(schemas, deployRequest) + } queryRequest, err := schemaGen(&apitypes.QueryRequest{}) if err == nil { schemas = append(schemas, queryRequest) @@ -54,7 +57,20 @@ var postRootCommand = func(m *manager) *ffapi.Route { }, }, err }, - JSONOutputCodes: []int{http.StatusOK}, + JSONOutputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) { + managedTX, _ := schemaGen(&apitypes.QueryRequest{}) + return &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + AnyOf: openapi3.SchemaRefs{ + {Value: &openapi3.Schema{ + Description: i18n.Expand(ctx, tmmsgs.APIEndpointDeleteEventStream), + }}, + managedTX, + }, + }, + }, nil + }, + JSONOutputCodes: []int{http.StatusAccepted}, JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { baseReq := r.Input.(*apitypes.BaseRequest) switch baseReq.Headers.Type { @@ -64,6 +80,12 @@ var postRootCommand = func(m *manager) *ffapi.Route { return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) } return m.sendManagedTransaction(r.Req.Context(), &tReq) + case apitypes.RequestTypeDeploy: + var tReq apitypes.ContractDeployRequest + if err = baseReq.UnmarshalTo(&tReq); err != nil { + return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) + } + return m.sendManagedContractDeployment(r.Req.Context(), &tReq) case apitypes.RequestTypeQuery: var tReq apitypes.QueryRequest if err = baseReq.UnmarshalTo(&tReq); err != nil { @@ -76,12 +98,6 @@ var postRootCommand = func(m *manager) *ffapi.Route { return nil, err } return res.Outputs, nil - case apitypes.RequestTypeDeploy: - var tReq apitypes.ContractDeployRequest - if err = baseReq.UnmarshalTo(&tReq); err != nil { - return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) - } - return m.sendManagedContractDeployment(r.Req.Context(), &tReq) default: return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgUnsupportedRequestType, baseReq.Headers.Type) } diff --git a/pkg/fftm/route_post_eventstream_listener_reset.go b/pkg/fftm/route_post_eventstream_listener_reset.go index d8f1fe19..39741977 100644 --- a/pkg/fftm/route_post_eventstream_listener_reset.go +++ b/pkg/fftm/route_post_eventstream_listener_reset.go @@ -34,7 +34,7 @@ var postEventStreamListenerReset = func(m *manager) *ffapi.Route { {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, }, QueryParams: nil, - Description: tmmsgs.APIEndpointPatchEventStreamListener, + Description: tmmsgs.APIEndpointPostEventStreamListenerReset, JSONInputValue: func() interface{} { return &apitypes.Listener{} }, JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, JSONOutputCodes: []int{http.StatusOK}, diff --git a/pkg/fftm/route_post_eventstream_listeners.go b/pkg/fftm/route_post_eventstream_listeners.go index aac77479..c5e03f08 100644 --- a/pkg/fftm/route_post_eventstream_listeners.go +++ b/pkg/fftm/route_post_eventstream_listeners.go @@ -32,10 +32,7 @@ var postEventStreamListeners = func(m *manager) *ffapi.Route { PathParams: []*ffapi.PathParam{ {Name: "streamId", Description: tmmsgs.APIParamStreamID}, }, - QueryParams: []*ffapi.QueryParam{ - {Name: "limit", Description: tmmsgs.APIParamLimit}, - {Name: "after", Description: tmmsgs.APIParamAfter}, - }, + QueryParams: nil, Description: tmmsgs.APIEndpointPostEventStreamListener, JSONInputValue: func() interface{} { return &apitypes.Listener{} }, JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, diff --git a/pkg/fftm/route_post_subscription_reset.go b/pkg/fftm/route_post_subscription_reset.go index dba74721..d6f82f91 100644 --- a/pkg/fftm/route_post_subscription_reset.go +++ b/pkg/fftm/route_post_subscription_reset.go @@ -34,7 +34,7 @@ var postSubscriptionReset = func(m *manager) *ffapi.Route { {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, }, QueryParams: nil, - Description: tmmsgs.APIEndpointPatchSubscription, + Description: tmmsgs.APIEndpointPostSubscriptionReset, JSONInputValue: func() interface{} { return &apitypes.Listener{} }, JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, JSONOutputCodes: []int{http.StatusOK}, diff --git a/pkg/fftm/route_post_subscriptions.go b/pkg/fftm/route_post_subscriptions.go index f3e31186..313a5fd9 100644 --- a/pkg/fftm/route_post_subscriptions.go +++ b/pkg/fftm/route_post_subscriptions.go @@ -26,15 +26,12 @@ import ( var postSubscriptions = func(m *manager) *ffapi.Route { return &ffapi.Route{ - Name: "postSubscriptions", - Path: "/subscriptions", - Deprecated: true, // in favor of "/eventstreams/{id}/listeners" - Method: http.MethodPost, - PathParams: nil, - QueryParams: []*ffapi.QueryParam{ - {Name: "limit", Description: tmmsgs.APIParamLimit}, - {Name: "after", Description: tmmsgs.APIParamAfter}, - }, + Name: "postSubscriptions", + Path: "/subscriptions", + Deprecated: true, // in favor of "/eventstreams/{id}/listeners" + Method: http.MethodPost, + PathParams: nil, + QueryParams: nil, Description: tmmsgs.APIEndpointPostSubscriptions, JSONInputValue: func() interface{} { return &apitypes.Listener{} }, JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, From 7fba6f7b23571572f8496cf3743d0e3547a29b1e Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 4 Aug 2022 14:26:28 -0400 Subject: [PATCH 74/95] Updates to deploy Signed-off-by: Peter Broadhurst --- internal/tmmsgs/en_api_descriptions.go | 1 + pkg/apitypes/tx_request.go | 2 +- pkg/ffcapi/api.go | 7 -- pkg/ffcapi/contract_deploy_prepare.go | 28 ++++++++ pkg/ffcapi/transaction_prepare.go | 4 -- pkg/fftm/api_test.go | 86 +++++++++++++++++++++++++ pkg/fftm/route_get_transaction.go | 44 +++++++++++++ pkg/fftm/route_get_transaction_test.go | 46 +++++++++++++ pkg/fftm/route_get_transactions_test.go | 2 +- pkg/fftm/routes.go | 1 + pkg/fftm/send_tx.go | 4 +- pkg/fftm/transaction_management.go | 4 ++ 12 files changed, 213 insertions(+), 16 deletions(-) create mode 100644 pkg/ffcapi/contract_deploy_prepare.go create mode 100644 pkg/fftm/route_get_transaction.go create mode 100644 pkg/fftm/route_get_transaction_test.go diff --git a/internal/tmmsgs/en_api_descriptions.go b/internal/tmmsgs/en_api_descriptions.go index 3ab420f9..1070cac8 100644 --- a/internal/tmmsgs/en_api_descriptions.go +++ b/internal/tmmsgs/en_api_descriptions.go @@ -51,6 +51,7 @@ var ( APIParamStreamID = ffm("api.params.streamId", "Event Stream ID") APIParamListenerID = ffm("api.params.listenerId", "Listener ID") + APIParamTransactionID = ffm("api.params.transactionId", "Transaction ID") APIParamLimit = ffm("api.params.limit", "Maximum number of entries to return") APIParamAfter = ffm("api.params.after", "Return entries after this ID - for pagination (non-inclusive)") APIParamTXSigner = ffm("api.params.txSigner", "Return only transactions for a specific signing address, in reverse nonce order") diff --git a/pkg/apitypes/tx_request.go b/pkg/apitypes/tx_request.go index 2bd153a7..9da83fa5 100644 --- a/pkg/apitypes/tx_request.go +++ b/pkg/apitypes/tx_request.go @@ -29,5 +29,5 @@ type TransactionRequest struct { // ContractDeployRequest is the payload sent to initiate a new transaction type ContractDeployRequest struct { Headers RequestHeaders `json:"headers"` - ffcapi.ContractDeployInput + ffcapi.ContractDeployPrepareRequest } diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 4d8a63aa..2af799d8 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -188,13 +188,6 @@ type TransactionInput struct { Params []*fftypes.JSONAny `json:"params"` } -type ContractDeployInput struct { - TransactionHeaders - ABI *fftypes.JSONAny `json:"abi"` - Bytecode *fftypes.JSONAny `json:"bytecode"` - Params []*fftypes.JSONAny `json:"params"` -} - type TransactionHeaders struct { From string `json:"from,omitempty"` To string `json:"to,omitempty"` diff --git a/pkg/ffcapi/contract_deploy_prepare.go b/pkg/ffcapi/contract_deploy_prepare.go new file mode 100644 index 00000000..cde62ebd --- /dev/null +++ b/pkg/ffcapi/contract_deploy_prepare.go @@ -0,0 +1,28 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type ContractDeployPrepareRequest struct { + TransactionHeaders + Definition *fftypes.JSONAny `json:"definition"` // such as an ABI for EVM + Contract *fftypes.JSONAny `json:"contract"` // such as the Bytecode for EVM + Params []*fftypes.JSONAny `json:"params"` // such as the inputs to the constructor for EVM +} diff --git a/pkg/ffcapi/transaction_prepare.go b/pkg/ffcapi/transaction_prepare.go index 0e7c2c01..a9552d90 100644 --- a/pkg/ffcapi/transaction_prepare.go +++ b/pkg/ffcapi/transaction_prepare.go @@ -42,10 +42,6 @@ type TransactionPrepareRequest struct { TransactionInput } -type ContractDeployPrepareRequest struct { - ContractDeployInput -} - type TransactionPrepareResponse struct { Gas *fftypes.FFBigInt `json:"gas"` TransactionData string `json:"transactionData"` diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go index 0a3ffa0b..d3257d13 100644 --- a/pkg/fftm/api_test.go +++ b/pkg/fftm/api_test.go @@ -61,6 +61,32 @@ const sampleSendTX = `{ ] }` +const sampleDeployTX = `{ + "headers": { + "id": "ns1:904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "DeployContract" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "gas": 1000000, + "contract": "0xfeedbeef", + "definition": [{ + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + } + ], + "type":"constructor" + }], + "params": [ + { + "value": 4276993775, + "type": "uint256" + } + ] +}` + func TestSendTransactionE2E(t *testing.T) { txSent := make(chan struct{}) @@ -122,6 +148,66 @@ func TestSendTransactionE2E(t *testing.T) { } +func TestDeployTransactionE2E(t *testing.T) { + + txSent := make(chan struct{}) + + url, m, cancel := newTestManager(t) + defer cancel() + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(12345), + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("DeployContractPrepare", mock.Anything, mock.MatchedBy(func(prepTX *ffcapi.ContractDeployPrepareRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == prepTX.From && + `constructor` == prepTX.Definition.JSONObjectArray()[0].GetString("type") && + `"0xfeedbeef"` == prepTX.Contract.String() && + uint64(1000000) == prepTX.Gas.Uint64() && + 1 == len(prepTX.Params) && + "4276993775" == prepTX.Params[0].JSONObject().GetString("value") && + "4276993775" == prepTX.Params[0].JSONObject().GetString("value") + })).Return(&ffcapi.TransactionPrepareResponse{ + TransactionData: "RAW_UNSIGNED_BYTES", + Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("TransactionSend", mock.Anything, mock.MatchedBy(func(sendTX *ffcapi.TransactionSendRequest) bool { + matches := "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == sendTX.From && + uint64(2000000) == sendTX.Gas.Uint64() && + `223344556677` == sendTX.GasPrice.String() && + "RAW_UNSIGNED_BYTES" == sendTX.TransactionData + if matches { + // We're at end of job for this test + close(txSent) + } + return matches + })).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: "0x106215b9c0c9372e3f541beff0cdc3cd061a26f69f3808e28fd139a1abc9d345", + }, ffcapi.ErrorReason(""), nil) + + mc := m.confirmations.(*confirmationsmocks.Manager) + mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { + return n.NotificationType == confirmations.NewTransaction + })).Return(nil) + + m.Start() + + req := strings.NewReader(sampleDeployTX) + res, err := resty.New().R(). + SetBody(req). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 202, res.StatusCode()) + + <-txSent + +} + func TestSendInvalidRequestBadTXType(t *testing.T) { url, m, cancel := newTestManager(t) diff --git a/pkg/fftm/route_get_transaction.go b/pkg/fftm/route_get_transaction.go new file mode 100644 index 00000000..2125abd3 --- /dev/null +++ b/pkg/fftm/route_get_transaction.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getTransaction = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getTransaction", + Path: "/transactions/{transactionId}", + Method: http.MethodGet, + PathParams: []*ffapi.PathParam{ + {Name: "transactionId", Description: tmmsgs.APIParamTransactionID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointGetSubscriptions, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.ManagedTX{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getTransactionByID(r.Req.Context(), r.PP["transactionId"]) + }, + } +} diff --git a/pkg/fftm/route_get_transaction_test.go b/pkg/fftm/route_get_transaction_test.go new file mode 100644 index 00000000..286fe79a --- /dev/null +++ b/pkg/fftm/route_get_transaction_test.go @@ -0,0 +1,46 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestGetTransaction(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + txIn := newTestTxn(t, m, "0xaaaaa", 10001, apitypes.TxStatusSucceeded) + + var txOut *apitypes.ManagedTX + res, err := resty.New().R(). + SetResult(&txOut). + Get(fmt.Sprintf("%s/transactions/%s", url, txIn.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.Equal(t, *txIn, *txOut) + +} diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go index e0c97605..3a694749 100644 --- a/pkg/fftm/route_get_transactions_test.go +++ b/pkg/fftm/route_get_transactions_test.go @@ -30,7 +30,7 @@ import ( func newTestTxn(t *testing.T, m *manager, signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { tx := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + ID: fmt.Sprintf("ns1:%s", fftypes.NewUUID()), Created: fftypes.Now(), SequenceID: apitypes.UUIDVersion1(), Nonce: fftypes.NewFFBigInt(nonce), diff --git a/pkg/fftm/routes.go b/pkg/fftm/routes.go index c78fafdd..04f57582 100644 --- a/pkg/fftm/routes.go +++ b/pkg/fftm/routes.go @@ -29,6 +29,7 @@ func (m *manager) routes() []*ffapi.Route { getEventStreams(m), getSubscription(m), getSubscriptions(m), + getTransaction(m), getTransactions(m), patchEventStream(m), patchEventStreamListener(m), diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index 833b321d..c4b21669 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -44,9 +44,7 @@ func (m *manager) sendManagedContractDeployment(ctx context.Context, request *ap // Prepare the transaction, which will mean we have a transaction that should be submittable. // If we fail at this stage, we don't need to write any state as we are sure we haven't submitted // anything to the blockchain itself. - prepared, _, err := m.connector.DeployContractPrepare(ctx, &ffcapi.ContractDeployPrepareRequest{ - ContractDeployInput: request.ContractDeployInput, - }) + prepared, _, err := m.connector.DeployContractPrepare(ctx, &request.ContractDeployPrepareRequest) if err != nil { return nil, err } diff --git a/pkg/fftm/transaction_management.go b/pkg/fftm/transaction_management.go index c1f2671f..fa897318 100644 --- a/pkg/fftm/transaction_management.go +++ b/pkg/fftm/transaction_management.go @@ -27,6 +27,10 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" ) +func (m *manager) getTransactionByID(ctx context.Context, txID string) (transaction *apitypes.ManagedTX, err error) { + return m.persistence.GetTransactionByID(ctx, txID) +} + func (m *manager) getTransactions(ctx context.Context, afterStr, limitStr, signer string, pending bool, dirString string) (transactions []*apitypes.ManagedTX, err error) { limit, err := m.parseLimit(ctx, limitStr) if err != nil { From ad38af735a2c23443e11e17b41fc03e9d478c8cd Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 4 Aug 2022 17:26:53 -0400 Subject: [PATCH 75/95] Update sync between confirmation manager and policy loop Signed-off-by: Peter Broadhurst --- pkg/fftm/manager.go | 24 +++++++++++++----------- pkg/fftm/policyloop.go | 21 +++++++++++++++++++-- pkg/fftm/policyloop_test.go | 10 ++++++++++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index ed583ec4..cb0c9a88 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -44,17 +44,18 @@ type Manager interface { } type manager struct { - ctx context.Context - cancelCtx func() - retry *retry.Retry - connector ffcapi.API - confirmations confirmations.Manager - policyEngine policyengine.PolicyEngine - apiServer httpserver.HTTPServer - wsServer ws.WebSocketServer - persistence persistence.Persistence - inflightStale chan bool - inflight []*pendingState + ctx context.Context + cancelCtx func() + retry *retry.Retry + connector ffcapi.API + confirmations confirmations.Manager + policyEngine policyengine.PolicyEngine + apiServer httpserver.HTTPServer + wsServer ws.WebSocketServer + persistence persistence.Persistence + inflightStale chan bool + inflightUpdate chan bool + inflight []*pendingState mux sync.Mutex lockedNonces map[string]*lockedNonce @@ -106,6 +107,7 @@ func newManager(ctx context.Context, connector ffcapi.API) *manager { errorHistoryCount: config.GetInt(tmconfig.TransactionsErrorHistoryCount), maxInFlight: config.GetInt(tmconfig.TransactionsMaxInFlight), inflightStale: make(chan bool, 1), + inflightUpdate: make(chan bool, 1), retry: &retry.Retry{ InitialDelay: config.GetDuration(tmconfig.PolicyLoopRetryInitDelay), MaximumDelay: config.GetDuration(tmconfig.PolicyLoopRetryMaxDelay), diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index efc53141..7ae3e90e 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -35,6 +35,8 @@ func (m *manager) policyLoop() { for { timer := time.NewTimer(m.policyLoopInterval) select { + case <-m.inflightUpdate: + m.policyLoopCycle(false) case <-m.inflightStale: m.policyLoopCycle(true) case <-timer.C: @@ -53,6 +55,13 @@ func (m *manager) markInflightStale() { } } +func (m *manager) markInflightUpdate() { + select { + case m.inflightUpdate <- true: + default: + } +} + func (m *manager) updateInflightSet() bool { oldInflight := m.inflight @@ -131,9 +140,15 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { var updated bool completed := false + + // Check whether this has been confirmed by the confirmation manager + m.mux.Lock() mtx := pending.mtx + confirmed := pending.confirmed + m.mux.Unlock() + switch { - case pending.confirmed: + case confirmed: updated = true completed = true if mtx.Receipt.Success { @@ -166,7 +181,7 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { return err } if completed { - pending.remove = true + pending.remove = true // for the next time round the loop m.markInflightStale() } m.sendWSReply(mtx) @@ -218,6 +233,7 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { m.mux.Lock() pending.mtx.Receipt = receipt m.mux.Unlock() + m.markInflightUpdate() }, Confirmed: func(confirmations []confirmations.BlockInfo) { // Will be picked up on the next policy loop cycle @@ -225,6 +241,7 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { pending.confirmed = true pending.mtx.Confirmations = confirmations m.mux.Unlock() + m.markInflightUpdate() }, }, }) diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index 63bfe6b3..547a560c 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -336,3 +336,13 @@ func TestMarkInflightStaleDoesNotBlock(t *testing.T) { m.markInflightStale() } + +func TestMarkInflightUpdateDoesNotBlock(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + m.markInflightUpdate() + m.markInflightUpdate() + +} From 4bfe60cfbe8111db781847534d226ec9ece11707 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 4 Aug 2022 22:18:52 -0400 Subject: [PATCH 76/95] Updates to policy loop Signed-off-by: Peter Broadhurst --- pkg/fftm/api_test.go | 50 ++++++++++++++++++++++ pkg/fftm/manager.go | 2 + pkg/fftm/manager_test.go | 2 +- pkg/fftm/policyloop.go | 28 ++++++++----- pkg/fftm/policyloop_test.go | 26 ++++++++++-- pkg/fftm/route_get_transactions_test.go | 10 ++--- pkg/fftm/send_tx_test.go | 54 ++++++++++++++++++++++++ pkg/fftm/transaction_management_test.go | 55 +++++++++++++++++++++++++ 8 files changed, 208 insertions(+), 19 deletions(-) create mode 100644 pkg/fftm/send_tx_test.go create mode 100644 pkg/fftm/transaction_management_test.go diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go index d3257d13..d71e70b3 100644 --- a/pkg/fftm/api_test.go +++ b/pkg/fftm/api_test.go @@ -232,6 +232,30 @@ func TestSendInvalidRequestBadTXType(t *testing.T) { assert.Regexp(t, "FF21022", errRes.Error) } +func TestSendInvalidDeployBadTXType(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + req := strings.NewReader(`{ + "headers": { + "type": "DeployContract" + }, + "from": { + "Not": "a string" + } + }`) + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetBody(req). + SetError(&errRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 400, res.StatusCode()) + assert.Regexp(t, "FF21022", errRes.Error) +} + func TestSwaggerEndpoints(t *testing.T) { url, m, cancel := newTestManager(t) @@ -299,6 +323,32 @@ func TestSendTransactionPrepareFail(t *testing.T) { } +func TestDeployContractPrepareFail(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(12345), + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("DeployContractPrepare", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + m.Start() + + req := strings.NewReader(sampleDeployTX) + res, err := resty.New().R(). + SetBody(req). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 500, res.StatusCode()) + +} + func TestQueryOK(t *testing.T) { url, m, cancel := newTestManager(t) diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index cb0c9a88..6c5bc5e0 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -106,6 +106,7 @@ func newManager(ctx context.Context, connector ffcapi.API) *manager { policyLoopInterval: config.GetDuration(tmconfig.PolicyLoopInterval), errorHistoryCount: config.GetInt(tmconfig.TransactionsErrorHistoryCount), maxInFlight: config.GetInt(tmconfig.TransactionsMaxInFlight), + nonceStateTimeout: config.GetDuration(tmconfig.TransactionsNonceStateTimeout), inflightStale: make(chan bool, 1), inflightUpdate: make(chan bool, 1), retry: &retry.Retry{ @@ -120,6 +121,7 @@ func newManager(ctx context.Context, connector ffcapi.API) *manager { type pendingState struct { mtx *apitypes.ManagedTX + lastPolicyCycle time.Time confirmed bool remove bool trackingTransactionHash string diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index df543240..5afd798e 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -59,7 +59,7 @@ func newTestManager(t *testing.T) (string, *manager, func()) { tmconfig.APIConfig.Set(httpserver.HTTPConfPort, managerPort) tmconfig.APIConfig.Set(httpserver.HTTPConfAddress, "127.0.0.1") - config.Set(tmconfig.PolicyLoopInterval, "1ms") + config.Set(tmconfig.PolicyLoopInterval, "1ns") tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") mm, err := NewManager(context.Background(), &ffcapimocks.API{}) diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index 7ae3e90e..0c498cb6 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -160,16 +160,24 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { } default: - // Pass the state to the pluggable policy engine to potentially perform more actions against it, - // such as submitting for the first time, or raising the gas etc. - var reason ffcapi.ErrorReason - updated, reason, err = m.policyEngine.Execute(m.ctx, m.connector, pending.mtx) - if err != nil { - log.L(m.ctx).Errorf("Policy engine returned error for operation %s reason=%s: %s", mtx.ID, reason, err) - m.addError(mtx, reason, err) - } else if mtx.FirstSubmit != nil && pending.trackingTransactionHash != mtx.TransactionHash { - // If now submitted, add to confirmations manager for receipt checking - m.trackSubmittedTransaction(pending) + // We get woken for lots of reasons to go through the policy loop, but we only want + // to drive the policy engine at regular intervals. + // So we track the last time we ran the policy engine against each pending item. + if time.Since(pending.lastPolicyCycle) > m.policyLoopInterval { + // Pass the state to the pluggable policy engine to potentially perform more actions against it, + // such as submitting for the first time, or raising the gas etc. + var reason ffcapi.ErrorReason + updated, reason, err = m.policyEngine.Execute(m.ctx, m.connector, pending.mtx) + if err != nil { + log.L(m.ctx).Errorf("Policy engine returned error for operation %s reason=%s: %s", mtx.ID, reason, err) + m.addError(mtx, reason, err) + } else { + if mtx.FirstSubmit != nil && pending.trackingTransactionHash != mtx.TransactionHash { + // If now submitted, add to confirmations manager for receipt checking + m.trackSubmittedTransaction(pending) + } + pending.lastPolicyCycle = time.Now() + } } } diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index 547a560c..9cf24cc2 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -19,6 +19,7 @@ package fftm import ( "fmt" "testing" + "time" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" @@ -309,19 +310,38 @@ func TestPolicyLoopUpdateFail(t *testing.T) { } -func TestPolicyEngineFail(t *testing.T) { +func TestPolicyEngineFailStaleThenUpdated(t *testing.T) { _, m, cancel := newTestManager(t) defer cancel() + m.policyLoopInterval = 1 * time.Hour mpe := &policyenginemocks.PolicyEngine{} m.policyEngine = mpe + done1 := make(chan struct{}) mpe.On("Execute", mock.Anything, mock.Anything, mock.Anything). - Return(false, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + Return(false, ffcapi.ErrorReason(""), fmt.Errorf("pop")). + Once(). + Run(func(args mock.Arguments) { + close(done1) + m.markInflightUpdate() + }) + + done2 := make(chan struct{}) + mpe.On("Execute", mock.Anything, mock.Anything, mock.Anything). + Return(false, ffcapi.ErrorReason(""), fmt.Errorf("pop")). + Once(). + Run(func(args mock.Arguments) { + close(done2) + }) _ = sendSampleTX(t, m, "0xaaaaa", 12345) - m.policyLoopCycle(true) + m.Start() + + <-done1 + + <-done2 mpe.AssertExpectations(t) diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go index 3a694749..4103412c 100644 --- a/pkg/fftm/route_get_transactions_test.go +++ b/pkg/fftm/route_get_transactions_test.go @@ -62,14 +62,14 @@ func TestGetTransactions(t *testing.T) { var transactions []*apitypes.ManagedTX res, err := resty.New().R(). SetResult(&transactions). - Get(url + "/transactions") + Get(url + "/transactions?direction=asc") assert.NoError(t, err) assert.Equal(t, 200, res.StatusCode()) assert.Len(t, transactions, 4) - assert.Equal(t, s1t3.ID, transactions[0].ID) - assert.Equal(t, s1t2.ID, transactions[1].ID) - assert.Equal(t, s2t1.ID, transactions[2].ID) - assert.Equal(t, s1t1.ID, transactions[3].ID) + assert.Equal(t, s1t1.ID, transactions[0].ID) + assert.Equal(t, s2t1.ID, transactions[1].ID) + assert.Equal(t, s1t2.ID, transactions[2].ID) + assert.Equal(t, s1t3.ID, transactions[3].ID) // Test pagination on default sort/filter res, err = resty.New().R(). diff --git a/pkg/fftm/send_tx_test.go b/pkg/fftm/send_tx_test.go new file mode 100644 index 00000000..1e8a4359 --- /dev/null +++ b/pkg/fftm/send_tx_test.go @@ -0,0 +1,54 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestSendTXPersistFail(t *testing.T) { + + _, m, cancel := newTestManager(t) + cancel() + + mp := &persistencemocks.Persistence{} + m.persistence = mp + + mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]*apitypes.ManagedTX{ + {ID: "id12345", Created: fftypes.Now(), Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, + }, nil) + mp.On("WriteTransaction", m.ctx, mock.Anything, true).Return(fmt.Errorf("pop")) + mp.On("Close", mock.Anything).Return(nil).Maybe() + + var txReq *ffcapi.TransactionSendRequest + err := json.Unmarshal([]byte(sampleSendTX), &txReq) + assert.NoError(t, err) + + _, err = m.submitPreparedTX(m.ctx, "id1", &txReq.TransactionHeaders, fftypes.NewFFBigInt(12345), "0x123456") + assert.Regexp(t, "pop", err) + +} diff --git a/pkg/fftm/transaction_management_test.go b/pkg/fftm/transaction_management_test.go new file mode 100644 index 00000000..9752d68d --- /dev/null +++ b/pkg/fftm/transaction_management_test.go @@ -0,0 +1,55 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 fftm + +import ( + "fmt" + "testing" + + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetTransactionsErrors(t *testing.T) { + + _, m, cancel := newTestManager(t) + cancel() + + mp := &persistencemocks.Persistence{} + m.persistence = mp + + mp.On("GetTransactionByID", m.ctx, mock.Anything).Return(nil, fmt.Errorf("pop")).Once() + mp.On("GetTransactionByID", m.ctx, mock.Anything).Return(nil, nil).Once() + mp.On("Close", mock.Anything).Return(nil).Maybe() + + _, err := m.getTransactions(m.ctx, "", "bad limit", "", false, "") + assert.Regexp(t, "FF21044", err) + + _, err = m.getTransactions(m.ctx, "", "", "", false, "wrong") + assert.Regexp(t, "FF21064", err) + + _, err = m.getTransactions(m.ctx, "", "", "cannot be specified with pending", true, "") + assert.Regexp(t, "FF21063", err) + + _, err = m.getTransactions(m.ctx, "after-causes-failure", "", "", false, "") + assert.Regexp(t, "pop", err) + + _, err = m.getTransactions(m.ctx, "after-not-found", "", "", false, "") + assert.Regexp(t, "FF21062", err) + +} From dfdee60eafe1a123c3dcace24a2e7d414d794dd5 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 5 Aug 2022 23:54:24 -0400 Subject: [PATCH 77/95] Add a block listener for the receipts confirmation manager too Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 1 + internal/blocklistener/blocklistener.go | 85 +++++++++++++ .../blocklistener_test.go | 56 ++++----- internal/events/blocklistener.go | 74 ----------- internal/events/eventstream.go | 13 +- mocks/ffcapimocks/api.go | 30 +++++ mocks/persistencemocks/persistence.go | 8 +- pkg/ffcapi/api.go | 3 + pkg/ffcapi/event_stream_start copy.go | 29 +++++ pkg/fftm/manager.go | 49 +++++--- pkg/fftm/manager_test.go | 72 ++++++++--- pkg/fftm/nonces_test.go | 25 ++-- pkg/fftm/policyloop_test.go | 15 +-- pkg/fftm/send_tx_test.go | 9 +- pkg/fftm/stream_management_test.go | 117 +++++++++++++----- pkg/fftm/transaction_management_test.go | 8 +- 16 files changed, 380 insertions(+), 214 deletions(-) create mode 100644 internal/blocklistener/blocklistener.go rename internal/{events => blocklistener}/blocklistener_test.go (58%) delete mode 100644 internal/events/blocklistener.go create mode 100644 pkg/ffcapi/event_stream_start copy.go diff --git a/.vscode/settings.json b/.vscode/settings.json index cd5f120b..a6a1789a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "APIID", "apitypes", "badurl", + "blocklistener", "ccache", "confirmationsmocks", "dataexchange", diff --git a/internal/blocklistener/blocklistener.go b/internal/blocklistener/blocklistener.go new file mode 100644 index 00000000..98c0c55a --- /dev/null +++ b/internal/blocklistener/blocklistener.go @@ -0,0 +1,85 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 blocklistener + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +type NewBlockHashConsumer interface { + NewBlockHashes() chan<- *ffcapi.BlockHashEvent +} + +// BufferChannel ensures it always pulls blocks from the channel passed to the connector +// for new block events, regardless of whether the downstream confirmations update queue +// is full blocked (likely because the event stream is blocked). +// This is critical to avoid the situation where one blocked stream, stops another stream +// from receiving block events. +// We use the same "GapPotential" flag that the connector can mark on a reconnect, to mark +// when we've had to discard events for a blocked event listener (event listeners could stay +// blocked indefinitely, so we can't leak memory by storing up an indefinite number of new +// block events). +func BufferChannel(ctx context.Context, target NewBlockHashConsumer) (buffered chan *ffcapi.BlockHashEvent, done chan struct{}) { + buffered = make(chan *ffcapi.BlockHashEvent) + done = make(chan struct{}) + go func() { + defer close(done) + var blockedUpdate *ffcapi.BlockHashEvent + for { + if blockedUpdate != nil { + select { + case blockUpdate := <-buffered: + // Have to discard this + blockedUpdate.GapPotential = true // there is a gap for sure at this point + log.L(ctx).Debugf("Blocked event stream missed new block event: %v", blockUpdate.BlockHashes) + case target.NewBlockHashes() <- blockedUpdate: + // We're not blocked any more + log.L(ctx).Infof("Event stream block-listener unblocked") + blockedUpdate = nil + case <-ctx.Done(): + log.L(ctx).Debugf("Block listener exiting (previously blocked)") + return + } + } else { + select { + case update := <-buffered: + log.L(ctx).Debugf("Received block event: %v", update.BlockHashes) + // Nothing to do unless we have confirmations turned on + if target != nil { + select { + case target.NewBlockHashes() <- update: + // all good, we passed it on + default: + // we can't deliver it immediately, we switch to blocked mode + log.L(ctx).Infof("Event stream block-listener became blocked") + // Take a copy of the block update, so we can modify (to mark a gap) without affecting other streams + var bu = *update + blockedUpdate = &bu + } + } + case <-ctx.Done(): + log.L(ctx).Debugf("Block listener exiting") + return + } + } + } + }() + return buffered, done +} diff --git a/internal/events/blocklistener_test.go b/internal/blocklistener/blocklistener_test.go similarity index 58% rename from internal/events/blocklistener_test.go rename to internal/blocklistener/blocklistener_test.go index e51a97e0..b4d8ed29 100644 --- a/internal/events/blocklistener_test.go +++ b/internal/blocklistener/blocklistener_test.go @@ -14,61 +14,61 @@ // See the License for the specific language governing permissions and // limitations under the License. -package events +package blocklistener import ( "context" - "sync" "testing" - "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" ) -func TestBlockListenerDoesNotBlock(t *testing.T) { +type testBlockConsumer struct { + c chan *ffcapi.BlockHashEvent +} - es := newTestEventStream(t, `{ - "name": "ut_stream" - }`) +func (tbc *testBlockConsumer) NewBlockHashes() chan<- *ffcapi.BlockHashEvent { + return tbc.c +} - ss := &startedStreamState{ - blocks: make(chan *ffcapi.BlockHashEvent, 1), - blockListenerDone: make(chan struct{}), - } - ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) +func TestBlockListenerDoesNotBlock(t *testing.T) { - blockIt := make(chan *ffcapi.BlockHashEvent, 1) - mcm := &confirmationsmocks.Manager{} - mcm.On("NewBlockHashes").Return((chan<- *ffcapi.BlockHashEvent)(blockIt)) - es.confirmations = mcm + unBuffered := make(chan *ffcapi.BlockHashEvent, 1) + ctx, cancelCtx := context.WithCancel(context.Background()) - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - es.blockListener(ss) - wg.Done() - }() + buffered, blockListenerDone := BufferChannel(ctx, &testBlockConsumer{c: unBuffered}) for i := 0; i < 100; i++ { - ss.blocks <- &ffcapi.BlockHashEvent{} + buffered <- &ffcapi.BlockHashEvent{} } // Get the one that was stuck in the pipe - bhe := <-blockIt + bhe := <-unBuffered assert.False(t, bhe.GapPotential) // We should get the unblocking one too, with GapPotential set - bhe = <-blockIt + bhe = <-unBuffered assert.True(t, bhe.GapPotential) // Block it again for i := 0; i < 100; i++ { - ss.blocks <- &ffcapi.BlockHashEvent{} + buffered <- &ffcapi.BlockHashEvent{} } // And check we can exit while blocked - ss.cancelCtx() - wg.Wait() + cancelCtx() + <-blockListenerDone + +} + +func TestExitOnContextCancel(t *testing.T) { + + unBuffered := make(chan *ffcapi.BlockHashEvent) + ctx, cancelCtx := context.WithCancel(context.Background()) + cancelCtx() + + _, blockListenerDone := BufferChannel(ctx, &testBlockConsumer{c: unBuffered}) + <-blockListenerDone } diff --git a/internal/events/blocklistener.go b/internal/events/blocklistener.go deleted file mode 100644 index 4840deee..00000000 --- a/internal/events/blocklistener.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// 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 events - -import ( - "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" -) - -// blockListener ensures it always pulls blocks from the channel passed to the connector -// for new block events, regardless of whether the downstream confirmations update queue -// is full blocked (likely because the event stream is blocked). -// This is critical to avoid the situation where one blocked stream, stops another stream -// from receiving block events. -// We use the same "GapPotential" flag that the connector can mark on a reconnect, to mark -// when we've had to discard events for a blocked event listener (event listeners could stay -// blocked indefinitely, so we can't leak memory by storing up an indefinite number of new -// block events). -func (es *eventStream) blockListener(startedState *startedStreamState) { - defer close(startedState.blockListenerDone) - var blockedUpdate *ffcapi.BlockHashEvent - for { - if blockedUpdate != nil { - select { - case blockUpdate := <-startedState.blocks: - // Have to discard this - blockedUpdate.GapPotential = true // there is a gap for sure at this point - log.L(startedState.ctx).Debugf("Blocked event stream missed new block event: %v", blockUpdate.BlockHashes) - case es.confirmations.NewBlockHashes() <- blockedUpdate: - // We're not blocked any more - log.L(startedState.ctx).Infof("Event stream block-listener unblocked") - blockedUpdate = nil - case <-startedState.ctx.Done(): - log.L(startedState.ctx).Debugf("Block listener exiting (previously blocked)") - return - } - } else { - select { - case update := <-startedState.blocks: - log.L(startedState.ctx).Debugf("Received block event: %v", update.BlockHashes) - // Nothing to do unless we have confirmations turned on - if es.confirmations != nil { - select { - case es.confirmations.NewBlockHashes() <- update: - // all good, we passed it on - default: - // we can't deliver it immediately, we switch to blocked mode - log.L(startedState.ctx).Infof("Event stream block-listener became blocked") - // Take a copy of the block update, so we can modify (to mark a gap) without affecting other streams - var bu = *update - blockedUpdate = &bu - } - } - case <-startedState.ctx.Done(): - log.L(startedState.ctx).Debugf("Block listener exiting") - return - } - } - } -} diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index d0be00ae..783b39b5 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -27,6 +27,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-common/pkg/retry" + "github.com/hyperledger/firefly-transaction-manager/internal/blocklistener" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" @@ -474,12 +475,10 @@ func (es *eventStream) Start(ctx context.Context) error { log.L(ctx).Infof("Starting event stream %s", es) startedState := &startedStreamState{ - startTime: fftypes.Now(), - eventLoopDone: make(chan struct{}), - batchLoopDone: make(chan struct{}), - blockListenerDone: make(chan struct{}), - updates: make(chan *ffcapi.ListenerEvent, int(*es.spec.BatchSize)), - blocks: make(chan *ffcapi.BlockHashEvent), // we promise to consume immediately + startTime: fftypes.Now(), + eventLoopDone: make(chan struct{}), + batchLoopDone: make(chan struct{}), + updates: make(chan *ffcapi.ListenerEvent, int(*es.spec.BatchSize)), } startedState.ctx, startedState.cancelCtx = context.WithCancel(es.bgCtx) es.currentState = startedState @@ -509,7 +508,7 @@ func (es *eventStream) Start(ctx context.Context) error { // Kick off the loops go es.eventLoop(startedState) go es.batchLoop(startedState) - go es.blockListener(startedState) + startedState.blocks, startedState.blockListenerDone = blocklistener.BufferChannel(startedState.ctx, es.confirmations) // Start the confirmations manager if es.confirmations != nil { diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index cdc9d4c0..aa4a30a4 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -330,6 +330,36 @@ func (_m *API) GasPriceEstimate(ctx context.Context, req *ffcapi.GasPriceEstimat return r0, r1, r2 } +// NewBlockListener provides a mock function with given fields: ctx, req +func (_m *API) NewBlockListener(ctx context.Context, req *ffcapi.NewBlockListenerRequest) (*ffcapi.NewBlockListenerResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.NewBlockListenerResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.NewBlockListenerRequest) *ffcapi.NewBlockListenerResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.NewBlockListenerResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.NewBlockListenerRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.NewBlockListenerRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // NextNonceForSigner provides a mock function with given fields: ctx, req func (_m *API) NextNonceForSigner(ctx context.Context, req *ffcapi.NextNonceForSignerRequest) (*ffcapi.NextNonceForSignerResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go index 4153cf39..309fa3dd 100644 --- a/mocks/persistencemocks/persistence.go +++ b/mocks/persistencemocks/persistence.go @@ -375,13 +375,13 @@ func (_m *Persistence) WriteStream(ctx context.Context, spec *apitypes.EventStre return r0 } -// WriteTransaction provides a mock function with given fields: ctx, tx, possiblyNew -func (_m *Persistence) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, possiblyNew bool) error { - ret := _m.Called(ctx, tx, possiblyNew) +// WriteTransaction provides a mock function with given fields: ctx, tx, new +func (_m *Persistence) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, new bool) error { + ret := _m.Called(ctx, tx, new) var r0 error if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, bool) error); ok { - r0 = rf(ctx, tx, possiblyNew) + r0 = rf(ctx, tx, new) } else { r0 = ret.Error(0) } diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 2af799d8..02eb3381 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -76,6 +76,9 @@ type API interface { // EventStreamNewCheckpointStruct used during checkpoint restore, to get the specific into which to restore the JSON bytes EventStreamNewCheckpointStruct() EventListenerCheckpoint + + // NewBlockListener creates a new block listener, decoupled from an event stream + NewBlockListener(ctx context.Context, req *NewBlockListenerRequest) (*NewBlockListenerResponse, ErrorReason, error) } type BlockHashEvent struct { diff --git a/pkg/ffcapi/event_stream_start copy.go b/pkg/ffcapi/event_stream_start copy.go new file mode 100644 index 00000000..bcd1733c --- /dev/null +++ b/pkg/ffcapi/event_stream_start copy.go @@ -0,0 +1,29 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ffcapi + +import ( + "context" +) + +type NewBlockListenerRequest struct { + ListenerContext context.Context // Context that will be cancelled when the listener needs to stop - no further events will be consumed after this, so all pushes to the listener should select on the done channel too + BlockListener chan<- *BlockHashEvent // The connector should push new blocks to every listener, marking if it's possible blocks were missed (due to reconnect). The listener guarantees to always consume from this channel, until the listener context closes. +} + +type NewBlockListenerResponse struct { +} diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index 6c5bc5e0..b01e6bcd 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -26,6 +26,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/httpserver" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/retry" + "github.com/hyperledger/firefly-transaction-manager/internal/blocklistener" "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" "github.com/hyperledger/firefly-transaction-manager/internal/events" "github.com/hyperledger/firefly-transaction-manager/internal/persistence" @@ -57,13 +58,14 @@ type manager struct { inflightUpdate chan bool inflight []*pendingState - mux sync.Mutex - lockedNonces map[string]*lockedNonce - eventStreams map[fftypes.UUID]events.Stream - streamsByName map[string]*fftypes.UUID - policyLoopDone chan struct{} - started bool - apiServerDone chan error + mux sync.Mutex + lockedNonces map[string]*lockedNonce + eventStreams map[fftypes.UUID]events.Stream + streamsByName map[string]*fftypes.UUID + policyLoopDone chan struct{} + blockListenerDone chan struct{} + started bool + apiServerDone chan error policyLoopInterval time.Duration nonceStateTimeout time.Duration @@ -79,14 +81,7 @@ func InitConfig() { func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { var err error m := newManager(ctx, connector) - m.confirmations = confirmations.NewBlockConfirmationManager(ctx, m.connector) - m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBaseConfig, config.GetString(tmconfig.PolicyEngineName)) - if err != nil { - return nil, err - } - m.wsServer = ws.NewWebSocketServer(ctx) - m.apiServer, err = httpserver.NewHTTPServer(ctx, "api", m.router(), m.apiServerDone, tmconfig.APIConfig, tmconfig.CorsConfig) - if err != nil { + if err = m.initServices(ctx); err != nil { return nil, err } if err = m.initPersistence(ctx); err != nil { @@ -127,6 +122,20 @@ type pendingState struct { trackingTransactionHash string } +func (m *manager) initServices(ctx context.Context) (err error) { + m.confirmations = confirmations.NewBlockConfirmationManager(ctx, m.connector) + m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBaseConfig, config.GetString(tmconfig.PolicyEngineName)) + if err != nil { + return err + } + m.wsServer = ws.NewWebSocketServer(ctx) + m.apiServer, err = httpserver.NewHTTPServer(ctx, "api", m.router(), m.apiServerDone, tmconfig.APIConfig, tmconfig.CorsConfig) + if err != nil { + return err + } + return nil +} + func (m *manager) initPersistence(ctx context.Context) (err error) { pType := config.GetString(tmconfig.PersistenceType) switch pType { @@ -144,11 +153,20 @@ func (m *manager) Start() error { if err := m.restoreStreams(); err != nil { return err } + + blReq := &ffcapi.NewBlockListenerRequest{ListenerContext: m.ctx} + blReq.BlockListener, m.blockListenerDone = blocklistener.BufferChannel(m.ctx, m.confirmations) + _, _, err := m.connector.NewBlockListener(m.ctx, blReq) + if err != nil { + return err + } + go m.runAPIServer() m.policyLoopDone = make(chan struct{}) m.markInflightStale() go m.policyLoop() go m.confirmations.Start() + m.started = true return nil } @@ -159,6 +177,7 @@ func (m *manager) Close() { m.started = false <-m.apiServerDone <-m.policyLoopDone + <-m.blockListenerDone streams := []events.Stream{} m.mux.Lock() diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go index 5afd798e..9c457d01 100644 --- a/pkg/fftm/manager_test.go +++ b/pkg/fftm/manager_test.go @@ -44,12 +44,9 @@ const testManagerName = "unittest" func strPtr(s string) *string { return &s } -func newTestManager(t *testing.T) (string, *manager, func()) { +func testManagerCommonInit(t *testing.T) string { InitConfig() policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) - dir, err := ioutil.TempDir("", "ldb_*") - assert.NoError(t, err) - config.Set(tmconfig.PersistenceLevelDBPath, dir) tmconfig.PolicyEngineBaseConfig.SubSection("simple").SubSection(simple.GasOracleConfig).Set(simple.GasOracleMode, simple.GasOracleModeDisabled) ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -62,14 +59,28 @@ func newTestManager(t *testing.T) (string, *manager, func()) { config.Set(tmconfig.PolicyLoopInterval, "1ns") tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") - mm, err := NewManager(context.Background(), &ffcapimocks.API{}) + return fmt.Sprintf("http://127.0.0.1:%s", managerPort) +} + +func newTestManager(t *testing.T) (string, *manager, func()) { + + url := testManagerCommonInit(t) + + dir, err := ioutil.TempDir("", "ldb_*") + assert.NoError(t, err) + config.Set(tmconfig.PersistenceLevelDBPath, dir) + + mca := &ffcapimocks.API{} + mca.On("NewBlockListener", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil).Maybe() + mm, err := NewManager(context.Background(), mca) assert.NoError(t, err) + m := mm.(*manager) mcm := &confirmationsmocks.Manager{} mcm.On("Start").Return().Maybe() m.confirmations = mcm - return fmt.Sprintf("http://127.0.0.1:%s", managerPort), + return url, m, func() { m.Close() @@ -78,13 +89,21 @@ func newTestManager(t *testing.T) (string, *manager, func()) { } -func newMockPersistenceManager(t *testing.T) (*persistencemocks.Persistence, *ffcapimocks.API, *manager) { - InitConfig() - mca := &ffcapimocks.API{} - mps := &persistencemocks.Persistence{} - m := newManager(context.Background(), mca) - m.persistence = mps - return mps, mca, m +func newTestManagerMockPersistence(t *testing.T) (string, *manager, func()) { + + url := testManagerCommonInit(t) + + m := newManager(context.Background(), &ffcapimocks.API{}) + mp := &persistencemocks.Persistence{} + mp.On("Close", mock.Anything).Return(nil).Maybe() + m.persistence = mp + + err := m.initServices(context.Background()) + assert.NoError(t, err) + + return url, m, func() { + m.Close() + } } func TestNewManagerBadHttpConfig(t *testing.T) { @@ -144,9 +163,8 @@ func TestNewManagerBadPolicyEngine(t *testing.T) { func TestAddErrorMessageMax(t *testing.T) { - var m *manager - _, m, cancel := newTestManager(t) - defer cancel() + _, m, close := newTestManagerMockPersistence(t) + defer close() m.errorHistoryCount = 2 mtx := &apitypes.ManagedTX{} @@ -160,14 +178,28 @@ func TestAddErrorMessageMax(t *testing.T) { } func TestStartRestoreFail(t *testing.T) { - _, m, cancel := newTestManager(t) - cancel() + _, m, close := newTestManagerMockPersistence(t) + defer close() - mp := &persistencemocks.Persistence{} - m.persistence = mp + mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreams", mock.Anything, mock.Anything, startupPaginationLimit, persistence.SortDirectionAscending). Return(nil, fmt.Errorf("pop")) err := m.Start() assert.Regexp(t, "pop", err) } + +func TestStartBlockListenerFail(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListStreams", mock.Anything, mock.Anything, startupPaginationLimit, persistence.SortDirectionAscending).Return(nil, nil) + + mca := m.connector.(*ffcapimocks.API) + mca.On("NewBlockListener", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + err := m.Start() + assert.Regexp(t, "pop", err) + +} diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go index e44bc6c0..8fee86aa 100644 --- a/pkg/fftm/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -106,8 +106,8 @@ func TestNonceStaleStateContention(t *testing.T) { func TestNonceListError(t *testing.T) { - _, m, cancel := newTestManager(t) - defer cancel() + _, m, close := newTestManagerMockPersistence(t) + defer close() mFFC := m.connector.(*ffcapimocks.API) mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{ @@ -115,11 +115,9 @@ func TestNonceListError(t *testing.T) { Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation }, ffcapi.ErrorReason(""), nil) - mp := &persistencemocks.Persistence{} - m.persistence = mp + mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(nil, fmt.Errorf("pop")) - mp.On("Close", mock.Anything).Return(nil).Maybe() _, err := m.sendManagedTransaction(context.Background(), &apitypes.TransactionRequest{ TransactionInput: ffcapi.TransactionInput{ @@ -137,17 +135,15 @@ func TestNonceListError(t *testing.T) { func TestNonceListStaleThenQueryFail(t *testing.T) { - _, m, cancel := newTestManager(t) - defer cancel() + _, m, close := newTestManagerMockPersistence(t) + defer close() - mp := &persistencemocks.Persistence{} - m.persistence = mp + mp := m.persistence.(*persistencemocks.Persistence) old := fftypes.FFTime(time.Now().Add(-10000 * time.Hour)) mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return([]*apitypes.ManagedTX{ {ID: "id12345", Created: &old, Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, }, nil) - mp.On("Close", mock.Anything).Return(nil).Maybe() mFFC := m.connector.(*ffcapimocks.API) mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{ @@ -172,17 +168,16 @@ func TestNonceListStaleThenQueryFail(t *testing.T) { func TestNonceListNotStale(t *testing.T) { - _, m, cancel := newTestManager(t) - defer cancel() + _, m, close := newTestManagerMockPersistence(t) + defer close() m.nonceStateTimeout = 1 * time.Hour - mp := &persistencemocks.Persistence{} - m.persistence = mp + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return([]*apitypes.ManagedTX{ {ID: "id12345", Created: fftypes.Now(), Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, }, nil) - mp.On("Close", mock.Anything).Return(nil).Maybe() n, err := m.calcNextNonce(context.Background(), "0x12345") assert.NoError(t, err) diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index 9cf24cc2..667c3111 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -261,14 +261,12 @@ func TestNotifyConfirmationMgrFail(t *testing.T) { func TestInflightSetListFailCancel(t *testing.T) { - _, m, cancel := newTestManager(t) - cancel() + _, m, close := newTestManagerMockPersistence(t) + close() - mp := &persistencemocks.Persistence{} - m.persistence = mp + mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListTransactionsPending", m.ctx, (*fftypes.UUID)(nil), m.maxInFlight, persistence.SortDirectionAscending). Return(nil, fmt.Errorf("pop")) - mp.On("Close", mock.Anything).Return(nil).Maybe() m.policyLoopCycle(true) @@ -278,8 +276,8 @@ func TestInflightSetListFailCancel(t *testing.T) { func TestPolicyLoopUpdateFail(t *testing.T) { - _, m, cancel := newTestManager(t) - defer cancel() + _, m, close := newTestManagerMockPersistence(t) + defer close() m.inflight = []*pendingState{ { @@ -299,8 +297,7 @@ func TestPolicyLoopUpdateFail(t *testing.T) { }, } - mp := &persistencemocks.Persistence{} - m.persistence = mp + mp := m.persistence.(*persistencemocks.Persistence) mp.On("WriteTransaction", m.ctx, mock.Anything, false).Return(fmt.Errorf("pop")) mp.On("Close", mock.Anything).Return(nil).Maybe() diff --git a/pkg/fftm/send_tx_test.go b/pkg/fftm/send_tx_test.go index 1e8a4359..dbbba75e 100644 --- a/pkg/fftm/send_tx_test.go +++ b/pkg/fftm/send_tx_test.go @@ -31,18 +31,15 @@ import ( func TestSendTXPersistFail(t *testing.T) { - _, m, cancel := newTestManager(t) - cancel() - - mp := &persistencemocks.Persistence{} - m.persistence = mp + _, m, close := newTestManagerMockPersistence(t) + defer close() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return([]*apitypes.ManagedTX{ {ID: "id12345", Created: fftypes.Now(), Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, }, nil) mp.On("WriteTransaction", m.ctx, mock.Anything, true).Return(fmt.Errorf("pop")) - mp.On("Close", mock.Anything).Return(nil).Maybe() var txReq *ffcapi.TransactionSendRequest err := json.Unmarshal([]byte(sampleSendTX), &txReq) diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go index 2623cde8..225611ef 100644 --- a/pkg/fftm/stream_management_test.go +++ b/pkg/fftm/stream_management_test.go @@ -23,6 +23,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/persistence" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" @@ -74,8 +75,10 @@ func TestRestoreStreamsAndListenersOK(t *testing.T) { func TestRestoreStreamsReadFailed(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreams", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending).Return(nil, fmt.Errorf("pop")) err := m.restoreStreams() @@ -86,8 +89,10 @@ func TestRestoreStreamsReadFailed(t *testing.T) { func TestRestoreListenersReadFailed(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreams", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending).Return([]*apitypes.EventStream{ {ID: fftypes.NewUUID()}, }, nil) @@ -170,10 +175,12 @@ func TestDeleteStartedListener(t *testing.T) { func TestDeleteStartedListenerFail(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() esID := apitypes.UUIDVersion1() lID := apitypes.UUIDVersion1() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{ {ID: lID, StreamID: esID}, }, nil) @@ -187,7 +194,8 @@ func TestDeleteStartedListenerFail(t *testing.T) { func TestDeleteStreamBadID(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() err := m.deleteStream(m.ctx, "Bad ID") assert.Regexp(t, "FF00138", err) @@ -196,9 +204,11 @@ func TestDeleteStreamBadID(t *testing.T) { func TestDeleteStreamListenerPersistenceFail(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() esID := apitypes.UUIDVersion1() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return(nil, fmt.Errorf("pop")) err := m.deleteStream(m.ctx, esID.String()) @@ -209,9 +219,11 @@ func TestDeleteStreamListenerPersistenceFail(t *testing.T) { func TestDeleteStreamPersistenceFail(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() esID := apitypes.UUIDVersion1() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{}, nil) mp.On("DeleteStream", m.ctx, esID).Return(fmt.Errorf("pop")) @@ -223,9 +235,11 @@ func TestDeleteStreamPersistenceFail(t *testing.T) { func TestDeleteStreamNotInitialized(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() esID := apitypes.UUIDVersion1() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{}, nil) mp.On("DeleteStream", m.ctx, esID).Return(nil) @@ -237,10 +251,13 @@ func TestDeleteStreamNotInitialized(t *testing.T) { func TestCreateRenameStreamNameReservation(t *testing.T) { - mp, mfc, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mp := m.persistence.(*persistencemocks.Persistence) mp.On("WriteStream", m.ctx, mock.Anything).Return(fmt.Errorf("temporary")).Once() mp.On("DeleteCheckpoint", m.ctx, mock.Anything).Return(fmt.Errorf("temporary")).Once() mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) @@ -279,7 +296,8 @@ func TestCreateRenameStreamNameReservation(t *testing.T) { func TestCreateStreamValidateFail(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() wrongType := apitypes.DistributionMode("wrong") _, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1"), Type: &wrongType}) @@ -288,15 +306,18 @@ func TestCreateStreamValidateFail(t *testing.T) { } func TestCreateAndStoreNewStreamListenerBadID(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.createAndStoreNewStreamListener(m.ctx, "bad", nil) assert.Regexp(t, "FF00138", err) } func TestUpdateExistingListenerNotFound(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("GetListener", m.ctx, mock.Anything).Return(nil, nil) _, err := m.updateExistingListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String(), &apitypes.Listener{}, false) @@ -306,7 +327,8 @@ func TestUpdateExistingListenerNotFound(t *testing.T) { } func TestCreateOrUpdateListenerNotFound(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.createOrUpdateListener(m.ctx, apitypes.UUIDVersion1(), &apitypes.Listener{StreamID: apitypes.UUIDVersion1()}, false) assert.Regexp(t, "FF21045", err) @@ -314,11 +336,14 @@ func TestCreateOrUpdateListenerNotFound(t *testing.T) { } func TestCreateOrUpdateListenerFail(t *testing.T) { - mp, mfc, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) @@ -332,12 +357,15 @@ func TestCreateOrUpdateListenerFail(t *testing.T) { } func TestCreateOrUpdateListenerWriteFail(t *testing.T) { - mp, mfc, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) mp.On("WriteListener", m.ctx, mock.Anything).Return(fmt.Errorf("pop")) mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) @@ -352,7 +380,8 @@ func TestCreateOrUpdateListenerWriteFail(t *testing.T) { } func TestDeleteListenerBadID(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() err := m.deleteListener(m.ctx, "bad ID", "bad ID") assert.Regexp(t, "FF00138", err) @@ -360,9 +389,11 @@ func TestDeleteListenerBadID(t *testing.T) { } func TestDeleteListenerStreamNotFound(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: apitypes.UUIDVersion1()} + mp := m.persistence.(*persistencemocks.Persistence) mp.On("GetListener", m.ctx, mock.Anything).Return(l1, nil) err := m.deleteListener(m.ctx, l1.StreamID.String(), l1.ID.String()) @@ -373,12 +404,15 @@ func TestDeleteListenerStreamNotFound(t *testing.T) { } func TestDeleteListenerFail(t *testing.T) { - mp, mfc, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) mp.On("WriteListener", m.ctx, mock.Anything).Return(nil) mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + mfc := m.connector.(*ffcapimocks.API) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) @@ -399,7 +433,8 @@ func TestDeleteListenerFail(t *testing.T) { } func TestUpdateStreamBadID(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.updateStream(m.ctx, "bad ID", &apitypes.EventStream{}) assert.Regexp(t, "FF00138", err) @@ -407,7 +442,8 @@ func TestUpdateStreamBadID(t *testing.T) { } func TestUpdateStreamNotFound(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.updateStream(m.ctx, apitypes.UUIDVersion1().String(), &apitypes.EventStream{}) assert.Regexp(t, "FF21045", err) @@ -415,9 +451,13 @@ func TestUpdateStreamNotFound(t *testing.T) { } func TestUpdateStreamBadChanges(t *testing.T) { - mp, mfc, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mfc := m.connector.(*ffcapimocks.API) + mp := m.persistence.(*persistencemocks.Persistence) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) @@ -430,7 +470,10 @@ func TestUpdateStreamBadChanges(t *testing.T) { } func TestUpdateStreamWriteFail(t *testing.T) { - mp, mfc, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mfc := m.connector.(*ffcapimocks.API) + mp := m.persistence.(*persistencemocks.Persistence) mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) mp.On("WriteStream", m.ctx, mock.Anything).Return(nil).Once() @@ -447,7 +490,8 @@ func TestUpdateStreamWriteFail(t *testing.T) { } func TestGetStreamBadID(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.getStream(m.ctx, "bad ID") assert.Regexp(t, "FF00138", err) @@ -455,7 +499,8 @@ func TestGetStreamBadID(t *testing.T) { } func TestGetStreamNotFound(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.getStream(m.ctx, apitypes.UUIDVersion1().String()) assert.Regexp(t, "FF21045", err) @@ -463,7 +508,8 @@ func TestGetStreamNotFound(t *testing.T) { } func TestGetStreamsBadLimit(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.getStreams(m.ctx, "", "wrong") assert.Regexp(t, "FF21044", err) @@ -471,7 +517,8 @@ func TestGetStreamsBadLimit(t *testing.T) { } func TestGetListenerBadAfter(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.getListeners(m.ctx, "!bad UUID", "") assert.Regexp(t, "FF00138", err) @@ -479,7 +526,8 @@ func TestGetListenerBadAfter(t *testing.T) { } func TestGetListenerBadStreamID(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.getListener(m.ctx, "bad ID", apitypes.UUIDVersion1().String()) assert.Regexp(t, "FF00138", err) @@ -487,7 +535,8 @@ func TestGetListenerBadStreamID(t *testing.T) { } func TestGetListenerBadListenerID(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.getListener(m.ctx, apitypes.UUIDVersion1().String(), "bad ID") assert.Regexp(t, "FF00138", err) @@ -495,8 +544,10 @@ func TestGetListenerBadListenerID(t *testing.T) { } func TestGetListenerLookupErr(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("GetListener", m.ctx, mock.Anything).Return(nil, fmt.Errorf("pop")) _, err := m.getListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String()) @@ -507,8 +558,10 @@ func TestGetListenerLookupErr(t *testing.T) { } func TestGetListenerNotFound(t *testing.T) { - mp, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("GetListener", m.ctx, mock.Anything).Return(nil, nil) _, err := m.getListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String()) @@ -519,7 +572,8 @@ func TestGetListenerNotFound(t *testing.T) { } func TestGetStreamListenersBadLimit(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.getStreamListeners(m.ctx, "", "!bad limit", apitypes.UUIDVersion1().String()) assert.Regexp(t, "FF21044", err) @@ -527,7 +581,8 @@ func TestGetStreamListenersBadLimit(t *testing.T) { } func TestGetStreamListenersBadStreamID(t *testing.T) { - _, _, m := newMockPersistenceManager(t) + _, m, close := newTestManagerMockPersistence(t) + defer close() _, err := m.getStreamListeners(m.ctx, "", "", "bad ID") assert.Regexp(t, "FF00138", err) diff --git a/pkg/fftm/transaction_management_test.go b/pkg/fftm/transaction_management_test.go index 9752d68d..f3b7e952 100644 --- a/pkg/fftm/transaction_management_test.go +++ b/pkg/fftm/transaction_management_test.go @@ -27,12 +27,10 @@ import ( func TestGetTransactionsErrors(t *testing.T) { - _, m, cancel := newTestManager(t) - cancel() - - mp := &persistencemocks.Persistence{} - m.persistence = mp + _, m, close := newTestManagerMockPersistence(t) + defer close() + mp := m.persistence.(*persistencemocks.Persistence) mp.On("GetTransactionByID", m.ctx, mock.Anything).Return(nil, fmt.Errorf("pop")).Once() mp.On("GetTransactionByID", m.ctx, mock.Anything).Return(nil, nil).Once() mp.On("Close", mock.Anything).Return(nil).Maybe() From 03f9290cc32dc26530bd10731f9502eeedc70710 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 5 Aug 2022 23:56:49 -0400 Subject: [PATCH 78/95] Rename file Signed-off-by: Peter Broadhurst --- pkg/ffcapi/{event_stream_start copy.go => new_block_listener.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg/ffcapi/{event_stream_start copy.go => new_block_listener.go} (100%) diff --git a/pkg/ffcapi/event_stream_start copy.go b/pkg/ffcapi/new_block_listener.go similarity index 100% rename from pkg/ffcapi/event_stream_start copy.go rename to pkg/ffcapi/new_block_listener.go From b37145a1d68fb3fd136b327edf176f10bc1df14e Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 5 Aug 2022 23:58:49 -0400 Subject: [PATCH 79/95] Add ID Signed-off-by: Peter Broadhurst --- pkg/ffcapi/new_block_listener.go | 1 + pkg/fftm/manager.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/ffcapi/new_block_listener.go b/pkg/ffcapi/new_block_listener.go index bcd1733c..82a6cd54 100644 --- a/pkg/ffcapi/new_block_listener.go +++ b/pkg/ffcapi/new_block_listener.go @@ -21,6 +21,7 @@ import ( ) type NewBlockListenerRequest struct { + ID string // unique identifier for this listener ListenerContext context.Context // Context that will be cancelled when the listener needs to stop - no further events will be consumed after this, so all pushes to the listener should select on the done channel too BlockListener chan<- *BlockHashEvent // The connector should push new blocks to every listener, marking if it's possible blocks were missed (due to reconnect). The listener guarantees to always consume from this channel, until the listener context closes. } diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index b01e6bcd..84b227fd 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -154,7 +154,7 @@ func (m *manager) Start() error { return err } - blReq := &ffcapi.NewBlockListenerRequest{ListenerContext: m.ctx} + blReq := &ffcapi.NewBlockListenerRequest{ListenerContext: m.ctx, ID: "fftm-receipts"} blReq.BlockListener, m.blockListenerDone = blocklistener.BufferChannel(m.ctx, m.confirmations) _, _, err := m.connector.NewBlockListener(m.ctx, blReq) if err != nil { From 3306be256cff78cf943e007cd38a5e35139d8916 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 5 Aug 2022 23:59:42 -0400 Subject: [PATCH 80/95] Add ID Signed-off-by: Peter Broadhurst --- pkg/ffcapi/new_block_listener.go | 4 +++- pkg/fftm/manager.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/ffcapi/new_block_listener.go b/pkg/ffcapi/new_block_listener.go index 82a6cd54..4ee68db7 100644 --- a/pkg/ffcapi/new_block_listener.go +++ b/pkg/ffcapi/new_block_listener.go @@ -18,10 +18,12 @@ package ffcapi import ( "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" ) type NewBlockListenerRequest struct { - ID string // unique identifier for this listener + ID *fftypes.UUID // unique identifier for this listener ListenerContext context.Context // Context that will be cancelled when the listener needs to stop - no further events will be consumed after this, so all pushes to the listener should select on the done channel too BlockListener chan<- *BlockHashEvent // The connector should push new blocks to every listener, marking if it's possible blocks were missed (due to reconnect). The listener guarantees to always consume from this channel, until the listener context closes. } diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index 84b227fd..31d55f90 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -154,7 +154,7 @@ func (m *manager) Start() error { return err } - blReq := &ffcapi.NewBlockListenerRequest{ListenerContext: m.ctx, ID: "fftm-receipts"} + blReq := &ffcapi.NewBlockListenerRequest{ListenerContext: m.ctx, ID: fftypes.NewUUID()} blReq.BlockListener, m.blockListenerDone = blocklistener.BufferChannel(m.ctx, m.confirmations) _, _, err := m.connector.NewBlockListener(m.ctx, blReq) if err != nil { From a4602df4a598905c2cbf703eae2ec2b6105fc919 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sat, 6 Aug 2022 15:06:42 -0400 Subject: [PATCH 81/95] Resubmit after warn interval Signed-off-by: Peter Broadhurst --- config.md | 4 +- internal/tmconfig/tmconfig.go | 2 +- internal/tmmsgs/en_config_descriptions.go | 2 +- pkg/policyengines/simple/config.go | 8 +-- .../simple/simple_policy_engine.go | 58 ++++++++++++------- .../simple/simple_policy_engine_test.go | 12 ++-- 6 files changed, 52 insertions(+), 34 deletions(-) diff --git a/config.md b/config.md index be2a1c54..929678af 100644 --- a/config.md +++ b/config.md @@ -156,7 +156,7 @@ nav_order: 2 |Key|Description|Type|Default Value| |---|-----------|----|-------------| |fixedGasPrice|A fixed gasPrice value/structure to pass to the connector|Raw JSON|`` -|warnInterval|The time between warnings when a blockchain transaction has not been allocated a receipt|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|resubmitInterval|The time between warning and re-sending a transaction (same nonce) when a blockchain transaction has not been allocated a receipt|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` ## policyengine.simple.gasOracle @@ -201,7 +201,7 @@ nav_order: 2 |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|interval|Interval at which to invoke the policy engine to evaluate outstanding transactions|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` +|interval|Interval at which to invoke the policy engine to evaluate outstanding transactions|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` ## policyloop.retry diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 317ba181..0505611b 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -74,7 +74,7 @@ func setDefaults() { viper.SetDefault(string(ConfirmationsBlockQueueLength), 50) viper.SetDefault(string(ConfirmationsNotificationQueueLength), 50) viper.SetDefault(string(ConfirmationsStaleReceiptTimeout), "1m") - viper.SetDefault(string(PolicyLoopInterval), "1s") + viper.SetDefault(string(PolicyLoopInterval), "10s") viper.SetDefault(string(PolicyEngineName), "simple") viper.SetDefault(string(EventStreamsDefaultsBatchSize), 50) diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go index cbbeac7a..b5322076 100644 --- a/internal/tmmsgs/en_config_descriptions.go +++ b/internal/tmmsgs/en_config_descriptions.go @@ -51,7 +51,7 @@ var ( ConfigLoopInterval = ffc("config.policyloop.interval", "Interval at which to invoke the policy engine to evaluate outstanding transactions", i18n.TimeDurationType) ConfigPolicyEngineSimpleFixedGasPrice = ffc("config.policyengine.simple.fixedGasPrice", "A fixed gasPrice value/structure to pass to the connector", "Raw JSON") - ConfigPolicyEngineSimpleWarnInterval = ffc("config.policyengine.simple.warnInterval", "The time between warnings when a blockchain transaction has not been allocated a receipt", i18n.TimeDurationType) + ConfigPolicyEngineSimpleResubmitInterval = ffc("config.policyengine.simple.resubmitInterval", "The time between warning and re-sending a transaction (same nonce) when a blockchain transaction has not been allocated a receipt", i18n.TimeDurationType) ConfigPolicyEngineSimpleGasOracleEnabled = ffc("config.policyengine.simple.gasOracle.mode", "The gas oracle mode", "connector | restapi | disabled") ConfigPolicyEngineSimpleGasOracleGoTemplate = ffc("config.policyengine.simple.gasOracle.template", "REST API Gas Oracle: A go template to execute against the result from the Gas Oracle, to create a JSON block that will be passed as the gas price to the connector", i18n.GoTemplateType) ConfigPolicyEngineSimpleGasOracleURL = ffc("config.policyengine.simple.gasOracle.url", "REST API Gas Oracle: The URL of a Gas Oracle REST API to call", i18n.StringType) diff --git a/pkg/policyengines/simple/config.go b/pkg/policyengines/simple/config.go index bf636d12..4e3d2fc0 100644 --- a/pkg/policyengines/simple/config.go +++ b/pkg/policyengines/simple/config.go @@ -24,8 +24,8 @@ import ( ) const ( - FixedGasPrice = "fixedGasPrice" // when not using a gas station - will be treated as a raw JSON string, so can be numeric 123, or string "123", or object {"maxPriorityFeePerGas":123}) - WarnInterval = "warnInterval" // warnings will be written to the log at this interval if mining has not occurred + FixedGasPrice = "fixedGasPrice" // when not using a gas station - will be treated as a raw JSON string, so can be numeric 123, or string "123", or object {"maxPriorityFeePerGas":123}) + ResubmitInterval = "resubmitInterval" // warnings will be written to the log at this interval if mining has not occurred, and the TX will be resubmitted GasOracleConfig = "gasOracle" GasOracleMode = "mode" GasOracleMethod = "method" @@ -40,7 +40,7 @@ const ( ) const ( - defaultWarnInterval = "15m" + defaultResubmitInterval = "5m" defaultGasOracleQueryInterval = "5m" defaultGasOracleMethod = http.MethodGet defaultGasOracleMode = GasOracleModeConnector @@ -48,7 +48,7 @@ const ( func (f *PolicyEngineFactory) InitConfig(conf config.Section) { conf.AddKnownKey(FixedGasPrice) - conf.AddKnownKey(WarnInterval, defaultWarnInterval) + conf.AddKnownKey(ResubmitInterval, defaultResubmitInterval) gasOracleConfig := conf.SubSection(GasOracleConfig) ffresty.InitConfig(gasOracleConfig) diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index ae467cba..6acd83de 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -48,8 +48,8 @@ func (f *PolicyEngineFactory) Name() string { func (f *PolicyEngineFactory) NewPolicyEngine(ctx context.Context, conf config.Section) (pe policyengine.PolicyEngine, err error) { gasOracleConfig := conf.SubSection(GasOracleConfig) p := &simplePolicyEngine{ - warnInterval: conf.GetDuration(WarnInterval), - fixedGasPrice: fftypes.JSONAnyPtr(conf.GetString(FixedGasPrice)), + resubmitInterval: conf.GetDuration(ResubmitInterval), + fixedGasPrice: fftypes.JSONAnyPtr(conf.GetString(FixedGasPrice)), gasOracleMethod: gasOracleConfig.GetString(GasOracleMethod), gasOracleQueryInterval: gasOracleConfig.GetDuration(GasOracleQueryInterval), @@ -77,8 +77,8 @@ func (f *PolicyEngineFactory) NewPolicyEngine(ctx context.Context, conf config.S } type simplePolicyEngine struct { - fixedGasPrice *fftypes.JSONAny - warnInterval time.Duration + fixedGasPrice *fftypes.JSONAny + resubmitInterval time.Duration gasOracleMode string gasOracleClient *resty.Client @@ -111,31 +111,39 @@ func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *apitypes.M return updated, reason, err } +func (p *simplePolicyEngine) submitTX(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (reason ffcapi.ErrorReason, err error) { + sendTX := &ffcapi.TransactionSendRequest{ + TransactionHeaders: mtx.TransactionHeaders, + GasPrice: mtx.GasPrice, + TransactionData: mtx.TransactionData, + } + sendTX.TransactionHeaders.Nonce = (*fftypes.FFBigInt)(mtx.Nonce.Int()) + sendTX.TransactionHeaders.Gas = (*fftypes.FFBigInt)(mtx.Gas.Int()) + log.L(ctx).Debugf("Sending transaction: %+v", sendTX) + res, reason, err := cAPI.TransactionSend(ctx, sendTX) + if err != nil { + // A more sophisticated policy engine would consider the reason here, and potentially adjust the transaction for future attempts + return reason, err + } + log.L(ctx).Infof("Transaction hash=%s", res.TransactionHash) + mtx.TransactionHash = res.TransactionHash + mtx.LastSubmit = fftypes.Now() + return "", nil +} + func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (updated bool, reason ffcapi.ErrorReason, err error) { // Simple policy engine only submits once. if mtx.FirstSubmit == nil { - + // Only calculate gas price here in the simple policy engine mtx.GasPrice, err = p.getGasPrice(ctx, cAPI) if err != nil { return false, "", err } - sendTX := &ffcapi.TransactionSendRequest{ - TransactionHeaders: mtx.TransactionHeaders, - GasPrice: mtx.GasPrice, - TransactionData: mtx.TransactionData, - } - sendTX.TransactionHeaders.Nonce = (*fftypes.FFBigInt)(mtx.Nonce.Int()) - sendTX.TransactionHeaders.Gas = (*fftypes.FFBigInt)(mtx.Gas.Int()) - log.L(ctx).Debugf("Sending transaction: %+v", sendTX) - res, reason, err := cAPI.TransactionSend(ctx, sendTX) - if err != nil { - // A more sophisticated policy engine would consider the reason here, and potentially adjust the transaction for future attempts - return false, reason, err + // Submit the first time + if reason, err := p.submitTX(ctx, cAPI, mtx); err != nil { + return true, reason, err } - log.L(ctx).Infof("Transaction hash=%s", res.TransactionHash) - mtx.TransactionHash = res.TransactionHash - mtx.FirstSubmit = fftypes.Now() - mtx.LastSubmit = mtx.FirstSubmit + mtx.FirstSubmit = mtx.LastSubmit return true, "", nil } else if mtx.Receipt == nil { @@ -149,10 +157,16 @@ func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx * lastWarnTime = mtx.FirstSubmit } now := fftypes.Now() - if now.Time().Sub(*lastWarnTime.Time()) > p.warnInterval { + if now.Time().Sub(*lastWarnTime.Time()) > p.resubmitInterval { secsSinceSubmit := float64(now.Time().Sub(*mtx.FirstSubmit.Time())) / float64(time.Second) log.L(ctx).Warnf("Transaction %s (op=%s) has not been mined after %.2fs", mtx.TransactionHash, mtx.ID, secsSinceSubmit) info.LastWarnTime = now + // We do a resubmit at this point - as it might no longer be in the TX pool + if reason, err := p.submitTX(ctx, cAPI, mtx); err != nil { + if reason != ffcapi.ErrorKnownTransaction { + return true, reason, err + } + } return true, "", nil } return false, "", nil diff --git a/pkg/policyengines/simple/simple_policy_engine_test.go b/pkg/policyengines/simple/simple_policy_engine_test.go index a4414f23..6e1135f8 100644 --- a/pkg/policyengines/simple/simple_policy_engine_test.go +++ b/pkg/policyengines/simple/simple_policy_engine_test.go @@ -391,6 +391,8 @@ func TestWarnStaleWarningCannotParse(t *testing.T) { } mockFFCAPI := &ffcapimocks.API{} + mockFFCAPI.On("TransactionSend", mock.Anything, mock.Anything). + Return(nil, ffcapi.ErrorKnownTransaction, fmt.Errorf("Known transaction")) ctx := context.Background() updated, _, err := p.Execute(ctx, mockFFCAPI, mtx) @@ -401,7 +403,7 @@ func TestWarnStaleWarningCannotParse(t *testing.T) { mockFFCAPI.AssertExpectations(t) } -func TestWarnStaleAdditionalWarning(t *testing.T) { +func TestWarnStaleAdditionalWarningResubmitFail(t *testing.T) { f, conf := newTestPolicyEngineFactory(t) conf.Set(FixedGasPrice, `12345`) p, err := f.NewPolicyEngine(context.Background(), conf) @@ -419,10 +421,12 @@ func TestWarnStaleAdditionalWarning(t *testing.T) { } mockFFCAPI := &ffcapimocks.API{} + mockFFCAPI.On("TransactionSend", mock.Anything, mock.Anything). + Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) ctx := context.Background() updated, reason, err := p.Execute(ctx, mockFFCAPI, mtx) - assert.NoError(t, err) + assert.Regexp(t, "pop", err) assert.Empty(t, reason) assert.True(t, updated) assert.NotEmpty(t, mtx.PolicyInfo.JSONObject().GetString("lastWarnTime")) @@ -433,7 +437,7 @@ func TestWarnStaleAdditionalWarning(t *testing.T) { func TestWarnStaleNoWarning(t *testing.T) { f, conf := newTestPolicyEngineFactory(t) conf.Set(FixedGasPrice, `12345`) - conf.Set(WarnInterval, "100s") + conf.Set(ResubmitInterval, "100s") p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) @@ -462,7 +466,7 @@ func TestWarnStaleNoWarning(t *testing.T) { func TestNoOpWithReceipt(t *testing.T) { f, conf := newTestPolicyEngineFactory(t) conf.Set(FixedGasPrice, `12345`) - conf.Set(WarnInterval, "100s") + conf.Set(ResubmitInterval, "100s") p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) From ad23a3fb725d50d9f7483c2064dd13057ad4afe1 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sun, 7 Aug 2022 00:30:54 -0400 Subject: [PATCH 82/95] Ensure sequence allocated in nonce lock Signed-off-by: Peter Broadhurst --- pkg/fftm/send_tx.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index c4b21669..91b6aea9 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -20,6 +20,7 @@ import ( "context" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -54,12 +55,9 @@ func (m *manager) sendManagedContractDeployment(ctx context.Context, request *ap func (m *manager) submitPreparedTX(ctx context.Context, txID string, txHeaders *ffcapi.TransactionHeaders, gas *fftypes.FFBigInt, transactionData string) (*apitypes.ManagedTX, error) { - // Sequencing ID is always generated by us - so we have a deterministic order of transactions - seqID := apitypes.UUIDVersion1() - // The request ID is the primary ID, and should be supplied by the user for idempotence if txID == "" { - txID = seqID.String() + txID = fftypes.NewUUID().String() } // First job is to assign the next nonce to this request. @@ -72,6 +70,11 @@ func (m *manager) submitPreparedTX(ctx context.Context, txID string, txHeaders * // We will call markSpent() once we reach the point the nonce has been used defer lockedNonce.complete(ctx) + // Sequencing ID is always generated by us - so we have a deterministic order of transactions + // Note: We must allocate this within the nonce lock, to ensure that the nonce sequence and the + // global transaction sequence line up. + seqID := apitypes.UUIDVersion1() + // Next we update FireFly core with the pre-submitted record pending record, with the allocated nonce. // From this point on, we will guide this transaction through to submission. // We return an "ack" at this point, and dispatch the work of getting the transaction submitted @@ -92,6 +95,7 @@ func (m *manager) submitPreparedTX(ctx context.Context, txID string, txHeaders * if err = m.persistence.WriteTransaction(m.ctx, mtx, true); err != nil { return nil, err } + log.L(m.ctx).Infof("Tracking transaction %s at nonce %d", mtx.ID, mtx.Nonce.Int64()) m.markInflightStale() // Ok - we've spent it. The rest of the processing will be triggered off of lockedNonce From e526185ee8978f89a4157683c3cda19781451b95 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sun, 7 Aug 2022 00:39:07 -0400 Subject: [PATCH 83/95] Improve logging Signed-off-by: Peter Broadhurst --- pkg/fftm/send_tx.go | 2 +- pkg/policyengines/simple/simple_policy_engine.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index 91b6aea9..5dea6ade 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -95,7 +95,7 @@ func (m *manager) submitPreparedTX(ctx context.Context, txID string, txHeaders * if err = m.persistence.WriteTransaction(m.ctx, mtx, true); err != nil { return nil, err } - log.L(m.ctx).Infof("Tracking transaction %s at nonce %d", mtx.ID, mtx.Nonce.Int64()) + log.L(m.ctx).Infof("Tracking transaction %s at nonce %s / %d", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64()) m.markInflightStale() // Ok - we've spent it. The rest of the processing will be triggered off of lockedNonce diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index 6acd83de..022a09e7 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -119,15 +119,15 @@ func (p *simplePolicyEngine) submitTX(ctx context.Context, cAPI ffcapi.API, mtx } sendTX.TransactionHeaders.Nonce = (*fftypes.FFBigInt)(mtx.Nonce.Int()) sendTX.TransactionHeaders.Gas = (*fftypes.FFBigInt)(mtx.Gas.Int()) - log.L(ctx).Debugf("Sending transaction: %+v", sendTX) + log.L(ctx).Debugf("Sending transaction %s at nonce %s / %d", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64()) res, reason, err := cAPI.TransactionSend(ctx, sendTX) if err != nil { // A more sophisticated policy engine would consider the reason here, and potentially adjust the transaction for future attempts return reason, err } - log.L(ctx).Infof("Transaction hash=%s", res.TransactionHash) mtx.TransactionHash = res.TransactionHash mtx.LastSubmit = fftypes.Now() + log.L(ctx).Infof("Transaction %s at nonce %s / %d submitted. Hash: %s", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64(), mtx.TransactionHash) return "", nil } @@ -159,7 +159,7 @@ func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx * now := fftypes.Now() if now.Time().Sub(*lastWarnTime.Time()) > p.resubmitInterval { secsSinceSubmit := float64(now.Time().Sub(*mtx.FirstSubmit.Time())) / float64(time.Second) - log.L(ctx).Warnf("Transaction %s (op=%s) has not been mined after %.2fs", mtx.TransactionHash, mtx.ID, secsSinceSubmit) + log.L(ctx).Infof("Transaction %s at nonce %s / %d has not been mined after %.2fs", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64(), secsSinceSubmit) info.LastWarnTime = now // We do a resubmit at this point - as it might no longer be in the TX pool if reason, err := p.submitTX(ctx, cAPI, mtx); err != nil { From 4041d4d1646e8beabbfeffa87b08aff3a7e876a1 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sun, 7 Aug 2022 19:51:44 -0400 Subject: [PATCH 84/95] Switch from UUIDv1 to ULID for sortable identifiers Signed-off-by: Peter Broadhurst --- .vscode/settings.json | 1 + go.mod | 3 +- go.sum | 3 + internal/events/eventstream_test.go | 6 +- .../persistence/leveldb_persistence_test.go | 52 ++++++++--------- pkg/apitypes/api_types.go | 7 --- pkg/apitypes/api_types_test.go | 11 +--- pkg/apitypes/ulid.go | 40 +++++++++++++ pkg/apitypes/ulid_test.go | 30 ++++++++++ pkg/fftm/nonces_test.go | 4 +- pkg/fftm/policyloop.go | 4 ++ pkg/fftm/policyloop_test.go | 2 +- pkg/fftm/route_get_transactions_test.go | 2 +- pkg/fftm/send_tx.go | 2 +- pkg/fftm/stream_management.go | 4 +- pkg/fftm/stream_management_test.go | 56 +++++++++---------- 16 files changed, 146 insertions(+), 81 deletions(-) create mode 100644 pkg/apitypes/ulid.go create mode 100644 pkg/apitypes/ulid_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json index a6a1789a..aa256abf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -59,6 +59,7 @@ "NATS", "Nowarn", "oapispec", + "oklog", "openapi", "optype", "persistencemocks", diff --git a/go.mod b/go.mod index 053f8f47..a1d2c6b6 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ require ( github.com/getkin/kin-openapi v0.96.0 github.com/ghodss/yaml v1.0.0 github.com/go-resty/resty/v2 v2.7.0 - github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/hyperledger/firefly-common v0.1.16 + github.com/oklog/ulid/v2 v2.1.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.1 @@ -25,6 +25,7 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.19.15 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/invopop/yaml v0.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/go.sum b/go.sum index 2e355b9b..8b240498 100644 --- a/go.sum +++ b/go.sum @@ -346,6 +346,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= @@ -360,6 +362,7 @@ github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 31cf9408..1cdfb123 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -60,7 +60,7 @@ func (cp *utCheckpointType) LessThan(b ffcapi.EventListenerCheckpoint) bool { func testESConf(t *testing.T, j string) (spec *apitypes.EventStream) { err := json.Unmarshal([]byte(j), &spec) assert.NoError(t, err) - spec.ID = apitypes.UUIDVersion1() + spec.ID = apitypes.NewULID() return spec } @@ -302,7 +302,7 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { addr := "0x12345" l := &apitypes.Listener{ - ID: apitypes.UUIDVersion1(), + ID: apitypes.NewULID(), Name: strPtr("ut_listener"), DeprecatedAddress: &addr, DeprecatedEvent: fftypes.JSONAnyPtr(`{"event":"definition"}`), @@ -420,7 +420,7 @@ func TestStartEventStreamCheckpointInvalid(t *testing.T) { }`) l := &apitypes.Listener{ - ID: apitypes.UUIDVersion1(), + ID: apitypes.NewULID(), Name: strPtr("ut_listener"), Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), FromBlock: strPtr("12345"), diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go index fca9bede..61d5912d 100644 --- a/internal/persistence/leveldb_persistence_test.go +++ b/internal/persistence/leveldb_persistence_test.go @@ -101,17 +101,17 @@ func TestReadWriteStreams(t *testing.T) { ctx := context.Background() s1 := &apitypes.EventStream{ - ID: apitypes.UUIDVersion1(), // ensure we get sequentially ascending IDs + ID: apitypes.NewULID(), // ensure we get sequentially ascending IDs Name: strPtr("stream1"), } p.WriteStream(ctx, s1) s2 := &apitypes.EventStream{ - ID: apitypes.UUIDVersion1(), + ID: apitypes.NewULID(), Name: strPtr("stream2"), } p.WriteStream(ctx, s2) s3 := &apitypes.EventStream{ - ID: apitypes.UUIDVersion1(), + ID: apitypes.NewULID(), Name: strPtr("stream3"), } p.WriteStream(ctx, s3) @@ -166,25 +166,25 @@ func TestReadWriteListeners(t *testing.T) { ctx := context.Background() - sID1 := apitypes.UUIDVersion1() - sID2 := apitypes.UUIDVersion1() + sID1 := apitypes.NewULID() + sID2 := apitypes.NewULID() s1l1 := &apitypes.Listener{ - ID: apitypes.UUIDVersion1(), + ID: apitypes.NewULID(), StreamID: sID1, } err := p.WriteListener(ctx, s1l1) assert.NoError(t, err) s2l1 := &apitypes.Listener{ - ID: apitypes.UUIDVersion1(), + ID: apitypes.NewULID(), StreamID: sID2, } err = p.WriteListener(ctx, s2l1) assert.NoError(t, err) s1l2 := &apitypes.Listener{ - ID: apitypes.UUIDVersion1(), + ID: apitypes.NewULID(), StreamID: sID1, } err = p.WriteListener(ctx, s1l2) @@ -232,10 +232,10 @@ func TestReadWriteCheckpoints(t *testing.T) { ctx := context.Background() cp1 := &apitypes.EventStreamCheckpoint{ - StreamID: apitypes.UUIDVersion1(), + StreamID: apitypes.NewULID(), } cp2 := &apitypes.EventStreamCheckpoint{ - StreamID: apitypes.UUIDVersion1(), + StreamID: apitypes.NewULID(), } err := p.WriteCheckpoint(ctx, cp1) @@ -266,7 +266,7 @@ func newTestTX(signer string, nonce int64, status apitypes.TxStatus) *apitypes.M TransactionHeaders: ffcapi.TransactionHeaders{ From: signer, }, - SequenceID: apitypes.UUIDVersion1(), + SequenceID: apitypes.NewULID(), Nonce: fftypes.NewFFBigInt(nonce), Status: status, } @@ -356,7 +356,7 @@ func TestListStreamsBadJSON(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - sID := apitypes.UUIDVersion1() + sID := apitypes.NewULID() err := p.db.Put(prefixedKey(eventstreamsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) @@ -369,14 +369,14 @@ func TestListListenersBadJSON(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - lID := apitypes.UUIDVersion1() + lID := apitypes.NewULID() err := p.db.Put(prefixedKey(listenersPrefix, lID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) _, err = p.ListListeners(context.Background(), nil, 0, SortDirectionDescending) assert.Error(t, err) - _, err = p.ListStreamListeners(context.Background(), nil, 0, SortDirectionDescending, apitypes.UUIDVersion1()) + _, err = p.ListStreamListeners(context.Background(), nil, 0, SortDirectionDescending, apitypes.NewULID()) assert.Error(t, err) } @@ -387,7 +387,7 @@ func TestDeleteStreamFail(t *testing.T) { p.db.Close() - err := p.DeleteStream(context.Background(), apitypes.UUIDVersion1()) + err := p.DeleteStream(context.Background(), apitypes.NewULID()) assert.Error(t, err) } @@ -411,7 +411,7 @@ func TestWriteCheckpointFailMarshal(t *testing.T) { p.db.Close() - id1 := apitypes.UUIDVersion1() + id1 := apitypes.NewULID() err := p.WriteCheckpoint(context.Background(), &apitypes.EventStreamCheckpoint{ Listeners: map[fftypes.UUID]json.RawMessage{ *id1: json.RawMessage([]byte(`{"bad": "json"!`)), @@ -427,7 +427,7 @@ func TestWriteCheckpointFail(t *testing.T) { p.db.Close() - id1 := apitypes.UUIDVersion1() + id1 := apitypes.NewULID() err := p.WriteCheckpoint(context.Background(), &apitypes.EventStreamCheckpoint{ Listeners: map[fftypes.UUID]json.RawMessage{ *id1: json.RawMessage([]byte(`{}`)), @@ -443,7 +443,7 @@ func TestReadListenerFail(t *testing.T) { p.db.Close() - _, err := p.GetListener(context.Background(), apitypes.UUIDVersion1()) + _, err := p.GetListener(context.Background(), apitypes.NewULID()) assert.Error(t, err) } @@ -452,7 +452,7 @@ func TestReadCheckpointFail(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - sID := apitypes.UUIDVersion1() + sID := apitypes.NewULID() err := p.db.Put(prefixedKey(checkpointsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) @@ -466,9 +466,9 @@ func TestListManagedTransactionFail(t *testing.T) { defer done() tx := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + ID: fmt.Sprintf("ns1:%s", apitypes.NewULID()), Created: fftypes.Now(), - SequenceID: apitypes.UUIDVersion1(), + SequenceID: apitypes.NewULID(), } err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) assert.NoError(t, err) @@ -485,9 +485,9 @@ func TestListManagedTransactionCleanupOrphans(t *testing.T) { defer done() tx := &apitypes.ManagedTX{ - ID: fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()), + ID: fmt.Sprintf("ns1:%s", apitypes.NewULID()), Created: fftypes.Now(), - SequenceID: apitypes.UUIDVersion1(), + SequenceID: apitypes.NewULID(), } err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) assert.NoError(t, err) @@ -506,7 +506,7 @@ func TestListNonceAllocationsFail(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - txID := fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()) + txID := fmt.Sprintf("ns1:%s", apitypes.NewULID()) err := p.writeKeyValue(context.Background(), txNonceAllocationKey("0xaaa", fftypes.NewFFBigInt(12345)), txDataKey(txID)) assert.NoError(t, err) err = p.db.Put(txDataKey(txID), []byte("{! not json"), &opt.WriteOptions{}) @@ -521,8 +521,8 @@ func TestListInflightTransactionFail(t *testing.T) { p, done := newTestLevelDBPersistence(t) defer done() - txID := fmt.Sprintf("ns1:%s", apitypes.UUIDVersion1()) - err := p.writeKeyValue(context.Background(), txPendingIndexKey(apitypes.UUIDVersion1()), txDataKey(txID)) + txID := fmt.Sprintf("ns1:%s", apitypes.NewULID()) + err := p.writeKeyValue(context.Background(), txPendingIndexKey(apitypes.NewULID()), txDataKey(txID)) assert.NoError(t, err) err = p.db.Put(txDataKey(txID), []byte("{! not json"), &opt.WriteOptions{}) assert.NoError(t, err) diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 472742bd..3154c425 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -21,7 +21,6 @@ import ( "encoding/json" "reflect" - "github.com/google/uuid" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/jsonmap" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" @@ -117,12 +116,6 @@ type Listener struct { FromBlock *string `ffstruct:"listener" json:"fromBlock,omitempty"` } -// UUIDVersion1 returns a version 1 UUID - where the alphanumeric sequence is assured to be ascending based on the order of generation -func UUIDVersion1() *fftypes.UUID { - u, _ := uuid.NewUUID() - return (*fftypes.UUID)(&u) -} - // CheckUpdateString helper merges supplied configuration, with a base, and applies a default if unset func CheckUpdateString(changed bool, merged **string, old *string, new *string, defValue string) bool { if new != nil { diff --git a/pkg/apitypes/api_types_test.go b/pkg/apitypes/api_types_test.go index 59737f41..585581f6 100644 --- a/pkg/apitypes/api_types_test.go +++ b/pkg/apitypes/api_types_test.go @@ -18,7 +18,6 @@ package apitypes import ( "encoding/json" - "strings" "testing" "time" @@ -27,12 +26,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestV1UUID(t *testing.T) { - u1 := UUIDVersion1() - u2 := UUIDVersion1() - assert.Negative(t, strings.Compare(u1.String(), u2.String())) -} - func TestCheckUpdateString(t *testing.T) { var val1 = "val1" var val2 = "val2" @@ -193,9 +186,9 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { e := &EventWithContext{ StandardContext: EventContext{ - StreamID: UUIDVersion1(), + StreamID: NewULID(), ListenerName: "listener1", - DeprecatedSubID: UUIDVersion1(), + DeprecatedSubID: NewULID(), }, Event: ffcapi.Event{ ID: ffcapi.EventID{ diff --git a/pkg/apitypes/ulid.go b/pkg/apitypes/ulid.go new file mode 100644 index 00000000..72ae1219 --- /dev/null +++ b/pkg/apitypes/ulid.go @@ -0,0 +1,40 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 apitypes + +import ( + "crypto/rand" + "time" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/oklog/ulid/v2" +) + +var ulidReader = &ulid.LockedMonotonicReader{ + MonotonicReader: &ulid.MonotonicEntropy{ + Reader: rand.Reader, + }, +} + +// NewULID returns a Universally Unique Lexicographically Sortable Identifier (ULID). +// For consistency we impersonate the formatting of a UUID, so they can be used +// interchangeably. +// This can be used in database tables to ensure monotonic increasing identifiers. +func NewULID() *fftypes.UUID { + u := ulid.MustNew(ulid.Timestamp(time.Now()), ulidReader) + return (*fftypes.UUID)(&u) +} diff --git a/pkg/apitypes/ulid_test.go b/pkg/apitypes/ulid_test.go new file mode 100644 index 00000000..d9c6560c --- /dev/null +++ b/pkg/apitypes/ulid_test.go @@ -0,0 +1,30 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 apitypes + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestULID(t *testing.T) { + u1 := NewULID() + u2 := NewULID() + assert.Negative(t, strings.Compare(u1.String(), u2.String())) +} diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go index 8fee86aa..7712a282 100644 --- a/pkg/fftm/nonces_test.go +++ b/pkg/fftm/nonces_test.go @@ -42,7 +42,7 @@ func TestNonceStaleStateContention(t *testing.T) { ID: "stale1", Created: &oldTime, Status: apitypes.TxStatusSucceeded, - SequenceID: apitypes.UUIDVersion1(), + SequenceID: apitypes.NewULID(), Nonce: fftypes.NewFFBigInt(1000), // old nonce TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x12345", @@ -76,7 +76,7 @@ func TestNonceStaleStateContention(t *testing.T) { Created: &oldTime, Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), Status: apitypes.TxStatusPending, - SequenceID: apitypes.UUIDVersion1(), + SequenceID: apitypes.NewULID(), TransactionHeaders: ffcapi.TransactionHeaders{ From: "0x12345", }, diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index 0c498cb6..f9af6cd3 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -94,6 +94,10 @@ func (m *manager) updateInflightSet() bool { for _, mtx := range additional { m.inflight = append(m.inflight, &pendingState{mtx: mtx}) } + newLen := len(m.inflight) + if newLen > 0 { + log.L(m.ctx).Debugf("Inflight set updated len=%d head-seq=%s tail-seq=%s old-tail=%s", len(m.inflight), m.inflight[0].mtx.SequenceID, m.inflight[newLen-1].mtx.SequenceID, after) + } } return true diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index 667c3111..6beb5a8b 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -285,7 +285,7 @@ func TestPolicyLoopUpdateFail(t *testing.T) { mtx: &apitypes.ManagedTX{ ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), Created: fftypes.Now(), - SequenceID: apitypes.UUIDVersion1(), + SequenceID: apitypes.NewULID(), Nonce: fftypes.NewFFBigInt(1000), Status: apitypes.TxStatusSucceeded, FirstSubmit: fftypes.Now(), diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go index 4103412c..98151a03 100644 --- a/pkg/fftm/route_get_transactions_test.go +++ b/pkg/fftm/route_get_transactions_test.go @@ -32,7 +32,7 @@ func newTestTxn(t *testing.T, m *manager, signer string, nonce int64, status api tx := &apitypes.ManagedTX{ ID: fmt.Sprintf("ns1:%s", fftypes.NewUUID()), Created: fftypes.Now(), - SequenceID: apitypes.UUIDVersion1(), + SequenceID: apitypes.NewULID(), Nonce: fftypes.NewFFBigInt(nonce), Status: status, TransactionHeaders: ffcapi.TransactionHeaders{ diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go index 5dea6ade..6d6dc8c0 100644 --- a/pkg/fftm/send_tx.go +++ b/pkg/fftm/send_tx.go @@ -73,7 +73,7 @@ func (m *manager) submitPreparedTX(ctx context.Context, txID string, txHeaders * // Sequencing ID is always generated by us - so we have a deterministic order of transactions // Note: We must allocate this within the nonce lock, to ensure that the nonce sequence and the // global transaction sequence line up. - seqID := apitypes.UUIDVersion1() + seqID := apitypes.NewULID() // Next we update FireFly core with the pre-submitted record pending record, with the allocated nonce. // From this point on, we will guide this transaction through to submission. diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index f08333e1..36e298a3 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -156,7 +156,7 @@ func (m *manager) reserveStreamName(ctx context.Context, name string, id *fftype } func (m *manager) createAndStoreNewStream(ctx context.Context, def *apitypes.EventStream) (*apitypes.EventStream, error) { - def.ID = apitypes.UUIDVersion1() + def.ID = apitypes.NewULID() def.Created = nil // set to updated time by events.NewEventStream if def.Name == nil || *def.Name == "" { return nil, i18n.NewError(ctx, tmmsgs.MsgMissingName) @@ -200,7 +200,7 @@ func (m *manager) createAndStoreNewStreamListener(ctx context.Context, idStr str } func (m *manager) createAndStoreNewListener(ctx context.Context, def *apitypes.Listener) (*apitypes.Listener, error) { - return m.createOrUpdateListener(ctx, apitypes.UUIDVersion1(), def, false) + return m.createOrUpdateListener(ctx, apitypes.NewULID(), def, false) } func (m *manager) updateExistingListener(ctx context.Context, streamIDStr, listenerIDStr string, updates *apitypes.Listener, reset bool) (*apitypes.Listener, error) { diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go index 225611ef..b86326cd 100644 --- a/pkg/fftm/stream_management_test.go +++ b/pkg/fftm/stream_management_test.go @@ -43,23 +43,23 @@ func TestRestoreStreamsAndListenersOK(t *testing.T) { falsy := false - es1 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr("stream1"), Suspended: &falsy} + es1 := &apitypes.EventStream{ID: apitypes.NewULID(), Name: strPtr("stream1"), Suspended: &falsy} err := m.persistence.WriteStream(m.ctx, es1) assert.NoError(t, err) - e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: strPtr("listener1"), StreamID: es1.ID} + e1l1 := &apitypes.Listener{ID: apitypes.NewULID(), Name: strPtr("listener1"), StreamID: es1.ID} err = m.persistence.WriteListener(m.ctx, e1l1) assert.NoError(t, err) - e1l2 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: strPtr("listener2"), StreamID: es1.ID} + e1l2 := &apitypes.Listener{ID: apitypes.NewULID(), Name: strPtr("listener2"), StreamID: es1.ID} err = m.persistence.WriteListener(m.ctx, e1l2) assert.NoError(t, err) - es2 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr("stream2"), Suspended: &falsy} + es2 := &apitypes.EventStream{ID: apitypes.NewULID(), Name: strPtr("stream2"), Suspended: &falsy} err = m.persistence.WriteStream(m.ctx, es2) assert.NoError(t, err) - e2l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: strPtr("listener3"), StreamID: es2.ID} + e2l1 := &apitypes.Listener{ID: apitypes.NewULID(), Name: strPtr("listener3"), StreamID: es2.ID} err = m.persistence.WriteListener(m.ctx, e2l1) assert.NoError(t, err) @@ -110,7 +110,7 @@ func TestRestoreStreamsValidateFail(t *testing.T) { defer done() falsy := false - es1 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr(""), Suspended: &falsy} + es1 := &apitypes.EventStream{ID: apitypes.NewULID(), Name: strPtr(""), Suspended: &falsy} err := m.persistence.WriteStream(m.ctx, es1) assert.NoError(t, err) @@ -129,11 +129,11 @@ func TestRestoreListenersStartFail(t *testing.T) { mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")) falsy := false - es1 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr("stream1"), Suspended: &falsy} + es1 := &apitypes.EventStream{ID: apitypes.NewULID(), Name: strPtr("stream1"), Suspended: &falsy} err := m.persistence.WriteStream(m.ctx, es1) assert.NoError(t, err) - e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: strPtr("listener1"), StreamID: es1.ID} + e1l1 := &apitypes.Listener{ID: apitypes.NewULID(), Name: strPtr("listener1"), StreamID: es1.ID} err = m.persistence.WriteListener(m.ctx, e1l1) assert.NoError(t, err) @@ -155,11 +155,11 @@ func TestDeleteStartedListener(t *testing.T) { mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() falsy := false - es1 := &apitypes.EventStream{ID: apitypes.UUIDVersion1(), Name: strPtr("stream1"), Suspended: &falsy} + es1 := &apitypes.EventStream{ID: apitypes.NewULID(), Name: strPtr("stream1"), Suspended: &falsy} err := m.persistence.WriteStream(m.ctx, es1) assert.NoError(t, err) - e1l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), Name: strPtr("listener1"), StreamID: es1.ID} + e1l1 := &apitypes.Listener{ID: apitypes.NewULID(), Name: strPtr("listener1"), StreamID: es1.ID} err = m.persistence.WriteListener(m.ctx, e1l1) assert.NoError(t, err) @@ -178,8 +178,8 @@ func TestDeleteStartedListenerFail(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - esID := apitypes.UUIDVersion1() - lID := apitypes.UUIDVersion1() + esID := apitypes.NewULID() + lID := apitypes.NewULID() mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{ {ID: lID, StreamID: esID}, @@ -207,7 +207,7 @@ func TestDeleteStreamListenerPersistenceFail(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - esID := apitypes.UUIDVersion1() + esID := apitypes.NewULID() mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return(nil, fmt.Errorf("pop")) @@ -222,7 +222,7 @@ func TestDeleteStreamPersistenceFail(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - esID := apitypes.UUIDVersion1() + esID := apitypes.NewULID() mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{}, nil) mp.On("DeleteStream", m.ctx, esID).Return(fmt.Errorf("pop")) @@ -238,7 +238,7 @@ func TestDeleteStreamNotInitialized(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - esID := apitypes.UUIDVersion1() + esID := apitypes.NewULID() mp := m.persistence.(*persistencemocks.Persistence) mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{}, nil) mp.On("DeleteStream", m.ctx, esID).Return(nil) @@ -320,7 +320,7 @@ func TestUpdateExistingListenerNotFound(t *testing.T) { mp := m.persistence.(*persistencemocks.Persistence) mp.On("GetListener", m.ctx, mock.Anything).Return(nil, nil) - _, err := m.updateExistingListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String(), &apitypes.Listener{}, false) + _, err := m.updateExistingListener(m.ctx, apitypes.NewULID().String(), apitypes.NewULID().String(), &apitypes.Listener{}, false) assert.Regexp(t, "FF21046", err) mp.AssertExpectations(t) @@ -330,7 +330,7 @@ func TestCreateOrUpdateListenerNotFound(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - _, err := m.createOrUpdateListener(m.ctx, apitypes.UUIDVersion1(), &apitypes.Listener{StreamID: apitypes.UUIDVersion1()}, false) + _, err := m.createOrUpdateListener(m.ctx, apitypes.NewULID(), &apitypes.Listener{StreamID: apitypes.NewULID()}, false) assert.Regexp(t, "FF21045", err) } @@ -350,7 +350,7 @@ func TestCreateOrUpdateListenerFail(t *testing.T) { es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) - _, err = m.createOrUpdateListener(m.ctx, apitypes.UUIDVersion1(), &apitypes.Listener{StreamID: es.ID}, false) + _, err = m.createOrUpdateListener(m.ctx, apitypes.NewULID(), &apitypes.Listener{StreamID: es.ID}, false) assert.Regexp(t, "pop", err) mp.AssertExpectations(t) @@ -373,7 +373,7 @@ func TestCreateOrUpdateListenerWriteFail(t *testing.T) { es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) - _, err = m.createOrUpdateListener(m.ctx, apitypes.UUIDVersion1(), &apitypes.Listener{StreamID: es.ID}, false) + _, err = m.createOrUpdateListener(m.ctx, apitypes.NewULID(), &apitypes.Listener{StreamID: es.ID}, false) assert.Regexp(t, "pop", err) mp.AssertExpectations(t) @@ -392,7 +392,7 @@ func TestDeleteListenerStreamNotFound(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - l1 := &apitypes.Listener{ID: apitypes.UUIDVersion1(), StreamID: apitypes.UUIDVersion1()} + l1 := &apitypes.Listener{ID: apitypes.NewULID(), StreamID: apitypes.NewULID()} mp := m.persistence.(*persistencemocks.Persistence) mp.On("GetListener", m.ctx, mock.Anything).Return(l1, nil) @@ -420,7 +420,7 @@ func TestDeleteListenerFail(t *testing.T) { es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) - l1, err := m.createOrUpdateListener(m.ctx, apitypes.UUIDVersion1(), &apitypes.Listener{StreamID: es.ID}, false) + l1, err := m.createOrUpdateListener(m.ctx, apitypes.NewULID(), &apitypes.Listener{StreamID: es.ID}, false) assert.NoError(t, err) mp.On("GetListener", m.ctx, mock.Anything).Return(l1, nil) @@ -445,7 +445,7 @@ func TestUpdateStreamNotFound(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - _, err := m.updateStream(m.ctx, apitypes.UUIDVersion1().String(), &apitypes.EventStream{}) + _, err := m.updateStream(m.ctx, apitypes.NewULID().String(), &apitypes.EventStream{}) assert.Regexp(t, "FF21045", err) } @@ -502,7 +502,7 @@ func TestGetStreamNotFound(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - _, err := m.getStream(m.ctx, apitypes.UUIDVersion1().String()) + _, err := m.getStream(m.ctx, apitypes.NewULID().String()) assert.Regexp(t, "FF21045", err) } @@ -529,7 +529,7 @@ func TestGetListenerBadStreamID(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - _, err := m.getListener(m.ctx, "bad ID", apitypes.UUIDVersion1().String()) + _, err := m.getListener(m.ctx, "bad ID", apitypes.NewULID().String()) assert.Regexp(t, "FF00138", err) } @@ -538,7 +538,7 @@ func TestGetListenerBadListenerID(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - _, err := m.getListener(m.ctx, apitypes.UUIDVersion1().String(), "bad ID") + _, err := m.getListener(m.ctx, apitypes.NewULID().String(), "bad ID") assert.Regexp(t, "FF00138", err) } @@ -550,7 +550,7 @@ func TestGetListenerLookupErr(t *testing.T) { mp := m.persistence.(*persistencemocks.Persistence) mp.On("GetListener", m.ctx, mock.Anything).Return(nil, fmt.Errorf("pop")) - _, err := m.getListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String()) + _, err := m.getListener(m.ctx, apitypes.NewULID().String(), apitypes.NewULID().String()) assert.Regexp(t, "pop", err) mp.AssertExpectations(t) @@ -564,7 +564,7 @@ func TestGetListenerNotFound(t *testing.T) { mp := m.persistence.(*persistencemocks.Persistence) mp.On("GetListener", m.ctx, mock.Anything).Return(nil, nil) - _, err := m.getListener(m.ctx, apitypes.UUIDVersion1().String(), apitypes.UUIDVersion1().String()) + _, err := m.getListener(m.ctx, apitypes.NewULID().String(), apitypes.NewULID().String()) assert.Regexp(t, "FF21046", err) mp.AssertExpectations(t) @@ -575,7 +575,7 @@ func TestGetStreamListenersBadLimit(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() - _, err := m.getStreamListeners(m.ctx, "", "!bad limit", apitypes.UUIDVersion1().String()) + _, err := m.getStreamListeners(m.ctx, "", "!bad limit", apitypes.NewULID().String()) assert.Regexp(t, "FF21044", err) } From 542116ad080b1f4e296e2b30bea2589bd4c2f6b4 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sun, 7 Aug 2022 22:36:48 -0400 Subject: [PATCH 85/95] Logging and performance improvements Signed-off-by: Peter Broadhurst --- config.md | 2 +- internal/confirmations/confirmations.go | 16 +++--- internal/confirmations/confirmations_test.go | 16 +++--- internal/events/eventstream.go | 2 +- internal/events/eventstream_test.go | 2 +- internal/tmconfig/tmconfig.go | 2 +- pkg/fftm/policyloop.go | 54 +++++++++++--------- pkg/fftm/policyloop_test.go | 37 +++++++------- 8 files changed, 68 insertions(+), 63 deletions(-) diff --git a/config.md b/config.md index 929678af..afb12c8c 100644 --- a/config.md +++ b/config.md @@ -143,7 +143,7 @@ nav_order: 2 |---|-----------|----|-------------| |maxHandles|The maximum number of cached file handles LevelDB should keep open|`int`|`100` |path|The path for the LevelDB persistence directory|`string`|`` -|syncWrites|Whether to synchronously perform writes to the storage|`boolean`|`true` +|syncWrites|Whether to synchronously perform writes to the storage|`boolean`|`false` ## policyengine diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index d2248d6d..c22e0cdf 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -61,13 +61,13 @@ type Notification struct { type EventInfo struct { ID *ffcapi.EventID - Confirmed func(confirmations []BlockInfo) + Confirmed func(ctx context.Context, confirmations []BlockInfo) } type TransactionInfo struct { TransactionHash string - Receipt func(receipt *ffcapi.TransactionReceiptResponse) - Confirmed func(confirmations []BlockInfo) + Receipt func(ctx context.Context, receipt *ffcapi.TransactionReceiptResponse) + Confirmed func(ctx context.Context, confirmations []BlockInfo) } type RemovedListenerInfo struct { @@ -101,7 +101,7 @@ type blockConfirmationManager struct { func NewBlockConfirmationManager(baseContext context.Context, connector ffcapi.API) Manager { bcm := &blockConfirmationManager{ - baseContext: baseContext, + baseContext: log.WithLogField(baseContext, "role", "confirmations"), connector: connector, blockListenerStale: true, requiredConfirmations: config.GetInt(tmconfig.ConfirmationsRequired), @@ -129,8 +129,8 @@ type pendingItem struct { added time.Time confirmations []*BlockInfo lastReceiptCheck time.Time - receiptCallback func(receipt *ffcapi.TransactionReceiptResponse) - confirmedCallback func(confirmations []BlockInfo) + receiptCallback func(ctx context.Context, receipt *ffcapi.TransactionReceiptResponse) + confirmedCallback func(ctx context.Context, confirmations []BlockInfo) transactionHash string blockHash string // can be notified of changes to this for receipts blockNumber uint64 // known at creation time for event logs @@ -432,7 +432,7 @@ func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem, blocks * log.L(bcm.ctx).Infof("Receipt for transaction %s downloaded. BlockNumber=%d BlockHash=%s", pending.transactionHash, pending.blockNumber, pending.blockHash) // Notify of the receipt if pending.receiptCallback != nil { - pending.receiptCallback(res) + pending.receiptCallback(bcm.ctx, res) } if bcm.requiredConfirmations == 0 { @@ -564,7 +564,7 @@ func (bcm *blockConfirmationManager) dispatchConfirmed(item *pendingItem) { bcm.removeItem(pendingKey, false) log.L(bcm.ctx).Infof("Confirmed with %d confirmations event=%s", len(item.confirmations), pendingKey) - item.confirmedCallback(item.copyConfirmations() /* a safe copy outside of our cache */) + item.confirmedCallback(bcm.ctx, item.copyConfirmations() /* a safe copy outside of our cache */) } // walkChain goes through each event and sees whether it's valid, diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index 0366a3ef..c7b0ca21 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -58,7 +58,7 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { TransactionIndex: 5, LogIndex: 10, }, - Confirmed: func(confirmations []BlockInfo) { + Confirmed: func(ctx context.Context, confirmations []BlockInfo) { confirmed <- confirmations }, } @@ -169,7 +169,7 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { TransactionIndex: 5, LogIndex: 10, }, - Confirmed: func(confirmations []BlockInfo) { + Confirmed: func(ctx context.Context, confirmations []BlockInfo) { confirmed <- confirmations }, } @@ -294,10 +294,10 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { receiptReceived := make(chan *ffcapi.TransactionReceiptResponse, 1) txToConfirmForkA := &TransactionInfo{ TransactionHash: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - Confirmed: func(confirmations []BlockInfo) { + Confirmed: func(ctx context.Context, confirmations []BlockInfo) { confirmed <- confirmations }, - Receipt: func(receipt *ffcapi.TransactionReceiptResponse) { + Receipt: func(ctx context.Context, receipt *ffcapi.TransactionReceiptResponse) { receiptReceived <- receipt }, } @@ -470,7 +470,7 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { TransactionIndex: 5, LogIndex: 10, }, - Confirmed: func(confirmations []BlockInfo) { + Confirmed: func(ctx context.Context, confirmations []BlockInfo) { confirmed <- confirmations }, } @@ -601,7 +601,7 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { TransactionIndex: 5, LogIndex: 10, }, - Confirmed: func(confirmations []BlockInfo) { + Confirmed: func(ctx context.Context, confirmations []BlockInfo) { confirmed <- confirmations }, } @@ -926,7 +926,7 @@ func TestNotificationValidation(t *testing.T) { NotificationType: NewTransaction, Transaction: &TransactionInfo{ TransactionHash: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - Confirmed: func(confirmations []BlockInfo) {}, + Confirmed: func(ctx context.Context, confirmations []BlockInfo) {}, }, }) assert.NoError(t, err) @@ -970,7 +970,7 @@ func TestCheckReceiptImmediateConfirm(t *testing.T) { pending := &pendingItem{ pType: pendingTypeTransaction, transactionHash: txHash, - confirmedCallback: func(confirmations []BlockInfo) { + confirmedCallback: func(ctx context.Context, confirmations []BlockInfo) { close(done) }, } diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 783b39b5..8e45d941 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -621,7 +621,7 @@ func (es *eventStream) processNewEvent(ctx context.Context, fev *ffcapi.Listener NotificationType: confirmations.NewEventLog, Event: &confirmations.EventInfo{ ID: &event.ID, - Confirmed: func(confirmations []confirmations.BlockInfo) { + Confirmed: func(ctx context.Context, confirmations []confirmations.BlockInfo) { // Push it to the batch when confirmed // - Note this will block the confirmation manager when the event stream is blocked es.batchChannel <- fev diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 1cdfb123..6f61b4b8 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -92,7 +92,7 @@ func newTestEventStreamWithListener(t *testing.T, mfc *ffcapimocks.API, conf str mcm.On("Notify", mock.Anything).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) if n.Event != nil { - go n.Event.Confirmed([]confirmations.BlockInfo{}) + go n.Event.Confirmed(context.Background(), []confirmations.BlockInfo{}) } }).Return(nil).Maybe() return es, err diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 0505611b..746df23f 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -89,7 +89,7 @@ func setDefaults() { viper.SetDefault(string(PersistenceType), "leveldb") viper.SetDefault(string(PersistenceLevelDBMaxHandles), 100) - viper.SetDefault(string(PersistenceLevelDBSyncWrites), true) + viper.SetDefault(string(PersistenceLevelDBSyncWrites), false) viper.SetDefault(string(APIDefaultRequestTimeout), "30s") viper.SetDefault(string(APIMaxRequestTimeout), "10m") diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index f9af6cd3..eb993fbf 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -17,6 +17,7 @@ package fftm import ( + "context" "time" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -31,18 +32,19 @@ import ( func (m *manager) policyLoop() { defer close(m.policyLoopDone) + ctx := log.WithLogField(m.ctx, "role", "policyloop") for { timer := time.NewTimer(m.policyLoopInterval) select { case <-m.inflightUpdate: - m.policyLoopCycle(false) + m.policyLoopCycle(ctx, false) case <-m.inflightStale: - m.policyLoopCycle(true) + m.policyLoopCycle(ctx, true) case <-timer.C: - m.policyLoopCycle(false) - case <-m.ctx.Done(): - log.L(m.ctx).Infof("Receipt poller exiting") + m.policyLoopCycle(ctx, false) + case <-ctx.Done(): + log.L(ctx).Infof("Receipt poller exiting") return } } @@ -62,7 +64,7 @@ func (m *manager) markInflightUpdate() { } } -func (m *manager) updateInflightSet() bool { +func (m *manager) updateInflightSet(ctx context.Context) bool { oldInflight := m.inflight m.inflight = make([]*pendingState, 0, len(oldInflight)) @@ -83,12 +85,12 @@ func (m *manager) updateInflightSet() bool { } var additional []*apitypes.ManagedTX // We retry the get from persistence indefinitely (until the context cancels) - err := m.retry.Do(m.ctx, "get pending transactions", func(attempt int) (retry bool, err error) { - additional, err = m.persistence.ListTransactionsPending(m.ctx, after, spaces, persistence.SortDirectionAscending) + err := m.retry.Do(ctx, "get pending transactions", func(attempt int) (retry bool, err error) { + additional, err = m.persistence.ListTransactionsPending(ctx, after, spaces, persistence.SortDirectionAscending) return true, err }) if err != nil { - log.L(m.ctx).Infof("Policy loop context cancelled while retrying") + log.L(ctx).Infof("Policy loop context cancelled while retrying") return false } for _, mtx := range additional { @@ -96,26 +98,26 @@ func (m *manager) updateInflightSet() bool { } newLen := len(m.inflight) if newLen > 0 { - log.L(m.ctx).Debugf("Inflight set updated len=%d head-seq=%s tail-seq=%s old-tail=%s", len(m.inflight), m.inflight[0].mtx.SequenceID, m.inflight[newLen-1].mtx.SequenceID, after) + log.L(ctx).Debugf("Inflight set updated len=%d head-seq=%s tail-seq=%s old-tail=%s", len(m.inflight), m.inflight[0].mtx.SequenceID, m.inflight[newLen-1].mtx.SequenceID, after) } } return true } -func (m *manager) policyLoopCycle(inflightStale bool) { +func (m *manager) policyLoopCycle(ctx context.Context, inflightStale bool) { if inflightStale { - if !m.updateInflightSet() { + if !m.updateInflightSet(ctx) { return } } // Go through executing the policy engine against them for _, pending := range m.inflight { - err := m.execPolicy(pending) + err := m.execPolicy(ctx, pending) if err != nil { - log.L(m.ctx).Errorf("Failed policy cycle transaction=%s operation=%s: %s", pending.mtx.TransactionHash, pending.mtx.ID, err) + log.L(ctx).Errorf("Failed policy cycle transaction=%s operation=%s: %s", pending.mtx.TransactionHash, pending.mtx.ID, err) } } @@ -140,7 +142,7 @@ func (m *manager) addError(mtx *apitypes.ManagedTX, reason ffcapi.ErrorReason, e } } -func (m *manager) execPolicy(pending *pendingState) (err error) { +func (m *manager) execPolicy(ctx context.Context, pending *pendingState) (err error) { var updated bool completed := false @@ -160,7 +162,7 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { mtx.ErrorMessage = "" } else { mtx.Status = apitypes.TxStatusFailed - mtx.ErrorMessage = i18n.NewError(m.ctx, tmmsgs.MsgTransactionFailed).Error() + mtx.ErrorMessage = i18n.NewError(ctx, tmmsgs.MsgTransactionFailed).Error() } default: @@ -171,14 +173,14 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { // Pass the state to the pluggable policy engine to potentially perform more actions against it, // such as submitting for the first time, or raising the gas etc. var reason ffcapi.ErrorReason - updated, reason, err = m.policyEngine.Execute(m.ctx, m.connector, pending.mtx) + updated, reason, err = m.policyEngine.Execute(ctx, m.connector, pending.mtx) if err != nil { - log.L(m.ctx).Errorf("Policy engine returned error for operation %s reason=%s: %s", mtx.ID, reason, err) + log.L(ctx).Errorf("Policy engine returned error for transaction %s reason=%s: %s", mtx.ID, reason, err) m.addError(mtx, reason, err) } else { if mtx.FirstSubmit != nil && pending.trackingTransactionHash != mtx.TransactionHash { // If now submitted, add to confirmations manager for receipt checking - m.trackSubmittedTransaction(pending) + m.trackSubmittedTransaction(ctx, pending) } pending.lastPolicyCycle = time.Now() } @@ -187,9 +189,9 @@ func (m *manager) execPolicy(pending *pendingState) (err error) { if updated || err != nil { mtx.Updated = fftypes.Now() - err := m.persistence.WriteTransaction(m.ctx, mtx, false) + err := m.persistence.WriteTransaction(ctx, mtx, false) if err != nil { - log.L(m.ctx).Errorf("Failed to update operation %s (status=%s): %s", mtx.ID, mtx.Status, err) + log.L(ctx).Errorf("Failed to update transaction %s (status=%s): %s", mtx.ID, mtx.Status, err) return err } if completed { @@ -221,7 +223,7 @@ func (m *manager) sendWSReply(mtx *apitypes.ManagedTX) { m.wsServer.SendReply(wsr) } -func (m *manager) trackSubmittedTransaction(pending *pendingState) { +func (m *manager) trackSubmittedTransaction(ctx context.Context, pending *pendingState) { var err error // Clear any old transaction hash @@ -240,19 +242,21 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { NotificationType: confirmations.NewTransaction, Transaction: &confirmations.TransactionInfo{ TransactionHash: pending.mtx.TransactionHash, - Receipt: func(receipt *ffcapi.TransactionReceiptResponse) { + Receipt: func(ctx context.Context, receipt *ffcapi.TransactionReceiptResponse) { // Will be picked up on the next policy loop cycle - guaranteed to occur before Confirmed m.mux.Lock() pending.mtx.Receipt = receipt m.mux.Unlock() + log.L(m.ctx).Infof("Receipt received for transaction %s at nonce %s / %d - hash: %s", pending.mtx.ID, pending.mtx.TransactionHeaders.From, pending.mtx.Nonce.Int64(), pending.mtx.TransactionHash) m.markInflightUpdate() }, - Confirmed: func(confirmations []confirmations.BlockInfo) { + Confirmed: func(ctx context.Context, confirmations []confirmations.BlockInfo) { // Will be picked up on the next policy loop cycle m.mux.Lock() pending.confirmed = true pending.mtx.Confirmations = confirmations m.mux.Unlock() + log.L(m.ctx).Infof("Confirmed transaction %s at nonce %s / %d - hash: %s", pending.mtx.ID, pending.mtx.TransactionHeaders.From, pending.mtx.Nonce.Int64(), pending.mtx.TransactionHash) m.markInflightUpdate() }, }, @@ -261,7 +265,7 @@ func (m *manager) trackSubmittedTransaction(pending *pendingState) { // Only reason for error here should be a cancelled context if err != nil { - log.L(m.ctx).Infof("Error detected notifying confirmation manager: %s", err) + log.L(ctx).Infof("Error detected notifying confirmation manager: %s", err) } else { pending.trackingTransactionHash = pending.mtx.TransactionHash } diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go index 6beb5a8b..16e2882a 100644 --- a/pkg/fftm/policyloop_test.go +++ b/pkg/fftm/policyloop_test.go @@ -17,6 +17,7 @@ package fftm import ( + "context" "fmt" "testing" "time" @@ -82,26 +83,26 @@ func TestPolicyLoopE2EOk(t *testing.T) { return n.NotificationType == confirmations.NewTransaction })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.TransactionReceiptResponse{ + n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ BlockNumber: fftypes.NewFFBigInt(12345), TransactionIndex: fftypes.NewFFBigInt(10), BlockHash: fftypes.NewRandB32().String(), Success: true, }) - n.Transaction.Confirmed([]confirmations.BlockInfo{}) + n.Transaction.Confirmed(context.Background(), []confirmations.BlockInfo{}) }).Return(nil) // Run the policy once to do the send <-m.inflightStale // from sending the TX - m.policyLoopCycle(true) + m.policyLoopCycle(m.ctx, true) assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) // A second time will mark it complete for flush - m.policyLoopCycle(false) + m.policyLoopCycle(m.ctx, false) <-m.inflightStale // policy loop should have marked us stale, to clean up the TX - m.policyLoopCycle(true) + m.policyLoopCycle(m.ctx, true) assert.Empty(t, m.inflight) // Check the update is persisted @@ -133,26 +134,26 @@ func TestPolicyLoopE2EReverted(t *testing.T) { return n.NotificationType == confirmations.NewTransaction })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.TransactionReceiptResponse{ + n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ BlockNumber: fftypes.NewFFBigInt(12345), TransactionIndex: fftypes.NewFFBigInt(10), BlockHash: fftypes.NewRandB32().String(), Success: false, }) - n.Transaction.Confirmed([]confirmations.BlockInfo{}) + n.Transaction.Confirmed(context.Background(), []confirmations.BlockInfo{}) }).Return(nil) // Run the policy once to do the send <-m.inflightStale // from sending the TX - m.policyLoopCycle(true) + m.policyLoopCycle(m.ctx, true) assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) // A second time will mark it complete for flush - m.policyLoopCycle(false) + m.policyLoopCycle(m.ctx, false) <-m.inflightStale // policy loop should have marked us stale, to clean up the TX - m.policyLoopCycle(true) + m.policyLoopCycle(m.ctx, true) assert.Empty(t, m.inflight) // Check the update is persisted @@ -204,31 +205,31 @@ func TestPolicyLoopResubmitNewTXID(t *testing.T) { n.Transaction.TransactionHash == txHash2 })).Run(func(args mock.Arguments) { n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.TransactionReceiptResponse{ + n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ BlockNumber: fftypes.NewFFBigInt(12345), TransactionIndex: fftypes.NewFFBigInt(10), BlockHash: fftypes.NewRandB32().String(), Success: true, }) - n.Transaction.Confirmed([]confirmations.BlockInfo{}) + n.Transaction.Confirmed(context.Background(), []confirmations.BlockInfo{}) }).Return(nil) // Run the policy once to do the send with the first hash <-m.inflightStale // from sending the TX - m.policyLoopCycle(true) + m.policyLoopCycle(m.ctx, true) assert.Len(t, m.inflight, 1) assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) // Run again to confirm it does not change anything, when the state is the same - m.policyLoopCycle(true) + m.policyLoopCycle(m.ctx, true) assert.Len(t, m.inflight, 1) assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) // Reset the transaction so the policy manager resubmits it m.inflight[0].mtx.FirstSubmit = nil - m.policyLoopCycle(false) + m.policyLoopCycle(m.ctx, false) assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) @@ -252,7 +253,7 @@ func TestNotifyConfirmationMgrFail(t *testing.T) { mc := m.confirmations.(*confirmationsmocks.Manager) mc.On("Notify", mock.Anything).Return(fmt.Errorf("pop")) - m.policyLoopCycle(true) + m.policyLoopCycle(m.ctx, true) mc.AssertExpectations(t) mfc.AssertExpectations(t) @@ -268,7 +269,7 @@ func TestInflightSetListFailCancel(t *testing.T) { mp.On("ListTransactionsPending", m.ctx, (*fftypes.UUID)(nil), m.maxInFlight, persistence.SortDirectionAscending). Return(nil, fmt.Errorf("pop")) - m.policyLoopCycle(true) + m.policyLoopCycle(m.ctx, true) mp.AssertExpectations(t) @@ -301,7 +302,7 @@ func TestPolicyLoopUpdateFail(t *testing.T) { mp.On("WriteTransaction", m.ctx, mock.Anything, false).Return(fmt.Errorf("pop")) mp.On("Close", mock.Anything).Return(nil).Maybe() - m.policyLoopCycle(false) + m.policyLoopCycle(m.ctx, false) mp.AssertExpectations(t) From 5dbbe86132830d73061f50616c5046805f08a91e Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Sun, 7 Aug 2022 23:20:11 -0400 Subject: [PATCH 86/95] Pin lint version Signed-off-by: Peter Broadhurst --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 09bad3a6..54b49a9d 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ lint: ${LINT} ${MOCKERY}: $(VGO) install github.com/vektra/mockery/cmd/mockery@latest ${LINT}: - $(VGO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + $(VGO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.0 define makemock From ebdf71bf93d098dce26537c27383334b0b7b8f4f Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 8 Aug 2022 15:41:17 -0400 Subject: [PATCH 87/95] Move blockNumber etc. serialization in JSON to be strings Signed-off-by: Peter Broadhurst --- go.mod | 2 +- go.sum | 4 ++-- internal/confirmations/confirmations.go | 22 +++++++++---------- internal/events/eventstream_test.go | 2 +- pkg/apitypes/api_types_test.go | 7 +++--- pkg/ffcapi/api.go | 16 +++++++------- pkg/ffcapi/api_test.go | 7 +++--- .../simple/simple_policy_engine.go | 17 +++++++++----- 8 files changed, 43 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index a1d2c6b6..53c2fafc 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/go-resty/resty/v2 v2.7.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 - github.com/hyperledger/firefly-common v0.1.16 + github.com/hyperledger/firefly-common v0.1.17-0.20220808193503-961a6b241a1a github.com/oklog/ulid/v2 v2.1.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.12.0 diff --git a/go.sum b/go.sum index 8b240498..ed8e0870 100644 --- a/go.sum +++ b/go.sum @@ -267,8 +267,8 @@ github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hyperledger/firefly-common v0.1.16 h1:21xidDEKrJhtGdBSRqHN4PfDi7aYxF0HOFuAa04V1AE= -github.com/hyperledger/firefly-common v0.1.16/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= +github.com/hyperledger/firefly-common v0.1.17-0.20220808193503-961a6b241a1a h1:KQJuUGh4CdZ5XXwKjyY9M0hm//uqwcUkwpxkiIEaziQ= +github.com/hyperledger/firefly-common v0.1.17-0.20220808193503-961a6b241a1a/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index c22e0cdf..3d6cb083 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -76,10 +76,10 @@ type RemovedListenerInfo struct { } type BlockInfo struct { - BlockNumber uint64 `json:"blockNumber"` - BlockHash string `json:"blockHash"` - ParentHash string `json:"parentHash"` - TransactionHashes []string `json:"transactionHashes,omitempty"` + BlockNumber fftypes.FFuint64 `json:"blockNumber"` + BlockHash string `json:"blockHash"` + ParentHash string `json:"parentHash"` + TransactionHashes []string `json:"transactionHashes,omitempty"` } type blockConfirmationManager struct { @@ -174,11 +174,11 @@ func (n *Notification) eventPendingItem() *pendingItem { return &pendingItem{ pType: pendingTypeEvent, listenerID: n.Event.ID.ListenerID, - blockNumber: n.Event.ID.BlockNumber, + blockNumber: n.Event.ID.BlockNumber.Uint64(), blockHash: n.Event.ID.BlockHash, transactionHash: n.Event.ID.TransactionHash, - transactionIndex: n.Event.ID.TransactionIndex, - logIndex: n.Event.ID.LogIndex, + transactionIndex: n.Event.ID.TransactionIndex.Uint64(), + logIndex: n.Event.ID.LogIndex.Uint64(), confirmedCallback: n.Event.Confirmed, } } @@ -302,7 +302,7 @@ func (bcm *blockConfirmationManager) getBlockByNumber(blockNumber uint64, expect func transformBlockInfo(res *ffcapi.BlockInfo) *BlockInfo { return &BlockInfo{ - BlockNumber: res.BlockNumber.Uint64(), + BlockNumber: fftypes.FFuint64(res.BlockNumber.Uint64()), BlockHash: res.BlockHash, ParentHash: res.ParentHash, TransactionHashes: res.TransactionHashes, @@ -498,8 +498,8 @@ func (bcm *blockConfirmationManager) processBlockHashes(blockHashes []string) { bcm.processBlock(block) // Update the highest block (used for efficiency in chain walks) - if block.BlockNumber > bcm.highestBlockSeen { - bcm.highestBlockSeen = block.BlockNumber + if block.BlockNumber.Uint64() > bcm.highestBlockSeen { + bcm.highestBlockSeen = block.BlockNumber.Uint64() } } } @@ -522,7 +522,7 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { // Go through all the events, adding in the confirmations, and popping any out // that have reached their threshold. Then drop the log before logging/processing them. - blockNumber := block.BlockNumber + blockNumber := block.BlockNumber.Uint64() var confirmed pendingItems for pendingKey, pending := range bcm.pending { if pending.blockHash != "" { diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 6f61b4b8..7fcee60a 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -1611,7 +1611,7 @@ func TestSkipEventsBehindCheckpoint(t *testing.T) { batchLoopDone: make(chan struct{}), action: func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { assert.Len(t, events, 1) - assert.Equal(t, events[0].ID.BlockNumber, uint64(2001)) + assert.Equal(t, events[0].ID.BlockNumber.Uint64(), uint64(2001)) return nil }, } diff --git a/pkg/apitypes/api_types_test.go b/pkg/apitypes/api_types_test.go index 585581f6..03a69666 100644 --- a/pkg/apitypes/api_types_test.go +++ b/pkg/apitypes/api_types_test.go @@ -211,16 +211,17 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { assert.NoError(t, err) assert.JSONEq(t, `{ "blockHash":"0x12345", - "blockNumber":12345, + "blockNumber":"12345", "data": {"dk1":"dv1"}, "key1":"val1", "listenerId":"`+e.ID.ListenerID.String()+`", "listenerName":"listener1", - "logIndex":1, "signature":"ev()", + "logIndex":"1", + "signature":"ev()", "subId":"`+e.StandardContext.DeprecatedSubID.String()+`", "streamId":"`+e.StandardContext.StreamID.String()+`", "transactionHash":"0x23456", - "transactionIndex":10 + "transactionIndex":"10" }`, string(b)) var e2 *EventWithContext diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go index 02eb3381..3458b319 100644 --- a/pkg/ffcapi/api.go +++ b/pkg/ffcapi/api.go @@ -88,14 +88,14 @@ type BlockHashEvent struct { // EventID are the set of required fields an FFCAPI compatible connector needs to map to the underlying blockchain constructs, to uniquely identify an event type EventID struct { - ListenerID *fftypes.UUID `json:"listenerId"` // The listener for the event - Signature string `json:"signature"` // The signature of this specific event (noting a listener might filter on multiple events) - BlockHash string `json:"blockHash"` // String representation of the block, which will change if any transaction info in the block changes - BlockNumber uint64 `json:"blockNumber"` // A numeric identifier for the block - TransactionHash string `json:"transactionHash"` // The transaction - TransactionIndex uint64 `json:"transactionIndex"` // Index within the block of the transaction that emitted the event - LogIndex uint64 `json:"logIndex"` // Index within the transaction of this emitted event log - Timestamp *fftypes.FFTime `json:"timestamp,omitempty"` // The on-chain timestamp + ListenerID *fftypes.UUID `json:"listenerId"` // The listener for the event + Signature string `json:"signature"` // The signature of this specific event (noting a listener might filter on multiple events) + BlockHash string `json:"blockHash"` // String representation of the block, which will change if any transaction info in the block changes + BlockNumber fftypes.FFuint64 `json:"blockNumber"` // A numeric identifier for the block + TransactionHash string `json:"transactionHash"` // The transaction + TransactionIndex fftypes.FFuint64 `json:"transactionIndex"` // Index within the block of the transaction that emitted the event + LogIndex fftypes.FFuint64 `json:"logIndex"` // Index within the transaction of this emitted event log + Timestamp *fftypes.FFTime `json:"timestamp,omitempty"` // The on-chain timestamp } // Event is a blockchain event that matches one of the started listeners, diff --git a/pkg/ffcapi/api_test.go b/pkg/ffcapi/api_test.go index 55d27a8f..2cf29d64 100644 --- a/pkg/ffcapi/api_test.go +++ b/pkg/ffcapi/api_test.go @@ -23,6 +23,7 @@ import ( "strings" "testing" + "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/stretchr/testify/assert" ) @@ -36,9 +37,9 @@ func TestSortEvents(t *testing.T) { l, _ := rand.Int(rand.Reader, big.NewInt(10)) events[i] = &Event{ ID: EventID{ - BlockNumber: b.Uint64(), - TransactionIndex: t.Uint64(), - LogIndex: l.Uint64(), + BlockNumber: fftypes.FFuint64(b.Uint64()), + TransactionIndex: fftypes.FFuint64(t.Uint64()), + LogIndex: fftypes.FFuint64(l.Uint64()), }, } listenerUpdates[i] = &ListenerEvent{ diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index 022a09e7..6fafd87b 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -121,12 +121,19 @@ func (p *simplePolicyEngine) submitTX(ctx context.Context, cAPI ffcapi.API, mtx sendTX.TransactionHeaders.Gas = (*fftypes.FFBigInt)(mtx.Gas.Int()) log.L(ctx).Debugf("Sending transaction %s at nonce %s / %d", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64()) res, reason, err := cAPI.TransactionSend(ctx, sendTX) - if err != nil { - // A more sophisticated policy engine would consider the reason here, and potentially adjust the transaction for future attempts - return reason, err + if err == nil { + mtx.TransactionHash = res.TransactionHash + mtx.LastSubmit = fftypes.Now() + } else { + // We have some simple rules for handling reasons from the connector, which could be enhanced by extending the connector. + switch reason { + case ffcapi.ErrorKnownTransaction: + // If we already have a transaction hash, this is fine - we just return as if we submitted it + + default: + return reason, err + } } - mtx.TransactionHash = res.TransactionHash - mtx.LastSubmit = fftypes.Now() log.L(ctx).Infof("Transaction %s at nonce %s / %d submitted. Hash: %s", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64(), mtx.TransactionHash) return "", nil } From 4f954f66c64644bb4e6fee19321c0da351a34b36 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 8 Aug 2022 16:13:01 -0400 Subject: [PATCH 88/95] Treat "nonce too low" as ok in the stale TX check, if we have a transaction hash Signed-off-by: Peter Broadhurst --- pkg/policyengines/simple/simple_policy_engine.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go index 6fafd87b..3bd09583 100644 --- a/pkg/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -119,7 +119,7 @@ func (p *simplePolicyEngine) submitTX(ctx context.Context, cAPI ffcapi.API, mtx } sendTX.TransactionHeaders.Nonce = (*fftypes.FFBigInt)(mtx.Nonce.Int()) sendTX.TransactionHeaders.Gas = (*fftypes.FFBigInt)(mtx.Gas.Int()) - log.L(ctx).Debugf("Sending transaction %s at nonce %s / %d", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64()) + log.L(ctx).Debugf("Sending transaction %s at nonce %s / %d (lastSubmit=%s)", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64(), mtx.LastSubmit) res, reason, err := cAPI.TransactionSend(ctx, sendTX) if err == nil { mtx.TransactionHash = res.TransactionHash @@ -127,9 +127,17 @@ func (p *simplePolicyEngine) submitTX(ctx context.Context, cAPI ffcapi.API, mtx } else { // We have some simple rules for handling reasons from the connector, which could be enhanced by extending the connector. switch reason { - case ffcapi.ErrorKnownTransaction: + case ffcapi.ErrorKnownTransaction, ffcapi.ErrorReasonNonceTooLow: // If we already have a transaction hash, this is fine - we just return as if we submitted it - + if mtx.TransactionHash != "" { + log.L(ctx).Debugf("Transaction %s at nonce %s / %d known with hash: %s (%s)", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64(), mtx.TransactionHash, err) + return "", nil + } + // TODO: to cover the edge case where we had a timeout or other failure during the initial TransactionSend, we need to + // be able to re-calculate the hash that we would expect for the transaction. + // This would require a new FFCAPI API to calculate that hash, which requires the connector to perform the signing + // without submission to the node. For example using `eth_signTransaction` for EVM JSON/RPC. + return reason, err default: return reason, err } From 13bf8837a5b0c4ab1d9c1b9e7c1d2269417774ca Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 8 Aug 2022 16:56:09 -0400 Subject: [PATCH 89/95] Add debug for why receipt check missed Signed-off-by: Peter Broadhurst --- internal/confirmations/confirmations.go | 12 ++++++++---- internal/confirmations/confirmations_test.go | 2 +- internal/events/eventstream.go | 2 +- pkg/fftm/manager.go | 2 +- pkg/fftm/policyloop.go | 4 ++-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index 3d6cb083..05ff6848 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -99,9 +99,9 @@ type blockConfirmationManager struct { done chan struct{} } -func NewBlockConfirmationManager(baseContext context.Context, connector ffcapi.API) Manager { +func NewBlockConfirmationManager(baseContext context.Context, connector ffcapi.API, desc string) Manager { bcm := &blockConfirmationManager{ - baseContext: log.WithLogField(baseContext, "role", "confirmations"), + baseContext: baseContext, connector: connector, blockListenerStale: true, requiredConfirmations: config.GetInt(tmconfig.ConfirmationsRequired), @@ -112,6 +112,8 @@ func NewBlockConfirmationManager(baseContext context.Context, connector ffcapi.A newBlockHashes: make(chan *ffcapi.BlockHashEvent, config.GetInt(tmconfig.ConfirmationsBlockQueueLength)), } bcm.ctx, bcm.cancelFunc = context.WithCancel(baseContext) + // add a log context for this specific confirmation manager (as there are many within the ) + bcm.ctx = log.WithLogField(bcm.ctx, "role", fmt.Sprintf("confirmations_%s", desc)) return bcm } @@ -476,7 +478,7 @@ func (bcm *blockConfirmationManager) addOrReplaceItem(pending *pendingItem) { func (bcm *blockConfirmationManager) removeItem(pendingKey string, stale bool) { bcm.pendingMux.Lock() defer bcm.pendingMux.Unlock() - log.L(bcm.ctx).Infof("Removing item %s (stale=%t)", pendingKey, stale) + log.L(bcm.ctx).Debugf("Removing pending item %s (stale=%t)", pendingKey, stale) delete(bcm.pending, pendingKey) delete(bcm.staleReceipts, pendingKey) } @@ -508,12 +510,14 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { // For any transactions in the block that are known to us, we need to mark them // stale to go query the receipt + l := log.L(bcm.ctx) + l.Debugf("Transactions mined in block %d / %s: %v", block.BlockNumber, block.BlockHash, block.TransactionHashes) bcm.pendingMux.Lock() for _, txHash := range block.TransactionHashes { txKey := pendingKeyForTX(txHash) if pending, ok := bcm.pending[txKey]; ok { if pending.blockHash != block.BlockHash { - log.L(bcm.ctx).Infof("Detected transaction %s added to block %d / %s - receipt check scheduled", txHash, block.BlockNumber, block.BlockHash) + l.Infof("Detected transaction %s added to block %d / %s - receipt check scheduled", txHash, block.BlockNumber, block.BlockHash) bcm.staleReceipts[txKey] = true } } diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index c7b0ca21..761a3725 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -41,7 +41,7 @@ func newTestBlockConfirmationManager(t *testing.T, enabled bool) (*blockConfirma func newTestBlockConfirmationManagerCustomConfig(t *testing.T) (*blockConfirmationManager, *ffcapimocks.API) { logrus.SetLevel(logrus.DebugLevel) mca := &ffcapimocks.API{} - bcm := NewBlockConfirmationManager(context.Background(), mca) + bcm := NewBlockConfirmationManager(context.Background(), mca, "ut") return bcm.(*blockConfirmationManager), mca } diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 8e45d941..a2e15abb 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -136,7 +136,7 @@ func NewEventStream( checkpointInterval: config.GetDuration(tmconfig.EventStreamsCheckpointInterval), } if config.GetInt(tmconfig.ConfirmationsRequired) > 0 { - es.confirmations = confirmations.NewBlockConfirmationManager(esCtx, connector) + es.confirmations = confirmations.NewBlockConfirmationManager(esCtx, connector, "_es_"+persistedSpec.ID.String()) } // The configuration we have in memory, applies all the defaults to what is passed in // to ensure there are no nil fields on the configuration object. diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go index 31d55f90..35a006e4 100644 --- a/pkg/fftm/manager.go +++ b/pkg/fftm/manager.go @@ -123,7 +123,7 @@ type pendingState struct { } func (m *manager) initServices(ctx context.Context) (err error) { - m.confirmations = confirmations.NewBlockConfirmationManager(ctx, m.connector) + m.confirmations = confirmations.NewBlockConfirmationManager(ctx, m.connector, "receipts") m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBaseConfig, config.GetString(tmconfig.PolicyEngineName)) if err != nil { return err diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go index eb993fbf..411b4450 100644 --- a/pkg/fftm/policyloop.go +++ b/pkg/fftm/policyloop.go @@ -247,7 +247,7 @@ func (m *manager) trackSubmittedTransaction(ctx context.Context, pending *pendin m.mux.Lock() pending.mtx.Receipt = receipt m.mux.Unlock() - log.L(m.ctx).Infof("Receipt received for transaction %s at nonce %s / %d - hash: %s", pending.mtx.ID, pending.mtx.TransactionHeaders.From, pending.mtx.Nonce.Int64(), pending.mtx.TransactionHash) + log.L(m.ctx).Debugf("Receipt received for transaction %s at nonce %s / %d - hash: %s", pending.mtx.ID, pending.mtx.TransactionHeaders.From, pending.mtx.Nonce.Int64(), pending.mtx.TransactionHash) m.markInflightUpdate() }, Confirmed: func(ctx context.Context, confirmations []confirmations.BlockInfo) { @@ -256,7 +256,7 @@ func (m *manager) trackSubmittedTransaction(ctx context.Context, pending *pendin pending.confirmed = true pending.mtx.Confirmations = confirmations m.mux.Unlock() - log.L(m.ctx).Infof("Confirmed transaction %s at nonce %s / %d - hash: %s", pending.mtx.ID, pending.mtx.TransactionHeaders.From, pending.mtx.Nonce.Int64(), pending.mtx.TransactionHash) + log.L(m.ctx).Debugf("Confirmed transaction %s at nonce %s / %d - hash: %s", pending.mtx.ID, pending.mtx.TransactionHeaders.From, pending.mtx.Nonce.Int64(), pending.mtx.TransactionHash) m.markInflightUpdate() }, }, From 2f2ffe148600daa3cb068815b3197234f69e0e9b Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Mon, 8 Aug 2022 22:59:24 -0400 Subject: [PATCH 90/95] Correct order of starting event listener Signed-off-by: Peter Broadhurst --- internal/confirmations/confirmations.go | 4 ++-- internal/events/eventstream.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index 05ff6848..0fa15039 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -535,10 +535,10 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { expectedParentHash := pending.blockHash expectedBlockNumber := pending.blockNumber + 1 for i := 0; i < (len(pending.confirmations) + 1); i++ { - log.L(bcm.ctx).Tracef("Comparing block number=%d parent=%s to %d / %s for %s", blockNumber, block.ParentHash, expectedBlockNumber, expectedParentHash, pendingKey) + l.Tracef("Comparing block number=%d parent=%s to %d / %s for %s", blockNumber, block.ParentHash, expectedBlockNumber, expectedParentHash, pendingKey) if block.ParentHash == expectedParentHash && blockNumber == expectedBlockNumber { pending.confirmations = append(pending.confirmations[0:i], block) - log.L(bcm.ctx).Infof("Confirmation %d at block %d / %s item=%s", + l.Infof("Confirmation %d at block %d / %s item=%s", len(pending.confirmations), block.BlockNumber, block.BlockHash, pending.getKey()) break } diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index a2e15abb..321cf777 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -493,6 +493,7 @@ func (es *eventStream) Start(ctx context.Context) error { for _, l := range es.listeners { initialListeners = append(initialListeners, l.buildAddRequest(ctx, cp)) } + startedState.blocks, startedState.blockListenerDone = blocklistener.BufferChannel(startedState.ctx, es.confirmations) _, _, err = es.connector.EventStreamStart(startedState.ctx, &ffcapi.EventStreamStartRequest{ ID: es.spec.ID, EventStream: startedState.updates, @@ -508,7 +509,6 @@ func (es *eventStream) Start(ctx context.Context) error { // Kick off the loops go es.eventLoop(startedState) go es.batchLoop(startedState) - startedState.blocks, startedState.blockListenerDone = blocklistener.BufferChannel(startedState.ctx, es.confirmations) // Start the confirmations manager if es.confirmations != nil { From 28545ba184dbc2594f3959f47cf80134a419aac0 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 9 Aug 2022 15:54:42 -0400 Subject: [PATCH 91/95] Logging updates Signed-off-by: Peter Broadhurst --- internal/events/eventstream.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 321cf777..092d514f 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -360,7 +360,7 @@ func (es *eventStream) verifyListenerOptions(ctx context.Context, id *fftypes.UU } func (es *eventStream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, updates *apitypes.Listener, reset bool) (merged *apitypes.Listener, err error) { - log.L(ctx).Warnf("Adding/updating listener %s", id) + log.L(ctx).Infof("Adding/updating listener %s", id) // Ask the connector to verify the options, and apply defaults spec, err := es.verifyListenerOptions(ctx, id, updates) @@ -852,7 +852,7 @@ func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch * if l, ok := es.listeners[lID]; ok { l.checkpoint = lCP l.lastCheckpoint = fftypes.Now() - log.L(es.bgCtx).Tracef("%s (%s) checkpoint: %s", l.spec.Signature, l.spec.ID, lCP) + log.L(es.bgCtx).Tracef("%s (%s) checkpoint: %+v", l.spec.Signature, l.spec.ID, lCP) } } } From be30e469a1f2d4ab4588f549635084ed5868682b Mon Sep 17 00:00:00 2001 From: Nicko Guyer Date: Tue, 9 Aug 2022 17:16:29 -0400 Subject: [PATCH 92/95] Add backwards compatibility for listener methods Signed-off-by: Nicko Guyer --- internal/events/eventstream.go | 28 ++++++------ internal/events/eventstream_test.go | 12 ++--- internal/events/webhooks.go | 4 +- pkg/apitypes/api_types.go | 45 +++++++++---------- pkg/apitypes/api_types_test.go | 8 ++-- pkg/fftm/stream_management.go | 25 +++++++++++ pkg/fftm/stream_management_test.go | 68 +++++++++++++++++++++++++++++ 7 files changed, 142 insertions(+), 48 deletions(-) diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go index 092d514f..8b6aee7e 100644 --- a/internal/events/eventstream.go +++ b/internal/events/eventstream.go @@ -173,7 +173,7 @@ func (es *eventStream) initAction(startedState *startedStreamState) { func mergeValidateEsConfig(ctx context.Context, base *apitypes.EventStream, updates *apitypes.EventStream) (merged *apitypes.EventStream, changed bool, err error) { - // Merged is assured to not have any unset values (default set in all cases), or any deprecated fields + // Merged is assured to not have any unset values (default set in all cases), or any EthCompat fields if base == nil { base = &apitypes.EventStream{} } @@ -210,24 +210,24 @@ func mergeValidateEsConfig(ctx context.Context, base *apitypes.EventStream, upda changed = apitypes.CheckUpdateEnum(changed, &merged.ErrorHandling, base.ErrorHandling, updates.ErrorHandling, esDefaults.errorHandling) // Batch timeout - if updates.DeprecatedBatchTimeoutMS != nil { - dv := fftypes.FFDuration(*updates.DeprecatedBatchTimeoutMS) * fftypes.FFDuration(time.Millisecond) + if updates.EthCompatBatchTimeoutMS != nil { + dv := fftypes.FFDuration(*updates.EthCompatBatchTimeoutMS) * fftypes.FFDuration(time.Millisecond) changed = apitypes.CheckUpdateDuration(changed, &merged.BatchTimeout, base.BatchTimeout, &dv, esDefaults.batchTimeout) } else { changed = apitypes.CheckUpdateDuration(changed, &merged.BatchTimeout, base.BatchTimeout, updates.BatchTimeout, esDefaults.batchTimeout) } // Retry timeout - if updates.DeprecatedRetryTimeoutSec != nil { - dv := fftypes.FFDuration(*updates.DeprecatedRetryTimeoutSec) * fftypes.FFDuration(time.Second) + if updates.EthCompatRetryTimeoutSec != nil { + dv := fftypes.FFDuration(*updates.EthCompatRetryTimeoutSec) * fftypes.FFDuration(time.Second) changed = apitypes.CheckUpdateDuration(changed, &merged.RetryTimeout, base.RetryTimeout, &dv, esDefaults.retryTimeout) } else { changed = apitypes.CheckUpdateDuration(changed, &merged.RetryTimeout, base.RetryTimeout, updates.RetryTimeout, esDefaults.retryTimeout) } // Blocked retry delay - if updates.DeprecatedBlockedRetryDelaySec != nil { - dv := fftypes.FFDuration(*updates.DeprecatedBlockedRetryDelaySec) * fftypes.FFDuration(time.Second) + if updates.EthCompatBlockedRetryDelaySec != nil { + dv := fftypes.FFDuration(*updates.EthCompatBlockedRetryDelaySec) * fftypes.FFDuration(time.Second) changed = apitypes.CheckUpdateDuration(changed, &merged.BlockedRetryDelay, base.BlockedRetryDelay, &dv, esDefaults.blockedRetryDelay) } else { changed = apitypes.CheckUpdateDuration(changed, &merged.BlockedRetryDelay, base.BlockedRetryDelay, updates.BlockedRetryDelay, esDefaults.blockedRetryDelay) @@ -319,12 +319,12 @@ func (es *eventStream) mergeListenerOptions(id *fftypes.UUID, updates *apitypes. // Allow a single "event" object to be specified instead of a filter, with an optional "address". // This is migrated to the new syntax: `"filters":[{"address":"0x1235","event":{...}}]` // (only expected to work for the eth connector that supports address/event) - if updates.DeprecatedEvent != nil { + if updates.EthCompatEvent != nil { migrationFilter := fftypes.JSONObject{ - "event": updates.DeprecatedEvent, + "event": updates.EthCompatEvent, } - if updates.DeprecatedAddress != nil { - migrationFilter["address"] = *updates.DeprecatedAddress + if updates.EthCompatAddress != nil { + migrationFilter["address"] = *updates.EthCompatAddress } merged.Filters = []fftypes.JSONAny{fftypes.JSONAny(migrationFilter.String())} } else { @@ -722,9 +722,9 @@ func (es *eventStream) batchLoop(startedState *startedStreamState) { log.L(es.bgCtx).Debugf("%s '%s' event confirmed: %s", l.spec.ID, l.spec.Signature, fev.Event) batch.events = append(batch.events, &apitypes.EventWithContext{ StandardContext: apitypes.EventContext{ - StreamID: es.spec.ID, - DeprecatedSubID: l.spec.ID, - ListenerName: *l.spec.Name, + StreamID: es.spec.ID, + EthCompatSubID: l.spec.ID, + ListenerName: *l.spec.Name, }, Event: *fev.Event, }) diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go index 7fcee60a..d1671c23 100644 --- a/internal/events/eventstream_test.go +++ b/internal/events/eventstream_test.go @@ -302,12 +302,12 @@ func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { addr := "0x12345" l := &apitypes.Listener{ - ID: apitypes.NewULID(), - Name: strPtr("ut_listener"), - DeprecatedAddress: &addr, - DeprecatedEvent: fftypes.JSONAnyPtr(`{"event":"definition"}`), - Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), - FromBlock: strPtr("12345"), + ID: apitypes.NewULID(), + Name: strPtr("ut_listener"), + EthCompatAddress: &addr, + EthCompatEvent: fftypes.JSONAnyPtr(`{"event":"definition"}`), + Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), + FromBlock: strPtr("12345"), } mfc := es.connector.(*ffcapimocks.API) diff --git a/internal/events/webhooks.go b/internal/events/webhooks.go index bd3eaece..7a63617f 100644 --- a/internal/events/webhooks.go +++ b/internal/events/webhooks.go @@ -57,8 +57,8 @@ func mergeValidateWhConfig(ctx context.Context, changed bool, base *apitypes.Web changed = apitypes.CheckUpdateBool(changed, &merged.TLSkipHostVerify, base.TLSkipHostVerify, updates.TLSkipHostVerify, false) // Request timeout - if updates.DeprecatedRequestTimeoutSec != nil { - dv := fftypes.FFDuration(*updates.DeprecatedRequestTimeoutSec) * fftypes.FFDuration(time.Second) + if updates.EthCompatRequestTimeoutSec != nil { + dv := fftypes.FFDuration(*updates.EthCompatRequestTimeoutSec) * fftypes.FFDuration(time.Second) changed = apitypes.CheckUpdateDuration(changed, &merged.RequestTimeout, base.RequestTimeout, &dv, esDefaults.webhookRequestTimeout) } else { changed = apitypes.CheckUpdateDuration(changed, &merged.RequestTimeout, base.RequestTimeout, updates.RequestTimeout, esDefaults.webhookRequestTimeout) diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go index 3154c425..d247ea7f 100644 --- a/pkg/apitypes/api_types.go +++ b/pkg/apitypes/api_types.go @@ -61,9 +61,9 @@ type EventStream struct { RetryTimeout *fftypes.FFDuration `ffstruct:"eventstream" json:"retryTimeout"` BlockedRetryDelay *fftypes.FFDuration `ffstruct:"eventstream" json:"blockedRetryDelay"` - DeprecatedBatchTimeoutMS *uint64 `ffstruct:"eventstream" json:"batchTimeoutMS,omitempty"` // input only, for backwards compatibility - DeprecatedRetryTimeoutSec *uint64 `ffstruct:"eventstream" json:"retryTimeoutSec,omitempty"` // input only, for backwards compatibility - DeprecatedBlockedRetryDelaySec *uint64 `ffstruct:"eventstream" json:"blockedRetryDelaySec,omitempty"` // input only, for backwards compatibility + EthCompatBatchTimeoutMS *uint64 `ffstruct:"eventstream" json:"batchTimeoutMS,omitempty"` // input only, for backwards compatibility + EthCompatRetryTimeoutSec *uint64 `ffstruct:"eventstream" json:"retryTimeoutSec,omitempty"` // input only, for backwards compatibility + EthCompatBlockedRetryDelaySec *uint64 `ffstruct:"eventstream" json:"blockedRetryDelaySec,omitempty"` // input only, for backwards compatibility Webhook *WebhookConfig `ffstruct:"eventstream" json:"webhook,omitempty"` WebSocket *WebSocketConfig `ffstruct:"eventstream" json:"websocket,omitempty"` @@ -90,11 +90,11 @@ type EventStreamCheckpoint struct { } type WebhookConfig struct { - URL *string `ffstruct:"whconfig" json:"url,omitempty"` - Headers map[string]string `ffstruct:"whconfig" json:"headers,omitempty"` - TLSkipHostVerify *bool `ffstruct:"whconfig" json:"tlsSkipHostVerify,omitempty"` - RequestTimeout *fftypes.FFDuration `ffstruct:"whconfig" json:"requestTimeout,omitempty"` - DeprecatedRequestTimeoutSec *int64 `ffstruct:"whconfig" json:"requestTimeoutSec,omitempty"` // input only, for backwards compatibility + URL *string `ffstruct:"whconfig" json:"url,omitempty"` + Headers map[string]string `ffstruct:"whconfig" json:"headers,omitempty"` + TLSkipHostVerify *bool `ffstruct:"whconfig" json:"tlsSkipHostVerify,omitempty"` + RequestTimeout *fftypes.FFDuration `ffstruct:"whconfig" json:"requestTimeout,omitempty"` + EthCompatRequestTimeoutSec *int64 `ffstruct:"whconfig" json:"requestTimeoutSec,omitempty"` // input only, for backwards compatibility } type WebSocketConfig struct { @@ -103,17 +103,18 @@ type WebSocketConfig struct { } type Listener struct { - ID *fftypes.UUID `ffstruct:"listener" json:"id,omitempty"` - Created *fftypes.FFTime `ffstruct:"listener" json:"created"` - Updated *fftypes.FFTime `ffstruct:"listener" json:"updated"` - Name *string `ffstruct:"listener" json:"name"` - StreamID *fftypes.UUID `ffstruct:"listener" json:"stream" ffexcludeoutput:"true"` - DeprecatedAddress *string `ffstruct:"listener" json:"address,omitempty"` - DeprecatedEvent *fftypes.JSONAny `ffstruct:"listener" json:"event,omitempty"` - Filters []fftypes.JSONAny `ffstruct:"listener" json:"filters"` - Options *fftypes.JSONAny `ffstruct:"listener" json:"options"` - Signature string `ffstruct:"listener" json:"signature,omitempty" ffexcludeinput:"true"` - FromBlock *string `ffstruct:"listener" json:"fromBlock,omitempty"` + ID *fftypes.UUID `ffstruct:"listener" json:"id,omitempty"` + Created *fftypes.FFTime `ffstruct:"listener" json:"created"` + Updated *fftypes.FFTime `ffstruct:"listener" json:"updated"` + Name *string `ffstruct:"listener" json:"name"` + StreamID *fftypes.UUID `ffstruct:"listener" json:"stream" ffexcludeoutput:"true"` + EthCompatAddress *string `ffstruct:"listener" json:"address,omitempty"` + EthCompatEvent *fftypes.JSONAny `ffstruct:"listener" json:"event,omitempty"` + EthCompatMethods *fftypes.JSONAny `ffstruct:"listener" json:"methods,omitempty"` + Filters []fftypes.JSONAny `ffstruct:"listener" json:"filters"` + Options *fftypes.JSONAny `ffstruct:"listener" json:"options"` + Signature string `ffstruct:"listener" json:"signature,omitempty" ffexcludeinput:"true"` + FromBlock *string `ffstruct:"listener" json:"fromBlock,omitempty"` } // CheckUpdateString helper merges supplied configuration, with a base, and applies a default if unset @@ -210,9 +211,9 @@ func CheckUpdateStringMap(changed bool, merged *map[string]string, old map[strin } type EventContext struct { - StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event - DeprecatedSubID *fftypes.UUID `json:"subId"` // ID of the listener - deprecated "subscription" naming - ListenerName string `json:"listenerName"` // name of the listener + StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event + EthCompatSubID *fftypes.UUID `json:"subId"` // ID of the listener - EthCompat "subscription" naming + ListenerName string `json:"listenerName"` // name of the listener } // EventWithContext is what is delivered diff --git a/pkg/apitypes/api_types_test.go b/pkg/apitypes/api_types_test.go index 03a69666..f6e04c07 100644 --- a/pkg/apitypes/api_types_test.go +++ b/pkg/apitypes/api_types_test.go @@ -186,9 +186,9 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { e := &EventWithContext{ StandardContext: EventContext{ - StreamID: NewULID(), - ListenerName: "listener1", - DeprecatedSubID: NewULID(), + StreamID: NewULID(), + ListenerName: "listener1", + EthCompatSubID: NewULID(), }, Event: ffcapi.Event{ ID: ffcapi.EventID{ @@ -218,7 +218,7 @@ func TestMarshalUnmarshalEventOK(t *testing.T) { "listenerName":"listener1", "logIndex":"1", "signature":"ev()", - "subId":"`+e.StandardContext.DeprecatedSubID.String()+`", + "subId":"`+e.StandardContext.EthCompatSubID.String()+`", "streamId":"`+e.StandardContext.StreamID.String()+`", "transactionHash":"0x23456", "transactionIndex":"10" diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go index 36e298a3..4f8b0e88 100644 --- a/pkg/fftm/stream_management.go +++ b/pkg/fftm/stream_management.go @@ -18,6 +18,7 @@ package fftm import ( "context" + "encoding/json" "strconv" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -213,6 +214,9 @@ func (m *manager) updateExistingListener(ctx context.Context, streamIDStr, liste } func (m *manager) createOrUpdateListener(ctx context.Context, id *fftypes.UUID, newOrUpdates *apitypes.Listener, reset bool) (*apitypes.Listener, error) { + if err := mergeEthCompatMethods(ctx, newOrUpdates); err != nil { + return nil, err + } var s events.Stream if newOrUpdates.StreamID != nil { m.mux.Lock() @@ -381,3 +385,24 @@ func (m *manager) getStreamListeners(ctx context.Context, afterStr, limitStr, id } return m.persistence.ListStreamListeners(ctx, after, limit, persistence.SortDirectionDescending, id) } + +func mergeEthCompatMethods(ctx context.Context, listener *apitypes.Listener) error { + if listener.EthCompatMethods != nil { + if listener.Options == nil { + listener.Options = fftypes.JSONAnyPtr("{}") + } + var optionsMap map[string]interface{} + if err := listener.Options.Unmarshal(ctx, &optionsMap); err != nil { + return err + } + var methodList []interface{} + if err := listener.EthCompatMethods.Unmarshal(ctx, &methodList); err != nil { + return err + } + optionsMap["methods"] = methodList + b, _ := json.Marshal(optionsMap) + listener.Options = fftypes.JSONAnyPtrBytes(b) + listener.EthCompatMethods = nil + } + return nil +} diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go index b86326cd..8a60c80a 100644 --- a/pkg/fftm/stream_management_test.go +++ b/pkg/fftm/stream_management_test.go @@ -17,6 +17,8 @@ package fftm import ( + "context" + "encoding/json" "fmt" "testing" @@ -356,6 +358,32 @@ func TestCreateOrUpdateListenerFail(t *testing.T) { mp.AssertExpectations(t) } +func TestCreateOrUpdateListenerFailMergeEthCompatMethods(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + l := &apitypes.Listener{ + StreamID: es.ID, + EthCompatMethods: fftypes.JSONAnyPtr(`{}`), + } + + _, err = m.createOrUpdateListener(m.ctx, apitypes.NewULID(), l, false) + assert.Error(t, err) + + mp.AssertExpectations(t) +} + func TestCreateOrUpdateListenerWriteFail(t *testing.T) { _, m, close := newTestManagerMockPersistence(t) defer close() @@ -588,3 +616,43 @@ func TestGetStreamListenersBadStreamID(t *testing.T) { assert.Regexp(t, "FF00138", err) } + +func TestMergeEthCompatMethods(t *testing.T) { + l := &apitypes.Listener{ + EthCompatMethods: fftypes.JSONAnyPtr(`[{"method1": "awesomeMethod"}]`), + Options: fftypes.JSONAnyPtr(`{"otherOption": "otherValue"}`), + } + err := mergeEthCompatMethods(context.Background(), l) + assert.NoError(t, err) + b, err := json.Marshal(l.Options) + assert.NoError(t, err) + assert.JSONEq(t, `{"methods": [{"method1":"awesomeMethod"}], "otherOption":"otherValue"}`, string(b)) + assert.Nil(t, l.EthCompatMethods) + + l = &apitypes.Listener{ + EthCompatMethods: fftypes.JSONAnyPtr(`[{"method1": "awesomeMethod"}]`), + Options: nil, + } + err = mergeEthCompatMethods(context.Background(), l) + assert.NoError(t, err) + b, err = json.Marshal(l.Options) + assert.NoError(t, err) + assert.JSONEq(t, `{"methods": [{"method1":"awesomeMethod"}]}`, string(b)) + assert.Nil(t, l.EthCompatMethods) +} + +func TestMergeEthCompatMethodsFail(t *testing.T) { + l := &apitypes.Listener{ + EthCompatMethods: fftypes.JSONAnyPtr(`[{"method1": "awesomeMethod"}`), + Options: fftypes.JSONAnyPtr(`{"otherOption": "otherValue"}`), + } + err := mergeEthCompatMethods(context.Background(), l) + assert.Error(t, err) + + l = &apitypes.Listener{ + EthCompatMethods: fftypes.JSONAnyPtr(`[{"method1": "awesomeMethod"}]`), + Options: fftypes.JSONAnyPtr(`{"otherOption": "otherValue"`), + } + err = mergeEthCompatMethods(context.Background(), l) + assert.Error(t, err) +} From 48c077dfb600f0a61e5d9087bf09b9522cccd204 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 9 Aug 2022 21:52:08 -0400 Subject: [PATCH 93/95] Run tests in verbose mode on Github actions Signed-off-by: Peter Broadhurst --- .github/workflows/go.yml | 2 ++ Makefile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 91371aa9..ff3dd072 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -21,6 +21,8 @@ jobs: go-version: 1.17 - name: Build and Test + env: + TEST_FLAGS: -v run: make - name: Upload coverage diff --git a/Makefile b/Makefile index 54b49a9d..2fb5969e 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ GOGC=30 all: build test go-mod-tidy test: deps lint - $(VGO) test ./internal/... ./pkg/... -cover -coverprofile=coverage.txt -covermode=atomic -timeout=30s + $(VGO) test ./internal/... ./pkg/... -cover -coverprofile=coverage.txt -covermode=atomic -timeout=30s ${TEST_FLAGS} coverage.html: $(VGO) tool cover -html=coverage.txt coverage: test coverage.html From 251633a2d419e4e79f9867ad01fee3c592ee075d Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 9 Aug 2022 21:56:30 -0400 Subject: [PATCH 94/95] Avoid policy loop runing on API tests Signed-off-by: Peter Broadhurst --- pkg/fftm/route_get_transaction_test.go | 2 ++ pkg/fftm/route_get_transactions_test.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pkg/fftm/route_get_transaction_test.go b/pkg/fftm/route_get_transaction_test.go index 286fe79a..db04c883 100644 --- a/pkg/fftm/route_get_transaction_test.go +++ b/pkg/fftm/route_get_transaction_test.go @@ -19,6 +19,7 @@ package fftm import ( "fmt" "testing" + "time" "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" @@ -29,6 +30,7 @@ func TestGetTransaction(t *testing.T) { url, m, done := newTestManager(t) defer done() + m.policyLoopInterval = 1 * time.Hour err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go index 98151a03..a059ffc7 100644 --- a/pkg/fftm/route_get_transactions_test.go +++ b/pkg/fftm/route_get_transactions_test.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "testing" + "time" "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -48,6 +49,7 @@ func TestGetTransactions(t *testing.T) { url, m, done := newTestManager(t) defer done() + m.policyLoopInterval = 1 * time.Hour err := m.Start() assert.NoError(t, err) From b9bae8548622da55e16337e34b10853774867a58 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 9 Aug 2022 23:56:47 -0400 Subject: [PATCH 95/95] Put in a no-op policy engine for API transaction tests Signed-off-by: Peter Broadhurst --- pkg/fftm/route_get_transaction_test.go | 3 +-- pkg/fftm/route_get_transactions_test.go | 11 +++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/fftm/route_get_transaction_test.go b/pkg/fftm/route_get_transaction_test.go index db04c883..0fda2662 100644 --- a/pkg/fftm/route_get_transaction_test.go +++ b/pkg/fftm/route_get_transaction_test.go @@ -19,7 +19,6 @@ package fftm import ( "fmt" "testing" - "time" "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" @@ -30,7 +29,7 @@ func TestGetTransaction(t *testing.T) { url, m, done := newTestManager(t) defer done() - m.policyLoopInterval = 1 * time.Hour + noopPolicyEngine(m) err := m.Start() assert.NoError(t, err) diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go index a059ffc7..b2fd9924 100644 --- a/pkg/fftm/route_get_transactions_test.go +++ b/pkg/fftm/route_get_transactions_test.go @@ -20,13 +20,14 @@ import ( "context" "fmt" "testing" - "time" "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks" "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func newTestTxn(t *testing.T, m *manager, signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { @@ -45,11 +46,17 @@ func newTestTxn(t *testing.T, m *manager, signer string, nonce int64, status api return tx } +func noopPolicyEngine(m *manager) { + mpe := &policyenginemocks.PolicyEngine{} + m.policyEngine = mpe + mpe.On("Execute", mock.Anything, mock.Anything, mock.Anything).Return(false, ffcapi.ErrorReason(""), nil).Maybe() +} + func TestGetTransactions(t *testing.T) { url, m, done := newTestManager(t) defer done() - m.policyLoopInterval = 1 * time.Hour + noopPolicyEngine(m) err := m.Start() assert.NoError(t, err)