github.com/docker/cnab-to-oci@v0.3.0-beta4/remotes/promises.go (about)

     1  package remotes
     2  
     3  import (
     4  	"context"
     5  	"reflect"
     6  
     7  	"golang.org/x/sync/errgroup"
     8  )
     9  
    10  // scheduler is an abstraction over a component capable of running tasks
    11  // concurrently
    12  type scheduler interface {
    13  	// schedule a task and returns a promise notifying completion
    14  	schedule(func(ctx context.Context) error) promise
    15  	// ctx returns the context associated with this scheduler
    16  	ctx() context.Context
    17  }
    18  
    19  // dependency represents an asynchronous operation with its completion channel and its error state
    20  type dependency interface {
    21  	Done() <-chan struct{}
    22  	Err() error
    23  }
    24  
    25  // failedDependency is a dependency already ran to completion with an error
    26  type failedDependency struct {
    27  	err error
    28  }
    29  
    30  func (f failedDependency) Done() <-chan struct{} {
    31  	return doneCh
    32  }
    33  
    34  func (f failedDependency) Err() error {
    35  	return f.err
    36  }
    37  
    38  // doneDependency is a dependency already ran to completion without error
    39  type doneDependency struct {
    40  }
    41  
    42  func (doneDependency) Done() <-chan struct{} {
    43  	return doneCh
    44  }
    45  
    46  func (doneDependency) Err() error {
    47  	return nil
    48  }
    49  
    50  // doneCh is used internally by doneDependency and failedDependency (it is a closed channel)
    51  var doneCh = func() chan struct{} {
    52  	ch := make(chan struct{})
    53  	close(ch)
    54  	return ch
    55  }()
    56  
    57  // whenAll wrapps multiple dependencies in a single dependency
    58  // the result is completed once any dependency completes with an error
    59  // or once all dependencies ran to completion without error
    60  func whenAll(dependencies []dependency) dependency {
    61  	completionSource := &completionSource{
    62  		done: make(chan struct{}),
    63  	}
    64  	go func() {
    65  		defer close(completionSource.done)
    66  		cases := make([]reflect.SelectCase, len(dependencies))
    67  		for ix, dependency := range dependencies {
    68  			cases[ix] = reflect.SelectCase{
    69  				Chan: reflect.ValueOf(dependency.Done()),
    70  				Dir:  reflect.SelectRecv,
    71  			}
    72  		}
    73  		for len(dependencies) > 0 {
    74  			ix, _, _ := reflect.Select(cases)
    75  			if err := dependencies[ix].Err(); err != nil {
    76  				completionSource.err = err
    77  				return
    78  			}
    79  			cases = append(cases[:ix], cases[ix+1:]...)
    80  			dependencies = append(dependencies[:ix], dependencies[ix+1:]...)
    81  		}
    82  	}()
    83  	return completionSource
    84  }
    85  
    86  // promise is a dependency attached to a scheduler. It allows to schedule continuations
    87  type promise struct {
    88  	dependency
    89  	scheduler scheduler
    90  }
    91  
    92  func (p promise) wait() error {
    93  	<-p.Done()
    94  	return p.Err()
    95  }
    96  
    97  // then schedules a continuation task once the current promise is completed.
    98  // It propagates errors and returns a promise wrapping the continuation
    99  func (p promise) then(next func(ctx context.Context) error) promise {
   100  	completionSource := &completionSource{
   101  		done: make(chan struct{}),
   102  	}
   103  	go func() {
   104  		defer close(completionSource.done)
   105  		<-p.Done()
   106  		if err := p.Err(); err != nil {
   107  			completionSource.err = err
   108  			return
   109  		}
   110  		completionSource.err = p.scheduler.schedule(next).wait()
   111  	}()
   112  	return newPromise(p.scheduler, completionSource)
   113  }
   114  
   115  // newPromise creates a promise out of a dependency
   116  func newPromise(scheduler scheduler, dependency dependency) promise {
   117  	return promise{scheduler: scheduler, dependency: dependency}
   118  }
   119  
   120  // this schedule a task that itself produces a promise, and returns a promise wrapping the produced promise
   121  func scheduleAndUnwrap(scheduler scheduler, do func(ctx context.Context) (dependency, error)) promise {
   122  	completionSource := &completionSource{
   123  		done: make(chan struct{}),
   124  	}
   125  	scheduler.schedule(func(ctx context.Context) error {
   126  		p, err := do(ctx)
   127  		if err != nil {
   128  			completionSource.err = err
   129  			close(completionSource.done)
   130  			return err
   131  		}
   132  		go func() {
   133  			<-p.Done()
   134  			completionSource.err = p.Err()
   135  			close(completionSource.done)
   136  		}()
   137  		return nil
   138  	})
   139  	return newPromise(scheduler, completionSource)
   140  }
   141  
   142  // completion source is a a low-level dependency implementation used internally by the schedulers and promises
   143  type completionSource struct {
   144  	done chan struct{}
   145  	err  error
   146  }
   147  
   148  func (cs *completionSource) Done() <-chan struct{} {
   149  	return cs.done
   150  }
   151  
   152  func (cs *completionSource) Err() error {
   153  	return cs.err
   154  }
   155  
   156  // todoItem is an internal structure used by errgroupScheduler
   157  type todoItem struct {
   158  	completionSource *completionSource
   159  	do               func(ctx context.Context) error
   160  }
   161  
   162  // errgroupScheduler is a scheduler that cancels all tasks at the first error occurred
   163  type errgroupScheduler struct {
   164  	workGroup *errgroup.Group
   165  	todoList  chan todoItem
   166  	context   context.Context
   167  }
   168  
   169  func newErrgroupScheduler(ctx context.Context, workerCount, todoBuffer int) *errgroupScheduler {
   170  	todoList := make(chan todoItem, todoBuffer)
   171  	workGroup, ctx := errgroup.WithContext(ctx)
   172  	for i := 0; i < workerCount; i++ {
   173  		workGroup.Go(func() error {
   174  			for {
   175  				select {
   176  				case todoItem := <-todoList:
   177  					todoItem.completionSource.err = todoItem.do(ctx)
   178  					close(todoItem.completionSource.done)
   179  					if todoItem.completionSource.err != nil {
   180  						return todoItem.completionSource.err
   181  					}
   182  				case <-ctx.Done():
   183  					return ctx.Err()
   184  				}
   185  
   186  			}
   187  		})
   188  	}
   189  	return &errgroupScheduler{
   190  		todoList:  todoList,
   191  		workGroup: workGroup,
   192  		context:   ctx,
   193  	}
   194  }
   195  
   196  func (s *errgroupScheduler) schedule(do func(ctx context.Context) error) promise {
   197  	select {
   198  	case <-s.context.Done():
   199  		return newPromise(s, failedDependency{s.context.Err()})
   200  	default:
   201  	}
   202  	completionSource := &completionSource{
   203  		done: make(chan struct{}),
   204  	}
   205  	s.todoList <- todoItem{completionSource: completionSource, do: do}
   206  	return newPromise(s, completionSource)
   207  }
   208  
   209  func (s *errgroupScheduler) ctx() context.Context {
   210  	return s.context
   211  }
   212  
   213  // nolint: unparam
   214  func (s *errgroupScheduler) drain() error {
   215  	return s.workGroup.Wait()
   216  }