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 }