diff --git a/internal/lkgateway/nsdhealth.go b/internal/lkgateway/nsdhealth.go index 4889a6f..b7d446e 100644 --- a/internal/lkgateway/nsdhealth.go +++ b/internal/lkgateway/nsdhealth.go @@ -39,6 +39,44 @@ const ( var nsdHistoryFile = "/var/lib/bj/.bj/nsd-health.json" +// nsdProbeFile — время последней проверки M2M и ответа на неё: переживает перезапуск bj. Без него каждый +// перезапуск (установка обновления) слал запрос справочника через минуту после старта; 22.10.2026 на 9 +// набралось больше 100 запросов за день, и с 14:01 НРД перестал на них отвечать. +var nsdProbeFile = "/var/lib/bj/.bj/nsd-probe.json" + +type nsdProbeSaved struct { + Sent time.Time `json:"sent"` + Reply time.Time `json:"reply"` +} + +func nsdProbeSave() { + nsdH.Lock() + v := nsdProbeSaved{Sent: nsdH.ProbeSentAt, Reply: nsdH.ProbeReplyAt} + nsdH.Unlock() + if raw, err := json.Marshal(v); err == nil { + _ = os.WriteFile(nsdProbeFile, raw, 0o600) + } +} + +// nsdProbeLoad — восстановить время проверки после перезапуска; возвращает, когда слать следующую. +func nsdProbeLoad(now time.Time) time.Duration { + var v nsdProbeSaved + if raw, err := os.ReadFile(nsdProbeFile); err == nil && json.Unmarshal(raw, &v) == nil { + nsdH.Lock() + if nsdH.ProbeSentAt.IsZero() { + nsdH.ProbeSentAt = v.Sent + } + if v.Reply.After(nsdH.ProbeReplyAt) { + nsdH.ProbeReplyAt = v.Reply + } + nsdH.Unlock() + if left := nsdProbeEvery - now.Sub(v.Sent); left > time.Minute { + return left + } + } + return time.Minute +} + // Состояния участков по возрастанию тяжести. const ( segOK = "ok" @@ -105,11 +143,12 @@ type nsdSample struct { // nsdProbeReply — пришёл справочник: НРД ответил (по нашей проверке или сам). func nsdProbeReply(at time.Time) { nsdH.Lock() - defer nsdH.Unlock() if !nsdH.ProbeSentAt.IsZero() && nsdH.ProbeReplyAt.Before(nsdH.ProbeSentAt) && at.After(nsdH.ProbeSentAt) { nsdH.ProbeLatency = at.Sub(nsdH.ProbeSentAt) } nsdH.ProbeReplyAt = at + nsdH.Unlock() + nsdProbeSave() } // nsdDelivery — итог передачи пакета в НРД (из клиента ИШ). @@ -200,7 +239,12 @@ func nsdHealthNow(rc *RuntimeConfig, now time.Time) NSDHealth { case nsdH.ProbeErr != "": m.State, m.Text, m.Detail = segErr, "запрос проверки не доставлен", nsdH.ProbeErr case pending && now.Sub(nsdH.ProbeSentAt) >= nsdProbeErrAfter: - m.State, m.Text = segErr, "НРД не ответил на проверку "+humanDur(now.Sub(nsdH.ProbeSentAt)) + // запрос доставлен, но ответа нет: НРД отвечает на запрос справочника не всегда (ограничивает их + // число) — это не обрыв связи, поэтому жёлтый; обрыв видно по участкам «Шлюз» и «НРД» + m.State, m.Text = segWarn, "НРД не ответил на проверку "+humanDur(now.Sub(nsdH.ProbeSentAt)) + if !nsdH.ProbeReplyAt.IsZero() { + m.Text += " (прошлый ответ " + agoText(nsdH.ProbeReplyAt, now) + ")" + } case pending && now.Sub(nsdH.ProbeSentAt) >= nsdProbeWarnAfter: m.State, m.Text = segWarn, "ждём ответ НРД на проверку "+humanDur(now.Sub(nsdH.ProbeSentAt)) case pending && nsdH.ProbeReplyAt.IsZero(): @@ -239,6 +283,9 @@ func nsdHealthNow(rc *RuntimeConfig, now time.Time) NSDHealth { // серым работающий обмен. Серый — только когда по известным участкам связь не подтверждена. h.State = segOK for _, s := range h.Segs { + if s.Key == "m2m" && ns.State == segOK && s.State != segErr { + continue // НРД отвечает шлюзу — задержка ответа на справочник не красит шапку (видна в панели) + } if s.State != segUnknown && segRank(s.State) > segRank(h.State) { h.State = s.State } @@ -284,6 +331,7 @@ func (s *Server) sendNSDProbe(ctx context.Context) { nsdH.Lock() nsdH.ProbeSentAt, nsdH.ProbeErr = time.Now(), "" nsdH.Unlock() + nsdProbeSave() cctx, cancel := context.WithTimeout(ctx, 40*time.Second) defer cancel() if _, err := sendHandbookRequest(cctx, s.igwClient, s.igwChannel); err != nil { @@ -298,7 +346,7 @@ func (s *Server) sendNSDProbe(ctx context.Context) { func (s *Server) nsdHealthLoop(ctx context.Context) { healthRegister("nsd-health", 3*time.Minute) nsdHistoryLoad() - probeT := time.NewTimer(time.Minute) + probeT := time.NewTimer(nsdProbeLoad(time.Now())) // после перезапуска — не раньше часа с прошлой проверки defer probeT.Stop() minute := time.NewTicker(time.Minute) defer minute.Stop() diff --git a/internal/lkgateway/nsdhealth_test.go b/internal/lkgateway/nsdhealth_test.go index fa2b97d..fe61fc1 100644 --- a/internal/lkgateway/nsdhealth_test.go +++ b/internal/lkgateway/nsdhealth_test.go @@ -32,6 +32,7 @@ func nsdTestReset(t *testing.T) { sendFails.loaded = false oldHist := nsdHistoryFile nsdHistoryFile = filepath.Join(t.TempDir(), "h.json") + nsdProbeFile = filepath.Join(t.TempDir(), "p.json") t.Cleanup(func() { sendFailFile, sendFails.loaded, nsdHistoryFile = old, false, oldHist igwWatch.mu.Lock() @@ -98,7 +99,8 @@ func TestNSDHealthStates(t *testing.T) { nsdH.ProbeSentAt = now.Add(-12 * time.Minute) nsdH.ProbeReplyAt = now.Add(-25 * time.Minute) nsdH.Unlock() - if h = nsdHealthNow(rc, now); segState(h, "m2m") != segErr { + // молчание на запрос справочника при живом НРД — жёлтый участок, шапку не красит + if h = nsdHealthNow(rc, now); segState(h, "m2m") != segWarn || h.State != segOK { t.Fatalf("НРД молчит на проверку 12 мин: %+v", h) } }