blob: c61607add55b6312c64acd970f297df5efffae73 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test that a pending job array drops JobArrayTaskLimit once its limit has room.
Ticket 25946: after the running task of an array throttled to one task ended,
the remaining tasks could show Reason=JobArrayTaskLimit for as long as they
kept waiting. For a job it skips behind a higher priority job, the main
scheduler only replaces a reason of None or Resources, and backfill does not
set reasons, so nothing replaced the stale one.
Here the array's running task is cancelled while a higher priority job waits
for the whole partition. The next task then waits behind that job, and has to
show Reason=Priority.
"""
import pytest
import atf
_STALE_REASON_FIXED = atf.get_version("sbin/slurmctld") >= (26, 11)
_STALE_REASON = (
"Ticket 25946: pending array tasks drop JobArrayTaskLimit once the limit "
"has room in 26.11"
)
# The filler's time limit is shorter than the array tasks', so backfill can't
# start the next task without delaying the higher priority job.
FILLER_MINUTES = 10
TASK_MINUTES = 30
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_nodes(1, [("CPUs", 2)])
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CORE")
# Below this priority, neither scheduler holds resources for the higher
# priority job, so the next task would start instead of waiting.
atf.require_config_parameter_includes(
"SchedulerParameters", ("bf_min_prio_reserve", 0)
)
atf.require_slurm_running()
def pending_reason(array_id):
"""Returns the reason of the array's pending tasks."""
return atf.run_command_output(
f"squeue -h -j {array_id} -t PD -o %r", fatal=True
).strip()
def running_task(array_id):
"""Returns the job ID of the array's running task, if any."""
return atf.run_command_output(
f"squeue -h -j {array_id} -t R -o %A", fatal=True
).strip()
@pytest.mark.xfail(not _STALE_REASON_FIXED, reason=_STALE_REASON)
def test_reason_after_limit_has_room():
"""Test that the next task waits for Priority, not the array task limit."""
partition = atf.default_partition()
node_count = sum(
int(count)
for count in atf.run_command_output(
f"sinfo -h -p {partition} -o %D", fatal=True
).split()
)
filler_id = atf.submit_job_sbatch(
f"-p {partition} -n1 -t {FILLER_MINUTES} -o /dev/null "
"--wrap 'sleep infinity'",
fatal=True,
)
atf.wait_for_job_state(filler_id, "RUNNING", fatal=True)
array_id = atf.submit_job_sbatch(
f"-p {partition} -n1 -t {TASK_MINUTES} --array=1-2%1 -o /dev/null "
"--wrap 'sleep infinity'",
fatal=True,
)
for _ in atf.timer(fatal=True):
task_id = running_task(array_id)
if task_id:
break
for _ in atf.timer(fatal=True):
if pending_reason(array_id) == "JobArrayTaskLimit":
break
# Needs every node, so it waits for the filler and fails the partition
# for the jobs below it.
blocker_id = atf.submit_job_sbatch(
f"-p {partition} -N {node_count} --exclusive -t 1 -o /dev/null "
"--wrap 'sleep 1'",
fatal=True,
)
array_priority = int(atf.get_job_parameter(array_id, "Priority", fatal=True))
atf.run_command(
f"scontrol update JobId={blocker_id} Priority={array_priority + 1000}",
user=atf.properties["slurm-user"],
fatal=True,
)
atf.wait_for_job_state(blocker_id, "PENDING", "Resources", fatal=True)
atf.run_command(f"scancel {task_id}", fatal=True)
atf.wait_for_job_state(int(task_id), "CANCELLED", fatal=True)
reason = None
for _ in atf.timer(timeout=90):
reason = pending_reason(array_id)
if reason == "Priority":
break
assert (
running_task(array_id) == ""
), "The next task should still wait behind the higher priority job"
assert reason == "Priority", (
"The next task should wait for Priority once the limit has room, "
f"but shows {reason!r}"
)