Implementación de Esquema de Llamadas Salientes en Ronda para Múltiples Puertas de Enlace con Lua

Planteamiento del Problema

En configuraciones previas de integración entre FreeSWITCH y puertas de enlace de voz, se utilizó una estrategia de enrutamiento simple basada en el final del número de destino. Esta aproximación tiene limitaciones: cuando múltiples llamadas coinciden con la misma regla, una única puerta de enlace solo puede manejar una llamada simultáneamente, lo que provoca bloqueos.

Solución con Enrutamiento Cíclico

Se implementa un mecanismo de llamadas salientes en ronda usando scripts Lua para optimizar la eficiencia y el rendimiento del sistema, distribuyendo la carga entre múltiples puertas de enlace.

Script Lua Principal: multi_gw_dial.lua


-- Definición de proveedores SIP
local sip_providers = {
    {
        id = "provider_a",
        caller_num = "559947",
        caller_name = "LineaA"
    },
    {
        id = "provider_b",
        caller_num = "559948",
        caller_name = "LineaB"
    },
    {
        id = "provider_c",
        caller_num = "559944",
        caller_name = "LineaC"
    },
    {
        id = "provider_d",
        caller_num = "559945",
        caller_name = "LineaD"
    },
    {
        id = "provider_e",
        caller_num = "559946",
        caller_name = "LineaE"
    }
    -- Se pueden agregar más proveedores
}

-- Obtener número destino
local target_number = argv[1] or session:getVariable("destination_number")

-- Tabla de seguimiento de llamadas activas
local ongoing_calls = {}

-- Verificar si un número está en uso
function check_call_status(number)
    return ongoing_calls[number] ~= nil
end

-- Registrar número como en uso
function set_call_active(number)
    ongoing_calls[number] = os.time()
    freeswitch.consoleLog("info", "Número " .. number .. " en estado activo\n")
end

-- Liberar número
function release_call(number)
    ongoing_calls[number] = nil
    freeswitch.consoleLog("info", "Número " .. number .. " liberado\n")
end

-- Función para intentar conexiones cíclicas
function initiate_round_robin()
    local attempt_count = 0
    local total_attempts = #sip_providers
    
    freeswitch.consoleLog("info", "Iniciando llamada a [" .. target_number .. "]\n")
    
    if check_call_status(target_number) then
        freeswitch.consoleLog("warning", "Número " .. target_number .. " ya está en uso\n")
        session:hangup("USER_BUSY")
        return
    end
    
    set_call_active(target_number)
    
    while attempt_count < total_attempts and session.ready() do
        attempt_count = attempt_count + 1
        
        local current_provider = sip_providers[attempt_count]
        session:setVariable("effective_caller_id_number", current_provider.caller_num)
        session:setVariable("effective_caller_id_name", current_provider.caller_name)
        
        local dial_path = string.format("sofia/gateway/%s/%s", current_provider.id, target_number)
        freeswitch.consoleLog("info", "Conectando vía [" .. dial_path .. "]\n")
        
        session:execute("bridge", dial_path)
        
        local hangup_reason = session:getVariable("bridge_hangup_cause") or 
                             session:getVariable("originate_disposition") or
                             "DESCONOCIDO"
        
        freeswitch.consoleLog("info", "Intento " .. attempt_count .. " terminado: " .. hangup_reason .. "\n")
        
        if hangup_reason == "NORMAL_CLEARING" then
            break
        end
        
        if attempt_count < total_attempts then
            session:sleep(1500)  -- Pausa entre intentos
        end
    end
    
    release_call(target_number)
    
    if attempt_count >= total_attempts then
        freeswitch.consoleLog("notice", "Agotados todos los intentos para: " .. target_number .. "\n")
    end
end

-- Ejecución principal
if session ~= nil then
    session:preAnswer()
    session:setVariable("hangup_after_bridge", "true")
    session:setVariable("continue_on_fail", "true")
    initiate_round_robin()
else
    freeswitch.consoleLog("error", "Sesión no disponible para llamada\n")
end

Configuración del Plan de Marcado

Modificar el archivo /usr/local/freeswitch/conf/dialplan/public.xml:


<extension name="lua_multi_gateway">
  <condition field="destination_number" expression="^(\d+)$">
    <action application="lua" data="multi_gw_dial.lua"/>
  </condition>
</extension>

Pruebas con FS_CLI


# Prueba mediante originate
originate {dest=017858661999}loopback/1234 &lua(multi_gw_dial.lua)

# Ejecución directa del script
lua multi_gw_dial.lua 017858661999

Mejoras y Optimizaciones

Gestión de Estado en Memoria

Uso de una tabla hash para rastrear números activos y evitar duplicados:


local active_registry = {}

function is_number_busy(num)
    return active_registry[num] == true
end

function mark_as_busy(num)
    active_registry[num] = true
end

function clear_busy_state(num)
    active_registry[num] = nil
end

Algoritmo de Balanceo de Carga

Implementación de selección de puerta de enlace basada en métricas:


local provider_metrics = {}

-- Inicialización de métricas
for _, provider in ipairs(sip_providers) do
    provider_metrics[provider.id] = {
        concurrent = 0,
        success_rate = 1.0,
        last_used = os.time()
    }
end

-- Seleccionar proveedor óptimo
function choose_optimal_provider()
    local best_choice = nil
    local lowest_score = math.huge
    
    for _, provider in ipairs(sip_providers) do
        local metrics = provider_metrics[provider.id]
        local score = metrics.concurrent * 0.6 + (1 - metrics.success_rate) * 0.4
        
        if score < lowest_score then
            lowest_score = score
            best_choice = provider
        end
    end
    
    return best_choice
end

Monitoreo en Tiempo Real


-- Estadísticas de llamadas
local session_stats = {
    total = 0,
    successful = 0,
    failed = 0,
    per_provider = {}
}

-- Actualizar estadísticas
function log_call_stats(provider_id, is_success)
    session_stats.total = session_stats.total + 1
    
    if is_success then
        session_stats.successful = session_stats.successful + 1
    else
        session_stats.failed = session_stats.failed + 1
    end
    
    session_stats.per_provider[provider_id] = (session_stats.per_provider[provider_id] or 0) + 1
end

Mecanismo de Fallover


-- Evaluar salud de la puerta de enlace
function is_provider_available(provider_id)
    local metrics = provider_metrics[provider_id]
    
    -- Umbral de fallos consecutivos
    if metrics.success_rate < 0.2 then
        return false
    end
    
    return true
end

Este enfoque permite un enrutamiento dinámico, evita sobrecargas en puertas de enlace individuales y proporciona resistencia ante fallos. La arquitectura escalable facilita la incorporación de nuevos proveedores SIP según las necesidades del sistema.

Etiquetas: FreeSWITCH Lua SIP Gateway VoIP

Publicado el 6-19 19:16