1、状态更新错误处理
This commit is contained in:
676
services/binanceservice/error_handler_optimized.go
Normal file
676
services/binanceservice/error_handler_optimized.go
Normal file
@ -0,0 +1,676 @@
|
||||
package binanceservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ErrorType 错误类型
|
||||
type ErrorType string
|
||||
|
||||
const (
|
||||
ErrorTypeNetwork ErrorType = "network"
|
||||
ErrorTypeDatabase ErrorType = "database"
|
||||
ErrorTypeBusiness ErrorType = "business"
|
||||
ErrorTypeSystem ErrorType = "system"
|
||||
ErrorTypeValidation ErrorType = "validation"
|
||||
ErrorTypeTimeout ErrorType = "timeout"
|
||||
ErrorTypeRateLimit ErrorType = "rate_limit"
|
||||
ErrorTypeAuth ErrorType = "auth"
|
||||
)
|
||||
|
||||
// ErrorSeverity 错误严重程度
|
||||
type ErrorSeverity string
|
||||
|
||||
const (
|
||||
SeverityLow ErrorSeverity = "low"
|
||||
SeverityMedium ErrorSeverity = "medium"
|
||||
SeverityHigh ErrorSeverity = "high"
|
||||
SeverityCritical ErrorSeverity = "critical"
|
||||
)
|
||||
|
||||
// ErrorAction 错误处理动作
|
||||
type ErrorAction string
|
||||
|
||||
const (
|
||||
ActionRetry ErrorAction = "retry"
|
||||
ActionFallback ErrorAction = "fallback"
|
||||
ActionAlert ErrorAction = "alert"
|
||||
ActionIgnore ErrorAction = "ignore"
|
||||
ActionCircuit ErrorAction = "circuit"
|
||||
ActionDegrade ErrorAction = "degrade"
|
||||
)
|
||||
|
||||
// ErrorInfo 错误信息
|
||||
type ErrorInfo struct {
|
||||
Type ErrorType `json:"type"`
|
||||
Severity ErrorSeverity `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
OriginalErr error `json:"-"`
|
||||
Context string `json:"context"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Retryable bool `json:"retryable"`
|
||||
Action ErrorAction `json:"action"`
|
||||
}
|
||||
|
||||
// RetryConfig 和 CircuitBreakerConfig 已在 config_optimized.go 中定义
|
||||
|
||||
// ErrorHandler 错误处理器
|
||||
type ErrorHandler struct {
|
||||
config *OptimizedConfig
|
||||
metricsCollector *MetricsCollector
|
||||
circuitBreakers map[string]*CircuitBreaker
|
||||
}
|
||||
|
||||
// CircuitBreaker 熔断器
|
||||
type CircuitBreaker struct {
|
||||
name string
|
||||
config CircuitBreakerConfig
|
||||
state CircuitState
|
||||
failureCount int
|
||||
successCount int
|
||||
lastFailTime time.Time
|
||||
nextRetry time.Time
|
||||
halfOpenCalls int
|
||||
}
|
||||
|
||||
// CircuitState 熔断器状态
|
||||
type CircuitState string
|
||||
|
||||
const (
|
||||
StateClosed CircuitState = "closed"
|
||||
StateOpen CircuitState = "open"
|
||||
StateHalfOpen CircuitState = "half_open"
|
||||
)
|
||||
|
||||
// NewErrorHandler 创建错误处理器
|
||||
func NewErrorHandler(config *OptimizedConfig, metricsCollector *MetricsCollector) *ErrorHandler {
|
||||
return &ErrorHandler{
|
||||
config: config,
|
||||
metricsCollector: metricsCollector,
|
||||
circuitBreakers: make(map[string]*CircuitBreaker),
|
||||
}
|
||||
}
|
||||
|
||||
// ClassifyError 分类错误
|
||||
func (eh *ErrorHandler) ClassifyError(err error, context string) *ErrorInfo {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errorInfo := &ErrorInfo{
|
||||
OriginalErr: err,
|
||||
Message: err.Error(),
|
||||
Context: context,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// 根据错误类型分类
|
||||
switch {
|
||||
case isNetworkError(err):
|
||||
errorInfo.Type = ErrorTypeNetwork
|
||||
errorInfo.Severity = SeverityMedium
|
||||
errorInfo.Retryable = true
|
||||
errorInfo.Action = ActionRetry
|
||||
|
||||
case isDatabaseError(err):
|
||||
errorInfo.Type = ErrorTypeDatabase
|
||||
errorInfo.Severity = SeverityHigh
|
||||
errorInfo.Retryable = isRetryableDatabaseError(err)
|
||||
errorInfo.Action = ActionRetry
|
||||
|
||||
case isTimeoutError(err):
|
||||
errorInfo.Type = ErrorTypeTimeout
|
||||
errorInfo.Severity = SeverityMedium
|
||||
errorInfo.Retryable = true
|
||||
errorInfo.Action = ActionRetry
|
||||
|
||||
case isRateLimitError(err):
|
||||
errorInfo.Type = ErrorTypeRateLimit
|
||||
errorInfo.Severity = SeverityMedium
|
||||
errorInfo.Retryable = true
|
||||
errorInfo.Action = ActionRetry
|
||||
|
||||
case isAuthError(err):
|
||||
errorInfo.Type = ErrorTypeAuth
|
||||
errorInfo.Severity = SeverityHigh
|
||||
errorInfo.Retryable = false
|
||||
errorInfo.Action = ActionAlert
|
||||
|
||||
case isValidationError(err):
|
||||
errorInfo.Type = ErrorTypeValidation
|
||||
errorInfo.Severity = SeverityLow
|
||||
errorInfo.Retryable = false
|
||||
errorInfo.Action = ActionIgnore
|
||||
|
||||
case isBusinessError(err):
|
||||
errorInfo.Type = ErrorTypeBusiness
|
||||
errorInfo.Severity = SeverityMedium
|
||||
errorInfo.Retryable = false
|
||||
errorInfo.Action = ActionFallback
|
||||
|
||||
default:
|
||||
errorInfo.Type = ErrorTypeSystem
|
||||
errorInfo.Severity = SeverityHigh
|
||||
errorInfo.Retryable = true
|
||||
errorInfo.Action = ActionRetry
|
||||
}
|
||||
|
||||
return errorInfo
|
||||
}
|
||||
|
||||
// HandleError 处理错误
|
||||
func (eh *ErrorHandler) HandleError(err error, context string) *ErrorInfo {
|
||||
errorInfo := eh.ClassifyError(err, context)
|
||||
if errorInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 记录错误指标
|
||||
if eh.metricsCollector != nil {
|
||||
eh.metricsCollector.RecordError(string(errorInfo.Type), errorInfo.Message, context)
|
||||
}
|
||||
|
||||
// 根据错误动作处理
|
||||
switch errorInfo.Action {
|
||||
case ActionAlert:
|
||||
eh.sendAlert(errorInfo)
|
||||
case ActionCircuit:
|
||||
eh.triggerCircuitBreaker(context)
|
||||
case ActionDegrade:
|
||||
eh.enableDegradedMode(context)
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
eh.logError(errorInfo)
|
||||
|
||||
return errorInfo
|
||||
}
|
||||
|
||||
// RetryWithBackoff 带退避的重试
|
||||
func (eh *ErrorHandler) RetryWithBackoff(ctx context.Context, operation func() error, config RetryConfig, context string) error {
|
||||
var lastErr error
|
||||
delay := config.RetryDelay
|
||||
|
||||
for attempt := 1; attempt <= config.MaxRetries; attempt++ {
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 检查熔断器状态
|
||||
if !eh.canExecute(context) {
|
||||
return fmt.Errorf("circuit breaker is open for %s", context)
|
||||
}
|
||||
|
||||
// 执行操作
|
||||
// 注意:config中没有TimeoutPerTry字段,这里暂时注释掉
|
||||
// if config.TimeoutPerTry > 0 {
|
||||
// var cancel context.CancelFunc
|
||||
// operationCtx, cancel = context.WithTimeout(ctx, config.TimeoutPerTry)
|
||||
// defer cancel()
|
||||
// }
|
||||
|
||||
err := operation()
|
||||
if err == nil {
|
||||
// 成功,记录成功指标
|
||||
if eh.metricsCollector != nil {
|
||||
eh.metricsCollector.RecordRetry(true)
|
||||
}
|
||||
eh.recordSuccess(context)
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
errorInfo := eh.ClassifyError(err, context)
|
||||
|
||||
// 记录失败
|
||||
eh.recordFailure(context)
|
||||
|
||||
// 如果不可重试,直接返回
|
||||
if errorInfo != nil && !errorInfo.Retryable {
|
||||
logger.Warnf("不可重试的错误 [%s]: %v", context, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 最后一次尝试,不再等待
|
||||
if attempt == config.MaxRetries {
|
||||
break
|
||||
}
|
||||
|
||||
// 计算下次重试延迟
|
||||
nextDelay := eh.calculateDelay(delay, config)
|
||||
logger.Infof("重试 %d/%d [%s] 在 %v 后,错误: %v", attempt, config.MaxRetries, context, nextDelay, err)
|
||||
|
||||
// 等待重试
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(nextDelay):
|
||||
delay = nextDelay
|
||||
}
|
||||
}
|
||||
|
||||
// 记录重试失败指标
|
||||
if eh.metricsCollector != nil {
|
||||
eh.metricsCollector.RecordRetry(false)
|
||||
}
|
||||
|
||||
return fmt.Errorf("重试 %d 次后仍然失败 [%s]: %w", config.MaxRetries, context, lastErr)
|
||||
}
|
||||
|
||||
// calculateDelay 计算重试延迟
|
||||
func (eh *ErrorHandler) calculateDelay(currentDelay time.Duration, config RetryConfig) time.Duration {
|
||||
nextDelay := time.Duration(float64(currentDelay) * config.BackoffFactor)
|
||||
|
||||
// 限制最大延迟
|
||||
if nextDelay > config.MaxRetryDelay {
|
||||
nextDelay = config.MaxRetryDelay
|
||||
}
|
||||
|
||||
// 添加10%抖动(简化版本,不依赖JitterEnabled字段)
|
||||
jitter := time.Duration(float64(nextDelay) * 0.1)
|
||||
if jitter > 0 {
|
||||
nextDelay += time.Duration(time.Now().UnixNano() % int64(jitter))
|
||||
}
|
||||
|
||||
return nextDelay
|
||||
}
|
||||
|
||||
// canExecute 检查是否可以执行操作(熔断器检查)
|
||||
func (eh *ErrorHandler) canExecute(context string) bool {
|
||||
cb := eh.getCircuitBreaker(context)
|
||||
if cb == nil || !cb.config.Enabled {
|
||||
return true
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
switch cb.state {
|
||||
case StateClosed:
|
||||
return true
|
||||
|
||||
case StateOpen:
|
||||
if now.After(cb.nextRetry) {
|
||||
cb.state = StateHalfOpen
|
||||
cb.halfOpenCalls = 0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
case StateHalfOpen:
|
||||
return cb.halfOpenCalls < cb.config.HalfOpenMaxCalls
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// recordSuccess 记录成功
|
||||
func (eh *ErrorHandler) recordSuccess(context string) {
|
||||
cb := eh.getCircuitBreaker(context)
|
||||
if cb == nil || !cb.config.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
switch cb.state {
|
||||
case StateHalfOpen:
|
||||
cb.successCount++
|
||||
if cb.successCount >= cb.config.SuccessThreshold {
|
||||
cb.state = StateClosed
|
||||
cb.failureCount = 0
|
||||
cb.successCount = 0
|
||||
}
|
||||
|
||||
case StateClosed:
|
||||
cb.failureCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
// recordFailure 记录失败
|
||||
func (eh *ErrorHandler) recordFailure(context string) {
|
||||
cb := eh.getCircuitBreaker(context)
|
||||
if cb == nil || !cb.config.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
cb.failureCount++
|
||||
cb.lastFailTime = time.Now()
|
||||
|
||||
switch cb.state {
|
||||
case StateClosed:
|
||||
if cb.failureCount >= cb.config.FailureThreshold {
|
||||
cb.state = StateOpen
|
||||
cb.nextRetry = time.Now().Add(cb.config.Timeout)
|
||||
logger.Warnf("熔断器开启 [%s]: 失败次数 %d", context, cb.failureCount)
|
||||
}
|
||||
|
||||
case StateHalfOpen:
|
||||
cb.state = StateOpen
|
||||
cb.nextRetry = time.Now().Add(cb.config.Timeout)
|
||||
cb.successCount = 0
|
||||
logger.Warnf("熔断器重新开启 [%s]", context)
|
||||
}
|
||||
}
|
||||
|
||||
// getCircuitBreaker 获取熔断器
|
||||
func (eh *ErrorHandler) getCircuitBreaker(context string) *CircuitBreaker {
|
||||
cb, exists := eh.circuitBreakers[context]
|
||||
if !exists {
|
||||
cb = &CircuitBreaker{
|
||||
name: context,
|
||||
config: eh.config.CircuitBreakerConfig,
|
||||
state: StateClosed,
|
||||
}
|
||||
eh.circuitBreakers[context] = cb
|
||||
}
|
||||
return cb
|
||||
}
|
||||
|
||||
// triggerCircuitBreaker 触发熔断器
|
||||
func (eh *ErrorHandler) triggerCircuitBreaker(context string) {
|
||||
cb := eh.getCircuitBreaker(context)
|
||||
if cb != nil && cb.config.Enabled {
|
||||
cb.state = StateOpen
|
||||
cb.nextRetry = time.Now().Add(cb.config.Timeout)
|
||||
logger.Warnf("手动触发熔断器 [%s]", context)
|
||||
}
|
||||
}
|
||||
|
||||
// sendAlert 发送告警
|
||||
func (eh *ErrorHandler) sendAlert(errorInfo *ErrorInfo) {
|
||||
// 这里可以集成告警系统,如钉钉、邮件等
|
||||
logger.Errorf("告警: [%s] %s - %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
|
||||
}
|
||||
|
||||
// enableDegradedMode 启用降级模式
|
||||
func (eh *ErrorHandler) enableDegradedMode(context string) {
|
||||
logger.Warnf("启用降级模式 [%s]", context)
|
||||
// 这里可以实现具体的降级逻辑
|
||||
}
|
||||
|
||||
// logError 记录错误日志
|
||||
func (eh *ErrorHandler) logError(errorInfo *ErrorInfo) {
|
||||
switch errorInfo.Severity {
|
||||
case SeverityLow:
|
||||
logger.Infof("[%s] %s: %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
|
||||
case SeverityMedium:
|
||||
logger.Warnf("[%s] %s: %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
|
||||
case SeverityHigh, SeverityCritical:
|
||||
logger.Errorf("[%s] %s: %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCircuitBreakerStatus 获取熔断器状态
|
||||
func (eh *ErrorHandler) GetCircuitBreakerStatus() map[string]interface{} {
|
||||
status := make(map[string]interface{})
|
||||
for name, cb := range eh.circuitBreakers {
|
||||
status[name] = map[string]interface{}{
|
||||
"state": cb.state,
|
||||
"failure_count": cb.failureCount,
|
||||
"success_count": cb.successCount,
|
||||
"last_fail_time": cb.lastFailTime,
|
||||
"next_retry": cb.nextRetry,
|
||||
}
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
// 错误分类辅助函数
|
||||
|
||||
// isNetworkError 判断是否为网络错误
|
||||
func isNetworkError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查网络相关错误
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查常见网络错误消息
|
||||
errorMsg := strings.ToLower(err.Error())
|
||||
networkKeywords := []string{
|
||||
"connection refused",
|
||||
"connection reset",
|
||||
"connection timeout",
|
||||
"network unreachable",
|
||||
"no route to host",
|
||||
"dns",
|
||||
"socket",
|
||||
}
|
||||
|
||||
for _, keyword := range networkKeywords {
|
||||
if strings.Contains(errorMsg, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isDatabaseError 判断是否为数据库错误
|
||||
func isDatabaseError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查GORM错误
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) ||
|
||||
errors.Is(err, gorm.ErrInvalidTransaction) ||
|
||||
errors.Is(err, gorm.ErrNotImplemented) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查数据库相关错误消息
|
||||
errorMsg := strings.ToLower(err.Error())
|
||||
dbKeywords := []string{
|
||||
"database",
|
||||
"sql",
|
||||
"mysql",
|
||||
"postgres",
|
||||
"connection pool",
|
||||
"deadlock",
|
||||
"constraint",
|
||||
"duplicate key",
|
||||
}
|
||||
|
||||
for _, keyword := range dbKeywords {
|
||||
if strings.Contains(errorMsg, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isRetryableDatabaseError 判断是否为可重试的数据库错误
|
||||
func isRetryableDatabaseError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 不可重试的错误
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false
|
||||
}
|
||||
|
||||
errorMsg := strings.ToLower(err.Error())
|
||||
nonRetryableKeywords := []string{
|
||||
"constraint",
|
||||
"duplicate key",
|
||||
"foreign key",
|
||||
"syntax error",
|
||||
}
|
||||
|
||||
for _, keyword := range nonRetryableKeywords {
|
||||
if strings.Contains(errorMsg, keyword) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isTimeoutError 判断是否为超时错误
|
||||
func isTimeoutError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查超时错误
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
return true
|
||||
}
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
|
||||
errorMsg := strings.ToLower(err.Error())
|
||||
return strings.Contains(errorMsg, "timeout") ||
|
||||
strings.Contains(errorMsg, "deadline exceeded")
|
||||
}
|
||||
|
||||
// isRateLimitError 判断是否为限流错误
|
||||
func isRateLimitError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errorMsg := strings.ToLower(err.Error())
|
||||
rateLimitKeywords := []string{
|
||||
"rate limit",
|
||||
"too many requests",
|
||||
"429",
|
||||
"quota exceeded",
|
||||
"throttle",
|
||||
}
|
||||
|
||||
for _, keyword := range rateLimitKeywords {
|
||||
if strings.Contains(errorMsg, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isAuthError 判断是否为认证错误
|
||||
func isAuthError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errorMsg := strings.ToLower(err.Error())
|
||||
authKeywords := []string{
|
||||
"unauthorized",
|
||||
"authentication",
|
||||
"invalid signature",
|
||||
"api key",
|
||||
"401",
|
||||
"403",
|
||||
"forbidden",
|
||||
}
|
||||
|
||||
for _, keyword := range authKeywords {
|
||||
if strings.Contains(errorMsg, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isValidationError 判断是否为验证错误
|
||||
func isValidationError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errorMsg := strings.ToLower(err.Error())
|
||||
validationKeywords := []string{
|
||||
"validation",
|
||||
"invalid parameter",
|
||||
"bad request",
|
||||
"400",
|
||||
"missing required",
|
||||
"invalid format",
|
||||
}
|
||||
|
||||
for _, keyword := range validationKeywords {
|
||||
if strings.Contains(errorMsg, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isBusinessError 判断是否为业务错误
|
||||
func isBusinessError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errorMsg := strings.ToLower(err.Error())
|
||||
businessKeywords := []string{
|
||||
"insufficient balance",
|
||||
"order not found",
|
||||
"position not found",
|
||||
"invalid order status",
|
||||
"market closed",
|
||||
"symbol not found",
|
||||
}
|
||||
|
||||
for _, keyword := range businessKeywords {
|
||||
if strings.Contains(errorMsg, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 全局错误处理器实例
|
||||
var GlobalErrorHandler *ErrorHandler
|
||||
|
||||
// InitErrorHandler 初始化错误处理器
|
||||
func InitErrorHandler(config *OptimizedConfig, metricsCollector *MetricsCollector) {
|
||||
GlobalErrorHandler = NewErrorHandler(config, metricsCollector)
|
||||
}
|
||||
|
||||
// GetErrorHandler 获取全局错误处理器
|
||||
func GetErrorHandler() *ErrorHandler {
|
||||
return GlobalErrorHandler
|
||||
}
|
||||
|
||||
// HandleErrorWithRetry 处理错误并重试的便捷函数
|
||||
func HandleErrorWithRetry(ctx context.Context, operation func() error, context string) error {
|
||||
if GlobalErrorHandler == nil {
|
||||
return operation()
|
||||
}
|
||||
|
||||
config := RetryConfig{
|
||||
MaxRetries: 3,
|
||||
RetryDelay: 100 * time.Millisecond,
|
||||
MaxRetryDelay: 5 * time.Second,
|
||||
BackoffFactor: 2.0,
|
||||
ApiRetryCount: 3,
|
||||
DbRetryCount: 3,
|
||||
}
|
||||
|
||||
return GlobalErrorHandler.RetryWithBackoff(ctx, operation, config, context)
|
||||
}
|
||||
Reference in New Issue
Block a user