blob: ce1cfd531184c7d5cc459404b04d2755f2b543c6 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""test_130_12.py - accrue limits with PriorityFlags=ACCRUE_ALWAYS.
ACCRUE_ALWAYS ignores accrue limits. Held, dependent and future begin time
jobs accrue too. test_130_11 covers the case without this flag.
A job accrues when squeue shows its AccrueTime.
"""
import re
import time
import pytest
import atf
pytestmark = pytest.mark.slow
test_user = atf.properties["test-user"]
# LIMIT jobs may accrue. Each case submits JOBS.
LIMIT = 2
JOBS = 4
acct_free = "acct_free_130_12"
acct_grp = "acct_grp_130_12"
acct_max = "acct_max_130_12"
acct_parent = "acct_parent_130_12"
acct_children = ["acct_child1_130_12", "acct_child2_130_12"]
qos_free = "qos_free_130_12"
qos_grp = "qos_grp_130_12"
qos_pu = "qos_pu_130_12"
qos_pa = "qos_pa_130_12"
# The unset AccrueTime value is not documented: no timestamp means unset.
ACCRUE_TIME = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}")
# Each case: the accounts and the QOS of the jobs.
LIMITS = [
([acct_grp], qos_free),
([acct_max], qos_free),
(acct_children, qos_free),
([acct_free], qos_grp),
([acct_free], qos_pu),
([acct_free], qos_pa),
]
LIMIT_IDS = [
"assoc_GrpJobsAccrue",
"assoc_MaxJobsAccrue",
"parent_GrpJobsAccrue",
"qos_GrpJobsAccrue",
"qos_MaxJobsAccruePerUser",
"qos_MaxJobsAccruePerAccount",
]
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_accounting(modify=True)
atf.require_config_parameter_includes("AccountingStorageEnforce", "limits")
atf.require_config_parameter_includes("AccountingStorageEnforce", "qos")
atf.require_config_parameter("PriorityType", "priority/multifactor")
atf.require_config_parameter_includes("PriorityFlags", "ACCRUE_ALWAYS")
atf.require_slurm_running()
su = atf.properties["slurm-user"]
qos_limits = {
qos_free: "",
qos_grp: f"GrpJobsAccrue={LIMIT}",
qos_pu: f"MaxJobsAccruePerUser={LIMIT}",
qos_pa: f"MaxJobsAccruePerAccount={LIMIT}",
}
for qos, limits in qos_limits.items():
atf.run_command(f"sacctmgr -i add qos {qos} {limits}", user=su, fatal=True)
atf.run_command(
f"sacctmgr -i add account {acct_free},{acct_max}", user=su, fatal=True
)
for account in (acct_grp, acct_parent):
atf.run_command(
f"sacctmgr -i add account {account} GrpJobsAccrue={LIMIT}",
user=su,
fatal=True,
)
atf.run_command(
f"sacctmgr -i add account {','.join(acct_children)} parent={acct_parent}",
user=su,
fatal=True,
)
accounts = ",".join([acct_free, acct_grp, acct_max, *acct_children])
qos_list = ",".join([qos_free, qos_grp, qos_pu, qos_pa])
atf.run_command(
f"sacctmgr -i add user {test_user} account={accounts} qos={qos_list}",
user=su,
fatal=True,
)
atf.run_command(
f"sacctmgr -i modify user where name={test_user} account={acct_max} "
f"set MaxJobsAccrue={LIMIT}",
user=su,
fatal=True,
)
# Load the new accounts, QOS and limits before any job.
atf.restart_slurmctld()
@pytest.fixture
def blocker():
"""Keep one node busy. Returns the job ID and the node name."""
job_id = atf.submit_job_sbatch(
f"-A {acct_free} --qos={qos_free} -N1 --exclusive --wrap 'sleep infinity'",
fatal=True,
)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
return job_id, atf.get_job_parameter(job_id, "NodeList", fatal=True)
def submit(node, account, qos, options=""):
"""Submit a job that stays pending on node."""
return atf.submit_job_sbatch(
f"-A {account} --qos={qos} -w {node} --exclusive {options} "
"--wrap 'sleep infinity'",
fatal=True,
)
def accrue_times(job_ids):
"""Return the AccrueTime of each pending job in job_ids."""
ids = {str(job_id) for job_id in job_ids}
output = atf.run_command_output(
f"squeue -h -t PD -u {test_user} -O JobID,AccrueTime", fatal=True
)
times = {}
for line in output.splitlines():
fields = line.split()
if fields and fields[0] in ids:
times[fields[0]] = fields[1] if len(fields) > 1 else ""
return times
def accruing(job_ids):
"""Return the pending jobs in job_ids that accrue."""
return {
job_id
for job_id, value in accrue_times(job_ids).items()
if ACCRUE_TIME.fullmatch(value)
}
@pytest.mark.parametrize("accounts,qos", LIMITS, ids=LIMIT_IDS)
def test_accrue_limit_ignored(blocker, accounts, qos):
"""Verify every pending job accrues, even over an accrue limit."""
_, node = blocker
per_account = JOBS // len(accounts)
job_ids = [
submit(node, account, qos) for account in accounts for _ in range(per_account)
]
# Once a later job accrues, the controller has checked the earlier ones.
marker = submit(node, acct_free, qos_free)
for _ in atf.timer(fatal=True):
if accruing([marker]):
break
jobs = accruing(job_ids)
assert len(jobs) == JOBS, (
f"All {JOBS} pending jobs should accrue, as ACCRUE_ALWAYS ignores the "
f"limit of {LIMIT}, but only {sorted(jobs)} do"
)
@pytest.mark.parametrize(
"option",
["--hold", "--dependency=afterok:{blocker}", "--begin=now+1hour"],
ids=["held", "dependent", "begin"],
)
def test_ineligible_job_accrues(blocker, option):
"""Verify held, dependent and future begin time jobs accrue now."""
blocker_id, node = blocker
job = submit(node, acct_free, qos_free, option.format(blocker=blocker_id))
value = ""
for _ in atf.timer(fatal=True):
value = accrue_times([job]).get(str(job), "")
if ACCRUE_TIME.fullmatch(value):
break
accrue_time = time.mktime(time.strptime(value, "%Y-%m-%dT%H:%M:%S"))
assert (
accrue_time <= time.time()
), f"Job {job} should accrue now, but its AccrueTime {value} is in the future"