@@ -3,7 +3,9 @@ package rpc
33import (
44 "context"
55 "fmt"
6+ "math"
67 "sync"
8+ "time"
79
810 "github.com/ethereum/go-ethereum/ethclient"
911 "github.com/flashbots/chain-monitor/utils"
@@ -177,3 +179,118 @@ func callFallbackThenMainWithResult[R any](
177179 var _nil R
178180 return _nil , utils .FlattenErrors (errs )
179181}
182+
183+ // callMainThenFallbackWithBackoff calls the main RPC then fallback RPCs with exponential backoff
184+ func callMainThenFallbackWithBackoff (
185+ ctx context.Context ,
186+ rpc * RPC ,
187+ call func (ctx context.Context , rpc * ethclient.Client ) error ,
188+ maxRetries int ,
189+ initialDelay time.Duration ,
190+ ) error {
191+ _ , err := callMainThenFallbackWithBackoffAndResult (ctx , rpc , func (ctx context.Context , cli * ethclient.Client ) (struct {}, error ) {
192+ return struct {}{}, call (ctx , cli )
193+ }, maxRetries , initialDelay )
194+ return err
195+ }
196+
197+ // callMainThenFallbackWithBackoffAndResult calls the main RPC then fallback RPCs with exponential backoff
198+ func callMainThenFallbackWithBackoffAndResult [R any ](
199+ ctx context.Context ,
200+ rpc * RPC ,
201+ call func (ctx context.Context , rpc * ethclient.Client ) (R , error ),
202+ maxRetries int ,
203+ initialDelay time.Duration ,
204+ ) (R , error ) {
205+ errs := make ([]error , 0 , len (rpc .fallback )+ 1 )
206+
207+ // Try main with exponential backoff
208+ if res , err := retryWithBackoff (ctx , rpc , rpc .main , call , maxRetries , initialDelay ); err == nil {
209+ return res , nil
210+ } else {
211+ errs = append (errs , fmt .Errorf ("%s: %w" , rpc .url .main , err ))
212+ }
213+
214+ // Try fallback RPCs with exponential backoff
215+ for idx , fallback := range rpc .fallback {
216+ if res , err := retryWithBackoff (ctx , rpc , fallback , call , maxRetries , initialDelay ); err == nil {
217+ return res , nil
218+ } else {
219+ errs = append (errs , fmt .Errorf ("%s: %w" , rpc .url .fallback [idx ], err ))
220+ }
221+ }
222+
223+ var _nil R
224+ return _nil , utils .FlattenErrors (errs )
225+ }
226+
227+ // callFallbackThenMainWithBackoffAndResult calls fallback RPCs then main RPC with exponential backoff
228+ func callFallbackThenMainWithBackoffAndResult [R any ](
229+ ctx context.Context ,
230+ rpc * RPC ,
231+ call func (ctx context.Context , rpc * ethclient.Client ) (R , error ),
232+ maxRetries int ,
233+ initialDelay time.Duration ,
234+ ) (R , error ) {
235+ errs := make ([]error , 0 , len (rpc .fallback )+ 1 )
236+
237+ // Try fallback RPCs with exponential backoff
238+ for idx , fallback := range rpc .fallback {
239+ if res , err := retryWithBackoff (ctx , rpc , fallback , call , maxRetries , initialDelay ); err == nil {
240+ return res , nil
241+ } else {
242+ errs = append (errs , fmt .Errorf ("%s: %w" , rpc .url .fallback [idx ], err ))
243+ }
244+ }
245+
246+ // Try main with exponential backoff
247+ if res , err := retryWithBackoff (ctx , rpc , rpc .main , call , maxRetries , initialDelay ); err == nil {
248+ return res , nil
249+ } else {
250+ errs = append (errs , fmt .Errorf ("%s: %w" , rpc .url .main , err ))
251+ }
252+
253+ var _nil R
254+ return _nil , utils .FlattenErrors (errs )
255+ }
256+
257+ // retryWithBackoff retries a call with exponential backoff
258+ func retryWithBackoff [R any ](
259+ ctx context.Context ,
260+ rpc * RPC ,
261+ client * ethclient.Client ,
262+ call func (ctx context.Context , rpc * ethclient.Client ) (R , error ),
263+ maxRetries int ,
264+ initialDelay time.Duration ,
265+ ) (R , error ) {
266+ var lastErr error
267+ var result R
268+
269+ for attempt := 0 ; attempt <= maxRetries ; attempt ++ {
270+ _ctx , cancel := context .WithTimeout (ctx , rpc .timeout )
271+ res , err := call (_ctx , client )
272+ cancel ()
273+
274+ if err == nil {
275+ return res , nil
276+ }
277+
278+ lastErr = err
279+
280+ // Don't sleep after the last attempt
281+ if attempt < maxRetries {
282+ // Calculate exponential backoff delay: initialDelay * 2^attempt
283+ delay := time .Duration (float64 (initialDelay ) * math .Pow (2 , float64 (attempt )))
284+
285+ // Wait for the backoff delay or context cancellation
286+ select {
287+ case <- ctx .Done ():
288+ var _nil R
289+ return _nil , ctx .Err ()
290+ case <- time .After (delay ):
291+ }
292+ }
293+ }
294+
295+ return result , lastErr
296+ }
0 commit comments