blob: c8758702337c07193980f5886426dd9c4a8a55d7 [file]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved
############################################################################
import pytest
import atf
pytestmark = pytest.mark.slow
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_config_parameter("PreemptType", "preempt/partition_prio")
atf.require_config_parameter("PreemptMode", "SUSPEND,GANG")
atf.require_config_parameter_includes("SchedulerParameters", ("bf_interval", 1))
atf.require_config_parameter_includes("SchedulerParameters", ("sched_interval", 1))
atf.require_nodes(1, [("CPUs", 2), ("RealMemory", 512)])
atf.require_config_parameter("DefMemPerNode", "128")
atf.require_config_parameter(
"PartitionName",
{
"lowprio": {"Nodes": "ALL", "PriorityTier": "1"},
"highprio": {"Nodes": "ALL", "PriorityTier": "2"},
},
)
atf.require_version((26, 5), "sbin/slurmctld")
atf.require_slurm_running()
@pytest.mark.parametrize(
"preempt_mode,preempted_state",
[
("SUSPEND,GANG", "SUSPENDED"),
("REQUEUE", "PENDING"),
("CANCEL", "PREEMPTED"),
],
)
def test_preempt_exempt_time(preempt_mode, preempted_state):
"""Verify that PreemptExemptTime protects a job across preemption modes.
For each PreemptMode (SUSPEND,GANG / REQUEUE / CANCEL), submit a
low-priority job, then a high-priority job on the same node. The
low-priority job should remain running during the 10-second exempt
window and only be preempted after it expires.
"""
atf.set_config_parameter("PreemptMode", preempt_mode, restart=True)
atf.set_config_parameter("PreemptExemptTime", "00:00:10")
job_id1 = atf.submit_job_sbatch(
'-c2 -o /dev/null -p lowprio --wrap "sleep infinity"',
fatal=True,
)
assert atf.wait_for_job_state(
job_id1, "RUNNING", timeout=10
), f"Low-priority job ({job_id1}) did not start"
job_id2 = atf.submit_job_sbatch(
'-c2 -o /dev/null -p highprio --wrap "sleep 20"',
fatal=True,
)
# Low-priority job should NOT be preempted during the exempt window
assert not atf.wait_for_job_state(
job_id1, preempted_state, timeout=5, xfail=True
), f"Low-priority job ({job_id1}) was preempted during exempt window"
assert (
atf.get_job_parameter(job_id1, "JobState") == "RUNNING"
), f"Low-priority job ({job_id1}) should still be running during exempt window"
# The preemptor must not have started either. Under SUSPEND,GANG the way
# the exemption fails is that the preemptor is placed alongside the exempt
# job on its cores, which leaves the victim RUNNING and never SUSPENDED --
# indistinguishable from a working exemption unless the preemptor is
# checked too.
assert (
atf.get_job_parameter(job_id2, "JobState") == "PENDING"
), f"High-priority job ({job_id2}) started during the exempt window"
# After PreemptExemptTime expires the low-priority job must be preempted
assert atf.wait_for_job_state(
job_id1, preempted_state, timeout=15
), f"Low-priority job ({job_id1}) was not preempted ({preempted_state}) after exempt time expired"
# High-priority job should now be running
assert atf.wait_for_job_state(
job_id2, "RUNNING", timeout=10
), f"High-priority job ({job_id2}) did not start after preemption"
atf.cancel_jobs([job_id1, job_id2], fatal=True)
@pytest.mark.skipif(
atf.get_version("sbin/slurmctld") < (26, 11),
reason="Ticket 23654: exempt cores were rebuilt at most once a second, "
"leaving a window at job start, until 26.11",
)
def test_preempt_exempt_time_covers_job_start():
"""A job is exempt from the moment it starts holding its allocation.
test_preempt_exempt_time lets the low-priority job settle before the
preemptor is submitted. That hides a window at job start: a preemptor
scheduled in the same moment could be placed on the new job's cores before
they were protected, and the victim stays RUNNING and unsuspended
throughout, so only the preemptor shows it.
Submit the preemptor as soon as the low-priority job is running, and repeat,
so a window that only opens around job start is exercised rather than
stepped over.
"""
atf.set_config_parameter("PreemptMode", "SUSPEND,GANG", restart=True)
atf.set_config_parameter("PreemptExemptTime", "00:00:10")
for attempt in range(3):
job_id1 = atf.submit_job_sbatch(
'-c2 -o /dev/null -p lowprio --wrap "sleep infinity"',
fatal=True,
)
# Poll hard so the preemptor lands as close to the job start as the
# test can manage.
assert atf.wait_for_job_state(
job_id1, "RUNNING", poll_interval=0.1
), f"Attempt {attempt}: low-priority job ({job_id1}) did not start"
job_id2 = atf.submit_job_sbatch(
'-c2 -o /dev/null -p highprio --wrap "sleep 20"',
fatal=True,
)
# Bounded by the 10 second PreemptExemptTime above, not by the atf
# default: past that window the job is no longer exempt and the
# preemptor is meant to start, so a longer wait would assert the
# opposite of this test.
assert not atf.wait_for_job_state(job_id2, "RUNNING", timeout=5, xfail=True), (
f"Attempt {attempt}: high-priority job ({job_id2}) started while "
f"({job_id1}) was still within its PreemptExemptTime"
)
assert atf.get_job_parameter(job_id2, "JobState") == "PENDING", (
f"Attempt {attempt}: high-priority job ({job_id2}) should be "
f"pending, not in a terminal state, during the exempt window"
)
assert atf.get_job_parameter(job_id1, "JobState") == "RUNNING", (
f"Attempt {attempt}: low-priority job ({job_id1}) should still be "
f"running during its exempt window"
)
atf.cancel_jobs([job_id1, job_id2], fatal=True)