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 igwLocal(base) { nsdLinkTick(rc, now) } 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 }