blob: 2f42b61186dad6673b776dd7b47d822d8ce6e75c [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test the DBD agent queue drains after a stranded requeue of a resized job.
MR 4087: a resize splits off an accounting record keyed on the resize time.
A requeue whose steps cannot be reaped never finishes, and the job_start the
next accounting re-registration sends for it collided with the existing
records. slurmdbd rejected it with a duplicate key and slurmctld retried it
forever, so sdiag's DBD Agent queue size never went back to zero.
"""
import re
import pytest
import atf
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 5, 5),
"sbin/slurmctld",
reason="MR 4087: a requeue stranded after a resize wedged the DBD agent queue",
)
atf.require_accounting()
atf.require_nodes(2)
atf.require_slurm_running()
@pytest.fixture
def kill_slurmd():
"""Kill a node's slurmd under a job, restarting it at teardown.
Teardown also waits for the job's steps to finish on the restarted node.
"""
killed = []
def kill(node, job_id):
killed.append((node, job_id))
assert atf.slurmd_pids(node), f"slurmd for {node} should be running"
assert atf.stop_slurmd(
node, signal="KILL"
), f"slurmd for {node} should be gone after SIGKILL"
yield kill
for node, job_id in killed:
atf.start_slurmd(node, quiet=True)
atf.run_command(
f"scontrol update nodename={node} state=resume",
user=atf.properties["slurm-user"],
fatal=True,
)
atf.wait_for_node_state(node, "IDLE", fatal=True)
# The stranded step reports its completion only once its node's
# slurmd is back, and it can't do that after slurmctld is stopped at
# module teardown.
atf.cancel_jobs([job_id], fatal=True)
for t in atf.timer(fatal=True):
if atf.run_command_exit(
f"pgrep -f '[s]lurmstepd: \\[{job_id}\\.'", quiet=True
):
break
def dbd_agent_queue_size():
"""Return slurmctld's count of messages still queued for slurmdbd."""
output = atf.run_command_output("sdiag", fatal=True)
match = re.search(r"DBD Agent queue size:\s*(\d+)", output)
assert match, f"sdiag reported no DBD agent queue size:\n{output}"
return int(match.group(1))
def wait_for_dbd_agent_drained():
"""Wait until slurmctld has nothing left queued for slurmdbd.
A message slurmdbd rejects stays queued and is retried, so two empty
readings in a row are required rather than a momentarily empty queue.
"""
drained_polls = 0
for t in atf.timer(fatal=True):
drained_polls = drained_polls + 1 if dbd_agent_queue_size() == 0 else 0
if drained_polls == 2:
break
def test_stranded_requeue_after_resize(kill_slurmd):
"""Verify a requeue stranded after a resize doesn't wedge the DBD agent queue."""
# The batch script outlives its step so the job survives the node loss.
atf.make_bash_script(
"job.sh", "srun --no-kill -N2 -n2 sleep infinity &\nsleep infinity"
)
job_id = atf.submit_job_sbatch("-N2 --no-kill --requeue job.sh", fatal=True)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
atf.wait_for_step(job_id, 0, fatal=True)
nodes = atf.run_command_output(
f"scontrol show hostnames $(squeue -h -j {job_id} -o %N)", fatal=True
).split()
batch_host = atf.get_job_parameter(job_id, "BatchHost", fatal=True)
assert (
len(nodes) == 2 and batch_host in nodes
), f"Job {job_id} should run on 2 nodes including its batch host {batch_host}, got {nodes}"
dead_node = next(node for node in nodes if node != batch_host)
# The step's task on a node without a slurmd can't be reaped, so the
# requeue below never finishes. A --no-kill job is only shrunk off a node
# that is explicitly DOWN, not one that is merely not responding.
kill_slurmd(dead_node, job_id)
atf.run_command(
f"scontrol update nodename={dead_node} state=down reason=test_173_1",
user=atf.properties["slurm-user"],
fatal=True,
)
atf.wait_for_node_state(dead_node, "DOWN", fatal=True)
for t in atf.timer(fatal=True):
if atf.get_job_parameter(job_id, "NumNodes", fatal=True) == 1:
break
atf.run_command(
f"scontrol requeue {job_id}",
user=atf.properties["slurm-user"],
fatal=True,
)
atf.wait_for_job_state(job_id, "COMPLETING", fatal=True)
# A reconfigure re-registers the stranded job with accounting.
atf.run_command(
"scontrol reconfigure", user=atf.properties["slurm-user"], fatal=True
)
wait_for_dbd_agent_drained()