Связь с НРД: проверка M2M не повторяется при каждом перезапуске bj; молчание НРД на справочник — жёлтый, шапку не красит

22.10.2026 на 9 за день ушло больше 100 запросов #M2MHR (проверка + каждый перезапуск), с 14:01 НРД перестал отвечать — шапка уходила в красный при работающем обмене.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
fontvielle
2026-10-22 16:12:03 +03:00
parent 7fd4bd0af9
commit d8649e2765
2 changed files with 54 additions and 4 deletions
+51 -3
View File
@@ -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()
+3 -1
View File
@@ -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)
}
}