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  }