"""Link existing CMDB realized-savings events to handled recommendations.""" from __future__ import annotations import uuid from collections import defaultdict from typing import Any, cast from sqlalchemy import insert, select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.cmdb.models import CmdbEfficiencySavingsEvent from app.intelligence.models import ( IntelligenceRecommendation, IntelligenceRecommendationSavingsLink, IntelligenceSnapshot, ) _ELIGIBLE_ACTIONS = frozenset({"reclaim", "configure"}) _EFFICIENCY_DIMENSIONS = frozenset({"cloud_network", "cost_efficiency"}) def _action_is_eligible(contract: dict[str, Any] | None) -> bool: contract = contract or {} if str(contract.get("action_kind") or "").strip().lower() not in _ELIGIBLE_ACTIONS: return False return bool(set(contract.get("dimensions") or ()) & _EFFICIENCY_DIMENSIONS) def _evidence_server_ids( recommendation: IntelligenceRecommendation, snapshot: IntelligenceSnapshot, ) -> set[str]: refs = { str(item.get("ref")) for item in (recommendation.evidence_refs or []) if isinstance(item, dict) and isinstance(item.get("ref"), str) } return { str(value["object_id"]) for ref, value in (snapshot.reference_map or {}).items() if ref in refs and isinstance(value, dict) and value.get("kind") == "server" and isinstance(value.get("object_id"), str) } async def reconcile_recommendation_savings(db: AsyncSession) -> int: """Durably link a real saving once to one explicitly-started suggestion. We never infer a match from a title or model narrative. Eligibility needs an in-progress recommendation, an allowed resource-efficiency handling category, and a server anonymous reference that resolves locally to the exact CMDB saving-event server. A saving event can have one link only. """ # Do the anti-join in SQL. Apart from avoiding an ever-growing Python # set, the unique key below is still essential: two scheduled workers can # observe the same unlinked event before either commits. events = list((await db.execute( select(CmdbEfficiencySavingsEvent) .outerjoin( IntelligenceRecommendationSavingsLink, IntelligenceRecommendationSavingsLink.savings_event_id == CmdbEfficiencySavingsEvent.id, ) .where(IntelligenceRecommendationSavingsLink.savings_event_id.is_(None)) )).scalars().all()) if not events: return 0 recommendations = list((await db.execute( select(IntelligenceRecommendation) .where(IntelligenceRecommendation.status == "in_progress") .order_by(IntelligenceRecommendation.priority, IntelligenceRecommendation.created_at) )).scalars().all()) snapshot_ids = {row.snapshot_id for row in recommendations} snapshots = { row.id: row for row in (await db.execute( select(IntelligenceSnapshot).where(IntelligenceSnapshot.id.in_(snapshot_ids)) )).scalars().all() } if snapshot_ids else {} candidates: dict[str, list[IntelligenceRecommendation]] = defaultdict(list) for recommendation in recommendations: if not _action_is_eligible(recommendation.action_contract): continue snapshot = snapshots.get(recommendation.snapshot_id) if snapshot is None: continue for server_id in _evidence_server_ids(recommendation, snapshot): candidates[server_id].append(recommendation) created = 0 for event in events: server_candidates = candidates.get(str(event.server_id), []) if not server_candidates: continue # SQL ordering above makes a tie deterministic. INSERT IGNORE makes # the unique event key an actual concurrency boundary instead of an # IntegrityError that would roll back an otherwise healthy scheduled # efficiency recompute. MySQL and TiDB both support this syntax. recommendation = server_candidates[0] result = await db.execute( insert(IntelligenceRecommendationSavingsLink) .prefix_with("IGNORE") .values( id=uuid.uuid4(), recommendation_id=recommendation.id, savings_event_id=event.id, server_id=str(event.server_id), ) ) # ``rowcount`` lives on CursorResult; AsyncSession.execute is typed as # Result[Any], so cast to read it without a mypy attr-defined. With # INSERT IGNORE it is the only honest signal for "this link is new": # a duplicate event silently inserts zero rows. created += max(int(cast("Any", result).rowcount or 0), 0) if created: await db.commit() return created __all__ = ["reconcile_recommendation_savings"]