@@ -151,6 +151,7 @@ class ClustermgtdConfig:
151151 "terminate_down_nodes" : True ,
152152 "orphaned_instance_timeout" : 300 ,
153153 "ec2_instance_missing_max_count" : 0 ,
154+ "compute_node_missing_ip_max_count" : 10 ,
154155 # Health check configs
155156 "disable_ec2_health_check" : False ,
156157 "disable_scheduled_event_health_check" : False ,
@@ -310,6 +311,11 @@ def _get_terminate_config(self, config):
310311 "ec2_instance_missing_max_count" ,
311312 fallback = self .DEFAULTS .get ("ec2_instance_missing_max_count" ),
312313 )
314+ self .compute_node_missing_ip_max_count = config .getint (
315+ "clustermgtd" ,
316+ "compute_node_missing_ip_max_count" ,
317+ fallback = self .DEFAULTS .get ("compute_node_missing_ip_max_count" ),
318+ )
313319 self .disable_nodes_on_insufficient_capacity = self .insufficient_capacity_timeout > 0
314320
315321 def _get_dns_config (self , config ):
@@ -391,6 +397,7 @@ def __init__(self, config):
391397 self ._static_nodes_in_replacement = set ()
392398 self ._partitions_protected_failure_count_map = {}
393399 self ._nodes_without_backing_instance_count_map = {}
400+ self ._fallback_match_count_map = {}
394401 self ._compute_fleet_status = ComputeFleetStatus .RUNNING
395402 self ._current_time = None
396403 self ._config = None
@@ -551,7 +558,13 @@ def manage_cluster(self):
551558 return
552559 log .debug ("Current cluster instances in EC2: %s" , cluster_instances )
553560 partitions = list (partitions_name_map .values ())
554- self ._update_slurm_nodes_with_ec2_info (nodes , cluster_instances )
561+ self ._update_slurm_nodes_with_ec2_info (
562+ nodes ,
563+ cluster_instances ,
564+ self ._instance_manager ,
565+ self ._fallback_match_count_map ,
566+ self ._config .compute_node_missing_ip_max_count ,
567+ )
555568 self ._event_publisher .publish_compute_node_events (nodes , cluster_instances )
556569 # Handle inactive partition and terminate backing instances
557570 self ._clean_up_inactive_partition (partitions )
@@ -1144,16 +1157,90 @@ def _parse_scheduler_nodes_data(nodes):
11441157 raise
11451158
11461159 @staticmethod
1147- def _update_slurm_nodes_with_ec2_info (nodes , cluster_instances ):
1148- if cluster_instances :
1149- ip_to_slurm_node_map = {node .nodeaddr : node for node in nodes }
1150- for instance in cluster_instances :
1151- for private_ip in instance .all_private_ips :
1152- if private_ip in ip_to_slurm_node_map :
1153- slurm_node = ip_to_slurm_node_map .get (private_ip )
1154- slurm_node .instance = instance
1155- instance .slurm_node = slurm_node
1156- break
1160+ def _update_slurm_nodes_with_ec2_info ( # noqa: C901
1161+ nodes ,
1162+ cluster_instances ,
1163+ instance_manager = None ,
1164+ fallback_match_count_map = None ,
1165+ compute_node_missing_ip_max_count = 10 ,
1166+ ):
1167+ """
1168+ Associate EC2 instances with Slurm nodes.
1169+
1170+ Primary matching is by IP address. If instances have empty IPs (due to EC2 eventual consistency),
1171+ a fallback matching by instance ID is attempted using the DynamoDB node-name-to-instance-id mapping.
1172+
1173+ Args:
1174+ fallback_match_count_map: dict tracking consecutive fallback match counts per node name.
1175+ If a node is matched via fallback for more than compute_node_missing_ip_max_count
1176+ consecutive iterations, the association is removed so that existing health check
1177+ mechanisms can handle it.
1178+ compute_node_missing_ip_max_count: max consecutive iterations to tolerate missing IP
1179+ before dissociating the instance. Configurable via clustermgtd conf file.
1180+ """
1181+ if not cluster_instances :
1182+ return
1183+
1184+ # First pass: match by IP address (primary method)
1185+ ip_to_slurm_node_map = {node .nodeaddr : node for node in nodes }
1186+ unmatched_instances = []
1187+ for instance in cluster_instances :
1188+ matched = False
1189+ for private_ip in instance .all_private_ips :
1190+ if private_ip in ip_to_slurm_node_map :
1191+ slurm_node = ip_to_slurm_node_map .get (private_ip )
1192+ slurm_node .instance = instance
1193+ instance .slurm_node = slurm_node
1194+ matched = True
1195+ break
1196+ if not matched and not instance .all_private_ips :
1197+ unmatched_instances .append (instance )
1198+
1199+ # Second pass: fallback matching by instance ID for instances with missing IPs
1200+ if unmatched_instances and instance_manager :
1201+ log .info (
1202+ "Found %d instances with missing IPs, attempting fallback matching by instance ID" ,
1203+ len (unmatched_instances ),
1204+ )
1205+ try :
1206+ instance_id_to_node_name = instance_manager .get_instance_id_to_node_name_mapping ()
1207+ except Exception as e :
1208+ log .warning ("Failed to read instance-ID-to-node-name mapping for fallback matching: %s" , e )
1209+ instance_id_to_node_name = {}
1210+
1211+ if instance_id_to_node_name :
1212+ name_to_slurm_node_map = {node .name : node for node in nodes }
1213+ for instance in unmatched_instances :
1214+ node_name = instance_id_to_node_name .get (instance .id )
1215+ if node_name and node_name in name_to_slurm_node_map :
1216+ slurm_node = name_to_slurm_node_map [node_name ]
1217+ if not slurm_node .instance :
1218+ # Track consecutive fallback match count
1219+ if fallback_match_count_map is not None :
1220+ count = fallback_match_count_map .get (node_name , 0 ) + 1
1221+ fallback_match_count_map [node_name ] = count
1222+ if count > compute_node_missing_ip_max_count :
1223+ log .warning (
1224+ "Instance %s matched to node %s via fallback for %d consecutive iterations "
1225+ "(IP never appeared). Dissociating to let health checks handle it." ,
1226+ instance .id ,
1227+ node_name ,
1228+ count ,
1229+ )
1230+ continue
1231+ log .info (
1232+ "Matched instance %s to node %s via instance ID fallback (IP not yet available)" ,
1233+ instance .id ,
1234+ node_name ,
1235+ )
1236+ slurm_node .instance = instance
1237+ instance .slurm_node = slurm_node
1238+
1239+ # Clean up fallback counts for nodes no longer needing fallback matching
1240+ # (either IP appeared and they matched via primary path, or the instance no longer exists)
1241+ if fallback_match_count_map is not None and not unmatched_instances :
1242+ # No unmatched instances this iteration — all previously tracked nodes have resolved
1243+ fallback_match_count_map .clear ()
11571244
11581245 @staticmethod
11591246 def get_instance_id_to_active_node_map (partitions : List [SlurmPartition ]) -> Dict :
0 commit comments