Repository: go-co-op/gocron
Pull request #813 (2025-01-08 02:56:13)

feat(event listener): introduce BeforeJobRunsSkipIfBeforeFuncErrors as a new Eventlistener
### What does this do?
This PR adds a new `EventListener` `BeforeJobRunsSkipIfBeforeFuncErrors` that exits job execution if the provided func returns an error

**Use Case**:
Im using `gocron` as a command wrapper for `terraform` to perform synthetic monitoring checks and came up with a configuration as follows:

```yaml
settings:
  interval: 10s
  extra_labels:
    development: true

tests:
  vmware_vm:
    env:
      TF_VAR_content: "This is a variable"

    before_script:
      - cmd: terraform init
        retries: 2

    script:
      - cmd: terraform apply -auto-approve

    after_script:
      - cmd: terraform destroy -auto-approve
```

Where as the commands specified in the `before_script` block would be passed to `BeforeJobRuns()`, without this PR there was no way to stop the currents jobs execution in case of an error in any of the commands in `before_script`. 

Introducing `BeforeJobRunsSkipIfBeforeFuncErrors` allows me to exit the jobs execution gracefully and reschedule to job after the given interval.

### List any changes that modify/break current functionality
`none`

### Have you included tests for your changes?
Si

### Did you document any new/modified functionality?

- [x] Updated `example_test.go`
- [ ] Updated `README.md`

### Notes



Patch:
diff --git a/.github/workflows/file_formatting.yml b/.github/workflows/file_formatting.yml
index 448affe..e8772a6 100644
--- a/.github/workflows/file_formatting.yml
+++ b/.github/workflows/file_formatting.yml
@@ -16,4 +16,4 @@ jobs:
         uses: actions/checkout@v4
       - name: verify example_test.go
         run: |
-          grep "^func " example_test.go | sort -c
+          grep "^func [a-z-A-Z]" example_test.go | sort -c
diff --git a/Makefile b/Makefile
index abaf708..de74714 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ fmt:
 	@go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {}
 
 lint:
-	@grep "^func " example_test.go | sort -c
+	@grep "^func [a-zA-Z]" example_test.go | sort -c
 	@golangci-lint run
 
 test:
diff --git a/executor.go b/executor.go
index 439cacb..3a9dc0d 100644
--- a/executor.go
+++ b/executor.go
@@ -389,8 +389,21 @@ func (e *executor) runJob(j internalJob, jIn jobIn) {
 		}
 		defer func() { _ = lock.Unlock(j.ctx) }()
 	}
+
 	_ = callJobFuncWithParams(j.beforeJobRuns, j.id, j.name)
 
+	err := callJobFuncWithParams(j.beforeJobRunsSkipIfBeforeFuncErrors, j.id, j.name)
+	if err != nil {
+		e.sendOutForRescheduling(&jIn)
+
+		select {
+		case e.jobsOutCompleted <- j.id:
+		case <-e.ctx.Done():
+		}
+
+		return
+	}
+
 	e.sendOutForRescheduling(&jIn)
 	select {
 	case e.jobsOutCompleted <- j.id:
@@ -398,7 +411,6 @@ func (e *executor) runJob(j internalJob, jIn jobIn) {
 	}
 
 	startTime := time.Now()
-	var err error
 	if j.afterJobRunsWithPanic != nil {
 		err = e.callJobWithRecover(j)
 	} else {
diff --git a/job.go b/job.go
index 4d868e6..700a0b6 100644
--- a/job.go
+++ b/job.go
@@ -40,12 +40,13 @@ type internalJob struct {
 	startImmediately   bool
 	stopTime           time.Time
 	// event listeners
-	afterJobRuns          func(jobID uuid.UUID, jobName string)
-	beforeJobRuns         func(jobID uuid.UUID, jobName string)
-	afterJobRunsWithError func(jobID uuid.UUID, jobName string, err error)
-	afterJobRunsWithPanic func(jobID uuid.UUID, jobName string, recoverData any)
-	afterLockError        func(jobID uuid.UUID, jobName string, err error)
-	disabledLocker        bool
+	afterJobRuns                        func(jobID uuid.UUID, jobName string)
+	beforeJobRuns                       func(jobID uuid.UUID, jobName string)
+	beforeJobRunsSkipIfBeforeFuncErrors func(jobID uuid.UUID, jobName string) error
+	afterJobRunsWithError               func(jobID uuid.UUID, jobName string, err error)
+	afterJobRunsWithPanic               func(jobID uuid.UUID, jobName string, recoverData any)
+	afterLockError                      func(jobID uuid.UUID, jobName string, err error)
+	disabledLocker                      bool
 
 	locker Locker
 }
@@ -724,6 +725,19 @@ func BeforeJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) Even
 	}
 }
 
+// BeforeJobRunsSkipIfBeforeFuncErrors is used to listen for when a job is about to run and
+// then runs the provided function. If the provided function returns an error, the job will be
+// rescheduled and the current run will be skipped.
+func BeforeJobRunsSkipIfBeforeFuncErrors(eventListenerFunc func(jobID uuid.UUID, jobName string) error) EventListener {
+	return func(j *internalJob) error {
+		if eventListenerFunc == nil {
+			return ErrEventListenerFuncNil
+		}
+		j.beforeJobRunsSkipIfBeforeFuncErrors = eventListenerFunc
+		return nil
+	}
+}
+
 // AfterJobRuns is used to listen for when a job has run
 // without an error, and then run the provided function.
 func AfterJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {


Test patch:
diff --git a/example_test.go b/example_test.go
index fb71c20..b077300 100644
--- a/example_test.go
+++ b/example_test.go
@@ -104,6 +104,29 @@ func ExampleBeforeJobRuns() {
 	)
 }
 
+func ExampleBeforeJobRunsSkipIfBeforeFuncErrors() {
+	s, _ := gocron.NewScheduler()
+	defer func() { _ = s.Shutdown() }()
+
+	_, _ = s.NewJob(
+		gocron.DurationJob(
+			time.Second,
+		),
+		gocron.NewTask(
+			func() {
+				fmt.Println("Will never run, because before job func errors")
+			},
+		),
+		gocron.WithEventListeners(
+			gocron.BeforeJobRunsSkipIfBeforeFuncErrors(
+				func(jobID uuid.UUID, jobName string) error {
+					return fmt.Errorf("error")
+				},
+			),
+		),
+	)
+}
+
 func ExampleCronJob() {
 	s, _ := gocron.NewScheduler()
 	defer func() { _ = s.Shutdown() }()
diff --git a/job_test.go b/job_test.go
index b729486..f3d960f 100644
--- a/job_test.go
+++ b/job_test.go
@@ -482,6 +482,13 @@ func TestWithEventListeners(t *testing.T) {
 			},
 			ErrEventListenerFuncNil,
 		},
+		{
+			"nil before job runs error listener",
+			[]EventListener{
+				BeforeJobRunsSkipIfBeforeFuncErrors(nil),
+			},
+			ErrEventListenerFuncNil,
+		},
 	}
 
 	for _, tt := range tests {