Skip to content

Commit 2e984ca

Browse files
committed
add stall detection
Signed-off-by: AndreasK <andreas.kirschner@mailbox.org>
1 parent 6a37923 commit 2e984ca

6 files changed

Lines changed: 75 additions & 0 deletions

File tree

src/libsync/abstractnetworkjob.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,40 @@ void AbstractNetworkJob::setupConnections(QNetworkReply *reply)
110110
connect(reply, &QNetworkReply::downloadProgress, this, &AbstractNetworkJob::networkActivity);
111111
connect(reply, &QNetworkReply::uploadProgress, this, &AbstractNetworkJob::networkActivity);
112112
connect(reply, &QNetworkReply::redirected, this, [reply, this] (const QUrl &url) { emit redirected(reply, url, 0);});
113+
114+
if (_stallDetectionEnabled) {
115+
// Reset per-reply state so progress from a previous attempt does not
116+
// carry over (e.g. after an HTTP redirect).
117+
_lastTransferBytes = -1;
118+
connect(reply, &QNetworkReply::uploadProgress, this, [this](qint64 bytesSent, qint64) {
119+
if (bytesSent > _lastTransferBytes) {
120+
_lastTransferBytes = bytesSent;
121+
_stallTimer.start();
122+
}
123+
});
124+
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 bytesReceived, qint64) {
125+
if (bytesReceived > _lastTransferBytes) {
126+
_lastTransferBytes = bytesReceived;
127+
_stallTimer.start();
128+
}
129+
});
130+
_stallTimer.start();
131+
}
132+
}
133+
134+
void AbstractNetworkJob::enableStallDetection(int timeoutMs)
135+
{
136+
_stallDetectionEnabled = true;
137+
_stallTimer.setSingleShot(true);
138+
_stallTimer.setInterval(timeoutMs);
139+
connect(&_stallTimer, &QTimer::timeout, this, [this]() {
140+
qCWarning(lcNetworkJob) << "Transfer stalled: no bytes transferred for"
141+
<< _stallTimer.interval() / 1000 << "seconds on" << path();
142+
emit transferStalled();
143+
if (reply()) {
144+
reply()->abort();
145+
}
146+
});
113147
}
114148

115149
QNetworkReply *AbstractNetworkJob::addTimer(QNetworkReply *reply)
@@ -175,6 +209,7 @@ QUrl AbstractNetworkJob::makeDavUrl(const QString &relativePath) const
175209
void AbstractNetworkJob::slotFinished()
176210
{
177211
_timer.stop();
212+
_stallTimer.stop();
178213

179214
if (_reply->error() == QNetworkReply::SslHandshakeFailedError) {
180215
qCWarning(lcNetworkJob) << "SslHandshakeFailedError: " << errorString() << " : can be caused by a webserver wanting SSL client certificates";

src/libsync/abstractnetworkjob.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ class OWNCLOUDSYNC_EXPORT AbstractNetworkJob : public QObject
110110
/// Returns a standardised error message in case of HSTS errors
111111
[[nodiscard]] static std::optional<QString> hstsErrorStringFromReply(QNetworkReply *reply);
112112

113+
public:
114+
/**
115+
* Enables per-job stall detection for uploads and downloads.
116+
*
117+
* Once enabled, a timer is started when the job begins transferring data.
118+
* The timer is reset whenever progress is made. If no bytes are transferred
119+
* within @p timeoutMs milliseconds, the transferStalled() signal is emitted
120+
* and the reply is aborted.
121+
*
122+
* Call this from a subclass's start() before invoking sendRequest().
123+
*/
124+
void enableStallDetection(int timeoutMs = 30 * 1000);
125+
113126
public slots:
114127
void setTimeout(qint64 msec);
115128
void resetTimeout();
@@ -121,6 +134,12 @@ public slots:
121134
void networkError(QNetworkReply *reply);
122135
void networkActivity();
123136

137+
/**
138+
* Emitted when stall detection is active and no bytes have been transferred
139+
* within the configured timeout. The reply is aborted immediately after.
140+
*/
141+
void transferStalled();
142+
124143
/** Emitted when a redirect is followed.
125144
*
126145
* \a reply The "please redirect" reply
@@ -220,6 +239,10 @@ private slots:
220239
//
221240
// Reparented to the currently running QNetworkReply.
222241
QPointer<QIODevice> _requestBody;
242+
243+
QTimer _stallTimer;
244+
qint64 _lastTransferBytes = -1;
245+
bool _stallDetectionEnabled = false;
223246
};
224247

225248
/**

src/libsync/propagatedownload.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ void GETFileJob::start()
119119
req.setPriority(QNetworkRequest::LowPriority); // Long downloads must not block non-propagation jobs.
120120
req.setDecompressedSafetyCheckThreshold(_decompressionThresholdBase + CustomDecompressedSafetyCheckThreshold);
121121

122+
enableStallDetection();
123+
122124
if (_directDownloadUrl.isEmpty()) {
123125
sendRequest("GET", makeDavUrl(path()), req);
124126
} else {
@@ -745,6 +747,11 @@ void PropagateDownloadFile::startDownload()
745747
_job->setBandwidthManager(&propagator()->_bandwidthManager);
746748
connect(_job.data(), &GETFileJob::finishedSignal, this, &PropagateDownloadFile::slotGetFinished);
747749
connect(_job.data(), &GETFileJob::downloadProgress, this, &PropagateDownloadFile::slotDownloadProgress);
750+
connect(_job.data(), &AbstractNetworkJob::transferStalled, this, [this]() {
751+
qCWarning(lcPropagateDownload) << "Download stalled for" << _item->_file << "- scheduling retry.";
752+
_job->cancel();
753+
done(SyncFileItem::SoftError, tr("Download stalled: no data was transferred. The download will be retried."), ErrorCategory::GenericError);
754+
});
748755
propagator()->_activeJobList.append(this);
749756
_job->start();
750757
}

src/libsync/propagateupload.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ void PUTFileJob::start()
5757

5858
req.setPriority(QNetworkRequest::LowPriority); // Long uploads must not block non-propagation jobs.
5959

60+
enableStallDetection();
61+
6062
auto requestID = QByteArray{};
6163

6264
if (_url.isValid()) {

src/libsync/propagateuploadng.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,10 @@ void PropagateUploadFileNG::startNextChunk()
391391
connect(job, &PUTFileJob::uploadProgress,
392392
devicePtr, &UploadDevice::slotJobUploadProgress);
393393
connect(job, &QObject::destroyed, this, &PropagateUploadFileCommon::slotJobDestroyed);
394+
connect(job, &AbstractNetworkJob::transferStalled, this, [this]() {
395+
qCWarning(lcPropagateUploadNG) << "Upload stalled for" << _item->_file << "- scheduling retry.";
396+
abortWithError(SyncFileItem::SoftError, tr("Upload stalled: no data was transferred. The upload will be retried."));
397+
});
394398
job->start();
395399
propagator()->_activeJobList.append(this);
396400
_currentChunk++;

src/libsync/propagateuploadv1.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ void PropagateUploadFileV1::startNextChunk()
160160
connect(job, &PUTFileJob::uploadProgress, this, &PropagateUploadFileV1::slotUploadProgress);
161161
connect(job, &PUTFileJob::uploadProgress, devicePtr, &UploadDevice::slotJobUploadProgress);
162162
connect(job, &QObject::destroyed, this, &PropagateUploadFileCommon::slotJobDestroyed);
163+
connect(job, &AbstractNetworkJob::transferStalled, this, [this]() {
164+
qCWarning(lcPropagateUploadV1) << "Upload stalled for" << _item->_file << "- scheduling retry.";
165+
abortWithError(SyncFileItem::SoftError, tr("Upload stalled: no data was transferred. The upload will be retried."));
166+
});
163167
if (isFinalChunk)
164168
adjustLastJobTimeout(job, fileSize);
165169
job->start();

0 commit comments

Comments
 (0)