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

     1  package trigger
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/qri-io/iso8601"
    10  	"github.com/qri-io/qri/event"
    11  )
    12  
    13  const (
    14  	// CronType denotes a `CronTrigger`
    15  	CronType = "cron"
    16  	// DefaultInterval is the default amount of time to wait before checking
    17  	// if any CronTriggers have fired
    18  	DefaultInterval = time.Second
    19  )
    20  
    21  // NowFunc returns a new timestamp. can be overridden for testing purposes
    22  var NowFunc = time.Now
    23  
    24  // CronTrigger implements the Trigger interface & keeps track of periodicity
    25  // and the next run time
    26  type CronTrigger struct {
    27  	id           string
    28  	active       bool
    29  	periodicity  iso8601.RepeatingInterval
    30  	nextRunStart *time.Time
    31  }
    32  
    33  var _ Trigger = (*CronTrigger)(nil)
    34  
    35  // NewCronTrigger constructs a CronTrigger
    36  func NewCronTrigger(cfg map[string]interface{}) (Trigger, error) {
    37  	typ := cfg["type"]
    38  	if typ != CronType {
    39  		return nil, fmt.Errorf("%w, expected %q but got %q", ErrTypeMismatch, CronType, typ)
    40  	}
    41  
    42  	_, ok := cfg["periodicity"]
    43  	if !ok {
    44  		return nil, fmt.Errorf("field %q required", "periodicity")
    45  	}
    46  
    47  	data, err := json.Marshal(cfg)
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  	trig := &CronTrigger{}
    52  	err = trig.UnmarshalJSON(data)
    53  	if trig.id == "" {
    54  		trig.id = NewID()
    55  	}
    56  	if trig.nextRunStart == nil {
    57  		trig.nextRunStart = trig.periodicity.Interval.Start
    58  	}
    59  	return trig, err
    60  }
    61  
    62  // ID returns the trigger.ID
    63  func (ct *CronTrigger) ID() string { return ct.id }
    64  
    65  // Active returns true if the CronTrigger is active
    66  func (ct *CronTrigger) Active() bool { return ct.active }
    67  
    68  // SetActive sets the active status
    69  func (ct *CronTrigger) SetActive(active bool) error {
    70  	ct.active = active
    71  	return nil
    72  }
    73  
    74  // Type returns the CronType
    75  func (CronTrigger) Type() string { return CronType }
    76  
    77  // Advance sets the periodicity and nextRunStart to be ready for the next run
    78  func (ct *CronTrigger) Advance() error {
    79  	ct.periodicity = ct.periodicity.NextRep()
    80  	next := ct.periodicity.After(NowFunc())
    81  	ct.nextRunStart = &next
    82  	return nil
    83  }
    84  
    85  // ToMap returns the trigger as a map[string]interface{}
    86  func (ct *CronTrigger) ToMap() map[string]interface{} {
    87  	v := map[string]interface{}{
    88  		"id":          ct.id,
    89  		"active":      ct.active,
    90  		"periodicity": ct.periodicity.String(),
    91  		"type":        CronType,
    92  	}
    93  
    94  	if ct.nextRunStart != nil {
    95  		v["nextRunStart"] = ct.nextRunStart.Format(time.RFC3339)
    96  	}
    97  
    98  	return v
    99  }
   100  
   101  // MarshalJSON satisfies the json.Marshaller interface
   102  func (ct *CronTrigger) MarshalJSON() ([]byte, error) {
   103  	return json.Marshal(ct.ToMap())
   104  }
   105  
   106  // UnmarshalJSON satisfies the json.Unmarshaller interface
   107  func (ct *CronTrigger) UnmarshalJSON(p []byte) error {
   108  	v := struct {
   109  		Type         string     `json:"type"`
   110  		ID           string     `json:"id"`
   111  		Active       bool       `json:"active"`
   112  		Start        time.Time  `json:"start"`
   113  		Periodicity  string     `json:"periodicity"`
   114  		NextRunStart *time.Time `json:"nextRunStart"`
   115  	}{}
   116  
   117  	if err := json.Unmarshal(p, &v); err != nil {
   118  		return err
   119  	}
   120  	if v.Type != CronType {
   121  		return ErrUnexpectedType
   122  	}
   123  
   124  	ct.id = v.ID
   125  	ct.active = v.Active
   126  	periodicity, err := iso8601.ParseRepeatingInterval(v.Periodicity)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	ct.periodicity = periodicity
   131  	ct.nextRunStart = v.NextRunStart
   132  	return nil
   133  }
   134  
   135  // CronListener listens for CronTriggers
   136  type CronListener struct {
   137  	cancel   context.CancelFunc
   138  	pub      event.Publisher
   139  	interval time.Duration
   140  	triggers *Set
   141  }
   142  
   143  var _ Listener = (*CronListener)(nil)
   144  
   145  // NewCronListener returns a CronListener with the DefaultInterval
   146  func NewCronListener(pub event.Publisher) *CronListener {
   147  	return NewCronListenerInterval(pub, DefaultInterval)
   148  }
   149  
   150  // NewCronListenerInterval returns a CronListener with the given interval
   151  func NewCronListenerInterval(pub event.Publisher, interval time.Duration) *CronListener {
   152  	return &CronListener{
   153  		pub:      pub,
   154  		interval: interval,
   155  		triggers: NewSet(CronType, NewCronTrigger),
   156  	}
   157  }
   158  
   159  // ConstructTrigger binds NewCronTrigger to CronListener
   160  func (c *CronListener) ConstructTrigger(cfg map[string]interface{}) (Trigger, error) {
   161  	return NewCronTrigger(cfg)
   162  }
   163  
   164  // Listen takes a list of sources and adds or updates the Listener's store to
   165  // include all the active triggers of the CronType
   166  func (c *CronListener) Listen(sources ...Source) error {
   167  	return c.triggers.Add(sources...)
   168  }
   169  
   170  // Type returns the CronType
   171  func (c *CronListener) Type() string { return CronType }
   172  
   173  // Start tells the CronListener to begin listening for CronTriggers
   174  func (c *CronListener) Start(ctx context.Context) error {
   175  	ctxWithCancel, cancel := context.WithCancel(ctx)
   176  	c.cancel = cancel
   177  	check := func(ctx context.Context) {
   178  		now := NowFunc()
   179  		for ownerID, wids := range c.triggers.Active() {
   180  			for workflowID, triggers := range wids {
   181  				for _, trig := range triggers {
   182  					t := trig.(*CronTrigger)
   183  					if t.nextRunStart != nil && now.After(*t.nextRunStart) {
   184  						wte := event.WorkflowTriggerEvent{
   185  							WorkflowID: workflowID,
   186  							OwnerID:    ownerID,
   187  							TriggerID:  t.ID(),
   188  						}
   189  						if err := c.pub.Publish(ctx, event.ETAutomationWorkflowTrigger, wte); err != nil {
   190  							log.Debugw("CronListener: publish ETAutomationWorkflowTrigger", "error", err, "WorkflowTriggerEvent", wte)
   191  						}
   192  					}
   193  				}
   194  			}
   195  		}
   196  	}
   197  
   198  	go func() {
   199  		t := time.NewTicker(c.interval)
   200  		for {
   201  			select {
   202  			case <-t.C:
   203  				check(ctx)
   204  			case <-ctxWithCancel.Done():
   205  				return
   206  			}
   207  		}
   208  	}()
   209  	return nil
   210  }
   211  
   212  // Stop tells the CronListener to stop listening for CronTriggers
   213  func (c *CronListener) Stop() error {
   214  	// cancel will be nil if listener is never started
   215  	if c.cancel != nil {
   216  		c.cancel()
   217  	}
   218  	return nil
   219  }