Skip to content

Commit 1ad931b

Browse files
committed
feat: add retry mechanism
0 parents  commit 1ad931b

File tree

3 files changed

+227
-0
lines changed

3 files changed

+227
-0
lines changed

README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Retry
2+
Retry is a golang pure package implements the `retry-pattern` to retry failures until certain attempts in certain time.
3+
4+
## Installation
5+
At first you need to download the library using `go get`
6+
```bash
7+
$ go get -u github.com/morilog/go-retry
8+
```
9+
10+
Or just use it on your package by importing this:
11+
```golang
12+
import "github.com/morilog/retry"
13+
```
14+
And download it with `go mod`
15+
```bash
16+
$ go mod tidy
17+
```
18+
19+
## Quick Start
20+
Assume you have a function that calls an external api:
21+
22+
```golang
23+
func getSomeExternalData(url string) error {
24+
// your business logic goes here
25+
}
26+
```
27+
It might be failed during network errors or any other external errors. With `retry` you can retry your logic until maximum number of attempts exceeded.
28+
```golang
29+
package main
30+
31+
import "github.com/morilog/retry"
32+
33+
func main() {
34+
ctx := context.Background()
35+
36+
// As default
37+
// It retries 10 times with increased 100ms delay
38+
err := retry.Retry(ctx, func() error {
39+
return getSomeExternalData("https://example.com")
40+
}, retry.Delay(time.Second))
41+
42+
if err != nil {
43+
log.Fatal(err)
44+
}
45+
}
46+
```
47+
48+
## Options
49+
### retry.Delay(delay time.Duration)
50+
To set minimum delay duration between each retry.
51+
> Default: 100ms
52+
### retry.DelayFactor(factor int)
53+
To set number that multiplies to delay to increase delay time by increasing attempts.
54+
The delay for each retry calculated by this formula:
55+
```
56+
AttemptDelay = Attempt * DelayFactor * Delay
57+
```
58+
> Default: 1
59+
### retry.MaxAttempts(attempts uint8)
60+
Sets maximum number of attempts to handle the operation
61+
62+
> Default: 10
63+
### retry.StopRetryIf(fn StopRetryIfFunc)
64+
Sometimes you need to cancel the retry mechanism when your operation returns some specific error. To achieve to this purpose, You can use this option.
65+
> Default: `<not set>`
66+
67+
For example i stop the retry when an `SomeError` occurred:
68+
```golang
69+
err := retry.Retry(ctx, func() error {
70+
return getSomeExternalData("https://example.com")
71+
},
72+
retry.Delay(time.Second),
73+
retry.StopRetryIf(func(ctx context.Context, err error) bool {
74+
if _, ok := err.(*SomeError); ok {
75+
return true
76+
}
77+
78+
return false
79+
}),
80+
)
81+
82+
if err != nil {
83+
log.Fatal(err)
84+
}
85+
```
86+
### retry.OnRetry(fn OnRetryFunc)
87+
Sometimes you need to know retry is on which attempt to do something like logging
88+
> Default: `<not set>`
89+
90+
For example:
91+
```golang
92+
err := retry.Retry(ctx, func() error {
93+
return getSomeExternalData("https://example.com")
94+
},
95+
retry.Delay(time.Second),
96+
retry.StopRetryIf(func(ctx context.Context, attempt int) error {
97+
logrus.Printf("The function retries for %d times.\n", attempt)
98+
return nil
99+
}),
100+
)
101+
102+
if err != nil {
103+
log.Fatal(err)
104+
}
105+
```

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/morilog/retry
2+
3+
go 1.18

retry.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package retry
2+
3+
import (
4+
"context"
5+
"time"
6+
)
7+
8+
// Default Delay func options
9+
const (
10+
DefaultAttempts uint8 = 10
11+
DefaultDelay = time.Millisecond * 100
12+
DefaultDelayFactor = 1
13+
)
14+
15+
type config struct {
16+
maxAttempts uint8
17+
delay time.Duration
18+
delayFactor int
19+
stopRetryIf StopRetryIfFunc
20+
onRetry OnRetryFunc
21+
}
22+
23+
// Option is a function type for
24+
// manipulating Retry configs
25+
type Option func(*config)
26+
27+
// MaxAttempts is an option to set maximum attempts number
28+
// It could be a integer number between 0-255
29+
// Default is 10
30+
func MaxAttempts(attempts uint8) Option {
31+
return func(c *config) {
32+
c.maxAttempts = attempts
33+
}
34+
}
35+
36+
// Delay is an option to set minimum delay between each retries
37+
// Default is 100ms
38+
func Delay(delay time.Duration) Option {
39+
return func(c *config) {
40+
c.delay = delay
41+
}
42+
}
43+
44+
// DelayFactor is an option to set increase of delay
45+
// duration on each retries
46+
// Actual delay calculated by: (delay * delayFactor * attemptNumber)
47+
// Default is 1
48+
func DelayFactor(factor int) Option {
49+
return func(c *config) {
50+
c.delayFactor = factor
51+
}
52+
}
53+
54+
// StopRetryIfFunc is a function type to set conditions
55+
// To stop continuing the retry mechanism
56+
type StopRetryIfFunc func(ctx context.Context, err error) bool
57+
58+
// StopRetryIf is an option to set StopRetryIfFunc
59+
func StopRetryIf(fn StopRetryIfFunc) Option {
60+
return func(c *config) {
61+
c.stopRetryIf = fn
62+
}
63+
}
64+
65+
// OnRetryFunc is a function type to set some
66+
// functionality to retry function
67+
// It stops retry mechanism when error was not nil
68+
type OnRetryFunc func(ctx context.Context, attempt int) error
69+
70+
// OnRetry is an option to set OnRetryFunc
71+
func OnRetry(fn OnRetryFunc) Option {
72+
return func(c *config) {
73+
c.onRetry = fn
74+
}
75+
}
76+
77+
// Retry tries to handle the operation func
78+
// It tries until maxAttempts exceeded
79+
// At the end of retries returns error from operation func
80+
func Retry(ctx context.Context, operation func() error, opts ...Option) error {
81+
cfg := &config{
82+
maxAttempts: DefaultAttempts,
83+
delay: DefaultDelay,
84+
delayFactor: DefaultDelayFactor,
85+
}
86+
87+
for _, opt := range opts {
88+
opt(cfg)
89+
}
90+
91+
var err error
92+
for attempt := 0; attempt < int(cfg.maxAttempts); attempt++ {
93+
err = operation()
94+
if err == nil {
95+
return nil
96+
}
97+
98+
if cfg.stopRetryIf != nil {
99+
if cfg.stopRetryIf(ctx, err) {
100+
break
101+
}
102+
}
103+
104+
if cfg.onRetry != nil {
105+
if err := cfg.onRetry(ctx, attempt+1); err != nil {
106+
return err
107+
}
108+
}
109+
110+
select {
111+
case <-ctx.Done():
112+
return err
113+
case <-time.After(cfg.delay * time.Duration(attempt*cfg.delayFactor)):
114+
continue
115+
}
116+
}
117+
118+
return err
119+
}

0 commit comments

Comments
 (0)