feat(ha): leader-election via Redis lock + /ready endpoint (active/passive, off by default)

- app/leader: Elector (SETNX lock + PEXPIRE heartbeat via Lua, IsLeader)
- app/persistence: TryAcquireLock/RefreshLock/ReleaseLock (no-op when Redis absent)
- app/router: /ready -> 200 if ReadyFunc nil/true, else 503 (both planes)
- app/cmd: --leader-election / SHARED_SQS_LEADER_ELECTION, --leader-ttl, --leader-interval;
  ReadyFunc wired only when enabled, lock released on graceful shutdown
- default OFF: single-instance behaviour unchanged
This commit is contained in:
Repinoid
2026-10-10 16:30:15 +03:00
parent 607aebb8ef
commit d7b3c653e0
4 changed files with 253 additions and 0 deletions
+22
View File
@@ -44,6 +44,9 @@ func NewDataPlane(tenantStore *tenant.TenantStore) http.Handler {
// /health — публичный, без auth
r.HandleFunc("/health", health).Methods("GET")
// /ready — готовность инстанса (для failover/балансировщика)
r.HandleFunc("/ready", readyHandler).Methods("GET")
// /static — логотип, favicon для публичных страниц
r.PathPrefix("/static").Handler(ui.StaticHandler()).Methods("GET")
@@ -83,6 +86,9 @@ func NewControlPlane(tenantStore *tenant.TenantStore, adminToken string) http.Ha
// /health — для проверки control-plane локально
r.HandleFunc("/health", health).Methods("GET")
// /ready — готовность (лидер ли)
r.HandleFunc("/ready", readyHandler).Methods("GET")
// /static — нужен UI-консоли (логотип/favicon)
r.PathPrefix("/static").Handler(ui.StaticHandler()).Methods("GET")
@@ -205,6 +211,22 @@ func health(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, `{"status":"ok","version":%q}`, models.Version)
}
// ReadyFunc — если задана, /ready отдаёт 200 при true и 503 иначе (для failover/LB).
// Задаётся в main при включённом leader-election. nil → всегда готовы.
var ReadyFunc func() bool
// readyHandler — готовность инстанса обслуживать трафик (для балансировщика/failover).
// Пассивный инстанс отдаёт 503, чтобы LB не направлял на него трафик.
func readyHandler(w http.ResponseWriter, req *http.Request) {
if ReadyFunc != nil && !ReadyFunc() {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"ready"}`))
}
// metricsAuth — оборачивает /metrics в проверку Bearer admin-токена.
// В отличие от публичного /health, метрики содержат per-tenant данные
// (accessKey, число очередей/сообщений) и не должны быть доступны без auth.