@@ -198,21 +198,33 @@ async def start(self):
198198 )
199199
200200 # Sync from chain and update Redis
201- axons , has_missing_country = await self ._resync (
202- last_update = last_update
203- )
201+ try :
202+ axons , has_missing_country = await self ._resync (
203+ last_update = last_update
204+ )
204205
205- # Store the sync block
206- last_synced_block = block
206+ # Store the sync block
207+ last_synced_block = block
207208
208- # Notify listener the metagraph is ready with retry logic
209- await self ._notify_with_retry (state == "ready" )
209+ # Notify listener the metagraph is ready with retry logic
210+ await self ._notify_with_retry (state == "ready" )
211+
212+ except Exception as resync_error :
213+ btul .logging .error (
214+ f"❌ Resync failed: { resync_error } " ,
215+ prefix = self .settings .logging_name ,
216+ )
217+ # Mark metagraph as unready due to resync failure
218+ await self ._mark_unready_on_error (f"Resync failed: { resync_error } " )
219+ # Continue loop to retry
210220
211221 # Store the new axons
212222 axons = new_axons
213223
214224 except ConnectionRefusedError as e :
215225 btul .logging .error (f"Connection refused: { e } " )
226+ # Mark metagraph as unready due to connection issues
227+ await self ._mark_unready_on_error ("Connection refused" )
216228 await asyncio .sleep (1 )
217229
218230 except Exception as e :
@@ -223,6 +235,8 @@ async def start(self):
223235 btul .logging .debug (
224236 traceback .format_exc (), prefix = self .settings .logging_name
225237 )
238+ # Mark metagraph as unready due to unhandled error
239+ await self ._mark_unready_on_error (f"Unhandled error: { e } " )
226240
227241 finally :
228242 # Stop the geo lookup service if it was started
@@ -278,11 +292,18 @@ async def _resync(self, last_update: bool) -> dict[str, str]:
278292 old_ips_to_cleanup : list [str ] = []
279293 has_missing_country = False
280294
281- stored_neurons = await self .database .get_neurons ()
282- btul .logging .debug (
283- f"💾 Neurons loaded from Redis: { len (stored_neurons )} " ,
284- prefix = self .settings .logging_name ,
285- )
295+ try :
296+ stored_neurons = await self .database .get_neurons ()
297+ btul .logging .debug (
298+ f"💾 Neurons loaded from Redis: { len (stored_neurons )} " ,
299+ prefix = self .settings .logging_name ,
300+ )
301+ except Exception as e :
302+ btul .logging .error (
303+ f"❌ Failed to load neurons from Redis: { e } " ,
304+ prefix = self .settings .logging_name ,
305+ )
306+ raise # Re-raise to trigger resync failure handling
286307
287308 mhotkeys = set ()
288309
@@ -416,9 +437,16 @@ async def _resync(self, last_update: bool) -> dict[str, str]:
416437 if stale_ips :
417438 sccc .cleanup_rate_limits_for_ips (stale_ips )
418439
419- not self .settings .dry_run and await self .database .remove_neurons (
420- stale_neurons
421- )
440+ try :
441+ not self .settings .dry_run and await self .database .remove_neurons (
442+ stale_neurons
443+ )
444+ except Exception as e :
445+ btul .logging .error (
446+ f"❌ Failed to remove stale neurons from Redis: { e } " ,
447+ prefix = self .settings .logging_name ,
448+ )
449+ raise # Re-raise to trigger resync failure handling
422450
423451 if neurons_to_delete :
424452 btul .logging .debug (
@@ -431,9 +459,16 @@ async def _resync(self, last_update: bool) -> dict[str, str]:
431459 )
432460
433461 # Remove the neurons
434- not self .settings .dry_run and await self .database .remove_neurons (
435- neurons_to_delete
436- )
462+ try :
463+ not self .settings .dry_run and await self .database .remove_neurons (
464+ neurons_to_delete
465+ )
466+ except Exception as e :
467+ btul .logging .error (
468+ f"❌ Failed to remove deleted neurons from Redis: { e } " ,
469+ prefix = self .settings .logging_name ,
470+ )
471+ raise # Re-raise to trigger resync failure handling
437472
438473 # Clean up rate limit data for deleted neuron IPs
439474 deleted_ips = [
@@ -451,9 +486,16 @@ async def _resync(self, last_update: bool) -> dict[str, str]:
451486 f"🧠 Neurons updated: { [n .hotkey for n in updated_neurons ]} " ,
452487 prefix = self .settings .logging_name ,
453488 )
454- not self .settings .dry_run and await self .database .update_neurons (
455- updated_neurons
456- )
489+ try :
490+ not self .settings .dry_run and await self .database .update_neurons (
491+ updated_neurons
492+ )
493+ except Exception as e :
494+ btul .logging .error (
495+ f"❌ Failed to update neurons in Redis: { e } " ,
496+ prefix = self .settings .logging_name ,
497+ )
498+ raise # Re-raise to trigger resync failure handling
457499
458500 # Clean up rate limit data for old IPs when neurons changed IP
459501 if old_ips_to_cleanup :
@@ -465,11 +507,18 @@ async def _resync(self, last_update: bool) -> dict[str, str]:
465507
466508 if last_update is None or updated_neurons or neurons_to_delete :
467509 block = await self .subtensor .get_current_block ()
468- not self .settings .dry_run and await self .database .set_last_updated (block )
469- btul .logging .debug (
470- f"📅 Last updated block recorded: #{ block } " ,
471- prefix = self .settings .logging_name ,
472- )
510+ try :
511+ not self .settings .dry_run and await self .database .set_last_updated (block )
512+ btul .logging .debug (
513+ f"📅 Last updated block recorded: #{ block } " ,
514+ prefix = self .settings .logging_name ,
515+ )
516+ except Exception as e :
517+ btul .logging .error (
518+ f"❌ Failed to set last updated block in Redis: { e } " ,
519+ prefix = self .settings .logging_name ,
520+ )
521+ raise # Re-raise to trigger resync failure handling
473522
474523 else :
475524 btul .logging .info (
@@ -519,6 +568,7 @@ async def _notify_with_retry(self, ready) -> bool:
519568 """
520569 Notify with retry logic using adaptive delays based on actual Redis response times.
521570 Returns True if successful, False if failed after all retries.
571+ Marks metagraph as unready if Redis operations fail.
522572 """
523573 # If already ready, skip all checks
524574 if ready :
@@ -528,36 +578,50 @@ async def _notify_with_retry(self, ready) -> bool:
528578 base_delay = 0.01 # Start with 10ms base
529579
530580 for attempt in range (max_retries ):
531- # Measure how long the consistency check takes
532- start_time = asyncio .get_event_loop ().time ()
533- success = await self ._notify_if_needed (ready )
534- check_duration = asyncio .get_event_loop ().time () - start_time
581+ try :
582+ # Measure how long the consistency check takes
583+ start_time = asyncio .get_event_loop ().time ()
584+ success = await self ._notify_if_needed (ready )
585+ check_duration = asyncio .get_event_loop ().time () - start_time
535586
536- if success :
537- if attempt > 0 : # Only log if we actually retried
538- btul .logging .info (
539- f"✅ Data consistency verified on retry { attempt + 1 } (took { check_duration * 1000 :.1f} ms)" ,
587+ if success :
588+ if attempt > 0 : # Only log if we actually retried
589+ btul .logging .info (
590+ f"✅ Data consistency verified on retry { attempt + 1 } (took { check_duration * 1000 :.1f} ms)" ,
591+ prefix = self .settings .logging_name ,
592+ )
593+ return True
594+
595+ if attempt < max_retries - 1 : # Don't delay on last attempt
596+ # Adaptive delay: wait 2-5x the time it took for the check
597+ # This accounts for Redis load, network latency, etc.
598+ adaptive_delay = max (base_delay , check_duration * (2 + attempt * 1.5 ))
599+
600+ btul .logging .debug (
601+ f"🔄 Retry { attempt + 1 } /{ max_retries } : check took { check_duration * 1000 :.1f} ms, "
602+ f"waiting { adaptive_delay * 1000 :.1f} ms..." ,
540603 prefix = self .settings .logging_name ,
541604 )
542- return True
543-
544- if attempt < max_retries - 1 : # Don't delay on last attempt
545- # Adaptive delay: wait 2-5x the time it took for the check
546- # This accounts for Redis load, network latency, etc.
547- adaptive_delay = max (base_delay , check_duration * (2 + attempt * 1.5 ))
605+ await asyncio .sleep (adaptive_delay )
606+ else :
607+ btul .logging .error (
608+ f"❌ Failed to verify data consistency after { max_retries } attempts "
609+ f"(last check took { check_duration * 1000 :.1f} ms)" ,
610+ prefix = self .settings .logging_name ,
611+ )
612+ # Mark as unready due to consistency failure
613+ await self ._mark_unready_on_error ("Data consistency verification failed" )
548614
549- btul .logging .debug (
550- f"🔄 Retry { attempt + 1 } /{ max_retries } : check took { check_duration * 1000 :.1f} ms, "
551- f"waiting { adaptive_delay * 1000 :.1f} ms..." ,
552- prefix = self .settings .logging_name ,
553- )
554- await asyncio .sleep (adaptive_delay )
555- else :
615+ except Exception as e :
556616 btul .logging .error (
557- f"❌ Failed to verify data consistency after { max_retries } attempts "
558- f"(last check took { check_duration * 1000 :.1f} ms)" ,
617+ f"❌ Redis operation failed during notify retry (attempt { attempt + 1 } ): { e } " ,
559618 prefix = self .settings .logging_name ,
560619 )
620+ # Mark as unready due to Redis failure
621+ await self ._mark_unready_on_error (f"Redis operation failed: { e } " )
622+
623+ if attempt < max_retries - 1 :
624+ await asyncio .sleep (base_delay * (attempt + 1 ))
561625
562626 return False
563627
@@ -780,3 +844,23 @@ def _get_country_for_ip(self, ip: str) -> str:
780844
781845 # Fall back to API lookup
782846 return sccc .get_country (ip )
847+
848+ async def _mark_unready_on_error (self , error_reason : str ):
849+ """
850+ Mark metagraph as unready when errors occur and notify listeners.
851+ This ensures downstream services know the metagraph is in a problematic state.
852+ """
853+ try :
854+ if not self .settings .dry_run :
855+ await self .database .mark_as_unready ()
856+ await self .database .notify_state ()
857+ btul .logging .warning (
858+ f"🚫 Metagraph marked as unready due to: { error_reason } " ,
859+ prefix = self .settings .logging_name ,
860+ )
861+ except Exception as notify_error :
862+ # Even if we can't mark as unready (e.g., Redis down), log the attempt
863+ btul .logging .error (
864+ f"❌ Failed to mark metagraph as unready (reason: { error_reason } ): { notify_error } " ,
865+ prefix = self .settings .logging_name ,
866+ )
0 commit comments