Files
cloud-ip-validator/internal/httpapi/handlers_prober.go
T

151 lines
5.0 KiB
Go
Raw Normal View History

2026-08-21 07:34:45 +03:00
package httpapi
import (
"context"
"fmt"
2026-08-21 07:34:45 +03:00
"net/http"
"time"
"cloudipvalidator/internal/db"
)
func (s *Server) handleProberRegister(w http.ResponseWriter, r *http.Request) {
var req registerProberRequest
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
return
}
2026-08-23 20:39:22 +03:00
idx, err := s.Orch.SiteIndexForID(r.Context(), req.SiteID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if idx == 0 {
2026-08-21 07:34:45 +03:00
writeError(w, http.StatusBadRequest, "unknown site_id: "+req.SiteID)
return
}
if err := s.DB.RegisterSiteProber(r.Context(), req.SiteID, req.Hostname); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
2026-08-21 07:34:45 +03:00
s.Orch.RecordEvent(r.Context(), "prober", req.SiteID, nil, "registered", "")
writeJSON(w, http.StatusOK, registerAgentResponse{OK: true, PollIntervalSeconds: s.Orch.Cfg.PollIntervalSeconds})
}
func (s *Server) handleProberHeartbeat(w http.ResponseWriter, r *http.Request) {
siteID := r.PathValue("site_id")
var req heartbeatRequest
_ = readJSON(r, &req) // heartbeat body is informational only; tolerate empty/missing
if err := s.DB.SiteHeartbeat(r.Context(), siteID); err != nil {
writeError(w, http.StatusNotFound, "unknown site_id: "+siteID)
return
}
writeJSON(w, http.StatusOK, okResponse{OK: true})
}
// handleProberAssignments returns every IP currently in the checking state
// that this site has not yet finished probing in the current attempt —
// probers work the whole active set each poll, not one IP at a time, since
// multiple validators run in parallel. An address is handed out until the
// site reports it complete, then no more: one probe round per site per
// attempt, so results are written once and never overwritten.
2026-08-21 07:34:45 +03:00
func (s *Server) handleProberAssignments(w http.ResponseWriter, r *http.Request) {
siteID := r.PathValue("site_id")
2026-08-23 20:39:22 +03:00
idx, err := s.Orch.SiteIndexForID(r.Context(), siteID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if idx == 0 {
2026-08-21 07:34:45 +03:00
writeError(w, http.StatusNotFound, "unknown site_id: "+siteID)
return
}
items, err := s.DB.ListCheckingForSite(r.Context(), idx)
2026-08-21 07:34:45 +03:00
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
inbound, err := s.DB.GetInboundChecks(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
2026-08-21 07:34:45 +03:00
out := make([]proberAssignment, 0, len(items))
for _, item := range items {
out = append(out, proberAssignment{
IPID: item.ID, IPAddress: item.IPAddress,
Ports: inbound.Ports, ICMP: inbound.ICMP,
2026-08-21 07:34:45 +03:00
})
}
writeJSON(w, http.StatusOK, out)
}
func (s *Server) handleProberResults(w http.ResponseWriter, r *http.Request) {
siteID := r.PathValue("site_id")
2026-08-23 20:39:22 +03:00
siteIndex, err := s.Orch.SiteIndexForID(r.Context(), siteID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
2026-08-21 07:34:45 +03:00
if siteIndex == 0 {
writeError(w, http.StatusNotFound, "unknown site_id: "+siteID)
return
}
var req proberResultsRequest
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
return
}
completed := map[int64]bool{}
dropped := map[int64]int{}
2026-08-21 07:34:45 +03:00
for _, res := range req.Results {
item, err := s.DB.GetIP(r.Context(), res.IPID)
if err != nil {
writeError(w, http.StatusNotFound, "unknown ip_id")
return
}
checkedAt, err := time.Parse(time.RFC3339Nano, res.CheckedAt)
if err != nil {
checkedAt = db.Now()
}
written, err := s.Orch.RecordCheckIfOpen(r.Context(), db.Check{
2026-08-21 07:34:45 +03:00
IPID: item.ID, IPAddress: item.IPAddress, AttemptNumber: item.AttemptNumber,
Source: db.InboundSource(siteIndex), CheckType: res.CheckType, Target: res.IPAddress,
Success: res.Success, LatencyMS: res.LatencyMS, Detail: res.Detail, CheckedAt: checkedAt,
})
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if !written {
dropped[item.ID]++
continue
}
2026-08-21 07:34:45 +03:00
if res.Complete {
completed[res.IPID] = true
}
}
for ipID := range completed {
if err := s.Orch.MarkSiteComplete(r.Context(), ipID, siteIndex); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
writeJSON(w, http.StatusOK, s.reportDropped(r.Context(), "prober", siteID, db.InboundSource(siteIndex), dropped))
}
// reportDropped records one result_dropped event per address whose submitted
// checks were refused (the verdict already exists, or the attempt is old) and
// builds the response. The events are the audit trail of how often senders
// are late; in a healthy run there are none.
func (s *Server) reportDropped(ctx context.Context, sourceType, sourceID, checkSource string, dropped map[int64]int) resultsResponse {
total := 0
for ipID, n := range dropped {
total += n
id := ipID
s.Orch.RecordEvent(ctx, sourceType, sourceID, &id, "result_dropped",
fmt.Sprintf(`{"source":%q,"dropped":%d}`, checkSource, n))
}
return resultsResponse{OK: true, Ignored: total}
2026-08-21 07:34:45 +03:00
}