feat: weighted random routing + mode switching + EWMA smoothing + stale eviction

- Weighted random routing (math/rand/v2 per-goroutine ChaCha8)
- PUT /api/route/mode for runtime mode switch (best / weighted_random)
- EWMA score smoothing (configurable smoothing_factor, default 0.3)
- Periodic stale node eviction (5 min threshold, 60s interval)
- Non-blocking WS broadcast (per-client buffered channel + writePump)
- Background rate limiter cleanup goroutine
- In-memory trunk gateway cache (zero disk I/O in hot path)
- Configurable load_avg multiplier (default 50.0, backward compat)
- UI mode indicator + admin Switch button in dashboard
- 42 tests: weighted random, mode switching, EWMA, eviction, API integration
This commit is contained in:
Maksim Totmin
2026-06-25 20:42:46 +07:00
parent ff037f9c1e
commit 938c9e9362
15 changed files with 1026 additions and 65 deletions
+19
View File
@@ -30,6 +30,8 @@ type ScoringConfig struct {
IdleCPUMin float64 `json:"idle_cpu_min"`
CallFailureRateLethal float64 `json:"call_failure_rate_lethal"`
Weights ScoringWeights `json:"weights"`
SmoothingFactor float64 `json:"smoothing_factor"` // 0 = без сглаживания, 0.3 = рекомендуемый EWMA
LoadAvgMultiplier float64 `json:"load_avg_multiplier"` // множитель load_avg → score (стандартный 50)
}
// TLSConfig — настройки HTTPS.
@@ -97,6 +99,7 @@ type Config struct {
RateLimit RateLimitConfig `json:"rate_limit"`
Log LogConfig `json:"log"`
ESL ESLConfig `json:"esl"`
BalanceMode string `json:"balance_mode"` // "best" | "weighted_random"
}
// Validate проверяет корректность всех полей конфига.
@@ -125,6 +128,12 @@ func (c *Config) Validate() error {
if c.Scoring.CallFailureRateLethal < 0 || c.Scoring.CallFailureRateLethal > 100 {
errs = append(errs, "scoring.call_failure_rate_lethal должен быть 0..100")
}
if c.Scoring.SmoothingFactor < 0 || c.Scoring.SmoothingFactor > 1 {
errs = append(errs, "scoring.smoothing_factor должен быть 0..1")
}
if c.Scoring.LoadAvgMultiplier < 0 {
errs = append(errs, "scoring.load_avg_multiplier должен быть >= 0")
}
w := &c.Scoring.Weights
sum := w.CallScore + w.LoadScore + w.IdleScore + w.FailScore
@@ -158,6 +167,13 @@ func (c *Config) Validate() error {
errs = append(errs, "log.format должен быть text/json")
}
// Balance mode validation
switch c.BalanceMode {
case "best", "weighted_random", "":
default:
errs = append(errs, "balance_mode должен быть best или weighted_random")
}
// ESL validation
if c.ESL.Host != "" {
if c.ESL.Port <= 0 || c.ESL.Port > 65535 {
@@ -374,6 +390,8 @@ func (m *Manager) createDefaultConfig() error {
IdleScore: 0.20,
FailScore: 0.10,
},
SmoothingFactor: 0.3,
LoadAvgMultiplier: 50.0,
},
TLS: TLSConfig{
Enabled: false,
@@ -397,6 +415,7 @@ func (m *Manager) createDefaultConfig() error {
SofiaProfile: "external",
GatewayPrefix: "pulse",
},
BalanceMode: "weighted_random",
}
return m.SaveConfig(&cfg)