QGIS API Documentation 3.41.0-Master (1deb1daf037)
Loading...
Searching...
No Matches
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#include "moc_qgstiledownloadmanager.cpp"
20
21#include "qgslogger.h"
24#include "qgssettings.h"
27
28#include <QElapsedTimer>
29#include <QNetworkReply>
30#include <QStandardPaths>
31#include <QRegularExpression>
32
34
35QgsTileDownloadManagerWorker::QgsTileDownloadManagerWorker( QgsTileDownloadManager *manager, QObject *parent )
36 : QObject( parent )
37 , mManager( manager )
38 , mIdleTimer( this )
39{
40 connect( &mIdleTimer, &QTimer::timeout, this, &QgsTileDownloadManagerWorker::idleTimerTimeout );
41}
42
43void QgsTileDownloadManagerWorker::startIdleTimer()
44{
45 if ( !mIdleTimer.isActive() )
46 {
47 mIdleTimer.start( mManager->mIdleThreadTimeoutMs );
48 }
49}
50
51void QgsTileDownloadManagerWorker::queueUpdated()
52{
53 const QMutexLocker locker( &mManager->mMutex );
54
55 if ( mManager->mShuttingDown )
56 {
57 // here we HAVE to build up a list of replies from the queue before do anything
58 // with them. Otherwise we can hit the situation where aborting the replies
59 // triggers immediately their removal from the queue, and we'll be modifying
60 // mQueue elsewhere while still trying to iterate over it here => crash
61 // WARNING: there may be event loops/processEvents in play here, because in some circumstances
62 // (authentication handling, ssl errors) QgsNetworkAccessManager will trigger these.
63 std::vector< QNetworkReply * > replies;
64 replies.reserve( mManager->mQueue.size() );
65 for ( auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
66 {
67 replies.emplace_back( it->networkReply );
68 }
69 // now abort all replies
70 for ( QNetworkReply *reply : replies )
71 {
72 reply->abort();
73 }
74
75 quitThread();
76 return;
77 }
78
79 if ( mIdleTimer.isActive() && !mManager->mQueue.empty() )
80 {
81 // if timer to kill thread is running: stop the timer, we have work to do
82 mIdleTimer.stop();
83 }
84
85 // There's a potential race here -- if a reply finishes while we're still in the middle of iterating over the queue,
86 // then the associated queue entry would get removed while we're iterating over the queue here.
87 // So instead defer the actual queue removal until we've finished iterating over the queue.
88 // WARNING: there may be event loops/processEvents in play here, because in some circumstances
89 // (authentication handling, ssl errors) QgsNetworkAccessManager will trigger these.
90 mManager->mStageQueueRemovals = true;
91 for ( auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
92 {
93 if ( !it->networkReply )
94 {
95 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: starting request: " ) + it->request.url().toString(), 2 );
96 // start entries which are not in progress
97
98 QNetworkRequest request( it->request );
99 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
100 it->networkReply = QgsNetworkAccessManager::instance()->get( request );
101 connect( it->networkReply, &QNetworkReply::finished, it->objWorker, &QgsTileDownloadManagerReplyWorkerObject::replyFinished );
102
103 ++mManager->mStats.networkRequestsStarted;
104 }
105 }
106 mManager->mStageQueueRemovals = false;
107 mManager->processStagedEntryRemovals();
108}
109
110void QgsTileDownloadManagerWorker::quitThread()
111{
112 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: stopping worker thread" ), 2 );
113
114 mManager->mWorker->deleteLater();
115 mManager->mWorker = nullptr;
116 // we signal to our worker thread it's time to go. Its finished() signal is connected
117 // to deleteLater() call, so it will get deleted automatically
118 mManager->mWorkerThread->quit();
119 mManager->mWorkerThread = nullptr;
120 mManager->mShuttingDown = false;
121}
122
123void QgsTileDownloadManagerWorker::idleTimerTimeout()
124{
125 const QMutexLocker locker( &mManager->mMutex );
126 Q_ASSERT( mManager->mQueue.empty() );
127 quitThread();
128}
129
130
132
133
134void QgsTileDownloadManagerReplyWorkerObject::replyFinished()
135{
136 const QMutexLocker locker( &mManager->mMutex );
137
138 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: internal reply finished: " ) + mRequest.url().toString(), 2 );
139
140 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
141 QByteArray data;
142
143 if ( reply->error() == QNetworkReply::NoError )
144 {
145 ++mManager->mStats.networkRequestsOk;
146 data = reply->readAll();
147 }
148 else
149 {
150 ++mManager->mStats.networkRequestsFailed;
151 const QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
152 if ( contentType.startsWith( QLatin1String( "text/plain" ) ) )
153 data = reply->readAll();
154 }
155
156 QMap<QNetworkRequest::Attribute, QVariant> attributes;
157 attributes.insert( QNetworkRequest::SourceIsFromCacheAttribute, reply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ) );
158 attributes.insert( QNetworkRequest::RedirectionTargetAttribute, reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) );
159 attributes.insert( QNetworkRequest::HttpStatusCodeAttribute, reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ) );
160 attributes.insert( QNetworkRequest::HttpReasonPhraseAttribute, reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ) );
161
162 QMap<QNetworkRequest::KnownHeaders, QVariant> headers;
163 headers.insert( QNetworkRequest::ContentTypeHeader, reply->header( QNetworkRequest::ContentTypeHeader ) );
164
165 // Save loaded data to cache
166 int httpStatusCode = reply->attribute( QNetworkRequest::Attribute::HttpStatusCodeAttribute ).toInt();
167 if ( httpStatusCode == 206 && mManager->isRangeRequest( mRequest ) )
168 {
169 mManager->mRangesCache->registerEntry( mRequest, data );
170 }
171
172 emit finished( data, reply->url(), attributes, headers, reply->rawHeaderPairs(), reply->error(), reply->errorString() );
173
174 reply->deleteLater();
175
176 // kill the worker obj
177 deleteLater();
178
179 mManager->removeEntry( mRequest );
180
181 if ( mManager->mQueue.empty() )
182 {
183 // if this was the last thing in the queue, start a timer to kill thread after X seconds
184 mManager->mWorker->startIdleTimer();
185 }
186}
187
189
191
192
194{
195 mRangesCache.reset( new QgsRangeRequestCache );
196
197 const QgsSettings settings;
199 if ( cacheDirectory.isEmpty() )
200 cacheDirectory = QStandardPaths::writableLocation( QStandardPaths::CacheLocation );
201 if ( !cacheDirectory.endsWith( QDir::separator() ) )
202 {
203 cacheDirectory.push_back( QDir::separator() );
204 }
205 cacheDirectory += QLatin1String( "http-ranges" );
206 mRangesCache->setCacheDirectory( cacheDirectory );
208 mRangesCache->setCacheSize( cacheSize );
209}
210
212{
213 // make sure the worker thread is gone and any pending requests are canceled
214 shutdown();
215}
216
218{
219 const QMutexLocker locker( &mMutex );
220
221 if ( isCachedRangeRequest( request ) )
222 {
223 QgsTileDownloadManagerReply *reply = new QgsTileDownloadManagerReply( this, request ); // lives in the same thread as the caller
224 QTimer::singleShot( 0, reply, &QgsTileDownloadManagerReply::cachedRangeRequestFinished );
225 return reply;
226 }
227
228 if ( !mWorker )
229 {
230 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: starting worker thread" ), 2 );
231 mWorkerThread = new QThread;
232 mWorker = new QgsTileDownloadManagerWorker( this );
233 mWorker->moveToThread( mWorkerThread );
234 QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
235 mWorkerThread->start();
236 }
237
238 QgsTileDownloadManagerReply *reply = new QgsTileDownloadManagerReply( this, request ); // lives in the same thread as the caller
239
240 ++mStats.requestsTotal;
241
242 QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
243 if ( !entry.isValid() )
244 {
245 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (new entry): " ) + request.url().toString(), 2 );
246 // create a new entry and add it to queue
247 entry.request = request;
248 entry.objWorker = new QgsTileDownloadManagerReplyWorkerObject( this, request );
249 entry.objWorker->moveToThread( mWorkerThread );
250
251 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
252
253 addEntry( entry );
254 }
255 else
256 {
257 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (existing entry): " ) + request.url().toString(), 2 );
258
259 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
260
261 ++mStats.requestsMerged;
262 }
263
264 signalQueueModified();
265
266 return reply;
267}
268
270{
271 const QMutexLocker locker( &mMutex );
272
273 return !mQueue.empty();
274}
275
277{
278 QElapsedTimer t;
279 t.start();
280
281 while ( msec == -1 || t.elapsed() < msec )
282 {
283 {
284 const QMutexLocker locker( &mMutex );
285 if ( mQueue.empty() )
286 return true;
287 }
288 QThread::usleep( 1000 );
289 }
290
291 return false;
292}
293
295{
296 {
297 const QMutexLocker locker( &mMutex );
298 if ( !mWorkerThread )
299 return; // nothing to stop
300
301 // let's signal to the thread
302 mShuttingDown = true;
303 signalQueueModified();
304 }
305
306 // wait until the thread is gone
307 while ( 1 )
308 {
309 {
310 const QMutexLocker locker( &mMutex );
311 if ( !mWorkerThread )
312 return; // the thread has stopped
313 }
314
315 QThread::usleep( 1000 );
316 }
317}
318
320{
321 return mWorkerThread && mWorkerThread->isRunning();
322}
323
325{
326 const QMutexLocker locker( &mMutex );
328}
329
330QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest( const QNetworkRequest &request )
331{
332 for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
333 {
334 if ( it->request.url() == request.url() && it->request.rawHeader( "Range" ) == request.rawHeader( "Range" ) )
335 return *it;
336 }
337 return QgsTileDownloadManager::QueueEntry();
338}
339
340void QgsTileDownloadManager::addEntry( const QgsTileDownloadManager::QueueEntry &entry )
341{
342 for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
343 {
344 Q_ASSERT( entry.request.url() != it->request.url() || entry.request.rawHeader( "Range" ) != it->request.rawHeader( "Range" ) );
345 }
346
347 mQueue.emplace_back( entry );
348}
349
350void QgsTileDownloadManager::updateEntry( const QgsTileDownloadManager::QueueEntry &entry )
351{
352 for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
353 {
354 if ( entry.request.url() == it->request.url() && entry.request.rawHeader( "Range" ) == it->request.rawHeader( "Range" ) )
355 {
356 *it = entry;
357 return;
358 }
359 }
360 Q_ASSERT( false );
361}
362
363void QgsTileDownloadManager::removeEntry( const QNetworkRequest &request )
364{
365 if ( mStageQueueRemovals )
366 {
367 mStagedQueueRemovals.emplace_back( request );
368 }
369 else
370 {
371 for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
372 {
373 if ( it->request.url() == request.url() && it->request.rawHeader( "Range" ) == request.rawHeader( "Range" ) )
374 {
375 mQueue.erase( it );
376 return;
377 }
378 }
379 Q_ASSERT( false );
380 }
381}
382
383void QgsTileDownloadManager::processStagedEntryRemovals()
384{
385 Q_ASSERT( !mStageQueueRemovals );
386 for ( const QNetworkRequest &request : mStagedQueueRemovals )
387 {
388 removeEntry( request );
389 }
390 mStagedQueueRemovals.clear();
391}
392
393void QgsTileDownloadManager::signalQueueModified()
394{
395 QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
396}
397
398bool QgsTileDownloadManager::isRangeRequest( const QNetworkRequest &request )
399{
400 if ( request.rawHeader( "Range" ).isEmpty() )
401 return false;
402 const thread_local QRegularExpression regex( "^bytes=\\d+-\\d+$" );
403 QRegularExpressionMatch match = regex.match( QString::fromUtf8( request.rawHeader( "Range" ) ) );
404 return match.hasMatch();
405}
406
407bool QgsTileDownloadManager::isCachedRangeRequest( const QNetworkRequest &request )
408{
409 QNetworkRequest::CacheLoadControl loadControl = ( QNetworkRequest::CacheLoadControl ) request.attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt();
410 bool saveControl = request.attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool();
411 return isRangeRequest( request ) && saveControl && loadControl != QNetworkRequest::AlwaysNetwork && mRangesCache->hasEntry( request );
412}
413
415
416
417QgsTileDownloadManagerReply::QgsTileDownloadManagerReply( QgsTileDownloadManager *manager, const QNetworkRequest &request )
418 : mManager( manager )
419 , mRequest( request )
420{
421}
422
424{
425 const QMutexLocker locker( &mManager->mMutex );
426
427 if ( !mHasFinished )
428 {
429 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: reply deleted before finished: " ) + mRequest.url().toString(), 2 );
430
431 ++mManager->mStats.requestsEarlyDeleted;
432 }
433}
434
435void 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 )
436{
437 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: reply finished: " ) + mRequest.url().toString(), 2 );
438
439 mHasFinished = true;
440 mData = data;
441 mUrl = url;
442 mAttributes = attributes;
443 mHeaders = headers;
444 mRawHeaderPairs = rawHeaderPairs;
445 mError = error;
446 mErrorString = errorString;
447 emit finished();
448}
449
450void QgsTileDownloadManagerReply::cachedRangeRequestFinished()
451{
452 QgsDebugMsgLevel( QStringLiteral( "Tile download manager: internal range request reply loaded from cache: " ) + mRequest.url().toString(), 2 );
453 mHasFinished = true;
454 mData = mManager->mRangesCache->entry( mRequest );
455 mUrl = mRequest.url();
456 emit finished();
457}
458
459QVariant QgsTileDownloadManagerReply::attribute( QNetworkRequest::Attribute code )
460{
461 return mAttributes.contains( code ) ? mAttributes.value( code ) : QVariant();
462}
463
464QVariant QgsTileDownloadManagerReply::header( QNetworkRequest::KnownHeaders header )
465{
466 return mHeaders.contains( header ) ? mHeaders.value( header ) : QVariant();
467}
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.
T value(const QString &dynamicKeyPart=QString()) const
Returns settings value.
static const QgsSettingsEntryInteger64 * settingsNetworkCacheSize
Settings entry network cache directory.
static const QgsSettingsEntryString * settingsNetworkCacheDirectory
Settings entry network cache directory.
This class is a composition of two QSettings instances:
Definition qgssettings.h:64
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:41