Skip to content

Commit eb641fe

Browse files
committed
p2plimits: a stream budget carries the memory its streams reserve
yamux reserves one 256 KiB receive window per stream it opens, through the resource manager, and releases it with the stream; the bytes are allocated only as data arrives. With the node dialling TCP first, every member's hub link runs over yamux, and a router's memory budget, which libp2p scales from host memory and nothing here had set, filled at 1500 members: 1.17 GB reserved of 1.18 GB on the testnet's routers, after which the relay refused every circuit with RESOURCE_LIMIT_EXCEEDED (3200 to 3400 per router in one ladder step) while every stream counter stayed at zero, since a refused memory reservation is not a refused stream. Streams(n) now carries the memory its 2n streams reserve, one window each, so the system, transient, service and protocol budgets built from it follow the connection budget as the streams do; the router's and the node's peer scope get the same budget as the per-peer circuits, since the peer scope for a service many members call carries one stop stream per caller. The reporter counts refused memory reservations in agentmesh_{router,node}_memory_reservations_refused_total and logs them once a minute, naming the libp2p gauges that show which scope is full. Tests pin the budgets and check that a manager built from Streams(n) admits 2n windows and refuses the next.
1 parent b220c2f commit eb641fe

9 files changed

Lines changed: 188 additions & 30 deletions

File tree

‎internal/node/resource_limits.go‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ func nodeLimits(conns int) rcmgr.ConcreteLimitConfig {
9393
overrides := rcmgr.PartialLimitConfig{
9494
System: system,
9595
Transient: transient,
96+
// The peer scope for a router carries one relay stop stream per
97+
// caller it relays, as many as the connection budget admits.
98+
PeerDefault: p2plimits.Streams(conns),
9699
Service: map[string]rcmgr.ResourceLimits{
97100
identify.ServiceName: p2plimits.Streams(conns),
98101
},
@@ -131,6 +134,13 @@ var streamsRefusedTotal = promauto.NewCounterVec(
131134
[]string{"scope", "protocol"},
132135
)
133136

137+
// memoryRefusedTotal counts the memory reservations the resource manager
138+
// refused: the muxer's window for a new stream, among others.
139+
var memoryRefusedTotal = promauto.NewCounter(prometheus.CounterOpts{
140+
Name: "agentmesh_node_memory_reservations_refused_total",
141+
Help: "Memory reservations the resource manager refused; a new stream that needed one was refused with it",
142+
})
143+
134144
// busiestConnStreams is the most streams any one connection carries. On a
135145
// provider the connection to a router carries one stream per caller
136146
// relayed through it; a QUIC connection admits
@@ -152,5 +162,5 @@ const streamPressureInterval = 15 * time.Second
152162
// charged to the router's address; see p2plimits.Observed.
153163
func newResourceManager() (network.ResourceManager, error) {
154164
return p2plimits.New(nodeLimits(ConnHighWaterMark), ConnsPerSourceIP, inboundRefusedTotal, logger.Warnf,
155-
rcmgr.WithMetrics(p2plimits.CountStreamRefusals(streamsRefusedTotal, logger.Warnf)))
165+
rcmgr.WithMetrics(p2plimits.CountStreamRefusals(streamsRefusedTotal, memoryRefusedTotal, logger.Warnf)))
156166
}

‎internal/node/resource_limits_test.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"testing"
1919

2020
"github.com/google/agentmesh/api"
21+
"github.com/google/agentmesh/internal/p2plimits"
2122
pubsub "github.com/libp2p/go-libp2p-pubsub"
2223
"github.com/libp2p/go-libp2p/core/connmgr"
2324
rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager"
@@ -50,6 +51,15 @@ func TestNodeLimitsFollowTheConnectionBudget(t *testing.T) {
5051
// Every caller through one router is a stop stream from that one peer.
5152
want("relay stop protocol inbound streams", cfg.Protocol[circuit.ProtoIDv2Stop].StreamsInbound, ConnHighWaterMark)
5253
want("relay stop protocol per-peer inbound streams", cfg.ProtocolPeer[circuit.ProtoIDv2Stop].StreamsInbound, ConnHighWaterMark)
54+
// A router peer carries one stop stream per relayed caller; yamux
55+
// reserves one window per open stream, so memory follows the streams.
56+
want("peer inbound streams", cfg.PeerDefault.StreamsInbound, ConnHighWaterMark)
57+
if got, min := int64(cfg.System.Memory), int64(cfg.System.Streams)*p2plimits.StreamWindow; got < min {
58+
t.Errorf("system memory = %d, want at least a window per stream, %d", got, min)
59+
}
60+
if got, min := int64(cfg.PeerDefault.Memory), int64(ConnHighWaterMark)*p2plimits.StreamWindow; got < min {
61+
t.Errorf("peer memory = %d, want at least a window per stop stream, %d", got, min)
62+
}
5363

5464
if ConnsPerSourceIP != ConnHighWaterMark/4 {
5565
t.Errorf("ConnsPerSourceIP = %d, want a quarter of the high watermark, %d", ConnsPerSourceIP, ConnHighWaterMark/4)

‎internal/p2plimits/limits.go‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,27 @@ func sourceRateLimiter(perIP int) *xrate.Limiter {
122122
}
123123
}
124124

125-
// Streams is a stream budget of n each way.
125+
// Streams is a stream budget of n each way, with the memory the streams
126+
// reserve: yamux reserves one receive window per open stream through the
127+
// resource manager, so a budget of streams is a budget of memory, and
128+
// libp2p's default for the latter is scaled from host memory, which on the
129+
// testnet's routers was 1.1 GiB and full at 1500 members over TCP. The
130+
// memory is a reservation, released with the stream, and allocated only
131+
// as data arrives and waits to be read.
126132
func Streams(n int) rcmgr.ResourceLimits {
127133
return rcmgr.ResourceLimits{
128134
Streams: rcmgr.LimitVal(2 * n),
129135
StreamsInbound: rcmgr.LimitVal(n),
130136
StreamsOutbound: rcmgr.LimitVal(n),
137+
Memory: rcmgr.LimitVal64(int64(2*n) * StreamWindow),
131138
}
132139
}
133140

141+
// StreamWindow is the receive window yamux reserves for a stream when it
142+
// opens (go-yamux's initialStreamWindow); a stream that carries a fast
143+
// transfer to a slow reader may grow it, up to the muxer's maximum.
144+
const StreamWindow = 256 << 10
145+
134146
// RefusalLogInterval bounds how often one source IP's refusals are logged;
135147
// the counter carries the rate.
136148
const RefusalLogInterval = time.Minute

‎internal/p2plimits/streams.go‎

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package p2plimits
1616

1717
import (
18+
"fmt"
1819
"sync"
1920
"time"
2021

@@ -40,32 +41,37 @@ const (
4041
)
4142

4243
// StreamRefusals counts the streams the resource manager refused, by scope
43-
// and protocol (or service), and logs each scope and name once per
44-
// RefusalLogInterval. It is libp2p's MetricsReporter; the Allow side is a
45-
// no-op.
44+
// and protocol (or service), and the memory reservations it refused, and
45+
// logs each once per RefusalLogInterval. It is libp2p's MetricsReporter;
46+
// the Allow side is a no-op.
4647
type StreamRefusals struct {
47-
refused *prometheus.CounterVec
48-
warnf func(format string, args ...any)
49-
mu sync.Mutex
50-
since map[string]uint64
51-
lastLogAt map[string]time.Time
52-
now func() time.Time
48+
refused *prometheus.CounterVec
49+
memoryRefused prometheus.Counter
50+
warnf func(format string, args ...any)
51+
mu sync.Mutex
52+
since map[string]uint64
53+
lastLogAt map[string]time.Time
54+
now func() time.Time
5355
}
5456

5557
var _ rcmgr.MetricsReporter = (*StreamRefusals)(nil)
5658

5759
// CountStreamRefusals returns a reporter for rcmgr.WithMetrics. refused
58-
// carries the labels scope and protocol; a nil warnf only counts.
59-
func CountStreamRefusals(refused *prometheus.CounterVec, warnf func(format string, args ...any)) *StreamRefusals {
60+
// carries the labels scope and protocol; memoryRefused counts the memory
61+
// reservations refused, which the stream muxer makes one of per stream it
62+
// opens and the relay one of per connection it relays; a nil warnf only
63+
// counts.
64+
func CountStreamRefusals(refused *prometheus.CounterVec, memoryRefused prometheus.Counter, warnf func(format string, args ...any)) *StreamRefusals {
6065
if warnf == nil {
6166
warnf = func(string, ...any) {}
6267
}
6368
return &StreamRefusals{
64-
refused: refused,
65-
warnf: warnf,
66-
since: map[string]uint64{},
67-
lastLogAt: map[string]time.Time{},
68-
now: time.Now,
69+
refused: refused,
70+
memoryRefused: memoryRefused,
71+
warnf: warnf,
72+
since: map[string]uint64{},
73+
lastLogAt: map[string]time.Time{},
74+
now: time.Now,
6975
}
7076
}
7177

@@ -74,7 +80,31 @@ func (s *StreamRefusals) SetClock(now func() time.Time) { s.now = now }
7480

7581
func (s *StreamRefusals) refusedStream(scope, name string) {
7682
s.refused.WithLabelValues(scope, name).Inc()
77-
key := scope + " " + name
83+
s.report(scope+" "+name, func(n uint64) string {
84+
what := "at the " + scope + " scope"
85+
if name != "" {
86+
what = "on " + name + " " + what
87+
}
88+
return fmt.Sprintf("[Stream] Refused %d stream(s) %s since the last report; the peer sees its stream reset", n, what)
89+
})
90+
}
91+
92+
// BlockMemory is a memory reservation the manager refused: the muxer's
93+
// window for a new stream, or the relay's buffers for a connection it
94+
// would relay. The budget it ran into is libp2p_rcmgr_limit{resource=
95+
// "memory"} on the scope libp2p_rcmgr_memory shows full.
96+
func (s *StreamRefusals) BlockMemory(size int) {
97+
if s.memoryRefused != nil {
98+
s.memoryRefused.Inc()
99+
}
100+
s.report("memory", func(n uint64) string {
101+
return fmt.Sprintf("[Memory] Refused %d memory reservation(s) since the last report, the last for %d bytes: the resource manager's memory budget is full (libp2p_rcmgr_memory against libp2p_rcmgr_limit{resource=\"memory\"}); a new stream or a relayed connection that needs it is refused", n, size)
102+
})
103+
}
104+
105+
// report counts one refusal under key and, once per RefusalLogInterval,
106+
// logs the line for the count since the last report.
107+
func (s *StreamRefusals) report(key string, line func(n uint64) string) {
78108
s.mu.Lock()
79109
s.since[key]++
80110
n := s.since[key]
@@ -87,11 +117,7 @@ func (s *StreamRefusals) refusedStream(scope, name string) {
87117
}
88118
s.mu.Unlock()
89119
if shouldLog {
90-
what := "at the " + scope + " scope"
91-
if name != "" {
92-
what = "on " + name + " " + what
93-
}
94-
s.warnf("[Stream] Refused %d stream(s) %s since the last report; the peer sees its stream reset", n, what)
120+
s.warnf("%s", line(n))
95121
}
96122
}
97123

@@ -124,4 +150,3 @@ func (s *StreamRefusals) AllowPeer(peer.ID) {}
124150
func (s *StreamRefusals) AllowProtocol(protocol.ID) {}
125151
func (s *StreamRefusals) AllowService(string) {}
126152
func (s *StreamRefusals) AllowMemory(int) {}
127-
func (s *StreamRefusals) BlockMemory(int) {}

‎internal/p2plimits/streams_test.go‎

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ package p2plimits
1616

1717
import (
1818
"fmt"
19+
"strings"
1920
"testing"
2021
"time"
2122

2223
"github.com/libp2p/go-libp2p/core/network"
2324
"github.com/libp2p/go-libp2p/core/protocol"
2425
"github.com/libp2p/go-libp2p/core/test"
2526
rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager"
27+
"github.com/multiformats/go-multiaddr"
2628
"github.com/prometheus/client_golang/prometheus"
2729
)
2830

@@ -38,7 +40,7 @@ func TestStreamRefusalsCountTheProtocol(t *testing.T) {
3840
Protocol: map[protocol.ID]rcmgr.ResourceLimits{proto: Streams(1)},
3941
}.Build(rcmgr.InfiniteLimits)
4042
refused := newStreamsRefusedCounter()
41-
m, err := NewResourceManager(limits, rcmgr.WithMetrics(CountStreamRefusals(refused, nil)))
43+
m, err := NewResourceManager(limits, rcmgr.WithMetrics(CountStreamRefusals(refused, nil, nil)))
4244
if err != nil {
4345
t.Fatal(err)
4446
}
@@ -70,7 +72,7 @@ func TestStreamRefusalsCountTheProtocol(t *testing.T) {
7072
func TestStreamRefusalsLogOneProtocolOncePerInterval(t *testing.T) {
7173
now := time.Unix(1_700_000_000, 0)
7274
var lines []string
73-
s := CountStreamRefusals(newStreamsRefusedCounter(), func(format string, args ...any) {
75+
s := CountStreamRefusals(newStreamsRefusedCounter(), nil, func(format string, args ...any) {
7476
lines = append(lines, fmt.Sprintf(format, args...))
7577
})
7678
s.SetClock(func() time.Time { return now })
@@ -96,3 +98,61 @@ func TestStreamRefusalsLogOneProtocolOncePerInterval(t *testing.T) {
9698
t.Errorf("peer scope: %q", lines[len(lines)-1])
9799
}
98100
}
101+
102+
// A refused memory reservation is counted and said once per interval; the
103+
// muxer makes one per stream it opens, so a full memory budget is a stream
104+
// the peer never gets, with nothing in the stream counters.
105+
func TestStreamRefusalsCountRefusedMemory(t *testing.T) {
106+
memory := prometheus.NewCounter(prometheus.CounterOpts{Name: "test_memory_refused_total"})
107+
var lines []string
108+
now := time.Unix(1_700_000_000, 0)
109+
s := CountStreamRefusals(newStreamsRefusedCounter(), memory, func(format string, args ...any) {
110+
lines = append(lines, fmt.Sprintf(format, args...))
111+
})
112+
s.SetClock(func() time.Time { return now })
113+
for i := 0; i < 3; i++ {
114+
s.BlockMemory(256 << 10)
115+
}
116+
if got := counterValue(t, memory); got != 3 {
117+
t.Errorf("memory refusals counted = %v, want 3", got)
118+
}
119+
if len(lines) != 1 || !strings.Contains(lines[0], "Refused 1 memory reservation(s)") || !strings.Contains(lines[0], "262144 bytes") {
120+
t.Fatalf("lines = %q", lines)
121+
}
122+
now = now.Add(RefusalLogInterval)
123+
s.BlockMemory(4096)
124+
if len(lines) != 2 || !strings.Contains(lines[1], "Refused 3 memory reservation(s)") {
125+
t.Errorf("after the interval: %q", lines)
126+
}
127+
}
128+
129+
// The memory a stream budget carries admits the streams it admits: a
130+
// manager built from Streams(n) opens n streams on one yamux-sized
131+
// reservation each, and refuses the reservation past the budget.
132+
func TestStreamsBudgetCarriesTheMuxerWindows(t *testing.T) {
133+
const n = 4
134+
limits := rcmgr.PartialLimitConfig{System: Streams(n), Transient: Streams(n), PeerDefault: Streams(n)}.Build(rcmgr.InfiniteLimits)
135+
m, err := NewResourceManager(limits)
136+
if err != nil {
137+
t.Fatal(err)
138+
}
139+
defer func() { _ = m.Close() }()
140+
p := test.RandPeerIDFatal(t)
141+
conn, err := m.OpenConnection(network.DirInbound, true, multiaddr.StringCast("/ip4/203.0.113.9/tcp/1"))
142+
if err != nil {
143+
t.Fatal(err)
144+
}
145+
if err := conn.SetPeer(p); err != nil {
146+
t.Fatal(err)
147+
}
148+
// Streams(n) admits 2n streams in all; each takes one window, as yamux
149+
// reserves it, so the 2n+1-th window is refused.
150+
for i := 0; i < 2*n; i++ {
151+
if err := conn.ReserveMemory(StreamWindow, network.ReservationPriorityAlways); err != nil {
152+
t.Fatalf("window %d refused: %v", i+1, err)
153+
}
154+
}
155+
if err := conn.ReserveMemory(StreamWindow, network.ReservationPriorityAlways); err == nil {
156+
t.Fatal("a window past the stream budget was admitted")
157+
}
158+
}

‎internal/router/resource_limits.go‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ func hubLimits(highWaterMark, conns, circuits int) rcmgr.ConcreteLimitConfig {
8585

8686
// A relayed connection is one hop stream from the caller and one stop
8787
// stream to the destination, both held while it lasts; the memory the
88-
// relay reserves per connection is a few KiB on each.
88+
// relay reserves per connection is a few KiB on each, on top of the
89+
// streams' windows.
8990
relayStreams := streams(sysBase)
90-
relayStreams.Memory = rcmgr.LimitVal64(int64(sysBase) * 64 << 10)
9191
perPeerCircuits := streams(circuits)
9292

9393
protocols := map[protocol.ID]rcmgr.ResourceLimits{
@@ -101,6 +101,9 @@ func hubLimits(highWaterMark, conns, circuits int) rcmgr.ConcreteLimitConfig {
101101
overrides := rcmgr.PartialLimitConfig{
102102
System: system,
103103
Transient: transient,
104+
// The peer scope for a service many members call carries one stop
105+
// stream per relayed caller, up to the per-peer circuit budget.
106+
PeerDefault: perPeerCircuits,
104107
Service: map[string]rcmgr.ResourceLimits{
105108
identify.ServiceName: streams(sysBase),
106109
relay.ServiceName: relayStreams,

‎internal/router/resource_limits_test.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"testing"
2020

2121
"github.com/google/agentmesh/api"
22+
"github.com/google/agentmesh/internal/p2plimits"
2223
dht "github.com/libp2p/go-libp2p-kad-dht"
2324
pubsub "github.com/libp2p/go-libp2p-pubsub"
2425
rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager"
@@ -59,6 +60,18 @@ func TestHubLimitsFollowTheConnectionBudget(t *testing.T) {
5960
want("gossip protocol "+string(p)+" inbound streams", cfg.Protocol[p].StreamsInbound, highWaterMark)
6061
}
6162
want("relay service inbound streams", cfg.Service[relay.ServiceName].StreamsInbound, highWaterMark)
63+
// yamux reserves one window per open stream through the manager, so
64+
// the memory budget follows the stream budget; libp2p's default is
65+
// scaled from host memory and was full at 1500 members over TCP.
66+
if got, min := int64(cfg.System.Memory), int64(cfg.System.Streams)*p2plimits.StreamWindow; got < min {
67+
t.Errorf("system memory = %d, want at least a window per stream, %d", got, min)
68+
}
69+
if got, min := int64(cfg.PeerDefault.Memory), int64(circuits)*p2plimits.StreamWindow; got < min {
70+
t.Errorf("peer memory = %d, want at least a window per circuit, %d", got, min)
71+
}
72+
// A service many members call holds one stop stream per caller on the
73+
// router's peer scope for it.
74+
want("peer outbound streams", cfg.PeerDefault.StreamsOutbound, circuits)
6275
want("hop protocol inbound streams", cfg.Protocol[circuit.ProtoIDv2Hop].StreamsInbound, highWaterMark)
6376
// A destination many members call holds one stop stream per caller.
6477
want("relay per-peer outbound streams", cfg.ServicePeer[relay.ServiceName].StreamsOutbound, circuits)

‎internal/router/resource_observer.go‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ var streamsRefusedTotal = promauto.NewCounterVec(
4040
[]string{"scope", "protocol"},
4141
)
4242

43+
// memoryRefusedTotal counts the memory reservations the resource manager
44+
// refused: the muxer's window for a new stream, the relay's buffers for a
45+
// connection. The budget follows the stream budget (see p2plimits.Streams).
46+
var memoryRefusedTotal = promauto.NewCounter(prometheus.CounterOpts{
47+
Name: "agentmesh_router_memory_reservations_refused_total",
48+
Help: "Memory reservations the resource manager refused; a new stream or a relayed connection that needed one was refused with it",
49+
})
50+
4351
// busiestConnStreams is the most streams any one connection carries. A
4452
// QUIC connection admits p2plimits.QUICStreamsPerConnection; the router's
4553
// connection to a service it relays for carries one stream per relayed
@@ -67,7 +75,7 @@ func init() {
6775
// countStreamRefusals is the reporter the resource manager is built with;
6876
// it counts refused streams in streamsRefusedTotal by protocol.
6977
func countStreamRefusals() *p2plimits.StreamRefusals {
70-
return p2plimits.CountStreamRefusals(streamsRefusedTotal, logger.Warnf)
78+
return p2plimits.CountStreamRefusals(streamsRefusedTotal, memoryRefusedTotal, logger.Warnf)
7179
}
7280

7381
// connsPerSourceIPDesc exports the cap in force, so a dashboard can read the

‎site/content/docs/guides/scalability.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,22 @@ from host memory. A refused stream is counted in
101101
`agentmesh_router_streams_refused_total{protocol}` and
102102
`agentmesh_node_streams_refused_total{protocol}`.
103103

104+
A stream over TCP also reserves memory. yamux reserves one receive window
105+
(256 KiB) per stream it opens, through the resource manager, and releases
106+
it when the stream closes; the memory is allocated only as data arrives
107+
and waits to be read, so an idle stream holds a reservation and no bytes.
108+
The memory budget therefore follows the stream budget, one window per
109+
stream the budget admits, on the system scope and on the scope of each
110+
peer. libp2p's own default for the system scope is scaled from host
111+
memory and was 1.1 GiB on the testnet's routers, full at 1500 members
112+
over TCP, at which point the relay refused every circuit with
113+
`RESOURCE_LIMIT_EXCEEDED` and no stream counter moved. A refused
114+
reservation is counted in
115+
`agentmesh_router_memory_reservations_refused_total` and
116+
`agentmesh_node_memory_reservations_refused_total`, and the scope that is
117+
full reads from `libp2p_rcmgr_memory{scope}` against
118+
`libp2p_rcmgr_limit{resource="memory",scope}`.
119+
104120
### Hub links and peer links
105121

106122
A member's connection to a router is a hub link. It carries the member's
@@ -367,6 +383,7 @@ The router and the control plane serve Prometheus metrics on
367383
| `libp2p_relaysvc_reservations_total{type="opened"}` minus `{type="closed"}`, against `agentmesh_router_relay_limit{limit="reservations"}` | Reservations in use against the budget. |
368384
| `libp2p_relaysvc_connection_rejections_total{reason="resource limit exceeded"}` | A service has more callers than `--relay-max-circuits`. |
369385
| `libp2p_rcmgr_blocked_resources` | libp2p refused a connection or stream on a scope. On a router this should stay flat. |
386+
| `agentmesh_router_memory_reservations_refused_total`, `agentmesh_node_memory_reservations_refused_total`, with `libp2p_rcmgr_memory{scope}` against `libp2p_rcmgr_limit{resource="memory",scope}` | The resource manager's memory budget on some scope is full. Over TCP every open stream holds a 256 KiB reservation, so this is the stream budget met from the other side: new streams and relayed connections are refused, with `RESOURCE_LIMIT_EXCEEDED` at the relay and nothing in the stream counters. The scope whose `libp2p_rcmgr_memory` is at its limit names the budget. |
370387
| `agentmesh_router_streams_refused_total{scope,protocol}`, `agentmesh_node_streams_refused_total{scope,protocol}` | The streams behind that gauge, by protocol. `/mesh/kad/1.0.0` or `/meshsub/*` at the `protocol` scope on a router is members past the stream budget of a protocol, which keeps them out of discovery; `agentmesh_router_dht_routing_table_size` falls with it. |
371388
| `agentmesh_router_auth_handshakes_total{result="read_failed"}` | Members that timed out mid-handshake: the router is slow, usually CPU. |
372389
| `agentmesh_router_draining` | The router is stopping. Exclude it from capacity sums. |

0 commit comments

Comments
 (0)