weiqingy commented on code in PR #944:
URL: https://github.com/apache/flink-agents/pull/944#discussion_r3696783616


##########
runtime/src/main/java/org/apache/flink/agents/runtime/python/utils/PythonActionExecutor.java:
##########
@@ -188,17 +189,32 @@ public boolean callPythonAwaitable(String 
pythonAwaitableRef) {
     }
 
     public void close() throws Exception {
-        if (interpreter != null) {
-            if (pythonAsyncThreadPool != null) {
-                interpreter.invoke(CLOSE_ASYNC_THREAD_POOL, 
pythonAsyncThreadPool);
-            }
+        PyObject asyncThreadPool = pythonAsyncThreadPool;
+        PyObject runnerContext = pythonRunnerContext;
+        pythonAsyncThreadPool = null;
+        pythonRunnerContext = null;
+
+        Exception exception = null;
+        try {
+            closePythonObject(CLOSE_ASYNC_THREAD_POOL, asyncThreadPool);
+        } catch (Exception e) {
+            exception = ExceptionUtils.firstOrSuppressed(e, exception);
+        }
+        try {
+            closePythonObject(CLOSE_FLINK_RUNNER_CONTEXT, runnerContext);
+        } catch (Exception e) {
+            exception = ExceptionUtils.firstOrSuppressed(e, exception);
+        }
+
+        if (exception != null) {
+            throw exception;

Review Comment:
   Combining both failures and rethrowing is the right call. What I keep 
looking at is what happens to this exception one frame up:
   
   ```java
   // PythonBridgeManager.close(), lines 292-302
   if (pythonActionExecutor != null) { pythonActionExecutor.close(); }
   if (pythonInterpreter != null) { pythonInterpreter.close(); }
   if (pythonEnvironmentManager != null) { pythonEnvironmentManager.close(); }
   ```
   
   That is a plain sequence, so on exactly the failure path this PR is built 
for, `pythonInterpreter.close()` never runs, and that is the release that tears 
down the interpreter owning every handle still outstanding. 
`ActionExecutionOperator.close()` (lines 469-489) has the same shape across its 
five closes.
   
   To be clear, this is pre-existing. The old `close()` threw on a failed 
`interpreter.invoke` too, so nothing has regressed here. But given the stated 
goal is "releases both handles even if one cleanup operation fails", how do you 
see that goal holding one frame up? Carrying the same `firstOrSuppressed` 
pattern into `PythonBridgeManager.close()` would make the guarantee end-to-end, 
though I may be missing a reason the interpreter is fine to leak on that path.



##########
runtime/src/main/java/org/apache/flink/agents/runtime/python/utils/PythonActionExecutor.java:
##########
@@ -188,17 +189,32 @@ public boolean callPythonAwaitable(String 
pythonAwaitableRef) {
     }
 
     public void close() throws Exception {
-        if (interpreter != null) {
-            if (pythonAsyncThreadPool != null) {
-                interpreter.invoke(CLOSE_ASYNC_THREAD_POOL, 
pythonAsyncThreadPool);
-            }
+        PyObject asyncThreadPool = pythonAsyncThreadPool;

Review Comment:
   nit: The copy-then-null reads as defensive style, but it looks load-bearing. 
`PyObject.close()` in pemja 0.5.7 is an unguarded `decRef(tState, pyobject)` 
with no null check and no double-close flag, so clearing the fields first is 
the only thing stopping a repeated `close()` from decrementing a second time on 
an already-released handle. Someone later tidying this into 
`closePythonObject(CLOSE_ASYNC_THREAD_POOL, pythonAsyncThreadPool)` would drop 
that quietly, and the only assertion that would notice is the three-line tail 
of `releasesBothPythonObjectsWhenLogicalCleanupFails`.
   
   Would a short comment here save the next reader that trip? There is 
precedent right next door at `ActionExecutionOperator.java:471` (`// Must close 
before pythonInterpreter since cached resources may hold Python references.`).
   
   Something like this, if it helps:
   
   ```java
   // Clear the fields before releasing: PyObject.close() is an unguarded 
native decRef,
   // so a repeated close() must not reach the same handle twice.
   ```



##########
runtime/src/main/java/org/apache/flink/agents/runtime/python/utils/PythonActionExecutor.java:
##########
@@ -188,17 +189,32 @@ public boolean callPythonAwaitable(String 
pythonAwaitableRef) {
     }
 
     public void close() throws Exception {
-        if (interpreter != null) {
-            if (pythonAsyncThreadPool != null) {
-                interpreter.invoke(CLOSE_ASYNC_THREAD_POOL, 
pythonAsyncThreadPool);
-            }
+        PyObject asyncThreadPool = pythonAsyncThreadPool;
+        PyObject runnerContext = pythonRunnerContext;
+        pythonAsyncThreadPool = null;
+        pythonRunnerContext = null;
+
+        Exception exception = null;
+        try {
+            closePythonObject(CLOSE_ASYNC_THREAD_POOL, asyncThreadPool);
+        } catch (Exception e) {
+            exception = ExceptionUtils.firstOrSuppressed(e, exception);
+        }
+        try {
+            closePythonObject(CLOSE_FLINK_RUNNER_CONTEXT, runnerContext);
+        } catch (Exception e) {
+            exception = ExceptionUtils.firstOrSuppressed(e, exception);
+        }
+
+        if (exception != null) {
+            throw exception;
+        }
+    }
 
-            if (pythonRunnerContext != null) {
-                try {
-                    interpreter.invoke(CLOSE_FLINK_RUNNER_CONTEXT, 
pythonRunnerContext);
-                } finally {
-                    pythonRunnerContext = null;
-                }
+    private void closePythonObject(String closeFunction, PyObject 
pythonObject) throws Exception {
+        if (pythonObject != null) {
+            try (pythonObject) {

Review Comment:
   This is the shape the whole fix turns on: logical cleanup inside, native 
release on the way out. Two other Pemja handles in the same lifecycle still 
have the pre-PR shape.
   
   `Mem0LongTermMemory.close()` (`Mem0LongTermMemory.java:137-140`) calls 
`adapter.callMethod(pyMem0, "close", Map.of())` and never `pyMem0.close()`, 
which is the old `PythonActionExecutor.close()` exactly. It is live rather than 
dead code: `RunnerContextImpl.java:339-345` calls `ltm.close()`, and the handle 
comes from `PythonBridgeManager.java:237`.
   
   `PythonResourceAdapterImpl.pythonResourceContext` 
(`PythonResourceAdapterImpl.java:86,99`) is built as 
`interpreter.invoke(GET_RESOURCE_CONTEXT, this)`, so it is a Java object handed 
into Python, the same JNI-global-ref pattern #942 describes. That class has no 
`close()` at all, and `PythonBridgeManager.close()` never touches the adapter.
   
   I read the PR and #942 as deliberately scoped to the action executor, so I 
am not suggesting you widen this one. Is a follow-up issue the plan for the 
sibling handles, or is there something that already releases those two that I 
have missed?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to