go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/_motor/providers/os/events/jobmanager.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package events 5 6 import ( 7 "errors" 8 "sync" 9 "time" 10 11 "go.mondoo.com/cnquery/motor/providers/os" 12 13 uuid "github.com/gofrs/uuid" 14 "github.com/rs/zerolog/log" 15 "go.mondoo.com/cnquery/motor/providers" 16 ) 17 18 // the job state 19 type JobState int32 20 21 const ( 22 // pending is the default state 23 Job_PENDING JobState = 0 24 Job_RUNNING JobState = 1 25 Job_TERMINATED JobState = 2 26 ) 27 28 type Job struct { 29 ID string 30 31 Runnable func(provider os.OperatingSystemProvider) (providers.Observable, error) 32 Callback []func(providers.Observable) 33 34 State JobState 35 ScheduledFor time.Time 36 Interval time.Duration 37 // -1 means infinity 38 Repeat int32 39 40 Metrics struct { 41 RunAt time.Time 42 Duration time.Duration 43 Count int32 44 Errors int32 45 Successes int32 46 } 47 } 48 49 func (j *Job) sanitize() error { 50 // ensure we have an id 51 if len(j.ID) == 0 { 52 j.ID = uuid.Must(uuid.NewV4()).String() 53 } 54 55 // verify that the interval is set for the job, otherwise overwrite with the default 56 if j.Interval == 0 { 57 j.Interval = time.Duration(60 * time.Second) 58 } 59 60 // verify that we have the required things for a schedule 61 if j.ScheduledFor.Before(time.Now().Add(time.Duration(-10 * time.Second))) { 62 return errors.New("schedule for the past") 63 } 64 65 if j.Runnable == nil { 66 return errors.New("no runnable defined") 67 } 68 69 if len(j.Callback) == 0 { 70 return errors.New("no callback defined") 71 } 72 73 return nil 74 } 75 76 func (j *Job) SetInfinity() { 77 j.Repeat = -1 78 } 79 80 func (j *Job) isPending() bool { 81 return j.State == Job_PENDING 82 } 83 84 func NewJobManager(provider os.OperatingSystemProvider) *JobManager { 85 jm := &JobManager{provider: provider, jobs: &Jobs{}} 86 jm.jobSelectionMutex = &sync.Mutex{} 87 jm.quit = make(chan chan struct{}) 88 jm.Serve() 89 return jm 90 } 91 92 type JobManagerMetrics struct { 93 Jobs int 94 } 95 96 // Jobs is a map to store all jobs 97 type Jobs struct{ sync.Map } 98 99 // Store a new job 100 func (c *Jobs) Store(k string, v *Job) { 101 c.Map.Store(k, v) 102 } 103 104 // Load a job 105 func (c *Jobs) Load(k string) (*Job, bool) { 106 res, ok := c.Map.Load(k) 107 if !ok { 108 return nil, ok 109 } 110 return res.(*Job), ok 111 } 112 113 func (c *Jobs) Range(f func(string, *Job) bool) { 114 c.Map.Range(func(key interface{}, value interface{}) bool { 115 return f(key.(string), value.(*Job)) 116 }) 117 } 118 119 func (c *Jobs) Len() int { 120 i := 0 121 c.Range(func(k string, j *Job) bool { 122 i++ 123 return true 124 }) 125 return i 126 } 127 128 func (c *Jobs) Delete(k string) { 129 c.Map.Delete(k) 130 } 131 132 type JobManager struct { 133 provider os.OperatingSystemProvider 134 quit chan chan struct{} 135 jobSelectionMutex *sync.Mutex 136 jobs *Jobs 137 jobMetrics JobManagerMetrics 138 } 139 140 // Schedule stores the job in the run list and sanitize the job before execution 141 func (jm *JobManager) Schedule(job *Job) (string, error) { 142 // ensure all defaults are set 143 err := job.sanitize() 144 if err != nil { 145 return "", err 146 } 147 148 log.Trace().Str("jobid", job.ID).Msg("motor.job> schedule new job") 149 150 // store job, with a mutex 151 jm.jobs.Store(job.ID, job) 152 153 // return job id 154 return job.ID, nil 155 } 156 157 func (jm *JobManager) GetJob(jobid string) (*Job, error) { 158 job, ok := jm.jobs.Load(jobid) 159 if !ok { 160 return nil, errors.New("job " + jobid + " does not exist") 161 } 162 return job, nil 163 } 164 165 func (jm *JobManager) Delete(jobid string) { 166 log.Trace().Str("jobid", jobid).Msg("motor.job> delete job") 167 jm.jobs.Delete(jobid) 168 } 169 170 func (jm *JobManager) Metrics() *JobManagerMetrics { 171 jm.jobMetrics.Jobs = jm.jobs.Len() 172 return &jm.jobMetrics 173 } 174 175 // Serve creates a goroutine and runs jobs in the background 176 func (jm *JobManager) Serve() { 177 // create a new channel and starte a new go routine 178 go func() { 179 for { 180 select { 181 case doneChan := <-jm.quit: 182 close(doneChan) 183 return 184 default: 185 // fetch job 186 job, err := jm.nextJob() 187 188 if err == nil { 189 // run job 190 jm.Run(job) 191 192 // if repeat is 0 and it is not the last iteration of a reoccuring task, 193 // we need to remove the job 194 if job.Repeat == 0 && job.State == Job_TERMINATED { 195 jm.Delete(job.ID) 196 } 197 } 198 199 // TODO: wake up, when new jobs come in 200 time.Sleep(100 * time.Millisecond) 201 } 202 } 203 }() 204 } 205 206 func (jm *JobManager) Run(job *Job) { 207 log.Trace().Str("jobid", job.ID).Msg("motor.job> run job") 208 job.Metrics.RunAt = time.Now() 209 210 // execute job 211 observable, err := job.Runnable(jm.provider) 212 213 // update metrics 214 job.Metrics.Count = job.Metrics.Count + 1 215 if err != nil { 216 job.Metrics.Errors = job.Metrics.Errors + 1 217 } else { 218 job.Metrics.Successes = job.Metrics.Successes + 1 219 } 220 221 // determine the next run or delete the job 222 if job.Repeat != 0 { 223 job.ScheduledFor = time.Now().Add(job.Interval) 224 log.Trace().Str("jobid", job.ID).Time("time", job.ScheduledFor).Msg("motor.job> scheduled job for the future") 225 job.State = Job_PENDING 226 } else { 227 log.Trace().Str("jobid", job.ID).Msg("motor.job> last run for this job, yeah") 228 job.State = Job_TERMINATED 229 } 230 231 // if we have a positive repeat, we need to decrement 232 if job.Repeat > 0 { 233 job.Repeat = job.Repeat - 1 234 } 235 236 // calc duration 237 job.Metrics.Duration = time.Now().Sub(job.Metrics.RunAt) 238 log.Trace().Str("jobid", job.ID).Msg("motor.job> completed") 239 240 // send observable to all subscribers 241 // since this call is synchronous in the same go routine, we need to do this as the last step, to ensure 242 // all job planning is completed before a potential canceling comes in. 243 log.Trace().Str("jobid", job.ID).Msg("motor.job> call subscriber") 244 for _, subscriber := range job.Callback { 245 subscriber(observable) 246 } 247 } 248 249 // nextJob looks for the oldest job and does that one first 250 func (jm *JobManager) nextJob() (*Job, error) { 251 // use lock to prevent concurrent access on that list 252 var oldestJob *Job 253 oldest := time.Date(9999, 1, 1, 0, 0, 0, 0, time.UTC) 254 255 // iterate over list of jobs of pending jobs and find the oldest one 256 jm.jobSelectionMutex.Lock() 257 now := time.Now() 258 259 jm.jobs.Range(func(k string, job *Job) bool { 260 if job.State == Job_PENDING && oldest.After(job.ScheduledFor) && job.ScheduledFor.Before(now) { 261 oldest = job.ScheduledFor 262 oldestJob = job 263 } 264 return true 265 }) 266 267 // set the job to running to ensure other parallel go routines do not fetch the same job 268 if oldestJob != nil { 269 oldestJob.State = Job_RUNNING 270 } 271 jm.jobSelectionMutex.Unlock() 272 273 if oldestJob == nil { 274 return nil, errors.New("no job available") 275 } 276 277 // extrats the next run from the nextruns 278 return oldestJob, nil 279 } 280 281 // TeadDown deletes all 282 func (jm *JobManager) TearDown() { 283 log.Trace().Msg("motor.job> tear down") 284 // ensures the go routines are canceled 285 done := make(chan struct{}) 286 jm.quit <- done 287 <-done 288 }