GH-50684: [Python][FlightRPC] Break the reference cycle between the C++ FlightServerBase and the Python object to avoid leaking server - #50687
Conversation
… the C++ FlightServerBase and the Python object to avoid leaking server
|
@github-actions crossbow submit wheel-manylinux-2-28-cp313-cp313-amd64 |
|
Revision: 6c7c46a Submitted crossbow builds: ursacomputing/crossbow @ actions-e80f3b2c61
|
This won't work is neither |
Correct, this is why the newly added test which doesn't call shutdown or exit has an xfail mark. |
|
@lidavidm I'd appreciate some feedback on the approach here as you probably have a better mental model of how all this fits together. |
|
It seems reasonable to me. It's been a while since I looked at this code. I do agree with Antoine that maybe we just make C++ hold a weakref to the Python side, presumably nobody is intentionally leaking a server and assuming it will keep running...? |
|
Here's an idea: instead of: def shutdown(self):
with nogil:
check_flight_status(self.server.get().Shutdown())
self.server.get().ReleasePythonServerRef()How about we do: def serve(self):
self.server.get().SetPythonServerRef(self)
try:
with nogil:
check_flight_status(self.server.get().ServeWithSignals())
finally:
self.server.get().ReleasePythonServerRef()and avoid passing |
I don't think calling serve is mandatory. We have plenty of tests where we call the constructor and dispatch without ever calling |
Hmm, I see. I would consider these tests a bit artificial (end users aren't supposed to call these methods directly AFAIU), but that's a good argument for keeping the current behavior I suppose. |
|
I agree that the tests are artificial but our current documentation states that calling arrow/python/pyarrow/_flight.pyx Lines 2893 to 2900 in 3deec0b |
I had missed that part. It's a bit unfortunate :-/ |
|
moving into review to correctly reflect state as we have already been reviewing it :) |
There was a problem hiding this comment.
Pull request overview
This PR addresses GH-50684 by breaking a reference cycle between the C++ PyFlightServer (which owns an OwnedRefNoGIL to the Python FlightServerBase) and the Python object (which owns the C++ server), allowing servers to be freed after shutdown instead of leaking for the lifetime of the process.
Changes:
- Add
PyFlightServer::ReleasePythonServerRef()to clear the C++ → Python reference and break the cycle. - Invoke
ReleasePythonServerRef()after shutdown paths in the Cython bindings (explicitshutdown()and the server finalizer). - Add Python tests asserting the server object is freed after shutdown / context-manager exit, plus an
xfailtest documenting the “no shutdown” leak case.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| python/pyarrow/tests/test_flight.py | Adds regression tests validating server objects are freed after shutdown/exit (and documents current behavior without shutdown). |
| python/pyarrow/src/arrow/python/flight.h | Declares ReleasePythonServerRef() on PyFlightServer. |
| python/pyarrow/src/arrow/python/flight.cc | Implements ReleasePythonServerRef() by resetting the stored Python reference. |
| python/pyarrow/includes/libarrow_flight.pxd | Exposes ReleasePythonServerRef() to Cython. |
| python/pyarrow/_flight.pyx | Calls ReleasePythonServerRef() after shutdown and in the server finalizer to break the cycle. |
| @@ -2887,6 +2887,7 @@ cdef class _FlightServerFinalizer(_Weakrefable): | |||
| status = server.Wait() | |||
There was a problem hiding this comment.
By the way, is the Wait call useful here? My understanding is that Wait can be used from another thread than the thread that calls Shutdown, but calling it synchronously after Shutdown should return immediately? @lidavidm
There was a problem hiding this comment.
Indeed it should...so this seems redundant.
There was a problem hiding this comment.
Should I try removing it here? Probably not worth just creating a follow up issue for it.
There was a problem hiding this comment.
Yes, I think you can try removing it.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
python/pyarrow/src/arrow/python/flight.cc:93
ReleasePythonServerRef()callsserver_.reset(), which ultimately doesPy_XDECREFand therefore requires the Python runtime to be initialized and the GIL to be held. Since this method is exposed to Cython asnogil(and is also called from shutdown/finalizers), it should defensively acquire the GIL (and avoid touching refcounts if the interpreter is already finalized) to prevent potential crashes if it is ever invoked without the GIL or during interpreter teardown.
void PyFlightServer::ReleasePythonServerRef() {
// Resets OwnedRefNoGIL to break the reference cycle between the C++ FlightServerBase
// and the Python object.
server_.reset();
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
python/pyarrow/src/arrow/python/flight.cc:93
ReleasePythonServerRef()callsOwnedRefNoGIL::reset(), butreset()does not acquire the GIL (only the destructor does). Since this method is declared in anogilCython extern block, it must be safe to call without the GIL; as written it can decref a Python object without the GIL, which is unsafe and can crash.
void PyFlightServer::ReleasePythonServerRef() {
// Resets OwnedRefNoGIL to break the reference cycle between the C++ FlightServerBase
// and the Python object.
server_.reset();
}
python/pyarrow/tests/test_flight.py:1157
- This
xfailis non-strict, so if the underlying leak is fixed later the test will become an XPASS and may not fail CI, leaving a stale xfail marker in place.
@pytest.mark.xfail(
reason="GH-50684: FlightServerBase is not freed on delete without shutdown"
)
|
@github-actions crossbow submit -g python |
|
Revision: 5b600c2 Submitted crossbow builds: ursacomputing/crossbow @ actions-ed9acf3136 |
Rationale for this change
PyFlightServer keeps a reference towards the Python server via
OwnedRefNoGIL server_, the Python server also keeps a reference of the C++PyFlightServercreating a cycle that is never freed during the process lifetime.What changes are included in this PR?
Create a new
ReleasePythonServerRefmethod that is called after anyserver.Shutdown(including at__exit__) allowing for theOwnedRefNoGILto be cleared breaking the cycle. This lets normal reference counting free the previously leaked Python object.Are these changes tested?
Yes, the newly added tests were failing leaking the references before the fix.
Currently a test demonstrating a leak when not calling server.Shutdown is added for discussion purposes.
Are there any user-facing changes?
No