aboutsummaryrefslogtreecommitdiff
path: root/lib/jobs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/jobs')
-rw-r--r--lib/jobs/basejob.cpp159
-rw-r--r--lib/jobs/basejob.h63
-rw-r--r--lib/jobs/downloadfilejob.cpp4
-rw-r--r--lib/jobs/downloadfilejob.h4
4 files changed, 137 insertions, 93 deletions
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index 621762be..0a17431c 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -31,6 +31,8 @@
#include <array>
using namespace Quotient;
+using std::chrono::seconds, std::chrono::milliseconds;
+using std::chrono_literals::operator""s;
struct NetworkReplyDeleter : public QScopedPointerDeleteLater {
static inline void cleanup(QNetworkReply* reply)
@@ -43,6 +45,11 @@ struct NetworkReplyDeleter : public QScopedPointerDeleteLater {
class BaseJob::Private {
public:
+ struct JobTimeoutConfig {
+ seconds jobTimeout;
+ seconds nextRetryInterval;
+ };
+
// Using an idiom from clang-tidy:
// http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html
Private(HttpVerb v, QString endpoint, const QUrlQuery& q, Data&& data,
@@ -52,12 +59,14 @@ public:
, requestQuery(q)
, requestData(std::move(data))
, needsToken(nt)
- {}
+ {
+ timer.setSingleShot(true);
+ retryTimer.setSingleShot(true);
+ }
- void sendRequest(bool inBackground);
- const JobTimeoutConfig& getCurrentTimeoutConfig() const;
+ void sendRequest();
- const ConnectionData* connection = nullptr;
+ ConnectionData* connection = nullptr;
// Contents for the network request
HttpVerb verb;
@@ -67,13 +76,15 @@ public:
Data requestData;
bool needsToken;
+ bool inBackground = false;
+
// There's no use of QMimeType here because we don't want to match
// content types against the known MIME type hierarchy; and at the same
// type QMimeType is of little help with MIME type globs (`text/*` etc.)
- QByteArrayList expectedContentTypes;
+ QByteArrayList expectedContentTypes { "application/json" };
QScopedPointer<QNetworkReply, NetworkReplyDeleter> reply;
- Status status = Pending;
+ Status status = Unprepared;
QByteArray rawResponse;
QUrl errorUrl; //< May contain a URL to help with some errors
@@ -82,12 +93,18 @@ public:
QTimer timer;
QTimer retryTimer;
- QVector<JobTimeoutConfig> errorStrategy = { { 90, 5 },
- { 90, 10 },
- { 120, 30 } };
- int maxRetries = errorStrategy.size();
+ static constexpr std::array<const JobTimeoutConfig, 3> errorStrategy {
+ { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } }
+ };
+ int maxRetries = int(errorStrategy.size());
int retriesTaken = 0;
+ const JobTimeoutConfig& getCurrentTimeoutConfig() const
+ {
+ return errorStrategy[std::min(size_t(retriesTaken),
+ errorStrategy.size() - 1)];
+ }
+
QString urlForLog() const
{
return reply
@@ -106,9 +123,11 @@ BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
: d(new Private(verb, endpoint, query, std::move(data), needsToken))
{
setObjectName(name);
- setExpectedContentTypes({ "application/json" });
- d->timer.setSingleShot(true);
connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
+ connect(&d->retryTimer, &QTimer::timeout, this, [this] {
+ setStatus(Pending);
+ sendRequest();
+ });
}
BaseJob::~BaseJob()
@@ -124,10 +143,7 @@ QUrl BaseJob::requestUrl() const
bool BaseJob::isBackground() const
{
- return d->reply
- && d->reply->request()
- .attribute(QNetworkRequest::BackgroundRequestAttribute)
- .toBool();
+ return d->inBackground;
}
const QString& BaseJob::apiEndpoint() const { return d->apiEndpoint; }
@@ -191,7 +207,7 @@ QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path,
return baseUrl;
}
-void BaseJob::Private::sendRequest(bool inBackground)
+void BaseJob::Private::sendRequest()
{
QNetworkRequest req { makeRequestUrl(connection->baseUrl(), apiEndpoint,
requestQuery) };
@@ -223,36 +239,31 @@ void BaseJob::Private::sendRequest(bool inBackground)
}
}
-void BaseJob::beforeStart(const ConnectionData*) {}
+void BaseJob::doPrepare() {}
-void BaseJob::afterStart(const ConnectionData*, QNetworkReply*) {}
+void BaseJob::onSentRequest(QNetworkReply*) {}
void BaseJob::beforeAbandon(QNetworkReply*) {}
-void BaseJob::start(const ConnectionData* connData, bool inBackground)
+void BaseJob::prepare(ConnectionData* connData, bool inBackground)
{
+ d->inBackground = inBackground;
d->connection = connData;
- d->retryTimer.setSingleShot(true);
- connect(&d->retryTimer, &QTimer::timeout, this,
- [this, inBackground] { sendRequest(inBackground); });
-
- beforeStart(connData);
- if (status().good())
- sendRequest(inBackground);
- if (status().good())
- afterStart(connData, d->reply.data());
- if (!status().good())
+ doPrepare();
+ if (status().code != Unprepared && status().code != Pending)
QTimer::singleShot(0, this, &BaseJob::finishJob);
+ setStatus(Pending);
}
-void BaseJob::sendRequest(bool inBackground)
+void BaseJob::sendRequest()
{
- emit aboutToStart();
- d->retryTimer.stop(); // In case we were counting down at the moment
- qCDebug(d->logCat) << this << "sending request to" << d->apiEndpoint;
- if (!d->requestQuery.isEmpty())
- qCDebug(d->logCat) << " query:" << d->requestQuery.toString();
- d->sendRequest(inBackground);
+ if (status().code == Abandoned)
+ return;
+ Q_ASSERT(d->connection && status().code == Pending);
+ qCDebug(d->logCat) << "Making request to" << d->urlForLog();
+ emit aboutToSendRequest();
+ d->sendRequest();
+ Q_ASSERT(d->reply);
connect(d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply);
if (d->reply->isRunning()) {
connect(d->reply.data(), &QNetworkReply::metaDataChanged, this,
@@ -262,10 +273,12 @@ void BaseJob::sendRequest(bool inBackground)
connect(d->reply.data(), &QNetworkReply::downloadProgress, this,
&BaseJob::downloadProgress);
d->timer.start(getCurrentTimeout());
- qCDebug(d->logCat) << this << "request has been sent";
- emit started();
+ qCInfo(d->logCat).noquote() << "Request sent to" << d->urlForLog();
+ onSentRequest(d->reply.data());
+ emit sentRequest();
} else
- qCWarning(d->logCat) << this << "request could not start";
+ qCWarning(d->logCat).noquote()
+ << "Request could not start:" << d->urlForLog();
}
void BaseJob::checkReply() { setStatus(doCheckReply(d->reply.data())); }
@@ -286,13 +299,7 @@ void BaseJob::gotReply()
parseError(d->reply.data(),
QJsonDocument::fromJson(d->rawResponse).object()));
}
-
- if (status().code != TooManyRequestsError)
- finishJob();
- else {
- stop();
- emit retryScheduled(d->retriesTaken, d->retryTimer.interval());
- }
+ finishJob();
}
bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
@@ -416,14 +423,13 @@ BaseJob::Status BaseJob::parseError(QNetworkReply* /*reply*/,
const auto errCode = errorJson.value("errcode"_ls).toString();
if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") {
QString msg = tr("Too many requests");
- auto retryInterval = errorJson.value("retry_after_ms"_ls).toInt(-1);
- if (retryInterval != -1)
- msg += tr(", next retry advised after %1 ms").arg(retryInterval);
+ int64_t retryAfterMs = errorJson.value("retry_after_ms"_ls).toInt(-1);
+ if (retryAfterMs >= 0)
+ msg += tr(", next retry advised after %1 ms").arg(retryAfterMs);
else // We still have to figure some reasonable interval
- retryInterval = getNextRetryInterval();
+ retryAfterMs = getNextRetryMs();
- qCWarning(d->logCat) << this << "will retry in" << retryInterval << "ms";
- d->retryTimer.start(retryInterval);
+ d->connection->limitRate(milliseconds(retryAfterMs));
return { TooManyRequestsError, msg };
}
@@ -470,19 +476,23 @@ void BaseJob::stop()
void BaseJob::finishJob()
{
stop();
- if ((error() == NetworkError || error() == TimeoutError)
+ if (error() == TooManyRequests) {
+ emit rateLimited();
+ setStatus(Pending);
+ d->connection->submit(this);
+ return;
+ }
+ if ((error() == NetworkError || error() == Timeout)
&& d->retriesTaken < d->maxRetries) {
- // TODO: The whole retrying thing should be put to ConnectionManager
+ // TODO: The whole retrying thing should be put to Connection(Manager)
// otherwise independently retrying jobs make a bit of notification
// storm towards the UI.
- const auto retryInterval = error() == TimeoutError
- ? 0
- : getNextRetryInterval();
+ const seconds retryIn = error() == Timeout ? 0s : getNextRetryInterval();
++d->retriesTaken;
qCWarning(d->logCat).nospace() << this << ": retry #" << d->retriesTaken
- << " in " << retryInterval / 1000 << " s";
- d->retryTimer.start(retryInterval);
- emit retryScheduled(d->retriesTaken, retryInterval);
+ << " in " << retryIn.count() << " s";
+ d->retryTimer.start(retryIn);
+ emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count());
return;
}
@@ -498,24 +508,35 @@ void BaseJob::finishJob()
deleteLater();
}
-const JobTimeoutConfig& BaseJob::Private::getCurrentTimeoutConfig() const
+seconds BaseJob::getCurrentTimeout() const
+{
+ return d->getCurrentTimeoutConfig().jobTimeout;
+}
+
+BaseJob::duration_ms_t BaseJob::getCurrentTimeoutMs() const
+{
+ return milliseconds(getCurrentTimeout()).count();
+}
+
+seconds BaseJob::getNextRetryInterval() const
{
- return errorStrategy[std::min(retriesTaken, errorStrategy.size() - 1)];
+ return d->getCurrentTimeoutConfig().nextRetryInterval;
}
-BaseJob::duration_t BaseJob::getCurrentTimeout() const
+BaseJob::duration_ms_t BaseJob::getNextRetryMs() const
{
- return d->getCurrentTimeoutConfig().jobTimeout * 1000;
+ return milliseconds(getNextRetryInterval()).count();
}
-BaseJob::duration_t BaseJob::getNextRetryInterval() const
+milliseconds BaseJob::timeToRetry() const
{
- return d->getCurrentTimeoutConfig().nextRetryInterval * 1000;
+ return d->retryTimer.isActive() ? d->retryTimer.remainingTimeAsDuration()
+ : 0s;
}
-BaseJob::duration_t BaseJob::millisToRetry() const
+BaseJob::duration_ms_t BaseJob::millisToRetry() const
{
- return d->retryTimer.isActive() ? d->retryTimer.remainingTime() : 0;
+ return timeToRetry().count();
}
int BaseJob::maxRetries() const { return d->maxRetries; }
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
index 9de7b49d..4dc287f8 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -34,11 +34,6 @@ class ConnectionData;
enum class HttpVerb { Get, Put, Post, Delete };
-struct JobTimeoutConfig {
- int jobTimeout;
- int nextRetryInterval;
-};
-
class BaseJob : public QObject {
Q_OBJECT
Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT)
@@ -59,6 +54,7 @@ public:
WarningLevel = 20, //< Warnings have codes starting from this
UnexpectedResponseType = 21,
UnexpectedResponseTypeWarning = UnexpectedResponseType,
+ Unprepared = 25, //< Initial job state is incomplete, hence warning level
Abandoned = 50, //< A tiny period between abandoning and object deletion
ErrorLevel = 100, //< Errors have codes starting from this
NetworkError = 100,
@@ -140,8 +136,6 @@ public:
QString message;
};
- using duration_t = int; // milliseconds
-
public:
BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
bool needsToken = true);
@@ -153,13 +147,16 @@ public:
/** Current status of the job */
Status status() const;
+
/** Short human-friendly message on the job status */
QString statusCaption() const;
+
/** Get raw response body as received from the server
* \param bytesAtMost return this number of leftmost bytes, or -1
* to return the entire response
*/
QByteArray rawData(int bytesAtMost = -1) const;
+
/** Get UI-friendly sample of raw data
*
* This is almost the same as rawData but appends the "truncated"
@@ -175,17 +172,24 @@ public:
* \sa status
*/
int error() const;
+
/** Error-specific message, as returned by the server */
virtual QString errorString() const;
+
/** A URL to help/clarify the error, if provided by the server */
QUrl errorUrl() const;
int maxRetries() const;
void setMaxRetries(int newMaxRetries);
- Q_INVOKABLE duration_t getCurrentTimeout() const;
- Q_INVOKABLE duration_t getNextRetryInterval() const;
- Q_INVOKABLE duration_t millisToRetry() const;
+ using duration_ms_t = std::chrono::milliseconds::rep; // normally int64_t
+
+ std::chrono::seconds getCurrentTimeout() const;
+ Q_INVOKABLE duration_ms_t getCurrentTimeoutMs() const;
+ std::chrono::seconds getNextRetryInterval() const;
+ Q_INVOKABLE duration_ms_t getNextRetryMs() const;
+ std::chrono::milliseconds timeToRetry() const;
+ Q_INVOKABLE duration_ms_t millisToRetry() const;
friend QDebug operator<<(QDebug dbg, const BaseJob* j)
{
@@ -193,7 +197,7 @@ public:
}
public slots:
- void start(const ConnectionData* connData, bool inBackground = false);
+ void prepare(ConnectionData* connData, bool inBackground);
/**
* Abandons the result of this job, arrived or unarrived.
@@ -206,10 +210,10 @@ public slots:
signals:
/** The job is about to send a network request */
- void aboutToStart();
+ void aboutToSendRequest();
/** The job has sent a network request */
- void started();
+ void sentRequest();
/** The job has changed its status */
void statusChanged(Status newStatus);
@@ -222,7 +226,14 @@ signals:
* @param inMilliseconds the interval after which the next attempt will be
* taken
*/
- void retryScheduled(int nextAttempt, int inMilliseconds);
+ void retryScheduled(int nextAttempt, duration_ms_t inMilliseconds);
+
+ /**
+ * The previous network request has been rate-limited; the next attempt
+ * will be queued and run sometime later. Since other jobs may already
+ * wait in the queue, it's not possible to predict the wait time.
+ */
+ void rateLimited();
/**
* Emitted when the job is finished, in any case. It is used to notify
@@ -297,9 +308,20 @@ protected:
static QUrl makeRequestUrl(QUrl baseUrl, const QString& path,
const QUrlQuery& query = {});
- virtual void beforeStart(const ConnectionData* connData);
- virtual void afterStart(const ConnectionData* connData,
- QNetworkReply* reply);
+ /*! Prepares the job for execution
+ *
+ * This method is called no more than once per job lifecycle,
+ * when it's first scheduled for execution; in particular, it is not called
+ * on retries.
+ */
+ virtual void doPrepare();
+ /*! Postprocessing after the network request has been sent
+ *
+ * This method is called every time the job receives a running
+ * QNetworkReply object from NetworkAccessManager - basically, after
+ * successfully sending a network request (including retries).
+ */
+ virtual void onSentRequest(QNetworkReply*);
virtual void beforeAbandon(QNetworkReply*);
/**
@@ -341,8 +363,7 @@ protected:
* @param reply the HTTP reply from the server
* @param errorJson the JSON payload describing the error
*/
- virtual Status parseError(QNetworkReply* reply,
- const QJsonObject& errorJson);
+ virtual Status parseError(QNetworkReply*, const QJsonObject& errorJson);
void setStatus(Status s);
void setStatus(int code, QString message);
@@ -359,10 +380,12 @@ protected slots:
void timeout();
private slots:
- void sendRequest(bool inBackground);
+ void sendRequest();
void checkReply();
void gotReply();
+ friend class ConnectionData; // to provide access to sendRequest()
+
private:
void stop();
void finishJob();
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
index 3a03efde..4e997326 100644
--- a/lib/jobs/downloadfilejob.cpp
+++ b/lib/jobs/downloadfilejob.cpp
@@ -39,7 +39,7 @@ QString DownloadFileJob::targetFileName() const
return (d->targetFile ? d->targetFile : d->tempFile)->fileName();
}
-void DownloadFileJob::beforeStart(const ConnectionData*)
+void DownloadFileJob::doPrepare()
{
if (d->targetFile && !d->targetFile->isReadable()
&& !d->targetFile->open(QIODevice::WriteOnly)) {
@@ -57,7 +57,7 @@ void DownloadFileJob::beforeStart(const ConnectionData*)
qCDebug(JOBS) << "Downloading to" << d->tempFile->fileName();
}
-void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply)
+void DownloadFileJob::onSentRequest(QNetworkReply* reply)
{
connect(reply, &QNetworkReply::metaDataChanged, this, [this, reply] {
if (!status().good())
diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h
index fa697219..b7d2d75b 100644
--- a/lib/jobs/downloadfilejob.h
+++ b/lib/jobs/downloadfilejob.h
@@ -19,8 +19,8 @@ private:
class Private;
QScopedPointer<Private> d;
- void beforeStart(const ConnectionData*) override;
- void afterStart(const ConnectionData*, QNetworkReply* reply) override;
+ void doPrepare() override;
+ void onSentRequest(QNetworkReply* reply) override;
void beforeAbandon(QNetworkReply*) override;
Status parseReply(QNetworkReply*) override;
};