Skip to content

Commit fc09951

Browse files
committed
connection: tolerate clean startup close
1 parent bcc2d3d commit fc09951

2 files changed

Lines changed: 98 additions & 3 deletions

File tree

cassandra/connection.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -986,8 +986,6 @@ def factory(cls, endpoint, timeout, host_conn = None, *args, **kwargs):
986986
conn.close()
987987
raise OperationTimedOut("Timed out creating connection (%s seconds)" % timeout,
988988
timeout=timeout)
989-
elif conn.is_closed:
990-
raise ConnectionShutdown("Connection to %s was closed by server" % conn.endpoint)
991989
else:
992990
return conn
993991

tests/unit/test_connection.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import itertools
15+
import socket
1516
import unittest
1617
from io import BytesIO
1718
import time
18-
from threading import Lock
19+
from threading import Event, Lock, Thread
1920
from unittest.mock import Mock, ANY, call, patch
2021

2122
from cassandra import OperationTimedOut
@@ -383,6 +384,102 @@ def test_wait_for_responses_shutdown_includes_last_error(self):
383384
assert "already closed" in error_message
384385
assert "Bad file descriptor" in error_message
385386

387+
def test_factory_returns_maintenance_mode_clean_startup_close(self):
388+
"""
389+
Maintenance mode accepts regular CQL sockets and closes them during
390+
startup. The factory must leave that clean close for the owning
391+
pool/control connection instead of turning it into an immediate failure.
392+
"""
393+
394+
class MaintenanceModeCqlServer(object):
395+
def __init__(self):
396+
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
397+
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
398+
self._sock.bind(('127.0.0.1', 0))
399+
self._sock.listen(1)
400+
self._sock.settimeout(2)
401+
self.port = self._sock.getsockname()[1]
402+
self.first_frame = b''
403+
self.ready = Event()
404+
self.received_frame = Event()
405+
self.error = None
406+
self.thread = Thread(target=self._run)
407+
self.thread.daemon = True
408+
self.thread.start()
409+
410+
def _run(self):
411+
self.ready.set()
412+
try:
413+
client, _ = self._sock.accept()
414+
with client:
415+
client.settimeout(2)
416+
while len(self.first_frame) < 9:
417+
chunk = client.recv(9 - len(self.first_frame))
418+
if not chunk:
419+
break
420+
self.first_frame += chunk
421+
except Exception as exc:
422+
self.error = exc
423+
finally:
424+
self.received_frame.set()
425+
426+
def close(self):
427+
self._sock.close()
428+
self.thread.join(2)
429+
430+
class MaintenanceModeConnection(Connection):
431+
def __init__(self, *args, **kwargs):
432+
super(MaintenanceModeConnection, self).__init__(*args, **kwargs)
433+
self._reader = None
434+
self._connect_socket()
435+
self._send_options_message()
436+
self._reader = Thread(target=self._read_until_server_closes)
437+
self._reader.daemon = True
438+
self._reader.start()
439+
440+
def push(self, data):
441+
self._socket.sendall(data)
442+
443+
def close(self):
444+
with self.lock:
445+
if self.is_closed:
446+
return
447+
self.is_closed = True
448+
449+
if self._socket:
450+
self._socket.close()
451+
452+
if not self.is_defunct:
453+
self.error_all_requests(
454+
ConnectionShutdown("Connection to %s was closed" % self.endpoint))
455+
self.connected_event.set()
456+
457+
def _read_until_server_closes(self):
458+
try:
459+
while True:
460+
data = self._socket.recv(self.in_buffer_size)
461+
if not data:
462+
self.close()
463+
return
464+
self._iobuf.write(data)
465+
self.process_io_buffer()
466+
except socket.error as exc:
467+
if not self.is_closed:
468+
self.defunct(exc)
469+
470+
server = MaintenanceModeCqlServer()
471+
try:
472+
assert server.ready.wait(2)
473+
conn = MaintenanceModeConnection.factory(
474+
DefaultEndPoint('127.0.0.1', server.port), timeout=2)
475+
476+
assert conn.is_closed
477+
assert server.received_frame.wait(2)
478+
assert server.error is None
479+
assert server.first_frame[4] == 0x05 # OPTIONS
480+
finally:
481+
server.close()
482+
386483

387484
@patch('cassandra.connection.ConnectionHeartbeat._raise_if_stopped')
388485
class ConnectionHeartbeatTest(unittest.TestCase):

0 commit comments

Comments
 (0)