github.com/ngicks/gokugen@v0.0.5/cron/cron_like_rescheduler.go (about)

     1  package cron
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sync"
     8  	"sync/atomic"
     9  	"time"
    10  
    11  	"github.com/ngicks/gokugen"
    12  )
    13  
    14  var (
    15  	ErrOnceTask          = errors.New("task returned same schedule time")
    16  	ErrNonexistentWorkId = errors.New("nonexistent work id")
    17  	ErrStillWorking      = errors.New("task is still working")
    18  	ErrAlreadyScheduled  = errors.New("task is already scheduled")
    19  )
    20  
    21  type WorkFnWParam = gokugen.WorkFnWParam
    22  type WorkRegistry interface {
    23  	Load(key string) (value WorkFnWParam, ok bool)
    24  }
    25  
    26  type Scheduler interface {
    27  	Schedule(ctx gokugen.SchedulerContext) (gokugen.Task, error)
    28  }
    29  
    30  type RowLike interface {
    31  	NextScheduler
    32  	GetCommand() []string
    33  }
    34  
    35  // CronLikeRescheduler schedules command of RowLike according to its configuration.
    36  type CronLikeRescheduler struct {
    37  	mu               sync.Mutex
    38  	err              error
    39  	scheduler        Scheduler
    40  	row              RowLike
    41  	state            *ScheduleState
    42  	scheduled        bool
    43  	isWorking        int64
    44  	ongoingTask      gokugen.Task
    45  	shouldReschedule func(workErr error, callCount int) bool
    46  	workRegistry     WorkRegistry
    47  }
    48  
    49  func NewCronLikeRescheduler(
    50  	rowLike RowLike,
    51  	whence time.Time,
    52  	shouldReschedule func(workErr error, callCount int) bool,
    53  	scheduler Scheduler,
    54  	workRegistry WorkRegistry,
    55  ) *CronLikeRescheduler {
    56  	return &CronLikeRescheduler{
    57  		row:              rowLike,
    58  		state:            NewScheduleState(rowLike, whence),
    59  		scheduler:        scheduler,
    60  		shouldReschedule: shouldReschedule,
    61  		workRegistry:     workRegistry,
    62  	}
    63  }
    64  
    65  // Schedule starts scheduling.
    66  // If shouldReschedule is non nil and if it returns true,
    67  // Rowlike would be rescheduled to its next time.
    68  //
    69  // - ErrStillWorking is returned if task c created is still being worked on.
    70  //   - Schedule right after Cancel may cause this state. No overlapping schedule is not allowed.
    71  // - ErrAlreadyScheduled is returned if second or more call is without preceding Cancel.
    72  // - ErrOnceTask is returned if RowLike is once task.
    73  //   - c is returned if command of RowLike is invalid.
    74  // - ErrNonexistentWorkId is returned when command does not exist in WorkRegistry.
    75  // - Scheduler's error is returned when Schedule returns error.
    76  //
    77  // ErrOnceTask, ErrOnceTask and Scheduler's error are sticky.
    78  // Once Schedule returned it, Schedule always return that error.
    79  //
    80  // All error may or may not be wrapped. User should use errors.Is().
    81  func (c *CronLikeRescheduler) Schedule() error {
    82  	c.mu.Lock()
    83  	defer c.mu.Unlock()
    84  
    85  	if c.err != nil {
    86  		return c.err
    87  	}
    88  
    89  	if atomic.LoadInt64(&c.isWorking) == 1 {
    90  		// Schedule right after Cancel may lead to this state.
    91  		// Overlapping scheduling is not allowed.
    92  		return ErrStillWorking
    93  	}
    94  
    95  	if c.ongoingTask != nil || c.scheduled {
    96  		return ErrAlreadyScheduled
    97  	}
    98  	c.scheduled = true
    99  
   100  	c.mu.Unlock()
   101  	// defer-ing in case of runtime panic.
   102  	defer c.mu.Lock()
   103  	// schedule takes lock by itself.
   104  	err := c.schedule()
   105  	return err
   106  }
   107  
   108  func (c *CronLikeRescheduler) schedule() error {
   109  	c.mu.Lock()
   110  	defer c.mu.Unlock()
   111  
   112  	if errors.Is(c.err, ErrOnceTask) {
   113  		return nil
   114  	} else if c.err != nil {
   115  		return c.err
   116  	}
   117  
   118  	same, callCount, next := c.state.Next()
   119  	if same {
   120  		c.err = ErrOnceTask
   121  	}
   122  
   123  	command := c.row.GetCommand()
   124  	if command == nil || len(command) == 0 {
   125  		c.err = ErrMalformed
   126  		return c.err
   127  	}
   128  
   129  	workRaw, ok := c.workRegistry.Load(command[0])
   130  	if !ok {
   131  		c.err = fmt.Errorf("%w: workId = %s", ErrNonexistentWorkId, command[0])
   132  		return c.err
   133  	}
   134  
   135  	task, err := c.scheduler.Schedule(
   136  		gokugen.BuildContext(
   137  			next,
   138  			nil,
   139  			nil,
   140  			gokugen.WithWorkId(command[0]),
   141  			gokugen.WithParam(command[1:]),
   142  			gokugen.WithWorkFn(
   143  				func(taskCtx context.Context, scheduled time.Time) (any, error) {
   144  					atomic.StoreInt64(&c.isWorking, 1)
   145  					defer atomic.StoreInt64(&c.isWorking, 0)
   146  
   147  					ret, err := workRaw(taskCtx, scheduled, command[1:])
   148  					if c.shouldReschedule != nil && c.shouldReschedule(err, callCount) {
   149  						c.schedule()
   150  					}
   151  					return ret, err
   152  				},
   153  			),
   154  		),
   155  	)
   156  	if err != nil {
   157  		c.err = err
   158  		return c.err
   159  	}
   160  	if task != nil {
   161  		c.ongoingTask = task
   162  	}
   163  	return nil
   164  }
   165  
   166  // Cancel stops scheduling of c.
   167  // If a task is being worked on at the time Cancel is called,
   168  // this also cancels task itself.
   169  func (c *CronLikeRescheduler) Cancel() (cancelled bool) {
   170  	c.mu.Lock()
   171  	defer c.mu.Unlock()
   172  
   173  	c.scheduled = false
   174  	if c.ongoingTask != nil {
   175  		cancelled := c.ongoingTask.Cancel()
   176  		c.ongoingTask = nil
   177  		return cancelled
   178  	}
   179  	return
   180  }