Refactor network requests: consolidate retry logic with backoff, handle transient errors, and improve timeout handling across NetworkService methods. Emit result signals for monitoring dumps.
This commit is contained in:
parent
cef3515790
commit
fac49447bf
@ -56,6 +56,42 @@ inline QString networkErrorCategory(QNetworkReply::NetworkError err) {
|
||||
}
|
||||
}
|
||||
|
||||
// Транзиентная ошибка CONNECT-фазы: сбой произошёл ДО того, как байты запроса
|
||||
// ушли на сервер (соединение не установилось), поэтому повтор безопасен даже для
|
||||
// неидемпотентных POST — дубля быть не может. Сюда же ETIMEDOUT-connect, который
|
||||
// Qt отдаёт как UnknownNetworkError "Connection timed out".
|
||||
inline bool isConnectPhaseTransient(QNetworkReply::NetworkError err) {
|
||||
switch (err) {
|
||||
case QNetworkReply::ConnectionRefusedError:
|
||||
case QNetworkReply::HostNotFoundError:
|
||||
case QNetworkReply::ProxyConnectionRefusedError:
|
||||
case QNetworkReply::ProxyNotFoundError:
|
||||
case QNetworkReply::ProxyTimeoutError:
|
||||
case QNetworkReply::UnknownNetworkError:
|
||||
case QNetworkReply::TemporaryNetworkFailureError:
|
||||
case QNetworkReply::NetworkSessionFailedError:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Более широкий набор транзиентных ошибок — безопасен ТОЛЬКО для идемпотентных
|
||||
// запросов (GET/PATCH/DELETE, аплоады), т.к. сбой мог случиться уже ПОСЛЕ отправки
|
||||
// тела (сервер мог его обработать) → повтор неидемпотентного POST дал бы дубль.
|
||||
inline bool isIdempotentTransient(QNetworkReply::NetworkError err) {
|
||||
if (isConnectPhaseTransient(err)) return true;
|
||||
switch (err) {
|
||||
case QNetworkReply::RemoteHostClosedError:
|
||||
case QNetworkReply::TimeoutError:
|
||||
case QNetworkReply::ProxyConnectionClosedError:
|
||||
case QNetworkReply::ServiceUnavailableError:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Развёрнутое описание сетевой ошибки: HTTP-код + категория или просто
|
||||
// категория + Qt errorString. Эндпоинт — без хоста.
|
||||
inline QString describeNetError(QNetworkReply *reply, const QString &path) {
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
#include <QTimer>
|
||||
#include <QTimeZone>
|
||||
#include <QMutexLocker>
|
||||
#include <QEventLoop>
|
||||
#include <functional>
|
||||
|
||||
#include "dao/BankDAO.h"
|
||||
#include "dao/GeneralLogDAO.h"
|
||||
@ -34,31 +36,101 @@
|
||||
using NetworkErrorUtils::sanitizePath;
|
||||
using NetworkErrorUtils::describeNetError;
|
||||
using NetworkErrorUtils::describeClientTimeout;
|
||||
using NetworkErrorUtils::isConnectPhaseTransient;
|
||||
using NetworkErrorUtils::isIdempotentTransient;
|
||||
|
||||
namespace {
|
||||
// Прокручивает event loop пока reply не завершится либо не сработает client timeout.
|
||||
// true — reply finished (success или network error), false — сработал наш QTimer.
|
||||
bool awaitReply(QNetworkReply *reply, int timeoutMs) {
|
||||
enum class AwaitOutcome { FinishedClean, TransportError, ClientTimeout };
|
||||
|
||||
// Прокручивает event loop пока reply не завершится (finished приходит и на успех,
|
||||
// и сразу после errorOccurred) либо не сработает наш QTimer. Раньше здесь отдельно
|
||||
// коннектился errorOccurred → loop.quit(), из-за чего транспортная ошибка считалась
|
||||
// «успешным завершением» и цикл ретраев делал break на первой же Connection timed out.
|
||||
// FinishedClean — reply завершился без ошибки;
|
||||
// TransportError — reply завершился, но reply->error() != NoError;
|
||||
// ClientTimeout — наш таймер сработал раньше, reply ещё в полёте (нужен abort()).
|
||||
AwaitOutcome awaitReply(QNetworkReply *reply, int timeoutMs) {
|
||||
QEventLoop loop;
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
QObject::connect(reply, &QNetworkReply::errorOccurred, &loop,
|
||||
[&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
||||
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
timer.start(timeoutMs);
|
||||
loop.exec();
|
||||
return timer.isActive();
|
||||
if (!timer.isActive()) return AwaitOutcome::ClientTimeout;
|
||||
return reply->error() == QNetworkReply::NoError
|
||||
? AwaitOutcome::FinishedClean
|
||||
: AwaitOutcome::TransportError;
|
||||
}
|
||||
|
||||
// На client timeout повторяем запрос ещё 2 раза (всего до 3 попыток).
|
||||
// На HTTP/network ошибках не ретраим — они детерминированы (401/409/refused/etc).
|
||||
constexpr int kMaxRequestAttempts = 3;
|
||||
// Ретраим при (а) нашем client-timeout и (б) ТРАНЗИЕНТНЫХ транспортных ошибках.
|
||||
// Для неидемпотентных запросов (POST) — только connect-phase сбои (соединение не
|
||||
// установилось ⇒ сервер запрос не видел ⇒ дубля быть не может); для идемпотентных
|
||||
// (GET/PATCH/DELETE/аплоады) набор шире (см. NetworkErrorUtils.h). Детерминированные
|
||||
// HTTP-ошибки (401/404/409, SSL) НЕ ретраим — их разбирает вызывающий.
|
||||
constexpr int kMaxRequestAttempts = 5;
|
||||
constexpr int kDefaultTimeoutMs = 30000;
|
||||
constexpr int kDumpTimeoutMs = 120000;
|
||||
constexpr int kDumpTimeoutMs = 300000; // архив с БД по медленному каналу
|
||||
// Мониторинг — best-effort: одна попытка, короткий таймаут. Failure не должен
|
||||
// блокировать потоки и накапливать QThread'ы при недоступном сервере.
|
||||
constexpr int kMonitoringTimeoutMs = 10000;
|
||||
constexpr int kBackoffBaseMs = 1000;
|
||||
constexpr int kBackoffMaxMs = 4000;
|
||||
|
||||
// Пауза между попытками: 1с, 2с, 4с (cap). Через QEventLoop, а не QThread::sleep —
|
||||
// event loop потока остаётся живым (abort/deleteLater/queued-слоты продолжают идти).
|
||||
void backoffSleep(int attempt) {
|
||||
const int ms = qMin(kBackoffBaseMs << (attempt - 1), kBackoffMaxMs);
|
||||
QEventLoop loop;
|
||||
QTimer::singleShot(ms, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
struct SendOutcome {
|
||||
QNetworkReply *reply = nullptr; // последний reply; вызывающий парсит и делает deleteLater
|
||||
bool clientTimedOut = false; // финальная попытка упёрлась в наш QTimer (reply aborted)
|
||||
QString timeoutDetail; // describeClientTimeout последнего таймаута
|
||||
};
|
||||
|
||||
// Выполняет issueRequest() с таймаутом и ретраями транзиентных сбоев + бэкоффом.
|
||||
// issueRequest должен КАЖДЫЙ раз создавать новый reply (и, для multipart, новый
|
||||
// QHttpMultiPart — он потребляется за попытку). Возвращает последний reply, чтобы
|
||||
// вызывающий сохранил свой разбор ответа/ошибки.
|
||||
SendOutcome sendWithRetry(const std::function<QNetworkReply*()> &issueRequest,
|
||||
int timeoutMs, const QString &path,
|
||||
const char *method, bool idempotent) {
|
||||
QNetworkReply *reply = nullptr;
|
||||
QString timeoutDetail;
|
||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||
reply = issueRequest();
|
||||
switch (awaitReply(reply, timeoutMs)) {
|
||||
case AwaitOutcome::FinishedClean:
|
||||
return { reply, false, {} };
|
||||
case AwaitOutcome::TransportError: {
|
||||
const QNetworkReply::NetworkError err = reply->error();
|
||||
const bool transient = isConnectPhaseTransient(err)
|
||||
|| (idempotent && isIdempotentTransient(err));
|
||||
if (transient && attempt < kMaxRequestAttempts) {
|
||||
qCritical().noquote() << "Network transient error (" << method << ") attempt"
|
||||
<< attempt << "/" << kMaxRequestAttempts << ":"
|
||||
<< describeNetError(reply, path) << "— retrying after backoff";
|
||||
backoffSleep(attempt);
|
||||
continue;
|
||||
}
|
||||
return { reply, false, {} }; // детерминированная ошибка или попытки исчерпаны
|
||||
}
|
||||
case AwaitOutcome::ClientTimeout:
|
||||
timeoutDetail = describeClientTimeout(reply, path);
|
||||
qCritical().noquote() << "Network timeout (" << method << ") attempt" << attempt
|
||||
<< "/" << kMaxRequestAttempts << ":" << timeoutDetail;
|
||||
reply->abort();
|
||||
if (attempt < kMaxRequestAttempts) { backoffSleep(attempt); continue; }
|
||||
return { reply, true, timeoutDetail };
|
||||
}
|
||||
}
|
||||
return { reply, true, timeoutDetail }; // недостижимо
|
||||
}
|
||||
} // namespace
|
||||
|
||||
NetworkService::NetworkService(QObject *parent) : QObject(parent) {
|
||||
@ -116,28 +188,24 @@ void addFormMetadata(QHttpMultiPart *multiPart, const QByteArray &fieldValue) {
|
||||
multiPart->append(jsonPart);
|
||||
}
|
||||
|
||||
QJsonValue NetworkService::post(QHttpMultiPart *multiPart, const QString &path) {
|
||||
QJsonValue NetworkService::post(const std::function<QHttpMultiPart*()> &buildMultiPart,
|
||||
const QString &path, bool idempotent) {
|
||||
const QUrl url(path);
|
||||
const QNetworkRequest request(url);
|
||||
|
||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
timeout.setSingleShot(true);
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
|
||||
loop.quit();
|
||||
});
|
||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
timeout.start(30000);
|
||||
loop.exec();
|
||||
const SendOutcome outcome = sendWithRetry(
|
||||
[&] {
|
||||
QHttpMultiPart *multiPart = buildMultiPart();
|
||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
return reply;
|
||||
},
|
||||
kDefaultTimeoutMs, path, "POST(mp)", idempotent);
|
||||
QNetworkReply *reply = outcome.reply;
|
||||
|
||||
QJsonValue result;
|
||||
if (!timeout.isActive()) {
|
||||
qCritical().noquote() << "Network timeout:" << describeClientTimeout(reply, path);
|
||||
reply->abort();
|
||||
if (outcome.clientTimedOut) {
|
||||
qCritical().noquote() << "Network timeout:" << outcome.timeoutDetail;
|
||||
} else if (reply->error() == QNetworkReply::NoError) {
|
||||
const QByteArray raw = reply->readAll();
|
||||
QJsonParseError err;
|
||||
@ -167,18 +235,12 @@ QJsonArray NetworkService::get(const QString &path, const QMap<QString, QString>
|
||||
|
||||
const QNetworkRequest request(url);
|
||||
|
||||
QNetworkReply *reply = nullptr;
|
||||
bool timedOut = true;
|
||||
QString lastTimeoutDetail;
|
||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||
reply = m_manager->get(request);
|
||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||
lastTimeoutDetail = describeClientTimeout(reply, path);
|
||||
qCritical().noquote() << "Network timeout (GET) attempt" << attempt
|
||||
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
||||
reply->abort();
|
||||
}
|
||||
const SendOutcome outcome = sendWithRetry(
|
||||
[&] { return m_manager->get(request); },
|
||||
kDefaultTimeoutMs, path, "GET", /*idempotent=*/true);
|
||||
QNetworkReply *reply = outcome.reply;
|
||||
const bool timedOut = outcome.clientTimedOut;
|
||||
const QString lastTimeoutDetail = outcome.timeoutDetail;
|
||||
|
||||
QJsonArray result;
|
||||
if (timedOut) {
|
||||
@ -245,13 +307,13 @@ void NetworkService::getPayments() {
|
||||
}
|
||||
|
||||
void NetworkService::postAppStatus() {
|
||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
|
||||
addFormMetadata(multiPart, m_json);
|
||||
addFormField(multiPart, "token", m_token);
|
||||
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
||||
|
||||
const bool isSuccess = post(multiPart, m_urlAppStatusUpdate).toBool();
|
||||
const bool isSuccess = post([this] {
|
||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
addFormMetadata(multiPart, m_json);
|
||||
addFormField(multiPart, "token", m_token);
|
||||
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
||||
return multiPart;
|
||||
}, m_urlAppStatusUpdate, /*idempotent=*/false).toBool();
|
||||
if (!isSuccess) {
|
||||
qCritical() << "Cant post data: postAppStatus";
|
||||
}
|
||||
@ -271,18 +333,12 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
|
||||
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
||||
const QString shortPath = sanitizePath(path);
|
||||
|
||||
QNetworkReply *reply = nullptr;
|
||||
bool timedOut = true;
|
||||
QString lastTimeoutDetail;
|
||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||
reply = m_manager->post(request, bodyBytes);
|
||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||
lastTimeoutDetail = describeClientTimeout(reply, path);
|
||||
qCritical().noquote() << "Network timeout (POST) attempt" << attempt
|
||||
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
||||
reply->abort();
|
||||
}
|
||||
const SendOutcome outcome = sendWithRetry(
|
||||
[&] { return m_manager->post(request, bodyBytes); },
|
||||
kDefaultTimeoutMs, path, "POST", /*idempotent=*/false);
|
||||
QNetworkReply *reply = outcome.reply;
|
||||
const bool timedOut = outcome.clientTimedOut;
|
||||
const QString lastTimeoutDetail = outcome.timeoutDetail;
|
||||
|
||||
QJsonValue result;
|
||||
m_lastErrorSummary.clear();
|
||||
@ -515,11 +571,11 @@ void NetworkService::finishSetupWithDesktop(const QString &desktopId) {
|
||||
|
||||
|
||||
void NetworkService::postEventStatus() {
|
||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
|
||||
addFormMetadata(multiPart, m_json);
|
||||
|
||||
const bool isSuccess = post(multiPart, m_urlEventStatusUpdate).toBool();
|
||||
const bool isSuccess = post([this] {
|
||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
addFormMetadata(multiPart, m_json);
|
||||
return multiPart;
|
||||
}, m_urlEventStatusUpdate, /*idempotent=*/false).toBool();
|
||||
if (!isSuccess) {
|
||||
qCritical() << "Cant post data: postEventStatus";
|
||||
}
|
||||
@ -528,14 +584,14 @@ void NetworkService::postEventStatus() {
|
||||
}
|
||||
|
||||
void NetworkService::updateTransactionData() {
|
||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
|
||||
addFormMetadata(multiPart, m_json);
|
||||
addFormField(multiPart, "token", m_token);
|
||||
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
||||
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
|
||||
|
||||
const bool isSuccess = post(multiPart, m_urlTransactionUpdate).toBool();
|
||||
const bool isSuccess = post([this] {
|
||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
addFormMetadata(multiPart, m_json);
|
||||
addFormField(multiPart, "token", m_token);
|
||||
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
||||
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
|
||||
return multiPart;
|
||||
}, m_urlTransactionUpdate, /*idempotent=*/false).toBool();
|
||||
if (!isSuccess) {
|
||||
qCritical() << "Cant update transaction: updateTransactionData";
|
||||
}
|
||||
@ -544,14 +600,14 @@ void NetworkService::updateTransactionData() {
|
||||
}
|
||||
|
||||
void NetworkService::insertTransactionData() {
|
||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
|
||||
addFormMetadata(multiPart, m_json);
|
||||
addFormField(multiPart, "token", m_token);
|
||||
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
||||
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
|
||||
|
||||
const int id = post(multiPart, m_urlTransactionInsert).toInt();
|
||||
const int id = post([this] {
|
||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
addFormMetadata(multiPart, m_json);
|
||||
addFormField(multiPart, "token", m_token);
|
||||
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
||||
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
|
||||
return multiPart;
|
||||
}, m_urlTransactionInsert, /*idempotent=*/false).toInt();
|
||||
if (id > 0) {
|
||||
const int transactionId = m_extraData["transactionId"].toInt();
|
||||
if (!TransactionDAO::updateExternalTransactionId(transactionId, id)) {
|
||||
@ -572,18 +628,12 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod
|
||||
const QByteArray bodyBytes = QJsonDocument(body).toJson();
|
||||
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
||||
|
||||
QNetworkReply *reply = nullptr;
|
||||
bool timedOut = true;
|
||||
QString lastTimeoutDetail;
|
||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||
reply = m_manager->sendCustomRequest(request, "PATCH", bodyBytes);
|
||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||
lastTimeoutDetail = describeClientTimeout(reply, path);
|
||||
qCritical().noquote() << "Network timeout (PATCH) attempt" << attempt
|
||||
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
||||
reply->abort();
|
||||
}
|
||||
const SendOutcome outcome = sendWithRetry(
|
||||
[&] { return m_manager->sendCustomRequest(request, "PATCH", bodyBytes); },
|
||||
kDefaultTimeoutMs, path, "PATCH", /*idempotent=*/true);
|
||||
QNetworkReply *reply = outcome.reply;
|
||||
const bool timedOut = outcome.clientTimedOut;
|
||||
const QString lastTimeoutDetail = outcome.timeoutDetail;
|
||||
|
||||
QJsonValue result;
|
||||
if (timedOut) {
|
||||
@ -657,18 +707,12 @@ QJsonValue NetworkService::deleteJson(const QString &path, bool withAuth) {
|
||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = nullptr;
|
||||
bool timedOut = true;
|
||||
QString lastTimeoutDetail;
|
||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||
reply = m_manager->deleteResource(request);
|
||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||
lastTimeoutDetail = describeClientTimeout(reply, path);
|
||||
qCritical().noquote() << "Network timeout (DELETE) attempt" << attempt
|
||||
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
||||
reply->abort();
|
||||
}
|
||||
const SendOutcome outcome = sendWithRetry(
|
||||
[&] { return m_manager->deleteResource(request); },
|
||||
kDefaultTimeoutMs, path, "DELETE", /*idempotent=*/true);
|
||||
QNetworkReply *reply = outcome.reply;
|
||||
const bool timedOut = outcome.clientTimedOut;
|
||||
const QString lastTimeoutDetail = outcome.timeoutDetail;
|
||||
|
||||
QJsonValue result;
|
||||
if (timedOut) {
|
||||
@ -704,23 +748,15 @@ QJsonValue NetworkService::getJson(const QString &path, bool withAuth) {
|
||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = nullptr;
|
||||
bool timedOut = true;
|
||||
QString lastTimeoutDetail;
|
||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||
reply = m_manager->get(request);
|
||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||
lastTimeoutDetail = describeClientTimeout(reply, path);
|
||||
qCritical().noquote() << "Network timeout (getJson) attempt" << attempt
|
||||
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
||||
reply->abort();
|
||||
}
|
||||
const SendOutcome outcome = sendWithRetry(
|
||||
[&] { return m_manager->get(request); },
|
||||
kDefaultTimeoutMs, path, "getJson", /*idempotent=*/true);
|
||||
QNetworkReply *reply = outcome.reply;
|
||||
|
||||
QJsonValue result;
|
||||
if (timedOut) {
|
||||
if (outcome.clientTimedOut) {
|
||||
qCritical().noquote() << "Network timeout (getJson) after"
|
||||
<< kMaxRequestAttempts << "attempts:" << lastTimeoutDetail;
|
||||
<< kMaxRequestAttempts << "attempts:" << outcome.timeoutDetail;
|
||||
result = QJsonValue::Undefined;
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
qCritical().noquote() << "Network error (GET):" << describeNetError(reply, path)
|
||||
@ -900,18 +936,12 @@ void NetworkService::syncBanksIfEmpty() {
|
||||
const QString path = QStringLiteral("https://api.brtservice.io/api/rest/info/");
|
||||
QNetworkRequest request{QUrl(path)};
|
||||
|
||||
QNetworkReply *reply = nullptr;
|
||||
bool timedOut = true;
|
||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||
reply = m_manager->get(request);
|
||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||
qWarning().noquote() << "[syncBanksIfEmpty] timeout attempt" << attempt
|
||||
<< "/" << kMaxRequestAttempts;
|
||||
reply->abort();
|
||||
}
|
||||
const SendOutcome outcome = sendWithRetry(
|
||||
[&] { return m_manager->get(request); },
|
||||
kDefaultTimeoutMs, path, "syncBanksIfEmpty", /*idempotent=*/true);
|
||||
QNetworkReply *reply = outcome.reply;
|
||||
|
||||
if (timedOut) {
|
||||
if (outcome.clientTimedOut) {
|
||||
qWarning() << "[syncBanksIfEmpty] timeout after" << kMaxRequestAttempts << "attempts";
|
||||
reply->deleteLater();
|
||||
return;
|
||||
@ -1416,31 +1446,25 @@ void NetworkService::postDeviceScreenshot() {
|
||||
QNetworkRequest request(url);
|
||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||
|
||||
QNetworkReply *reply = nullptr;
|
||||
bool timedOut = true;
|
||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||
const SendOutcome outcome = sendWithRetry(
|
||||
[&] {
|
||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
QHttpPart filePart;
|
||||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
|
||||
filePart.setHeader(
|
||||
QNetworkRequest::ContentDispositionHeader,
|
||||
QStringLiteral("form-data; name=\"file\"; filename=\"%1.jpg\"").arg(m_deviceId)
|
||||
);
|
||||
filePart.setBody(m_json);
|
||||
multiPart->append(filePart);
|
||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
return reply;
|
||||
},
|
||||
kDefaultTimeoutMs, url.toString(), "postDeviceScreenshot", /*idempotent=*/true);
|
||||
QNetworkReply *reply = outcome.reply;
|
||||
|
||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
QHttpPart filePart;
|
||||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
|
||||
filePart.setHeader(
|
||||
QNetworkRequest::ContentDispositionHeader,
|
||||
QStringLiteral("form-data; name=\"file\"; filename=\"%1.jpg\"").arg(m_deviceId)
|
||||
);
|
||||
filePart.setBody(m_json);
|
||||
multiPart->append(filePart);
|
||||
|
||||
reply = m_manager->post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
|
||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||
qWarning() << "[NetworkService] postDeviceScreenshot timeout attempt"
|
||||
<< attempt << "/" << kMaxRequestAttempts << "for device" << device.apiId;
|
||||
reply->abort();
|
||||
}
|
||||
|
||||
if (timedOut) {
|
||||
if (outcome.clientTimedOut) {
|
||||
qWarning() << "[NetworkService] postDeviceScreenshot timeout after"
|
||||
<< kMaxRequestAttempts << "attempts for device" << device.apiId;
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
@ -1679,7 +1703,7 @@ void NetworkService::postMonitoringLog(const QString &type, const QString &event
|
||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
|
||||
if (!awaitReply(reply, kMonitoringTimeoutMs)) {
|
||||
if (awaitReply(reply, kMonitoringTimeoutMs) == AwaitOutcome::ClientTimeout) {
|
||||
qWarning() << "[NetworkService] postMonitoringLog timeout";
|
||||
reply->abort();
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
@ -1703,59 +1727,60 @@ void NetworkService::postMonitoringDump(const QString &text, const QByteArray &a
|
||||
|
||||
qDebug() << "[NetworkService] postMonitoringDump POST" << url.toString();
|
||||
|
||||
QNetworkReply *reply = nullptr;
|
||||
bool timedOut = true;
|
||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||
const SendOutcome outcome = sendWithRetry(
|
||||
[&] {
|
||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
addFormField(multiPart, "text", text);
|
||||
if (!archive.isEmpty()) {
|
||||
QHttpPart filePart;
|
||||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
|
||||
filePart.setHeader(
|
||||
QNetworkRequest::ContentDispositionHeader,
|
||||
QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(archiveFilename)
|
||||
);
|
||||
filePart.setBody(archive);
|
||||
multiPart->append(filePart);
|
||||
}
|
||||
if (!image.isEmpty()) {
|
||||
QHttpPart imagePart;
|
||||
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
|
||||
imagePart.setHeader(
|
||||
QNetworkRequest::ContentDispositionHeader,
|
||||
QStringLiteral("form-data; name=\"image\"; filename=\"final_screen.png\"")
|
||||
);
|
||||
imagePart.setBody(image);
|
||||
multiPart->append(imagePart);
|
||||
}
|
||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
return reply;
|
||||
},
|
||||
kDumpTimeoutMs, url.toString(), "postMonitoringDump", /*idempotent=*/true);
|
||||
QNetworkReply *reply = outcome.reply;
|
||||
|
||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
addFormField(multiPart, "text", text);
|
||||
if (!archive.isEmpty()) {
|
||||
QHttpPart filePart;
|
||||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
|
||||
filePart.setHeader(
|
||||
QNetworkRequest::ContentDispositionHeader,
|
||||
QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(archiveFilename)
|
||||
);
|
||||
filePart.setBody(archive);
|
||||
multiPart->append(filePart);
|
||||
}
|
||||
if (!image.isEmpty()) {
|
||||
QHttpPart imagePart;
|
||||
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
|
||||
imagePart.setHeader(
|
||||
QNetworkRequest::ContentDispositionHeader,
|
||||
QStringLiteral("form-data; name=\"image\"; filename=\"final_screen.png\"")
|
||||
);
|
||||
imagePart.setBody(image);
|
||||
multiPart->append(imagePart);
|
||||
}
|
||||
|
||||
reply = m_manager->post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
|
||||
if (awaitReply(reply, kDumpTimeoutMs)) { timedOut = false; break; }
|
||||
qWarning() << "[NetworkService] postMonitoringDump timeout attempt"
|
||||
<< attempt << "/" << kMaxRequestAttempts;
|
||||
reply->abort();
|
||||
}
|
||||
|
||||
if (timedOut) {
|
||||
bool ok = false;
|
||||
QString detail;
|
||||
if (outcome.clientTimedOut) {
|
||||
detail = "timeout: " + outcome.timeoutDetail;
|
||||
qWarning() << "[NetworkService] postMonitoringDump timeout after"
|
||||
<< kMaxRequestAttempts << "attempts";
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "[NetworkService] postMonitoringDump failed:" << reply->errorString()
|
||||
detail = describeNetError(reply, url.toString());
|
||||
qWarning() << "[NetworkService] postMonitoringDump failed:" << detail
|
||||
<< "\n response body:" << reply->readAll();
|
||||
} else {
|
||||
const QByteArray raw = reply->readAll();
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw);
|
||||
if (doc.isObject() && !doc.object().value("success").toBool()) {
|
||||
detail = "сервер отклонил: " + QString::fromUtf8(raw).left(300);
|
||||
qWarning() << "[NetworkService] postMonitoringDump rejected:" << raw;
|
||||
} else {
|
||||
ok = true;
|
||||
qDebug() << "[NetworkService] postMonitoringDump OK:" << raw;
|
||||
}
|
||||
}
|
||||
reply->deleteLater();
|
||||
emit monitoringDumpFinished(ok, detail, archiveFilename);
|
||||
}
|
||||
|
||||
void NetworkService::postMonitoringScreenshot(const QString &desktopId,
|
||||
@ -1785,7 +1810,7 @@ void NetworkService::postMonitoringScreenshot(const QString &desktopId,
|
||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
|
||||
if (!awaitReply(reply, kMonitoringTimeoutMs)) {
|
||||
if (awaitReply(reply, kMonitoringTimeoutMs) == AwaitOutcome::ClientTimeout) {
|
||||
qWarning() << "[NetworkService] postMonitoringScreenshot timeout";
|
||||
reply->abort();
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <QMutex>
|
||||
#include <QSharedPointer>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
|
||||
class QWidget;
|
||||
|
||||
@ -145,6 +146,10 @@ signals:
|
||||
// healthy=false при kSyncFailureThreshold подряд неудачных тиков; healthy=true
|
||||
// при первом успехе после такой серии. consecutiveFailures даёт текущий счёт.
|
||||
void syncHealthChanged(bool healthy, int consecutiveFailures);
|
||||
// Результат postMonitoringDump (включая ручную отправку логов из меню).
|
||||
// archiveFilename — ключ, по которому инициатор отличает свой аплоад (синглтон
|
||||
// общий). ok=false ⇒ detail содержит причину (таймаут/сетевая/HTTP-ошибка).
|
||||
void monitoringDumpFinished(bool ok, QString detail, QString archiveFilename);
|
||||
|
||||
private:
|
||||
QByteArray m_json;
|
||||
@ -207,7 +212,10 @@ private:
|
||||
|
||||
QNetworkAccessManager *m_manager;
|
||||
|
||||
QJsonValue post(QHttpMultiPart *multiPart, const QString &path);
|
||||
// buildMultiPart вызывается на КАЖДУЮ попытку (multipart потребляется за запрос).
|
||||
// idempotent=false ⇒ ретраим только connect-phase сбои (без риска двойной отправки).
|
||||
QJsonValue post(const std::function<QHttpMultiPart*()> &buildMultiPart,
|
||||
const QString &path, bool idempotent);
|
||||
QJsonArray get(const QString &path, const QMap<QString, QString> ¶ms);
|
||||
QJsonValue getJson(const QString &path, bool withAuth = false);
|
||||
QJsonValue postJson(const QString &path, const QJsonObject &body, bool withAuth = false);
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
#include <QProcess>
|
||||
#include <QPixmap>
|
||||
#include <QVBoxLayout>
|
||||
#include <memory>
|
||||
|
||||
#include "dump/TaskDumpRecorder.h"
|
||||
#include "dao/GeneralLogDAO.h"
|
||||
@ -284,6 +285,32 @@ void FileLogWindow::sendLogsToBackend() {
|
||||
|
||||
qDebug() << "[sendLogsToBackend] dispatching to NetworkService::postMonitoringDump"
|
||||
<< "archive.size=" << archiveBytes.size() << "filename=" << archiveFilename;
|
||||
|
||||
// Подписываемся на результат ДО диспатча. svc живёт в потоке paymentThread —
|
||||
// соединение автоматически становится QueuedConnection, лямбда исполнится в
|
||||
// UI-потоке. Контекст = this ⇒ при закрытии окна Qt сам отключит слот (без
|
||||
// обращения к висячему указателю). Фильтруем по своему archiveFilename:
|
||||
// синглтон общий, параллельно могут уходить и другие дампы (TaskDumpRecorder).
|
||||
auto handled = std::make_shared<bool>(false);
|
||||
auto conn = std::make_shared<QMetaObject::Connection>();
|
||||
*conn = connect(svc, &NetworkService::monitoringDumpFinished, this,
|
||||
[this, conn, handled, expected = archiveFilename](
|
||||
bool ok, const QString &detail, const QString &fn) {
|
||||
if (fn != expected || *handled) return;
|
||||
*handled = true;
|
||||
QObject::disconnect(*conn);
|
||||
m_sendBtn->setEnabled(true);
|
||||
m_sendBtn->setText("Отправить");
|
||||
if (ok) {
|
||||
QMessageBox::information(this, "Готово", "Логи отправлены на бэкенд.");
|
||||
} else {
|
||||
QMessageBox::warning(this, "Ошибка",
|
||||
"Не удалось отправить логи: " + detail);
|
||||
}
|
||||
});
|
||||
|
||||
m_sendBtn->setText("Отправка...");
|
||||
|
||||
const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(QString, text),
|
||||
@ -291,15 +318,12 @@ void FileLogWindow::sendLogsToBackend() {
|
||||
Q_ARG(QByteArray, QByteArray()),
|
||||
Q_ARG(QString, archiveFilename));
|
||||
|
||||
m_sendBtn->setEnabled(true);
|
||||
m_sendBtn->setText("Отправить");
|
||||
|
||||
if (!dispatched) {
|
||||
*handled = true;
|
||||
QObject::disconnect(*conn);
|
||||
m_sendBtn->setEnabled(true);
|
||||
m_sendBtn->setText("Отправить");
|
||||
QMessageBox::warning(this, "Ошибка",
|
||||
"Не удалось поставить отправку в очередь NetworkService.");
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::information(this, "Готово",
|
||||
"Логи поставлены в очередь на отправку на бэкенд.");
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user