github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/coalescing_context.go (about)

     1  package libkbfs
     2  
     3  import (
     4  	"reflect"
     5  	"sync"
     6  	"time"
     7  
     8  	"golang.org/x/net/context"
     9  )
    10  
    11  // CoalescingContext allows many contexts to be treated as one.  It waits on
    12  // all its contexts' Context.Done() channels, and when all of them have
    13  // returned, this CoalescingContext is canceled. At any point, a context can be
    14  // added to the list, and will subsequently also be part of the wait condition.
    15  // TODO: add timeout channel in case there is a goroutine leak.
    16  type CoalescingContext struct {
    17  	context.Context
    18  	doneCh   chan struct{}
    19  	mutateCh chan context.Context
    20  	selects  []reflect.SelectCase
    21  	start    sync.Once
    22  }
    23  
    24  const (
    25  	mutateChanSelectIndex       int = 0
    26  	closeChanSelectIndex        int = 1
    27  	numExplicitlyHandledSelects int = 2
    28  )
    29  
    30  func (ctx *CoalescingContext) loop() {
    31  	for {
    32  		chosen, val, _ := reflect.Select(ctx.selects)
    33  		switch chosen {
    34  		case mutateChanSelectIndex:
    35  			// request to mutate the select list
    36  			newCase := val.Interface().(context.Context)
    37  			if newCase != nil {
    38  				ctx.appendContext(newCase)
    39  			}
    40  		case closeChanSelectIndex:
    41  			// Done
    42  			close(ctx.doneCh)
    43  			return
    44  		default:
    45  			// The chosen channel has been closed. Remove it from our select list.
    46  			ctx.selects = append(ctx.selects[:chosen], ctx.selects[chosen+1:]...)
    47  			// If we have no more selects available, the request is done.
    48  			if len(ctx.selects) == numExplicitlyHandledSelects {
    49  				close(ctx.doneCh)
    50  				return
    51  			}
    52  		}
    53  	}
    54  }
    55  
    56  func (ctx *CoalescingContext) appendContext(other context.Context) {
    57  	ctx.selects = append(ctx.selects, reflect.SelectCase{
    58  		Dir:  reflect.SelectRecv,
    59  		Chan: reflect.ValueOf(other.Done()),
    60  	})
    61  }
    62  
    63  // NewCoalescingContext creates a new CoalescingContext. The context _must_ be
    64  // canceled to avoid a goroutine leak.
    65  func NewCoalescingContext(parent context.Context) (*CoalescingContext, context.CancelFunc) {
    66  	ctx := &CoalescingContext{
    67  		// Make the parent's `Value()` method available to consumers of this
    68  		// context. For example, this maintains the parent's log debug tags.
    69  		// TODO: Make _all_ parents' values available.
    70  		Context:  parent,
    71  		doneCh:   make(chan struct{}),
    72  		mutateCh: make(chan context.Context),
    73  	}
    74  	closeCh := make(chan struct{})
    75  	ctx.selects = []reflect.SelectCase{
    76  		{
    77  			Dir:  reflect.SelectRecv,
    78  			Chan: reflect.ValueOf(ctx.mutateCh),
    79  		},
    80  		{
    81  			Dir:  reflect.SelectRecv,
    82  			Chan: reflect.ValueOf(closeCh),
    83  		},
    84  	}
    85  	ctx.appendContext(parent)
    86  	cancelFunc := func() {
    87  		select {
    88  		case <-closeCh:
    89  		default:
    90  			close(closeCh)
    91  		}
    92  	}
    93  	return ctx, cancelFunc
    94  }
    95  
    96  func (ctx *CoalescingContext) startLoop() {
    97  	ctx.start.Do(func() {
    98  		go ctx.loop()
    99  	})
   100  }
   101  
   102  // Deadline overrides the default parent's Deadline().
   103  func (ctx *CoalescingContext) Deadline() (time.Time, bool) {
   104  	return time.Time{}, false
   105  }
   106  
   107  // Done returns a channel that is closed when the CoalescingContext is
   108  // canceled.
   109  func (ctx *CoalescingContext) Done() <-chan struct{} {
   110  	ctx.startLoop()
   111  	return ctx.doneCh
   112  }
   113  
   114  // Err returns context.Canceled if the CoalescingContext has been canceled, and
   115  // nil otherwise.
   116  func (ctx *CoalescingContext) Err() error {
   117  	ctx.startLoop()
   118  	select {
   119  	case <-ctx.doneCh:
   120  		return context.Canceled
   121  	default:
   122  	}
   123  	return nil
   124  }
   125  
   126  // AddContext adds a context to the set of contexts that we're waiting on.
   127  func (ctx *CoalescingContext) AddContext(other context.Context) error {
   128  	ctx.startLoop()
   129  	select {
   130  	case ctx.mutateCh <- other:
   131  		return nil
   132  	case <-ctx.doneCh:
   133  		return context.Canceled
   134  	}
   135  }