Files
Bridge-and-Join-s/internal/lkgateway/igwwatch.go
T

227 lines
8.9 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.
package lkgateway
// igwwatch.go — сторож интеграционного шлюза НРД (ИШ). ИШ может «жить» как процесс
// (systemd считает службу active), но не слушать порт: тогда bj не получает входящие
// и не может отправить ни одного пакета. Сторож каждые 30 с проверяет, отвечает ли ИШ
// по HTTP, и:
// - после 2 мин недоступности — оповещение (лента «Новости» + e-mail/Zabbix) и
// попытка перезапустить службу igate (если ИШ на этом сервере; через root-демон
// bj-keymedia, список служб закрытый);
// - перезапуск не помог — оповещение с числом попыток; повтор раз в 20 мин, до 3
// попыток за эпизод, дальше напоминание раз в 2 ч;
// - ИШ снова отвечает — оповещение «восстановлен» с длительностью простоя.
// Состояние видно в шапке (индикатор «НРД на связи» → «ИШ НРД недоступен»).
import (
"context"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
const (
igwCheckEvery = 30 * time.Second
igwAlertAfter = 2 * time.Minute
igwRestartEvery = 20 * time.Minute
igwRestartMax = 3
igwRemindEvery = 2 * time.Hour
igwRestartSettle = 4 * time.Minute
igwServiceUnit = "igate"
)
type igwWatchState struct {
mu sync.Mutex
Checked bool
Up bool
DownSince time.Time
LastOK time.Time
LastErr string
Alerted bool
Restarts int
LastRestart time.Time
LastRemind time.Time
RestartMsg string
}
var igwWatch igwWatchState
// igwStatus — для шапки и страницы статуса: недоступен ли ИШ и сколько.
func igwStatus() (down bool, since time.Duration, detail string) {
igwWatch.mu.Lock()
defer igwWatch.mu.Unlock()
if !igwWatch.Checked || igwWatch.Up {
return false, 0, ""
}
d := fmt.Sprintf("нет ответа с %s: %s", igwWatch.DownSince.Format("02.01 15:04"), igwWatch.LastErr)
if igwWatch.Restarts > 0 {
d += fmt.Sprintf("; попыток перезапуска: %d (%s)", igwWatch.Restarts, igwWatch.RestartMsg)
}
return true, time.Since(igwWatch.DownSince).Round(time.Minute), d
}
// igwProbe — отвечает ли ИШ по HTTP (любой HTTP-ответ = жив; отказ соединения/таймаут = нет).
func igwProbe(ctx context.Context, base string) error {
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(base, "/")+"/", nil)
if err != nil {
return err
}
resp, err := (&http.Client{Transport: &http.Transport{Proxy: nil}}).Do(req)
if err != nil {
return err
}
resp.Body.Close()
return nil
}
func igwLocal(base string) bool {
u, err := url.Parse(base)
if err != nil {
return false
}
h := u.Hostname()
return h == "localhost" || h == "127.0.0.1" || h == "::1"
}
func humanDur(d time.Duration) string {
d = d.Round(time.Minute)
if d < time.Hour {
return fmt.Sprintf("%d мин", int(d.Minutes()))
}
if d < 48*time.Hour {
return fmt.Sprintf("%d ч %d мин", int(d.Hours()), int(d.Minutes())%60)
}
return fmt.Sprintf("%d сут %d ч", int(d.Hours())/24, int(d.Hours())%24)
}
// igwWatchLoop — фоновый сторож (запускается через superviseLoop).
func (s *Server) igwWatchLoop(ctx context.Context) {
healthRegister("igw-watch", 2*igwCheckEvery)
t := time.NewTicker(igwCheckEvery)
defer t.Stop()
for {
igwWatchTick(ctx, s.rc)
healthBeat("igw-watch")
select {
case <-ctx.Done():
return
case <-t.C:
}
}
}
func igwWatchTick(ctx context.Context, rc *RuntimeConfig) {
base := strings.TrimSpace(rc.Snapshot().NSD.IGWBaseURL)
if base == "" {
igwWatch.mu.Lock()
igwWatch.Checked = false
igwWatch.mu.Unlock()
return
}
err := igwProbe(ctx, base)
now := time.Now()
w := &igwWatch
w.mu.Lock()
w.Checked = true
if err == nil {
wasDown, since, alerted, restarts := !w.Up && !w.DownSince.IsZero(), w.DownSince, w.Alerted, w.Restarts
w.Up, w.LastOK, w.LastErr, w.DownSince, w.Alerted, w.Restarts, w.RestartMsg = true, now, "", time.Time{}, false, 0, ""
w.mu.Unlock()
if wasDown && alerted {
how := "сам"
if restarts > 0 {
how = fmt.Sprintf("после перезапуска bj (попыток: %d)", restarts)
}
log.Printf("igw-watch: ИШ снова отвечает (%s), простой %s", how, humanDur(now.Sub(since)))
raiseSystemAlert(rc, "ИШ НРД снова доступен", fmt.Sprintf("Интеграционный шлюз НРД (%s) отвечает — восстановился %s. Простой %s (с %s). Обмен с НРД возобновлён: входящие переводы будут получены, ожидающие отправки уйдут.",
base, how, humanDur(now.Sub(since)), since.Format("02.01 15:04")))
}
return
}
if w.Up || w.DownSince.IsZero() {
w.Up, w.DownSince = false, now
log.Printf("igw-watch: ИШ НРД не отвечает (%s): %v", base, err)
}
w.LastErr = shortErr(err)
down := now.Sub(w.DownSince)
needAlert := !w.Alerted && down >= igwAlertAfter
canRestart := igwLocal(base) && down >= igwAlertAfter && w.Restarts < igwRestartMax &&
(w.LastRestart.IsZero() || now.Sub(w.LastRestart) >= igwRestartEvery)
remind := w.Alerted && !canRestart && w.Restarts >= igwRestartMax && now.Sub(w.LastRemind) >= igwRemindEvery
if needAlert {
w.Alerted, w.LastRemind = true, now
}
if remind {
w.LastRemind = now
}
if canRestart {
w.Restarts++
w.LastRestart = now
}
attempt, since, lastErr := w.Restarts, w.DownSince, w.LastErr
w.mu.Unlock()
if needAlert {
plan := "ИШ на другом сервере — перезапустите службу ИШ там."
if igwLocal(base) {
plan = "bj пробует перезапустить службу " + igwServiceUnit + " автоматически и сообщит результат."
}
raiseSystemAlert(rc, "ИШ НРД недоступен", fmt.Sprintf("Интеграционный шлюз НРД (%s) не отвечает с %s (%s): %s. Пока ИШ недоступен, входящие переводы не принимаются, отправка в НРД невозможна. %s",
base, since.Format("02.01 15:04"), humanDur(down), lastErr, plan))
}
if remind {
raiseSystemAlert(rc, "ИШ НРД всё ещё недоступен", fmt.Sprintf("ИШ (%s) не отвечает %s (с %s); автоматические перезапуски (%d) не помогли: %s. Нужна проверка администратором: journalctl -u %s, igate-cli.",
base, humanDur(down), since.Format("02.01 15:04"), igwRestartMax, lastErr, igwServiceUnit))
}
if canRestart {
go igwTryRestart(rc, base, attempt)
}
}
// igwTryRestart — перезапуск службы ИШ и проверка, поднялся ли он.
func igwTryRestart(rc *RuntimeConfig, base string, attempt int) {
log.Printf("igw-watch: перезапуск службы %s (попытка %d из %d)", igwServiceUnit, attempt, igwRestartMax)
out, err := restartUnit(context.Background(), igwServiceUnit)
msg := out
if err != nil {
msg = "служба не перезапущена: " + err.Error()
} else {
deadline := time.Now().Add(igwRestartSettle)
for time.Now().Before(deadline) {
time.Sleep(15 * time.Second)
if igwProbe(context.Background(), base) == nil {
msg = "перезапуск выполнен, ИШ ответил"
break
}
msg = fmt.Sprintf("служба перезапущена, но ИШ не отвечает %s после перезапуска", humanDur(igwRestartSettle))
}
}
igwWatch.mu.Lock()
igwWatch.RestartMsg = msg
up := igwWatch.Up
igwWatch.mu.Unlock()
log.Printf("igw-watch: попытка %d: %s", attempt, msg)
if strings.HasPrefix(msg, "перезапуск выполнен") || up {
return // «восстановлен» сообщит igwWatchTick
}
next := "следующая попытка через 20 мин"
if attempt >= igwRestartMax {
next = "попытки исчерпаны — нужна проверка администратором (journalctl -u " + igwServiceUnit + ")"
}
raiseSystemAlert(rc, fmt.Sprintf("Перезапуск ИШ НРД не помог (попытка %d из %d)", attempt, igwRestartMax),
fmt.Sprintf("bj перезапустил службу %s, но шлюз %s не отвечает: %s. %s.", igwServiceUnit, base, msg, next))
}
func shortErr(err error) string {
s := err.Error()
if i := strings.LastIndex(s, ": "); i > 0 && len(s) > 120 {
s = s[i+2:]
}
return s
}