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  }