Skip to content

Commit b9267cf

Browse files
committed
fix few stuffs
1 parent d2be80f commit b9267cf

24 files changed

Lines changed: 404 additions & 139 deletions

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.1.8
1+
3.1.9-alpha.5

subvortex/core/country/country.py

Lines changed: 169 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
}
3737

3838

39-
# Simple tracking
40-
_last_call_time = {}
39+
# Per-IP per-API rate limit tracking: {ip: {api_name: last_call_time}}
40+
_per_ip_rate_limits = {}
4141

4242
countries = {}
4343

@@ -52,35 +52,38 @@ def __init__(self, message: str, rate_limited: dict = None):
5252

5353
def get_country(ip: str):
5454
"""
55-
Get the country code of the ip
55+
Get the country code of the ip with aggressive API calling until rate limited.
56+
Single pass through APIs - infinite retry is handled by caller.
5657
"""
5758
ip_ipv4 = get_ipv4(ip)
58-
errors = []
59-
rate_limited = {}
6059

6160
# APIs ordered by accuracy (highest to lowest)
6261
apis = [
63-
(_get_country_by_ipinfo_io, "ipinfo"), # Highest accuracy (~99%)
64-
(_get_country_by_ip_api, "ip_api"), # Very high accuracy (~98%)
65-
(_get_country_by_country_is, "country_is"), # Good accuracy (~95%)
62+
(_get_country_by_ipinfo_io, "ipinfo"), # Highest accuracy (~99%)
63+
(_get_country_by_ip_api, "ip_api"), # Very high accuracy (~98%)
64+
(_get_country_by_country_is, "country_is"), # Good accuracy (~95%)
6665
# (_get_country_by_subvortex_api, "subvortex"), # Custom API - Not ready yet
6766
# (_get_country_by_my_api, "my_api"), # Down
6867
]
6968

69+
# Initialize per-IP tracking if not exists
70+
if ip_ipv4 not in _per_ip_rate_limits:
71+
_per_ip_rate_limits[ip_ipv4] = {}
72+
73+
errors = []
74+
rate_limited = {}
75+
7076
for api_func, api_name in apis:
71-
# Check if this API is still in cooldown
77+
# Check if this API is still in rate limit cooldown for this specific IP
7278
now = time.time()
73-
last_call = _last_call_time.get(api_name, 0)
74-
cooldown = API_RATE_LIMITS[api_name]
75-
76-
if now - last_call < cooldown:
77-
remaining = cooldown - (now - last_call)
78-
rate_limited[api_name] = remaining
79-
continue
79+
if api_name in _per_ip_rate_limits[ip_ipv4]:
80+
rate_limit_end = _per_ip_rate_limits[ip_ipv4][api_name]
81+
if now < rate_limit_end:
82+
remaining = rate_limit_end - now
83+
rate_limited[api_name] = remaining
84+
continue
8085

8186
try:
82-
_last_call_time[api_name] = now
83-
8487
country, reason = api_func(ip_ipv4)
8588
if country:
8689
return country
@@ -89,22 +92,106 @@ def get_country(ip: str):
8992
errors.append(f"{api_name}: {reason}")
9093

9194
except requests.HTTPError as e:
92-
status_code = getattr(e.response, 'status_code', 'unknown')
93-
errors.append(f"{api_name} ({status_code}): {e}")
95+
status_code = getattr(e.response, "status_code", "unknown")
9496

95-
except Exception as e:
96-
errors.append(f"{api_name}: {e}")
97+
# Check for rate limit status codes
98+
if status_code in [429, 403]: # Common rate limit codes
99+
rate_limit_duration = _extract_rate_limit_from_response(e.response, api_name)
100+
rate_limit_end = now + rate_limit_duration
101+
_per_ip_rate_limits[ip_ipv4][api_name] = rate_limit_end
102+
103+
btul.logging.warning(
104+
f"🚫 {api_name} rate limited for {ip_ipv4} (HTTP {status_code}). "
105+
f"Will retry after {rate_limit_duration}s"
106+
)
107+
rate_limited[api_name] = rate_limit_duration
108+
109+
else:
110+
errors.append(f"{api_name} ({status_code}): {e}")
97111

98-
# Build error message with rate limit info
112+
except Exception as e:
113+
# Check if error message indicates rate limiting
114+
error_msg = str(e).lower()
115+
if any(phrase in error_msg for phrase in ['rate limit', 'too many requests', 'quota exceeded']):
116+
# Apply default rate limit if we detect rate limiting but no HTTP status
117+
rate_limit_duration = API_RATE_LIMITS.get(api_name, 60) # Default to 60s
118+
rate_limit_end = now + rate_limit_duration
119+
_per_ip_rate_limits[ip_ipv4][api_name] = rate_limit_end
120+
121+
btul.logging.warning(
122+
f"🚫 {api_name} rate limited for {ip_ipv4} (detected from error). "
123+
f"Will retry after {rate_limit_duration}s"
124+
)
125+
rate_limited[api_name] = rate_limit_duration
126+
127+
else:
128+
errors.append(f"{api_name}: {e}")
129+
130+
# Combine all errors and rate limits
99131
all_errors = errors + [
100132
f"{api}: {remaining:.0f}s cooldown" for api, remaining in rate_limited.items()
101133
]
102134

135+
if all_errors:
136+
raise CountryApiException(
137+
f"All APIs failed for {ip_ipv4} - {'; '.join(all_errors)}", rate_limited
138+
)
139+
140+
# This should not happen, but safety fallback
103141
raise CountryApiException(
104-
f"All APIs failed for {ip_ipv4} - {'; '.join(all_errors)}", rate_limited
142+
f"No APIs available for {ip_ipv4}", rate_limited
105143
)
106144

107145

146+
def _extract_rate_limit_from_response(response, api_name: str) -> float:
147+
"""
148+
Extract rate limit duration from HTTP response headers.
149+
Returns duration in seconds to wait before retrying.
150+
"""
151+
try:
152+
# Try common rate limit headers
153+
headers_to_check = [
154+
'retry-after', # Standard header
155+
'x-ratelimit-reset', # Unix timestamp
156+
'x-rate-limit-reset', # Unix timestamp
157+
'x-ratelimit-retry-after', # Seconds
158+
'rate-limit-reset', # Seconds
159+
]
160+
161+
for header in headers_to_check:
162+
if header in response.headers:
163+
value = response.headers[header]
164+
165+
# Handle different formats
166+
if header in ['x-ratelimit-reset', 'x-rate-limit-reset']:
167+
# Unix timestamp - calculate difference from now
168+
try:
169+
reset_time = int(value)
170+
current_time = int(time.time())
171+
duration = max(reset_time - current_time, 1) # At least 1 second
172+
btul.logging.debug(f"Extracted rate limit from {header}: {duration}s")
173+
return float(duration)
174+
except (ValueError, TypeError):
175+
continue
176+
else:
177+
# Direct seconds value
178+
try:
179+
duration = float(value)
180+
btul.logging.debug(f"Extracted rate limit from {header}: {duration}s")
181+
return max(duration, 1) # At least 1 second
182+
except (ValueError, TypeError):
183+
continue
184+
185+
# If no headers found, use the official API rate limits
186+
duration = API_RATE_LIMITS.get(api_name, 60) # Default to 60s
187+
btul.logging.debug(f"No rate limit headers found, using default for {api_name}: {duration}s")
188+
return float(duration)
189+
190+
except Exception as e:
191+
btul.logging.warning(f"Error extracting rate limit from response: {e}")
192+
return 60.0 # Safe default
193+
194+
108195
def _get_country_by_subvortex_api(ip: str):
109196
"""
110197
Get the country code of the ip (use Maxwind and IpInfo)
@@ -199,3 +286,61 @@ def get_ipv4(ip):
199286
pass
200287

201288
return ip
289+
290+
291+
def cleanup_rate_limits_for_ips(ips: list[str]):
292+
"""
293+
Clean up rate limit tracking for specific IPs.
294+
Call this when neurons with these IPs are deleted from the metagraph.
295+
296+
Args:
297+
ips: List of IP addresses to clean up
298+
"""
299+
cleaned_count = 0
300+
for ip in ips:
301+
ip_ipv4 = get_ipv4(ip)
302+
if ip_ipv4 in _per_ip_rate_limits:
303+
del _per_ip_rate_limits[ip_ipv4]
304+
cleaned_count += 1
305+
306+
if cleaned_count > 0:
307+
btul.logging.debug(f"🧹 Cleaned up rate limit data for {cleaned_count} IPs")
308+
309+
return cleaned_count
310+
311+
312+
def cleanup_rate_limits_for_api(api_name: str):
313+
"""
314+
Clean up rate limit tracking for a specific API across all IPs.
315+
Call this if an API is permanently disabled or changed.
316+
317+
Args:
318+
api_name: Name of the API to clean up ("ipinfo", "ip_api", "country_is", etc.)
319+
"""
320+
cleaned_count = 0
321+
for ip_dict in _per_ip_rate_limits.values():
322+
if api_name in ip_dict:
323+
del ip_dict[api_name]
324+
cleaned_count += 1
325+
326+
if cleaned_count > 0:
327+
btul.logging.debug(
328+
f"🧹 Cleaned up {api_name} rate limit data for {cleaned_count} IPs"
329+
)
330+
331+
return cleaned_count
332+
333+
334+
def cleanup_all_rate_limits():
335+
"""
336+
Clean up all rate limit tracking data.
337+
Call this for complete reset or during shutdown.
338+
"""
339+
global _per_ip_rate_limits
340+
count = len(_per_ip_rate_limits)
341+
_per_ip_rate_limits.clear()
342+
343+
if count > 0:
344+
btul.logging.info(f"🧹 Cleaned up all rate limit data ({count} IPs)")
345+
346+
return count

0 commit comments

Comments
 (0)