- Synology: what each disk and volume reads and writes, bytes and requests a second and the load DSM gives it (SYNOLOGY-STORAGEIO- and -SPACEIO-MIB, 64-bit byte counters), and each disk's reconnects and sectors not found. - Linux server: CPU waiting on disk, the wait tick counter's share of the CPU's, and memory available (free + buffers + cache). A reading may now be worked out from others: the poll asks for its inputs, rates each counter, and stores only the result. - Interface traffic: errors and discards in and out, per second. - byte_count unit: a byte counter shown as bytes a second. Asustor's MIB has nothing more for this; its Linux server readings gain the same CPU wait and available memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
4.8 KiB
Python
111 lines
4.8 KiB
Python
"""An SNMP poll collects on schedule while the database hangs, and what it
|
|
held is stored oldest first, each at its own time, once it answers (#5995)."""
|
|
import asyncio
|
|
from contextlib import asynccontextmanager
|
|
from types import SimpleNamespace as NS
|
|
|
|
import pytest
|
|
|
|
import plugins.snmp.scheduler as scheduler
|
|
import plugins.snmp.service as service
|
|
from plugins.snmp.poller import Credentials, PollResult
|
|
|
|
|
|
def _device(presets=("system",)):
|
|
return NS(id="d1", name="nas", address="10.0.0.5", port=161, version="2c", presets=list(presets),
|
|
interfaces="", custom=None, tables={}, tables_at=None, last_error=None, texts={})
|
|
|
|
|
|
@asynccontextmanager
|
|
async def _hung_session():
|
|
await asyncio.Event().wait() # never answers: the device list comes from memory
|
|
yield None
|
|
|
|
|
|
def test_polls_go_on_while_the_database_hangs_and_are_stored_in_order_after(monkeypatch):
|
|
async def main():
|
|
stalled = asyncio.Event()
|
|
|
|
@asynccontextmanager
|
|
async def hung_session():
|
|
await stalled.wait() # the storage under Postgres has stalled
|
|
yield None
|
|
|
|
app = NS(db_sessionmaker=hung_session)
|
|
gate, stored = asyncio.Event(), []
|
|
|
|
async def store(app_, rounds):
|
|
await gate.wait()
|
|
stored.extend(rounds)
|
|
|
|
uptimes = iter([100.0, 200.0])
|
|
|
|
async def poll(address, port, creds, oids):
|
|
return PollResult(readings={"uptime": next(uptimes)})
|
|
|
|
monkeypatch.setattr(scheduler, "DEVICES", [_device()])
|
|
monkeypatch.setattr(scheduler, "DEVICES_READ_S", 0.05)
|
|
monkeypatch.setattr(scheduler, "STORE_WAIT_S", 0.05)
|
|
monkeypatch.setattr(scheduler, "_store", store)
|
|
monkeypatch.setattr(service, "credentials", lambda d: Credentials(community="public"))
|
|
scheduler.RATES = None
|
|
|
|
await scheduler._do_poll(app, poll=poll) # the device list is the last one read
|
|
await asyncio.sleep(0.01)
|
|
await scheduler._do_poll(app, poll=poll) # the first store hasn't returned: polled anyway
|
|
gate.set()
|
|
await app._holdings["SNMP poll"].flush(1.0)
|
|
return stored
|
|
|
|
rounds = asyncio.run(main())
|
|
assert [[(p.name, p.values) for p in r] for r in rounds] == [
|
|
[("nas", [("uptime", 100.0)])], [("nas", [("uptime", 200.0)])]]
|
|
assert rounds[0][0].at < rounds[1][0].at # each stored at the time it was collected
|
|
|
|
|
|
def test_a_reading_worked_out_from_others_is_asked_for_by_its_inputs_and_kept_alone(monkeypatch):
|
|
"""#5997: CPU waiting on disk from the tick counters' rates, and available
|
|
memory from three gauges; the inputs are neither stored nor named missing."""
|
|
mem = {"memory_free_kb": 1000.0, "memory_buffers_kb": 200.0, "memory_cached_kb": 3000.0}
|
|
answers = iter([
|
|
{"cpu_ticks_user": 1000.0, "cpu_ticks_nice": 0.0, "cpu_ticks_system": 500.0,
|
|
"cpu_ticks_idle": 8000.0, "cpu_ticks_wait": 500.0} | mem,
|
|
{"cpu_ticks_user": 1100.0, "cpu_ticks_nice": 0.0, "cpu_ticks_system": 550.0,
|
|
"cpu_ticks_idle": 8800.0, "cpu_ticks_wait": 550.0} | mem,
|
|
{"cpu_ticks_user": 1200.0, "cpu_ticks_nice": 0.0, "cpu_ticks_system": 600.0,
|
|
"cpu_ticks_idle": 9600.0} | mem]) # this time it doesn't answer the wait counter
|
|
asked, stored = [], []
|
|
|
|
async def poll(address, port, creds, oids):
|
|
asked.append(dict((k, o) for o, k in oids))
|
|
got = next(answers)
|
|
return PollResult(readings=got, missing=[k for _, k in oids if k not in got])
|
|
|
|
async def store(app_, rounds):
|
|
stored.extend(rounds)
|
|
|
|
async def main():
|
|
monkeypatch.setattr(scheduler, "DEVICES", [_device(["linux"])])
|
|
monkeypatch.setattr(scheduler, "DEVICES_READ_S", 0.05)
|
|
monkeypatch.setattr(scheduler, "_store", store)
|
|
monkeypatch.setattr(service, "credentials", lambda d: Credentials(community="public"))
|
|
scheduler.RATES = None
|
|
app = NS(db_sessionmaker=_hung_session)
|
|
for _ in range(3):
|
|
await scheduler._do_poll(app, poll=poll, walk=_no_tables)
|
|
await asyncio.sleep(0.01)
|
|
|
|
asyncio.run(main())
|
|
assert asked[0]["cpu_ticks_wait"] == "1.3.6.1.4.1.2021.11.54.0" and "cpu_iowait" not in asked[0]
|
|
assert "" not in asked[0].values()
|
|
first, second, third = (dict(r[0].values) for r in stored)
|
|
assert first == {"memory_available": 4200.0} # a counter needs two samples
|
|
assert second == {"cpu_iowait": pytest.approx(5.0), "memory_available": 4200.0} # 50 of 1,000 ticks waited
|
|
assert third == {"memory_available": 4200.0}
|
|
assert "cpu_iowait" in stored[2][0].missing and not any(k.startswith("cpu_ticks") for k in stored[2][0].missing)
|
|
|
|
|
|
async def _no_tables(address, port, creds, tables):
|
|
from plugins.snmp.poller import TableScan
|
|
return TableScan(rows={})
|