Skip to content

Commit 3ce67a2

Browse files
Extract leader-change error codes into dqlitewire.constants
Move the 10250/10506 magic numbers (SQLITE_IOERR_NOT_LEADER, SQLITE_IOERR_LEADERSHIP_LOST) into dqlitewire, where the rest of the protocol constants live, and re-export them from the package root. Downstream packages (dqliteclient, sqlalchemy-dqlite) can now import a single authoritative definition instead of duplicating the tuple. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent afbae2d commit 3ce67a2

3 files changed

Lines changed: 71 additions & 0 deletions

File tree

src/dqlitewire/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,16 @@
4141
from dqlitewire.buffer import ReadBuffer, WriteBuffer
4242
from dqlitewire.codec import MessageDecoder, MessageEncoder, decode_message, encode_message
4343
from dqlitewire.constants import (
44+
LEADER_ERROR_CODES,
4445
PROTOCOL_VERSION,
4546
PROTOCOL_VERSION_LEGACY,
4647
ROW_DONE_BYTE,
4748
ROW_DONE_MARKER,
4849
ROW_PART_BYTE,
4950
ROW_PART_MARKER,
51+
SQLITE_IOERR,
52+
SQLITE_IOERR_LEADERSHIP_LOST,
53+
SQLITE_IOERR_NOT_LEADER,
5054
NodeRole,
5155
RequestType,
5256
ResponseType,
@@ -68,6 +72,7 @@
6872
"DecodeError",
6973
"EncodeError",
7074
"HandshakeError",
75+
"LEADER_ERROR_CODES",
7176
"MessageDecoder",
7277
"MessageEncoder",
7378
"NodeRole",
@@ -82,6 +87,9 @@
8287
"ReadBuffer",
8388
"RequestType",
8489
"ResponseType",
90+
"SQLITE_IOERR",
91+
"SQLITE_IOERR_LEADERSHIP_LOST",
92+
"SQLITE_IOERR_NOT_LEADER",
8593
"ServerFailure",
8694
"StreamError",
8795
"ValueType",

src/dqlitewire/constants.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,20 @@ class NodeRole(IntEnum):
9797
VOTER = 0
9898
STANDBY = 1
9999
SPARE = 2
100+
101+
102+
# SQLite extended error codes that signal leader changes in a dqlite
103+
# cluster. Upstream definitions in ``dqlite-upstream/include/dqlite.h``:
104+
#
105+
# #define SQLITE_IOERR_NOT_LEADER (SQLITE_IOERR | (40 << 8))
106+
# #define SQLITE_IOERR_LEADERSHIP_LOST (SQLITE_IOERR | (41 << 8))
107+
#
108+
# where ``SQLITE_IOERR = 10``. Callers (``dqliteclient`` and
109+
# ``sqlalchemy-dqlite``) import these to decide whether to invalidate a
110+
# connection and retry against a fresh leader.
111+
SQLITE_IOERR = 10
112+
SQLITE_IOERR_NOT_LEADER = SQLITE_IOERR | (40 << 8) # 10250
113+
SQLITE_IOERR_LEADERSHIP_LOST = SQLITE_IOERR | (41 << 8) # 10506
114+
LEADER_ERROR_CODES: frozenset[int] = frozenset(
115+
{SQLITE_IOERR_NOT_LEADER, SQLITE_IOERR_LEADERSHIP_LOST}
116+
)

tests/test_constants.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,49 @@ def test_response_types_covers_all_enum_members(self) -> None:
175175
assert member.value in RESPONSE_TYPES, (
176176
f"ResponseType.{member.name} ({member.value}) has no entry in RESPONSE_TYPES"
177177
)
178+
179+
180+
class TestLeaderErrorCodes:
181+
"""Pin the SQLite extended error codes that signal leader changes."""
182+
183+
def test_sqlite_ioerr_base(self) -> None:
184+
from dqlitewire.constants import SQLITE_IOERR
185+
186+
assert SQLITE_IOERR == 10
187+
188+
def test_not_leader_code_value(self) -> None:
189+
from dqlitewire.constants import SQLITE_IOERR, SQLITE_IOERR_NOT_LEADER
190+
191+
assert SQLITE_IOERR_NOT_LEADER == 10250
192+
assert SQLITE_IOERR_NOT_LEADER == SQLITE_IOERR | (40 << 8)
193+
194+
def test_leadership_lost_code_value(self) -> None:
195+
from dqlitewire.constants import SQLITE_IOERR, SQLITE_IOERR_LEADERSHIP_LOST
196+
197+
assert SQLITE_IOERR_LEADERSHIP_LOST == 10506
198+
assert SQLITE_IOERR_LEADERSHIP_LOST == SQLITE_IOERR | (41 << 8)
199+
200+
def test_leader_error_codes_is_a_frozenset_of_both(self) -> None:
201+
from dqlitewire.constants import (
202+
LEADER_ERROR_CODES,
203+
SQLITE_IOERR_LEADERSHIP_LOST,
204+
SQLITE_IOERR_NOT_LEADER,
205+
)
206+
207+
assert isinstance(LEADER_ERROR_CODES, frozenset)
208+
expected = frozenset({SQLITE_IOERR_NOT_LEADER, SQLITE_IOERR_LEADERSHIP_LOST})
209+
assert expected == LEADER_ERROR_CODES
210+
211+
def test_leader_error_codes_importable_from_top_level(self) -> None:
212+
from dqlitewire import (
213+
LEADER_ERROR_CODES,
214+
SQLITE_IOERR,
215+
SQLITE_IOERR_LEADERSHIP_LOST,
216+
SQLITE_IOERR_NOT_LEADER,
217+
)
218+
219+
assert SQLITE_IOERR == 10
220+
assert SQLITE_IOERR_NOT_LEADER == 10250
221+
assert SQLITE_IOERR_LEADERSHIP_LOST == 10506
222+
expected = frozenset({10250, 10506})
223+
assert expected == LEADER_ERROR_CODES

0 commit comments

Comments
 (0)