github.com/blend/go-sdk@v1.20240719.1/async/worker.go (about)

     1  /*
     2  
     3  Copyright (c) 2024 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package async
     9  
    10  import (
    11  	"context"
    12  
    13  	"github.com/blend/go-sdk/ex"
    14  )
    15  
    16  // NewWorker creates a new worker.
    17  func NewWorker(action WorkAction) *Worker {
    18  	return &Worker{
    19  		Latch:   NewLatch(),
    20  		Context: context.Background(),
    21  		Action:  action,
    22  		Work:    make(chan interface{}),
    23  	}
    24  }
    25  
    26  // Worker is a worker that is pushed work over a channel.
    27  // It is used by other work distribution types (i.e. queue and batch)
    28  // but can also be used independently.
    29  type Worker struct {
    30  	*Latch
    31  
    32  	Context   context.Context
    33  	Action    WorkAction
    34  	Finalizer WorkerFinalizer
    35  
    36  	SkipRecoverPanics bool
    37  	Errors            chan error
    38  	Work              chan interface{}
    39  }
    40  
    41  // Background returns the queue worker background context.
    42  func (w *Worker) Background() context.Context {
    43  	if w.Context != nil {
    44  		return w.Context
    45  	}
    46  	return context.Background()
    47  }
    48  
    49  // NotifyStarted returns the underlying latch signal.
    50  func (w *Worker) NotifyStarted() <-chan struct{} {
    51  	return w.Latch.NotifyStarted()
    52  }
    53  
    54  // NotifyStopped returns the underlying latch signal.
    55  func (w *Worker) NotifyStopped() <-chan struct{} {
    56  	return w.Latch.NotifyStopped()
    57  }
    58  
    59  // Enqueue adds an item to the work queue.
    60  func (w *Worker) Enqueue(obj interface{}) {
    61  	w.Work <- obj
    62  }
    63  
    64  // Start starts the worker with a given context.
    65  func (w *Worker) Start() error {
    66  	if !w.Latch.CanStart() {
    67  		return ex.New(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) Dispatch() {
    76  	w.Latch.Started()
    77  	defer w.Latch.Stopped()
    78  
    79  	var workItem interface{}
    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) Execute(ctx context.Context, workItem interface{}) {
   105  	defer func() {
   106  		if !w.SkipRecoverPanics {
   107  			if r := recover(); r != nil {
   108  				w.HandleError(ex.New(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) Stop() error {
   125  	if !w.Latch.CanStop() {
   126  		return ex.New(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) 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) HandleError(err error) {
   160  	if err == nil {
   161  		return
   162  	}
   163  	if w.Errors == nil {
   164  		return
   165  	}
   166  	w.Errors <- err
   167  }