Line data Source code
1 : #include "ResilientNetworkReply.h"
2 : #include "FangNetworkAccessManager.h"
3 : #include "../utilities/FangLogging.h"
4 : #include <QDebug>
5 :
6 : static const int DEFAULT_TIMEOUT_MS = 30000; // 30 seconds
7 :
8 32 : ResilientNetworkReply::ResilientNetworkReply(FangNetworkAccessManager* manager,
9 : const QNetworkRequest& request,
10 : QNetworkAccessManager::Operation operation,
11 : const NetworkRetryPolicy& policy,
12 32 : QObject* parent)
13 : : QObject(parent)
14 32 : , networkManager(manager)
15 32 : , networkRequest(request)
16 32 : , operation(operation)
17 32 : , retryPolicy(policy)
18 32 : , currentReply(nullptr)
19 32 : , currentAttemptCount(0)
20 32 : , customTimeout(-1)
21 32 : , lastError(QNetworkReply::NoError)
22 32 : , isSuccessful(false)
23 32 : , fromCache(false)
24 32 : , aborted(false)
25 : {
26 32 : timeoutTimer.setSingleShot(true);
27 32 : connect(&timeoutTimer, &QTimer::timeout, this, &ResilientNetworkReply::onTimeout);
28 :
29 32 : retryTimer.setSingleShot(true);
30 32 : connect(&retryTimer, &QTimer::timeout, this, &ResilientNetworkReply::onRetryTimerTimeout);
31 32 : }
32 :
33 64 : ResilientNetworkReply::~ResilientNetworkReply()
34 : {
35 32 : cleanup();
36 64 : }
37 :
38 32 : void ResilientNetworkReply::start()
39 : {
40 32 : if (aborted) {
41 0 : qCWarning(logNetwork) << "Cannot start aborted request";
42 0 : return;
43 : }
44 :
45 32 : currentAttemptCount = 0;
46 32 : errorHistory.clear();
47 32 : accumulatedData.clear();
48 32 : executeRequest();
49 : }
50 :
51 0 : void ResilientNetworkReply::abort()
52 : {
53 0 : aborted = true;
54 0 : retryTimer.stop();
55 0 : cleanup();
56 0 : }
57 :
58 0 : void ResilientNetworkReply::setTimeout(int timeout)
59 : {
60 0 : customTimeout = timeout;
61 0 : }
62 :
63 45 : void ResilientNetworkReply::executeRequest()
64 : {
65 45 : if (aborted) {
66 0 : return;
67 : }
68 :
69 45 : currentAttemptCount++;
70 45 : requestTimer.start();
71 :
72 135 : qCDebug(logNetwork) << "Network request attempt" << currentAttemptCount
73 90 : << "for" << networkRequest.url();
74 :
75 : // Clean up previous reply if exists
76 45 : cleanup();
77 :
78 : // Create new network request
79 45 : switch (operation) {
80 45 : case QNetworkAccessManager::GetOperation:
81 45 : currentReply = networkManager->get(networkRequest);
82 45 : break;
83 0 : case QNetworkAccessManager::PostOperation:
84 : // For POST, would need to handle body data
85 0 : qCWarning(logNetwork) << "POST operation not yet implemented in ResilientNetworkReply";
86 0 : currentReply = networkManager->get(networkRequest);
87 0 : break;
88 0 : case QNetworkAccessManager::HeadOperation:
89 0 : currentReply = networkManager->head(networkRequest);
90 0 : break;
91 0 : default:
92 0 : qCWarning(logNetwork) << "Unsupported network operation:" << operation;
93 0 : currentReply = networkManager->get(networkRequest);
94 0 : break;
95 : }
96 :
97 45 : if (!currentReply) {
98 0 : qCCritical(logNetwork) << "Failed to create network reply";
99 0 : emit failed(QNetworkReply::UnknownNetworkError);
100 0 : return;
101 : }
102 :
103 : // Connect signals
104 45 : connect(currentReply, &QNetworkReply::finished,
105 45 : this, &ResilientNetworkReply::onReplyFinished);
106 45 : connect(currentReply, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred),
107 45 : this, &ResilientNetworkReply::onReplyError);
108 45 : connect(currentReply, &QNetworkReply::downloadProgress,
109 45 : this, &ResilientNetworkReply::onDownloadProgress);
110 :
111 : // Start timeout.
112 45 : int timeout = (customTimeout > 0) ? customTimeout : DEFAULT_TIMEOUT_MS;
113 45 : timeoutTimer.start(timeout);
114 :
115 : // Check if cached.
116 45 : QVariant fromCache = currentReply->attribute(QNetworkRequest::SourceIsFromCacheAttribute);
117 45 : fromCache = fromCache.isValid() && fromCache.toBool();
118 : }
119 :
120 45 : void ResilientNetworkReply::onReplyFinished()
121 : {
122 45 : timeoutTimer.stop();
123 :
124 45 : if (aborted || !currentReply) {
125 0 : return;
126 : }
127 :
128 45 : qint64 elapsed = requestTimer.elapsed();
129 :
130 : // Check for errors
131 45 : QNetworkReply::NetworkError error = currentReply->error();
132 :
133 45 : if (error == QNetworkReply::NoError) {
134 : // Yay! Success!
135 21 : accumulatedData = currentReply->readAll();
136 21 : isSuccessful = true;
137 21 : lastError = QNetworkReply::NoError;
138 :
139 42 : qCDebug(logNetwork) << "Network request succeeded for" << networkRequest.url()
140 21 : << "in" << elapsed << "ms"
141 21 : << "(" << accumulatedData.size() << "bytes)"
142 21 : << "after" << currentAttemptCount << "attempts";
143 :
144 21 : emit finished();
145 : } else {
146 : // Error occurred :(
147 24 : onReplyError(error);
148 : }
149 : }
150 :
151 24 : void ResilientNetworkReply::onReplyError(QNetworkReply::NetworkError error)
152 : {
153 24 : timeoutTimer.stop();
154 :
155 24 : if (aborted) {
156 0 : return;
157 : }
158 :
159 24 : lastError = error;
160 24 : lastErrorString = currentReply ? currentReply->errorString() : "Unknown error";
161 :
162 : // Record error.
163 24 : QString errorDesc = QString("Attempt %1: %2 (%3)")
164 48 : .arg(currentAttemptCount)
165 48 : .arg(QVariant::fromValue(error).toString())
166 24 : .arg(lastErrorString);
167 24 : errorHistory.append(qMakePair(currentAttemptCount, errorDesc));
168 :
169 48 : qCWarning(logNetwork) << "Network error for" << networkRequest.url()
170 24 : << ":" << errorDesc;
171 :
172 : // Check if it's worth retrying.
173 24 : if (retryPolicy.shouldRetry(currentAttemptCount) && retryPolicy.isRetryable(error)) {
174 13 : scheduleRetry();
175 : } else {
176 : // No more retries or error not retryable
177 33 : qCWarning(logNetwork) << "Request failed after " << currentAttemptCount << " attempts:"
178 22 : << networkRequest.url();
179 22 : qCWarning(logNetwork) << "Error history: ";
180 26 : for (const auto& entry : errorHistory) {
181 30 : qCWarning(logNetwork) << " " << entry.second;
182 : }
183 :
184 11 : emit failed(error);
185 : }
186 24 : }
187 :
188 0 : void ResilientNetworkReply::onTimeout()
189 : {
190 0 : if (aborted) {
191 0 : return;
192 : }
193 :
194 0 : qCWarning(logNetwork) << "Network timeout for" << networkRequest.url()
195 0 : << "after" << requestTimer.elapsed() << "ms";
196 :
197 : // Record timeout.
198 0 : errorHistory.append(qMakePair(currentAttemptCount,
199 0 : QString("Attempt %1: Timeout").arg(currentAttemptCount)));
200 :
201 : // Abort!
202 0 : if (currentReply) {
203 0 : currentReply->abort();
204 : }
205 :
206 0 : lastError = QNetworkReply::TimeoutError;
207 0 : lastErrorString = "Request timed out";
208 :
209 0 : emit timeout();
210 :
211 : // Check if should retry.
212 0 : if (retryPolicy.shouldRetry(currentAttemptCount) && retryPolicy.isRetryable(QNetworkReply::TimeoutError)) {
213 0 : scheduleRetry();
214 : } else {
215 0 : qCWarning(logNetwork) << "Request timed out after " << currentAttemptCount << " attempts";
216 0 : emit failed(QNetworkReply::TimeoutError);
217 : }
218 : }
219 :
220 13 : void ResilientNetworkReply::scheduleRetry()
221 : {
222 13 : int delay = retryPolicy.calculateDelay(currentAttemptCount);
223 :
224 39 : qCInfo(logNetwork) << "Scheduling retry " << (currentAttemptCount + 1)
225 39 : << "for " << networkRequest.url()
226 13 : << "in " << delay << " ms";
227 :
228 13 : emit retrying(currentAttemptCount + 1, delay);
229 :
230 13 : retryTimer.start(delay);
231 13 : }
232 :
233 13 : void ResilientNetworkReply::onRetryTimerTimeout()
234 : {
235 13 : if (aborted) {
236 0 : return;
237 : }
238 :
239 13 : executeRequest();
240 : }
241 :
242 0 : void ResilientNetworkReply::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
243 : {
244 : // Reset timeout if the download is progressing slowly (slow connection, large file, etc.)
245 0 : if (bytesReceived > 0) {
246 0 : int timeout = (customTimeout > 0) ? customTimeout : DEFAULT_TIMEOUT_MS;
247 0 : timeoutTimer.start(timeout);
248 : }
249 :
250 0 : emit downloadProgress(bytesReceived, bytesTotal);
251 0 : }
252 :
253 77 : void ResilientNetworkReply::cleanup()
254 : {
255 77 : timeoutTimer.stop();
256 :
257 77 : if (currentReply) {
258 45 : currentReply->disconnect(this);
259 45 : if (currentReply->isRunning()) {
260 45 : currentReply->abort();
261 : }
262 45 : currentReply->deleteLater();
263 45 : currentReply = nullptr;
264 : }
265 77 : }
266 :
267 0 : QUrl ResilientNetworkReply::url() const
268 : {
269 0 : if (currentReply) {
270 0 : return currentReply->url();
271 : }
272 0 : return networkRequest.url();
273 : }
274 :
275 9 : QByteArray ResilientNetworkReply::readAll()
276 : {
277 9 : return accumulatedData;
278 : }
279 :
280 0 : QString ResilientNetworkReply::errorString() const
281 : {
282 0 : if (!lastErrorString.isEmpty()) {
283 0 : return lastErrorString;
284 : }
285 0 : if (currentReply) {
286 0 : return currentReply->errorString();
287 : }
288 0 : return QString();
289 : }
290 :
291 0 : int ResilientNetworkReply::httpStatusCode() const
292 : {
293 0 : if (currentReply) {
294 0 : return currentReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
295 : }
296 0 : return 0;
297 : }
|