mirror of
https://github.com/IBM/fp-go.git
synced 2025-08-10 22:31:32 +02:00
124 lines
4.0 KiB
Go
124 lines
4.0 KiB
Go
// Copyright (c) 2023 IBM Corp.
|
|
// All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package retry
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
|
|
F "github.com/IBM/fp-go/function"
|
|
M "github.com/IBM/fp-go/monoid"
|
|
O "github.com/IBM/fp-go/option"
|
|
"github.com/IBM/fp-go/ord"
|
|
)
|
|
|
|
type RetryStatus struct {
|
|
// Iteration number, where `0` is the first try
|
|
IterNumber uint
|
|
// Delay incurred so far from retries
|
|
CumulativeDelay time.Duration
|
|
// Latest attempt's delay. Will always be `none` on first run.
|
|
PreviousDelay O.Option[time.Duration]
|
|
}
|
|
|
|
// RetryPolicy is a function that takes an `RetryStatus` and
|
|
// possibly returns a delay in milliseconds. Iteration numbers start
|
|
// at zero and increase by one on each retry. A //None// return value from
|
|
// the function implies we have reached the retry limit.
|
|
type RetryPolicy = func(RetryStatus) O.Option[time.Duration]
|
|
|
|
const emptyDuration = time.Duration(0)
|
|
|
|
var ordDuration = ord.FromStrictCompare[time.Duration]()
|
|
|
|
// 'RetryPolicy' is a 'Monoid'. You can collapse multiple strategies into one using 'concat'.
|
|
// The semantics of this combination are as follows:
|
|
//
|
|
// 1. If either policy returns 'None', the combined policy returns
|
|
// 'None'. This can be used to inhibit after a number of retries,
|
|
// for example.
|
|
//
|
|
// 2. If both policies return a delay, the larger delay will be used.
|
|
// This is quite natural when combining multiple policies to achieve a
|
|
// certain effect.
|
|
var Monoid = M.FunctionMonoid[RetryStatus](O.ApplicativeMonoid(M.MakeMonoid(
|
|
ord.MaxSemigroup(ordDuration).Concat, emptyDuration)))
|
|
|
|
// LimitRetries retries immediately, but only up to `i` times.
|
|
func LimitRetries(i uint) RetryPolicy {
|
|
pred := func(value uint) bool {
|
|
return value < i
|
|
}
|
|
empty := F.Constant1[uint](emptyDuration)
|
|
return func(status RetryStatus) O.Option[time.Duration] {
|
|
return F.Pipe2(
|
|
status.IterNumber,
|
|
O.FromPredicate(pred),
|
|
O.Map(empty),
|
|
)
|
|
}
|
|
}
|
|
|
|
// ConstantDelay delays with unlimited retries
|
|
func ConstantDelay(delay time.Duration) RetryPolicy {
|
|
return F.Constant1[RetryStatus](O.Of(delay))
|
|
}
|
|
|
|
// CapDelay sets a time-upperbound for any delays that may be directed by the
|
|
// given policy. This function does not terminate the retrying. The policy
|
|
// capDelay(maxDelay, exponentialBackoff(n))` will never stop retrying. It
|
|
// will reach a state where it retries forever with a delay of `maxDelay`
|
|
// between each one. To get termination you need to use one of the
|
|
// 'limitRetries' function variants.
|
|
func CapDelay(maxDelay time.Duration, policy RetryPolicy) RetryPolicy {
|
|
return F.Flow2(
|
|
policy,
|
|
O.Map(F.Bind1st(ord.Min(ordDuration), maxDelay)),
|
|
)
|
|
}
|
|
|
|
// ExponentialBackoff grows delay exponentially each iteration.
|
|
// Each delay will increase by a factor of two.
|
|
func ExponentialBackoff(delay time.Duration) RetryPolicy {
|
|
return func(status RetryStatus) O.Option[time.Duration] {
|
|
return O.Some(delay * time.Duration(math.Pow(2, float64(status.IterNumber))))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initial, default retry status. Exported mostly to allow user code
|
|
* to test their handlers and retry policies.
|
|
*/
|
|
var DefaultRetryStatus = RetryStatus{
|
|
IterNumber: 0,
|
|
CumulativeDelay: 0,
|
|
PreviousDelay: O.None[time.Duration](),
|
|
}
|
|
|
|
var getOrElseDelay = O.GetOrElse(F.Constant(emptyDuration))
|
|
|
|
/**
|
|
* Apply policy on status to see what the decision would be.
|
|
*/
|
|
func ApplyPolicy(policy RetryPolicy, status RetryStatus) RetryStatus {
|
|
previousDelay := policy(status)
|
|
return RetryStatus{
|
|
IterNumber: status.IterNumber + 1,
|
|
CumulativeDelay: status.CumulativeDelay + getOrElseDelay(previousDelay),
|
|
PreviousDelay: previousDelay,
|
|
}
|
|
}
|