Principio "Menos es Más": código común centralizado en un solo lugar. Cambios: - Nuevo módulo Dron::Base con funcionalidad compartida: - logger: Logger compartido (evita crear múltiples instancias) - db_query: Ejecución de SQL con resultados - db_exec: Ejecución de SQL sin resultados - formato_duracion: Utilitario para formatear segundos - slice_safe: Slice seguro de strings - blank?: Verificación de strings vacíos/nulos - Todos los módulos atómicos (ejecutor, vigilante, sanador, bitacora) ahora usan Base.logger, Base.db_query, Base.db_exec Beneficios: - Un solo lugar para corregir errores comunes - Menos duplicación de código (~40% menos líneas) - Consistencia en manejo de logs y DB - Fácil extensión: nuevos drones heredan funcionalidad automática Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
3.7 KiB
Ruby
85 lines
3.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Dron::Sanador — Módulo atómico para reparación de drones
|
|
#
|
|
# Responsabilidad única: Reparar drones zombies/fallidos
|
|
|
|
require_relative 'base'
|
|
require_relative '../../db/core/dron_db'
|
|
|
|
module Dron
|
|
class Sanador
|
|
MAX_REINTENTOS = 3
|
|
BACKOFF_BASE = 60
|
|
CLEANUP_DAYS = 7
|
|
|
|
class << self
|
|
def reintentar_fallidos(dry_run: false)
|
|
Base.logger.info("🩹 Dron Sanador: Re-intentando drones fallidos...")
|
|
fallidos = Base.db_query("SELECT * FROM bitacoras.dron_logs WHERE estado = 'failed' AND reintentos < #{MAX_REINTENTOS} AND cmd IS NOT NULL ORDER BY completed_at ASC")
|
|
return { reintentados: 0, omitidos: 0 } if fallidos.empty?
|
|
|
|
reintentados = 0
|
|
omitidos = 0
|
|
|
|
fallidos.each do |dron|
|
|
espera = BACKOFF_BASE * (2 ** dron['reintentos'].to_i)
|
|
tiempo_desde_fallo = Time.now - dron['completed_at']
|
|
|
|
if tiempo_desde_fallo < espera
|
|
Base.logger.debug("⏳ #{dron['dron_id']} en backoff (#{tiempo_desde_fallo.round}s/#{espera}s)")
|
|
omitidos += 1
|
|
next
|
|
end
|
|
|
|
if dry_run
|
|
Base.logger.info("🩹 [DRY RUN] Re-intentaría: #{dron['dron_id']}")
|
|
reintentados += 1
|
|
else
|
|
Base.logger.info("🩹 Re-intentando #{dron['dron_id']} (intento #{dron['reintentos'].to_i + 1}/#{MAX_REINTENTOS})")
|
|
ADN::DB::DronDB.incrementar_reintentos(dron['dron_id'])
|
|
resultado = Dron::Ejecutor.lanzar(cmd: dron['cmd'], nota: "Reintento: #{dron.dig('metadata', 'nota')}", flujo_id: dron['flujo_id'])
|
|
reintentados += 1 if resultado[:exit_code] == 0
|
|
notificar_fallo_persistente(dron) if dron['reintentos'].to_i >= MAX_REINTENTOS && resultado[:exit_code] != 0
|
|
end
|
|
end
|
|
|
|
puts "\n🩹 Resultado: Reintentados=#{reintentados} | Omitidos=#{omitidos}"
|
|
{ reintentados: reintentados, omitidos: omitidos }
|
|
end
|
|
|
|
def limpiar_antiguos(dias: CLEANUP_DAYS, dry_run: false)
|
|
Base.logger.info("🩹 Dron Sanador: Limpiando drones antiguos (>#{dias} días)...")
|
|
resultado = Base.db_query("SELECT COUNT(*) as cantidad FROM bitacoras.dron_logs WHERE estado IN ('completed', 'failed', 'zombie') AND completed_at < NOW() - INTERVAL '#{dias} days'")
|
|
cantidad = resultado.first['cantidad'].to_i
|
|
return { eliminados: 0 } if cantidad == 0
|
|
|
|
if dry_run
|
|
Base.logger.info("🩹 [DRY RUN] Eliminaría #{cantidad} drones antiguos")
|
|
return { eliminados: 0 }
|
|
end
|
|
|
|
Base.db_exec("DELETE FROM bitacoras.dron_logs WHERE estado IN ('completed', 'failed', 'zombie') AND completed_at < NOW() - INTERVAL '#{dias} days'")
|
|
Base.logger.info("🩹 ✅ Limpiados #{cantidad} drones antiguos")
|
|
{ eliminados: cantidad }
|
|
end
|
|
|
|
def sanear(dias_limpieza: CLEANUP_DAYS, dry_run: false)
|
|
Base.logger.info("🩹 Dron Sanador: Saneamiento completo...")
|
|
resultados = { reintentos: reintentar_fallidos(dry_run: dry_run), limpieza: limpiar_antiguos(dias: dias_limpieza, dry_run: dry_run) }
|
|
puts "\n🩹 Saneamiento completado"
|
|
puts " Reintentos: #{resultados[:reintentos][:reintentados]}"
|
|
puts " Limpieza: #{resultados[:limpieza][:eliminados]}"
|
|
resultados
|
|
end
|
|
|
|
private
|
|
|
|
def notificar_fallo_persistente(dron)
|
|
Base.logger.error("🚨 FALLO PERSISTENTE: #{dron['dron_id']} - #{Base.slice_safe(dron['cmd'], 50)}")
|
|
Base.db_exec("INSERT INTO bitacoras.dron_avances (dron_id, paso, descripcion, estado) VALUES ($1, -99, $2, 'critical')", [dron['dron_id'], "FALLO PERSISTENTE tras #{dron['reintentos']} reintentos"])
|
|
end
|
|
end
|
|
end
|
|
end
|