github.com/Aoi-hosizora/ahlib-more@v1.5.1-0.20230404072844-256112befaf6/xtask/xtask.go (about)

     1  package xtask
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/Aoi-hosizora/ahlib/xcolor"
     6  	"github.com/Aoi-hosizora/ahlib/xreflect"
     7  	"github.com/Aoi-hosizora/ahlib/xslice"
     8  	"github.com/robfig/cron/v3"
     9  	"log"
    10  	"reflect"
    11  	"runtime"
    12  	"sync"
    13  )
    14  
    15  // CronTask represents a job collection, or called a task, which is implemented by wrapping cron.Cron.
    16  type CronTask struct {
    17  	cron  *cron.Cron
    18  	jobs  []*FuncJob
    19  	muJob sync.RWMutex
    20  
    21  	addedCallback     func(job *FuncJob)
    22  	removedCallback   func(job *FuncJob)
    23  	scheduledCallback func(job *FuncJob)
    24  	panicHandler      func(job *FuncJob, v interface{})
    25  }
    26  
    27  // FuncJob represents a cron.Job with some information such as title, cron.Schedule and cron.Entry, stored in CronTask.
    28  type FuncJob struct {
    29  	title    string
    30  	cronSpec string        // can be empty
    31  	schedule cron.Schedule // can be nil
    32  	function func()
    33  	entry    *cron.Entry
    34  	entryID  cron.EntryID
    35  
    36  	parent *CronTask
    37  }
    38  
    39  var _ cron.Job = (*FuncJob)(nil)
    40  
    41  // ========
    42  // CronTask
    43  // ========
    44  
    45  // NewCronTask creates a default CronTask with given cron.Cron and default callbacks and handlers.
    46  func NewCronTask(c *cron.Cron) *CronTask {
    47  	return &CronTask{
    48  		cron: c,
    49  		jobs: make([]*FuncJob, 0),
    50  
    51  		addedCallback:     DefaultAddedCallback,
    52  		removedCallback:   defaultJobRemovedCallback,
    53  		scheduledCallback: nil, // defaults to do nothing
    54  		panicHandler: func(job *FuncJob, v interface{}) {
    55  			log.Printf("xtask warning: Job \"%s\" panicked with `%v`", job.title, v)
    56  		},
    57  	}
    58  }
    59  
    60  // Cron returns cron.Cron from CronTask.
    61  func (c *CronTask) Cron() *cron.Cron {
    62  	return c.cron
    63  }
    64  
    65  // ScheduleParser returns cron.ScheduleParser from cron.Cron in CronTask.
    66  func (c *CronTask) ScheduleParser() cron.ScheduleParser {
    67  	return xreflect.GetUnexportedField(xreflect.FieldValueOf(c.cron, "parser")).Interface().(cron.ScheduleParser)
    68  }
    69  
    70  // Jobs returns FuncJob slice from CronTask.
    71  func (c *CronTask) Jobs() []*FuncJob {
    72  	c.muJob.RLock()
    73  	out := c.jobs
    74  	c.muJob.RUnlock()
    75  	return out
    76  }
    77  
    78  const (
    79  	panicNilFunction = "xtask: nil job function"
    80  	panicNilSchedule = "xtask: nil cron schedule"
    81  )
    82  
    83  // AddJobByCronSpec adds a FuncJob to cron.Cron and CronTask by given repeatable title, cron spec and function.
    84  func (c *CronTask) AddJobByCronSpec(title string, spec string, f func()) (cron.EntryID, error) {
    85  	if f == nil {
    86  		panic(panicNilFunction)
    87  	}
    88  	schedule, err := c.ScheduleParser().Parse(spec) // use cron.Schedule() rather than cron.AddJob()
    89  	if err != nil {
    90  		return 0, err
    91  	}
    92  	return c.addJob(title, spec, schedule, f), nil // by spec when spec is not empty
    93  }
    94  
    95  // AddJobBySchedule adds a FuncJob to cron.Cron and CronTask by given repeatable title, cron.Schedule and function.
    96  func (c *CronTask) AddJobBySchedule(title string, schedule cron.Schedule, f func()) cron.EntryID {
    97  	if schedule == nil {
    98  		panic(panicNilSchedule)
    99  	}
   100  	if f == nil {
   101  		panic(panicNilFunction)
   102  	}
   103  	return c.addJob(title, "", schedule, f) // by schedule when spec is empty
   104  }
   105  
   106  // addJob adds given FuncJob's fields to cron.Cron using given parsed cron.Schedule and returns the added cron.EntryID.
   107  func (c *CronTask) addJob(title string, spec string, schedule cron.Schedule, f func()) cron.EntryID {
   108  	job := &FuncJob{parent: c, title: title, cronSpec: spec /* maybe empty */, schedule: schedule, function: f}
   109  
   110  	c.muJob.Lock()
   111  	id := c.cron.Schedule(schedule, job) // always use schedule
   112  	entry := c.cron.Entry(id)
   113  	job.entry = &entry
   114  	job.entryID = entry.ID
   115  	c.jobs = append(c.jobs, job)
   116  	c.muJob.Unlock()
   117  
   118  	if c.addedCallback != nil {
   119  		c.addedCallback(job)
   120  	}
   121  	return id
   122  }
   123  
   124  // RemoveJob removes a cron.Entry by given cron.EntryID from cron.Cron and CronTask.
   125  func (c *CronTask) RemoveJob(id cron.EntryID) {
   126  	c.muJob.Lock()
   127  	c.cron.Remove(id)
   128  	c.jobs = xslice.DeleteAllWithG(c.jobs, &FuncJob{entryID: id}, func(i, j interface{}) bool {
   129  		if i.(*FuncJob).entryID == j.(*FuncJob).entryID {
   130  			if c.removedCallback != nil {
   131  				c.removedCallback(i.(*FuncJob))
   132  			}
   133  			return true
   134  		}
   135  		return false
   136  	}).([]*FuncJob)
   137  	c.muJob.Unlock()
   138  }
   139  
   140  // DefaultAddedCallback is the default CronTask's addedCallback, can be modified by CronTask.SetAddedCallback.
   141  //
   142  // The default callback logs like (just like gin.DebugPrintRouteFunc):
   143  // 	[Task] job1, 0/1 * * * * *             --> ... (EntryID: 1)
   144  // 	[Task] job3, every 3s                  --> ... (EntryID: 3)
   145  // 	[Task] job4, <parsed SpecSchedule>     --> ... (EntryID: 4)
   146  // 	      |-------------------------------|   |----------------|
   147  // 	                     31                          ...
   148  func DefaultAddedCallback(j *FuncJob) {
   149  	fmt.Printf("[Task] %-31s --> %s (EntryID: %d)\n", fmt.Sprintf("%s, %s", j.Title(), j.ScheduleExpr()), j.Funcname(), j.EntryID())
   150  }
   151  
   152  // DefaultColorizedAddedCallback is the DefaultAddedCallback (CronTask's addedCallback) in color.
   153  //
   154  // The default callback logs like (just like gin.DebugPrintRouteFunc):
   155  // 	[Task] job1, 0/1 * * * * *             --> ... (EntryID: 1)
   156  // 	[Task] job3, every 3s                  --> ... (EntryID: 3)
   157  // 	[Task] job4, <parsed SpecSchedule>     --> ... (EntryID: 4)
   158  // 	      |-------------------------------|   |----------------|
   159  // 	                  31 (blue)                      ...
   160  func DefaultColorizedAddedCallback(j *FuncJob) {
   161  	fmt.Printf("[Task] %s --> %s (EntryID: %d)\n", xcolor.Blue.ASprintf(-31, "%s, %s", j.Title(), j.ScheduleExpr()), j.Funcname(), j.EntryID())
   162  }
   163  
   164  // defaultJobRemovedCallback is the default removedCallback, can be modified by CronTask.SetRemovedCallback
   165  //
   166  // The default callback logs like:
   167  // 	[Task] Remove job: job1, EntryID: 1
   168  func defaultJobRemovedCallback(j *FuncJob) {
   169  	fmt.Printf("[Task] Remove job: %s, EntryID: %d\n", j.Title(), j.EntryID())
   170  }
   171  
   172  // SetAddedCallback sets job added callback, this will be invoked after FuncJob added, defaults to DefaultAddedCallback.
   173  func (c *CronTask) SetAddedCallback(cb func(job *FuncJob)) {
   174  	c.addedCallback = cb
   175  }
   176  
   177  // SetRemovedCallback sets job removed callback, this will be invoked after FuncJob removed, defaults to defaultJobRemovedCallback.
   178  func (c *CronTask) SetRemovedCallback(cb func(job *FuncJob)) {
   179  	c.removedCallback = cb
   180  }
   181  
   182  // SetScheduledCallback sets job scheduled callback, this will be invoked when after FuncJob scheduled, defaults to do nothing.
   183  func (c *CronTask) SetScheduledCallback(cb func(job *FuncJob)) {
   184  	c.scheduledCallback = cb
   185  }
   186  
   187  // SetPanicHandler sets panic handler for jobs executing, defaults to print warning message.
   188  func (c *CronTask) SetPanicHandler(handler func(job *FuncJob, v interface{})) {
   189  	c.panicHandler = handler
   190  }
   191  
   192  // =======
   193  // FuncJob
   194  // =======
   195  
   196  // Title returns title from FuncJob.
   197  func (f *FuncJob) Title() string {
   198  	return f.title
   199  }
   200  
   201  // CronSpec returns cron spec string from FuncJob.
   202  func (f *FuncJob) CronSpec() string {
   203  	return f.cronSpec
   204  }
   205  
   206  // Schedule returns cron.Schedule from FuncJob.
   207  func (f *FuncJob) Schedule() cron.Schedule {
   208  	return f.schedule
   209  }
   210  
   211  // ScheduleExpr returns schedule expr from FuncJob, is generated by cron spec string and cron.Schedule.
   212  func (f *FuncJob) ScheduleExpr() string {
   213  	if f.cronSpec != "" { // by spec
   214  		return f.cronSpec
   215  	}
   216  	if _, ok := f.schedule.(*cron.SpecSchedule); ok {
   217  		return "<parsed SpecSchedule>" // hide origin spec
   218  	}
   219  	if s, ok := f.schedule.(cron.ConstantDelaySchedule); ok {
   220  		return fmt.Sprintf("every %s", s.Delay.String())
   221  	}
   222  	return "<unknown Schedule>"
   223  }
   224  
   225  // Funcname returns job function name from FuncJob.
   226  func (f *FuncJob) Funcname() string {
   227  	return runtime.FuncForPC(reflect.ValueOf(f.function).Pointer()).Name()
   228  }
   229  
   230  // Entry returns cron.Entry from FuncJob.
   231  func (f *FuncJob) Entry() *cron.Entry {
   232  	return f.entry
   233  }
   234  
   235  // EntryID returns cron.EntryID from FuncJob.
   236  func (f *FuncJob) EntryID() cron.EntryID {
   237  	return f.entryID
   238  }
   239  
   240  // Run runs the FuncJob with panic handler, this implements cron.Job interface.
   241  func (f *FuncJob) Run() {
   242  	defer func() {
   243  		v := recover()
   244  		if v != nil && f.parent.panicHandler != nil {
   245  			f.parent.panicHandler(f, v) // defaults to print warning message
   246  		}
   247  	}()
   248  
   249  	if f.parent.scheduledCallback != nil {
   250  		f.parent.scheduledCallback(f) // defaults to do nothing
   251  	}
   252  	f.function()
   253  }