Files
EvoBGP/internal/httpapi/peers_live.go
T
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 17:23:59 +07:00

365 lines
9.5 KiB
Go

package httpapi
import (
"net/netip"
"strings"
"time"
"evobgp/internal/birdfmt"
"evobgp/internal/store"
)
type speakerBGPLive struct {
SpeakerID string
Label string
Sessions []birdfmt.BGPSession
Error string
Polled bool
ObservedAt time.Time
ReceivedAt time.Time
LastSuccessAt time.Time
Source string
ObservationOverride *liveObservation
}
type liveObservation struct {
ObservedAt *string `json:"observed_at"`
ReceivedAt *string `json:"received_at"`
LastSuccessAt *string `json:"last_success_at"`
ExpiresAt *string `json:"expires_at,omitempty"`
Source string `json:"source"`
Quality string `json:"quality"`
ErrorCode string `json:"error_code,omitempty"`
}
func liveTime(t time.Time) *string {
if t.IsZero() {
return nil
}
v := t.UTC().Format(time.RFC3339Nano)
return &v
}
func (v speakerBGPLive) observed() bool {
return v.Polled || v.Error != "" || len(v.Sessions) > 0
}
func (v speakerBGPLive) successful() bool {
return v.observed() && v.Error == ""
}
func (v speakerBGPLive) observation() liveObservation {
if v.ObservationOverride != nil {
return *v.ObservationOverride
}
o := liveObservation{ObservedAt: liveTime(v.ObservedAt), ReceivedAt: liveTime(v.ReceivedAt), LastSuccessAt: liveTime(v.LastSuccessAt), Source: v.Source, Quality: "unknown"}
if o.Source == "" {
o.Source = "none"
}
switch {
case v.successful():
o.Quality = "ok"
case v.Error != "" && !v.LastSuccessAt.IsZero():
o.Quality = "stale"
o.ErrorCode = liveErrorCode(v.Error)
case v.Error != "":
o.Quality = "unreachable"
o.ErrorCode = liveErrorCode(v.Error)
default:
o.ErrorCode = "not_polled"
}
return o
}
func liveErrorCode(err string) string {
if strings.Contains(strings.ToLower(err), "not configured") || strings.Contains(strings.ToLower(err), "missing") {
return "not_configured"
}
return "poll_failed"
}
type peerSessionOnSpeaker struct {
SpeakerID string `json:"speaker_id"`
Label string `json:"label"`
State string `json:"state"`
PollError string `json:"poll_error,omitempty"`
Observation liveObservation `json:"observation"`
}
type liveSpeakerPoll struct {
SpeakerID string `json:"speaker_id"`
Label string `json:"label"`
OK bool `json:"ok"`
SessionCount *int `json:"session_count"`
PollError string `json:"poll_error,omitempty"`
Observation liveObservation `json:"observation"`
}
func speakerDisplayLabel(sp *store.Speaker) string {
if sp == nil {
return ""
}
meta := store.ParseSpeakerMeta(sp.MetaJSON)
host := strings.TrimSpace(meta.AgentDomain)
if host == "" {
host = strings.TrimSpace(sp.Endpoint)
}
if strings.EqualFold(strings.TrimSpace(sp.Role), "master") {
if host != "" {
return "CP · " + host
}
return "CP (master)"
}
if host != "" {
return host
}
return sp.ID
}
func liveSpeakerPollJSON(views []speakerBGPLive) []liveSpeakerPoll {
out := make([]liveSpeakerPoll, 0, len(views))
for _, v := range views {
var sessionCount *int
if v.successful() {
count := len(v.Sessions)
sessionCount = &count
}
out = append(out, liveSpeakerPoll{
SpeakerID: v.SpeakerID,
Label: v.Label,
OK: v.successful(),
SessionCount: sessionCount,
PollError: v.Error,
Observation: v.observation(),
})
}
return out
}
func findPeerSession(sessions []birdfmt.BGPSession, protoName string, neighbor netip.Addr, hasNeighbor bool) *birdfmt.BGPSession {
for i := range sessions {
if peerSessionMatches(sessions[i], protoName, neighbor, hasNeighbor) {
return &sessions[i]
}
}
return nil
}
func matchPeerOnSpeakers(peer *store.BGPPeer, views []speakerBGPLive) (
bestState string,
connectedID string,
connectedLabel string,
establishedOn []peerSessionOnSpeaker,
on []peerSessionOnSpeaker,
mismatch bool,
) {
if peer == nil {
return "", "", "", nil, nil, false
}
neighbor, hasNeighbor := store.ParsePeerNeighbor(peer.Neighbor)
protoName := birdfmt.PeerProtocolName(peer.ID)
for _, v := range views {
if !v.successful() {
on = append(on, peerSessionOnSpeaker{
SpeakerID: v.SpeakerID,
Label: v.Label,
PollError: v.Error,
Observation: v.observation(),
})
continue
}
sess := findPeerSession(v.Sessions, protoName, neighbor, hasNeighbor)
if sess == nil {
on = append(on, peerSessionOnSpeaker{
SpeakerID: v.SpeakerID,
Label: v.Label,
State: "absent",
Observation: v.observation(),
})
continue
}
hit := peerSessionOnSpeaker{
SpeakerID: v.SpeakerID,
Label: v.Label,
State: sess.State,
Observation: v.observation(),
}
on = append(on, hit)
if strings.EqualFold(strings.TrimSpace(sess.State), "Established") {
establishedOn = append(establishedOn, hit)
}
if bestState == "" || sessionStateRank(sess.State) > sessionStateRank(bestState) {
bestState = sess.State
}
}
if len(establishedOn) > 0 {
bestState = "Established"
labels := make([]string, 0, len(establishedOn))
for _, e := range establishedOn {
labels = append(labels, e.Label)
}
connectedLabel = strings.Join(labels, ", ")
if len(establishedOn) == 1 {
connectedID = establishedOn[0].SpeakerID
}
} else if len(on) == 1 && on[0].PollError == "" && on[0].State != "" {
connectedID = on[0].SpeakerID
connectedLabel = on[0].Label
}
if peer.SpeakerID != nil && strings.TrimSpace(*peer.SpeakerID) != "" && len(establishedOn) > 0 {
want := strings.TrimSpace(*peer.SpeakerID)
found := false
for _, e := range establishedOn {
if strings.EqualFold(strings.TrimSpace(e.SpeakerID), want) {
found = true
break
}
}
mismatch = !found
}
return bestState, connectedID, connectedLabel, establishedOn, on, mismatch
}
func peerSessionMatches(sess birdfmt.BGPSession, protoName string, neighbor netip.Addr, hasNeighbor bool) bool {
if strings.EqualFold(strings.TrimSpace(sess.Name), protoName) {
return true
}
if !hasNeighbor || strings.TrimSpace(sess.Neighbor) == "" {
return false
}
peerAddr, ok := store.ParsePeerNeighbor(sess.Neighbor)
if !ok {
return false
}
return peerAddr == neighbor
}
func sessionStateRank(state string) int {
switch strings.ToLower(strings.TrimSpace(state)) {
case "established":
return 100
case "openconfirm", "opensent":
return 80
case "active", "connect":
return 60
case "idle":
return 20
default:
return 10
}
}
func applyPeerLiveFields(row map[string]any, peer *store.BGPPeer, views []speakerBGPLive) {
expected := expectedPeerViews(peer, views)
state, _, _, _, _, _ := matchPeerOnSpeakers(peer, expected)
_, connID, connLabel, establishedOn, on, mismatch := matchPeerOnSpeakers(peer, views)
row["session_on_speakers"] = on
row["established_on_speakers"] = establishedOn
row["session_conflict"] = false
row["session_mismatch"] = mismatch
row["last_known_session_state"] = row["session_state"]
row["session_state"] = nil
if state != "" {
row["session_state"] = state
}
if state == "" && len(expected) > 0 && allSpeakerPollsSuccessful(expected) {
row["session_state"] = "absent"
}
row["live_observation"] = peerLiveObservation(expected)
if connLabel != "" {
row["connected_speaker_label"] = connLabel
}
row["connected_speaker_id"] = peerLiveSpeakerIDOrNull(connID)
}
func expectedPeerViews(peer *store.BGPPeer, views []speakerBGPLive) []speakerBGPLive {
if peer == nil || peer.SpeakerID == nil || strings.TrimSpace(*peer.SpeakerID) == "" {
return views
}
want := strings.TrimSpace(*peer.SpeakerID)
expected := make([]speakerBGPLive, 0, 1)
for _, v := range views {
if strings.EqualFold(v.SpeakerID, want) {
expected = append(expected, v)
}
}
return expected
}
func allSpeakerPollsSuccessful(views []speakerBGPLive) bool {
for _, v := range views {
if !v.successful() {
return false
}
}
return true
}
func peerLiveObservation(views []speakerBGPLive) liveObservation {
if len(views) == 0 {
return speakerBGPLive{}.observation()
}
result := liveObservation{Source: views[0].Source, Quality: "unknown"}
if result.Source == "" {
result.Source = "none"
}
var latestObserved, latestReceived, latestSuccess time.Time
allOK, allFailed, anyStale := true, true, false
for _, v := range views {
o := v.observation()
if v.ObservedAt.After(latestObserved) {
latestObserved = v.ObservedAt
}
if v.ReceivedAt.After(latestReceived) {
latestReceived = v.ReceivedAt
}
if v.LastSuccessAt.After(latestSuccess) {
latestSuccess = v.LastSuccessAt
}
allOK = allOK && v.successful()
allFailed = allFailed && o.Quality == "unreachable"
anyStale = anyStale || o.Quality == "stale"
if result.ErrorCode == "" && o.ErrorCode != "" {
result.ErrorCode = o.ErrorCode
}
if o.ExpiresAt != nil {
expires, err := time.Parse(time.RFC3339Nano, *o.ExpiresAt)
if err == nil {
previous := time.Time{}
if result.ExpiresAt != nil {
previous, _ = time.Parse(time.RFC3339Nano, *result.ExpiresAt)
}
if previous.IsZero() || expires.Before(previous) {
result.ExpiresAt = o.ExpiresAt
}
}
}
}
result.ObservedAt = liveTime(latestObserved)
result.ReceivedAt = liveTime(latestReceived)
result.LastSuccessAt = liveTime(latestSuccess)
if len(views) > 1 {
result.Source = "mixed"
}
switch {
case allOK:
result.Quality = "ok"
result.ErrorCode = ""
case anyStale:
result.Quality = "stale"
case allFailed:
result.Quality = "unreachable"
}
return result
}
func peerLiveSpeakerIDOrNull(id string) any {
if strings.TrimSpace(id) == "" {
return nil
}
return id
}