github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/rc/jobs/job_test.go (about)

     1  package jobs
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"runtime"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/rclone/rclone/fs"
    11  	"github.com/rclone/rclone/fs/accounting"
    12  	"github.com/rclone/rclone/fs/filter"
    13  	"github.com/rclone/rclone/fs/rc"
    14  	"github.com/rclone/rclone/fs/rc/rcflags"
    15  	"github.com/rclone/rclone/fstest/testy"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  )
    19  
    20  func TestNewJobs(t *testing.T) {
    21  	jobs := newJobs()
    22  	assert.Equal(t, 0, len(jobs.jobs))
    23  }
    24  
    25  func TestJobsKickExpire(t *testing.T) {
    26  	testy.SkipUnreliable(t)
    27  	jobs := newJobs()
    28  	jobs.opt.JobExpireInterval = time.Millisecond
    29  	assert.Equal(t, false, jobs.expireRunning)
    30  	jobs.kickExpire()
    31  	jobs.mu.Lock()
    32  	assert.Equal(t, true, jobs.expireRunning)
    33  	jobs.mu.Unlock()
    34  	time.Sleep(10 * time.Millisecond)
    35  	jobs.mu.Lock()
    36  	assert.Equal(t, false, jobs.expireRunning)
    37  	jobs.mu.Unlock()
    38  }
    39  
    40  func TestJobsExpire(t *testing.T) {
    41  	testy.SkipUnreliable(t)
    42  	ctx := context.Background()
    43  	wait := make(chan struct{})
    44  	jobs := newJobs()
    45  	jobs.opt.JobExpireInterval = time.Millisecond
    46  	assert.Equal(t, false, jobs.expireRunning)
    47  	var gotJobID int64
    48  	var gotJob *Job
    49  	job, out, err := jobs.NewJob(ctx, func(ctx context.Context, in rc.Params) (rc.Params, error) {
    50  		defer close(wait)
    51  		var ok bool
    52  		gotJobID, ok = GetJobID(ctx)
    53  		assert.True(t, ok)
    54  		gotJob, ok = GetJob(ctx)
    55  		assert.True(t, ok)
    56  		return in, nil
    57  	}, rc.Params{"_async": true})
    58  	require.NoError(t, err)
    59  	assert.Equal(t, 1, len(out))
    60  	<-wait
    61  	assert.Equal(t, job.ID, gotJobID, "check can get JobID from ctx")
    62  	assert.Equal(t, job, gotJob, "check can get Job from ctx")
    63  	assert.Equal(t, 1, len(jobs.jobs))
    64  	jobs.Expire()
    65  	assert.Equal(t, 1, len(jobs.jobs))
    66  	jobs.mu.Lock()
    67  	job.mu.Lock()
    68  	job.EndTime = time.Now().Add(-rcflags.Opt.JobExpireDuration - 60*time.Second)
    69  	assert.Equal(t, true, jobs.expireRunning)
    70  	job.mu.Unlock()
    71  	jobs.mu.Unlock()
    72  	time.Sleep(250 * time.Millisecond)
    73  	jobs.mu.Lock()
    74  	assert.Equal(t, false, jobs.expireRunning)
    75  	assert.Equal(t, 0, len(jobs.jobs))
    76  	jobs.mu.Unlock()
    77  }
    78  
    79  var noopFn = func(ctx context.Context, in rc.Params) (rc.Params, error) {
    80  	return nil, nil
    81  }
    82  
    83  func TestJobsIDs(t *testing.T) {
    84  	ctx := context.Background()
    85  	jobs := newJobs()
    86  	job1, _, err := jobs.NewJob(ctx, noopFn, rc.Params{"_async": true})
    87  	require.NoError(t, err)
    88  	job2, _, err := jobs.NewJob(ctx, noopFn, rc.Params{"_async": true})
    89  	require.NoError(t, err)
    90  	wantIDs := []int64{job1.ID, job2.ID}
    91  	gotIDs := jobs.IDs()
    92  	require.Equal(t, 2, len(gotIDs))
    93  	if gotIDs[0] != wantIDs[0] {
    94  		gotIDs[0], gotIDs[1] = gotIDs[1], gotIDs[0]
    95  	}
    96  	assert.Equal(t, wantIDs, gotIDs)
    97  }
    98  
    99  func TestJobsGet(t *testing.T) {
   100  	ctx := context.Background()
   101  	jobs := newJobs()
   102  	job, _, err := jobs.NewJob(ctx, noopFn, rc.Params{"_async": true})
   103  	require.NoError(t, err)
   104  	assert.Equal(t, job, jobs.Get(job.ID))
   105  	assert.Nil(t, jobs.Get(123123123123))
   106  }
   107  
   108  var longFn = func(ctx context.Context, in rc.Params) (rc.Params, error) {
   109  	time.Sleep(1 * time.Hour)
   110  	return nil, nil
   111  }
   112  
   113  var shortFn = func(ctx context.Context, in rc.Params) (rc.Params, error) {
   114  	time.Sleep(time.Millisecond)
   115  	return nil, nil
   116  }
   117  
   118  var ctxFn = func(ctx context.Context, in rc.Params) (rc.Params, error) {
   119  	<-ctx.Done()
   120  	return nil, ctx.Err()
   121  }
   122  
   123  var ctxParmFn = func(paramCtx context.Context, returnError bool) func(ctx context.Context, in rc.Params) (rc.Params, error) {
   124  	return func(ctx context.Context, in rc.Params) (rc.Params, error) {
   125  		<-paramCtx.Done()
   126  		if returnError {
   127  			return nil, ctx.Err()
   128  		}
   129  		return rc.Params{}, nil
   130  	}
   131  }
   132  
   133  const (
   134  	sleepTime      = 100 * time.Millisecond
   135  	floatSleepTime = float64(sleepTime) / 1e9 / 2
   136  )
   137  
   138  // sleep for some time so job.Duration is non-0
   139  func sleepJob() {
   140  	time.Sleep(sleepTime)
   141  }
   142  
   143  func TestJobFinish(t *testing.T) {
   144  	ctx := context.Background()
   145  	jobs := newJobs()
   146  	job, _, err := jobs.NewJob(ctx, longFn, rc.Params{"_async": true})
   147  	require.NoError(t, err)
   148  	sleepJob()
   149  
   150  	assert.Equal(t, true, job.EndTime.IsZero())
   151  	assert.Equal(t, rc.Params(nil), job.Output)
   152  	assert.Equal(t, 0.0, job.Duration)
   153  	assert.Equal(t, "", job.Error)
   154  	assert.Equal(t, false, job.Success)
   155  	assert.Equal(t, false, job.Finished)
   156  
   157  	wantOut := rc.Params{"a": 1}
   158  	job.finish(wantOut, nil)
   159  
   160  	assert.Equal(t, false, job.EndTime.IsZero())
   161  	assert.Equal(t, wantOut, job.Output)
   162  	assert.True(t, job.Duration >= floatSleepTime)
   163  	assert.Equal(t, "", job.Error)
   164  	assert.Equal(t, true, job.Success)
   165  	assert.Equal(t, true, job.Finished)
   166  
   167  	job, _, err = jobs.NewJob(ctx, longFn, rc.Params{"_async": true})
   168  	require.NoError(t, err)
   169  	sleepJob()
   170  	job.finish(nil, nil)
   171  
   172  	assert.Equal(t, false, job.EndTime.IsZero())
   173  	assert.Equal(t, rc.Params{}, job.Output)
   174  	assert.True(t, job.Duration >= floatSleepTime)
   175  	assert.Equal(t, "", job.Error)
   176  	assert.Equal(t, true, job.Success)
   177  	assert.Equal(t, true, job.Finished)
   178  
   179  	job, _, err = jobs.NewJob(ctx, longFn, rc.Params{"_async": true})
   180  	require.NoError(t, err)
   181  	sleepJob()
   182  	job.finish(wantOut, errors.New("potato"))
   183  
   184  	assert.Equal(t, false, job.EndTime.IsZero())
   185  	assert.Equal(t, wantOut, job.Output)
   186  	assert.True(t, job.Duration >= floatSleepTime)
   187  	assert.Equal(t, "potato", job.Error)
   188  	assert.Equal(t, false, job.Success)
   189  	assert.Equal(t, true, job.Finished)
   190  }
   191  
   192  // We've tested the functionality of run() already as it is
   193  // part of NewJob, now just test the panic catching
   194  func TestJobRunPanic(t *testing.T) {
   195  	ctx := context.Background()
   196  	wait := make(chan struct{})
   197  	boom := func(ctx context.Context, in rc.Params) (rc.Params, error) {
   198  		sleepJob()
   199  		defer close(wait)
   200  		panic("boom")
   201  	}
   202  
   203  	jobs := newJobs()
   204  	job, _, err := jobs.NewJob(ctx, boom, rc.Params{"_async": true})
   205  	require.NoError(t, err)
   206  	<-wait
   207  	runtime.Gosched() // yield to make sure job is updated
   208  
   209  	// Wait a short time for the panic to propagate
   210  	for i := uint(0); i < 10; i++ {
   211  		job.mu.Lock()
   212  		e := job.Error
   213  		job.mu.Unlock()
   214  		if e != "" {
   215  			break
   216  		}
   217  		time.Sleep(time.Millisecond << i)
   218  	}
   219  
   220  	job.mu.Lock()
   221  	assert.Equal(t, false, job.EndTime.IsZero())
   222  	assert.Equal(t, rc.Params{}, job.Output)
   223  	assert.True(t, job.Duration >= floatSleepTime)
   224  	assert.Contains(t, job.Error, "panic received: boom")
   225  	assert.Equal(t, false, job.Success)
   226  	assert.Equal(t, true, job.Finished)
   227  	job.mu.Unlock()
   228  }
   229  
   230  func TestJobsNewJob(t *testing.T) {
   231  	ctx := context.Background()
   232  	jobID.Store(0)
   233  	jobs := newJobs()
   234  	job, out, err := jobs.NewJob(ctx, noopFn, rc.Params{"_async": true})
   235  	require.NoError(t, err)
   236  	assert.Equal(t, int64(1), job.ID)
   237  	assert.Equal(t, rc.Params{"jobid": int64(1)}, out)
   238  	assert.Equal(t, job, jobs.Get(1))
   239  	assert.NotEmpty(t, job.Stop)
   240  }
   241  
   242  func TestStartJob(t *testing.T) {
   243  	ctx := context.Background()
   244  	jobID.Store(0)
   245  	job, out, err := NewJob(ctx, longFn, rc.Params{"_async": true})
   246  	assert.NoError(t, err)
   247  	assert.Equal(t, rc.Params{"jobid": int64(1)}, out)
   248  	assert.Equal(t, int64(1), job.ID)
   249  }
   250  
   251  func TestExecuteJob(t *testing.T) {
   252  	jobID.Store(0)
   253  	job, out, err := NewJob(context.Background(), shortFn, rc.Params{})
   254  	assert.NoError(t, err)
   255  	assert.Equal(t, int64(1), job.ID)
   256  	assert.Equal(t, rc.Params{}, out)
   257  }
   258  
   259  func TestExecuteJobWithConfig(t *testing.T) {
   260  	ctx := context.Background()
   261  	jobID.Store(0)
   262  	called := false
   263  	jobFn := func(ctx context.Context, in rc.Params) (rc.Params, error) {
   264  		ci := fs.GetConfig(ctx)
   265  		assert.Equal(t, 42*fs.Mebi, ci.BufferSize)
   266  		called = true
   267  		return nil, nil
   268  	}
   269  	_, _, err := NewJob(context.Background(), jobFn, rc.Params{
   270  		"_config": rc.Params{
   271  			"BufferSize": "42M",
   272  		},
   273  	})
   274  	require.NoError(t, err)
   275  	assert.Equal(t, true, called)
   276  	// Retest with string parameter
   277  	jobID.Store(0)
   278  	called = false
   279  	_, _, err = NewJob(ctx, jobFn, rc.Params{
   280  		"_config": `{"BufferSize": "42M"}`,
   281  	})
   282  	require.NoError(t, err)
   283  	assert.Equal(t, true, called)
   284  	// Check that wasn't the default
   285  	ci := fs.GetConfig(ctx)
   286  	assert.NotEqual(t, 42*fs.Mebi, ci.BufferSize)
   287  }
   288  
   289  func TestExecuteJobWithFilter(t *testing.T) {
   290  	ctx := context.Background()
   291  	called := false
   292  	jobID.Store(0)
   293  	jobFn := func(ctx context.Context, in rc.Params) (rc.Params, error) {
   294  		fi := filter.GetConfig(ctx)
   295  		assert.Equal(t, fs.SizeSuffix(1024), fi.Opt.MaxSize)
   296  		assert.Equal(t, []string{"a", "b", "c"}, fi.Opt.IncludeRule)
   297  		called = true
   298  		return nil, nil
   299  	}
   300  	_, _, err := NewJob(ctx, jobFn, rc.Params{
   301  		"_filter": rc.Params{
   302  			"IncludeRule": []string{"a", "b", "c"},
   303  			"MaxSize":     "1k",
   304  		},
   305  	})
   306  	require.NoError(t, err)
   307  	assert.Equal(t, true, called)
   308  }
   309  
   310  func TestExecuteJobWithGroup(t *testing.T) {
   311  	ctx := context.Background()
   312  	jobID.Store(0)
   313  	called := false
   314  	jobFn := func(ctx context.Context, in rc.Params) (rc.Params, error) {
   315  		called = true
   316  		group, found := accounting.StatsGroupFromContext(ctx)
   317  		assert.Equal(t, true, found)
   318  		assert.Equal(t, "myparty", group)
   319  		return nil, nil
   320  	}
   321  	_, _, err := NewJob(ctx, jobFn, rc.Params{
   322  		"_group": "myparty",
   323  	})
   324  	require.NoError(t, err)
   325  	assert.Equal(t, true, called)
   326  }
   327  
   328  func TestExecuteJobErrorPropagation(t *testing.T) {
   329  	ctx := context.Background()
   330  	jobID.Store(0)
   331  
   332  	testErr := errors.New("test error")
   333  	errorFn := func(ctx context.Context, in rc.Params) (out rc.Params, err error) {
   334  		return nil, testErr
   335  	}
   336  	_, _, err := NewJob(ctx, errorFn, rc.Params{})
   337  	assert.Equal(t, testErr, err)
   338  }
   339  
   340  func TestRcJobStatus(t *testing.T) {
   341  	ctx := context.Background()
   342  	jobID.Store(0)
   343  	_, _, err := NewJob(ctx, longFn, rc.Params{"_async": true})
   344  	assert.NoError(t, err)
   345  
   346  	call := rc.Calls.Get("job/status")
   347  	assert.NotNil(t, call)
   348  	in := rc.Params{"jobid": 1}
   349  	out, err := call.Fn(context.Background(), in)
   350  	require.NoError(t, err)
   351  	require.NotNil(t, out)
   352  	assert.Equal(t, float64(1), out["id"])
   353  	assert.Equal(t, "", out["error"])
   354  	assert.Equal(t, false, out["finished"])
   355  	assert.Equal(t, false, out["success"])
   356  
   357  	in = rc.Params{"jobid": 123123123}
   358  	_, err = call.Fn(context.Background(), in)
   359  	require.Error(t, err)
   360  	assert.Contains(t, err.Error(), "job not found")
   361  
   362  	in = rc.Params{"jobidx": 123123123}
   363  	_, err = call.Fn(context.Background(), in)
   364  	require.Error(t, err)
   365  	assert.Contains(t, err.Error(), "Didn't find key")
   366  }
   367  
   368  func TestRcJobList(t *testing.T) {
   369  	ctx := context.Background()
   370  	jobID.Store(0)
   371  	_, _, err := NewJob(ctx, longFn, rc.Params{"_async": true})
   372  	assert.NoError(t, err)
   373  
   374  	call := rc.Calls.Get("job/list")
   375  	assert.NotNil(t, call)
   376  	in := rc.Params{}
   377  	out1, err := call.Fn(context.Background(), in)
   378  	require.NoError(t, err)
   379  	require.NotNil(t, out1)
   380  	assert.Equal(t, []int64{1}, out1["jobids"], "should have job listed")
   381  
   382  	_, _, err = NewJob(ctx, longFn, rc.Params{"_async": true})
   383  	assert.NoError(t, err)
   384  
   385  	call = rc.Calls.Get("job/list")
   386  	assert.NotNil(t, call)
   387  	in = rc.Params{}
   388  	out2, err := call.Fn(context.Background(), in)
   389  	require.NoError(t, err)
   390  	require.NotNil(t, out2)
   391  	assert.Equal(t, 2, len(out2["jobids"].([]int64)), "should have all jobs listed")
   392  
   393  	require.NotNil(t, out1["executeId"], "should have executeId")
   394  	assert.Equal(t, out1["executeId"], out2["executeId"], "executeId should be the same")
   395  }
   396  
   397  func TestRcAsyncJobStop(t *testing.T) {
   398  	ctx := context.Background()
   399  	jobID.Store(0)
   400  	_, _, err := NewJob(ctx, ctxFn, rc.Params{"_async": true})
   401  	assert.NoError(t, err)
   402  
   403  	call := rc.Calls.Get("job/stop")
   404  	assert.NotNil(t, call)
   405  	in := rc.Params{"jobid": 1}
   406  	out, err := call.Fn(context.Background(), in)
   407  	require.NoError(t, err)
   408  	require.Empty(t, out)
   409  
   410  	in = rc.Params{"jobid": 123123123}
   411  	_, err = call.Fn(context.Background(), in)
   412  	require.Error(t, err)
   413  	assert.Contains(t, err.Error(), "job not found")
   414  
   415  	in = rc.Params{"jobidx": 123123123}
   416  	_, err = call.Fn(context.Background(), in)
   417  	require.Error(t, err)
   418  	assert.Contains(t, err.Error(), "Didn't find key")
   419  
   420  	time.Sleep(10 * time.Millisecond)
   421  
   422  	call = rc.Calls.Get("job/status")
   423  	assert.NotNil(t, call)
   424  	in = rc.Params{"jobid": 1}
   425  	out, err = call.Fn(context.Background(), in)
   426  	require.NoError(t, err)
   427  	require.NotNil(t, out)
   428  	assert.Equal(t, float64(1), out["id"])
   429  	assert.Equal(t, "context canceled", out["error"])
   430  	assert.Equal(t, true, out["finished"])
   431  	assert.Equal(t, false, out["success"])
   432  }
   433  
   434  func TestRcSyncJobStop(t *testing.T) {
   435  	ctx, cancel := context.WithCancel(context.Background())
   436  	go func() {
   437  		jobID.Store(0)
   438  		job, out, err := NewJob(ctx, ctxFn, rc.Params{})
   439  		assert.Error(t, err)
   440  		assert.Equal(t, int64(1), job.ID)
   441  		assert.Equal(t, rc.Params{}, out)
   442  	}()
   443  
   444  	time.Sleep(10 * time.Millisecond)
   445  
   446  	call := rc.Calls.Get("job/stop")
   447  	assert.NotNil(t, call)
   448  	in := rc.Params{"jobid": 1}
   449  	out, err := call.Fn(context.Background(), in)
   450  	require.NoError(t, err)
   451  	require.Empty(t, out)
   452  
   453  	in = rc.Params{"jobid": 123123123}
   454  	_, err = call.Fn(context.Background(), in)
   455  	require.Error(t, err)
   456  	assert.Contains(t, err.Error(), "job not found")
   457  
   458  	in = rc.Params{"jobidx": 123123123}
   459  	_, err = call.Fn(context.Background(), in)
   460  	require.Error(t, err)
   461  	assert.Contains(t, err.Error(), "Didn't find key")
   462  
   463  	cancel()
   464  	time.Sleep(10 * time.Millisecond)
   465  
   466  	call = rc.Calls.Get("job/status")
   467  	assert.NotNil(t, call)
   468  	in = rc.Params{"jobid": 1}
   469  	out, err = call.Fn(context.Background(), in)
   470  	require.NoError(t, err)
   471  	require.NotNil(t, out)
   472  	assert.Equal(t, float64(1), out["id"])
   473  	assert.Equal(t, "context canceled", out["error"])
   474  	assert.Equal(t, true, out["finished"])
   475  	assert.Equal(t, false, out["success"])
   476  }
   477  
   478  func TestRcJobStopGroup(t *testing.T) {
   479  	ctx := context.Background()
   480  	jobID.Store(0)
   481  	_, _, err := NewJob(ctx, ctxFn, rc.Params{
   482  		"_async": true,
   483  		"_group": "myparty",
   484  	})
   485  	require.NoError(t, err)
   486  	_, _, err = NewJob(ctx, ctxFn, rc.Params{
   487  		"_async": true,
   488  		"_group": "myparty",
   489  	})
   490  	require.NoError(t, err)
   491  
   492  	call := rc.Calls.Get("job/stopgroup")
   493  	assert.NotNil(t, call)
   494  	in := rc.Params{"group": "myparty"}
   495  	out, err := call.Fn(context.Background(), in)
   496  	require.NoError(t, err)
   497  	require.Empty(t, out)
   498  
   499  	in = rc.Params{}
   500  	_, err = call.Fn(context.Background(), in)
   501  	require.Error(t, err)
   502  	assert.Contains(t, err.Error(), "Didn't find key")
   503  
   504  	time.Sleep(10 * time.Millisecond)
   505  
   506  	call = rc.Calls.Get("job/status")
   507  	assert.NotNil(t, call)
   508  	for i := 1; i <= 2; i++ {
   509  		in = rc.Params{"jobid": i}
   510  		out, err = call.Fn(context.Background(), in)
   511  		require.NoError(t, err)
   512  		require.NotNil(t, out)
   513  		assert.Equal(t, "myparty", out["group"])
   514  		assert.Equal(t, "context canceled", out["error"])
   515  		assert.Equal(t, true, out["finished"])
   516  		assert.Equal(t, false, out["success"])
   517  	}
   518  }
   519  
   520  func TestOnFinish(t *testing.T) {
   521  	jobID.Store(0)
   522  	done := make(chan struct{})
   523  	ctx, cancel := context.WithCancel(context.Background())
   524  	job, _, err := NewJob(ctx, ctxParmFn(ctx, false), rc.Params{"_async": true})
   525  	assert.NoError(t, err)
   526  
   527  	stop, err := OnFinish(job.ID, func() { close(done) })
   528  	defer stop()
   529  	assert.NoError(t, err)
   530  
   531  	cancel()
   532  
   533  	select {
   534  	case <-done:
   535  	case <-time.After(time.Second):
   536  		t.Fatal("Timeout waiting for OnFinish to fire")
   537  	}
   538  }
   539  
   540  func TestOnFinishAlreadyFinished(t *testing.T) {
   541  	jobID.Store(0)
   542  	done := make(chan struct{})
   543  	ctx, cancel := context.WithCancel(context.Background())
   544  	defer cancel()
   545  	job, _, err := NewJob(ctx, shortFn, rc.Params{})
   546  	assert.NoError(t, err)
   547  
   548  	stop, err := OnFinish(job.ID, func() { close(done) })
   549  	defer stop()
   550  	assert.NoError(t, err)
   551  
   552  	select {
   553  	case <-done:
   554  	case <-time.After(time.Second):
   555  		t.Fatal("Timeout waiting for OnFinish to fire")
   556  	}
   557  }