This is an automated email from the ASF dual-hosted git repository. FreeOnePlus pushed a commit to branch agent/fix-resource-growth-availability in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
commit 828d096d69ad836ed6b2bea1d0005d382340871b Author: FreeOnePlus <[email protected]> AuthorDate: Sat Aug 1 14:44:32 2026 +0800 fix: preserve partial resource history access --- CHANGELOG.md | 3 + docs/tool-registry.md | 2 +- doris_mcp_server/tools/capability_detector.py | 73 ++++++++++++++++++++++++ doris_mcp_server/tools/doris_feature_matrix.py | 17 +++++- test/tools/test_capability_detector.py | 37 +++++++++++++ test/tools/test_capability_registry.py | 77 ++++++++++++++++++++++++++ test/tools/test_doris_feature_matrix.py | 17 ++++++ 7 files changed, 223 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20641c7..70200b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,9 @@ under **Unreleased** until a new version is selected and published. - Declared the exact `storage`, `query_volume`, and `user_activity` selectors for resource-growth analysis, including their recorded evidence sources, so Hosts reject ambiguous values before dispatching a Doris query. +- Kept resource-growth analysis callable in degraded mode when only audit-log + history or partition-creation history is readable, instead of hiding every + selector behind the audit-log capability probe. - Updated JWT decoding type contracts for PyJWT 2.13 while preserving the existing signature, claim, audience, issuer, and unsafe-debug validation behavior. diff --git a/docs/tool-registry.md b/docs/tool-registry.md index a11acb3..0b7d47c 100644 --- a/docs/tool-registry.md +++ b/docs/tool-registry.md @@ -57,7 +57,7 @@ | `doris_cluster.get_compaction_status` | `child:doris_cluster:get_compaction_status` | `child:call:doris_cluster:get_compaction_status` | `compaction_task_tracker`, `legacy_compaction_summary` | | `doris_cluster.get_workload_group_status` | `child:doris_cluster:get_workload_group_status` | `child:call:doris_cluster:get_workload_group_status` | `workload_group_metrics` | | `doris_cluster.get_compute_group_status` | `child:doris_cluster:get_compute_group_status` | `child:call:doris_cluster:get_compute_group_status` | `compute_group` | -| `doris_cluster.analyze_resource_growth` | `child:doris_cluster:analyze_resource_growth` | `child:call:doris_cluster:analyze_resource_growth` | `historical_resource_metrics` | +| `doris_cluster.analyze_resource_growth` | `child:doris_cluster:analyze_resource_growth` | `child:call:doris_cluster:analyze_resource_growth` | `all_recorded_resource_history`, `audit_resource_history`, `partition_creation_history` | | `doris_cluster.get_runtime_capabilities` | `child:doris_cluster:get_runtime_capabilities` | `child:call:doris_cluster:get_runtime_capabilities` | `capability_snapshot` | | `doris_pipeline.get_ingestion_status` | `child:doris_pipeline:get_ingestion_status` | `child:call:doris_pipeline:get_ingestion_status` | `load_jobs`, `continuous_load` | | `doris_pipeline.diagnose_ingestion` | `child:doris_pipeline:diagnose_ingestion` | `child:call:doris_pipeline:diagnose_ingestion` | `deterministic_ingestion_diagnosis` | diff --git a/doris_mcp_server/tools/capability_detector.py b/doris_mcp_server/tools/capability_detector.py index b59e1ae..191f987 100644 --- a/doris_mcp_server/tools/capability_detector.py +++ b/doris_mcp_server/tools/capability_detector.py @@ -237,6 +237,13 @@ _DOMAIN_PROBES: Mapping[str, tuple[tuple[str, tuple[str, ...]], ...]] = { ("SELECT `time` FROM internal.__internal_schema.audit_log LIMIT 1"), ("metrics_history_readable",), ), + ( + ( + "SELECT CREATE_TIME, DATA_LENGTH, INDEX_LENGTH " + "FROM information_schema.partitions LIMIT 1" + ), + ("resource_storage_history_readable",), + ), ), "doris_pipeline": ( ( @@ -499,6 +506,7 @@ class DorisCapabilityDetector: ) elif domain_name == "doris_cluster": probes.update(await self._safe_probe_cluster_services(auth_context)) + probes.update(_combine_cluster_evidence_probes(probes)) elif domain_name == "doris_pipeline": probes.update(_combine_pipeline_evidence_probes(probes)) elif domain_name == "doris_search": @@ -1836,6 +1844,71 @@ def _combine_query_evidence_probe( ) +def _combine_cluster_evidence_probes( + probes: Mapping[str, CapabilityProbeEvidence], +) -> dict[str, CapabilityProbeEvidence]: + audit = probes.get("metrics_history_readable") + storage = probes.get("resource_storage_history_readable") + full = ( + _combine_all_evidence( + "resource_history_all_sources_readable", + (audit, storage), + supported_reason="ALL_RESOURCE_HISTORY_SOURCES_READABLE", + ) + if audit is not None and storage is not None + else CapabilityProbeEvidence( + probe_id="resource_history_all_sources_readable", + status=CapabilityProbeStatus.UNKNOWN, + reason_code="CAPABILITY_PROBE_PENDING", + evidence_sources=("runtime_probe",), + ) + ) + + def partial_source( + probe_id: str, + source: CapabilityProbeEvidence | None, + *, + reason_code: str, + ) -> CapabilityProbeEvidence: + if source is None: + return CapabilityProbeEvidence( + probe_id=probe_id, + status=CapabilityProbeStatus.UNKNOWN, + reason_code="CAPABILITY_PROBE_PENDING", + evidence_sources=("runtime_probe",), + ) + return CapabilityProbeEvidence( + probe_id=probe_id, + status=( + CapabilityProbeStatus.DEGRADED + if source.status is CapabilityProbeStatus.SUPPORTED + else source.status + ), + reason_code=( + reason_code + if source.status is CapabilityProbeStatus.SUPPORTED + else source.reason_code + ), + evidence_sources=source.evidence_sources or ("runtime_probe",), + ) + + audit_only = partial_source( + "resource_growth_audit_history_readable", + audit, + reason_code="AUDIT_RESOURCE_HISTORY_ONLY", + ) + storage_only = partial_source( + "resource_growth_storage_history_readable", + storage, + reason_code="PARTITION_CREATION_HISTORY_ONLY", + ) + return { + full.probe_id: full, + audit_only.probe_id: audit_only, + storage_only.probe_id: storage_only, + } + + def _combine_pipeline_evidence_probes( probes: Mapping[str, CapabilityProbeEvidence], ) -> dict[str, CapabilityProbeEvidence]: diff --git a/doris_mcp_server/tools/doris_feature_matrix.py b/doris_mcp_server/tools/doris_feature_matrix.py index 66121ea..585dac2 100644 --- a/doris_mcp_server/tools/doris_feature_matrix.py +++ b/doris_mcp_server/tools/doris_feature_matrix.py @@ -1592,11 +1592,24 @@ FEATURE_DEFINITIONS = ( "analyze_resource_growth", A, _variant( - "historical_resource_metrics", + "all_recorded_resource_history", providers=("metrics_history_provider",), - probes=("metrics_history_readable",), + probes=("resource_history_all_sources_readable",), evidence_quality="recorded", ), + _variant( + "audit_resource_history", + providers=("metrics_history_provider",), + probes=("resource_growth_audit_history_readable",), + evidence_quality="partial", + callable_when_degraded=True, + ), + _variant( + "partition_creation_history", + probes=("resource_growth_storage_history_readable",), + evidence_quality="partial", + callable_when_degraded=True, + ), ), _feature( "doris_cluster", diff --git a/test/tools/test_capability_detector.py b/test/tools/test_capability_detector.py index 8c07e1d..b474b22 100644 --- a/test/tools/test_capability_detector.py +++ b/test/tools/test_capability_detector.py @@ -169,6 +169,7 @@ async def test_detector_builds_version_vector_and_extends_domains_lazily() -> No ) query = await detector.detect_domain(base, "doris_query", None) catalog = await detector.detect_domain(base, "doris_catalog", None) + cluster = await detector.detect_domain(base, "doris_cluster", None) assert base.route.fingerprint == "route-a" assert base.capability_generation == 3 @@ -211,9 +212,45 @@ async def test_detector_builds_version_vector_and_extends_domains_lazily() -> No catalog.probe("table_partition_statistics_readable").status is CapabilityProbeStatus.SUPPORTED ) + assert ( + cluster.probe("resource_history_all_sources_readable").status + is CapabilityProbeStatus.SUPPORTED + ) assert connection.statements.count("SELECT @@version_comment;") == 1 [email protected] +async def test_cluster_history_keeps_storage_fallback_without_audit_access() -> None: + connection = _ProbeConnection() + audit_probe = ( + "SELECT `time` FROM internal.__internal_schema.audit_log LIMIT 1" + ) + connection.failures[audit_probe] = RuntimeError( + "Access denied; user lacks SELECT privilege" + ) + manager = _ProbeConnectionManager(connection) + detector = DorisCapabilityDetector(manager) # type: ignore[arg-type] + base = await detector.detect_base( + None, + capability_generation=1, + provider_generation="provider.cluster", + ) + + cluster = await detector.detect_domain(base, "doris_cluster", None) + + assert ( + cluster.probe("metrics_history_readable").status + is CapabilityProbeStatus.UNKNOWN + ) + assert ( + cluster.probe("resource_storage_history_readable").status + is CapabilityProbeStatus.SUPPORTED + ) + storage = cluster.probe("resource_growth_storage_history_readable") + assert storage.status is CapabilityProbeStatus.DEGRADED + assert storage.reason_code == "PARTITION_CREATION_HISTORY_ONLY" + + @pytest.mark.asyncio async def test_lakehouse_probes_derive_target_sensitive_advanced_facets() -> None: connection = _ProbeConnection() diff --git a/test/tools/test_capability_registry.py b/test/tools/test_capability_registry.py index 35bbb87..11d70c3 100644 --- a/test/tools/test_capability_registry.py +++ b/test/tools/test_capability_registry.py @@ -415,6 +415,83 @@ def test_lakehouse_prefers_4_1_variants_and_falls_back_on_4_0() -> None: assert variant_410.callable is True [email protected]( + ("partial_probe", "active_variant"), + [ + ( + "resource_growth_audit_history_readable", + "audit_resource_history", + ), + ( + "resource_growth_storage_history_readable", + "partition_creation_history", + ), + ], +) +def test_resource_growth_keeps_each_partial_history_source_callable( + partial_probe: str, + active_variant: str, +) -> None: + evaluator = CapabilityEvaluator( + matrix=DORIS_FEATURE_MATRIX, + bound_handlers=_BoundHandlers( # type: ignore[arg-type] + "doris_cluster.analyze_resource_growth" + ), + ) + domain = DORIS_DOMAIN_CATALOG.resolve_domain("doris_cluster") + child = DORIS_DOMAIN_CATALOG.resolve_child( + "doris_cluster", + "analyze_resource_growth", + ) + probes = { + "resource_history_all_sources_readable": CapabilityProbeEvidence( + probe_id="resource_history_all_sources_readable", + status=CapabilityProbeStatus.UNKNOWN, + reason_code="PROBE_PERMISSION_DENIED", + ), + **{ + probe_id: CapabilityProbeEvidence( + probe_id=probe_id, + status=( + CapabilityProbeStatus.DEGRADED + if probe_id == partial_probe + else CapabilityProbeStatus.UNKNOWN + ), + reason_code=( + "PARTIAL_RESOURCE_HISTORY_ONLY" + if probe_id == partial_probe + else "PROBE_PERMISSION_DENIED" + ), + ) + for probe_id in ( + "resource_growth_audit_history_readable", + "resource_growth_storage_history_readable", + ) + }, + } + providers = CapabilityProviderRegistry( + { + "metrics_history_provider": CapabilityProviderEvidence( + provider_id="metrics_history_provider", + status=CapabilityProbeStatus.SUPPORTED, + reason_code="PROVIDER_CONFIGURED", + ) + } + ).snapshot() + + availability = evaluator.evaluate( + snapshot=_snapshot(probes=probes), + providers=providers, + domain=domain, + child=child, + auth_context=None, + ) + + assert availability.callable is True + assert availability.status is AvailabilityStatus.DEGRADED + assert availability.active_variant == active_variant + + def test_evaluator_normalizes_system_object_probe_evidence_for_manifest() -> None: evaluator = CapabilityEvaluator( matrix=DORIS_FEATURE_MATRIX, diff --git a/test/tools/test_doris_feature_matrix.py b/test/tools/test_doris_feature_matrix.py index d101224..4316b2e 100644 --- a/test/tools/test_doris_feature_matrix.py +++ b/test/tools/test_doris_feature_matrix.py @@ -596,6 +596,23 @@ def test_compaction_child_keeps_a_degraded_legacy_variant() -> None: assert result.certified is True +def test_resource_growth_declares_full_and_partial_evidence_variants() -> None: + feature = DORIS_FEATURE_MATRIX.get_feature( + "doris_cluster", + "analyze_resource_growth", + ) + + assert tuple( + variant.name for variant in feature.support_contract.variants + ) == ( + "all_recorded_resource_history", + "audit_resource_history", + "partition_creation_history", + ) + assert feature.support_contract.variants[1].callable_when_degraded is True + assert feature.support_contract.variants[2].callable_when_degraded is True + + def test_lineage_native_and_audit_paths_are_both_explicit() -> None: feature = DORIS_FEATURE_MATRIX.get_feature( "doris_governance", --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
