Operational model for SMTK

There is currently a bug in SMTK that allows operation observers to run after the operation’s resource locks have been released. This results in other operations modifying the same resources while observers attempt to update the UI in response to the first operation.

There is a way to force observers to run in the GUI thread while the operation’s thread holds the resource locks, but this can cause deadlocks if the GUI thread ever attempts to obtain a resource lock – because now an operation can be waiting for the GUI event loop to run observers while the GUI event loop is running code that is waiting for the operation’s resources to become free.

For this reason, we are proposing …

GUI Code Must Never Attempt To Acquire Resource Locks

If the GUI thread never blocks waiting for resource locks, then it is safe for Qt code to block until GUI code runs.

This rule forms the basis for an operational model for SMTK applications. As a corollary, the second tenet of this operational model is that …

GUI Applications Must Always Launch Operations …

… as opposed to calling operate() on them directly. Because smtk::operation::Operation’s operate() method acquires resource locks, it must always run on a non-GUI thread.

We have considered making operate() a protected method, but this greatly complicates Python scripting. If you always had to launch operations, even in Python scripts, scripting would become tedious and verbose.

Instead we propose emitting an error message when operate() is invoked in a GUI thread. This error message would cause test failures (to prevent bad code from getting merged) and, should a deadlock occur, indicates the cause of the deadlock.

One minor variation we might consider is adding a new method to operations named launch() that returns a std::future<Result> instead of a Result and queues the operation to run in a separate thread. Scripts could simply wait for the future to become available. However, waiting like this would leave GUI non-responsive.

Also, you might be asking “What about the Python shell inside modelbuilder?” Yes, even here calls to operate() must be avoided.

Another corner case to consider is Python operations (i.e., operations written in Python that inherit smtk.operation.Operation). Because of the GIL, this operation must be run in the GUI thread. Some work may need to be done to lock resources on a separate thread and then signal the operation to begin on the GUI thread.

Once we have audited SMTK to ensure the rules above are met, we will modify pqSMTKCallObserversOnMainThread to properly block operations from releasing resource locks until the main thread completes running the operation’s observers.