Skip to content

fix(async): make AsyncMilvusClient.refresh_load actually refresh (#3772) - #3773

Open
Anai-Guo wants to merge 3 commits into
milvus-io:masterfrom
Anai-Guo:fix-async-refresh-load
Open

fix(async): make AsyncMilvusClient.refresh_load actually refresh (#3772)#3773
Anai-Guo wants to merge 3 commits into
milvus-io:masterfrom
Anai-Guo:fix-async-refresh-load

Conversation

@Anai-Guo

Copy link
Copy Markdown

Fixes #3772

What

AsyncMilvusClient.refresh_load() delegated to AsyncGrpcHandler.refresh_load(), which
issues a single GetLoadingProgress RPC and returns response.refresh_progress. That is a
read-only poll — it never asks the server to refresh anything.

The synchronous twin does the real work:

# pymilvus/milvus_client/milvus_client.py:1072
def refresh_load(self, collection_name, timeout=None, **kwargs):
    kwargs.pop("_refresh", None)
    conn = self._get_connection()
    conn.load_collection(collection_name, timeout=timeout, _refresh=True, ...)

LoadCollection(refresh=True) followed by wait_for_loading_collection(is_refresh=True).

Why it matters

optimize_collection() exists in both clients and is structured line-for-line the same.
Both end with:

if <collection is loaded>:
    task.set_progress(ProgressStage.REFRESHING_LOAD)
    await self.refresh_load(collection_name, timeout=remaining_timeout(), **kwargs)

(async async_milvus_client.py:2415, sync milvus_client.py:3074)

On the sync client the compacted segments are reloaded before the call returns. On the async
client nothing is reloaded — optimize_collection() still reports
ProgressStage.REFRESHING_LOAD and returns status="success" while queries keep being served
from the pre-compaction segments. Same for a direct await client.refresh_load(name) after a
bulk import. No error, no warning.

partition_names was affected too: the async signature accepted it and forwarded it to
GetLoadingProgress, so it selected which progress was read rather than what got refreshed.

The fix

Route refresh_load() through the async load primitives, mirroring the sync client:

  • partition_names given → conn.load_partitions(..., _refresh=True)
  • otherwise → conn.load_collection(..., _refresh=True)

Both already exist on AsyncGrpcHandler (async_grpc_handler.py:1321 and :399), already
forward _refresh through Prepare, and already wait via wait_for_loading_partitions /
wait_for_loading_collection. They were simply never called from refresh_load. A str
partition name is normalized to a list, matching load_partitions / release_partitions on
the same client.

Deliberately not in scope: AsyncGrpcHandler.refresh_load() is left exactly as it is.
Reading refresh progress is a reasonable low-level operation; only the name and the client
wiring were wrong. Renaming it (e.g. get_refresh_progress) would change a public handler
method and belongs in a separate call by maintainers.

Behavior note for review: the client method now returns None, like the sync twin,
instead of an integer. The previous return value was the progress of a refresh that was never
triggered, so it carried no usable information — but flagging it explicitly in case you would
rather preserve the old signature.

Why CI did not catch it

The async test asserted delegation by handler method name only
(tests/unit/test_async_milvus_client_ops.py, _SIMPLE_ASYNC_DELEGATION_CASES):

("refresh_load", ("col",), {}, "refresh_load"),

That stays green no matter what the handler does. The sync test asserts the actual contract
(tests/unit/test_milvus_client.py::test_refresh_load: _refresh is True on the
load_collection call), so the sync side was protected and the async side was not. This PR
brings the async test up to the same contract and adds partition scoping.

Verification

Red/green on the new tests, same tree, only async_milvus_client.py reverted for the red run:

# unpatched source, new tests
4 failed  (TestAsyncClientRefreshLoad x3 + delegation[refresh_load])
    AssertionError: Expected 'load_collection' to have been called once. Called 0 times.

# patched
tests/unit/test_async_milvus_client_ops.py .......  83 passed

Full unit suite on this tree: 4486 passed, 1 skipped, 2 failed. Both failures
(orm/test_iterator.py::...test_deleted_cp_file_is_recreated_during_iteration and
test_check.py::TestGetCommit::test_get_commit) reproduce identically with the whole change
stashed, i.e. they are pre-existing on this machine and unrelated. Six test_bulk_* /
test_version.py modules fail to collect locally for missing optional deps (minio, azure,
setuptools_scm) and were excluded.

ruff check and ruff format --check clean on both changed files (ruff 0.15.11, within the
ruff>=0.12.9,<1 pin in pyproject.toml).

Not run locally: any test needing a live Milvus server (no server on this machine), so the
refresh RPC itself is covered by mock-level assertions only.

🤖 Generated with Claude Code

@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 czs007 after the PR has been reviewed.
You can assign the PR to them by writing /assign @czs007 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

…vus-io#3772)

AsyncMilvusClient.refresh_load() delegated to AsyncGrpcHandler.refresh_load(),
which issues a single GetLoadingProgress RPC and returns refresh_progress. It
polls progress and never asks the server to refresh anything.

The synchronous twin MilvusClient.refresh_load() re-issues LoadCollection with
refresh=True and waits for the refresh to complete. Because optimize_collection()
is structured identically in both clients and ends with a refresh_load() call,
the async variant reported ProgressStage.REFRESHING_LOAD and status="success"
while the compacted segments were never reloaded.

Route refresh_load() through the async load primitives instead, mirroring the
sync client: load_collection(_refresh=True), or load_partitions(_refresh=True)
when partition_names is given. Both already forward _refresh through Prepare and
wait via wait_for_loading_collection / wait_for_loading_partitions. A str
partition name is normalized to a list, matching load_partitions/release_partitions.

AsyncGrpcHandler.refresh_load() is left untouched; reading refresh progress is a
reasonable low-level operation, only the client wiring was wrong.

The async unit test asserted delegation by handler method name only, so it stayed
green regardless of what the handler did. It now asserts the same contract the
sync test does (_refresh=True on the load call), plus partition scoping.

Signed-off-by: Anai-Guo <antai12232931@outlook.com>
@mergify

mergify Bot commented Aug 24, 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

self,
collection_name: str,
partition_names: Optional[List[str]] = None,
partition_names: Optional[Union[str, List[str]]] = None,

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.

pymilvus/milvus_client/async_milvus_client.py line:864
Low ---- This method public contract changed (returns None instead of the old refresh_progress int, and partition_names now accepts str or List[str]) but it has no docstring, so the new contract is not discoverable from code. Callers that read the old return value break silently; consider adding a short docstring documenting the None return and the partition_names union so future maintainers know the change is deliberate.

@pytest.mark.asyncio
async def test_refresh_load_requests_a_refresh(self):
client, handler = _make_client()
await client.refresh_load("col")

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.

tests/unit/test_async_milvus_client_ops.py line:125
Low ---- These tests lock the handler calls but not the public return contract this PR deliberately changes (None instead of the old refresh_progress int). Consider asserting result = await client.refresh_load("col"); assert result is None here so the new contract cannot silently regress.

Signed-off-by: Anai-Guo <antai12232931@outlook.com>
Signed-off-by: Anai-Guo <antai12232931@outlook.com>
@Anai-Guo

Copy link
Copy Markdown
Author

Thanks @yhmo — both points addressed in e36271c:

  1. Docstring on refresh_load — added, and it states the two things the review flagged: the method returns None (a MilvusException is raised on failure), and partition_names now accepts a bare str as well as a list. The wording mirrors the list_indexes docstring style already in the file.

  2. Return contract pinned in the teststest_refresh_load_requests_a_refresh and test_refresh_load_scopes_to_partitions now capture the awaited value and assert result is None, so if a future change re-leaks the handler's return value the unit suite catches it instead of the callers.

pytest tests/unit/test_async_milvus_client_ops.py → 83 passed. ruff check (repo pyproject.toml) and black --line-length 100 are both clean on the two touched files.

🤖 Generated with Claude Code

@mergify mergify Bot added needs-dco and removed dco-passed labels Aug 29, 2026
@Anai-Guo
Anai-Guo force-pushed the fix-async-refresh-load branch 2 times, most recently from 9c28268 to 6f6f048 Compare August 31, 2026 10:25
@mergify mergify Bot added dco-passed and removed needs-dco labels Aug 31, 2026
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.

[Bug]: AsyncMilvusClient.refresh_load() never triggers a refresh, so optimize_collection() leaves stale segments loaded

3 participants