1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-19 23:42:05 +02:00
Files
fp-go/v2/idiomatic/context/readerresult/retry.go
Dr. Carsten Leue 20398e67a9 fix: better doc and implementation of retry
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-17 15:58:11 +01:00

147 lines
6.0 KiB
Go

// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache LicensVersion 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 readerresult
import (
RS "github.com/IBM/fp-go/v2/context/readerresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
R "github.com/IBM/fp-go/v2/retry"
)
// Retrying retries a ReaderResult computation according to a retry policy with context awareness.
//
// This is the idiomatic wrapper around the functional [github.com/IBM/fp-go/v2/context/readerresult.Retrying]
// function. It provides a more Go-friendly API by working with (value, error) tuples instead of Result types.
//
// The function implements a retry mechanism for operations that depend on a [context.Context] and can fail.
// It respects context cancellation, meaning that if the context is cancelled during retry delays, the
// operation will stop immediately and return the cancellation error.
//
// The retry loop will continue until one of the following occurs:
// - The action succeeds and the check function returns false (no retry needed)
// - The retry policy returns None (retry limit reached)
// - The check function returns false (indicating success or a non-retryable failure)
// - The context is cancelled (returns context.Canceled or context.DeadlineExceeded)
//
// Parameters:
//
// - policy: A RetryPolicy that determines when and how long to wait between retries.
// The policy receives a RetryStatus on each iteration and returns an optional delay.
// If it returns None, retrying stops. Common policies include LimitRetries,
// ExponentialBackoff, and CapDelay from the retry package.
//
// - action: A Kleisli arrow that takes a RetryStatus and returns a ReaderResult[A].
// This function is called on each retry attempt and receives information about the
// current retry state (iteration number, cumulative delay, etc.). The action depends
// on a context.Context and produces (A, error). The context passed to the action
// will be the same context used for retry delays, so cancellation is properly propagated.
//
// - check: A predicate function that examines the result value and error, returning true
// if the operation should be retried, or false if it should stop. This allows you to
// distinguish between retryable failures (e.g., network timeouts) and permanent
// failures (e.g., invalid input). The function receives both the value and error from
// the action's result. Note that context cancellation errors will automatically stop
// retrying regardless of this function's return value.
//
// Returns:
//
// A ReaderResult[A] that, when executed with a context, will perform the retry
// logic with context cancellation support and return the final (value, error) tuple.
//
// Type Parameters:
// - A: The type of the success value
//
// Context Cancellation:
//
// The retry mechanism respects context cancellation in two ways:
// 1. During retry delays: If the context is cancelled while waiting between retries,
// the operation stops immediately and returns the context error.
// 2. During action execution: If the action itself checks the context and returns
// an error due to cancellation, the retry loop will stop (assuming the check
// function doesn't force a retry on context errors).
//
// Implementation Details:
//
// This function wraps the functional [github.com/IBM/fp-go/v2/context/readerresult.Retrying]
// by converting between the idiomatic (value, error) tuple representation and the functional
// Result[A] representation. The conversion is handled by ToReaderResult and FromReaderResult,
// ensuring seamless integration with the underlying retry mechanism that uses delayWithCancel
// to properly handle context cancellation during delays.
//
// Example:
//
// // Create a retry policy: exponential backoff with a cap, limited to 5 retries
// policy := retry.Monoid.Concat(
// retry.LimitRetries(5),
// retry.CapDelay(10*time.Second, retry.ExponentialBackoff(100*time.Millisecond)),
// )
//
// // Action that fetches data, with retry status information
// fetchData := func(status retry.RetryStatus) ReaderResult[string] {
// return func(ctx context.Context) (string, error) {
// // Check if context is cancelled
// if ctx.Err() != nil {
// return "", ctx.Err()
// }
// // Simulate an HTTP request that might fail
// if status.IterNumber < 3 {
// return "", fmt.Errorf("temporary error")
// }
// return "success", nil
// }
// }
//
// // Check function: retry on any error except context cancellation
// shouldRetry := func(val string, err error) bool {
// return err != nil && !errors.Is(err, context.Canceled)
// }
//
// // Create the retrying computation
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute with a cancellable context
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// result, err := retryingFetch(ctx)
//
// See also:
// - retry.RetryPolicy for available retry policies
// - retry.RetryStatus for information passed to the action
// - context.Context for context cancellation semantics
// - github.com/IBM/fp-go/v2/context/readerresult.Retrying for the underlying functional implementation
//
//go:inline
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(A, error) bool,
) ReaderResult[A] {
return F.Pipe1(
RS.Retrying(
policy,
F.Flow2(
action,
ToReaderResult,
),
func(a Result[A]) bool {
return check(result.Unwrap(a))
},
),
FromReaderResult,
)
}