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 }