|  | 
|  | 1 | +// timeouts.go implements adaptive timeout management for HTTP requests to Starknet nodes. | 
|  | 2 | +// This file handles dynamic timeout adjustments based on request performance, automatically | 
|  | 3 | +// scaling timeouts up or down depending on success/failure rates. | 
|  | 4 | + | 
|  | 5 | +package feeder | 
|  | 6 | + | 
|  | 7 | +import ( | 
|  | 8 | +	"fmt" | 
|  | 9 | +	"math" | 
|  | 10 | +	"strings" | 
|  | 11 | +	"sync" | 
|  | 12 | +	"time" | 
|  | 13 | +) | 
|  | 14 | + | 
|  | 15 | +const ( | 
|  | 16 | +	growthFactorFast    = 2 | 
|  | 17 | +	growthFactorMedium  = 1.5 | 
|  | 18 | +	growthFactorSlow    = 1.2 | 
|  | 19 | +	fastGrowThreshold   = 1 * time.Minute | 
|  | 20 | +	mediumGrowThreshold = 2 * time.Minute | 
|  | 21 | +	timeoutsCount       = 30 | 
|  | 22 | +	DefaultTimeouts     = "5s" | 
|  | 23 | +) | 
|  | 24 | + | 
|  | 25 | +type Timeouts struct { | 
|  | 26 | +	timeouts   []time.Duration | 
|  | 27 | +	curTimeout int | 
|  | 28 | +	mu         sync.RWMutex | 
|  | 29 | +} | 
|  | 30 | + | 
|  | 31 | +func (t *Timeouts) GetCurrentTimeout() time.Duration { | 
|  | 32 | +	t.mu.RLock() | 
|  | 33 | +	defer t.mu.RUnlock() | 
|  | 34 | +	return t.timeouts[t.curTimeout] | 
|  | 35 | +} | 
|  | 36 | + | 
|  | 37 | +func (t *Timeouts) DecreaseTimeout() { | 
|  | 38 | +	t.mu.Lock() | 
|  | 39 | +	defer t.mu.Unlock() | 
|  | 40 | +	if t.curTimeout > 0 { | 
|  | 41 | +		t.curTimeout-- | 
|  | 42 | +	} | 
|  | 43 | +} | 
|  | 44 | + | 
|  | 45 | +func (t *Timeouts) IncreaseTimeout() { | 
|  | 46 | +	t.mu.Lock() | 
|  | 47 | +	defer t.mu.Unlock() | 
|  | 48 | +	t.curTimeout++ | 
|  | 49 | +	if t.curTimeout >= len(t.timeouts) { | 
|  | 50 | +		t.curTimeout = len(t.timeouts) - 1 | 
|  | 51 | +	} | 
|  | 52 | +} | 
|  | 53 | + | 
|  | 54 | +func (t *Timeouts) String() string { | 
|  | 55 | +	t.mu.RLock() | 
|  | 56 | +	defer t.mu.RUnlock() | 
|  | 57 | +	timeouts := make([]string, len(t.timeouts)) | 
|  | 58 | +	for i, t := range t.timeouts { | 
|  | 59 | +		timeouts[i] = t.String() | 
|  | 60 | +	} | 
|  | 61 | +	return strings.Join(timeouts, ",") | 
|  | 62 | +} | 
|  | 63 | + | 
|  | 64 | +// timeoutsListFromNumber generates a list of timeouts based on the initial timeout and the number of retries. | 
|  | 65 | +// The list is generated using a geometric progression with a growth factor of 2 for the first 1 minute, | 
|  | 66 | +// 1.5 for the next 1 minute, and 1.2 for the rest. | 
|  | 67 | +func timeoutsListFromNumber(initial time.Duration) []time.Duration { | 
|  | 68 | +	timeouts := make([]time.Duration, timeoutsCount) | 
|  | 69 | +	timeouts[0] = initial | 
|  | 70 | + | 
|  | 71 | +	for i := 1; i < timeoutsCount; i++ { | 
|  | 72 | +		prev := timeouts[i-1] | 
|  | 73 | +		next := increaseDuration(prev) | 
|  | 74 | +		timeouts[i] = next | 
|  | 75 | +	} | 
|  | 76 | + | 
|  | 77 | +	return timeouts | 
|  | 78 | +} | 
|  | 79 | + | 
|  | 80 | +func increaseDuration(prev time.Duration) time.Duration { | 
|  | 81 | +	var next time.Duration | 
|  | 82 | +	if prev < fastGrowThreshold { | 
|  | 83 | +		seconds := math.Ceil(float64(prev.Seconds()) * growthFactorFast) | 
|  | 84 | +		return time.Duration(seconds) * time.Second | 
|  | 85 | +	} else if prev < mediumGrowThreshold { | 
|  | 86 | +		seconds := math.Ceil(float64(prev.Seconds()) * growthFactorMedium) | 
|  | 87 | +		return time.Duration(seconds) * time.Second | 
|  | 88 | +	} else { | 
|  | 89 | +		seconds := math.Ceil(float64(prev.Seconds()) * growthFactorSlow) | 
|  | 90 | +		next = time.Duration(seconds) * time.Second | 
|  | 91 | +	} | 
|  | 92 | +	return next | 
|  | 93 | +} | 
|  | 94 | + | 
|  | 95 | +func getDynamicTimeouts(timeouts time.Duration) Timeouts { | 
|  | 96 | +	return Timeouts{ | 
|  | 97 | +		curTimeout: 0, | 
|  | 98 | +		timeouts:   timeoutsListFromNumber(timeouts), | 
|  | 99 | +	} | 
|  | 100 | +} | 
|  | 101 | + | 
|  | 102 | +func getFixedTimeouts(timeouts []time.Duration) Timeouts { | 
|  | 103 | +	return Timeouts{ | 
|  | 104 | +		curTimeout: 0, | 
|  | 105 | +		timeouts:   timeouts, | 
|  | 106 | +	} | 
|  | 107 | +} | 
|  | 108 | + | 
|  | 109 | +func getDefaultFixedTimeouts() Timeouts { | 
|  | 110 | +	timeouts, _, _ := ParseTimeouts(DefaultTimeouts) | 
|  | 111 | +	return getFixedTimeouts(timeouts) | 
|  | 112 | +} | 
|  | 113 | + | 
|  | 114 | +// ParseTimeouts parses a comma-separated string of duration values into a slice of time.Duration. | 
|  | 115 | +// Returns: | 
|  | 116 | +// - the parsed timeout values | 
|  | 117 | +// - if a fixed or dynamic timeouts should be used | 
|  | 118 | +// - an error in case the string cannot be parsed | 
|  | 119 | +func ParseTimeouts(value string) ([]time.Duration, bool, error) { | 
|  | 120 | +	if value == "" { | 
|  | 121 | +		return nil, true, fmt.Errorf("timeouts are not set") | 
|  | 122 | +	} | 
|  | 123 | + | 
|  | 124 | +	values := strings.Split(value, ",") | 
|  | 125 | +	for i := range values { | 
|  | 126 | +		values[i] = strings.TrimSpace(values[i]) | 
|  | 127 | +	} | 
|  | 128 | + | 
|  | 129 | +	hasTrailingComma := len(values) > 0 && values[len(values)-1] == "" | 
|  | 130 | +	if hasTrailingComma { | 
|  | 131 | +		values = values[:len(values)-1] | 
|  | 132 | +	} | 
|  | 133 | + | 
|  | 134 | +	timeouts := make([]time.Duration, 0, len(values)) | 
|  | 135 | +	for i, v := range values { | 
|  | 136 | +		d, err := time.ParseDuration(v) | 
|  | 137 | +		if err != nil { | 
|  | 138 | +			return nil, false, fmt.Errorf("parsing timeout parameter number %d: %v", i+1, err) | 
|  | 139 | +		} | 
|  | 140 | +		timeouts = append(timeouts, d) | 
|  | 141 | +	} | 
|  | 142 | +	if len(timeouts) == 1 && hasTrailingComma { | 
|  | 143 | +		return timeouts, true, nil | 
|  | 144 | +	} | 
|  | 145 | + | 
|  | 146 | +	for i := 1; i < len(timeouts); i++ { | 
|  | 147 | +		if timeouts[i] <= timeouts[i-1] { | 
|  | 148 | +			return nil, false, fmt.Errorf("timeout values must be in ascending order, got %v <= %v", timeouts[i], timeouts[i-1]) | 
|  | 149 | +		} | 
|  | 150 | +	} | 
|  | 151 | + | 
|  | 152 | +	if len(timeouts) > timeoutsCount { | 
|  | 153 | +		return nil, false, fmt.Errorf("exceeded max amount of allowed timeout parameters. Set %d but max is %d", len(timeouts), timeoutsCount) | 
|  | 154 | +	} | 
|  | 155 | +	return timeouts, false, nil | 
|  | 156 | +} | 
0 commit comments