215 lines
7.7 KiB
Python
215 lines
7.7 KiB
Python
import pytest
|
|
|
|
"""Tests for reclaiming the virtiofs mount's file handles.
|
|
These guard a mechanism that exists only because of an environment defect: the
|
|
mount's daemon holds a host descriptor per inode the guest looks up, and runs out.
|
|
The properties that matter are that reclaim is *actually verified* rather than
|
|
assumed, and that the expensive fallback is used only when it must be.
|
|
"""
|
|
|
|
|
|
from suvi import vfs
|
|
|
|
|
|
def test_drop_caches_reports_failure_without_root(monkeypatch):
|
|
"""Must return False rather than raise, so the fallback can run."""
|
|
def refuse(*args, **kwargs):
|
|
raise PermissionError(13, "Permission denied")
|
|
|
|
monkeypatch.setattr("builtins.open", refuse)
|
|
assert vfs.drop_caches() is False
|
|
|
|
|
|
def test_reclaimable_kb_reads_the_slab():
|
|
assert vfs.reclaimable_kb() > 0 # real /proc/meminfo
|
|
|
|
|
|
def test_reclaimable_kb_survives_a_broken_meminfo(monkeypatch):
|
|
monkeypatch.setattr("builtins.open", lambda *a, **k: (_ for _ in ()).throw(OSError()))
|
|
assert vfs.reclaimable_kb() == 0
|
|
|
|
|
|
def test_release_prefers_drop_caches(monkeypatch):
|
|
"""The fallback allocates tens of GiB; it must not run when drop_caches works."""
|
|
monkeypatch.setattr(vfs, "drop_caches", lambda: True)
|
|
|
|
def explode(*args, **kwargs):
|
|
raise AssertionError("allocated memory despite drop_caches succeeding")
|
|
|
|
monkeypatch.setattr(vfs, "bytearray", explode, raising=False)
|
|
assert vfs.release_handles() is True
|
|
|
|
|
|
def test_fallback_reports_failure_when_nothing_is_reclaimed(monkeypatch):
|
|
monkeypatch.setattr(vfs, "drop_caches", lambda: False)
|
|
monkeypatch.setattr(vfs, "reclaimable_kb", lambda: 1000)
|
|
assert vfs.release_handles(budget_gib=1, floor_kb=0) is False
|
|
|
|
|
|
def test_release_gives_up_when_the_slab_cannot_be_read(monkeypatch):
|
|
monkeypatch.setattr(vfs, "drop_caches", lambda: False)
|
|
monkeypatch.setattr(vfs, "reclaimable_kb", lambda: 0)
|
|
assert vfs.release_handles() is False
|
|
|
|
|
|
# --------------------------------------------------------------- memory bounding
|
|
|
|
|
|
def test_available_gib_reads_meminfo():
|
|
assert vfs.available_gib() > 0
|
|
|
|
|
|
def test_pressure_stops_at_the_reserve_not_a_fraction(monkeypatch):
|
|
"""Capping at a fraction of available memory reclaims nothing.
|
|
|
|
The kernel only frees slab under genuine pressure, so leaving 40% headroom
|
|
means it never triggers: an earlier version allocated 14 GiB of a 23 GiB
|
|
allowance and freed zero, costing the machine the memory for no benefit.
|
|
Pressure must run down to a fixed reserve instead.
|
|
"""
|
|
monkeypatch.setattr(vfs, "drop_caches", lambda: False)
|
|
monkeypatch.setattr(vfs, "reclaimable_kb", lambda: 1000) # never yields
|
|
available = [20.0]
|
|
monkeypatch.setattr(vfs, "available_gib", lambda: available[0])
|
|
allocated = []
|
|
real = bytearray
|
|
|
|
def fake(n):
|
|
allocated.append(n)
|
|
available[0] -= 1.0 # each GiB claimed reduces what is available
|
|
return real(1)
|
|
|
|
monkeypatch.setattr(vfs, "bytearray", fake, raising=False)
|
|
assert vfs.release_handles(reserve_gib=2.0, floor_kb=0) is False
|
|
# Pushed from 20 GiB down to the 2 GiB reserve, not stopped at 60%.
|
|
assert 17 <= len(allocated) <= 19, f"allocated {len(allocated)} GiB"
|
|
|
|
|
|
def test_pressure_stops_as_soon_as_the_slab_yields(monkeypatch):
|
|
monkeypatch.setattr(vfs, "drop_caches", lambda: False)
|
|
readings = iter([1000, 1000, 1000, 400])
|
|
monkeypatch.setattr(vfs, "reclaimable_kb", lambda: next(readings))
|
|
monkeypatch.setattr(vfs, "available_gib", lambda: 20.0)
|
|
allocated = []
|
|
real = bytearray
|
|
monkeypatch.setattr(
|
|
vfs, "bytearray", lambda n: allocated.append(n) or real(1), raising=False
|
|
)
|
|
assert vfs.release_handles(floor_kb=0) is True
|
|
assert len(allocated) == 2, "kept allocating after the kernel gave ground"
|
|
|
|
|
|
# ------------------------------------------------------------------- Reliever
|
|
|
|
|
|
def test_reliever_fires_on_the_interval(monkeypatch):
|
|
calls = []
|
|
monkeypatch.setattr(vfs, "release_handles", lambda *a, **k: calls.append(1) or True)
|
|
reliever = vfs.Reliever(interval=10)
|
|
for _ in range(9):
|
|
reliever.tick()
|
|
assert calls == []
|
|
reliever.tick()
|
|
assert len(calls) == 1
|
|
|
|
|
|
def test_reliever_keeps_firing_across_a_long_run(monkeypatch):
|
|
calls = []
|
|
monkeypatch.setattr(vfs, "release_handles", lambda *a, **k: calls.append(1) or True)
|
|
reliever = vfs.Reliever(interval=10)
|
|
for _ in range(100):
|
|
reliever.tick()
|
|
assert len(calls) == 10, "stopped reclaiming partway through a long traversal"
|
|
assert reliever.releases == 10
|
|
|
|
|
|
def test_reliever_counts_batched_items(monkeypatch):
|
|
"""Callers processing six bands per timestamp tick by six, not one."""
|
|
calls = []
|
|
monkeypatch.setattr(vfs, "release_handles", lambda *a, **k: calls.append(1) or True)
|
|
reliever = vfs.Reliever(interval=10)
|
|
reliever.tick(6)
|
|
reliever.tick(6)
|
|
assert len(calls) == 1
|
|
assert reliever.seen == 12
|
|
|
|
|
|
def test_reliever_can_be_disabled(monkeypatch):
|
|
def explode(*a, **k):
|
|
raise AssertionError("reclaimed while disabled")
|
|
|
|
monkeypatch.setattr(vfs, "release_handles", explode)
|
|
reliever = vfs.Reliever(interval=1, enabled=False)
|
|
for _ in range(50):
|
|
reliever.tick()
|
|
assert reliever.releases == 0
|
|
|
|
|
|
def test_reliever_reclaims_when_the_run_ends(monkeypatch):
|
|
"""Reclaiming only during a run leaves the machine loaded once it finishes.
|
|
|
|
The inodes a traversal cached stay pinned -- and with them the host's file
|
|
handles -- until something else forces reclaim. That is why the system was
|
|
still saturated with nothing of ours running.
|
|
"""
|
|
calls = []
|
|
monkeypatch.setattr(vfs, "release_handles", lambda *a, **k: calls.append(1) or True)
|
|
reliever = vfs.Reliever(interval=1000, on_exit=False)
|
|
reliever.tick(5)
|
|
assert calls == [], "should not have reclaimed mid-run yet"
|
|
reliever.finish()
|
|
assert len(calls) == 1
|
|
|
|
|
|
def test_finish_is_idempotent(monkeypatch):
|
|
calls = []
|
|
monkeypatch.setattr(vfs, "release_handles", lambda *a, **k: calls.append(1) or True)
|
|
reliever = vfs.Reliever(interval=1000, on_exit=False)
|
|
reliever.tick()
|
|
reliever.finish()
|
|
reliever.finish()
|
|
reliever.finish()
|
|
assert len(calls) == 1
|
|
|
|
|
|
def test_finish_does_nothing_if_no_work_happened(monkeypatch):
|
|
def explode(*a, **k):
|
|
raise AssertionError("reclaimed despite processing nothing")
|
|
|
|
monkeypatch.setattr(vfs, "release_handles", explode)
|
|
vfs.Reliever(on_exit=False).finish()
|
|
|
|
|
|
def test_reliever_works_as_a_context_manager(monkeypatch):
|
|
calls = []
|
|
monkeypatch.setattr(vfs, "release_handles", lambda *a, **k: calls.append(1) or True)
|
|
with vfs.Reliever(interval=1000, on_exit=False) as reliever:
|
|
reliever.tick(3)
|
|
assert calls == []
|
|
assert len(calls) == 1
|
|
|
|
|
|
def test_context_manager_reclaims_even_when_the_body_raises(monkeypatch):
|
|
calls = []
|
|
monkeypatch.setattr(vfs, "release_handles", lambda *a, **k: calls.append(1) or True)
|
|
with pytest.raises(ValueError):
|
|
with vfs.Reliever(interval=1000, on_exit=False) as reliever:
|
|
reliever.tick()
|
|
raise ValueError("boom")
|
|
assert len(calls) == 1, "a failed job must still hand its handles back"
|
|
|
|
|
|
def test_no_pressure_is_applied_when_the_slab_is_already_small(monkeypatch):
|
|
"""Below the floor the slab is live processes' working set, not archive inodes.
|
|
|
|
Squeezing the machine to shave a few hundred megabytes off it costs far more
|
|
than it gains -- and during a long job this runs every RELIEF_INTERVAL items.
|
|
"""
|
|
monkeypatch.setattr(vfs, "drop_caches", lambda: False)
|
|
monkeypatch.setattr(vfs, "reclaimable_kb", lambda: 500 * 1024) # 0.5 GiB
|
|
|
|
def explode(*a, **k):
|
|
raise AssertionError("applied memory pressure below the floor")
|
|
|
|
monkeypatch.setattr(vfs, "bytearray", explode, raising=False)
|
|
assert vfs.release_handles() is True
|