github.com/mailru/activerecord@v1.12.2/pkg/iproto/syncutil/taskgroup.go (about)

     1  package syncutil
     2  
     3  import (
     4  	"sync"
     5  
     6  	"golang.org/x/net/context"
     7  )
     8  
     9  // TaskGroup helps to control execution flow of repeatable tasks.
    10  // It is intended to execute at most N tasks at one time.
    11  type TaskGroup struct {
    12  	// N is a maximum number of tasks TaskGroup can allow to execute.
    13  	// If N is zero then TaskGroup with value 1 is used by default.
    14  	N int
    15  
    16  	// Goer starts a goroutine which executes a given task.
    17  	// It is useful when client using some pool of goroutines.
    18  	//
    19  	// If nil, then default `go` is used and context is ignored.
    20  	//
    21  	// Non-nil error from Goer means that some resources are temporarily
    22  	// unavailable and given task will not be executed.
    23  	//
    24  	// Note that for goroutine pool implementations it is required for pool to
    25  	// have at least capacity of N goroutines. In other way deadlock may occur.
    26  	Goer GoerFn
    27  
    28  	mu      sync.Mutex
    29  	once    sync.Once
    30  	n       int
    31  	pending []chan error
    32  	cancel  func()
    33  }
    34  
    35  func (t *TaskGroup) init() {
    36  	t.once.Do(func() {
    37  		if t.N == 0 {
    38  			t.N = 1
    39  		}
    40  
    41  		t.pending = make([]chan error, t.N)
    42  	})
    43  }
    44  
    45  // Do executes given function task in separate goroutine n minus <currently
    46  // running tasks number> times. It returns slice of n channels which
    47  // fulfillment means the end of appropriate task execution.
    48  //
    49  // That is, for m already running tasks Do(n, n < m) will return n channels
    50  // referring to a previously spawned task goroutines.
    51  //
    52  // All currenlty executing tasks can be signaled to cancel by calling
    53  // TaskGroup's Cancel() method.
    54  //
    55  // nolint:gocognit
    56  func (t *TaskGroup) Do(ctx context.Context, n int, task func(context.Context, int) error) []<-chan error {
    57  	t.init()
    58  
    59  	if n > t.N {
    60  		n = t.N
    61  	}
    62  
    63  	ret := make([]<-chan error, 0, n)
    64  
    65  	t.mu.Lock()
    66  	defer t.mu.Unlock()
    67  
    68  	if exec := n - t.n; exec > 0 {
    69  		// Start remaining tasks.
    70  		subctx, cancel := context.WithCancel(ctx)
    71  		// Append current call context to previous.
    72  		prev := t.cancel
    73  		t.cancel = func() {
    74  			if prev != nil {
    75  				prev()
    76  			}
    77  
    78  			cancel()
    79  		}
    80  
    81  		for i := 0; i < exec; i++ {
    82  			var j int
    83  
    84  			for ; j < len(t.pending); j++ {
    85  				if t.pending[j] != nil {
    86  					// Filter out already active "promises".
    87  					continue
    88  				}
    89  
    90  				break
    91  			}
    92  
    93  			done := make(chan error, 1)
    94  			err := goer(ctx, t.Goer, func() {
    95  				done <- task(subctx, j)
    96  
    97  				t.mu.Lock()
    98  				defer t.mu.Unlock()
    99  
   100  				exec--
   101  				if exec == 0 {
   102  					// Cancel current sub context.
   103  					cancel()
   104  				}
   105  				if t.pending[j] == done {
   106  					// Current activity was not canceled.
   107  					t.pending[j] = nil
   108  					t.n--
   109  					if t.n == 0 {
   110  						t.cancel = nil
   111  					}
   112  				}
   113  			})
   114  
   115  			if err != nil {
   116  				// Spawn goroutine error. Fulfill channel immediately.
   117  				done <- err
   118  			} else {
   119  				t.pending[j] = done
   120  				t.n++
   121  			}
   122  		}
   123  	}
   124  
   125  	for i := 0; i < len(t.pending) && len(ret) < n; i++ {
   126  		if t.pending[i] == nil {
   127  			continue
   128  		}
   129  
   130  		ret = append(ret, t.pending[i])
   131  	}
   132  
   133  	return ret
   134  }
   135  
   136  // Cancel cancels context of all currently running tasks. Further Do() calls
   137  // will not be blocked on waiting for exit of previous tasks.
   138  func (t *TaskGroup) Cancel() {
   139  	t.init()
   140  
   141  	t.mu.Lock()
   142  	defer t.mu.Unlock()
   143  
   144  	if t.cancel != nil {
   145  		t.cancel()
   146  		t.cancel = nil
   147  	}
   148  
   149  	for i := range t.pending {
   150  		// NOTE: Do not close the pending channel.
   151  		// It will be closed by a task runner.
   152  		//
   153  		// Set to nil to prevent memory leaks.
   154  		t.pending[i] = nil
   155  	}
   156  
   157  	t.n = 0
   158  }