github.com/git-amp/amp-sdk-go@v0.7.5/stdlib/task/pool.go (about)

     1  package task
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"golang.org/x/sync/semaphore"
    10  
    11  	"github.com/git-amp/amp-sdk-go/stdlib/utils"
    12  )
    13  
    14  type Pool struct {
    15  	Context
    16  	itemsAvailable *utils.Mailbox
    17  	chItems        chan PoolUniqueID
    18  	sem            *semaphore.Weighted
    19  	retryInterval  time.Duration
    20  	poolItems      map[PoolUniqueID]poolItem
    21  	poolItemsMu    sync.RWMutex
    22  }
    23  
    24  func StartNewPool(name string, concurrency int, retryInterval time.Duration) *Pool {
    25  	p := &Pool{
    26  		itemsAvailable: utils.NewMailbox(1000),
    27  		chItems:        make(chan PoolUniqueID),
    28  		sem:            semaphore.NewWeighted(int64(concurrency)),
    29  		retryInterval:  retryInterval,
    30  		poolItems:      make(map[PoolUniqueID]poolItem),
    31  	}
    32  	
    33  	p.Context, _ = Start(&Task{
    34  		Label: "pool",
    35  		OnStart: p.OnContextStarted,
    36  	})
    37  	return p
    38  }
    39  
    40  type PoolUniqueID interface{}
    41  
    42  type poolItem struct {
    43  	item      PoolUniqueIDer
    44  	state     poolItemState
    45  	retryWhen time.Time
    46  }
    47  
    48  type PoolUniqueIDer interface {
    49  	ID() PoolUniqueID
    50  }
    51  
    52  type poolItemState int
    53  
    54  const (
    55  	poolItemState_Available poolItemState = iota
    56  	poolItemState_InUse
    57  	poolItemState_Done
    58  	poolItemState_InRetryPool
    59  )
    60  
    61  func (p *Pool) OnContextStarted(ctx Context) error {
    62  	p.Context.Go("deliverAvailableItems", p.deliverAvailableItems)
    63  	p.Context.Go("handleItemsAwaitingRetry", p.handleItemsAwaitingRetry)
    64  	return nil
    65  }
    66  
    67  func (p *Pool) NumItemsPending() int {
    68  	p.poolItemsMu.RLock()
    69  	defer p.poolItemsMu.RUnlock()
    70  
    71  	var n int
    72  	for _, item := range p.poolItems {
    73  		if item.state == poolItemState_Available || item.state == poolItemState_InRetryPool {
    74  			n++
    75  		}
    76  	}
    77  	return n
    78  }
    79  
    80  func (p *Pool) Add(item PoolUniqueIDer) {
    81  	p.poolItemsMu.Lock()
    82  	defer p.poolItemsMu.Unlock()
    83  
    84  	_, exists := p.poolItems[item.ID()]
    85  	if exists {
    86  		return
    87  	}
    88  	p.poolItems[item.ID()] = poolItem{item, poolItemState_Available, time.Time{}}
    89  	p.itemsAvailable.Deliver(item.ID())
    90  }
    91  
    92  func (p *Pool) Get(ctx context.Context) (item interface{}, err error) {
    93  	err = p.sem.Acquire(ctx, 1)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	defer func() {
    98  		if err != nil {
    99  			p.sem.Release(1)
   100  		}
   101  	}()
   102  
   103  	select {
   104  	case <-ctx.Done():
   105  		return nil, ctx.Err()
   106  
   107  	case id := <-p.chItems:
   108  		p.poolItemsMu.RLock()
   109  		defer p.poolItemsMu.RUnlock()
   110  		entry, exists := p.poolItems[id]
   111  		if !exists {
   112  			panic(fmt.Sprintf("(%T) %v", id, id))
   113  		} else if entry.state != poolItemState_Available {
   114  			panic(fmt.Sprintf("(%T) %v", id, id))
   115  		}
   116  		return entry.item, nil
   117  	}
   118  }
   119  
   120  func (p *Pool) deliverAvailableItems(ctx Context) {
   121  	for {
   122  		select {
   123  		case <-ctx.Done():
   124  			return
   125  		case <-p.itemsAvailable.Notify():
   126  			for _, id := range p.itemsAvailable.RetrieveAll() {
   127  				var entry poolItem
   128  				var exists bool
   129  				func() {
   130  					p.poolItemsMu.RLock()
   131  					defer p.poolItemsMu.RUnlock()
   132  					entry, exists = p.poolItems[id]
   133  				}()
   134  				if !exists {
   135  					continue
   136  				} else if entry.state != poolItemState_Available {
   137  					panic("no")
   138  				}
   139  
   140  				select {
   141  				case <-ctx.Done():
   142  					return
   143  				case p.chItems <- id:
   144  				}
   145  			}
   146  		}
   147  	}
   148  }
   149  
   150  func (p *Pool) setState(id PoolUniqueID, state poolItemState, retryWhen time.Time) {
   151  	entry, exists := p.poolItems[id]
   152  	if !exists {
   153  		panic(fmt.Sprintf("(%T) %v", id, id))
   154  	}
   155  	entry.state = state
   156  	entry.retryWhen = retryWhen
   157  	p.poolItems[id] = entry
   158  }
   159  
   160  func (p *Pool) RetryLater(id PoolUniqueID, when time.Time) {
   161  	p.poolItemsMu.Lock()
   162  	defer p.poolItemsMu.Unlock()
   163  	p.setState(id, poolItemState_InRetryPool, when)
   164  	p.sem.Release(1)
   165  }
   166  
   167  func (p *Pool) ForceRetry(id PoolUniqueID) {
   168  	p.poolItemsMu.Lock()
   169  	defer p.poolItemsMu.Unlock()
   170  
   171  	entry, exists := p.poolItems[id]
   172  	if !exists {
   173  		panic(fmt.Sprintf("(%T) %v", id, id))
   174  	}
   175  
   176  	switch entry.state {
   177  	case poolItemState_Available:
   178  	case poolItemState_InUse:
   179  	case poolItemState_InRetryPool:
   180  		p.setState(id, poolItemState_Available, time.Time{})
   181  		p.itemsAvailable.Deliver(id)
   182  	case poolItemState_Done:
   183  	}
   184  }
   185  
   186  func (p *Pool) Complete(id PoolUniqueID) {
   187  	p.poolItemsMu.Lock()
   188  	defer p.poolItemsMu.Unlock()
   189  	p.setState(id, poolItemState_Done, time.Time{})
   190  	p.sem.Release(1)
   191  }
   192  
   193  func (p *Pool) handleItemsAwaitingRetry(ctx Context) {
   194  	ticker := time.NewTicker(p.retryInterval)
   195  	for {
   196  		select {
   197  		case <-p.Context.Done():
   198  			return
   199  		case <-ctx.Done():
   200  			return
   201  
   202  		case <-ticker.C:
   203  			func() {
   204  				p.poolItemsMu.Lock()
   205  				defer p.poolItemsMu.Unlock()
   206  
   207  				now := time.Now()
   208  
   209  				for id, entry := range p.poolItems {
   210  					if entry.state != poolItemState_InRetryPool {
   211  						continue
   212  					} else if !entry.retryWhen.Before(now) {
   213  						continue
   214  					}
   215  					p.setState(id, poolItemState_Available, time.Time{})
   216  					p.itemsAvailable.Deliver(id)
   217  				}
   218  			}()
   219  		}
   220  	}
   221  }