Repository: go-co-op/gocron
Pull request #145 (2021-03-26 18:40:50)

Support rescheduling jobs
### What does this do?

- Adds support for rescheduling jobs using the following syntax:
    ```golang
	func ExampleScheduler_Update() {
		s := gocron.NewScheduler(time.UTC)
		j, _ := s.Every("1s").Do(func() {})
	
		time.Sleep(10 * time.Second)
		j, _ = s.Job(j).Every("10m").Update()
	
		time.Sleep(30 * time.Minute)
		_, _ = s.Job(j).Every(1).Day().At("02:00").Update()
	}
    ```

### Which issue(s) does this PR fix/relate to?
<!--- Put `Resolves #XXX` here to auto-close the issue that your PR fixes (if such) --->
#72 

### List any changes that modify/break current functionality


### Have you included tests for your changes?


### Did you document any new/modified functionality?

- [x] Updated `example_test.go`
- [ ] Updated `README.md`

### Notes



Patch:
diff --git a/executor.go b/executor.go
index 077a90e..784e58e 100644
--- a/executor.go
+++ b/executor.go
@@ -77,7 +77,7 @@ func (e *executor) start() {
 
 				switch f.runConfig.mode {
 				case defaultMode:
-					callJobFuncWithParams(f.functions[f.name], f.params[f.name])
+					callJobFuncWithParams(f.function, f.parameters)
 				case singletonMode:
 					_, _, _ = f.limiter.Do("main", func() (interface{}, error) {
 						select {
@@ -87,7 +87,7 @@ func (e *executor) start() {
 							return nil, nil
 						default:
 						}
-						callJobFuncWithParams(f.functions[f.name], f.params[f.name])
+						callJobFuncWithParams(f.function, f.parameters)
 						return nil, nil
 					})
 				}
diff --git a/go.mod b/go.mod
index 43c8197..076eb43 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,7 @@ module github.com/go-co-op/gocron
 go 1.16
 
 require (
-	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/stretchr/testify v1.7.0
-	golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
+	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
diff --git a/go.sum b/go.sum
index c642744..180c005 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,12 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/gocron.go b/gocron.go
index 797a527..450ac5a 100644
--- a/gocron.go
+++ b/gocron.go
@@ -25,10 +25,11 @@ var (
 	ErrInvalidIntervalType           = errors.New(".Every() interval must be int, time.Duration, or string")
 	ErrInvalidIntervalUnitsSelection = errors.New("an .Every() duration interval cannot be used with units (e.g. .Seconds())")
 
-	ErrAtTimeNotSupported  = errors.New("the At() method is not supported for this time unit")
-	ErrWeekdayNotSupported = errors.New("weekday is not supported for time unit")
-	ErrTagsUnique          = func(tag string) error { return fmt.Errorf("a non-unique tag was set on the job: %s", tag) }
-	ErrWrongParams         = errors.New("wrong list of params")
+	ErrAtTimeNotSupported     = errors.New("the At() method is not supported for this time unit")
+	ErrWeekdayNotSupported    = errors.New("weekday is not supported for time unit")
+	ErrTagsUnique             = func(tag string) error { return fmt.Errorf("a non-unique tag was set on the job: %s", tag) }
+	ErrWrongParams            = errors.New("wrong list of params")
+	ErrUpdateCalledWithoutJob = errors.New("a call to Scheduler.Update() requires a call to Scheduler.Job() first")
 )
 
 func wrapOrError(toWrap error, err error) error {
diff --git a/job.go b/job.go
index 162b169..cba7030 100644
--- a/job.go
+++ b/job.go
@@ -26,17 +26,17 @@ type Job struct {
 	dayOfTheMonth     int           // Specific day of the month to run the job
 	tags              []string      // allow the user to tag Jobs with certain labels
 	runCount          int           // number of times the job ran
-	timer             *time.Timer
+	timer             *time.Timer   // handles running tasks at specific time
 }
 
 type jobFunction struct {
-	functions map[string]interface{}   // Map for the function task store
-	params    map[string][]interface{} // Map for function and params of function
-	name      string                   // the Job name to run, func[jobFunc]
-	runConfig runConfig                // configuration for how many times to run the job
-	limiter   *singleflight.Group      // limits inflight runs of job to one
-	ctx       context.Context          // for cancellation
-	cancel    context.CancelFunc       // for cancellation
+	function   interface{}         // task's function
+	parameters []interface{}       // task's function parameters
+	name       string              //nolint the function name to run
+	runConfig  runConfig           // configuration for how many times to run the job
+	limiter    *singleflight.Group // limits inflight runs of job to one
+	ctx        context.Context     // for cancellation
+	cancel     context.CancelFunc  // for cancellation
 }
 
 type runConfig struct {
@@ -65,10 +65,8 @@ func NewJob(interval int) *Job {
 		lastRun:  time.Time{},
 		nextRun:  time.Time{},
 		jobFunction: jobFunction{
-			functions: make(map[string]interface{}),
-			params:    make(map[string][]interface{}),
-			ctx:       ctx,
-			cancel:    cancel,
+			ctx:    ctx,
+			cancel: cancel,
 		},
 		tags:              []string{},
 		startsImmediately: true,
@@ -109,6 +107,30 @@ func (j *Job) setStartAtTime(t time.Time) {
 	j.startAtTime = t
 }
 
+func (j *Job) getUnit() timeUnit {
+	j.RLock()
+	defer j.RUnlock()
+	return j.unit
+}
+
+func (j *Job) setUnit(t timeUnit) {
+	j.Lock()
+	defer j.Unlock()
+	j.unit = t
+}
+
+func (j *Job) getDuration() time.Duration {
+	j.RLock()
+	defer j.RUnlock()
+	return j.duration
+}
+
+func (j *Job) setDuration(t time.Duration) {
+	j.Lock()
+	defer j.Unlock()
+	j.duration = t
+}
+
 // Error returns an error if one occurred while creating the Job.
 // If multiple errors occurred, they will be wrapped and can be
 // checked using the standard unwrap options.
@@ -221,10 +243,11 @@ func (j *Job) RunCount() int {
 	return j.runCount
 }
 
-func (j *Job) stopTimer() {
+func (j *Job) stop() {
 	j.Lock()
 	defer j.Unlock()
 	if j.timer != nil {
 		j.timer.Stop()
 	}
+	j.cancel()
 }
diff --git a/scheduler.go b/scheduler.go
index c3e1d4a..683a291 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -28,6 +28,8 @@ type Scheduler struct {
 	executor *executor   // executes jobs passed via chan
 
 	tags map[string]struct{} // for storing tags when unique tags is set
+
+	updateJob bool // so the scheduler knows to create a new job or update the current
 }
 
 // NewScheduler creates a new Scheduler
@@ -111,10 +113,10 @@ func (s *Scheduler) Len() int {
 
 // Swap places each job into the other job's position given
 // the provided job indexes.
-func (s *Scheduler) Swap(i, job int) {
+func (s *Scheduler) Swap(i, j int) {
 	s.jobsMutex.Lock()
 	defer s.jobsMutex.Unlock()
-	s.jobs[i], s.jobs[job] = s.jobs[job], s.jobs[i]
+	s.jobs[i], s.jobs[j] = s.jobs[j], s.jobs[i]
 }
 
 // Less compares the next run of jobs based on their index.
@@ -179,7 +181,7 @@ func (s *Scheduler) durationToNextRun(lastRun time.Time, job *Job) time.Duration
 	}
 
 	var d time.Duration
-	switch job.unit {
+	switch job.getUnit() {
 	case milliseconds, seconds, minutes, hours:
 		d = s.calculateDuration(job)
 	case days:
@@ -193,7 +195,7 @@ func (s *Scheduler) durationToNextRun(lastRun time.Time, job *Job) time.Duration
 	case months:
 		d = s.calculateMonths(job, lastRun)
 	case duration:
-		d = job.duration
+		d = job.getDuration()
 	}
 	return d
 }
@@ -279,7 +281,7 @@ func (s *Scheduler) calculateDuration(job *Job) time.Duration {
 	}
 
 	interval := job.interval
-	switch job.unit {
+	switch job.getUnit() {
 	case milliseconds:
 		return time.Duration(interval) * time.Millisecond
 	case seconds:
@@ -324,32 +326,56 @@ func (s *Scheduler) NextRun() (*Job, time.Time) {
 // parses with time.ParseDuration().
 // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
 func (s *Scheduler) Every(interval interface{}) *Scheduler {
+	job := &Job{}
+	if s.updateJob {
+		job = s.getCurrentJob()
+	}
+
 	switch interval := interval.(type) {
 	case int:
-		job := NewJob(interval)
+		if !s.updateJob {
+			job = NewJob(interval)
+		} else {
+			job.interval = interval
+		}
 		if interval <= 0 {
 			job.error = wrapOrError(job.error, ErrInvalidInterval)
 		}
-		s.setJobs(append(s.Jobs(), job))
 	case time.Duration:
-		job := NewJob(0)
-		job.duration = interval
-		job.unit = duration
-		s.setJobs(append(s.Jobs(), job))
+		if !s.updateJob {
+			job = NewJob(0)
+		} else {
+			job.interval = 0
+		}
+		job.setDuration(interval)
+		job.setUnit(duration)
 	case string:
-		job := NewJob(0)
+		if !s.updateJob {
+			job = NewJob(0)
+		} else {
+			job.interval = 0
+		}
 		d, err := time.ParseDuration(interval)
 		if err != nil {
 			job.error = wrapOrError(job.error, err)
 		}
-		job.duration = d
-		job.unit = duration
-		s.setJobs(append(s.Jobs(), job))
+		job.setDuration(d)
+		job.setUnit(duration)
 	default:
-		job := NewJob(0)
+		if !s.updateJob {
+			job = NewJob(0)
+		} else {
+			job.interval = 0
+		}
 		job.error = wrapOrError(job.error, ErrInvalidIntervalType)
+	}
+
+	if s.updateJob {
+		s.setJobs(append(s.Jobs()[:len(s.Jobs())-1], job))
+	} else {
 		s.setJobs(append(s.Jobs(), job))
 	}
+
 	return s
 }
 
@@ -406,8 +432,7 @@ func (s *Scheduler) removeByCondition(shouldRemove func(*Job) bool) {
 		if !shouldRemove(job) {
 			retainedJobs = append(retainedJobs, job)
 		} else {
-			job.stopTimer()
-			job.cancel()
+			job.stop()
 		}
 	}
 	s.setJobs(retainedJobs)
@@ -420,8 +445,7 @@ func (s *Scheduler) RemoveByTag(tag string) error {
 		return err
 	}
 	// Remove job if job index is valid
-	s.jobs[index].stopTimer()
-	s.jobs[index].cancel()
+	s.jobs[index].stop()
 	s.setJobs(removeAtIndex(s.jobs, index))
 	return nil
 }
@@ -486,7 +510,7 @@ func (s *Scheduler) jobPresent(j *Job) bool {
 // Clear clear all Jobs from this scheduler
 func (s *Scheduler) Clear() {
 	for _, j := range s.Jobs() {
-		j.stopTimer()
+		j.stop()
 	}
 	s.setJobs(make([]*Job, 0))
 }
@@ -507,11 +531,11 @@ func (s *Scheduler) stop() {
 func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error) {
 	job := s.getCurrentJob()
 
-	if job.atTime != 0 && job.unit <= hours {
+	if job.atTime != 0 && job.getUnit() <= hours {
 		job.error = wrapOrError(job.error, ErrAtTimeNotSupported)
 	}
 
-	if job.scheduledWeekday != nil && job.unit != weeks {
+	if job.scheduledWeekday != nil && job.getUnit() != weeks {
 		job.error = wrapOrError(job.error, ErrWeekdayNotSupported)
 	}
 
@@ -537,9 +561,11 @@ func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error)
 	}
 
 	fname := getFunctionName(jobFun)
-	job.functions[fname] = jobFun
-	job.params[fname] = params
-	job.name = fname
+	if job.name != fname {
+		job.function = jobFun
+		job.parameters = params
+		job.name = fname
+	}
 
 	// we should not schedule if not running since we cant foresee how long it will take for the scheduler to start
 	if s.IsRunning() {
@@ -602,10 +628,10 @@ func (s *Scheduler) StartAt(t time.Time) *Scheduler {
 // setUnit sets the unit type
 func (s *Scheduler) setUnit(unit timeUnit) {
 	job := s.getCurrentJob()
-	if job.unit == duration {
+	if job.getUnit() == duration {
 		job.error = wrapOrError(job.error, ErrInvalidIntervalUnitsSelection)
 	}
-	job.unit = unit
+	job.setUnit(unit)
 }
 
 // Second sets the unit with seconds
@@ -740,7 +766,7 @@ func (s *Scheduler) Sunday() *Scheduler {
 }
 
 func (s *Scheduler) getCurrentJob() *Job {
-	return s.Jobs()[len(s.jobs)-1]
+	return s.Jobs()[len(s.Jobs())-1]
 }
 
 func (s *Scheduler) now() time.Time {
@@ -754,3 +780,31 @@ func (s *Scheduler) now() time.Time {
 func (s *Scheduler) TagsUnique() {
 	s.tags = make(map[string]struct{})
 }
+
+// Job puts the provided job in focus for the purpose
+// of making changes to the job with the scheduler chain
+// and finalized by calling Update()
+func (s *Scheduler) Job(j *Job) *Scheduler {
+	jobs := s.Jobs()
+	for index, job := range jobs {
+		if job == j {
+			// the current job is always last, so put this job there
+			s.Swap(len(jobs)-1, index)
+		}
+	}
+	s.updateJob = true
+	return s
+}
+
+// Update stops the job (if running) and starts it with any updates
+// that were made to the job in the scheduler chain. Job() must be
+// called first to put the given job in focus.
+func (s *Scheduler) Update() (*Job, error) {
+	j := s.getCurrentJob()
+
+	if !s.updateJob {
+		return j, wrapOrError(j.error, ErrUpdateCalledWithoutJob)
+	}
+	j.stop()
+	return s.Do(j.function, j.parameters...)
+}


Test patch:
diff --git a/example_test.go b/example_test.go
index 891ab9d..e229223 100644
--- a/example_test.go
+++ b/example_test.go
@@ -241,6 +241,18 @@ func ExampleScheduler_IsRunning() {
 	// true
 }
 
+func ExampleScheduler_Job() {
+	s := gocron.NewScheduler(time.UTC)
+	j, _ := s.Every("1s").Do(func() {})
+	s.StartAsync()
+
+	time.Sleep(10 * time.Second)
+	_, _ = s.Job(j).Every("10m").Update()
+
+	time.Sleep(30 * time.Minute)
+	_, _ = s.Job(j).Every(1).Day().At("02:00").Update()
+}
+
 func ExampleScheduler_Jobs() {
 	s := gocron.NewScheduler(time.UTC)
 
@@ -292,6 +304,24 @@ func ExampleScheduler_Location() {
 	// UTC
 }
 
+func ExampleScheduler_Millisecond() {
+	s := gocron.NewScheduler(time.UTC)
+
+	_, _ = s.Every(1).Millisecond().Do(task)
+	_, _ = s.Every(1).Milliseconds().Do(task)
+	_, _ = s.Every("1ms").Seconds().Do(task)
+	_, _ = s.Every(time.Millisecond).Seconds().Do(task)
+}
+
+func ExampleScheduler_Milliseconds() {
+	s := gocron.NewScheduler(time.UTC)
+
+	_, _ = s.Every(1).Millisecond().Do(task)
+	_, _ = s.Every(1).Milliseconds().Do(task)
+	_, _ = s.Every("1ms").Seconds().Do(task)
+	_, _ = s.Every(time.Millisecond).Seconds().Do(task)
+}
+
 func ExampleScheduler_Minute() {
 	s := gocron.NewScheduler(time.UTC)
 
@@ -545,6 +575,18 @@ func ExampleScheduler_Tuesday() {
 	// Tuesday
 }
 
+func ExampleScheduler_Update() {
+	s := gocron.NewScheduler(time.UTC)
+	j, _ := s.Every("1s").Do(func() {})
+	s.StartAsync()
+
+	time.Sleep(10 * time.Second)
+	_, _ = s.Job(j).Every("10m").Update()
+
+	time.Sleep(30 * time.Minute)
+	_, _ = s.Job(j).Every(1).Day().At("02:00").Update()
+}
+
 func ExampleScheduler_Wednesday() {
 	s := gocron.NewScheduler(time.UTC)
 	j, _ := s.Every(1).Day().Wednesday().Do(task)
diff --git a/scheduler_test.go b/scheduler_test.go
index 72746e7..5168a6b 100644
--- a/scheduler_test.go
+++ b/scheduler_test.go
@@ -2,7 +2,7 @@ package gocron
 
 import (
 	"fmt"
-	"strings"
+	"log"
 	"sync"
 	"sync/atomic"
 	"testing"
@@ -55,7 +55,7 @@ func TestImmediateExecution(t *testing.T) {
 
 }
 
-func TestInvalidEveryInterval(t *testing.T) {
+func TestScheduler_Every_InvalidInterval(t *testing.T) {
 	testCases := []struct {
 		description   string
 		interval      interface{}
@@ -63,7 +63,7 @@ func TestInvalidEveryInterval(t *testing.T) {
 	}{
 		{"zero", 0, ErrInvalidInterval.Error()},
 		{"negative", -1, ErrInvalidInterval.Error()},
-		{"invalid string duration", "bad", "time: invalid duration"},
+		{"invalid string duration", "bad", "time: invalid duration \"bad\""},
 	}
 
 	s := NewScheduler(time.UTC)
@@ -72,9 +72,7 @@ func TestInvalidEveryInterval(t *testing.T) {
 		t.Run(tc.description, func(t *testing.T) {
 			_, err := s.Every(tc.interval).Do(func() {})
 			require.Error(t, err)
-			// wonky way to assert on the error message, but between go 1.14
-			// and go 1.15 the error value was wrapped in quotes
-			assert.True(t, strings.HasPrefix(err.Error(), tc.expectedError))
+			assert.EqualError(t, err, tc.expectedError)
 		})
 	}
 
@@ -108,7 +106,7 @@ func TestScheduler_Every(t *testing.T) {
 		s := NewScheduler(time.UTC)
 		semaphore := make(chan bool)
 
-		_, err := s.Every(1).Second().Do(func() {
+		_, err := s.Every(100).Milliseconds().Do(func() {
 			semaphore <- true
 		})
 		require.NoError(t, err)
@@ -118,7 +116,7 @@ func TestScheduler_Every(t *testing.T) {
 		var counter int
 
 		now := time.Now()
-		for time.Now().Before(now.Add(1 * time.Second)) {
+		for time.Now().Before(now.Add(100 * time.Millisecond)) {
 			if <-semaphore {
 				counter++
 			}
@@ -131,7 +129,7 @@ func TestScheduler_Every(t *testing.T) {
 		s := NewScheduler(time.UTC)
 		semaphore := make(chan bool)
 
-		_, err := s.Every("1s").Do(func() {
+		_, err := s.Every("100ms").Do(func() {
 			semaphore <- true
 		})
 		require.NoError(t, err)
@@ -141,7 +139,7 @@ func TestScheduler_Every(t *testing.T) {
 		var counter int
 
 		now := time.Now()
-		for time.Now().Before(now.Add(1 * time.Second)) {
+		for time.Now().Before(now.Add(100 * time.Millisecond)) {
 			if <-semaphore {
 				counter++
 			}
@@ -149,6 +147,7 @@ func TestScheduler_Every(t *testing.T) {
 		s.Stop()
 		assert.Equal(t, 2, counter)
 	})
+
 }
 
 func TestExecutionSeconds(t *testing.T) {
@@ -157,12 +156,12 @@ func TestExecutionSeconds(t *testing.T) {
 
 	var (
 		executions         []int64
-		interval           = 2
-		expectedExecutions = 4
+		interval           = 1
+		expectedExecutions = 2
 		mu                 sync.RWMutex
 	)
 
-	runTime := 6 * time.Second
+	runTime := 1 * time.Second
 	startTime := time.Now()
 
 	// default unit is seconds
@@ -347,17 +346,17 @@ func TestScheduler_Remove(t *testing.T) {
 		s := NewScheduler(time.UTC)
 		semaphore := make(chan bool)
 
-		j, err := s.Every(1).Seconds().StartAt(s.time.Now(s.location).Add(time.Second)).Do(func() {
+		j, err := s.Every("100ms").StartAt(s.time.Now(s.location).Add(time.Second)).Do(func() {
 			semaphore <- true
 		})
 		require.NoError(t, err)
 
 		s.StartAsync()
 
-		s.Remove(j.functions[j.name])
+		s.Remove(j.function)
 
 		select {
-		case <-time.After(2 * time.Second):
+		case <-time.After(200 * time.Millisecond):
 			// test passed
 		case <-semaphore:
 			t.Fatal("job ran after being removed")
@@ -381,7 +380,7 @@ func TestScheduler_RemoveByReference(t *testing.T) {
 		s := NewScheduler(time.UTC)
 		semaphore := make(chan bool)
 
-		j, err := s.Every(1).Seconds().StartAt(s.time.Now(s.location).Add(time.Second)).Do(func() {
+		j, err := s.Every("100ms").StartAt(s.time.Now(s.location).Add(100 * time.Millisecond)).Do(func() {
 			semaphore <- true
 		})
 		require.NoError(t, err)
@@ -391,7 +390,7 @@ func TestScheduler_RemoveByReference(t *testing.T) {
 		s.RemoveByReference(j)
 
 		select {
-		case <-time.After(2 * time.Second):
+		case <-time.After(200 * time.Millisecond):
 			// test passed
 		case <-semaphore:
 			t.Fatal("job ran after being removed")
@@ -487,7 +486,7 @@ func TestClear(t *testing.T) {
 	s := NewScheduler(time.UTC)
 	semaphore := make(chan bool)
 
-	_, err := s.Every(1).Second().Do(func() {
+	_, err := s.Every("100ms").Do(func() {
 		semaphore <- true
 	})
 	require.NoError(t, err)
@@ -499,7 +498,7 @@ func TestClear(t *testing.T) {
 
 	var counter int
 	now := time.Now()
-	for time.Now().Before(now.Add(1 * time.Second)) {
+	for time.Now().Before(now.Add(200 * time.Millisecond)) {
 		select {
 		case <-semaphore:
 			counter++
@@ -591,14 +590,14 @@ func TestScheduler_StartAt(t *testing.T) {
 		s := NewScheduler(time.UTC)
 		semaphore := make(chan bool)
 
-		s.Every(1).Day().StartAt(s.time.Now(s.location).Add(time.Second)).Do(func() {
+		s.Every(1).Day().StartAt(s.time.Now(s.location).Add(100 * time.Millisecond)).Do(func() {
 			semaphore <- true
 		})
 
 		s.StartAsync()
 
 		select {
-		case <-time.After(2 * time.Second):
+		case <-time.After(200 * time.Millisecond):
 			t.Fatal("job did not run at 1 second")
 		case <-semaphore:
 			// test passed
@@ -761,18 +760,18 @@ func TestRunJobsWithLimit(t *testing.T) {
 
 		s := NewScheduler(time.UTC)
 
-		j, err := s.Every(1).Second().Do(func() {
+		j, err := s.Every("100ms").Do(func() {
 			semaphore <- true
 		})
 		require.NoError(t, err)
 		j.LimitRunsTo(1)
 
 		s.StartAsync()
-		time.Sleep(2 * time.Second)
+		time.Sleep(200 * time.Millisecond)
 
 		var counter int
 		now := time.Now()
-		for time.Now().Before(now.Add(2 * time.Second)) {
+		for time.Now().Before(now.Add(200 * time.Millisecond)) {
 			select {
 			case <-semaphore:
 				counter++
@@ -788,18 +787,18 @@ func TestRunJobsWithLimit(t *testing.T) {
 
 		s := NewScheduler(time.UTC)
 
-		j, err := s.Every(1).Second().Do(func() {
+		j, err := s.Every("100ms").Do(func() {
 			semaphore <- true
 		})
 		require.NoError(t, err)
 		j.LimitRunsTo(1)
 
 		s.StartAsync()
-		time.Sleep(2 * time.Second)
+		time.Sleep(200 * time.Millisecond)
 
 		var counter int
 		select {
-		case <-time.After(2 * time.Second):
+		case <-time.After(200 * time.Millisecond):
 			assert.Equal(t, 0, s.Len())
 		case <-semaphore:
 			counter++
@@ -840,21 +839,21 @@ func TestScheduler_SingletonMode(t *testing.T) {
 			s := NewScheduler(time.UTC)
 			var trigger int32
 
-			j, err := s.Every(1).Second().SingletonMode().Do(func() {
+			j, err := s.Every("100ms").SingletonMode().Do(func() {
 				if atomic.LoadInt32(&trigger) == 1 {
 					t.Fatal("Restart should not occur")
 				}
 				atomic.AddInt32(&trigger, 1)
-				time.Sleep(3 * time.Second)
+				time.Sleep(300 * time.Millisecond)
 			})
 			require.NoError(t, err)
 
 			s.StartAsync()
-			time.Sleep(2 * time.Second)
+			time.Sleep(200 * time.Millisecond)
 
 			if tc.removeJob {
 				s.RemoveByReference(j)
-				time.Sleep(3 * time.Second)
+				time.Sleep(300 * time.Millisecond)
 			}
 			s.Stop()
 		})
@@ -868,17 +867,17 @@ func TestScheduler_LimitRunsTo(t *testing.T) {
 
 		s := NewScheduler(time.UTC)
 
-		_, err := s.Every(1).Second().LimitRunsTo(1).Do(func() {
+		_, err := s.Every("100ms").LimitRunsTo(1).Do(func() {
 			semaphore <- true
 		})
 		require.NoError(t, err)
 
 		s.StartAsync()
-		time.Sleep(2 * time.Second)
+		time.Sleep(200 * time.Millisecond)
 
 		var counter int
 		select {
-		case <-time.After(2 * time.Second):
+		case <-time.After(200 * time.Millisecond):
 			assert.Equal(t, 1, counter)
 		case <-semaphore:
 			counter++
@@ -892,16 +891,16 @@ func TestScheduler_LimitRunsTo(t *testing.T) {
 		s := NewScheduler(time.UTC)
 		s.StartAsync()
 
-		_, err := s.Every(1).Second().LimitRunsTo(1).Do(func() {
+		_, err := s.Every("100ms").LimitRunsTo(1).Do(func() {
 			semaphore <- true
 		})
 		require.NoError(t, err)
 
-		time.Sleep(2 * time.Second)
+		time.Sleep(200 * time.Millisecond)
 
 		var counter int
 		select {
-		case <-time.After(2 * time.Second):
+		case <-time.After(200 * time.Millisecond):
 			assert.Equal(t, 1, counter)
 		case <-semaphore:
 			counter++
@@ -915,17 +914,17 @@ func TestScheduler_LimitRunsTo(t *testing.T) {
 		s := NewScheduler(time.UTC)
 		s.StartAsync()
 
-		j, err := s.Every(1).Second().Do(func() {
+		j, err := s.Every("100ms").Do(func() {
 			semaphore <- true
 		})
 		require.NoError(t, err)
 		j.LimitRunsTo(1)
 
-		time.Sleep(2 * time.Second)
+		time.Sleep(200 * time.Millisecond)
 
 		var counter int
 		select {
-		case <-time.After(2 * time.Second):
+		case <-time.After(200 * time.Millisecond):
 			assert.Equal(t, 2, counter)
 		case <-semaphore:
 			counter++
@@ -953,7 +952,7 @@ func TestScheduler_SetMaxConcurrentJobs(t *testing.T) {
 		{"reschedule mode", 2, RescheduleMode, 4, false,
 			func() {
 				semaphore <- true
-				time.Sleep(2 * time.Second)
+				time.Sleep(200 * time.Millisecond)
 			},
 		},
 
@@ -965,7 +964,7 @@ func TestScheduler_SetMaxConcurrentJobs(t *testing.T) {
 		{"wait mode", 2, WaitMode, 8, false,
 			func() {
 				semaphore <- true
-				time.Sleep(1 * time.Second)
+				time.Sleep(100 * time.Millisecond)
 			},
 		},
 
@@ -973,7 +972,7 @@ func TestScheduler_SetMaxConcurrentJobs(t *testing.T) {
 		{"wait mode - with job removal", 2, WaitMode, 8, true,
 			func() {
 				semaphore <- true
-				time.Sleep(1 * time.Second)
+				time.Sleep(100 * time.Millisecond)
 			},
 		},
 	}
@@ -984,13 +983,13 @@ func TestScheduler_SetMaxConcurrentJobs(t *testing.T) {
 			s := NewScheduler(time.UTC)
 			s.SetMaxConcurrentJobs(tc.maxConcurrentJobs, tc.mode)
 
-			j1, err := s.Every(1).Second().Do(tc.f)
+			j1, err := s.Every("100ms").Do(tc.f)
 			require.NoError(t, err)
 
-			j2, err := s.Every(2).Second().Do(tc.f)
+			j2, err := s.Every("200ms").Do(tc.f)
 			require.NoError(t, err)
 
-			j3, err := s.Every(3).Second().Do(tc.f)
+			j3, err := s.Every("300ms").Do(tc.f)
 			require.NoError(t, err)
 
 			s.StartAsync()
@@ -998,7 +997,7 @@ func TestScheduler_SetMaxConcurrentJobs(t *testing.T) {
 			var counter int
 
 			now := time.Now()
-			for time.Now().Before(now.Add(4 * time.Second)) {
+			for time.Now().Before(now.Add(400 * time.Millisecond)) {
 				select {
 				case <-semaphore:
 					counter++
@@ -1019,7 +1018,7 @@ func TestScheduler_SetMaxConcurrentJobs(t *testing.T) {
 			// or job should be properly stopped
 
 			now = time.Now()
-			for time.Now().Before(now.Add(1 * time.Second)) {
+			for time.Now().Before(now.Add(200 * time.Millisecond)) {
 				select {
 				case <-semaphore:
 					counter++
@@ -1080,3 +1079,59 @@ func TestScheduler_DoParameterValidation(t *testing.T) {
 		})
 	}
 }
+
+func TestScheduler_Job(t *testing.T) {
+	s := NewScheduler(time.UTC)
+
+	j1, err := s.Every("1s").Do(func() { log.Println("one") })
+	require.NoError(t, err)
+	assert.Equal(t, j1, s.getCurrentJob())
+
+	j2, err := s.Every("1s").Do(func() { log.Println("two") })
+	require.NoError(t, err)
+	assert.Equal(t, j2, s.getCurrentJob())
+
+	s.Job(j1)
+	assert.Equal(t, j1, s.getCurrentJob())
+
+	s.Job(j2)
+	assert.Equal(t, j2, s.getCurrentJob())
+}
+
+func TestScheduler_Update(t *testing.T) {
+	t.Run("happy path", func(t *testing.T) {
+		s := NewScheduler(time.UTC)
+
+		var counterMutex sync.RWMutex
+		counter := 0
+
+		j, err := s.Every(1).Day().Do(func() { counterMutex.Lock(); defer counterMutex.Unlock(); counter++ })
+		require.NoError(t, err)
+
+		s.StartAsync()
+
+		time.Sleep(300 * time.Millisecond)
+		_, err = s.Job(j).Every("500ms").Update()
+		require.NoError(t, err)
+
+		time.Sleep(550 * time.Millisecond)
+		_, err = s.Job(j).Every("750ms").Update()
+		require.NoError(t, err)
+
+		time.Sleep(800 * time.Millisecond)
+		s.Stop()
+
+		counterMutex.RLock()
+		defer counterMutex.RUnlock()
+		assert.Equal(t, 3, counter)
+	})
+
+	t.Run("update called with job call", func(t *testing.T) {
+		s := NewScheduler(time.UTC)
+		_, err := s.Every("1s").Do(func() {})
+		require.NoError(t, err)
+
+		_, err = s.Update()
+		assert.EqualError(t, err, ErrUpdateCalledWithoutJob.Error())
+	})
+}