retry.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package grpc
  14. import (
  15. "context"
  16. cryptorand "crypto/rand"
  17. "encoding/binary"
  18. "errors"
  19. "fmt"
  20. "math"
  21. "sync"
  22. "time"
  23. "google.golang.org/grpc/codes"
  24. "google.golang.org/grpc/status"
  25. )
  26. // RetryConfig configures retry behavior.
  27. type RetryConfig struct {
  28. // MaxAttempts is the maximum number of retry attempts.
  29. MaxAttempts int
  30. // InitialBackoff is the initial backoff duration.
  31. InitialBackoff time.Duration
  32. // MaxBackoff is the maximum backoff duration.
  33. MaxBackoff time.Duration
  34. // BackoffMultiplier is the multiplier for exponential backoff.
  35. BackoffMultiplier float64
  36. // Jitter adds randomness to backoff to prevent thundering herd.
  37. Jitter bool
  38. }
  39. // DefaultRetryConfig returns sensible defaults for retry behavior.
  40. func DefaultRetryConfig() RetryConfig {
  41. return RetryConfig{
  42. MaxAttempts: 3,
  43. InitialBackoff: 100 * time.Millisecond,
  44. MaxBackoff: 10 * time.Second,
  45. BackoffMultiplier: 2.0,
  46. Jitter: true,
  47. }
  48. }
  49. // CircuitBreakerConfig configures circuit breaker behavior.
  50. type CircuitBreakerConfig struct {
  51. // MaxFailures is the number of consecutive failures before opening.
  52. MaxFailures int
  53. // Timeout is how long to wait in open state before trying again.
  54. Timeout time.Duration
  55. // HalfOpenMaxRequests is max requests allowed in half-open state.
  56. HalfOpenMaxRequests int
  57. }
  58. // DefaultCircuitBreakerConfig returns sensible defaults.
  59. func DefaultCircuitBreakerConfig() CircuitBreakerConfig {
  60. return CircuitBreakerConfig{
  61. MaxFailures: 5,
  62. Timeout: 30 * time.Second,
  63. HalfOpenMaxRequests: 1,
  64. }
  65. }
  66. // CircuitState represents the state of a circuit breaker.
  67. type CircuitState int
  68. const (
  69. // StateClosed means the circuit is closed and requests flow normally.
  70. StateClosed CircuitState = iota
  71. // StateOpen means the circuit is open and requests fail fast.
  72. StateOpen
  73. // StateHalfOpen means the circuit is testing if the service has recovered.
  74. StateHalfOpen
  75. )
  76. // CircuitBreaker implements the circuit breaker pattern.
  77. type CircuitBreaker struct {
  78. mu sync.RWMutex
  79. state CircuitState
  80. failures int
  81. lastFailureTime time.Time
  82. halfOpenReqs int
  83. config CircuitBreakerConfig
  84. }
  85. // NewCircuitBreaker creates a new circuit breaker.
  86. func NewCircuitBreaker(config CircuitBreakerConfig) *CircuitBreaker {
  87. return &CircuitBreaker{
  88. state: StateClosed,
  89. config: config,
  90. }
  91. }
  92. // Call executes the given function with circuit breaker protection.
  93. func (cb *CircuitBreaker) Call(_ context.Context, fn func() error) error {
  94. // Check if we should allow the request.
  95. if err := cb.beforeRequest(); err != nil {
  96. return err
  97. }
  98. // Execute the function.
  99. err := fn()
  100. // Record the result.
  101. cb.afterRequest(err)
  102. return err
  103. }
  104. // beforeRequest checks if the request should be allowed.
  105. func (cb *CircuitBreaker) beforeRequest() error {
  106. cb.mu.Lock()
  107. defer cb.mu.Unlock()
  108. switch cb.state {
  109. case StateClosed:
  110. return nil
  111. case StateOpen:
  112. // Check if timeout has elapsed
  113. if time.Since(cb.lastFailureTime) > cb.config.Timeout {
  114. cb.state = StateHalfOpen
  115. cb.halfOpenReqs = 0
  116. return nil
  117. }
  118. return fmt.Errorf("circuit breaker is open")
  119. case StateHalfOpen:
  120. // Allow limited requests in half-open state
  121. if cb.halfOpenReqs >= cb.config.HalfOpenMaxRequests {
  122. return fmt.Errorf("circuit breaker is half-open, max requests reached")
  123. }
  124. cb.halfOpenReqs++
  125. return nil
  126. default:
  127. return fmt.Errorf("unknown circuit breaker state")
  128. }
  129. }
  130. // afterRequest records the result of a request.
  131. func (cb *CircuitBreaker) afterRequest(err error) {
  132. cb.mu.Lock()
  133. defer cb.mu.Unlock()
  134. if err == nil {
  135. // Success
  136. cb.onSuccess()
  137. } else {
  138. // Failure
  139. cb.onFailure()
  140. }
  141. }
  142. // onSuccess handles a successful request.
  143. func (cb *CircuitBreaker) onSuccess() {
  144. switch cb.state {
  145. case StateClosed:
  146. cb.failures = 0
  147. case StateOpen:
  148. // Requests should not reach onSuccess while open, keep state unchanged.
  149. case StateHalfOpen:
  150. // Success in half-open state means we can close the circuit
  151. cb.state = StateClosed
  152. cb.failures = 0
  153. cb.halfOpenReqs = 0
  154. }
  155. }
  156. // onFailure handles a failed request.
  157. func (cb *CircuitBreaker) onFailure() {
  158. cb.failures++
  159. cb.lastFailureTime = time.Now()
  160. switch cb.state {
  161. case StateClosed:
  162. if cb.failures >= cb.config.MaxFailures {
  163. cb.state = StateOpen
  164. }
  165. case StateOpen:
  166. // Keep tracking failure timing while the circuit remains open.
  167. case StateHalfOpen:
  168. // Failure in half-open state means we go back to open
  169. cb.state = StateOpen
  170. cb.halfOpenReqs = 0
  171. }
  172. }
  173. // State returns the current state of the circuit breaker.
  174. func (cb *CircuitBreaker) State() CircuitState {
  175. cb.mu.RLock()
  176. defer cb.mu.RUnlock()
  177. return cb.state
  178. }
  179. // RetryableFunc is a function that can be retried.
  180. type RetryableFunc func(ctx context.Context, attempt int) error
  181. // WithRetry executes a function with exponential backoff retry logic.
  182. func WithRetry(ctx context.Context, config RetryConfig, fn RetryableFunc) error {
  183. var lastErr error
  184. for attempt := 0; attempt < config.MaxAttempts; attempt++ {
  185. // Execute the function
  186. err := fn(ctx, attempt)
  187. if err == nil {
  188. return nil
  189. }
  190. lastErr = err
  191. // Check if error is retryable
  192. if !isRetryable(err) {
  193. return err
  194. }
  195. // Last attempt, don't wait
  196. if attempt == config.MaxAttempts-1 {
  197. break
  198. }
  199. // Calculate backoff
  200. backoff := calculateBackoff(attempt, config)
  201. // Wait with context cancellation support
  202. select {
  203. case <-ctx.Done():
  204. return ctx.Err()
  205. case <-time.After(backoff):
  206. // Continue to next attempt
  207. }
  208. }
  209. return fmt.Errorf("max retry attempts (%d) exceeded: %w", config.MaxAttempts, lastErr)
  210. }
  211. // isRetryable determines if an error should trigger a retry.
  212. func isRetryable(err error) bool {
  213. // Check gRPC status codes
  214. st, ok := status.FromError(err)
  215. if ok {
  216. switch st.Code() {
  217. case codes.Unavailable,
  218. codes.DeadlineExceeded,
  219. codes.ResourceExhausted,
  220. codes.Aborted:
  221. return true
  222. case codes.OK,
  223. codes.Canceled,
  224. codes.InvalidArgument,
  225. codes.NotFound,
  226. codes.AlreadyExists,
  227. codes.PermissionDenied,
  228. codes.Unauthenticated,
  229. codes.FailedPrecondition,
  230. codes.OutOfRange,
  231. codes.Unimplemented:
  232. return false
  233. case codes.Unknown,
  234. codes.Internal,
  235. codes.DataLoss:
  236. // Retry transient or ambiguous failures.
  237. return true
  238. }
  239. }
  240. // For non-gRPC errors, retry network-related issues.
  241. // Context errors should not be retried.
  242. if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
  243. return false
  244. }
  245. // Default: retry.
  246. return true
  247. }
  248. // calculateBackoff calculates the backoff duration for a given attempt.
  249. func calculateBackoff(attempt int, config RetryConfig) time.Duration {
  250. // Exponential backoff: initialBackoff * (multiplier ^ attempt).
  251. backoff := float64(config.InitialBackoff) * math.Pow(config.BackoffMultiplier, float64(attempt))
  252. // Apply max backoff.
  253. if backoff > float64(config.MaxBackoff) {
  254. backoff = float64(config.MaxBackoff)
  255. }
  256. // Add jitter if enabled.
  257. if config.Jitter {
  258. // Add random jitter between 0 and 25% of backoff.
  259. jitter := randomFloat64() * backoff * 0.25
  260. backoff += jitter
  261. }
  262. return time.Duration(backoff)
  263. }
  264. func randomFloat64() float64 {
  265. var buf [8]byte
  266. if _, err := cryptorand.Read(buf[:]); err != nil {
  267. return 0
  268. }
  269. return float64(binary.BigEndian.Uint64(buf[:])) / float64(^uint64(0))
  270. }