LoadTest v2: добавлена проверка приёма СООБЩЕНИЙ (/messages, случайное содержимое заданного размера)

This commit is contained in:
naeel
2026-08-18 08:49:35 +04:00
parent c0916d5642
commit 9de9bd414f
2 changed files with 240 additions and 0 deletions
+48
View File
@@ -86,6 +86,54 @@ def upload():
return jsonify(result)
@app.route("/messages", methods=["POST"])
def messages():
"""Приём СООБЩЕНИЯ в память (для проверки очередей/ИоТ).
От клиента приходит сырое тело запроса (raw body) нужного размера.
Задача - проверить, ДОХОДИТ ли сообщение нужного размера до бэкенда
(в отличие от /upload здесь нет multipart - только тело, как в очередях).
"""
t0 = time.time()
meta = {
"server_start": time.strftime("%Y-%m-%d %H:%M:%S"),
"remote_addr": request.remote_addr or "",
# Заголовки, которые обычно шлёт очередь/ИоТ-клиент.
"content_length_header": request.headers.get("Content-Length", ""),
"content_type_header": request.headers.get("Content-Type", ""),
"user_agent": request.headers.get("User-Agent", ""),
"x_amz_sns_message_type": request.headers.get("X-Amz-Sns-Message-Type", ""),
"ce_specversion": request.headers.get("Ce-Specversion", ""),
}
# Читаем ВСЁ сырое тело сообщения в память и замеряем время.
t_read = time.time()
data = request.get_data() # сырое тело без разбора (как в SQS/IoT)
read_s = time.time() - t_read
size = len(data) # сколько байт РЕАЛЬНО дошло до бэкенда
total_s = time.time() - t0
result = {
"ok": True,
"message": {
"size_bytes": size,
"size_kb": round(size / 1024, 3),
"size_mb": round(size / (1024 * 1024), 3),
"prefix": data[:64].decode("utf-8", "replace"), # первые 64 байта для наглядности
},
"server": {
"read_ms": round(read_s * 1000, 2),
"total_ms": round(total_s * 1000, 2),
"speed_mb_s": round(size / (1024 * 1024) / read_s, 3) if read_s > 0 else 0,
},
"md5": hashlib.md5(data).hexdigest(), # контрольная сумма принятых байт
"meta": meta,
}
return jsonify(result)
if __name__ == "__main__":
# ОБЯЗАТЕЛЬНО для платформы: `python site/app.py` должен стартовать сервер.
# debug=False - прод-режим, без reloader (иначе поднимается второй процесс).
+192
View File
@@ -39,8 +39,200 @@
<!-- Максимум статистики (клиент + сервер) -->
<pre id="result"></pre>
<!-- ============================================================ -->
<!-- СЕКЦИЯ СООБЩЕНИЙ (проверка очередей/ИоТ) -->
<!-- ============================================================ -->
<hr style="margin:24px 0;border:none;border-top:2px dashed #ccc;">
<h2>Сообщение (очередь / ИоТ)</h2>
<p class="hint">
Задайте размер сообщения (в байтах). Содержимое - случайное.
Проверяется, ДОХОДИТ ли тело сообщения нужного размера до бэкенда (без multipart, как в очереди).
</p>
<div class="panel">
<label for="msgSize">Размер (байт):</label>
<input type="number" id="msgSize" value="1024" min="1" step="1" style="width:180px;">
<button id="btnMsg" onclick="startMessage()">Отправить сообщение</button>
</div>
<!-- Прогресс отправки сообщения -->
<div class="progress-wrap" id="msgProgressWrap" style="display:none">
<div class="progress-bar">
<div class="progress-fill" id="msgProgressFill"></div>
</div>
<div class="progress-info">
<span id="msgProgressPct">0%</span>
<span id="msgProgressTimer">0с</span>
<span id="msgProgressSpeed"></span>
</div>
</div>
<!-- Итоговый статус сообщения -->
<div id="msgStatus"></div>
<!-- Статистика сообщения -->
<pre id="msgResult"></pre>
</div>
<script>
// ---------- Проверка приёма СООБЩЕНИЯ (очередь/ИоТ) ----------
// Тело - сырое (raw), без multipart, со случайным содержимым заданного размера.
function startMessage() {
var sizeEl = document.getElementById('msgSize');
var size = parseInt(sizeEl.value, 10);
var status = document.getElementById('msgStatus');
var result = document.getElementById('msgResult');
var wrap = document.getElementById('msgProgressWrap');
result.textContent = '';
status.textContent = '';
if (!size || size < 1) {
status.textContent = 'ОШИБКА: укажите размер';
status.className = 'status error';
return;
}
// ---- Генерируем случайное содержимое нужного размера ----
var startMs = Date.now();
var startLocal = new Date().toLocaleTimeString();
var randomStr = randomBytes(size); // строка случайных символов длиной size
// ---- Прогресс ----
wrap.style.display = 'block';
var fill = document.getElementById('msgProgressFill');
var pctEl = document.getElementById('msgProgressPct');
var timerEl = document.getElementById('msgProgressTimer');
var speedEl = document.getElementById('msgProgressSpeed');
fill.style.width = '0%';
pctEl.textContent = '0%';
timerEl.textContent = '0с';
speedEl.textContent = '';
var timer = setInterval(function () {
var sec = Math.floor((Date.now() - startMs) / 1000);
timerEl.textContent = sec + 'с';
}, 1000);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/messages');
xhr.timeout = 600000;
// Шлём как текстовое тело (как очередь обычно шлёт payload).
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.upload.onprogress = function (e) {
if (!e.lengthComputable) { pctEl.textContent = '?%'; return; }
var pct = Math.round(e.loaded / e.total * 100);
fill.style.width = pct + '%';
pctEl.textContent = pct + '%';
var sec = (Date.now() - startMs) / 1000;
if (sec > 0) speedEl.textContent = ((e.loaded / (1024 * 1024)) / sec).toFixed(2) + ' МБ/с';
};
xhr.onload = function () {
clearInterval(timer);
var sec = ((Date.now() - startMs) / 1000).toFixed(2);
var httpStatus = xhr.status;
var ok = xhr.status >= 200 && xhr.status < 300;
var data = null;
try { data = JSON.parse(xhr.responseText); } catch (e) { data = null; }
if (ok && data && data.ok) {
status.textContent = '✅ СООБЩЕНИЕ ПРИНЯТО (' + size + ' байт)';
status.className = 'status ok';
renderMsgStats(size, startLocal, sec, httpStatus, data);
} else {
status.textContent = '❌ СООБЩЕНИЕ НЕ ПРИНЯТО (HTTP ' + httpStatus + ')';
status.className = 'status error';
var errBody = data && data.error ? 'ошибка: ' + data.error : xhr.responseText;
result.textContent =
'HTTP статус: ' + httpStatus + '\n' +
'Ответ сервера: ' + (errBody || '(пусто)') + '\n' +
msgClientStats(size, startLocal, sec);
}
};
xhr.onerror = function () {
clearInterval(timer);
var sec = ((Date.now() - startMs) / 1000).toFixed(2);
status.textContent = '❌ СЕТЕВАЯ ОШИБКА - ответа нет';
status.className = 'status error';
result.textContent = msgClientStats(size, startLocal, sec) +
'\n(соединение прервано, прокси/платформа не ответила)';
};
xhr.ontimeout = function () {
clearInterval(timer);
var sec = ((Date.now() - startMs) / 1000).toFixed(2);
status.textContent = '❌ ТАЙМАУТ (10 мин) - сообщение не принято';
status.className = 'status error';
result.textContent = msgClientStats(size, startLocal, sec);
};
xhr.send(randomStr);
}
// ---- Генерация случайной строки заданной длины ----
function randomBytes(n) {
var chars = 'ABCDEF0123456789abcdefghijklmnopqrstuvwxyz';
var out = '';
// Генерируем частями (по 8192) - для очень больших n без зависания строки.
var chunk = 8192;
while (out.length < n) {
var need = Math.min(chunk, n - out.length);
var s = '';
for (var i = 0; i < need; i++) s += chars[Math.floor(Math.random() * chars.length)];
out += s;
}
return out;
}
// ---- Клиентская статистика сообщения ----
function msgClientStats(size, startLocal, sec) {
var avg = sec > 0 ? (size / (1024 * 1024) / sec).toFixed(2) : '0';
return '-- КЛИЕНТ (сообщение) --\n' +
'Размер: ' + size + ' байт (' + (size / 1024).toFixed(3) + ' КБ)\n' +
'Начало: ' + startLocal + '\n' +
'Длительность: ' + sec + ' с\n' +
'Средняя скорость: ' + avg + ' МБ/с\n';
}
// ---- Полная статистика при успехе ----
function renderMsgStats(size, startLocal, sec, httpStatus, data) {
var m = data.message;
var s = data.server;
var meta = data.meta;
var avg = sec > 0 ? (size / (1024 * 1024) / sec).toFixed(2) : '0';
var out = '';
out += '-- КЛИЕНТ (сообщение) --\n' +
'Размер: ' + size + ' байт (' + (size / 1024).toFixed(3) + ' КБ)\n' +
'Начало: ' + startLocal + '\n' +
'Длительность: ' + sec + ' с\n' +
'Средняя скорость: ' + avg + ' МБ/с\n' +
'HTTP статус: ' + httpStatus + '\n\n';
out += '-- СЕРВЕР (что реально дошло) --\n' +
'Принято байт: ' + m.size_bytes + ' (' + m.size_kb + ' КБ)\n' +
'Чтение в память: ' + s.read_ms + ' мс\n' +
'Обработка всего: ' + s.total_ms + ' мс\n' +
'Скорость (сервер): ' + s.speed_mb_s + ' МБ/с\n' +
'MD5 принятых байт: ' + data.md5 + '\n' +
'Префикс тела: ' + m.prefix + '\n\n';
out += '-- ЗАПРОС --\n' +
'Время сервера: ' + meta.server_start + '\n' +
'Remote addr: ' + meta.remote_addr + '\n' +
'Content-Length (заголовок): ' + meta.content_length_header + '\n' +
'Content-Type (заголовок): ' + meta.content_type_header + '\n' +
'User-Agent: ' + meta.user_agent;
result.textContent = out;
}
</script>
<script>
// ---------- Загрузка одного файла через XMLHttpRequest ----------
// XHR даёт реальный прогресс отправки (fetch - не даёт), поэтому берём XHR.