github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/job/scheduler.go (about) 1 package job 2 3 import ( 4 "context" 5 "encoding/json" 6 "time" 7 8 "github.com/cozy/cozy-stack/model/permission" 9 "github.com/cozy/cozy-stack/pkg/consts" 10 "github.com/cozy/cozy-stack/pkg/couchdb" 11 "github.com/cozy/cozy-stack/pkg/couchdb/mango" 12 "github.com/cozy/cozy-stack/pkg/metadata" 13 "github.com/cozy/cozy-stack/pkg/prefixer" 14 "github.com/cozy/cozy-stack/pkg/realtime" 15 ) 16 17 // DocTypeVersionTrigger represents the doctype version. Each time this document 18 // structure is modified, update this value 19 const DocTypeVersionTrigger = "1" 20 21 // The different modes for combining job requests when debounced. 22 const ( 23 keepOriginalRequest = "original" 24 suppressPayload = "recreate" 25 appendPayload = "append" 26 ) 27 28 type ( 29 // Trigger interface is used to represent a trigger. 30 Trigger interface { 31 prefixer.Prefixer 32 permission.Fetcher 33 Type() string 34 Infos() *TriggerInfos 35 // Schedule should return a channel on which the trigger can send job 36 // requests when it decides to. 37 Schedule() <-chan *JobRequest 38 // Unschedule should be used to clean the trigger states and should close 39 // the returns jobs channel. 40 Unschedule() 41 CombineRequest() string 42 } 43 44 // Scheduler interface is used to represent a scheduler that is responsible 45 // to listen respond to triggers jobs requests and send them to the broker. 46 Scheduler interface { 47 StartScheduler(broker Broker) error 48 ShutdownScheduler(ctx context.Context) error 49 PollScheduler(now int64) error 50 AddTrigger(trigger Trigger) error 51 GetTrigger(db prefixer.Prefixer, id string) (Trigger, error) 52 UpdateMessage(db prefixer.Prefixer, trigger Trigger, message json.RawMessage) error 53 UpdateCron(db prefixer.Prefixer, trigger Trigger, arguments string) error 54 DeleteTrigger(db prefixer.Prefixer, id string) error 55 GetAllTriggers(db prefixer.Prefixer) ([]Trigger, error) 56 HasTrigger(db prefixer.Prefixer, infos TriggerInfos) bool 57 CleanRedis() error 58 RebuildRedis(db prefixer.Prefixer) error 59 } 60 61 // TriggerInfos is a struct containing all the options of a trigger. 62 TriggerInfos struct { 63 TID string `json:"_id,omitempty"` 64 TRev string `json:"_rev,omitempty"` 65 Cluster int `json:"couch_cluster,omitempty"` 66 Domain string `json:"domain"` 67 Prefix string `json:"prefix,omitempty"` 68 Type string `json:"type"` 69 WorkerType string `json:"worker"` 70 Arguments string `json:"arguments"` 71 Debounce string `json:"debounce"` 72 Options *JobOptions `json:"options"` 73 Message Message `json:"message"` 74 CurrentState *TriggerState `json:"current_state,omitempty"` 75 Metadata *metadata.CozyMetadata `json:"cozyMetadata,omitempty"` 76 } 77 78 // TriggerState represent the current state of the trigger 79 TriggerState struct { 80 TID string `json:"trigger_id"` 81 Status State `json:"status"` 82 LastSuccess *time.Time `json:"last_success,omitempty"` 83 LastSuccessfulJobID string `json:"last_successful_job_id,omitempty"` 84 LastExecution *time.Time `json:"last_execution,omitempty"` 85 LastExecutedJobID string `json:"last_executed_job_id,omitempty"` 86 LastFailure *time.Time `json:"last_failure,omitempty"` 87 LastFailedJobID string `json:"last_failed_job_id,omitempty"` 88 LastError string `json:"last_error,omitempty"` 89 LastManualExecution *time.Time `json:"last_manual_execution,omitempty"` 90 LastManualJobID string `json:"last_manual_job_id,omitempty"` 91 } 92 ) 93 94 // DBCluster implements the prefixer.Prefixer interface. 95 func (t *TriggerInfos) DBCluster() int { 96 return t.Cluster 97 } 98 99 // DBPrefix implements the prefixer.Prefixer interface. 100 func (t *TriggerInfos) DBPrefix() string { 101 if t.Prefix != "" { 102 return t.Prefix 103 } 104 return t.Domain 105 } 106 107 // DomainName implements the prefixer.Prefixer interface. 108 func (t *TriggerInfos) DomainName() string { 109 return t.Domain 110 } 111 112 func (t *TriggerInfos) IsKonnectorTrigger() bool { 113 return t.WorkerType == "konnector" || t.WorkerType == "client" 114 } 115 116 // NewTrigger creates the trigger associates with the specified trigger 117 // options. 118 func NewTrigger(db prefixer.Prefixer, infos TriggerInfos, data interface{}) (Trigger, error) { 119 var msg Message 120 var err error 121 if data != nil { 122 msg, err = NewMessage(data) 123 if err != nil { 124 return nil, err 125 } 126 infos.Message = msg 127 } 128 infos.Cluster = db.DBCluster() 129 infos.Prefix = db.DBPrefix() 130 infos.Domain = db.DomainName() 131 132 // Adding metadata 133 md := metadata.New() 134 md.DocTypeVersion = DocTypeVersionTrigger 135 if infos.Metadata == nil { 136 infos.Metadata = md 137 } else { 138 infos.Metadata.EnsureCreatedFields(md) 139 } 140 141 return fromTriggerInfos(&infos) 142 } 143 144 func fromTriggerInfos(infos *TriggerInfos) (Trigger, error) { 145 switch infos.Type { 146 case "@at": 147 return NewAtTrigger(infos) 148 case "@in": 149 return NewInTrigger(infos) 150 case "@hourly": 151 return NewHourlyTrigger(infos) 152 case "@daily": 153 return NewDailyTrigger(infos) 154 case "@weekly": 155 return NewWeeklyTrigger(infos) 156 case "@monthly": 157 return NewMonthlyTrigger(infos) 158 case "@cron": 159 return NewCronTrigger(infos) 160 case "@every": 161 return NewEveryTrigger(infos) 162 case "@event": 163 return NewEventTrigger(infos) 164 case "@webhook": 165 return NewWebhookTrigger(infos) 166 case "@client": 167 return NewClientTrigger(infos) 168 default: 169 return nil, ErrUnknownTrigger 170 } 171 } 172 173 // ID implements the couchdb.Doc interface 174 func (t *TriggerInfos) ID() string { return t.TID } 175 176 // Rev implements the couchdb.Doc interface 177 func (t *TriggerInfos) Rev() string { return t.TRev } 178 179 // DocType implements the couchdb.Doc interface 180 func (t *TriggerInfos) DocType() string { return consts.Triggers } 181 182 // Clone implements the couchdb.Doc interface 183 func (t *TriggerInfos) Clone() couchdb.Doc { 184 cloned := *t 185 if t.Options != nil { 186 tmp := *t.Options 187 cloned.Options = &tmp 188 } 189 190 if t.Message != nil { 191 cloned.Message = make([]byte, len(t.Message)) 192 copy(cloned.Message, t.Message) 193 } 194 195 if t.CurrentState != nil { 196 tmp := *t.CurrentState 197 cloned.CurrentState = &tmp 198 } 199 200 if t.Metadata != nil { 201 cloned.Metadata = t.Metadata.Clone() 202 } 203 204 return &cloned 205 } 206 207 // JobRequest returns a job request associated with the scheduler informations. 208 func (t *TriggerInfos) JobRequest() *JobRequest { 209 trigger, _ := fromTriggerInfos(t) 210 return &JobRequest{ 211 WorkerType: t.WorkerType, 212 TriggerID: t.ID(), 213 Trigger: trigger, 214 Message: t.Message, 215 Options: t.Options, 216 } 217 } 218 219 // JobRequestWithEvent returns a job request associated with the scheduler 220 // informations associated to the specified realtime event. 221 func (t *TriggerInfos) JobRequestWithEvent(event *realtime.Event) (*JobRequest, error) { 222 evt, err := NewEvent(event) 223 if err != nil { 224 return nil, err 225 } 226 req := t.JobRequest() 227 req.Event = evt 228 return req, nil 229 } 230 231 // SetID implements the couchdb.Doc interface 232 func (t *TriggerInfos) SetID(id string) { t.TID = id } 233 234 // SetRev implements the couchdb.Doc interface 235 func (t *TriggerInfos) SetRev(rev string) { t.TRev = rev } 236 237 // Fetch implements the permission.Fetcher interface 238 func (t *TriggerInfos) Fetch(field string) []string { 239 switch field { 240 case "worker": 241 return []string{t.WorkerType} 242 default: 243 return nil 244 } 245 } 246 247 func createTrigger(t Trigger) error { 248 infos := t.Infos() 249 if infos.TID != "" { 250 return couchdb.CreateNamedDoc(t, infos) 251 } 252 return couchdb.CreateDoc(t, infos) 253 } 254 255 // GetJobs returns the jobs launched by the given trigger. 256 func GetJobs(db prefixer.Prefixer, triggerID string, limit int) ([]*Job, error) { 257 if limit <= 0 || limit > 50 { 258 limit = 50 259 } 260 var jobs []*Job 261 req := &couchdb.FindRequest{ 262 UseIndex: "by-trigger-id", 263 Selector: mango.Equal("trigger_id", triggerID), 264 Sort: mango.SortBy{ 265 {Field: "trigger_id", Direction: mango.Desc}, 266 {Field: "queued_at", Direction: mango.Desc}, 267 }, 268 Limit: limit, 269 } 270 err := couchdb.FindDocs(db, consts.Jobs, req, &jobs) 271 if err != nil { 272 return nil, err 273 } 274 return jobs, nil 275 } 276 277 // GetTriggerState returns the state of the trigger, calculated from the last 278 // launched jobs. 279 func GetTriggerState(db prefixer.Prefixer, triggerID string) (*TriggerState, error) { 280 js, err := GetJobs(db, triggerID, 0) 281 if err != nil { 282 return nil, err 283 } 284 285 var state TriggerState 286 287 state.Status = Done 288 state.TID = triggerID 289 290 // jobs are ordered from the oldest to most recent job 291 for i := len(js) - 1; i >= 0; i-- { 292 j := js[i] 293 startedAt := &j.StartedAt 294 295 state.Status = j.State 296 state.LastExecution = startedAt 297 state.LastExecutedJobID = j.ID() 298 299 if j.Manual { 300 state.LastManualExecution = startedAt 301 state.LastManualJobID = j.ID() 302 } 303 304 switch j.State { 305 case Errored: 306 state.LastFailure = startedAt 307 state.LastFailedJobID = j.ID() 308 state.LastError = j.Error 309 case Done: 310 state.LastSuccess = startedAt 311 state.LastSuccessfulJobID = j.ID() 312 default: 313 // skip any job that is not done or errored 314 continue 315 } 316 } 317 318 return &state, nil 319 } 320 321 var _ couchdb.Doc = &TriggerInfos{} 322 var _ permission.Fetcher = &TriggerInfos{}