github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/workerpool/workerpool.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package workerpool
     5  
     6  import (
     7  	"strings"
     8  	"sync"
     9  
    10  	"github.com/juju/errors"
    11  )
    12  
    13  // Logger defines the logging methods that the worker pool uses.
    14  type Logger interface {
    15  	Errorf(string, ...interface{})
    16  	Debugf(string, ...interface{})
    17  	Tracef(string, ...interface{})
    18  }
    19  
    20  // Task represents a unit of work which should be executed by the pool workers.
    21  type Task struct {
    22  	// Type is a short, optional description of the task which will be
    23  	// included in emitted log messages.
    24  	Type string
    25  
    26  	// Process encapsulates the logic for processing a task.
    27  	Process func() error
    28  }
    29  
    30  // WorkerPool implements a fixed-size worker pool for processing tasks in
    31  // parallel. If any of the tasks fails, the pool workers will be shut down, and
    32  // any detected errors (different tasks may fail concurrently) will be
    33  // consolidated and reported when the pool's Close() method is invoked.
    34  //
    35  // New tasks can be enqueued by writing to the channel returned by the
    36  // pool's Queue() method while the pool's Done() method returns a channel
    37  // which can be used by callers to detect when an error occurred and the
    38  // pool is shutting down.
    39  type WorkerPool struct {
    40  	logger Logger
    41  
    42  	// A channel used to signal workers that they should finish their
    43  	// work and exit.
    44  	shutdownTriggerCh chan struct{}
    45  
    46  	// The worker pool can be stopped either via a call to Shutdown() or
    47  	// directly by one of the workers when it encounters an error. A
    48  	// sync.Once primitive ensures that the shutdown trigger channel can
    49  	// only be closed once.
    50  	shutdownTrigger   sync.Once
    51  	closeErrorChannel sync.Once
    52  
    53  	// A waitgroup for ensuring that all workers have exited. It is used
    54  	// both when the pool is being shutdown and also as a barrier to ensure
    55  	// that workers have exited before draining the error reporting channel.
    56  	wg sync.WaitGroup
    57  
    58  	// A buffered channel (cap equal to pool size) which workers monitor
    59  	// for incoming processing tasks.
    60  	taskQueueCh chan Task
    61  
    62  	// A buffered channel (cap equal to pool size) which workers emit
    63  	// any errors they encounter before taskuesting the pool to be shut
    64  	// down.
    65  	taskErrCh chan error
    66  }
    67  
    68  // NewWorkerPool returns a pool with the taskuested number of workers. Callers
    69  // must ensure to call the pool's Close() method to avoid leaking goroutines.
    70  func NewWorkerPool(logger Logger, size int) *WorkerPool {
    71  	// Size must be at least one
    72  	if size <= 0 {
    73  		size = 1
    74  	}
    75  
    76  	wp := &WorkerPool{
    77  		logger:            logger,
    78  		shutdownTriggerCh: make(chan struct{}),
    79  		taskQueueCh:       make(chan Task, size),
    80  		taskErrCh:         make(chan error, size),
    81  	}
    82  
    83  	wp.wg.Add(size)
    84  	for workerID := 0; workerID < size; workerID++ {
    85  		wp.logger.Tracef("worker %d: starting new worker pool", workerID)
    86  		go wp.taskWorker(workerID)
    87  	}
    88  
    89  	return wp
    90  }
    91  
    92  // Size returns the number of workers in the pool.
    93  func (wp *WorkerPool) Size() int {
    94  	return cap(wp.taskQueueCh)
    95  }
    96  
    97  // Queue returns a channel for enqueueing processing tasks.
    98  func (wp *WorkerPool) Queue() chan<- Task {
    99  	return wp.taskQueueCh
   100  }
   101  
   102  // Done returns a channel which is closed if the pool has detected one or more
   103  // errors and is shutting down. Callers must then invoke the pool's Close method
   104  // to obtain any reported errors.
   105  func (wp *WorkerPool) Done() <-chan struct{} {
   106  	return wp.shutdownTriggerCh
   107  }
   108  
   109  // Close the pool and return any queued errors. The method signals and waits
   110  // for all workers to exit before draining the worker error channel and
   111  // returning a combined error (if any errors were reported) value. After a call
   112  // to Shutdown, no further provision tasks will be accepted by the pool.
   113  func (wp *WorkerPool) Close() error {
   114  	wp.triggerShutdown()
   115  	wp.wg.Wait() // wait for workers to exit and write any errors they encounter
   116  
   117  	// Now we can safely close and drain the error channel.
   118  	wp.closeErrorChannel.Do(func() {
   119  		close(wp.taskErrCh)
   120  	})
   121  
   122  	var errList []string
   123  	for err := range wp.taskErrCh {
   124  		errList = append(errList, err.Error())
   125  	}
   126  
   127  	if len(errList) == 0 {
   128  		return nil
   129  	}
   130  	return errors.New(strings.Join(errList, "\n"))
   131  }
   132  
   133  func (wp *WorkerPool) taskWorker(workerID int) {
   134  	defer wp.wg.Done()
   135  	for {
   136  		select {
   137  		case task := <-wp.taskQueueCh:
   138  			wp.logger.Debugf("worker %d: processing task %q", workerID, task.Type)
   139  			if err := task.Process(); err != nil {
   140  				wp.logger.Errorf("worker %d: shutting down pool due to error while handling a %q task: %v", workerID, task.Type, err)
   141  
   142  				// This is a buffered channel to allow every pool worker to report
   143  				// a single error before it exits. Consequently, this call can never
   144  				// block.
   145  				wp.taskErrCh <- err
   146  				wp.triggerShutdown()
   147  
   148  				return // worker cannot process any further tasks.
   149  			}
   150  		case <-wp.shutdownTriggerCh:
   151  			wp.logger.Tracef("worker %d: terminating as worker pool is shutting down", workerID)
   152  			return
   153  		}
   154  	}
   155  }
   156  
   157  // triggerShutdown notifies all workers to exit once they complete the tasks
   158  // they are currently processing. Workers can only be notified once; subsequent
   159  // calls to this method are no-ops.
   160  func (wp *WorkerPool) triggerShutdown() {
   161  	wp.shutdownTrigger.Do(func() {
   162  		close(wp.shutdownTriggerCh)
   163  	})
   164  }