fix: review findings (async SQS dispatcher, backoff, batch insert, shutdown order, pagination, HMAC) + loadtest fixes; v0.1.6
This commit is contained in:
+7
-3
@@ -54,9 +54,13 @@ python3 -m loadtests.run soak --devices 50 --rate 1 --duration 3600 --slice-sec
|
||||
|
||||
## Ограничения
|
||||
|
||||
- API отдаёт максимум **1000 строк** телеметрии на запрос → верификатор
|
||||
считает окно ~900 сообщений на устройство. Сценарии с большим expected
|
||||
на устройство недосчитают (для точных потерь — дробить на прогоны).
|
||||
- **SQS-лимит 256KB** на сообщение (shared-sqs отклоняет: `InvalidParameterValue
|
||||
message size exceeds the limit`) → payload устройств ≤ ~250KB с учётом
|
||||
envelope. Бридж дропает oversize с логом `payload too large for SQS`.
|
||||
EMQX пропускает до 1MB, но узким местом конвейера является SQS.
|
||||
- API отдаёт максимум **1000 строк** телеметрии на запрос; шлюз платформы
|
||||
рвёт ответы >~15КБ (баг MSS/MTU, тикет Nubes) → верификатор ходит
|
||||
страницами по 50 (`offset`). Большие страницы НЕ использовать.
|
||||
- Задержка = `ts(PG) - sent_at - skew`; skew калибруется первым сообщением
|
||||
(часы тест-машины и PG могут расходиться).
|
||||
- Consumer long-poll до ~20с → после публикаций нужна пауза перед сверкой
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+19
-9
@@ -49,13 +49,23 @@ class API:
|
||||
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())
|
||||
# Внешний путь платформы даёт таймауты ~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",
|
||||
@@ -70,8 +80,8 @@ class API:
|
||||
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}"
|
||||
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}")
|
||||
|
||||
+19
-15
@@ -118,22 +118,21 @@ def burst(args):
|
||||
duration=spike, qos=args.qos)
|
||||
tail = _run_publishers(run_id + "-t", ns, devices,
|
||||
rate=args.rate, duration=quiet, qos=args.qos)
|
||||
expected_low = max(1, int(args.rate * quiet))
|
||||
expected_high = max(1, int(args.rate * args.burst_mult * spike))
|
||||
rep = Verifier(api, ns, run_id)
|
||||
# считаем только фазу всплеска (низкие фазы — фон)
|
||||
rep_high = rep.run_report(devices, expected_high)
|
||||
# recovery: сколько времени после окончания всплеска очередь отдала всё
|
||||
# Фазы различаются суффиксом run_id — верифицируем фазу "-h" отдельно.
|
||||
high_sent = high.get("sent", 0)
|
||||
expected_per_device = max(1, high_sent // max(1, len(devices)))
|
||||
rep_high = Verifier(api, ns, run_id + "-h").run_report(
|
||||
devices, expected_per_device)
|
||||
# recovery: сколько времени после конца всплеска очередь отдала всё
|
||||
_, recovery_s = _wait_delivery(
|
||||
api, ns, run_id + "-h", devices, expected_high * len(devices),
|
||||
timeout=300)
|
||||
api, ns, run_id + "-h", devices, high_sent, timeout=300)
|
||||
return {
|
||||
"scenario": "burst",
|
||||
"ns": ns,
|
||||
"quiet_phase": low,
|
||||
"spike_phase": high,
|
||||
"tail_phase": tail,
|
||||
"expected_high_total": expected_high * len(devices),
|
||||
"sent_high_total": high_sent,
|
||||
"recovery_sec": round(recovery_s, 1),
|
||||
"high": rep_high,
|
||||
}
|
||||
@@ -143,6 +142,9 @@ def burst(args):
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 3. LARGE_PAYLOAD — крупные сообщения
|
||||
# Верификация через API невозможна: строка с большим payload в ответе >15 КБ,
|
||||
# а шлюз платформы рвёт такие ответы (баг MSS/MTU, тикет Nubes). Доставку
|
||||
# проверять по логам монолита: 'consumer: telemetry saved'.
|
||||
# --------------------------------------------------------------------------
|
||||
def large_payload(args):
|
||||
run_id = f"big-{int(time.time())}"
|
||||
@@ -150,15 +152,17 @@ def large_payload(args):
|
||||
api = API()
|
||||
devices = _create_devices(api, ns, run_id, min(args.devices, 10))
|
||||
try:
|
||||
expected = max(1, int(args.rate * args.duration))
|
||||
counters = _run_publishers(
|
||||
run_id, ns, devices, rate=args.rate, duration=args.duration,
|
||||
qos=args.qos, payload_size=args.payload_size)
|
||||
time.sleep(10)
|
||||
rep = Verifier(api, ns, run_id).run_report(devices, expected)
|
||||
rep.update({"scenario": "large_payload", "ns": ns,
|
||||
"publisher": counters})
|
||||
return rep
|
||||
return {
|
||||
"scenario": "large_payload",
|
||||
"ns": ns,
|
||||
"payload_size": args.payload_size,
|
||||
"publisher": counters,
|
||||
"note": "доставку смотреть в логах монолита (telemetry saved); "
|
||||
"API-верификация невозможна из-за бага шлюза (>15KB ответ)",
|
||||
}
|
||||
finally:
|
||||
_cleanup(api, ns, devices)
|
||||
|
||||
|
||||
+15
-4
@@ -28,11 +28,22 @@ class Verifier:
|
||||
|
||||
def device_report(self, device_id, expected):
|
||||
"""Сверка по одному устройству. expected — сколько seq ждали.
|
||||
Ограничение: API отдаёт максимум 1000 строк на устройство —
|
||||
сценарии должны укладывать expected в ~900 на устройство."""
|
||||
rows = self.api.telemetry(self.ns, device_id=device_id, limit=1000)
|
||||
Пагинация по 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.get("items", []):
|
||||
for r in rows:
|
||||
p = r.get("payload") or {}
|
||||
if p.get("run_id") != self.run_id:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user