Skip to content

Commit aa4f17c

Browse files
Add is_disconnect() to detect broken dqlite connections
The inherited SQLiteDialect.is_disconnect() only checks for pysqlite patterns. Add dqlite-specific detection for network errors like "Connection closed", "timed out", "Failed to connect", etc. so the pool can properly invalidate broken connections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 41a2b2f commit aa4f17c

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

src/sqlalchemydqlite/base.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,29 @@ def do_commit(self, dbapi_connection: DBAPIConnection) -> None:
9292
if "no transaction is active" not in str(e):
9393
raise
9494

95+
_dqlite_disconnect_messages = (
96+
"Connection closed",
97+
"timed out",
98+
"Failed to connect",
99+
"not connected",
100+
"Not connected",
101+
)
102+
103+
def is_disconnect(self, e: Any, connection: Any, cursor: Any) -> bool:
104+
"""Detect whether an exception indicates a broken connection.
105+
106+
dqlite is a network database, so we must detect TCP-level and
107+
leader-change errors that the inherited pysqlite patterns miss.
108+
"""
109+
import dqlitedbapi.exceptions
110+
111+
if isinstance(e, dqlitedbapi.exceptions.OperationalError):
112+
msg = str(e)
113+
for pattern in self._dqlite_disconnect_messages:
114+
if pattern in msg:
115+
return True
116+
return super().is_disconnect(e, connection, cursor)
117+
95118
def do_ping(self, dbapi_connection: Any) -> bool:
96119
"""Check if the connection is still alive."""
97120
try:

tests/test_dialect.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,54 @@ def test_async_dialect_returns_underlying_connection(self) -> None:
152152
)
153153

154154

155+
class TestIsDisconnect:
156+
def test_recognizes_connection_closed(self) -> None:
157+
"""is_disconnect should return True for connection-closed errors."""
158+
import dqlitedbapi.exceptions
159+
160+
dialect = DqliteDialect()
161+
e = dqlitedbapi.exceptions.OperationalError("Connection closed by server")
162+
assert dialect.is_disconnect(e, None, None) is True
163+
164+
def test_recognizes_failed_to_connect(self) -> None:
165+
"""is_disconnect should return True for connection-failure errors."""
166+
import dqlitedbapi.exceptions
167+
168+
dialect = DqliteDialect()
169+
e = dqlitedbapi.exceptions.OperationalError("Failed to connect: refused")
170+
assert dialect.is_disconnect(e, None, None) is True
171+
172+
def test_does_not_flag_normal_errors(self) -> None:
173+
"""is_disconnect should return False for normal operational errors."""
174+
import dqlitedbapi.exceptions
175+
176+
dialect = DqliteDialect()
177+
e = dqlitedbapi.exceptions.OperationalError("no such table: users")
178+
assert dialect.is_disconnect(e, None, None) is False
179+
180+
def test_recognizes_not_connected(self) -> None:
181+
"""is_disconnect should return True for 'not connected' errors."""
182+
import dqlitedbapi.exceptions
183+
184+
dialect = DqliteDialect()
185+
e = dqlitedbapi.exceptions.OperationalError("Not connected to database")
186+
assert dialect.is_disconnect(e, None, None) is True
187+
188+
def test_recognizes_timed_out(self) -> None:
189+
"""is_disconnect should return True for timeout errors."""
190+
import dqlitedbapi.exceptions
191+
192+
dialect = DqliteDialect()
193+
e = dqlitedbapi.exceptions.OperationalError("Connection timed out")
194+
assert dialect.is_disconnect(e, None, None) is True
195+
196+
def test_is_defined_on_dialect(self) -> None:
197+
"""DqliteDialect must define its own is_disconnect, not just inherit."""
198+
assert "is_disconnect" in DqliteDialect.__dict__, (
199+
"DqliteDialect must override is_disconnect"
200+
)
201+
202+
155203
class TestURLParsing:
156204
def test_parse_basic_url(self) -> None:
157205
url = URL.create("dqlite", host="localhost", port=9001, database="test")

0 commit comments

Comments
 (0)