--[[
route.lua — запрашивает pulse-lets-go для маршрутизации вызова.
Телефонный балансировщик нагрузки на базе FreeSWITCH + NATS.
┌─ Размещение ─────────────────────────────────────────────────────┐
│ /opt/pulse-lets-go/contrib/route.lua │
│ │
│ ┌─ Использование в dialplan ────────────────────────────────────┐│
│ │ ││
│ │ ││
│ │ ││
│ │ ││
│ │ ││
│ └───────────────────────────────────────────────────────────────┘│
│ │
│ ┌─ Кэширование (защита от блокировки dialplan-тредов) ───────────┐│
│ │ Уровень 1: FS global variable (ESL push от balancer, ~0.1µs) ││
│ │ Уровень 2: File cache (/tmp/pulse_route_cache.json, ~50µs) ││
│ │ Уровень 3: HTTP API (/api/route, ~1-5ms, safety net) ││
│ └───────────────────────────────────────────────────────────────┘│
│ │
│ ┌─ Переменные канала (опционально) ─────────────────────────────┐│
│ │ balancer_url — URL pulse-lets-go (default: localhost:8080)││
│ │ ingress_trunk — ID входящего транка ││
│ │ route_timeout — таймаут HTTP fallback (default: 3с) ││
│ │ route_cache_path — путь к файловому кэшу (default: /data/…) ││
│ └───────────────────────────────────────────────────────────────┘│
│ │
│ ┌─ Требования FreeSWITCH ───────────────────────────────────────┐│
│ │ mod_curl — (только для уровня 3) ││
│ │ cjson — входит в стандартную поставку FS Lua ││
│ └───────────────────────────────────────────────────────────────┘│
└───────────────────────────────────────────────────────────────────┘
Логика:
1. Попробовать FS global variable pulse_route_json (ESL push)
2. Попробовать файловый кэш
3. HTTP API /api/route (fallback)
4. Разобрать JSON → bridge на sip_gateway
]]--
-- ===================================================================
-- Конфигурация
-- ===================================================================
-- URL балансировщика (переопределяется через переменную канала)
local BALANCER_URL = session:getVariable("balancer_url") or "http://localhost:8080"
-- Таймаут HTTP-запроса в секундах
local HTTP_TIMEOUT = tonumber(session:getVariable("route_timeout") or "3")
-- Путь к файловому кэшу маршрута
local ROUTE_CACHE_PATH = session:getVariable("route_cache_path") or "/opt/pulse-lets-go/data/route_cache.json"
-- JSON-парсер
local cjson = require("cjson")
-- API FreeSWITCH для вызова curl
local api = freeswitch.API()
-- ===================================================================
-- Вспомогательные функции
-- ===================================================================
-- Логирование с префиксом [route]
local function log(level, msg)
freeswitch.consoleLog(level, "[route] " .. msg .. "\n")
end
-- Безопасная отправка трубки с кодом причины
local function safe_hangup(cause)
if session:ready() then
session:hangup(cause or "NORMAL_TEMPORARY_FAILURE")
end
end
-- Попробовать получить JSON-маршрут из FS global variable (уровень 1).
local function try_global_cache()
-- session:getVariable с префиксом global_ читает глобальные переменные FS
local val = session:getVariable("global_pulse_route_json")
if val and val ~= "" then
return val
end
return nil
end
-- Попробовать получить JSON-маршрут из файлового кэша (уровень 2).
local function try_file_cache()
local f, err = io.open(ROUTE_CACHE_PATH, "r")
if not f then
return nil
end
local content = f:read("*a")
f:close()
if content and content ~= "" then
return content
end
return nil
end
-- Попробовать получить JSON-маршрут через HTTP API (уровень 3, fallback).
local function try_http(caller_id, dest, ing_trunk)
local query = string.format("caller_id=%s&dest=%s", caller_id, dest)
if ing_trunk ~= "" then
query = query .. "&ingress_trunk=" .. ing_trunk
end
local url = BALANCER_URL .. "/api/route?" .. query
local cmd = "curl " .. url
local raw = api:execute("curl", cmd)
if not raw or raw == "" then
return nil, "пустой ответ от curl"
end
-- Отделяем заголовки от тела (mod_curl возвращает сырой HTTP)
local body = raw:match("\r?\n\r?\n(.+)")
if not body then
body = raw
end
return body, nil
end
-- Разобрать JSON и выполнить bridge.
-- Возвращает true если звонок обработан (bridge или error).
local function process_route_json(json_str, source)
if not json_str then
return false
end
local ok, route = pcall(cjson.decode, json_str)
if not ok or type(route) ~= "table" then
log("warn", source .. ": некорректный JSON")
return false
end
-- Ошибка балансировщика (нет зарегистрированных нод)
if route.error then
log("err", source .. ": " .. tostring(route.error))
safe_hangup()
return true
end
-- Фолбэк (все ноды unhealthy)
if route.fallback and route.sip_gateway and route.sip_gateway ~= "" then
log("info", string.format("%s: FALLBACK → %s (причина: %s)",
source, route.sip_gateway, route.reason or "all_nodes_unhealthy"))
session:execute("bridge", route.sip_gateway)
return true
end
-- Успешный маршрут
if route.sip_gateway and route.sip_gateway ~= "" then
log("info", string.format("%s: ROUTE %s score=%s → %s",
source, route.node_id or "?", tostring(route.score or 0), route.sip_gateway))
session:execute("bridge", route.sip_gateway)
return true
end
log("warn", source .. ": нет sip_gateway в ответе")
return false
end
-- ===================================================================
-- Основная логика (трёхуровневый кэш)
-- ===================================================================
-- Извлекаем параметры вызова
local caller_id = session:getVariable("caller_id_number") or ""
local dest = session:getVariable("destination_number") or ""
local ing_trunk = session:getVariable("ingress_trunk") or ""
-- Уровень 1: FS global variable (ESL push, sub-µs)
local json_str = try_global_cache()
if process_route_json(json_str, "global") then
return
end
-- Уровень 2: файловый кэш (локальный, ~50µs)
json_str = try_file_cache()
if process_route_json(json_str, "file") then
return
end
-- Уровень 3: HTTP API (fallback, ~1-5ms)
log("info", "запрос маршрута HTTP: " .. BALANCER_URL .. "/api/route?caller_id=" .. caller_id .. "&dest=" .. dest)
local body, http_err = try_http(caller_id, dest, ing_trunk)
if not body then
log("err", "все уровни кэша недоступны: " .. (http_err or "unknown"))
safe_hangup()
return
end
if not process_route_json(body, "http") then
log("err", "непредвиденный формат ответа балансировщика")
safe_hangup()
end