github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/remotestorage/internal/pool/pool.go (about) 1 // Copyright 2024 Dolthub, Inc. 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 pool 16 17 import ( 18 "context" 19 "errors" 20 "sync" 21 22 "golang.org/x/sync/errgroup" 23 ) 24 25 type Func func(ctx context.Context, shutdown <-chan struct{}) error 26 27 // A |pool.Dynamic| is a thread pool which can be dynamically sized from the 28 // outside. It is created with a |Func|, which represents the thread function 29 // to run on each worker thread. That |Func| must adhere to a contract where if 30 // it reads from the supplied |shutdown| channel, it shutsdown in a timely 31 // manner. 32 // 33 // To run the threads in the pool, call |Run|. 34 // 35 // To set the desired size of the pool, call |SetSize|. If |size| is larger 36 // than the current size, new threads will be created immediately to bring the 37 // size up to the requested size. If |size| is smaller than the requested size, 38 // then |curSize-size| sends on |shutdown| will be performed, to bring 39 // |curSize| down to |size|. |SetSize| does not return until the pool is 40 // resized or the pool's |Run| method has errored. 41 // 42 // |Func|s are run within an |errgroup|. If any |Func| exists with a non-|nil| 43 // error, |Run| will exit with an error as well. 44 type Dynamic struct { 45 // errgroup Context in which all spawned |Func|s are run. 46 ctx context.Context 47 // errgroup in which all spawned |Func|s are run. 48 eg *errgroup.Group 49 50 // Guards |size| and |running|. 51 mu sync.Mutex 52 53 // |SetSize| does not adjust the thread pool unless the pool is already running. 54 running bool 55 56 // The requested size of the pool. After the pool is running, this is 57 // also the current size of the pool. 58 size int 59 60 // The worker function each thread in the pool runs. 61 f Func 62 63 // Sends are made on this channel to request a spawn. 64 spawnCh chan struct{} 65 66 // Sends are made on this channel to request a thread to shutdown. This 67 // channel is supplied to |Func|s and they are responsible for adhering 68 // to the request. 69 shutdownCh chan struct{} 70 71 // Sends are made on this channel when the thread successfully exits. 72 // When decreasing the number of workers, delivers on |shutdownCh| must 73 // be paired up with receives on |exitCh|. 74 exitCh chan struct{} 75 76 // Closed when it is time for the pool to shutdown cleanly. Used by |Close|. 77 poolShutdownCh chan struct{} 78 } 79 80 func NewDynamic(ctx context.Context, f Func, size int) *Dynamic { 81 if size == 0 { 82 panic("cannot create pool of initial size 0") 83 } 84 if f == nil { 85 panic("cannot create pool with nil Func") 86 } 87 eg, ctx := errgroup.WithContext(ctx) 88 return &Dynamic{ 89 ctx: ctx, 90 eg: eg, 91 size: size, 92 f: f, 93 94 spawnCh: make(chan struct{}), 95 shutdownCh: make(chan struct{}), 96 exitCh: make(chan struct{}), 97 98 poolShutdownCh: make(chan struct{}), 99 } 100 } 101 102 func (d *Dynamic) Run() error { 103 d.mu.Lock() 104 if d.running { 105 d.mu.Unlock() 106 return errors.New("internal error: Dynamic Run() was called on a pool which has already been run or is currently running.") 107 } 108 d.running = true 109 d.eg.Go(func() error { 110 for { 111 select { 112 case <-d.spawnCh: 113 d.eg.Go(func() (err error) { 114 defer func() { 115 if err != nil { 116 return 117 } 118 select { 119 case d.exitCh <- struct{}{}: 120 case <-d.ctx.Done(): 121 } 122 }() 123 return d.f(d.ctx, d.shutdownCh) 124 }) 125 case <-d.ctx.Done(): 126 return context.Cause(d.ctx) 127 case <-d.poolShutdownCh: 128 return nil 129 } 130 } 131 }) 132 for i := 0; i < d.size; i++ { 133 select { 134 case d.spawnCh <- struct{}{}: 135 case <-d.ctx.Done(): 136 d.mu.Unlock() 137 return d.eg.Wait() 138 } 139 } 140 d.mu.Unlock() 141 return d.eg.Wait() 142 } 143 144 func (d *Dynamic) Close() { 145 d.SetSize(0) 146 close(d.poolShutdownCh) 147 } 148 149 func (d *Dynamic) SetSize(n int) { 150 d.mu.Lock() 151 defer d.mu.Unlock() 152 if d.size == 0 { 153 // Ignore the request; this pool is shutdown. 154 return 155 } 156 // We are spawning new threads. 157 for d.size < n { 158 select { 159 case d.spawnCh <- struct{}{}: 160 case <-d.ctx.Done(): 161 return 162 } 163 d.size += 1 164 } 165 // We are shutting down existing threads. 166 for d.size > n { 167 select { 168 case d.shutdownCh <- struct{}{}: 169 case <-d.ctx.Done(): 170 return 171 } 172 select { 173 case <-d.exitCh: 174 case <-d.ctx.Done(): 175 return 176 } 177 d.size -= 1 178 } 179 }