190 lines
5.2 KiB
Python
190 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
POLICY_PATH = ROOT / "ops" / "gitea-deploy-broker" / "runner_capacity_policy.py"
|
|
|
|
|
|
@pytest.fixture
|
|
def policy_module():
|
|
spec = importlib.util.spec_from_file_location("appops_runner_capacity_policy_test", POLICY_PATH)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _state(policy_module, **overrides):
|
|
values = {
|
|
"queue_depth": 0,
|
|
"oldest_queued_seconds": None,
|
|
"load_ratio": 0.30,
|
|
"available_memory_bytes": 20 * policy_module.GIB,
|
|
"burst_running": False,
|
|
"burst_busy": False,
|
|
"burst_disabled": True,
|
|
"seconds_since_demand": 600,
|
|
"seconds_since_action": 600,
|
|
"swap_critical": False,
|
|
"cache_free_bytes": 100 * policy_module.GIB,
|
|
}
|
|
values.update(overrides)
|
|
return policy_module.BurstCapacityInput(**values)
|
|
|
|
|
|
def test_burst_starts_only_for_queue_demand_with_headroom(policy_module):
|
|
decision = policy_module.decide_burst_capacity(
|
|
_state(policy_module, queue_depth=2)
|
|
)
|
|
|
|
assert decision.action == "start"
|
|
assert decision.reason == "queue_pressure_with_host_headroom"
|
|
assert decision.pressure == "headroom"
|
|
|
|
|
|
def test_old_job_can_trigger_burst_even_with_single_queued_job(policy_module):
|
|
decision = policy_module.decide_burst_capacity(
|
|
_state(policy_module, queue_depth=1, oldest_queued_seconds=60)
|
|
)
|
|
|
|
assert decision.action == "start"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("load_ratio", "memory_gib", "swap_critical"),
|
|
[
|
|
(0.75, 20, False),
|
|
(0.30, 10, False),
|
|
(0.30, 20, True),
|
|
],
|
|
)
|
|
def test_burst_does_not_start_without_headroom(
|
|
policy_module, load_ratio, memory_gib, swap_critical
|
|
):
|
|
decision = policy_module.decide_burst_capacity(
|
|
_state(
|
|
policy_module,
|
|
queue_depth=3,
|
|
load_ratio=load_ratio,
|
|
available_memory_bytes=memory_gib * policy_module.GIB,
|
|
swap_critical=swap_critical,
|
|
)
|
|
)
|
|
|
|
assert decision.action == "hold"
|
|
assert decision.reason == "queue_pressure_but_host_headroom_insufficient"
|
|
|
|
|
|
def test_high_pressure_drains_busy_burst_instead_of_stopping_it(policy_module):
|
|
decision = policy_module.decide_burst_capacity(
|
|
_state(
|
|
policy_module,
|
|
queue_depth=3,
|
|
load_ratio=0.95,
|
|
burst_running=True,
|
|
burst_busy=True,
|
|
burst_disabled=False,
|
|
)
|
|
)
|
|
|
|
assert decision.action == "drain"
|
|
assert decision.reason == "host_pressure_drain_after_current_job"
|
|
|
|
|
|
def test_high_pressure_stops_only_already_drained_idle_burst(policy_module):
|
|
decision = policy_module.decide_burst_capacity(
|
|
_state(
|
|
policy_module,
|
|
load_ratio=0.95,
|
|
burst_running=True,
|
|
burst_busy=False,
|
|
burst_disabled=True,
|
|
seconds_since_action=600,
|
|
)
|
|
)
|
|
|
|
assert decision.action == "stop"
|
|
assert decision.reason == "host_pressure_idle_burst_can_stop"
|
|
|
|
|
|
def test_idle_burst_is_drained_before_stop(policy_module):
|
|
first = policy_module.decide_burst_capacity(
|
|
_state(
|
|
policy_module,
|
|
burst_running=True,
|
|
burst_disabled=False,
|
|
seconds_since_demand=600,
|
|
seconds_since_action=600,
|
|
)
|
|
)
|
|
second = policy_module.decide_burst_capacity(
|
|
_state(
|
|
policy_module,
|
|
burst_running=True,
|
|
burst_disabled=True,
|
|
seconds_since_demand=600,
|
|
seconds_since_action=600,
|
|
)
|
|
)
|
|
|
|
assert first.action == "drain"
|
|
assert second.action == "stop"
|
|
|
|
|
|
def test_disabled_running_burst_reenables_when_demand_returns(policy_module):
|
|
decision = policy_module.decide_burst_capacity(
|
|
_state(
|
|
policy_module,
|
|
queue_depth=2,
|
|
burst_running=True,
|
|
burst_disabled=True,
|
|
seconds_since_action=600,
|
|
)
|
|
)
|
|
|
|
assert decision.action == "enable"
|
|
|
|
|
|
def test_start_cooldown_prevents_flapping(policy_module):
|
|
decision = policy_module.decide_burst_capacity(
|
|
_state(
|
|
policy_module,
|
|
queue_depth=2,
|
|
seconds_since_action=30,
|
|
)
|
|
)
|
|
|
|
assert decision.action == "hold"
|
|
|
|
|
|
def test_burst_does_not_start_when_cache_headroom_is_low(policy_module):
|
|
decision = policy_module.decide_burst_capacity(
|
|
_state(
|
|
policy_module,
|
|
queue_depth=3,
|
|
cache_free_bytes=20 * policy_module.GIB,
|
|
)
|
|
)
|
|
assert decision.action == "hold"
|
|
assert decision.reason == "queue_pressure_but_host_headroom_insufficient"
|
|
|
|
|
|
def test_low_cache_headroom_drains_busy_burst(policy_module):
|
|
decision = policy_module.decide_burst_capacity(
|
|
_state(
|
|
policy_module,
|
|
queue_depth=3,
|
|
burst_running=True,
|
|
burst_busy=True,
|
|
burst_disabled=False,
|
|
cache_free_bytes=10 * policy_module.GIB,
|
|
)
|
|
)
|
|
assert decision.action == "drain"
|
|
assert decision.pressure == "critical"
|