Merge lp:~marcustomlinson/unity-scopes-api/lp-1267917-2 into lp:unity-scopes-api

Proposed by Marcus Tomlinson
Status: Merged
Approved by: Michal Hruby
Approved revision: 138
Merged at revision: 144
Proposed branch: lp:~marcustomlinson/unity-scopes-api/lp-1267917-2
Merge into: lp:unity-scopes-api
Diff against target: 208 lines (+83/-57)
3 files modified
include/unity/scopes/internal/smartscopes/HttpClientQtThread.h (+8/-4)
src/scopes/internal/smartscopes/HttpClientQt.cpp (+8/-22)
src/scopes/internal/smartscopes/HttpClientQtThread.cpp (+67/-31)
To merge this branch: bzr merge lp:~marcustomlinson/unity-scopes-api/lp-1267917-2
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Approve
Michal Hruby (community) Approve
Review via email: mp+202428@code.launchpad.net

Commit message

Ensure that manager_ and reply_ objects are created and destroyed from within the same Qt thread.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michal Hruby (mhr3) wrote :

11 + moveToThread(this);

Shouldn't be necessary.

20 + reply_->deleteLater();

The reply_ object is created inside the thread, so it should be destroyed inside the thread, isn't that what the original error was about?

review: Needs Information
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michal Hruby (mhr3) wrote :

Hmm, looks a bit odd, are we really creating a thread solely to serve one request?

Nonetheless, it does fix the issue, I'm just not convinced it's a good idea to rely on emitting a signal to quit the thread, as this is inverting the responsibility and the thread is no longer a worker, but a "I'll quit when you ask me to" thing.

Revision history for this message
Marcus Tomlinson (marcustomlinson) wrote :

This is a bit of a tricky situation. Consider the requirements:

1. HttpClientQt.h should not contain Qt code that requires any more than forward declarations (this is to avoid anything outside of this class having to link to Qt).

2. A call to HttpClientQt::get needs to return a handle to the future result and asynchronously execute the request.

3. QSocketNotifier (in QNetworkAccessManager) can only be used with threads started with QThread.

4. The manager_ and reply_ Qt members (regardless of who their parent class is) need to be created and destroyed in the same QThread.

In order to satisfy req.1, we include the HttpClientThread class in the HttpClientQt cpp file. This class can then contain all the Qt specific stuff like signals/slots.

req.2 is performed by spawning a C++11 worker thread on request which eventually assigns a promise with the result. This thread then uses an HttpClientQtThread object to execute the query (yes, HttpClientQtThread is not really used as a worker thread as you pointed out)

Considering we already have a C++11 worker thread per request, does the helper Qt class really need to be a QThread? Well yes. I tried this class as just a QObject with synchronous methods but ended up with errors on new-ing QNetworkAccessManager (hence req.3).

Naturally then, as we now have a QThread available, we can use this thread to create, use and destroy the manager_ and reply_ HttpClientQtThread members (req.4). Lastly, because we need to access the reply_ object after it is assigned but before it is destroyed, we need to suspend the QThread until HttpClientQtThread signals it to exit (on destruction).

I hope this makes some sense as to why the code is structured this way (By no means am I saying that this is the only possible solution).

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michal Hruby (mhr3) wrote :

> Naturally then, as we now have a QThread available, we can use this thread to
> create, use and destroy the manager_ and reply_ HttpClientQtThread members
> (req.4). Lastly, because we need to access the reply_ object after it is
> assigned but before it is destroyed, we need to suspend the QThread until
> HttpClientQtThread signals it to exit (on destruction).

I wouldn't agree here, all the worker is asked to do is to return whether the http request succeeded plus a memory buffer with the reply, there's absolutely no need to keep the reply_ object alive as long as these two properties are stored somewhere. And then the inversion goes away.

I still disagree with using a thread per-request, but that's outside of scope of this MP.

Revision history for this message
Michal Hruby (mhr3) wrote :

Why is the query_done still there? Why not just quit() the thread?

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michal Hruby (mhr3) wrote :

+1, let's get it in!

review: Approve
138. By Marcus Tomlinson

Create a direct connection between manager.finished() and got_reply() in order to avoid reply being freed before use.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'include/unity/scopes/internal/smartscopes/HttpClientQtThread.h'
2--- include/unity/scopes/internal/smartscopes/HttpClientQtThread.h 2014-01-13 07:18:08 +0000
3+++ include/unity/scopes/internal/smartscopes/HttpClientQtThread.h 2014-01-22 11:48:09 +0000
4@@ -38,12 +38,15 @@
5 HttpClientQtThread(const QUrl& url, uint timeout);
6 ~HttpClientQtThread();
7
8+ bool get_reply(std::string& reply);
9+
10+private:
11 void run();
12- QNetworkReply* getReply();
13
14 public Q_SLOTS:
15 void cancel();
16- void queryDone(QNetworkReply*);
17+ void timeout();
18+ void got_reply(QNetworkReply* reply);
19
20 Q_SIGNALS:
21 void abort();
22@@ -51,9 +54,10 @@
23 private:
24 QUrl url_;
25 uint timeout_;
26- QNetworkReply* reply_;
27- QNetworkAccessManager* manager_;
28 std::mutex reply_mutex_;
29+
30+ bool success_;
31+ std::string reply_;
32 };
33
34 #endif // UNITY_SCOPES_INTERNAL_SMARTSCOPES_HTTPCLIENTQTTHREAD_H
35
36=== modified file 'src/scopes/internal/smartscopes/HttpClientQt.cpp'
37--- src/scopes/internal/smartscopes/HttpClientQt.cpp 2014-01-13 12:25:13 +0000
38+++ src/scopes/internal/smartscopes/HttpClientQt.cpp 2014-01-22 11:48:09 +0000
39@@ -104,32 +104,18 @@
40
41 get_qt_thread_->start();
42 loop.exec();
43- get_qt_thread_->wait();
44-
45- QNetworkReply* reply = get_qt_thread_->getReply();
46-
47- if (!reply)
48- {
49- // no reply
50- unity::ResourceException e("No reply from " + request_url + ":" + std::to_string(port));
51- promise_->set_exception(e.self());
52- }
53- else if (!reply->isFinished())
54- {
55- // incomplete reply
56- unity::ResourceException e("Incomplete reply from " + request_url + ":" + std::to_string(port));
57- promise_->set_exception(e.self());
58- }
59- else if (reply->error() != QNetworkReply::NoError)
60- {
61- // communication error
62- unity::ResourceException e(reply->errorString().toStdString());
63+
64+ std::string reply;
65+ bool success = get_qt_thread_->get_reply(reply);
66+
67+ if (!success)
68+ {
69+ unity::ResourceException e(reply);
70 promise_->set_exception(e.self());
71 }
72 else
73 {
74- QString reply_string(reply->readAll());
75- promise_->set_value(reply_string.toStdString());
76+ promise_->set_value(reply);
77 }
78 });
79
80
81=== modified file 'src/scopes/internal/smartscopes/HttpClientQtThread.cpp'
82--- src/scopes/internal/smartscopes/HttpClientQtThread.cpp 2014-01-13 12:25:13 +0000
83+++ src/scopes/internal/smartscopes/HttpClientQtThread.cpp 2014-01-22 11:48:09 +0000
84@@ -27,8 +27,7 @@
85 : QThread(),
86 url_(url),
87 timeout_(timeout),
88- reply_(nullptr),
89- manager_(nullptr)
90+ success_(false)
91 {
92 }
93
94@@ -36,48 +35,85 @@
95 {
96 cancel();
97
98- delete reply_;
99- delete manager_;
100+ wait();
101+}
102+
103+bool HttpClientQtThread::get_reply(std::string& reply)
104+{
105+ std::lock_guard<std::mutex> lock(reply_mutex_);
106+
107+ reply = reply_;
108+ return success_;
109 }
110
111 void HttpClientQtThread::run()
112 {
113- {
114- std::lock_guard<std::mutex> lock(reply_mutex_);
115-
116- manager_ = new QNetworkAccessManager();
117-
118- QNetworkRequest request(url_);
119-
120- reply_ = manager_->get(request);
121-
122- connect(manager_, &QNetworkAccessManager::finished, this, &HttpClientQtThread::queryDone);
123- connect(this, &HttpClientQtThread::abort, reply_, &QNetworkReply::abort);
124- }
125+ QNetworkAccessManager* manager = new QNetworkAccessManager();
126+
127+ QNetworkRequest request(url_);
128+
129+ QNetworkReply* reply = manager->get(request);
130+
131+ connect(manager, &QNetworkAccessManager::finished, this, &HttpClientQtThread::got_reply, Qt::DirectConnection);
132+ connect(this, &HttpClientQtThread::abort, reply, &QNetworkReply::abort);
133
134 QTimer timeout;
135- timeout.singleShot(timeout_, this, SLOT(cancel()));
136+ timeout.singleShot(timeout_, this, SLOT(timeout()));
137 QThread::exec(); // enter event loop
138-}
139-
140-QNetworkReply* HttpClientQtThread::getReply()
141-{
142- std::lock_guard<std::mutex> lock(reply_mutex_);
143- return reply_;
144-}
145-
146-void HttpClientQtThread::queryDone(QNetworkReply*)
147-{
148- quit();
149+
150+ delete reply;
151+ delete manager;
152 }
153
154 void HttpClientQtThread::cancel()
155 {
156 std::lock_guard<std::mutex> lock(reply_mutex_);
157
158- if (reply_)
159- {
160- emit abort();
161+ success_ = false;
162+ reply_ = "Request to " + url_.url().toStdString() + ":" + std::to_string(url_.port()) + " cancelled";
163+
164+ emit abort();
165+ quit();
166+}
167+
168+void HttpClientQtThread::timeout()
169+{
170+ std::lock_guard<std::mutex> lock(reply_mutex_);
171+
172+ success_ = false;
173+ reply_ = "Request to " + url_.url().toStdString() + ":" + std::to_string(url_.port()) + " timed out";
174+
175+ emit abort();
176+ quit();
177+}
178+
179+void HttpClientQtThread::got_reply(QNetworkReply* reply)
180+{
181+ std::lock_guard<std::mutex> lock(reply_mutex_);
182+
183+ if (!reply)
184+ {
185+ // no reply
186+ success_ = false;
187+ reply_ = "No reply from " + url_.url().toStdString() + ":" + std::to_string(url_.port());
188+ }
189+ else if (!reply->isFinished())
190+ {
191+ // incomplete reply
192+ success_ = false;
193+ reply_ = "Incomplete reply from " + url_.url().toStdString() + ":" + std::to_string(url_.port());
194+ }
195+ else if (reply->error() != QNetworkReply::NoError)
196+ {
197+ // communication error
198+ success_ = false;
199+ reply_ = reply->errorString().toStdString();
200+ }
201+ else
202+ {
203+ success_ = true;
204+ QByteArray byte_array = reply->readAll();
205+ reply_ = std::string(byte_array.constData(), byte_array.size());
206 }
207
208 quit();

Subscribers

People subscribed via source and target branches

to all changes: