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:
- Reconocimiento manual (
acknowledge_insight). - Auto-resolución por conectividad (
auto_resolve_connectivity_insights). - Aplicación de fix exitosa (
record_execution_resultconsuccess=True). - 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:
| Estado | Abierto | Nota |
|---|---|---|
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_aten 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_iddebe 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 insightfailedmantiene 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 conNoneo 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]]