Skip to content

fix(async): honor reset_connection when AsyncMilvusClient rotates a password - #3756

Open
Anai-Guo wants to merge 1 commit into
milvus-io:masterfrom
Anai-Guo:fix-async-update-password-reset-connection
Open

fix(async): honor reset_connection when AsyncMilvusClient rotates a password#3756
Anai-Guo wants to merge 1 commit into
milvus-io:masterfrom
Anai-Guo:fix-async-update-password-reset-connection

Conversation

@Anai-Guo

Copy link
Copy Markdown

What

AsyncMilvusClient.update_password() has no reset_connection flag, so an async client that rotates its own password keeps sending the old one on every subsequent call.

The sync client has had this since the beginning:

# pymilvus/milvus_client/milvus_client.py
def update_password(self, user_name, old_password, new_password,
                    reset_connection: Optional[bool] = False, ...):
    ...
    if reset_connection:
        conn._setup_authorization_interceptor(user_name, new_password, None)
        conn._setup_grpc_channel()

AsyncGrpcHandler even carries a mirrored _setup_authorization_interceptor() — but it has zero call sites; the async client never wires it up. So today the only way for an async caller to keep working after update_password() is to throw the client away and build a new one.

Why adding the flag alone isn't enough

Porting the sync two-liner verbatim does not work, because the two handlers install interceptors differently:

  • sync _setup_authorization_interceptor() only records self._authorization_interceptor; _setup_grpc_channel() then rebuilds _final_channel from the raw channel, so the old interceptor is dropped.
  • async _setup_authorization_interceptor() appends to the live _final_channel._unary_unary_interceptors, and _build_stub() appends again. Nothing is ever removed.

Each authorization interceptor appends its own authorization metadata entry (async_header_adder_interceptor), so the superseded credential stays on the wire — and, being first in the list, is the one the server sees. Measured against the current master with a naive port of the sync code:

init      : ['root:old_pw']
1st reset : ['root:old_pw', 'root:new_pw', 'root:new_pw']
2nd reset : ['root:old_pw', 'root:new_pw', 'root:new_pw', 'root:newer_pw', 'root:newer_pw']

(_setup_grpc_channel() carries the comment # avoid to add duplicate headers._build_stub does not currently honor it.)

What this PR changes

  1. AsyncMilvusClient.update_password(..., reset_connection=False) — same semantics as the sync client.
  2. AsyncGrpcHandler._uninstall_authorization_interceptor() (new) — detaches the interceptor being replaced from the live channel; _setup_authorization_interceptor() calls it before installing the new one.
  3. AsyncGrpcHandler._build_stub() — no longer re-appends an authorization interceptor that is already installed on that channel, honoring its own comment. Building a stub on a fresh channel is unchanged.

After the change, the same measurement:

init      : ['root:old_pw']
1st reset : ['root:new_pw']
2nd reset : ['root:newer_pw']

Signature note: reset_connection is appended at the end of the async signature rather than placed after new_password as in the sync client, so existing positional calls like update_password(u, old, new, 30) keep binding timeout. Happy to mirror the sync ordering instead if you'd prefer exact positional parity.

Tests

New in tests/unit/async_grpc_handler/test_async_auth.py (5 tests): credential rotation replaces the stale interceptor and installs exactly one; the uninstall helper is a no-op when nothing is installed; reset_connection=True re-authorizes the connection and the default leaves it alone; and the sync/async signatures carry the same parameter set.

The rotation tests assert on the decoded authorization headers the installed interceptors would emit, not just list length, so they fail loudly if a stale credential survives.

Verification (Windows, Python 3.12, no server required — grpc.aio channels are constructed but never connected):

  • new code + new tests → 174 passed in tests/unit/async_grpc_handler/

  • unmodified source + new tests → 5 failed, 169 passed, the key one being

    AssertionError: assert ['root:old_pa...oot:new_pass'] == ['root:new_pass']
    

No existing test was modified or weakened. An earlier draft moved interceptor installation out of _setup_authorization_interceptor and broke test_setup_authorization_interceptor_appends_header; the design in this PR keeps that method's contract intact (it still appends to the channel) and instead removes the interceptor it supersedes.

Full unit suite: 3377 passed. The two unrelated failures on my machine (test_check.py::test_get_commit, plus collection errors in test_version.py / the bulk-writer modules) are missing optional dev dependencies (setuptools_scm) and reproduce identically on unmodified master.

ruff check and ruff format --check clean on all three files (tests checked with tests/ruff.toml).


This was found by diffing MilvusClient against AsyncMilvusClient method signatures. The scan also surfaced two naming drifts I have deliberately not touched, since they may be intentional and changing them would be breaking: get_load_state(partition_name=) vs (partition_names=), and transfer_replica(source_group, target_group, num_replicas) vs (source, target, num_replica). Happy to file those separately if they're unintended.

🤖 Generated with Claude Code

AsyncMilvusClient.update_password had no reset_connection flag, so an async
client that changed its own password kept sending the old one on every later
call. Adding the flag alone was not enough: the async channel appends
authorization interceptors and never replaces them, so the superseded
credential stayed installed -- and, being first, still won.

_setup_authorization_interceptor now detaches the interceptor it is replacing,
and _build_stub no longer re-appends one that is already installed (honoring
its own "avoid to add duplicate headers" comment).

Signed-off-by: Tai An <antai12232931@anaiguo.com>
@sre-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Anai-Guo
To complete the pull request process, please assign longjiquan after the PR has been reviewed.
You can assign the PR to them by writing /assign @longjiquan in a comment when ready.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@mergify

mergify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants