go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/sync/parallel/buffer.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package parallel
    16  
    17  import (
    18  	"container/list"
    19  	"sync"
    20  	"sync/atomic"
    21  )
    22  
    23  // A Buffer embeds a Runner, overriding its RunOne method to buffer tasks
    24  // indefinitely without blocking.
    25  type Buffer struct {
    26  	Runner
    27  
    28  	// lifo, if non-zero-indicates a LIFO task dispatch or, if zero, a FIFO task
    29  	// dispatch. For more informatio, see SetFIFO.
    30  	lifo int32
    31  
    32  	// initOnce ensures that the Buffer is initialized at most once.
    33  	initOnce sync.Once
    34  	// workC receives enqueued tasks for processing.
    35  	workC chan WorkItem
    36  	// tasksFinishedC is used to signal Close when our list has finished
    37  	// dispatching tasks.
    38  	tasksFinishedC chan struct{}
    39  }
    40  
    41  func (b *Buffer) init() {
    42  	b.initOnce.Do(func() {
    43  		b.workC = make(chan WorkItem)
    44  		b.tasksFinishedC = make(chan struct{})
    45  
    46  		go b.process()
    47  	})
    48  }
    49  
    50  // process enqueues tasks into the Buffer and dispatches them to the underlying
    51  // Runner when available.
    52  func (b *Buffer) process() {
    53  	defer close(b.tasksFinishedC)
    54  
    55  	// outC is the channel that we send work to. We toggle it between nil and our
    56  	// Runner's WorkC depending on whether we have work.
    57  	//
    58  	// cur is the current work to send. It is only valid if hasWork is true.
    59  	var outC chan<- WorkItem
    60  	var cur WorkItem
    61  
    62  	// This is our work buffer. If we have unsent work, any additional work will
    63  	// be written to this buffer.
    64  	var buf list.List
    65  
    66  	// Our main processing loop.
    67  	inC := b.workC
    68  	for {
    69  		select {
    70  		case work, ok := <-inC:
    71  			if !ok {
    72  				// Our work channel has been closed. We aren't accepting any new tasks.
    73  				if outC == nil && buf.Len() == 0 {
    74  					// We have no buffered work; exit immediately.
    75  					return
    76  				}
    77  
    78  				// Mark that we're closed. When all of our work drains, we will exit.
    79  				inC = nil
    80  				break
    81  			}
    82  
    83  			// If we have no immediate work, send "work" directly; otherwise, buffer
    84  			// work for future sending.
    85  			if outC == nil {
    86  				cur = work
    87  				outC = b.Runner.WorkC()
    88  			} else {
    89  				buf.PushBack(&work)
    90  			}
    91  
    92  		case outC <- cur:
    93  			// "cur" has been sent. Dequeue the next work item, or set outC to nil if
    94  			// there are no more items.
    95  			switch {
    96  			case buf.Len() > 0:
    97  				var e *list.Element
    98  				if b.isFIFO() {
    99  					e = buf.Front()
   100  				} else {
   101  					e = buf.Back()
   102  				}
   103  
   104  				cur = *(buf.Remove(e).(*WorkItem))
   105  			case inC == nil:
   106  				//  There's no more immediate work, no buffered work, and we're closed,
   107  				//  so we're finished.
   108  				return
   109  			default:
   110  				// No more work to send.
   111  				outC = nil
   112  			}
   113  		}
   114  	}
   115  }
   116  
   117  // Run implements the same semantics as Runner's Run. However, if the
   118  // dispatch pipeline is full, Run will buffer the work and return immediately
   119  // rather than block.
   120  func (b *Buffer) Run(gen func(chan<- func() error)) <-chan error {
   121  	return b.runThen(gen, nil)
   122  }
   123  
   124  // Run implements the same semantics as Runner's Run. However, if the
   125  // dispatch pipeline is full, Run will buffer the work and return immediately
   126  // rather than block.
   127  func (b *Buffer) runThen(gen func(chan<- func() error), then func()) <-chan error {
   128  	b.init()
   129  	return runImpl(gen, b.workC, then)
   130  }
   131  
   132  // RunOne implements the same semantics as Runner's RunOne. However, if the
   133  // dispatch pipeline is full, RunOne will buffer the work and return immediately
   134  // rather than block.
   135  func (b *Buffer) RunOne(f func() error) <-chan error {
   136  	b.init()
   137  
   138  	errC := make(chan error)
   139  	b.workC <- WorkItem{
   140  		F:     f,
   141  		ErrC:  errC,
   142  		After: func() { close(errC) },
   143  	}
   144  	return errC
   145  }
   146  
   147  // WorkC implements the same semantics as Runner's WorkC. However, this channel
   148  // will not block pending work dispatch. Any tasks written to this channel that
   149  // would block are instead buffered pending dispatch availability.
   150  func (b *Buffer) WorkC() chan<- WorkItem {
   151  	b.init()
   152  
   153  	return b.workC
   154  }
   155  
   156  // Close flushes the remaining tasks in the Buffer and Closes the underlying
   157  // Runner.
   158  //
   159  // Adding new tasks to the Buffer after Close has been invoked will cause a
   160  // panic.
   161  func (b *Buffer) Close() {
   162  	b.init()
   163  
   164  	close(b.workC)
   165  	<-b.tasksFinishedC
   166  	b.Runner.Close()
   167  }
   168  
   169  // SetFIFO sets the Buffer's task dispatch order to FIFO (true) or LIFO (false).
   170  // This determines the order in which buffered tasks will be dispatched. In
   171  // FIFO (first in, first out) mode, the first tasks to be buffered will be
   172  // dispatchd first. In LIFO (last in, last out) mode, the last tasks to be
   173  // buffered will be dispatched first.
   174  func (b *Buffer) SetFIFO(fifo bool) {
   175  	if fifo {
   176  		atomic.StoreInt32(&b.lifo, 0)
   177  	} else {
   178  		atomic.StoreInt32(&b.lifo, 1)
   179  	}
   180  }
   181  
   182  func (b *Buffer) isFIFO() bool {
   183  	return atomic.LoadInt32(&b.lifo) == 0
   184  }