| 1 | // SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> |
| 2 | // SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> |
| 3 | // SPDX-License-Identifier: LGPL-2.1-or-later |
| 4 | |
| 5 | #include "connectiondata.h" |
| 6 | |
| 7 | #include "logging_categories_p.h" |
| 8 | #include "networkaccessmanager.h" |
| 9 | |
| 10 | #include "jobs/basejob.h" |
| 11 | |
| 12 | #include <QtCore/QPointer> |
| 13 | #include <QtCore/QTimer> |
| 14 | |
| 15 | #include <array> |
| 16 | #include <queue> |
| 17 | |
| 18 | using namespace Quotient; |
| 19 | |
| 20 | class ConnectionData::Private { |
| 21 | public: |
| 22 | explicit Private(QUrl url) : baseUrl(std::move(url)) |
| 23 | { |
| 24 | rateLimiter.setSingleShot(true); |
| 25 | } |
| 26 | |
| 27 | QUrl baseUrl; |
| 28 | QByteArray accessToken; |
| 29 | QString lastEvent; |
| 30 | QString userId; |
| 31 | QString deviceId; |
| 32 | std::vector<QString> needToken; |
| 33 | |
| 34 | mutable unsigned int txnCounter = 0; |
| 35 | const qint64 txnBase = QDateTime::currentMSecsSinceEpoch(); |
| 36 | |
| 37 | QString id() const { return userId + u'/' + deviceId; } |
| 38 | |
| 39 | using job_queue_t = std::queue<QPointer<BaseJob>>; |
| 40 | std::array<job_queue_t, 2> jobs; // 0 - foreground, 1 - background |
| 41 | QTimer rateLimiter; |
| 42 | }; |
| 43 | |
| 44 | ConnectionData::ConnectionData(QUrl baseUrl) |
| 45 | : d(makeImpl<Private>(args: std::move(baseUrl))) |
| 46 | { |
| 47 | // Each lambda invocation below takes no more than one job from the |
| 48 | // queues (first foreground, then background) and resumes it; then |
| 49 | // restarts the rate limiter timer with duration 0, effectively yielding |
| 50 | // to the event loop and then resuming until both queues are empty. |
| 51 | QObject::connect(sender: &d->rateLimiter, signal: &QTimer::timeout, slot: [this] { |
| 52 | // TODO: Consider moving out all job->sendRequest() invocations to |
| 53 | // a dedicated thread |
| 54 | d->rateLimiter.setInterval(0); |
| 55 | for (auto& q : d->jobs) |
| 56 | while (!q.empty()) { |
| 57 | const auto job = q.front(); |
| 58 | q.pop(); |
| 59 | if (!job || job->error() == BaseJob::Abandoned) |
| 60 | continue; |
| 61 | if (job->error() != BaseJob::Pending) { |
| 62 | qCCritical(MAIN) |
| 63 | << "Job" << job |
| 64 | << "is in the wrong status:" << job->status(); |
| 65 | Q_ASSERT(false); |
| 66 | job->setStatus(BaseJob::Pending); |
| 67 | } |
| 68 | job->sendRequest(); |
| 69 | d->rateLimiter.start(); |
| 70 | return; |
| 71 | } |
| 72 | qCDebug(MAIN) << d->id() << "job queues are empty" ; |
| 73 | }); |
| 74 | } |
| 75 | |
| 76 | ConnectionData::~ConnectionData() |
| 77 | { |
| 78 | d->rateLimiter.disconnect(); |
| 79 | d->rateLimiter.stop(); |
| 80 | } |
| 81 | |
| 82 | void ConnectionData::submit(BaseJob* job) |
| 83 | { |
| 84 | job->setStatus(BaseJob::Pending); |
| 85 | if (!d->rateLimiter.isActive()) { |
| 86 | QTimer::singleShot(interval: 0, receiver: job, slot: &BaseJob::sendRequest); |
| 87 | return; |
| 88 | } |
| 89 | d->jobs[size_t(job->isBackground())].emplace(args&: job); |
| 90 | qCDebug(MAIN) << job << "queued," << d->jobs.front().size() << "(fg) +" |
| 91 | << d->jobs.back().size() << "(bg) total jobs in" << d->id() |
| 92 | << "queues" ; |
| 93 | } |
| 94 | |
| 95 | void ConnectionData::limitRate(std::chrono::milliseconds nextCallAfter) |
| 96 | { |
| 97 | qCDebug(MAIN) << "Jobs for" << (d->userId + u'/' + d->deviceId) |
| 98 | << "suspended for" << nextCallAfter.count() << "ms" ; |
| 99 | d->rateLimiter.start(value: nextCallAfter); |
| 100 | } |
| 101 | |
| 102 | QByteArray ConnectionData::accessToken() const { return d->accessToken; } |
| 103 | |
| 104 | QUrl ConnectionData::baseUrl() const { return d->baseUrl; } |
| 105 | |
| 106 | NetworkAccessManager* ConnectionData::nam() const |
| 107 | { |
| 108 | return NetworkAccessManager::instance(); |
| 109 | } |
| 110 | |
| 111 | void ConnectionData::setBaseUrl(QUrl baseUrl) |
| 112 | { |
| 113 | d->baseUrl = std::move(baseUrl); |
| 114 | qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; |
| 115 | if (!d->userId.isEmpty() && d->baseUrl.isValid()) |
| 116 | NetworkAccessManager::addBaseUrl(accountId: d->userId, homeserver: d->baseUrl); |
| 117 | } |
| 118 | |
| 119 | void ConnectionData::setToken(QByteArray token) |
| 120 | { |
| 121 | d->accessToken = std::move(token); |
| 122 | } |
| 123 | |
| 124 | const QString& ConnectionData::deviceId() const { return d->deviceId; } |
| 125 | |
| 126 | const QString& ConnectionData::userId() const { return d->userId; } |
| 127 | |
| 128 | bool ConnectionData::needsToken(const QString& requestName) const |
| 129 | { |
| 130 | return std::find(first: d->needToken.cbegin(), last: d->needToken.cend(), val: requestName) |
| 131 | != d->needToken.cend(); |
| 132 | } |
| 133 | |
| 134 | void ConnectionData::setDeviceId(const QString& deviceId) |
| 135 | { |
| 136 | d->deviceId = deviceId; |
| 137 | } |
| 138 | |
| 139 | void ConnectionData::setUserId(const QString& userId) |
| 140 | { |
| 141 | if (d->baseUrl.isValid()) { |
| 142 | if (d->userId != userId) |
| 143 | NetworkAccessManager::dropBaseUrl(accountId: d->userId); |
| 144 | if (!userId.isEmpty()) |
| 145 | NetworkAccessManager::addBaseUrl(accountId: userId, homeserver: d->baseUrl); |
| 146 | } |
| 147 | d->userId = userId; |
| 148 | } |
| 149 | |
| 150 | void ConnectionData::setNeedsToken(const QString& requestName) |
| 151 | { |
| 152 | d->needToken.push_back(x: requestName); |
| 153 | } |
| 154 | |
| 155 | QString ConnectionData::lastEvent() const { return d->lastEvent; } |
| 156 | |
| 157 | void ConnectionData::setLastEvent(QString identifier) |
| 158 | { |
| 159 | d->lastEvent = std::move(identifier); |
| 160 | } |
| 161 | |
| 162 | QString ConnectionData::generateTxnId() const |
| 163 | { |
| 164 | return d->deviceId + QString::number(d->txnBase) |
| 165 | + QString::number(++d->txnCounter); |
| 166 | } |
| 167 | |