QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
qgstiledownloadmanager.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstiledownloadmanager.cpp
3 --------------------------
4 begin : January 2021
5 copyright : (C) 2021 by Martin Dobias
6 email : wonder dot sk at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19
20#include "qgslogger.h"
23
24#include <QElapsedTimer>
25#include <QNetworkReply>
26#include <QStandardPaths>
27#include <QRegularExpression>
28
30
31QgsTileDownloadManagerWorker::QgsTileDownloadManagerWorker( QgsTileDownloadManager *manager, QObject *parent )
32 : QObject( parent )
33 , mManager( manager )
34 , mIdleTimer( this )
35{
36 connect( &mIdleTimer, &QTimer::timeout, this, &QgsTileDownloadManagerWorker::idleTimerTimeout );
37}
38
39void QgsTileDownloadManagerWorker::startIdleTimer()
40{
41 if ( !mIdleTimer.isActive() )
42 {
43 mIdleTimer.start( mManager->mIdleThreadTimeoutMs );
44 }
45}
46
47void QgsTileDownloadManagerWorker::queueUpdated()
48{
49 const QMutexLocker locker( &mManager->mMutex );
50
51 if ( mManager->mShuttingDown )
52 {
53 // here we HAVE to build up a list of replies from the queue before do anything
54 // with them. Otherwise we can hit the situation where aborting the replies
55 // triggers immediately their removal from the queue, and we'll be modifying
56 // mQueue elsewhere while still trying to iterate over it here => crash
57 // WARNING: there may be event loops/processEvents in play here, because in some circumstances
58 // (authentication handling, ssl errors) QgsNetworkAccessManager will trigger these.
59 std::vector< QNetworkReply * > replies;
60 replies.reserve( mManager->mQueue.size() );
61 for ( auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
62 {
63 replies.emplace_back( it->networkReply );
64 }
65 // now abort all replies
66 for ( QNetworkReply *reply : replies )
67 {
68 reply->abort();
69 }
70
71 quitThread();
72 return;
73 }
74
75 if ( mIdleTimer.isActive() && !mManager->mQueue.empty() )
76 {
77 // if timer to kill thread is running: stop the timer, we have work to do
78 mIdleTimer.stop();
79 }
80
81 // There's a potential race here -- if a reply finishes while we're still in the middle of iterating over the queue,
82 // then the associated queue entry would get removed while we're iterating over the queue here.
83 // So instead defer the actual queue removal until we've finished iterating over the queue.
84 // WARNING: there may be event loops/processEvents in play here, because in some circumstances
85 // (authentication handling, ssl errors) QgsNetworkAccessManager will trigger these.
86 mManager->mStageQueueRemovals = true;
87 for ( auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
88 {
89 if ( !it->networkReply )
90 {
91 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: starting request: " ) + it->request.url().toString(), 2 );
92 // start entries which are not in progress
93
94 it->networkReply = QgsNetworkAccessManager::instance()->get( it->request );
95 connect( it->networkReply, &QNetworkReply::finished, it->objWorker, &QgsTileDownloadManagerReplyWorkerObject::replyFinished );
96
97 ++mManager->mStats.networkRequestsStarted;
98 }
99 }
100 mManager->mStageQueueRemovals = false;
101 mManager->processStagedEntryRemovals();
102}
103
104void QgsTileDownloadManagerWorker::quitThread()
105{
106 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: stopping worker thread" ), 2 );
107
108 mManager->mWorker->deleteLater();
109 mManager->mWorker = nullptr;
110 // we signal to our worker thread it's time to go. Its finished() signal is connected
111 // to deleteLater() call, so it will get deleted automatically
112 mManager->mWorkerThread->quit();
113 mManager->mWorkerThread = nullptr;
114 mManager->mShuttingDown = false;
115}
116
117void QgsTileDownloadManagerWorker::idleTimerTimeout()
118{
119 const QMutexLocker locker( &mManager->mMutex );
120 Q_ASSERT( mManager->mQueue.empty() );
121 quitThread();
122}
123
124
126
127
128void QgsTileDownloadManagerReplyWorkerObject::replyFinished()
129{
130 const QMutexLocker locker( &mManager->mMutex );
131
132 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: internal reply finished: " ) + mRequest.url().toString(), 2 );
133
134 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
135 QByteArray data;
136
137 if ( reply->error() == QNetworkReply::NoError )
138 {
139 ++mManager->mStats.networkRequestsOk;
140 data = reply->readAll();
141 }
142 else
143 {
144 ++mManager->mStats.networkRequestsFailed;
145 const QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
146 if ( contentType.startsWith( QLatin1String( "text/plain" ) ) )
147 data = reply->readAll();
148 }
149
150 QMap<QNetworkRequest::Attribute, QVariant> attributes;
151 attributes.insert( QNetworkRequest::SourceIsFromCacheAttribute, reply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ) );
152 attributes.insert( QNetworkRequest::RedirectionTargetAttribute, reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) );
153 attributes.insert( QNetworkRequest::HttpStatusCodeAttribute, reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ) );
154 attributes.insert( QNetworkRequest::HttpReasonPhraseAttribute, reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ) );
155
156 QMap<QNetworkRequest::KnownHeaders, QVariant> headers;
157 headers.insert( QNetworkRequest::ContentTypeHeader, reply->header( QNetworkRequest::ContentTypeHeader ) );
158
159 // Save loaded data to cache
160 int httpStatusCode = reply->attribute( QNetworkRequest::Attribute::HttpStatusCodeAttribute ).toInt();
161 if ( httpStatusCode == 206 && mManager->isRangeRequest( mRequest ) )
162 {
163 mManager->mRangesCache->registerEntry( mRequest, data );
164 }
165
166 emit finished( data, reply->url(), attributes, headers, reply->rawHeaderPairs(), reply->error(), reply->errorString() );
167
168 reply->deleteLater();
169
170 // kill the worker obj
171 deleteLater();
172
173 mManager->removeEntry( mRequest );
174
175 if ( mManager->mQueue.empty() )
176 {
177 // if this was the last thing in the queue, start a timer to kill thread after X seconds
178 mManager->mWorker->startIdleTimer();
179 }
180}
181
183
185
186
188{
189 mRangesCache.reset( new QgsRangeRequestCache );
190
191 const QgsSettings settings;
192 QString cacheDirectory = settings.value( QStringLiteral( "cache/directory" ) ).toString();
193 if ( cacheDirectory.isEmpty() )
194 cacheDirectory = QStandardPaths::writableLocation( QStandardPaths::CacheLocation );
195 if ( !cacheDirectory.endsWith( QDir::separator() ) )
196 {
197 cacheDirectory.push_back( QDir::separator() );
198 }
199 cacheDirectory += QLatin1String( "http-ranges" );
200 const qint64 cacheSize = settings.value( QStringLiteral( "cache/size" ), 256 * 1024 * 1024 ).toLongLong();
201
202 mRangesCache->setCacheDirectory( cacheDirectory );
203 mRangesCache->setCacheSize( cacheSize );
204}
205
207{
208 // make sure the worker thread is gone and any pending requests are canceled
209 shutdown();
210}
211
213{
214 const QMutexLocker locker( &mMutex );
215
216 if ( isCachedRangeRequest( request ) )
217 {
218 QgsTileDownloadManagerReply *reply = new QgsTileDownloadManagerReply( this, request ); // lives in the same thread as the caller
219 QTimer::singleShot( 0, reply, &QgsTileDownloadManagerReply::cachedRangeRequestFinished );
220 return reply;
221 }
222
223 if ( !mWorker )
224 {
225 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: starting worker thread" ), 2 );
226 mWorkerThread = new QThread;
227 mWorker = new QgsTileDownloadManagerWorker( this );
228 mWorker->moveToThread( mWorkerThread );
229 QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
230 mWorkerThread->start();
231 }
232
233 QgsTileDownloadManagerReply *reply = new QgsTileDownloadManagerReply( this, request ); // lives in the same thread as the caller
234
235 ++mStats.requestsTotal;
236
237 QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
238 if ( !entry.isValid() )
239 {
240 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (new entry): " ) + request.url().toString(), 2 );
241 // create a new entry and add it to queue
242 entry.request = request;
243 entry.objWorker = new QgsTileDownloadManagerReplyWorkerObject( this, request );
244 entry.objWorker->moveToThread( mWorkerThread );
245
246 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
247
248 addEntry( entry );
249 }
250 else
251 {
252 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (existing entry): " ) + request.url().toString(), 2 );
253
254 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
255
256 ++mStats.requestsMerged;
257 }
258
259 signalQueueModified();
260
261 return reply;
262}
263
265{
266 const QMutexLocker locker( &mMutex );
267
268 return !mQueue.empty();
269}
270
272{
273 QElapsedTimer t;
274 t.start();
275
276 while ( msec == -1 || t.elapsed() < msec )
277 {
278 {
279 const QMutexLocker locker( &mMutex );
280 if ( mQueue.empty() )
281 return true;
282 }
283 QThread::usleep( 1000 );
284 }
285
286 return false;
287}
288
290{
291 {
292 const QMutexLocker locker( &mMutex );
293 if ( !mWorkerThread )
294 return; // nothing to stop
295
296 // let's signal to the thread
297 mShuttingDown = true;
298 signalQueueModified();
299 }
300
301 // wait until the thread is gone
302 while ( 1 )
303 {
304 {
305 const QMutexLocker locker( &mMutex );
306 if ( !mWorkerThread )
307 return; // the thread has stopped
308 }
309
310 QThread::usleep( 1000 );
311 }
312}
313
315{
316 return mWorkerThread && mWorkerThread->isRunning();
317}
318
320{
321 const QMutexLocker locker( &mMutex );
323}
324
325QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest( const QNetworkRequest &request )
326{
327 for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
328 {
329 if ( it->request.url() == request.url() && it->request.rawHeader( "Range" ) == request.rawHeader( "Range" ) )
330 return *it;
331 }
332 return QgsTileDownloadManager::QueueEntry();
333}
334
335void QgsTileDownloadManager::addEntry( const QgsTileDownloadManager::QueueEntry &entry )
336{
337 for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
338 {
339 Q_ASSERT( entry.request.url() != it->request.url() || entry.request.rawHeader( "Range" ) != it->request.rawHeader( "Range" ) );
340 }
341
342 mQueue.emplace_back( entry );
343}
344
345void QgsTileDownloadManager::updateEntry( const QgsTileDownloadManager::QueueEntry &entry )
346{
347 for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
348 {
349 if ( entry.request.url() == it->request.url() && entry.request.rawHeader( "Range" ) == it->request.rawHeader( "Range" ) )
350 {
351 *it = entry;
352 return;
353 }
354 }
355 Q_ASSERT( false );
356}
357
358void QgsTileDownloadManager::removeEntry( const QNetworkRequest &request )
359{
360 if ( mStageQueueRemovals )
361 {
362 mStagedQueueRemovals.emplace_back( request );
363 }
364 else
365 {
366 for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
367 {
368 if ( it->request.url() == request.url() && it->request.rawHeader( "Range" ) == request.rawHeader( "Range" ) )
369 {
370 mQueue.erase( it );
371 return;
372 }
373 }
374 Q_ASSERT( false );
375 }
376}
377
378void QgsTileDownloadManager::processStagedEntryRemovals()
379{
380 Q_ASSERT( !mStageQueueRemovals );
381 for ( const QNetworkRequest &request : mStagedQueueRemovals )
382 {
383 removeEntry( request );
384 }
385 mStagedQueueRemovals.clear();
386}
387
388void QgsTileDownloadManager::signalQueueModified()
389{
390 QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
391}
392
393bool QgsTileDownloadManager::isRangeRequest( const QNetworkRequest &request )
394{
395 if ( request.rawHeader( "Range" ).isEmpty() )
396 return false;
397 QRegularExpression regex( "^bytes=\\d+-\\d+$" );
398 QRegularExpressionMatch match = regex.match( QString::fromUtf8( request.rawHeader( "Range" ) ) );
399 return match.hasMatch();
400}
401
402bool QgsTileDownloadManager::isCachedRangeRequest( const QNetworkRequest &request )
403{
404 QNetworkRequest::CacheLoadControl loadControl = ( QNetworkRequest::CacheLoadControl ) request.attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt();
405 bool saveControl = request.attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool();
406 return isRangeRequest( request ) && saveControl && loadControl != QNetworkRequest::AlwaysNetwork && mRangesCache->hasEntry( request );
407}
408
410
411
412QgsTileDownloadManagerReply::QgsTileDownloadManagerReply( QgsTileDownloadManager *manager, const QNetworkRequest &request )
413 : mManager( manager )
414 , mRequest( request )
415{
416}
417
419{
420 const QMutexLocker locker( &mManager->mMutex );
421
422 if ( !mHasFinished )
423 {
424 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: reply deleted before finished: " ) + mRequest.url().toString(), 2 );
425
426 ++mManager->mStats.requestsEarlyDeleted;
427 }
428}
429
430void QgsTileDownloadManagerReply::requestFinished( QByteArray data, QUrl url, const QMap<QNetworkRequest::Attribute, QVariant> &attributes, const QMap<QNetworkRequest::KnownHeaders, QVariant> &headers, const QList<QNetworkReply::RawHeaderPair> rawHeaderPairs, QNetworkReply::NetworkError error, const QString &errorString )
431{
432 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: reply finished: " ) + mRequest.url().toString(), 2 );
433
434 mHasFinished = true;
435 mData = data;
436 mUrl = url;
437 mAttributes = attributes;
438 mHeaders = headers;
439 mRawHeaderPairs = rawHeaderPairs;
440 mError = error;
441 mErrorString = errorString;
442 emit finished();
443}
444
445void QgsTileDownloadManagerReply::cachedRangeRequestFinished()
446{
447 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: internal range request reply loaded from cache: " ) + mRequest.url().toString(), 2 );
448 mHasFinished = true;
449 mData = mManager->mRangesCache->entry( mRequest );
450 mUrl = mRequest.url();
451 emit finished();
452}
453
454QVariant QgsTileDownloadManagerReply::attribute( QNetworkRequest::Attribute code )
455{
456 return mAttributes.contains( code ) ? mAttributes.value( code ) : QVariant();
457}
458
459QVariant QgsTileDownloadManagerReply::header( QNetworkRequest::KnownHeaders header )
460{
461 return mHeaders.contains( header ) ? mHeaders.value( header ) : QVariant();
462}
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
A custom cache for handling the storage and retrieval of HTTP range requests on disk.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
Reply object for tile download manager requests returned from calls to QgsTileDownloadManager::get().
QString errorString() const
Returns error string (only valid when already finished)
const QList< QNetworkReply::RawHeaderPair > rawHeaderPairs() const
Returns a list of raw header pairs.
QByteArray data() const
Returns binary data returned in the reply (only valid when already finished)
QNetworkReply::NetworkError error() const
Returns error code (only valid when already finished)
QUrl url() const
Returns the reply URL.
QVariant header(QNetworkRequest::KnownHeaders header)
Returns the value of the known header header.
QVariant attribute(QNetworkRequest::Attribute code)
Returns the attribute associated with the code.
void finished()
Emitted when the reply has finished (either with a success or with a failure)
Encapsulates any statistics we would like to keep about requests.
int requestsMerged
How many requests were same as some other pending request and got "merged".
int requestsEarlyDeleted
How many requests were deleted early by the client (i.e. lost interest)
int requestsTotal
How many requests were done through the download manager.
Tile download manager handles downloads of map tiles for the purpose of map rendering.
bool hasWorkerThreadRunning() const
Returns whether the worker thread is running currently (it may be stopped if there were no requests r...
friend class QgsTileDownloadManagerReplyWorkerObject
bool waitForPendingRequests(int msec=-1) const
Blocks the current thread until the queue is empty.
QgsTileDownloadManagerReply * get(const QNetworkRequest &request)
Starts a request.
bool hasPendingRequests() const
Returns whether there are any pending requests in the queue.
void resetStatistics()
Resets statistics of numbers of queries handled by this class.
void shutdown()
Asks the worker thread to stop and blocks until it is not stopped.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39