From 96add5254d1c4a598cabb771dd22f6972c08b626 Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Sat, 14 Feb 2026 07:57:35 -0400 Subject: [PATCH] Fix LibevConnection close() race causing EBADF errors (#614) LibevConnection.close() closes the socket immediately while watchers are stopped asynchronously in the next event loop iteration via _loop_will_run(). This creates a race window where handle_read() or handle_write() can operate on a closed socket fd, producing EBADF errors that surface as ConnectionShutdown. - Add is_closed/is_defunct guards in handle_read() and handle_write() error paths to silently exit during shutdown instead of calling defunct() with EBADF - Set last_error in close() when connected_event is not yet set to prevent factory() from returning a dead connection - Set last_error on server-initiated close (EOF) in handle_read() before calling close() --- cassandra/io/libevreactor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cassandra/io/libevreactor.py b/cassandra/io/libevreactor.py index d7b365e451..a98e735b2c 100644 --- a/cassandra/io/libevreactor.py +++ b/cassandra/io/libevreactor.py @@ -300,7 +300,10 @@ def close(self): msg = "Connection to %s was closed" % self.endpoint if self.last_error: msg += ": %s" % (self.last_error,) - self.error_all_requests(ConnectionShutdown(msg)) + shutdown_exc = ConnectionShutdown(msg) + self.error_all_requests(shutdown_exc) + if not self.connected_event.is_set(): + self.last_error = shutdown_exc self.connected_event.set() def handle_write(self, watcher, revents, errno=None): @@ -378,6 +381,8 @@ def handle_read(self, watcher, revents, errno=None): self.process_io_buffer() else: log.debug("Connection %s closed by server", self) + self.last_error = ConnectionShutdown( + "Connection to %s was closed by server" % self.endpoint) self.close() def push(self, data):