PythonProxyNotificationHandler::OnEventGenericHook acquires the Python GIL before executing (#3904)

- Protected python execution of OnEventGenericHook by trying to lock the GIL and show a descriptive error when it was not possible to lock.
- Improved mechanism to lock python mutex and GIL in PhytonSystemComponent. Acquiring/releasing GIL once per thread.
- Added unit test to verify errors are fired when trying to execute OnEventGenericHook from another thread (as it should not able to acquire the GIL).
- Improved python threading tests to actually using python buses.

Signed-off-by: moraaar moraaar@amazon.com
This commit is contained in:
moraaar
2021-09-07 15:35:04 +01:00
committed by GitHub
parent 33299399af
commit f551e69b2b
16 changed files with 259 additions and 67 deletions
@@ -20,6 +20,7 @@
#include <AzCore/RTTI/AttributeReader.h>
#include <AzCore/std/optional.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
namespace EditorPythonBindings
{
@@ -261,19 +262,39 @@ namespace EditorPythonBindings
static void OnEventGenericHook(void* userData, const char* eventName, int eventIndex, AZ::BehaviorValueParameter* result, int numParameters, AZ::BehaviorValueParameter* parameters)
{
reinterpret_cast<PythonProxyNotificationHandler*>(userData)->OnEventGenericHook(eventName, eventIndex, result, numParameters, parameters);
}
void OnEventGenericHook(const char* eventName, [[maybe_unused]] int eventIndex, AZ::BehaviorValueParameter* result, int numParameters, AZ::BehaviorValueParameter* parameters)
{
// find the callback for the event
const auto& callbackEntry = m_callbackMap.find(eventName);
if (callbackEntry == m_callbackMap.end())
auto editorPythonEventsInterface = AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Get();
if (!editorPythonEventsInterface)
{
return;
}
pybind11::function callback = callbackEntry->second;
// find the callback for the event
auto* handler = reinterpret_cast<PythonProxyNotificationHandler*>(userData);
const auto& callbackEntry = handler->m_callbackMap.find(eventName);
if (callbackEntry == handler->m_callbackMap.end())
{
return;
}
// This function can reach from multiple threads, which means OnEventGenericHook
// will require to acquire the Python GIL, make sure it tries to lock it using TryExecuteWithLock.
[[maybe_unused]] const bool executed = editorPythonEventsInterface->TryExecuteWithLock(
[handler, eventName, callback = callbackEntry->second, eventIndex, result, numParameters, parameters]()
{
handler->OnEventGenericHook(eventName, callback, eventIndex, result, numParameters, parameters);
});
AZ_Error("python", executed,
"Ebus(%s) event(%s) could not be executed because it could not acquire the Python GIL. "
"This occurs when there is already another thread executing python, which has the GIL locked, "
"making it not possible for this thread to callback python at the same time. "
"This is a limitation of python interpreter. Python scripts executions and event callbacks "
"from EBuses need be designed to avoid this scenario.",
handler->m_ebus->m_name.c_str(), eventName);
}
void OnEventGenericHook(const char* eventName, pybind11::function callback, [[maybe_unused]] int eventIndex, AZ::BehaviorValueParameter* result, int numParameters, AZ::BehaviorValueParameter* parameters)
{
// build the parameters to send to callback
Convert::StackVariableAllocator stackVariableAllocator;
pybind11::tuple pythonParamters(numParameters);