blob: b59b8e963c80e3c817a1802a702ee02378e64343 [file]
############################################################################
# Copyright (C) SchedMD LLC.
############################################################################
import re
from datetime import datetime
from pathlib import Path
import pytest
import atf
# Setup
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_slurm_running()
@pytest.fixture(scope="function")
def idle_node():
"""Wait until at least one node is idle"""
atf.repeat_until(
lambda: atf.get_nodes(quiet=True),
lambda nodes: any(node["state"] == ["IDLE"] for node in nodes.values()),
fatal=True,
)
@pytest.mark.parametrize("itime", ["", "=1", "=5", "=10"])
def test_immediate_run(itime, idle_node):
"""
Verify that a job submitted with --immediate runs if the system has
available resources.
"""
assert (
atf.run_command_exit(f"srun --immediate{itime} true", timeout=5) == 0
), "srun --immediate should end correctly and quickly"
def assert_fail(results):
assert results["exit_code"] == 1, "srun should fail"
assert (
results["duration"] < 2
), "srun should fail as soon as the controller responds"
assert (
"Unable to allocate resources" in results["stderr"]
), "srun message should be correct"
def assert_cancel(job_id, itime):
job = atf.get_jobs()[job_id]
submit_time = datetime.fromisoformat(job["SubmitTime"])
end_time = datetime.fromisoformat(job["EndTime"])
elapsed = (end_time - submit_time).total_seconds()
assert elapsed >= itime, f"Job should wait at least {itime} seconds"
assert elapsed < itime + 5, f"Job should be cancelled soon after {itime} seconds"
assert job["JobState"] == "CANCELLED", "Job should be cancelled"
def test_immediate_hold():
"""
Spawn a srun with --immediate and --hold (priority==0) option.
The job can't run immediately with a priority of zero.
"""
results = atf.run_command("srun --immediate --hold true", xfail=True)
assert_fail(results)
@pytest.fixture(scope="function")
def block_job_node():
# submit a job to block the cluster
job_id = atf.submit_job_sbatch("--exclusive --wrap 'sleep infinity'", fatal=True)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
yield atf.get_jobs()[job_id]["NodeList"]
atf.cancel_jobs([job_id], fatal=True)
@pytest.mark.parametrize("itime", ["", "=1"])
def test_immediate_fail(itime, block_job_node):
"""
Spawn a srun with --immediate with default 1s while the cluster is busy.
The job can't run immediately, so submission should fail immediately.
"""
results = atf.run_command(
f"srun -w {block_job_node} --immediate{itime} true", xfail=True
)
assert_fail(results)
@pytest.mark.parametrize("itime", [2, 5, 10])
def test_immediate_cancel(itime, block_job_node):
"""
Spawn a srun with --immediate with some seconds while the cluster is busy.
The job can't run on those seconds, so job should exists but should be
cancelled once those seconds pass.
"""
job_id = atf.submit_job_srun(
f"-w {block_job_node} --immediate={itime} true", xfail=True
)
assert_cancel(job_id, itime)
def test_immediate_does_not_wait_on_a_queued_step():
"""
Spawn a srun with --immediate for a step while the job's own resources
are busy. --immediate applies to step allocations as well as job
allocations, so it must fail promptly instead of inheriting the
MAX(60, SlurmctldTimeout) wait a queued step otherwise gets.
"""
# With defer, job start waits for the periodic scheduling loop, which can
# consume most of this test's budget before the step srun even runs.
atf.require_config_parameter_excludes("SchedulerParameters", "defer")
out = Path("immediate_step.out")
ready = Path("immediate_step_ready")
script = Path("immediate_step.sh")
# The first step takes the whole allocation, so the second cannot start
# until it ends. Aborting when the hog never took it keeps a slow runner
# from reading as a product failure.
atf.make_bash_script(
script,
f"""srun -N1 --exclusive sh -c 'touch "{ready}"; exec sleep infinity' &
for _ in $(seq 1 60); do [ -f '{ready}' ] && break; sleep 0.5; done
[ -f '{ready}' ] || {{ echo HOG_NOT_READY; exit 1; }}
start=$SECONDS
srun --immediate=1 -N1 --exclusive true
echo "IMMEDIATE_RC=$?"
echo "IMMEDIATE_ELAPSED=$((SECONDS - start))"
""",
)
job_id = atf.submit_job(
"sbatch",
f"-N1 --exclusive -t5 --output={out} --error={out}",
str(script),
wrap_job=False,
fatal=True,
)
assert job_id != 0, "sbatch should submit the job"
# Longer than the atf default: the script first waits up to 30s for the
# hog to take the allocation before srun --immediate even runs.
atf.assert_file_contents(out, "IMMEDIATE_ELAPSED=", contains=True, timeout=90)
text = atf.run_command_output(f"cat {out}", quiet=True, fatal=True)
assert (
"IMMEDIATE_RC=0" not in text
), f"srun --immediate=1 should fail while the allocation is busy, got: {text}"
# Pin the failure to the --immediate path rather than any other srun error.
assert (
"Unable to create step for job" in text
), f"srun should report it could not create the step, got: {text}"
elapsed = int(re.search(r"IMMEDIATE_ELAPSED=(\d+)", text).group(1))
assert elapsed < 5, (
f"srun --immediate=1 should give up after its 1s step wait, not wait "
f"out the pending-step timeout; took {elapsed}s"
)