blob: 64797802b42c7ad8183294d227149578cdbe0ccc [file]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Stepmgr/async-step integration tests.
These tests pin operational properties of async steps that go beyond the
srun CLI surface (covered in test_116_55.py): scontrol visibility of the
running step, and stepmgr state persistence across `scontrol reconfigure`.
"""
import re
import pytest
import atf
PARSABLE_RE = re.compile(r"^(\d+)\.(\d+)$", re.MULTILINE)
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 5),
"bin/srun",
reason="Issue 50739: srun --async was added in 26.05",
)
atf.require_config_parameter_includes("SlurmctldParameters", "enable_stepmgr")
atf.require_config_parameter_includes("PrologFlags", "Contain")
atf.require_accounting()
atf.require_nodes(1, [("CPUs", 2)])
atf.require_slurm_running()
def test_async_step_state_visible_in_scontrol():
"""A running async step is reported by `scontrol show steps` with State=RUNNING.
Confirms stepmgr maintains the step record after srun has exited and
that the basic JobId/StepId fields match the values srun reported.
"""
out_file = "state.out"
job_id = atf.submit_job_sbatch(
f"-N1 -n1 -t1 --wrap 'srun --async -o {out_file} sleep 30; sleep 30'",
fatal=True,
)
step_key = f"{job_id}.0"
state = None
for _t in atf.timer(quiet=True):
steps = atf.get_steps(job_id, quiet=True)
if step_key in steps:
state = steps[step_key].get("State")
if state == "RUNNING":
break
else:
pytest.fail(
f"Async step {step_key} did not reach RUNNING in scontrol; "
f"last observed State={state!r}"
)
step = steps[step_key]
assert step["StepId"] == step_key, f"Step StepId should be {step_key}: {step!r}"
atf.run_command(f"scancel {job_id}", quiet=True)
atf.wait_for_job_state(job_id, "DONE")
def test_async_step_survives_reconfigure():
"""A queued async step still launches after `scontrol reconfigure`.
Stepmgr state (the pending async step held in the job's step list)
must be preserved across a controller reconfigure.
"""
out1 = "rcfg1.out"
out2 = "rcfg2.out"
marker = "rcfg_marker"
script = "rcfg_submit.sh"
release = "rcfg_release"
# Step 1 holds the exclusive resource until the release file is written,
# giving deterministic control over when step 2 can promote from PENDING.
atf.make_bash_script(
script,
f"""srun --async --parsable --exclusive -o {out1} \\
bash -c 'until [ -f {release} ]; do sleep 0.2; done' > {marker} 2>&1
srun --async --parsable --exclusive -o {out2} bash -c 'echo second-ran' >> {marker} 2>&1
swait
""",
)
job_id = atf.submit_job_sbatch(f"-N1 -n1 -t2 {script}", fatal=True)
atf.wait_for_file(marker, fatal=True)
text = ""
for _t in atf.timer():
text = atf.run_command_output(f"cat {marker}", quiet=True)
if len(PARSABLE_RE.findall(text)) >= 2:
break
else:
pytest.fail(f"Expected two parsable step-id lines, got: {text!r}")
matches = PARSABLE_RE.findall(text)
second_step = int(matches[1][1])
second_key = f"{job_id}.{second_step}"
saw_pending = False
# Poll fast: the queued step may be PENDING only briefly.
for _t in atf.timer(poll_interval=0.5, quiet=True):
steps = atf.get_steps(job_id, quiet=True)
if steps.get(second_key, {}).get("State") == "PENDING":
saw_pending = True
break
assert saw_pending, (
f"Second async step {second_key} did not reach PENDING before "
f"reconfigure; cannot exercise the survives-reconfigure path"
)
atf.run_command(
"scontrol reconfigure",
user=atf.properties["slurm-user"],
fatal=True,
)
steps_after = atf.get_steps(job_id, quiet=True)
state_after = steps_after.get(second_key, {}).get("State")
assert state_after == "PENDING", (
f"Second async step {second_key} should still be PENDING "
f"immediately after scontrol reconfigure; got State={state_after!r}"
)
atf.run_command(f"touch {release}", fatal=True)
assert atf.wait_for_step_accounted(
job_id, second_step, fatal=True
), f"async step {job_id}.{second_step} should be recorded in accounting"
atf.wait_for_job_state(job_id, "DONE", fatal=True)
atf.wait_for_file(out2, fatal=True)
out2_text = atf.run_command_output(f"cat {out2}", fatal=True)
assert (
"second-ran" in out2_text
), f"Queued async step should run after reconfigure; got {out2_text!r}"