feat: production-grade routing cache + stale detection fixes
Three-tier routing cache (ESL global -> file -> HTTP) eliminates HTTP
from call path for 2500-5000 concurrent calls. Lua reads cached route
in ~0.1us instead of blocking on api:execute('curl', ...).
Engine fixes:
- recalcBestLocked now skips stale nodes (was 5 min bug -> now ~10-15s)
- PickNodeForCall action_type='node' checks staleness for consistency
- onMetric callback pushes cache on every metric (no ticker delay)
Config:
- stale_threshold_sec default 20 -> 10 (industry standard)
- contrib/ included in Makefile deploy target
- route.lua paths updated for /opt/pulse-lets-go
This commit is contained in:
@@ -47,6 +47,8 @@ func (a *API) handleToggleNode(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
a.PushRouteCache()
|
||||
|
||||
info, _ := a.engine.GetNodeInfo(nodeID)
|
||||
writeJSON(w, http.StatusOK, info)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/pulse-lets-go/internal/esl"
|
||||
"github.com/pulse-lets-go/internal/models"
|
||||
)
|
||||
|
||||
@@ -90,6 +93,45 @@ func (a *API) handleSetRouteMode(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"mode": req.Mode})
|
||||
}
|
||||
|
||||
// GetBalanceGateway возвращает gateway для ноды из in-memory кэша (O(1)).
|
||||
func (a *API) GetBalanceGateway(nodeID string) (string, bool) {
|
||||
return a.cachedBalanceGateway(nodeID)
|
||||
}
|
||||
|
||||
// GetFallbackGateway возвращает fallback gateway из in-memory кэша (O(1)).
|
||||
func (a *API) GetFallbackGateway() string {
|
||||
return a.cachedFallbackGateway()
|
||||
}
|
||||
|
||||
// SetRouteCachePath устанавливает путь к файловому кэшу маршрута.
|
||||
func (a *API) SetRouteCachePath(path string) {
|
||||
a.routeCachePath = path
|
||||
}
|
||||
|
||||
// PushRouteCache читает best-node из engine, резолвит gateway из trunk cache,
|
||||
// атомарно пишет файловый кэш и пушит маршрут в FreeSWITCH через ESL.
|
||||
func (a *API) PushRouteCache() {
|
||||
nodeID, score, fallback := a.engine.GetBestNode()
|
||||
|
||||
var gw string
|
||||
if nodeID != "" && !fallback {
|
||||
gw, _ = a.cachedBalanceGateway(nodeID)
|
||||
}
|
||||
if gw == "" {
|
||||
gw = a.cachedFallbackGateway()
|
||||
}
|
||||
|
||||
entry := buildRouteCacheEntry(nodeID, gw, score, fallback)
|
||||
|
||||
if a.routeCachePath != "" {
|
||||
writeRouteCacheFile(a.routeCachePath, entry)
|
||||
}
|
||||
|
||||
if a.eslClient != nil && a.eslClient.IsConnected() {
|
||||
esl.PushRoute(a.eslClient, entry)
|
||||
}
|
||||
}
|
||||
|
||||
// cachedBalanceGateway возвращает gateway для balance-транка из in-memory кэша (O(1)).
|
||||
func (a *API) cachedBalanceGateway(nodeID string) (string, bool) {
|
||||
a.trunkCacheMu.RLock()
|
||||
@@ -105,6 +147,36 @@ func (a *API) cachedFallbackGateway() string {
|
||||
return a.fallbackGateway
|
||||
}
|
||||
|
||||
// buildRouteCacheEntry строит запись для кэширования в ESL global / файл.
|
||||
func buildRouteCacheEntry(nodeID, gw string, score float64, fallback bool) esl.RouteCacheEntry {
|
||||
entry := esl.RouteCacheEntry{
|
||||
SIPGateway: gw,
|
||||
Score: score,
|
||||
Fallback: fallback,
|
||||
}
|
||||
if nodeID == "" {
|
||||
entry.Error = "no_nodes_registered"
|
||||
} else if fallback {
|
||||
entry.Reason = "all_nodes_unhealthy"
|
||||
} else {
|
||||
entry.NodeID = nodeID
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
// writeRouteCacheFile атомарно записывает файловый кэш маршрута.
|
||||
func writeRouteCacheFile(path string, entry esl.RouteCacheEntry) {
|
||||
b, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tmpPath := path + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, b, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
os.Rename(tmpPath, path)
|
||||
}
|
||||
|
||||
// getRouteNodeInfo возвращает список нод с их скорами для ответа маршрутизации.
|
||||
func (a *API) getRouteNodeInfo() []models.RouteNodeInfo {
|
||||
nodes := a.engine.GetAllNodes()
|
||||
|
||||
@@ -34,6 +34,8 @@ type API struct {
|
||||
gatewayStates map[string]string // trunkID → "up"/"down"
|
||||
fsStats esl.FsStats
|
||||
fsStatsMu sync.RWMutex
|
||||
|
||||
routeCachePath string // путь к файловому кэшу маршрута (для Lua)
|
||||
}
|
||||
|
||||
// NewAPI создаёт новый HTTP API с заданными зависимостями.
|
||||
|
||||
Reference in New Issue
Block a user