Skip to content

Commit 3474cce

Browse files
Classify InterfaceError("Connection is closed") as a disconnect
After the underlying DBAPI connection is closed out of band (pool invalidate, cluster membership change, explicit close), the next statement raises dqlitedbapi.InterfaceError("Connection is closed"). is_disconnect was only testing OperationalError subtypes, so the pool did NOT invalidate the stale slot and the error kept surfacing on every check-out. Add a narrow InterfaceError branch keyed on the "connection is closed" / "cursor is closed" messages — programming- error InterfaceErrors (e.g. arraysize validation) stay unclassified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ef47f8b commit 3474cce

2 files changed

Lines changed: 39 additions & 0 deletions

File tree

src/sqlalchemydqlite/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,16 @@ def is_disconnect(self, e: Any, connection: Any, cursor: Any) -> bool:
296296
# DNS, connect refused) surface as these stdlib types.
297297
if isinstance(e, (ConnectionError, BrokenPipeError, TimeoutError, OSError)):
298298
return True
299+
# ``dqlitedbapi.Connection`` / ``Cursor`` raise ``InterfaceError``
300+
# when operated on after ``close()``; match the narrow
301+
# "closed" substring so programming-error InterfaceErrors (e.g.
302+
# setinputsizes on a closed cursor) are NOT classified as
303+
# disconnect. The do_ping path already catches InterfaceError
304+
# for the same reason.
305+
if isinstance(e, _dbapi_exc.InterfaceError):
306+
message = str(e).lower()
307+
if "connection is closed" in message or "cursor is closed" in message:
308+
return True
299309
# Leader-change error codes signal that the connection is useless
300310
# even though it's TCP-alive.
301311
for err in (_dbapi_exc.OperationalError, _client_exc.OperationalError):

tests/test_dialect.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,35 @@ def test_is_defined_on_dialect(self) -> None:
319319
"DqliteDialect must override is_disconnect"
320320
)
321321

322+
def test_recognizes_interface_error_connection_closed(self) -> None:
323+
"""An InterfaceError raised after the underlying DBAPI connection
324+
was closed (e.g. pool invalidate, cluster membership change)
325+
must be classified as disconnect so the pool recycles the slot.
326+
"""
327+
import dqlitedbapi.exceptions
328+
329+
dialect = DqliteDialect()
330+
e = dqlitedbapi.exceptions.InterfaceError("Connection is closed")
331+
assert dialect.is_disconnect(e, None, None) is True
332+
333+
def test_recognizes_interface_error_cursor_closed(self) -> None:
334+
import dqlitedbapi.exceptions
335+
336+
dialect = DqliteDialect()
337+
e = dqlitedbapi.exceptions.InterfaceError("Cursor is closed")
338+
assert dialect.is_disconnect(e, None, None) is True
339+
340+
def test_does_not_flag_other_interface_errors(self) -> None:
341+
"""Narrowly-worded programming-error InterfaceErrors (e.g.
342+
`arraysize must be positive`) must NOT route through the
343+
disconnect path.
344+
"""
345+
import dqlitedbapi.exceptions
346+
347+
dialect = DqliteDialect()
348+
e = dqlitedbapi.exceptions.InterfaceError("arraysize must be positive")
349+
assert dialect.is_disconnect(e, None, None) is False
350+
322351

323352
class TestIsolationLevel:
324353
def test_set_isolation_level_warns_on_unsupported(self) -> None:

0 commit comments

Comments
 (0)