github.com/lingyao2333/mo-zero@v1.4.1/core/executors/periodicalexecutor.go (about)

     1  package executors
     2  
     3  import (
     4  	"reflect"
     5  	"sync"
     6  	"sync/atomic"
     7  	"time"
     8  
     9  	"github.com/lingyao2333/mo-zero/core/lang"
    10  	"github.com/lingyao2333/mo-zero/core/proc"
    11  	"github.com/lingyao2333/mo-zero/core/syncx"
    12  	"github.com/lingyao2333/mo-zero/core/threading"
    13  	"github.com/lingyao2333/mo-zero/core/timex"
    14  )
    15  
    16  const idleRound = 10
    17  
    18  type (
    19  	// TaskContainer interface defines a type that can be used as the underlying
    20  	// container that used to do periodical executions.
    21  	TaskContainer interface {
    22  		// AddTask adds the task into the container.
    23  		// Returns true if the container needs to be flushed after the addition.
    24  		AddTask(task interface{}) bool
    25  		// Execute handles the collected tasks by the container when flushing.
    26  		Execute(tasks interface{})
    27  		// RemoveAll removes the contained tasks, and return them.
    28  		RemoveAll() interface{}
    29  	}
    30  
    31  	// A PeriodicalExecutor is an executor that periodically execute tasks.
    32  	PeriodicalExecutor struct {
    33  		commander chan interface{}
    34  		interval  time.Duration
    35  		container TaskContainer
    36  		waitGroup sync.WaitGroup
    37  		// avoid race condition on waitGroup when calling wg.Add/Done/Wait(...)
    38  		wgBarrier   syncx.Barrier
    39  		confirmChan chan lang.PlaceholderType
    40  		inflight    int32
    41  		guarded     bool
    42  		newTicker   func(duration time.Duration) timex.Ticker
    43  		lock        sync.Mutex
    44  	}
    45  )
    46  
    47  // NewPeriodicalExecutor returns a PeriodicalExecutor with given interval and container.
    48  func NewPeriodicalExecutor(interval time.Duration, container TaskContainer) *PeriodicalExecutor {
    49  	executor := &PeriodicalExecutor{
    50  		// buffer 1 to let the caller go quickly
    51  		commander:   make(chan interface{}, 1),
    52  		interval:    interval,
    53  		container:   container,
    54  		confirmChan: make(chan lang.PlaceholderType),
    55  		newTicker: func(d time.Duration) timex.Ticker {
    56  			return timex.NewTicker(d)
    57  		},
    58  	}
    59  	proc.AddShutdownListener(func() {
    60  		executor.Flush()
    61  	})
    62  
    63  	return executor
    64  }
    65  
    66  // Add adds tasks into pe.
    67  func (pe *PeriodicalExecutor) Add(task interface{}) {
    68  	if vals, ok := pe.addAndCheck(task); ok {
    69  		pe.commander <- vals
    70  		<-pe.confirmChan
    71  	}
    72  }
    73  
    74  // Flush forces pe to execute tasks.
    75  func (pe *PeriodicalExecutor) Flush() bool {
    76  	pe.enterExecution()
    77  	return pe.executeTasks(func() interface{} {
    78  		pe.lock.Lock()
    79  		defer pe.lock.Unlock()
    80  		return pe.container.RemoveAll()
    81  	}())
    82  }
    83  
    84  // Sync lets caller to run fn thread-safe with pe, especially for the underlying container.
    85  func (pe *PeriodicalExecutor) Sync(fn func()) {
    86  	pe.lock.Lock()
    87  	defer pe.lock.Unlock()
    88  	fn()
    89  }
    90  
    91  // Wait waits the execution to be done.
    92  func (pe *PeriodicalExecutor) Wait() {
    93  	pe.Flush()
    94  	pe.wgBarrier.Guard(func() {
    95  		pe.waitGroup.Wait()
    96  	})
    97  }
    98  
    99  func (pe *PeriodicalExecutor) addAndCheck(task interface{}) (interface{}, bool) {
   100  	pe.lock.Lock()
   101  	defer func() {
   102  		if !pe.guarded {
   103  			pe.guarded = true
   104  			// defer to unlock quickly
   105  			defer pe.backgroundFlush()
   106  		}
   107  		pe.lock.Unlock()
   108  	}()
   109  
   110  	if pe.container.AddTask(task) {
   111  		atomic.AddInt32(&pe.inflight, 1)
   112  		return pe.container.RemoveAll(), true
   113  	}
   114  
   115  	return nil, false
   116  }
   117  
   118  func (pe *PeriodicalExecutor) backgroundFlush() {
   119  	threading.GoSafe(func() {
   120  		// flush before quit goroutine to avoid missing tasks
   121  		defer pe.Flush()
   122  
   123  		ticker := pe.newTicker(pe.interval)
   124  		defer ticker.Stop()
   125  
   126  		var commanded bool
   127  		last := timex.Now()
   128  		for {
   129  			select {
   130  			case vals := <-pe.commander:
   131  				commanded = true
   132  				atomic.AddInt32(&pe.inflight, -1)
   133  				pe.enterExecution()
   134  				pe.confirmChan <- lang.Placeholder
   135  				pe.executeTasks(vals)
   136  				last = timex.Now()
   137  			case <-ticker.Chan():
   138  				if commanded {
   139  					commanded = false
   140  				} else if pe.Flush() {
   141  					last = timex.Now()
   142  				} else if pe.shallQuit(last) {
   143  					return
   144  				}
   145  			}
   146  		}
   147  	})
   148  }
   149  
   150  func (pe *PeriodicalExecutor) doneExecution() {
   151  	pe.waitGroup.Done()
   152  }
   153  
   154  func (pe *PeriodicalExecutor) enterExecution() {
   155  	pe.wgBarrier.Guard(func() {
   156  		pe.waitGroup.Add(1)
   157  	})
   158  }
   159  
   160  func (pe *PeriodicalExecutor) executeTasks(tasks interface{}) bool {
   161  	defer pe.doneExecution()
   162  
   163  	ok := pe.hasTasks(tasks)
   164  	if ok {
   165  		pe.container.Execute(tasks)
   166  	}
   167  
   168  	return ok
   169  }
   170  
   171  func (pe *PeriodicalExecutor) hasTasks(tasks interface{}) bool {
   172  	if tasks == nil {
   173  		return false
   174  	}
   175  
   176  	val := reflect.ValueOf(tasks)
   177  	switch val.Kind() {
   178  	case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
   179  		return val.Len() > 0
   180  	default:
   181  		// unknown type, let caller execute it
   182  		return true
   183  	}
   184  }
   185  
   186  func (pe *PeriodicalExecutor) shallQuit(last time.Duration) (stop bool) {
   187  	if timex.Since(last) <= pe.interval*idleRound {
   188  		return
   189  	}
   190  
   191  	// checking pe.inflight and setting pe.guarded should be locked together
   192  	pe.lock.Lock()
   193  	if atomic.LoadInt32(&pe.inflight) == 0 {
   194  		pe.guarded = false
   195  		stop = true
   196  	}
   197  	pe.lock.Unlock()
   198  
   199  	return
   200  }