Files
ragflow/internal/harness/graph/pregel/retry.go
Yingfeng 706e0d2d06 Refactor harness framework (#16271)
### What problem does this PR solve?

- Tools management
- Pregel engine wrapper for better usage
- UT race
- Coding style

### Type of change

- [x] Refactoring
2026-06-23 20:18:04 +08:00

261 lines
6.5 KiB
Go

// Package pregel provides enhanced retry policies for Pregel execution.
package pregel
import (
"context"
"fmt"
"time"
"ragflow/internal/harness/graph/errors"
"ragflow/internal/harness/graph/types"
)
// RetryExecutor handles node execution with sophisticated retry logic.
type RetryExecutor struct {
policy *types.RetryPolicy
}
// NewRetryExecutor creates a new retry executor with the given policy.
func NewRetryExecutor(policy *types.RetryPolicy) *RetryExecutor {
if policy == nil {
defaultPolicy := types.DefaultRetryPolicy()
policy = &defaultPolicy
}
return &RetryExecutor{policy: policy}
}
// Execute executes a function with retry logic.
func (e *RetryExecutor) Execute(ctx context.Context, name string, fn func(context.Context) (any, error)) (output any, err error) {
defer func() {
if r := recover(); r != nil {
output = nil
err = fmt.Errorf("node %s panicked: %v", name, r)
}
}()
var lastErr error
var lastOutput any
for attempt := 1; attempt <= e.policy.MaxAttempts; attempt++ {
// Execute the function
output, err = fn(ctx)
if err == nil {
return output, nil
}
// Check if this is a non-retryable error
if errors.IsGraphInterrupt(err) {
return nil, err // propagate GraphInterrupt immediately without wrapping
}
if e.policy.RetryOn != nil && !e.policy.RetryOn(err) {
return nil, fmt.Errorf("node %s failed with non-retryable error: %w", name, err)
}
lastErr = err
lastOutput = output
// If we've exhausted attempts, break
if attempt >= e.policy.MaxAttempts {
break
}
// Calculate backoff with jitter
backoff := e.calculateBackoff(attempt)
// Wait before retry
select {
case <-ctx.Done():
return nil, fmt.Errorf("node %s cancelled during retry: %w", name, ctx.Err())
case <-time.After(backoff):
// Continue to next attempt
}
}
return nil, &RetryExhaustedError{
NodeName: name,
Attempts: e.policy.MaxAttempts,
LastErr: lastErr,
LastOutput: lastOutput,
}
}
// calculateBackoff calculates the backoff duration with optional jitter.
// Delegates to the shared RetryPolicy.CalculateBackoff method.
func (e *RetryExecutor) calculateBackoff(attempt int) time.Duration {
if e.policy == nil {
defaultPolicy := types.DefaultRetryPolicy()
return defaultPolicy.CalculateBackoff(attempt)
}
return e.policy.CalculateBackoff(attempt)
}
// RetryExhaustedError is returned when all retry attempts are exhausted.
type RetryExhaustedError struct {
NodeName string
Attempts int
LastErr error
LastOutput any
}
// Error implements the error interface.
func (e *RetryExhaustedError) Error() string {
return fmt.Sprintf("node %s failed after %d attempts: %v", e.NodeName, e.Attempts, e.LastErr)
}
// Unwrap returns the underlying error.
func (e *RetryExhaustedError) Unwrap() error {
return e.LastErr
}
// IsRetryExhausted checks if an error is a RetryExhaustedError.
func IsRetryExhausted(err error) bool {
_, ok := err.(*RetryExhaustedError)
return ok
}
// RetryPredicates provides common retry condition predicates.
var RetryPredicates = struct {
Always func(error) bool
Never func(error) bool
NetworkErrors func(error) bool
TemporaryErrors func(error) bool
}{
Always: func(error) bool {
return true
},
Never: func(error) bool {
return false
},
// NetworkErrors retries on common network-related errors
NetworkErrors: func(err error) bool {
if err == nil {
return false
}
// Check for common network error patterns
errMsg := err.Error()
networkKeywords := []string{
"connection refused",
"connection reset",
"timeout",
"network",
"dns",
"temporary failure",
"503", // Service Unavailable
"502", // Bad Gateway
"504", // Gateway Timeout
}
for _, kw := range networkKeywords {
if contains(errMsg, kw) {
return true
}
}
return false
},
// TemporaryErrors retries on errors that might be transient
TemporaryErrors: func(err error) bool {
if err == nil {
return false
}
// Check for temporary error patterns
errMsg := err.Error()
tempKeywords := []string{
"temporary",
"transient",
"rate limit",
"too many requests",
"429", // Too Many Requests
}
for _, kw := range tempKeywords {
if contains(errMsg, kw) {
return true
}
}
return false
},
}
// contains checks if a string contains a substring (case-insensitive).
func contains(s, substr string) bool {
return len(s) >= len(substr) &&
(s == substr ||
len(s) > len(substr) &&
(s[0:len(substr)] == substr ||
containsIgnoreCase(s, substr)))
}
// containsIgnoreCase performs case-insensitive substring check.
func containsIgnoreCase(s, substr string) bool {
s = toLower(s)
substr = toLower(substr)
return len(s) >= len(substr) && s[:len(substr)] == substr
}
// toLower converts a string to lowercase.
func toLower(s string) string {
result := make([]byte, len(s))
for i := 0; i < len(s); i++ {
c := s[i]
if c >= 'A' && c <= 'Z' {
c += 'a' - 'A'
}
result[i] = c
}
return string(result)
}
// RetryConfig provides configuration for retry behavior.
type RetryConfig struct {
// Policy is the retry policy to use
Policy *types.RetryPolicy
// OnRetry is called after each failed attempt
OnRetry func(attempt int, err error)
// OnSuccess is called on successful completion
OnSuccess func(attempt int)
}
// NewRetryConfig creates a new retry config with defaults.
func NewRetryConfig() *RetryConfig {
defaultPolicy := types.DefaultRetryPolicy()
return &RetryConfig{
Policy: &defaultPolicy,
}
}
// WithRetryOn sets the retry-on predicate.
func (c *RetryConfig) WithRetryOn(predicate func(error) bool) *RetryConfig {
c.Policy.RetryOn = predicate
return c
}
// WithMaxAttempts sets the maximum number of attempts.
func (c *RetryConfig) WithMaxAttempts(maxAttempts int) *RetryConfig {
c.Policy.MaxAttempts = maxAttempts
return c
}
// WithBackoff sets the backoff parameters.
func (c *RetryConfig) WithBackoff(initial, max time.Duration, factor float64) *RetryConfig {
c.Policy.InitialInterval = initial
c.Policy.MaxInterval = max
c.Policy.BackoffFactor = factor
return c
}
// WithJitter enables or disables jitter.
func (c *RetryConfig) WithJitter(enabled bool) *RetryConfig {
c.Policy.Jitter = enabled
return c
}
// WithOnRetry sets the callback to call after each retry.
func (c *RetryConfig) WithOnRetry(callback func(attempt int, err error)) *RetryConfig {
c.OnRetry = callback
return c
}
// WithOnSuccess sets the callback to call on success.
func (c *RetryConfig) WithOnSuccess(callback func(attempt int)) *RetryConfig {
c.OnSuccess = callback
return c
}