blob: adb1b4a4bf636481108d0eefeb7113c993980aee [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test updating the MODE_3 HRES request of a pending job.
Updating a pending job's licenses must keep the job's MODE_3 HRES check, so
the job must not run while its HRES layer has no capacity left.
"""
import pytest
import atf
# Only leaf1 has capacity, so a job requesting power:2000 fills it and a
# second job must pend until the first ends.
resources_yaml = """
- resource: power
mode: MODE_3
layers:
- layer_name: "root"
count: 100000
- layer_name: "leaf1"
parent_name: "root"
nodes:
- "node[1-3]"
count: 2000
"""
leaf_total = 2000
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 11),
component="bin/scontrol",
reason="Issue #50445: the _update_job() hres_select_t fix is 26.11 only, "
"and LayerName was added to scontrol show license output in 26.11.",
)
atf.require_nodes(3)
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CPU")
atf.require_config_parameter_includes("SchedulerParameters", ("bf_interval", 1))
atf.require_config_parameter_includes("SchedulerParameters", ("sched_interval", 1))
atf.require_config_file("resources.yaml", resources_yaml)
atf.require_slurm_running()
def get_leaf1_usage():
"""Return (total, used) for the leaf1 layer of the power HRES."""
lic = atf.get_licenses(fatal=True)["power(leaf1)"]
return lic["Total"], lic["Used"]
def fill_leaf1():
"""Start one job that uses the whole capacity of leaf1."""
job_id = atf.submit_job_sbatch(
f"-N1 --resources=power:{leaf_total} --mem=1 --wrap='sleep infinity'",
fatal=True,
)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
assert get_leaf1_usage() == (
leaf_total,
leaf_total,
), "The running job should use all of leaf1"
return job_id
def assert_stays_pending(job_id):
# With bf_interval=1 and sched_interval=1, a job that is not blocked by
# the HRES check starts within a few seconds.
assert not atf.wait_for_job_state(
job_id, "RUNNING", timeout=5, xfail=True
), f"Job {job_id} should not run while leaf1 has no capacity left"
# Not RUNNING is not the same as still pending: a job that failed or was
# cancelled also never reaches RUNNING, and would satisfy the wait above.
assert (
atf.get_job_parameter(job_id, "JobState") == "PENDING"
), f"Job {job_id} should still be PENDING"
total, used = get_leaf1_usage()
assert used <= total, f"leaf1 usage ({used}) should not exceed its total ({total})"
def test_update_pending_job():
"""Updating a pending job's HRES request must keep the HRES check"""
running_job_id = fill_leaf1()
job_id = atf.submit_job_sbatch(
f"-N1 --resources=power:{leaf_total} --mem=1 --wrap='sleep infinity'",
fatal=True,
)
assert_stays_pending(job_id)
update_total = leaf_total - 1
atf.run_command(
f"scontrol update jobid={job_id} licenses=power:{update_total}",
fatal=True,
)
assert_stays_pending(job_id)
assert get_leaf1_usage() == (
leaf_total,
leaf_total,
), "Updating the pending job should not change leaf1 usage"
assert (
atf.get_job_parameter(job_id, "Licenses") == f"power:{update_total}"
), "Licenses should reflect the update"
atf.cancel_jobs([running_job_id], fatal=True)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
assert get_leaf1_usage() == (
leaf_total,
update_total,
), f"The updated job should use power:{update_total} on leaf1"