github.com/ngicks/gokugen@v0.0.5/scheduler/worker.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  )
     7  
     8  // Worker represents a single task executor.
     9  // It will work on a single task at a time.
    10  // It may be in stopped-state where loop is stopped,
    11  // working-state where looping in goroutine,
    12  // or ended-state where no way is given to step into working-state again.
    13  type Worker[T any] struct {
    14  	workingState
    15  	endState
    16  	mu             sync.Mutex
    17  	id             T
    18  	stopCh         chan struct{}
    19  	killCh         chan struct{}
    20  	taskCh         <-chan *Task
    21  	onTaskReceived func()
    22  	onTaskDone     func()
    23  	cancel         func()
    24  }
    25  
    26  func NewWorker[T any](id T, taskCh <-chan *Task, taskReceived, taskDone func()) (*Worker[T], error) {
    27  	if taskCh == nil {
    28  		return nil, ErrInvalidArg
    29  	}
    30  
    31  	if taskReceived == nil {
    32  		taskReceived = func() {}
    33  	}
    34  	if taskDone == nil {
    35  		taskDone = func() {}
    36  	}
    37  
    38  	worker := &Worker[T]{
    39  		id:             id,
    40  		stopCh:         make(chan struct{}, 1),
    41  		killCh:         make(chan struct{}),
    42  		taskCh:         taskCh,
    43  		onTaskReceived: taskReceived,
    44  		onTaskDone:     taskDone,
    45  		cancel:         func() {},
    46  	}
    47  	return worker, nil
    48  }
    49  
    50  // Start starts worker loop. So it would block long.
    51  //
    52  // If worker is already ended, it returns `ErrAlreadyEnded`.
    53  // If worker is already started, it returns `ErrAlreadyStarted`.
    54  // If taskCh is closed, Start returns nil, becoming ended-state.
    55  func (w *Worker[T]) Start() (err error) {
    56  	if w.IsEnded() {
    57  		return ErrAlreadyEnded
    58  	}
    59  	if !w.setWorking() {
    60  		return ErrAlreadyStarted
    61  	}
    62  	defer w.setWorking(false)
    63  
    64  	defer func() {
    65  		select {
    66  		case <-w.stopCh:
    67  		default:
    68  		}
    69  	}()
    70  
    71  	var normalReturn bool
    72  	defer func() {
    73  		if !normalReturn {
    74  			w.setEnded()
    75  		}
    76  	}()
    77  
    78  LOOP:
    79  	for {
    80  		select {
    81  		case <-w.killCh:
    82  			// in case of racy kill
    83  			break LOOP
    84  		case <-w.stopCh:
    85  			break LOOP
    86  		default:
    87  			select {
    88  			case <-w.stopCh:
    89  				break LOOP
    90  			case task, ok := <-w.taskCh:
    91  				if !ok {
    92  					w.setEnded()
    93  					break LOOP
    94  				}
    95  				func() {
    96  					ctx, cancel := context.WithCancel(context.Background())
    97  					defer cancel()
    98  
    99  					w.mu.Lock()
   100  					w.cancel = cancel
   101  					w.mu.Unlock()
   102  
   103  					select {
   104  					// in case of racy kill
   105  					case <-w.killCh:
   106  						return
   107  					default:
   108  					}
   109  
   110  					w.onTaskReceived()
   111  					defer w.onTaskDone()
   112  					task.Do(ctx)
   113  				}()
   114  			}
   115  		}
   116  	}
   117  	// If task exits abnormally, called runtime.Goexit or panicking, it would not reach this line.
   118  	normalReturn = true
   119  	return
   120  }
   121  
   122  // Stop stops an active Start loop.
   123  // If Start is not in use when Stop is called,
   124  // it will stops next Start immediately.
   125  func (w *Worker[T]) Stop() {
   126  	select {
   127  	case <-w.stopCh:
   128  	default:
   129  	}
   130  	w.stopCh <- struct{}{}
   131  	return
   132  }
   133  
   134  // Kill kills this worker.
   135  // If a task is being worked at the time of invocation,
   136  // a contex passed to the task will be cancelled immediately.
   137  // Kill makes this worker to step into ended state, making it impossible to Start-ed again.
   138  func (w *Worker[T]) Kill() {
   139  	if w.setEnded() {
   140  		close(w.killCh)
   141  	}
   142  
   143  	w.mu.Lock()
   144  	w.cancel()
   145  	w.mu.Unlock()
   146  
   147  	w.Stop()
   148  }
   149  
   150  func (w *Worker[T]) Id() T {
   151  	return w.id
   152  }