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  }