// 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) } }