Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 10 additions & 34 deletions cmd/workflow/simulate/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import (

"github.com/ethereum/go-ethereum/common"

cronservice "github.com/smartcontractkit/capabilities/scheduler/cron/service"
corekeys "github.com/smartcontractkit/chainlink-common/keystore/corekeys"
"github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key"
capreg "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry"
confhttpserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp/server"
httpserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/http/server"
consensusserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/consensus/server"
crontrigger "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron/server"
httptrigger "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/http/server"
"github.com/smartcontractkit/chainlink-common/pkg/config"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
Expand All @@ -21,19 +21,15 @@ import (

// ManualTriggers holds chain-agnostic trigger services used in simulation.
type ManualTriggers struct {
ManualCronTrigger *fakes.ManualCronTriggerService
// Capabilities are the capabilities from the capabilities repo.
Capabilities *Capabilities
ManualHTTPTrigger *ManualHTTPTriggerService
}

// NewManualTriggerCapabilities creates and registers cron and HTTP trigger capabilities.
// These are chain-agnostic and shared across all chain types.
func NewManualTriggerCapabilities(ctx context.Context, lggr logger.Logger, registry *capreg.Registry, httpTriggerPort int, limits *SimulationLimits) (*ManualTriggers, error) {
manualCronTrigger, err := fakes.NewManualCronTriggerService(lggr)
if err != nil {
return nil, err
}
manualCronTriggerServer := crontrigger.NewCronServer(manualCronTrigger)
if err := registry.Add(ctx, manualCronTriggerServer); err != nil {
func NewManualTriggerCapabilities(ctx context.Context, lggr logger.Logger, registry *capreg.Registry, caps *Capabilities, httpTriggerPort int, limits *SimulationLimits) (*ManualTriggers, error) {
if err := caps.Add(ctx, registry, cronservice.New); err != nil {
return nil, err
}

Expand All @@ -49,39 +45,19 @@ func NewManualTriggerCapabilities(ctx context.Context, lggr logger.Logger, regis
}

return &ManualTriggers{
ManualCronTrigger: manualCronTrigger,
Capabilities: caps,
ManualHTTPTrigger: manualHTTPTrigger,
}, nil
}

// Start starts cron and HTTP trigger services.
// Start starts the HTTP trigger service. Capabilities from the capabilities repo are started when they are built.
func (m *ManualTriggers) Start(ctx context.Context) error {
err := m.ManualCronTrigger.Start(ctx)
if err != nil {
return err
}

err = m.ManualHTTPTrigger.Start(ctx)
if err != nil {
return err
}

return nil
return m.ManualHTTPTrigger.Start(ctx)
}

// Close closes cron and HTTP trigger services.
// Close closes the HTTP trigger service. Capabilities from the capabilities repo are closed with their services.
func (m *ManualTriggers) Close() error {
err := m.ManualCronTrigger.Close()
if err != nil {
return err
}

err = m.ManualHTTPTrigger.Close()
if err != nil {
return err
}

return nil
return m.ManualHTTPTrigger.Close()
}

// NewFakeActionCapabilities builds faked capabilities, then registers them with the capability registry.
Expand Down
115 changes: 115 additions & 0 deletions cmd/workflow/simulate/repo_capabilities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package simulate

import (
"context"
"sync"

"github.com/jonboulle/clockwork"
"github.com/samber/do/v2"

"github.com/smartcontractkit/capabilities/libs/capability"
"github.com/smartcontractkit/chainlink-common/pkg/capabilities"
capreg "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/services"
)

// Capabilities builds the simulator's capabilities from the capabilities repo, all from one injector. What the
// simulator injects lets it trigger them early: skipping Clock fires their timers now.
type Capabilities struct {
Clock *SkipClock
// Services are what the capabilities started, for the caller to close, even when Add fails.
Services []services.Service

lggr logger.Logger
injector *capability.Injector
// selected is the only trigger registration the capabilities are asked for, set before the engine registers.
selected string
triggered chan struct{}
}

// initCapabilities clones the injector capabilities are embedded from, for the simulator to override what it
// needs to on it. Capabilities provide their own configs, so they run with their defaults unless overridden.
func initCapabilities(lggr logger.Logger) *Capabilities {
return &Capabilities{
Clock: NewSkipClock(),
lggr: lggr,
injector: capability.EmbedInjector.Clone(),
triggered: make(chan struct{}, 1),
}
}

// Add builds and starts the capability ctor builds, then registers it.
func (c *Capabilities) Add[T capability.Capability](ctx context.Context, registry *capreg.Registry, ctor func(do.Injector) (T, error)) error {
// StartCapabilities provides the real clock after cloning the injector, so only WithProvider overrides it.
built, svcs, err := capability.StartCapabilities(ctx, c.lggr, c.injector, 1, ctor,
capability.WithProvider(func(i *capability.Injector) error {
return i.ProvideValue[clockwork.Clock](c.Clock)
}))
c.Services = append(c.Services, svcs...)
if err != nil {
return err
}

return registry.Add(ctx, &selectableTrigger{ExecutableAndTriggerCapability: built[0], capabilities: c})
}

// Select makes triggerID the only trigger registration passed on to the capabilities. Call it before the engine
// registers its triggers.
func (c *Capabilities) Select(triggerID string) {
c.selected = triggerID
}

// WaitForTrigger waits for the selected trigger to send an event.
func (c *Capabilities) WaitForTrigger(ctx context.Context) error {
select {
case <-c.triggered:
return nil
case <-ctx.Done():
return ctx.Err()
}
}

// selectableTrigger passes on only the selected trigger registration. Any other is a no-op whose events
// channel closes when it is unregistered.
type selectableTrigger struct {
capabilities.ExecutableAndTriggerCapability
capabilities *Capabilities
unselected sync.Map // trigger ID to chan capabilities.TriggerResponse
}

func (s *selectableTrigger) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) {
if request.TriggerID != s.capabilities.selected {
events := make(chan capabilities.TriggerResponse)
s.unselected.Store(request.TriggerID, events)
return events, nil
}

events, err := s.ExecutableAndTriggerCapability.RegisterTrigger(ctx, request)
if err != nil {
return nil, err
}

forwarded := make(chan capabilities.TriggerResponse)
go func() {
defer close(forwarded)
for event := range events {
forwarded <- event
select {
case s.capabilities.triggered <- struct{}{}:
default:
}
}
}()

return forwarded, nil
}

func (s *selectableTrigger) UnregisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) error {
if events, ok := s.unselected.LoadAndDelete(request.TriggerID); ok {
close(events.(chan capabilities.TriggerResponse))
return nil
}

return s.ExecutableAndTriggerCapability.UnregisterTrigger(ctx, request)
}
45 changes: 28 additions & 17 deletions cmd/workflow/simulate/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command {
simulateCmd.Flags().Int("trigger-index", -1, "Index of the trigger to run (0-based)")
simulateCmd.Flags().String("http-payload", "", "HTTP trigger payload as JSON string or path to JSON file")
simulateCmd.Flags().Int("http-trigger-port", defaultHTTPTriggerServerPort, "Port used by the local HTTP trigger server")
simulateCmd.Flags().Bool("listen", false, "Listen for HTTP requests or supported log triggers and run the simulator for each match (not supported by cron)")
simulateCmd.Flags().Bool("listen", false, "Listen for HTTP requests, cron schedules or supported log triggers and run the simulator for each match")

// Register chain-type-specific CLI flags (e.g., --evm-tx-hash).
chain.RegisterAllCLIFlags(simulateCmd)
Expand Down Expand Up @@ -529,15 +529,18 @@ func run(
beholderStarted = true
}

caps := initCapabilities(lggr.Named("Capabilities"))

// Register chain-agnostic cron and HTTP triggers
triggerLggr := lggr.Named("TriggerCapabilities")
var err error
manualTriggerCaps, err = NewManualTriggerCapabilities(ctx, triggerLggr, registry, inputs.HTTPTriggerPort, simLimits)
manualTriggerCaps, err = NewManualTriggerCapabilities(ctx, triggerLggr, registry, caps, inputs.HTTPTriggerPort, simLimits)
srvcs = append(srvcs, caps.Services...)
if err != nil {
setLifecycleErr(fmt.Errorf("failed to create trigger capabilities: %w", err))
return registry, srvcs
}
srvcs = append(srvcs, manualTriggerCaps.ManualCronTrigger, manualTriggerCaps.ManualHTTPTrigger)
srvcs = append(srvcs, manualTriggerCaps.ManualHTTPTrigger)

// nil capLimits disables enforcement.
var capLimits *cresettings.Workflows
Expand Down Expand Up @@ -1044,24 +1047,27 @@ func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs
triggerRegistrationID := fmt.Sprintf("trigger_reg_1111111111111111111111111111111111111111111111111111111111111111_%d", triggerIndex)
trigger := holder.TriggerToRun.Id
manualTriggerCaps := manualTriggerCapsGetter()
manualTriggerCaps.Capabilities.Select(triggerRegistrationID)

switch trigger {
case "cron-trigger@1.0.0":
caps := manualTriggerCaps.Capabilities
holder.ListenSupported = true
holder.TriggerFunc = func() error {
skipWaitSignal := make(chan struct{}, 1)

userPromptCtx, cancel := context.WithCancel(ctx)
defer cancel()

go func() {
ui.Line()
pressed := ui.WaitForEnter(userPromptCtx, "Cron scheduler started. Press Enter to skip waiting...")
if pressed {
skipWaitSignal <- struct{}{}
}
}()
// Listening runs on the schedule, so only a single run can skip ahead to it.
if !inputs.Listen {
go func() {
ui.Line()
if ui.WaitForEnter(userPromptCtx, "Cron scheduler started. Press Enter to skip waiting...") {
caps.Clock.Skip()
}
}()
}

return manualTriggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, skipWaitSignal)
return caps.WaitForTrigger(ctx)
}
case "http-trigger@1.0.0-alpha":
payload, err := getHTTPTriggerPayloadFromInput(inputs.InvocationDir, inputs.HTTPPayload)
Expand Down Expand Up @@ -1187,14 +1193,19 @@ func makeBeforeStartNonInteractive(holder *TriggerInfoAndBeforeStart, inputs Inp
triggerRegistrationID := fmt.Sprintf("trigger_reg_1111111111111111111111111111111111111111111111111111111111111111_%d", inputs.TriggerIndex)
trigger := holder.TriggerToRun.Id
manualTriggerCaps := manualTriggerCapsGetter()
manualTriggerCaps.Capabilities.Select(triggerRegistrationID)

switch trigger {
case "cron-trigger@1.0.0":
caps := manualTriggerCaps.Capabilities
holder.ListenSupported = true
holder.TriggerFunc = func() error {
skipWaitSignal := make(chan struct{}, 1)
// With cron schedule on non-interactive mode
skipWaitSignal <- struct{}{}
return manualTriggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, skipWaitSignal)
// A single non-interactive run does not wait for the schedule. Listening runs on it.
if !inputs.Listen {
caps.Clock.Skip()
}

return caps.WaitForTrigger(ctx)
}
case "http-trigger@1.0.0-alpha":
if strings.TrimSpace(inputs.HTTPPayload) == "" {
Expand Down
Loading
Loading