Skip to content

Commit

Permalink
Added unit tests and README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
rbrahul committed Oct 15, 2023
1 parent df5b10a commit 2f9f484
Show file tree
Hide file tree
Showing 8 changed files with 387 additions and 50 deletions.
33 changes: 33 additions & 0 deletions .github/workflow/go.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: All Test

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16

- name: Build
run: go build -v ./...

- name: Test
run: go test -timeout 300s -race -coverprofile=coverage.xml -covermode=atomic -v ./...
#- uses: actions/checkout@master
- uses: codecov/codecov-action@v1
with:
files: ./coverage.xml
flags: unittests # optional
name: codecov-umbrella # optional
fail_ci_if_error: true # optional (default = false)
verbose: true # optional (default = false)
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
.DS_Store

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Rahul Baruri
Copyright (c) 2023 Rahul Baruri<rahulbaruri1@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Retry

An essential retry-operation related library for Golang to build fault-tolerant system.

## Usages:

```go
import("github.com/rbrahul/retry")
```

### Retry the operation maximum 10 times in 3 seconds delay in between
```go

err := retry.Retry(func() bool {
err := doesHeavyLifting()
if err != nil {
return true // retry operation
}
return false // No need to retry
}, 10, 3*time.Second)

if err != nil {
fmt.Error("Maxium retry exceeded")
}
```

### Retry failed operations with a deadline of 1 minute and with a random interval of 2 to 10 seconds.

```go
err := retry.Retry(func() bool {
err := doesHeavyLifting()
if err != nil {
return true // retry operation
}
return false // No need to retry
}, 1 * time.Minute(), retry.RandomBackoff(2, 10))

if err == retry.ErrDeadlineExceeded {
fmt.Error("Retry deadline exceeded")
}

```
### Retry failed operations with a maximum 10 retries and with an Custom Backoff function.

```go
err := retry.Retry(func() bool {
err := doesHeavyLifting()
if err != nil {
return true // retry operation
}
return false // No need to retry
}, 10, func(previousDelay uint64) uint64 {
return previousDelay * 1.5
})

if err == retry.ErrMaximumRetryExceeded {
fmt.Error("Maxium retry exceeded")
}

```
62 changes: 30 additions & 32 deletions retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,96 +2,95 @@ package retry

import (
"errors"
"fmt"
"math"
"reflect"
"time"
)

type Retryable func() bool
type BackOffFn func(uint64) uint64
type BackoffFunc func(uint64) uint64

type retryManger struct {
type retryManager struct {
maxRetry int64
delayerFn BackOffFn
backoffFn BackoffFunc
retryUntil time.Duration
startedAt time.Time
delay time.Duration
lastBackOff uint64 // in number of seconds
lastBackoff uint64 // in number of seconds
}

var (
ErrMaxRetryOrRetryUntilInvalidArg = errors.New("invalid argument type. maxRetry can be either integer, time.Duration or func(uint64) uint64")
ErrDelayOrBackOffFuncInvalidArg = errors.New("invalid argument type. delay can be either time.Duration or `func(uint64) uint64`")
ErrDeadlineExceeded = errors.New("retry deadline has been exceeded")
ErrMaximumRetryExceeded = errors.New("maximum retry has been exceeded")
)

const defaultDelayDuration = 1 * time.Second

func (rm *retryManger) parseParams(args ...interface{}) error {
func (rm *retryManager) parseParams(args ...interface{}) error {
if len(args) > 0 {
firstArgKind := reflect.TypeOf(args[0]).Kind()
if reflect.TypeOf(args[0]).String() == "time.Duration" {
maxDuration := args[0].(time.Duration)
rm.retryUntil = maxDuration
} else if isIntKind(firstArgKind) {
rm.maxRetry = int64(math.Abs(float64(reflect.ValueOf(args[0]).Int())))
} else if len(args) == 1 && reflect.TypeOf(args[0]).String() == "func(uint64) uint64" {
rm.delayerFn = args[0].(func(uint64) uint64)
} else {
return errors.New("invalid argument type. maxRetry can be either integer or time.Duration")
}

if len(args) == 1 && reflect.TypeOf(args[0]).String() == "func(uint64) uint64" {
rm.delayerFn = args[0].(func(uint64) uint64)
return ErrMaxRetryOrRetryUntilInvalidArg
}

// delay in duration /backOff as func --> exponentialBackOff, randInt, customBackOffFn
// delay in time.Duration or backOffFunc as func(uint64) uint64
if len(args) > 1 {
if reflect.TypeOf(args[1]).String() == "time.Duration" {
rm.delay = args[1].(time.Duration)
} else if reflect.TypeOf(args[1]).String() == "func(uint64) uint64" {
rm.delayerFn = args[1].(func(uint64) uint64)
rm.backoffFn = args[1].(func(uint64) uint64)
} else {
return errors.New("invalid argument type for delay. delay can be either time.Duration or `func(uint64) uint64`")
return ErrDelayOrBackOffFuncInvalidArg
}
}
}
if rm.delayerFn == nil && rm.delay == 0 {
if rm.backoffFn == nil && rm.delay == 0 {
rm.delay = defaultDelayDuration
}
return nil
}

func (rm *retryManger) addDelay() {
func (rm *retryManager) addDelay() {
var delayInBetween time.Duration
if rm.lastBackOff == 0 {
rm.lastBackOff = 1
if rm.lastBackoff == 0 {
rm.lastBackoff = 1
}
if rm.delayerFn != nil {
numberOfSeconds := rm.delayerFn(rm.lastBackOff)
rm.lastBackOff = numberOfSeconds
if rm.backoffFn != nil {
numberOfSeconds := rm.backoffFn(rm.lastBackoff)
rm.lastBackoff = numberOfSeconds
delayInBetween = time.Duration(numberOfSeconds) * time.Second
} else {
delayInBetween = rm.delay
}
fmt.Println("delayInBetween:", delayInBetween)
withJitter := addJitter(delayInBetween)
fmt.Println("With Jitter:", withJitter)
time.Sleep(withJitter)
}

func (rm *retryManger) execute(fn Retryable) error {
func (rm *retryManager) execute(fn Retryable) error {
shouldRetry := fn()
for shouldRetry {
if rm.retryUntil > 0 {
rm.addDelay()
deadLineExceeded := time.Now().After(rm.startedAt.Add(rm.retryUntil))
if deadLineExceeded {
return errors.New("retry deadline has been exceeded")
return ErrDeadlineExceeded
}
rm.addDelay()
shouldRetry = fn()
continue
}

// If maxRetry is set 5 then 5-1 time will be retried. Because initially function was already excuted once.
// Which means: 1 (Initial Call) + 4 retries == 5 maxRetry
if rm.maxRetry > 0 {
rm.maxRetry -= 1
if rm.maxRetry == 0 {
return errors.New("maximum retry has been exceeded")
return ErrMaximumRetryExceeded
}
}

Expand All @@ -113,10 +112,9 @@ func (rm *retryManger) execute(fn Retryable) error {
// Retry(retriableFn func() bool, retryUntil time.Duration, delay time.Duration)
// Retry(retriableFn func() bool, retryUntil time.Duration, backOffFn func(uint64) uint64)
// Retry(retriableFn func() bool, maxNumberOfRetry int)
// Retry(retriableFn, backOffFn func(uint64) uint64)) [If 2nd argument is function then it will be treated as backOffFn and maxNumberOfRetry will be considered Infinity]
// Retry(retriableFn)
func Retry(fn Retryable, args ...interface{}) error {
retryManager := &retryManger{
retryManager := &retryManager{
startedAt: time.Now(),
}
err := retryManager.parseParams(args...)
Expand Down
Loading

0 comments on commit 2f9f484

Please sign in to comment.