github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/rc/jobs/job.go (about) 1 // Package jobs manages background jobs that the rc is running. 2 package jobs 3 4 import ( 5 "context" 6 "errors" 7 "fmt" 8 "runtime/debug" 9 "sync" 10 "sync/atomic" 11 "time" 12 13 "github.com/google/uuid" 14 "github.com/rclone/rclone/fs" 15 "github.com/rclone/rclone/fs/accounting" 16 "github.com/rclone/rclone/fs/cache" 17 "github.com/rclone/rclone/fs/filter" 18 "github.com/rclone/rclone/fs/rc" 19 ) 20 21 // Fill in these to avoid circular dependencies 22 func init() { 23 cache.JobOnFinish = OnFinish 24 cache.JobGetJobID = GetJobID 25 } 26 27 // Job describes an asynchronous task started via the rc package 28 type Job struct { 29 mu sync.Mutex 30 ID int64 `json:"id"` 31 Group string `json:"group"` 32 StartTime time.Time `json:"startTime"` 33 EndTime time.Time `json:"endTime"` 34 Error string `json:"error"` 35 Finished bool `json:"finished"` 36 Success bool `json:"success"` 37 Duration float64 `json:"duration"` 38 Output rc.Params `json:"output"` 39 Stop func() `json:"-"` 40 listeners []*func() 41 42 // realErr is the Error before printing it as a string, it's used to return 43 // the real error to the upper application layers while still printing the 44 // string error message. 45 realErr error 46 } 47 48 // mark the job as finished 49 func (job *Job) finish(out rc.Params, err error) { 50 job.mu.Lock() 51 job.EndTime = time.Now() 52 if out == nil { 53 out = make(rc.Params) 54 } 55 job.Output = out 56 job.Duration = job.EndTime.Sub(job.StartTime).Seconds() 57 if err != nil { 58 job.realErr = err 59 job.Error = err.Error() 60 job.Success = false 61 } else { 62 job.realErr = nil 63 job.Error = "" 64 job.Success = true 65 } 66 job.Finished = true 67 68 // Notify listeners that the job is finished 69 for i := range job.listeners { 70 go (*job.listeners[i])() 71 } 72 73 job.mu.Unlock() 74 running.kickExpire() // make sure this job gets expired 75 } 76 77 func (job *Job) addListener(fn *func()) { 78 job.mu.Lock() 79 defer job.mu.Unlock() 80 job.listeners = append(job.listeners, fn) 81 } 82 83 func (job *Job) removeListener(fn *func()) { 84 job.mu.Lock() 85 defer job.mu.Unlock() 86 for i, ln := range job.listeners { 87 if ln == fn { 88 job.listeners = append(job.listeners[:i], job.listeners[i+1:]...) 89 return 90 } 91 } 92 } 93 94 // OnFinish adds listener to job that will be triggered when job is finished. 95 // It returns a function to cancel listening. 96 func (job *Job) OnFinish(fn func()) func() { 97 if job.Finished { 98 fn() 99 } else { 100 job.addListener(&fn) 101 } 102 return func() { job.removeListener(&fn) } 103 } 104 105 // run the job until completion writing the return status 106 func (job *Job) run(ctx context.Context, fn rc.Func, in rc.Params) { 107 defer func() { 108 if r := recover(); r != nil { 109 job.finish(nil, fmt.Errorf("panic received: %v \n%s", r, string(debug.Stack()))) 110 } 111 }() 112 job.finish(fn(ctx, in)) 113 } 114 115 // Jobs describes a collection of running tasks 116 type Jobs struct { 117 mu sync.RWMutex 118 jobs map[int64]*Job 119 opt *rc.Options 120 expireRunning bool 121 } 122 123 var ( 124 running = newJobs() 125 jobID atomic.Int64 126 executeID = uuid.New().String() 127 ) 128 129 // newJobs makes a new Jobs structure 130 func newJobs() *Jobs { 131 return &Jobs{ 132 jobs: map[int64]*Job{}, 133 opt: &rc.DefaultOpt, 134 } 135 } 136 137 // SetOpt sets the options when they are known 138 func SetOpt(opt *rc.Options) { 139 running.opt = opt 140 } 141 142 // SetInitialJobID allows for setting jobID before starting any jobs. 143 func SetInitialJobID(id int64) { 144 if !jobID.CompareAndSwap(0, id) { 145 panic("Setting jobID is only possible before starting any jobs") 146 } 147 } 148 149 // kickExpire makes sure Expire is running 150 func (jobs *Jobs) kickExpire() { 151 jobs.mu.Lock() 152 defer jobs.mu.Unlock() 153 if !jobs.expireRunning { 154 time.AfterFunc(jobs.opt.JobExpireInterval, jobs.Expire) 155 jobs.expireRunning = true 156 } 157 } 158 159 // Expire expires any jobs that haven't been collected 160 func (jobs *Jobs) Expire() { 161 jobs.mu.Lock() 162 defer jobs.mu.Unlock() 163 now := time.Now() 164 for ID, job := range jobs.jobs { 165 job.mu.Lock() 166 if job.Finished && now.Sub(job.EndTime) > jobs.opt.JobExpireDuration { 167 delete(jobs.jobs, ID) 168 } 169 job.mu.Unlock() 170 } 171 if len(jobs.jobs) != 0 { 172 time.AfterFunc(jobs.opt.JobExpireInterval, jobs.Expire) 173 jobs.expireRunning = true 174 } else { 175 jobs.expireRunning = false 176 } 177 } 178 179 // IDs returns the IDs of the running jobs 180 func (jobs *Jobs) IDs() (IDs []int64) { 181 jobs.mu.RLock() 182 defer jobs.mu.RUnlock() 183 IDs = []int64{} 184 for ID := range jobs.jobs { 185 IDs = append(IDs, ID) 186 } 187 return IDs 188 } 189 190 // Get a job with a given ID or nil if it doesn't exist 191 func (jobs *Jobs) Get(ID int64) *Job { 192 jobs.mu.RLock() 193 defer jobs.mu.RUnlock() 194 return jobs.jobs[ID] 195 } 196 197 // Check to see if the group is set 198 func getGroup(ctx context.Context, in rc.Params, id int64) (context.Context, string, error) { 199 group, err := in.GetString("_group") 200 if rc.NotErrParamNotFound(err) { 201 return ctx, "", err 202 } 203 delete(in, "_group") 204 if group == "" { 205 group = fmt.Sprintf("job/%d", id) 206 } 207 ctx = accounting.WithStatsGroup(ctx, group) 208 return ctx, group, nil 209 } 210 211 // See if _async is set returning a boolean and a possible new context 212 func getAsync(ctx context.Context, in rc.Params) (context.Context, bool, error) { 213 isAsync, err := in.GetBool("_async") 214 if rc.NotErrParamNotFound(err) { 215 return ctx, false, err 216 } 217 delete(in, "_async") // remove the async parameter after parsing 218 if isAsync { 219 // unlink this job from the current context 220 ctx = context.Background() 221 } 222 return ctx, isAsync, nil 223 } 224 225 // See if _config is set and if so adjust ctx to include it 226 func getConfig(ctx context.Context, in rc.Params) (context.Context, error) { 227 if _, ok := in["_config"]; !ok { 228 return ctx, nil 229 } 230 ctx, ci := fs.AddConfig(ctx) 231 err := in.GetStruct("_config", ci) 232 if err != nil { 233 return ctx, err 234 } 235 delete(in, "_config") // remove the parameter 236 return ctx, nil 237 } 238 239 // See if _filter is set and if so adjust ctx to include it 240 func getFilter(ctx context.Context, in rc.Params) (context.Context, error) { 241 if _, ok := in["_filter"]; !ok { 242 return ctx, nil 243 } 244 // Copy of the current filter options 245 opt := filter.GetConfig(ctx).Opt 246 // Update the options from the parameter 247 err := in.GetStruct("_filter", &opt) 248 if err != nil { 249 return ctx, err 250 } 251 fi, err := filter.NewFilter(&opt) 252 if err != nil { 253 return ctx, err 254 } 255 ctx = filter.ReplaceConfig(ctx, fi) 256 delete(in, "_filter") // remove the parameter 257 return ctx, nil 258 } 259 260 type jobKeyType struct{} 261 262 // Key for adding jobs to ctx 263 var jobKey = jobKeyType{} 264 265 // NewJob creates a Job and executes it, possibly in the background if _async is set 266 func (jobs *Jobs) NewJob(ctx context.Context, fn rc.Func, in rc.Params) (job *Job, out rc.Params, err error) { 267 id := jobID.Add(1) 268 in = in.Copy() // copy input so we can change it 269 270 ctx, isAsync, err := getAsync(ctx, in) 271 if err != nil { 272 return nil, nil, err 273 } 274 275 ctx, err = getConfig(ctx, in) 276 if err != nil { 277 return nil, nil, err 278 } 279 280 ctx, err = getFilter(ctx, in) 281 if err != nil { 282 return nil, nil, err 283 } 284 285 ctx, group, err := getGroup(ctx, in, id) 286 if err != nil { 287 return nil, nil, err 288 } 289 290 ctx, cancel := context.WithCancel(ctx) 291 stop := func() { 292 cancel() 293 // Wait for cancel to propagate before returning. 294 <-ctx.Done() 295 } 296 job = &Job{ 297 ID: id, 298 Group: group, 299 StartTime: time.Now(), 300 Stop: stop, 301 } 302 303 jobs.mu.Lock() 304 jobs.jobs[job.ID] = job 305 jobs.mu.Unlock() 306 307 // Add the job to the context 308 ctx = context.WithValue(ctx, jobKey, job) 309 310 if isAsync { 311 go job.run(ctx, fn, in) 312 out = make(rc.Params) 313 out["jobid"] = job.ID 314 err = nil 315 } else { 316 job.run(ctx, fn, in) 317 out = job.Output 318 err = job.realErr 319 } 320 return job, out, err 321 } 322 323 // NewJob creates a Job and executes it on the global job queue, 324 // possibly in the background if _async is set 325 func NewJob(ctx context.Context, fn rc.Func, in rc.Params) (job *Job, out rc.Params, err error) { 326 return running.NewJob(ctx, fn, in) 327 } 328 329 // OnFinish adds listener to jobid that will be triggered when job is finished. 330 // It returns a function to cancel listening. 331 func OnFinish(jobID int64, fn func()) (func(), error) { 332 job := running.Get(jobID) 333 if job == nil { 334 return func() {}, errors.New("job not found") 335 } 336 return job.OnFinish(fn), nil 337 } 338 339 // GetJob gets the Job from the context if possible 340 func GetJob(ctx context.Context) (job *Job, ok bool) { 341 job, ok = ctx.Value(jobKey).(*Job) 342 return job, ok 343 } 344 345 // GetJobID gets the Job from the context if possible 346 func GetJobID(ctx context.Context) (jobID int64, ok bool) { 347 job, ok := GetJob(ctx) 348 if !ok { 349 return -1, ok 350 } 351 return job.ID, true 352 } 353 354 func init() { 355 rc.Add(rc.Call{ 356 Path: "job/status", 357 Fn: rcJobStatus, 358 Title: "Reads the status of the job ID", 359 Help: `Parameters: 360 361 - jobid - id of the job (integer). 362 363 Results: 364 365 - finished - boolean 366 - duration - time in seconds that the job ran for 367 - endTime - time the job finished (e.g. "2018-10-26T18:50:20.528746884+01:00") 368 - error - error from the job or empty string for no error 369 - finished - boolean whether the job has finished or not 370 - id - as passed in above 371 - startTime - time the job started (e.g. "2018-10-26T18:50:20.528336039+01:00") 372 - success - boolean - true for success false otherwise 373 - output - output of the job as would have been returned if called synchronously 374 - progress - output of the progress related to the underlying job 375 `, 376 }) 377 } 378 379 // Returns the status of a job 380 func rcJobStatus(ctx context.Context, in rc.Params) (out rc.Params, err error) { 381 jobID, err := in.GetInt64("jobid") 382 if err != nil { 383 return nil, err 384 } 385 job := running.Get(jobID) 386 if job == nil { 387 return nil, errors.New("job not found") 388 } 389 job.mu.Lock() 390 defer job.mu.Unlock() 391 out = make(rc.Params) 392 err = rc.Reshape(&out, job) 393 if err != nil { 394 return nil, fmt.Errorf("reshape failed in job status: %w", err) 395 } 396 return out, nil 397 } 398 399 func init() { 400 rc.Add(rc.Call{ 401 Path: "job/list", 402 Fn: rcJobList, 403 Title: "Lists the IDs of the running jobs", 404 Help: `Parameters: None. 405 406 Results: 407 408 - executeId - string id of rclone executing (change after restart) 409 - jobids - array of integer job ids (starting at 1 on each restart) 410 `, 411 }) 412 } 413 414 // Returns list of job ids. 415 func rcJobList(ctx context.Context, in rc.Params) (out rc.Params, err error) { 416 out = make(rc.Params) 417 out["jobids"] = running.IDs() 418 out["executeId"] = executeID 419 return out, nil 420 } 421 422 func init() { 423 rc.Add(rc.Call{ 424 Path: "job/stop", 425 Fn: rcJobStop, 426 Title: "Stop the running job", 427 Help: `Parameters: 428 429 - jobid - id of the job (integer). 430 `, 431 }) 432 } 433 434 // Stops the running job. 435 func rcJobStop(ctx context.Context, in rc.Params) (out rc.Params, err error) { 436 jobID, err := in.GetInt64("jobid") 437 if err != nil { 438 return nil, err 439 } 440 job := running.Get(jobID) 441 if job == nil { 442 return nil, errors.New("job not found") 443 } 444 job.mu.Lock() 445 defer job.mu.Unlock() 446 out = make(rc.Params) 447 job.Stop() 448 return out, nil 449 } 450 451 func init() { 452 rc.Add(rc.Call{ 453 Path: "job/stopgroup", 454 Fn: rcGroupStop, 455 Title: "Stop all running jobs in a group", 456 Help: `Parameters: 457 458 - group - name of the group (string). 459 `, 460 }) 461 } 462 463 // Stops all running jobs in a group 464 func rcGroupStop(ctx context.Context, in rc.Params) (out rc.Params, err error) { 465 group, err := in.GetString("group") 466 if err != nil { 467 return nil, err 468 } 469 running.mu.RLock() 470 defer running.mu.RUnlock() 471 for _, job := range running.jobs { 472 if job.Group == group { 473 job.mu.Lock() 474 job.Stop() 475 job.mu.Unlock() 476 } 477 } 478 out = make(rc.Params) 479 return out, nil 480 }