github.com/ethersphere/bee/v2@v2.2.0/pkg/p2p/libp2p/internal/breaker/breaker.go (about)

     1  // Copyright 2020 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package breaker
     6  
     7  import (
     8  	"errors"
     9  	"sync"
    10  	"time"
    11  )
    12  
    13  const (
    14  	// defaults
    15  	limit        = 100
    16  	failInterval = 30 * time.Minute
    17  	maxBackoff   = time.Hour
    18  	backoff      = 2 * time.Minute
    19  )
    20  
    21  var (
    22  	_ Interface = (*breaker)(nil)
    23  
    24  	// ErrClosed is the special error type that indicates that breaker is closed and that is not executing functions at the moment.
    25  	ErrClosed = errors.New("breaker closed")
    26  )
    27  
    28  type Interface interface {
    29  	// Execute runs f() if the limit number of consecutive failed calls is not reached within fail interval.
    30  	// f() call is not locked so it can still be executed concurrently.
    31  	// Returns `ErrClosed` if the limit is reached or f() result otherwise.
    32  	Execute(f func() error) error
    33  
    34  	// ClosedUntil returns the timestamp when the breaker will become open again.
    35  	ClosedUntil() time.Time
    36  }
    37  
    38  type currentTimeFn = func() time.Time
    39  
    40  type breaker struct {
    41  	limit                int // breaker will not execute any more tasks after limit number of consecutive failures happen
    42  	consFailedCalls      int // current number of consecutive fails
    43  	firstFailedTimestamp time.Time
    44  	closedTimestamp      time.Time
    45  	backoff              time.Duration // initial backoff duration
    46  	maxBackoff           time.Duration
    47  	failInterval         time.Duration // consecutive failures are counted if they happen within this interval
    48  	currentTimeFn        currentTimeFn
    49  	mtx                  sync.Mutex
    50  }
    51  
    52  type Options struct {
    53  	Limit        int
    54  	FailInterval time.Duration
    55  	StartBackoff time.Duration
    56  	MaxBackoff   time.Duration
    57  }
    58  
    59  func NewBreaker(o Options) Interface {
    60  	return newBreakerWithCurrentTimeFn(o, time.Now)
    61  }
    62  
    63  func newBreakerWithCurrentTimeFn(o Options, currentTimeFn currentTimeFn) Interface {
    64  	breaker := &breaker{
    65  		limit:         o.Limit,
    66  		backoff:       o.StartBackoff,
    67  		maxBackoff:    o.MaxBackoff,
    68  		failInterval:  o.FailInterval,
    69  		currentTimeFn: currentTimeFn,
    70  	}
    71  
    72  	if o.Limit == 0 {
    73  		breaker.limit = limit
    74  	}
    75  
    76  	if o.FailInterval == 0 {
    77  		breaker.failInterval = failInterval
    78  	}
    79  
    80  	if o.MaxBackoff == 0 {
    81  		breaker.maxBackoff = maxBackoff
    82  	}
    83  
    84  	if o.StartBackoff == 0 {
    85  		breaker.backoff = backoff
    86  	}
    87  
    88  	return breaker
    89  }
    90  
    91  func (b *breaker) Execute(f func() error) error {
    92  	if err := b.beforef(); err != nil {
    93  		return err
    94  	}
    95  
    96  	return b.afterf(f())
    97  }
    98  
    99  func (b *breaker) ClosedUntil() time.Time {
   100  	b.mtx.Lock()
   101  	defer b.mtx.Unlock()
   102  
   103  	if b.consFailedCalls >= b.limit {
   104  		return b.closedTimestamp.Add(b.backoff)
   105  	}
   106  
   107  	return b.currentTimeFn()
   108  }
   109  
   110  func (b *breaker) beforef() error {
   111  	b.mtx.Lock()
   112  	defer b.mtx.Unlock()
   113  
   114  	// use currentTimeFn().Sub() instead of time.Since() so it can be deterministically mocked in tests
   115  	if b.consFailedCalls >= b.limit {
   116  		if b.closedTimestamp.IsZero() || b.currentTimeFn().Sub(b.closedTimestamp) < b.backoff {
   117  			return ErrClosed
   118  		}
   119  
   120  		b.resetFailed()
   121  		if newBackoff := b.backoff * 2; newBackoff <= b.maxBackoff {
   122  			b.backoff = newBackoff
   123  		} else {
   124  			b.backoff = b.maxBackoff
   125  		}
   126  	}
   127  
   128  	if !b.firstFailedTimestamp.IsZero() && b.currentTimeFn().Sub(b.firstFailedTimestamp) >= b.failInterval {
   129  		b.resetFailed()
   130  	}
   131  
   132  	return nil
   133  }
   134  
   135  func (b *breaker) afterf(err error) error {
   136  	b.mtx.Lock()
   137  	defer b.mtx.Unlock()
   138  	if err != nil {
   139  		if b.consFailedCalls == 0 {
   140  			b.firstFailedTimestamp = b.currentTimeFn()
   141  		}
   142  
   143  		b.consFailedCalls++
   144  		if b.consFailedCalls == b.limit {
   145  			b.closedTimestamp = b.currentTimeFn()
   146  		}
   147  
   148  		return err
   149  	}
   150  
   151  	b.resetFailed()
   152  	return nil
   153  }
   154  
   155  func (b *breaker) resetFailed() {
   156  	b.consFailedCalls = 0
   157  	b.firstFailedTimestamp = time.Time{}
   158  }