github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/fs/rc/job.go (about) 1 // Manage background jobs that the rc is running 2 3 package rc 4 5 import ( 6 "context" 7 "sync" 8 "sync/atomic" 9 "time" 10 11 "github.com/ncw/rclone/fs" 12 "github.com/pkg/errors" 13 ) 14 15 // Job describes a asynchronous task started via the rc package 16 type Job struct { 17 mu sync.Mutex 18 ID int64 `json:"id"` 19 StartTime time.Time `json:"startTime"` 20 EndTime time.Time `json:"endTime"` 21 Error string `json:"error"` 22 Finished bool `json:"finished"` 23 Success bool `json:"success"` 24 Duration float64 `json:"duration"` 25 Output Params `json:"output"` 26 Stop func() `json:"-"` 27 } 28 29 // Jobs describes a collection of running tasks 30 type Jobs struct { 31 mu sync.RWMutex 32 jobs map[int64]*Job 33 expireInterval time.Duration 34 expireRunning bool 35 } 36 37 var ( 38 running = newJobs() 39 jobID = int64(0) 40 ) 41 42 // newJobs makes a new Jobs structure 43 func newJobs() *Jobs { 44 return &Jobs{ 45 jobs: map[int64]*Job{}, 46 expireInterval: fs.Config.RcJobExpireInterval, 47 } 48 } 49 50 // kickExpire makes sure Expire is running 51 func (jobs *Jobs) kickExpire() { 52 jobs.mu.Lock() 53 defer jobs.mu.Unlock() 54 if !jobs.expireRunning { 55 time.AfterFunc(jobs.expireInterval, jobs.Expire) 56 jobs.expireRunning = true 57 } 58 } 59 60 // Expire expires any jobs that haven't been collected 61 func (jobs *Jobs) Expire() { 62 jobs.mu.Lock() 63 defer jobs.mu.Unlock() 64 now := time.Now() 65 for ID, job := range jobs.jobs { 66 job.mu.Lock() 67 if job.Finished && now.Sub(job.EndTime) > fs.Config.RcJobExpireDuration { 68 delete(jobs.jobs, ID) 69 } 70 job.mu.Unlock() 71 } 72 if len(jobs.jobs) != 0 { 73 time.AfterFunc(jobs.expireInterval, jobs.Expire) 74 jobs.expireRunning = true 75 } else { 76 jobs.expireRunning = false 77 } 78 } 79 80 // IDs returns the IDs of the running jobs 81 func (jobs *Jobs) IDs() (IDs []int64) { 82 jobs.mu.RLock() 83 defer jobs.mu.RUnlock() 84 IDs = []int64{} 85 for ID := range jobs.jobs { 86 IDs = append(IDs, ID) 87 } 88 return IDs 89 } 90 91 // Get a job with a given ID or nil if it doesn't exist 92 func (jobs *Jobs) Get(ID int64) *Job { 93 jobs.mu.RLock() 94 defer jobs.mu.RUnlock() 95 return jobs.jobs[ID] 96 } 97 98 // mark the job as finished 99 func (job *Job) finish(out Params, err error) { 100 job.mu.Lock() 101 job.EndTime = time.Now() 102 if out == nil { 103 out = make(Params) 104 } 105 job.Output = out 106 job.Duration = job.EndTime.Sub(job.StartTime).Seconds() 107 if err != nil { 108 job.Error = err.Error() 109 job.Success = false 110 } else { 111 job.Error = "" 112 job.Success = true 113 } 114 job.Finished = true 115 job.mu.Unlock() 116 running.kickExpire() // make sure this job gets expired 117 } 118 119 // run the job until completion writing the return status 120 func (job *Job) run(ctx context.Context, fn Func, in Params) { 121 defer func() { 122 if r := recover(); r != nil { 123 job.finish(nil, errors.Errorf("panic received: %v", r)) 124 } 125 }() 126 job.finish(fn(ctx, in)) 127 } 128 129 // NewJob start a new Job off 130 func (jobs *Jobs) NewJob(fn Func, in Params) *Job { 131 ctx, cancel := context.WithCancel(context.Background()) 132 stop := func() { 133 cancel() 134 // Wait for cancel to propagate before returning. 135 <-ctx.Done() 136 } 137 job := &Job{ 138 ID: atomic.AddInt64(&jobID, 1), 139 StartTime: time.Now(), 140 Stop: stop, 141 } 142 go job.run(ctx, fn, in) 143 jobs.mu.Lock() 144 jobs.jobs[job.ID] = job 145 jobs.mu.Unlock() 146 return job 147 148 } 149 150 // StartJob starts a new job and returns a Param suitable for output 151 func StartJob(fn Func, in Params) (Params, error) { 152 job := running.NewJob(fn, in) 153 out := make(Params) 154 out["jobid"] = job.ID 155 return out, nil 156 } 157 158 func init() { 159 Add(Call{ 160 Path: "job/status", 161 Fn: rcJobStatus, 162 Title: "Reads the status of the job ID", 163 Help: `Parameters 164 - jobid - id of the job (integer) 165 166 Results 167 - finished - boolean 168 - duration - time in seconds that the job ran for 169 - endTime - time the job finished (eg "2018-10-26T18:50:20.528746884+01:00") 170 - error - error from the job or empty string for no error 171 - finished - boolean whether the job has finished or not 172 - id - as passed in above 173 - startTime - time the job started (eg "2018-10-26T18:50:20.528336039+01:00") 174 - success - boolean - true for success false otherwise 175 - output - output of the job as would have been returned if called synchronously 176 - progress - output of the progress related to the underlying job 177 `, 178 }) 179 } 180 181 // Returns the status of a job 182 func rcJobStatus(ctx context.Context, in Params) (out Params, err error) { 183 jobID, err := in.GetInt64("jobid") 184 if err != nil { 185 return nil, err 186 } 187 job := running.Get(jobID) 188 if job == nil { 189 return nil, errors.New("job not found") 190 } 191 job.mu.Lock() 192 defer job.mu.Unlock() 193 out = make(Params) 194 err = Reshape(&out, job) 195 if err != nil { 196 return nil, errors.Wrap(err, "reshape failed in job status") 197 } 198 return out, nil 199 } 200 201 func init() { 202 Add(Call{ 203 Path: "job/list", 204 Fn: rcJobList, 205 Title: "Lists the IDs of the running jobs", 206 Help: `Parameters - None 207 208 Results 209 - jobids - array of integer job ids 210 `, 211 }) 212 } 213 214 // Returns list of job ids. 215 func rcJobList(ctx context.Context, in Params) (out Params, err error) { 216 out = make(Params) 217 out["jobids"] = running.IDs() 218 return out, nil 219 } 220 221 func init() { 222 Add(Call{ 223 Path: "job/stop", 224 Fn: rcJobStop, 225 Title: "Stop the running job", 226 Help: `Parameters 227 - jobid - id of the job (integer) 228 `, 229 }) 230 } 231 232 // Stops the running job. 233 func rcJobStop(ctx context.Context, in Params) (out Params, err error) { 234 jobID, err := in.GetInt64("jobid") 235 if err != nil { 236 return nil, err 237 } 238 job := running.Get(jobID) 239 if job == nil { 240 return nil, errors.New("job not found") 241 } 242 job.mu.Lock() 243 defer job.mu.Unlock() 244 out = make(Params) 245 job.Stop() 246 return out, nil 247 }