@@ -38,23 +38,22 @@ def __init__(
3838 rate: Maximum requests per second (refill rate)
3939 burst: Maximum tokens (bucket size)
4040 """
41- self .chain_name = chain_name
41+ # Normalize chain name to lowercase to prevent split buckets
42+ self .chain_name = chain_name .lower ().strip ()
4243 self .rate = rate
4344 self .burst = burst
4445
4546 # Ensure cache directory exists
4647 CACHE_DIR .mkdir (parents = True , exist_ok = True )
4748
4849 # Sanitize chain name for filename
49- safe_name = "" .join (c for c in chain_name if c .isalnum () or c in ('-' , '_' ))
50+ safe_name = "" .join (c for c in self . chain_name if c .isalnum () or c in ('-' , '_' ))
5051 self .state_file : Path = CACHE_DIR / f"rate_limit_{ safe_name } .json"
5152
5253 # Use a string path for FileLock to ensure compatibility
5354 lock_path = CACHE_DIR / f"rate_limit_{ safe_name } .lock"
5455 self ._lock = FileLock (str (lock_path ))
5556
56- self ._backoff_until = 0.0
57-
5857 def _load_state (self ) -> dict :
5958 """Load state from file, initializing if necessary."""
6059 try :
@@ -68,6 +67,7 @@ def _load_state(self) -> dict:
6867 return {
6968 "tokens" : float (self .burst ),
7069 "last_update" : time .time (),
70+ "backoff_until" : 0.0 ,
7171 }
7272
7373 def _save_state (self , state : dict ):
@@ -86,18 +86,18 @@ def acquire(self, timeout: float = 30.0) -> bool:
8686 try :
8787 # Acquire file lock to read/write state safely across processes
8888 with self ._lock .acquire (timeout = timeout ):
89+ state = self ._load_state ()
8990 now = time .time ()
9091
91- # Check backoff (in-memory only for now, could be persisted if needed globally)
92- if now < self ._backoff_until :
93- wait_time = self ._backoff_until - now
92+ # Check global persisted backoff
93+ backoff_until = state .get ("backoff_until" , 0.0 )
94+ if now < backoff_until :
95+ wait_time = backoff_until - now
9496 if now + wait_time > deadline :
9597 return False
9698 else :
97- state = self ._load_state ()
98-
99- tokens = state ["tokens" ]
100- last_update = state ["last_update" ]
99+ tokens = state .get ("tokens" , float (self .burst ))
100+ last_update = state .get ("last_update" , now )
101101
102102 # Refill tokens based on elapsed time
103103 elapsed = now - last_update
@@ -136,20 +136,31 @@ def acquire(self, timeout: float = 30.0) -> bool:
136136 time .sleep (0.1 )
137137
138138 def trigger_backoff (self , seconds : float = 5.0 ):
139- """Trigger rate limit backoff."""
140- # Simple in-memory backoff for now
141- self ._backoff_until = time .time () + seconds
142- logger .warning (f"RPC rate limit triggered, backing off for { seconds } s" )
139+ """Trigger rate limit backoff globally."""
140+ backoff_until = time .time () + seconds
141+ logger .warning (f"RPC rate limit triggered for { self .chain_name } , backing off for { seconds } s (GLOBAL)" )
142+
143+ try :
144+ with self ._lock .acquire (timeout = 2.0 ):
145+ state = self ._load_state ()
146+ # Only update if our backoff is further in future
147+ current_backoff = state .get ("backoff_until" , 0.0 )
148+ if backoff_until > current_backoff :
149+ state ["backoff_until" ] = backoff_until
150+ self ._save_state (state )
151+ except Exception as e :
152+ logger .error (f"Failed to persist backoff state: { e } " )
143153
144154 def get_status (self ) -> dict :
145155 """Get current rate limiter status."""
146156 try :
147157 with self ._lock .acquire (timeout = 1.0 ):
148158 state = self ._load_state ()
149159 return {
150- "tokens" : state [ "tokens" ] ,
160+ "tokens" : state . get ( "tokens" , 0 ) ,
151161 "rate" : self .rate ,
152162 "burst" : self .burst ,
163+ "backoff_until" : state .get ("backoff_until" , 0.0 ),
153164 }
154165 except TimeoutError :
155166 return {"error" : "Could not acquire lock to read status" }
@@ -165,35 +176,35 @@ def get_rate_limiter(chain_name: str, rate: float = None, burst: int = None) ->
165176
166177 If the limiter already exists but with different parameters, update it.
167178 """
179+ # Normalize chain name for cache lookup
180+ normalized_name = chain_name .lower ().strip ()
181+
168182 with _rate_limiters_lock :
169- if chain_name not in _rate_limiters :
170- _rate_limiters [chain_name ] = RPCRateLimiter (
171- chain_name = chain_name ,
183+ if normalized_name not in _rate_limiters :
184+ _rate_limiters [normalized_name ] = RPCRateLimiter (
185+ chain_name = normalized_name ,
172186 rate = rate or RPCRateLimiter .DEFAULT_RATE ,
173187 burst = burst or RPCRateLimiter .DEFAULT_BURST ,
174188 )
175189 else :
176190 # Update existing limiter if stricter parameters are requested
177- limiter = _rate_limiters [chain_name ]
191+ limiter = _rate_limiters [normalized_name ]
178192
179193 if rate is not None and rate != limiter .rate :
180194 logger .info (
181- f"Updating rate limiter for { chain_name } : "
195+ f"Updating rate limiter for { normalized_name } : "
182196 f"rate { limiter .rate } ->{ rate } "
183197 )
184198 limiter .rate = rate
185199
186200 if burst is not None and burst != limiter .burst :
187201 logger .info (
188- f"Updating rate limiter for { chain_name } : "
202+ f"Updating rate limiter for { normalized_name } : "
189203 f"burst { limiter .burst } ->{ burst } "
190204 )
191205 limiter .burst = burst
192206
193- # If parameters updated, we might want to force a state save/refresh,
194- # but next acquire will handle it naturally.
195-
196- return _rate_limiters [chain_name ]
207+ return _rate_limiters [normalized_name ]
197208
198209
199210class RateLimitedEth :
0 commit comments