Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

qt: Call setParent() in the parent's context #18948

Merged
merged 3 commits into from
Dec 2, 2020

Conversation

hebasto
Copy link
Member

@hebasto hebasto commented May 11, 2020

The setParent(parent) internally calls QCoreApplication::sendEvent(parent, QChildEvent) that implies running in the thread which created the parent object. That is not the case always, and an internal assertion fails in the debug mode.

Steps to reproduce this issue on master (007e15d) on Linux Mint 20 (x86_64):

$ make -C depends DEBUG=1
$ CONFIG_SITE=$PWD/depends/x86_64-pc-linux-gnu/share/config.site ./configure
$ make
$ QT_FATAL_WARNINGS=1 lldb src/qt/bitcoin-qt -- --regtest -debug=qt
(lldb) target create "src/qt/bitcoin-qt"
Current executable set to '/home/hebasto/GitHub/bitcoin/src/qt/bitcoin-qt' (x86_64).
(lldb) settings set -- target.run-args  "--regtest" "-debug=qt"
(lldb) run
Process 431562 launched: '/home/hebasto/GitHub/bitcoin/src/qt/bitcoin-qt' (x86_64)
# load wallet via GUI
Process 431562 stopped
* thread #24, name = 'QThread', stop reason = signal SIGABRT
    frame #0: 0x00007ffff794518b libc.so.6`__GI_raise(sig=2) at raise.c:51:1
(lldb) bt
* thread #24, name = 'QThread', stop reason = signal SIGABRT
  * frame #0: 0x00007ffff794518b libc.so.6`__GI_raise(sig=2) at raise.c:51:1
    frame #1: 0x00007ffff7924859 libc.so.6`__GI_abort at abort.c:79:7
    frame #2: 0x0000555556508ec4 bitcoin-qt`::qt_message_fatal((null)=<unavailable>, context=<unavailable>, message=<unavailable>) at qlogging.cpp:1690:15
    frame #3: 0x00005555565099cf bitcoin-qt`QMessageLogger::fatal(this=<unavailable>, msg=<unavailable>) const at qlogging.cpp:796:21
    frame #4: 0x000055555650479d bitcoin-qt`qt_assert_x(where=<unavailable>, what=<unavailable>, file=<unavailable>, line=<unavailable>) at qglobal.cpp:3088:46
    frame #5: 0x0000555556685733 bitcoin-qt`QCoreApplicationPrivate::checkReceiverThread(receiver=0x0000555557b27510) at qcoreapplication.cpp:557:5
    frame #6: 0x00005555567ced86 bitcoin-qt`QApplication::notify(this=0x00007fffffffd4a0, receiver=0x0000555557b27510, e=0x00007fff9a7f8ce0) at qapplication.cpp:2956:27
    frame #7: 0x0000555556685d31 bitcoin-qt`QCoreApplication::notifyInternal2(receiver=0x0000555557b27510, event=0x00007fff9a7f8ce0) at qcoreapplication.cpp:1024:24
    frame #8: 0x00005555566c9224 bitcoin-qt`QObjectPrivate::setParent_helper(QObject*) [inlined] QCoreApplication::sendEvent(event=<unavailable>, receiver=<unavailable>) at qcoreapplication.h:233:59
    frame #9: 0x00005555566c9210 bitcoin-qt`QObjectPrivate::setParent_helper(this=0x00007fff85855260, o=0x0000555557b27510) at qobject.cpp:2036
    frame #10: 0x00005555566c9b41 bitcoin-qt`QObject::setParent(this=<unavailable>, parent=<unavailable>) at qobject.cpp:1980:24
    frame #11: 0x0000555555710be8 bitcoin-qt`WalletController::getOrCreateWallet(std::unique_ptr<interfaces::Wallet, std::default_delete<interfaces::Wallet> >) + 2534

...

Fixes #18835.

@hebasto
Copy link
Member Author

hebasto commented May 11, 2020

The needless of setParent() was pointed already:

Curious: Is this setParent call actually necessary for anything? Maybe consider dropping it or adding a comment that says why it's useful.

setParent is not necessary at the moment because (afaict) the wallet is always unloaded here...


Comments are adjusted according to @ryanofsky's suggestions:

@promag
Copy link
Member

promag commented May 11, 2020

Concept ACK, but now it must destroy wallet models on quit, or am I wrong?

@hebasto
Copy link
Member Author

hebasto commented May 11, 2020

Concept ACK, but now it must destroy wallet models on quit, or am I wrong?

Correct. Aren't they destroyed now?

@promag
Copy link
Member

promag commented May 11, 2020

Concept ACK, but now it must destroy wallet models on quit, or am I wrong?

Correct. Aren't they destroyed now?

Where?

@hebasto
Copy link
Member Author

hebasto commented May 11, 2020

Concept ACK, but now it must destroy wallet models on quit, or am I wrong?

Correct. Aren't they destroyed now?

Where?

It seems the procedure starts in this destructor:

~WalletClientImpl() override { UnloadWallets(); }

and through the WalletModel::unload() signal

connect(wallet_model, &WalletModel::unload, this, [this, wallet_model] {
// Defer removeAndDeleteWallet when no modal widget is active.
// TODO: remove this workaround by removing usage of QDiallog::exec.
if (QApplication::activeModalWidget()) {
connect(qApp, &QApplication::focusWindowChanged, wallet_model, [this, wallet_model]() {
if (!QApplication::activeModalWidget()) {
removeAndDeleteWallet(wallet_model);
}
}, Qt::QueuedConnection);
} else {
removeAndDeleteWallet(wallet_model);
}
}, Qt::QueuedConnection);

it ends here:

void WalletController::removeAndDeleteWallet(WalletModel* wallet_model)
{
// Unregister wallet model.
{
QMutexLocker locker(&m_mutex);
m_wallets.erase(std::remove(m_wallets.begin(), m_wallets.end(), wallet_model));
}
Q_EMIT walletRemoved(wallet_model);
// Currently this can trigger the unload since the model can hold the last
// CWallet shared pointer.
delete wallet_model;
}

@DrahtBot DrahtBot added the GUI label May 11, 2020
@promag
Copy link
Member

promag commented May 11, 2020

Yes, but it should also delete when wallet controller is deleted.

@hebasto
Copy link
Member Author

hebasto commented May 11, 2020

Yes, but it should also delete when wallet controller is deleted.

From the WalletController interface does not follow that its instance takes the ownership of WalletModel objects. What are cases when WalletModel objects could live until WalletController destructor is called?

@promag
Copy link
Member

promag commented May 11, 2020

What are cases when WalletModel objects could live until WalletController destructor is called?

None. But before this change wallet models were owned and managed by wallet controller. I just think that it shouldn't depend on whether or not wallets are unloaded before destroying the window on exit. Suppose that with multiprocess you want to just restart the GUI process, in that case you want to just teardown the GUI, which includes wallet models.

@tarboss
Copy link

tarboss commented May 12, 2020

seems to work(quick check), but i still like the solution from branch 18.
(auto delete of walletmodel from walletcontroller if u forget to delete & if u want send msgs from walletcontroller to three open walletmodels for example (future). Just on my mind atm.
Checking l8ter a bit more.

@DrahtBot
Copy link
Contributor

DrahtBot commented May 12, 2020

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Conflicts

No conflicts as of last run.

@hebasto
Copy link
Member Author

hebasto commented May 13, 2020

@promag

Yes, but it should also delete when wallet controller is deleted.

I agree this is important for robustness and safety. Now I can see the following ways to move on:

  1. drop this PR in favor of gui: remove assert in walletcontroller & run setparent in gui-qt main thread #18961
  2. use wrapped setParent()
--- a/src/qt/walletcontroller.cpp
+++ b/src/qt/walletcontroller.cpp
@@ -110,7 +110,7 @@ WalletModel* WalletController::getOrCreateWallet(std::unique_ptr<interfaces::Wal
     WalletModel* wallet_model = new WalletModel(std::move(wallet), m_client_model, m_platform_style, nullptr);
     // Handler callback runs in a different thread so fix wallet model thread affinity.
     wallet_model->moveToThread(thread());
-    wallet_model->setParent(this);
+    QTimer::singleShot(0, this, [wallet_model, this]() { wallet_model->setParent(this); });
     m_wallets.push_back(wallet_model);
 
     // WalletModel::startPollBalance needs to be called in a thread managed by
  1. enhance ~WalletController() by adding code to delete WalletModel objects, or use smth like deleteLater()

Which way is preferable?

@tarboss
Copy link

tarboss commented May 13, 2020

let promag decide what he prefers.

sidenote:
if u breakpoint after:
QTimer::singleShot(0, this, [wallet_model, this] () { wallet_model->setParent(this); });
wallet_model parent is "null"!!!

walletmodel's parent is set later - i hope - Queued Connection

@tryphe
Copy link
Contributor

tryphe commented May 17, 2020

Concept ACK

@hebasto
Copy link
Member Author

hebasto commented May 20, 2020

Updated d6afd1c -> 679b548 (pr18948.01 -> pr18948.02, diff):

Concept ACK, but now it must destroy wallet models on quit, or am I wrong?

Copy link
Member

@promag promag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With lambda slots we could make this more elegant, something like:

WalletModel* wallet_model;
QMetaObject::invokeMethod(this, [this, &wallet_model] {
    wallet_model = new WalletModel(this);
    // ...
}, GUIUtil::blockingGUIThreadConnection());

@hebasto
Copy link
Member Author

hebasto commented Jul 4, 2020

@promag

With lambda slots we could make this more elegant, something like:

WalletModel* wallet_model;
QMetaObject::invokeMethod(this, [this, &wallet_model] {
    wallet_model = new WalletModel(this);
    // ...
}, GUIUtil::blockingGUIThreadConnection());

Unfortunately, QMetaObject::invokeMethod(QObject*, Functor, Qt::ConnectionType, FunctorReturnType*) was introduced in Qt 5.10 only.

@hebasto hebasto changed the title qt: Do not call setParent() for WalletModel instances qt: Call setParent() in the parent's context Jul 4, 2020
@promag
Copy link
Member

promag commented Jul 5, 2020

Ah yes, too soon then 😅

@ryanofsky
Copy link
Contributor

Unfortunately, QMetaObject::invokeMethod(QObject*, Functor, Qt::ConnectionType, FunctorReturnType*) was introduced in Qt 5.10 only.

Not sure about the context, but it's trivial to replace this method. You can write a 2-line function that just connects the lambda to a temporary object slot and triggers the object. e.g. ObjectInvoke from #16874 (comment)

@hebasto
Copy link
Member Author

hebasto commented Jul 24, 2020

Reworked.

The previous implementation was broken due to the fact the QTimer requires an event loop, that is not the case when a wallet is loaded in the RPC thread.

@ryanofsky

Unfortunately, QMetaObject::invokeMethod(QObject*, Functor, Qt::ConnectionType, FunctorReturnType*) was introduced in Qt 5.10 only.

Not sure about the context, but it's trivial to replace this method. You can write a 2-line function that just connects the lambda to a temporary object slot and triggers the object. e.g. ObjectInvoke from #16874 (comment)

As this place is the only in the code base, I've chosen a different approach, that would be more convenient when migration to Qt 5.10+ occurs.

src/qt/walletcontroller.cpp Outdated Show resolved Hide resolved
}, GUIUtil::blockingGUIThreadConnection());
#else
{
QEventLoop event_loop;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "qt: Call setParent() in the parent's context" (3200c44)

This workaround is confusing and I think should have some kind of comment explaining what it is doing and how it works. I think the goal is to get setParent to run in the the GUI thread rather than the calling thread, and to have the calling thread block until setParent returns. But it's unclear why setParent needs to run on the gui thread, or why setting a single shot timer would block until after the timer runs (instead of scheduling the timer asynchronously), or how declaring an event_loop variable that is never referenced would have any effect.

Honestly I think the stack overflow approach from https://github.com/ryanofsky/bitcoin/blob/eacd34144c1ee989c9ab43eb439fecca76addc76/src/qt/async_update.h#L18-L23 #18948 (comment) seems simpler and more lightweight, but this subjective. Any workaround seems fine if it has an explanation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WalletModel* wallet_model = new WalletModel(std::move(wallet), m_client_model, m_platform_style, nullptr);
// Handler callback runs in a different thread so fix wallet model thread affinity.
WalletModel* wallet_model = new WalletModel(std::move(wallet), m_client_model, m_platform_style,
nullptr /* required for the following moveToThread() call */);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "qt: Improve comments in WalletController::getOrCreateWallet()" (2d6672e)

Nice comments, these do a good job of explaining the context

@hebasto
Copy link
Member Author

hebasto commented Aug 14, 2020

Updated 2d6672e -> 34b83a1 (pr18948.03 -> pr18948.04, diff):

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK 34b83a1. Just suggested changes since review (thanks!) avoiding using preprocessor and timer.

@fanquake
Copy link
Member

fanquake commented Oct 3, 2020

@promag can you take a look here again?

@jonasschnelli
Copy link
Contributor

utACK 34b83a1

hebasto and others added 3 commits November 25, 2020 16:12
This is a replacement of the QMetaObject::invokeMethod functor overload
which is available in Qt 5.10+.

Co-authored-by: Russell Yanofsky <russ@yanofsky.org>
setParent(parent) calls sendEvent(parent, QChildEvent) that implies
running in the thread which created the parent object.
@hebasto
Copy link
Member Author

hebasto commented Nov 25, 2020

Rebased 34b83a1 -> 8963b2c (pr18948.04 -> pr18948.05) due to the conflict with bitcoin-core/gui#46.

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK 8963b2c. No changes since last review, just rebase because of conflict on some adjacent lines

Copy link
Contributor

@jonasschnelli jonasschnelli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK 8963b2c

@jonasschnelli jonasschnelli merged commit c33662a into bitcoin:master Dec 2, 2020
@hebasto hebasto deleted the 200511-qt-assert branch December 2, 2020 09:59
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Dec 2, 2020
8963b2c qt: Improve comments in WalletController::getOrCreateWallet() (Hennadii Stepanov)
5fcfee6 qt: Call setParent() in the parent's context (Hennadii Stepanov)
5659e73 qt: Add ObjectInvoke template function (Hennadii Stepanov)

Pull request description:

  The `setParent(parent)` internally calls `QCoreApplication::sendEvent(parent, QChildEvent)` that implies running in the thread which created the parent object. That is not the case always, and an internal assertion fails in the debug mode.

  Steps to reproduce this issue on master (007e15d) on Linux Mint 20 (x86_64):

  ```
  $ make -C depends DEBUG=1
  $ CONFIG_SITE=$PWD/depends/x86_64-pc-linux-gnu/share/config.site ./configure
  $ make
  $ QT_FATAL_WARNINGS=1 lldb src/qt/bitcoin-qt -- --regtest -debug=qt
  (lldb) target create "src/qt/bitcoin-qt"
  Current executable set to '/home/hebasto/GitHub/bitcoin/src/qt/bitcoin-qt' (x86_64).
  (lldb) settings set -- target.run-args  "--regtest" "-debug=qt"
  (lldb) run
  Process 431562 launched: '/home/hebasto/GitHub/bitcoin/src/qt/bitcoin-qt' (x86_64)
  # load wallet via GUI
  Process 431562 stopped
  * thread #24, name = 'QThread', stop reason = signal SIGABRT
      frame #0: 0x00007ffff794518b libc.so.6`__GI_raise(sig=2) at raise.c:51:1
  (lldb) bt
  * thread #24, name = 'QThread', stop reason = signal SIGABRT
    * frame #0: 0x00007ffff794518b libc.so.6`__GI_raise(sig=2) at raise.c:51:1
      frame #1: 0x00007ffff7924859 libc.so.6`__GI_abort at abort.c:79:7
      frame #2: 0x0000555556508ec4 bitcoin-qt`::qt_message_fatal((null)=<unavailable>, context=<unavailable>, message=<unavailable>) at qlogging.cpp:1690:15
      frame #3: 0x00005555565099cf bitcoin-qt`QMessageLogger::fatal(this=<unavailable>, msg=<unavailable>) const at qlogging.cpp:796:21
      frame #4: 0x000055555650479d bitcoin-qt`qt_assert_x(where=<unavailable>, what=<unavailable>, file=<unavailable>, line=<unavailable>) at qglobal.cpp:3088:46
      frame #5: 0x0000555556685733 bitcoin-qt`QCoreApplicationPrivate::checkReceiverThread(receiver=0x0000555557b27510) at qcoreapplication.cpp:557:5
      frame #6: 0x00005555567ced86 bitcoin-qt`QApplication::notify(this=0x00007fffffffd4a0, receiver=0x0000555557b27510, e=0x00007fff9a7f8ce0) at qapplication.cpp:2956:27
      frame #7: 0x0000555556685d31 bitcoin-qt`QCoreApplication::notifyInternal2(receiver=0x0000555557b27510, event=0x00007fff9a7f8ce0) at qcoreapplication.cpp:1024:24
      frame #8: 0x00005555566c9224 bitcoin-qt`QObjectPrivate::setParent_helper(QObject*) [inlined] QCoreApplication::sendEvent(event=<unavailable>, receiver=<unavailable>) at qcoreapplication.h:233:59
      frame #9: 0x00005555566c9210 bitcoin-qt`QObjectPrivate::setParent_helper(this=0x00007fff85855260, o=0x0000555557b27510) at qobject.cpp:2036
      frame #10: 0x00005555566c9b41 bitcoin-qt`QObject::setParent(this=<unavailable>, parent=<unavailable>) at qobject.cpp:1980:24
      frame #11: 0x0000555555710be8 bitcoin-qt`WalletController::getOrCreateWallet(std::unique_ptr<interfaces::Wallet, std::default_delete<interfaces::Wallet> >) + 2534

  ...
  ```

  Fixes bitcoin#18835.

ACKs for top commit:
  ryanofsky:
    Code review ACK 8963b2c. No changes since last review, just rebase because of conflict on some adjacent lines
  jonasschnelli:
    utACK 8963b2c

Tree-SHA512: fef615904168717df3d8a0bd85eccc3eef990cc3e66c9fa280c8ef08ea009a7cb5a2a4f868ed0be3c0fe5bf683e8465850b5958deb896fdadd22d296186c9586
@bitcoin bitcoin locked as resolved and limited conversation to collaborators Feb 15, 2022
@hebasto
Copy link
Member Author

hebasto commented Apr 16, 2022

5659e73 has been reverted in bitcoin-core/gui#587.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

debug assert in walletcontroller.cpp in func getOrCreateWallet, if thread is not gui
8 participants