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.