mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-27 16:32:20 -07:00
Compare commits
3 commits
e3820c3f34
...
5a6db206ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a6db206ce | ||
|
|
64b3b7e0b4 | ||
|
|
61035f8ae0 |
4 changed files with 71 additions and 18 deletions
|
|
@ -17,8 +17,10 @@
|
||||||
#include <version_string.h>
|
#include <version_string.h>
|
||||||
|
|
||||||
static constexpr int MAX_REQUESTS_PER_SEC = 10;
|
static constexpr int MAX_REQUESTS_PER_SEC = 10;
|
||||||
static constexpr int MIN_HOST_QUOTA = 1; ///< Floor for the per-host request allowance
|
static constexpr int MIN_HOST_QUOTA = 1; ///< Floor for the per-host request allowance
|
||||||
static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering
|
static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering
|
||||||
|
static constexpr int DISPATCH_INTERVAL_MS = 100; ///< Pacing between individual network requests
|
||||||
|
static constexpr qint64 QUOTA_RESET_INTERVAL_MS = 1000; ///< Interval at which the request quota resets
|
||||||
|
|
||||||
CardPictureLoaderWorker::CardPictureLoaderWorker()
|
CardPictureLoaderWorker::CardPictureLoaderWorker()
|
||||||
: QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
|
: QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
|
||||||
|
|
@ -60,11 +62,18 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
|
||||||
pictureLoaderThread->start(QThread::LowPriority);
|
pictureLoaderThread->start(QThread::LowPriority);
|
||||||
moveToThread(pictureLoaderThread);
|
moveToThread(pictureLoaderThread);
|
||||||
|
|
||||||
|
// QTimer value members are not QObject children, so moveToThread on the worker doesn't move
|
||||||
|
// them. They must live in the worker's thread to be started from the slot code that runs there.
|
||||||
|
requestTimer.moveToThread(pictureLoaderThread);
|
||||||
|
dispatchTimer.moveToThread(pictureLoaderThread);
|
||||||
|
|
||||||
connect(this, &CardPictureLoaderWorker::imageLoadEnqueued, this, &CardPictureLoaderWorker::handleImageLoadEnqueued);
|
connect(this, &CardPictureLoaderWorker::imageLoadEnqueued, this, &CardPictureLoaderWorker::handleImageLoadEnqueued);
|
||||||
|
|
||||||
connect(&requestTimer, &QTimer::timeout, this, &CardPictureLoaderWorker::resetRequestQuota);
|
connect(&requestTimer, &QTimer::timeout, this, &CardPictureLoaderWorker::resetRequestQuota);
|
||||||
requestTimer.setInterval(1000);
|
requestTimer.setInterval(static_cast<int>(QUOTA_RESET_INTERVAL_MS));
|
||||||
requestTimer.start();
|
|
||||||
|
connect(&dispatchTimer, &QTimer::timeout, this, &CardPictureLoaderWorker::dispatchQueuedRequest);
|
||||||
|
dispatchTimer.setInterval(DISPATCH_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
CardPictureLoaderWorker::~CardPictureLoaderWorker()
|
CardPictureLoaderWorker::~CardPictureLoaderWorker()
|
||||||
|
|
@ -84,8 +93,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
|
||||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
||||||
cache->metaData(url).isValid()) {
|
cache->metaData(url).isValid()) {
|
||||||
// If we hit a cached url, we get to make the request for free, since it won't contribute towards the
|
// A request that will be served from the disk cache never touches the network and therefore
|
||||||
// rate-limit
|
// doesn't use up any of the rate limit, so it gets to skip the queue.
|
||||||
makeRequest(url, worker);
|
makeRequest(url, worker);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -107,10 +116,13 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
||||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||||
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
|
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
|
||||||
|
|
||||||
bool useNetworkCache =
|
// Cached entries are served straight from the disk cache even when picture downloads are
|
||||||
!picDownload && static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
// enabled: re-fetching an already-cached image would burn the rate limit for nothing. Only a
|
||||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
// genuine cache miss goes to the network, and only when downloads are enabled.
|
||||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
|
bool useNetworkCache = static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||||
|
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||||
|
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
||||||
|
(cache->metaData(url).isValid() || !picDownload);
|
||||||
|
|
||||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
||||||
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
|
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
|
||||||
|
|
@ -126,6 +138,9 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
||||||
void CardPictureLoaderWorker::resetRequestQuota()
|
void CardPictureLoaderWorker::resetRequestQuota()
|
||||||
{
|
{
|
||||||
requestQuota = MAX_REQUESTS_PER_SEC;
|
requestQuota = MAX_REQUESTS_PER_SEC;
|
||||||
|
// Allowances are seeded per host on demand in processSingleRequest(), so a
|
||||||
|
// rate-limited host never gets a fresh full quota mid-second.
|
||||||
|
hostQuotaRemaining.clear();
|
||||||
|
|
||||||
QDateTime now = QDateTime::currentDateTime();
|
QDateTime now = QDateTime::currentDateTime();
|
||||||
for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) {
|
for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) {
|
||||||
|
|
@ -134,27 +149,53 @@ void CardPictureLoaderWorker::resetRequestQuota()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &request : requestLoadQueue) {
|
|
||||||
const QString host = request.first.host();
|
|
||||||
hostQuotaRemaining.insert(host, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC));
|
|
||||||
}
|
|
||||||
|
|
||||||
processQueuedRequests();
|
processQueuedRequests();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardPictureLoaderWorker::processQueuedRequests()
|
void CardPictureLoaderWorker::processQueuedRequests()
|
||||||
{
|
{
|
||||||
while (requestQuota > 0 && processSingleRequest()) {
|
if (requestLoadQueue.isEmpty()) {
|
||||||
|
dispatchTimer.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Start lazily from the worker's own thread: QTimer must be started in the thread it lives in.
|
||||||
|
if (!requestTimer.isActive()) {
|
||||||
|
requestTimer.start();
|
||||||
|
}
|
||||||
|
dispatchTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardPictureLoaderWorker::dispatchQueuedRequest()
|
||||||
|
{
|
||||||
|
if (requestLoadQueue.isEmpty() || requestQuota <= 0) {
|
||||||
|
dispatchTimer.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processSingleRequest()) {
|
||||||
--requestQuota;
|
--requestQuota;
|
||||||
|
} else {
|
||||||
|
// No queued host currently has allowance left in this second; wait for the quota reset.
|
||||||
|
dispatchTimer.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CardPictureLoaderWorker::processSingleRequest()
|
bool CardPictureLoaderWorker::processSingleRequest()
|
||||||
{
|
{
|
||||||
|
QDateTime now = QDateTime::currentDateTime();
|
||||||
for (int i = 0; i < requestLoadQueue.size(); ++i) {
|
for (int i = 0; i < requestLoadQueue.size(); ++i) {
|
||||||
const auto &request = requestLoadQueue.at(i);
|
const auto &request = requestLoadQueue.at(i);
|
||||||
QString host = request.first.host();
|
const QString host = request.first.host();
|
||||||
int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC);
|
// Don't dispatch requests to a host that is currently in its 429 backoff.
|
||||||
|
if (CardPictureLoaderWorkerWork::rateLimiter().isRateLimited(host, now)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Seed the allowance only now, so a host that was rate limited last second
|
||||||
|
// doesn't get a fresh full quota the moment it is queried mid-second.
|
||||||
|
if (!hostQuotaRemaining.contains(host)) {
|
||||||
|
hostQuotaRemaining.insert(host, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC));
|
||||||
|
}
|
||||||
|
int allowance = hostQuotaRemaining.value(host);
|
||||||
if (allowance > 0) {
|
if (allowance > 0) {
|
||||||
hostQuotaRemaining.insert(host, allowance - 1);
|
hostQuotaRemaining.insert(host, allowance - 1);
|
||||||
makeRequest(request.first, request.second);
|
makeRequest(request.first, request.second);
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,9 @@ public slots:
|
||||||
/** @brief Processes all queued requests respecting the request quota. */
|
/** @brief Processes all queued requests respecting the request quota. */
|
||||||
void processQueuedRequests();
|
void processQueuedRequests();
|
||||||
|
|
||||||
|
/** @brief Chooses a request from the queue and starts it, respecting the quota and pacing. */
|
||||||
|
void dispatchQueuedRequest();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Processes a single queued request.
|
* @brief Processes a single queued request.
|
||||||
* @return true if a request was processed, false if queue is empty.
|
* @return true if a request was processed, false if queue is empty.
|
||||||
|
|
@ -120,6 +123,7 @@ private:
|
||||||
|
|
||||||
int requestQuota; ///< Remaining requests allowed per second
|
int requestQuota; ///< Remaining requests allowed per second
|
||||||
QTimer requestTimer; ///< Timer to reset the request quota
|
QTimer requestTimer; ///< Timer to reset the request quota
|
||||||
|
QTimer dispatchTimer; ///< Timer pacing individual network requests
|
||||||
QHash<QString, int> hostRequestQuota; ///< Sustained per-host request allowance
|
QHash<QString, int> hostRequestQuota; ///< Sustained per-host request allowance
|
||||||
QHash<QString, int> hostQuotaRemaining; ///< Per-host allowance left in the current second
|
QHash<QString, int> hostQuotaRemaining; ///< Per-host allowance left in the current second
|
||||||
QHash<QString, QDateTime> hostLast429; ///< When each host was last rate limited
|
QHash<QString, QDateTime> hostLast429; ///< When each host was last rate limited
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ static const QStringList MD5_BLACKLIST = {
|
||||||
"fbc7d763c08771c260b39e2115414eeb" // Current card back hash
|
"fbc7d763c08771c260b39e2115414eeb" // Current card back hash
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ServerRateLimiter &CardPictureLoaderWorkerWork::rateLimiter()
|
||||||
|
{
|
||||||
|
return s_rateLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
|
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
|
||||||
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
|
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
|
||||||
picDownload(SettingsCache::instance().downloads().getPicDownload())
|
picDownload(SettingsCache::instance().downloads().getPicDownload())
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ public:
|
||||||
|
|
||||||
CardPictureToLoad cardToDownload; ///< The card and associated URLs to try downloading
|
CardPictureToLoad cardToDownload; ///< The card and associated URLs to try downloading
|
||||||
|
|
||||||
|
/** @brief Shared per-server 429 backoff state. */
|
||||||
|
static ServerRateLimiter &rateLimiter();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
/**
|
||||||
* @brief Handles a finished network reply for the card image.
|
* @brief Handles a finished network reply for the card image.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue