Documentation
¶
Overview ¶
Package setupmetrics pkg/router/setupmetrics/empty.go c2-net-routing
Package setupmetrics pkg/router/setupmetrics/kinds.go c2-net-routing
Which SETUP PATH each request took, and what batching bought.
A route can reach a setup node three ways today and the /stats snapshot could not tell them apart: the legacy single DialRouteGroup (what production runs — routing.enable_cascade_route_setup defaults off), the batched DialRouteGroupBatch added alongside it, and the source-driven cascade, where the node only SIGNS and the source injects the cascade down its own transports. "total_requests 54" said nothing about which of the three, so an operator could not see whether a deployed client had negotiated the batch form at all.
RequestsByKind answers that, RoutesPerBatch shows the batch sizes actually arriving, and PerHopRPCsSaved is the concrete coalescing win: the per-hop RPCs the batched path did NOT issue because one id reservation and one intermediary install covered several routes.
Package setupmetrics pkg/router/setupmetrics/own_view.go c2-net-routing
The caller-scoped view of the route-setup snapshot.
/stats splits in two: the aggregate half is public (anyone depends on this shared layer and deserves to see whether it works), the topology half — top_destinations, top_failed_destinations, recent_failures — is gated on the survey whitelist, because the setup node participates in EVERY route setup and its record of who sets up routes to whom is traffic-analysis material.
The gap that left: an ordinary visor asking its own setup node "which destination is failing FOR ME" got `"top_destinations": null` and `"recent_failures": null`, so `pk`, `src_pk` and `dst_pk` were empty for every operator not on the whitelist — which is every operator, since the whitelist holds seven deployment keys. The structured fields exist and are filled in correctly; they were simply never sent.
A caller does not need privilege to be told about its OWN route setups: it already knows which destinations it dialed and which of them failed. So the non-whitelisted view is no longer empty — it is the caller's own rows, and nobody else's. The whitelisted view is unchanged (every row, every key).
Package setupmetrics pkg/router/setupmetrics/stats.go c2-net-routing requests. Complements victoria_metrics.go (which publishes Prometheus gauges for external scrape) with a structured snapshot the visor can expose over RPC and the CLI can pretty-print for quick triage.
The collector is safe for concurrent use. It categorizes each failure into a small set of well-known reasons so operators can answer "which part of the setup path is broken?" without correlating raw log lines.
Package setupmetrics pkg/router/setupmetrics/victoria_metrics.go c2-net-routing
Index ¶
- Constants
- type BatchStats
- type BreakerState
- type CircuitState
- type Collector
- func (c *Collector) AllowDestination(dstPK cipher.PubKey, held *ProbeHolder) (bool, string)
- func (c *Collector) AllowIntermediate(interPK cipher.PubKey, held *ProbeHolder) (bool, string)
- func (c *Collector) RecordBatch(routes, installed, rpcsSaved int)
- func (c *Collector) RecordConcurrencyDrop()
- func (c *Collector) RecordRequest() func(*routing.EdgeRules, *error)
- func (c *Collector) RecordRoute() func(*error)
- func (c *Collector) RecordRouteContext(ctx context.Context, srcPK, dstPK cipher.PubKey, hopCount int) func(*error)
- func (c *Collector) RecordRouteContextProbes(ctx context.Context, srcPK, dstPK cipher.PubKey, hopCount int, ...) func(*error)
- func (c *Collector) RecordSetupKind(kind SetupKind, routes int)
- func (c *Collector) Reset()
- func (c *Collector) Snapshot() StatsSnapshot
- func (c *Collector) SnapshotFor(caller cipher.PubKey, whitelisted bool) StatsSnapshot
- type CollectorConfig
- type DestStat
- type Empty
- type FailureEvent
- type FailureReason
- type LatencyStats
- type Metrics
- type ProbeHolder
- type SetupKind
- type StatsSnapshot
- type VictoriaMetrics
Constants ¶
const ( // CircuitBreakerEnabledDefault is OFF. // // The breaker is a per-destination lockout owned by whichever route // setup node served the request, and that node cannot see what the // SOURCE visor already has: on 2026-09-18 a visor holding 32 live // route groups to one exit was refused every further setup to it for // minutes, and a fresh proxy start got 0 route groups for 180s. The // deployment RSN's own counters at the time read 481 failures of // which 457 were circuit_open — the breaker's refusals, not route // setups that were tried and failed, and they had evicted the real // failures out of the diagnostic ring. A setup that is attempted and // fails costs one id-reservation timeout; a breaker that opens on a // misattributed failure costs every caller of that destination. Off // is the safer default. CircuitBreakerEnabledDefault = false // CircuitFailureThresholdDefault is the number of consecutive // failures to a destination that trips the breaker to OPEN. // // Lowered from 5 → 3 after the 1.3.59-era observation that // dmsg-error-202 (intermediate's stale dmsg session) consistently // produces 3+ failures back-to-back; tolerating 5 wastes the next // two setup attempts on doomed routes. With the open duration // tuned to dmsg-session-refresh cadence (below), tripping earlier // is the right balance. CircuitFailureThresholdDefault = 3 // CircuitOpenDurationDefault is how long the breaker stays OPEN // before transitioning to HALF_OPEN to allow a probe setup. // // Bumped from 60s → 5min to align with dmsg's own session-refresh // cadence (~5min). dmsg-error-202 means a visor is registered in // dmsg-disc but its actual delegated-server session is stale; the // session re-publishes itself on a ~5min cycle, so retrying inside // that window is doomed to hit the same stale state. 60s let // half-open probes burn through the same failure on every cycle. CircuitOpenDurationDefault = 5 * time.Minute // CircuitMaxOpenDurationDefault is the maximum time a breaker can // stay in the open/half_open cycle before being force-reset to // closed. This prevents permanent lockout when the RSN's own DMSG // sessions are stale but the destination is actually alive — fresh // traffic will establish new DMSG paths. // // Bumped from 10min → 30min in concert with the longer open // duration; with 5min between half-open probes, 10min is only two // retry windows which is tight for a genuinely-down peer waiting // to come back. 30min gives ~6 half-open probes before force-reset. CircuitMaxOpenDurationDefault = 30 * time.Minute // CircuitFailureWindowDefault bounds how long consecutive failures // must occur within to count toward the threshold. Failures older // than this window are considered stale and the consecutive counter // is reset on the next record. CircuitFailureWindowDefault = 5 * time.Minute )
Circuit breaker defaults. These are what the setup.circuit_* knobs in the router settings catalog ship with; the live values are read through the accessors below, so an operator can retune the breaker — or turn it back on — without a restart.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type BatchStats ¶
type BatchStats struct {
// Batches is how many batched requests were handled.
Batches uint64 `json:"batches"`
// Routes is the total number of routes those batches carried.
Routes uint64 `json:"routes"`
// Installed is how many of those routes came back installed — a batch
// returns per-route results and partial success is normal.
Installed uint64 `json:"installed"`
// RoutesPerBatch is a histogram of batch sizes: routes-in-the-batch →
// number of batches of that size.
RoutesPerBatch map[int]uint64 `json:"routes_per_batch"`
// PerHopRPCsSaved is the running total of per-hop RPCs the coalescing
// removed — the id-reservation and intermediary-install calls the batch
// did NOT have to issue because one call covered several routes.
PerHopRPCsSaved uint64 `json:"per_hop_rpcs_saved"`
}
BatchStats is the batched-path summary in a StatsSnapshot. It is present even when no batch has arrived (all zeroes), so an operator can tell "no client has negotiated the batch form" from "this field does not exist on this build".
type BreakerState ¶
type BreakerState struct {
State CircuitState `json:"state"`
OpenedAt time.Time `json:"opened_at"`
ConsecutiveFails int `json:"consecutive_fails"`
ProbeInFlight bool `json:"probe_in_flight"`
}
BreakerState is the JSON view of one circuit breaker that is not closed. Snapshot only emits non-closed entries, so an empty "breakers" object means route setup is short-circuiting nobody — and a lingering half_open entry with probe_in_flight=true is immediately visible to an operator instead of only showing up as "probe in flight" strings in the caller's error.
type CircuitState ¶
type CircuitState string
CircuitState is the per-destination breaker state. "closed" = normal, "open" = fast-fail new setups, "half_open" = allow one probe to test recovery.
const ( // CircuitClosed means setups to this destination proceed normally. CircuitClosed CircuitState = "closed" // CircuitOpen means setups to this destination are short-circuited. CircuitOpen CircuitState = "open" // CircuitHalfOpen means one probe setup is allowed to test recovery. CircuitHalfOpen CircuitState = "half_open" )
type Collector ¶
type Collector struct {
// contains filtered or unexported fields
}
Collector is a thread-safe aggregator of route-setup attempt metadata. It implements Metrics so it can be wired anywhere the existing Victoria Metrics struct is used (the two are independent — wiring the Collector does not remove Prometheus output).
func NewCollector ¶
func NewCollector(cfg CollectorConfig) *Collector
NewCollector returns a ready-to-use Collector.
func (*Collector) AllowDestination ¶
AllowDestination reports whether a new route setup to dst should proceed. Returns (true, "") when the breaker is closed or half-open, (false, reason) when it is open. Call this from the request handler BEFORE doing any dial work — rejecting early is the whole point.
The half-open transition is driven here: when we find an open breaker that has been open for circuitOpenDuration, we flip it to half-open and allow the current probe through. A subsequent success will close the breaker (via finish); a failure re-opens it and resets the per-cycle timer.
held records the probe against the calling request; pass the same *ProbeHolder that was given to RecordRouteContextProbes so finish() can release the slot however the request ends. A nil holder is accepted for callers that only inspect the breaker and never run a request through finish() (tests, diagnostics) — such a caller never takes the probe slot.
AllowIntermediate is the per-intermediate sibling — see below.
func (*Collector) AllowIntermediate ¶
AllowIntermediate reports whether a route candidate whose path traverses interPK should be allowed to proceed. Intermediate breakers are populated by id_reservation failures attributed to the intermediate (see the default branch in finish()). Callers that have already computed a candidate route's intermediates (typically the route-setup-node, or a router that wants to pre-filter route-finder output) can call this for each hop and reject the route early if any intermediate's breaker is open — avoiding a ~10s id-reservation timeout per known-bad hop.
Uses the same allowPK helper as AllowDestination; intermediate breakers live in the same map as destination breakers (keyed by PK string), which keeps the bookkeeping uniform — including the half-open probe slot, which is why held matters here just as much as it does for the destination.
func (*Collector) RecordBatch ¶
RecordBatch records one completed batch: how many routes it carried, how many installed, and how many per-hop RPCs the coalescing saved.
func (*Collector) RecordConcurrencyDrop ¶
func (c *Collector) RecordConcurrencyDrop()
RecordConcurrencyDrop is called when a request is refused at the accept-loop backpressure check (never reaches the handler).
func (*Collector) RecordRequest ¶
RecordRequest satisfies setupmetrics.Metrics — it bumps the active request counter up-front and decrements it when the deferred closure runs. Per-request success/failure counts are deliberately kept in RecordRoute so the two don't double-count when both are called (which is what the standalone setup node does today).
func (*Collector) RecordRoute ¶
RecordRoute satisfies setupmetrics.Metrics. When the collector is wired through the normal path this records the attempt under its "unknown" labels — callers who want richer data should use RecordRouteContext instead.
func (*Collector) RecordRouteContext ¶
func (c *Collector) RecordRouteContext(ctx context.Context, srcPK, dstPK cipher.PubKey, hopCount int) func(*error)
RecordRouteContext is the richer variant of RecordRoute that captures enough context to fill a FailureEvent. Call it once per route attempt at the top of the setup handler and defer the returned closure.
defer collector.RecordRouteContext(ctx, src, dst, hopCount)(&err)
func (*Collector) RecordRouteContextProbes ¶
func (c *Collector) RecordRouteContextProbes(ctx context.Context, srcPK, dstPK cipher.PubKey, hopCount int, held *ProbeHolder) func(*error)
RecordRouteContextProbes is RecordRouteContext plus the half-open probe bookkeeping: held is the same *ProbeHolder the request passes to AllowDestination / AllowIntermediate, and every probe it holds is resolved when the returned closure runs. Callers that consult the breakers MUST use this variant — a probe taken by a request that never releases it blocks the breaker's only recovery slot until the 30-minute force-reset.
var probes setupmetrics.ProbeHolder defer collector.RecordRouteContextProbes(ctx, src, dst, hopCount, &probes)(&err)
func (*Collector) RecordSetupKind ¶
RecordSetupKind counts one request against a setup path. routes is how many routes the request carried (1 for single and cascade-sign), which is what makes "requests by kind" comparable with "routes by kind".
func (*Collector) Reset ¶
func (c *Collector) Reset()
Reset clears all counters and ring buffers. Useful before starting a diagnostic capture window.
func (*Collector) Snapshot ¶
func (c *Collector) Snapshot() StatsSnapshot
Snapshot returns a deep copy of the current state. Safe to marshal.
func (*Collector) SnapshotFor ¶
func (c *Collector) SnapshotFor(caller cipher.PubKey, whitelisted bool) StatsSnapshot
SnapshotFor returns the snapshot a specific caller may see. A whitelisted caller gets the full snapshot; everyone else gets the aggregate plus their OWN rows — the destinations they themselves set up routes to, and their own failures, error strings included (the keys in those strings are the caller's own peers).
A null caller PK — an unparseable RemoteAddr — gets the aggregate alone.
What a non-whitelisted caller still never sees is the who-talks-to-whom half for anyone else: the other visors this node set up routes for, the destinations they used, and the FailureEvent.Error strings that embed the same keys in prose. The RSN participates in EVERY route setup, so its view of which visors route to which is unusually complete, and publishing that is traffic-analysis material a routing overlay must not hand out.
type CollectorConfig ¶
type CollectorConfig struct {
LatencyRingSize int // number of recent successful latencies to keep for percentile calc
FailureRingSize int // number of recent failures to keep in RecentFailures
DestCapacity int // max number of distinct destinations to track
}
CollectorConfig configures the ring buffer / destination cap sizes. Zero values mean "use the default".
type DestStat ¶
type DestStat struct {
PK string `json:"pk"`
Total uint64 `json:"total"`
Failed uint64 `json:"failed"`
Circuit string `json:"circuit,omitempty"`
}
DestStat is a per-destination counter used to surface "hot" visors (targets of many requests) and "troublesome" visors (high failure rate). Circuit state is the CircuitState (closed/open/half_open) as of the last update.
type Empty ¶
type Empty struct{}
Empty is a `Metrics` implementation which does nothing.
func NewEmpty ¶
func NewEmpty() Empty
NewEmpty creates a new metrics implementation that does nothing.
func (Empty) RecordRequest ¶
RecordRequest implements `Metrics`.
func (Empty) RecordRoute ¶
RecordRoute implements `Metrics`.
type FailureEvent ¶
type FailureEvent struct {
Timestamp time.Time `json:"timestamp"`
SrcPK string `json:"src_pk"`
DstPK string `json:"dst_pk"`
HopCount int `json:"hop_count"`
Reason FailureReason `json:"reason"`
Error string `json:"error"`
DurationMs int64 `json:"duration_ms"`
}
FailureEvent captures a single failed setup attempt with enough context to start an investigation without a log trawl.
type FailureReason ¶
type FailureReason string
FailureReason is a short stable identifier for why a route setup attempt failed. Keep values lowercase-snake so they render cleanly in JSON / CLI tables.
const ( ReasonInvalidRoute FailureReason = "invalid_route" // BidirectionalRoute.Check failed ReasonRuleGeneration FailureReason = "rule_generation" // GenerateRules returned ErrNoKey etc. ReasonIDReservation FailureReason = "id_reservation" // ReserveRouteIDs failed dialing the destination ReasonSourceUnreachable FailureReason = "source_unreachable" // ReserveRouteIDs could not reach the SOURCE visor (not the dst's fault) ReasonIntermediateUnreachable FailureReason = "intermediate_unreachable" // ReserveRouteIDs could not reach an INTERMEDIATE visor (not the dst's fault) ReasonIntermediaryRules FailureReason = "intermediary_rules" // BroadcastIntermediaryRules failed ReasonDestinationRules FailureReason = "destination_rules" // AddEdgeRules on destination router failed ReasonContextDeadline FailureReason = "context_deadline" // overall request timed out ReasonContextCanceled FailureReason = "context_canceled" // caller canceled before completion ReasonConcurrencyLimit FailureReason = "concurrency_limit" // dropped at the accept loop backpressure check ReasonCircuitOpen FailureReason = "circuit_open" // per-destination breaker short-circuited the setup ReasonUnknown FailureReason = "unknown" // failure classifier could not decide )
Failure reason categories. The set intentionally stays small so operators can reason about them; unknown / unclassified failures fall through to ReasonUnknown and the raw error message is still captured in the RecentFailures ring buffer for investigation.
type LatencyStats ¶
type LatencyStats struct {
Count uint64 `json:"count"`
Min int64 `json:"min_ms"`
Max int64 `json:"max_ms"`
Mean int64 `json:"mean_ms"`
P50 int64 `json:"p50_ms"`
P95 int64 `json:"p95_ms"`
P99 int64 `json:"p99_ms"`
}
LatencyStats are percentile summaries (milliseconds) computed over the collector's latency ring on demand.
type Metrics ¶
type Metrics interface {
RecordRequest() func(*routing.EdgeRules, *error)
RecordRoute() func(*error)
}
Metrics collects metrics in prometheus format.
type ProbeHolder ¶
type ProbeHolder struct {
// contains filtered or unexported fields
}
ProbeHolder records which PKs a single request was admitted through as the half-open probe, so finish() can resolve every one of them when that request ends. Admission and resolution MUST be symmetric: a probe left in flight past the end of the request that took it locks the breaker's half-open slot until circuitMaxOpenDuration force-resets it (30 minutes of refusing everybody), which is exactly the failure observed live on 2026-09-16 for an intermediate that was perfectly reachable.
The zero value is ready to use. Create one per request, pass it to every Allow* call that request makes, and hand the same holder to RecordRouteContextProbes.
type SetupKind ¶
type SetupKind string
SetupKind names a route-setup path. Values are lowercase-snake so they render as JSON object keys.
const ( // SetupKindSingle is one BidirectionalRoute per DialRouteGroup request — // the legacy path, and what the live deployment uses today. SetupKindSingle SetupKind = "single" // SetupKindBatch is one DialRouteGroupBatch carrying N routes to one // destination, with the per-hop work coalesced. SetupKindBatch SetupKind = "batch" // SetupKindCascadeSign is the source-driven cascade: the node signs the // reserve/install cascades and the SOURCE injects them down its own // transports. Counted once per signing request pair. SetupKindCascadeSign SetupKind = "cascade_sign" )
The setup paths a request can take.
type StatsSnapshot ¶
type StatsSnapshot struct {
// Version is the build of the setup node that answered.
Version string `json:"version,omitempty"`
StartedAt time.Time `json:"started_at"`
UptimeSec int64 `json:"uptime_sec"`
TotalRequests uint64 `json:"total_requests"`
Successful uint64 `json:"successful"`
Failed uint64 `json:"failed"`
ConcurrencyDrops uint64 `json:"concurrency_drops"`
ActiveRequests int `json:"active_requests"`
SuccessRatePct float64 `json:"success_rate_pct"`
FailuresByReason map[FailureReason]uint64 `json:"failures_by_reason"`
LatencyMs LatencyStats `json:"latency_ms"`
// RouteLengthHist maps hop count → number of successful setups.
RouteLengthHist map[int]uint64 `json:"route_length_hist"`
// RequestsByKind / RoutesByKind split the request count by the setup PATH
// it took — single, batch, cascade_sign (see kinds.go). Aggregate, so both
// are part of the PUBLIC view: they name no visor.
RequestsByKind map[SetupKind]uint64 `json:"requests_by_kind"`
RoutesByKind map[SetupKind]uint64 `json:"routes_by_kind"`
// Batch is the batched-path summary: sizes seen and per-hop RPCs the
// coalescing saved. Also aggregate, also public.
Batch BatchStats `json:"batch"`
// Top destinations sorted by total request count (descending).
// Capped at the collector's top-N config.
TopDestinations []DestStat `json:"top_destinations"`
// Destinations ranked by failure count (descending). Same cap.
TopFailedDestinations []DestStat `json:"top_failed_destinations"`
// Most recent N failures, newest first.
RecentFailures []FailureEvent `json:"recent_failures"`
// Breakers holds every circuit breaker that is not closed, keyed
// by PK — destinations and intermediates alike. Omitted when all
// breakers are closed.
Breakers map[string]BreakerState `json:"breakers,omitempty"`
LastSuccessAt *time.Time `json:"last_success_at,omitempty"`
LastFailureAt *time.Time `json:"last_failure_at,omitempty"`
}
StatsSnapshot is the public, JSON-friendly view exposed over RPC/CLI. Every field is a point-in-time copy — the underlying collector holds its own mutex during Snapshot so the struct is safe to marshal and return.
type VictoriaMetrics ¶
type VictoriaMetrics struct {
// contains filtered or unexported fields
}
VictoriaMetrics implements `Metrics` using Victoria Metrics.
func NewVictoriaMetrics ¶
func NewVictoriaMetrics() *VictoriaMetrics
NewVictoriaMetrics returns the Victoria Metrics implementation of Metrics.
func (*VictoriaMetrics) RecordRequest ¶
func (m *VictoriaMetrics) RecordRequest() func(rules *routing.EdgeRules, err *error)
RecordRequest implements `Metrics`.
func (*VictoriaMetrics) RecordRoute ¶
func (m *VictoriaMetrics) RecordRoute() func(err *error)
RecordRoute implements `Metrics`.