1

I have a class that performs a request to the network and parses the data. How do I properly implement the request abort for it?

Imagine that I have such a class:

class MyClass
{
public:
...
    void doRequest()
    {
        m_reply = m_manager.get(...);

        QEventLoop waitForResponse;
        connect(m_reply, &QNetworkReply::finished, &waitForResponse, &QEventLoop::quit);
        waitForResponse.exec();

        // Check if request was aborted (otherwise, application will crash)
        if (m_reply == nullptr)
            return;

        // Check for network errors, write result to m_data and delete m_reply;
        ...
    }
    void abort()
    {
        if (m_reply != nullptr)
            m_reply->abort();
    }
    QString data()
    {
        return m_data;
    }
...
private:
    QNetworkAccessManager *m_manager;
    QPiinter<QNetworkReply> m_reply;
    QString m_data;
}

Here is an example of its use by pressing the button:

class MainWindow : public QMainWindow
{
...
private slots:
    MainWindow::on_myButton_pressed()
    {
        m_myClass->abort();
        m_myClass->doRequest();
        ui->myTextEdit->setText(m_myClass->data());
    }

private:
    MyClass m_myClass;
}

When you press the button, if the previous request is not completed, then it is canceled. This works. But under the hood in this case a new request writing data into the QTextEdit and exit the function, then old request returning from it's own loop and writes the same m_data to QTextEdit again.

Is it corrent? Maybe there is a more correct way to implement this?

Shatur95
  • 93
  • 1
  • 9
  • Wait-a-minute... Sorry, didn't get it right: are you having `QTextEdit` text set twice? But why? Can't you check if `m_reply` was aborted before setting `m_data`? – MasterAler Apr 20 '19 at 20:39
  • @MasterAler, yes, I having `setText` twice. I have `error()` method, that returns error enum that occured in `doRequest()`. I can set the error after cancel and it will work, but it's looks weird... In this case I'll have MyClass with valid data and `Error::Aborted` from `error()`. This does not look very clear, as for me ... – Shatur95 Apr 20 '19 at 22:05
  • As a class desing problem, this doesn't look clear, that's true. I would use something like `QtConcurrent::run` + `QFuture` instead of event loop, so that one click == one task. But with the current code you could reset the error state after the successfull request as well (or even use your own enum), so that at any arbitrary moment there exists a pair of data & error_state, which are valid and conformed. – MasterAler Apr 20 '19 at 22:34
  • Unfortunately, due to the `QEventLoop`, the old request that was canceled is executed LATER than the successful request. Therefore, I will have a valid `m_data` (which generated with a new query) and `error()`, which returns an error from the old query. And there is my problem. I thought about `QtConcurrent`, but I don't know how to integrate it with QNetworkReply, that is already async. – Shatur95 Apr 20 '19 at 22:47
  • Then it looks like a design flaw. You have a button, that sends _multiple_ independent requests, but their are handled by _one_ class instance with one common state. No good. There could be different solutions, but in general there probably should be one MyClass instance per request. By the way, doen't seem a problem using async class inside async call, afaik. – MasterAler Apr 20 '19 at 22:56
  • @MasterAler, I thought about several instances, but then it is impossible to implement an abort. If I delete the old MyClass object and create a new one, then I will get a crash, because last request did not end :( Maybe I wrong, so it would be nice to see any example of how this can be done. – Shatur95 Apr 21 '19 at 15:00

1 Answers1

3

Nested event loops are the root of all evil. It is much simpler to write a function like doRequest without pretending to the user that it is a synchronous function. It seems that you have already traced the convoluted control-flow that happens when you call abort() and you understand how subsequent calls to doRequest() end up being nested calls due to re-entering the event loop. If you restart your request multiple times, your stack would look something like (the stack grows downwards):

1.  main function
2.  main event loop
3.  [...] (Qt functions)
4.  MainWindow::on_myButton_pressed()
5.  MyClass::doRequest()
6.  QEventLoop::exec()
7.  [...] (Qt functions)
8.  MainWindow::on_myButton_pressed()
9.  MyClass::doRequest()
10. QEventLoop::exec()
11. [...] (Qt functions)
12. MainWindow::on_myButton_pressed()   and so on...

Each one of the calls to MainWindow::on_myButton_pressed() need to call ui->myTextEdit->setText() when its nested event loop exits.

An alternative would be make your functions fully asynchronous and rely on signals/slots if you need things to be executed when a particular operation finishes. Here is a minimal implementation for what you are trying to achieve:

#include <QtNetwork>
#include <QtWidgets>

/// A class responsible for communication with the web backend
class Backend : public QObject {
  Q_OBJECT
public:
  explicit Backend(QObject *parent = nullptr)
      : QObject(parent), m_reply(nullptr) {}
public slots:
  void doRequest() {
    // abort and delete ongoing request (if any)
    if (m_reply) {
      m_reply->abort();
      delete m_reply;
      m_reply = nullptr;
    }
    emit textUpdated(QString());
    // send request
    QUrl url("http://wtfismyip.com/text");
    QNetworkRequest request(url);
    m_reply = m_manager.get(request);
    // when the request is finished,
    QObject::connect(m_reply, &QNetworkReply::finished, [this] {
      // if the request ended successfully, read the received ip into m_lastData
      if (m_reply->error() == QNetworkReply::NoError)
        m_lastData = QString::fromUtf8(m_reply->readAll());
      // otherwise, emit errorOccured() signal (if the request has not been
      // actively canceled)
      else if (m_reply->error() != QNetworkReply::OperationCanceledError)
        emit errorOccured(m_reply->errorString());
      // in all cases, emit updateText and do cleanup
      emit textUpdated(m_lastData);
      m_reply->deleteLater();
      m_reply = nullptr;
    });
  }

  void abort() {
    if (m_reply != nullptr)
      m_reply->abort();
  }

signals:
  void textUpdated(const QString &);
  void errorOccured(const QString &);

private:
  QNetworkAccessManager m_manager;
  QNetworkReply *m_reply;
  QString m_lastData;
};

/// A minimal widget that contains a QPushButton and a QLabel
class Widget : public QWidget {
  Q_OBJECT
public:
  explicit Widget(QWidget *parent = nullptr) : QWidget(parent) {
    m_layout.addWidget(&m_pushButton);
    m_layout.addWidget(&m_label);

    connect(&m_pushButton, &QPushButton::clicked, this, &Widget::buttonClicked);
  }

signals:
  void buttonClicked();

public slots:
  void updateText(const QString &text) { m_label.setText(text); }
  void showError(const QString &error) {
    QMessageBox::warning(this, tr("Error"), error);
  }

private:
  QVBoxLayout m_layout{this};
  QPushButton m_pushButton{"Retrieve Name"};
  QLabel m_label;
};

int main(int argc, char *argv[]) {
  QApplication a(argc, argv);

  Backend backend;
  Widget widget;

  // connect components
  QObject::connect(&backend, &Backend::textUpdated, &widget,
                   &Widget::updateText);
  QObject::connect(&backend, &Backend::errorOccured, &widget,
                   &Widget::showError);
  QObject::connect(&widget, &Widget::buttonClicked, &backend,
                   &Backend::doRequest);
  widget.show();

  return a.exec();
}

#include "main.moc"
Mike
  • 8,055
  • 1
  • 30
  • 44
  • Unfortunately, this method will not work if `doRequest()` has several network requests inside that need to be processed sequentially in a loop :( What I need to do in this case? – Shatur95 Apr 21 '19 at 18:08
  • @Shatur95, The approach can be extended by using states, see [this answer](https://stackoverflow.com/a/32595398/2666212) for an example. The [general](https://stackoverflow.com/q/49665721) [consensus](https://stackoverflow.com/q/32309737) for this kind of problem is to avoid nested event loops (whether through `QEventLoop` or `QApplication::processEvents()`), since they tend to cause far more problems on the long term. – Mike Apr 21 '19 at 19:08