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

[internal] Refactor engine_aware.rs to acquire the GIL fewer times #13515

Merged

Conversation

Eric-Arellano
Copy link
Contributor

@Eric-Arellano Eric-Arellano commented Nov 7, 2021

The PyO3 maintainers recommended that our low-level helpers request Python (a lock for the GIL) as an argument, rather than acquiring it directly: #12451 (comment). This is because acquiring the GIL has a non-trivial overhead, and it allows callers to batch the operation. Indeed, this PR has two map operations where we now only acquire the GIL once, rather than n times.

This also simplifies our handling of TypeErrors for less verbose code. As explained in #13515 (comment), MyPy already will handle these issues and they're also not mission-critical. Not worth the boilerplate to duplicate MyPy.

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
Copy link
Contributor Author

@Eric-Arellano Eric-Arellano left a comment

Choose a reason for hiding this comment

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

FYI my main motivation is refactoring our rust-cpython code so that it's easier to port to PyO3.

let gil = Python::acquire_gil();
let args_tuple = PyTuple::new(gil.python(), &arg_handles);
value.call_method(gil.python(), method, args_tuple, None)
pub fn call_method0(py: Python, value: &PyObject, method: &str) -> Result<PyObject, PyErr> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

PyO3 has this as a method on PyAny, so we'll be able to inline this soon!

Apparently we can't panic

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
@Eric-Arellano
Copy link
Contributor Author

Apparently, we can't panic in engine_aware.rs like I was originally trying to do.

So then, I was trying to improve this code so that we safely handle errors but log them. It was really boiler-platey, and I only got halfway:

diff --git a/src/rust/engine/src/externs/engine_aware.rs b/src/rust/engine/src/externs/engine_aware.rs
index 22dc4c2e6..33a1ba0e0 100644
--- a/src/rust/engine/src/externs/engine_aware.rs
+++ b/src/rust/engine/src/externs/engine_aware.rs
@@ -4,7 +4,7 @@
 use crate::context::Context;
 use crate::externs;
 use crate::nodes::{lift_directory_digest, lift_file_digest};
-use crate::python::{TypeId, Value};
+use crate::python::{Failure, TypeId, Value};
 use crate::Types;
 
 use cpython::{ObjectProtocol, PyDict, PyString, Python};
@@ -43,19 +43,39 @@ impl EngineAwareReturnType {
   }
 
   fn level(py: Python, value: &Value) -> Option<Level> {
-    let level_val = externs::call_method0(py, value, "level").unwrap();
-    if level_val.is_none(py) {
-      return None;
-    }
-    Some(externs::val_to_log_level(&level_val).unwrap())
+    externs::call_method0(py, value, "level")
+      .and_then(|res| {
+        if res.is_none(py) {
+          Ok(None)
+        } else {
+          Ok(externs::val_to_log_level(&res).ok())
+        }
+      })
+      .unwrap_or_else(|err| {
+        log::error!(
+          "Error in EngineAwareReturnType.level(): {}",
+          Failure::from_py_err_with_gil(py, err)
+        );
+        None
+      })
   }
 
   fn message(py: Python, value: &Value) -> Option<String> {
-    let msg_val = externs::call_method0(py, value, "message").unwrap();
-    if msg_val.is_none(py) {
-      return None;
-    }
-    msg_val.extract(py).unwrap()
+    externs::call_method0(py, value, "message")
+      .and_then(|res| {
+        if res.is_none(py) {
+          Ok(None)
+        } else {
+          res.extract(py)
+        }
+      })
+      .unwrap_or_else(|err| {
+        log::error!(
+          "Error in EngineAwareReturnType.message(): {}",
+          Failure::from_py_err_with_gil(py, err)
+        );
+        None
+      })
   }
 
   fn artifacts(py: Python, types: &Types, value: &Value) -> Vec<(String, ArtifactOutput)> {
@@ -88,9 +108,14 @@ impl EngineAwareReturnType {
 
   pub(crate) fn is_cacheable(py: Python, value: &Value) -> bool {
     externs::call_method0(py, value, "cacheable")
-      .unwrap()
-      .extract(py)
-      .unwrap()
+      .and_then(|res| res.extract(py))
+      .unwrap_or_else(|err| {
+        log::error!(
+          "Error in EngineAwareReturnType.cacheable(): {}",
+          Failure::from_py_err_with_gil(py, err)
+        );
+        true
+      })
   }
 }
 
@@ -98,11 +123,21 @@ pub struct EngineAwareParameter;
 
 impl EngineAwareParameter {
   pub fn debug_hint(py: Python, value: &Value) -> Option<String> {
-    let hint = externs::call_method0(py, value, "debug_hint").ok()?;
-    if hint.is_none(py) {
-      return None;
-    }
-    hint.extract(py).ok()
+    externs::call_method0(py, value, "debug_hint")
+      .and_then(|res| {
+        if res.is_none(py) {
+          Ok(None)
+        } else {
+          res.extract(py)
+        }
+      })
+      .unwrap_or_else(|err| {
+        log::error!(
+          "Error in EngineAwareReturnType.debug_hint(): {}",
+          Failure::from_py_err_with_gil(py, err)
+        );
+        None
+      })
   }
 
   pub fn metadata(py: Python, context: &Context, value: &Value) -> Vec<(String, UserMetadataItem)> {

And then it dawned on me, MyPy will catch all these issues that we were trying to log. They're all TypeError! Not worth the complexity IMO for our Rust code to duplicate MyPy, especially given that these errors are not mission critical.

@Eric-Arellano Eric-Arellano merged commit 5aa46a5 into pantsbuild:main Nov 7, 2021
@Eric-Arellano Eric-Arellano deleted the rust-cpython-engine-awar branch November 7, 2021 23:20
Eric-Arellano added a commit that referenced this pull request Nov 7, 2021
#13516)

See #13515 for the motivation. Making GIL access explicit does make call sites more verbose, but it also makes it easier to reason about where the GIL is used and ensures we are not continually dropping and then reacquiring the GIL. 

Compare to #11186, which resulted in sharing the GIL but also relied on an intricate implementation detail. The modeling is now explicit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants