| ############################################################################ |
| # 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" |
| ) |