--[[ 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