Skip to content

Commit 514411b

Browse files
marten-seemannschomatislidel
authored
feat: opt-in Swarm.ResourceMgr (go-libp2p v0.18) (#8680)
* update go-libp2p to v0.18.0 * initialize the resource manager * add resource manager stats/limit commands * load limit file when building resource manager * log absent limit file * write rcmgr to file when IPFS_DEBUG_RCMGR is set * fix: mark swarm limit|stats as experimental * feat(cfg): opt-in Swarm.ResourceMgr This ensures we can safely test the resource manager without impacting default behavior. - Resource manager is disabled by default - Default for Swarm.ResourceMgr.Enabled is false for now - Swarm.ResourceMgr.Limits allows user to tweak limits per specific scope in a way that is persisted across restarts - 'ipfs swarm limit system' outputs human-readable json - 'ipfs swarm limit system new-limits.json' sets new runtime limits (but does not change Swarm.ResourceMgr.Limits in the config) Conventions to make libp2p devs life easier: - 'IPFS_RCMGR=1 ipfs daemon' overrides the config and enables resource manager - 'limit.json' overrides implicit defaults from libp2p (if present) * docs(config): small tweaks * fix: skip libp2p.ResourceManager if disabled This ensures 'ipfs swarm limit|stats' work only when enabled. * fix: use NullResourceManager when disabled This reverts commit b19f7c9. after clarification feedback from #8680 (comment) * style: rename IPFS_RCMGR to LIBP2P_RCMGR preexisting libp2p toggles use LIBP2P_ prefix * test: Swarm.ResourceMgr * fix: location of opt-in limit.json and rcmgr.json.gz Places these files inside of IPFS_PATH * Update docs/config.md * feat: expose rcmgr metrics when enabled (#8785) * add metrics for the resource manager * export protocol and service name in Prometheus metrics * fix: expose rcmgr metrics only when enabled Co-authored-by: Marcin Rataj <lidel@lidel.org> * refactor: rcmgr_metrics.go * refactor: rcmgr_defaults.go This file defines implicit limit defaults used when Swarm.ResourceMgr.Enabled We keep vendored copy to ensure go-ipfs is not impacted when go-libp2p decides to change defaults in any of the future releases. * refactor: adjustedDefaultLimits Cleans up the way we initialize defaults and adds a fix for case when connection manager runs with high limits. It also hides `Swarm.ResourceMgr.Limits` until we have a better understanding what syntax makes sense. * chore: cleanup after a review * fix: restore go-ipld-prime v0.14.2 * fix: restore go-ds-flatfs v0.5.1 Co-authored-by: Lucas Molas <schomatis@gmail.com> Co-authored-by: Marcin Rataj <lidel@lidel.org>
1 parent 7871a0b commit 514411b

24 files changed

+1439
-109
lines changed

config/swarm.go

+59
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ type SwarmConfig struct {
4949

5050
// ConnMgr configures the connection manager.
5151
ConnMgr ConnMgr
52+
53+
// ResourceMgr configures the libp2p Network Resource Manager
54+
ResourceMgr ResourceMgr
5255
}
5356

5457
type RelayClient struct {
@@ -129,3 +132,59 @@ type ConnMgr struct {
129132
HighWater int
130133
GracePeriod string
131134
}
135+
136+
// ResourceMgr defines configuration options for the libp2p Network Resource Manager
137+
// <https://github.com/libp2p/go-libp2p-resource-manager#readme>
138+
type ResourceMgr struct {
139+
// Enables the Network Resource Manager feature
140+
Enabled Flag `json:",omitempty"`
141+
142+
/* TODO: decide if and how we want to expose limits in our config
143+
Limits *ResourceMgrScopeConfig `json:",omitempty"` */
144+
}
145+
146+
const (
147+
ResourceMgrSystemScope = "system"
148+
ResourceMgrTransientScope = "transient"
149+
ResourceMgrServiceScopePrefix = "svc:"
150+
ResourceMgrProtocolScopePrefix = "proto:"
151+
ResourceMgrPeerScopePrefix = "peer:"
152+
)
153+
154+
/* TODO: decide if and how we want to expose limits in our config
155+
type ResourceMgrLimitsConfig struct {
156+
System *ResourceMgrScopeConfig `json:",omitempty"`
157+
Transient *ResourceMgrScopeConfig `json:",omitempty"`
158+
159+
ServiceDefault *ResourceMgrScopeConfig `json:",omitempty"`
160+
ServicePeerDefault *ResourceMgrScopeConfig `json:",omitempty"`
161+
Service map[string]ResourceMgrScopeConfig `json:",omitempty"`
162+
ServicePeer map[string]ResourceMgrScopeConfig `json:",omitempty"`
163+
164+
ProtocolDefault *ResourceMgrScopeConfig `json:",omitempty"`
165+
ProtocolPeerDefault *ResourceMgrScopeConfig `json:",omitempty"`
166+
Protocol map[string]ResourceMgrScopeConfig `json:",omitempty"`
167+
ProtocolPeer map[string]ResourceMgrScopeConfig `json:",omitempty"`
168+
169+
PeerDefault *ResourceMgrScopeConfig `json:",omitempty"`
170+
Peer map[string]ResourceMgrScopeConfig `json:",omitempty"`
171+
172+
Conn *ResourceMgrScopeConfig `json:",omitempty"`
173+
Stream *ResourceMgrScopeConfig `json:",omitempty"`
174+
}
175+
*/
176+
177+
// libp2p Network Resource Manager config for a scope
178+
type ResourceMgrScopeConfig struct {
179+
Dynamic bool `json:",omitempty"`
180+
// set if Dynamic is false
181+
Memory int64 `json:",omitempty"`
182+
// set if Dynamic is true
183+
MemoryFraction float64 `json:",omitempty"`
184+
MinMemory int64 `json:",omitempty"`
185+
MaxMemory int64 `json:",omitempty"`
186+
187+
Streams, StreamsInbound, StreamsOutbound int
188+
Conns, ConnsInbound, ConnsOutbound int
189+
FD int
190+
}

core/commands/commands_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,13 @@ func TestCommands(t *testing.T) {
237237
"/swarm/filters",
238238
"/swarm/filters/add",
239239
"/swarm/filters/rm",
240+
"/swarm/limit",
240241
"/swarm/peers",
241242
"/swarm/peering",
242243
"/swarm/peering/add",
243244
"/swarm/peering/ls",
244245
"/swarm/peering/rm",
246+
"/swarm/stats",
245247
"/tar",
246248
"/tar/add",
247249
"/tar/cat",

core/commands/config.go

+11-9
Original file line numberDiff line numberDiff line change
@@ -215,18 +215,20 @@ NOTE: For security reasons, this command will omit your private key and remote s
215215
return cmds.EmitOnce(res, &cfg)
216216
},
217217
Encoders: cmds.EncoderMap{
218-
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *map[string]interface{}) error {
219-
buf, err := config.HumanOutput(out)
220-
if err != nil {
221-
return err
222-
}
223-
buf = append(buf, byte('\n'))
224-
_, err = w.Write(buf)
225-
return err
226-
}),
218+
cmds.Text: HumanJSONEncoder,
227219
},
228220
}
229221

222+
var HumanJSONEncoder = cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *map[string]interface{}) error {
223+
buf, err := config.HumanOutput(out)
224+
if err != nil {
225+
return err
226+
}
227+
buf = append(buf, byte('\n'))
228+
_, err = w.Write(buf)
229+
return err
230+
})
231+
230232
// Scrubs value and returns error if missing
231233
func scrubValue(m map[string]interface{}, key []string) (map[string]interface{}, error) {
232234
return scrubMapInternal(m, key, false)

core/commands/swarm.go

+139-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package commands
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
57
"errors"
68
"fmt"
79
"io"
@@ -10,15 +12,17 @@ import (
1012
"sync"
1113
"time"
1214

13-
commands "github.com/ipfs/go-ipfs/commands"
14-
cmdenv "github.com/ipfs/go-ipfs/core/commands/cmdenv"
15-
repo "github.com/ipfs/go-ipfs/repo"
16-
fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
15+
files "github.com/ipfs/go-ipfs-files"
16+
"github.com/ipfs/go-ipfs/commands"
17+
"github.com/ipfs/go-ipfs/config"
18+
"github.com/ipfs/go-ipfs/core/commands/cmdenv"
19+
"github.com/ipfs/go-ipfs/core/node/libp2p"
20+
"github.com/ipfs/go-ipfs/repo"
21+
"github.com/ipfs/go-ipfs/repo/fsrepo"
1722

1823
cmds "github.com/ipfs/go-ipfs-cmds"
19-
config "github.com/ipfs/go-ipfs/config"
2024
inet "github.com/libp2p/go-libp2p-core/network"
21-
peer "github.com/libp2p/go-libp2p-core/peer"
25+
"github.com/libp2p/go-libp2p-core/peer"
2226
ma "github.com/multiformats/go-multiaddr"
2327
madns "github.com/multiformats/go-multiaddr-dns"
2428
mamask "github.com/whyrusleeping/multiaddr-filter"
@@ -52,6 +56,8 @@ ipfs peers in the internet.
5256
"filters": swarmFiltersCmd,
5357
"peers": swarmPeersCmd,
5458
"peering": swarmPeeringCmd,
59+
"stats": swarmStatsCmd, // libp2p Network Resource Manager
60+
"limit": swarmLimitCmd, // libp2p Network Resource Manager
5561
},
5662
}
5763

@@ -304,6 +310,133 @@ var swarmPeersCmd = &cmds.Command{
304310
Type: connInfos{},
305311
}
306312

313+
var swarmStatsCmd = &cmds.Command{
314+
Status: cmds.Experimental,
315+
Helptext: cmds.HelpText{
316+
Tagline: "Report resource usage for a scope.",
317+
LongDescription: `Report resource usage for a scope.
318+
The scope can be one of the following:
319+
- system -- reports the system aggregate resource usage.
320+
- transient -- reports the transient resource usage.
321+
- svc:<service> -- reports the resource usage of a specific service.
322+
- proto:<proto> -- reports the resource usage of a specific protocol.
323+
- peer:<peer> -- reports the resource usage of a specific peer.
324+
- all -- reports the resource usage for all currently active scopes.
325+
326+
The output of this command is JSON.
327+
`},
328+
Arguments: []cmds.Argument{
329+
cmds.StringArg("scope", true, false, "scope of the stat report"),
330+
},
331+
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
332+
node, err := cmdenv.GetNode(env)
333+
if err != nil {
334+
return err
335+
}
336+
337+
if node.ResourceManager == nil {
338+
return libp2p.NoResourceMgrError
339+
}
340+
341+
if len(req.Arguments) != 1 {
342+
return fmt.Errorf("must specify exactly one scope")
343+
}
344+
scope := req.Arguments[0]
345+
result, err := libp2p.NetStat(node.ResourceManager, scope)
346+
if err != nil {
347+
return err
348+
}
349+
350+
b := new(bytes.Buffer)
351+
enc := json.NewEncoder(b)
352+
err = enc.Encode(result)
353+
if err != nil {
354+
return err
355+
}
356+
return cmds.EmitOnce(res, b)
357+
},
358+
Encoders: cmds.EncoderMap{
359+
cmds.Text: HumanJSONEncoder,
360+
},
361+
}
362+
363+
var swarmLimitCmd = &cmds.Command{
364+
Status: cmds.Experimental,
365+
Helptext: cmds.HelpText{
366+
Tagline: "Get or set resource limits for a scope.",
367+
LongDescription: `Get or set resource limits for a scope.
368+
The scope can be one of the following:
369+
- system -- limits for the system aggregate resource usage.
370+
- transient -- limits for the transient resource usage.
371+
- svc:<service> -- limits for the resource usage of a specific service.
372+
- proto:<proto> -- limits for the resource usage of a specific protocol.
373+
- peer:<peer> -- limits for the resource usage of a specific peer.
374+
375+
The output of this command is JSON.
376+
377+
It is possible to use this command to inspect and tweak limits at runtime:
378+
379+
$ ipfs swarm limit system > limit.json
380+
$ vi limit.json
381+
$ ipfs swarm limit system limit.json
382+
383+
Changes made via command line are discarded on node shutdown.
384+
For permanent limits set Swarm.ResourceMgr.Limits in the $IPFS_PATH/config file.
385+
`},
386+
Arguments: []cmds.Argument{
387+
cmds.StringArg("scope", true, false, "scope of the limit"),
388+
cmds.FileArg("limit.json", false, false, "limits to be set").EnableStdin(),
389+
},
390+
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
391+
node, err := cmdenv.GetNode(env)
392+
if err != nil {
393+
return err
394+
}
395+
396+
if node.ResourceManager == nil {
397+
return libp2p.NoResourceMgrError
398+
}
399+
400+
scope := req.Arguments[0]
401+
402+
// set scope limit to new values (when limit.json is passed as a second arg)
403+
if req.Files != nil {
404+
var newLimit config.ResourceMgrScopeConfig
405+
it := req.Files.Entries()
406+
if it.Next() {
407+
file := files.FileFromEntry(it)
408+
if file == nil {
409+
return errors.New("expected a JSON file")
410+
}
411+
if err := json.NewDecoder(file).Decode(&newLimit); err != nil {
412+
return errors.New("failed to decode JSON as ResourceMgrScopeConfig")
413+
}
414+
return libp2p.NetSetLimit(node.ResourceManager, scope, newLimit)
415+
}
416+
if err := it.Err(); err != nil {
417+
return fmt.Errorf("error opening limit JSON file: %w", err)
418+
}
419+
}
420+
421+
// get scope limit
422+
result, err := libp2p.NetLimit(node.ResourceManager, scope)
423+
if err != nil {
424+
return err
425+
}
426+
427+
b := new(bytes.Buffer)
428+
enc := json.NewEncoder(b)
429+
err = enc.Encode(result)
430+
if err != nil {
431+
return err
432+
}
433+
return cmds.EmitOnce(res, b)
434+
},
435+
Encoders: cmds.EncoderMap{
436+
cmds.Text: HumanJSONEncoder,
437+
},
438+
}
439+
307440
type streamInfo struct {
308441
Protocol string
309442
}

core/core.go

+13-11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
ic "github.com/libp2p/go-libp2p-core/crypto"
3131
p2phost "github.com/libp2p/go-libp2p-core/host"
3232
metrics "github.com/libp2p/go-libp2p-core/metrics"
33+
"github.com/libp2p/go-libp2p-core/network"
3334
peer "github.com/libp2p/go-libp2p-core/peer"
3435
pstore "github.com/libp2p/go-libp2p-core/peerstore"
3536
routing "github.com/libp2p/go-libp2p-core/routing"
@@ -85,17 +86,18 @@ type IpfsNode struct {
8586
RecordValidator record.Validator
8687

8788
// Online
88-
PeerHost p2phost.Host `optional:"true"` // the network host (server+client)
89-
Peering *peering.PeeringService `optional:"true"`
90-
Filters *ma.Filters `optional:"true"`
91-
Bootstrapper io.Closer `optional:"true"` // the periodic bootstrapper
92-
Routing routing.Routing `optional:"true"` // the routing system. recommend ipfs-dht
93-
DNSResolver *madns.Resolver // the DNS resolver
94-
Exchange exchange.Interface // the block exchange + strategy (bitswap)
95-
Namesys namesys.NameSystem // the name system, resolves paths to hashes
96-
Provider provider.System // the value provider system
97-
IpnsRepub *ipnsrp.Republisher `optional:"true"`
98-
GraphExchange graphsync.GraphExchange `optional:"true"`
89+
PeerHost p2phost.Host `optional:"true"` // the network host (server+client)
90+
Peering *peering.PeeringService `optional:"true"`
91+
Filters *ma.Filters `optional:"true"`
92+
Bootstrapper io.Closer `optional:"true"` // the periodic bootstrapper
93+
Routing routing.Routing `optional:"true"` // the routing system. recommend ipfs-dht
94+
DNSResolver *madns.Resolver // the DNS resolver
95+
Exchange exchange.Interface // the block exchange + strategy (bitswap)
96+
Namesys namesys.NameSystem // the name system, resolves paths to hashes
97+
Provider provider.System // the value provider system
98+
IpnsRepub *ipnsrp.Republisher `optional:"true"`
99+
GraphExchange graphsync.GraphExchange `optional:"true"`
100+
ResourceManager network.ResourceManager `optional:"true"`
99101

100102
PubSub *pubsub.PubSub `optional:"true"`
101103
PSRouter *psrouter.PubsubValueStore `optional:"true"`

core/coreapi/test/api_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
coreiface "github.com/ipfs/interface-go-ipfs-core"
2424
"github.com/ipfs/interface-go-ipfs-core/tests"
2525
"github.com/libp2p/go-libp2p-core/crypto"
26-
peer "github.com/libp2p/go-libp2p-core/peer"
26+
"github.com/libp2p/go-libp2p-core/peer"
2727
mocknet "github.com/libp2p/go-libp2p/p2p/net/mock"
2828
)
2929

@@ -32,7 +32,7 @@ const testPeerID = "QmTFauExutTsy4XP6JbMFcw2Wa9645HJt2bTqL6qYDCKfe"
3232
type NodeProvider struct{}
3333

3434
func (NodeProvider) MakeAPISwarm(ctx context.Context, fullIdentity bool, n int) ([]coreiface.CoreAPI, error) {
35-
mn := mocknet.New(ctx)
35+
mn := mocknet.New()
3636

3737
nodes := make([]*core.IpfsNode, n)
3838
apis := make([]coreiface.CoreAPI, n)

core/mock/mock.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,10 @@ import (
2626

2727
// NewMockNode constructs an IpfsNode for use in tests.
2828
func NewMockNode() (*core.IpfsNode, error) {
29-
ctx := context.Background()
30-
3129
// effectively offline, only peer in its network
32-
return core.NewNode(ctx, &core.BuildCfg{
30+
return core.NewNode(context.Background(), &core.BuildCfg{
3331
Online: true,
34-
Host: MockHostOption(mocknet.New(ctx)),
32+
Host: MockHostOption(mocknet.New()),
3533
})
3634
}
3735

0 commit comments

Comments
 (0)