diff --git a/example/go.mod b/example/go.mod index 1e9669d..19f43df 100644 --- a/example/go.mod +++ b/example/go.mod @@ -22,6 +22,7 @@ require ( cloud.google.com/go/firestore v1.9.0 // indirect cloud.google.com/go/longrunning v0.3.0 // indirect github.com/armon/go-metrics v0.4.0 // indirect + github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/go.sum b/example/go.sum index 86aaf11..c080ecd 100644 --- a/example/go.sum +++ b/example/go.sum @@ -62,6 +62,8 @@ github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8 github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= diff --git a/example/roundtripwares/retry/client.go b/example/roundtripwares/retry/client.go new file mode 100644 index 0000000..15bc38d --- /dev/null +++ b/example/roundtripwares/retry/client.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "time" + + keellog "github.com/foomo/keel/log" + keelhttp "github.com/foomo/keel/net/http" + "github.com/foomo/keel/net/http/roundtripware" +) + +func client() { + l := keellog.Logger() + + client := keelhttp.NewHTTPClient( + keelhttp.HTTPClientWithRoundTripware(l, + roundtripware.Retry( + roundtripware.RetryWithAttempts(12), + roundtripware.RetryWithMaxDelay(time.Second), + ), + )) + + _, err := client.Get("http://localhost:8080/404") //nolint:all + keellog.Must(l, err, "failed to retrieve response") + + fmt.Printf("Repetition process is finished with: %v\n", err) //nolint:forbidigo +} diff --git a/example/roundtripwares/retry/main.go b/example/roundtripwares/retry/main.go new file mode 100644 index 0000000..ff29631 --- /dev/null +++ b/example/roundtripwares/retry/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "time" +) + +func main() { + go server() + + go client() + + time.Sleep(time.Second * 60) +} diff --git a/example/roundtripwares/retry/server.go b/example/roundtripwares/retry/server.go new file mode 100644 index 0000000..70a270d --- /dev/null +++ b/example/roundtripwares/retry/server.go @@ -0,0 +1,34 @@ +package main + +import ( + "net/http" + + "github.com/foomo/keel" +) + +func server() { + svr := keel.NewServer() + + // get logger + l := svr.Logger() + + // create demo service + svs := http.NewServeMux() + + counter := 0 + svs.HandleFunc("/404", func(w http.ResponseWriter, r *http.Request) { + counter++ + if counter < 10 { + http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + + svr.AddService( + keel.NewServiceHTTP(l, "demo", "localhost:8080", svs), + ) + + svr.Run() +} diff --git a/go.mod b/go.mod index bcdc15d..f5810de 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/foomo/keel go 1.19 require ( + github.com/avast/retry-go v3.0.0+incompatible github.com/foomo/gotsrpc/v2 v2.6.2 github.com/go-logr/logr v1.2.3 github.com/golang-jwt/jwt v3.2.2+incompatible diff --git a/go.sum b/go.sum index 5ada858..40a1ebe 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8 github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= diff --git a/net/http/roundtripware/retry.go b/net/http/roundtripware/retry.go new file mode 100644 index 0000000..768bd10 --- /dev/null +++ b/net/http/roundtripware/retry.go @@ -0,0 +1,103 @@ +package roundtripware + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/avast/retry-go" + "go.uber.org/zap" +) + +type ( + RetryOptions struct { + Handler RetryHandler + retryOptions []retry.Option + } + RetryHandler func(*http.Response) error + RetryOption func(*RetryOptions) +) + +func GetDefaultRetryOptions() RetryOptions { + return RetryOptions{ + Handler: func(resp *http.Response) error { + if resp.StatusCode != http.StatusOK { + return errors.New("status code not ok") + } + return nil + }, + } +} + +func RetryWithAttempts(v uint) RetryOption { + return func(o *RetryOptions) { + o.retryOptions = append(o.retryOptions, retry.Attempts(v)) + } +} + +func RetryWithContext(ctx context.Context) RetryOption { + return func(o *RetryOptions) { + o.retryOptions = append(o.retryOptions, retry.Context(ctx)) + } +} + +func RetryWithDelay(delay time.Duration) RetryOption { + return func(o *RetryOptions) { + o.retryOptions = append(o.retryOptions, retry.Delay(delay)) + } +} + +func RetryWithMaxDelay(maxDelay time.Duration) RetryOption { + return func(o *RetryOptions) { + o.retryOptions = append(o.retryOptions, retry.MaxDelay(maxDelay)) + } +} + +func RetryWithDelayType(delayType retry.DelayTypeFunc) RetryOption { + return func(o *RetryOptions) { + o.retryOptions = append(o.retryOptions, retry.DelayType(delayType)) + } +} + +func RetryWithOnRetry(onRetry retry.OnRetryFunc) RetryOption { + return func(o *RetryOptions) { + o.retryOptions = append(o.retryOptions, retry.OnRetry(onRetry)) + } +} + +func RetryWithLastErrorOnly(lastErrorOnly bool) RetryOption { + return func(o *RetryOptions) { + o.retryOptions = append(o.retryOptions, retry.LastErrorOnly(lastErrorOnly)) + } +} + +func RetryWithRetryIf(retryIf retry.RetryIfFunc) RetryOption { + return func(o *RetryOptions) { + o.retryOptions = append(o.retryOptions, retry.RetryIf(retryIf)) + } +} + +// Retry returns a RoundTripper which retries failed requests +func Retry(opts ...RetryOption) RoundTripware { + o := GetDefaultRetryOptions() + for _, opt := range opts { + if opt != nil { + opt(&o) + } + } + return func(l *zap.Logger, next Handler) Handler { + return func(req *http.Request) (*http.Response, error) { + var resp *http.Response + err := retry.Do(func() error { + var err error + resp, err = next(req) //nolint:bodyclose + if err != nil { + return err + } + return o.Handler(resp) + }, o.retryOptions...) + return resp, err + } + } +}