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  }