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:
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"shared-sqs/app/billing"
|
||||
"shared-sqs/app/conf"
|
||||
"shared-sqs/app/gosqs"
|
||||
"shared-sqs/app/leader"
|
||||
"shared-sqs/app/metrics"
|
||||
"shared-sqs/app/models"
|
||||
"shared-sqs/app/persistence"
|
||||
@@ -32,6 +34,9 @@ func main() {
|
||||
var controlPort string
|
||||
var debug bool
|
||||
var loglevel string
|
||||
var leaderElection bool
|
||||
var leaderTTL time.Duration
|
||||
var leaderInterval time.Duration
|
||||
|
||||
flag.StringVar(&configFile, "config", "", "config file location")
|
||||
flag.StringVar(&adminToken, "admin-token", "", "admin API bearer token")
|
||||
@@ -39,6 +44,11 @@ func main() {
|
||||
flag.StringVar(&controlPort, "control-port", "4101", "control-plane listen port (localhost only: admin/metrics/ui)")
|
||||
flag.BoolVar(&debug, "debug", false, "set debug log level")
|
||||
flag.StringVar(&loglevel, "loglevel", "info", "log level (info, debug, warn, error)")
|
||||
// Leader-election (active/passive HA): выключено по умолчанию — поведение
|
||||
// одиночного инстанса не меняется. Подробно — doc/thinking/state-extraction-and-ha-2026-10-10.md
|
||||
flag.BoolVar(&leaderElection, "leader-election", false, "enable leader election via Redis lock (active/passive HA); off = always ready")
|
||||
flag.DurationVar(&leaderTTL, "leader-ttl", 10*time.Second, "leader lock TTL (should be > 3x leader-interval)")
|
||||
flag.DurationVar(&leaderInterval, "leader-interval", 3*time.Second, "leader lock refresh/acquire interval")
|
||||
flag.Parse()
|
||||
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
@@ -147,6 +157,35 @@ func main() {
|
||||
dataRouter := router.NewDataPlane(tenantStore)
|
||||
controlRouter := router.NewControlPlane(tenantStore, adminToken)
|
||||
|
||||
// Leader-election (active/passive, Шаг 3 — вариант C).
|
||||
// Включается флагом --leader-election или SHARED_SQS_LEADER_ELECTION=true.
|
||||
// Только лидер отдаёт /ready → 200; пассивный — 503 → LB/failover держит
|
||||
// трафик на активном. Без флага ReadyFunc = nil (всегда готов) — одиночный
|
||||
// инстанс работает ровно как раньше.
|
||||
//
|
||||
// Ограничение (осознанное, см. док): in-memory состояние НЕ шарится. Пока
|
||||
// второй инстанс запущен «горячим», очереди живут в Redis, но изменения
|
||||
// второго (пассивного) инстанса не увидит активный до рестарта. Поэтому
|
||||
// на фазе C пассивный НЕ должен принимать SQS-трафик: nginx/LB обязан
|
||||
// проверять /ready и не отправлять API-запросы на 503.
|
||||
if !leaderElection && os.Getenv("SHARED_SQS_LEADER_ELECTION") == "true" {
|
||||
leaderElection = true
|
||||
}
|
||||
var elector *leader.Elector
|
||||
if leaderElection {
|
||||
hostname, _ := os.Hostname()
|
||||
if hostname == "" {
|
||||
hostname = "unknown"
|
||||
}
|
||||
instanceID := fmt.Sprintf("%s-%d", hostname, os.Getpid())
|
||||
elector = leader.New("ssq:leader", instanceID, leaderTTL, leaderInterval)
|
||||
elector.Start()
|
||||
router.ReadyFunc = elector.IsLeader
|
||||
log.Infof("leader-election включён (id=%s, ttl=%s, interval=%s)", instanceID, leaderTTL, leaderInterval)
|
||||
} else {
|
||||
log.Info("leader-election выключен — инстанс всегда ready (одиночный режим)")
|
||||
}
|
||||
|
||||
// PeriodicTasks — visibility timeout, DLQ, deduplication
|
||||
quit := make(chan bool)
|
||||
go gosqs.PeriodicTasks(1*time.Second, quit)
|
||||
@@ -209,6 +248,12 @@ func main() {
|
||||
// Остановить PeriodicTasks
|
||||
close(quit)
|
||||
|
||||
// Отпустить лидерский лок (если были лидером) до закрытия слушателей,
|
||||
// чтобы пассивный взял его быстрее, чем через TTL.
|
||||
if elector != nil {
|
||||
elector.Stop()
|
||||
}
|
||||
|
||||
// Закрыть billing (если был подключён)
|
||||
billing.Close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user