浏览代码

Merge pull request #51 from blackskad/cron-job-panic-recovery

Recover from panics in cron jobs
Rob Figueiredo 9 年之前
父节点
当前提交
0f39cf7ebc
共有 2 个文件被更改,包括 58 次插入1 次删除
  1. 26 1
      cron.go
  2. 32 0
      cron_test.go

+ 26 - 1
cron.go

@@ -3,6 +3,8 @@
 package cron
 
 import (
+	"log"
+	"runtime"
 	"sort"
 	"time"
 )
@@ -16,6 +18,7 @@ type Cron struct {
 	add      chan *Entry
 	snapshot chan []*Entry
 	running  bool
+	ErrorLog *log.Logger
 }
 
 // Job is an interface for submitted cron jobs.
@@ -74,6 +77,7 @@ func New() *Cron {
 		stop:     make(chan struct{}),
 		snapshot: make(chan []*Entry),
 		running:  false,
+		ErrorLog: nil,
 	}
 }
 
@@ -127,6 +131,18 @@ func (c *Cron) Start() {
 	go c.run()
 }
 
+func (c *Cron) runWithRecovery(j Job) {
+	defer func() {
+		if r := recover(); r != nil {
+			const size = 64 << 10
+			buf := make([]byte, size)
+			buf = buf[:runtime.Stack(buf, false)]
+			c.logf("cron: panic running job: %v\n%s", r, buf)
+		}
+	}()
+	j.Run()
+}
+
 // Run the scheduler.. this is private just due to the need to synchronize
 // access to the 'running' state variable.
 func (c *Cron) run() {
@@ -156,7 +172,7 @@ func (c *Cron) run() {
 				if e.Next != effective {
 					break
 				}
-				go e.Job.Run()
+				go c.runWithRecovery(e.Job)
 				e.Prev = e.Next
 				e.Next = e.Schedule.Next(effective)
 			}
@@ -178,6 +194,15 @@ func (c *Cron) run() {
 	}
 }
 
+// Logs an error to stderr or to the configured error log
+func (c *Cron) logf(format string, args ...interface{}) {
+	if c.ErrorLog != nil {
+		c.ErrorLog.Printf(format, args...)
+	} else {
+		log.Printf(format, args...)
+	}
+}
+
 // Stop stops the cron scheduler if it is running; otherwise it does nothing.
 func (c *Cron) Stop() {
 	if !c.running {

+ 32 - 0
cron_test.go

@@ -12,6 +12,38 @@ import (
 // compensate for a few milliseconds of runtime.
 const ONE_SECOND = 1*time.Second + 10*time.Millisecond
 
+func TestFuncPanicRecovery(t *testing.T) {
+	cron := New()
+	cron.Start()
+	defer cron.Stop()
+	cron.AddFunc("* * * * * ?", func() { panic("YOLO") })
+
+	select {
+	case <-time.After(ONE_SECOND):
+		return
+	}
+}
+
+type DummyJob struct{}
+
+func (d DummyJob) Run() {
+	panic("YOLO")
+}
+
+func TestJobPanicRecovery(t *testing.T) {
+	var job DummyJob
+
+	cron := New()
+	cron.Start()
+	defer cron.Stop()
+	cron.AddJob("* * * * * ?", job)
+
+	select {
+	case <-time.After(ONE_SECOND):
+		return
+	}
+}
+
 // Start and stop cron with no entries.
 func TestNoEntries(t *testing.T) {
 	cron := New()