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