CreaRack-SL

Servicio close_group_if_done — Auto-cierre de grupos cuando no quedan insights abiertos

Descripción

Función: close_group_if_done(group_id: int) -> bool

Ubicación: monitoring/services/correlation_service.py (líneas ~116–130)

Comprueba si un IncidentGroup no tiene insights con estado “abierto” (pending, executing, failed) y, si es así, lo marca como resolved automáticamente con fecha resolved_at.

Se invoca desde todos los caminos de cierre de un insight:

  1. Reconocimiento manual (acknowledge_insight).
  2. Auto-resolución por conectividad (auto_resolve_connectivity_insights).
  3. Aplicación de fix exitosa (record_execution_result con success=True).
  4. Expiración de stale insights (expire_stale_insights).

Firma y comportamiento

def close_group_if_done(group_id: int) -> bool:
    """
    Auto-resolve a group once none of its insights remain open.
    Called from every insight-closing path (ack, auto-resolve, applied, expire).
    
    Returns True si el grupo fue resuelto; False en caso contrario o error.
    """
    if not group_id:
        return False
    
    group = IncidentGroup.objects.filter(id=group_id, status="open").first()
    if group is None:
        return False
    
    # Comprobar si quedan insights en estado abierto
    if group.insights.filter(status__in=OPEN_INSIGHT_STATUSES).exists():
        return False
    
    # Si no hay abiertos, resolver grupo
    resolve_group(group)
    return True

Estados “abiertos”

OPEN_INSIGHT_STATUSES = ("pending", "executing", "failed")

Un insight está abierto si aún requiere intervención. Los estados terminales no lo mantienen:

EstadoAbiertoNota
pending✅No visto aún.
executing✅Fix en marcha.
failed✅Intento fallido, espera reconocimiento o reintento.
acknowledged❌Terminal: reconocido, no se necesita más.
applied❌Terminal: fix exitoso.
expired❌Terminal: timeout, abandono.

Integración con el ciclo

1. En acknowledge_insight() (reconocimiento manual)

def acknowledge_insight(insight: AIInsight, user=None, notes: str = "") -> AIInsight:
    # ... actualizar insight a "acknowledged" ...
    _close_group_of(insight)
    return insight

def _close_group_of(insight: AIInsight):
    """Best-effort: auto-resolve the insight's group if nothing open remains."""
    try:
        close_group_if_done(insight.incident_group_id)
    except Exception as e:
        logger.info(f"Group auto-close skipped for insight {insight.id}: {e}")

Caso: Usuario marca insight como reconocido.

  • Si es el último insight abierto del grupo → grupo pasa a resolved.

2. En auto_resolve_connectivity_insights() (auto-resolución)

async def auto_resolve_connectivity_insights(target_id: int) -> int:
    # ... cambiar insights a "acknowledged" + resolved_at ...
    for gid in group_ids:
        try:
            await sync_to_async(close_group_if_done)(gid)
        except Exception as e:
            logger.info(f"Group auto-close skipped for group {gid}: {e}")

Caso: El equipo se recupera, sistema lo detecta automáticamente.

  • Si todos los insights correlacionados del grupo ya están abiertos o cerrables → grupo se resuelve.

3. En record_execution_result() (fix aplicado)

def record_execution_result(insight: AIInsight, output: str, success: bool):
    # ...
    if success:
        insight.status = AIInsight.Status.APPLIED
        insight.resolved_at = timezone.now()
        # ...
        _close_group_of(insight)

Caso: Agente aplica fix y reporta éxito.

  • Insight pasa a applied → potencial cierre del grupo.

4. En expire_stale_insights() (expiración)

def expire_stale_insights():
    # ... marcar insights expirados ...
    group_ids = set(stale.exclude(incident_group__isnull=True).values_list("incident_group_id", flat=True))
    expired = stale.update(status=AIInsight.Status.EXPIRED)
    for gid in group_ids:
        try:
            close_group_if_done(gid)
        except Exception as e:
            logger.info(f"Group auto-close skipped for group {gid}: {e}")

Caso: Tarea diaria marca insights como expirados (timeout).

  • Nota: expiración NO escribe resolved_at en el insight (decisión de semántica).
  • Pero el grupo SÍ se cierra si todos sus insights ya están en estado terminal.

Precondiciones y garantías

Precondiciones:

  • group_id debe ser válido (se ignora si None o inexistente).
  • El grupo debe estar en estado open (si ya está resolved, no se reabre).

Garantías:

  • Operación idempotente: llamar dos veces sobre el mismo grupo no daña.
  • Best-effort: errores no interrumpen el flujo principal; se loguean.
  • Atómico: el estado del grupo se actualiza en una transacción.

Impacto en datos históricos

Con la migración 0029_backfill_itsm_close_cycle, 449 grupos que llevaban abiertos desde siempre (algunos desde 30-04-2026) se auto-resuelven en el primer pase, capturando la fecha real del último cierre de sus insights.


Pruebas

Véase tests/api/test_itsm_close_cycle.py:

  • ✅ test_group_closes_when_last_insight_acked — grupo se cierra cuando el último insight se reconoce.
  • ✅ test_failed_insight_keeps_group_open — un insight failed mantiene el grupo abierto.
  • ✅ test_expire_closes_group_without_resolving_insights — expiración cierra grupo pero NO resuelve insights.
  • ✅ test_close_group_if_done_ignores_missing_group — llamada con None o ID inexistente es segura.

Véase también

  • [[feature—monitoring—itsm-ciclo-cierre]]
  • [[decision—20260814—itsm-closed-at-semantics]]
  • [[entity—monitoring—service—reopen-group]]
  • [[entity—monitoring—model—incident-group]]