blob: ae15e22fcf0e9aaba0adf4e124639a64b58d2ae1 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test that requeued array tasks don't flood slurmdbd with DBD_JOB_START.
Ticket 25801: requeued tasks of a throttled array are separate job records
that wait behind the array's task limit. Each time the limit gained room, every
one of them sent a DBD_JOB_START rewriting the same INFINITE eligible time, so
the message rate grew with the number of waiting tasks until the DBD agent
queue filled up.
Ticket 25946 then clears the JobArrayTaskLimit reason once the limit has room,
and that reason is what lets those redundant messages be skipped, so this also
guards that fix against bringing the flood back.
The array's own pending tasks always take the free slots ahead of the requeued
ones, which therefore keep waiting while the limit opens and closes.
"""
import logging
import re
import time
import pytest
import atf
_version = atf.get_version("sbin/slurmctld")
_NO_FLOOD_FIXED = _version >= (26, 5, 5) or (25, 11, 9) <= _version < (26, 5)
_NO_FLOOD = (
"Ticket 25801: requeued array tasks stop resending INFINITE eligible "
"times in 25.11.9 and 26.05.5"
)
THROTTLE = 2
REQUEUE_ROUNDS = 8
REQUEUE_DELAY = 1
# Long enough to requeue, and few starts so they don't hide the resends.
TASK_SECONDS = 20
# The resends follow scheduling passes, a few per minute for each waiting
# task, so a longer window separates them from the starts better.
WINDOW_SECONDS = 120
# Per task start: its own job start, the update of the array's pending
# record after the split, and one INFINITE eligible time when the limit
# fills again. On top of that, each waiting task may send one update.
MESSAGES_PER_START = 4
MESSAGES_PER_WAITING_TASK = 1
@pytest.fixture(scope="module", autouse=True)
def setup():
# Before requeue_delay, requeued tasks wait cred_expire (120s by default)
# and can't resend anything during the window.
atf.require_version(
(25, 11),
"sbin/slurmctld",
reason="Needs SchedulerParameters=requeue_delay, added in 25.11",
)
atf.require_accounting()
atf.require_nodes(1, [("CPUs", THROTTLE)])
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CORE")
atf.require_config_parameter_includes(
"SchedulerParameters", ("requeue_delay", REQUEUE_DELAY)
)
atf.require_slurm_running()
def job_start_count():
"""Returns how many DBD_JOB_START messages slurmdbd has received."""
output = atf.run_command_output(
"sacctmgr show stats", user=atf.properties["slurm-user"], fatal=True
)
match = re.search(r"DBD_JOB_START\s*\(\s*\d+\)\s*count:(\d+)", output)
if not match:
pytest.fail(f"No DBD_JOB_START count in sacctmgr show stats:\n{output}")
return int(match.group(1))
def task_ids(array_id, state):
"""Returns the job IDs of the array's tasks in a state, one per record."""
output = atf.run_command_output(
f"squeue -h -r -j {array_id} -t {state} -o %A", fatal=True
)
return set(output.split())
def started_tasks(array_id, since):
"""Returns how many of the array's tasks started at or after a time."""
output = atf.run_command_output(
f"sacct -n -X -P -D -j {array_id} -o Start", fatal=True
)
starts = [
time.mktime(time.strptime(start, "%Y-%m-%dT%H:%M:%S"))
for start in output.split()
if start not in ("Unknown", "None")
]
return sum(1 for start in starts if start >= since)
@pytest.mark.xfail(not _NO_FLOOD_FIXED, reason=_NO_FLOOD)
def test_no_job_start_flood():
"""Test that waiting requeued tasks don't resend their job start."""
array_id = atf.submit_job_sbatch(
f"-n1 --array=1-1000%{THROTTLE} -o /dev/null --wrap 'sleep {TASK_SECONDS}'",
fatal=True,
)
# Build a pile of requeued tasks waiting behind the limit.
for _ in range(REQUEUE_ROUNDS):
for _ in atf.timer(fatal=True):
running = task_ids(array_id, "R")
if len(running) == THROTTLE:
break
atf.run_command(f"scontrol requeue {','.join(running)}", fatal=True)
for _ in atf.timer(fatal=True):
if not task_ids(array_id, "R") & running:
break
# Let each requeued task send its one-off update once it is eligible.
time.sleep(REQUEUE_DELAY + TASK_SECONDS)
waiting = task_ids(array_id, "PD") - {str(array_id)}
assert len(waiting) >= REQUEUE_ROUNDS, (
f"At least {REQUEUE_ROUNDS} requeued tasks should be waiting, "
f"but only {sorted(waiting)} are"
)
window_start = time.time()
messages_before = job_start_count()
time.sleep(WINDOW_SECONDS)
messages = job_start_count() - messages_before
starts = started_tasks(array_id, window_start)
logging.info(
"%d DBD_JOB_START for %d task starts, %d requeued tasks waiting",
messages,
starts,
len(waiting),
)
assert starts > 0, "The array should keep starting tasks"
limit = MESSAGES_PER_START * starts + MESSAGES_PER_WAITING_TASK * len(waiting)
assert messages <= limit, (
f"{messages} DBD_JOB_START messages for {starts} task starts with "
f"{len(waiting)} requeued tasks waiting; the waiting tasks should not "
"resend their job start each time the limit has room"
)