swarm: codebase split from go-ethereum (#1405)
This commit is contained in:
committed by
Anton Evangelatov
parent
7a22da98b9
commit
b046760db1
79
network/simulation/bucket.go
Normal file
79
network/simulation/bucket.go
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import "github.com/ethereum/go-ethereum/p2p/enode"
|
||||
|
||||
// BucketKey is the type that should be used for keys in simulation buckets.
|
||||
type BucketKey string
|
||||
|
||||
// NodeItem returns an item set in ServiceFunc function for a particular node.
|
||||
func (s *Simulation) NodeItem(id enode.ID, key interface{}) (value interface{}, ok bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.buckets[id]; !ok {
|
||||
return nil, false
|
||||
}
|
||||
return s.buckets[id].Load(key)
|
||||
}
|
||||
|
||||
// SetNodeItem sets a new item associated with the node with provided NodeID.
|
||||
// Buckets should be used to avoid managing separate simulation global state.
|
||||
func (s *Simulation) SetNodeItem(id enode.ID, key interface{}, value interface{}) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.buckets[id].Store(key, value)
|
||||
}
|
||||
|
||||
// NodesItems returns a map of items from all nodes that are all set under the
|
||||
// same BucketKey.
|
||||
func (s *Simulation) NodesItems(key interface{}) (values map[enode.ID]interface{}) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
ids := s.NodeIDs()
|
||||
values = make(map[enode.ID]interface{}, len(ids))
|
||||
for _, id := range ids {
|
||||
if _, ok := s.buckets[id]; !ok {
|
||||
continue
|
||||
}
|
||||
if v, ok := s.buckets[id].Load(key); ok {
|
||||
values[id] = v
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// UpNodesItems returns a map of items with the same BucketKey from all nodes that are up.
|
||||
func (s *Simulation) UpNodesItems(key interface{}) (values map[enode.ID]interface{}) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
ids := s.UpNodeIDs()
|
||||
values = make(map[enode.ID]interface{})
|
||||
for _, id := range ids {
|
||||
if _, ok := s.buckets[id]; !ok {
|
||||
continue
|
||||
}
|
||||
if v, ok := s.buckets[id].Load(key); ok {
|
||||
values[id] = v
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
155
network/simulation/bucket_test.go
Normal file
155
network/simulation/bucket_test.go
Normal file
@ -0,0 +1,155 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
)
|
||||
|
||||
// TestServiceBucket tests all bucket functionality using subtests.
|
||||
// It constructs a simulation of two nodes by adding items to their buckets
|
||||
// in ServiceFunc constructor, then by SetNodeItem. Testing UpNodesItems
|
||||
// is done by stopping one node and validating availability of its items.
|
||||
func TestServiceBucket(t *testing.T) {
|
||||
testKey := "Key"
|
||||
testValue := "Value"
|
||||
|
||||
sim := New(map[string]ServiceFunc{
|
||||
"noop": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
|
||||
b.Store(testKey, testValue+ctx.Config.ID.String())
|
||||
return newNoopService(), nil, nil
|
||||
},
|
||||
})
|
||||
defer sim.Close()
|
||||
|
||||
id1, err := sim.AddNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id2, err := sim.AddNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("ServiceFunc bucket Store", func(t *testing.T) {
|
||||
v, ok := sim.NodeItem(id1, testKey)
|
||||
if !ok {
|
||||
t.Fatal("bucket item not found")
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
t.Fatal("bucket item value is not string")
|
||||
}
|
||||
if s != testValue+id1.String() {
|
||||
t.Fatalf("expected %q, got %q", testValue+id1.String(), s)
|
||||
}
|
||||
|
||||
v, ok = sim.NodeItem(id2, testKey)
|
||||
if !ok {
|
||||
t.Fatal("bucket item not found")
|
||||
}
|
||||
s, ok = v.(string)
|
||||
if !ok {
|
||||
t.Fatal("bucket item value is not string")
|
||||
}
|
||||
if s != testValue+id2.String() {
|
||||
t.Fatalf("expected %q, got %q", testValue+id2.String(), s)
|
||||
}
|
||||
})
|
||||
|
||||
customKey := "anotherKey"
|
||||
customValue := "anotherValue"
|
||||
|
||||
t.Run("SetNodeItem", func(t *testing.T) {
|
||||
sim.SetNodeItem(id1, customKey, customValue)
|
||||
|
||||
v, ok := sim.NodeItem(id1, customKey)
|
||||
if !ok {
|
||||
t.Fatal("bucket item not found")
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
t.Fatal("bucket item value is not string")
|
||||
}
|
||||
if s != customValue {
|
||||
t.Fatalf("expected %q, got %q", customValue, s)
|
||||
}
|
||||
|
||||
_, ok = sim.NodeItem(id2, customKey)
|
||||
if ok {
|
||||
t.Fatal("bucket item should not be found")
|
||||
}
|
||||
})
|
||||
|
||||
if err := sim.StopNode(id2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("UpNodesItems", func(t *testing.T) {
|
||||
items := sim.UpNodesItems(testKey)
|
||||
|
||||
v, ok := items[id1]
|
||||
if !ok {
|
||||
t.Errorf("node 1 item not found")
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
t.Fatal("node 1 item value is not string")
|
||||
}
|
||||
if s != testValue+id1.String() {
|
||||
t.Fatalf("expected %q, got %q", testValue+id1.String(), s)
|
||||
}
|
||||
|
||||
_, ok = items[id2]
|
||||
if ok {
|
||||
t.Errorf("node 2 item should not be found")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NodeItems", func(t *testing.T) {
|
||||
items := sim.NodesItems(testKey)
|
||||
|
||||
v, ok := items[id1]
|
||||
if !ok {
|
||||
t.Errorf("node 1 item not found")
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
t.Fatal("node 1 item value is not string")
|
||||
}
|
||||
if s != testValue+id1.String() {
|
||||
t.Fatalf("expected %q, got %q", testValue+id1.String(), s)
|
||||
}
|
||||
|
||||
v, ok = items[id2]
|
||||
if !ok {
|
||||
t.Errorf("node 2 item not found")
|
||||
}
|
||||
s, ok = v.(string)
|
||||
if !ok {
|
||||
t.Fatal("node 1 item value is not string")
|
||||
}
|
||||
if s != testValue+id2.String() {
|
||||
t.Fatalf("expected %q, got %q", testValue+id2.String(), s)
|
||||
}
|
||||
})
|
||||
}
|
217
network/simulation/events.go
Normal file
217
network/simulation/events.go
Normal file
@ -0,0 +1,217 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations"
|
||||
)
|
||||
|
||||
// PeerEvent is the type of the channel returned by Simulation.PeerEvents.
|
||||
type PeerEvent struct {
|
||||
// NodeID is the ID of node that the event is caught on.
|
||||
NodeID enode.ID
|
||||
// PeerID is the ID of the peer node that the event is caught on.
|
||||
PeerID enode.ID
|
||||
// Event is the event that is caught.
|
||||
Event *simulations.Event
|
||||
// Error is the error that may have happened during event watching.
|
||||
Error error
|
||||
}
|
||||
|
||||
// PeerEventsFilter defines a filter on PeerEvents to exclude messages with
|
||||
// defined properties. Use PeerEventsFilter methods to set required options.
|
||||
type PeerEventsFilter struct {
|
||||
eventType simulations.EventType
|
||||
|
||||
connUp *bool
|
||||
|
||||
msgReceive *bool
|
||||
protocol *string
|
||||
msgCode *uint64
|
||||
}
|
||||
|
||||
// NewPeerEventsFilter returns a new PeerEventsFilter instance.
|
||||
func NewPeerEventsFilter() *PeerEventsFilter {
|
||||
return &PeerEventsFilter{}
|
||||
}
|
||||
|
||||
// Connect sets the filter to events when two nodes connect.
|
||||
func (f *PeerEventsFilter) Connect() *PeerEventsFilter {
|
||||
f.eventType = simulations.EventTypeConn
|
||||
b := true
|
||||
f.connUp = &b
|
||||
return f
|
||||
}
|
||||
|
||||
// Drop sets the filter to events when two nodes disconnect.
|
||||
func (f *PeerEventsFilter) Drop() *PeerEventsFilter {
|
||||
f.eventType = simulations.EventTypeConn
|
||||
b := false
|
||||
f.connUp = &b
|
||||
return f
|
||||
}
|
||||
|
||||
// ReceivedMessages sets the filter to only messages that are received.
|
||||
func (f *PeerEventsFilter) ReceivedMessages() *PeerEventsFilter {
|
||||
f.eventType = simulations.EventTypeMsg
|
||||
b := true
|
||||
f.msgReceive = &b
|
||||
return f
|
||||
}
|
||||
|
||||
// SentMessages sets the filter to only messages that are sent.
|
||||
func (f *PeerEventsFilter) SentMessages() *PeerEventsFilter {
|
||||
f.eventType = simulations.EventTypeMsg
|
||||
b := false
|
||||
f.msgReceive = &b
|
||||
return f
|
||||
}
|
||||
|
||||
// Protocol sets the filter to only one message protocol.
|
||||
func (f *PeerEventsFilter) Protocol(p string) *PeerEventsFilter {
|
||||
f.eventType = simulations.EventTypeMsg
|
||||
f.protocol = &p
|
||||
return f
|
||||
}
|
||||
|
||||
// MsgCode sets the filter to only one msg code.
|
||||
func (f *PeerEventsFilter) MsgCode(c uint64) *PeerEventsFilter {
|
||||
f.eventType = simulations.EventTypeMsg
|
||||
f.msgCode = &c
|
||||
return f
|
||||
}
|
||||
|
||||
// PeerEvents returns a channel of events that are captured by admin peerEvents
|
||||
// subscription nodes with provided NodeIDs. Additional filters can be set to ignore
|
||||
// events that are not relevant.
|
||||
func (s *Simulation) PeerEvents(ctx context.Context, ids []enode.ID, filters ...*PeerEventsFilter) <-chan PeerEvent {
|
||||
eventC := make(chan PeerEvent)
|
||||
|
||||
// wait group to make sure all subscriptions to admin peerEvents are established
|
||||
// before this function returns.
|
||||
var subsWG sync.WaitGroup
|
||||
for _, id := range ids {
|
||||
s.shutdownWG.Add(1)
|
||||
subsWG.Add(1)
|
||||
go func(id enode.ID) {
|
||||
defer s.shutdownWG.Done()
|
||||
|
||||
events := make(chan *simulations.Event)
|
||||
sub := s.Net.Events().Subscribe(events)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
subsWG.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if err := ctx.Err(); err != nil {
|
||||
select {
|
||||
case eventC <- PeerEvent{NodeID: id, Error: err}:
|
||||
case <-s.Done():
|
||||
}
|
||||
}
|
||||
return
|
||||
case <-s.Done():
|
||||
return
|
||||
case e := <-events:
|
||||
// ignore control events
|
||||
if e.Control {
|
||||
continue
|
||||
}
|
||||
match := len(filters) == 0 // if there are no filters match all events
|
||||
for _, f := range filters {
|
||||
if f.eventType == simulations.EventTypeConn && e.Conn != nil {
|
||||
if *f.connUp != e.Conn.Up {
|
||||
continue
|
||||
}
|
||||
// all connection filter parameters matched, break the loop
|
||||
match = true
|
||||
break
|
||||
}
|
||||
if f.eventType == simulations.EventTypeMsg && e.Msg != nil {
|
||||
if f.msgReceive != nil && *f.msgReceive != e.Msg.Received {
|
||||
continue
|
||||
}
|
||||
if f.protocol != nil && *f.protocol != e.Msg.Protocol {
|
||||
continue
|
||||
}
|
||||
if f.msgCode != nil && *f.msgCode != e.Msg.Code {
|
||||
continue
|
||||
}
|
||||
// all message filter parameters matched, break the loop
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
var peerID enode.ID
|
||||
switch e.Type {
|
||||
case simulations.EventTypeConn:
|
||||
peerID = e.Conn.One
|
||||
if peerID == id {
|
||||
peerID = e.Conn.Other
|
||||
}
|
||||
case simulations.EventTypeMsg:
|
||||
peerID = e.Msg.One
|
||||
if peerID == id {
|
||||
peerID = e.Msg.Other
|
||||
}
|
||||
}
|
||||
if match {
|
||||
select {
|
||||
case eventC <- PeerEvent{NodeID: id, PeerID: peerID, Event: e}:
|
||||
case <-ctx.Done():
|
||||
if err := ctx.Err(); err != nil {
|
||||
select {
|
||||
case eventC <- PeerEvent{NodeID: id, PeerID: peerID, Error: err}:
|
||||
case <-s.Done():
|
||||
}
|
||||
}
|
||||
return
|
||||
case <-s.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
case err := <-sub.Err():
|
||||
if err != nil {
|
||||
select {
|
||||
case eventC <- PeerEvent{NodeID: id, Error: err}:
|
||||
case <-ctx.Done():
|
||||
if err := ctx.Err(); err != nil {
|
||||
select {
|
||||
case eventC <- PeerEvent{NodeID: id, Error: err}:
|
||||
case <-s.Done():
|
||||
}
|
||||
}
|
||||
return
|
||||
case <-s.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}(id)
|
||||
}
|
||||
|
||||
// wait all subscriptions
|
||||
subsWG.Wait()
|
||||
return eventC
|
||||
}
|
107
network/simulation/events_test.go
Normal file
107
network/simulation/events_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestPeerEvents creates simulation, adds two nodes,
|
||||
// register for peer events, connects nodes in a chain
|
||||
// and waits for the number of connection events to
|
||||
// be received.
|
||||
func TestPeerEvents(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
_, err := sim.AddNodes(2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
events := sim.PeerEvents(ctx, sim.NodeIDs())
|
||||
|
||||
// two nodes -> two connection events
|
||||
expectedEventCount := 2
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(expectedEventCount)
|
||||
|
||||
go func() {
|
||||
for e := range events {
|
||||
if e.Error != nil {
|
||||
if e.Error == context.Canceled {
|
||||
return
|
||||
}
|
||||
t.Error(e.Error)
|
||||
continue
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
err = sim.Net.ConnectNodesChain(sim.NodeIDs())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestPeerEventsTimeout(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
_, err := sim.AddNodes(2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
events := sim.PeerEvents(ctx, sim.NodeIDs())
|
||||
|
||||
done := make(chan struct{})
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
for e := range events {
|
||||
if e.Error == context.Canceled {
|
||||
return
|
||||
}
|
||||
if e.Error == context.DeadlineExceeded {
|
||||
close(done)
|
||||
return
|
||||
} else {
|
||||
errC <- e.Error
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("no context deadline received")
|
||||
case err := <-errC:
|
||||
t.Fatal(err)
|
||||
case <-done:
|
||||
// all good, context deadline detected
|
||||
}
|
||||
}
|
141
network/simulation/example_test.go
Normal file
141
network/simulation/example_test.go
Normal file
@ -0,0 +1,141 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
"github.com/ethersphere/swarm/network"
|
||||
"github.com/ethersphere/swarm/network/simulation"
|
||||
)
|
||||
|
||||
// Every node can have a Kademlia associated using the node bucket under
|
||||
// BucketKeyKademlia key. This allows to use WaitTillHealthy to block until
|
||||
// all nodes have the their Kademlias healthy.
|
||||
func ExampleSimulation_WaitTillHealthy() {
|
||||
|
||||
sim := simulation.New(map[string]simulation.ServiceFunc{
|
||||
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
|
||||
addr := network.NewAddr(ctx.Config.Node())
|
||||
hp := network.NewHiveParams()
|
||||
hp.Discovery = false
|
||||
config := &network.BzzConfig{
|
||||
OverlayAddr: addr.Over(),
|
||||
UnderlayAddr: addr.Under(),
|
||||
HiveParams: hp,
|
||||
}
|
||||
kad := network.NewKademlia(addr.Over(), network.NewKadParams())
|
||||
// store kademlia in node's bucket under BucketKeyKademlia
|
||||
// so that it can be found by WaitTillHealthy method.
|
||||
b.Store(simulation.BucketKeyKademlia, kad)
|
||||
return network.NewBzz(config, kad, nil, nil, nil), nil, nil
|
||||
},
|
||||
})
|
||||
defer sim.Close()
|
||||
|
||||
_, err := sim.AddNodesAndConnectRing(10)
|
||||
if err != nil {
|
||||
// handle error properly...
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
ill, err := sim.WaitTillHealthy(ctx)
|
||||
if err != nil {
|
||||
// inspect the latest detected not healthy kademlias
|
||||
for id, kad := range ill {
|
||||
fmt.Println("Node", id)
|
||||
fmt.Println(kad.String())
|
||||
}
|
||||
// handle error...
|
||||
}
|
||||
|
||||
// continue with the test
|
||||
|
||||
}
|
||||
|
||||
// Watch all peer events in the simulation network, buy receiving from a channel.
|
||||
func ExampleSimulation_PeerEvents() {
|
||||
sim := simulation.New(nil)
|
||||
defer sim.Close()
|
||||
|
||||
events := sim.PeerEvents(context.Background(), sim.NodeIDs())
|
||||
|
||||
go func() {
|
||||
for e := range events {
|
||||
if e.Error != nil {
|
||||
log.Error("peer event", "err", e.Error)
|
||||
continue
|
||||
}
|
||||
log.Info("peer event", "node", e.NodeID, "peer", e.PeerID, "type", e.Event.Type)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Detect when a nodes drop a peer.
|
||||
func ExampleSimulation_PeerEvents_disconnections() {
|
||||
sim := simulation.New(nil)
|
||||
defer sim.Close()
|
||||
|
||||
disconnections := sim.PeerEvents(
|
||||
context.Background(),
|
||||
sim.NodeIDs(),
|
||||
simulation.NewPeerEventsFilter().Drop(),
|
||||
)
|
||||
|
||||
go func() {
|
||||
for d := range disconnections {
|
||||
if d.Error != nil {
|
||||
log.Error("peer drop", "err", d.Error)
|
||||
continue
|
||||
}
|
||||
log.Warn("peer drop", "node", d.NodeID, "peer", d.PeerID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Watch multiple types of events or messages. In this case, they differ only
|
||||
// by MsgCode, but filters can be set for different types or protocols, too.
|
||||
func ExampleSimulation_PeerEvents_multipleFilters() {
|
||||
sim := simulation.New(nil)
|
||||
defer sim.Close()
|
||||
|
||||
msgs := sim.PeerEvents(
|
||||
context.Background(),
|
||||
sim.NodeIDs(),
|
||||
// Watch when bzz messages 1 and 4 are received.
|
||||
simulation.NewPeerEventsFilter().ReceivedMessages().Protocol("bzz").MsgCode(1),
|
||||
simulation.NewPeerEventsFilter().ReceivedMessages().Protocol("bzz").MsgCode(4),
|
||||
)
|
||||
|
||||
go func() {
|
||||
for m := range msgs {
|
||||
if m.Error != nil {
|
||||
log.Error("bzz message", "err", m.Error)
|
||||
continue
|
||||
}
|
||||
log.Info("bzz message", "node", m.NodeID, "peer", m.PeerID)
|
||||
}
|
||||
}()
|
||||
}
|
68
network/simulation/http.go
Normal file
68
network/simulation/http.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations"
|
||||
)
|
||||
|
||||
// Package defaults.
|
||||
var (
|
||||
DefaultHTTPSimAddr = ":8888"
|
||||
)
|
||||
|
||||
//WithServer implements the builder pattern constructor for Simulation to
|
||||
//start with a HTTP server
|
||||
func (s *Simulation) WithServer(addr string) *Simulation {
|
||||
//assign default addr if nothing provided
|
||||
if addr == "" {
|
||||
addr = DefaultHTTPSimAddr
|
||||
}
|
||||
log.Info(fmt.Sprintf("Initializing simulation server on %s...", addr))
|
||||
//initialize the HTTP server
|
||||
s.handler = simulations.NewServer(s.Net)
|
||||
s.runC = make(chan struct{})
|
||||
//add swarm specific routes to the HTTP server
|
||||
s.addSimulationRoutes()
|
||||
s.httpSrv = &http.Server{
|
||||
Addr: addr,
|
||||
Handler: s.handler,
|
||||
}
|
||||
go func() {
|
||||
err := s.httpSrv.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Error("Error starting the HTTP server", "error", err)
|
||||
}
|
||||
}()
|
||||
return s
|
||||
}
|
||||
|
||||
//register additional HTTP routes
|
||||
func (s *Simulation) addSimulationRoutes() {
|
||||
s.handler.POST("/runsim", s.RunSimulation)
|
||||
}
|
||||
|
||||
// RunSimulation is the actual POST endpoint runner
|
||||
func (s *Simulation) RunSimulation(w http.ResponseWriter, req *http.Request) {
|
||||
log.Debug("RunSimulation endpoint running")
|
||||
s.runC <- struct{}{}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
110
network/simulation/http_test.go
Normal file
110
network/simulation/http_test.go
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
)
|
||||
|
||||
func TestSimulationWithHTTPServer(t *testing.T) {
|
||||
log.Debug("Init simulation")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
sim := New(
|
||||
map[string]ServiceFunc{
|
||||
"noop": func(_ *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
|
||||
return newNoopService(), nil, nil
|
||||
},
|
||||
}).WithServer(DefaultHTTPSimAddr)
|
||||
defer sim.Close()
|
||||
log.Debug("Done.")
|
||||
|
||||
_, err := sim.AddNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debug("Starting sim round and let it time out...")
|
||||
//first test that running without sending to the channel will actually
|
||||
//block the simulation, so let it time out
|
||||
result := sim.Run(ctx, func(ctx context.Context, sim *Simulation) error {
|
||||
log.Debug("Just start the sim without any action and wait for the timeout")
|
||||
//ensure with a Sleep that simulation doesn't terminate before the timeout
|
||||
time.Sleep(2 * time.Second)
|
||||
return nil
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
if result.Error.Error() == "context deadline exceeded" {
|
||||
log.Debug("Expected timeout error received")
|
||||
} else {
|
||||
t.Fatal(result.Error)
|
||||
}
|
||||
}
|
||||
|
||||
//now run it again and send the expected signal on the waiting channel,
|
||||
//then close the simulation
|
||||
log.Debug("Starting sim round and wait for frontend signal...")
|
||||
//this time the timeout should be long enough so that it doesn't kick in too early
|
||||
ctx, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel2()
|
||||
errC := make(chan error, 1)
|
||||
go triggerSimulationRun(t, errC)
|
||||
result = sim.Run(ctx, func(ctx context.Context, sim *Simulation) error {
|
||||
log.Debug("This run waits for the run signal from `frontend`...")
|
||||
//ensure with a Sleep that simulation doesn't terminate before the signal is received
|
||||
time.Sleep(2 * time.Second)
|
||||
return nil
|
||||
})
|
||||
if result.Error != nil {
|
||||
t.Fatal(result.Error)
|
||||
}
|
||||
if err := <-errC; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
log.Debug("Test terminated successfully")
|
||||
}
|
||||
|
||||
func triggerSimulationRun(t *testing.T, errC chan error) {
|
||||
//We need to first wait for the sim HTTP server to start running...
|
||||
time.Sleep(2 * time.Second)
|
||||
//then we can send the signal
|
||||
|
||||
log.Debug("Sending run signal to simulation: POST /runsim...")
|
||||
resp, err := http.Post(fmt.Sprintf("http://localhost%s/runsim", DefaultHTTPSimAddr), "application/json", nil)
|
||||
if err != nil {
|
||||
errC <- fmt.Errorf("Request failed: %v", err)
|
||||
return
|
||||
}
|
||||
log.Debug("Signal sent")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errC <- fmt.Errorf("err %s", resp.Status)
|
||||
return
|
||||
}
|
||||
errC <- resp.Body.Close()
|
||||
}
|
203
network/simulation/kademlia.go
Normal file
203
network/simulation/kademlia.go
Normal file
@ -0,0 +1,203 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations"
|
||||
"github.com/ethersphere/swarm/network"
|
||||
)
|
||||
|
||||
// BucketKeyKademlia is the key to be used for storing the kademlia
|
||||
// instance for particular node, usually inside the ServiceFunc function.
|
||||
var BucketKeyKademlia BucketKey = "kademlia"
|
||||
|
||||
// WaitTillHealthy is blocking until the health of all kademlias is true.
|
||||
// If error is not nil, a map of kademlia that was found not healthy is returned.
|
||||
// TODO: Check correctness since change in kademlia depth calculation logic
|
||||
func (s *Simulation) WaitTillHealthy(ctx context.Context) (ill map[enode.ID]*network.Kademlia, err error) {
|
||||
// Prepare PeerPot map for checking Kademlia health
|
||||
var ppmap map[string]*network.PeerPot
|
||||
kademlias := s.kademlias()
|
||||
addrs := make([][]byte, 0, len(kademlias))
|
||||
// TODO verify that all kademlias have same params
|
||||
for _, k := range kademlias {
|
||||
addrs = append(addrs, k.BaseAddr())
|
||||
}
|
||||
ppmap = network.NewPeerPotMap(s.neighbourhoodSize, addrs)
|
||||
|
||||
// Wait for healthy Kademlia on every node before checking files
|
||||
ticker := time.NewTicker(200 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
ill = make(map[enode.ID]*network.Kademlia)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ill, ctx.Err()
|
||||
case <-ticker.C:
|
||||
for k := range ill {
|
||||
delete(ill, k)
|
||||
}
|
||||
log.Debug("kademlia health check", "addr count", len(addrs), "kad len", len(kademlias))
|
||||
for id, k := range kademlias {
|
||||
//PeerPot for this node
|
||||
addr := common.Bytes2Hex(k.BaseAddr())
|
||||
pp := ppmap[addr]
|
||||
//call Healthy RPC
|
||||
h := k.GetHealthInfo(pp)
|
||||
//print info
|
||||
log.Debug(k.String())
|
||||
log.Debug("kademlia", "connectNN", h.ConnectNN, "knowNN", h.KnowNN)
|
||||
log.Debug("kademlia", "health", h.ConnectNN && h.KnowNN, "addr", hex.EncodeToString(k.BaseAddr()), "node", id)
|
||||
log.Debug("kademlia", "ill condition", !h.ConnectNN, "addr", hex.EncodeToString(k.BaseAddr()), "node", id)
|
||||
if !h.Healthy() {
|
||||
ill[id] = k
|
||||
}
|
||||
}
|
||||
if len(ill) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// kademlias returns all Kademlia instances that are set
|
||||
// in simulation bucket.
|
||||
func (s *Simulation) kademlias() (ks map[enode.ID]*network.Kademlia) {
|
||||
items := s.UpNodesItems(BucketKeyKademlia)
|
||||
log.Debug("kademlia len items", "len", len(items))
|
||||
ks = make(map[enode.ID]*network.Kademlia, len(items))
|
||||
for id, v := range items {
|
||||
k, ok := v.(*network.Kademlia)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ks[id] = k
|
||||
}
|
||||
return ks
|
||||
}
|
||||
|
||||
// WaitTillSnapshotRecreated is blocking until all the connections specified
|
||||
// in the snapshot are registered in the kademlia.
|
||||
// It differs from WaitTillHealthy, which waits only until all the kademlias are
|
||||
// healthy (it might happen even before all the connections are established).
|
||||
func (s *Simulation) WaitTillSnapshotRecreated(ctx context.Context, snap *simulations.Snapshot) error {
|
||||
expected := getSnapshotConnections(snap.Conns)
|
||||
ticker := time.NewTicker(150 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
actual := s.getActualConnections()
|
||||
if isAllDeployed(expected, actual) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Simulation) getActualConnections() (res []uint64) {
|
||||
kademlias := s.kademlias()
|
||||
for base, k := range kademlias {
|
||||
k.EachConn(base[:], 256, func(p *network.Peer, _ int) bool {
|
||||
res = append(res, getConnectionHash(base, p.ID()))
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// only list those connections that appear twice (both peers should recognize connection as active)
|
||||
res = removeDuplicatesAndSingletons(res)
|
||||
return res
|
||||
}
|
||||
|
||||
func getSnapshotConnections(conns []simulations.Conn) (res []uint64) {
|
||||
for _, c := range conns {
|
||||
res = append(res, getConnectionHash(c.One, c.Other))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// returns an integer connection identifier (similar to 8-byte hash)
|
||||
func getConnectionHash(a, b enode.ID) uint64 {
|
||||
var h [8]byte
|
||||
for i := 0; i < 8; i++ {
|
||||
h[i] = a[i] ^ b[i]
|
||||
}
|
||||
res := binary.LittleEndian.Uint64(h[:])
|
||||
return res
|
||||
}
|
||||
|
||||
// returns true if all connections in expected are listed in actual
|
||||
func isAllDeployed(expected []uint64, actual []uint64) bool {
|
||||
if len(expected) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
exp := make([]uint64, len(expected))
|
||||
copy(exp, expected)
|
||||
for _, c := range actual {
|
||||
// remove value c from exp
|
||||
for i := 0; i < len(exp); i++ {
|
||||
if exp[i] == c {
|
||||
exp = removeListElement(exp, i)
|
||||
if len(exp) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(exp) == 0
|
||||
}
|
||||
|
||||
func removeListElement(arr []uint64, i int) []uint64 {
|
||||
last := len(arr) - 1
|
||||
arr[i] = arr[last]
|
||||
arr = arr[:last]
|
||||
return arr
|
||||
}
|
||||
|
||||
func removeDuplicatesAndSingletons(arr []uint64) []uint64 {
|
||||
for i := 0; i < len(arr); {
|
||||
found := false
|
||||
for j := i + 1; j < len(arr); j++ {
|
||||
if arr[i] == arr[j] {
|
||||
arr = removeListElement(arr, j) // remove duplicate
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
i++
|
||||
} else {
|
||||
arr = removeListElement(arr, i) // remove singleton
|
||||
}
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
310
network/simulation/kademlia_test.go
Normal file
310
network/simulation/kademlia_test.go
Normal file
@ -0,0 +1,310 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
"github.com/ethersphere/swarm/network"
|
||||
)
|
||||
|
||||
/*
|
||||
TestWaitTillHealthy tests that we indeed get a healthy network after we wait for it.
|
||||
For this to be tested, a bit of a snake tail bite needs to happen:
|
||||
* First we create a first simulation
|
||||
* Run it as nodes connected in a ring
|
||||
* Wait until the network is healthy
|
||||
* Then we create a snapshot
|
||||
* With this snapshot we create a new simulation
|
||||
* This simulation is expected to have a healthy configuration, as it uses the snapshot
|
||||
* Thus we just iterate all nodes and check that their kademlias are healthy
|
||||
* If all kademlias are healthy, the test succeeded, otherwise it failed
|
||||
*/
|
||||
func TestWaitTillHealthy(t *testing.T) {
|
||||
t.Skip("this test is flaky; disabling till underlying problem is solved")
|
||||
testNodesNum := 10
|
||||
|
||||
// create the first simulation
|
||||
sim := New(createSimServiceMap(true))
|
||||
|
||||
// connect and...
|
||||
nodeIDs, err := sim.AddNodesAndConnectRing(testNodesNum)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// array of all overlay addresses
|
||||
var addrs [][]byte
|
||||
// iterate once to be able to build the peer map
|
||||
for _, node := range nodeIDs {
|
||||
//get the kademlia overlay address from this ID
|
||||
a := node.Bytes()
|
||||
//append it to the array of all overlay addresses
|
||||
addrs = append(addrs, a)
|
||||
}
|
||||
// build a PeerPot only once
|
||||
pp := network.NewPeerPotMap(network.NewKadParams().NeighbourhoodSize, addrs)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// ...wait until healthy
|
||||
ill, err := sim.WaitTillHealthy(ctx)
|
||||
if err != nil {
|
||||
for id, kad := range ill {
|
||||
t.Log("Node", id)
|
||||
t.Log(kad.String())
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// now create a snapshot of this network
|
||||
snap, err := sim.Net.Snapshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// close the initial simulation
|
||||
sim.Close()
|
||||
// create a control simulation
|
||||
controlSim := New(createSimServiceMap(false))
|
||||
defer controlSim.Close()
|
||||
|
||||
// load the snapshot into this control simulation
|
||||
err = controlSim.Net.Load(snap)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = controlSim.WaitTillHealthy(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, node := range nodeIDs {
|
||||
// ...get its kademlia
|
||||
item, ok := controlSim.NodeItem(node, BucketKeyKademlia)
|
||||
if !ok {
|
||||
t.Fatal("No kademlia bucket item")
|
||||
}
|
||||
kad := item.(*network.Kademlia)
|
||||
// get its base address
|
||||
kid := common.Bytes2Hex(kad.BaseAddr())
|
||||
|
||||
//get the health info
|
||||
info := kad.GetHealthInfo(pp[kid])
|
||||
log.Trace("Health info", "info", info)
|
||||
// check that it is healthy
|
||||
healthy := info.Healthy()
|
||||
if !healthy {
|
||||
t.Fatalf("Expected node %v of control simulation to be healthy, but it is not, unhealthy kademlias: %v", node, kad.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createSimServiceMap returns the services map
|
||||
// this function will create the sim services with or without discovery enabled
|
||||
// based on the flag passed
|
||||
func createSimServiceMap(discovery bool) map[string]ServiceFunc {
|
||||
return map[string]ServiceFunc{
|
||||
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
|
||||
addr := network.NewAddr(ctx.Config.Node())
|
||||
hp := network.NewHiveParams()
|
||||
hp.Discovery = discovery
|
||||
config := &network.BzzConfig{
|
||||
OverlayAddr: addr.Over(),
|
||||
UnderlayAddr: addr.Under(),
|
||||
HiveParams: hp,
|
||||
}
|
||||
kad := network.NewKademlia(addr.Over(), network.NewKadParams())
|
||||
// store kademlia in node's bucket under BucketKeyKademlia
|
||||
// so that it can be found by WaitTillHealthy method.
|
||||
b.Store(BucketKeyKademlia, kad)
|
||||
return network.NewBzz(config, kad, nil, nil, nil), nil, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestWaitTillSnapshotRecreated tests that we indeed have a network
|
||||
// configuration specified in the snapshot file, after we wait for it.
|
||||
//
|
||||
// First we create a first simulation
|
||||
// Run it as nodes connected in a ring
|
||||
// Wait until the network is healthy
|
||||
// Then we create a snapshot
|
||||
// With this snapshot we create a new simulation
|
||||
// Call WaitTillSnapshotRecreated() function and wait until it returns
|
||||
// Iterate the nodes and check if all the connections are successfully recreated
|
||||
func TestWaitTillSnapshotRecreated(t *testing.T) {
|
||||
t.Skip("test is flaky. disabling until underlying problem is addressed")
|
||||
var err error
|
||||
sim := New(createSimServiceMap(true))
|
||||
_, err = sim.AddNodesAndConnectRing(16)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
_, err = sim.WaitTillHealthy(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
originalConnections := sim.getActualConnections()
|
||||
snap, err := sim.Net.Snapshot()
|
||||
sim.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
controlSim := New(createSimServiceMap(false))
|
||||
defer controlSim.Close()
|
||||
err = controlSim.Net.Load(snap)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = controlSim.WaitTillSnapshotRecreated(ctx, snap)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
controlConnections := controlSim.getActualConnections()
|
||||
|
||||
for _, c := range originalConnections {
|
||||
if !exist(controlConnections, c) {
|
||||
t.Fatal("connection was not recreated")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// exist returns true if val is found in arr
|
||||
func exist(arr []uint64, val uint64) bool {
|
||||
for _, c := range arr {
|
||||
if c == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestRemoveDuplicatesAndSingletons(t *testing.T) {
|
||||
singletons := []uint64{
|
||||
0x3c127c6f6cb026b0,
|
||||
0x0f45190d72e71fc5,
|
||||
0xb0184c02449e0bb6,
|
||||
0xa85c7b84239c54d3,
|
||||
0xe3b0c44298fc1c14,
|
||||
0x9afbf4c8996fb924,
|
||||
0x27ae41e4649b934c,
|
||||
0xa495991b7852b855,
|
||||
}
|
||||
|
||||
doubles := []uint64{
|
||||
0x1b879f878de7fc7a,
|
||||
0xc6791470521bdab4,
|
||||
0xdd34b0ee39bbccc6,
|
||||
0x4d904fbf0f31da10,
|
||||
0x6403c2560432c8f8,
|
||||
0x18954e33cf3ad847,
|
||||
0x90db00e98dc7a8a6,
|
||||
0x92886b0dfcc1809b,
|
||||
}
|
||||
|
||||
var arr []uint64
|
||||
arr = append(arr, doubles...)
|
||||
arr = append(arr, singletons...)
|
||||
arr = append(arr, doubles...)
|
||||
arr = removeDuplicatesAndSingletons(arr)
|
||||
|
||||
for _, i := range singletons {
|
||||
if exist(arr, i) {
|
||||
t.Fatalf("singleton not removed: %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range doubles {
|
||||
if !exist(arr, i) {
|
||||
t.Fatalf("wrong value removed: %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
for j := 0; j < len(doubles); j++ {
|
||||
v := doubles[j] + singletons[j]
|
||||
if exist(arr, v) {
|
||||
t.Fatalf("non-existing value found, index: %d", j)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAllDeployed(t *testing.T) {
|
||||
a := []uint64{
|
||||
0x3c127c6f6cb026b0,
|
||||
0x0f45190d72e71fc5,
|
||||
0xb0184c02449e0bb6,
|
||||
0xa85c7b84239c54d3,
|
||||
0xe3b0c44298fc1c14,
|
||||
0x9afbf4c8996fb924,
|
||||
0x27ae41e4649b934c,
|
||||
0xa495991b7852b855,
|
||||
}
|
||||
|
||||
b := []uint64{
|
||||
0x1b879f878de7fc7a,
|
||||
0xc6791470521bdab4,
|
||||
0xdd34b0ee39bbccc6,
|
||||
0x4d904fbf0f31da10,
|
||||
0x6403c2560432c8f8,
|
||||
0x18954e33cf3ad847,
|
||||
0x90db00e98dc7a8a6,
|
||||
0x92886b0dfcc1809b,
|
||||
}
|
||||
|
||||
var c []uint64
|
||||
c = append(c, a...)
|
||||
c = append(c, b...)
|
||||
|
||||
if !isAllDeployed(a, c) {
|
||||
t.Fatal("isAllDeployed failed")
|
||||
}
|
||||
|
||||
if !isAllDeployed(b, c) {
|
||||
t.Fatal("isAllDeployed failed")
|
||||
}
|
||||
|
||||
if isAllDeployed(c, a) {
|
||||
t.Fatal("isAllDeployed failed: false positive")
|
||||
}
|
||||
|
||||
if isAllDeployed(c, b) {
|
||||
t.Fatal("isAllDeployed failed: false positive")
|
||||
}
|
||||
|
||||
c = c[2:]
|
||||
|
||||
if isAllDeployed(a, c) {
|
||||
t.Fatal("isAllDeployed failed: false positive")
|
||||
}
|
||||
|
||||
if !isAllDeployed(b, c) {
|
||||
t.Fatal("isAllDeployed failed")
|
||||
}
|
||||
}
|
341
network/simulation/node.go
Normal file
341
network/simulation/node.go
Normal file
@ -0,0 +1,341 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
"github.com/ethersphere/swarm/network"
|
||||
)
|
||||
|
||||
var (
|
||||
BucketKeyBzzPrivateKey BucketKey = "bzzprivkey"
|
||||
)
|
||||
|
||||
// NodeIDs returns NodeIDs for all nodes in the network.
|
||||
func (s *Simulation) NodeIDs() (ids []enode.ID) {
|
||||
nodes := s.Net.GetNodes()
|
||||
ids = make([]enode.ID, len(nodes))
|
||||
for i, node := range nodes {
|
||||
ids[i] = node.ID()
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// UpNodeIDs returns NodeIDs for nodes that are up in the network.
|
||||
func (s *Simulation) UpNodeIDs() (ids []enode.ID) {
|
||||
nodes := s.Net.GetNodes()
|
||||
for _, node := range nodes {
|
||||
if node.Up() {
|
||||
ids = append(ids, node.ID())
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// DownNodeIDs returns NodeIDs for nodes that are stopped in the network.
|
||||
func (s *Simulation) DownNodeIDs() (ids []enode.ID) {
|
||||
nodes := s.Net.GetNodes()
|
||||
for _, node := range nodes {
|
||||
if !node.Up() {
|
||||
ids = append(ids, node.ID())
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// AddNodeOption defines the option that can be passed
|
||||
// to Simulation.AddNode method.
|
||||
type AddNodeOption func(*adapters.NodeConfig)
|
||||
|
||||
// AddNodeWithMsgEvents sets the EnableMsgEvents option
|
||||
// to NodeConfig.
|
||||
func AddNodeWithMsgEvents(enable bool) AddNodeOption {
|
||||
return func(o *adapters.NodeConfig) {
|
||||
o.EnableMsgEvents = enable
|
||||
}
|
||||
}
|
||||
|
||||
// AddNodeWithService specifies a service that should be
|
||||
// started on a node. This option can be repeated as variadic
|
||||
// argument toe AddNode and other add node related methods.
|
||||
// If AddNodeWithService is not specified, all services will be started.
|
||||
func AddNodeWithService(serviceName string) AddNodeOption {
|
||||
return func(o *adapters.NodeConfig) {
|
||||
o.Services = append(o.Services, serviceName)
|
||||
}
|
||||
}
|
||||
|
||||
// AddNode creates a new node with random configuration,
|
||||
// applies provided options to the config and adds the node to network.
|
||||
// By default all services will be started on a node. If one or more
|
||||
// AddNodeWithService option are provided, only specified services will be started.
|
||||
func (s *Simulation) AddNode(opts ...AddNodeOption) (id enode.ID, err error) {
|
||||
conf := adapters.RandomNodeConfig()
|
||||
for _, o := range opts {
|
||||
o(conf)
|
||||
}
|
||||
if len(conf.Services) == 0 {
|
||||
conf.Services = s.serviceNames
|
||||
}
|
||||
|
||||
// add ENR records to the underlying node
|
||||
// most importantly the bzz overlay address
|
||||
//
|
||||
// for now we have no way of setting bootnodes or lightnodes in sims
|
||||
// so we just let them be set to false
|
||||
// they should perhaps be possible to override them with AddNodeOption
|
||||
bzzPrivateKey, err := BzzPrivateKeyFromConfig(conf)
|
||||
if err != nil {
|
||||
return enode.ID{}, err
|
||||
}
|
||||
|
||||
enodeParams := &network.EnodeParams{
|
||||
PrivateKey: bzzPrivateKey,
|
||||
}
|
||||
record, err := network.NewEnodeRecord(enodeParams)
|
||||
conf.Record = *record
|
||||
|
||||
// Add the bzz address to the node config
|
||||
node, err := s.Net.NewNodeWithConfig(conf)
|
||||
if err != nil {
|
||||
return id, err
|
||||
}
|
||||
s.buckets[node.ID()] = new(sync.Map)
|
||||
s.SetNodeItem(node.ID(), BucketKeyBzzPrivateKey, bzzPrivateKey)
|
||||
|
||||
return node.ID(), s.Net.Start(node.ID())
|
||||
}
|
||||
|
||||
// AddNodes creates new nodes with random configurations,
|
||||
// applies provided options to the config and adds nodes to network.
|
||||
func (s *Simulation) AddNodes(count int, opts ...AddNodeOption) (ids []enode.ID, err error) {
|
||||
ids = make([]enode.ID, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
id, err := s.AddNode(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// AddNodesAndConnectFull is a helpper method that combines
|
||||
// AddNodes and ConnectNodesFull. Only new nodes will be connected.
|
||||
func (s *Simulation) AddNodesAndConnectFull(count int, opts ...AddNodeOption) (ids []enode.ID, err error) {
|
||||
if count < 2 {
|
||||
return nil, errors.New("count of nodes must be at least 2")
|
||||
}
|
||||
ids, err = s.AddNodes(count, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = s.Net.ConnectNodesFull(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// AddNodesAndConnectChain is a helpper method that combines
|
||||
// AddNodes and ConnectNodesChain. The chain will be continued from the last
|
||||
// added node, if there is one in simulation using ConnectToLastNode method.
|
||||
func (s *Simulation) AddNodesAndConnectChain(count int, opts ...AddNodeOption) (ids []enode.ID, err error) {
|
||||
if count < 2 {
|
||||
return nil, errors.New("count of nodes must be at least 2")
|
||||
}
|
||||
id, err := s.AddNode(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = s.Net.ConnectToLastNode(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids, err = s.AddNodes(count-1, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append([]enode.ID{id}, ids...)
|
||||
err = s.Net.ConnectNodesChain(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// AddNodesAndConnectRing is a helpper method that combines
|
||||
// AddNodes and ConnectNodesRing.
|
||||
func (s *Simulation) AddNodesAndConnectRing(count int, opts ...AddNodeOption) (ids []enode.ID, err error) {
|
||||
if count < 2 {
|
||||
return nil, errors.New("count of nodes must be at least 2")
|
||||
}
|
||||
ids, err = s.AddNodes(count, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = s.Net.ConnectNodesRing(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// AddNodesAndConnectStar is a helpper method that combines
|
||||
// AddNodes and ConnectNodesStar.
|
||||
func (s *Simulation) AddNodesAndConnectStar(count int, opts ...AddNodeOption) (ids []enode.ID, err error) {
|
||||
if count < 2 {
|
||||
return nil, errors.New("count of nodes must be at least 2")
|
||||
}
|
||||
ids, err = s.AddNodes(count, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = s.Net.ConnectNodesStar(ids[1:], ids[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// UploadSnapshot uploads a snapshot to the simulation
|
||||
// This method tries to open the json file provided, applies the config to all nodes
|
||||
// and then loads the snapshot into the Simulation network
|
||||
func (s *Simulation) UploadSnapshot(ctx context.Context, snapshotFile string, opts ...AddNodeOption) error {
|
||||
f, err := os.Open(snapshotFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jsonbyte, err := ioutil.ReadAll(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var snap simulations.Snapshot
|
||||
if err := json.Unmarshal(jsonbyte, &snap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//the snapshot probably has the property EnableMsgEvents not set
|
||||
//set it to true (we need this to wait for messages before uploading)
|
||||
for i := range snap.Nodes {
|
||||
snap.Nodes[i].Node.Config.EnableMsgEvents = true
|
||||
snap.Nodes[i].Node.Config.Services = s.serviceNames
|
||||
for _, o := range opts {
|
||||
o(snap.Nodes[i].Node.Config)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Net.Load(&snap); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.WaitTillSnapshotRecreated(ctx, &snap)
|
||||
}
|
||||
|
||||
// StartNode starts a node by NodeID.
|
||||
func (s *Simulation) StartNode(id enode.ID) (err error) {
|
||||
return s.Net.Start(id)
|
||||
}
|
||||
|
||||
// StartRandomNode starts a random node.
|
||||
func (s *Simulation) StartRandomNode() (id enode.ID, err error) {
|
||||
n := s.Net.GetRandomDownNode()
|
||||
if n == nil {
|
||||
return id, ErrNodeNotFound
|
||||
}
|
||||
return n.ID(), s.Net.Start(n.ID())
|
||||
}
|
||||
|
||||
// StartRandomNodes starts random nodes.
|
||||
func (s *Simulation) StartRandomNodes(count int) (ids []enode.ID, err error) {
|
||||
ids = make([]enode.ID, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
n := s.Net.GetRandomDownNode()
|
||||
if n == nil {
|
||||
return nil, ErrNodeNotFound
|
||||
}
|
||||
err = s.Net.Start(n.ID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, n.ID())
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// StopNode stops a node by NodeID.
|
||||
func (s *Simulation) StopNode(id enode.ID) (err error) {
|
||||
return s.Net.Stop(id)
|
||||
}
|
||||
|
||||
// StopRandomNode stops a random node.
|
||||
func (s *Simulation) StopRandomNode() (id enode.ID, err error) {
|
||||
n := s.Net.GetRandomUpNode()
|
||||
if n == nil {
|
||||
return id, ErrNodeNotFound
|
||||
}
|
||||
return n.ID(), s.Net.Stop(n.ID())
|
||||
}
|
||||
|
||||
// StopRandomNodes stops random nodes.
|
||||
func (s *Simulation) StopRandomNodes(count int) (ids []enode.ID, err error) {
|
||||
ids = make([]enode.ID, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
n := s.Net.GetRandomUpNode()
|
||||
if n == nil {
|
||||
return nil, ErrNodeNotFound
|
||||
}
|
||||
err = s.Net.Stop(n.ID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, n.ID())
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// seed the random generator for Simulation.randomNode.
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// derive a private key for swarm for the node key
|
||||
// returns the private key used to generate the bzz key
|
||||
func BzzPrivateKeyFromConfig(conf *adapters.NodeConfig) (*ecdsa.PrivateKey, error) {
|
||||
// pad the seed key some arbitrary data as ecdsa.GenerateKey takes 40 bytes seed data
|
||||
privKeyBuf := append(crypto.FromECDSA(conf.PrivateKey), []byte{0x62, 0x7a, 0x7a, 0x62, 0x7a, 0x7a, 0x62, 0x7a}...)
|
||||
bzzPrivateKey, err := ecdsa.GenerateKey(crypto.S256(), bytes.NewReader(privKeyBuf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bzzPrivateKey, nil
|
||||
}
|
446
network/simulation/node_test.go
Normal file
446
network/simulation/node_test.go
Normal file
@ -0,0 +1,446 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
"github.com/ethersphere/swarm/network"
|
||||
)
|
||||
|
||||
func TestUpDownNodeIDs(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
ids, err := sim.AddNodes(10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotIDs := sim.NodeIDs()
|
||||
|
||||
if !equalNodeIDs(ids, gotIDs) {
|
||||
t.Error("returned nodes are not equal to added ones")
|
||||
}
|
||||
|
||||
stoppedIDs, err := sim.StopRandomNodes(3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotIDs = sim.UpNodeIDs()
|
||||
|
||||
for _, id := range gotIDs {
|
||||
if !sim.Net.GetNode(id).Up() {
|
||||
t.Errorf("node %s should not be down", id)
|
||||
}
|
||||
}
|
||||
|
||||
if !equalNodeIDs(ids, append(gotIDs, stoppedIDs...)) {
|
||||
t.Error("returned nodes are not equal to added ones")
|
||||
}
|
||||
|
||||
gotIDs = sim.DownNodeIDs()
|
||||
|
||||
for _, id := range gotIDs {
|
||||
if sim.Net.GetNode(id).Up() {
|
||||
t.Errorf("node %s should not be up", id)
|
||||
}
|
||||
}
|
||||
|
||||
if !equalNodeIDs(stoppedIDs, gotIDs) {
|
||||
t.Error("returned nodes are not equal to the stopped ones")
|
||||
}
|
||||
}
|
||||
|
||||
func equalNodeIDs(one, other []enode.ID) bool {
|
||||
if len(one) != len(other) {
|
||||
return false
|
||||
}
|
||||
var count int
|
||||
for _, a := range one {
|
||||
var found bool
|
||||
for _, b := range other {
|
||||
if a == b {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
count++
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return count == len(one)
|
||||
}
|
||||
|
||||
func TestAddNode(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
id, err := sim.AddNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n := sim.Net.GetNode(id)
|
||||
if n == nil {
|
||||
t.Fatal("node not found")
|
||||
}
|
||||
|
||||
if !n.Up() {
|
||||
t.Error("node not started")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddNodeWithMsgEvents(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
id, err := sim.AddNode(AddNodeWithMsgEvents(true))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !sim.Net.GetNode(id).Config.EnableMsgEvents {
|
||||
t.Error("EnableMsgEvents is false")
|
||||
}
|
||||
|
||||
id, err = sim.AddNode(AddNodeWithMsgEvents(false))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if sim.Net.GetNode(id).Config.EnableMsgEvents {
|
||||
t.Error("EnableMsgEvents is true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddNodeWithService(t *testing.T) {
|
||||
sim := New(map[string]ServiceFunc{
|
||||
"noop1": noopServiceFunc,
|
||||
"noop2": noopServiceFunc,
|
||||
})
|
||||
defer sim.Close()
|
||||
|
||||
id, err := sim.AddNode(AddNodeWithService("noop1"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n := sim.Net.GetNode(id).Node.(*adapters.SimNode)
|
||||
if n.Service("noop1") == nil {
|
||||
t.Error("service noop1 not found on node")
|
||||
}
|
||||
if n.Service("noop2") != nil {
|
||||
t.Error("service noop2 should not be found on node")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddNodeMultipleServices(t *testing.T) {
|
||||
sim := New(map[string]ServiceFunc{
|
||||
"noop1": noopServiceFunc,
|
||||
"noop2": noopService2Func,
|
||||
})
|
||||
defer sim.Close()
|
||||
|
||||
id, err := sim.AddNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n := sim.Net.GetNode(id).Node.(*adapters.SimNode)
|
||||
if n.Service("noop1") == nil {
|
||||
t.Error("service noop1 not found on node")
|
||||
}
|
||||
if n.Service("noop2") == nil {
|
||||
t.Error("service noop2 not found on node")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddNodeDuplicateServiceError(t *testing.T) {
|
||||
sim := New(map[string]ServiceFunc{
|
||||
"noop1": noopServiceFunc,
|
||||
"noop2": noopServiceFunc,
|
||||
})
|
||||
defer sim.Close()
|
||||
|
||||
wantErr := "duplicate service: *simulation.noopService"
|
||||
_, err := sim.AddNode()
|
||||
if err.Error() != wantErr {
|
||||
t.Errorf("got error %q, want %q", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddNodes(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
nodesCount := 12
|
||||
|
||||
ids, err := sim.AddNodes(nodesCount)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
count := len(ids)
|
||||
if count != nodesCount {
|
||||
t.Errorf("expected %v nodes, got %v", nodesCount, count)
|
||||
}
|
||||
|
||||
count = len(sim.Net.GetNodes())
|
||||
if count != nodesCount {
|
||||
t.Errorf("expected %v nodes, got %v", nodesCount, count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddNodesAndConnectFull(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
n := 12
|
||||
|
||||
ids, err := sim.AddNodesAndConnectFull(n)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
simulations.VerifyFull(t, sim.Net, ids)
|
||||
}
|
||||
|
||||
func TestAddNodesAndConnectChain(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
_, err := sim.AddNodesAndConnectChain(12)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// add another set of nodes to test
|
||||
// if two chains are connected
|
||||
_, err = sim.AddNodesAndConnectChain(7)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
simulations.VerifyChain(t, sim.Net, sim.UpNodeIDs())
|
||||
}
|
||||
|
||||
func TestAddNodesAndConnectRing(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
ids, err := sim.AddNodesAndConnectRing(12)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
simulations.VerifyRing(t, sim.Net, ids)
|
||||
}
|
||||
|
||||
func TestAddNodesAndConnectStar(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
ids, err := sim.AddNodesAndConnectStar(12)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
simulations.VerifyStar(t, sim.Net, ids, 0)
|
||||
}
|
||||
|
||||
//To test that uploading a snapshot works
|
||||
func TestUploadSnapshot(t *testing.T) {
|
||||
log.Debug("Creating simulation")
|
||||
s := New(map[string]ServiceFunc{
|
||||
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
|
||||
addr := network.NewAddr(ctx.Config.Node())
|
||||
hp := network.NewHiveParams()
|
||||
hp.Discovery = false
|
||||
config := &network.BzzConfig{
|
||||
OverlayAddr: addr.Over(),
|
||||
UnderlayAddr: addr.Under(),
|
||||
HiveParams: hp,
|
||||
}
|
||||
kad := network.NewKademlia(addr.Over(), network.NewKadParams())
|
||||
b.Store(BucketKeyKademlia, kad)
|
||||
return network.NewBzz(config, kad, nil, nil, nil), nil, nil
|
||||
},
|
||||
})
|
||||
defer s.Close()
|
||||
|
||||
nodeCount := 16
|
||||
log.Debug("Uploading snapshot")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
err := s.UploadSnapshot(ctx, fmt.Sprintf("../stream/testing/snapshot_%d.json", nodeCount))
|
||||
if err != nil {
|
||||
t.Fatalf("Error uploading snapshot to simulation network: %v", err)
|
||||
}
|
||||
|
||||
log.Debug("Starting simulation...")
|
||||
s.Run(ctx, func(ctx context.Context, sim *Simulation) error {
|
||||
log.Debug("Checking")
|
||||
nodes := sim.UpNodeIDs()
|
||||
if len(nodes) != nodeCount {
|
||||
t.Fatal("Simulation network node number doesn't match snapshot node number")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
log.Debug("Done.")
|
||||
}
|
||||
|
||||
func TestStartStopNode(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
id, err := sim.AddNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n := sim.Net.GetNode(id)
|
||||
if n == nil {
|
||||
t.Fatal("node not found")
|
||||
}
|
||||
if !n.Up() {
|
||||
t.Error("node not started")
|
||||
}
|
||||
|
||||
err = sim.StopNode(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n.Up() {
|
||||
t.Error("node not stopped")
|
||||
}
|
||||
|
||||
waitForPeerEventPropagation()
|
||||
|
||||
err = sim.StartNode(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !n.Up() {
|
||||
t.Error("node not started")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartStopRandomNode(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
_, err := sim.AddNodes(3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := sim.StopRandomNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n := sim.Net.GetNode(id)
|
||||
if n == nil {
|
||||
t.Fatal("node not found")
|
||||
}
|
||||
if n.Up() {
|
||||
t.Error("node not stopped")
|
||||
}
|
||||
|
||||
id2, err := sim.StopRandomNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
waitForPeerEventPropagation()
|
||||
|
||||
idStarted, err := sim.StartRandomNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if idStarted != id && idStarted != id2 {
|
||||
t.Error("unexpected started node ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartStopRandomNodes(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
_, err := sim.AddNodes(10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ids, err := sim.StopRandomNodes(3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
n := sim.Net.GetNode(id)
|
||||
if n == nil {
|
||||
t.Fatal("node not found")
|
||||
}
|
||||
if n.Up() {
|
||||
t.Error("node not stopped")
|
||||
}
|
||||
}
|
||||
|
||||
waitForPeerEventPropagation()
|
||||
|
||||
ids, err = sim.StartRandomNodes(2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
n := sim.Net.GetNode(id)
|
||||
if n == nil {
|
||||
t.Fatal("node not found")
|
||||
}
|
||||
if !n.Up() {
|
||||
t.Error("node not started")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitForPeerEventPropagation() {
|
||||
// Sleep here to ensure that Network.watchPeerEvents defer function
|
||||
// has set the `node.Up() = false` before we start the node again.
|
||||
//
|
||||
// The same node is stopped and started again, and upon start
|
||||
// watchPeerEvents is started in a goroutine. If the node is stopped
|
||||
// and then very quickly started, that goroutine may be scheduled later
|
||||
// then start and force `node.Up() = false` in its defer function.
|
||||
// This will make this test unreliable.
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
65
network/simulation/service.go
Normal file
65
network/simulation/service.go
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
)
|
||||
|
||||
// Service returns a single Service by name on a particular node
|
||||
// with provided id.
|
||||
func (s *Simulation) Service(name string, id enode.ID) node.Service {
|
||||
simNode, ok := s.Net.GetNode(id).Node.(*adapters.SimNode)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
services := simNode.ServiceMap()
|
||||
if len(services) == 0 {
|
||||
return nil
|
||||
}
|
||||
return services[name]
|
||||
}
|
||||
|
||||
// RandomService returns a single Service by name on a
|
||||
// randomly chosen node that is up.
|
||||
func (s *Simulation) RandomService(name string) node.Service {
|
||||
n := s.Net.GetRandomUpNode().Node.(*adapters.SimNode)
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
return n.Service(name)
|
||||
}
|
||||
|
||||
// Services returns all services with a provided name
|
||||
// from nodes that are up.
|
||||
func (s *Simulation) Services(name string) (services map[enode.ID]node.Service) {
|
||||
nodes := s.Net.GetNodes()
|
||||
services = make(map[enode.ID]node.Service)
|
||||
for _, node := range nodes {
|
||||
if !node.Up() {
|
||||
continue
|
||||
}
|
||||
simNode, ok := node.Node.(*adapters.SimNode)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
services[node.ID()] = simNode.Service(name)
|
||||
}
|
||||
return services
|
||||
}
|
46
network/simulation/service_test.go
Normal file
46
network/simulation/service_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
id, err := sim.AddNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, ok := sim.Service("noop", id).(*noopService)
|
||||
if !ok {
|
||||
t.Fatalf("service is not of %T type", &noopService{})
|
||||
}
|
||||
|
||||
_, ok = sim.RandomService("noop").(*noopService)
|
||||
if !ok {
|
||||
t.Fatalf("service is not of %T type", &noopService{})
|
||||
}
|
||||
|
||||
_, ok = sim.Services("noop")[id].(*noopService)
|
||||
if !ok {
|
||||
t.Fatalf("service is not of %T type", &noopService{})
|
||||
}
|
||||
}
|
218
network/simulation/simulation.go
Normal file
218
network/simulation/simulation.go
Normal file
@ -0,0 +1,218 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
"github.com/ethersphere/swarm/network"
|
||||
)
|
||||
|
||||
// Common errors that are returned by functions in this package.
|
||||
var (
|
||||
ErrNodeNotFound = errors.New("node not found")
|
||||
)
|
||||
|
||||
// Simulation provides methods on network, nodes and services
|
||||
// to manage them.
|
||||
type Simulation struct {
|
||||
// Net is exposed as a way to access lower level functionalities
|
||||
// of p2p/simulations.Network.
|
||||
Net *simulations.Network
|
||||
|
||||
serviceNames []string
|
||||
cleanupFuncs []func()
|
||||
buckets map[enode.ID]*sync.Map
|
||||
shutdownWG sync.WaitGroup
|
||||
done chan struct{}
|
||||
mu sync.RWMutex
|
||||
neighbourhoodSize int
|
||||
|
||||
httpSrv *http.Server //attach a HTTP server via SimulationOptions
|
||||
handler *simulations.Server //HTTP handler for the server
|
||||
runC chan struct{} //channel where frontend signals it is ready
|
||||
}
|
||||
|
||||
// ServiceFunc is used in New to declare new service constructor.
|
||||
// The first argument provides ServiceContext from the adapters package
|
||||
// giving for example the access to NodeID. Second argument is the sync.Map
|
||||
// where all "global" state related to the service should be kept.
|
||||
// All cleanups needed for constructed service and any other constructed
|
||||
// objects should ne provided in a single returned cleanup function.
|
||||
// Returned cleanup function will be called by Close function
|
||||
// after network shutdown.
|
||||
type ServiceFunc func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error)
|
||||
|
||||
// New creates a new simulation instance
|
||||
// Services map must have unique keys as service names and
|
||||
// every ServiceFunc must return a node.Service of the unique type.
|
||||
// This restriction is required by node.Node.Start() function
|
||||
// which is used to start node.Service returned by ServiceFunc.
|
||||
func New(services map[string]ServiceFunc) (s *Simulation) {
|
||||
s = &Simulation{
|
||||
buckets: make(map[enode.ID]*sync.Map),
|
||||
done: make(chan struct{}),
|
||||
neighbourhoodSize: network.NewKadParams().NeighbourhoodSize,
|
||||
}
|
||||
|
||||
adapterServices := make(map[string]adapters.ServiceFunc, len(services))
|
||||
for name, serviceFunc := range services {
|
||||
// Scope this variables correctly
|
||||
// as they will be in the adapterServices[name] function accessed later.
|
||||
name, serviceFunc := name, serviceFunc
|
||||
s.serviceNames = append(s.serviceNames, name)
|
||||
adapterServices[name] = func(ctx *adapters.ServiceContext) (node.Service, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
b, ok := s.buckets[ctx.Config.ID]
|
||||
if !ok {
|
||||
b = new(sync.Map)
|
||||
}
|
||||
service, cleanup, err := serviceFunc(ctx, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cleanup != nil {
|
||||
s.cleanupFuncs = append(s.cleanupFuncs, cleanup)
|
||||
}
|
||||
s.buckets[ctx.Config.ID] = b
|
||||
return service, nil
|
||||
}
|
||||
}
|
||||
|
||||
s.Net = simulations.NewNetwork(
|
||||
adapters.NewTCPAdapter(adapterServices),
|
||||
&simulations.NetworkConfig{ID: "0"},
|
||||
)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// RunFunc is the function that will be called
|
||||
// on Simulation.Run method call.
|
||||
type RunFunc func(context.Context, *Simulation) error
|
||||
|
||||
// Result is the returned value of Simulation.Run method.
|
||||
type Result struct {
|
||||
Duration time.Duration
|
||||
Error error
|
||||
}
|
||||
|
||||
// Run calls the RunFunc function while taking care of
|
||||
// cancellation provided through the Context.
|
||||
func (s *Simulation) Run(ctx context.Context, f RunFunc) (r Result) {
|
||||
//if the option is set to run a HTTP server with the simulation,
|
||||
//init the server and start it
|
||||
start := time.Now()
|
||||
if s.httpSrv != nil {
|
||||
log.Info("Waiting for frontend to be ready...(send POST /runsim to HTTP server)")
|
||||
//wait for the frontend to connect
|
||||
select {
|
||||
case <-s.runC:
|
||||
case <-ctx.Done():
|
||||
return Result{
|
||||
Duration: time.Since(start),
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
log.Info("Received signal from frontend - starting simulation run.")
|
||||
}
|
||||
errc := make(chan error)
|
||||
quit := make(chan struct{})
|
||||
defer close(quit)
|
||||
go func() {
|
||||
select {
|
||||
case errc <- f(ctx, s):
|
||||
case <-quit:
|
||||
}
|
||||
}()
|
||||
var err error
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
case err = <-errc:
|
||||
}
|
||||
return Result{
|
||||
Duration: time.Since(start),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Maximal number of parallel calls to cleanup functions on
|
||||
// Simulation.Close.
|
||||
var maxParallelCleanups = 10
|
||||
|
||||
// Close calls all cleanup functions that are returned by
|
||||
// ServiceFunc, waits for all of them to finish and other
|
||||
// functions that explicitly block shutdownWG
|
||||
// (like Simulation.PeerEvents) and shuts down the network
|
||||
// at the end. It is used to clean all resources from the
|
||||
// simulation.
|
||||
func (s *Simulation) Close() {
|
||||
close(s.done)
|
||||
|
||||
sem := make(chan struct{}, maxParallelCleanups)
|
||||
s.mu.RLock()
|
||||
cleanupFuncs := make([]func(), len(s.cleanupFuncs))
|
||||
for i, f := range s.cleanupFuncs {
|
||||
if f != nil {
|
||||
cleanupFuncs[i] = f
|
||||
}
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
var cleanupWG sync.WaitGroup
|
||||
for _, cleanup := range cleanupFuncs {
|
||||
cleanupWG.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func(cleanup func()) {
|
||||
defer cleanupWG.Done()
|
||||
defer func() { <-sem }()
|
||||
|
||||
cleanup()
|
||||
}(cleanup)
|
||||
}
|
||||
cleanupWG.Wait()
|
||||
|
||||
if s.httpSrv != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
err := s.httpSrv.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Error("Error shutting down HTTP server!", "err", err)
|
||||
}
|
||||
close(s.runC)
|
||||
}
|
||||
|
||||
s.shutdownWG.Wait()
|
||||
s.Net.Shutdown()
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the simulation
|
||||
// is closed by Close method. It is useful for signaling termination
|
||||
// of all possible goroutines that are created within the test.
|
||||
func (s *Simulation) Done() <-chan struct{} {
|
||||
return s.done
|
||||
}
|
203
network/simulation/simulation_test.go
Normal file
203
network/simulation/simulation_test.go
Normal file
@ -0,0 +1,203 @@
|
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
"github.com/mattn/go-colorable"
|
||||
)
|
||||
|
||||
var (
|
||||
loglevel = flag.Int("loglevel", 2, "verbosity of logs")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Parse()
|
||||
log.PrintOrigins(true)
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true))))
|
||||
}
|
||||
|
||||
// TestRun tests if Run method calls RunFunc and if it handles context properly.
|
||||
func TestRun(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
defer sim.Close()
|
||||
|
||||
t.Run("call", func(t *testing.T) {
|
||||
expect := "something"
|
||||
var got string
|
||||
r := sim.Run(context.Background(), func(ctx context.Context, sim *Simulation) error {
|
||||
got = expect
|
||||
return nil
|
||||
})
|
||||
|
||||
if r.Error != nil {
|
||||
t.Errorf("unexpected error: %v", r.Error)
|
||||
}
|
||||
if got != expect {
|
||||
t.Errorf("expected %q, got %q", expect, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cancellation", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
r := sim.Run(ctx, func(ctx context.Context, sim *Simulation) error {
|
||||
time.Sleep(time.Second)
|
||||
return nil
|
||||
})
|
||||
|
||||
if r.Error != context.DeadlineExceeded {
|
||||
t.Errorf("unexpected error: %v", r.Error)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("context value and duration", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "hey", "there")
|
||||
sleep := 50 * time.Millisecond
|
||||
|
||||
r := sim.Run(ctx, func(ctx context.Context, sim *Simulation) error {
|
||||
if ctx.Value("hey") != "there" {
|
||||
return errors.New("expected context value not passed")
|
||||
}
|
||||
time.Sleep(sleep)
|
||||
return nil
|
||||
})
|
||||
|
||||
if r.Error != nil {
|
||||
t.Errorf("unexpected error: %v", r.Error)
|
||||
}
|
||||
if r.Duration < sleep {
|
||||
t.Errorf("reported run duration less then expected: %s", r.Duration)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestClose tests are Close method triggers all close functions and are all nodes not up anymore.
|
||||
func TestClose(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
var cleanupCount int
|
||||
|
||||
sleep := 50 * time.Millisecond
|
||||
|
||||
sim := New(map[string]ServiceFunc{
|
||||
"noop": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
|
||||
return newNoopService(), func() {
|
||||
time.Sleep(sleep)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
cleanupCount++
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
nodeCount := 30
|
||||
|
||||
_, err := sim.AddNodes(nodeCount)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var upNodeCount int
|
||||
for _, n := range sim.Net.GetNodes() {
|
||||
if n.Up() {
|
||||
upNodeCount++
|
||||
}
|
||||
}
|
||||
if upNodeCount != nodeCount {
|
||||
t.Errorf("all nodes should be up, insted only %v are up", upNodeCount)
|
||||
}
|
||||
|
||||
sim.Close()
|
||||
|
||||
if cleanupCount != nodeCount {
|
||||
t.Errorf("number of cleanups expected %v, got %v", nodeCount, cleanupCount)
|
||||
}
|
||||
|
||||
upNodeCount = 0
|
||||
for _, n := range sim.Net.GetNodes() {
|
||||
if n.Up() {
|
||||
upNodeCount++
|
||||
}
|
||||
}
|
||||
if upNodeCount != 0 {
|
||||
t.Errorf("all nodes should be down, insted %v are up", upNodeCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDone checks if Close method triggers the closing of done channel.
|
||||
func TestDone(t *testing.T) {
|
||||
sim := New(noopServiceFuncMap)
|
||||
sleep := 50 * time.Millisecond
|
||||
timeout := 2 * time.Second
|
||||
|
||||
start := time.Now()
|
||||
go func() {
|
||||
time.Sleep(sleep)
|
||||
sim.Close()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
t.Error("done channel closing timed out")
|
||||
case <-sim.Done():
|
||||
if d := time.Since(start); d < sleep {
|
||||
t.Errorf("done channel closed sooner then expected: %s", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a helper map for usual services that do not do anything
|
||||
var noopServiceFuncMap = map[string]ServiceFunc{
|
||||
"noop": noopServiceFunc,
|
||||
}
|
||||
|
||||
// a helper function for most basic noop service
|
||||
func noopServiceFunc(_ *adapters.ServiceContext, _ *sync.Map) (node.Service, func(), error) {
|
||||
return newNoopService(), nil, nil
|
||||
}
|
||||
|
||||
func newNoopService() node.Service {
|
||||
return &noopService{}
|
||||
}
|
||||
|
||||
// a helper function for most basic Noop service
|
||||
// of a different type then NoopService to test
|
||||
// multiple services on one node.
|
||||
func noopService2Func(_ *adapters.ServiceContext, _ *sync.Map) (node.Service, func(), error) {
|
||||
return new(noopService2), nil, nil
|
||||
}
|
||||
|
||||
// NoopService2 is the service that does not do anything
|
||||
// but implements node.Service interface.
|
||||
type noopService2 struct {
|
||||
simulations.NoopService
|
||||
}
|
||||
|
||||
type noopService struct {
|
||||
simulations.NoopService
|
||||
}
|
Reference in New Issue
Block a user