Repository: go-co-op/gocron
Pull request #328 (2022-05-08 09:43:40)

add event listeners on before and after job executing
### What does this do?
Added event listeners on  before job executing and after job executing. #315 

### Have you included tests for your changes?
Tests are included.

### Did you document any new/modified functionality?
`example_test.go` updated


AlexanderSutul: @JohnRoesler review plz 😊 

Patch:
diff --git a/executor.go b/executor.go
index 43853fb..a33fcf2 100644
--- a/executor.go
+++ b/executor.go
@@ -75,11 +75,17 @@ func (e *executor) start() {
 					defer e.maxRunningJobs.Release(1)
 				}
 
-				switch f.runConfig.mode {
-				case defaultMode:
+				runJob := func() {
 					f.incrementRunState()
+					callJobFunc(f.eventListeners.onBeforeJobExecution)
 					callJobFuncWithParams(f.function, f.parameters)
+					callJobFunc(f.eventListeners.onAfterJobExecution)
 					f.decrementRunState()
+				}
+
+				switch f.runConfig.mode {
+				case defaultMode:
+					runJob()
 				case singletonMode:
 					_, _, _ = f.limiter.Do("main", func() (interface{}, error) {
 						select {
@@ -89,9 +95,7 @@ func (e *executor) start() {
 							return nil, nil
 						default:
 						}
-						f.incrementRunState()
-						callJobFuncWithParams(f.function, f.parameters)
-						f.decrementRunState()
+						runJob()
 						return nil, nil
 					})
 				}
diff --git a/gocron.go b/gocron.go
index e7d9880..6387cb4 100644
--- a/gocron.go
+++ b/gocron.go
@@ -67,6 +67,12 @@ const (
 	crontab
 )
 
+func callJobFunc(jobFunc interface{}) {
+	if jobFunc != nil {
+		reflect.ValueOf(jobFunc).Call([]reflect.Value{})
+	}
+}
+
 func callJobFuncWithParams(jobFunc interface{}, params []interface{}) {
 	f := reflect.ValueOf(jobFunc)
 	if len(params) != f.Type().NumIn() {
diff --git a/job.go b/job.go
index a3bbc12..cd9f21f 100644
--- a/job.go
+++ b/job.go
@@ -35,15 +35,21 @@ type Job struct {
 }
 
 type jobFunction struct {
-	function      interface{}         // task's function
-	parameters    []interface{}       // task's function parameters
-	parametersLen int                 // length of the passed 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
-	runState      *int64              // will be non-zero when jobs are running
+	eventListeners                     // additional functions to allow run 'em during job performing
+	function       interface{}         // task's function
+	parameters     []interface{}       // task's function parameters
+	parametersLen  int                 // length of the passed 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
+	runState       *int64              // will be non-zero when jobs are running
+}
+
+type eventListeners struct {
+	onBeforeJobExecution interface{} // performs before job executing
+	onAfterJobExecution  interface{} // performs after job executing
 }
 
 type jobMutex struct {
@@ -64,15 +70,16 @@ func (jf *jobFunction) decrementRunState() {
 
 func (jf *jobFunction) copy() jobFunction {
 	cp := jobFunction{
-		function:      jf.function,
-		parameters:    nil,
-		parametersLen: jf.parametersLen,
-		name:          jf.name,
-		runConfig:     jf.runConfig,
-		limiter:       jf.limiter,
-		ctx:           jf.ctx,
-		cancel:        jf.cancel,
-		runState:      jf.runState,
+		eventListeners: jf.eventListeners,
+		function:       jf.function,
+		parameters:     nil,
+		parametersLen:  jf.parametersLen,
+		name:           jf.name,
+		runConfig:      jf.runConfig,
+		limiter:        jf.limiter,
+		ctx:            jf.ctx,
+		cancel:         jf.cancel,
+		runState:       jf.runState,
 	}
 	cp.parameters = append(cp.parameters, jf.parameters...)
 	return cp
@@ -276,6 +283,14 @@ func (j *Job) Tags() []string {
 	return j.tags
 }
 
+// SetEventListeners accepts two functions that will be called, one before and one after the job is run
+func (j *Job) SetEventListeners(onBeforeJobExecution interface{}, onAfterJobExecution interface{}) {
+	j.eventListeners = eventListeners{
+		onBeforeJobExecution: onBeforeJobExecution,
+		onAfterJobExecution:  onAfterJobExecution,
+	}
+}
+
 // ScheduledTime returns the time of the Job's next scheduled run
 func (j *Job) ScheduledTime() time.Time {
 	j.mu.RLock()


Test patch:
diff --git a/example_test.go b/example_test.go
index c17d802..cda60d2 100644
--- a/example_test.go
+++ b/example_test.go
@@ -113,6 +113,12 @@ func ExampleJob_ScheduledTime() {
 	fmt.Println(job.ScheduledTime())
 }
 
+func ExampleJob_SetEventListeners() {
+	s := gocron.NewScheduler(time.UTC)
+	job, _ := s.Every(1).Week().Do(task)
+	job.SetEventListeners(func() {}, func() {})
+}
+
 func ExampleJob_SingletonMode() {
 	s := gocron.NewScheduler(time.UTC)
 	job, _ := s.Every(1).Second().Do(task)
diff --git a/job_test.go b/job_test.go
index 1a5d5b4..e5aa33a 100644
--- a/job_test.go
+++ b/job_test.go
@@ -1,6 +1,7 @@
 package gocron
 
 import (
+	"sync"
 	"testing"
 	"time"
 
@@ -224,3 +225,34 @@ func TestJob_CommonExports(t *testing.T) {
 	j.mu.Unlock()
 	assert.Equal(t, lastRun, j.LastRun())
 }
+
+func TestJob_SetEventListeners(t *testing.T) {
+	t.Run("run event listeners callbacks for a job", func(t *testing.T) {
+		var (
+			jobRanPassed         = false
+			beforeCallbackPassed = false
+			afterCallbackPassed  = false
+			wg                   = &sync.WaitGroup{}
+		)
+		wg.Add(1)
+		s := NewScheduler(time.UTC)
+		job, err := s.Tag("tag1").Every("1ms").Do(func() {
+			jobRanPassed = true
+		})
+		job.SetEventListeners(func() {
+			beforeCallbackPassed = true
+		}, func() {
+			defer wg.Done()
+			afterCallbackPassed = true
+		})
+
+		s.StartAsync()
+		s.stop()
+		wg.Wait()
+
+		require.NoError(t, err)
+		assert.True(t, jobRanPassed)
+		assert.True(t, beforeCallbackPassed)
+		assert.True(t, afterCallbackPassed)
+	})
+}