- Added comprehensive bounds checking to prevent buffer overruns in multicall parsing - Implemented graduated validation system (Strict/Moderate/Permissive) to reduce false positives - Added LRU caching system for address validation with 10-minute TTL - Enhanced ABI decoder with missing Universal Router and Arbitrum-specific DEX signatures - Fixed duplicate function declarations and import conflicts across multiple files - Added error recovery mechanisms with multiple fallback strategies - Updated tests to handle new validation behavior for suspicious addresses - Fixed parser test expectations for improved validation system - Applied gofmt formatting fixes to ensure code style compliance - Fixed mutex copying issues in monitoring package by introducing MetricsSnapshot - Resolved critical security vulnerabilities in heuristic address extraction - Progress: Updated TODO audit from 10% to 35% complete 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
540 lines
17 KiB
Go
540 lines
17 KiB
Go
package risk
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/fraktal/mev-beta/internal/logger"
|
|
"github.com/fraktal/mev-beta/pkg/math"
|
|
)
|
|
|
|
// RiskManager manages risk for MEV operations
|
|
type RiskManager struct {
|
|
logger *logger.Logger
|
|
mu sync.RWMutex
|
|
decimalConverter *math.DecimalConverter
|
|
|
|
// Position limits
|
|
maxPositionSize *big.Int // Maximum position size in wei
|
|
dailyLossLimit *big.Int // Maximum daily loss in wei
|
|
maxConcurrent int // Maximum concurrent positions
|
|
maxPositionSizeDecimal *math.UniversalDecimal
|
|
dailyLossLimitDecimal *math.UniversalDecimal
|
|
minProfitThresholdDecimal *math.UniversalDecimal
|
|
maxGasPriceDecimal *math.UniversalDecimal
|
|
|
|
// Current state
|
|
currentPositions int
|
|
dailyLoss *big.Int
|
|
lastReset time.Time
|
|
|
|
// Risk metrics
|
|
totalTrades uint64
|
|
successfulTrades uint64
|
|
failedTrades uint64
|
|
totalProfit *big.Int
|
|
totalLoss *big.Int
|
|
totalProfitDecimal *math.UniversalDecimal
|
|
totalLossDecimal *math.UniversalDecimal
|
|
|
|
// Configuration
|
|
minProfitThreshold *big.Int // Minimum profit threshold in wei
|
|
maxSlippage float64 // Maximum acceptable slippage
|
|
maxGasPrice *big.Int // Maximum gas price willing to pay
|
|
|
|
// Circuit breaker
|
|
circuitBreaker *CircuitBreaker
|
|
}
|
|
|
|
func (rm *RiskManager) fromWei(value *big.Int, symbol string) *math.UniversalDecimal {
|
|
if value == nil {
|
|
zero, _ := math.NewUniversalDecimal(big.NewInt(0), 18, symbol)
|
|
return zero
|
|
}
|
|
decimals := uint8(18)
|
|
if symbol == "GWEI" {
|
|
decimals = 9
|
|
}
|
|
return rm.decimalConverter.FromWei(value, decimals, symbol)
|
|
}
|
|
|
|
// RiskAssessment represents a risk assessment for an MEV opportunity
|
|
type RiskAssessment struct {
|
|
OpportunityID string
|
|
RiskScore float64 // 0-1, higher = riskier
|
|
Confidence float64 // 0-1, higher = more confident
|
|
MaxPositionSize *big.Int // Maximum position size for this opportunity
|
|
RecommendedGas *big.Int // Recommended gas price
|
|
SlippageLimit float64 // Maximum slippage for this opportunity
|
|
Profitability float64 // Expected ROI percentage
|
|
Acceptable bool // Whether the opportunity passes risk checks
|
|
Reason string // Reason for acceptance/rejection
|
|
MaxPositionSizeDecimal *math.UniversalDecimal
|
|
RecommendedGasDecimal *math.UniversalDecimal
|
|
}
|
|
|
|
// CircuitBreaker manages circuit breaking for risk control
|
|
type CircuitBreaker struct {
|
|
failures int
|
|
lastFailure time.Time
|
|
resetTimeout time.Duration
|
|
failureThreshold int
|
|
open bool
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewRiskManager creates a new risk manager
|
|
func NewRiskManager(logger *logger.Logger) *RiskManager {
|
|
dc := math.NewDecimalConverter()
|
|
|
|
rm := &RiskManager{
|
|
logger: logger,
|
|
decimalConverter: dc,
|
|
maxPositionSize: big.NewInt(1000000000000000000), // 1 ETH
|
|
dailyLossLimit: big.NewInt(100000000000000000), // 0.1 ETH
|
|
maxConcurrent: 5,
|
|
currentPositions: 0,
|
|
dailyLoss: big.NewInt(0),
|
|
lastReset: time.Now(),
|
|
totalTrades: 0,
|
|
successfulTrades: 0,
|
|
failedTrades: 0,
|
|
totalProfit: big.NewInt(0),
|
|
totalLoss: big.NewInt(0),
|
|
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH
|
|
maxSlippage: 0.01, // 1%
|
|
maxGasPrice: big.NewInt(20000000000), // 20 gwei
|
|
circuitBreaker: &CircuitBreaker{
|
|
resetTimeout: 5 * time.Minute,
|
|
failureThreshold: 3,
|
|
},
|
|
}
|
|
|
|
rm.maxPositionSizeDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.maxPositionSize), 18, "ETH")
|
|
rm.dailyLossLimitDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.dailyLossLimit), 18, "ETH")
|
|
rm.minProfitThresholdDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.minProfitThreshold), 18, "ETH")
|
|
rm.maxGasPriceDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.maxGasPrice), 9, "GWEI")
|
|
rm.totalProfitDecimal, _ = math.NewUniversalDecimal(big.NewInt(0), 18, "ETH")
|
|
rm.totalLossDecimal, _ = math.NewUniversalDecimal(big.NewInt(0), 18, "ETH")
|
|
|
|
// Start daily reset timer
|
|
go rm.dailyReset()
|
|
|
|
return rm
|
|
}
|
|
|
|
// AssessOpportunity assesses the risk of an MEV opportunity
|
|
func (rm *RiskManager) AssessOpportunity(opportunityID string, expectedProfit, gasCost *big.Int, slippage float64, gasPrice *big.Int) *RiskAssessment {
|
|
rm.mu.Lock()
|
|
defer rm.mu.Unlock()
|
|
|
|
assessment := &RiskAssessment{
|
|
OpportunityID: opportunityID,
|
|
RiskScore: 0.0,
|
|
Confidence: 0.0,
|
|
MaxPositionSize: big.NewInt(0),
|
|
RecommendedGas: big.NewInt(0),
|
|
SlippageLimit: 0.0,
|
|
Profitability: 0.0,
|
|
Acceptable: false,
|
|
Reason: "",
|
|
MaxPositionSizeDecimal: rm.maxPositionSizeDecimal,
|
|
RecommendedGasDecimal: rm.maxGasPriceDecimal,
|
|
}
|
|
|
|
// Check circuit breaker
|
|
if rm.circuitBreaker.IsOpen() {
|
|
assessment.Reason = "Circuit breaker is open"
|
|
return assessment
|
|
}
|
|
|
|
expectedProfitDec := rm.fromWei(expectedProfit, "ETH")
|
|
gasPriceDec := rm.fromWei(gasPrice, "GWEI")
|
|
dailyLossDec := rm.fromWei(rm.dailyLoss, "ETH")
|
|
|
|
// Check if we've exceeded concurrent position limits
|
|
if rm.currentPositions >= rm.maxConcurrent {
|
|
assessment.Reason = fmt.Sprintf("Maximum concurrent positions reached: %d", rm.maxConcurrent)
|
|
return assessment
|
|
}
|
|
|
|
// Check if we've exceeded daily loss limits
|
|
if cmp, err := rm.decimalConverter.Compare(dailyLossDec, rm.dailyLossLimitDecimal); err == nil && cmp > 0 {
|
|
assessment.Reason = fmt.Sprintf("Daily loss limit exceeded: %s > %s",
|
|
rm.decimalConverter.ToHumanReadable(dailyLossDec),
|
|
rm.decimalConverter.ToHumanReadable(rm.dailyLossLimitDecimal))
|
|
return assessment
|
|
}
|
|
|
|
// Check minimum profit threshold
|
|
if cmp, err := rm.decimalConverter.Compare(expectedProfitDec, rm.minProfitThresholdDecimal); err == nil && cmp < 0 {
|
|
assessment.Reason = fmt.Sprintf("Profit below minimum threshold: %s < %s",
|
|
rm.decimalConverter.ToHumanReadable(expectedProfitDec),
|
|
rm.decimalConverter.ToHumanReadable(rm.minProfitThresholdDecimal))
|
|
return assessment
|
|
}
|
|
|
|
// Check slippage tolerance
|
|
if slippage > rm.maxSlippage {
|
|
assessment.Reason = fmt.Sprintf("Slippage exceeds limit: %.2f%% > %.2f%%",
|
|
slippage*100, rm.maxSlippage*100)
|
|
return assessment
|
|
}
|
|
|
|
// Check gas price limits
|
|
if cmp, err := rm.decimalConverter.Compare(gasPriceDec, rm.maxGasPriceDecimal); err == nil && cmp > 0 {
|
|
assessment.Reason = fmt.Sprintf("Gas price exceeds limit: %s > %s",
|
|
rm.decimalConverter.ToHumanReadable(gasPriceDec),
|
|
rm.decimalConverter.ToHumanReadable(rm.maxGasPriceDecimal))
|
|
return assessment
|
|
}
|
|
|
|
// Calculate risk score based on multiple factors
|
|
riskScore := rm.calculateRiskScore(expectedProfit, gasCost, slippage, gasPrice)
|
|
assessment.RiskScore = riskScore
|
|
|
|
// Calculate confidence based on historical performance
|
|
confidence := rm.calculateConfidence()
|
|
assessment.Confidence = confidence
|
|
|
|
// Calculate profitability (ROI percentage)
|
|
profitability := rm.calculateProfitability(expectedProfit, gasCost)
|
|
assessment.Profitability = profitability
|
|
|
|
// Determine maximum position size based on risk
|
|
maxPosition := rm.calculateMaxPositionSize(riskScore, expectedProfit, gasCost)
|
|
assessment.MaxPositionSize = maxPosition
|
|
assessment.MaxPositionSizeDecimal = rm.fromWei(maxPosition, "ETH")
|
|
|
|
// Recommend gas price based on network conditions and risk
|
|
recommendedGas := rm.calculateRecommendedGas(gasPrice, riskScore)
|
|
assessment.RecommendedGas = recommendedGas
|
|
assessment.RecommendedGasDecimal = rm.fromWei(recommendedGas, "GWEI")
|
|
|
|
// Set slippage limit based on risk tolerance
|
|
slippageLimit := rm.calculateSlippageLimit(riskScore)
|
|
assessment.SlippageLimit = slippageLimit
|
|
|
|
// Determine if opportunity is acceptable
|
|
acceptable := rm.isAcceptable(riskScore, profitability, confidence)
|
|
assessment.Acceptable = acceptable
|
|
|
|
if acceptable {
|
|
assessment.Reason = "Opportunity passes all risk checks"
|
|
} else {
|
|
assessment.Reason = fmt.Sprintf("Risk score too high: %.2f", riskScore)
|
|
}
|
|
|
|
rm.logger.Debug(fmt.Sprintf("Risk assessment for %s: Risk=%.2f, Confidence=%.2f, Profitability=%.2f%%, Acceptable=%t",
|
|
opportunityID, riskScore, confidence, profitability, acceptable))
|
|
|
|
return assessment
|
|
}
|
|
|
|
// RecordTrade records the result of a trade for risk management
|
|
func (rm *RiskManager) RecordTrade(success bool, profit, gasCost *big.Int) {
|
|
rm.mu.Lock()
|
|
defer rm.mu.Unlock()
|
|
|
|
rm.totalTrades++
|
|
if success {
|
|
rm.successfulTrades++
|
|
if profit != nil {
|
|
rm.totalProfit.Add(rm.totalProfit, profit)
|
|
profitDec := rm.fromWei(profit, "ETH")
|
|
rm.totalProfitDecimal, _ = rm.decimalConverter.Add(rm.totalProfitDecimal, profitDec)
|
|
}
|
|
} else {
|
|
rm.failedTrades++
|
|
if gasCost != nil {
|
|
rm.totalLoss.Add(rm.totalLoss, gasCost)
|
|
rm.dailyLoss.Add(rm.dailyLoss, gasCost)
|
|
lossDec := rm.fromWei(gasCost, "ETH")
|
|
rm.totalLossDecimal, _ = rm.decimalConverter.Add(rm.totalLossDecimal, lossDec)
|
|
}
|
|
}
|
|
|
|
// Update circuit breaker
|
|
if !success {
|
|
rm.circuitBreaker.RecordFailure()
|
|
} else {
|
|
rm.circuitBreaker.RecordSuccess()
|
|
}
|
|
|
|
rm.logger.Debug(fmt.Sprintf("Trade recorded: Success=%t, Profit=%s, Gas=%s, DailyLoss=%s",
|
|
success,
|
|
rm.decimalConverter.ToHumanReadable(rm.fromWei(profit, "ETH")),
|
|
rm.decimalConverter.ToHumanReadable(rm.fromWei(gasCost, "ETH")),
|
|
rm.decimalConverter.ToHumanReadable(rm.fromWei(rm.dailyLoss, "ETH"))))
|
|
}
|
|
|
|
// UpdatePositionCount updates the current position count
|
|
func (rm *RiskManager) UpdatePositionCount(delta int) {
|
|
rm.mu.Lock()
|
|
defer rm.mu.Unlock()
|
|
|
|
rm.currentPositions += delta
|
|
if rm.currentPositions < 0 {
|
|
rm.currentPositions = 0
|
|
}
|
|
|
|
rm.logger.Debug(fmt.Sprintf("Position count updated: %d", rm.currentPositions))
|
|
}
|
|
|
|
// GetStatistics returns risk management statistics
|
|
func (rm *RiskManager) GetStatistics() map[string]interface{} {
|
|
rm.mu.RLock()
|
|
defer rm.mu.RUnlock()
|
|
|
|
return map[string]interface{}{
|
|
"total_trades": rm.totalTrades,
|
|
"successful_trades": rm.successfulTrades,
|
|
"failed_trades": rm.failedTrades,
|
|
"success_rate": float64(rm.successfulTrades) / float64(max(1, rm.totalTrades)),
|
|
"total_profit": rm.decimalConverter.ToHumanReadable(rm.totalProfitDecimal),
|
|
"total_loss": rm.decimalConverter.ToHumanReadable(rm.totalLossDecimal),
|
|
"daily_loss": rm.decimalConverter.ToHumanReadable(rm.fromWei(rm.dailyLoss, "ETH")),
|
|
"daily_loss_limit": rm.decimalConverter.ToHumanReadable(rm.dailyLossLimitDecimal),
|
|
"current_positions": rm.currentPositions,
|
|
"max_concurrent": rm.maxConcurrent,
|
|
"circuit_breaker_open": rm.circuitBreaker.IsOpen(),
|
|
"min_profit_threshold": rm.decimalConverter.ToHumanReadable(rm.minProfitThresholdDecimal),
|
|
"max_slippage": rm.maxSlippage * 100, // Convert to percentage
|
|
"max_gas_price": rm.decimalConverter.ToHumanReadable(rm.maxGasPriceDecimal),
|
|
}
|
|
}
|
|
|
|
// dailyReset resets daily counters
|
|
func (rm *RiskManager) dailyReset() {
|
|
ticker := time.NewTicker(24 * time.Hour)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
<-ticker.C
|
|
rm.mu.Lock()
|
|
rm.dailyLoss = big.NewInt(0)
|
|
rm.lastReset = time.Now()
|
|
rm.mu.Unlock()
|
|
rm.logger.Info("Daily risk counters reset")
|
|
}
|
|
}
|
|
|
|
// calculateRiskScore calculates a risk score based on multiple factors
|
|
func (rm *RiskManager) calculateRiskScore(expectedProfit, gasCost *big.Int, slippage float64, gasPrice *big.Int) float64 {
|
|
// Base risk (0-0.3)
|
|
baseRisk := 0.1
|
|
|
|
// Gas risk (0-0.3) - higher gas = higher risk
|
|
gasRisk := 0.0
|
|
if gasPrice != nil && rm.maxGasPrice != nil && rm.maxGasPrice.Sign() > 0 {
|
|
gasRatio := new(big.Float).Quo(new(big.Float).SetInt(gasPrice), new(big.Float).SetInt(rm.maxGasPrice))
|
|
gasRatioFloat, _ := gasRatio.Float64()
|
|
gasRisk = gasRatioFloat * 0.3
|
|
if gasRisk > 0.3 {
|
|
gasRisk = 0.3
|
|
}
|
|
}
|
|
|
|
// Slippage risk (0-0.2) - higher slippage = higher risk
|
|
slippageRisk := slippage * 0.2
|
|
if slippageRisk > 0.2 {
|
|
slippageRisk = 0.2
|
|
}
|
|
|
|
// Profit risk (0-0.2) - lower profit = higher relative risk
|
|
profitRisk := 0.0
|
|
if expectedProfit != nil && expectedProfit.Sign() > 0 {
|
|
// Lower profits are riskier relative to gas costs
|
|
if gasCost != nil && gasCost.Sign() > 0 {
|
|
profitRatio := new(big.Float).Quo(new(big.Float).SetInt(gasCost), new(big.Float).SetInt(expectedProfit))
|
|
profitRatioFloat, _ := profitRatio.Float64()
|
|
profitRisk = profitRatioFloat * 0.2
|
|
if profitRisk > 0.2 {
|
|
profitRisk = 0.2
|
|
}
|
|
}
|
|
}
|
|
|
|
totalRisk := baseRisk + gasRisk + slippageRisk + profitRisk
|
|
if totalRisk > 1.0 {
|
|
totalRisk = 1.0
|
|
}
|
|
|
|
return totalRisk
|
|
}
|
|
|
|
// calculateConfidence calculates confidence based on historical performance
|
|
func (rm *RiskManager) calculateConfidence() float64 {
|
|
if rm.totalTrades == 0 {
|
|
return 0.5 // Default confidence for new system
|
|
}
|
|
|
|
successRate := float64(rm.successfulTrades) / float64(rm.totalTrades)
|
|
confidence := successRate * 0.8 // Weight success rate at 80%
|
|
|
|
// Add bonus for high volume of trades
|
|
volumeBonus := float64(min(rm.totalTrades, 1000)) / 1000.0 * 0.2 // Max 20% bonus
|
|
confidence += volumeBonus
|
|
|
|
if confidence > 1.0 {
|
|
confidence = 1.0
|
|
}
|
|
|
|
return confidence
|
|
}
|
|
|
|
// calculateProfitability calculates profitability as ROI percentage
|
|
func (rm *RiskManager) calculateProfitability(expectedProfit, gasCost *big.Int) float64 {
|
|
if expectedProfit == nil || expectedProfit.Sign() <= 0 {
|
|
return 0.0
|
|
}
|
|
|
|
netProfit := new(big.Int).Sub(expectedProfit, gasCost)
|
|
if netProfit.Sign() <= 0 {
|
|
return 0.0
|
|
}
|
|
|
|
// For profitability calculation, we need an investment amount
|
|
// Use a reasonable default investment (e.g., 1 ETH)
|
|
investment := big.NewInt(1000000000000000000) // 1 ETH
|
|
|
|
roi := new(big.Float).Quo(new(big.Float).SetInt(netProfit), new(big.Float).SetInt(investment))
|
|
roi.Mul(roi, big.NewFloat(100))
|
|
|
|
roiFloat, _ := roi.Float64()
|
|
return roiFloat
|
|
}
|
|
|
|
// calculateMaxPositionSize calculates maximum position size based on risk
|
|
func (rm *RiskManager) calculateMaxPositionSize(riskScore float64, expectedProfit, gasCost *big.Int) *big.Int {
|
|
// Start with maximum position size
|
|
maxPosition := new(big.Int).Set(rm.maxPositionSize)
|
|
|
|
// Reduce position size based on risk score
|
|
riskMultiplier := 1.0 - riskScore
|
|
maxPositionFloat := new(big.Float).SetInt(maxPosition)
|
|
maxPositionFloat.Mul(maxPositionFloat, big.NewFloat(riskMultiplier))
|
|
|
|
result := new(big.Int)
|
|
maxPositionFloat.Int(result)
|
|
return result
|
|
}
|
|
|
|
// calculateRecommendedGas calculates recommended gas price based on risk
|
|
func (rm *RiskManager) calculateRecommendedGas(gasPrice *big.Int, riskScore float64) *big.Int {
|
|
if gasPrice == nil {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
// For high-risk opportunities, recommend higher gas price for faster execution
|
|
// For low-risk opportunities, can use lower gas price
|
|
gasMultiplier := 1.0 + (riskScore * 0.5) // Up to 50% increase for high risk
|
|
|
|
recommendedGas := new(big.Float).SetInt(gasPrice)
|
|
recommendedGas.Mul(recommendedGas, big.NewFloat(gasMultiplier))
|
|
|
|
result := new(big.Int)
|
|
recommendedGas.Int(result)
|
|
return result
|
|
}
|
|
|
|
// calculateSlippageLimit calculates slippage limit based on risk
|
|
func (rm *RiskManager) calculateSlippageLimit(riskScore float64) float64 {
|
|
// For high-risk opportunities, allow less slippage
|
|
// For low-risk opportunities, can tolerate more slippage
|
|
slippageLimit := rm.maxSlippage * (1.0 - riskScore*0.5) // Reduce by up to 50% for high risk
|
|
return slippageLimit
|
|
}
|
|
|
|
// isAcceptable determines if an opportunity is acceptable based on risk criteria
|
|
func (rm *RiskManager) isAcceptable(riskScore, profitability, confidence float64) bool {
|
|
// Must pass all individual criteria AND overall risk threshold
|
|
if riskScore > 0.7 {
|
|
return false // Too risky regardless of other factors
|
|
}
|
|
|
|
if profitability < 1.0 {
|
|
return false // Less than 1% ROI
|
|
}
|
|
|
|
if confidence < 0.3 {
|
|
return false // Low confidence
|
|
}
|
|
|
|
// Overall weighted score
|
|
weightedScore := (riskScore * 0.4) + ((100 - profitability) * 0.3) + ((1 - confidence) * 0.3)
|
|
return weightedScore < 0.5
|
|
}
|
|
|
|
// max returns the larger of two integers
|
|
func max(a, b uint64) uint64 {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// min returns the smaller of two integers
|
|
func min(a, b uint64) uint64 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// formatEther formats a big.Int wei amount as ETH string
|
|
func formatEther(wei *big.Int) string {
|
|
if wei == nil {
|
|
return "0"
|
|
}
|
|
|
|
ether := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e18))
|
|
result, _ := ether.Float64()
|
|
return fmt.Sprintf("%.6f", result)
|
|
}
|
|
|
|
// formatGwei formats a big.Int wei amount as Gwei string
|
|
func formatGwei(wei *big.Int) string {
|
|
if wei == nil {
|
|
return "0"
|
|
}
|
|
|
|
gwei := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e9))
|
|
result, _ := gwei.Float64()
|
|
return fmt.Sprintf("%.2f", result)
|
|
}
|
|
|
|
// IsOpen checks if the circuit breaker is open
|
|
func (cb *CircuitBreaker) IsOpen() bool {
|
|
cb.mu.RLock()
|
|
defer cb.mu.RUnlock()
|
|
return cb.open
|
|
}
|
|
|
|
// RecordFailure records a failure in the circuit breaker
|
|
func (cb *CircuitBreaker) RecordFailure() {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
cb.failures++
|
|
cb.lastFailure = time.Now()
|
|
|
|
if cb.failures >= cb.failureThreshold {
|
|
cb.open = true
|
|
}
|
|
}
|
|
|
|
// RecordSuccess records a success in the circuit breaker and potentially closes it
|
|
func (cb *CircuitBreaker) RecordSuccess() {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
// Reset failures if enough time has passed
|
|
if time.Since(cb.lastFailure) > cb.resetTimeout {
|
|
cb.failures = 0
|
|
cb.open = false
|
|
}
|
|
}
|