feat: implement peering service
MVP for #6097 This feature will repeatedly reconnect (with a randomized exponential backoff) to peers in a set of "peered" peers. In the future, this should be extended to: 1. Include a CLI for modifying this list at runtime. 2. Include additional options for peers we want to _protect_ but not connect to. 3. Allow configuring timeouts, backoff, etc. 4. Allow groups? Possibly through textile threads. 5. Allow for runtime-only peering rules. 6. Different reconnect policies. But this MVP should be a significant step forward.
Steven Allen committed
May 25, 2020 at 09:26 UTC
978091a626a0e1f00a797fc4e2de99f4bfee943b
9 files changed
+457
-3
core/core.go
+2
@@ -48,6 +48,7 @@ import (
48
"github.com/ipfs/go-ipfs/namesys"
49
ipnsrp "github.com/ipfs/go-ipfs/namesys/republisher"
50
"github.com/ipfs/go-ipfs/p2p"
51
+ "github.com/ipfs/go-ipfs/peering"
52
"github.com/ipfs/go-ipfs/repo"
53
)
54
@@ -83,6 +84,7 @@ type IpfsNode struct {
84
85
// Online
86
PeerHost p2phost.Host `optional:"true"` // the network host (server+client)
87
+ Peering peering.PeeringService `optional:"true"`
88
Filters *ma.Filters `optional:"true"`
89
Bootstrapper io.Closer `optional:"true"` // the periodic bootstrapper
90
Routing routing.Routing `optional:"true"` // the routing system. recommend ipfs-dht
core/node/groups.go
+2
@@ -250,6 +250,8 @@ func Online(bcfg *BuildCfg, cfg *config.Config) fx.Option {
250
fx.Provide(OnlineExchange(shouldBitswapProvide)),
251
maybeProvide(Graphsync, cfg.Experimental.GraphsyncEnabled),
252
fx.Provide(Namesys(ipnsCacheSize)),
253
+ fx.Provide(Peering),
254
+ PeerWith(cfg.Peering.Peers...),
255
256
fx.Invoke(IpnsRepublisher(repubPeriod, recordLifetime)),
257
core/node/peering.go
new
+34
@@ -0,0 +1,34 @@
1
+package node
2
+
3
+import (
4
+ "context"
5
+
6
+ "github.com/ipfs/go-ipfs/peering"
7
+ "github.com/libp2p/go-libp2p-core/host"
8
+ "github.com/libp2p/go-libp2p-core/peer"
9
+ "go.uber.org/fx"
10
+)
11
+
12
+// Peering constructs the peering service and hooks it into fx's lifetime
13
+// management system.
14
+func Peering(lc fx.Lifecycle, host host.Host) *peering.PeeringService {
15
+ ps := peering.NewPeeringService(host)
16
+ lc.Append(fx.Hook{
17
+ OnStart: func(context.Context) error {
18
+ return ps.Start()
19
+ },
20
+ OnStop: func(context.Context) error {
21
+ return ps.Stop()
22
+ },
23
+ })
24
+ return ps
25
+}
26
+
27
+// PeerWith configures the peering service to peer with the specified peers.
28
+func PeerWith(peers ...peer.AddrInfo) fx.Option {
29
+ return fx.Invoke(func(ps *peering.PeeringService) {
30
+ for _, ai := range peers {
31
+ ps.AddPeer(ai)
32
+ }
33
+ })
34
+}
docs/config.md
+23
@@ -139,6 +139,8 @@ documented in `ipfs config profile --help`.
139
- [`Pubsub`](#pubsub)
140
- [`Pubsub.Router`](#pubsubrouter)
141
- [`Pubsub.DisableSigning`](#pubsubdisablesigning)
142
+ - [`Peering`](#peering)
143
+ - [`Peering.Peers`](#peeringpeers)
144
- [`Reprovider`](#reprovider)
145
- [`Reprovider.Interval`](#reproviderinterval)
146
- [`Reprovider.Strategy`](#reproviderstrategy)
@@ -157,6 +159,7 @@ documented in `ipfs config profile --help`.
159
- [`Swarm.ConnMgr.HighWater`](#swarmconnmgrhighwater)
160
- [`Swarm.ConnMgr.GracePeriod`](#swarmconnmgrgraceperiod)
161
162
+
163
## `Addresses`
164
165
Contains information about various listener addresses to be used by this node.
@@ -703,6 +706,26 @@ intentionally re-using the real message's message ID.
706
707
Default: `false`
708
709
+### `Peering`
710
+
711
+Configures the peering subsystem. The peering subsystem configures go-ipfs to
712
+connect to, remain connected to, and reconnect to a set of peers. Peers should
713
+use this subsystem to create "sticky" links between frequently used peers for
714
+improved reliability.
715
+
716
+#### `Peering.Peers`
717
+
718
+The set of peers with which to peer. Each entry is of the form:
719
+
720
+```js
721
+{
722
+ "ID": "QmSomePeerID", # The peers ID.
723
+ "Addrs": ["/ip4/1.2.3.4/tcp/1234"] # Known addresses for the peer. If none are specified, the DHT will be queried.
724
+}
725
+```
726
+
727
+Additional fields may be added in the future.
728
+
729
## `Reprovider`
730
731
### `Reprovider.Interval`
go.mod
+2
-1
@@ -32,7 +32,7 @@ require (
32
github.com/ipfs/go-ipfs-blockstore v0.1.4
33
github.com/ipfs/go-ipfs-chunker v0.0.5
34
github.com/ipfs/go-ipfs-cmds v0.2.9
35
- github.com/ipfs/go-ipfs-config v0.6.1
35
+ github.com/ipfs/go-ipfs-config v0.7.0
36
github.com/ipfs/go-ipfs-ds-help v0.1.1
37
github.com/ipfs/go-ipfs-exchange-interface v0.0.1
38
github.com/ipfs/go-ipfs-exchange-offline v0.0.1
@@ -94,6 +94,7 @@ require (
94
github.com/opentracing/opentracing-go v1.1.0
95
github.com/pkg/errors v0.9.1
96
github.com/prometheus/client_golang v1.6.0
97
+ github.com/stretchr/testify v1.5.1
98
github.com/syndtr/goleveldb v1.0.0
99
github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc
100
github.com/whyrusleeping/go-sysinfo v0.0.0-20190219211824-4a357d4b90b1
go.sum
+2
-2
@@ -301,8 +301,8 @@ github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7Na
301
github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8=
302
github.com/ipfs/go-ipfs-cmds v0.2.9 h1:zQTENe9UJrtCb2bOtRoDGjtuo3rQjmuPdPnVlqoBV/M=
303
github.com/ipfs/go-ipfs-cmds v0.2.9/go.mod h1:ZgYiWVnCk43ChwoH8hAmI1IRbuVtq3GSTHwtRB/Kqhk=
304
-github.com/ipfs/go-ipfs-config v0.6.1 h1:d1f0fEEpUQ9R+6c0VZMNy2P+wCl4K4DO4VHJBvgWwFw=
305
-github.com/ipfs/go-ipfs-config v0.6.1/go.mod h1:GQUxqb0NfkZmEU92PxqqqLVVFTLpoGGUlBaTyDaAqrE=
304
+github.com/ipfs/go-ipfs-config v0.7.0 h1:cClINg8v28//KaYMwt1aSjbS8eGJjNKIEnahpT/2hYk=
305
+github.com/ipfs/go-ipfs-config v0.7.0/go.mod h1:GQUxqb0NfkZmEU92PxqqqLVVFTLpoGGUlBaTyDaAqrE=
306
github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
307
github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ=
308
github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
peering/peering.go
new
+259
@@ -0,0 +1,259 @@
1
+package peering
2
+
3
+import (
4
+ "context"
5
+ "errors"
6
+ "math/rand"
7
+ "sync"
8
+ "time"
9
+
10
+ "github.com/ipfs/go-log"
11
+ "github.com/libp2p/go-libp2p-core/host"
12
+ "github.com/libp2p/go-libp2p-core/network"
13
+ "github.com/libp2p/go-libp2p-core/peer"
14
+ "github.com/multiformats/go-multiaddr"
15
+)
16
+
17
+// maxBackoff is the maximum time between reconnect attempts.
18
+const (
19
+ maxBackoff = 10 * time.Minute
20
+ connmgrTag = "ipfs-peering"
21
+ // This needs to be sufficient to prevent two sides from simultaneously
22
+ // dialing.
23
+ initialDelay = 5 * time.Second
24
+)
25
+
26
+var logger = log.Logger("peering")
27
+
28
+type state int
29
+
30
+const (
31
+ stateInit state = iota
32
+ stateRunning
33
+ stateStopped
34
+)
35
+
36
+// peerHandler keeps track of all state related to a specific "peering" peer.
37
+type peerHandler struct {
38
+ peer peer.ID
39
+ host host.Host
40
+ ctx context.Context
41
+ cancel context.CancelFunc
42
+
43
+ mu sync.Mutex
44
+ addrs []multiaddr.Multiaddr
45
+ timer *time.Timer
46
+
47
+ nextDelay time.Duration
48
+}
49
+
50
+func (ph *peerHandler) stop() {
51
+ ph.mu.Lock()
52
+ defer ph.mu.Unlock()
53
+
54
+ if ph.timer != nil {
55
+ ph.timer.Stop()
56
+ ph.timer = nil
57
+ }
58
+}
59
+
60
+func (ph *peerHandler) nextBackoff() time.Duration {
61
+ // calculate the timeout
62
+ if ph.nextDelay < maxBackoff {
63
+ ph.nextDelay += ph.nextDelay/2 + time.Duration(rand.Int63n(int64(ph.nextDelay)))
64
+ }
65
+ return ph.nextDelay
66
+}
67
+
68
+func (ph *peerHandler) reconnect() {
69
+ // Try connecting
70
+
71
+ ph.mu.Lock()
72
+ addrs := append(([]multiaddr.Multiaddr)(nil), ph.addrs...)
73
+ ph.mu.Unlock()
74
+
75
+ logger.Debugw("reconnecting", "peer", ph.peer, "addrs", addrs)
76
+
77
+ err := ph.host.Connect(ph.ctx, peer.AddrInfo{ID: ph.peer, Addrs: addrs})
78
+ if err != nil {
79
+ logger.Debugw("failed to reconnect", "peer", ph.peer, "error", err)
80
+ // Ok, we failed. Extend the timeout.
81
+ ph.mu.Lock()
82
+ if ph.timer != nil {
83
+ // Only counts if the timer still exists. If not, a
84
+ // connection _was_ somehow established.
85
+ ph.timer.Reset(ph.nextBackoff())
86
+ }
87
+ // Otherwise, someone else has stopped us so we can assume that
88
+ // we're either connected or someone else will start us.
89
+ ph.mu.Unlock()
90
+ }
91
+
92
+ // Always call this. We could have connected since we processed the
93
+ // error.
94
+ ph.stopIfConnected()
95
+}
96
+
97
+func (ph *peerHandler) stopIfConnected() {
98
+ ph.mu.Lock()
99
+ defer ph.mu.Unlock()
100
+
101
+ if ph.timer != nil && ph.host.Network().Connectedness(ph.peer) == network.Connected {
102
+ logger.Debugw("successfully reconnected", "peer", ph.peer)
103
+ ph.timer.Stop()
104
+ ph.timer = nil
105
+ ph.nextDelay = initialDelay
106
+ }
107
+}
108
+
109
+// startIfDisconnected is the inverse of stopIfConnected.
110
+func (ph *peerHandler) startIfDisconnected() {
111
+ ph.mu.Lock()
112
+ defer ph.mu.Unlock()
113
+
114
+ if ph.timer == nil && ph.host.Network().Connectedness(ph.peer) != network.Connected {
115
+ logger.Debugw("disconnected from peer", "peer", ph.peer)
116
+ // Always start with a short timeout so we can stagger things a bit.
117
+ ph.timer = time.AfterFunc(ph.nextBackoff(), ph.reconnect)
118
+ }
119
+}
120
+
121
+// PeeringService maintains connections to specified peers, reconnecting on
122
+// disconnect with a back-off.
123
+type PeeringService struct {
124
+ host host.Host
125
+
126
+ mu sync.RWMutex
127
+ peers map[peer.ID]*peerHandler
128
+
129
+ ctx context.Context
130
+ cancel context.CancelFunc
131
+ state state
132
+}
133
+
134
+// NewPeeringService constructs a new peering service. Peers can be added and
135
+// removed immediately, but connections won't be formed until `Start` is called.
136
+func NewPeeringService(host host.Host) *PeeringService {
137
+ ps := &PeeringService{host: host, peers: make(map[peer.ID]*peerHandler)}
138
+ ps.ctx, ps.cancel = context.WithCancel(context.Background())
139
+ return ps
140
+}
141
+
142
+// Start starts the peering service, connecting and maintaining connections to
143
+// all registered peers. It returns an error if the service has already been
144
+// stopped.
145
+func (ps *PeeringService) Start() error {
146
+ ps.mu.Lock()
147
+ defer ps.mu.Unlock()
148
+
149
+ switch ps.state {
150
+ case stateInit:
151
+ logger.Infow("starting")
152
+ case stateRunning:
153
+ return nil
154
+ case stateStopped:
155
+ return errors.New("already stopped")
156
+ }
157
+ ps.host.Network().Notify((*netNotifee)(ps))
158
+ ps.state = stateRunning
159
+ for _, handler := range ps.peers {
160
+ go handler.startIfDisconnected()
161
+ }
162
+ return nil
163
+}
164
+
165
+// Stop stops the peering service.
166
+func (ps *PeeringService) Stop() error {
167
+ ps.cancel()
168
+ ps.host.Network().StopNotify((*netNotifee)(ps))
169
+
170
+ ps.mu.Lock()
171
+ defer ps.mu.Unlock()
172
+
173
+ if ps.state == stateRunning {
174
+ logger.Infow("stopping")
175
+ for _, handler := range ps.peers {
176
+ handler.stop()
177
+ }
178
+ }
179
+ return nil
180
+}
181
+
182
+// AddPeer adds a peer to the peering service. This function may be safely
183
+// called at any time: before the service is started, while running, or after it
184
+// stops.
185
+//
186
+// Add peer may also be called multiple times for the same peer. The new
187
+// addresses will replace the old.
188
+func (ps *PeeringService) AddPeer(info peer.AddrInfo) {
189
+ ps.mu.Lock()
190
+ defer ps.mu.Unlock()
191
+
192
+ if handler, ok := ps.peers[info.ID]; ok {
193
+ logger.Infow("updating addresses", "peer", info.ID, "addrs", info.Addrs)
194
+ handler.addrs = info.Addrs
195
+ } else {
196
+ logger.Infow("peer added", "peer", info.ID, "addrs", info.Addrs)
197
+ ps.host.ConnManager().Protect(info.ID, connmgrTag)
198
+
199
+ handler = &peerHandler{
200
+ host: ps.host,
201
+ peer: info.ID,
202
+ addrs: info.Addrs,
203
+ nextDelay: initialDelay,
204
+ }
205
+ handler.ctx, handler.cancel = context.WithCancel(ps.ctx)
206
+ ps.peers[info.ID] = handler
207
+ if ps.state == stateRunning {
208
+ go handler.startIfDisconnected()
209
+ }
210
+ }
211
+}
212
+
213
+// RemovePeer removes a peer from the peering service. This function may be
214
+// safely called at any time: before the service is started, while running, or
215
+// after it stops.
216
+func (ps *PeeringService) RemovePeer(id peer.ID) {
217
+ ps.mu.Lock()
218
+ defer ps.mu.Unlock()
219
+
220
+ if handler, ok := ps.peers[id]; ok {
221
+ logger.Infow("peer removed", "peer", id)
222
+ ps.host.ConnManager().Unprotect(id, connmgrTag)
223
+
224
+ handler.stop()
225
+ handler.cancel()
226
+ delete(ps.peers, id)
227
+ }
228
+}
229
+
230
+type netNotifee PeeringService
231
+
232
+func (nn *netNotifee) Connected(_ network.Network, c network.Conn) {
233
+ ps := (*PeeringService)(nn)
234
+
235
+ p := c.RemotePeer()
236
+ ps.mu.RLock()
237
+ defer ps.mu.RUnlock()
238
+
239
+ if handler, ok := ps.peers[p]; ok {
240
+ // use a goroutine to avoid blocking events.
241
+ go handler.stopIfConnected()
242
+ }
243
+}
244
+func (nn *netNotifee) Disconnected(_ network.Network, c network.Conn) {
245
+ ps := (*PeeringService)(nn)
246
+
247
+ p := c.RemotePeer()
248
+ ps.mu.RLock()
249
+ defer ps.mu.RUnlock()
250
+
251
+ if handler, ok := ps.peers[p]; ok {
252
+ // use a goroutine to avoid blocking events.
253
+ go handler.startIfDisconnected()
254
+ }
255
+}
256
+func (nn *netNotifee) OpenedStream(network.Network, network.Stream) {}
257
+func (nn *netNotifee) ClosedStream(network.Network, network.Stream) {}
258
+func (nn *netNotifee) Listen(network.Network, multiaddr.Multiaddr) {}
259
+func (nn *netNotifee) ListenClose(network.Network, multiaddr.Multiaddr) {}
peering/peering_test.go
new
+6
@@ -0,0 +1,6 @@
1
+package peering
2
+
3
+import "testing"
4
+
5
+func TestPeeringService(t *testing.T) {
6
+}
test/sharness/t0171-peering.sh
new
+127
@@ -0,0 +1,127 @@
1
+#!/usr/bin/env bash
2
+
3
+test_description="Test peering service"
4
+
5
+. lib/test-lib.sh
6
+
7
+NUM_NODES=3
8
+
9
+test_expect_success 'init iptb' '
10
+ rm -rf .iptb/ &&
11
+ iptb testbed create -type localipfs -count $NUM_NODES -init
12
+'
13
+
14
+test_expect_success 'disabling routing' '
15
+ iptb run -- ipfs config Routing.Type none
16
+'
17
+
18
+for i in $(seq 0 2); do
19
+ ADDR="$(printf '["/ip4/127.0.0.1/tcp/%s"]' "$(( 3000 + ( RANDOM % 1000 ) ))")"
20
+ test_expect_success "configuring node $i to listen on $ADDR" '
21
+ ipfsi "$i" config --json Addresses.Swarm "$ADDR"
22
+ '
23
+done
24
+
25
+peer_id() {
26
+ ipfsi "$1" config Identity.PeerID
27
+}
28
+
29
+peer_addrs() {
30
+ ipfsi "$1" config Addresses.Swarm
31
+}
32
+
33
+peer() {
34
+ PEER1="$1" &&
35
+ PEER2="$2" &&
36
+ PEER_LIST="$(ipfsi "$PEER1" config Peering.Peers)" &&
37
+ { [[ "$PEER_LIST" == "null" ]] || PEER_LIST_INNER="${PEER_LIST:1:-1}"; } &&
38
+ ADDR_INFO="$(printf '[%s{"ID": "%s", "Addrs": %s}]' \
39
+ "${PEER_LIST_INNER:+${PEER_LIST_INNER},}" \
40
+ "$(peer_id "$PEER2")" \
41
+ "$(peer_addrs "$PEER2")")" &&
42
+ ipfsi "$PEER1" config --json Peering.Peers "${ADDR_INFO}"
43
+}
44
+
45
+# Peer:
46
+# - 0 <-> 1
47
+# - 1 -> 2
48
+test_expect_success 'configure peering' '
49
+ peer 0 1 &&
50
+ peer 1 0 &&
51
+ peer 1 2
52
+'
53
+
54
+list_peers() {
55
+ ipfsi "$1" swarm peers | sed 's|.*/p2p/\([^/]*\)$|\1|' | sort -u
56
+}
57
+
58
+check_peers() {
59
+ sleep 20 # give it some time to settle.
60
+ test_expect_success 'verifying peering for peer 0' '
61
+ list_peers 0 > peers_0_actual &&
62
+ peer_id 1 > peers_0_expected &&
63
+ test_cmp peers_0_expected peers_0_actual
64
+ '
65
+
66
+ test_expect_success 'verifying peering for peer 1' '
67
+ list_peers 1 > peers_1_actual &&
68
+ { peer_id 0 && peer_id 2 ; } | sort -u > peers_1_expected &&
69
+ test_cmp peers_1_expected peers_1_actual
70
+ '
71
+
72
+ test_expect_success 'verifying peering for peer 2' '
73
+ list_peers 2 > peers_2_actual &&
74
+ peer_id 1 > peers_2_expected &&
75
+ test_cmp peers_2_expected peers_2_actual
76
+ '
77
+}
78
+
79
+test_expect_success 'startup cluster' '
80
+ iptb start -wait &&
81
+ iptb run -- ipfs log level peering debug
82
+'
83
+
84
+check_peers
85
+
86
+disconnect() {
87
+ ipfsi "$1" swarm disconnect "/p2p/$(peer_id "$2")"
88
+}
89
+
90
+# Bidiractional peering shouldn't cause problems (e.g., simultaneous connect
91
+# issues).
92
+test_expect_success 'disconnecting 0->1' '
93
+ disconnect 0 1
94
+'
95
+
96
+check_peers
97
+
98
+# 1 should reconnect to 2 when 2 disconnects from 1.
99
+test_expect_success 'disconnecting 2->1' '
100
+ disconnect 2 1
101
+'
102
+
103
+check_peers
104
+
105
+# 2 isn't peering. This test ensures that 1 will re-peer with 2 when it comes
106
+# back online.
107
+test_expect_success 'stopping 2' '
108
+ iptb stop 2
109
+'
110
+
111
+# Wait to disconnect
112
+sleep 30
113
+
114
+test_expect_success 'starting 2' '
115
+ iptb start 2
116
+'
117
+
118
+# Wait for backoff
119
+sleep 30
120
+
121
+check_peers
122
+
123
+test_expect_success "stop testbed" '
124
+ iptb stop
125
+'
126
+
127
+test_done