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