github.com/blend/go-sdk@v1.20220411.3/async/queue.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - 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  	"runtime"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/blend/go-sdk/ex"
    17  )
    18  
    19  // NewQueue returns a new parallel queue.
    20  func NewQueue(action WorkAction, options ...QueueOption) *Queue {
    21  	q := Queue{
    22  		Latch:               NewLatch(),
    23  		Action:              action,
    24  		Context:             context.Background(),
    25  		MaxWork:             DefaultQueueMaxWork,
    26  		Parallelism:         runtime.NumCPU(),
    27  		ShutdownGracePeriod: DefaultShutdownGracePeriod,
    28  	}
    29  	for _, option := range options {
    30  		option(&q)
    31  	}
    32  	return &q
    33  }
    34  
    35  // QueueOption is an option for the queue worker.
    36  type QueueOption func(*Queue)
    37  
    38  // OptQueueParallelism sets the queue worker parallelism.
    39  func OptQueueParallelism(parallelism int) QueueOption {
    40  	return func(q *Queue) {
    41  		q.Parallelism = parallelism
    42  	}
    43  }
    44  
    45  // OptQueueMaxWork sets the queue worker max work.
    46  func OptQueueMaxWork(maxWork int) QueueOption {
    47  	return func(q *Queue) {
    48  		q.MaxWork = maxWork
    49  	}
    50  }
    51  
    52  // OptQueueErrors sets the queue worker start error channel.
    53  func OptQueueErrors(errors chan error) QueueOption {
    54  	return func(q *Queue) {
    55  		q.Errors = errors
    56  	}
    57  }
    58  
    59  // OptQueueContext sets the queue worker context.
    60  func OptQueueContext(ctx context.Context) QueueOption {
    61  	return func(q *Queue) {
    62  		q.Context = ctx
    63  	}
    64  }
    65  
    66  // Queue is a queue with multiple workers.
    67  type Queue struct {
    68  	*Latch
    69  
    70  	Action              WorkAction
    71  	Context             context.Context
    72  	Errors              chan error
    73  	Parallelism         int
    74  	MaxWork             int
    75  	ShutdownGracePeriod time.Duration
    76  
    77  	// these will typically be set by Start
    78  	AvailableWorkers chan *Worker
    79  	Workers          []*Worker
    80  	Work             chan interface{}
    81  }
    82  
    83  // Background returns a background context.
    84  func (q *Queue) Background() context.Context {
    85  	if q.Context != nil {
    86  		return q.Context
    87  	}
    88  	return context.Background()
    89  }
    90  
    91  // Enqueue adds an item to the work queue.
    92  func (q *Queue) Enqueue(obj interface{}) {
    93  	q.Work <- obj
    94  }
    95  
    96  // Start starts the queue and its workers.
    97  // This call blocks.
    98  func (q *Queue) Start() error {
    99  	if !q.Latch.CanStart() {
   100  		return ex.New(ErrCannotStart)
   101  	}
   102  	q.Latch.Starting()
   103  
   104  	// create channel(s)
   105  	q.Work = make(chan interface{}, q.MaxWork)
   106  	q.AvailableWorkers = make(chan *Worker, q.Parallelism)
   107  	q.Workers = make([]*Worker, q.Parallelism)
   108  
   109  	for x := 0; x < q.Parallelism; x++ {
   110  		worker := NewWorker(q.Action)
   111  		worker.Context = q.Context
   112  		worker.Errors = q.Errors
   113  		worker.Finalizer = q.ReturnWorker
   114  
   115  		// start the worker on its own goroutine
   116  		go func() { _ = worker.Start() }()
   117  		<-worker.NotifyStarted()
   118  		q.AvailableWorkers <- worker
   119  		q.Workers[x] = worker
   120  	}
   121  	q.Dispatch()
   122  	return nil
   123  }
   124  
   125  // Dispatch processes work items in a loop.
   126  func (q *Queue) Dispatch() {
   127  	q.Latch.Started()
   128  	defer q.Latch.Stopped()
   129  
   130  	var workItem interface{}
   131  	var worker *Worker
   132  	var stopping <-chan struct{}
   133  	for {
   134  		stopping = q.Latch.NotifyStopping()
   135  		select {
   136  		case <-stopping:
   137  			return
   138  		case <-q.Background().Done():
   139  			return
   140  		default:
   141  		}
   142  
   143  		select {
   144  		case <-stopping:
   145  			return
   146  		case <-q.Background().Done():
   147  			return
   148  		case workItem = <-q.Work:
   149  			select {
   150  			case <-stopping:
   151  				q.Work <- workItem
   152  				return
   153  			case <-q.Background().Done():
   154  				q.Work <- workItem
   155  				return
   156  			case worker = <-q.AvailableWorkers:
   157  				worker.Enqueue(workItem)
   158  			}
   159  		}
   160  	}
   161  }
   162  
   163  // Stop stops the queue and processes any remaining items.
   164  func (q *Queue) Stop() error {
   165  	if !q.Latch.CanStop() {
   166  		return ex.New(ErrCannotStop)
   167  	}
   168  	q.Latch.WaitStopped() // wait for the dispatch loop to exit
   169  	defer q.Latch.Reset() // reset the latch in case we have to start again
   170  
   171  	timeoutContext, cancel := context.WithTimeout(q.Background(), q.ShutdownGracePeriod)
   172  	defer cancel()
   173  
   174  	if remainingWork := len(q.Work); remainingWork > 0 {
   175  		for x := 0; x < remainingWork; x++ {
   176  			// check the timeout first
   177  			select {
   178  			case <-timeoutContext.Done():
   179  				return nil
   180  			default:
   181  			}
   182  
   183  			select {
   184  			case <-timeoutContext.Done():
   185  				return nil
   186  			case workItem := <-q.Work:
   187  				select {
   188  				case <-timeoutContext.Done():
   189  					return nil
   190  				case worker := <-q.AvailableWorkers:
   191  					worker.Work <- workItem
   192  				}
   193  			}
   194  		}
   195  	}
   196  
   197  	workersStopped := make(chan struct{})
   198  	go func() {
   199  		defer close(workersStopped)
   200  		wg := sync.WaitGroup{}
   201  		wg.Add(len(q.Workers))
   202  		for _, worker := range q.Workers {
   203  			go func(w *Worker) {
   204  				defer wg.Done()
   205  				w.StopContext(timeoutContext)
   206  			}(worker)
   207  		}
   208  		wg.Wait()
   209  	}()
   210  
   211  	select {
   212  	case <-timeoutContext.Done():
   213  		return nil
   214  	case <-workersStopped:
   215  		return nil
   216  	}
   217  }
   218  
   219  // Close stops the queue.
   220  // Any work left in the queue will be discarded.
   221  func (q *Queue) Close() error {
   222  	q.Latch.WaitStopped()
   223  	q.Latch.Reset()
   224  	return nil
   225  }
   226  
   227  // ReturnWorker returns a given worker to the worker queue.
   228  func (q *Queue) ReturnWorker(ctx context.Context, worker *Worker) error {
   229  	q.AvailableWorkers <- worker
   230  	return nil
   231  }