mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-17 05:07:23 +08:00
### What problem does this PR solve? - Tools management - Pregel engine wrapper for better usage - UT race - Coding style ### Type of change - [x] Refactoring
261 lines
6.5 KiB
Go
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
|
|
}
|