Skip to content

Commit 54a0045

Browse files
fix: generate unique client_id per connection by default
handshake() previously defaulted to client_id=0, so every Python client registered under the same id and the server's per-client logs, traces, and metrics collapsed them into one. Generate a random non-zero 63-bit id when the caller does not provide one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 770041c commit 54a0045

2 files changed

Lines changed: 31 additions & 2 deletions

File tree

src/dqliteclient/protocol.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Low-level protocol handler for dqlite."""
22

33
import asyncio
4+
import secrets
45
from collections.abc import Sequence
56
from typing import Any
67

@@ -42,11 +43,16 @@ def __init__(
4243
self._heartbeat_timeout = 0
4344
self._timeout = timeout
4445

45-
async def handshake(self, client_id: int = 0) -> int:
46+
async def handshake(self, client_id: int | None = None) -> int:
4647
"""Perform protocol handshake.
4748
48-
Returns the heartbeat timeout from server.
49+
If ``client_id`` is not provided, a random non-zero 63-bit id is
50+
generated so each connection is distinguishable in server logs,
51+
traces, and per-client metrics. Returns the heartbeat timeout
52+
from the server.
4953
"""
54+
if client_id is None:
55+
client_id = secrets.randbits(63) or 1
5056
# Send protocol version + client registration together
5157
request = ClientRequest(client_id=client_id)
5258
self._writer.write(MessageEncoder().encode_handshake() + request.encode())

tests/test_protocol.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,29 @@ async def test_handshake_success(
2828

2929
assert timeout == 15000
3030

31+
async def test_handshake_generates_unique_client_id_when_unspecified(
32+
self,
33+
mock_reader: AsyncMock,
34+
mock_writer: MagicMock,
35+
welcome_response: bytes,
36+
) -> None:
37+
"""When no client_id is passed, handshake must generate a unique non-zero id.
38+
39+
Every connection defaulting to client_id=0 collapses all clients in
40+
the server's per-client metrics and tracing. The client must opaquely
41+
generate a random id per connection.
42+
"""
43+
mock_reader.read.return_value = welcome_response
44+
45+
p1 = DqliteProtocol(mock_reader, mock_writer)
46+
await p1.handshake()
47+
p2 = DqliteProtocol(mock_reader, mock_writer)
48+
await p2.handshake()
49+
50+
assert p1._client_id != 0
51+
assert p2._client_id != 0
52+
assert p1._client_id != p2._client_id
53+
3154
async def test_handshake_caps_heartbeat_timeout(
3255
self,
3356
mock_reader: AsyncMock,

0 commit comments

Comments
 (0)