Files
IoT/loadtests/verifier.py
T

103 lines
4.0 KiB
Python

# verifier.py — проверка доставки: потери, дубли, задержка.
#
# Схема: паблишер кладёт в payload run_id + seq + sent_at (epoch ms).
# Верификатор читает телеметрию через API и сверяет:
# - delivered — сколько строк с run_id дошло;
# - lost — сколько seq не доехало;
# - duplicates — сколько seq приехало более одного раза;
# - latency — ts(PG) - sent_at (epoch ms). Точность зависит от
# синхронизации часов тест-машины и платформы (обе NTP).
from datetime import datetime
from .common import latency_stats
def _ts_to_ms(ts):
"""'2026-08-16T14:23:22.363648+03:00' → epoch ms (UTC)."""
try:
return datetime.fromisoformat(ts).timestamp() * 1000
except (ValueError, TypeError):
return None
class Verifier:
def __init__(self, api, ns, run_id):
self.api = api
self.ns = ns
self.run_id = run_id
def device_report(self, device_id, expected):
"""Сверка по одному устройству. expected — сколько seq ждали.
Пагинация по 50 строк: шлюз платформы рвёт ответы >~15 КБ
(баг MSS/MTU, тикет Nubes) — большие страницы НЕ использовать."""
rows = []
offset = 0
pages = 0
while pages < 40:
page = self.api.telemetry(self.ns, device_id=device_id,
limit=50, offset=offset)
items = page.get("items", [])
rows.extend(items)
pages += 1
offset += 50
if len(items) < 50 or len(rows) >= max(expected + 100, 200):
break
seqs = {}
for r in rows:
p = r.get("payload") or {}
if p.get("run_id") != self.run_id:
continue
s = p.get("seq")
if s is None:
continue
seqs.setdefault(s, []).append(
(r.get("ts"), p.get("sent_at"))
)
delivered = len(seqs)
lost = max(0, expected - delivered)
duplicates = sum(len(v) - 1 for v in seqs.values() if len(v) > 1)
lat = []
first_lat = None
for s, entries in seqs.items():
ts, sent = entries[0]
ts_ms = _ts_to_ms(ts) if ts else None
if ts_ms is not None and isinstance(sent, (int, float)):
l = max(0.0, ts_ms - sent)
lat.append(l)
if s == 0:
first_lat = l
return {
"device_id": device_id,
"expected": expected,
"delivered": delivered,
"lost": lost,
"duplicates": duplicates,
"lat_ms": lat,
"latency": latency_stats(lat),
"first_latency_ms": round(first_lat, 1) if first_lat is not None else None,
}
def run_report(self, devices, expected_per_device):
"""Сводка по всем устройствам. Возвращает dict метрик."""
total_expected = expected_per_device * len(devices)
total_delivered = total_lost = total_dups = 0
per = []
all_lat = []
for d in devices:
r = self.device_report(d["device_id"], expected_per_device)
per.append(r)
total_delivered += r["delivered"]
total_lost += r["lost"]
total_dups += r["duplicates"]
all_lat += r["lat_ms"]
return {
"devices": len(devices),
"expected": total_expected,
"delivered": total_delivered,
"lost": total_lost,
"loss_rate": round(total_lost / total_expected, 6) if total_expected else 0.0,
"duplicates": total_dups,
"latency": latency_stats(all_lat),
"per_device": per,
}