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 }