Отправка в НРД с подтверждением доставки: 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:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user