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