Отправка в НРД с подтверждением доставки: ERROR от ИШ — заявка «Не отправлена», без BJ_SENT; «Отправить повторно» тем же GUID; сторож связи ИШ↔НРД по журналу ИШ

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-09-25 15:57:07 +03:00
parent eecb3b7923
commit c2aaa09fe3
15 changed files with 651 additions and 33 deletions
+157
View File
@@ -0,0 +1,157 @@
package lkgateway
// Сторож связи ИШ ↔ НРД. ИШ может отвечать bj, но сам не достукиваться до НРД (контур НРД отдаёт
// 404/500/502): тогда пакеты получают ERROR, входящие не приходят. В REST ИШ такого признака нет,
// поэтому смотрим журнал ИШ (если он на этом сервере): итог последних опросов НРД
// «GetPackageListFull (GET) -> OK | #код». НРД не отвечает nsdAlertAfter — оповещение; снова
// отвечает — оповещение с простоем и числом недоставленных переводов (их нужно отправить повторно).
import (
"fmt"
"io"
"log"
"os"
"regexp"
"strings"
"sync"
"time"
)
const (
nsdAlertAfter = 10 * time.Minute
nsdRemindEvery = 2 * time.Hour
nsdLogTail = 512 << 10
)
var igateLogFile = "/var/lib/igate/.local/share/NSD Integration Gateway/logs-svc/log.txt"
// PID_363248 2026-09-25 15:42:59.1873 T16 DEBUG [WSL] WSL TEST3 GOST: GetPackageListFull (GET) -> #404 (HTTP Error 404: NotFound)
var nsdPollRe = regexp.MustCompile(`^PID_\d+ (\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)\.\d+ .*WSL ([^:]+): GetPackageListFull \(GET\) -> (OK|#\d+[^\r\n]*)`)
type nsdPoll struct {
At time.Time
Contour string
OK bool
Result string
}
// nsdLastPolls — итоги опросов НРД из хвоста журнала ИШ: последний опрос и первый неудачный после
// последнего успешного (начало текущего сбоя; nil — сбоя нет).
func nsdLastPolls(tail []byte) (last, firstFail *nsdPoll) {
for _, ln := range strings.Split(string(tail), "\n") {
m := nsdPollRe.FindStringSubmatch(strings.TrimRight(ln, "\r"))
if m == nil {
continue
}
at, err := time.ParseInLocation("2006-01-02 15:04:05", m[1], time.Local)
if err != nil {
continue
}
p := &nsdPoll{At: at, Contour: m[2], OK: m[3] == "OK", Result: m[3]}
last = p
if p.OK {
firstFail = nil
} else if firstFail == nil {
firstFail = p
}
}
return last, firstFail
}
func readTail(path string, n int64) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return nil, err
}
off := st.Size() - n
if off < 0 {
off = 0
}
if _, err := f.Seek(off, io.SeekStart); err != nil {
return nil, err
}
return io.ReadAll(f)
}
var nsdWatch struct {
sync.Mutex
DownSince time.Time // первый неудачный опрос после последнего успешного
LastErr string
Contour string
Alerted bool
LastRemind time.Time
}
// nsdLinkStatus — для страницы статуса: не отвечает ли НРД шлюзу.
func nsdLinkStatus() (down bool, detail string) {
nsdWatch.Lock()
defer nsdWatch.Unlock()
if nsdWatch.DownSince.IsZero() {
return false, ""
}
return true, fmt.Sprintf("НРД (%s) не отвечает шлюзу с %s: %s", nsdWatch.Contour, nsdWatch.DownSince.Format("02.01 15:04"), nsdWatch.LastErr)
}
// nsdLinkTick — вызывается сторожем ИШ, когда сам ИШ отвечает и он на этом сервере.
func nsdLinkTick(rc *RuntimeConfig, now time.Time) {
tail, err := readTail(igateLogFile, nsdLogTail)
if err != nil {
return
}
last, firstFail := nsdLastPolls(tail)
if last == nil {
return
}
w := &nsdWatch
w.Lock()
if last.OK {
since, alerted := w.DownSince, w.Alerted
w.DownSince, w.LastErr, w.Alerted = time.Time{}, "", false
w.Unlock()
if !since.IsZero() {
log.Printf("nsd-watch: НРД (%s) снова отвечает шлюзу, простой %s", last.Contour, humanDur(last.At.Sub(since)))
}
if alerted {
body := fmt.Sprintf("Контур НРД %s снова отвечает интеграционному шлюзу (простой %s, с %s).", last.Contour, humanDur(last.At.Sub(since)), since.Format("02.01 15:04"))
if n := sendFailCount(); n > 0 {
body += fmt.Sprintf(" Недоставленных переводов: %d — отправьте их повторно из карточки заявки («Отправить повторно»).", n)
}
raiseSystemAlert(rc, "НРД снова на связи", body)
}
return
}
if w.DownSince.IsZero() {
w.DownSince = firstFail.At
log.Printf("nsd-watch: НРД (%s) не отвечает шлюзу: %s", last.Contour, last.Result)
}
w.LastErr, w.Contour = last.Result, last.Contour
down := now.Sub(w.DownSince)
alert := !w.Alerted && down >= nsdAlertAfter
remind := w.Alerted && now.Sub(w.LastRemind) >= nsdRemindEvery
if alert || remind {
w.Alerted, w.LastRemind = true, now
}
since, lastErr := w.DownSince, w.LastErr
w.Unlock()
if !alert && !remind {
return
}
title := "НРД не отвечает шлюзу"
if remind {
title = "НРД всё ещё не отвечает шлюзу"
}
raiseSystemAlert(rc, title, fmt.Sprintf("Интеграционный шлюз работает, но контур НРД %s не отвечает %s (с %s): %s. Пока так, отправки в НРД не доходят (заявка остаётся «Не отправлена»), входящие не поступают. Это сторона НРД — при затяжном сбое обратитесь в поддержку НРД. Когда связь восстановится, bj сообщит.",
last.Contour, humanDur(down), since.Format("02.01 15:04"), lastErr))
}
func sendFailCount() int {
sendFails.Lock()
defer sendFails.Unlock()
sendFailLoad()
return len(sendFails.m)
}