Skip to content

Commit 2fe6278

Browse files
committed
fix geolit lookup
1 parent 16b0816 commit 2fe6278

3 files changed

Lines changed: 200 additions & 69 deletions

File tree

subvortex/core/country/geolookup.py

Lines changed: 124 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def search(self, ip: int) -> Optional[int]:
292292
"""Find geoname_id for IP address."""
293293
return self._search(self.root, ip)
294294

295-
def _search(self, node: IntervalTreeNode, ip: str):
295+
def _search(self, node: IntervalTreeNode, ip: int):
296296
if not node:
297297
return None
298298

@@ -325,6 +325,9 @@ class UltraFastGeoLookup:
325325
def __init__(self, output_dir: str = "/var/tmp", license_key: Optional[str] = None):
326326
self.output_dir = output_dir
327327
self.license_key = license_key
328+
329+
# Ensure output directory exists
330+
os.makedirs(self.output_dir, exist_ok=True)
328331
self.locations_file = os.path.join(
329332
self.output_dir, "GeoLite2-Country-Locations-en.csv"
330333
)
@@ -335,13 +338,15 @@ def __init__(self, output_dir: str = "/var/tmp", license_key: Optional[str] = No
335338
# Thread-safe data structures
336339
self._lock = RLock()
337340
self._country_map: Dict[int, str] = {}
338-
self._interval_tree = IntervalTree()
341+
self._ip_ranges = [] # Sorted list of (start_ip, end_ip, geoname_id)
339342

340343
# File monitoring
341344
self._locations_mtime = 0
342345
self._blocks_mtime = 0
343346
self._loaded = False
344347
self._load_start_time = 0
348+
self._ready = None # Will be created in start()
349+
self._initial_load_complete = False
345350

346351
# Performance cache - much larger for metagraph use
347352
self._cache: Dict[str, str] = {}
@@ -356,6 +361,8 @@ async def start(self):
356361
"""Start the geo lookup service with preloading and background updates."""
357362
btul.logging.info("🚀 Starting UltraFastGeoLookup service...")
358363

364+
# Create the ready event in async context
365+
self._ready = asyncio.Event()
359366
self._running = True
360367

361368
# Preload data at startup
@@ -386,19 +393,32 @@ async def _preload_data(self):
386393
self._load_start_time = start_time
387394

388395
# Check for updates first
396+
btul.logging.debug("Checking for updates...")
389397
await self._updater.check_and_download_updates()
390398

391399
# Load the data
400+
btul.logging.debug("Loading data internal...")
392401
self._load_data_internal()
393402

394403
load_time = time.time() - start_time
395404
btul.logging.info(
396405
f"✅ GeoLite2 data preloaded in {load_time:.2f}s: "
397406
f"{len(self._country_map)} countries, interval tree ready"
398407
)
408+
409+
# Mark as ready for consumers
410+
self._initial_load_complete = True
411+
if self._ready:
412+
self._ready.set()
399413

400414
except Exception as e:
401415
btul.logging.error(f"❌ Failed to preload GeoLite2 data: {e}")
416+
import traceback
417+
btul.logging.error(f"Traceback: {traceback.format_exc()}")
418+
# Even on error, mark as "ready" to avoid blocking consumers indefinitely
419+
# They will fall back to API lookups
420+
if self._ready:
421+
self._ready.set()
402422

403423
def _start_background_updates(self):
404424
"""Start background task for periodic CSV updates."""
@@ -424,7 +444,20 @@ async def _background_update_loop(self):
424444

425445
if updated:
426446
btul.logging.info("🔄 GeoLite2 files updated, reloading data...")
427-
self._load_data_internal()
447+
try:
448+
# Temporarily mark as not ready during reload to prevent exceptions
449+
if self._ready:
450+
self._ready.clear()
451+
self._load_data_internal()
452+
# Mark as ready again after successful reload
453+
if self._ready:
454+
self._ready.set()
455+
btul.logging.info("✅ GeoLite2 data reloaded successfully")
456+
except Exception as e:
457+
btul.logging.error(f"❌ Failed to reload GeoLite2 data: {e}")
458+
# Re-mark as ready to continue operations (will use cache or fallback)
459+
if self._ready:
460+
self._ready.set()
428461

429462
# Sleep for 6 hours before next check
430463
await asyncio.sleep(6 * 3600)
@@ -470,14 +503,23 @@ def _check_file_changes(self) -> bool:
470503
def _load_data_internal(self):
471504
"""Internal data loading with maximum performance."""
472505
with self._lock:
473-
if not self._check_file_changes() and self._loaded:
474-
return
506+
# Always update file times first to prevent infinite recursion
507+
self._locations_mtime = (
508+
os.path.getmtime(self.locations_file)
509+
if os.path.exists(self.locations_file)
510+
else 0
511+
)
512+
self._blocks_mtime = (
513+
os.path.getmtime(self.blocks_file)
514+
if os.path.exists(self.blocks_file)
515+
else 0
516+
)
475517

476-
btul.logging.debug("📊 Reloading GeoLite2 data due to file changes...")
518+
btul.logging.debug("📊 Loading GeoLite2 data...")
477519

478520
# Clear previous data
479521
self._country_map.clear()
480-
self._interval_tree = IntervalTree()
522+
self._ip_ranges.clear()
481523
self._cache.clear()
482524

483525
# Load country mappings with memory-mapped file for large files
@@ -486,6 +528,7 @@ def _load_data_internal(self):
486528

487529
# Load IP blocks and build interval tree
488530
if os.path.exists(self.blocks_file):
531+
btul.logging.debug("Loading blocks file...")
489532
self._load_blocks_optimized()
490533

491534
self._loaded = True
@@ -503,13 +546,15 @@ def _load_locations_optimized(self):
503546
def _load_blocks_optimized(self):
504547
"""Load IP blocks and build interval tree with progress."""
505548
processed = 0
549+
blocks_data = []
506550

551+
# First pass: collect all data
507552
with open(self.blocks_file, "r", encoding="utf-8") as f:
508553
reader = csv.DictReader(f)
509554

510555
for row in reader:
511556
network = row.get("network")
512-
geoname_id = row.get("geoname_id")
557+
geoname_id = row.get("geoname_id") or row.get("registered_country_geoname_id")
513558

514559
if network and geoname_id:
515560
try:
@@ -518,40 +563,84 @@ def _load_blocks_optimized(self):
518563
start_ip = int(net.network_address)
519564
end_ip = int(net.broadcast_address)
520565

521-
# Insert into interval tree
522-
self._interval_tree.insert(start_ip, end_ip, int(geoname_id))
566+
blocks_data.append((start_ip, end_ip, int(geoname_id)))
523567
processed += 1
524568

525569
# Progress indicator for large datasets
526570
if processed % 50000 == 0:
527571
elapsed = time.time() - self._load_start_time
528572
btul.logging.debug(
529-
f"📈 Processed {processed:,} IP ranges in {elapsed:.1f}s"
573+
f"📈 Collected {processed:,} IP ranges in {elapsed:.1f}s"
530574
)
531575

532576
except (ValueError, ipaddress.AddressValueError):
533577
continue
534578

579+
# Sort IP ranges for binary search
580+
btul.logging.debug(f"Sorting {len(blocks_data)} IP blocks for binary search...")
581+
blocks_data.sort(key=lambda x: x[0]) # Sort by start IP
582+
583+
# Store sorted ranges
584+
self._ip_ranges = blocks_data
585+
btul.logging.debug(f"✅ Stored {len(self._ip_ranges)} IP ranges for ultra-fast lookups")
586+
587+
async def wait_for_ready(self, timeout: Optional[float] = 30.0):
588+
"""Wait for the geo lookup service to be ready for use."""
589+
if self._ready is None:
590+
btul.logging.warning("⚠️ GeoLookup service not started yet")
591+
return
592+
593+
if timeout:
594+
try:
595+
await asyncio.wait_for(self._ready.wait(), timeout=timeout)
596+
except asyncio.TimeoutError:
597+
btul.logging.warning(f"⚠️ GeoLite2 data loading timed out after {timeout}s - continuing with API fallback")
598+
else:
599+
await self._ready.wait()
600+
601+
def is_ready(self) -> bool:
602+
"""Check if the geo lookup service is ready for use."""
603+
return self._ready is not None and self._ready.is_set() and self._initial_load_complete
604+
535605
def lookup_country(self, ip_str: str) -> Optional[str]:
536606
"""
537607
Ultra-fast IP to country lookup with automatic reloading.
538608
Returns country code (e.g., 'US', 'DE') or None if not found.
539609
"""
610+
# If data is not ready yet, return None to trigger API fallback
611+
if self._ready is None or not self._ready.is_set():
612+
btul.logging.debug(f"GeoLite2 data not ready yet for {ip_str} - will use API fallback")
613+
return None
614+
540615
# Check cache first (fastest path)
541616
cached = self._cache.get(ip_str)
542617
if cached is not None:
543618
return cached if cached != "__NOT_FOUND__" else None
544619

545620
# Hot reload if files changed
546621
if self._check_file_changes():
547-
self._load_data_internal()
622+
try:
623+
# Temporarily mark as not ready during reload to prevent race conditions
624+
if self._ready:
625+
self._ready.clear()
626+
self._load_data_internal()
627+
# Mark as ready again after successful reload
628+
if self._ready:
629+
self._ready.set()
630+
except Exception as e:
631+
btul.logging.error(f"❌ Failed to hot reload GeoLite2 data: {e}")
632+
# Re-mark as ready to continue operations
633+
if self._ready:
634+
self._ready.set()
635+
# Return None to trigger API fallback for this lookup
636+
return None
548637

549638
try:
550-
# Convert IP to integer for ultra-fast tree search
639+
# Convert IP to integer for binary search
551640
ip_int = int(ipaddress.IPv4Address(ip_str))
552641

553-
# Search interval tree - O(log n)
554-
geoname_id = self._interval_tree.search(ip_int)
642+
# Binary search through sorted IP ranges - O(log n)
643+
geoname_id = self._binary_search_ip_ranges(ip_int)
555644
if geoname_id is None:
556645
# Cache negative result
557646
self._cache_result(ip_str, None)
@@ -566,6 +655,26 @@ def lookup_country(self, ip_str: str) -> Optional[str]:
566655
self._cache_result(ip_str, None)
567656
return None
568657

658+
def _binary_search_ip_ranges(self, ip_int: int) -> Optional[int]:
659+
"""Binary search to find the geoname_id for an IP address."""
660+
if not self._ip_ranges:
661+
return None
662+
663+
left, right = 0, len(self._ip_ranges) - 1
664+
665+
while left <= right:
666+
mid = (left + right) // 2
667+
start_ip, end_ip, geoname_id = self._ip_ranges[mid]
668+
669+
if start_ip <= ip_int <= end_ip:
670+
return geoname_id
671+
elif ip_int < start_ip:
672+
right = mid - 1
673+
else:
674+
left = mid + 1
675+
676+
return None
677+
569678
def _cache_result(self, ip_str: str, country: Optional[str]):
570679
"""Cache result with LRU-style eviction."""
571680
if len(self._cache) >= self._cache_max_size:

0 commit comments

Comments
 (0)