go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/async/worker.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package async
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  )
    14  
    15  // NewWorker creates a new worker.
    16  func NewWorker[T any](action WorkAction[T]) *Worker[T] {
    17  	return &Worker[T]{
    18  		Latch:   NewLatch(),
    19  		Context: context.Background(),
    20  		Action:  action,
    21  		Work:    make(chan T),
    22  	}
    23  }
    24  
    25  // WorkAction is an action handler for a queue.
    26  type WorkAction[T any] func(context.Context, T) error
    27  
    28  // WorkerFinalizer is an action handler for a queue.
    29  type WorkerFinalizer[T any] func(context.Context, *Worker[T]) error
    30  
    31  // Worker is a worker that is pushed work over a channel.
    32  // It is used by other work distribution types (i.e. queue and batch)
    33  // but can also be used independently.
    34  type Worker[T any] struct {
    35  	*Latch
    36  
    37  	Context   context.Context
    38  	Action    WorkAction[T]
    39  	Finalizer WorkerFinalizer[T]
    40  
    41  	SkipRecover bool
    42  	Errors      chan error
    43  	Work        chan T
    44  }
    45  
    46  // Background returns the queue worker background context.
    47  func (w *Worker[T]) Background() context.Context {
    48  	if w.Context != nil {
    49  		return w.Context
    50  	}
    51  	return context.Background()
    52  }
    53  
    54  // NotifyStarted returns the underlying latch signal.
    55  func (w *Worker[T]) NotifyStarted() <-chan struct{} {
    56  	return w.Latch.NotifyStarted()
    57  }
    58  
    59  // NotifyStopped returns the underlying latch signal.
    60  func (w *Worker[T]) NotifyStopped() <-chan struct{} {
    61  	return w.Latch.NotifyStarted()
    62  }
    63  
    64  // Start starts the worker with a given context.
    65  func (w *Worker[T]) Start() error {
    66  	if !w.Latch.CanStart() {
    67  		return ErrCannotStart
    68  	}
    69  	w.Latch.Starting()
    70  	w.Dispatch()
    71  	return nil
    72  }
    73  
    74  // Dispatch starts the listen loop for work.
    75  func (w *Worker[T]) Dispatch() {
    76  	w.Latch.Started()
    77  	defer w.Latch.Stopped()
    78  
    79  	var workItem T
    80  	var stopping <-chan struct{}
    81  	for {
    82  		stopping = w.Latch.NotifyStopping()
    83  		select {
    84  		case <-stopping:
    85  			return
    86  		case <-w.Background().Done():
    87  			return
    88  		default:
    89  		}
    90  
    91  		// block on work or stopping
    92  		select {
    93  		case workItem = <-w.Work:
    94  			w.Execute(w.Background(), workItem)
    95  		case <-stopping:
    96  			return
    97  		case <-w.Background().Done():
    98  			return
    99  		}
   100  	}
   101  }
   102  
   103  // Execute invokes the action and recovers panics.
   104  func (w *Worker[T]) Execute(ctx context.Context, workItem T) {
   105  	defer func() {
   106  		if !w.SkipRecover {
   107  			if r := recover(); r != nil {
   108  				w.HandlePanic(r)
   109  			}
   110  		}
   111  		if w.Finalizer != nil {
   112  			w.HandleError(w.Finalizer(ctx, w))
   113  		}
   114  	}()
   115  	if w.Action != nil {
   116  		w.HandleError(w.Action(ctx, workItem))
   117  	}
   118  }
   119  
   120  // Stop stops the worker.
   121  //
   122  // If there is an item left in the work channel
   123  // it will be processed synchronously.
   124  func (w *Worker[T]) Stop() error {
   125  	if !w.Latch.CanStop() {
   126  		return ErrCannotStop
   127  	}
   128  	w.Latch.WaitStopped()
   129  	w.Latch.Reset()
   130  	return nil
   131  }
   132  
   133  // StopContext stops the worker in a given cancellation context.
   134  func (w *Worker[T]) StopContext(ctx context.Context) {
   135  	stopped := make(chan struct{})
   136  	go func() {
   137  		defer func() {
   138  			w.Latch.Reset()
   139  			close(stopped)
   140  		}()
   141  
   142  		w.Latch.WaitStopped()
   143  		if workLeft := len(w.Work); workLeft > 0 {
   144  			for x := 0; x < workLeft; x++ {
   145  				w.Execute(ctx, <-w.Work)
   146  			}
   147  		}
   148  	}()
   149  	select {
   150  	case <-stopped:
   151  		return
   152  	case <-ctx.Done():
   153  		return
   154  	}
   155  }
   156  
   157  // HandleError sends a non-nil err to the error
   158  // collector if one is provided.
   159  func (w *Worker[T]) HandlePanic(r interface{}) {
   160  	if r == nil {
   161  		return
   162  	}
   163  	if w.Errors == nil {
   164  		return
   165  	}
   166  	w.Errors <- fmt.Errorf("%v", r)
   167  }
   168  
   169  // HandleError sends a non-nil err to the error
   170  // collector if one is provided.
   171  func (w *Worker[T]) HandleError(err error) {
   172  	if err == nil {
   173  		return
   174  	}
   175  	if w.Errors == nil {
   176  		return
   177  	}
   178  	w.Errors <- err
   179  }