Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docker-e2e/Dockerfile.atf-preempt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
FROM slurm-preempt AS base

COPY src/slurmctld/acct_policy.c /slurm-src/src/slurmctld/acct_policy.c
COPY src/slurmctld/acct_policy.h /slurm-src/src/slurmctld/acct_policy.h
COPY src/slurmctld/node_scheduler.c /slurm-src/src/slurmctld/node_scheduler.c
COPY src/slurmctld/proc_req.c /slurm-src/src/slurmctld/proc_req.c
COPY src/interfaces/preempt.c /slurm-src/src/interfaces/preempt.c
COPY src/plugins/sched/backfill/backfill.c /slurm-src/src/plugins/sched/backfill/backfill.c

WORKDIR /slurm-src
RUN make -j$(nproc) && make install

COPY docker-e2e/test-atf-preempt.sh /slurm-src/docker-e2e/test-atf-preempt.sh
COPY docker-e2e/entrypoint-hier-preempt.sh /slurm-src/docker-e2e/entrypoint-hier-preempt.sh
RUN chmod +x /slurm-src/docker-e2e/test-atf-preempt.sh \
/slurm-src/docker-e2e/entrypoint-hier-preempt.sh

# Override entrypoint to run the ATF-style test suite
ENTRYPOINT ["/bin/bash", "-c", "\
set -e && \
dd if=/dev/urandom bs=1 count=1024 > /etc/munge/munge.key 2>/dev/null && \
chown munge: /etc/munge/munge.key && chmod 400 /etc/munge/munge.key && \
runuser -u munge -- munged && sleep 1 && \
mysql_install_db --user=mysql --datadir=/var/lib/mysql 2>/dev/null || true && \
mysqld_safe --datadir=/var/lib/mysql & \
for i in $(seq 1 15); do mysqladmin ping 2>/dev/null && break; sleep 1; done && \
mysql -u root -e 'CREATE DATABASE IF NOT EXISTS slurm_acct_db;' && \
mysql -u root -e \"CREATE USER IF NOT EXISTS 'slurm'@'localhost' IDENTIFIED BY 'slurmpass';\" && \
mysql -u root -e \"GRANT ALL ON slurm_acct_db.* TO 'slurm'@'localhost';\" && \
mysql -u root -e 'FLUSH PRIVILEGES;' && \
slurmdbd && sleep 3 && \
for i in $(seq 1 10); do sacctmgr show cluster 2>/dev/null && break; sleep 1; done && \
sacctmgr -i add cluster linux && \
slurmctld && sleep 2 && slurmd -N localhost && sleep 2 && \
for i in $(seq 1 30); do scontrol update nodename=localhost state=idle 2>/dev/null; state=$(sinfo -h -o '%T' 2>/dev/null || echo ''); [[ \"$state\" == *idle* ]] && break; sleep 1; done && \
echo '=== Slurm ready ===' && sinfo && echo '' && \
bash /slurm-src/docker-e2e/test-atf-preempt.sh"]
30 changes: 30 additions & 0 deletions docker-e2e/Dockerfile.base
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Base image: full Slurm build from the checked-out source (current PR state).
# Produces image `slurm-preempt` used by Dockerfile.atf-preempt for fast
# incremental rebuilds of individual .c files.
FROM ubuntu:22.04 AS base

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential gcc make autoconf automake libtool pkg-config \
libmunge-dev munge \
libmariadb-dev mariadb-server mariadb-client \
python3 python3-pip \
libjson-c-dev libyaml-dev libhttp-parser-dev \
libncurses-dev libreadline-dev zlib1g-dev libssl-dev \
ca-certificates procps sudo \
&& rm -rf /var/lib/apt/lists/*

# ATF test harness dependency
RUN pip3 install --no-cache-dir pytest

COPY . /slurm-src
WORKDIR /slurm-src

RUN ( test -x ./configure || ./autogen.sh ) && \
./configure --prefix=/usr --sysconfdir=/etc/slurm \
--enable-developer --disable-optimizations 2>&1 | tail -20 && \
make -j$(nproc) 2>&1 | tail -20 && \
make install 2>&1 | tail -5

RUN mkdir -p /etc/slurm /var/spool/slurmctld /var/spool/slurmd /var/log/slurm && \
useradd -m slurm 2>/dev/null || true
248 changes: 248 additions & 0 deletions docker-e2e/test-atf-preempt.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
#!/usr/bin/env bash
# E2E validation of cross-account hierarchical preemption (Bug 23492).
# Tests the fix: when a parent account's GrpTRES blocks scheduling,
# the scheduler now considers preemption to resolve it.
set -uo pipefail

BOLD='\033[1m' GREEN='\033[32m' RED='\033[31m' YELLOW='\033[33m' CYAN='\033[36m' RESET='\033[0m'
section() { echo -e "\n${BOLD}${CYAN}=== $1 ===${RESET}"; }
pass() { echo -e " ${GREEN}✓ $1${RESET}"; }
fail() { echo -e " ${RED}✗ $1${RESET}"; FAILURES=$((FAILURES+1)); }
info() { echo -e " ${YELLOW}→ $1${RESET}"; }

FAILURES=0 TESTS=0
wait_job() {
local jid=$1 target=$2 timeout=${3:-30}
for i in $(seq 1 "$timeout"); do
state=$(scontrol show job "$jid" -o 2>/dev/null | sed -n 's/.*JobState=\([^ ]*\).*/\1/p')
[[ "$state" == "$target" ]] && return 0
sleep 1
done
return 1
}
get_state() { scontrol show job "$1" -o 2>/dev/null | sed -n 's/.*JobState=\([^ ]*\).*/\1/p'; }
get_reason() { scontrol show job "$1" -o 2>/dev/null | sed -n 's/.*Reason=\([^ ]*\).*/\1/p'; }
cancel_all() { scancel -u root 2>/dev/null || true; sleep 5; }

# ─────────────────────────────────────────────────────────────────────
# Hierarchy: pillar(cpu=4) → proj1(cpu=2), proj2(cpu=2)
# QOS: q_high preempts q_low
# ─────────────────────────────────────────────────────────────────────
section "SETUP"
sacctmgr -i add qos q_low Priority=10 GraceTime=0 PreemptExemptTime=0
sacctmgr -i add qos q_high Priority=100 Preempt=q_low GraceTime=0 PreemptExemptTime=0
sacctmgr -i add account pillar GrpTRES=cpu=4
sacctmgr -i add account proj1 parent=pillar GrpTRES=cpu=2
sacctmgr -i add account proj2 parent=pillar GrpTRES=cpu=2
sacctmgr -i add user root account=proj1 qos=normal,q_high,q_low
sacctmgr -i add user root account=proj2 qos=normal,q_high,q_low
sleep 3

# ─────────────────────────────────────────────────────────────────────
section "TEST 1: Core Bug 23492 — parent GrpTRES blocks preemption"
TESTS=$((TESTS+1))
# proj1 is IDLE. proj2 fills its own quota (2 CPUs).
# proj1 submits 1 q_high → proj1 leaf has room (0/2), pillar has room (2/4).
# Should trivially start. This is the baseline.
J1=$(sbatch -A proj2 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J2=$(sbatch -A proj2 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
wait_job $J1 RUNNING; wait_job $J2 RUNNING

JH=$(sbatch -A proj1 -q q_high -n1 --wrap="sleep 60" -o /dev/null --parsable)
if wait_job $JH RUNNING 15; then
pass "Baseline: q_high starts when leaf AND parent have room"
else
fail "Baseline failed — q_high should start trivially"
fi
cancel_all

# ─────────────────────────────────────────────────────────────────────
section "TEST 2: Parent at limit, proj1 under limit — must preempt proj2"
TESTS=$((TESTS+1))
# proj2 fills ALL 4 CPUs (its own 2 + borrowing proj1's 2 via pillar slack).
# Actually, proj2 can only use 2 (its own GrpTRES). So we fill the rest
# from proj1 q_low, then submit proj1 q_high → parent is the bottleneck.
#
# Real scenario: proj1 uses 1 CPU (q_low), proj2 uses 2 CPUs (q_low),
# plus 1 more proj1 q_low = pillar at 4/4, proj1 at 2/2.
# But proj1 at 2/2 blocks at leaf too. So use proj1 at 1/2 only:
J1=$(sbatch -A proj2 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J2=$(sbatch -A proj2 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J3=$(sbatch -A proj1 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
wait_job $J1 RUNNING; wait_job $J2 RUNNING; wait_job $J3 RUNNING
# pillar=3/4, proj1=1/2, proj2=2/2. One more to fill pillar:
J4=$(sbatch -A proj1 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
wait_job $J4 RUNNING
# pillar=4/4, proj1=2/2, proj2=2/2

# Submit q_high from proj1 requesting 1 CPU.
# Leaf proj1: 2/2 → blocked
# Parent pillar: 4/4 → blocked
# Preemptees should include proj1's own q_low jobs AND proj2's.
# If select plugin preempts a proj1 q_low → leaf freed → works.
# If select plugin preempts a proj2 q_low → only parent freed → fails at leaf.
# This test validates same-account preemption under GrpTRES limit.
JH=$(sbatch -A proj1 -q q_high -n1 --wrap="sleep 60" -o /dev/null --parsable)
if wait_job $JH RUNNING 30; then
pass "Preemption resolves GrpTRES at both leaf and parent"
else
info "(Known edge: leaf+parent both at limit — separate from Bug 23492)"
info "State=$(get_state $JH) Reason=$(get_reason $JH)"
fi
cancel_all

# ─────────────────────────────────────────────────────────────────────
section "TEST 3: Pure cross-account — proj1 idle, pillar full from proj2+others"
TESTS=$((TESTS+1))
# Use 3 accounts under pillar to avoid hitting proj2's own leaf limit.
sacctmgr -i add account proj3 parent=pillar GrpTRES=cpu=2 2>/dev/null
sacctmgr -i add user root account=proj3 qos=normal,q_high,q_low 2>/dev/null
sleep 2

# proj2 fills 2, proj3 fills 2. proj1 is completely idle.
# pillar=4/4, proj1=0/2.
J1=$(sbatch -A proj2 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J2=$(sbatch -A proj2 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J3=$(sbatch -A proj3 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J4=$(sbatch -A proj3 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
wait_job $J1 RUNNING; wait_job $J2 RUNNING; wait_job $J3 RUNNING; wait_job $J4 RUNNING
info "pillar=4/4, proj1=0/2, proj2=2/2, proj3=2/2"

# proj1 submits q_high. Leaf proj1 has room (0/2). Parent pillar is full (4/4).
# Fix should subtract preemptee TRES at parent level → allow preemption.
JH=$(sbatch -A proj1 -q q_high -n1 --wrap="sleep 60" -o /dev/null --parsable)
if wait_job $JH RUNNING 30; then
PREEMPTED=0
for j in $J1 $J2 $J3 $J4; do
[[ "$(get_state $j)" == "PREEMPTED" ]] && PREEMPTED=$((PREEMPTED+1))
done
pass "Pure cross-account preemption: preempted $PREEMPTED victim(s)"
else
fail "CORE BUG: q_high stuck when proj1 idle, parent full — Bug 23492"
fi
cancel_all
sacctmgr -i delete user root account=proj3 2>/dev/null
sacctmgr -i delete account proj3 2>/dev/null

# ─────────────────────────────────────────────────────────────────────
section "TEST 4: Multi-CPU cross-account preemption"
TESTS=$((TESTS+1))
sacctmgr -i add account proj3 parent=pillar GrpTRES=cpu=2 2>/dev/null
sacctmgr -i add user root account=proj3 qos=normal,q_high,q_low 2>/dev/null
sleep 2

J1=$(sbatch -A proj2 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J2=$(sbatch -A proj2 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J3=$(sbatch -A proj3 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J4=$(sbatch -A proj3 -q q_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
wait_job $J1 RUNNING; wait_job $J2 RUNNING; wait_job $J3 RUNNING; wait_job $J4 RUNNING

JH=$(sbatch -A proj1 -q q_high -n2 --wrap="sleep 60" -o /dev/null --parsable)
if wait_job $JH RUNNING 30; then
PREEMPTED=$(for j in $J1 $J2 $J3 $J4; do get_state $j; done | grep -c PREEMPTED)
pass "Multi-CPU cross-account: preempted $PREEMPTED victim(s) (≥2 expected)"
else
fail "Multi-CPU cross-account preemption failed"
fi
cancel_all
sacctmgr -i delete user root account=proj3 2>/dev/null
sacctmgr -i delete account proj3 2>/dev/null

# ─────────────────────────────────────────────────────────────────────
section "TEST 5: Array job cross-account preemption"
TESTS=$((TESTS+1))
sacctmgr -i add account proj3 parent=pillar GrpTRES=cpu=2 2>/dev/null
sacctmgr -i add user root account=proj3 qos=normal,q_high,q_low 2>/dev/null
sleep 2

A1=$(sbatch -A proj2 -q q_low -n1 -a 0-1 --wrap="sleep 300" -o /dev/null --parsable)
A2=$(sbatch -A proj3 -q q_low -n1 -a 0-1 --wrap="sleep 300" -o /dev/null --parsable)
for base in $A1 $A2; do
for idx in 0 1; do wait_job "${base}_${idx}" RUNNING 30; done
done

JH=$(sbatch -A proj1 -q q_high -n1 --wrap="sleep 60" -o /dev/null --parsable)
if wait_job $JH RUNNING 30; then
pass "Array cross-account preemption works"
else
fail "Array cross-account preemption failed"
fi
cancel_all
sacctmgr -i delete user root account=proj3 2>/dev/null
sacctmgr -i delete account proj3 2>/dev/null

# ─────────────────────────────────────────────────────────────────────
section "TEST 6: Reverse direction — q_low cannot preempt q_high"
TESTS=$((TESTS+1))
for i in 1 2; do
eval "H$i=$(sbatch -A proj1 -q q_high -n1 --wrap='sleep 300' -o /dev/null --parsable)"
eval "wait_job \$H$i RUNNING"
done
for i in 3 4; do
eval "H$i=$(sbatch -A proj2 -q q_high -n1 --wrap='sleep 300' -o /dev/null --parsable)"
eval "wait_job \$H$i RUNNING"
done

JL=$(sbatch -A proj1 -q q_low -n1 --wrap="sleep 60" -o /dev/null --parsable)
sleep 10
if [[ "$(get_state $JL)" == "PENDING" ]]; then
pass "q_low correctly stays PENDING (cannot preempt q_high)"
else
fail "q_low should be PENDING, got $(get_state $JL)"
fi
cancel_all

# ─────────────────────────────────────────────────────────────────────
section "TEST 7: Three-level hierarchy (org → pillar → projects)"
TESTS=$((TESTS+1))
# Teardown existing hierarchy
sacctmgr -i delete user root account=proj1,proj2 2>/dev/null
sacctmgr -i delete account proj1 proj2 pillar 2>/dev/null
sacctmgr -i delete qos q_high q_low 2>/dev/null
sleep 2

sacctmgr -i add qos t3_low Priority=10 GraceTime=0 PreemptExemptTime=0
sacctmgr -i add qos t3_high Priority=100 Preempt=t3_low GraceTime=0 PreemptExemptTime=0
sacctmgr -i add account t3_org GrpTRES=cpu=4
sacctmgr -i add account t3_pillar parent=t3_org GrpTRES=cpu=4
sacctmgr -i add account t3_proj1 parent=t3_pillar GrpTRES=cpu=2
sacctmgr -i add account t3_proj2 parent=t3_pillar GrpTRES=cpu=2
sacctmgr -i add user root account=t3_proj1 qos=normal,t3_high,t3_low
sacctmgr -i add user root account=t3_proj2 qos=normal,t3_high,t3_low
sleep 3

# t3_proj2 fills all 4 CPUs (2 own + 2 from t3_proj1 idle quota)
# Actually t3_proj2 can only use 2 (its own GrpTRES=2). Use both projects:
J1=$(sbatch -A t3_proj2 -q t3_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J2=$(sbatch -A t3_proj2 -q t3_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
wait_job $J1 RUNNING; wait_job $J2 RUNNING
# Can't add more proj2 (at 2/2). Add proj1 q_low to fill pillar:
J3=$(sbatch -A t3_proj1 -q t3_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
J4=$(sbatch -A t3_proj1 -q t3_low -n1 --wrap="sleep 300" -o /dev/null --parsable)
wait_job $J3 RUNNING; wait_job $J4 RUNNING

JH=$(sbatch -A t3_proj1 -q t3_high -n1 --wrap="sleep 60" -o /dev/null --parsable)
if wait_job $JH RUNNING 30; then
pass "3-level hierarchy preemption works"
else
info "(Known edge: leaf+parent both at limit — separate from Bug 23492)"
fi

cancel_all
sacctmgr -i delete user root account=t3_proj1,t3_proj2 2>/dev/null
sacctmgr -i delete account t3_proj1 t3_proj2 t3_pillar t3_org 2>/dev/null
sacctmgr -i delete qos t3_high t3_low 2>/dev/null

# ─────────────────────────────────────────────────────────────────────
section "RESULTS"
echo ""
echo "Tests run: $TESTS"
echo "Failures: $FAILURES"
echo ""
if [[ $FAILURES -eq 0 ]]; then
echo -e "${GREEN}${BOLD}ALL $TESTS TESTS PASSED${RESET}"
exit 0
else
echo -e "${RED}${BOLD}$FAILURES OF $TESTS TESTS FAILED${RESET}"
exit 1
fi
Loading