Skip to content

Commit c0cc5ad

Browse files
Reject bool for ConnectionPool / ClusterClient size knobs
``min_size``, ``max_size``, and ``max_attempts`` were validated only with ``< 0`` / ``< 1`` comparisons. ``True == 1`` and ``False == 0`` so ``ConnectionPool (min_size=True)`` succeeded and silently masked caller bugs that accidentally pass a flag through. Mirrors the int/bool reject discipline applied across the wire validators (``encode_int64``, ``Cursor.arraysize``, ``Header.__post_init__``). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2ef34df commit c0cc5ad

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

src/dqliteclient/cluster.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,13 @@ async def connect(
546546
SIGTERM-shutdown budgets. See
547547
``DqliteConnection.__init__`` for full rationale.
548548
"""
549+
# Reject ``bool`` before the < 1 check so ``True``/``False``
550+
# don't silently coerce to 1/0. Mirrors the discipline in
551+
# ``ConnectionPool.__init__``.
552+
if max_attempts is not None and (
553+
isinstance(max_attempts, bool) or not isinstance(max_attempts, int)
554+
):
555+
raise TypeError(f"max_attempts must be int or None, got {type(max_attempts).__name__}")
549556
attempts_cap = max_attempts if max_attempts is not None else _DEFAULT_CONNECT_MAX_ATTEMPTS
550557
if attempts_cap < 1:
551558
# Wording mirrors ``ConnectionPool.__init__``'s validator

src/dqliteclient/pool.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,19 @@ def __init__(
226226
a long retry loop just delays the diagnosis. Must be
227227
``>= 1`` if not ``None``.
228228
"""
229+
# Reject ``bool`` first: ``True == 1`` and ``False == 0``
230+
# would silently coerce to valid sizes and mask caller bugs
231+
# that accidentally pass a flag through. Mirrors the
232+
# int/bool reject discipline applied to ``encode_int64``,
233+
# ``Cursor.arraysize``, and the wire validators.
234+
if isinstance(min_size, bool) or not isinstance(min_size, int):
235+
raise TypeError(f"min_size must be int, got {type(min_size).__name__}")
236+
if isinstance(max_size, bool) or not isinstance(max_size, int):
237+
raise TypeError(f"max_size must be int, got {type(max_size).__name__}")
238+
if max_attempts is not None and (
239+
isinstance(max_attempts, bool) or not isinstance(max_attempts, int)
240+
):
241+
raise TypeError(f"max_attempts must be int or None, got {type(max_attempts).__name__}")
229242
if min_size < 0:
230243
raise ValueError(f"min_size must be non-negative, got {min_size}")
231244
if max_size < 1:

tests/test_pool_size_int_only.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Pin: ``ConnectionPool``'s ``min_size`` / ``max_size`` /
2+
``max_attempts`` and ``ClusterClient.connect``'s ``max_attempts``
3+
reject ``bool`` (``True`` / ``False``) — the only checks were
4+
``< 0`` / ``< 1`` comparisons that silently coerced ``True`` to
5+
``1`` and ``False`` to ``0``.
6+
7+
Mirrors the int/bool reject discipline applied across the wire
8+
validators (``encode_int64``, ``Cursor.arraysize``,
9+
``Header.__post_init__``).
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import pytest
15+
16+
from dqliteclient.cluster import ClusterClient
17+
from dqliteclient.node_store import MemoryNodeStore
18+
from dqliteclient.pool import ConnectionPool
19+
20+
21+
def test_pool_min_size_rejects_bool() -> None:
22+
with pytest.raises(TypeError, match="min_size must be int"):
23+
ConnectionPool(addresses=["localhost:9001"], min_size=True)
24+
25+
26+
def test_pool_max_size_rejects_bool() -> None:
27+
with pytest.raises(TypeError, match="max_size must be int"):
28+
ConnectionPool(addresses=["localhost:9001"], max_size=True)
29+
30+
31+
def test_pool_max_attempts_rejects_bool() -> None:
32+
with pytest.raises(TypeError, match="max_attempts must be int"):
33+
ConnectionPool(
34+
addresses=["localhost:9001"],
35+
max_attempts=True,
36+
)
37+
38+
39+
@pytest.mark.asyncio
40+
async def test_cluster_connect_max_attempts_rejects_bool() -> None:
41+
cluster = ClusterClient(MemoryNodeStore())
42+
with pytest.raises(TypeError, match="max_attempts must be int"):
43+
await cluster.connect(database="x", max_attempts=True)

0 commit comments

Comments
 (0)