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

Editor : Fix deadlock caused by garbage collection of Settings #5893

Merged
merged 3 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ Improvements
- SceneInspector :
- Added support for dragging inspector labels, such as those containing the names of attributes, options, output parameters, parameters, primitive variables, and sets.
- Set names beginning with "__" such as "__lights" or "__cameras" are now displayed as-is, rather than being transformed to "Lights" or "Cameras".
- BackgroundTask : Added reporting of tasks attempting to wait for themselves.

Fixes
-----

- Viewer : Fixed handling of Gaffer `${contextVariable}` references in OpenColorIO variable values. The Viewer now updates the Display Transform appropriately when the value of the context variable changes.

Fixes
-----

- UI : Fixed hangs caused by garbage collection of removed Editors. One common example involved viewing a Catalogue in the NodeEditor after removing the ImageInspector (#5877).

API
---

Expand Down
11 changes: 11 additions & 0 deletions python/GafferUI/Editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ def __init__( self, topLevelWidget, scriptNode, **kw ) :

self.__setContextInternal( scriptNode.context(), callUpdate=False )

def __del__( self ) :

for attr in self.__dict__.values() :
if isinstance( attr, GafferUI.Editor.Settings ) :
# Remove connection to ScriptNode now, on the UI thread.
# Otherwise we risk deadlock if the Settings node gets garbage
# collected in a BackgroundTask, which would attempt
# cancellation of all tasks for the ScriptNode, including the
# task itself.
attr["__scriptNode"].setInput( None )

def scriptNode( self ) :

return self.__scriptNode
Expand Down
47 changes: 47 additions & 0 deletions python/GafferUITest/EditorTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import unittest
import weakref

import IECore

import Gaffer
import GafferTest
import GafferUI
Expand Down Expand Up @@ -110,5 +112,50 @@ def pythonEditorCreated( editor ) :
self.assertEqual( editorsCreated, [ e1, e2, e3 ] )
self.assertEqual( pythonEditorsCreated, [ e2 ] )

def testIssue5877( self ) :

# This test models the bug reported in #5877. First we have
# an editor that uses a `Settings` node.

script = Gaffer.ScriptNode()
script["node"] = GafferTest.MultiplyNode()

class TestEditor( GafferUI.Editor ) :

def __init__( self, scriptNode, **kw ) :

GafferUI.Editor.__init__( self, GafferUI.Label( "MyEditor" ), scriptNode )

self.__settingsNode = self.Settings( "MyEditor", scriptNode )

editor = TestEditor( script )

# Then we dispose of that editor.

del editor

# Now we run a perfectly innocent background task that just
# wants to compute something perfectly innocently.

def f( canceller ) :

script["node"]["product"].getValue()

# But of course, garbage collection can occur at any time.
# For the purposes of this test we force it to happen explicitly,
# but in the real world it would be triggered by the creation
# of a new wrapped RefCounted instance. In #5877 this was commonly
# an `_ImagesPath` created by the CatalogueUI in a background task
# updating the PathListingWidget for the image listing.
IECore.RefCounted.collectGarbage()

task = Gaffer.BackgroundTask( script["node"]["product"], f )

# This would have deadlocked when `collectGarbage()` triggered the
# destruction of the Settings node, because that would trigger
# cancellation of the BackgroundTask from the task itself. And a task
# waiting on itself would never finish.
task.wait()

if __name__ == "__main__":
unittest.main()
12 changes: 11 additions & 1 deletion src/Gaffer/BackgroundTask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@

#include "fmt/format.h"

#include <thread>

using namespace IECore;
using namespace Gaffer;

Expand Down Expand Up @@ -155,9 +157,10 @@ struct BackgroundTask::TaskData : public boost::noncopyable

Function *function;
IECore::Canceller canceller;
std::mutex mutex; // Protects `conditionVariable` and `status`
std::mutex mutex; // Protects `conditionVariable`, `status` and `threadID`
std::condition_variable conditionVariable;
Status status;
std::thread::id threadId; // Thread that is executing `function`, if any
};

BackgroundTask::BackgroundTask( const Plug *subject, const Function &function )
Expand Down Expand Up @@ -186,6 +189,7 @@ BackgroundTask::BackgroundTask( const Plug *subject, const Function &function )
// Otherwise do the work.

taskData->status = Running;
taskData->threadId = std::this_thread::get_id();
lock.unlock();

// Reset thread state rather then inherit the random
Expand Down Expand Up @@ -225,6 +229,7 @@ BackgroundTask::BackgroundTask( const Plug *subject, const Function &function )

lock.lock();
taskData->status = status;
taskData->threadId = std::thread::id();
taskData->conditionVariable.notify_one();
}
);
Expand All @@ -248,6 +253,11 @@ void BackgroundTask::cancel()
void BackgroundTask::wait()
{
std::unique_lock<std::mutex> lock( m_taskData->mutex );
if( m_taskData->threadId == std::this_thread::get_id() )
{
IECore::msg( IECore::Msg::Error, "BackgroundTask::wait", "Deadlock detected : Task is attempting to wait for itself. Please provide stack trace in bug report." );
}

m_taskData->conditionVariable.wait(
lock,
[this]{
Expand Down
Loading