123 lines
3.3 KiB
Go
123 lines
3.3 KiB
Go
package arbitrum
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/ethclient"
|
|
"golang.org/x/time/rate"
|
|
|
|
pkgerrors "github.com/fraktal/mev-beta/pkg/errors"
|
|
)
|
|
|
|
// RateLimitedRPC wraps an ethclient.Client with rate limiting
|
|
type RateLimitedRPC struct {
|
|
client *ethclient.Client
|
|
limiter *rate.Limiter
|
|
retryCount int
|
|
}
|
|
|
|
// NewRateLimitedRPC creates a new rate limited RPC client
|
|
func NewRateLimitedRPC(client *ethclient.Client, requestsPerSecond float64, retryCount int) *RateLimitedRPC {
|
|
// Create a rate limiter that allows requestsPerSecond requests per second
|
|
// with a burst equal to 2x requests per second
|
|
limiter := rate.NewLimiter(rate.Limit(requestsPerSecond), int(requestsPerSecond*2))
|
|
|
|
return &RateLimitedRPC{
|
|
client: client,
|
|
limiter: limiter,
|
|
retryCount: retryCount,
|
|
}
|
|
}
|
|
|
|
// CallWithRetry calls an RPC method with rate limiting and retry logic
|
|
func (r *RateLimitedRPC) CallWithRetry(ctx context.Context, method string, args ...interface{}) (interface{}, error) {
|
|
for i := 0; i < r.retryCount; i++ {
|
|
// Wait for rate limiter allowance
|
|
if err := r.limiter.Wait(ctx); err != nil {
|
|
return nil, fmt.Errorf("rate limiter error: %w", err)
|
|
}
|
|
|
|
// Execute the call
|
|
result, err := r.executeCall(ctx, method, args...)
|
|
if err == nil {
|
|
return result, nil
|
|
}
|
|
|
|
// Check if this is a rate limit error that warrants retrying
|
|
if isRateLimitError(err) {
|
|
// Apply exponential backoff before retrying
|
|
backoffTime := time.Duration(1<<uint(i)) * time.Second
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, pkgerrors.WrapContextError(ctx.Err(), "RateLimitedRPC.CallWithRetry.rateLimitBackoff",
|
|
map[string]interface{}{
|
|
"method": method,
|
|
"attempt": i + 1,
|
|
"maxRetries": r.retryCount,
|
|
"backoffTime": backoffTime.String(),
|
|
"lastError": err.Error(),
|
|
})
|
|
case <-time.After(backoffTime):
|
|
// Continue to next retry
|
|
continue
|
|
}
|
|
}
|
|
|
|
// For non-rate limit errors, return immediately
|
|
return nil, err
|
|
}
|
|
|
|
return nil, fmt.Errorf("max retries (%d) exceeded for method %s", r.retryCount, method)
|
|
}
|
|
|
|
// executeCall executes the actual RPC call
|
|
func (r *RateLimitedRPC) executeCall(ctx context.Context, method string, args ...interface{}) (interface{}, error) {
|
|
// Since we don't have the specific method signatures here, we'll just
|
|
// return a generic success response for demonstration
|
|
// In a real implementation, you would actually call the appropriate method
|
|
|
|
// For now, we'll just simulate a successful call
|
|
return "success", nil
|
|
}
|
|
|
|
// isRateLimitError checks if an error is a rate limit error
|
|
func isRateLimitError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
errStr := err.Error()
|
|
|
|
// Common rate limit error indicators
|
|
rateLimitIndicators := []string{
|
|
"rate limit",
|
|
"rate-limit",
|
|
"rps limit",
|
|
"request limit",
|
|
"too many requests",
|
|
"429",
|
|
"exceeded",
|
|
"limit exceeded",
|
|
}
|
|
|
|
for _, indicator := range rateLimitIndicators {
|
|
if containsIgnoreCase(errStr, indicator) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// containsIgnoreCase checks if a string contains a substring (case insensitive)
|
|
func containsIgnoreCase(s, substr string) bool {
|
|
// Convert both strings to lowercase for comparison
|
|
sLower := strings.ToLower(s)
|
|
substrLower := strings.ToLower(substr)
|
|
|
|
return strings.Contains(sLower, substrLower)
|
|
}
|