github.com/wfusion/gofusion@v1.1.14/routine/pool.go (about) 1 package routine 2 3 import ( 4 "math" 5 "sync" 6 "time" 7 8 "github.com/panjf2000/ants/v2" 9 "go.uber.org/atomic" 10 11 "github.com/wfusion/gofusion/common/utils" 12 ) 13 14 var ( 15 // pools is a global map of goroutine pools, managing multiple goroutine pools, 16 // with total service instance quantity controlled by allocated 17 // TODO: Issue with nested goroutine pool allocation; 18 // if a task executed in a goroutine pool nests a new goroutine pool allocation, 19 // it might cause a deadlock, and errors cannot be quickly thrown. 20 pools map[string]map[string]Pool 21 rwlock sync.RWMutex 22 ignored map[string]*atomic.Int64 23 idles map[string]*atomic.Int64 24 defaultLogger map[string]ants.Logger 25 ) 26 27 type pool struct { 28 appName string 29 name string 30 pool *ants.Pool 31 option *NewPoolOption 32 } 33 34 func (p *pool) Submit(task any, opts ...utils.OptionExtender) (e error) { 35 opt := utils.ApplyOptions[candyOption](opts...) 36 wrapFn := utils.WrapFunc1[error](task) 37 if !forceSync(p.appName) { 38 return p.pool.Submit(func() { e = wrapFn(opt.args...) }) 39 } 40 41 return wrapFn(opt.args...) 42 } 43 func (p *pool) Running() int { return p.pool.Running() } 44 func (p *pool) Free() int { return p.pool.Free() } 45 func (p *pool) Waiting() int { return p.pool.Waiting() } 46 func (p *pool) Cap() int { return p.pool.Cap() } 47 func (p *pool) IsClosed() bool { return p.pool.IsClosed() } 48 func (p *pool) Release(opts ...utils.OptionExtender) { 49 defer release(p.appName, p, opts...) 50 p.pool.Release() 51 } 52 func (p *pool) ReleaseTimeout(timeout time.Duration, opts ...utils.OptionExtender) error { 53 defer release(p.appName, p, opts...) 54 return p.pool.ReleaseTimeout(timeout) 55 } 56 57 func NewPool(name string, size int, opts ...utils.OptionExtender) (p Pool) { 58 o := utils.ApplyOptions[NewPoolOption](opts...) 59 opt := utils.ApplyOptions[candyOption](opts...) 60 if o.Logger == nil { 61 o.Logger = defaultLogger[opt.appName] 62 } 63 64 validate(opt.appName, name) 65 allocate(opt.appName, size, o) 66 67 antsPool, err := ants.NewPool(size, ants.WithOptions(ants.Options{ 68 ExpiryDuration: o.ExpiryDuration, 69 PreAlloc: o.PreAlloc, 70 MaxBlockingTasks: o.MaxBlockingTasks, 71 Nonblocking: o.Nonblocking, 72 PanicHandler: o.PanicHandler, 73 Logger: o.Logger, 74 DisablePurge: o.DisablePurge, 75 })) 76 if err != nil { 77 panic(err) 78 } 79 80 p = &pool{appName: opt.appName, name: name, pool: antsPool, option: o} 81 addPool(opt.appName, name, p) 82 return 83 } 84 85 type internalOption struct { 86 // ignoreRecycled does not account for unrecycled successes during graceful exit, 87 // only used when calling the loop method 88 ignoreRecycled bool 89 // ignoreMutex does not lock on map operations, 90 // considering replacing with reentrant lock github.com/sasha-s/go-deadlock, 91 // but introduction will increase comprehension cost, also goes against go design 92 ignoreMutex bool 93 } 94 95 func ignoreRecycled() utils.OptionFunc[internalOption] { 96 return func(o *internalOption) { 97 o.ignoreRecycled = true 98 } 99 } 100 101 func ignoreMutex() utils.OptionFunc[internalOption] { 102 return func(o *internalOption) { 103 o.ignoreMutex = true 104 } 105 } 106 107 func allocate(appName string, delta int, o *NewPoolOption, opts ...utils.OptionExtender) { 108 oo := utils.ApplyOptions[internalOption](opts...) 109 demands := int64(delta) 110 rwlock.RLock() 111 defer rwlock.RUnlock() 112 if idles[appName].Load()-demands < 0 && o.ApplyTimeout == 0 { 113 panic(ErrPoolOverload) 114 } 115 116 if o.ApplyTimeout < 0 { 117 o.ApplyTimeout = math.MaxInt64 118 } 119 120 t := time.NewTimer(o.ApplyTimeout) 121 for { 122 select { 123 case <-t.C: 124 panic(ErrTimeout) 125 default: 126 minuend := idles[appName].Load() 127 diff := minuend - demands 128 // main thread is a goroutine as well, so diff should be greater than 0 129 if diff <= 0 || !idles[appName].CompareAndSwap(minuend, diff) { 130 continue 131 } 132 if oo.ignoreRecycled { 133 ignored[appName].Add(demands) 134 } 135 136 return 137 } 138 } 139 } 140 141 func release(appName string, p *pool, opts ...utils.OptionExtender) { 142 o := utils.ApplyOptions[internalOption](opts...) 143 delta := int64(1) 144 if pools == nil || pools[appName] == nil || idles == nil || idles[appName] == nil { 145 return 146 } 147 148 if p != nil { 149 if !o.ignoreMutex { 150 rwlock.Lock() 151 defer rwlock.Unlock() 152 } 153 delete(pools[appName], p.name) 154 delta = int64(p.pool.Cap()) 155 } 156 157 alloc := idles[appName] 158 if alloc == nil { 159 return 160 } 161 alloc.Add(delta) 162 if o != nil && o.ignoreRecycled { 163 alloc.Sub(delta) 164 } 165 } 166 167 func addPool(appName, name string, pool Pool) { 168 rwlock.Lock() 169 defer rwlock.Unlock() 170 pools[appName][name] = pool 171 } 172 173 func validate(appName, name string) { 174 rwlock.RLock() 175 defer rwlock.RUnlock() 176 if _, ok := pools[appName][name]; ok { 177 panic(ErrDuplicatedName) 178 } 179 } 180 181 type PoolOption struct { 182 // ExpiryDuration is a period for the scavenger goroutine to clean up those expired workers, 183 // the scavenger scans all workers every `ExpiryDuration` and clean up those workers that haven't been 184 // used for more than `ExpiryDuration`. 185 ExpiryDuration time.Duration 186 187 // PreAlloc indicates whether to make memory pre-allocation when initializing Pool. 188 PreAlloc bool 189 190 // Max number of goroutine blocking on pool.Submit. 191 // 0 (default value) means no such limit. 192 MaxBlockingTasks int 193 194 // When Nonblocking is true, Pool.Submit will never be blocked. 195 // ErrPoolOverload will be returned when Pool.Submit cannot be done at once. 196 // When Nonblocking is true, MaxBlockingTasks is inoperative. 197 Nonblocking bool 198 199 // PanicHandler is used to handle panics from each worker goroutine. 200 // if nil, panics will be thrown out again from worker goroutines. 201 PanicHandler func(any) 202 203 // Logger is the customized logger for logging info, if it is not set, 204 // default standard logger from log package is used. 205 Logger ants.Logger 206 207 // When DisablePurge is true, workers are not purged and are resident. 208 DisablePurge bool 209 } 210 211 type NewPoolOption struct { 212 PoolOption 213 // ApplyTimeout is the timeout duration for applying a goroutine pool 214 // Default = 0 means non-blocking and directly panic; 215 // < 0 means blocking and wait; 216 // > 0 means block and panic after timeout 217 ApplyTimeout time.Duration 218 } 219 220 func Timeout(t time.Duration) utils.OptionFunc[NewPoolOption] { 221 return func(o *NewPoolOption) { 222 o.ApplyTimeout = t 223 } 224 } 225 226 func WithoutTimeout() utils.OptionFunc[NewPoolOption] { 227 return func(o *NewPoolOption) { 228 o.ApplyTimeout = -1 229 } 230 } 231 232 func Options(in *NewPoolOption) utils.OptionFunc[NewPoolOption] { 233 return func(o *NewPoolOption) { 234 o.ApplyTimeout = in.ApplyTimeout 235 o.ExpiryDuration = in.PoolOption.ExpiryDuration 236 o.PreAlloc = in.PreAlloc 237 o.MaxBlockingTasks = in.MaxBlockingTasks 238 o.Nonblocking = in.Nonblocking 239 o.PanicHandler = in.PanicHandler 240 o.Logger = in.Logger 241 o.DisablePurge = in.DisablePurge 242 } 243 }