Skip to content

Commit

Permalink
chore: add initial retry mechanism (#2)
Browse files Browse the repository at this point in the history
* chore: add initial retry mechanism

* refactor: use defaultValkyrietry in defaultValkyrietryWithContext

* refactor: use time.timer directly instead of abstract it

* refactor: simplify the retry mechanism runner function into two
  • Loading branch information
RiskyFeryansyahP authored Dec 11, 2023
1 parent 9ae8da1 commit 99f584a
Show file tree
Hide file tree
Showing 11 changed files with 573 additions and 5 deletions.
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ To get started with Valkyrietry, simply install the library in your GoLang proje
package main

import (
"context"
"errors"
"fmt"
"net/http"
Expand All @@ -31,11 +32,13 @@ import (
)

func main() {
ctx := context.Background()

options := []valkyrietry.Option{
valkyrietry.WithMaxRetryAttempts(5),
valkyrietry.WithRetryDelay(0.5 * time.Second) // Google use 0.5 as initial retry,
valkyrietry.WithRetryBackoffMultiplier(1.5) // Google also use 1.5 for default multiplier,
valkyrietry.WithJitter(0.5),
valkyrietry.WithMaxRetryAttempts(1),
valkyrietry.WithRetryDelay(2 * time.Second),
valkyrietry.WithRetryBackoffMultiplier(2),
valkyrietry.WithJitter(0.2),
}

retryFunc := func() error {
Expand All @@ -55,7 +58,7 @@ func main() {
}

// Use Valkyrietry to handle the retry logic
if err := valkyrietry.Do(retryFunc, options...); err != nil {
if err := valkyrietry.Do(ctx, retryFunc, options...); err != nil {
fmt.Println("Operation failed after retries:", err)
return
}
Expand Down
10 changes: 10 additions & 0 deletions cons.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package valkyrietry

import "time"

const (
DefaultMaxRetryAttempt = 5
DefaultRetryDelay = time.Duration(0.5 * float64(time.Second))
DefaultRetryBackoffMultiplier = 1.5
DefaultJitter = 0.5
)
7 changes: 7 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package valkyrietry

import "fmt"

var (
ErrMaxRetryAttemptsExceeded = fmt.Errorf("function is failed to retry after max attemps retries")
)
46 changes: 46 additions & 0 deletions example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"context"
"errors"
"fmt"
"net/http"
"time"

"github.com/ruang-guru/valkyrietry"
)

func main() {
ctx := context.Background()

options := []valkyrietry.Option{
valkyrietry.WithMaxRetryAttempts(1),
valkyrietry.WithRetryDelay(2 * time.Second),
valkyrietry.WithRetryBackoffMultiplier(2),
valkyrietry.WithJitter(0.2),
}

retryFunc := func() error {
resp, err := http.Get("http://testingexample.com")
if err != nil {
fmt.Println("Request failed, will retry:", err)
return err
}
defer resp.Body.Close()

if resp.StatusCode >= 500 {
// Simulate server-side error
return errors.New("server error, retrying")
}
fmt.Println("Request succeeded")
return nil
}

// Use Valkyrietry to handle the retry logic
if err := valkyrietry.Do(ctx, retryFunc, options...); err != nil {
fmt.Println("Operation failed after retries:", err)
return
}

fmt.Println("Operation successful")
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/ruang-guru/valkyrietry

go 1.20
51 changes: 51 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package valkyrietry

import (
"time"
)

type Configuration struct {
MaxRetryAttempts uint
InitialRetryDelay time.Duration
RetryBackoffMultiplier float32
JitterPercentage float32
}

// option is a function option used to configure a Valkyrietry.
type Option func(c *Configuration)

// WithMaxRetryAttempts
// Set the maximum number of retry attempts for the retry mechanism.
//
// if you set the attempt to 0, it means it will run until the process succed
func WithMaxRetryAttempts(attempt uint) Option {
return func(c *Configuration) {
c.MaxRetryAttempts = attempt
}
}

// WithRetryDelay
// Set the initial duration value for the first retry.
func WithRetryDelay(delay time.Duration) Option {
return func(c *Configuration) {
c.InitialRetryDelay = delay
}
}

// WithRetryBackoffMultiplier
// Set the multiplier for each failed retry attempt.
// Formula: initial retry delay * multiplier.
func WithRetryBackoffMultiplier(multiplier float32) Option {
return func(c *Configuration) {
c.RetryBackoffMultiplier = multiplier
}
}

// WithJitter
// Set the percentage of jitter value to determine the lowest and highest
// random values.
func WithJitter(percentage float32) Option {
return func(c *Configuration) {
c.JitterPercentage = percentage
}
}
50 changes: 50 additions & 0 deletions option_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package valkyrietry

import (
"testing"
"time"
)

func TestWithMaxRetryAttempts(t *testing.T) {
expectedAttempts := uint(5)
config := &Configuration{}
option := WithMaxRetryAttempts(expectedAttempts)
option(config)

if config.MaxRetryAttempts != expectedAttempts {
t.Errorf("WithMaxRetryAttempts() = %v, want %v", config.MaxRetryAttempts, expectedAttempts)
}
}

func TestWithRetryDelay(t *testing.T) {
expectedDelay := 100 * time.Millisecond
config := &Configuration{}
option := WithRetryDelay(expectedDelay)
option(config)

if config.InitialRetryDelay != expectedDelay {
t.Errorf("WithRetryDelay() = %v, want %v", config.InitialRetryDelay, expectedDelay)
}
}

func TestWithRetryBackoffMultiplier(t *testing.T) {
expectedMultiplier := float32(2.0)
config := &Configuration{}
option := WithRetryBackoffMultiplier(expectedMultiplier)
option(config)

if config.RetryBackoffMultiplier != expectedMultiplier {
t.Errorf("WithRetryBackoffMultiplier() = %v, want %v", config.RetryBackoffMultiplier, expectedMultiplier)
}
}

func TestWithJitter(t *testing.T) {
expectedJitter := float32(0.25)
config := &Configuration{}
option := WithJitter(expectedJitter)
option(config)

if config.JitterPercentage != expectedJitter {
t.Errorf("WithJitter() = %v, want %v", config.JitterPercentage, expectedJitter)
}
}
38 changes: 38 additions & 0 deletions timer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package valkyrietry

import "time"

type Timer struct {
timer *time.Timer
}

func NewTimer() *Timer {
return &Timer{}
}

// Start
// Set the timer for the specified duration.
// If the current timer is nil, initialize a new one;
// otherwise, reset it to the new duration.
func (t *Timer) Start(duration time.Duration) {
if t.timer == nil {
t.timer = time.NewTimer(duration)
return
}

t.timer.Reset(duration)
}

// Stop
// Stop the current timer.
func (t *Timer) Stop() {
if t.timer != nil {
t.timer.Stop()
}
}

// C
// Retrieve the channel when either the timer stops or the timer completes.
func (t *Timer) C() <-chan time.Time {
return t.timer.C
}
50 changes: 50 additions & 0 deletions timer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package valkyrietry

import (
"testing"
"time"
)

func TestTimerStart(t *testing.T) {
timer := NewTimer()
duration := 100 * time.Millisecond

start := time.Now()
timer.Start(duration)
<-timer.C()

if time.Since(start) < duration {
t.Errorf("Timer fired before the expected duration")
}
}

func TestTimerReset(t *testing.T) {
timer := NewTimer()
firstDuration := 50 * time.Millisecond
secondDuration := 100 * time.Millisecond

timer.Start(firstDuration)
time.Sleep(30 * time.Millisecond)
timer.Start(secondDuration)

start := time.Now()
<-timer.C()

if elapsed := time.Since(start); elapsed < secondDuration {
t.Errorf("Timer fired before the expected reset duration, elapsed: %v", elapsed)
}
}

func TestTimerStop(t *testing.T) {
timer := NewTimer()
duration := 100 * time.Millisecond

timer.Start(duration)
timer.Stop()

select {
case <-timer.C():
t.Errorf("Timer channel should not receive after being stopped")
case <-time.After(150 * time.Millisecond):
}
}
Loading

0 comments on commit 99f584a

Please sign in to comment.