2018-03-14 09:36:59 +00:00
|
|
|
package network
|
|
|
|
|
|
|
|
import (
|
network: rework broadcast logic
We have a number of queues for different purposes:
* regular broadcast queue
* direct p2p queue
* high-priority queue
And two basic egress scenarios:
* direct p2p messages (replies to requests in Server's handle* methods)
* broadcasted messages
Low priority broadcasted messages:
* transaction inventories
* block inventories
* notary inventories
* non-consensus extensibles
High-priority broadcasted messages:
* consensus extensibles
* getdata transaction requests from consensus process
* getaddr requests
P2P messages are a bit more complicated, most of the time they use p2p queue,
but extensible message requests/replies use HP queue.
Server's handle* code is run from Peer's handleIncoming, every peer has this
thread that handles incoming messages. When working with the peer it's
important to reply to requests and blocking this thread until we send (queue)
a reply is fine, if the peer is slow we just won't get anything new from
it. The queue used is irrelevant wrt this issue.
Broadcasted messages are radically different, we want them to be delivered to
many peers, but we don't care about specific ones. If it's delivered to 2/3 of
the peers we're fine, if it's delivered to more of them --- it's not an
issue. But doing this fairly is not an easy thing, current code tries performing
unblocked sends and if this doesn't yield enough results it then blocks (but
has a timeout, we can't wait indefinitely). But it does so in sequential
manner, once the peer is chosen the code will wait for it (and only it) until
timeout happens.
What can be done instead is an attempt to push the message to all of the peers
simultaneously (or close to that). If they all deliver --- OK, if some block
and wait then we can wait until _any_ of them pushes the message through (or
global timeout happens, we still can't wait forever). If we have enough
deliveries then we can cancel pending ones and it's again not an error if
these canceled threads still do their job.
This makes the system more dynamic and adds some substantial processing
overhead, but it's a networking code, any of this overhead is much lower than
the actual packet delivery time. It also allows to spread the load more
fairly, if there is any spare queue it'll get the packet and release the
broadcaster. On the next broadcast iteration another peer is more likely to be
chosen just because it didn't get a message previously (and had some time to
deliver already queued messages).
It works perfectly in tests, with optimal networking conditions we have much
better block times and TPS increases by 5-25%% depending on the scenario.
I'd go as far as to say that it fixes the original problem of #2678, because
in this particular scenario we have empty queues in ~100% of the cases and
this new logic will likely lead to 100% fan out in this case (cancelation just
won't happen fast enough). But when the load grows and there is some waiting
in the queue it will optimize out the slowest links.
2022-10-10 19:48:06 +00:00
|
|
|
"context"
|
2020-12-07 09:52:19 +00:00
|
|
|
"fmt"
|
2019-09-09 14:54:38 +00:00
|
|
|
"net"
|
2020-12-07 09:52:19 +00:00
|
|
|
"sync"
|
2019-09-25 16:54:31 +00:00
|
|
|
"sync/atomic"
|
2018-03-14 09:36:59 +00:00
|
|
|
"testing"
|
2020-12-07 09:52:19 +00:00
|
|
|
"time"
|
2018-03-14 09:36:59 +00:00
|
|
|
|
2021-02-01 10:50:08 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/internal/fakechain"
|
2021-09-13 08:41:54 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
2020-03-03 14:21:42 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
2020-05-22 09:59:18 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/network/capability"
|
2020-03-03 14:21:42 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
2020-12-07 09:52:19 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2019-12-30 07:43:05 +00:00
|
|
|
"go.uber.org/zap/zaptest"
|
2018-03-14 09:36:59 +00:00
|
|
|
)
|
|
|
|
|
2020-12-07 09:52:19 +00:00
|
|
|
type testDiscovery struct {
|
|
|
|
sync.Mutex
|
|
|
|
bad []string
|
|
|
|
connected []string
|
|
|
|
unregistered []string
|
|
|
|
backfill []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func newTestDiscovery([]string, time.Duration, Transporter) Discoverer { return new(testDiscovery) }
|
2018-03-14 09:36:59 +00:00
|
|
|
|
2020-12-07 09:52:19 +00:00
|
|
|
func (d *testDiscovery) BackFill(addrs ...string) {
|
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
|
|
|
d.backfill = append(d.backfill, addrs...)
|
|
|
|
}
|
|
|
|
func (d *testDiscovery) PoolCount() int { return 0 }
|
|
|
|
func (d *testDiscovery) RegisterBadAddr(addr string) {
|
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
|
|
|
d.bad = append(d.bad, addr)
|
|
|
|
}
|
2022-10-12 19:57:49 +00:00
|
|
|
func (d *testDiscovery) GetFanOut() int {
|
2022-10-13 19:53:20 +00:00
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
|
|
|
return (len(d.connected) + len(d.backfill)) * 2 / 3
|
|
|
|
}
|
|
|
|
func (d *testDiscovery) NetworkSize() int {
|
2022-10-12 19:57:49 +00:00
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
|
|
|
return len(d.connected) + len(d.backfill)
|
|
|
|
}
|
2020-12-07 09:52:19 +00:00
|
|
|
func (d *testDiscovery) RegisterGoodAddr(string, capability.Capabilities) {}
|
|
|
|
func (d *testDiscovery) RegisterConnectedAddr(addr string) {
|
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
|
|
|
d.connected = append(d.connected, addr)
|
|
|
|
}
|
|
|
|
func (d *testDiscovery) UnregisterConnectedAddr(addr string) {
|
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
|
|
|
d.unregistered = append(d.unregistered, addr)
|
|
|
|
}
|
|
|
|
func (d *testDiscovery) UnconnectedPeers() []string {
|
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
|
|
|
return d.unregistered
|
|
|
|
}
|
|
|
|
func (d *testDiscovery) RequestRemote(n int) {}
|
|
|
|
func (d *testDiscovery) BadPeers() []string {
|
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
|
|
|
return d.bad
|
|
|
|
}
|
|
|
|
func (d *testDiscovery) GoodPeers() []AddressWithCapabilities { return []AddressWithCapabilities{} }
|
2018-03-14 09:36:59 +00:00
|
|
|
|
|
|
|
var defaultMessageHandler = func(t *testing.T, msg *Message) {}
|
|
|
|
|
|
|
|
type localPeer struct {
|
2019-09-09 14:54:38 +00:00
|
|
|
netaddr net.TCPAddr
|
2020-01-21 14:26:08 +00:00
|
|
|
server *Server
|
2018-03-14 09:36:59 +00:00
|
|
|
version *payload.Version
|
2020-01-17 10:17:19 +00:00
|
|
|
lastBlockIndex uint32
|
2022-10-12 12:46:58 +00:00
|
|
|
handshaked int32 // TODO: use atomic.Bool after #2626.
|
2020-05-22 09:17:17 +00:00
|
|
|
isFullNode bool
|
2018-03-14 09:36:59 +00:00
|
|
|
t *testing.T
|
|
|
|
messageHandler func(t *testing.T, msg *Message)
|
2020-01-20 16:02:19 +00:00
|
|
|
pingSent int
|
2020-11-25 10:34:38 +00:00
|
|
|
getAddrSent int
|
2020-12-07 09:52:19 +00:00
|
|
|
droppedWith atomic.Value
|
2018-03-14 09:36:59 +00:00
|
|
|
}
|
|
|
|
|
2020-01-21 14:26:08 +00:00
|
|
|
func newLocalPeer(t *testing.T, s *Server) *localPeer {
|
2019-09-09 14:54:38 +00:00
|
|
|
naddr, _ := net.ResolveTCPAddr("tcp", "0.0.0.0:0")
|
2018-03-14 09:36:59 +00:00
|
|
|
return &localPeer{
|
|
|
|
t: t,
|
2020-01-21 14:26:08 +00:00
|
|
|
server: s,
|
2019-09-09 14:54:38 +00:00
|
|
|
netaddr: *naddr,
|
2018-03-14 09:36:59 +00:00
|
|
|
messageHandler: defaultMessageHandler,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-06 07:55:21 +00:00
|
|
|
func (p *localPeer) RemoteAddr() net.Addr {
|
|
|
|
return &p.netaddr
|
|
|
|
}
|
|
|
|
func (p *localPeer) PeerAddr() net.Addr {
|
2019-09-09 14:54:38 +00:00
|
|
|
return &p.netaddr
|
2018-03-14 09:36:59 +00:00
|
|
|
}
|
2020-12-07 09:52:19 +00:00
|
|
|
func (p *localPeer) StartProtocol() {}
|
|
|
|
func (p *localPeer) Disconnect(err error) {
|
|
|
|
if p.droppedWith.Load() == nil {
|
|
|
|
p.droppedWith.Store(err)
|
|
|
|
}
|
|
|
|
fmt.Println("peer dropped:", err)
|
|
|
|
p.server.unregister <- peerDrop{p, err}
|
|
|
|
}
|
2020-01-16 18:16:31 +00:00
|
|
|
|
network: rework broadcast logic
We have a number of queues for different purposes:
* regular broadcast queue
* direct p2p queue
* high-priority queue
And two basic egress scenarios:
* direct p2p messages (replies to requests in Server's handle* methods)
* broadcasted messages
Low priority broadcasted messages:
* transaction inventories
* block inventories
* notary inventories
* non-consensus extensibles
High-priority broadcasted messages:
* consensus extensibles
* getdata transaction requests from consensus process
* getaddr requests
P2P messages are a bit more complicated, most of the time they use p2p queue,
but extensible message requests/replies use HP queue.
Server's handle* code is run from Peer's handleIncoming, every peer has this
thread that handles incoming messages. When working with the peer it's
important to reply to requests and blocking this thread until we send (queue)
a reply is fine, if the peer is slow we just won't get anything new from
it. The queue used is irrelevant wrt this issue.
Broadcasted messages are radically different, we want them to be delivered to
many peers, but we don't care about specific ones. If it's delivered to 2/3 of
the peers we're fine, if it's delivered to more of them --- it's not an
issue. But doing this fairly is not an easy thing, current code tries performing
unblocked sends and if this doesn't yield enough results it then blocks (but
has a timeout, we can't wait indefinitely). But it does so in sequential
manner, once the peer is chosen the code will wait for it (and only it) until
timeout happens.
What can be done instead is an attempt to push the message to all of the peers
simultaneously (or close to that). If they all deliver --- OK, if some block
and wait then we can wait until _any_ of them pushes the message through (or
global timeout happens, we still can't wait forever). If we have enough
deliveries then we can cancel pending ones and it's again not an error if
these canceled threads still do their job.
This makes the system more dynamic and adds some substantial processing
overhead, but it's a networking code, any of this overhead is much lower than
the actual packet delivery time. It also allows to spread the load more
fairly, if there is any spare queue it'll get the packet and release the
broadcaster. On the next broadcast iteration another peer is more likely to be
chosen just because it didn't get a message previously (and had some time to
deliver already queued messages).
It works perfectly in tests, with optimal networking conditions we have much
better block times and TPS increases by 5-25%% depending on the scenario.
I'd go as far as to say that it fixes the original problem of #2678, because
in this particular scenario we have empty queues in ~100% of the cases and
this new logic will likely lead to 100% fan out in this case (cancelation just
won't happen fast enough). But when the load grows and there is some waiting
in the queue it will optimize out the slowest links.
2022-10-10 19:48:06 +00:00
|
|
|
func (p *localPeer) BroadcastPacket(_ context.Context, m []byte) error {
|
2021-03-25 19:25:30 +00:00
|
|
|
msg := &Message{}
|
2020-01-16 18:16:31 +00:00
|
|
|
r := io.NewBinReaderFromBuf(m)
|
|
|
|
err := msg.Decode(r)
|
|
|
|
if err == nil {
|
|
|
|
p.messageHandler(p.t, msg)
|
|
|
|
}
|
2018-04-13 10:14:08 +00:00
|
|
|
return nil
|
2018-03-14 09:36:59 +00:00
|
|
|
}
|
2022-10-12 12:39:20 +00:00
|
|
|
func (p *localPeer) EnqueueP2PMessage(msg *Message) error {
|
|
|
|
return p.EnqueueHPMessage(msg)
|
|
|
|
}
|
|
|
|
func (p *localPeer) BroadcastHPPacket(ctx context.Context, m []byte) error {
|
|
|
|
return p.BroadcastPacket(ctx, m)
|
|
|
|
}
|
|
|
|
func (p *localPeer) EnqueueHPMessage(msg *Message) error {
|
|
|
|
p.messageHandler(p.t, msg)
|
|
|
|
return nil
|
|
|
|
}
|
2018-03-14 09:36:59 +00:00
|
|
|
func (p *localPeer) Version() *payload.Version {
|
|
|
|
return p.version
|
|
|
|
}
|
2020-01-17 10:17:19 +00:00
|
|
|
func (p *localPeer) LastBlockIndex() uint32 {
|
|
|
|
return p.lastBlockIndex
|
|
|
|
}
|
2019-09-13 12:43:22 +00:00
|
|
|
func (p *localPeer) HandleVersion(v *payload.Version) error {
|
2018-04-13 10:14:08 +00:00
|
|
|
p.version = v
|
2019-09-13 12:43:22 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-01-21 14:26:08 +00:00
|
|
|
func (p *localPeer) SendVersion() error {
|
2020-05-22 09:17:17 +00:00
|
|
|
m, err := p.server.getVersionMsg()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-10-12 12:39:20 +00:00
|
|
|
_ = p.EnqueueHPMessage(m)
|
2020-01-16 18:16:31 +00:00
|
|
|
return nil
|
2019-09-13 12:43:22 +00:00
|
|
|
}
|
|
|
|
func (p *localPeer) SendVersionAck(m *Message) error {
|
2022-10-12 12:39:20 +00:00
|
|
|
_ = p.EnqueueHPMessage(m)
|
2020-01-16 18:16:31 +00:00
|
|
|
return nil
|
2019-09-13 12:43:22 +00:00
|
|
|
}
|
|
|
|
func (p *localPeer) HandleVersionAck() error {
|
2022-10-12 12:46:58 +00:00
|
|
|
atomic.StoreInt32(&p.handshaked, 1)
|
2019-09-13 12:43:22 +00:00
|
|
|
return nil
|
|
|
|
}
|
2022-10-12 12:25:03 +00:00
|
|
|
func (p *localPeer) SetPingTimer() {
|
2020-01-20 16:02:19 +00:00
|
|
|
p.pingSent++
|
2020-01-17 10:17:19 +00:00
|
|
|
}
|
2020-08-14 13:22:15 +00:00
|
|
|
func (p *localPeer) HandlePing(ping *payload.Ping) error {
|
|
|
|
p.lastBlockIndex = ping.LastBlockIndex
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-01-20 16:02:19 +00:00
|
|
|
func (p *localPeer) HandlePong(pong *payload.Ping) error {
|
|
|
|
p.lastBlockIndex = pong.LastBlockIndex
|
|
|
|
p.pingSent--
|
|
|
|
return nil
|
2020-01-17 10:17:19 +00:00
|
|
|
}
|
2019-09-13 12:43:22 +00:00
|
|
|
|
|
|
|
func (p *localPeer) Handshaked() bool {
|
2022-10-12 12:46:58 +00:00
|
|
|
return atomic.LoadInt32(&p.handshaked) != 0
|
2018-04-13 10:14:08 +00:00
|
|
|
}
|
2018-03-14 09:36:59 +00:00
|
|
|
|
2020-05-22 09:17:17 +00:00
|
|
|
func (p *localPeer) IsFullNode() bool {
|
|
|
|
return p.isFullNode
|
|
|
|
}
|
|
|
|
|
2020-11-25 10:34:38 +00:00
|
|
|
func (p *localPeer) AddGetAddrSent() {
|
|
|
|
p.getAddrSent++
|
|
|
|
}
|
|
|
|
func (p *localPeer) CanProcessAddr() bool {
|
|
|
|
p.getAddrSent--
|
|
|
|
return p.getAddrSent >= 0
|
|
|
|
}
|
|
|
|
|
2020-05-22 09:17:17 +00:00
|
|
|
func newTestServer(t *testing.T, serverConfig ServerConfig) *Server {
|
2021-09-13 08:41:54 +00:00
|
|
|
return newTestServerWithCustomCfg(t, serverConfig, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func newTestServerWithCustomCfg(t *testing.T, serverConfig ServerConfig, protocolCfg func(*config.ProtocolConfiguration)) *Server {
|
2022-01-12 21:20:03 +00:00
|
|
|
s, err := newServerFromConstructors(serverConfig, fakechain.NewFakeChainWithCustomCfg(protocolCfg), new(fakechain.FakeStateSync), zaptest.NewLogger(t),
|
2022-01-12 20:04:07 +00:00
|
|
|
newFakeTransp, newTestDiscovery)
|
2020-12-07 09:52:19 +00:00
|
|
|
require.NoError(t, err)
|
2020-05-22 09:17:17 +00:00
|
|
|
return s
|
2018-03-14 09:36:59 +00:00
|
|
|
}
|