Files
SQS-service/app/leader/leader.go
T
Repinoid d7b3c653e0 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
2026-10-10 16:30:15 +03:00

127 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// app/leader/leader.go
// Простейший leader-election для active/passive (Шаг 3, вариант C).
//
// Лидер держит Redis-лок (ssq:leader) с TTL и продлевает его каждые interval.
// Пассивные периодически пытаются взять лок; когда лидер падает, TTL истекает и
// один из пассивных становится лидером. Готовность отдаётся через IsLeader().
//
// Почему так, а не полный вынос состояния (вариант A): дешевле в разы и не требует
// переписывания ядра очередей; снимает главную боль «одна ВМ = SPOF». Потолок
// (горизонтальное масштабирование) при этом остаётся — см.
// doc/thinking/state-extraction-and-ha-2026-10-10.md.
//
// ВАЖНО: при memory-only (нет Redis) — считаем себя лидером (мы единственные).
package leader
import (
"sync"
"time"
"shared-sqs/app/persistence"
log "github.com/sirupsen/logrus"
)
// Elector — выборы лидера через Redis-лок.
type Elector struct {
key string
id string
ttl time.Duration
interval time.Duration
mu sync.RWMutex
isLeader bool
stopCh chan struct{}
stopped bool
}
// New — создаёт Elector. key — ключ лока, id — уникальный id инстанса.
func New(key, id string, ttl, interval time.Duration) *Elector {
return &Elector{
key: key,
id: id,
ttl: ttl,
interval: interval,
stopCh: make(chan struct{}),
}
}
// Start — запускает цикл выборов в фоне (первая попытка — сразу).
func (e *Elector) Start() {
log.Infof("leader: старт выборов (key=%s, id=%s, ttl=%s)", e.key, e.id, e.ttl)
go e.loop()
}
// IsLeader — текущий статус (готовность обслуживать).
func (e *Elector) IsLeader() bool {
e.mu.RLock()
defer e.mu.RUnlock()
return e.isLeader
}
// Stop — останавливает цикл и освобождает лок (если были лидером).
func (e *Elector) Stop() {
e.mu.Lock()
if e.stopped {
e.mu.Unlock()
return
}
e.stopped = true
close(e.stopCh)
was := e.isLeader
e.mu.Unlock()
if was {
persistence.ReleaseLock(e.key, e.id)
}
}
func (e *Elector) setLeader(v bool) {
e.mu.Lock()
changed := e.isLeader != v
e.isLeader = v
e.mu.Unlock()
if changed {
if v {
log.Info("leader: инстанс стал ЛИДЕРОМ (активный)")
} else {
log.Warn("leader: потерян лидерский лок (пассивный)")
}
}
}
func (e *Elector) loop() {
t := time.NewTicker(e.interval)
defer t.Stop()
e.step()
for {
select {
case <-e.stopCh:
return
case <-t.C:
e.step()
}
}
}
func (e *Elector) step() {
if e.IsLeader() {
ok, err := persistence.RefreshLock(e.key, e.id, e.ttl)
if err != nil {
log.Errorf("leader: refresh: %v", err)
return
}
if !ok {
e.setLeader(false)
}
return
}
ok, err := persistence.TryAcquireLock(e.key, e.id, e.ttl)
if err != nil {
log.Errorf("leader: acquire: %v", err)
return
}
if ok {
e.setLeader(true)
}
}