Files
IoT/loadtests/common.py
T

130 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# common.py — общие примитивы нагрузочных тестов IoT.
#
# Конфигурация через env (дефолты — прод-платформа):
# IOT_API — базовый URL API (https://iot.containerk8s.dev.nubes.ru)
# IOT_WS — wss URL EMQX (wss://exqx.containerk8s.dev.nubes.ru/mqtt)
# IOT_NS_PREFIX — префикс namespace для тестов (lt)
# IOT_CLEANUP — удалять созданные устройства после теста (1)
import base64
import json
import os
import ssl
import threading
import time
import urllib.request
from urllib.parse import urlparse
API = os.environ.get("IOT_API", "https://iot.containerk8s.dev.nubes.ru")
WS = os.environ.get("IOT_WS", "wss://exqx.containerk8s.dev.nubes.ru/mqtt")
NS_PREFIX = os.environ.get("IOT_NS_PREFIX", "lt")
CLEANUP = os.environ.get("IOT_CLEANUP", "1") == "1"
_ws = urlparse(WS)
WS_HOST = _ws.hostname or ""
WS_PORT = _ws.port or 443
WS_PATH = _ws.path or "/mqtt"
# Платформа рвёт внешние wss ~150с: паблишер должен уметь реконнектиться.
EDGE_BREAK_SEC = 150
def mint_jwt(sub="loadtest", ttl=86400):
h = base64.urlsafe_b64encode(b'{"alg":"none","typ":"JWT"}').rstrip(b"=").decode()
p = base64.urlsafe_b64encode(
json.dumps({"sub": sub, "exp": int(time.time()) + ttl}).encode()
).rstrip(b"=").decode()
return h + "." + p + ".sig"
class API:
"""Тонкая обёртка над REST API монолита."""
def __init__(self, base=API):
self.base = base.rstrip("/")
self.token = mint_jwt()
self.ctx = ssl.create_default_context()
self.ctx.check_hostname = False
self.ctx.verify_mode = ssl.CERT_NONE
def _req(self, method, path, body=None):
url = self.base + path
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(
url, data=data, method=method,
headers={"Authorization": "Bearer " + self.token,
"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=30, context=self.ctx) as r:
return json.loads(r.read().decode())
def create_device(self, ns, name, device_id):
return self._req("POST", f"/v1/namespaces/{ns}/iot/devices",
{"name": name, "device_id": device_id})
def get_device(self, ns, name):
return self._req("GET", f"/v1/namespaces/{ns}/iot/devices/{name}")
def list_devices(self, ns):
return self._req("GET", f"/v1/namespaces/{ns}/iot/devices")
def delete_device(self, ns, name):
return self._req("DELETE", f"/v1/namespaces/{ns}/iot/devices/{name}")
def telemetry(self, ns, device_id="", limit=1000):
q = f"?limit={limit}"
if device_id:
q += f"&device_id={device_id}"
return self._req("GET", f"/v1/namespaces/{ns}/iot/telemetry{q}")
class Counter:
"""Потокобезопасные счётчики метрик."""
def __init__(self):
self._l = threading.Lock()
self.v = {}
def inc(self, key, n=1):
with self._l:
self.v[key] = self.v.get(key, 0) + n
def set(self, key, val):
with self._l:
self.v[key] = val
def get(self, key, default=0):
with self._l:
return self.v.get(key, default)
def snapshot(self):
with self._l:
return dict(self.v)
def ws_tls_opts(client):
"""wss с самоподписанным/платформенным сертификатом — без проверки."""
client.tls_set(cert_reqs=ssl.CERT_NONE)
client.tls_insecure_set(True)
def percentile(values, q):
if not values:
return 0.0
s = sorted(values)
i = min(len(s) - 1, int(round(q * (len(s) - 1))))
return s[i]
def latency_stats(ms_list):
return {
"n": len(ms_list),
"p50_ms": round(percentile(ms_list, 0.50), 1),
"p95_ms": round(percentile(ms_list, 0.95), 1),
"p99_ms": round(percentile(ms_list, 0.99), 1),
"max_ms": round(max(ms_list), 1) if ms_list else 0.0,
}
def now_ms():
return int(time.time() * 1000)