Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/libsync/abstractnetworkjob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,40 @@ void AbstractNetworkJob::setupConnections(QNetworkReply *reply)
connect(reply, &QNetworkReply::downloadProgress, this, &AbstractNetworkJob::networkActivity);
connect(reply, &QNetworkReply::uploadProgress, this, &AbstractNetworkJob::networkActivity);
connect(reply, &QNetworkReply::redirected, this, [reply, this] (const QUrl &url) { emit redirected(reply, url, 0);});

if (_stallDetectionEnabled) {
// Reset per-reply state so progress from a previous attempt does not
// carry over (e.g. after an HTTP redirect).
_lastTransferBytes = -1;
connect(reply, &QNetworkReply::uploadProgress, this, [this](qint64 bytesSent, qint64) {
if (bytesSent > _lastTransferBytes) {
_lastTransferBytes = bytesSent;
_stallTimer.start();
}
});
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 bytesReceived, qint64) {
if (bytesReceived > _lastTransferBytes) {
_lastTransferBytes = bytesReceived;
_stallTimer.start();
}
});
_stallTimer.start();
}
}

void AbstractNetworkJob::enableStallDetection(int timeoutMs)
{
_stallDetectionEnabled = true;
_stallTimer.setSingleShot(true);
_stallTimer.setInterval(timeoutMs);
connect(&_stallTimer, &QTimer::timeout, this, [this]() {
qCWarning(lcNetworkJob) << "Transfer stalled: no bytes transferred for"
<< _stallTimer.interval() / 1000 << "seconds on" << path();
emit transferStalled();
if (reply()) {
reply()->abort();
}
});
}

QNetworkReply *AbstractNetworkJob::addTimer(QNetworkReply *reply)
Expand Down Expand Up @@ -175,6 +209,7 @@ QUrl AbstractNetworkJob::makeDavUrl(const QString &relativePath) const
void AbstractNetworkJob::slotFinished()
{
_timer.stop();
_stallTimer.stop();

if (_reply->error() == QNetworkReply::SslHandshakeFailedError) {
qCWarning(lcNetworkJob) << "SslHandshakeFailedError: " << errorString() << " : can be caused by a webserver wanting SSL client certificates";
Expand Down
23 changes: 23 additions & 0 deletions src/libsync/abstractnetworkjob.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@ class OWNCLOUDSYNC_EXPORT AbstractNetworkJob : public QObject
/// Returns a standardised error message in case of HSTS errors
[[nodiscard]] static std::optional<QString> hstsErrorStringFromReply(QNetworkReply *reply);

public:
/**
* Enables per-job stall detection for uploads and downloads.
*
* Once enabled, a timer is started when the job begins transferring data.
* The timer is reset whenever progress is made. If no bytes are transferred
* within @p timeoutMs milliseconds, the transferStalled() signal is emitted
* and the reply is aborted.
*
* Call this from a subclass's start() before invoking sendRequest().
*/
void enableStallDetection(int timeoutMs = 30 * 1000);

public slots:
void setTimeout(qint64 msec);
void resetTimeout();
Expand All @@ -121,6 +134,12 @@ public slots:
void networkError(QNetworkReply *reply);
void networkActivity();

/**
* Emitted when stall detection is active and no bytes have been transferred
* within the configured timeout. The reply is aborted immediately after.
*/
void transferStalled();

/** Emitted when a redirect is followed.
*
* \a reply The "please redirect" reply
Expand Down Expand Up @@ -220,6 +239,10 @@ private slots:
//
// Reparented to the currently running QNetworkReply.
QPointer<QIODevice> _requestBody;

QTimer _stallTimer;
qint64 _lastTransferBytes = -1;
bool _stallDetectionEnabled = false;
};

/**
Expand Down
7 changes: 7 additions & 0 deletions src/libsync/propagatedownload.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ void GETFileJob::start()
req.setPriority(QNetworkRequest::LowPriority); // Long downloads must not block non-propagation jobs.
req.setDecompressedSafetyCheckThreshold(_decompressionThresholdBase + CustomDecompressedSafetyCheckThreshold);

enableStallDetection();

if (_directDownloadUrl.isEmpty()) {
sendRequest("GET", makeDavUrl(path()), req);
} else {
Expand Down Expand Up @@ -745,6 +747,11 @@ void PropagateDownloadFile::startDownload()
_job->setBandwidthManager(&propagator()->_bandwidthManager);
connect(_job.data(), &GETFileJob::finishedSignal, this, &PropagateDownloadFile::slotGetFinished);
connect(_job.data(), &GETFileJob::downloadProgress, this, &PropagateDownloadFile::slotDownloadProgress);
connect(_job.data(), &AbstractNetworkJob::transferStalled, this, [this]() {
qCWarning(lcPropagateDownload) << "Download stalled for" << _item->_file << "- scheduling retry.";
_job->cancel();
done(SyncFileItem::SoftError, tr("Download stalled: no data was transferred. The download will be retried."), ErrorCategory::GenericError);
});
propagator()->_activeJobList.append(this);
_job->start();
}
Expand Down
2 changes: 2 additions & 0 deletions src/libsync/propagateupload.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ void PUTFileJob::start()

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

enableStallDetection();

auto requestID = QByteArray{};

if (_url.isValid()) {
Expand Down
4 changes: 4 additions & 0 deletions src/libsync/propagateuploadng.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,10 @@ void PropagateUploadFileNG::startNextChunk()
connect(job, &PUTFileJob::uploadProgress,
devicePtr, &UploadDevice::slotJobUploadProgress);
connect(job, &QObject::destroyed, this, &PropagateUploadFileCommon::slotJobDestroyed);
connect(job, &AbstractNetworkJob::transferStalled, this, [this]() {
qCWarning(lcPropagateUploadNG) << "Upload stalled for" << _item->_file << "- scheduling retry.";
abortWithError(SyncFileItem::SoftError, tr("Upload stalled: no data was transferred. The upload will be retried."));
});
job->start();
propagator()->_activeJobList.append(this);
_currentChunk++;
Expand Down
4 changes: 4 additions & 0 deletions src/libsync/propagateuploadv1.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ void PropagateUploadFileV1::startNextChunk()
connect(job, &PUTFileJob::uploadProgress, this, &PropagateUploadFileV1::slotUploadProgress);
connect(job, &PUTFileJob::uploadProgress, devicePtr, &UploadDevice::slotJobUploadProgress);
connect(job, &QObject::destroyed, this, &PropagateUploadFileCommon::slotJobDestroyed);
connect(job, &AbstractNetworkJob::transferStalled, this, [this]() {
qCWarning(lcPropagateUploadV1) << "Upload stalled for" << _item->_file << "- scheduling retry.";
abortWithError(SyncFileItem::SoftError, tr("Upload stalled: no data was transferred. The upload will be retried."));
});
if (isFinalChunk)
adjustLastJobTimeout(job, fileSize);
job->start();
Expand Down