blob: 2a3b365a7c1a2defb3a373a3bc278589fa2aa820 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test sbatch --dependency after+time delays and special-exit afternotok."""
from datetime import datetime
import pytest
import atf
# Each job takes one CPU, so a dependent can run beside its target and only
# its dependency can hold it back.
MIN_NODE_CPUS = 2
JOB_OPTIONS = "-N1 -n1 -t5 -o /dev/null -e /dev/null"
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_nodes(1, [("CPUs", MIN_NODE_CPUS)])
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CPU")
atf.require_slurm_running()
def _submit_running_target():
"""Submit a target that runs until target_release exists."""
atf.make_bash_script(
"target.sh",
"touch target_started\nuntil [ -f target_release ]; do sleep 0.2; done",
)
target_id = atf.submit_job_sbatch(f"{JOB_OPTIONS} --requeue target.sh", fatal=True)
atf.wait_for_file("target_started", fatal=True)
return target_id
def test_after_with_time_waits_from_target_start():
"""after:<job_id>+<time> holds the job until 'time' minutes after the start."""
target_id = _submit_running_target()
dependent_id = atf.submit_job_sbatch(
f"{JOB_OPTIONS} --dependency=after:{target_id}+1 --wrap true", fatal=True
)
atf.wait_for_job_state(
dependent_id, "PENDING", desired_reason="Dependency", fatal=True
)
# One minute of delay, plus up to one sched_interval (60s by default) for
# the scheduler to notice it elapsed.
atf.wait_for_job_state(dependent_id, "COMPLETED", timeout=180, fatal=True)
atf.run_command("touch target_release", fatal=True)
start = {
job_id: datetime.fromisoformat(
atf.get_job_parameter(job_id, "StartTime", fatal=True)
)
for job_id in (target_id, dependent_id)
}
delay = (start[dependent_id] - start[target_id]).total_seconds()
assert delay >= 60, f"after+1 released the job {delay}s after the target started"
def test_afternotok_released_by_special_exit():
"""A target requeued and held with State=SpecialExit satisfies afternotok.
The target never ends while the dependent waits, so only the special exit
state can release it."""
target_id = _submit_running_target()
dependent_id = atf.submit_job_sbatch(
f"{JOB_OPTIONS} --dependency=afternotok:{target_id} --wrap true", fatal=True
)
atf.wait_for_job_state(
dependent_id, "PENDING", desired_reason="Dependency", fatal=True
)
atf.run_command(f"scontrol requeuehold State=SpecialExit {target_id}", fatal=True)
atf.wait_for_job_state(target_id, "SPECIAL_EXIT", fatal=True)
atf.wait_for_job_state(dependent_id, "COMPLETED", fatal=True)