Repository navigation
perf(registry): parallelize independent MCP server-info fetches - #2983
Sergio Sisternes (sergio-sisternes-epam) wants to merge 11 commits into
Conversation
Independent MCP registry document fetches now run through a bounded ThreadPoolExecutor (max 4 workers), matching check_servers_needing_installation. Closes #2981 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The worker limit must be enforced; the changelog heading also needs adjustment.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR parallelizes independent MCP server-info lookups while preserving ordering and exception handling.
Changes:
- Uses a bounded thread pool for registry lookups.
- Adds timing, ordering, and failure-handling tests.
- Documents the performance improvement.
File summaries
| File | Review |
|---|---|
tests/unit/integration/test_mcp_registry_parallel.py |
Adds parallelism and regression coverage. |
src/apm_cli/registry/operations.py |
Moderate (2 votes): Clamp max_workers to the established four-worker cap. |
CHANGELOG.md |
Nit (1 vote): Place the entry under the existing Changed section. |
Review details
Suppressed comments (1)
CHANGELOG.md:29
- The repository's changelog instruction only defines the
Added,Changed,Deprecated,Removed,Fixed, andSecuritygroups (.github/instructions/changelog.instructions.md:12-21).Performanceis a new, non-canonical heading here, so this entry will not follow the established changelog structure; place the bullet under the existingChangedsection instead.
### Performance
- `batch_fetch_server_info` now looks up independent MCP registry documents concurrently with a bounded `ThreadPoolExecutor` (max 4 workers), matching the existing install-check fan-out. Closes #2981. (#2983)
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Honor the established WS2b cap even when a caller passes a larger max_workers, and move the changelog bullet under Changed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep the #2983 changelog entry under Unreleased after the 0.31.0 cut. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
PR triage recommendationready-for-review Recommendation only, not merge or scope approval. A responsible Linked issue#2981 -- open, labelled Proposed classification
Suggested next actionWait for human CODEOWNERS review. A review request is already Linked issue #2981 is labelled Generated by autopilot-pr-triage-worker. This comment is AI-generated and may contain errors. |
Resolve CHANGELOG.md: keep 0.32.0 as released on main; leave the #2983 batch_fetch_server_info note under Unreleased Changed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve CHANGELOG.md: keep Unreleased Changed for #2983 and take main 0.33.0 release notes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve CHANGELOG.md conflict with #3150 (keep both Unreleased entries).
|
Thanks for the PR, Sergio Sisternes (@sergio-sisternes-epam), and for the fix for #2981. This branch had picked up a conflict with The PR is mergeable again and is waiting on code-owner review. Generated by autopilot-pr-ready-takeover. This comment is AI-generated and may contain errors. |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 0 | Bounded executor.map preserves order and exception→None; document client thread-safety assumption under concurrent lookups. |
| Test Coverage Expert | 0 | 0 | 0 | Wall-time, order/exception, and clamp tests cover the new contract; no missing unit-tier floor found. |
| Doc Writer | 0 | 1 | 0 | CHANGELOG Changed entry is correct; optional docstring note for empty-input early return. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 2 follow-ups
- [Python Architect] Document that
registry_client.find_server_by_referencemust be safe under concurrent calls from the bounded pool -- The new fan-out assumes the client has no shared mutable request state; a one-line docstring or module note locks that contract for future client changes. - [Doc Writer] Note the empty-input early return in the
batch_fetch_server_infodocstring -- Callers scanning the API surface should see that[]skips pool construction.
Recommendation
Ship this tip through normal human CODEOWNER review once checks are green. The parallelism matches an established in-repo pattern, the four-worker cap is tested, changelog placement is canonical, and no blocking findings remain. This is advisory prose for the reviewer, not approval or permission to merge.
Full per-persona findings
Python Architect
- [recommended] Document
registry_clientthread-safety under concurrentfind_server_by_referenceatsrc/apm_cli/registry/operations.py
batch_fetch_server_infonow calls the client from worker threads. The implementation is sound if the client is immutable per-request; a short docstring note prevents a future shared-session client from silently racing.
Suggested: Add one sentence to the method docstring: lookups may run concurrently;registry_client.find_server_by_referencemust be thread-safe.
Test Coverage Expert
No findings.
Doc Writer
- [recommended] Mention empty-input early return in the method docstring at
src/apm_cli/registry/operations.py
The earlyif not server_references: return {}is correct and avoids opening a pool for no work; surface it in the docstring beside themax_workersclamp note.
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Generated by autopilot-pr-review-worker. This comment is AI-generated and may contain errors.
TL;DR
MCPServerOperations.batch_fetch_server_infono longer walksserver_referencesone HTTP lookup at a time. Independentfind_server_by_referencecalls now run through the same boundedThreadPoolExecutor(max_workers=4)already used bycheck_servers_needing_installation. Public result shape is unchanged: submission order, per-refNoneon exception.Note
Closes #2981. Same 5-ref / 200 ms mocked lookup: 1.02 s serial on HEAD → 0.41 s on this branch.
Problem (WHY)
batch_fetch_server_infoinsrc/apm_cli/registry/operations.pyissued onefind_server_by_referenceper reference in a serialforloop, so wall time grew linearly with server count.check_servers_needing_installationalready parallelizes the identical fan-out withThreadPoolExecutor(max_workers=4), so this path was an established-pattern miss, not a new concurrency design.batch_fetch_server_infowhen no cache is present paid that serial cost on every multi-server project.Why these matter: the accepted scan in #2981 names the defect as "O(n) sequential registry HTTP round-trips" and the type/performance implement lens requires a "BEFORE measurement on HEAD and an AFTER measurement on your branch" before calling the change a perf fix (implement-refactor.md).
Approach (WHAT)
_fetch_oneand collect viaexecutor.mapso insertion order matchesserver_references.min(4, len(server_references)); skip the pool on an empty list.except Exception: Nonecontract so a single failed lookup cannot abort the batch.Implementation (HOW)
src/apm_cli/registry/operations.py--batch_fetch_server_infogains an optionalmax_workers: int = 4and usesthread_name_prefix="mcp-fetch". Call sites stay positional; no other methods were retuned.tests/unit/integration/test_mcp_registry_parallel.py-- wall-time trap (3 x 500 ms must finish under 1.0 s) plus order/exception characterization.CHANGELOG.md-- Unreleased Performance entry pointing at [perf-scan] 2026-09-15 -- performance opportunities found #2981.Diagrams
Legend: each registry document fetch is independent; this PR replaces the serial chain with a four-worker pool that still writes the cache in input order.
flowchart LR subgraph serial [HEAD serial] S1[ref 1] --> S2[ref 2] --> Sn[ref n] end subgraph parallel [This PR bounded pool] P1[ref 1] P2[ref 2] Pn[ref n] Pool[ThreadPoolExecutor max_workers 4] Cache[server_info_cache] P1 --> Pool P2 --> Pool Pn --> Pool Pool --> Cache end classDef new stroke-dasharray: 5 5 class P1,P2,Pn,Pool,Cache newTrade-offs
ThreadPoolExecutorbecausecheck_servers_needing_installationalready ships that shape in the same class; rejected asyncio to avoid a second concurrency model on a blocking HTTP client.max_workersrather than a hard-coded 4. Callers can tighten tests; production keeps the WS2b cap of 4.Benefits
test_batch_fetch_server_info_parallel_wall_time).Nonewithout aborting the rest of the batch.Validation
Lint (CI-mirror):
Targeted pytest:
Commands:
uv run --extra dev ruff check src/ tests/ scripts/lint_architecture_boundaries.py scripts/architecture_linter/·ruff format --check·pylint R0801·bash scripts/lint-auth-signals.sh·uv run --extra dev pytest -q tests/unit/integration/test_mcp_registry_parallel.py tests/unit/test_registry_operations_state.py tests/unit/test_registry_operations_phase3.pyHermetic before/after (5 refs, 200 ms sleep, one raising ref):
Scenario Evidence
tests/unit/integration/test_mcp_registry_parallel.py::TestParallelRegistryLookups::test_batch_fetch_server_info_parallel_wall_timeNonefor the failure.tests/unit/integration/test_mcp_registry_parallel.py::TestParallelRegistryLookups::test_batch_fetch_preserves_submission_order_and_exceptionstests/unit/test_registry_operations_state.py::TestBatchFetchServerInfo::test_returns_none_on_exceptionHow to test
uv run --extra dev pytest -q tests/unit/integration/test_mcp_registry_parallel.py-> all parallel wall-time tests pass, includingtest_batch_fetch_server_info_parallel_wall_time.uv run --extra dev pytest -q tests/unit/test_registry_operations_state.py::TestBatchFetchServerInfo-> empty list, success, and exception-to-Nonestill hold.MCPServerOperations.batch_fetch_server_info(["a","b"])still returns a dict keyed in input order with no CLI output change.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com