From 075377629c0f3143760eda9f284871775f458190 Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Thu, 18 Jan 2024 17:00:24 +0200 Subject: [PATCH 01/32] start working on capability.go --- core/capabilities/capability.go | 94 ++++++++++++++++++++++++++++ core/capabilities/capability_test.go | 26 ++++++++ go.sum | 9 +++ 3 files changed, 129 insertions(+) create mode 100644 core/capabilities/capability.go create mode 100644 core/capabilities/capability_test.go diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go new file mode 100644 index 00000000000..8e3566451fd --- /dev/null +++ b/core/capabilities/capability.go @@ -0,0 +1,94 @@ +package capabilities + +import ( + "fmt" + "regexp" + "golang.org/x/mod/semver" + "github.com/smartcontractkit/chainlink-common/pkg/values" +) + +type CapabilityType string + +const ( + CapabilityTypeTrigger CapabilityType = "trigger" + CapabilityTypeAction CapabilityType = "action" + CapabilityTypeReport CapabilityType = "report" + CapabilityTypeTarget CapabilityType = "target" +) + +func (c CapabilityType) IsValid() error { + switch c { + case CapabilityTypeTrigger, + CapabilityTypeAction, + CapabilityTypeReport, + CapabilityTypeTarget: + return nil + } + + return fmt.Errorf("invalid capability type: %s", c) +} + +type Validatable interface { + ValidateConfig(config values.Map) error + ExampleOutput() values.Value + ValidateInput(inputs values.Map) error +} + +type CapabilityInfo struct { + // We use `fmt.Stringer` for the ID, since an ID can take + // one of two forms (namely a fully-qualified ID expressed as a + // string), or a tags object. + id fmt.Stringer + capabilityType CapabilityType + description string + version string +} + +type Capability interface { + Validatable + Info() (CapabilityInfo) +} + +type CapabilityRegistry interface { + ListCapabilities() ([]CapabilityInfo) + Get(id string) (Capability, error) + Add(capability Capability) error +} + +type CapabilityInfoProvider struct { + info CapabilityInfo +} + +func (c *CapabilityInfoProvider) Info() CapabilityInfo { + return c.info +} + +var idRegex = regexp.MustCompile("[a-z0-9_-:]") + +func NewCapabilityInfoProvider( + id fmt.Stringer, + capabilityType CapabilityType, + description string, + version string, +) (*CapabilityInfoProvider, error) { + if !idRegex.MatchString(id.String()) { + return nil, fmt.Errorf("invalid id: %s. Allowed: %s", id, idRegex) + } + + if ok := semver.IsValid(version); !ok { + return nil, fmt.Errorf("invalid version: %+v", version) + } + + if err := capabilityType.IsValid(); err != nil { + return nil, err + } + + return &CapabilityInfoProvider{ + info: CapabilityInfo{ + id: id, + capabilityType: capabilityType, + description: description, + version: version, + }, + }, nil +} diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go new file mode 100644 index 00000000000..eea411e03c2 --- /dev/null +++ b/core/capabilities/capability_test.go @@ -0,0 +1,26 @@ +package capabilities + +import ( + "fmt" + "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type capabilityUnderTest struct { + Validatable + CapabilityInfoProvider +} + +func Test_CapabilityInfo(t *testing.T) { + + capability, err := NewCapabilityInfoProvider(fmt.Stringer(), CapabilityTypeAction, "This is a mock capability that doesn't do anything.", "test") + + require.NoError(t, err) + + capabilityUnderTest := capabilityUnderTest{ + capability, + } + + assert.Equal(t, mockCapabilityInfo, capability.Info()) +} diff --git a/go.sum b/go.sum index c8f6620af85..6590280dd40 100644 --- a/go.sum +++ b/go.sum @@ -1164,10 +1164,19 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429 h1:xkejUBZhcBpBrTSfxc91Iwzadrb6SXw8ks69bHIQ9Ww= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429/go.mod h1:wJmVvDf4XSjsahWtfUq3wvIAYEAuhr7oxmxYnEL/LGQ= +<<<<<<< HEAD github.com/smartcontractkit/chainlink-common v0.1.7-0.20240124161023-948579cbaffa h1:9g7e1C3295ALDK8Gs42fIKSSJfI+H1RoBmivGWTvIZo= github.com/smartcontractkit/chainlink-common v0.1.7-0.20240124161023-948579cbaffa/go.mod h1:05rRF84QKlIOF5LfTBPkHdw4UpBI2G3zxRcuZ65bPjk= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240120192246-4bb04c997ca0 h1:NALwENz6vQ972DuD9AZjqRjyNSxH9ptNapizQGLI+2s= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240120192246-4bb04c997ca0/go.mod h1:NcVAT/GETDBvIoAej5K6OYqAtDOkF6vO5pYw/hLuYVU= +======= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240118012339-4864e2306bb1 h1:3cWO2/lFVDul5SVTgl4/RX/GXcT8Zq5NGMPeNEz09tY= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240118012339-4864e2306bb1/go.mod h1:f+0ei9N4PlTJHu7pbGzEjTnBUr45syPdGFu5+31lS5Q= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240118113240-6076556bc113 h1:jamUJfP289sUgdsJcR1u6zTf5YB8zZ2wMmDbxfSfszo= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240118113240-6076556bc113/go.mod h1:f+0ei9N4PlTJHu7pbGzEjTnBUr45syPdGFu5+31lS5Q= +github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231218175426-6e0427c661e5 h1:kBnmjv3fxU7krVIqZFvo1m4F6qBc4vPURQFX/mcChhI= +github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231218175426-6e0427c661e5/go.mod h1:EoM7wQ81mov7wsUzG4zEnnr0EH0POEo/I0hRDg433TU= +>>>>>>> 5641cf429d (start working on capability.go) github.com/smartcontractkit/chainlink-data-streams v0.0.0-20231204152908-a6e3fe8ff2a1 h1:xYqRgZO0nMSO8CBCMR0r3WA+LZ4kNL8a6bnbyk/oBtQ= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20231204152908-a6e3fe8ff2a1/go.mod h1:GuPvyXryvbiUZIHmPeLBz4L+yJKeyGUjrDfd1KNne+o= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240119021347-3c541a78cdb8 h1:1BcjXuviSAKttOX7BZoVHRZZGfxqoA2+AL8tykmkdoc= From 1d0ea04fd5f66d5153747781809b22341e0c16a8 Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Thu, 18 Jan 2024 15:56:42 +0000 Subject: [PATCH 02/32] Combine CapabilityInfoProvider and CapabilityInfo; Add validation tests --- core/capabilities/capability.go | 80 +++++++++++++++------------- core/capabilities/capability_test.go | 44 ++++++++++----- core/capabilities/registry.go | 55 +++++++++++++++++++ core/capabilities/registry_test.go | 68 +++++++++++++++++++++++ 4 files changed, 199 insertions(+), 48 deletions(-) create mode 100644 core/capabilities/registry.go create mode 100644 core/capabilities/registry_test.go diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 8e3566451fd..4903bd7deed 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -3,7 +3,9 @@ package capabilities import ( "fmt" "regexp" + "golang.org/x/mod/semver" + "github.com/smartcontractkit/chainlink-common/pkg/values" ) @@ -11,18 +13,30 @@ type CapabilityType string const ( CapabilityTypeTrigger CapabilityType = "trigger" - CapabilityTypeAction CapabilityType = "action" - CapabilityTypeReport CapabilityType = "report" - CapabilityTypeTarget CapabilityType = "target" + CapabilityTypeAction CapabilityType = "action" + CapabilityTypeReport CapabilityType = "report" + CapabilityTypeTarget CapabilityType = "target" ) +type stringer struct { + s string +} + +func (s stringer) String() string { + return s.s +} + +func Stringer(s string) fmt.Stringer { + return stringer{s: s} +} + func (c CapabilityType) IsValid() error { switch c { - case CapabilityTypeTrigger, - CapabilityTypeAction, - CapabilityTypeReport, - CapabilityTypeTarget: - return nil + case CapabilityTypeTrigger, + CapabilityTypeAction, + CapabilityTypeReport, + CapabilityTypeTarget: + return nil } return fmt.Errorf("invalid capability type: %s", c) @@ -38,57 +52,51 @@ type CapabilityInfo struct { // We use `fmt.Stringer` for the ID, since an ID can take // one of two forms (namely a fully-qualified ID expressed as a // string), or a tags object. - id fmt.Stringer - capabilityType CapabilityType - description string - version string + Id fmt.Stringer + CapabilityType CapabilityType + Description string + Version string +} + +func (c CapabilityInfo) Info() CapabilityInfo { + return c } type Capability interface { Validatable - Info() (CapabilityInfo) + Info() CapabilityInfo } type CapabilityRegistry interface { - ListCapabilities() ([]CapabilityInfo) + ListCapabilities() []CapabilityInfo Get(id string) (Capability, error) Add(capability Capability) error } -type CapabilityInfoProvider struct { - info CapabilityInfo -} +var idRegex = regexp.MustCompile("[a-z0-9_\\-:]") -func (c *CapabilityInfoProvider) Info() CapabilityInfo { - return c.info -} - -var idRegex = regexp.MustCompile("[a-z0-9_-:]") - -func NewCapabilityInfoProvider( +func NewCapabilityInfo( id fmt.Stringer, capabilityType CapabilityType, description string, version string, -) (*CapabilityInfoProvider, error) { +) (CapabilityInfo, error) { if !idRegex.MatchString(id.String()) { - return nil, fmt.Errorf("invalid id: %s. Allowed: %s", id, idRegex) + return CapabilityInfo{}, fmt.Errorf("invalid id: %s. Allowed: %s", id, idRegex) } if ok := semver.IsValid(version); !ok { - return nil, fmt.Errorf("invalid version: %+v", version) + return CapabilityInfo{}, fmt.Errorf("invalid version: %+v", version) } - + if err := capabilityType.IsValid(); err != nil { - return nil, err + return CapabilityInfo{}, err } - return &CapabilityInfoProvider{ - info: CapabilityInfo{ - id: id, - capabilityType: capabilityType, - description: description, - version: version, - }, + return CapabilityInfo{ + Id: id, + CapabilityType: capabilityType, + Description: description, + Version: version, }, nil } diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index eea411e03c2..424ff51c228 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -1,26 +1,46 @@ package capabilities import ( - "fmt" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -type capabilityUnderTest struct { - Validatable - CapabilityInfoProvider -} - func Test_CapabilityInfo(t *testing.T) { + ci, err := NewCapabilityInfo( + Stringer("capability-id"), + CapabilityTypeAction, + "This is a mock capability that doesn't do anything.", + "v1.0.0", + ) + require.NoError(t, err) - capability, err := NewCapabilityInfoProvider(fmt.Stringer(), CapabilityTypeAction, "This is a mock capability that doesn't do anything.", "test") + assert.Equal(t, ci, ci.Info()) +} - require.NoError(t, err) +func Test_CapabilityInfo_Invalid(t *testing.T) { + _, err := NewCapabilityInfo( + Stringer("capability-id"), + "test", + "This is a mock capability that doesn't do anything.", + "v1.0.0", + ) + assert.ErrorContains(t, err, "invalid capability type") - capabilityUnderTest := capabilityUnderTest{ - capability, - } + _, err = NewCapabilityInfo( + Stringer("&!!!"), + CapabilityTypeAction, + "This is a mock capability that doesn't do anything.", + "v1.0.0", + ) + assert.ErrorContains(t, err, "invalid id") - assert.Equal(t, mockCapabilityInfo, capability.Info()) + _, err = NewCapabilityInfo( + Stringer("mock-capability"), + CapabilityTypeAction, + "This is a mock capability that doesn't do anything.", + "hello", + ) + assert.ErrorContains(t, err, "invalid version") } diff --git a/core/capabilities/registry.go b/core/capabilities/registry.go new file mode 100644 index 00000000000..20519456b01 --- /dev/null +++ b/core/capabilities/registry.go @@ -0,0 +1,55 @@ +package capabilities + +import ( + "fmt" + "sync" +) + +type Registry struct { + m map[string]Capability + mu sync.RWMutex +} + +func (r *Registry) Get(id fmt.Stringer) (Capability, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + c, ok := r.m[id.String()] + if !ok { + return nil, fmt.Errorf("capability not found with id %s", id) + } + + return c, nil +} + +func (r *Registry) List() []Capability { + r.mu.RLock() + defer r.mu.RUnlock() + cl := []Capability{} + for _, v := range r.m { + cl = append(cl, v) + } + + return cl +} + +func (r *Registry) Add(c Capability) error { + r.mu.Lock() + defer r.mu.Unlock() + + id := c.Info().Id.String() + _, ok := r.m[id] + if ok { + return fmt.Errorf("capability with id: %s already exists", id) + } + + r.m[id] = c + return nil + +} + +func NewRegistry() *Registry { + return &Registry{ + m: map[string]Capability{}, + } +} diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go new file mode 100644 index 00000000000..7703eb2ae42 --- /dev/null +++ b/core/capabilities/registry_test.go @@ -0,0 +1,68 @@ +package capabilities + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockCapability struct { + Validatable + CapabilityInfo +} + +func TestRegistry(t *testing.T) { + r := NewRegistry() + + id := Stringer("capability-1") + ci, err := NewCapabilityInfo( + id, + CapabilityTypeAction, + "capability-1-description", + "v1.0.0", + ) + require.NoError(t, err) + + c := &mockCapability{CapabilityInfo: ci} + err = r.Add(c) + require.NoError(t, err) + + gc, err := r.Get(id) + require.NoError(t, err) + + assert.Equal(t, c, gc) + + cs := r.List() + assert.Len(t, cs, 1) + assert.Equal(t, c, cs[0]) +} + +func TestRegistry_NoDuplicateIDs(t *testing.T) { + r := NewRegistry() + + id := Stringer("capability-1") + ci, err := NewCapabilityInfo( + id, + CapabilityTypeAction, + "capability-1-description", + "v1.0.0", + ) + require.NoError(t, err) + + c := &mockCapability{CapabilityInfo: ci} + err = r.Add(c) + require.NoError(t, err) + + ci, err = NewCapabilityInfo( + id, + CapabilityTypeReport, + "capability-2-description", + "v1.0.0", + ) + require.NoError(t, err) + c2 := &mockCapability{CapabilityInfo: ci} + + err = r.Add(c2) + assert.ErrorContains(t, err, "capability with id: capability-1 already exists") +} From bebe6cd02758e38ced98221b4a9d952ff71a804a Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Fri, 19 Jan 2024 10:46:18 +0000 Subject: [PATCH 03/32] Validate the capability type when adding it --- core/capabilities/capability.go | 57 ++++---- core/capabilities/registry.go | 61 ++++++++- core/capabilities/registry_test.go | 202 ++++++++++++++++++++++++++++- go.mod | 2 +- go.sum | 9 -- 5 files changed, 293 insertions(+), 38 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 4903bd7deed..b0b5d19d791 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -1,6 +1,7 @@ package capabilities import ( + "context" "fmt" "regexp" @@ -18,18 +19,6 @@ const ( CapabilityTypeTarget CapabilityType = "target" ) -type stringer struct { - s string -} - -func (s stringer) String() string { - return s.s -} - -func Stringer(s string) fmt.Stringer { - return stringer{s: s} -} - func (c CapabilityType) IsValid() error { switch c { case CapabilityTypeTrigger, @@ -48,6 +37,39 @@ type Validatable interface { ValidateInput(inputs values.Map) error } +type Capability interface { + Validatable + Info() CapabilityInfo +} + +type SynchronousCapability interface { + Capability + + Start(ctx context.Context, config values.Map) (values.Value, error) + Execute(ctx context.Context, inputs values.Map) (values.Value, error) + Stop(ctx context.Context) error +} + +type AsynchronousCapability interface { + Capability + + Start(ctx context.Context, config values.Map) (values.Value, error) + Execute(ctx context.Context, callback chan values.Map, inputs values.Map) (values.Value, error) + Stop(ctx context.Context) error +} + +type stringer struct { + s string +} + +func (s stringer) String() string { + return s.s +} + +func Stringer(s string) fmt.Stringer { + return stringer{s: s} +} + type CapabilityInfo struct { // We use `fmt.Stringer` for the ID, since an ID can take // one of two forms (namely a fully-qualified ID expressed as a @@ -62,17 +84,6 @@ func (c CapabilityInfo) Info() CapabilityInfo { return c } -type Capability interface { - Validatable - Info() CapabilityInfo -} - -type CapabilityRegistry interface { - ListCapabilities() []CapabilityInfo - Get(id string) (Capability, error) - Add(capability Capability) error -} - var idRegex = regexp.MustCompile("[a-z0-9_\\-:]") func NewCapabilityInfo( diff --git a/core/capabilities/registry.go b/core/capabilities/registry.go index 20519456b01..9cbc54e74fd 100644 --- a/core/capabilities/registry.go +++ b/core/capabilities/registry.go @@ -22,6 +22,51 @@ func (r *Registry) Get(id fmt.Stringer) (Capability, error) { return c, nil } +func (r *Registry) getCapabilityOfType(id fmt.Stringer, ct CapabilityType) (Capability, error) { + c, err := r.Get(id) + if err != nil { + return nil, err + } + + if c.Info().CapabilityType != ct { + return nil, fmt.Errorf("capability with id %s is not of type %s", id, ct) + } + + return c, nil +} + +func (r *Registry) GetAction(id fmt.Stringer) (SynchronousCapability, error) { + c, err := r.getCapabilityOfType(id, CapabilityTypeAction) + if err != nil { + return nil, err + } + return c.(SynchronousCapability), err +} + +func (r *Registry) GetTarget(id fmt.Stringer) (SynchronousCapability, error) { + c, err := r.getCapabilityOfType(id, CapabilityTypeTarget) + if err != nil { + return nil, err + } + return c.(SynchronousCapability), err +} + +func (r *Registry) GetTrigger(id fmt.Stringer) (AsynchronousCapability, error) { + c, err := r.getCapabilityOfType(id, CapabilityTypeTrigger) + if err != nil { + return nil, err + } + return c.(AsynchronousCapability), err +} + +func (r *Registry) GetReport(id fmt.Stringer) (AsynchronousCapability, error) { + c, err := r.getCapabilityOfType(id, CapabilityTypeReport) + if err != nil { + return nil, err + } + return c.(AsynchronousCapability), err +} + func (r *Registry) List() []Capability { r.mu.RLock() defer r.mu.RUnlock() @@ -37,12 +82,26 @@ func (r *Registry) Add(c Capability) error { r.mu.Lock() defer r.mu.Unlock() - id := c.Info().Id.String() + info := c.Info() + id := info.Id.String() _, ok := r.m[id] if ok { return fmt.Errorf("capability with id: %s already exists", id) } + switch info.CapabilityType { + case CapabilityTypeAction, CapabilityTypeTarget: + _, ok := c.(SynchronousCapability) + if !ok { + return fmt.Errorf("capability with id %s, type %s does not satisfy SynchronousCapability interface", id, info.CapabilityType) + } + case CapabilityTypeReport, CapabilityTypeTrigger: + _, ok := c.(AsynchronousCapability) + if !ok { + return fmt.Errorf("capability with id %s, type %s does not satisfy AsynchronousCapability interface", id, info.CapabilityType) + } + } + r.m[id] = c return nil diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index 7703eb2ae42..16a50907663 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -1,17 +1,50 @@ package capabilities import ( + "context" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/values" ) -type mockCapability struct { +type smockCapability struct { Validatable CapabilityInfo } +func (m *smockCapability) Start(ctx context.Context, config values.Map) (values.Value, error) { + return nil, nil +} + +func (m *smockCapability) Stop(ctx context.Context) error { + return nil +} + +func (m *smockCapability) Execute(ctx context.Context, inputs values.Map) (values.Value, error) { + return nil, nil +} + +type cmockCapability struct { + Validatable + CapabilityInfo +} + +func (m *cmockCapability) Start(ctx context.Context, config values.Map) (values.Value, error) { + return nil, nil +} + +func (m *cmockCapability) Stop(ctx context.Context) error { + return nil +} + +func (m *cmockCapability) Execute(ctx context.Context, callback chan values.Map, inputs values.Map) (values.Value, error) { + return nil, nil +} + func TestRegistry(t *testing.T) { r := NewRegistry() @@ -24,7 +57,7 @@ func TestRegistry(t *testing.T) { ) require.NoError(t, err) - c := &mockCapability{CapabilityInfo: ci} + c := &smockCapability{CapabilityInfo: ci} err = r.Add(c) require.NoError(t, err) @@ -50,7 +83,7 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { ) require.NoError(t, err) - c := &mockCapability{CapabilityInfo: ci} + c := &smockCapability{CapabilityInfo: ci} err = r.Add(c) require.NoError(t, err) @@ -61,8 +94,169 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { "v1.0.0", ) require.NoError(t, err) - c2 := &mockCapability{CapabilityInfo: ci} + c2 := &smockCapability{CapabilityInfo: ci} err = r.Add(c2) assert.ErrorContains(t, err, "capability with id: capability-1 already exists") } + +func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { + tcs := []struct { + name string + newCapability func() Capability + errContains string + }{ + { + name: "trigger, sync", + newCapability: func() Capability { + id := uuid.New() + ci, err := NewCapabilityInfo( + id, + CapabilityTypeTrigger, + "capability-1-description", + "v1.0.0", + ) + require.NoError(t, err) + + return &smockCapability{CapabilityInfo: ci} + }, + errContains: "does not satisfy AsynchronousCapability interface", + }, + { + name: "reports, sync", + newCapability: func() Capability { + id := uuid.New() + ci, err := NewCapabilityInfo( + id, + CapabilityTypeReport, + "capability-1-description", + "v1.0.0", + ) + require.NoError(t, err) + + return &smockCapability{CapabilityInfo: ci} + }, + errContains: "does not satisfy AsynchronousCapability interface", + }, + { + name: "action, sync", + newCapability: func() Capability { + id := uuid.New() + ci, err := NewCapabilityInfo( + id, + CapabilityTypeAction, + "capability-1-description", + "v1.0.0", + ) + require.NoError(t, err) + + return &smockCapability{CapabilityInfo: ci} + }, + }, + { + name: "target, sync", + newCapability: func() Capability { + id := uuid.New() + ci, err := NewCapabilityInfo( + id, + CapabilityTypeTarget, + "capability-1-description", + "v1.0.0", + ) + require.NoError(t, err) + + return &smockCapability{CapabilityInfo: ci} + }, + }, + { + name: "trigger, async", + newCapability: func() Capability { + id := uuid.New() + ci, err := NewCapabilityInfo( + id, + CapabilityTypeTrigger, + "capability-1-description", + "v1.0.0", + ) + require.NoError(t, err) + + return &cmockCapability{CapabilityInfo: ci} + }, + }, + { + name: "reports, async", + newCapability: func() Capability { + id := uuid.New() + ci, err := NewCapabilityInfo( + id, + CapabilityTypeReport, + "capability-1-description", + "v1.0.0", + ) + require.NoError(t, err) + + return &cmockCapability{CapabilityInfo: ci} + }, + }, + { + name: "action, async", + newCapability: func() Capability { + id := uuid.New() + ci, err := NewCapabilityInfo( + id, + CapabilityTypeAction, + "capability-1-description", + "v1.0.0", + ) + require.NoError(t, err) + + return &cmockCapability{CapabilityInfo: ci} + }, + errContains: "does not satisfy SynchronousCapability interface", + }, + { + name: "target, async", + newCapability: func() Capability { + id := uuid.New() + ci, err := NewCapabilityInfo( + id, + CapabilityTypeTarget, + "capability-1-description", + "v1.0.0", + ) + require.NoError(t, err) + + return &cmockCapability{CapabilityInfo: ci} + }, + errContains: "does not satisfy SynchronousCapability interface", + }, + } + + reg := NewRegistry() + for _, tc := range tcs { + c := tc.newCapability() + err := reg.Add(c) + if tc.errContains == "" { + require.NoError(t, err) + + info := c.Info() + id := info.Id + switch info.CapabilityType { + case CapabilityTypeAction: + _, err := reg.GetAction(id) + require.NoError(t, err) + case CapabilityTypeTarget: + _, err := reg.GetTarget(id) + require.NoError(t, err) + case CapabilityTypeTrigger: + _, err := reg.GetTrigger(id) + require.NoError(t, err) + case CapabilityTypeReport: + _, err := reg.GetReport(id) + require.NoError(t, err) + } + } else { + assert.ErrorContains(t, err, tc.errContains) + } + } +} diff --git a/go.mod b/go.mod index cbcad20919f..6c0ea3281c0 100644 --- a/go.mod +++ b/go.mod @@ -93,6 +93,7 @@ require ( go.uber.org/zap v1.26.0 golang.org/x/crypto v0.17.0 golang.org/x/exp v0.0.0-20231127185646-65229373498e + golang.org/x/mod v0.14.0 golang.org/x/sync v0.5.0 golang.org/x/term v0.15.0 golang.org/x/text v0.14.0 @@ -312,7 +313,6 @@ require ( go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect golang.org/x/arch v0.6.0 // indirect - golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect diff --git a/go.sum b/go.sum index 6590280dd40..c8f6620af85 100644 --- a/go.sum +++ b/go.sum @@ -1164,19 +1164,10 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429 h1:xkejUBZhcBpBrTSfxc91Iwzadrb6SXw8ks69bHIQ9Ww= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429/go.mod h1:wJmVvDf4XSjsahWtfUq3wvIAYEAuhr7oxmxYnEL/LGQ= -<<<<<<< HEAD github.com/smartcontractkit/chainlink-common v0.1.7-0.20240124161023-948579cbaffa h1:9g7e1C3295ALDK8Gs42fIKSSJfI+H1RoBmivGWTvIZo= github.com/smartcontractkit/chainlink-common v0.1.7-0.20240124161023-948579cbaffa/go.mod h1:05rRF84QKlIOF5LfTBPkHdw4UpBI2G3zxRcuZ65bPjk= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240120192246-4bb04c997ca0 h1:NALwENz6vQ972DuD9AZjqRjyNSxH9ptNapizQGLI+2s= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240120192246-4bb04c997ca0/go.mod h1:NcVAT/GETDBvIoAej5K6OYqAtDOkF6vO5pYw/hLuYVU= -======= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240118012339-4864e2306bb1 h1:3cWO2/lFVDul5SVTgl4/RX/GXcT8Zq5NGMPeNEz09tY= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240118012339-4864e2306bb1/go.mod h1:f+0ei9N4PlTJHu7pbGzEjTnBUr45syPdGFu5+31lS5Q= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240118113240-6076556bc113 h1:jamUJfP289sUgdsJcR1u6zTf5YB8zZ2wMmDbxfSfszo= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240118113240-6076556bc113/go.mod h1:f+0ei9N4PlTJHu7pbGzEjTnBUr45syPdGFu5+31lS5Q= -github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231218175426-6e0427c661e5 h1:kBnmjv3fxU7krVIqZFvo1m4F6qBc4vPURQFX/mcChhI= -github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231218175426-6e0427c661e5/go.mod h1:EoM7wQ81mov7wsUzG4zEnnr0EH0POEo/I0hRDg433TU= ->>>>>>> 5641cf429d (start working on capability.go) github.com/smartcontractkit/chainlink-data-streams v0.0.0-20231204152908-a6e3fe8ff2a1 h1:xYqRgZO0nMSO8CBCMR0r3WA+LZ4kNL8a6bnbyk/oBtQ= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20231204152908-a6e3fe8ff2a1/go.mod h1:GuPvyXryvbiUZIHmPeLBz4L+yJKeyGUjrDfd1KNne+o= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240119021347-3c541a78cdb8 h1:1BcjXuviSAKttOX7BZoVHRZZGfxqoA2+AL8tykmkdoc= From 7694b3f43c7d883c2664ddf8300c615157156c79 Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Tue, 23 Jan 2024 12:27:11 +0200 Subject: [PATCH 04/32] Replace Stringer with string for simplicity --- core/capabilities/capability.go | 21 ++----------- core/capabilities/capability_test.go | 8 ++--- core/capabilities/registry.go | 31 ++++++++++---------- core/capabilities/registry_test.go | 44 +++++++++++++++------------- 4 files changed, 47 insertions(+), 57 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index b0b5d19d791..4ef26cc7c7b 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -58,23 +58,8 @@ type AsynchronousCapability interface { Stop(ctx context.Context) error } -type stringer struct { - s string -} - -func (s stringer) String() string { - return s.s -} - -func Stringer(s string) fmt.Stringer { - return stringer{s: s} -} - type CapabilityInfo struct { - // We use `fmt.Stringer` for the ID, since an ID can take - // one of two forms (namely a fully-qualified ID expressed as a - // string), or a tags object. - Id fmt.Stringer + Id string CapabilityType CapabilityType Description string Version string @@ -87,12 +72,12 @@ func (c CapabilityInfo) Info() CapabilityInfo { var idRegex = regexp.MustCompile("[a-z0-9_\\-:]") func NewCapabilityInfo( - id fmt.Stringer, + id string, capabilityType CapabilityType, description string, version string, ) (CapabilityInfo, error) { - if !idRegex.MatchString(id.String()) { + if !idRegex.MatchString(id) { return CapabilityInfo{}, fmt.Errorf("invalid id: %s. Allowed: %s", id, idRegex) } diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index 424ff51c228..086e0c2bd57 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -9,7 +9,7 @@ import ( func Test_CapabilityInfo(t *testing.T) { ci, err := NewCapabilityInfo( - Stringer("capability-id"), + "capability-id", CapabilityTypeAction, "This is a mock capability that doesn't do anything.", "v1.0.0", @@ -21,7 +21,7 @@ func Test_CapabilityInfo(t *testing.T) { func Test_CapabilityInfo_Invalid(t *testing.T) { _, err := NewCapabilityInfo( - Stringer("capability-id"), + "capability-id", "test", "This is a mock capability that doesn't do anything.", "v1.0.0", @@ -29,7 +29,7 @@ func Test_CapabilityInfo_Invalid(t *testing.T) { assert.ErrorContains(t, err, "invalid capability type") _, err = NewCapabilityInfo( - Stringer("&!!!"), + "&!!!", CapabilityTypeAction, "This is a mock capability that doesn't do anything.", "v1.0.0", @@ -37,7 +37,7 @@ func Test_CapabilityInfo_Invalid(t *testing.T) { assert.ErrorContains(t, err, "invalid id") _, err = NewCapabilityInfo( - Stringer("mock-capability"), + "mock-capability", CapabilityTypeAction, "This is a mock capability that doesn't do anything.", "hello", diff --git a/core/capabilities/registry.go b/core/capabilities/registry.go index 9cbc54e74fd..aeed2c0348b 100644 --- a/core/capabilities/registry.go +++ b/core/capabilities/registry.go @@ -1,6 +1,7 @@ package capabilities import ( + "context" "fmt" "sync" ) @@ -10,11 +11,11 @@ type Registry struct { mu sync.RWMutex } -func (r *Registry) Get(id fmt.Stringer) (Capability, error) { +func (r *Registry) Get(_ context.Context, id string) (Capability, error) { r.mu.RLock() defer r.mu.RUnlock() - c, ok := r.m[id.String()] + c, ok := r.m[id] if !ok { return nil, fmt.Errorf("capability not found with id %s", id) } @@ -22,8 +23,8 @@ func (r *Registry) Get(id fmt.Stringer) (Capability, error) { return c, nil } -func (r *Registry) getCapabilityOfType(id fmt.Stringer, ct CapabilityType) (Capability, error) { - c, err := r.Get(id) +func (r *Registry) getCapabilityOfType(ctx context.Context, id string, ct CapabilityType) (Capability, error) { + c, err := r.Get(ctx, id) if err != nil { return nil, err } @@ -35,39 +36,39 @@ func (r *Registry) getCapabilityOfType(id fmt.Stringer, ct CapabilityType) (Capa return c, nil } -func (r *Registry) GetAction(id fmt.Stringer) (SynchronousCapability, error) { - c, err := r.getCapabilityOfType(id, CapabilityTypeAction) +func (r *Registry) GetAction(ctx context.Context, id string) (SynchronousCapability, error) { + c, err := r.getCapabilityOfType(ctx, id, CapabilityTypeAction) if err != nil { return nil, err } return c.(SynchronousCapability), err } -func (r *Registry) GetTarget(id fmt.Stringer) (SynchronousCapability, error) { - c, err := r.getCapabilityOfType(id, CapabilityTypeTarget) +func (r *Registry) GetTarget(ctx context.Context, id string) (SynchronousCapability, error) { + c, err := r.getCapabilityOfType(ctx, id, CapabilityTypeTarget) if err != nil { return nil, err } return c.(SynchronousCapability), err } -func (r *Registry) GetTrigger(id fmt.Stringer) (AsynchronousCapability, error) { - c, err := r.getCapabilityOfType(id, CapabilityTypeTrigger) +func (r *Registry) GetTrigger(ctx context.Context, id string) (AsynchronousCapability, error) { + c, err := r.getCapabilityOfType(ctx, id, CapabilityTypeTrigger) if err != nil { return nil, err } return c.(AsynchronousCapability), err } -func (r *Registry) GetReport(id fmt.Stringer) (AsynchronousCapability, error) { - c, err := r.getCapabilityOfType(id, CapabilityTypeReport) +func (r *Registry) GetReport(ctx context.Context, id string) (AsynchronousCapability, error) { + c, err := r.getCapabilityOfType(ctx, id, CapabilityTypeReport) if err != nil { return nil, err } return c.(AsynchronousCapability), err } -func (r *Registry) List() []Capability { +func (r *Registry) List(_ context.Context) []Capability { r.mu.RLock() defer r.mu.RUnlock() cl := []Capability{} @@ -78,12 +79,12 @@ func (r *Registry) List() []Capability { return cl } -func (r *Registry) Add(c Capability) error { +func (r *Registry) Add(_ context.Context, c Capability) error { r.mu.Lock() defer r.mu.Unlock() info := c.Info() - id := info.Id.String() + id := info.Id _, ok := r.m[id] if ok { return fmt.Errorf("capability with id: %s already exists", id) diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index 16a50907663..bc8a5be1de9 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -46,9 +46,11 @@ func (m *cmockCapability) Execute(ctx context.Context, callback chan values.Map, } func TestRegistry(t *testing.T) { + ctx := context.Background() + r := NewRegistry() - id := Stringer("capability-1") + id := "capability-1" ci, err := NewCapabilityInfo( id, CapabilityTypeAction, @@ -58,23 +60,24 @@ func TestRegistry(t *testing.T) { require.NoError(t, err) c := &smockCapability{CapabilityInfo: ci} - err = r.Add(c) + err = r.Add(ctx, c) require.NoError(t, err) - gc, err := r.Get(id) + gc, err := r.Get(ctx, id) require.NoError(t, err) assert.Equal(t, c, gc) - cs := r.List() + cs := r.List(ctx) assert.Len(t, cs, 1) assert.Equal(t, c, cs[0]) } func TestRegistry_NoDuplicateIDs(t *testing.T) { + ctx := context.Background() r := NewRegistry() - id := Stringer("capability-1") + id := "capability-1" ci, err := NewCapabilityInfo( id, CapabilityTypeAction, @@ -84,7 +87,7 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { require.NoError(t, err) c := &smockCapability{CapabilityInfo: ci} - err = r.Add(c) + err = r.Add(ctx, c) require.NoError(t, err) ci, err = NewCapabilityInfo( @@ -96,7 +99,7 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { require.NoError(t, err) c2 := &smockCapability{CapabilityInfo: ci} - err = r.Add(c2) + err = r.Add(ctx, c2) assert.ErrorContains(t, err, "capability with id: capability-1 already exists") } @@ -109,7 +112,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { { name: "trigger, sync", newCapability: func() Capability { - id := uuid.New() + id := uuid.New().String() ci, err := NewCapabilityInfo( id, CapabilityTypeTrigger, @@ -125,7 +128,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { { name: "reports, sync", newCapability: func() Capability { - id := uuid.New() + id := uuid.New().String() ci, err := NewCapabilityInfo( id, CapabilityTypeReport, @@ -141,7 +144,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { { name: "action, sync", newCapability: func() Capability { - id := uuid.New() + id := uuid.New().String() ci, err := NewCapabilityInfo( id, CapabilityTypeAction, @@ -156,7 +159,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { { name: "target, sync", newCapability: func() Capability { - id := uuid.New() + id := uuid.New().String() ci, err := NewCapabilityInfo( id, CapabilityTypeTarget, @@ -171,7 +174,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { { name: "trigger, async", newCapability: func() Capability { - id := uuid.New() + id := uuid.New().String() ci, err := NewCapabilityInfo( id, CapabilityTypeTrigger, @@ -186,7 +189,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { { name: "reports, async", newCapability: func() Capability { - id := uuid.New() + id := uuid.New().String() ci, err := NewCapabilityInfo( id, CapabilityTypeReport, @@ -201,7 +204,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { { name: "action, async", newCapability: func() Capability { - id := uuid.New() + id := uuid.New().String() ci, err := NewCapabilityInfo( id, CapabilityTypeAction, @@ -217,7 +220,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { { name: "target, async", newCapability: func() Capability { - id := uuid.New() + id := uuid.New().String() ci, err := NewCapabilityInfo( id, CapabilityTypeTarget, @@ -232,10 +235,11 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { }, } + ctx := context.Background() reg := NewRegistry() for _, tc := range tcs { c := tc.newCapability() - err := reg.Add(c) + err := reg.Add(ctx, c) if tc.errContains == "" { require.NoError(t, err) @@ -243,16 +247,16 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { id := info.Id switch info.CapabilityType { case CapabilityTypeAction: - _, err := reg.GetAction(id) + _, err := reg.GetAction(ctx, id) require.NoError(t, err) case CapabilityTypeTarget: - _, err := reg.GetTarget(id) + _, err := reg.GetTarget(ctx, id) require.NoError(t, err) case CapabilityTypeTrigger: - _, err := reg.GetTrigger(id) + _, err := reg.GetTrigger(ctx, id) require.NoError(t, err) case CapabilityTypeReport: - _, err := reg.GetReport(id) + _, err := reg.GetReport(ctx, id) require.NoError(t, err) } } else { From 8ab79455cfd8bfe354cfdafb0a30c3abda3a341f Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Tue, 23 Jan 2024 13:02:31 +0200 Subject: [PATCH 05/32] Remove SynchronousCapability interface in favour of using async --- core/capabilities/capability.go | 17 +---- core/capabilities/registry.go | 60 +-------------- core/capabilities/registry_test.go | 114 ++--------------------------- 3 files changed, 9 insertions(+), 182 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 4ef26cc7c7b..1132c5bb917 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -40,26 +40,13 @@ type Validatable interface { type Capability interface { Validatable Info() CapabilityInfo -} - -type SynchronousCapability interface { - Capability - - Start(ctx context.Context, config values.Map) (values.Value, error) - Execute(ctx context.Context, inputs values.Map) (values.Value, error) - Stop(ctx context.Context) error -} - -type AsynchronousCapability interface { - Capability - Start(ctx context.Context, config values.Map) (values.Value, error) Execute(ctx context.Context, callback chan values.Map, inputs values.Map) (values.Value, error) Stop(ctx context.Context) error } type CapabilityInfo struct { - Id string + ID string CapabilityType CapabilityType Description string Version string @@ -90,7 +77,7 @@ func NewCapabilityInfo( } return CapabilityInfo{ - Id: id, + ID: id, CapabilityType: capabilityType, Description: description, Version: version, diff --git a/core/capabilities/registry.go b/core/capabilities/registry.go index aeed2c0348b..76d4f47ce1b 100644 --- a/core/capabilities/registry.go +++ b/core/capabilities/registry.go @@ -23,51 +23,6 @@ func (r *Registry) Get(_ context.Context, id string) (Capability, error) { return c, nil } -func (r *Registry) getCapabilityOfType(ctx context.Context, id string, ct CapabilityType) (Capability, error) { - c, err := r.Get(ctx, id) - if err != nil { - return nil, err - } - - if c.Info().CapabilityType != ct { - return nil, fmt.Errorf("capability with id %s is not of type %s", id, ct) - } - - return c, nil -} - -func (r *Registry) GetAction(ctx context.Context, id string) (SynchronousCapability, error) { - c, err := r.getCapabilityOfType(ctx, id, CapabilityTypeAction) - if err != nil { - return nil, err - } - return c.(SynchronousCapability), err -} - -func (r *Registry) GetTarget(ctx context.Context, id string) (SynchronousCapability, error) { - c, err := r.getCapabilityOfType(ctx, id, CapabilityTypeTarget) - if err != nil { - return nil, err - } - return c.(SynchronousCapability), err -} - -func (r *Registry) GetTrigger(ctx context.Context, id string) (AsynchronousCapability, error) { - c, err := r.getCapabilityOfType(ctx, id, CapabilityTypeTrigger) - if err != nil { - return nil, err - } - return c.(AsynchronousCapability), err -} - -func (r *Registry) GetReport(ctx context.Context, id string) (AsynchronousCapability, error) { - c, err := r.getCapabilityOfType(ctx, id, CapabilityTypeReport) - if err != nil { - return nil, err - } - return c.(AsynchronousCapability), err -} - func (r *Registry) List(_ context.Context) []Capability { r.mu.RLock() defer r.mu.RUnlock() @@ -84,25 +39,12 @@ func (r *Registry) Add(_ context.Context, c Capability) error { defer r.mu.Unlock() info := c.Info() - id := info.Id + id := info.ID _, ok := r.m[id] if ok { return fmt.Errorf("capability with id: %s already exists", id) } - switch info.CapabilityType { - case CapabilityTypeAction, CapabilityTypeTarget: - _, ok := c.(SynchronousCapability) - if !ok { - return fmt.Errorf("capability with id %s, type %s does not satisfy SynchronousCapability interface", id, info.CapabilityType) - } - case CapabilityTypeReport, CapabilityTypeTrigger: - _, ok := c.(AsynchronousCapability) - if !ok { - return fmt.Errorf("capability with id %s, type %s does not satisfy AsynchronousCapability interface", id, info.CapabilityType) - } - } - r.m[id] = c return nil diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index bc8a5be1de9..8ef7f00df09 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -11,23 +11,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/values" ) -type smockCapability struct { - Validatable - CapabilityInfo -} - -func (m *smockCapability) Start(ctx context.Context, config values.Map) (values.Value, error) { - return nil, nil -} - -func (m *smockCapability) Stop(ctx context.Context) error { - return nil -} - -func (m *smockCapability) Execute(ctx context.Context, inputs values.Map) (values.Value, error) { - return nil, nil -} - type cmockCapability struct { Validatable CapabilityInfo @@ -59,7 +42,7 @@ func TestRegistry(t *testing.T) { ) require.NoError(t, err) - c := &smockCapability{CapabilityInfo: ci} + c := &cmockCapability{CapabilityInfo: ci} err = r.Add(ctx, c) require.NoError(t, err) @@ -86,7 +69,7 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { ) require.NoError(t, err) - c := &smockCapability{CapabilityInfo: ci} + c := &cmockCapability{CapabilityInfo: ci} err = r.Add(ctx, c) require.NoError(t, err) @@ -97,7 +80,7 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { "v1.0.0", ) require.NoError(t, err) - c2 := &smockCapability{CapabilityInfo: ci} + c2 := &cmockCapability{CapabilityInfo: ci} err = r.Add(ctx, c2) assert.ErrorContains(t, err, "capability with id: capability-1 already exists") @@ -109,38 +92,6 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { newCapability func() Capability errContains string }{ - { - name: "trigger, sync", - newCapability: func() Capability { - id := uuid.New().String() - ci, err := NewCapabilityInfo( - id, - CapabilityTypeTrigger, - "capability-1-description", - "v1.0.0", - ) - require.NoError(t, err) - - return &smockCapability{CapabilityInfo: ci} - }, - errContains: "does not satisfy AsynchronousCapability interface", - }, - { - name: "reports, sync", - newCapability: func() Capability { - id := uuid.New().String() - ci, err := NewCapabilityInfo( - id, - CapabilityTypeReport, - "capability-1-description", - "v1.0.0", - ) - require.NoError(t, err) - - return &smockCapability{CapabilityInfo: ci} - }, - errContains: "does not satisfy AsynchronousCapability interface", - }, { name: "action, sync", newCapability: func() Capability { @@ -153,7 +104,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { ) require.NoError(t, err) - return &smockCapability{CapabilityInfo: ci} + return &cmockCapability{CapabilityInfo: ci} }, }, { @@ -168,7 +119,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { ) require.NoError(t, err) - return &smockCapability{CapabilityInfo: ci} + return &cmockCapability{CapabilityInfo: ci} }, }, { @@ -201,38 +152,6 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { return &cmockCapability{CapabilityInfo: ci} }, }, - { - name: "action, async", - newCapability: func() Capability { - id := uuid.New().String() - ci, err := NewCapabilityInfo( - id, - CapabilityTypeAction, - "capability-1-description", - "v1.0.0", - ) - require.NoError(t, err) - - return &cmockCapability{CapabilityInfo: ci} - }, - errContains: "does not satisfy SynchronousCapability interface", - }, - { - name: "target, async", - newCapability: func() Capability { - id := uuid.New().String() - ci, err := NewCapabilityInfo( - id, - CapabilityTypeTarget, - "capability-1-description", - "v1.0.0", - ) - require.NoError(t, err) - - return &cmockCapability{CapabilityInfo: ci} - }, - errContains: "does not satisfy SynchronousCapability interface", - }, } ctx := context.Background() @@ -240,27 +159,6 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { for _, tc := range tcs { c := tc.newCapability() err := reg.Add(ctx, c) - if tc.errContains == "" { - require.NoError(t, err) - - info := c.Info() - id := info.Id - switch info.CapabilityType { - case CapabilityTypeAction: - _, err := reg.GetAction(ctx, id) - require.NoError(t, err) - case CapabilityTypeTarget: - _, err := reg.GetTarget(ctx, id) - require.NoError(t, err) - case CapabilityTypeTrigger: - _, err := reg.GetTrigger(ctx, id) - require.NoError(t, err) - case CapabilityTypeReport: - _, err := reg.GetReport(ctx, id) - require.NoError(t, err) - } - } else { - assert.ErrorContains(t, err, tc.errContains) - } + require.NoError(t, err) } } From 332d1806e8afc379ee75587538f058bd75f4fdda Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Tue, 23 Jan 2024 13:17:41 +0200 Subject: [PATCH 06/32] Add comments to public functions and variables --- core/capabilities/capability.go | 19 ++++++++++++++++--- core/capabilities/registry.go | 6 ++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 1132c5bb917..dbfc973155b 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -10,8 +10,10 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/values" ) +// CapabilityType is an enum for the type of capability. type CapabilityType string +// CapabilityType enum values. const ( CapabilityTypeTrigger CapabilityType = "trigger" CapabilityTypeAction CapabilityType = "action" @@ -19,6 +21,7 @@ const ( CapabilityTypeTarget CapabilityType = "target" ) +// IsValid checks if the capability type is valid. func (c CapabilityType) IsValid() error { switch c { case CapabilityTypeTrigger, @@ -31,20 +34,28 @@ func (c CapabilityType) IsValid() error { return fmt.Errorf("invalid capability type: %s", c) } +// Validatable is an interface for validating the config and inputs of a capability. type Validatable interface { ValidateConfig(config values.Map) error ExampleOutput() values.Value ValidateInput(inputs values.Map) error } -type Capability interface { - Validatable - Info() CapabilityInfo +// Executable is an interface for executing a capability. +type Executable interface { Start(ctx context.Context, config values.Map) (values.Value, error) Execute(ctx context.Context, callback chan values.Map, inputs values.Map) (values.Value, error) Stop(ctx context.Context) error } +// Capability is an interface for a capability. +type Capability interface { + Executable + Validatable + Info() CapabilityInfo +} + +// CapabilityInfo is a struct for the info of a capability. type CapabilityInfo struct { ID string CapabilityType CapabilityType @@ -52,12 +63,14 @@ type CapabilityInfo struct { Version string } +// Info returns the info of the capability. func (c CapabilityInfo) Info() CapabilityInfo { return c } var idRegex = regexp.MustCompile("[a-z0-9_\\-:]") +// NewCapabilityInfo returns a new CapabilityInfo. func NewCapabilityInfo( id string, capabilityType CapabilityType, diff --git a/core/capabilities/registry.go b/core/capabilities/registry.go index 76d4f47ce1b..dcd31df881c 100644 --- a/core/capabilities/registry.go +++ b/core/capabilities/registry.go @@ -6,11 +6,14 @@ import ( "sync" ) +// Registry is a struct for the registry of capabilities. +// Registry is safe for concurrent use. type Registry struct { m map[string]Capability mu sync.RWMutex } +// Get gets a capability from the registry. func (r *Registry) Get(_ context.Context, id string) (Capability, error) { r.mu.RLock() defer r.mu.RUnlock() @@ -23,6 +26,7 @@ func (r *Registry) Get(_ context.Context, id string) (Capability, error) { return c, nil } +// List lists all the capabilities in the registry. func (r *Registry) List(_ context.Context) []Capability { r.mu.RLock() defer r.mu.RUnlock() @@ -34,6 +38,7 @@ func (r *Registry) List(_ context.Context) []Capability { return cl } +// Add adds a capability to the registry. func (r *Registry) Add(_ context.Context, c Capability) error { r.mu.Lock() defer r.mu.Unlock() @@ -50,6 +55,7 @@ func (r *Registry) Add(_ context.Context, c Capability) error { } +// NewRegistry returns a new Registry. func NewRegistry() *Registry { return &Registry{ m: map[string]Capability{}, From 8cca9cef907cec077ab48e3d22b41fdc82724337 Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Tue, 23 Jan 2024 13:40:35 +0200 Subject: [PATCH 07/32] Add ExecuteSync method. Provide more info on the Executable interface. --- core/capabilities/capability.go | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index dbfc973155b..e0e953d8cc4 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -43,8 +43,16 @@ type Validatable interface { // Executable is an interface for executing a capability. type Executable interface { + // Start will be called when the capability is loaded by the application. + // Start will be called before the capability is added to the registry. Start(ctx context.Context, config values.Map) (values.Value, error) - Execute(ctx context.Context, callback chan values.Map, inputs values.Map) (values.Value, error) + // Capability must respect context.Done and cleanup any request specific resources + // when the context is cancelled. When a request has been completed the capability + // is also expected to close the callback channel. + // Request specific configuration is passed in via the inputs parameter. + Execute(ctx context.Context, callback chan values.Value, inputs values.Map) error + // Stop will be called before the application exits. + // Stop will be called after the capability is removed from the registry. Stop(ctx context.Context) error } @@ -96,3 +104,25 @@ func NewCapabilityInfo( Version: version, }, nil } + +// ExecuteSync executes a capability synchronously. +func ExecuteSync(ctx context.Context, c Capability, inputs values.Map) (values.Value, error) { + callback := make(chan values.Value) + vs := make([]values.Value, 0) + defer close(callback) + + err := c.Execute(ctx, callback, inputs) + if err != nil { + return nil, err + } + + for value := range callback { + vs = append(vs, value) + } + + if len(vs) == 0 { + return vs[0], nil + } + + return &values.List{Underlying: vs}, nil +} From 43053c6d86b4a96104fc010f382dcb0a31ef78ea Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Tue, 23 Jan 2024 15:34:47 +0200 Subject: [PATCH 08/32] Add a Test_ExecuteSyncReturnSingleValue --- core/capabilities/capability.go | 29 ++++++++++++++++++++++------ core/capabilities/capability_test.go | 27 ++++++++++++++++++++++++++ core/capabilities/registry_test.go | 4 ++-- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index e0e953d8cc4..51d9628bb19 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -106,21 +106,38 @@ func NewCapabilityInfo( } // ExecuteSync executes a capability synchronously. +// We are not handling a case where a capability panics and crashes. func ExecuteSync(ctx context.Context, c Capability, inputs values.Map) (values.Value, error) { callback := make(chan values.Value) vs := make([]values.Value, 0) - defer close(callback) - err := c.Execute(ctx, callback, inputs) - if err != nil { - return nil, err - } + var executionErr error + go func(innerCtx context.Context, innerC Capability, innerInputs values.Map) { + executionErr = innerC.Execute(ctx, callback, inputs) + }(ctx, c, inputs) for value := range callback { + // TODO: Handle the case where a capability returns an error as part of the callback. + // if valErr, ok := value.(values.Error); ok { + // return nil, valError.Underlying + // } vs = append(vs, value) } - if len(vs) == 0 { + // Something went wrong when executing a capability. If this happens at any point, + // we want to stop the capability and return the error. We are discarding all values + // returned up to the error. + if executionErr != nil { + return nil, executionErr + } + + // TODO: This is a special case for when a capability returns no values. + // It can happen when the channel is closed before a value is returned. + // if len(vs) == 0 { + // return nil, nil + // } + + if len(vs) == 1 { return vs[0], nil } diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index 086e0c2bd57..ba2e62c9a82 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -1,8 +1,10 @@ package capabilities import ( + "context" "testing" + "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -44,3 +46,28 @@ func Test_CapabilityInfo_Invalid(t *testing.T) { ) assert.ErrorContains(t, err, "invalid version") } + +type mockCapabilityWithExecute struct { + Executable + Validatable + CapabilityInfo +} + +var mcwe = &mockCapabilityWithExecute{} + +func (m *mockCapabilityWithExecute) Execute(ctx context.Context, callback chan values.Value, inputs values.Map) error { + val, _ := values.NewString("hello") + callback <- val + + close(callback) + + return nil +} + +func Test_ExecuteSyncReturnSingleValue(t *testing.T) { + config, _ := values.NewMap(map[string]interface{}{}) + val, err := ExecuteSync(nil, mcwe, *config) + + assert.NoError(t, err) + assert.Equal(t, "hello", val.(*values.String).Underlying) +} diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index 8ef7f00df09..60d1018c453 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -24,8 +24,8 @@ func (m *cmockCapability) Stop(ctx context.Context) error { return nil } -func (m *cmockCapability) Execute(ctx context.Context, callback chan values.Map, inputs values.Map) (values.Value, error) { - return nil, nil +func (m *cmockCapability) Execute(ctx context.Context, callback chan values.Value, inputs values.Map) error { + return nil } func TestRegistry(t *testing.T) { From f906ba3f6e01ae018c0f511a7967e43d44af7017 Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Thu, 25 Jan 2024 15:43:56 +0000 Subject: [PATCH 09/32] Review comments --- core/capabilities/capability.go | 91 ++++++++++++++++++++-------- core/capabilities/capability_test.go | 50 +++++++++++---- core/capabilities/registry_test.go | 4 +- go.sum | 7 +++ 4 files changed, 114 insertions(+), 38 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 51d9628bb19..b48d5195b86 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -2,8 +2,10 @@ package capabilities import ( "context" + "errors" "fmt" "regexp" + "time" "golang.org/x/mod/semver" @@ -11,22 +13,39 @@ import ( ) // CapabilityType is an enum for the type of capability. -type CapabilityType string +type CapabilityType int // CapabilityType enum values. const ( - CapabilityTypeTrigger CapabilityType = "trigger" - CapabilityTypeAction CapabilityType = "action" - CapabilityTypeReport CapabilityType = "report" - CapabilityTypeTarget CapabilityType = "target" + CapabilityTypeTrigger CapabilityType = iota + CapabilityTypeAction + CapabilityTypeConsensus + CapabilityTypeTarget ) +// String returns a string representation of CapabilityType +func (c CapabilityType) String() string { + switch c { + case CapabilityTypeTrigger: + return "trigger" + case CapabilityTypeAction: + return "action" + case CapabilityTypeConsensus: + return "report" + case CapabilityTypeTarget: + return "target" + } + + // Panic as this should be unreachable. + panic("unknown capability type") +} + // IsValid checks if the capability type is valid. func (c CapabilityType) IsValid() error { switch c { case CapabilityTypeTrigger, CapabilityTypeAction, - CapabilityTypeReport, + CapabilityTypeConsensus, CapabilityTypeTarget: return nil } @@ -36,8 +55,7 @@ func (c CapabilityType) IsValid() error { // Validatable is an interface for validating the config and inputs of a capability. type Validatable interface { - ValidateConfig(config values.Map) error - ExampleOutput() values.Value + ExampleOutput(inputs values.Map) values.Value ValidateInput(inputs values.Map) error } @@ -76,7 +94,11 @@ func (c CapabilityInfo) Info() CapabilityInfo { return c } -var idRegex = regexp.MustCompile("[a-z0-9_\\-:]") +var idRegex = regexp.MustCompile(`[a-z0-9_\-:]`) + +const ( + idMaxLength = 128 +) // NewCapabilityInfo returns a new CapabilityInfo. func NewCapabilityInfo( @@ -85,6 +107,9 @@ func NewCapabilityInfo( description string, version string, ) (CapabilityInfo, error) { + if len(id) > idMaxLength { + return CapabilityInfo{}, fmt.Errorf("invalid id: %s exceeds max length %d", id, idMaxLength) + } if !idRegex.MatchString(id) { return CapabilityInfo{}, fmt.Errorf("invalid id: %s. Allowed: %s", id, idRegex) } @@ -105,23 +130,41 @@ func NewCapabilityInfo( }, nil } +var defaultExecuteTimeout = 10 * time.Second + // ExecuteSync executes a capability synchronously. // We are not handling a case where a capability panics and crashes. func ExecuteSync(ctx context.Context, c Capability, inputs values.Map) (values.Value, error) { + ctxWithT, cancel := context.WithTimeout(ctx, defaultExecuteTimeout) + defer cancel() + callback := make(chan values.Value) vs := make([]values.Value, 0) var executionErr error - go func(innerCtx context.Context, innerC Capability, innerInputs values.Map) { - executionErr = innerC.Execute(ctx, callback, inputs) - }(ctx, c, inputs) - - for value := range callback { - // TODO: Handle the case where a capability returns an error as part of the callback. - // if valErr, ok := value.(values.Error); ok { - // return nil, valError.Underlying - // } - vs = append(vs, value) + go func(innerCtx context.Context, innerC Capability, innerInputs values.Map, innerCallback chan values.Value) { + executionErr = innerC.Execute(innerCtx, innerCallback, innerInputs) + }(ctxWithT, c, inputs, callback) + +outerLoop: + for { + select { + case value, isOpen := <-callback: + if !isOpen { + break outerLoop + } + // An error means execution has been interrupted. + // We'll return the value discarding values received + // until now. + if valErr, ok := value.(*values.Error); ok { + return nil, valErr.Underlying + } + + vs = append(vs, value) + case <-ctx.Done(): + return nil, errors.New("context timed out") + } + } // Something went wrong when executing a capability. If this happens at any point, @@ -131,12 +174,12 @@ func ExecuteSync(ctx context.Context, c Capability, inputs values.Map) (values.V return nil, executionErr } - // TODO: This is a special case for when a capability returns no values. - // It can happen when the channel is closed before a value is returned. - // if len(vs) == 0 { - // return nil, nil - // } + if len(vs) == 0 { + return nil, errors.New("capability did not return any values") + } + // If the capability returned only one value, + // let's unwrap it to improve usability. if len(vs) == 1 { return vs[0], nil } diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index ba2e62c9a82..932ad6da6b2 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -4,9 +4,10 @@ import ( "context" "testing" - "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/values" ) func Test_CapabilityInfo(t *testing.T) { @@ -24,7 +25,7 @@ func Test_CapabilityInfo(t *testing.T) { func Test_CapabilityInfo_Invalid(t *testing.T) { _, err := NewCapabilityInfo( "capability-id", - "test", + CapabilityType(5), "This is a mock capability that doesn't do anything.", "v1.0.0", ) @@ -51,23 +52,48 @@ type mockCapabilityWithExecute struct { Executable Validatable CapabilityInfo + ExecuteFn func(ctx context.Context, callback chan values.Value, inputs values.Map) error } -var mcwe = &mockCapabilityWithExecute{} - func (m *mockCapabilityWithExecute) Execute(ctx context.Context, callback chan values.Value, inputs values.Map) error { - val, _ := values.NewString("hello") - callback <- val - - close(callback) - - return nil + return m.ExecuteFn(ctx, callback, inputs) } func Test_ExecuteSyncReturnSingleValue(t *testing.T) { + mcwe := &mockCapabilityWithExecute{ + ExecuteFn: func(ctx context.Context, callback chan values.Value, inputs values.Map) error { + val, _ := values.NewString("hello") + callback <- val + + close(callback) + + return nil + }, + } config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(nil, mcwe, *config) + val, err := ExecuteSync(context.Background(), mcwe, *config) - assert.NoError(t, err) + assert.NoError(t, err, val) assert.Equal(t, "hello", val.(*values.String).Underlying) } + +func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { + es, _ := values.NewString("hello") + expectedList := []values.Value{es, es, es} + mcwe := &mockCapabilityWithExecute{ + ExecuteFn: func(ctx context.Context, callback chan values.Value, inputs values.Map) error { + callback <- es + callback <- es + callback <- es + + close(callback) + + return nil + }, + } + config, _ := values.NewMap(map[string]interface{}{}) + val, err := ExecuteSync(context.Background(), mcwe, *config) + + assert.NoError(t, err, val) + assert.ElementsMatch(t, expectedList, val.(*values.List).Underlying) +} diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index 60d1018c453..0264b668b6d 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -75,7 +75,7 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { ci, err = NewCapabilityInfo( id, - CapabilityTypeReport, + CapabilityTypeConsensus, "capability-2-description", "v1.0.0", ) @@ -143,7 +143,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { id := uuid.New().String() ci, err := NewCapabilityInfo( id, - CapabilityTypeReport, + CapabilityTypeConsensus, "capability-1-description", "v1.0.0", ) diff --git a/go.sum b/go.sum index c8f6620af85..33fcee912cc 100644 --- a/go.sum +++ b/go.sum @@ -1164,8 +1164,15 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429 h1:xkejUBZhcBpBrTSfxc91Iwzadrb6SXw8ks69bHIQ9Ww= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429/go.mod h1:wJmVvDf4XSjsahWtfUq3wvIAYEAuhr7oxmxYnEL/LGQ= +<<<<<<< HEAD github.com/smartcontractkit/chainlink-common v0.1.7-0.20240124161023-948579cbaffa h1:9g7e1C3295ALDK8Gs42fIKSSJfI+H1RoBmivGWTvIZo= github.com/smartcontractkit/chainlink-common v0.1.7-0.20240124161023-948579cbaffa/go.mod h1:05rRF84QKlIOF5LfTBPkHdw4UpBI2G3zxRcuZ65bPjk= +======= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240119143538-04c7f63ad53a h1:lgM0yPo0KqSntLY4Y42RAH3avdv+Kyne8n+VM7cwlxo= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240119143538-04c7f63ad53a/go.mod h1:05rRF84QKlIOF5LfTBPkHdw4UpBI2G3zxRcuZ65bPjk= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240123140427-baf1323e7639 h1:HUsMpiOfMLyz6TC2DWoJEDk+iL3wEuN2frE9y9L4LI8= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240123140427-baf1323e7639/go.mod h1:05rRF84QKlIOF5LfTBPkHdw4UpBI2G3zxRcuZ65bPjk= +>>>>>>> f1616f573a (Review comments) github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240120192246-4bb04c997ca0 h1:NALwENz6vQ972DuD9AZjqRjyNSxH9ptNapizQGLI+2s= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240120192246-4bb04c997ca0/go.mod h1:NcVAT/GETDBvIoAej5K6OYqAtDOkF6vO5pYw/hLuYVU= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20231204152908-a6e3fe8ff2a1 h1:xYqRgZO0nMSO8CBCMR0r3WA+LZ4kNL8a6bnbyk/oBtQ= From 3aaa43f86f37002559934183b82cb800a55f268d Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Fri, 26 Jan 2024 12:43:50 +0200 Subject: [PATCH 10/32] Add CapabilityResponse struct for Execute channel --- core/capabilities/capability.go | 20 +++++++++++++------- core/capabilities/capability_test.go | 16 ++++++++-------- core/capabilities/registry_test.go | 2 +- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index b48d5195b86..ab87ffa29e2 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -59,6 +59,12 @@ type Validatable interface { ValidateInput(inputs values.Map) error } +// CapabilityResponse is a struct for the Execute response of a capability. +type CapabilityResponse struct { + value values.Value + err error +} + // Executable is an interface for executing a capability. type Executable interface { // Start will be called when the capability is loaded by the application. @@ -68,7 +74,7 @@ type Executable interface { // when the context is cancelled. When a request has been completed the capability // is also expected to close the callback channel. // Request specific configuration is passed in via the inputs parameter. - Execute(ctx context.Context, callback chan values.Value, inputs values.Map) error + Execute(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error // Stop will be called before the application exits. // Stop will be called after the capability is removed from the registry. Stop(ctx context.Context) error @@ -138,29 +144,29 @@ func ExecuteSync(ctx context.Context, c Capability, inputs values.Map) (values.V ctxWithT, cancel := context.WithTimeout(ctx, defaultExecuteTimeout) defer cancel() - callback := make(chan values.Value) + callback := make(chan CapabilityResponse) vs := make([]values.Value, 0) var executionErr error - go func(innerCtx context.Context, innerC Capability, innerInputs values.Map, innerCallback chan values.Value) { + go func(innerCtx context.Context, innerC Capability, innerInputs values.Map, innerCallback chan CapabilityResponse) { executionErr = innerC.Execute(innerCtx, innerCallback, innerInputs) }(ctxWithT, c, inputs, callback) outerLoop: for { select { - case value, isOpen := <-callback: + case response, isOpen := <-callback: if !isOpen { break outerLoop } // An error means execution has been interrupted. // We'll return the value discarding values received // until now. - if valErr, ok := value.(*values.Error); ok { - return nil, valErr.Underlying + if response.err != nil { + return nil, response.err } - vs = append(vs, value) + vs = append(vs, response.value) case <-ctx.Done(): return nil, errors.New("context timed out") } diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index 932ad6da6b2..78a5eaa5149 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -52,18 +52,18 @@ type mockCapabilityWithExecute struct { Executable Validatable CapabilityInfo - ExecuteFn func(ctx context.Context, callback chan values.Value, inputs values.Map) error + ExecuteFn func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error } -func (m *mockCapabilityWithExecute) Execute(ctx context.Context, callback chan values.Value, inputs values.Map) error { +func (m *mockCapabilityWithExecute) Execute(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { return m.ExecuteFn(ctx, callback, inputs) } func Test_ExecuteSyncReturnSingleValue(t *testing.T) { mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan values.Value, inputs values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { val, _ := values.NewString("hello") - callback <- val + callback <- CapabilityResponse{val, nil} close(callback) @@ -81,10 +81,10 @@ func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { es, _ := values.NewString("hello") expectedList := []values.Value{es, es, es} mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan values.Value, inputs values.Map) error { - callback <- es - callback <- es - callback <- es + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + callback <- CapabilityResponse{es, nil} + callback <- CapabilityResponse{es, nil} + callback <- CapabilityResponse{es, nil} close(callback) diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index 0264b668b6d..b8b71ae5840 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -24,7 +24,7 @@ func (m *cmockCapability) Stop(ctx context.Context) error { return nil } -func (m *cmockCapability) Execute(ctx context.Context, callback chan values.Value, inputs values.Map) error { +func (m *cmockCapability) Execute(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { return nil } From af8eb3f3635bde6b0e80a50f48aac7442a0cb313 Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Fri, 26 Jan 2024 12:49:14 +0200 Subject: [PATCH 11/32] Undo chainlink-common change --- go.sum | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go.sum b/go.sum index 33fcee912cc..497ec227da8 100644 --- a/go.sum +++ b/go.sum @@ -1170,9 +1170,12 @@ github.com/smartcontractkit/chainlink-common v0.1.7-0.20240124161023-948579cbaff ======= github.com/smartcontractkit/chainlink-common v0.1.7-0.20240119143538-04c7f63ad53a h1:lgM0yPo0KqSntLY4Y42RAH3avdv+Kyne8n+VM7cwlxo= github.com/smartcontractkit/chainlink-common v0.1.7-0.20240119143538-04c7f63ad53a/go.mod h1:05rRF84QKlIOF5LfTBPkHdw4UpBI2G3zxRcuZ65bPjk= +<<<<<<< HEAD github.com/smartcontractkit/chainlink-common v0.1.7-0.20240123140427-baf1323e7639 h1:HUsMpiOfMLyz6TC2DWoJEDk+iL3wEuN2frE9y9L4LI8= github.com/smartcontractkit/chainlink-common v0.1.7-0.20240123140427-baf1323e7639/go.mod h1:05rRF84QKlIOF5LfTBPkHdw4UpBI2G3zxRcuZ65bPjk= >>>>>>> f1616f573a (Review comments) +======= +>>>>>>> 3943b41f3a (Undo chainlink-common change) github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240120192246-4bb04c997ca0 h1:NALwENz6vQ972DuD9AZjqRjyNSxH9ptNapizQGLI+2s= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240120192246-4bb04c997ca0/go.mod h1:NcVAT/GETDBvIoAej5K6OYqAtDOkF6vO5pYw/hLuYVU= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20231204152908-a6e3fe8ff2a1 h1:xYqRgZO0nMSO8CBCMR0r3WA+LZ4kNL8a6bnbyk/oBtQ= From 6a9ea800ca0608716c1b1b2c79c3cedb1df6423f Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Fri, 26 Jan 2024 13:03:15 +0200 Subject: [PATCH 12/32] Add reasoning behind code --- core/capabilities/capability.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index ab87ffa29e2..66c04a22851 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -74,6 +74,8 @@ type Executable interface { // when the context is cancelled. When a request has been completed the capability // is also expected to close the callback channel. // Request specific configuration is passed in via the inputs parameter. + // A successful response must always return a value. An error is assumed otherwise. + // The intent is to make the API explicit. Execute(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error // Stop will be called before the application exits. // Stop will be called after the capability is removed from the registry. @@ -140,6 +142,8 @@ var defaultExecuteTimeout = 10 * time.Second // ExecuteSync executes a capability synchronously. // We are not handling a case where a capability panics and crashes. +// There is default timeout of 10 seconds. If a capability takes longer than +// that then it should be executed asynchronously. func ExecuteSync(ctx context.Context, c Capability, inputs values.Map) (values.Value, error) { ctxWithT, cancel := context.WithTimeout(ctx, defaultExecuteTimeout) defer cancel() @@ -167,10 +171,11 @@ outerLoop: } vs = append(vs, response.value) - case <-ctx.Done(): - return nil, errors.New("context timed out") - } + // Timeout when a capability panics, crashes, and does not close the channel. + case <-ctxWithT.Done(): + return nil, fmt.Errorf("context timed out. If you did not set a timeout, be aware that the default ExecuteSync timeout is %f seconds", defaultExecuteTimeout.Seconds()) + } } // Something went wrong when executing a capability. If this happens at any point, @@ -180,6 +185,8 @@ outerLoop: return nil, executionErr } + // If the capability did not return any values, we deem it as an error. + // The intent is for the API to be explicit. if len(vs) == 0 { return nil, errors.New("capability did not return any values") } From efa3b910c426b2db2dc8df001b97ccc1de81a489 Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Fri, 26 Jan 2024 13:17:36 +0200 Subject: [PATCH 13/32] Add missing tests --- core/capabilities/capability.go | 12 +++-- core/capabilities/capability_test.go | 65 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 66c04a22851..70626c7021c 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -151,9 +151,9 @@ func ExecuteSync(ctx context.Context, c Capability, inputs values.Map) (values.V callback := make(chan CapabilityResponse) vs := make([]values.Value, 0) - var executionErr error + var setupErr error go func(innerCtx context.Context, innerC Capability, innerInputs values.Map, innerCallback chan CapabilityResponse) { - executionErr = innerC.Execute(innerCtx, innerCallback, innerInputs) + setupErr = innerC.Execute(innerCtx, innerCallback, innerInputs) }(ctxWithT, c, inputs, callback) outerLoop: @@ -178,11 +178,9 @@ outerLoop: } } - // Something went wrong when executing a capability. If this happens at any point, - // we want to stop the capability and return the error. We are discarding all values - // returned up to the error. - if executionErr != nil { - return nil, executionErr + // Something went wrong when setting up a capability. + if setupErr != nil { + return nil, setupErr } // If the capability did not return any values, we deem it as an error. diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index 78a5eaa5149..0392b02c14e 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -2,6 +2,7 @@ package capabilities import ( "context" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -97,3 +98,67 @@ func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { assert.NoError(t, err, val) assert.ElementsMatch(t, expectedList, val.(*values.List).Underlying) } + +func Test_ExecuteSyncCapabilitySetupErrors(t *testing.T) { + expectedErr := errors.New("something went wrong during setup") + mcwe := &mockCapabilityWithExecute{ + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + close(callback) + return expectedErr + }, + } + config, _ := values.NewMap(map[string]interface{}{}) + val, err := ExecuteSync(context.Background(), mcwe, *config) + + assert.ErrorContains(t, err, expectedErr.Error()) + assert.Nil(t, val) +} + +func Test_ExecuteSyncTimeout(t *testing.T) { + ctxWithTimeout := context.Background() + ctxWithTimeout, cancel := context.WithCancel(ctxWithTimeout) + cancel() + + mcwe := &mockCapabilityWithExecute{ + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + return nil + }, + } + config, _ := values.NewMap(map[string]interface{}{}) + val, err := ExecuteSync(ctxWithTimeout, mcwe, *config) + + assert.ErrorContains(t, err, "context timed out. If you did not set a timeout, be aware that the default ExecuteSync timeout is") + assert.Nil(t, val) +} + +func Test_ExecuteSyncCapabilityErrors(t *testing.T) { + expectedErr := errors.New("something went wrong during execution") + mcwe := &mockCapabilityWithExecute{ + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + callback <- CapabilityResponse{nil, expectedErr} + + close(callback) + + return nil + }, + } + config, _ := values.NewMap(map[string]interface{}{}) + val, err := ExecuteSync(context.Background(), mcwe, *config) + + assert.ErrorContains(t, err, expectedErr.Error()) + assert.Nil(t, val) +} + +func Test_ExecuteSyncDoesNotReturnValues(t *testing.T) { + mcwe := &mockCapabilityWithExecute{ + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + close(callback) + return nil + }, + } + config, _ := values.NewMap(map[string]interface{}{}) + val, err := ExecuteSync(context.Background(), mcwe, *config) + + assert.ErrorContains(t, err, "capability did not return any values") + assert.Nil(t, val) +} From 6274c1ecf578bdf1e0c97fb9557666eb83bcac2f Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Fri, 26 Jan 2024 13:37:37 +0200 Subject: [PATCH 14/32] Create CallbackExecutable. Trim interfaces. --- core/capabilities/capability.go | 51 +++++++++++++++++----------- core/capabilities/capability_test.go | 3 +- core/capabilities/registry.go | 12 +++---- core/capabilities/registry_test.go | 19 +++-------- 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 70626c7021c..1cb31b1083e 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -53,23 +53,14 @@ func (c CapabilityType) IsValid() error { return fmt.Errorf("invalid capability type: %s", c) } -// Validatable is an interface for validating the config and inputs of a capability. -type Validatable interface { - ExampleOutput(inputs values.Map) values.Value - ValidateInput(inputs values.Map) error -} - // CapabilityResponse is a struct for the Execute response of a capability. type CapabilityResponse struct { value values.Value err error } -// Executable is an interface for executing a capability. -type Executable interface { - // Start will be called when the capability is loaded by the application. - // Start will be called before the capability is added to the registry. - Start(ctx context.Context, config values.Map) (values.Value, error) +// CallbackExecutable is an interface for executing a capability. +type CallbackExecutable interface { // Capability must respect context.Done and cleanup any request specific resources // when the context is cancelled. When a request has been completed the capability // is also expected to close the callback channel. @@ -77,18 +68,38 @@ type Executable interface { // A successful response must always return a value. An error is assumed otherwise. // The intent is to make the API explicit. Execute(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error - // Stop will be called before the application exits. - // Stop will be called after the capability is removed from the registry. - Stop(ctx context.Context) error } -// Capability is an interface for a capability. -type Capability interface { - Executable - Validatable +// BaseCapability interface needs to be implemented by all capability types. +// Capability interfaces are intentionally duplicated to allow for an easy change +// or extension in the future. +type BaseCapability interface { Info() CapabilityInfo } +// TriggerCapability interface needs to be implemented by all trigger capabilities. +type TriggerCapability interface { + BaseCapability +} + +// ActionCapability interface needs to be implemented by all action capabilities. +type ActionCapability interface { + BaseCapability + CallbackExecutable +} + +// ConsensusCapability interface needs to be implemented by all consensus capabilities. +type ConsensusCapability interface { + BaseCapability + CallbackExecutable +} + +// TargetsCapability interface needs to be implemented by all target capabilities. +type TargetsCapability interface { + BaseCapability + CallbackExecutable +} + // CapabilityInfo is a struct for the info of a capability. type CapabilityInfo struct { ID string @@ -144,7 +155,7 @@ var defaultExecuteTimeout = 10 * time.Second // We are not handling a case where a capability panics and crashes. // There is default timeout of 10 seconds. If a capability takes longer than // that then it should be executed asynchronously. -func ExecuteSync(ctx context.Context, c Capability, inputs values.Map) (values.Value, error) { +func ExecuteSync(ctx context.Context, c CallbackExecutable, inputs values.Map) (values.Value, error) { ctxWithT, cancel := context.WithTimeout(ctx, defaultExecuteTimeout) defer cancel() @@ -152,7 +163,7 @@ func ExecuteSync(ctx context.Context, c Capability, inputs values.Map) (values.V vs := make([]values.Value, 0) var setupErr error - go func(innerCtx context.Context, innerC Capability, innerInputs values.Map, innerCallback chan CapabilityResponse) { + go func(innerCtx context.Context, innerC CallbackExecutable, innerInputs values.Map, innerCallback chan CapabilityResponse) { setupErr = innerC.Execute(innerCtx, innerCallback, innerInputs) }(ctxWithT, c, inputs, callback) diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index 0392b02c14e..b817060a755 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -50,8 +50,7 @@ func Test_CapabilityInfo_Invalid(t *testing.T) { } type mockCapabilityWithExecute struct { - Executable - Validatable + CallbackExecutable CapabilityInfo ExecuteFn func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error } diff --git a/core/capabilities/registry.go b/core/capabilities/registry.go index dcd31df881c..9e1b5d234f8 100644 --- a/core/capabilities/registry.go +++ b/core/capabilities/registry.go @@ -9,12 +9,12 @@ import ( // Registry is a struct for the registry of capabilities. // Registry is safe for concurrent use. type Registry struct { - m map[string]Capability + m map[string]BaseCapability mu sync.RWMutex } // Get gets a capability from the registry. -func (r *Registry) Get(_ context.Context, id string) (Capability, error) { +func (r *Registry) Get(_ context.Context, id string) (BaseCapability, error) { r.mu.RLock() defer r.mu.RUnlock() @@ -27,10 +27,10 @@ func (r *Registry) Get(_ context.Context, id string) (Capability, error) { } // List lists all the capabilities in the registry. -func (r *Registry) List(_ context.Context) []Capability { +func (r *Registry) List(_ context.Context) []BaseCapability { r.mu.RLock() defer r.mu.RUnlock() - cl := []Capability{} + cl := []BaseCapability{} for _, v := range r.m { cl = append(cl, v) } @@ -39,7 +39,7 @@ func (r *Registry) List(_ context.Context) []Capability { } // Add adds a capability to the registry. -func (r *Registry) Add(_ context.Context, c Capability) error { +func (r *Registry) Add(_ context.Context, c BaseCapability) error { r.mu.Lock() defer r.mu.Unlock() @@ -58,6 +58,6 @@ func (r *Registry) Add(_ context.Context, c Capability) error { // NewRegistry returns a new Registry. func NewRegistry() *Registry { return &Registry{ - m: map[string]Capability{}, + m: map[string]BaseCapability{}, } } diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index b8b71ae5840..f234c2c7ca9 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -12,18 +12,9 @@ import ( ) type cmockCapability struct { - Validatable CapabilityInfo } -func (m *cmockCapability) Start(ctx context.Context, config values.Map) (values.Value, error) { - return nil, nil -} - -func (m *cmockCapability) Stop(ctx context.Context) error { - return nil -} - func (m *cmockCapability) Execute(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { return nil } @@ -89,12 +80,12 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { tcs := []struct { name string - newCapability func() Capability + newCapability func() BaseCapability errContains string }{ { name: "action, sync", - newCapability: func() Capability { + newCapability: func() BaseCapability { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -109,7 +100,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { }, { name: "target, sync", - newCapability: func() Capability { + newCapability: func() BaseCapability { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -124,7 +115,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { }, { name: "trigger, async", - newCapability: func() Capability { + newCapability: func() BaseCapability { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -139,7 +130,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { }, { name: "reports, async", - newCapability: func() Capability { + newCapability: func() BaseCapability { id := uuid.New().String() ci, err := NewCapabilityInfo( id, From 508dddf022e0059b54aef5e06d97fc8d86cdb0ee Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Fri, 26 Jan 2024 14:04:32 +0200 Subject: [PATCH 15/32] Start on an example on demand capability --- core/capabilities/capability.go | 2 + .../examples/on_demand_trigger.go | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 core/capabilities/examples/on_demand_trigger.go diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 1cb31b1083e..0517d848ab3 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -80,6 +80,8 @@ type BaseCapability interface { // TriggerCapability interface needs to be implemented by all trigger capabilities. type TriggerCapability interface { BaseCapability + RegisterTrigger(ctx context.Context, inputs values.Map, callback chan CapabilityResponse) error + UnregisterTrigger(ctx context.Context, inputs values.Map) error } // ActionCapability interface needs to be implemented by all action capabilities. diff --git a/core/capabilities/examples/on_demand_trigger.go b/core/capabilities/examples/on_demand_trigger.go new file mode 100644 index 00000000000..08bd799faeb --- /dev/null +++ b/core/capabilities/examples/on_demand_trigger.go @@ -0,0 +1,57 @@ +package examples + +import ( + "context" + + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/capabilities" +) + +// OnDemandTrigger is an example on-demand trigger. +type OnDemandTrigger struct { + capabilities.CapabilityInfo + // map between Workflow Execution IDs and callback channels +} + +// NewOnDemandTrigger returns a new OnDemandTrigger. +func NewOnDemandTrigger() (*OnDemandTrigger, error) { + onDemandTriggerInfo, err := capabilities.NewCapabilityInfo( + "on-demand-trigger", + capabilities.CapabilityTypeTrigger, + "An example on-demand trigger.", + "v1.0.0", + ) + + if err != nil { + return nil, err + } + + return &OnDemandTrigger{ + CapabilityInfo: onDemandTriggerInfo, + }, nil +} + +// TODO SendMultipleEvents +// func (o *OnDemandTrigger) FanOutEvent(ctx context.Context, event capabilities.CapabilityResponse) error { +// o.eventsChannel <- event +// return nil +// } + +// Execute executes the on-demand trigger. +func (o *OnDemandTrigger) SendEvent(ctx context.Context, event capabilities.CapabilityResponse) error { + // TODO: Add a "workflow execution id" to be able to differentiate between recipient channels + return nil +} + +func (o *OnDemandTrigger) RegisterTrigger(ctx context.Context, callback chan capabilities.CapabilityResponse, inputs values.Map) error { + // Validate inputs. + // Get 1) a workflow execution id from the input so we can register a trigger + // and 2) + // Send any new events from the eventChannel to the newly register callback channel + + return nil +} + +func (o *OnDemandTrigger) UnregisterTrigger(ctx context.Context, inputs values.Map) error { + return nil +} From 1924ea82c46ef219287bb0c96e5d33f546e9d686 Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Fri, 26 Jan 2024 13:31:41 +0000 Subject: [PATCH 16/32] Add different capability types to registry --- core/capabilities/capability.go | 4 +- core/capabilities/registry.go | 86 ++++++++++++++++++++++++++++++ core/capabilities/registry_test.go | 66 +++++++++++++++++------ 3 files changed, 138 insertions(+), 18 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 0517d848ab3..642491a44d7 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -80,7 +80,7 @@ type BaseCapability interface { // TriggerCapability interface needs to be implemented by all trigger capabilities. type TriggerCapability interface { BaseCapability - RegisterTrigger(ctx context.Context, inputs values.Map, callback chan CapabilityResponse) error + RegisterTrigger(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error UnregisterTrigger(ctx context.Context, inputs values.Map) error } @@ -97,7 +97,7 @@ type ConsensusCapability interface { } // TargetsCapability interface needs to be implemented by all target capabilities. -type TargetsCapability interface { +type TargetCapability interface { BaseCapability CallbackExecutable } diff --git a/core/capabilities/registry.go b/core/capabilities/registry.go index 9e1b5d234f8..1632b17c4b9 100644 --- a/core/capabilities/registry.go +++ b/core/capabilities/registry.go @@ -26,6 +26,66 @@ func (r *Registry) Get(_ context.Context, id string) (BaseCapability, error) { return c, nil } +// GetTrigger gets a capability from the registry and tries to coerce it to the TriggerCapability interface. +func (r *Registry) GetTrigger(ctx context.Context, id string) (TriggerCapability, error) { + c, err := r.Get(ctx, id) + if err != nil { + return nil, err + } + + tc, ok := c.(TriggerCapability) + if !ok { + return nil, fmt.Errorf("capability with id: %s does not satisfy the capability interface", id) + } + + return tc, nil +} + +// GetAction gets a capability from the registry and tries to coerce it to the ActionCapability interface. +func (r *Registry) GetAction(ctx context.Context, id string) (ActionCapability, error) { + c, err := r.Get(ctx, id) + if err != nil { + return nil, err + } + + ac, ok := c.(ActionCapability) + if !ok { + return nil, fmt.Errorf("capability with id: %s does not satisfy the capability interface", id) + } + + return ac, nil +} + +// GetConsensus gets a capability from the registry and tries to coerce it to the ActionCapability interface. +func (r *Registry) GetConsensus(ctx context.Context, id string) (ConsensusCapability, error) { + c, err := r.Get(ctx, id) + if err != nil { + return nil, err + } + + cc, ok := c.(ConsensusCapability) + if !ok { + return nil, fmt.Errorf("capability with id: %s does not satisfy the capability interface", id) + } + + return cc, nil +} + +// GetTarget gets a capability from the registry and tries to coerce it to the ActionCapability interface. +func (r *Registry) GetTarget(ctx context.Context, id string) (TargetCapability, error) { + c, err := r.Get(ctx, id) + if err != nil { + return nil, err + } + + tc, ok := c.(TargetCapability) + if !ok { + return nil, fmt.Errorf("capability with id: %s does not satisfy the capability interface", id) + } + + return tc, nil +} + // List lists all the capabilities in the registry. func (r *Registry) List(_ context.Context) []BaseCapability { r.mu.RLock() @@ -44,6 +104,32 @@ func (r *Registry) Add(_ context.Context, c BaseCapability) error { defer r.mu.Unlock() info := c.Info() + + switch info.CapabilityType { + case CapabilityTypeTrigger: + _, ok := c.(TriggerCapability) + if !ok { + return fmt.Errorf("trigger capability does not satisfy TriggerCapability interface") + } + case CapabilityTypeAction: + _, ok := c.(ActionCapability) + if !ok { + return fmt.Errorf("action does not satisfy ActionCapability interface") + } + case CapabilityTypeConsensus: + _, ok := c.(ConsensusCapability) + if !ok { + return fmt.Errorf("consensus capability does not satisfy ConsensusCapability interface") + } + case CapabilityTypeTarget: + _, ok := c.(TargetCapability) + if !ok { + return fmt.Errorf("target capability does not satisfy TargetCapability interface") + } + default: + return fmt.Errorf("unknown capability type: %s", info.CapabilityType) + } + id := info.ID _, ok := r.m[id] if ok { diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index f234c2c7ca9..996174a77a1 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -19,6 +19,18 @@ func (m *cmockCapability) Execute(ctx context.Context, callback chan CapabilityR return nil } +type tmockCapability struct { + CapabilityInfo +} + +func (m *tmockCapability) RegisterTrigger(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + return nil +} + +func (m *tmockCapability) UnregisterTrigger(ctx context.Context, inputs values.Map) error { + return nil +} + func TestRegistry(t *testing.T) { ctx := context.Background() @@ -80,12 +92,13 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { tcs := []struct { name string - newCapability func() BaseCapability + newCapability func(ctx context.Context, reg *Registry) error + getCapability func(ctx context.Context, reg *Registry, id string) error errContains string }{ { - name: "action, sync", - newCapability: func() BaseCapability { + name: "action", + newCapability: func(ctx context.Context, reg *Registry) error { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -95,12 +108,17 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { ) require.NoError(t, err) - return &cmockCapability{CapabilityInfo: ci} + c := &cmockCapability{CapabilityInfo: ci} + return reg.Add(ctx, c) + }, + getCapability: func(ctx context.Context, reg *Registry, id string) error { + _, err := reg.GetAction(ctx, id) + return err }, }, { - name: "target, sync", - newCapability: func() BaseCapability { + name: "target", + newCapability: func(ctx context.Context, reg *Registry) error { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -110,12 +128,17 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { ) require.NoError(t, err) - return &cmockCapability{CapabilityInfo: ci} + c := &cmockCapability{CapabilityInfo: ci} + return reg.Add(ctx, c) + }, + getCapability: func(ctx context.Context, reg *Registry, id string) error { + _, err := reg.GetTarget(ctx, id) + return err }, }, { - name: "trigger, async", - newCapability: func() BaseCapability { + name: "trigger", + newCapability: func(ctx context.Context, reg *Registry) error { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -125,12 +148,17 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { ) require.NoError(t, err) - return &cmockCapability{CapabilityInfo: ci} + c := &tmockCapability{CapabilityInfo: ci} + return reg.Add(ctx, c) + }, + getCapability: func(ctx context.Context, reg *Registry, id string) error { + _, err := reg.GetTrigger(ctx, id) + return err }, }, { - name: "reports, async", - newCapability: func() BaseCapability { + name: "consensus", + newCapability: func(ctx context.Context, reg *Registry) error { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -140,7 +168,12 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { ) require.NoError(t, err) - return &cmockCapability{CapabilityInfo: ci} + c := &cmockCapability{CapabilityInfo: ci} + return reg.Add(ctx, c) + }, + getCapability: func(ctx context.Context, reg *Registry, id string) error { + _, err := reg.GetConsensus(ctx, id) + return err }, }, } @@ -148,8 +181,9 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { ctx := context.Background() reg := NewRegistry() for _, tc := range tcs { - c := tc.newCapability() - err := reg.Add(ctx, c) - require.NoError(t, err) + t.Run(tc.name, func(t *testing.T) { + err := tc.newCapability(ctx, reg) + require.NoError(t, err) + }) } } From a4b07ec11862007385685f02861e1aef6f78413b Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Fri, 26 Jan 2024 16:36:24 +0000 Subject: [PATCH 17/32] Complete example trigger --- core/capabilities/capability.go | 20 ++-- core/capabilities/capability_test.go | 28 +++--- .../examples/on_demand_trigger.go | 93 +++++++++++++++---- .../examples/on_demand_trigger_test.go | 45 +++++++++ core/capabilities/registry_test.go | 29 +++--- go.sum | 10 -- 6 files changed, 161 insertions(+), 64 deletions(-) create mode 100644 core/capabilities/examples/on_demand_trigger_test.go diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 642491a44d7..350d6ae6800 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -55,8 +55,8 @@ func (c CapabilityType) IsValid() error { // CapabilityResponse is a struct for the Execute response of a capability. type CapabilityResponse struct { - value values.Value - err error + Value values.Value + Err error } // CallbackExecutable is an interface for executing a capability. @@ -67,7 +67,7 @@ type CallbackExecutable interface { // Request specific configuration is passed in via the inputs parameter. // A successful response must always return a value. An error is assumed otherwise. // The intent is to make the API explicit. - Execute(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error + Execute(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error } // BaseCapability interface needs to be implemented by all capability types. @@ -80,8 +80,8 @@ type BaseCapability interface { // TriggerCapability interface needs to be implemented by all trigger capabilities. type TriggerCapability interface { BaseCapability - RegisterTrigger(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error - UnregisterTrigger(ctx context.Context, inputs values.Map) error + RegisterTrigger(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error + UnregisterTrigger(ctx context.Context, inputs *values.Map) error } // ActionCapability interface needs to be implemented by all action capabilities. @@ -157,7 +157,7 @@ var defaultExecuteTimeout = 10 * time.Second // We are not handling a case where a capability panics and crashes. // There is default timeout of 10 seconds. If a capability takes longer than // that then it should be executed asynchronously. -func ExecuteSync(ctx context.Context, c CallbackExecutable, inputs values.Map) (values.Value, error) { +func ExecuteSync(ctx context.Context, c CallbackExecutable, inputs *values.Map) (values.Value, error) { ctxWithT, cancel := context.WithTimeout(ctx, defaultExecuteTimeout) defer cancel() @@ -165,7 +165,7 @@ func ExecuteSync(ctx context.Context, c CallbackExecutable, inputs values.Map) ( vs := make([]values.Value, 0) var setupErr error - go func(innerCtx context.Context, innerC CallbackExecutable, innerInputs values.Map, innerCallback chan CapabilityResponse) { + go func(innerCtx context.Context, innerC CallbackExecutable, innerInputs *values.Map, innerCallback chan CapabilityResponse) { setupErr = innerC.Execute(innerCtx, innerCallback, innerInputs) }(ctxWithT, c, inputs, callback) @@ -179,11 +179,11 @@ outerLoop: // An error means execution has been interrupted. // We'll return the value discarding values received // until now. - if response.err != nil { - return nil, response.err + if response.Err != nil { + return nil, response.Err } - vs = append(vs, response.value) + vs = append(vs, response.Value) // Timeout when a capability panics, crashes, and does not close the channel. case <-ctxWithT.Done(): diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index b817060a755..ebd1a1d565d 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -52,16 +52,16 @@ func Test_CapabilityInfo_Invalid(t *testing.T) { type mockCapabilityWithExecute struct { CallbackExecutable CapabilityInfo - ExecuteFn func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error + ExecuteFn func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error } -func (m *mockCapabilityWithExecute) Execute(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { +func (m *mockCapabilityWithExecute) Execute(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { return m.ExecuteFn(ctx, callback, inputs) } func Test_ExecuteSyncReturnSingleValue(t *testing.T) { mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { val, _ := values.NewString("hello") callback <- CapabilityResponse{val, nil} @@ -71,7 +71,7 @@ func Test_ExecuteSyncReturnSingleValue(t *testing.T) { }, } config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(context.Background(), mcwe, *config) + val, err := ExecuteSync(context.Background(), mcwe, config) assert.NoError(t, err, val) assert.Equal(t, "hello", val.(*values.String).Underlying) @@ -81,7 +81,7 @@ func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { es, _ := values.NewString("hello") expectedList := []values.Value{es, es, es} mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { callback <- CapabilityResponse{es, nil} callback <- CapabilityResponse{es, nil} callback <- CapabilityResponse{es, nil} @@ -92,7 +92,7 @@ func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { }, } config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(context.Background(), mcwe, *config) + val, err := ExecuteSync(context.Background(), mcwe, config) assert.NoError(t, err, val) assert.ElementsMatch(t, expectedList, val.(*values.List).Underlying) @@ -101,13 +101,13 @@ func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { func Test_ExecuteSyncCapabilitySetupErrors(t *testing.T) { expectedErr := errors.New("something went wrong during setup") mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { close(callback) return expectedErr }, } config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(context.Background(), mcwe, *config) + val, err := ExecuteSync(context.Background(), mcwe, config) assert.ErrorContains(t, err, expectedErr.Error()) assert.Nil(t, val) @@ -119,12 +119,12 @@ func Test_ExecuteSyncTimeout(t *testing.T) { cancel() mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { return nil }, } config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(ctxWithTimeout, mcwe, *config) + val, err := ExecuteSync(ctxWithTimeout, mcwe, config) assert.ErrorContains(t, err, "context timed out. If you did not set a timeout, be aware that the default ExecuteSync timeout is") assert.Nil(t, val) @@ -133,7 +133,7 @@ func Test_ExecuteSyncTimeout(t *testing.T) { func Test_ExecuteSyncCapabilityErrors(t *testing.T) { expectedErr := errors.New("something went wrong during execution") mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { callback <- CapabilityResponse{nil, expectedErr} close(callback) @@ -142,7 +142,7 @@ func Test_ExecuteSyncCapabilityErrors(t *testing.T) { }, } config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(context.Background(), mcwe, *config) + val, err := ExecuteSync(context.Background(), mcwe, config) assert.ErrorContains(t, err, expectedErr.Error()) assert.Nil(t, val) @@ -150,13 +150,13 @@ func Test_ExecuteSyncCapabilityErrors(t *testing.T) { func Test_ExecuteSyncDoesNotReturnValues(t *testing.T) { mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { close(callback) return nil }, } config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(context.Background(), mcwe, *config) + val, err := ExecuteSync(context.Background(), mcwe, config) assert.ErrorContains(t, err, "capability did not return any values") assert.Nil(t, val) diff --git a/core/capabilities/examples/on_demand_trigger.go b/core/capabilities/examples/on_demand_trigger.go index 08bd799faeb..1739d660ffe 100644 --- a/core/capabilities/examples/on_demand_trigger.go +++ b/core/capabilities/examples/on_demand_trigger.go @@ -2,6 +2,11 @@ package examples import ( "context" + "errors" + "fmt" + "sync" + + "github.com/mitchellh/mapstructure" "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink/v2/core/capabilities" @@ -10,11 +15,12 @@ import ( // OnDemandTrigger is an example on-demand trigger. type OnDemandTrigger struct { capabilities.CapabilityInfo - // map between Workflow Execution IDs and callback channels + chans map[string]chan capabilities.CapabilityResponse + mu sync.Mutex } // NewOnDemandTrigger returns a new OnDemandTrigger. -func NewOnDemandTrigger() (*OnDemandTrigger, error) { +func NewOnDemandTrigger() *OnDemandTrigger { onDemandTriggerInfo, err := capabilities.NewCapabilityInfo( "on-demand-trigger", capabilities.CapabilityTypeTrigger, @@ -23,35 +29,88 @@ func NewOnDemandTrigger() (*OnDemandTrigger, error) { ) if err != nil { - return nil, err + panic(err) } return &OnDemandTrigger{ CapabilityInfo: onDemandTriggerInfo, - }, nil + chans: map[string]chan capabilities.CapabilityResponse{}, + } } -// TODO SendMultipleEvents -// func (o *OnDemandTrigger) FanOutEvent(ctx context.Context, event capabilities.CapabilityResponse) error { -// o.eventsChannel <- event -// return nil -// } +func (o *OnDemandTrigger) FanOutEvent(ctx context.Context, event capabilities.CapabilityResponse) error { + o.mu.Lock() + defer o.mu.Unlock() + for _, ch := range o.chans { + ch <- event + } + return nil +} // Execute executes the on-demand trigger. -func (o *OnDemandTrigger) SendEvent(ctx context.Context, event capabilities.CapabilityResponse) error { - // TODO: Add a "workflow execution id" to be able to differentiate between recipient channels +func (o *OnDemandTrigger) SendEvent(ctx context.Context, wid string, event capabilities.CapabilityResponse) error { + o.mu.Lock() + defer o.mu.Unlock() + ch, ok := o.chans[wid] + if !ok { + return fmt.Errorf("no registration for %s", wid) + } + + ch <- event return nil } -func (o *OnDemandTrigger) RegisterTrigger(ctx context.Context, callback chan capabilities.CapabilityResponse, inputs values.Map) error { - // Validate inputs. - // Get 1) a workflow execution id from the input so we can register a trigger - // and 2) - // Send any new events from the eventChannel to the newly register callback channel +type triggerRequest struct { + WorkflowExecutionID string `mapstructure:"weid"` +} + +func (o *OnDemandTrigger) Validate(inputs *values.Map) (*triggerRequest, error) { + i := &triggerRequest{} + m, err := inputs.Unwrap() + if err != nil { + return nil, err + } + err = mapstructure.Decode(m, i) + if err != nil { + return nil, err + } + + fmt.Print(i, m, inputs) + if i.WorkflowExecutionID == "" { + return nil, errors.New("must provide workflow execution id") + } + + return i, err + +} + +func (o *OnDemandTrigger) RegisterTrigger(ctx context.Context, callback chan capabilities.CapabilityResponse, inputs *values.Map) error { + i, err := o.Validate(inputs) + if err != nil { + return err + } + + o.mu.Lock() + defer o.mu.Unlock() + + o.chans[i.WorkflowExecutionID] = callback return nil } -func (o *OnDemandTrigger) UnregisterTrigger(ctx context.Context, inputs values.Map) error { +func (o *OnDemandTrigger) UnregisterTrigger(ctx context.Context, inputs *values.Map) error { + i, err := o.Validate(inputs) + if err != nil { + return err + } + + o.mu.Lock() + defer o.mu.Unlock() + + ch, ok := o.chans[i.WorkflowExecutionID] + if ok { + close(ch) + } + delete(o.chans, i.WorkflowExecutionID) return nil } diff --git a/core/capabilities/examples/on_demand_trigger_test.go b/core/capabilities/examples/on_demand_trigger_test.go new file mode 100644 index 00000000000..0716f188442 --- /dev/null +++ b/core/capabilities/examples/on_demand_trigger_test.go @@ -0,0 +1,45 @@ +package examples + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/capabilities" +) + +func TestOnDemandTrigger(t *testing.T) { + tr := NewOnDemandTrigger() + ctx := context.Background() + + callback := make(chan capabilities.CapabilityResponse, 10) + m, err := values.NewMap(map[string]any{"weid": "hello"}) + require.NoError(t, err) + + err = tr.RegisterTrigger(ctx, callback, m) + require.NoError(t, err) + + er := capabilities.CapabilityResponse{ + Value: &values.String{"hello"}, + } + + err = tr.FanOutEvent(ctx, er) + require.NoError(t, err) + + assert.Len(t, callback, 1) + assert.Equal(t, er, <-callback) +} + +func TestOnDemandTrigger_ChannelDoesntExist(t *testing.T) { + tr := NewOnDemandTrigger() + ctx := context.Background() + + er := capabilities.CapabilityResponse{ + Value: &values.String{"hello"}, + } + err := tr.SendEvent(ctx, "hello", er) + assert.ErrorContains(t, err, "no registration") +} diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index 996174a77a1..54937b67c50 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -15,7 +15,7 @@ type cmockCapability struct { CapabilityInfo } -func (m *cmockCapability) Execute(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { +func (m *cmockCapability) Execute(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { return nil } @@ -23,11 +23,11 @@ type tmockCapability struct { CapabilityInfo } -func (m *tmockCapability) RegisterTrigger(ctx context.Context, callback chan CapabilityResponse, inputs values.Map) error { +func (m *tmockCapability) RegisterTrigger(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { return nil } -func (m *tmockCapability) UnregisterTrigger(ctx context.Context, inputs values.Map) error { +func (m *tmockCapability) UnregisterTrigger(ctx context.Context, inputs *values.Map) error { return nil } @@ -92,13 +92,13 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { tcs := []struct { name string - newCapability func(ctx context.Context, reg *Registry) error + newCapability func(ctx context.Context, reg *Registry) (string, error) getCapability func(ctx context.Context, reg *Registry, id string) error errContains string }{ { name: "action", - newCapability: func(ctx context.Context, reg *Registry) error { + newCapability: func(ctx context.Context, reg *Registry) (string, error) { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -109,7 +109,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { require.NoError(t, err) c := &cmockCapability{CapabilityInfo: ci} - return reg.Add(ctx, c) + return id, reg.Add(ctx, c) }, getCapability: func(ctx context.Context, reg *Registry, id string) error { _, err := reg.GetAction(ctx, id) @@ -118,7 +118,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { }, { name: "target", - newCapability: func(ctx context.Context, reg *Registry) error { + newCapability: func(ctx context.Context, reg *Registry) (string, error) { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -129,7 +129,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { require.NoError(t, err) c := &cmockCapability{CapabilityInfo: ci} - return reg.Add(ctx, c) + return id, reg.Add(ctx, c) }, getCapability: func(ctx context.Context, reg *Registry, id string) error { _, err := reg.GetTarget(ctx, id) @@ -138,7 +138,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { }, { name: "trigger", - newCapability: func(ctx context.Context, reg *Registry) error { + newCapability: func(ctx context.Context, reg *Registry) (string, error) { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -149,7 +149,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { require.NoError(t, err) c := &tmockCapability{CapabilityInfo: ci} - return reg.Add(ctx, c) + return id, reg.Add(ctx, c) }, getCapability: func(ctx context.Context, reg *Registry, id string) error { _, err := reg.GetTrigger(ctx, id) @@ -158,7 +158,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { }, { name: "consensus", - newCapability: func(ctx context.Context, reg *Registry) error { + newCapability: func(ctx context.Context, reg *Registry) (string, error) { id := uuid.New().String() ci, err := NewCapabilityInfo( id, @@ -169,7 +169,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { require.NoError(t, err) c := &cmockCapability{CapabilityInfo: ci} - return reg.Add(ctx, c) + return id, reg.Add(ctx, c) }, getCapability: func(ctx context.Context, reg *Registry, id string) error { _, err := reg.GetConsensus(ctx, id) @@ -182,7 +182,10 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { reg := NewRegistry() for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - err := tc.newCapability(ctx, reg) + id, err := tc.newCapability(ctx, reg) + require.NoError(t, err) + + err = tc.getCapability(ctx, reg, id) require.NoError(t, err) }) } diff --git a/go.sum b/go.sum index 497ec227da8..c8f6620af85 100644 --- a/go.sum +++ b/go.sum @@ -1164,18 +1164,8 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429 h1:xkejUBZhcBpBrTSfxc91Iwzadrb6SXw8ks69bHIQ9Ww= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429/go.mod h1:wJmVvDf4XSjsahWtfUq3wvIAYEAuhr7oxmxYnEL/LGQ= -<<<<<<< HEAD github.com/smartcontractkit/chainlink-common v0.1.7-0.20240124161023-948579cbaffa h1:9g7e1C3295ALDK8Gs42fIKSSJfI+H1RoBmivGWTvIZo= github.com/smartcontractkit/chainlink-common v0.1.7-0.20240124161023-948579cbaffa/go.mod h1:05rRF84QKlIOF5LfTBPkHdw4UpBI2G3zxRcuZ65bPjk= -======= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240119143538-04c7f63ad53a h1:lgM0yPo0KqSntLY4Y42RAH3avdv+Kyne8n+VM7cwlxo= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240119143538-04c7f63ad53a/go.mod h1:05rRF84QKlIOF5LfTBPkHdw4UpBI2G3zxRcuZ65bPjk= -<<<<<<< HEAD -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240123140427-baf1323e7639 h1:HUsMpiOfMLyz6TC2DWoJEDk+iL3wEuN2frE9y9L4LI8= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240123140427-baf1323e7639/go.mod h1:05rRF84QKlIOF5LfTBPkHdw4UpBI2G3zxRcuZ65bPjk= ->>>>>>> f1616f573a (Review comments) -======= ->>>>>>> 3943b41f3a (Undo chainlink-common change) github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240120192246-4bb04c997ca0 h1:NALwENz6vQ972DuD9AZjqRjyNSxH9ptNapizQGLI+2s= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240120192246-4bb04c997ca0/go.mod h1:NcVAT/GETDBvIoAej5K6OYqAtDOkF6vO5pYw/hLuYVU= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20231204152908-a6e3fe8ff2a1 h1:xYqRgZO0nMSO8CBCMR0r3WA+LZ4kNL8a6bnbyk/oBtQ= From 0ff16b8befbd03eb046a78ad6eda2aa3ba745f79 Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Fri, 26 Jan 2024 18:06:48 +0000 Subject: [PATCH 18/32] Fix race --- core/capabilities/capability.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 350d6ae6800..14863daebbb 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -162,13 +162,14 @@ func ExecuteSync(ctx context.Context, c CallbackExecutable, inputs *values.Map) defer cancel() callback := make(chan CapabilityResponse) - vs := make([]values.Value, 0) + sec := make(chan error) - var setupErr error - go func(innerCtx context.Context, innerC CallbackExecutable, innerInputs *values.Map, innerCallback chan CapabilityResponse) { - setupErr = innerC.Execute(innerCtx, innerCallback, innerInputs) - }(ctxWithT, c, inputs, callback) + go func(innerCtx context.Context, innerC CallbackExecutable, innerInputs *values.Map, innerCallback chan CapabilityResponse, errCh chan error) { + setupErr := innerC.Execute(innerCtx, innerCallback, innerInputs) + sec <- setupErr + }(ctxWithT, c, inputs, callback, sec) + vs := make([]values.Value, 0) outerLoop: for { select { @@ -191,6 +192,7 @@ outerLoop: } } + setupErr := <-sec // Something went wrong when setting up a capability. if setupErr != nil { return nil, setupErr From 5bb25e01c1433ac36e73b2324f21b599e69f41aa Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Wed, 31 Jan 2024 20:53:57 +0000 Subject: [PATCH 19/32] Add TODOs for defaultExecuteTimeout/idMaxLength --- core/capabilities/capability.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 14863daebbb..651cdc78a70 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -118,6 +118,9 @@ func (c CapabilityInfo) Info() CapabilityInfo { var idRegex = regexp.MustCompile(`[a-z0-9_\-:]`) const ( + // TODO: this length was largely picked arbitrarily. + // Consider what a realistic/desirable value should be. + // See: https://smartcontract-it.atlassian.net/jira/software/c/projects/KS/boards/182 idMaxLength = 128 ) @@ -151,6 +154,9 @@ func NewCapabilityInfo( }, nil } +// TODO: this timeout was largely picked arbitrarily. +// Consider what a realistic/desirable value should be. +// See: https://smartcontract-it.atlassian.net/jira/software/c/projects/KS/boards/182 var defaultExecuteTimeout = 10 * time.Second // ExecuteSync executes a capability synchronously. From bfde8f1989516464c6ef2ff850ec4834d31a2231 Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Wed, 31 Jan 2024 21:02:17 +0000 Subject: [PATCH 20/32] Move capability info to a var; add must handle --- core/capabilities/capability.go | 40 ++++++++-- core/capabilities/capability_test.go | 53 ++++++++----- .../examples/on_demand_trigger.go | 69 ++++------------ .../examples/on_demand_trigger_test.go | 16 +++- core/capabilities/registry_test.go | 79 +++++++++---------- 5 files changed, 129 insertions(+), 128 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 651cdc78a70..5326c5731cd 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -59,6 +59,16 @@ type CapabilityResponse struct { Err error } +type Metadata struct { + WorkflowID string +} + +type CapabilityRequest struct { + Metadata Metadata + Config *values.Map + Inputs *values.Map +} + // CallbackExecutable is an interface for executing a capability. type CallbackExecutable interface { // Capability must respect context.Done and cleanup any request specific resources @@ -67,7 +77,7 @@ type CallbackExecutable interface { // Request specific configuration is passed in via the inputs parameter. // A successful response must always return a value. An error is assumed otherwise. // The intent is to make the API explicit. - Execute(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error + Execute(ctx context.Context, callback chan CapabilityResponse, request CapabilityRequest) error } // BaseCapability interface needs to be implemented by all capability types. @@ -80,8 +90,8 @@ type BaseCapability interface { // TriggerCapability interface needs to be implemented by all trigger capabilities. type TriggerCapability interface { BaseCapability - RegisterTrigger(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error - UnregisterTrigger(ctx context.Context, inputs *values.Map) error + RegisterTrigger(ctx context.Context, callback chan CapabilityResponse, request CapabilityRequest) error + UnregisterTrigger(ctx context.Context, request CapabilityRequest) error } // ActionCapability interface needs to be implemented by all action capabilities. @@ -154,6 +164,22 @@ func NewCapabilityInfo( }, nil } +// MustCapabilityInfo returns a new CapabilityInfo, +// panicking if we could not instantiate a CapabilityInfo. +func MustCapabilityInfo( + id string, + capabilityType CapabilityType, + description string, + version string, +) CapabilityInfo { + c, err := NewCapabilityInfo(id, capabilityType, description, version) + if err != nil { + panic(err) + } + + return c +} + // TODO: this timeout was largely picked arbitrarily. // Consider what a realistic/desirable value should be. // See: https://smartcontract-it.atlassian.net/jira/software/c/projects/KS/boards/182 @@ -163,17 +189,17 @@ var defaultExecuteTimeout = 10 * time.Second // We are not handling a case where a capability panics and crashes. // There is default timeout of 10 seconds. If a capability takes longer than // that then it should be executed asynchronously. -func ExecuteSync(ctx context.Context, c CallbackExecutable, inputs *values.Map) (values.Value, error) { +func ExecuteSync(ctx context.Context, c CallbackExecutable, request CapabilityRequest) (values.Value, error) { ctxWithT, cancel := context.WithTimeout(ctx, defaultExecuteTimeout) defer cancel() callback := make(chan CapabilityResponse) sec := make(chan error) - go func(innerCtx context.Context, innerC CallbackExecutable, innerInputs *values.Map, innerCallback chan CapabilityResponse, errCh chan error) { - setupErr := innerC.Execute(innerCtx, innerCallback, innerInputs) + go func(innerCtx context.Context, innerC CallbackExecutable, innerReq CapabilityRequest, innerCallback chan CapabilityResponse, errCh chan error) { + setupErr := innerC.Execute(innerCtx, innerCallback, innerReq) sec <- setupErr - }(ctxWithT, c, inputs, callback, sec) + }(ctxWithT, c, request, callback, sec) vs := make([]values.Value, 0) outerLoop: diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index ebd1a1d565d..13c23ef77e4 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -52,16 +52,16 @@ func Test_CapabilityInfo_Invalid(t *testing.T) { type mockCapabilityWithExecute struct { CallbackExecutable CapabilityInfo - ExecuteFn func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error + ExecuteFn func(ctx context.Context, callback chan CapabilityResponse, req CapabilityRequest) error } -func (m *mockCapabilityWithExecute) Execute(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { - return m.ExecuteFn(ctx, callback, inputs) +func (m *mockCapabilityWithExecute) Execute(ctx context.Context, callback chan CapabilityResponse, req CapabilityRequest) error { + return m.ExecuteFn(ctx, callback, req) } func Test_ExecuteSyncReturnSingleValue(t *testing.T) { mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, req CapabilityRequest) error { val, _ := values.NewString("hello") callback <- CapabilityResponse{val, nil} @@ -70,8 +70,8 @@ func Test_ExecuteSyncReturnSingleValue(t *testing.T) { return nil }, } - config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(context.Background(), mcwe, config) + req := CapabilityRequest{} + val, err := ExecuteSync(context.Background(), mcwe, req) assert.NoError(t, err, val) assert.Equal(t, "hello", val.(*values.String).Underlying) @@ -81,7 +81,7 @@ func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { es, _ := values.NewString("hello") expectedList := []values.Value{es, es, es} mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, req CapabilityRequest) error { callback <- CapabilityResponse{es, nil} callback <- CapabilityResponse{es, nil} callback <- CapabilityResponse{es, nil} @@ -91,8 +91,8 @@ func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { return nil }, } - config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(context.Background(), mcwe, config) + req := CapabilityRequest{} + val, err := ExecuteSync(context.Background(), mcwe, req) assert.NoError(t, err, val) assert.ElementsMatch(t, expectedList, val.(*values.List).Underlying) @@ -101,13 +101,13 @@ func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { func Test_ExecuteSyncCapabilitySetupErrors(t *testing.T) { expectedErr := errors.New("something went wrong during setup") mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, req CapabilityRequest) error { close(callback) return expectedErr }, } - config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(context.Background(), mcwe, config) + req := CapabilityRequest{} + val, err := ExecuteSync(context.Background(), mcwe, req) assert.ErrorContains(t, err, expectedErr.Error()) assert.Nil(t, val) @@ -119,12 +119,12 @@ func Test_ExecuteSyncTimeout(t *testing.T) { cancel() mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, req CapabilityRequest) error { return nil }, } - config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(ctxWithTimeout, mcwe, config) + req := CapabilityRequest{} + val, err := ExecuteSync(ctxWithTimeout, mcwe, req) assert.ErrorContains(t, err, "context timed out. If you did not set a timeout, be aware that the default ExecuteSync timeout is") assert.Nil(t, val) @@ -133,7 +133,7 @@ func Test_ExecuteSyncTimeout(t *testing.T) { func Test_ExecuteSyncCapabilityErrors(t *testing.T) { expectedErr := errors.New("something went wrong during execution") mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, req CapabilityRequest) error { callback <- CapabilityResponse{nil, expectedErr} close(callback) @@ -141,8 +141,8 @@ func Test_ExecuteSyncCapabilityErrors(t *testing.T) { return nil }, } - config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(context.Background(), mcwe, config) + req := CapabilityRequest{} + val, err := ExecuteSync(context.Background(), mcwe, req) assert.ErrorContains(t, err, expectedErr.Error()) assert.Nil(t, val) @@ -150,14 +150,25 @@ func Test_ExecuteSyncCapabilityErrors(t *testing.T) { func Test_ExecuteSyncDoesNotReturnValues(t *testing.T) { mcwe := &mockCapabilityWithExecute{ - ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { + ExecuteFn: func(ctx context.Context, callback chan CapabilityResponse, req CapabilityRequest) error { close(callback) return nil }, } - config, _ := values.NewMap(map[string]interface{}{}) - val, err := ExecuteSync(context.Background(), mcwe, config) + req := CapabilityRequest{} + val, err := ExecuteSync(context.Background(), mcwe, req) assert.ErrorContains(t, err, "capability did not return any values") assert.Nil(t, val) } + +func Test_MustCapabilityInfo(t *testing.T) { + assert.Panics(t, func() { + MustCapabilityInfo( + "capability-id", + CapabilityTypeAction, + "This is a mock capability that doesn't do anything.", + "should-panic", + ) + }) +} diff --git a/core/capabilities/examples/on_demand_trigger.go b/core/capabilities/examples/on_demand_trigger.go index 1739d660ffe..6b87f817891 100644 --- a/core/capabilities/examples/on_demand_trigger.go +++ b/core/capabilities/examples/on_demand_trigger.go @@ -2,16 +2,19 @@ package examples import ( "context" - "errors" "fmt" "sync" - "github.com/mitchellh/mapstructure" - - "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink/v2/core/capabilities" ) +var info = capabilities.MustCapabilityInfo( + "on-demand-trigger", + capabilities.CapabilityTypeTrigger, + "An example on-demand trigger.", + "v1.0.0", +) + // OnDemandTrigger is an example on-demand trigger. type OnDemandTrigger struct { capabilities.CapabilityInfo @@ -21,19 +24,8 @@ type OnDemandTrigger struct { // NewOnDemandTrigger returns a new OnDemandTrigger. func NewOnDemandTrigger() *OnDemandTrigger { - onDemandTriggerInfo, err := capabilities.NewCapabilityInfo( - "on-demand-trigger", - capabilities.CapabilityTypeTrigger, - "An example on-demand trigger.", - "v1.0.0", - ) - - if err != nil { - panic(err) - } - return &OnDemandTrigger{ - CapabilityInfo: onDemandTriggerInfo, + CapabilityInfo: info, chans: map[string]chan capabilities.CapabilityResponse{}, } } @@ -60,57 +52,26 @@ func (o *OnDemandTrigger) SendEvent(ctx context.Context, wid string, event capab return nil } -type triggerRequest struct { - WorkflowExecutionID string `mapstructure:"weid"` -} - -func (o *OnDemandTrigger) Validate(inputs *values.Map) (*triggerRequest, error) { - i := &triggerRequest{} - m, err := inputs.Unwrap() - if err != nil { - return nil, err - } - - err = mapstructure.Decode(m, i) - if err != nil { - return nil, err - } - - fmt.Print(i, m, inputs) - if i.WorkflowExecutionID == "" { - return nil, errors.New("must provide workflow execution id") - } - - return i, err - -} - -func (o *OnDemandTrigger) RegisterTrigger(ctx context.Context, callback chan capabilities.CapabilityResponse, inputs *values.Map) error { - i, err := o.Validate(inputs) - if err != nil { - return err - } +func (o *OnDemandTrigger) RegisterTrigger(ctx context.Context, callback chan capabilities.CapabilityResponse, req capabilities.CapabilityRequest) error { + weid := req.Metadata.WorkflowID o.mu.Lock() defer o.mu.Unlock() - o.chans[i.WorkflowExecutionID] = callback + o.chans[weid] = callback return nil } -func (o *OnDemandTrigger) UnregisterTrigger(ctx context.Context, inputs *values.Map) error { - i, err := o.Validate(inputs) - if err != nil { - return err - } +func (o *OnDemandTrigger) UnregisterTrigger(ctx context.Context, req capabilities.CapabilityRequest) error { + weid := req.Metadata.WorkflowID o.mu.Lock() defer o.mu.Unlock() - ch, ok := o.chans[i.WorkflowExecutionID] + ch, ok := o.chans[weid] if ok { close(ch) } - delete(o.chans, i.WorkflowExecutionID) + delete(o.chans, weid) return nil } diff --git a/core/capabilities/examples/on_demand_trigger_test.go b/core/capabilities/examples/on_demand_trigger_test.go index 0716f188442..0c5af4593bb 100644 --- a/core/capabilities/examples/on_demand_trigger_test.go +++ b/core/capabilities/examples/on_demand_trigger_test.go @@ -12,14 +12,24 @@ import ( ) func TestOnDemandTrigger(t *testing.T) { + r := capabilities.NewRegistry() tr := NewOnDemandTrigger() ctx := context.Background() - callback := make(chan capabilities.CapabilityResponse, 10) - m, err := values.NewMap(map[string]any{"weid": "hello"}) + err := r.Add(ctx, tr) + require.NoError(t, err) + + trigger, err := r.GetTrigger(ctx, tr.Info().ID) require.NoError(t, err) - err = tr.RegisterTrigger(ctx, callback, m) + callback := make(chan capabilities.CapabilityResponse, 10) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.Metadata{ + WorkflowID: "hello", + }, + } + err = trigger.RegisterTrigger(ctx, callback, req) require.NoError(t, err) er := capabilities.CapabilityResponse{ diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index 54937b67c50..3fc0b59c4df 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -1,4 +1,4 @@ -package capabilities +package capabilities_test import ( "context" @@ -8,38 +8,39 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/capabilities" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/examples" ) type cmockCapability struct { - CapabilityInfo + capabilities.CapabilityInfo } -func (m *cmockCapability) Execute(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { +func (m *cmockCapability) Execute(ctx context.Context, callback chan capabilities.CapabilityResponse, req capabilities.CapabilityRequest) error { return nil } type tmockCapability struct { - CapabilityInfo + capabilities.CapabilityInfo } -func (m *tmockCapability) RegisterTrigger(ctx context.Context, callback chan CapabilityResponse, inputs *values.Map) error { +func (m *tmockCapability) RegisterTrigger(ctx context.Context, callback chan capabilities.CapabilityResponse, req capabilities.CapabilityRequest) error { return nil } -func (m *tmockCapability) UnregisterTrigger(ctx context.Context, inputs *values.Map) error { +func (m *tmockCapability) UnregisterTrigger(ctx context.Context, req capabilities.CapabilityRequest) error { return nil } func TestRegistry(t *testing.T) { ctx := context.Background() - r := NewRegistry() + r := capabilities.NewRegistry() id := "capability-1" - ci, err := NewCapabilityInfo( + ci, err := capabilities.NewCapabilityInfo( id, - CapabilityTypeAction, + capabilities.CapabilityTypeAction, "capability-1-description", "v1.0.0", ) @@ -61,12 +62,12 @@ func TestRegistry(t *testing.T) { func TestRegistry_NoDuplicateIDs(t *testing.T) { ctx := context.Background() - r := NewRegistry() + r := capabilities.NewRegistry() id := "capability-1" - ci, err := NewCapabilityInfo( + ci, err := capabilities.NewCapabilityInfo( id, - CapabilityTypeAction, + capabilities.CapabilityTypeAction, "capability-1-description", "v1.0.0", ) @@ -76,9 +77,9 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { err = r.Add(ctx, c) require.NoError(t, err) - ci, err = NewCapabilityInfo( + ci, err = capabilities.NewCapabilityInfo( id, - CapabilityTypeConsensus, + capabilities.CapabilityTypeConsensus, "capability-2-description", "v1.0.0", ) @@ -92,17 +93,17 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { tcs := []struct { name string - newCapability func(ctx context.Context, reg *Registry) (string, error) - getCapability func(ctx context.Context, reg *Registry, id string) error + newCapability func(ctx context.Context, reg *capabilities.Registry) (string, error) + getCapability func(ctx context.Context, reg *capabilities.Registry, id string) error errContains string }{ { name: "action", - newCapability: func(ctx context.Context, reg *Registry) (string, error) { + newCapability: func(ctx context.Context, reg *capabilities.Registry) (string, error) { id := uuid.New().String() - ci, err := NewCapabilityInfo( + ci, err := capabilities.NewCapabilityInfo( id, - CapabilityTypeAction, + capabilities.CapabilityTypeAction, "capability-1-description", "v1.0.0", ) @@ -111,18 +112,18 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { c := &cmockCapability{CapabilityInfo: ci} return id, reg.Add(ctx, c) }, - getCapability: func(ctx context.Context, reg *Registry, id string) error { + getCapability: func(ctx context.Context, reg *capabilities.Registry, id string) error { _, err := reg.GetAction(ctx, id) return err }, }, { name: "target", - newCapability: func(ctx context.Context, reg *Registry) (string, error) { + newCapability: func(ctx context.Context, reg *capabilities.Registry) (string, error) { id := uuid.New().String() - ci, err := NewCapabilityInfo( + ci, err := capabilities.NewCapabilityInfo( id, - CapabilityTypeTarget, + capabilities.CapabilityTypeTarget, "capability-1-description", "v1.0.0", ) @@ -131,38 +132,30 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { c := &cmockCapability{CapabilityInfo: ci} return id, reg.Add(ctx, c) }, - getCapability: func(ctx context.Context, reg *Registry, id string) error { + getCapability: func(ctx context.Context, reg *capabilities.Registry, id string) error { _, err := reg.GetTarget(ctx, id) return err }, }, { name: "trigger", - newCapability: func(ctx context.Context, reg *Registry) (string, error) { - id := uuid.New().String() - ci, err := NewCapabilityInfo( - id, - CapabilityTypeTrigger, - "capability-1-description", - "v1.0.0", - ) - require.NoError(t, err) - - c := &tmockCapability{CapabilityInfo: ci} - return id, reg.Add(ctx, c) + newCapability: func(ctx context.Context, reg *capabilities.Registry) (string, error) { + odt := examples.NewOnDemandTrigger() + info := odt.Info() + return info.ID, reg.Add(ctx, odt) }, - getCapability: func(ctx context.Context, reg *Registry, id string) error { + getCapability: func(ctx context.Context, reg *capabilities.Registry, id string) error { _, err := reg.GetTrigger(ctx, id) return err }, }, { name: "consensus", - newCapability: func(ctx context.Context, reg *Registry) (string, error) { + newCapability: func(ctx context.Context, reg *capabilities.Registry) (string, error) { id := uuid.New().String() - ci, err := NewCapabilityInfo( + ci, err := capabilities.NewCapabilityInfo( id, - CapabilityTypeConsensus, + capabilities.CapabilityTypeConsensus, "capability-1-description", "v1.0.0", ) @@ -171,7 +164,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { c := &cmockCapability{CapabilityInfo: ci} return id, reg.Add(ctx, c) }, - getCapability: func(ctx context.Context, reg *Registry, id string) error { + getCapability: func(ctx context.Context, reg *capabilities.Registry, id string) error { _, err := reg.GetConsensus(ctx, id) return err }, @@ -179,7 +172,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { } ctx := context.Background() - reg := NewRegistry() + reg := capabilities.NewRegistry() for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { id, err := tc.newCapability(ctx, reg) From 516b50e255fe15bc5581b1d61c7226e60cb0d60b Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Thu, 1 Feb 2024 23:11:05 +0000 Subject: [PATCH 21/32] Remove tmockCapability --- core/capabilities/registry_test.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index 3fc0b59c4df..9665ce4a4cd 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -20,18 +20,6 @@ func (m *cmockCapability) Execute(ctx context.Context, callback chan capabilitie return nil } -type tmockCapability struct { - capabilities.CapabilityInfo -} - -func (m *tmockCapability) RegisterTrigger(ctx context.Context, callback chan capabilities.CapabilityResponse, req capabilities.CapabilityRequest) error { - return nil -} - -func (m *tmockCapability) UnregisterTrigger(ctx context.Context, req capabilities.CapabilityRequest) error { - return nil -} - func TestRegistry(t *testing.T) { ctx := context.Background() From 456a2e4c21060e2ab6dcb270cdb3a12cd3985e3b Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Fri, 2 Feb 2024 01:20:50 +0200 Subject: [PATCH 22/32] Udpate broken comment --- core/capabilities/capability.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 5326c5731cd..49c12bd1323 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -63,6 +63,7 @@ type Metadata struct { WorkflowID string } +// CapabilityRequest is a struct for the Execute request of a capability. type CapabilityRequest struct { Metadata Metadata Config *values.Map @@ -74,7 +75,7 @@ type CallbackExecutable interface { // Capability must respect context.Done and cleanup any request specific resources // when the context is cancelled. When a request has been completed the capability // is also expected to close the callback channel. - // Request specific configuration is passed in via the inputs parameter. + // Request specific configuration is passed in via the request parameter. // A successful response must always return a value. An error is assumed otherwise. // The intent is to make the API explicit. Execute(ctx context.Context, callback chan CapabilityResponse, request CapabilityRequest) error From 7b8ac45a5cb2d6f067877a21f08c42a8c329b9c2 Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Thu, 1 Feb 2024 23:53:10 +0000 Subject: [PATCH 23/32] Add some tests --- core/capabilities/capability_test.go | 9 +++++++ .../examples/on_demand_trigger_test.go | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index 13c23ef77e4..cffd014044b 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -3,6 +3,7 @@ package capabilities import ( "context" "errors" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -47,6 +48,14 @@ func Test_CapabilityInfo_Invalid(t *testing.T) { "hello", ) assert.ErrorContains(t, err, "invalid version") + + _, err = NewCapabilityInfo( + strings.Repeat("n", 256), + CapabilityTypeAction, + "This is a mock capability that doesn't do anything.", + "hello", + ) + assert.ErrorContains(t, err, "exceeds max length 128") } type mockCapabilityWithExecute struct { diff --git a/core/capabilities/examples/on_demand_trigger_test.go b/core/capabilities/examples/on_demand_trigger_test.go index 0c5af4593bb..6dae5a26299 100644 --- a/core/capabilities/examples/on_demand_trigger_test.go +++ b/core/capabilities/examples/on_demand_trigger_test.go @@ -53,3 +53,27 @@ func TestOnDemandTrigger_ChannelDoesntExist(t *testing.T) { err := tr.SendEvent(ctx, "hello", er) assert.ErrorContains(t, err, "no registration") } + +func TestOnDemandTrigger_(t *testing.T) { + tr := NewOnDemandTrigger() + ctx := context.Background() + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.Metadata{ + WorkflowID: "hello", + }, + } + callback := make(chan capabilities.CapabilityResponse, 10) + + err := tr.RegisterTrigger(ctx, callback, req) + require.NoError(t, err) + + er := capabilities.CapabilityResponse{ + Value: &values.String{"hello"}, + } + err = tr.SendEvent(ctx, "hello", er) + require.NoError(t, err) + + assert.Len(t, callback, 1) + assert.Equal(t, er, <-callback) +} From 42732ec62236bf314529932c459e89f1754c744f Mon Sep 17 00:00:00 2001 From: "DeividasK (YubiKey 5 Nano)" Date: Fri, 2 Feb 2024 01:48:50 +0200 Subject: [PATCH 24/32] RegisterUnregisterWorkflow interface --- core/capabilities/capability.go | 21 +++++++++++++++++-- .../examples/on_demand_trigger_test.go | 4 ++-- core/capabilities/registry_test.go | 8 +++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 49c12bd1323..29f4fd11534 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -59,19 +59,36 @@ type CapabilityResponse struct { Err error } -type Metadata struct { +type RequestMetadata struct { + WorkflowID string + WorkflowExecutionID string +} + +type RegistrationMetadata struct { WorkflowID string } // CapabilityRequest is a struct for the Execute request of a capability. type CapabilityRequest struct { - Metadata Metadata + Metadata RequestMetadata Config *values.Map Inputs *values.Map } +type RegisterToWorkflowRequest struct { + Metadata RegistrationMetadata + Config *values.Map +} + +type UnregisterFromWorkflowRequest struct { + Metadata RegistrationMetadata + Config *values.Map +} + // CallbackExecutable is an interface for executing a capability. type CallbackExecutable interface { + RegisterToWorkflow(ctx context.Context, request RegisterToWorkflowRequest) error + UnregisterFromWorkflow(ctx context.Context, request UnregisterFromWorkflowRequest) error // Capability must respect context.Done and cleanup any request specific resources // when the context is cancelled. When a request has been completed the capability // is also expected to close the callback channel. diff --git a/core/capabilities/examples/on_demand_trigger_test.go b/core/capabilities/examples/on_demand_trigger_test.go index 6dae5a26299..fd917bbd34c 100644 --- a/core/capabilities/examples/on_demand_trigger_test.go +++ b/core/capabilities/examples/on_demand_trigger_test.go @@ -25,8 +25,8 @@ func TestOnDemandTrigger(t *testing.T) { callback := make(chan capabilities.CapabilityResponse, 10) req := capabilities.CapabilityRequest{ - Metadata: capabilities.Metadata{ - WorkflowID: "hello", + Metadata: capabilities.RequestMetadata{ + WorkflowExecutionID: "hello", }, } err = trigger.RegisterTrigger(ctx, callback, req) diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index 9665ce4a4cd..e49e71ce074 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -20,6 +20,14 @@ func (m *cmockCapability) Execute(ctx context.Context, callback chan capabilitie return nil } +func (m *cmockCapability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { + return nil +} + +func (m *cmockCapability) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { + return nil +} + func TestRegistry(t *testing.T) { ctx := context.Background() From 7c4ae33dd9e2e7919126dd42d606e467af192673 Mon Sep 17 00:00:00 2001 From: DeividasK Date: Fri, 2 Feb 2024 18:44:21 +0200 Subject: [PATCH 25/32] cmock => mock --- core/capabilities/registry_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index e49e71ce074..0829a7053bf 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -12,19 +12,19 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/capabilities/examples" ) -type cmockCapability struct { +type mockCapability struct { capabilities.CapabilityInfo } -func (m *cmockCapability) Execute(ctx context.Context, callback chan capabilities.CapabilityResponse, req capabilities.CapabilityRequest) error { +func (m *mockCapability) Execute(ctx context.Context, callback chan capabilities.CapabilityResponse, req capabilities.CapabilityRequest) error { return nil } -func (m *cmockCapability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { +func (m *mockCapability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { return nil } -func (m *cmockCapability) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { +func (m *mockCapability) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { return nil } @@ -42,7 +42,7 @@ func TestRegistry(t *testing.T) { ) require.NoError(t, err) - c := &cmockCapability{CapabilityInfo: ci} + c := &mockCapability{CapabilityInfo: ci} err = r.Add(ctx, c) require.NoError(t, err) @@ -69,7 +69,7 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { ) require.NoError(t, err) - c := &cmockCapability{CapabilityInfo: ci} + c := &mockCapability{CapabilityInfo: ci} err = r.Add(ctx, c) require.NoError(t, err) @@ -80,7 +80,7 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { "v1.0.0", ) require.NoError(t, err) - c2 := &cmockCapability{CapabilityInfo: ci} + c2 := &mockCapability{CapabilityInfo: ci} err = r.Add(ctx, c2) assert.ErrorContains(t, err, "capability with id: capability-1 already exists") @@ -105,7 +105,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { ) require.NoError(t, err) - c := &cmockCapability{CapabilityInfo: ci} + c := &mockCapability{CapabilityInfo: ci} return id, reg.Add(ctx, c) }, getCapability: func(ctx context.Context, reg *capabilities.Registry, id string) error { @@ -125,7 +125,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { ) require.NoError(t, err) - c := &cmockCapability{CapabilityInfo: ci} + c := &mockCapability{CapabilityInfo: ci} return id, reg.Add(ctx, c) }, getCapability: func(ctx context.Context, reg *capabilities.Registry, id string) error { @@ -157,7 +157,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { ) require.NoError(t, err) - c := &cmockCapability{CapabilityInfo: ci} + c := &mockCapability{CapabilityInfo: ci} return id, reg.Add(ctx, c) }, getCapability: func(ctx context.Context, reg *capabilities.Registry, id string) error { From 2b60ec4f7afcfcec4432c0d359de76a73e22be0d Mon Sep 17 00:00:00 2001 From: DeividasK Date: Fri, 2 Feb 2024 18:47:30 +0200 Subject: [PATCH 26/32] Create a const for test var --- core/capabilities/examples/on_demand_trigger_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/capabilities/examples/on_demand_trigger_test.go b/core/capabilities/examples/on_demand_trigger_test.go index fd917bbd34c..0c23cb340fe 100644 --- a/core/capabilities/examples/on_demand_trigger_test.go +++ b/core/capabilities/examples/on_demand_trigger_test.go @@ -11,6 +11,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/capabilities" ) +const testID = "test-id-1" + func TestOnDemandTrigger(t *testing.T) { r := capabilities.NewRegistry() tr := NewOnDemandTrigger() @@ -26,14 +28,14 @@ func TestOnDemandTrigger(t *testing.T) { req := capabilities.CapabilityRequest{ Metadata: capabilities.RequestMetadata{ - WorkflowExecutionID: "hello", + WorkflowExecutionID: testID, }, } err = trigger.RegisterTrigger(ctx, callback, req) require.NoError(t, err) er := capabilities.CapabilityResponse{ - Value: &values.String{"hello"}, + Value: &values.String{Underlying: testID}, } err = tr.FanOutEvent(ctx, er) @@ -48,9 +50,9 @@ func TestOnDemandTrigger_ChannelDoesntExist(t *testing.T) { ctx := context.Background() er := capabilities.CapabilityResponse{ - Value: &values.String{"hello"}, + Value: &values.String{Underlying: testID}, } - err := tr.SendEvent(ctx, "hello", er) + err := tr.SendEvent(ctx, testID, er) assert.ErrorContains(t, err, "no registration") } From 6eb8ad1f0cce12efff3fc4e16f95a13ea43dbbd4 Mon Sep 17 00:00:00 2001 From: DeividasK Date: Fri, 2 Feb 2024 18:50:06 +0200 Subject: [PATCH 27/32] Use testutils.Context(t) --- core/capabilities/capability_test.go | 13 +++++++------ .../capabilities/examples/on_demand_trigger_test.go | 6 +++--- core/capabilities/registry_test.go | 7 ++++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index cffd014044b..d2cf2bc70ea 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) func Test_CapabilityInfo(t *testing.T) { @@ -80,7 +81,7 @@ func Test_ExecuteSyncReturnSingleValue(t *testing.T) { }, } req := CapabilityRequest{} - val, err := ExecuteSync(context.Background(), mcwe, req) + val, err := ExecuteSync(testutils.Context(t), mcwe, req) assert.NoError(t, err, val) assert.Equal(t, "hello", val.(*values.String).Underlying) @@ -101,7 +102,7 @@ func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { }, } req := CapabilityRequest{} - val, err := ExecuteSync(context.Background(), mcwe, req) + val, err := ExecuteSync(testutils.Context(t), mcwe, req) assert.NoError(t, err, val) assert.ElementsMatch(t, expectedList, val.(*values.List).Underlying) @@ -116,14 +117,14 @@ func Test_ExecuteSyncCapabilitySetupErrors(t *testing.T) { }, } req := CapabilityRequest{} - val, err := ExecuteSync(context.Background(), mcwe, req) + val, err := ExecuteSync(testutils.Context(t), mcwe, req) assert.ErrorContains(t, err, expectedErr.Error()) assert.Nil(t, val) } func Test_ExecuteSyncTimeout(t *testing.T) { - ctxWithTimeout := context.Background() + ctxWithTimeout := testutils.Context(t) ctxWithTimeout, cancel := context.WithCancel(ctxWithTimeout) cancel() @@ -151,7 +152,7 @@ func Test_ExecuteSyncCapabilityErrors(t *testing.T) { }, } req := CapabilityRequest{} - val, err := ExecuteSync(context.Background(), mcwe, req) + val, err := ExecuteSync(testutils.Context(t), mcwe, req) assert.ErrorContains(t, err, expectedErr.Error()) assert.Nil(t, val) @@ -165,7 +166,7 @@ func Test_ExecuteSyncDoesNotReturnValues(t *testing.T) { }, } req := CapabilityRequest{} - val, err := ExecuteSync(context.Background(), mcwe, req) + val, err := ExecuteSync(testutils.Context(t), mcwe, req) assert.ErrorContains(t, err, "capability did not return any values") assert.Nil(t, val) diff --git a/core/capabilities/examples/on_demand_trigger_test.go b/core/capabilities/examples/on_demand_trigger_test.go index 0c23cb340fe..326becaba59 100644 --- a/core/capabilities/examples/on_demand_trigger_test.go +++ b/core/capabilities/examples/on_demand_trigger_test.go @@ -1,7 +1,6 @@ package examples import ( - "context" "testing" "github.com/stretchr/testify/assert" @@ -9,6 +8,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink/v2/core/capabilities" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) const testID = "test-id-1" @@ -16,7 +16,7 @@ const testID = "test-id-1" func TestOnDemandTrigger(t *testing.T) { r := capabilities.NewRegistry() tr := NewOnDemandTrigger() - ctx := context.Background() + ctx := testutils.Context(t) err := r.Add(ctx, tr) require.NoError(t, err) @@ -47,7 +47,7 @@ func TestOnDemandTrigger(t *testing.T) { func TestOnDemandTrigger_ChannelDoesntExist(t *testing.T) { tr := NewOnDemandTrigger() - ctx := context.Background() + ctx := testutils.Context(t) er := capabilities.CapabilityResponse{ Value: &values.String{Underlying: testID}, diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index 0829a7053bf..ee5d2694b5c 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/capabilities" "github.com/smartcontractkit/chainlink/v2/core/capabilities/examples" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) type mockCapability struct { @@ -29,7 +30,7 @@ func (m *mockCapability) UnregisterFromWorkflow(ctx context.Context, request cap } func TestRegistry(t *testing.T) { - ctx := context.Background() + ctx := testutils.Context(t) r := capabilities.NewRegistry() @@ -57,7 +58,7 @@ func TestRegistry(t *testing.T) { } func TestRegistry_NoDuplicateIDs(t *testing.T) { - ctx := context.Background() + ctx := testutils.Context(t) r := capabilities.NewRegistry() id := "capability-1" @@ -167,7 +168,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { }, } - ctx := context.Background() + ctx := testutils.Context(t) reg := capabilities.NewRegistry() for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { From 74ef706e373847766897bccf963db3a74b446c25 Mon Sep 17 00:00:00 2001 From: DeividasK Date: Fri, 2 Feb 2024 18:56:49 +0200 Subject: [PATCH 28/32] Do not unwrap single value from a list --- core/capabilities/capability.go | 8 +------- core/capabilities/capability_test.go | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 29f4fd11534..2d2b096799a 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -207,7 +207,7 @@ var defaultExecuteTimeout = 10 * time.Second // We are not handling a case where a capability panics and crashes. // There is default timeout of 10 seconds. If a capability takes longer than // that then it should be executed asynchronously. -func ExecuteSync(ctx context.Context, c CallbackExecutable, request CapabilityRequest) (values.Value, error) { +func ExecuteSync(ctx context.Context, c CallbackExecutable, request CapabilityRequest) (*values.List, error) { ctxWithT, cancel := context.WithTimeout(ctx, defaultExecuteTimeout) defer cancel() @@ -254,11 +254,5 @@ outerLoop: return nil, errors.New("capability did not return any values") } - // If the capability returned only one value, - // let's unwrap it to improve usability. - if len(vs) == 1 { - return vs[0], nil - } - return &values.List{Underlying: vs}, nil } diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index d2cf2bc70ea..1d9d2c44115 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -84,7 +84,7 @@ func Test_ExecuteSyncReturnSingleValue(t *testing.T) { val, err := ExecuteSync(testutils.Context(t), mcwe, req) assert.NoError(t, err, val) - assert.Equal(t, "hello", val.(*values.String).Underlying) + assert.Equal(t, "hello", val.Underlying[0].(*values.String).Underlying) } func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { @@ -105,7 +105,7 @@ func Test_ExecuteSyncReturnMultipleValues(t *testing.T) { val, err := ExecuteSync(testutils.Context(t), mcwe, req) assert.NoError(t, err, val) - assert.ElementsMatch(t, expectedList, val.(*values.List).Underlying) + assert.ElementsMatch(t, expectedList, val.Underlying) } func Test_ExecuteSyncCapabilitySetupErrors(t *testing.T) { From f79fc5b3135789514ea76418ca98c52793f3aa76 Mon Sep 17 00:00:00 2001 From: DeividasK Date: Fri, 2 Feb 2024 18:58:09 +0200 Subject: [PATCH 29/32] Channel naming --- core/capabilities/capability.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 2d2b096799a..9795f22d560 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -211,19 +211,19 @@ func ExecuteSync(ctx context.Context, c CallbackExecutable, request CapabilityRe ctxWithT, cancel := context.WithTimeout(ctx, defaultExecuteTimeout) defer cancel() - callback := make(chan CapabilityResponse) - sec := make(chan error) + responseCh := make(chan CapabilityResponse) + setupCh := make(chan error) go func(innerCtx context.Context, innerC CallbackExecutable, innerReq CapabilityRequest, innerCallback chan CapabilityResponse, errCh chan error) { setupErr := innerC.Execute(innerCtx, innerCallback, innerReq) - sec <- setupErr - }(ctxWithT, c, request, callback, sec) + setupCh <- setupErr + }(ctxWithT, c, request, responseCh, setupCh) vs := make([]values.Value, 0) outerLoop: for { select { - case response, isOpen := <-callback: + case response, isOpen := <-responseCh: if !isOpen { break outerLoop } @@ -242,7 +242,7 @@ outerLoop: } } - setupErr := <-sec + setupErr := <-setupCh // Something went wrong when setting up a capability. if setupErr != nil { return nil, setupErr From 3709f1ac8f377940b86041f1cceb6cbc3d21fbeb Mon Sep 17 00:00:00 2001 From: DeividasK Date: Fri, 2 Feb 2024 19:00:33 +0200 Subject: [PATCH 30/32] MustCapabilityInfo => MustNewCapabilityInfo --- core/capabilities/capability.go | 4 ++-- core/capabilities/capability_test.go | 4 ++-- core/capabilities/examples/on_demand_trigger.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index 9795f22d560..c6c21cddbe6 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -182,9 +182,9 @@ func NewCapabilityInfo( }, nil } -// MustCapabilityInfo returns a new CapabilityInfo, +// MustNewCapabilityInfo returns a new CapabilityInfo, // panicking if we could not instantiate a CapabilityInfo. -func MustCapabilityInfo( +func MustNewCapabilityInfo( id string, capabilityType CapabilityType, description string, diff --git a/core/capabilities/capability_test.go b/core/capabilities/capability_test.go index 1d9d2c44115..2414e87bba9 100644 --- a/core/capabilities/capability_test.go +++ b/core/capabilities/capability_test.go @@ -172,9 +172,9 @@ func Test_ExecuteSyncDoesNotReturnValues(t *testing.T) { assert.Nil(t, val) } -func Test_MustCapabilityInfo(t *testing.T) { +func Test_MustNewCapabilityInfo(t *testing.T) { assert.Panics(t, func() { - MustCapabilityInfo( + MustNewCapabilityInfo( "capability-id", CapabilityTypeAction, "This is a mock capability that doesn't do anything.", diff --git a/core/capabilities/examples/on_demand_trigger.go b/core/capabilities/examples/on_demand_trigger.go index 6b87f817891..5f20fe2d37e 100644 --- a/core/capabilities/examples/on_demand_trigger.go +++ b/core/capabilities/examples/on_demand_trigger.go @@ -8,7 +8,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/capabilities" ) -var info = capabilities.MustCapabilityInfo( +var info = capabilities.MustNewCapabilityInfo( "on-demand-trigger", capabilities.CapabilityTypeTrigger, "An example on-demand trigger.", From a5c8b66bfc03158883981bfc343f05adda0067ec Mon Sep 17 00:00:00 2001 From: DeividasK Date: Fri, 2 Feb 2024 19:28:18 +0200 Subject: [PATCH 31/32] Tidy more --- core/capabilities/capability.go | 1 + core/capabilities/examples/on_demand_trigger.go | 2 -- core/capabilities/examples/on_demand_trigger_test.go | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/capabilities/capability.go b/core/capabilities/capability.go index c6c21cddbe6..84ef243c271 100644 --- a/core/capabilities/capability.go +++ b/core/capabilities/capability.go @@ -243,6 +243,7 @@ outerLoop: } setupErr := <-setupCh + // Something went wrong when setting up a capability. if setupErr != nil { return nil, setupErr diff --git a/core/capabilities/examples/on_demand_trigger.go b/core/capabilities/examples/on_demand_trigger.go index 5f20fe2d37e..4c5c1c359f9 100644 --- a/core/capabilities/examples/on_demand_trigger.go +++ b/core/capabilities/examples/on_demand_trigger.go @@ -15,14 +15,12 @@ var info = capabilities.MustNewCapabilityInfo( "v1.0.0", ) -// OnDemandTrigger is an example on-demand trigger. type OnDemandTrigger struct { capabilities.CapabilityInfo chans map[string]chan capabilities.CapabilityResponse mu sync.Mutex } -// NewOnDemandTrigger returns a new OnDemandTrigger. func NewOnDemandTrigger() *OnDemandTrigger { return &OnDemandTrigger{ CapabilityInfo: info, diff --git a/core/capabilities/examples/on_demand_trigger_test.go b/core/capabilities/examples/on_demand_trigger_test.go index 326becaba59..ff1b934170b 100644 --- a/core/capabilities/examples/on_demand_trigger_test.go +++ b/core/capabilities/examples/on_demand_trigger_test.go @@ -58,10 +58,10 @@ func TestOnDemandTrigger_ChannelDoesntExist(t *testing.T) { func TestOnDemandTrigger_(t *testing.T) { tr := NewOnDemandTrigger() - ctx := context.Background() + ctx := testutils.Context(t) req := capabilities.CapabilityRequest{ - Metadata: capabilities.Metadata{ + Metadata: capabilities.RequestMetadata{ WorkflowID: "hello", }, } @@ -71,7 +71,7 @@ func TestOnDemandTrigger_(t *testing.T) { require.NoError(t, err) er := capabilities.CapabilityResponse{ - Value: &values.String{"hello"}, + Value: &values.String{Underlying: testID}, } err = tr.SendEvent(ctx, "hello", er) require.NoError(t, err) From 555b0875bf731fcedb4eb8da7772b22db8319bba Mon Sep 17 00:00:00 2001 From: DeividasK Date: Fri, 2 Feb 2024 19:35:58 +0200 Subject: [PATCH 32/32] examples => triggers && downstream --- core/capabilities/registry_test.go | 4 ++-- .../{examples => triggers}/on_demand_trigger.go | 16 ++++++++-------- .../on_demand_trigger_test.go | 14 +++++++------- 3 files changed, 17 insertions(+), 17 deletions(-) rename core/capabilities/{examples => triggers}/on_demand_trigger.go (62%) rename core/capabilities/{examples => triggers}/on_demand_trigger_test.go (87%) diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index ee5d2694b5c..a8c51827b32 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink/v2/core/capabilities" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/examples" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/triggers" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) @@ -137,7 +137,7 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { { name: "trigger", newCapability: func(ctx context.Context, reg *capabilities.Registry) (string, error) { - odt := examples.NewOnDemandTrigger() + odt := triggers.NewOnDemand() info := odt.Info() return info.ID, reg.Add(ctx, odt) }, diff --git a/core/capabilities/examples/on_demand_trigger.go b/core/capabilities/triggers/on_demand_trigger.go similarity index 62% rename from core/capabilities/examples/on_demand_trigger.go rename to core/capabilities/triggers/on_demand_trigger.go index 4c5c1c359f9..1260a14568f 100644 --- a/core/capabilities/examples/on_demand_trigger.go +++ b/core/capabilities/triggers/on_demand_trigger.go @@ -1,4 +1,4 @@ -package examples +package triggers import ( "context" @@ -15,20 +15,20 @@ var info = capabilities.MustNewCapabilityInfo( "v1.0.0", ) -type OnDemandTrigger struct { +type OnDemand struct { capabilities.CapabilityInfo chans map[string]chan capabilities.CapabilityResponse mu sync.Mutex } -func NewOnDemandTrigger() *OnDemandTrigger { - return &OnDemandTrigger{ +func NewOnDemand() *OnDemand { + return &OnDemand{ CapabilityInfo: info, chans: map[string]chan capabilities.CapabilityResponse{}, } } -func (o *OnDemandTrigger) FanOutEvent(ctx context.Context, event capabilities.CapabilityResponse) error { +func (o *OnDemand) FanOutEvent(ctx context.Context, event capabilities.CapabilityResponse) error { o.mu.Lock() defer o.mu.Unlock() for _, ch := range o.chans { @@ -38,7 +38,7 @@ func (o *OnDemandTrigger) FanOutEvent(ctx context.Context, event capabilities.Ca } // Execute executes the on-demand trigger. -func (o *OnDemandTrigger) SendEvent(ctx context.Context, wid string, event capabilities.CapabilityResponse) error { +func (o *OnDemand) SendEvent(ctx context.Context, wid string, event capabilities.CapabilityResponse) error { o.mu.Lock() defer o.mu.Unlock() ch, ok := o.chans[wid] @@ -50,7 +50,7 @@ func (o *OnDemandTrigger) SendEvent(ctx context.Context, wid string, event capab return nil } -func (o *OnDemandTrigger) RegisterTrigger(ctx context.Context, callback chan capabilities.CapabilityResponse, req capabilities.CapabilityRequest) error { +func (o *OnDemand) RegisterTrigger(ctx context.Context, callback chan capabilities.CapabilityResponse, req capabilities.CapabilityRequest) error { weid := req.Metadata.WorkflowID o.mu.Lock() @@ -60,7 +60,7 @@ func (o *OnDemandTrigger) RegisterTrigger(ctx context.Context, callback chan cap return nil } -func (o *OnDemandTrigger) UnregisterTrigger(ctx context.Context, req capabilities.CapabilityRequest) error { +func (o *OnDemand) UnregisterTrigger(ctx context.Context, req capabilities.CapabilityRequest) error { weid := req.Metadata.WorkflowID o.mu.Lock() diff --git a/core/capabilities/examples/on_demand_trigger_test.go b/core/capabilities/triggers/on_demand_trigger_test.go similarity index 87% rename from core/capabilities/examples/on_demand_trigger_test.go rename to core/capabilities/triggers/on_demand_trigger_test.go index ff1b934170b..ee7337a1824 100644 --- a/core/capabilities/examples/on_demand_trigger_test.go +++ b/core/capabilities/triggers/on_demand_trigger_test.go @@ -1,4 +1,4 @@ -package examples +package triggers import ( "testing" @@ -13,9 +13,9 @@ import ( const testID = "test-id-1" -func TestOnDemandTrigger(t *testing.T) { +func TestOnDemand(t *testing.T) { r := capabilities.NewRegistry() - tr := NewOnDemandTrigger() + tr := NewOnDemand() ctx := testutils.Context(t) err := r.Add(ctx, tr) @@ -45,8 +45,8 @@ func TestOnDemandTrigger(t *testing.T) { assert.Equal(t, er, <-callback) } -func TestOnDemandTrigger_ChannelDoesntExist(t *testing.T) { - tr := NewOnDemandTrigger() +func TestOnDemand_ChannelDoesntExist(t *testing.T) { + tr := NewOnDemand() ctx := testutils.Context(t) er := capabilities.CapabilityResponse{ @@ -56,8 +56,8 @@ func TestOnDemandTrigger_ChannelDoesntExist(t *testing.T) { assert.ErrorContains(t, err, "no registration") } -func TestOnDemandTrigger_(t *testing.T) { - tr := NewOnDemandTrigger() +func TestOnDemand_(t *testing.T) { + tr := NewOnDemand() ctx := testutils.Context(t) req := capabilities.CapabilityRequest{