Сторож ИШ НРД: проверка каждые 30 с, оповещение через 2 мин недоступности, автоперезапуск igate (до 3 попыток, раз в 20 мин), оповещения «не помог»/«восстановлен», красный индикатор в шапке; igate в списке служб root-демона
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ process_one(){
|
|||||||
# (и сам этот демон) отсюда тронуть нельзя.
|
# (и сам этот демон) отсюда тронуть нельзя.
|
||||||
unit-restart)
|
unit-restart)
|
||||||
case "$extra" in
|
case "$extra" in
|
||||||
bj-server|bj-npr|bj-quikexport|bj-lk|bj-crypto)
|
bj-server|bj-npr|bj-quikexport|bj-lk|bj-crypto|igate)
|
||||||
systemctl reset-failed "$extra" >/dev/null 2>&1
|
systemctl reset-failed "$extra" >/dev/null 2>&1
|
||||||
if err=$(systemctl restart "$extra" 2>&1); then
|
if err=$(systemctl restart "$extra" 2>&1); then
|
||||||
out="{\"ok\":true,\"output\":\"перезапущено: $extra\"}"
|
out="{\"ok\":true,\"output\":\"перезапущено: $extra\"}"
|
||||||
|
|||||||
@@ -356,6 +356,8 @@ type page struct {
|
|||||||
IsMockMode bool // true если ИШ или СКЗИ не настроены — обмена с НРД нет
|
IsMockMode bool // true если ИШ или СКЗИ не настроены — обмена с НРД нет
|
||||||
MockReason string // короткое описание причины (у клиента — без «mock»)
|
MockReason string // короткое описание причины (у клиента — без «mock»)
|
||||||
NPR bool // модуль «НПР / УДС» включён и разрешён лицензией
|
NPR bool // модуль «НПР / УДС» включён и разрешён лицензией
|
||||||
|
IGWDown string // ИШ НРД не отвечает: «N мин» (пусто — отвечает или не проверялся)
|
||||||
|
IGWDetail string
|
||||||
DevMode bool // режим разработки (сервер лицензий): видны служебные разделы
|
DevMode bool // режим разработки (сервер лицензий): видны служебные разделы
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,6 +722,9 @@ func nowPage(title, active string) page {
|
|||||||
if globalRC != nil {
|
if globalRC != nil {
|
||||||
p.NPR = nprAvailable(globalRC)
|
p.NPR = nprAvailable(globalRC)
|
||||||
p.DevMode = devMode(globalRC)
|
p.DevMode = devMode(globalRC)
|
||||||
|
if down, d, det := igwStatus(); down {
|
||||||
|
p.IGWDown, p.IGWDetail = humanDur(d), det
|
||||||
|
}
|
||||||
s := globalRC.Snapshot()
|
s := globalRC.Snapshot()
|
||||||
switch {
|
switch {
|
||||||
case s.NSD.IGWBaseURL == "":
|
case s.NSD.IGWBaseURL == "":
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -279,6 +279,7 @@ func (s *Server) Run(ctx context.Context) error {
|
|||||||
// Все фоновые горутины — под supervise: паника ловится recover(), пишется
|
// Все фоновые горутины — под supervise: паника ловится recover(), пишется
|
||||||
// в лог и в ленту, горутина перезапускается. Один сбой не роняет сервис.
|
// в лог и в ленту, горутина перезапускается. Один сбой не роняет сервис.
|
||||||
go superviseLoop(ctx, "consume-decisions", s.rc, s.consumeDecisions)
|
go superviseLoop(ctx, "consume-decisions", s.rc, s.consumeDecisions)
|
||||||
|
go superviseLoop(ctx, "igw-watch", s.rc, s.igwWatchLoop) // сторож ИШ НРД: оповещения и автоперезапуск
|
||||||
|
|
||||||
// Поллер входящих от НРД (только в реальном режиме ИШ): забирает
|
// Поллер входящих от НРД (только в реальном режиме ИШ): забирает
|
||||||
// ответы робота/контрагента и применяет их через ApplyDecision.
|
// ответы робота/контрагента и применяет их через ApplyDecision.
|
||||||
|
|||||||
@@ -343,8 +343,9 @@ label { font-size:13px; font-weight:500; }
|
|||||||
</nav>
|
</nav>
|
||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
<span class="sys-block">
|
<span class="sys-block">
|
||||||
<span class="sys-status {{if .IsMockMode}}warn{{end}}" title="Состояние обмена с НРД">
|
{{if and .IGWDown (not .IsMockMode)}}<span class="sys-status err" title="{{.IGWDetail}}" style="background:var(--err-weak,#fbe9e7);color:var(--err,#b3261e)">
|
||||||
<span class="pulse"></span>{{if .IsMockMode}}{{if devOn}}Эмуляция{{else}}Нет связи с НРД{{end}}{{else}}НРД на связи{{end}}</span>
|
<span class="pulse"></span>ИШ НРД недоступен · {{.IGWDown}}</span>{{else}}<span class="sys-status {{if .IsMockMode}}warn{{end}}" title="Состояние обмена с НРД">
|
||||||
|
<span class="pulse"></span>{{if .IsMockMode}}{{if devOn}}Эмуляция{{else}}Нет связи с НРД{{end}}{{else}}НРД на связи{{end}}</span>{{end}}
|
||||||
<span class="clock">{{.Now}}</span>
|
<span class="clock">{{.Now}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="dd user-dd{{if or (eq .Active "setup") (eq .Active "status") (eq .Active "help")}} active{{end}}">
|
<span class="dd user-dd{{if or (eq .Active "setup") (eq .Active "status") (eq .Active "help")}} active{{end}}">
|
||||||
|
|||||||
Reference in New Issue
Block a user