Skip to content

Commit ee92105

Browse files
Strip internal issue-tracker references from tests and comments
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6ba6754 commit ee92105

21 files changed

Lines changed: 138 additions & 146 deletions

src/dqlitewire/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
# C-level atomicity for bytearray.extend(), slicing, and attribute rebinding
1010
# inside _maybe_compact(). Under a free-threaded build, concurrent access
1111
# produces SIGSEGV in the read path and a process-level hang in the write
12-
# path (see issues/033-free-threaded-segfault-in-readbuffer.md for the full
13-
# reproduction and rationale). The single-owner-per-instance contract alone
12+
# path. The single-owner-per-instance contract alone
1413
# is not enough to keep users safe, because "share across threads" is an
1514
# easy mistake and the failure mode is an interpreter crash rather than a
1615
# Python exception.
@@ -24,8 +23,7 @@
2423
"dqlitewire does not support free-threaded Python "
2524
"(python3.13t / no-GIL). The ReadBuffer and WriteBuffer classes "
2625
"rely on bytearray mutation semantics that cause SIGSEGV and "
27-
"process hangs under free-threading. See "
28-
"issues/033-free-threaded-segfault-in-readbuffer.md. "
26+
"process hangs under free-threading. "
2927
"To override at your own risk, set "
3028
"DQLITEWIRE_ALLOW_FREE_THREADED=1."
3129
)

src/dqlitewire/buffer.py

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ class WriteBuffer:
1212
Thread-safety: NOT thread-safe. A single ``WriteBuffer``
1313
instance must be owned by one thread (or one asyncio
1414
coroutine) at a time. The single-owner contract matches Go's
15-
``driver.Conn`` layer in go-dqlite; see issue 021 for the full
16-
analysis. ``write_padded`` guards against the specific
17-
torn-payload/pad interleave described in issue 034, but that is
18-
a narrow defense, not a general thread-safety guarantee.
15+
``driver.Conn`` layer in go-dqlite. ``write_padded`` guards
16+
against a specific torn-payload/pad interleave, but that is a
17+
narrow defense, not a general thread-safety guarantee.
1918
"""
2019

2120
def __reduce__(self) -> None: # type: ignore[override]
@@ -36,8 +35,8 @@ def write_padded(self, data: bytes) -> None:
3635
3736
The padded bytes are built locally and emitted via a single
3837
``bytearray.extend`` so that under accidental concurrent misuse
39-
(see issue 021 for the single-owner contract) two threads'
40-
payloads and padding cannot interleave. This still does not make
38+
two threads' payloads and padding cannot interleave. This
39+
still does not make
4140
``WriteBuffer`` thread-safe in any strong sense, but it removes
4241
the torn payload/pad split that used to be visible to callers.
4342
"""
@@ -67,7 +66,7 @@ class ReadBuffer:
6766
Thread-safety: NOT thread-safe. A single ``ReadBuffer`` instance
6867
must be owned by one thread (or one asyncio coroutine) at a
6968
time. The single-owner contract matches Go's ``driver.Conn``
70-
layer in go-dqlite; see issue 021 for the full analysis.
69+
layer in go-dqlite.
7170
7271
Concurrent misuse from multiple threads produces **silent data
7372
corruption**, not exceptions. Specifically:
@@ -81,10 +80,9 @@ class ReadBuffer:
8180
8281
The ``poison()`` mechanism does NOT and CANNOT detect this
8382
class of failure. Poison is designed to catch single-owner
84-
torn state from interrupted signal delivery (see issues 037,
85-
041, 045). It cannot observe lost-update races or torn reads
86-
that produce valid-looking output. See issue 050 for
87-
reproduction details.
83+
torn state from interrupted signal delivery. It cannot observe
84+
lost-update races or torn reads that produce valid-looking
85+
output.
8886
8987
If you need concurrent access to a single wire stream, wrap
9088
every call site in an ``asyncio.Lock`` (for coroutines) or
@@ -167,7 +165,7 @@ def feed(self, data: bytes) -> None:
167165
``reset()``/``clear()``) if the resulting buffer size would
168166
exceed ``max_message_size``.
169167
170-
Signal-safety note (issue 048): the mutation block below is
168+
Signal-safety note: the mutation block below is
171169
wrapped in ``try/except BaseException`` so that any async
172170
exception leaking out — most notably between the
173171
``_maybe_compact()`` return and the subsequent
@@ -235,7 +233,7 @@ def has_message(self) -> bool:
235233

236234
# Read size from header (first 4 bytes = size in words)
237235
size_words = int.from_bytes(self._data[self._pos : self._pos + 4], "little")
238-
# Torn-read sanity (issue 051): if the slice was widened by
236+
# Torn-read sanity: if the slice was widened by
239237
# a concurrent realloc, report "something to consume" so the
240238
# caller's while-loop proceeds to read_message(), which then
241239
# poisons. has_message() itself is a total predicate and
@@ -280,7 +278,7 @@ def peek_header(self) -> tuple[int, int, int] | None:
280278
self._check_torn_size(size_words)
281279
total_size = HEADER_SIZE + (size_words * WORD_SIZE)
282280
if total_size > self._max_message_size:
283-
# Format size in hex: under concurrent misuse (see issue 033)
281+
# Format size in hex: under concurrent misuse
284282
# `total_size` can be a torn bigint whose decimal form exceeds
285283
# CPython's 4300-digit int-to-str limit, which would make this
286284
# f-string itself raise ValueError. Hex formatting has no cap.
@@ -311,7 +309,7 @@ def read_message(self) -> bytes | None:
311309
total_size = HEADER_SIZE + (size_words * WORD_SIZE)
312310

313311
if total_size > self._max_message_size:
314-
# Format size in hex: under concurrent misuse (see issue 033)
312+
# Format size in hex: under concurrent misuse
315313
# `total_size` can be a torn bigint whose decimal form exceeds
316314
# CPython's 4300-digit int-to-str limit, which would make this
317315
# f-string itself raise ValueError. Hex formatting has no cap.
@@ -458,7 +456,7 @@ def peek_bytes(self, n: int) -> bytes | None:
458456
def _maybe_compact(self) -> None:
459457
"""Compact buffer if we've consumed a lot.
460458
461-
Signal-safety note (issue 037): this method mutates two
459+
Signal-safety note: this method mutates two
462460
attributes — ``_data`` and ``_pos`` — which compile to two
463461
``STORE_ATTR`` bytecodes. CPython checks for pending signals
464462
at bytecode line transitions, so a ``KeyboardInterrupt`` (or
@@ -507,11 +505,11 @@ def clear(self) -> None:
507505
"""Clear buffer state and un-poison.
508506
509507
Equivalent to ``reset()``. Kept as a convenience alias because
510-
``clear()`` predates the poison concept (issue 026) and was
511-
briefly inconsistent with ``reset()`` — it used to leave the
508+
``clear()`` predates the poison concept and was briefly
509+
inconsistent with ``reset()`` — it used to leave the
512510
``_poisoned`` flag intact, which meant a caller who reached
513511
for ``clear()`` as a recovery primitive got a half-fresh
514512
buffer that still raised ``ProtocolError`` on the next
515-
operation (issue 040).
513+
operation.
516514
"""
517515
self.reset()

src/dqlitewire/codec.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,7 @@ class MessageEncoder:
113113
at a time. The encoder is effectively stateless after
114114
construction (it only caches a protocol ``_version``), but the
115115
single-owner contract matches the rest of the package — see
116-
issue 021 and the class docstring on ``MessageDecoder`` /
117-
``ReadBuffer``.
116+
the class docstring on ``MessageDecoder`` / ``ReadBuffer``.
118117
"""
119118

120119
def __reduce__(self) -> None: # type: ignore[override]
@@ -156,24 +155,23 @@ class MessageDecoder:
156155
Thread-safety: NOT thread-safe. A single ``MessageDecoder``
157156
instance must be owned by one thread (or one asyncio coroutine)
158157
at a time. The single-owner contract matches Go's
159-
``driver.Conn`` layer in go-dqlite; see issue 021 for the full
160-
analysis.
158+
``driver.Conn`` layer in go-dqlite.
161159
162160
Concurrent misuse from multiple threads produces **silent data
163161
corruption**, not exceptions. The underlying ``ReadBuffer``
164162
suffers from lost-update races on ``_pos`` and torn
165163
``_data``/``_pos`` snapshots across ``_maybe_compact()``
166164
calls; these produce valid-looking byte slices that decode
167165
cleanly to wrong (or duplicated) messages. Fuzz testing
168-
(issue 050) confirms this reliably on every trial.
166+
confirms this reliably on every trial.
169167
170168
The ``is_poisoned`` flag does NOT detect concurrent misuse.
171169
Poison is designed to catch single-owner torn state from
172-
interrupted signal delivery (see issues 037, 041, 045). It
173-
cannot observe lost-update races or torn reads that produce
174-
valid-looking output. If you need concurrent access, wrap every
175-
call site in an ``asyncio.Lock`` or ``threading.Lock`` at the
176-
layer that owns the socket.
170+
interrupted signal delivery. It cannot observe lost-update
171+
races or torn reads that produce valid-looking output. If you
172+
need concurrent access, wrap every call site in an
173+
``asyncio.Lock`` or ``threading.Lock`` at the layer that owns
174+
the socket.
177175
"""
178176

179177
def __reduce__(self) -> None: # type: ignore[override]
@@ -389,8 +387,7 @@ def decode(self) -> Message | None:
389387
# Bytes have been consumed. ANY failure now leaves the buffer
390388
# at an unknown offset; poison so subsequent calls fail fast.
391389
# Catch BaseException so that signal-delivered
392-
# KeyboardInterrupt (issue 045) also poisons before
393-
# propagating. `decode_body` implementations can raise
390+
# KeyboardInterrupt also poisons before propagating. `decode_body` implementations can raise
394391
# struct.error, ValueError, UnicodeDecodeError, IndexError,
395392
# etc., and all of them mean the stream is desynchronized.
396393
try:
@@ -401,7 +398,7 @@ def decode(self) -> Message | None:
401398
# between the successful decode and the flag store would
402399
# otherwise leave the stream desynchronized without
403400
# poisoning — the next ``decode()`` would mis-frame the
404-
# continuation frame as a top-level message (issue 233).
401+
# continuation frame as a top-level message.
405402
if isinstance(msg, RowsResponse) and msg.has_more:
406403
self._continuation_expected = True
407404
except BaseException as e:
@@ -480,7 +477,7 @@ def decode_handshake(self) -> int | None:
480477
buffer untouched so that a retry is deterministic (same bytes, same
481478
error) rather than silently consuming the next 8 bytes of real data.
482479
483-
Signal-safety (issue 041): the commit order is
480+
Signal-safety: the commit order is
484481
``_version``/``_handshake_done`` FIRST, then ``read_bytes(8)``.
485482
If an async exception (``KeyboardInterrupt``) lands between the
486483
state commit and the buffer consume, the except block reverts

src/dqlitewire/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class ServerFailure(ProtocolError):
7171
state is disturbed. This exception is raised only from
7272
``decode_continuation()`` when FAILURE arrives mid-stream during
7373
a multi-part ``RowsResponse``. In that path the buffer is also
74-
poisoned (see issue 083): the remaining continuation frames the
74+
poisoned: the remaining continuation frames the
7575
caller was expecting will never arrive, and the next decode must
7676
start fresh. Callers catching ``ServerFailure`` in that path
7777
should treat the decoder as requiring ``reset()`` and reconnect.

src/dqlitewire/messages/responses.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ class RowsResponse(Message):
246246
has_more: bool = False
247247

248248
def __post_init__(self) -> None:
249-
# Defensive copies (issue 042, ISSUE-61). Two sources of
249+
# Defensive copies. Two sources of
250250
# aliasing motivate this:
251251
#
252252
# 1. ``decode_body`` stores ``column_types = types`` where
@@ -273,11 +273,11 @@ def _get_row_types(self, row_idx: int, row: list[Any]) -> list[ValueType]:
273273
``self.column_types`` itself, so that a caller who mutates the
274274
return value cannot silently rewrite the message's private
275275
copy. This preserves the aliasing invariant that
276-
``__post_init__`` establishes (issue 042, issue 052).
276+
``__post_init__`` establishes.
277277
278278
None values override the declared type to NULL, matching Go's
279279
per-row type header behavior where the nibble reflects the actual
280-
value, not the column schema (issue 137).
280+
value, not the column schema.
281281
"""
282282
if self.row_types and row_idx < len(self.row_types):
283283
types = list(self.row_types[row_idx])
@@ -334,7 +334,7 @@ def decode_body(
334334
) -> "RowsResponse":
335335
# Wrap in memoryview so per-iteration slices are O(1) rather
336336
# than O(remaining). Without this, a body with many small rows
337-
# triggers quadratic-time decode (issue 228): each
337+
# triggers quadratic-time decode: each
338338
# ``data[offset:]`` allocates a fresh ``bytes`` copy of the
339339
# tail. Memoryview slicing is a view, so slicing is free.
340340
view = memoryview(data)
@@ -475,7 +475,7 @@ def encode_body(self) -> bytes:
475475
# entries are written back-to-back with no explicit padding
476476
# and SQLite pages are always 8-byte aligned multiples of
477477
# 512. Validate here so a Python-encoded mock-server frame
478-
# cannot diverge from what a real C peer produces (ISSUE-59).
478+
# cannot diverge from what a real C peer produces.
479479
if len(content) % 8 != 0:
480480
raise EncodeError(
481481
f"FilesResponse content for {name!r} must be 8-byte aligned "
@@ -489,7 +489,7 @@ def encode_body(self) -> bytes:
489489

490490
@classmethod
491491
def decode_body(cls, data: bytes, schema: int = 0) -> "FilesResponse":
492-
# Memoryview for O(1) slicing in the per-file loop (issue 228).
492+
# Memoryview for O(1) slicing in the per-file loop.
493493
view = memoryview(data)
494494
files: dict[str, bytes] = {}
495495
offset = 0
@@ -551,7 +551,7 @@ def encode_body(self) -> bytes:
551551

552552
@classmethod
553553
def decode_body(cls, data: bytes, schema: int = 0) -> "ServersResponse":
554-
# Memoryview for O(1) slicing in the per-node loop (issue 228).
554+
# Memoryview for O(1) slicing in the per-node loop.
555555
view = memoryview(data)
556556
nodes: list[NodeInfo] = []
557557
offset = 0

src/dqlitewire/tuples.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
# Full 8-byte sentinels matching DQLITE_RESPONSE_ROWS_DONE/PART. Used to
2121
# reject torn/corrupt row markers instead of accepting any 8 bytes that
22-
# happen to start with 0xff/0xee (ISSUE-63).
22+
# happen to start with 0xff/0xee.
2323
_ROW_DONE_MARKER = bytes([ROW_DONE_BYTE]) * 8
2424
_ROW_PART_MARKER = bytes([ROW_PART_BYTE]) * 8
2525

@@ -236,7 +236,7 @@ def decode_row_header(
236236
# = 0xff..ff, _PART = 0xee..ee). Go's reference client checks only the
237237
# first byte; we validate all 8 bytes so torn/corrupt markers like
238238
# ``0xff 0x00..`` are rejected as malformed rather than silently
239-
# truncating the result stream (ISSUE-63). This is strictly tighter
239+
# truncating the result stream. This is strictly tighter
240240
# than the Go behavior.
241241
if len(data) >= 8:
242242
marker = bytes(data[:8]) if isinstance(data, memoryview) else data[:8]

src/dqlitewire/types.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def decode_uint64(data: bytes | memoryview) -> int:
2828
"""Decode an unsigned 64-bit integer (little-endian).
2929
3030
Accepts ``bytes`` or ``memoryview`` so hot-path body decoders
31-
(issue 228) can pass memoryview slices without copying.
31+
can pass memoryview slices without copying.
3232
"""
3333
if len(data) < 8:
3434
raise DecodeError(f"Need 8 bytes for uint64, got {len(data)}")
@@ -46,7 +46,7 @@ def encode_int64(value: int) -> bytes:
4646
def decode_int64(data: bytes | memoryview) -> int:
4747
"""Decode a signed 64-bit integer (little-endian).
4848
49-
Accepts ``bytes`` or ``memoryview`` (issue 228).
49+
Accepts ``bytes`` or ``memoryview``.
5050
"""
5151
if len(data) < 8:
5252
raise DecodeError(f"Need 8 bytes for int64, got {len(data)}")
@@ -64,7 +64,7 @@ def encode_uint32(value: int) -> bytes:
6464
def decode_uint32(data: bytes | memoryview) -> int:
6565
"""Decode an unsigned 32-bit integer (little-endian).
6666
67-
Accepts ``bytes`` or ``memoryview`` (issue 228).
67+
Accepts ``bytes`` or ``memoryview``.
6868
"""
6969
if len(data) < 4:
7070
raise DecodeError(f"Need 4 bytes for uint32, got {len(data)}")
@@ -86,7 +86,7 @@ def decode_double(data: bytes | memoryview) -> float:
8686
8787
All IEEE 754 values are accepted, including NaN and infinity,
8888
matching the Go reference implementation behavior. Accepts
89-
``bytes`` or ``memoryview`` (issue 228).
89+
``bytes`` or ``memoryview``.
9090
"""
9191
if len(data) < 8:
9292
raise DecodeError(f"Need 8 bytes for double, got {len(data)}")
@@ -122,7 +122,7 @@ def encode_text(value: str) -> bytes:
122122
# Threshold below which we materialize a memoryview to bytes in one
123123
# shot (one allocation + one ``bytes.find``) instead of the chunked
124124
# scan. Row text payloads are almost always well under 64 KiB, so the
125-
# one-shot path dominates the common case (ISSUE-65). Above the
125+
# one-shot path dominates the common case. Above the
126126
# threshold we fall back to chunked scanning to bound peak memory for
127127
# pathologically long texts.
128128
_TEXT_ONE_SHOT_MAX = 65_536
@@ -137,14 +137,14 @@ def decode_text(data: bytes | memoryview) -> tuple[str, int]:
137137
138138
The decoder's hot body loops (RowsResponse, FilesResponse,
139139
ServersResponse) wrap the body in a ``memoryview`` so
140-
per-iteration slices are O(1) rather than O(remaining) — see
141-
issue 228. ``bytes`` inputs use zero-copy ``.index(b"\\x00")``.
140+
per-iteration slices are O(1) rather than O(remaining).
141+
``bytes`` inputs use zero-copy ``.index(b"\\x00")``.
142142
143143
``memoryview`` inputs use a single ``bytes(mv).find(b"\\x00")``
144144
when the remaining buffer is small (< 64 KiB). This is one
145145
allocation and one C-level scan, matching the hot-path cost of the
146146
``bytes`` branch. For larger buffers we fall back to a chunked
147-
scan so peak memory stays bounded (ISSUE-65).
147+
scan so peak memory stays bounded.
148148
"""
149149
if isinstance(data, memoryview):
150150
data_len = len(data)
@@ -263,7 +263,7 @@ def encode_value(value: Any, value_type: ValueType | None = None) -> tuple[bytes
263263
# Reject arbitrary ints — the previous ``1 if value else 0``
264264
# coercion silently mapped values like ``5`` or ``-1`` to True,
265265
# which round-trips as the bool True and loses the caller's
266-
# original value (ISSUE-60).
266+
# original value.
267267
if isinstance(value, bool):
268268
return encode_uint64(1 if value else 0), value_type
269269
if isinstance(value, int) and value in (0, 1):
@@ -320,7 +320,7 @@ def decode_value(data: bytes | memoryview, value_type: ValueType) -> tuple[Any,
320320
# Return raw int64 to preserve round-trip identity at the wire level.
321321
# Higher-level clients (like the dqlite DBAPI) turn this into a
322322
# datetime, matching what Go's Rows.Next() does in the database/sql
323-
# driver layer. See issue 006.
323+
# driver layer.
324324
return decode_int64(data), 8
325325
elif value_type == ValueType.FLOAT:
326326
return decode_double(data), 8

0 commit comments

Comments
 (0)