github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/automation/trigger/trigger.go (about) 1 package trigger 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "math/rand" 8 "sync" 9 "time" 10 11 golog "github.com/ipfs/go-log" 12 "github.com/qri-io/qri/profile" 13 ) 14 15 var ( 16 log = golog.Logger("trigger") 17 18 // ErrUnexpectedType indicates the trigger type is unexpected 19 ErrUnexpectedType = fmt.Errorf("unexpected trigger type") 20 // ErrTypeMismatch indicates the given TriggerType does not match the expected TriggerType 21 ErrTypeMismatch = fmt.Errorf("TriggerType mismatch") 22 // ErrEmptyOwnerID indicates the given Source has an empty ScopeID, known in other systems as the OwnerID 23 ErrEmptyOwnerID = fmt.Errorf("empty OwnerID") 24 // ErrEmptyWorkflowID indicates the given Source has an empty WorkflowID 25 ErrEmptyWorkflowID = fmt.Errorf("empty WorkflowID") 26 // ErrNotFound indicates that the trigger cannot be found 27 ErrNotFound = fmt.Errorf("trigger not found") 28 ) 29 30 const charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 31 32 var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) 33 34 // NewID returns a random string ID of alphanumeric characters 35 // These IDs only have to be unique within a single workflow 36 // This can be replaced with a determinate `NewID` function for testing 37 var NewID = func() string { 38 b := make([]byte, 5) 39 for i := range b { 40 b[i] = charset[seededRand.Intn(len(charset))] 41 } 42 return string(b) 43 } 44 45 // A Trigger determines under what circumstances an `event.ETAutomationWorkflowTrigger` 46 // should be emitted on the given event.Bus. It knows how to `Advance` itself. 47 type Trigger interface { 48 json.Marshaler 49 json.Unmarshaler 50 // ID returns the Trigger ID 51 ID() string 52 // Active returns whether the Trigger is enabled 53 Active() bool 54 // SetActive sets the enabled status 55 SetActive(active bool) error 56 // Type returns the Type of this Trigger 57 Type() string 58 // Advance adjusts the Trigger once it has been triggered 59 Advance() error 60 // ToMap returns the trigger as a map[string]interface 61 ToMap() map[string]interface{} 62 } 63 64 // Constructor is a function that creates a Trigger from a 65 // map[string]interface{} 66 type Constructor func(opts map[string]interface{}) (Trigger, error) 67 68 // A Listener emits a `event.ETTriggerWorkflow` event when a specific stimulus 69 // is triggered 70 type Listener interface { 71 // ConstructTrigger returns a Trigger of the associated Type 72 ConstructTrigger(opts map[string]interface{}) (Trigger, error) 73 // Listen takes a list of sources and adds or updates the Listener's 74 // store to include all the active triggers of the correct type 75 Listen(source ...Source) error 76 // Type returns the type of Trigger that this Listener listens for 77 Type() string 78 // Start puts the Listener in an active state of listening for triggers 79 Start(ctx context.Context) error 80 // Stop stops the Listener from listening for triggers 81 Stop() error 82 } 83 84 // Source is an abstraction for a `workflow.Workflow` 85 type Source interface { 86 WorkflowID() string 87 ActiveTriggers(triggerType string) []map[string]interface{} 88 Owner() profile.ID 89 } 90 91 // Set stores triggers of a common type, uniquely identified by ownerID and 92 // workflowID 93 type Set struct { 94 triggerType string 95 activeLock sync.Mutex 96 active map[profile.ID]map[string][]Trigger 97 constructTrigger func(opt map[string]interface{}) (Trigger, error) 98 } 99 100 // NewSet creates a Set with types matched to a given listener 101 func NewSet(triggerType string, ctor Constructor) *Set { 102 return &Set{ 103 activeLock: sync.Mutex{}, 104 active: map[profile.ID]map[string][]Trigger{}, 105 triggerType: triggerType, 106 constructTrigger: ctor, 107 } 108 } 109 110 // Add popuates the set with Triggers from a Source whos type matches the set's 111 // trigger type 112 func (t *Set) Add(sources ...Source) error { 113 t.activeLock.Lock() 114 defer t.activeLock.Unlock() 115 for _, s := range sources { 116 workflowID := s.WorkflowID() 117 if workflowID == "" { 118 return ErrEmptyWorkflowID 119 } 120 ownerID := s.Owner() 121 if ownerID == "" { 122 return ErrEmptyOwnerID 123 } 124 triggerOpts := s.ActiveTriggers(t.triggerType) 125 triggers := []Trigger{} 126 for _, triggerOpt := range triggerOpts { 127 t, err := t.constructTrigger(triggerOpt) 128 if err != nil { 129 return err 130 } 131 triggers = append(triggers, t) 132 } 133 // either we are not adding triggers or we are removing them 134 if len(triggers) == 0 { 135 wfs, ok := t.active[ownerID] 136 if !ok { 137 continue 138 } 139 if len(wfs) == 0 { 140 delete(t.active, ownerID) 141 continue 142 } 143 _, ok = wfs[workflowID] 144 if !ok { 145 continue 146 } 147 if ok && len(wfs) == 1 { 148 delete(t.active, ownerID) 149 continue 150 } 151 delete(t.active[ownerID], workflowID) 152 continue 153 } 154 _, ok := t.active[ownerID] 155 if !ok { 156 t.active[ownerID] = map[string][]Trigger{ 157 workflowID: triggers, 158 } 159 continue 160 } 161 t.active[ownerID][workflowID] = triggers 162 } 163 return nil 164 } 165 166 // Exists returns true if all the triggers from the Source exist in the store 167 func (t *Set) Exists(source Source) bool { 168 t.activeLock.Lock() 169 defer t.activeLock.Unlock() 170 171 ownerID := source.Owner() 172 workflowID := source.WorkflowID() 173 wids, ok := t.active[ownerID] 174 if !ok { 175 return false 176 } 177 triggers, ok := wids[workflowID] 178 if !ok { 179 return false 180 } 181 triggerOpts := source.ActiveTriggers(t.triggerType) 182 if len(triggerOpts) != len(triggers) { 183 return false 184 } 185 for i, opt := range triggerOpts { 186 sourceTrigger, err := t.constructTrigger(opt) 187 if err != nil { 188 log.Errorw("runtimeListener.TriggersExist", "error", err) 189 return false 190 } 191 if sourceTrigger.ID() != triggers[i].ID() { 192 return false 193 } 194 } 195 return true 196 } 197 198 // Active returns the map of active triggers, organized by OwnerID and WorkflowID 199 func (t *Set) Active() map[profile.ID]map[string][]Trigger { 200 return t.active 201 }