Skip to content

Commit 790b502

Browse files
committed
stopping wrapping of driver exceptions as is maintenance issue
1 parent bc2584d commit 790b502

File tree

3 files changed

+144
-256
lines changed

3 files changed

+144
-256
lines changed

src/async_cassandra/session.py

Lines changed: 34 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,7 @@
1010
import time
1111
from typing import Any, Dict, Optional
1212

13-
from cassandra import (
14-
AlreadyExists,
15-
AuthenticationFailed,
16-
CDCWriteFailure,
17-
CoordinationFailure,
18-
FunctionFailure,
19-
InvalidRequest,
20-
OperationTimedOut,
21-
ReadFailure,
22-
ReadTimeout,
23-
Unauthorized,
24-
Unavailable,
25-
UnsupportedOperation,
26-
WriteFailure,
27-
WriteTimeout,
28-
)
29-
from cassandra.cluster import _NOT_SET, EXEC_PROFILE_DEFAULT, Cluster, NoHostAvailable, Session
30-
from cassandra.connection import ConnectionBusy, ConnectionShutdown, ProtocolError
31-
from cassandra.pool import NoConnectionsAvailable
32-
from cassandra.protocol import ErrorMessage
13+
from cassandra.cluster import _NOT_SET, EXEC_PROFILE_DEFAULT, Cluster, Session
3314
from cassandra.query import BatchStatement, PreparedStatement, SimpleStatement
3415

3516
from .base import AsyncContextManageable
@@ -193,46 +174,19 @@ async def execute(
193174
result_size = len(result.rows) if hasattr(result, "rows") else 0
194175
return result
195176

196-
except (
197-
# These exceptions should NOT be wrapped - pass through as-is
198-
ReadTimeout,
199-
WriteTimeout,
200-
OperationTimedOut,
201-
Unavailable,
202-
InvalidRequest,
203-
AlreadyExists,
204-
Unauthorized,
205-
AuthenticationFailed,
206-
ErrorMessage, # Base class for protocol-level errors like SyntaxException
207-
NoHostAvailable, # Not a DriverException but should pass through
208-
) as e:
209-
# Re-raise these specific Cassandra exceptions without wrapping
210-
error_type = type(e).__name__
211-
raise
212-
except (
213-
# These exceptions SHOULD be wrapped in QueryError
214-
UnsupportedOperation,
215-
ReadFailure,
216-
WriteFailure,
217-
FunctionFailure,
218-
CDCWriteFailure,
219-
CoordinationFailure,
220-
ProtocolError,
221-
ConnectionBusy,
222-
ConnectionShutdown,
223-
NoConnectionsAvailable,
224-
) as e:
225-
# Wrap these exceptions in QueryError
226-
error_type = type(e).__name__
227-
raise QueryError(f"Query execution failed: {str(e)}", cause=e) from e
228-
except asyncio.TimeoutError:
229-
# Re-raise timeout errors without wrapping
230-
error_type = "TimeoutError"
231-
raise
232177
except Exception as e:
233-
# Only wrap non-Cassandra exceptions
234178
error_type = type(e).__name__
235-
raise QueryError(f"Query execution failed: {str(e)}", cause=e) from e
179+
# Check if this is a Cassandra driver exception by looking at its module
180+
if (
181+
hasattr(e, "__module__")
182+
and (e.__module__ == "cassandra" or e.__module__.startswith("cassandra."))
183+
or isinstance(e, asyncio.TimeoutError)
184+
):
185+
# Pass through all Cassandra driver exceptions and asyncio.TimeoutError
186+
raise
187+
else:
188+
# Only wrap unexpected exceptions
189+
raise QueryError(f"Query execution failed: {str(e)}", cause=e) from e
236190
finally:
237191
# Record metrics in a fire-and-forget manner
238192
duration = time.perf_counter() - start_time
@@ -330,46 +284,19 @@ async def execute_stream(
330284
success = True
331285
return result
332286

333-
except (
334-
# These exceptions should NOT be wrapped - pass through as-is
335-
ReadTimeout,
336-
WriteTimeout,
337-
OperationTimedOut,
338-
Unavailable,
339-
InvalidRequest,
340-
AlreadyExists,
341-
Unauthorized,
342-
AuthenticationFailed,
343-
ErrorMessage, # Base class for protocol-level errors like SyntaxException
344-
NoHostAvailable, # Not a DriverException but should pass through
345-
) as e:
346-
# Re-raise these specific Cassandra exceptions without wrapping
347-
error_type = type(e).__name__
348-
raise
349-
except (
350-
# These exceptions SHOULD be wrapped in QueryError
351-
UnsupportedOperation,
352-
ReadFailure,
353-
WriteFailure,
354-
FunctionFailure,
355-
CDCWriteFailure,
356-
CoordinationFailure,
357-
ProtocolError,
358-
ConnectionBusy,
359-
ConnectionShutdown,
360-
NoConnectionsAvailable,
361-
) as e:
362-
# Wrap these exceptions in QueryError
363-
error_type = type(e).__name__
364-
raise QueryError(f"Streaming query execution failed: {str(e)}", cause=e) from e
365-
except asyncio.TimeoutError:
366-
# Re-raise timeout errors without wrapping
367-
error_type = "TimeoutError"
368-
raise
369287
except Exception as e:
370-
# Only wrap non-Cassandra exceptions
371288
error_type = type(e).__name__
372-
raise QueryError(f"Streaming query execution failed: {str(e)}") from e
289+
# Check if this is a Cassandra driver exception by looking at its module
290+
if (
291+
hasattr(e, "__module__")
292+
and (e.__module__ == "cassandra" or e.__module__.startswith("cassandra."))
293+
or isinstance(e, asyncio.TimeoutError)
294+
):
295+
# Pass through all Cassandra driver exceptions and asyncio.TimeoutError
296+
raise
297+
else:
298+
# Only wrap unexpected exceptions
299+
raise QueryError(f"Streaming query execution failed: {str(e)}", cause=e) from e
373300
finally:
374301
# Record metrics in a fire-and-forget manner
375302
duration = time.perf_counter() - start_time
@@ -462,38 +389,18 @@ async def prepare(
462389
)
463390

464391
return prepared
465-
except asyncio.TimeoutError:
466-
raise
467-
except (
468-
# These exceptions should NOT be wrapped - pass through as-is
469-
ReadTimeout,
470-
WriteTimeout,
471-
OperationTimedOut,
472-
Unavailable,
473-
InvalidRequest,
474-
AlreadyExists,
475-
ErrorMessage, # Base class for protocol-level errors like SyntaxException
476-
NoHostAvailable, # Not a DriverException but should pass through
477-
):
478-
# Re-raise these specific Cassandra exceptions without wrapping
479-
raise
480-
except (
481-
# These exceptions SHOULD be wrapped in QueryError
482-
UnsupportedOperation,
483-
ReadFailure,
484-
WriteFailure,
485-
FunctionFailure,
486-
CDCWriteFailure,
487-
CoordinationFailure,
488-
ProtocolError,
489-
ConnectionBusy,
490-
ConnectionShutdown,
491-
NoConnectionsAvailable,
492-
) as e:
493-
# Wrap these exceptions in QueryError
494-
raise QueryError(f"Statement preparation failed: {str(e)}", cause=e) from e
495392
except Exception as e:
496-
raise QueryError(f"Statement preparation failed: {str(e)}") from e
393+
# Check if this is a Cassandra driver exception by looking at its module
394+
if (
395+
hasattr(e, "__module__")
396+
and (e.__module__ == "cassandra" or e.__module__.startswith("cassandra."))
397+
or isinstance(e, asyncio.TimeoutError)
398+
):
399+
# Pass through all Cassandra driver exceptions and asyncio.TimeoutError
400+
raise
401+
else:
402+
# Only wrap unexpected exceptions
403+
raise QueryError(f"Statement preparation failed: {str(e)}", cause=e) from e
497404

498405
async def close(self) -> None:
499406
"""

tests/unit/test_protocol_edge_cases.py

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,9 @@ async def test_protocol_error_during_query(self, mock_session):
142142
What this tests:
143143
---------------
144144
1. Protocol errors during execution
145-
2. ProtocolError wrapped in QueryError
146-
3. Original exception preserved
147-
4. Error details maintained
145+
2. ProtocolError passed through without wrapping
146+
3. Direct exception access
147+
4. Error details preserved as-is
148148
149149
Why this matters:
150150
----------------
@@ -153,8 +153,8 @@ async def test_protocol_error_during_query(self, mock_session):
153153
- Protocol violations
154154
- Driver/server bugs
155155
156-
These are serious errors requiring
157-
investigation, not just retry.
156+
Users need direct access for
157+
proper error handling and debugging.
158158
"""
159159
async_session = AsyncCassandraSession(mock_session)
160160

@@ -163,12 +163,11 @@ async def test_protocol_error_during_query(self, mock_session):
163163
ProtocolError("Invalid or unsupported protocol version")
164164
)
165165

166-
# ProtocolError is wrapped in QueryError
167-
with pytest.raises(QueryError) as exc_info:
166+
# ProtocolError is now passed through without wrapping
167+
with pytest.raises(ProtocolError) as exc_info:
168168
await async_session.execute("SELECT * FROM test")
169169

170170
assert "Invalid or unsupported protocol version" in str(exc_info.value)
171-
assert isinstance(exc_info.value.__cause__, ProtocolError)
172171

173172
@pytest.mark.asyncio
174173
async def test_custom_payload_handling(self, mock_session):
@@ -262,10 +261,10 @@ async def test_unsupported_operation(self, mock_session):
262261
263262
What this tests:
264263
---------------
265-
1. UnsupportedOperation errors handled
266-
2. Wrapped in QueryError
267-
3. Feature limitations clear
268-
4. Version-specific features
264+
1. UnsupportedOperation errors passed through
265+
2. No wrapping - direct exception access
266+
3. Feature limitations clearly visible
267+
4. Version-specific features preserved
269268
270269
Why this matters:
271270
----------------
@@ -274,8 +273,8 @@ async def test_unsupported_operation(self, mock_session):
274273
- Duration type (v5+)
275274
- Per-query keyspace (v5+)
276275
277-
Clear errors help users understand
278-
feature availability.
276+
Users need direct access to handle
277+
version-specific feature errors.
279278
"""
280279
async_session = AsyncCassandraSession(mock_session)
281280

@@ -284,12 +283,11 @@ async def test_unsupported_operation(self, mock_session):
284283
UnsupportedOperation("Continuous paging is not supported by this protocol version")
285284
)
286285

287-
# UnsupportedOperation is wrapped in QueryError
288-
with pytest.raises(QueryError) as exc_info:
286+
# UnsupportedOperation is now passed through without wrapping
287+
with pytest.raises(UnsupportedOperation) as exc_info:
289288
await async_session.execute("SELECT * FROM test")
290289

291290
assert "Continuous paging is not supported" in str(exc_info.value)
292-
assert isinstance(exc_info.value.__cause__, UnsupportedOperation)
293291

294292
@pytest.mark.asyncio
295293
async def test_protocol_error_recovery(self, mock_session):
@@ -414,10 +412,9 @@ async def test_timeout_vs_protocol_error(self, mock_session):
414412
ProtocolError("Protocol violation")
415413
)
416414

417-
# ProtocolError is wrapped in QueryError
418-
with pytest.raises(QueryError) as exc_info:
415+
# ProtocolError is now passed through without wrapping
416+
with pytest.raises(ProtocolError):
419417
await async_session.execute("SELECT * FROM test")
420-
assert isinstance(exc_info.value.__cause__, ProtocolError)
421418

422419
@pytest.mark.asyncio
423420
async def test_prepare_with_protocol_error(self, mock_session):
@@ -427,9 +424,9 @@ async def test_prepare_with_protocol_error(self, mock_session):
427424
What this tests:
428425
---------------
429426
1. Prepare can fail with protocol error
430-
2. Wrapped in QueryError
431-
3. Statement preparation issues
432-
4. Error details preserved
427+
2. Passed through without wrapping
428+
3. Statement preparation issues visible
429+
4. Direct exception access
433430
434431
Why this matters:
435432
----------------
@@ -438,20 +435,19 @@ async def test_prepare_with_protocol_error(self, mock_session):
438435
- Protocol limitations
439436
- Query complexity problems
440437
441-
Clear errors help debug
442-
statement preparation issues.
438+
Users need direct access to
439+
handle preparation failures.
443440
"""
444441
async_session = AsyncCassandraSession(mock_session)
445442

446443
# Prepare fails with protocol error
447444
mock_session.prepare.side_effect = ProtocolError("Cannot prepare statement")
448445

449-
# Should be wrapped in QueryError
450-
with pytest.raises(QueryError) as exc_info:
446+
# ProtocolError is now passed through without wrapping
447+
with pytest.raises(ProtocolError) as exc_info:
451448
await async_session.prepare("SELECT * FROM test WHERE id = ?")
452449

453450
assert "Cannot prepare statement" in str(exc_info.value)
454-
assert isinstance(exc_info.value.__cause__, ProtocolError)
455451

456452
@pytest.mark.asyncio
457453
async def test_execution_profile_with_protocol_settings(self, mock_session):
@@ -499,9 +495,9 @@ async def test_batch_with_protocol_error(self, mock_session):
499495
What this tests:
500496
---------------
501497
1. Batch operations can hit protocol limits
502-
2. Protocol errors wrapped appropriately
503-
3. Batch size limits enforced
504-
4. Clear error messaging
498+
2. Protocol errors passed through directly
499+
3. Batch size limits visible to users
500+
4. Native exception handling
505501
506502
Why this matters:
507503
----------------
@@ -510,8 +506,8 @@ async def test_batch_with_protocol_error(self, mock_session):
510506
- Statement count limits
511507
- Protocol buffer constraints
512508
513-
Large batches must be split
514-
to avoid protocol errors.
509+
Users need direct access to
510+
handle batch size errors.
515511
"""
516512
from cassandra.query import BatchStatement, BatchType
517513

@@ -527,12 +523,11 @@ async def test_batch_with_protocol_error(self, mock_session):
527523
ProtocolError("Batch too large for protocol")
528524
)
529525

530-
# Should be wrapped in QueryError
531-
with pytest.raises(QueryError) as exc_info:
526+
# ProtocolError is now passed through without wrapping
527+
with pytest.raises(ProtocolError) as exc_info:
532528
await async_session.execute_batch(batch)
533529

534530
assert "Batch too large" in str(exc_info.value)
535-
assert isinstance(exc_info.value.__cause__, ProtocolError)
536531

537532
@pytest.mark.asyncio
538533
async def test_no_host_available_with_protocol_errors(self, mock_session):

0 commit comments

Comments
 (0)