# 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 # Внешний путь платформы даёт таймауты ~31-33с (~5.5% запросов) — # ретраи 3 с паузами (см. doc/sqs-integration.md, тикет Nubes). last = None for attempt in range(3): try: req = urllib.request.Request( url, data=data, method=method, headers={"Authorization": "Bearer " + self.token, "Content-Type": "application/json"}, ) with urllib.request.urlopen(req, timeout=15, context=self.ctx) as r: raw = r.read().decode() return json.loads(raw) if raw.strip() else {} except Exception as e: last = e time.sleep(1 + attempt) raise last 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, offset=0): q = f"?limit={limit}&offset={offset}" 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)