github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/automation/trigger/runtime.go (about)

     1  package trigger
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  
     8  	"github.com/qri-io/qri/event"
     9  )
    10  
    11  // RuntimeType denotes a RuntimeTrigger
    12  const RuntimeType = "runtime"
    13  
    14  // A RuntimeTrigger implements the Trigger interface & keeps track of the number
    15  // of times it had been advanced
    16  type RuntimeTrigger struct {
    17  	id           string
    18  	active       bool
    19  	AdvanceCount int
    20  }
    21  
    22  var _ Trigger = (*RuntimeTrigger)(nil)
    23  
    24  // NewEmptyRuntimeTrigger returns a RuntimeTrigger
    25  func NewEmptyRuntimeTrigger() *RuntimeTrigger {
    26  	return &RuntimeTrigger{
    27  		id:           NewID(),
    28  		active:       false,
    29  		AdvanceCount: 0,
    30  	}
    31  }
    32  
    33  // NewRuntimeTrigger constructs a RuntimeTrigger from a configuration object
    34  func NewRuntimeTrigger(opt map[string]interface{}) (Trigger, error) {
    35  	t := opt["type"]
    36  	if t != RuntimeType {
    37  		return nil, fmt.Errorf("%w, expected %q but got %q", ErrTypeMismatch, RuntimeType, t)
    38  	}
    39  
    40  	data, err := json.Marshal(opt)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	rt := &RuntimeTrigger{}
    45  	err = rt.UnmarshalJSON(data)
    46  
    47  	if rt.id == "" {
    48  		rt.id = NewID()
    49  	}
    50  	return rt, err
    51  }
    52  
    53  // ID return the trigger.ID
    54  func (rt *RuntimeTrigger) ID() string {
    55  	return rt.id
    56  }
    57  
    58  // Active returns if the RuntimeTrigger is active
    59  func (rt *RuntimeTrigger) Active() bool {
    60  	return rt.active
    61  }
    62  
    63  // SetActive sets the active status
    64  func (rt *RuntimeTrigger) SetActive(active bool) error {
    65  	rt.active = active
    66  	return nil
    67  }
    68  
    69  // Type returns the RuntimeType
    70  func (rt *RuntimeTrigger) Type() string {
    71  	return RuntimeType
    72  }
    73  
    74  // Advance increments the AdvanceCount
    75  func (rt *RuntimeTrigger) Advance() error {
    76  	rt.AdvanceCount++
    77  	return nil
    78  }
    79  
    80  // ToMap returns the trigger as a map[string]interface{}
    81  func (rt *RuntimeTrigger) ToMap() map[string]interface{} {
    82  	return map[string]interface{}{
    83  		"id":           rt.id,
    84  		"active":       rt.active,
    85  		"type":         RuntimeType,
    86  		"advanceCount": rt.AdvanceCount,
    87  	}
    88  }
    89  
    90  type runtimeTrigger struct {
    91  	ID           string `json:"id"`
    92  	Active       bool   `json:"active"`
    93  	Type         string `json:"type"`
    94  	AdvanceCount int    `json:"advanceCount"`
    95  }
    96  
    97  // MarshalJSON implements the json.Marshaller interface
    98  func (rt *RuntimeTrigger) MarshalJSON() ([]byte, error) {
    99  	if rt == nil {
   100  		rt = &RuntimeTrigger{}
   101  	}
   102  	return json.Marshal(runtimeTrigger{
   103  		ID:           rt.ID(),
   104  		Active:       rt.active,
   105  		Type:         rt.Type(),
   106  		AdvanceCount: rt.AdvanceCount,
   107  	})
   108  }
   109  
   110  // UnmarshalJSON implements the json.Unmarshaller interface
   111  func (rt *RuntimeTrigger) UnmarshalJSON(d []byte) error {
   112  	t := &runtimeTrigger{}
   113  	err := json.Unmarshal(d, t)
   114  	if err != nil {
   115  		return err
   116  	}
   117  	if t.Type != RuntimeType {
   118  		return fmt.Errorf("%w, got %s, expected %s", ErrUnexpectedType, t.Type, RuntimeType)
   119  	}
   120  	*rt = RuntimeTrigger{
   121  		id:           t.ID,
   122  		active:       t.Active,
   123  		AdvanceCount: t.AdvanceCount,
   124  	}
   125  	return nil
   126  }
   127  
   128  // RuntimeListener listens for RuntimeTriggers to fire
   129  type RuntimeListener struct {
   130  	bus       event.Bus
   131  	TriggerCh chan event.WorkflowTriggerEvent
   132  	listening bool
   133  	triggers  *Set
   134  }
   135  
   136  var _ Listener = (*RuntimeListener)(nil)
   137  
   138  // NewRuntimeListener creates a RuntimeListener, and begin receiving on the
   139  // trigger channel. Any triggers received before the RuntimeListener has been
   140  // started using `runtimeListener.Start(ctx)` will be ignored
   141  func NewRuntimeListener(ctx context.Context, bus event.Bus) *RuntimeListener {
   142  	rl := &RuntimeListener{
   143  		bus:       bus,
   144  		TriggerCh: make(chan event.WorkflowTriggerEvent),
   145  		triggers:  NewSet(RuntimeType, NewRuntimeTrigger),
   146  	}
   147  	// start ensures that if a RuntimeTrigger attempts to trigger a workflow,
   148  	// but the RuntimeListener has not been told to start listening for
   149  	// triggers, the RuntimeTrigger won't block waiting for the
   150  	// RuntimeListener to start. Instead, the trigger will just get ignored
   151  	go rl.start(ctx)
   152  	return rl
   153  }
   154  
   155  // ConstructTrigger binds NewRuntimeTrigger to RuntimeListener
   156  func (l *RuntimeListener) ConstructTrigger(opt map[string]interface{}) (Trigger, error) {
   157  	return NewRuntimeTrigger(opt)
   158  }
   159  
   160  // Listen takes a list of sources and adds or updates the Listener's
   161  // store to include all the active triggers of the correct type
   162  func (l *RuntimeListener) Listen(sources ...Source) error {
   163  	return l.triggers.Add(sources...)
   164  }
   165  
   166  // Type returns the Type `RuntimeType`
   167  func (l *RuntimeListener) Type() string {
   168  	return RuntimeType
   169  }
   170  
   171  func (l *RuntimeListener) start(ctx context.Context) error {
   172  	go func() {
   173  		for {
   174  			select {
   175  			case wtp := <-l.TriggerCh:
   176  				if !l.listening {
   177  					log.Debugf("RuntimeListener: trigger ignored")
   178  					continue
   179  				}
   180  				if err := l.shouldTrigger(ctx, wtp); err != nil {
   181  					log.Debugf("RuntimeListener error: %s", err)
   182  					continue
   183  				}
   184  
   185  				err := l.bus.Publish(ctx, event.ETAutomationWorkflowTrigger, wtp)
   186  				if err != nil {
   187  					log.Debugf("RuntimeListener error publishing event.ETAutomationWorkflowTrigger: %s", err)
   188  					continue
   189  				}
   190  			case <-ctx.Done():
   191  				return
   192  			}
   193  		}
   194  	}()
   195  	return nil
   196  }
   197  
   198  func (l *RuntimeListener) shouldTrigger(ctx context.Context, wtp event.WorkflowTriggerEvent) error {
   199  	activeTriggers := l.triggers.Active()
   200  	workflowIDs, ok := activeTriggers[wtp.OwnerID]
   201  	if !ok {
   202  		return ErrNotFound
   203  	}
   204  	triggers, ok := workflowIDs[wtp.WorkflowID]
   205  	if !ok {
   206  		return ErrNotFound
   207  	}
   208  	for _, t := range triggers {
   209  		if t.ID() == wtp.TriggerID {
   210  			return nil
   211  		}
   212  	}
   213  	return ErrNotFound
   214  }
   215  
   216  // Start tells the RuntimeListener to begin actively listening for RuntimeTriggers
   217  func (l *RuntimeListener) Start(ctx context.Context) error {
   218  	l.listening = true
   219  	go func() {
   220  		<-ctx.Done()
   221  		l.Stop()
   222  	}()
   223  	return nil
   224  }
   225  
   226  // Stop tells the RuntimeListener to stop actively listening for RuntimeTriggers
   227  func (l *RuntimeListener) Stop() error {
   228  	l.listening = false
   229  	return nil
   230  }
   231  
   232  // TriggersExists returns true if triggers in the source match the triggers stored in
   233  // the runtime listener
   234  func (l *RuntimeListener) TriggersExists(source Source) bool {
   235  	return l.triggers.Exists(source)
   236  }