3737from cassandra .connection import ConnectionShutdown , DefaultEndPoint
3838from cassandra .cluster import NoHostAvailable
3939
40- try :
41- from cassandra .io .asyncorereactor import AsyncoreConnection
42- except ImportError :
43- AsyncoreConnection = None
44-
4540try :
4641 from cassandra .io .asyncioreactor import AsyncioConnection
4742except ImportError :
4843 AsyncioConnection = None
4944
5045
51- @unittest .skipIf (AsyncoreConnection is None , "asyncore reactor not available" )
52- class AsyncoreSSLConnectionFailureTest (unittest .TestCase ):
53- """
54- Test SSL connection failures with AsyncoreConnection.
55-
56- These tests simulate connection failures that can occur in production,
57- particularly when nodes are rebooted or network issues occur.
58- """
59-
60- def test_socket_closed_forcefully_during_send (self ):
61- """
62- Test that forcefully closing a socket during send preserves error info.
63-
64- Simulates: Node reboot causing socket to be closed while sending data
65- """
66- # Create a connection
67- # Note: Using CERT_NONE for testing only - this is intentionally insecure
68- conn = AsyncoreConnection (
69- DefaultEndPoint ('127.0.0.1' , 9999 ),
70- ssl_options = {'cert_reqs' : ssl .CERT_NONE }
71- )
72-
73- # Create a mock socket that will fail on send
74- mock_socket = Mock (spec = ssl .SSLSocket )
75- bad_fd_error = OSError (errno .EBADF , "Bad file descriptor" )
76- mock_socket .send .side_effect = bad_fd_error
77- mock_socket .fileno .return_value = 999
78-
79- conn ._socket = mock_socket
80-
81- # Try to trigger defunct by simulating a send error
82- conn .defunct (bad_fd_error )
83-
84- # Verify the error is preserved
85- self .assertTrue (conn .is_defunct )
86- self .assertEqual (conn .last_error , bad_fd_error )
87- self .assertIn ("Bad file descriptor" , str (conn .last_error ))
88-
89- def test_connection_reset_during_recv (self ):
90- """
91- Test handling of connection reset during receive operation.
92-
93- Simulates: Node reboot causing connection reset while reading response
94- """
95- # Note: Using CERT_NONE for testing only - this is intentionally insecure
96- conn = AsyncoreConnection (
97- DefaultEndPoint ('127.0.0.1' , 9999 ),
98- ssl_options = {'cert_reqs' : ssl .CERT_NONE }
99- )
100-
101- # Create a mock socket that will fail on recv
102- mock_socket = Mock (spec = ssl .SSLSocket )
103- conn_reset_error = OSError (errno .ECONNRESET , "Connection reset by peer" )
104- mock_socket .recv .side_effect = conn_reset_error
105- mock_socket .fileno .return_value = 999
106-
107- conn ._socket = mock_socket
108- conn .defunct (conn_reset_error )
109-
110- # Verify the error is preserved
111- self .assertTrue (conn .is_defunct )
112- self .assertEqual (conn .last_error , conn_reset_error )
113- self .assertIn ("Connection reset by peer" , str (conn .last_error ))
114-
115- def test_ssl_handshake_failure (self ):
116- """
117- Test SSL handshake failure is properly captured and reported.
118-
119- Simulates: SSL handshake failure due to connection issues
120- """
121- conn = AsyncoreConnection (
122- DefaultEndPoint ('127.0.0.1' , 9999 ),
123- ssl_options = {'cert_reqs' : ssl .CERT_REQUIRED }
124- )
125-
126- # Simulate SSL error
127- ssl_error = ssl .SSLError (ssl .SSL_ERROR_SYSCALL , "Unexpected EOF" )
128- conn .defunct (ssl_error )
129-
130- # Verify the error is preserved
131- self .assertTrue (conn .is_defunct )
132- self .assertEqual (conn .last_error , ssl_error )
133- self .assertIn ("Unexpected EOF" , str (conn .last_error ))
134-
135- def test_broken_pipe_on_ssl_socket (self ):
136- """
137- Test handling of broken pipe error on SSL socket.
138-
139- Simulates: Write to socket whose peer has closed connection
140- """
141- # Note: Using CERT_NONE for testing only - this is intentionally insecure
142- conn = AsyncoreConnection (
143- DefaultEndPoint ('127.0.0.1' , 9999 ),
144- ssl_options = {'cert_reqs' : ssl .CERT_NONE }
145- )
146-
147- broken_pipe_error = OSError (errno .EPIPE , "Broken pipe" )
148- conn .defunct (broken_pipe_error )
149-
150- # Verify the error is preserved and accessible
151- self .assertTrue (conn .is_defunct )
152- self .assertEqual (conn .last_error , broken_pipe_error )
153-
154- # Verify that subsequent operations include the error
155- with self .assertRaises (ConnectionShutdown ) as cm :
156- conn .send_msg (Mock (), 1 , Mock ())
157-
158- self .assertIn ("Broken pipe" , str (cm .exception ))
159-
160- def test_concurrent_operations_on_closing_ssl_connection (self ):
161- """
162- Test concurrent operations when SSL connection is being closed.
163-
164- Simulates: Multiple threads operating on connection during node reboot
165- This is the core scenario from the reported issue.
166- """
167- # Note: Using CERT_NONE for testing only - this is intentionally insecure
168- conn = AsyncoreConnection (
169- DefaultEndPoint ('127.0.0.1' , 9999 ),
170- ssl_options = {'cert_reqs' : ssl .CERT_NONE }
171- )
172-
173- # Simulate the connection having some requests
174- conn ._requests = {
175- 1 : (Mock (), Mock (), Mock ()),
176- 2 : (Mock (), Mock (), Mock ()),
177- }
178-
179- # Original error that triggers closure
180- original_error = OSError (errno .ECONNRESET , "Connection reset by peer" )
181-
182- # Mark as defunct
183- conn .defunct (original_error )
184-
185- # Verify error is preserved
186- self .assertTrue (conn .is_defunct )
187- self .assertEqual (conn .last_error , original_error )
188-
189- # Concurrent thread tries to send - should see original error
190- with self .assertRaises (ConnectionShutdown ) as cm :
191- conn .send_msg (Mock (), 3 , Mock ())
192-
193- error_msg = str (cm .exception )
194- self .assertIn ("Connection reset by peer" , error_msg )
195-
196- # Another thread tries to wait for responses - should also see original error
197- with self .assertRaises (ConnectionShutdown ) as cm :
198- conn .wait_for_responses (Mock ())
199-
200- error_msg = str (cm .exception )
201- self .assertIn ("Connection reset by peer" , error_msg )
202-
203-
20446@unittest .skipIf (AsyncioConnection is None , "asyncio reactor not available" )
20547class AsyncioSSLConnectionFailureTest (unittest .TestCase ):
20648 """
@@ -264,20 +106,14 @@ def test_error_message_includes_root_cause(self):
264106 Verify that ConnectionShutdown messages include the root cause error.
265107 """
266108 # Note: Using CERT_NONE for testing only - this is intentionally insecure
267- # This can work with any connection type
268- if AsyncoreConnection :
269- conn = AsyncoreConnection (
270- DefaultEndPoint ('127.0.0.1' , 9999 ),
271- ssl_options = {'cert_reqs' : ssl .CERT_NONE }
272- )
273- elif AsyncioConnection :
274- conn = AsyncioConnection (
275- DefaultEndPoint ('127.0.0.1' , 9999 ),
276- ssl_options = {'cert_reqs' : ssl .CERT_NONE }
277- )
278- else :
109+ if AsyncioConnection is None :
279110 self .skipTest ("No connection implementation available" )
280111
112+ conn = AsyncioConnection (
113+ DefaultEndPoint ('127.0.0.1' , 9999 ),
114+ ssl_options = {'cert_reqs' : ssl .CERT_NONE }
115+ )
116+
281117 # Simulate root cause
282118 root_cause = OSError (errno .ECONNRESET , "Connection reset by peer" )
283119 conn .defunct (root_cause )
@@ -297,19 +133,14 @@ def test_multiple_errors_preserves_first(self):
297133 Verify that when multiple errors occur, the first (root cause) is preserved.
298134 """
299135 # Note: Using CERT_NONE for testing only - this is intentionally insecure
300- if AsyncoreConnection :
301- conn = AsyncoreConnection (
302- DefaultEndPoint ('127.0.0.1' , 9999 ),
303- ssl_options = {'cert_reqs' : ssl .CERT_NONE }
304- )
305- elif AsyncioConnection :
306- conn = AsyncioConnection (
307- DefaultEndPoint ('127.0.0.1' , 9999 ),
308- ssl_options = {'cert_reqs' : ssl .CERT_NONE }
309- )
310- else :
136+ if AsyncioConnection is None :
311137 self .skipTest ("No connection implementation available" )
312138
139+ conn = AsyncioConnection (
140+ DefaultEndPoint ('127.0.0.1' , 9999 ),
141+ ssl_options = {'cert_reqs' : ssl .CERT_NONE }
142+ )
143+
313144 # First error - the root cause
314145 root_cause = OSError (errno .ETIMEDOUT , "Connection timed out" )
315146 conn .defunct (root_cause )
0 commit comments