Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
asaf-shitrit committed Dec 22, 2021
2 parents 790afca + 58be0e6 commit 5a68809
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 81 deletions.
59 changes: 53 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
A tiny util library for programs that require a little patience ⏰

## Usage

#### Basic
### Wait
Useful for simple cases where a predictable `bool` check should be ran
in a set interval and continue when it is satisfied.
#### Basic usage
```go
checkFunc := func() (bool, error) {
// any bool based logic that changes over a
Expand All @@ -19,14 +21,14 @@ if err := wait.Until(ctx, checkFunc); err != nil {

// logic that should happen after check is satisfied
```
#### With explicit options:
#### With explicit options
```go
checkFunc := func() (bool, error) {
// any bool based logic that changes over a
// given period of time
}

options := &wait.Options{
options := &wait.UntilOptions{
timeout: time.Minute
interval: time.Second
}
Expand All @@ -39,7 +41,52 @@ if err := wait.Until(ctx, checkFunc, options); err != nil {
// logic that should happen after check is satisfied
```

### Options
##### Jitter
### Backoff
Really useful in cases that low CPU overhead a constraint and the check intervals
should be backed off after each run.

It was inspired by Go's own `http.Server` `Shutdown` implementation ❤️

#### Basic usage
```go
checkFunc := func() (bool, error) {
// any bool based logic that changes over a
// given period of time
}

ctx := context.Background() // or pass any ctx you would like
if err := wait.Backoff(ctx, checkFunc); err != nil {
// handle logical/timeout err
}

// logic that should happen after check is satisfied
```

#### With explicit options
```go
checkFunc := func() (bool, error) {
// any bool based logic that changes over a
// given period of time
}

options := &wait.BackoffOptions{
baselineDuration: time.Millisecond,
limit: 500 * time.Millisecond,
multiplier: 2,
}

ctx := context.Background() // or pass any ctx you would like
if err := wait.Backoff(ctx, checkFunc, options); err != nil {
// handle logical/timeout err
}

// logic that should happen after check is satisfied
```

### Capabilities
### Timeout & Cancel ⏰
It is aligned with Golang concept of context so explicit cancels & timeout will work
out of the box.
#### Jitter
Allows you to set an amount of jitter percentage that will apply
for the calculation of each interval.
11 changes: 11 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package wait

import (
"math/rand"
"time"
)

func jitterDuration(duration time.Duration, jitterPercentage int) time.Duration {
maxTimeJitter := int64(duration) / int64(jitterPercentage)
return time.Duration(int64(duration) + rand.Int63n(maxTimeJitter*2) - maxTimeJitter)
}
132 changes: 99 additions & 33 deletions wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,54 +7,48 @@ import (
"time"
)

type Options struct {
interval time.Duration
timeout time.Duration
jitter int
func init() {
rand.Seed(time.Now().UnixNano())
}

var defaultOptions = &Options{
interval: time.Millisecond*100,
timeout: time.Second*10,
jitter: 0,
var (
canceledErr = errors.New("the operation was canceled")
invalidBackoffLimitErr = errors.New("the provided backoff limit is lower then the baseline")
)

type UntilOptions struct {
interval time.Duration
jitter int
}

func init(){
rand.Seed(time.Now().UnixNano())
func (o *UntilOptions) jitterDefined() bool {
return o.jitter != -1 && o.jitter != 0
}

var ctxErr = errors.New("the operation was either canceled or had a timeout")
var defaultUntilOptions = &UntilOptions{
interval: time.Millisecond * 100,
jitter: 0,
}

func Until(ctx context.Context, check func () (bool, error), o ...*Options) error {
options := defaultOptions
// Until allows for a predictable interval based waiting mechanism until
// the given bool based check is satisfied.
func Until(ctx context.Context, check func() (bool, error), o ...*UntilOptions) error {
options := defaultUntilOptions
if len(o) != 0 {
options = o[0]
}

tCtx, cancel := context.WithTimeout(ctx, options.timeout)
defer cancel()

// we pre-calculate the jitter offset to reduce
// overhead in each run of calculateNextInterval
var maxTimeJitter int64
if options.jitter != 0 {
maxTimeJitter = int64(int64(options.interval)/int64(options.jitter))
}

calculateNextInterval := func () time.Duration {
if options.jitter == 0 {
calculateNextInterval := func() time.Duration {
if !options.jitterDefined() {
return options.interval
}
// we want to jitter to be in the range of
// [interval - jitter] ~ [interval + jitter]
timeJitter := time.Duration(rand.Int63n(maxTimeJitter*2)-maxTimeJitter)
return options.interval + timeJitter
return jitterDuration(options.interval, options.jitter)
}

t := time.NewTimer(calculateNextInterval())
for {
select {
case <- t.C:
case <-t.C:
res, err := check()
if err != nil {
return err
Expand All @@ -64,8 +58,80 @@ func Until(ctx context.Context, check func () (bool, error), o ...*Options) erro
continue
}
return nil
case <- tCtx.Done():
return ctxErr
case <-ctx.Done():
return canceledErr
}
}
}
}

type BackoffOptions struct {
jitter int
baselineDuration, limit time.Duration
multiplier int64
}

func (o * BackoffOptions) jitterDefined() bool {
return o.jitter != -1 && o.jitter != 0
}

var defaultBackoffOptions = &BackoffOptions{
baselineDuration: time.Millisecond,
limit: 500 * time.Millisecond,
multiplier: 2,
jitter: 0,
}

// Backoff is a waiting mechanism that allows for better CPU load as the interval
// starts from a given baseline and then backs off until it reaches the provided
// limit.
//
// Note: this is partially bases off of http.Server implementation of their
// Shutdown polling mechanism.
func Backoff(ctx context.Context, check func() (bool, error), o ...*BackoffOptions) error {

options := defaultBackoffOptions
if len(o) != 0 {
options = o[0]
}

// make sure limit is greater then the given duration
if options.limit < options.baselineDuration {
return invalidBackoffLimitErr
}

duration := options.baselineDuration
t := time.NewTimer(duration)

calcNewDuration := func(previous time.Duration) time.Duration {
d := time.Duration(int64(previous) * int64(options.multiplier))
if !options.jitterDefined() {
return d
}
return jitterDuration(d, options.jitter)
}

for {
select {
case <-ctx.Done():
return canceledErr
case <-t.C:

res, err := check()
if err != nil {
return err
}

if res {
return nil
}

if duration < options.limit {
duration = calcNewDuration(duration)
} else {
// we cap the timer duration to the limit
duration = options.limit
}
t.Reset(duration)
}
}
}
Loading

0 comments on commit 5a68809

Please sign in to comment.