Skip to content

Run the UDS handshake once, via connect(), not also inside _connect() - #337

Open
simpleqt wants to merge 1 commit into
valkey-io:mainfrom
simpleqt:fix/uds-double-handshake
Open

simpleqt wants to merge 1 commit into
valkey-io:mainfrom
simpleqt:fix/uds-double-handshake

Conversation

@simpleqt

Copy link
Copy Markdown

Problem

valkey/asyncio/connection.py's UnixDomainSocketConnection._connect() ends with its own await self.on_connect(), but AbstractConnection.connect() also runs the handshake (on_connect, or the caller's valkey_connect_func) right after _connect() returns:

    async def _connect(self):
        async with async_timeout(self.socket_connect_timeout):
            reader, writer = await asyncio.open_unix_connection(path=self.path)
        self._reader = reader
        self._writer = writer
        await self.on_connect()      # <- connect() runs the handshake again

Every asyncio UDS connection therefore runs the default handshake twice (verified on master f3a73ec against a fake AF_UNIX RESP server that records commands):

TCP: 2 handshake commands -> [CLIENT SETINFO LIB-NAME, CLIENT SETINFO LIB-VER]
UDS: 4 handshake commands -> [the same two, twice]

Consequences:

  • with password/username, AUTH credentials are sent twice per connection (and per reconnect), doubling credential traffic and round trips;
  • a caller-supplied valkey_connect_func, which on TCP replaces the default handshake entirely, cannot do so on UDS — the default handshake still runs first:
TCP+custom: 1 command -> [CUSTOMINIT]
UDS+custom: 3 commands -> [CLIENT SETINFO LIB-NAME, CLIENT SETINFO LIB-VER, CUSTOMINIT]

Changes

_connect() now only opens the socket; the handshake is left to connect(), matching the TCP and SSL paths.

Testing

Two server-free tests in tests/test_asyncio/test_connection.py spin up a minimal AF_UNIX RESP server and count the handshake commands:

  • on unpatched master: both fail (4 handshake commands; default handshake leaks in with valkey_connect_func) — red
  • with this patch: both pass (2 commands; custom func replaces the handshake) — green
$ pytest tests/test_asyncio/test_connection.py -q -k handshake
2 passed

(flake8 / black --target-version py37 / isort clean on the touched files)

UnixDomainSocketConnection._connect() ended with an extra
'await self.on_connect()', while AbstractConnection.connect()
also runs the handshake (on_connect, or the caller's
valkey_connect_func) right after _connect() returns. Every async
UDS connection therefore ran the default handshake twice:

- HELLO/AUTH/CLIENT SETNAME/CLIENT SETINFO/SELECT were all sent
  twice per connection (with AUTH credentials that is double the
  credential traffic and double the round trips), and twice again
  on every reconnect;
- when the caller passes valkey_connect_func, which is meant to
  replace the default handshake entirely (and does so on TCP),
  the default handshake still ran first on UDS - the same
  argument behaved differently on the two transports.

Drop the on_connect() call from _connect() so it only opens the
socket, leaving the handshake to connect() like the TCP and SSL
paths.

Repro on master (f3a73ec), fake AF_UNIX RESP server counting
handshake commands:

    TCP: 2 handshake commands
    UDS: 4 handshake commands          <- doubled
    TCP+custom valkey_connect_func: 1
    UDS+custom valkey_connect_func: 3  <- default handshake leaked in

Signed-off-by: 金豆 <jindou@jindoudeMacBook-Air.local>
Copilot AI lite review requested due to automatic review settings September 24, 2026 16:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants