Files
EvoBGP/internal/httpapi/speakers_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

116 lines
3.3 KiB
Go

package httpapi
import (
"strings"
"time"
"evobgp/internal/birdfmt"
"evobgp/internal/nodedispatch"
"evobgp/internal/store"
)
type agentHealthPoll struct {
result nodedispatch.AgentHealthResult
at time.Time
lastSuccessAt time.Time
observationOverride *liveObservation
}
func agentHealthObservation(poll *agentHealthPoll) liveObservation {
if poll == nil {
return liveObservation{Source: "none", Quality: "unknown", ErrorCode: "not_polled"}
}
if poll.observationOverride != nil {
return *poll.observationOverride
}
o := liveObservation{ObservedAt: liveTime(poll.at), ReceivedAt: liveTime(poll.at), LastSuccessAt: liveTime(poll.lastSuccessAt), Source: "agent_health", Quality: "ok"}
if !poll.result.OK {
o.Quality = "unreachable"
if !poll.lastSuccessAt.IsZero() {
o.Quality = "stale"
}
o.ErrorCode = liveErrorCode(poll.result.Error)
if poll.result.Error == "agent reported ok=false" {
o.ErrorCode = "reported_unhealthy"
}
}
return o
}
func countBGPSessions(sessions []birdfmt.BGPSession) (total, established int) {
total = len(sessions)
for _, s := range sessions {
if strings.EqualFold(strings.TrimSpace(s.State), "Established") {
established++
}
}
return total, established
}
func speakerLiveStatusJSON(sp *store.Speaker, view speakerBGPLive, health *nodedispatch.AgentHealthResult) map[string]any {
var poll *agentHealthPoll
if health != nil {
poll = &agentHealthPoll{result: *health}
}
return speakerLiveStatusWithPoll(sp, view, poll)
}
func speakerLiveStatusWithPoll(sp *store.Speaker, view speakerBGPLive, health *agentHealthPoll) map[string]any {
total, established := countBGPSessions(view.Sessions)
bgpObservation := view.observation()
agentObservation := agentHealthObservation(health)
if sp != nil && strings.EqualFold(strings.TrimSpace(sp.Role), "master") && health == nil {
agentObservation = bgpObservation
}
m := map[string]any{
"label": view.Label,
"bgp_poll_ok": view.successful(),
"bgp_sessions_total": nil,
"bgp_established": nil,
"bgp_observation": bgpObservation,
"agent_observation": agentObservation,
}
if view.successful() {
m["bgp_sessions_total"] = total
m["bgp_established"] = established
}
if view.Error != "" {
m["bgp_poll_error"] = view.Error
}
if health != nil {
m["agent_ok"] = health.result.OK
if health.result.Error != "" {
m["agent_error"] = health.result.Error
}
if health.result.LastSyncAt != "" {
m["agent_last_sync_at"] = health.result.LastSyncAt
}
if health.result.LastAppliedRevisionID != "" {
m["agent_last_applied_revision_id"] = health.result.LastAppliedRevisionID
}
} else if sp != nil && strings.EqualFold(strings.TrimSpace(sp.Role), "master") {
m["agent_ok"] = view.successful()
if view.Error != "" {
m["agent_error"] = view.Error
}
} else if sp != nil && store.SpeakerNeedsRemoteDispatch(sp.Role, store.ParseSpeakerMeta(sp.MetaJSON)) {
m["agent_ok"] = false
m["agent_error"] = "agent health not polled"
}
if view.successful() {
sess := make([]map[string]any, 0, len(view.Sessions))
for _, s := range view.Sessions {
row := map[string]any{
"name": s.Name,
"state": s.State,
}
if strings.TrimSpace(s.Neighbor) != "" {
row["neighbor"] = s.Neighbor
}
sess = append(sess, row)
}
m["sessions"] = sess
}
return m
}