QGIS API Documentation  3.26.3-Buenos Aires (65e4edfdad)
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 
18 #include "qgstiledownloadmanager.h"
19 
20 #include "qgslogger.h"
22 #include "qgsrangerequestcache.h"
23 
24 #include <QElapsedTimer>
25 #include <QNetworkReply>
26 #include <QStandardPaths>
27 #include <QRegularExpression>
28 
30 
31 QgsTileDownloadManagerWorker::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 
39 void QgsTileDownloadManagerWorker::startIdleTimer()
40 {
41  if ( !mIdleTimer.isActive() )
42  {
43  mIdleTimer.start( mManager->mIdleThreadTimeoutMs );
44  }
45 }
46 
47 void 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 
104 void 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 
117 void QgsTileDownloadManagerWorker::idleTimerTimeout()
118 {
119  const QMutexLocker locker( &mManager->mMutex );
120  Q_ASSERT( mManager->mQueue.empty() );
121  quitThread();
122 }
123 
124 
126 
127 
128 void 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 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
189  : mMutex( QMutex::Recursive )
190 #endif
191 {
192  mRangesCache.reset( new QgsRangeRequestCache );
193 
194  const QgsSettings settings;
195  QString cacheDirectory = settings.value( QStringLiteral( "cache/directory" ) ).toString();
196  if ( cacheDirectory.isEmpty() )
197  cacheDirectory = QStandardPaths::writableLocation( QStandardPaths::CacheLocation );
198  if ( !cacheDirectory.endsWith( QDir::separator() ) )
199  {
200  cacheDirectory.push_back( QDir::separator() );
201  }
202  cacheDirectory += QLatin1String( "http-ranges" );
203  const qint64 cacheSize = settings.value( QStringLiteral( "cache/size" ), 256 * 1024 * 1024 ).toLongLong();
204 
205  mRangesCache->setCacheDirectory( cacheDirectory );
206  mRangesCache->setCacheSize( cacheSize );
207 }
208 
210 {
211  // make sure the worker thread is gone and any pending requests are canceled
212  shutdown();
213 }
214 
216 {
217  const QMutexLocker locker( &mMutex );
218 
219  if ( isCachedRangeRequest( request ) )
220  {
221  QgsTileDownloadManagerReply *reply = new QgsTileDownloadManagerReply( this, request ); // lives in the same thread as the caller
222  QTimer::singleShot( 0, reply, &QgsTileDownloadManagerReply::cachedRangeRequestFinished );
223  return reply;
224  }
225 
226  if ( !mWorker )
227  {
228  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: starting worker thread" ), 2 );
229  mWorkerThread = new QThread;
230  mWorker = new QgsTileDownloadManagerWorker( this );
231  mWorker->moveToThread( mWorkerThread );
232  QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
233  mWorkerThread->start();
234  }
235 
236  QgsTileDownloadManagerReply *reply = new QgsTileDownloadManagerReply( this, request ); // lives in the same thread as the caller
237 
238  ++mStats.requestsTotal;
239 
240  QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
241  if ( !entry.isValid() )
242  {
243  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (new entry): " ) + request.url().toString(), 2 );
244  // create a new entry and add it to queue
245  entry.request = request;
246  entry.objWorker = new QgsTileDownloadManagerReplyWorkerObject( this, request );
247  entry.objWorker->moveToThread( mWorkerThread );
248 
249  QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
250 
251  addEntry( entry );
252  }
253  else
254  {
255  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (existing entry): " ) + request.url().toString(), 2 );
256 
257  QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
258 
259  ++mStats.requestsMerged;
260  }
261 
262  signalQueueModified();
263 
264  return reply;
265 }
266 
268 {
269  const QMutexLocker locker( &mMutex );
270 
271  return !mQueue.empty();
272 }
273 
275 {
276  QElapsedTimer t;
277  t.start();
278 
279  while ( msec == -1 || t.elapsed() < msec )
280  {
281  {
282  const QMutexLocker locker( &mMutex );
283  if ( mQueue.empty() )
284  return true;
285  }
286  QThread::usleep( 1000 );
287  }
288 
289  return false;
290 }
291 
293 {
294  {
295  const QMutexLocker locker( &mMutex );
296  if ( !mWorkerThread )
297  return; // nothing to stop
298 
299  // let's signal to the thread
300  mShuttingDown = true;
301  signalQueueModified();
302  }
303 
304  // wait until the thread is gone
305  while ( 1 )
306  {
307  {
308  const QMutexLocker locker( &mMutex );
309  if ( !mWorkerThread )
310  return; // the thread has stopped
311  }
312 
313  QThread::usleep( 1000 );
314  }
315 }
316 
318 {
319  return mWorkerThread && mWorkerThread->isRunning();
320 }
321 
323 {
324  const QMutexLocker locker( &mMutex );
326 }
327 
328 QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest( const QNetworkRequest &request )
329 {
330  for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
331  {
332  if ( it->request.url() == request.url() && it->request.rawHeader( "Range" ) == request.rawHeader( "Range" ) )
333  return *it;
334  }
335  return QgsTileDownloadManager::QueueEntry();
336 }
337 
338 void QgsTileDownloadManager::addEntry( const QgsTileDownloadManager::QueueEntry &entry )
339 {
340  for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
341  {
342  Q_ASSERT( entry.request.url() != it->request.url() || entry.request.rawHeader( "Range" ) != it->request.rawHeader( "Range" ) );
343  }
344 
345  mQueue.emplace_back( entry );
346 }
347 
348 void QgsTileDownloadManager::updateEntry( const QgsTileDownloadManager::QueueEntry &entry )
349 {
350  for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
351  {
352  if ( entry.request.url() == it->request.url() && entry.request.rawHeader( "Range" ) == it->request.rawHeader( "Range" ) )
353  {
354  *it = entry;
355  return;
356  }
357  }
358  Q_ASSERT( false );
359 }
360 
361 void QgsTileDownloadManager::removeEntry( const QNetworkRequest &request )
362 {
363  if ( mStageQueueRemovals )
364  {
365  mStagedQueueRemovals.emplace_back( request );
366  }
367  else
368  {
369  for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
370  {
371  if ( it->request.url() == request.url() && it->request.rawHeader( "Range" ) == request.rawHeader( "Range" ) )
372  {
373  mQueue.erase( it );
374  return;
375  }
376  }
377  Q_ASSERT( false );
378  }
379 }
380 
381 void QgsTileDownloadManager::processStagedEntryRemovals()
382 {
383  Q_ASSERT( !mStageQueueRemovals );
384  for ( const QNetworkRequest &request : mStagedQueueRemovals )
385  {
386  removeEntry( request );
387  }
388  mStagedQueueRemovals.clear();
389 }
390 
391 void QgsTileDownloadManager::signalQueueModified()
392 {
393  QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
394 }
395 
396 bool QgsTileDownloadManager::isRangeRequest( const QNetworkRequest &request )
397 {
398  if ( request.rawHeader( "Range" ).isEmpty() )
399  return false;
400  QRegularExpression regex( "^bytes=\\d+-\\d+$" );
401  QRegularExpressionMatch match = regex.match( QString::fromUtf8( request.rawHeader( "Range" ) ) );
402  return match.hasMatch();
403 }
404 
405 bool QgsTileDownloadManager::isCachedRangeRequest( const QNetworkRequest &request )
406 {
407  QNetworkRequest::CacheLoadControl loadControl = ( QNetworkRequest::CacheLoadControl ) request.attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt();
408  bool saveControl = request.attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool();
409  return isRangeRequest( request ) && saveControl && loadControl != QNetworkRequest::AlwaysNetwork && mRangesCache->hasEntry( request );
410 }
411 
413 
414 
415 QgsTileDownloadManagerReply::QgsTileDownloadManagerReply( QgsTileDownloadManager *manager, const QNetworkRequest &request )
416  : mManager( manager )
417  , mRequest( request )
418 {
419 }
420 
422 {
423  const QMutexLocker locker( &mManager->mMutex );
424 
425  if ( !mHasFinished )
426  {
427  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: reply deleted before finished: " ) + mRequest.url().toString(), 2 );
428 
429  ++mManager->mStats.requestsEarlyDeleted;
430  }
431 }
432 
433 void 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 )
434 {
435  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: reply finished: " ) + mRequest.url().toString(), 2 );
436 
437  mHasFinished = true;
438  mData = data;
439  mUrl = url;
440  mAttributes = attributes;
441  mHeaders = headers;
442  mRawHeaderPairs = rawHeaderPairs;
443  mError = error;
444  mErrorString = errorString;
445  emit finished();
446 }
447 
448 void QgsTileDownloadManagerReply::cachedRangeRequestFinished()
449 {
450  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: internal range request reply loaded from cache: " ) + mRequest.url().toString(), 2 );
451  mHasFinished = true;
452  mData = mManager->mRangesCache->entry( mRequest );
453  mUrl = mRequest.url();
454  emit finished();
455 }
456 
457 QVariant QgsTileDownloadManagerReply::attribute( QNetworkRequest::Attribute code )
458 {
459  return mAttributes.contains( code ) ? mAttributes.value( code ) : QVariant();
460 }
461 
462 QVariant QgsTileDownloadManagerReply::header( QNetworkRequest::KnownHeaders header )
463 {
464  return mHeaders.contains( header ) ? mHeaders.value( header ) : QVariant();
465 }
QgsSettings::value
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
Definition: qgssettings.cpp:161
QgsTileDownloadManager
Tile download manager handles downloads of map tiles for the purpose of map rendering....
Definition: qgstiledownloadmanager.h:202
QgsTileDownloadManager::shutdown
void shutdown()
Asks the worker thread to stop and blocks until it is not stopped.
Definition: qgstiledownloadmanager.cpp:292
QgsTileDownloadManagerReply::rawHeaderPairs
const QList< QNetworkReply::RawHeaderPair > rawHeaderPairs() const
Returns a list of raw header pairs.
Definition: qgstiledownloadmanager.h:64
QgsTileDownloadManagerReply::finished
void finished()
Emitted when the reply has finished (either with a success or with a failure)
QgsDebugMsgLevel
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
qgstiledownloadmanager.h
QgsTileDownloadManager::QgsTileDownloadManager
QgsTileDownloadManager()
Definition: qgstiledownloadmanager.cpp:187
QgsSettings
This class is a composition of two QSettings instances:
Definition: qgssettings.h:61
QgsTileDownloadManager::Stats::requestsTotal
int requestsTotal
How many requests were done through the download manager.
Definition: qgstiledownloadmanager.h:230
QgsTileDownloadManager::waitForPendingRequests
bool waitForPendingRequests(int msec=-1) const
Blocks the current thread until the queue is empty.
Definition: qgstiledownloadmanager.cpp:274
QgsTileDownloadManagerReply::~QgsTileDownloadManagerReply
~QgsTileDownloadManagerReply()
Definition: qgstiledownloadmanager.cpp:421
QgsTileDownloadManager::resetStatistics
void resetStatistics()
Resets statistics of numbers of queries handled by this class.
Definition: qgstiledownloadmanager.cpp:322
QgsTileDownloadManager::QgsTileDownloadManagerReply
friend class QgsTileDownloadManagerReply
Definition: qgstiledownloadmanager.h:284
QgsTileDownloadManager::QgsTileDownloadManagerWorker
friend class QgsTileDownloadManagerWorker
Definition: qgstiledownloadmanager.h:283
QgsTileDownloadManager::~QgsTileDownloadManager
~QgsTileDownloadManager()
Definition: qgstiledownloadmanager.cpp:209
QgsTileDownloadManager::QgsTileDownloadManagerReplyWorkerObject
friend class QgsTileDownloadManagerReplyWorkerObject
Definition: qgstiledownloadmanager.h:285
QgsTileDownloadManagerReply::errorString
QString errorString() const
Returns error string (only valid when already finished)
Definition: qgstiledownloadmanager.h:68
qgsnetworkaccessmanager.h
QgsTileDownloadManagerReply::data
QByteArray data() const
Returns binary data returned in the reply (only valid when already finished)
Definition: qgstiledownloadmanager.h:56
qgsrangerequestcache.h
QgsTileDownloadManager::Stats::requestsMerged
int requestsMerged
How many requests were same as some other pending request and got "merged".
Definition: qgstiledownloadmanager.h:232
QgsTileDownloadManager::hasWorkerThreadRunning
bool hasWorkerThreadRunning() const
Returns whether the worker thread is running currently (it may be stopped if there were no requests r...
Definition: qgstiledownloadmanager.cpp:317
QgsNetworkAccessManager::instance
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
Definition: qgsnetworkaccessmanager.cpp:202
QgsRangeRequestCache
A custom cache for handling the storage and retrieval of HTTP range requests on disk.
Definition: qgsrangerequestcache.h:43
QgsTileDownloadManagerReply::attribute
QVariant attribute(QNetworkRequest::Attribute code)
Returns the attribute associated with the code.
Definition: qgstiledownloadmanager.cpp:457
QgsTileDownloadManagerReply::header
QVariant header(QNetworkRequest::KnownHeaders header)
Returns the value of the known header header.
Definition: qgstiledownloadmanager.cpp:462
QgsTileDownloadManager::Stats::requestsEarlyDeleted
int requestsEarlyDeleted
How many requests were deleted early by the client (i.e. lost interest)
Definition: qgstiledownloadmanager.h:234
qgslogger.h
QgsTileDownloadManager::get
QgsTileDownloadManagerReply * get(const QNetworkRequest &request)
Starts a request.
Definition: qgstiledownloadmanager.cpp:215
QgsTileDownloadManager::Stats
Encapsulates any statistics we would like to keep about requests.
Definition: qgstiledownloadmanager.h:226
QgsTileDownloadManagerReply::error
QNetworkReply::NetworkError error() const
Returns error code (only valid when already finished)
Definition: qgstiledownloadmanager.h:66
QgsTileDownloadManager::hasPendingRequests
bool hasPendingRequests() const
Returns whether there are any pending requests in the queue.
Definition: qgstiledownloadmanager.cpp:267
QgsTileDownloadManagerReply
Reply object for tile download manager requests returned from calls to QgsTileDownloadManager::get().
Definition: qgstiledownloadmanager.h:47
QgsTileDownloadManagerReply::url
QUrl url() const
Returns the reply URL.
Definition: qgstiledownloadmanager.h:58