raulcd commented on code in PR #50687:
URL: https://github.com/apache/arrow/pull/50687#discussion_r3673826598
##########
python/pyarrow/tests/test_flight.py:
##########
@@ -1128,6 +1130,43 @@ def test_flight_server_location_argument():
assert isinstance(server, FlightServerBase)
+# The following tests are for GH-50684, which was a memory leak
+# in FlightServerBase.
+def test_flight_server_is_freed():
+ # Calling server.shutdown manually should free the server object.
+ server = FlightServerBase('grpc://localhost:0')
+ server.shutdown()
+ server.wait()
+ ref = weakref.ref(server)
+ del server
+ gc.collect()
+ assert ref() is None
+
+
+def test_flight_server_is_freed_on_exit():
+ # Using FlightServerBase as a context manager should free
+ # the server object on exit.
+ with FlightServerBase('grpc://localhost:0') as server:
+ ref = weakref.ref(server)
+ del server
+ gc.collect()
+ assert ref() is None
+
+
[email protected](
+ reason="GH-50684: FlightServerBase is not freed on delete without shutdown"
+)
+def test_flight_server_is_freed_without_shutdown():
+ # GH-50684: Not calling server.shutdown() currently leaks the server
object.
+ # This test is expected to fail until the issue is fixed but is included
+ # for completeness and further discussion.
Review Comment:
I've reproduced the hang with the following small snippet and a local PoC
moving to a weakref, done is never printed, it hangs forever:
```python
import threading
import time
import pyarrow as pa
import pyarrow.flight as fl
class Server(fl.FlightServerBase):
def do_get(self, context, ticket):
time.sleep(2)
return fl.RecordBatchStream(pa.table({"a": [1]}).to_reader())
server = Server("grpc://127.0.0.1:0")
location = f"grpc://127.0.0.1:{server.port}"
def client():
fl.FlightClient(location).do_get(fl.Ticket(b"")).read_all()
thread = threading.Thread(target=client)
thread.start()
time.sleep(1)
del server
thread.join()
print("done")
```
With py-spy dump I can see the idle thread and part of the stack pointing to
the DoGet thread calling shutdown, this is trimmed:
```gdb
grpc_completion_queue_next (completion_queue.cc:1154)
grpc::CompletionQueue::AsyncNextInternal (completion_queue_cc.cc:147)
grpc::CompletionQueue::AsyncNext<gpr_timespec> (completion_queue.h:202)
grpc::Server::ShutdownInternal (server_cc.cc:1296)
grpc::ServerInterface::Shutdown (server_interface.h:106)
Shutdown (grpc_server.cc:649)
arrow::flight::FlightServerBase::Shutdown (server.cc:216)
_FlightServerFinalizer_finalize (pyarrow/_flight.pyx:2885)
finalize (pyarrow/_flight.pyx:2877)
__Pyx_CyFunction_Vectorcall_FASTCALL_KEYWORDS (pyarrow/_flight.pyx:1666)
__call__ (weakref.py:590)
__pyx_tp_dealloc_7pyarrow_3lib__Weakrefable (pyarrow/public-api.pxi:439)
__pyx_tp_dealloc_7pyarrow_7_flight_FlightServerBase
(pyarrow/_flight.pyx:3251)
Py_XDECREF (object.h:1049)
arrow::py::OwnedRef::reset (common.h:202)
arrow::py::OwnedRef::reset (common.h:205)
arrow::py::OwnedRef::~OwnedRef (common.h:198)
operator() (flight.cc:160)
SafeCallIntoPython<arrow::py::flight::PyFlightServer::DoGet(const
arrow::flight::ServerCallContext&, const arrow::flight::Ticket&,
std::unique_ptr<arrow::flight::FlightDataStream>*)::<lambda()> > (common.h:161)
arrow::py::flight::PyFlightServer::DoGet (flight.cc:161)
arrow::flight::internal::ServerTransport::DoGet (transport_server.cc:301)
DoGet (grpc_server.cc:493)
operator() (Flight.grpc.pb.cc:300)
__invoke_impl<grpc::Status,
arrow::flight::protocol::FlightService::Service::Service()::<lambda(arrow::flight::protocol::FlightService::Service*,
grpc::ServerContext*, const arrow::flight::protocol::Ticket*,
grpc::ServerWriter<arrow::flight::protocol::FlightData>*)>&,
arrow::flight::protocol::FlightService::Service*, grpc::ServerContext*, const
arrow::flight::protocol::Ticket*,
grpc::ServerWriter<arrow::flight::protocol::FlightData>*> (invoke.h:63)
```
This is not happening today because the strong reference enforces refcount >
0, we have to manually call shutdown for the reference to be released which
should never be done from a serving thread as the method documents. I am now
wondering whether not calling shutdown and keeping the Python object alive
should be the intended behavior instead of being treated as a bug leaking an
object on this case.
--
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]