github.com/Microsoft/azure-vhd-utils@v0.0.0-20230613175315-7c30a3748a1b/upload/concurrent/balancer.go (about)

     1  package concurrent
     2  
     3  import (
     4  	"container/heap"
     5  	"time"
     6  )
     7  
     8  // Balancer is a type that can balance load among a set of workers
     9  //
    10  type Balancer struct {
    11  	errorChan              chan error   // The channel used by all workers to report error
    12  	requestHandledChan     chan *Worker // The channel used by a worker to signal balancer that a work has been executed
    13  	tearDownChan           chan bool    // The channel that all workers listening for force quit signal
    14  	workerFinishedChan     chan *Worker // The channel that all worker used to signal balancer that it exiting
    15  	allWorkersFinishedChan chan bool    // The channel this balancer signals once all worker signals it's exit on workerFinishedChan
    16  	pool                   Pool         // Pool of workers that this load balancer balances
    17  	workerCount            int          // The number of workers
    18  }
    19  
    20  // The size of work channel associated with each worker this balancer manages.
    21  //
    22  const workerQueueSize int = 3
    23  
    24  // NewBalancer creates a new instance of Balancer that needs to balance load between 'workerCount' workers
    25  //
    26  func NewBalancer(workerCount int) *Balancer {
    27  	balancer := &Balancer{
    28  		workerCount: workerCount,
    29  		pool: Pool{
    30  			Workers: make([]*Worker, workerCount),
    31  		},
    32  	}
    33  	return balancer
    34  }
    35  
    36  // Init initializes all channels and start the workers.
    37  //
    38  func (b *Balancer) Init() {
    39  	b.errorChan = make(chan error, 0)
    40  	b.requestHandledChan = make(chan *Worker, 0)
    41  	b.workerFinishedChan = make(chan *Worker, 0)
    42  	b.allWorkersFinishedChan = make(chan bool, 0)
    43  	b.tearDownChan = make(chan bool, 0)
    44  	for i := 0; i < b.workerCount; i++ {
    45  		b.pool.Workers[i] = NewWorker(i, workerQueueSize, &(b.pool), b.errorChan, b.requestHandledChan, b.workerFinishedChan)
    46  		(b.pool.Workers[i]).Run(b.tearDownChan)
    47  	}
    48  }
    49  
    50  // TearDownWorkers sends a force quit signal to all workers, which case worker to quit as soon as possible,
    51  // workers won't drain it's request channel in this case.
    52  //
    53  func (b *Balancer) TearDownWorkers() {
    54  	close(b.tearDownChan)
    55  }
    56  
    57  // Run read request from the request channel identified by the parameter requestChan and dispatch it the worker
    58  // with least load. This method returns two channels, a channel to communicate error from any worker back to
    59  // the consumer of balancer and second channel is used by the balancer to signal consumer that all workers has
    60  // been finished executing.
    61  //
    62  func (b *Balancer) Run(requestChan <-chan *Request) (<-chan error, <-chan bool) {
    63  	// Request dispatcher
    64  	go func() {
    65  		for {
    66  			requestToHandle, ok := <-requestChan
    67  			if !ok {
    68  				b.closeWorkersRequestChannel()
    69  				return
    70  			}
    71  			b.dispatch(requestToHandle)
    72  		}
    73  	}()
    74  
    75  	// listener for worker status
    76  	go func() {
    77  		remainingWorkers := b.workerCount
    78  		for {
    79  			select {
    80  			case w := <-b.requestHandledChan:
    81  				b.completed(w)
    82  			case _ = <-b.workerFinishedChan:
    83  				remainingWorkers--
    84  				if remainingWorkers == 0 {
    85  					b.allWorkersFinishedChan <- true // All workers has been exited
    86  					return
    87  				}
    88  			}
    89  		}
    90  	}()
    91  
    92  	return b.errorChan, b.allWorkersFinishedChan
    93  }
    94  
    95  // closeWorkersRequestChannel closes the Request channel of all workers, this indicates that no
    96  // more work will not be send the channel so that the workers can gracefully exit after handling
    97  // any pending work in the channel.
    98  //
    99  func (b *Balancer) closeWorkersRequestChannel() {
   100  	for i := 0; i < b.workerCount; i++ {
   101  		close((b.pool.Workers[i]).RequestsToHandleChan)
   102  	}
   103  }
   104  
   105  // dispatch dispatches the request to the worker with least load. If all workers are completely
   106  // busy (i.e. there Pending request count is currently equal to the maximum load) then this
   107  // method will poll until one worker is available.
   108  //
   109  func (b *Balancer) dispatch(request *Request) {
   110  	for {
   111  		if b.pool.Workers[0].Pending >= workerQueueSize {
   112  			// Wait for a worker to be available
   113  			time.Sleep(500 * time.Millisecond)
   114  		} else {
   115  			b.pool.Lock()
   116  			worker := b.pool.Workers[0]
   117  			worker.Pending++
   118  			heap.Fix(&b.pool, worker.Index)
   119  			worker.RequestsToHandleChan <- request
   120  			b.pool.Unlock()
   121  			return
   122  		}
   123  	}
   124  }
   125  
   126  // completed is called when a worker finishes one work, it updates the load status of the given the
   127  // worker.
   128  //
   129  func (b *Balancer) completed(worker *Worker) {
   130  	b.pool.Lock()
   131  	worker.Pending--
   132  	heap.Fix(&b.pool, worker.Index)
   133  	b.pool.Unlock()
   134  }
   135  
   136  // WorkersCurrentLoad returns the load of the workers this balancer manages as comma separated string
   137  // values where each value consists of worker id (Worker.Id property) and pending requests associated
   138  // with the worker.
   139  //
   140  func (b *Balancer) WorkersCurrentLoad() string {
   141  	return b.pool.WorkersCurrentLoad()
   142  }