Skip to content

Commit 509282d

Browse files
andybubuWouter de GeusDuncan Lyall
committed
fix(slurm-controller): add HA single-active guard and defer scontrol reconfigure
co-authored-by: Wouter de Geus <wouter.x.degeus@gsk.com> co-authored-by: Duncan Lyall <duncan.x.lyall@gsk.com>
1 parent 3d418d9 commit 509282d

3 files changed

Lines changed: 140 additions & 18 deletions

File tree

community/modules/scheduler/schedmd-slurm-gcp-v6-controller/modules/slurm_files/scripts/setup.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,26 @@ def self_report_controller_address(lkp: util.Lookup) -> None:
459459
with blob.open('w') as f:
460460
f.write(yaml.dump(data))
461461

462+
def check_slurmdbd_ready(maxtries: int = 30, interval: float = 15.0) -> None:
463+
"""Poll sacctmgr until slurmdbd is ready, with timeout safety and backoff."""
464+
sacctmgr = slurmdirs.prefix / "bin" / "sacctmgr"
465+
wait = 10.0
466+
for try_cnt in range(1, maxtries + 1):
467+
try:
468+
res = run(f"{sacctmgr} -n -i show cluster", check=False, timeout=10)
469+
if res.returncode == 0:
470+
log.info(f"slurmdbd is ready (try {try_cnt}).")
471+
return
472+
except Exception as e:
473+
log.debug(f"slurmdbd check error: {e}")
474+
475+
log.info(f"Waiting for slurmdbd to be ready... (try {try_cnt}/{maxtries}, retrying in {int(wait)}s)")
476+
time.sleep(wait)
477+
wait = min(wait * 1.5, interval)
478+
479+
log.error(f"Slurmdbd is still not ready after {maxtries} tries. Expecting issues!")
480+
raise Exception("Slurmdbd is not ready")
481+
462482
def setup_slurm_health_check_service(lkp: util.Lookup) -> None:
463483
if not lkp.cfg.get("slurm_backup_controller_name") or not lkp.cfg.get("enable_controller_load_balancer"):
464484
return
@@ -546,8 +566,9 @@ def setup_controller(is_primary: bool):
546566
run("systemctl enable slurmdbd", timeout=30)
547567
run("systemctl restart slurmdbd", timeout=30)
548568

549-
# Wait for slurmdbd to come up
550-
time.sleep(5)
569+
# Wait for slurmdbd to come up.
570+
# This can take a while in case of database migration after upgrades (up to an hour).
571+
check_slurmdbd_ready(maxtries=30, interval=240)
551572

552573
sacctmgr = f"{slurmdirs.prefix}/bin/sacctmgr -i"
553574
result = run(

community/modules/scheduler/schedmd-slurm-gcp-v6-controller/modules/slurm_files/scripts/slurmsync.py

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -451,32 +451,40 @@ def sync_instances():
451451
action.apply(list(nodes))
452452

453453

454-
def reconfigure_slurm():
454+
def reconfigure_slurm() -> bool:
455+
"""Fetch new config and regenerate Slurm config files.
456+
457+
Returns True if config was updated and a scontrol reconfigure is needed.
458+
On the controller, the reconfigure is deferred until after topology
459+
regeneration to avoid issuing two sequential scontrol reconfigure calls
460+
in the same sync cycle (which can cause a socket timeout).
461+
"""
455462
update_msg = "*** slurm configuration was updated ***"
456463

457464
if lookup().cfg.hybrid:
458465
# terraform handles generating the config.yaml, don't do it here
459-
return
466+
return False
460467

461468
upd, cfg_new = util.fetch_config()
462469
if not upd:
463470
log.debug("No changes in config detected.")
464-
return
471+
return False
465472
log.debug("Changes in config detected. Reconfiguring Slurm now.")
466473
util.update_config(cfg_new)
467474

468475
if lookup().is_controller:
469476
conf.get_generator(lookup()).generate_configs()
470477

471-
log.info("Restarting slurmctld to make changes take effect.")
478+
# Defer slurmctld restart and scontrol reconfigure:
479+
# Running them now would cause slurmctld to temporarily load new node defs with old topology.
480+
# This causes slurmctld to hang/timeout when update_topology tries to reconfigure it subsequently.
481+
log.info("Config files regenerated. Deferring scontrol reconfigure until after topology update.")
472482
try:
473-
# TODO: consider removing "restart" since "reconfigure" should restart slurmctld as well
474-
run("sudo systemctl restart slurmctld.service", check=False)
475-
util.scontrol_reconfigure(lookup())
483+
util.run(f"wall '{update_msg}'", timeout=30)
476484
except Exception:
477-
log.exception("failed to reconfigure slurmctld")
478-
util.run(f"wall '{update_msg}'", timeout=30)
485+
log.warning("failed to broadcast wall message")
479486
log.debug("Done.")
487+
return True
480488
elif lookup().instance_role_safe == "compute":
481489
log.info("Restarting slurmd to make changes take effect.")
482490
run("systemctl restart slurmd")
@@ -487,19 +495,23 @@ def reconfigure_slurm():
487495
run("systemctl restart sackd")
488496
util.run(f"wall '{update_msg}'", timeout=30)
489497
log.debug("Done.")
498+
return False
490499

491500

492501
def _generate_topology(lkp: util.Lookup) -> Tuple[bool, Any]:
493502
return conf.get_generator(lkp).generate_topology_data()
494503

495-
def update_topology(lkp: util.Lookup) -> None:
504+
def update_topology(lkp: util.Lookup) -> Tuple[bool, Any]:
505+
"""Regenerate topology files.
506+
507+
Returns (updated, summary) so the caller can decide when to
508+
issue the scontrol reconfigure and persist the summary.
509+
"""
496510
updated, summary = _generate_topology(lkp) # type: ignore[attr-defined]
497511

498512
if updated:
499-
log.info("Topology configuration updated. Reconfiguring Slurm.")
500-
util.scontrol_reconfigure(lkp)
501-
# Safe summary only after Slurm got reconfigured, so summary reflects Slurm POV
502-
summary.dump(lkp)
513+
log.info("Topology configuration updated.")
514+
return updated, summary
503515

504516

505517
def delete_reservation(lkp: util.Lookup, reservation_name: str) -> None:
@@ -662,11 +674,19 @@ def main():
662674
lkp = lookup()
663675
if util.should_mount_slurm_bucket() and not lkp.is_controller:
664676
return
677+
678+
# Track if reconfigure_slurm changed cloud.conf so we can trigger
679+
# a deferred scontrol reconfigure inside update_topology
680+
config_changed = False
665681
try:
666-
reconfigure_slurm()
682+
config_changed = reconfigure_slurm()
667683
except Exception:
668684
log.exception("failed to reconfigure slurm")
669685
if lkp.is_controller:
686+
if not util.is_active_controller(lkp):
687+
log.info("Local controller is in STANDBY mode. Skipping instance sync, repair, and reconfigure.")
688+
return
689+
670690
try:
671691
process_messages(lkp)
672692
except:
@@ -692,11 +712,30 @@ def main():
692712
except Exception:
693713
log.exception("failed to sync placement groups")
694714

715+
topology_changed = False
716+
topology_summary = None
695717
try:
696-
update_topology(lkp)
718+
topology_changed, topology_summary = update_topology(lkp)
697719
except Exception:
698720
log.exception("failed to update topology")
699721

722+
# Single deferred reconfigure for both config and topology changes.
723+
# Placed here so it runs even if topology generation failed above.
724+
if config_changed or topology_changed:
725+
reasons = []
726+
if config_changed:
727+
reasons.append("config updated")
728+
if topology_changed:
729+
reasons.append("topology changed")
730+
try:
731+
log.info(f"Reconfiguring Slurm ({', '.join(reasons)}).")
732+
util.scontrol_reconfigure(lkp)
733+
# Only dump summary after successful reconfigure so it reflects Slurm's view
734+
if topology_changed and topology_summary is not None:
735+
topology_summary.dump(lkp)
736+
except Exception:
737+
log.exception("failed to reconfigure slurmctld")
738+
700739
try:
701740
sync_maintenance_reservation(lkp)
702741
except Exception:

community/modules/scheduler/schedmd-slurm-gcp-v6-controller/modules/slurm_files/scripts/util.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2381,12 +2381,74 @@ def update_config(cfg: NSDict) -> None:
23812381
global _lkp
23822382
_lkp = Lookup(cfg)
23832383

2384+
def wait_slurmctld_up(lkp: Lookup, timeout: float = 60) -> None:
2385+
"""Wait for slurmctld to respond to scontrol ping and report UP status.
2386+
This ensure scontrol reconfigure does not fail due to slurmctld not being ready
2387+
after a service restart.
2388+
"""
2389+
log.info("Waiting for slurmctld to be fully up...")
2390+
for wait in backoff_delay(0.5, timeout=timeout):
2391+
try:
2392+
res = run(f"{lkp.scontrol} ping", check=False, timeout=5)
2393+
output = (res.stdout + res.stderr).lower()
2394+
if res.returncode == 0 and any(
2395+
"up" in line for line in output.splitlines() if "down" not in line
2396+
):
2397+
log.info("slurmctld is fully up.")
2398+
return
2399+
except Exception as e:
2400+
log.debug(f"scontrol ping check failed: {e}")
2401+
if wait > 0:
2402+
sleep(wait)
2403+
raise TimeoutError(f"slurmctld is not fully up after {timeout} seconds")
2404+
23842405
def scontrol_reconfigure(lkp: Lookup) -> None:
23852406
log.info("Running systemctl restart slurmctld.service")
23862407
run("sudo systemctl restart slurmctld.service", timeout=30)
2408+
wait_slurmctld_up(lkp)
23872409
log.info("Running scontrol reconfigure")
23882410
run(f"{lkp.scontrol} reconfigure")
23892411

2412+
def is_active_controller(lkp: Lookup) -> bool:
2413+
"""Returns True if the local node is the currently active Slurm controller.
2414+
In non-HA setups, always returns True for the controller.
2415+
In HA setups, queries scontrol ping to check if local node is active primary or active takeover backup.
2416+
"""
2417+
if not lkp.is_controller:
2418+
return False
2419+
2420+
backup_name = lkp.cfg.get("slurm_backup_controller_name")
2421+
if not backup_name:
2422+
return True
2423+
2424+
hostname = socket.gethostname().split(".")[0]
2425+
is_backup_instance = hostname.endswith("-1")
2426+
2427+
try:
2428+
res = run(f"{lkp.scontrol} ping", check=False, timeout=5)
2429+
output = (res.stdout + res.stderr).lower()
2430+
2431+
primary_up = any(
2432+
"primary controller: up" in line
2433+
or "primary controller(up)" in line
2434+
or ("slurmctld(primary) at" in line and "is up" in line)
2435+
for line in output.splitlines()
2436+
)
2437+
backup_up = any(
2438+
"backup controller: up" in line
2439+
or "backup controller(up)" in line
2440+
or ("slurmctld(backup) at" in line and "is up" in line)
2441+
for line in output.splitlines()
2442+
)
2443+
2444+
if not is_backup_instance:
2445+
return primary_up
2446+
else:
2447+
return (not primary_up) and backup_up
2448+
except Exception as e:
2449+
log.warning(f"Failed to query scontrol ping for HA active check: {e}")
2450+
return not is_backup_instance
2451+
23902452
def slurm_version_gte(v1: str, v2: str) -> bool:
23912453
"""Returns true if v1 >= v2, expects YY.MM format"""
23922454
try:

0 commit comments

Comments
 (0)