Skip to content

Commit 66ad9d9

Browse files
authoredJun 15, 2018
Merge pull request #7055 from m-kuhn/wfsNoMainThreadEventLoopb
Avoid running QEventLoop on main thread (in WFS provider)
2 parents 6b08eff + 478d6d0 commit 66ad9d9

File tree

7 files changed

+181
-31
lines changed

7 files changed

+181
-31
lines changed
 

‎.ci/travis/linux/docker-build-test.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ cmake \
5555
-DWITH_BINDINGS=ON \
5656
-DWITH_SERVER=ON \
5757
-DDISABLE_DEPRECATED=ON \
58+
-DPYTHON_TEST_WRAPPER="timeout -sSIGSEGV 55s"\
5859
-DCXX_EXTRA_FLAGS=${CLANG_WARNINGS} ..
5960
echo "travis_fold:end:cmake"
6061

‎python/core/auto_generated/qgsnetworkaccessmanager.sip.in

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,26 @@ that the fallback proxy should not be used for, then no proxy will be used.
3232
#include "qgsnetworkaccessmanager.h"
3333
%End
3434
public:
35-
static QgsNetworkAccessManager *instance();
35+
36+
static QgsNetworkAccessManager *instance( Qt::ConnectionType connectionType = Qt::BlockingQueuedConnection );
37+
%Docstring
38+
Returns a pointer to the active QgsNetworkAccessManager
39+
for the current thread.
40+
41+
With the ``connectionType`` parameter it is possible to setup the default connection
42+
type that is used to handle signals that might require user interaction and therefore
43+
need to be handled on the main thread. See in-depth discussion below.
44+
45+
:param connectionType: In most cases the default of using a ``Qt.BlockingQueuedConnection``
46+
is ok, to make a background thread wait for the main thread to answer such a request is
47+
fine and anything else is dangerous.
48+
However, in case the request was started on the main thread, one should execute a
49+
local event loop in a helper thread and freeze the main thread for the duration of the
50+
download. In this case, if an authentication request is sent from the background thread
51+
network access manager, the background thread should be blocked, the main thread be woken
52+
up, processEvents() executed once, the main thread frozen again and the background thread
53+
continued.
54+
%End
3655

3756
QgsNetworkAccessManager( QObject *parent = 0 );
3857

@@ -76,9 +95,13 @@ Gets name for QNetworkRequest.CacheLoadControl
7695
Gets QNetworkRequest.CacheLoadControl from name
7796
%End
7897

79-
void setupDefaultProxyAndCache();
98+
void setupDefaultProxyAndCache( Qt::ConnectionType connectionType = Qt::BlockingQueuedConnection );
8099
%Docstring
81-
Setup the NAM according to the user's settings
100+
Setup the QgsNetworkAccessManager (NAM) according to the user's settings.
101+
The ``connectionType`` sets up the default connection type that is used to
102+
handle signals that might require user interaction and therefore
103+
need to be handled on the main thread. See in-depth discussion in the documentation
104+
for the constructor of this class.
82105
%End
83106

84107
bool useSystemProxy() const;

‎src/core/qgsnetworkaccessmanager.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class QgsNetworkProxyFactory : public QNetworkProxyFactory
102102
//
103103
// Static calls to enforce singleton behavior
104104
//
105-
QgsNetworkAccessManager *QgsNetworkAccessManager::instance()
105+
QgsNetworkAccessManager *QgsNetworkAccessManager::instance( Qt::ConnectionType connectionType )
106106
{
107107
static QThreadStorage<QgsNetworkAccessManager> sInstances;
108108
QgsNetworkAccessManager *nam = &sInstances.localData();
@@ -111,7 +111,7 @@ QgsNetworkAccessManager *QgsNetworkAccessManager::instance()
111111
sMainNAM = nam;
112112

113113
if ( !nam->mInitialized )
114-
nam->setupDefaultProxyAndCache();
114+
nam->setupDefaultProxyAndCache( connectionType );
115115

116116
return nam;
117117
}
@@ -281,7 +281,7 @@ QNetworkRequest::CacheLoadControl QgsNetworkAccessManager::cacheLoadControlFromN
281281
return QNetworkRequest::PreferNetwork;
282282
}
283283

284-
void QgsNetworkAccessManager::setupDefaultProxyAndCache()
284+
void QgsNetworkAccessManager::setupDefaultProxyAndCache( Qt::ConnectionType connectionType )
285285
{
286286
mInitialized = true;
287287
mUseSystemProxy = false;
@@ -292,19 +292,19 @@ void QgsNetworkAccessManager::setupDefaultProxyAndCache()
292292
{
293293
connect( this, &QNetworkAccessManager::authenticationRequired,
294294
sMainNAM, &QNetworkAccessManager::authenticationRequired,
295-
Qt::BlockingQueuedConnection );
295+
connectionType );
296296

297297
connect( this, &QNetworkAccessManager::proxyAuthenticationRequired,
298298
sMainNAM, &QNetworkAccessManager::proxyAuthenticationRequired,
299-
Qt::BlockingQueuedConnection );
299+
connectionType );
300300

301301
connect( this, &QgsNetworkAccessManager::requestTimedOut,
302302
sMainNAM, &QgsNetworkAccessManager::requestTimedOut );
303303

304304
#ifndef QT_NO_SSL
305305
connect( this, &QNetworkAccessManager::sslErrors,
306306
sMainNAM, &QNetworkAccessManager::sslErrors,
307-
Qt::BlockingQueuedConnection );
307+
connectionType );
308308
#endif
309309
}
310310

‎src/core/qgsnetworkaccessmanager.h

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,26 @@ class CORE_EXPORT QgsNetworkAccessManager : public QNetworkAccessManager
4949
Q_OBJECT
5050

5151
public:
52-
//! returns a pointer to the single instance
53-
// and creates that instance on the first call.
54-
static QgsNetworkAccessManager *instance();
52+
53+
/**
54+
* Returns a pointer to the active QgsNetworkAccessManager
55+
* for the current thread.
56+
*
57+
* With the \a connectionType parameter it is possible to setup the default connection
58+
* type that is used to handle signals that might require user interaction and therefore
59+
* need to be handled on the main thread. See in-depth discussion below.
60+
*
61+
* \param connectionType In most cases the default of using a ``Qt::BlockingQueuedConnection``
62+
* is ok, to make a background thread wait for the main thread to answer such a request is
63+
* fine and anything else is dangerous.
64+
* However, in case the request was started on the main thread, one should execute a
65+
* local event loop in a helper thread and freeze the main thread for the duration of the
66+
* download. In this case, if an authentication request is sent from the background thread
67+
* network access manager, the background thread should be blocked, the main thread be woken
68+
* up, processEvents() executed once, the main thread frozen again and the background thread
69+
* continued.
70+
*/
71+
static QgsNetworkAccessManager *instance( Qt::ConnectionType connectionType = Qt::BlockingQueuedConnection );
5572

5673
QgsNetworkAccessManager( QObject *parent = nullptr );
5774

@@ -79,8 +96,14 @@ class CORE_EXPORT QgsNetworkAccessManager : public QNetworkAccessManager
7996
//! Gets QNetworkRequest::CacheLoadControl from name
8097
static QNetworkRequest::CacheLoadControl cacheLoadControlFromName( const QString &name );
8198

82-
//! Setup the NAM according to the user's settings
83-
void setupDefaultProxyAndCache();
99+
/**
100+
* Setup the QgsNetworkAccessManager (NAM) according to the user's settings.
101+
* The \a connectionType sets up the default connection type that is used to
102+
* handle signals that might require user interaction and therefore
103+
* need to be handled on the main thread. See in-depth discussion in the documentation
104+
* for the constructor of this class.
105+
*/
106+
void setupDefaultProxyAndCache( Qt::ConnectionType connectionType = Qt::BlockingQueuedConnection );
84107

85108
//! Returns whether the system proxy should be used
86109
bool useSystemProxy() const { return mUseSystemProxy; }

‎src/providers/wfs/qgswfscapabilities.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@
3030
QgsWfsCapabilities::QgsWfsCapabilities( const QString &uri )
3131
: QgsWfsRequest( QgsWFSDataSourceURI( uri ) )
3232
{
33-
connect( this, &QgsWfsRequest::downloadFinished, this, &QgsWfsCapabilities::capabilitiesReplyFinished );
33+
// Using Qt::DirectConnection since the download might be running on a different thread.
34+
// In this case, the request was sent from the main thread and is executed with the main
35+
// thread being blocked in future.waitForFinished() so we can run code on this object which
36+
// lives in the main thread without risking havoc.
37+
connect( this, &QgsWfsRequest::downloadFinished, this, &QgsWfsCapabilities::capabilitiesReplyFinished, Qt::DirectConnection );
3438
}
3539

3640
bool QgsWfsCapabilities::requestCapabilities( bool synchronous, bool forceRefresh )

‎src/providers/wfs/qgswfsrequest.cpp

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#include <QEventLoop>
2525
#include <QNetworkCacheMetaData>
2626
#include <QCryptographicHash> // just for testin file:// fake_qgis_http_endpoint hack
27+
#include <QFuture>
28+
#include <QtConcurrent>
2729

2830
const qint64 READ_BUFFER_SIZE_HINT = 1024 * 1024;
2931

@@ -126,26 +128,95 @@ bool QgsWfsRequest::sendGET( const QUrl &url, bool synchronous, bool forceRefres
126128
request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
127129
}
128130

129-
mReply = QgsNetworkAccessManager::instance()->get( request );
130-
mReply->setReadBufferSize( READ_BUFFER_SIZE_HINT );
131-
if ( !mUri.auth().setAuthorizationReply( mReply ) )
131+
QWaitCondition waitCondition;
132+
QMutex waitConditionMutex;
133+
134+
std::function<bool()> downloaderFunction = [ this, request, synchronous, &waitConditionMutex, &waitCondition ]()
132135
{
133-
mErrorCode = QgsWfsRequest::NetworkError;
134-
mErrorMessage = errorMessageFailedAuth();
135-
QgsMessageLog::logMessage( mErrorMessage, tr( "WFS" ) );
136-
return false;
137-
}
138-
connect( mReply, &QNetworkReply::finished, this, &QgsWfsRequest::replyFinished );
139-
connect( mReply, &QNetworkReply::downloadProgress, this, &QgsWfsRequest::replyProgress );
136+
if ( QThread::currentThread() != QgsApplication::instance()->thread() )
137+
QgsNetworkAccessManager::instance( Qt::DirectConnection );
140138

141-
if ( !synchronous )
142-
return true;
139+
bool success = true;
140+
mReply = QgsNetworkAccessManager::instance()->get( request );
143141

144-
QEventLoop loop;
145-
connect( this, &QgsWfsRequest::downloadFinished, &loop, &QEventLoop::quit );
146-
loop.exec( QEventLoop::ExcludeUserInputEvents );
142+
mReply->setReadBufferSize( READ_BUFFER_SIZE_HINT );
143+
if ( !mUri.auth().setAuthorizationReply( mReply ) )
144+
{
145+
mErrorCode = QgsWfsRequest::NetworkError;
146+
mErrorMessage = errorMessageFailedAuth();
147+
QgsMessageLog::logMessage( mErrorMessage, tr( "WFS" ) );
148+
waitCondition.wakeAll();
149+
success = false;
150+
}
151+
else
152+
{
153+
// We are able to use direct connection here, because we
154+
// * either run on the thread mReply lives in, so DirectConnection is standard and safe anyway
155+
// * or the owner thread of mReply is currently not doing anything because it's blocked in future.waitForFinished() (if it is the main thread)
156+
connect( mReply, &QNetworkReply::finished, this, &QgsWfsRequest::replyFinished, Qt::DirectConnection );
157+
connect( mReply, &QNetworkReply::downloadProgress, this, &QgsWfsRequest::replyProgress, Qt::DirectConnection );
147158

148-
return mErrorMessage.isEmpty();
159+
if ( synchronous )
160+
{
161+
auto resumeMainThread = [&waitConditionMutex, &waitCondition]()
162+
{
163+
waitConditionMutex.lock();
164+
waitCondition.wakeAll();
165+
waitConditionMutex.unlock();
166+
167+
waitConditionMutex.lock();
168+
waitCondition.wait( &waitConditionMutex );
169+
waitConditionMutex.unlock();
170+
};
171+
172+
connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::authenticationRequired, this, resumeMainThread, Qt::DirectConnection );
173+
connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::proxyAuthenticationRequired, this, resumeMainThread, Qt::DirectConnection );
174+
175+
#ifndef QT_NO_SSL
176+
connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::sslErrors, this, resumeMainThread, Qt::DirectConnection );
177+
#endif
178+
QEventLoop loop;
179+
connect( this, &QgsWfsRequest::downloadFinished, &loop, &QEventLoop::quit, Qt::DirectConnection );
180+
loop.exec();
181+
}
182+
}
183+
waitCondition.wakeAll();
184+
return success;
185+
};
186+
187+
bool success;
188+
189+
if ( synchronous && QThread::currentThread() == QApplication::instance()->thread() )
190+
{
191+
std::unique_ptr<DownloaderThread> downloaderThread = qgis::make_unique<DownloaderThread>( downloaderFunction );
192+
downloaderThread->start();
193+
194+
while ( !downloaderThread->isFinished() )
195+
{
196+
waitConditionMutex.lock();
197+
waitCondition.wait( &waitConditionMutex );
198+
waitConditionMutex.unlock();
199+
200+
// If the downloader thread wakes us (the main thread) up and is not yet finished
201+
// he needs the authentication to run.
202+
// The processEvents() call gives the auth manager the chance to show a dialog and
203+
// once done with that, we can wake the downloaderThread again and continue the download.
204+
if ( !downloaderThread->isFinished() )
205+
{
206+
QgsApplication::instance()->processEvents();
207+
waitConditionMutex.lock();
208+
waitCondition.wakeAll();
209+
waitConditionMutex.unlock();
210+
}
211+
}
212+
213+
success = downloaderThread->success();
214+
}
215+
else
216+
{
217+
success = downloaderFunction();
218+
}
219+
return success && mErrorMessage.isEmpty();
149220
}
150221

151222
bool QgsWfsRequest::sendPOST( const QUrl &url, const QString &contentTypeHeader, const QByteArray &data )

‎src/providers/wfs/qgswfsrequest.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <QNetworkRequest>
2020
#include <QNetworkReply>
2121
#include <QUrl>
22+
#include <QAuthenticator>
2223

2324
#include "qgswfsdatasourceuri.h"
2425

@@ -117,4 +118,31 @@ class QgsWfsRequest : public QObject
117118

118119
};
119120

121+
122+
class DownloaderThread : public QThread
123+
{
124+
Q_OBJECT
125+
126+
public:
127+
DownloaderThread( std::function<bool()> function, QObject *parent = nullptr )
128+
: QThread( parent )
129+
, mFunction( function )
130+
{
131+
}
132+
133+
void run() override
134+
{
135+
mSuccess = mFunction();
136+
}
137+
138+
bool success() const
139+
{
140+
return mSuccess;
141+
}
142+
143+
private:
144+
std::function<bool()> mFunction;
145+
bool mSuccess = false;
146+
};
147+
120148
#endif // QGSWFSREQUEST_H

6 commit comments

Comments
 (6)

rduivenvoorde commented on Jun 15, 2018

@rduivenvoorde
Contributor

@m-kuhn Hi Matthias, seeing this commit. My eye was pulled yesterday to this log msg which you get when you draw an xyz (eg OSM):

../src/providers/wms/qgswmsprovider.cpp: 617: (draw) [0ms] [thread:0x558840d62910] Trying to draw a WMS image on the main thread. Stop it!

Confirmed with a just compiled version now. Is that a call that a thread is stopped? Or is it asking devs to stop drawing in the main thread?

m-kuhn commented on Jun 15, 2018

@m-kuhn
MemberAuthor

@rduivenvoorde in an idea world yes: there should be no blocking downloads done on the main thread.
The message is an advice for developers, no effect at the moment (except for an increased risk of crash in such a scenario).
This pull request is the first brick of a code refactoring to make things more stable (i.e. still not bullet proof, but the risk surface becomes smaller and is limited to cases when authentication is involved).

nyalldawson commented on Jun 15, 2018

@nyalldawson
Collaborator

@m-kuhn what's the grand plan here?

m-kuhn commented on Jun 15, 2018

@m-kuhn
MemberAuthor
  • Stop using blocking downloads wherever possible
  • Have a class to abstract away those threading details which are too hard to get right
  • Find a way to properly deal with things that require user interaction (foremost authentication)

rduivenvoorde commented on Jun 15, 2018

@rduivenvoorde
Contributor

I think indeed the online datasources will become more and more used. That is why I hope we can make QgsNetworkManager more user/python friendly (see qgis/QGIS-Enhancement-Proposals#123).
Also: what is https://qgis.org/pyqgis/master/core/Network/QgsNetworkContentFetcher.html becoming then? Looks to me as a second http-fetch class (though does not do POST?), isn't that a little redundant then?

m-kuhn commented on Jun 15, 2018

@m-kuhn
MemberAuthor

The right thing to use in 90% of the cases when you are able and not so lazy to use blocking downloads.

Please sign in to comment.