| ############################################################################ | |
| # Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| ############################################################################ | |
| """Ticket 24934. | |
| With Flags=IGNORE_JOBS a NodeCnt= reservation over a PartitionName may be | |
| created even when every node in the partition is busy; without IGNORE_JOBS | |
| the same request is denied. Uses a dedicated partition where a running job | |
| occupies all partition nodes so placement relies on IGNORE_JOBS rather than | |
| idle nodes only. The empty Features= field is the reproduction trigger from | |
| bug 3214, not a documented requirement. | |
| The regression exists only on 25.11.0 through 25.11.4 (before the fix in | |
| 25.11.5). Slurm 25.05 passes without the fix because _resv_select() still | |
| uses the older min_nodes WILL_RUN path; do not xfail releases before 25.11. | |
| """ | |
| import os | |
| import pytest | |
| import atf | |
| test_name = os.path.splitext(os.path.basename(__file__))[0] | |
| partition = f"{test_name}_part" | |
| testuser = atf.properties["test-user"] | |
| # Setup | |
| @pytest.fixture(scope="module", autouse=True) | |
| def setup(): | |
| atf.require_nodes(4, [("CPUs", 2), ("RealMemory", 50)]) | |
| atf.require_slurm_running() | |
| @pytest.fixture(scope="module") | |
| def part_nodes(setup): | |
| node_names = sorted(n for n in atf.get_nodes().keys() if n != "DEFAULT") | |
| four_nodes = node_names[:4] | |
| nodelist = ",".join(four_nodes) | |
| atf.run_command( | |
| f"scontrol create PartitionName={partition} Nodes={nodelist} " | |
| f"State=UP Default=NO", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| yield four_nodes | |
| atf.run_command( | |
| f"scontrol delete PartitionName={partition}", | |
| user=atf.properties["slurm-user"], | |
| quiet=True, | |
| ) | |
| @pytest.fixture(scope="function") | |
| def reservation(): | |
| """Delete any reservation registered during the test at teardown.""" | |
| created = [] | |
| yield created | |
| for name in created: | |
| atf.run_command( | |
| f"scontrol delete ReservationName={name}", | |
| user=atf.properties["slurm-user"], | |
| quiet=True, | |
| ) | |
| @pytest.fixture(scope="function") | |
| def busy_partition(part_nodes): | |
| """Occupy every node in the test partition so placement needs IGNORE_JOBS.""" | |
| job_id = atf.submit_job_sbatch( | |
| f"-p {partition} -N4 --wrap='sleep infinity'", | |
| fatal=True, | |
| ) | |
| atf.wait_for_job_state(job_id, "RUNNING", fatal=True) | |
| return part_nodes | |
| @pytest.mark.xfail( | |
| (25, 11) <= atf.get_version() < (25, 11, 5), | |
| reason="Ticket 24934: reservation create with NodeCnt and Flags=IGNORE_JOBS fails when partition nodes are occupied", | |
| ) | |
| def test_reservation_nodecnt_ignore_jobs_busy_partition(busy_partition, reservation): | |
| """NodeCnt + partition + IGNORE_JOBS succeeds when jobs use all nodes.""" | |
| resv_auto = f"{test_name}_auto" | |
| resv_exp = f"{test_name}_exp" | |
| # Baseline: an explicit Nodes= IGNORE_JOBS reservation over the busy nodes | |
| # must succeed first, isolating the NodeCnt path below from the IGNORE_JOBS | |
| # behavior itself. | |
| two_nodes = ",".join(busy_partition[:2]) | |
| cmd_exp = ( | |
| f"scontrol create reservation ReservationName={resv_exp} " | |
| f"PartitionName={partition} Nodes={two_nodes} StartTime=now " | |
| f"Duration=30 Users={testuser} Flags=IGNORE_JOBS" | |
| ) | |
| out_exp = atf.run_command( | |
| cmd_exp, | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| assert ( | |
| out_exp["exit_code"] == 0 | |
| ), f"Explicit Nodes= should succeed as baseline: {out_exp['stderr']}" | |
| atf.run_command( | |
| f"scontrol delete ReservationName={resv_exp}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| cmd_auto = ( | |
| f"scontrol create reservation ReservationName={resv_auto} " | |
| f"PartitionName={partition} NodeCnt=2 StartTime=now Duration=30 " | |
| f"Users={testuser} Flags=IGNORE_JOBS Features=" | |
| ) | |
| reservation.append(resv_auto) | |
| out_auto = atf.run_command( | |
| cmd_auto, | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| assert ( | |
| out_auto["exit_code"] == 0 | |
| ), f"Expected NodeCnt automatic selection to succeed: {out_auto['stderr']}" | |
| # The reservation must actually hold 2 of the (busy) partition nodes, | |
| # confirming IGNORE_JOBS selected occupied nodes rather than nothing. | |
| assert ( | |
| atf.get_reservation_parameter(resv_auto, "NodeCnt") == 2 | |
| ), "NodeCnt reservation did not reserve 2 nodes" | |
| reserved = set( | |
| atf.node_range_to_list(atf.get_reservation_parameter(resv_auto, "Nodes")) | |
| ) | |
| assert reserved <= set(busy_partition), ( | |
| f"Reserved nodes {sorted(reserved)} are not a subset of the busy " | |
| f"partition nodes {sorted(busy_partition)}" | |
| ) | |
| def test_reservation_nodecnt_without_ignore_jobs_fails_busy_partition( | |
| busy_partition, reservation | |
| ): | |
| """NodeCnt + partition + empty Features fails when all nodes busy, no IGNORE_JOBS.""" | |
| resv_neg = f"{test_name}_neg" | |
| cmd = ( | |
| f"scontrol create reservation ReservationName={resv_neg} " | |
| f"PartitionName={partition} NodeCnt=2 StartTime=now Duration=30 " | |
| f"Users={testuser} Features=" | |
| ) | |
| reservation.append(resv_neg) | |
| out = atf.run_command( | |
| cmd, | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| xfail=True, | |
| ) | |
| assert "nodes are busy" in out["stderr"].lower(), ( | |
| "Expected reservation without IGNORE_JOBS to fail due to no idle " | |
| f"nodes in the partition: {out['stderr']}" | |
| ) |