github.com/susy-go/susy-graviton@v0.0.0-20190614130430-36cddae42305/swarm/network_test.go (about)

     1  // Copyleft 2018 The susy-graviton Authors
     2  // This file is part of the susy-graviton library.
     3  //
     4  // The susy-graviton library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The susy-graviton library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MSRCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the susy-graviton library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package swarm
    18  
    19  import (
    20  	"context"
    21  	"flag"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"math/rand"
    25  	"os"
    26  	"sync"
    27  	"sync/atomic"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/susy-go/susy-graviton/crypto"
    32  	"github.com/susy-go/susy-graviton/log"
    33  	"github.com/susy-go/susy-graviton/node"
    34  	"github.com/susy-go/susy-graviton/p2p/enode"
    35  	"github.com/susy-go/susy-graviton/p2p/simulations/adapters"
    36  	"github.com/susy-go/susy-graviton/swarm/api"
    37  	"github.com/susy-go/susy-graviton/swarm/network/simulation"
    38  	"github.com/susy-go/susy-graviton/swarm/storage"
    39  	colorable "github.com/mattn/go-colorable"
    40  )
    41  
    42  var (
    43  	loglevel     = flag.Int("loglevel", 2, "verbosity of logs")
    44  	longrunning  = flag.Bool("longrunning", false, "do run long-running tests")
    45  	waitKademlia = flag.Bool("waitkademlia", false, "wait for healthy kademlia before checking files availability")
    46  )
    47  
    48  func init() {
    49  	rand.Seed(time.Now().UnixNano())
    50  
    51  	flag.Parse()
    52  
    53  	log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true))))
    54  }
    55  
    56  // TestSwarmNetwork runs a series of test simulations with
    57  // static and dynamic Swarm nodes in network simulation, by
    58  // uploading files to every node and retrieving them.
    59  func TestSwarmNetwork(t *testing.T) {
    60  	for _, tc := range []struct {
    61  		name     string
    62  		steps    []testSwarmNetworkStep
    63  		options  *testSwarmNetworkOptions
    64  		disabled bool
    65  	}{
    66  		{
    67  			name: "10_nodes",
    68  			steps: []testSwarmNetworkStep{
    69  				{
    70  					nodeCount: 10,
    71  				},
    72  			},
    73  			options: &testSwarmNetworkOptions{
    74  				Timeout: 45 * time.Second,
    75  			},
    76  		},
    77  		{
    78  			name: "10_nodes_skip_check",
    79  			steps: []testSwarmNetworkStep{
    80  				{
    81  					nodeCount: 10,
    82  				},
    83  			},
    84  			options: &testSwarmNetworkOptions{
    85  				Timeout:   45 * time.Second,
    86  				SkipCheck: true,
    87  			},
    88  		},
    89  		{
    90  			name: "50_nodes",
    91  			steps: []testSwarmNetworkStep{
    92  				{
    93  					nodeCount: 50,
    94  				},
    95  			},
    96  			options: &testSwarmNetworkOptions{
    97  				Timeout: 3 * time.Minute,
    98  			},
    99  			disabled: !*longrunning,
   100  		},
   101  		{
   102  			name: "50_nodes_skip_check",
   103  			steps: []testSwarmNetworkStep{
   104  				{
   105  					nodeCount: 50,
   106  				},
   107  			},
   108  			options: &testSwarmNetworkOptions{
   109  				Timeout:   3 * time.Minute,
   110  				SkipCheck: true,
   111  			},
   112  			disabled: !*longrunning,
   113  		},
   114  		{
   115  			name: "inc_node_count",
   116  			steps: []testSwarmNetworkStep{
   117  				{
   118  					nodeCount: 2,
   119  				},
   120  				{
   121  					nodeCount: 5,
   122  				},
   123  				{
   124  					nodeCount: 10,
   125  				},
   126  			},
   127  			options: &testSwarmNetworkOptions{
   128  				Timeout: 90 * time.Second,
   129  			},
   130  			disabled: !*longrunning,
   131  		},
   132  		{
   133  			name: "dec_node_count",
   134  			steps: []testSwarmNetworkStep{
   135  				{
   136  					nodeCount: 10,
   137  				},
   138  				{
   139  					nodeCount: 6,
   140  				},
   141  				{
   142  					nodeCount: 3,
   143  				},
   144  			},
   145  			options: &testSwarmNetworkOptions{
   146  				Timeout: 90 * time.Second,
   147  			},
   148  			disabled: !*longrunning,
   149  		},
   150  		{
   151  			name: "dec_inc_node_count",
   152  			steps: []testSwarmNetworkStep{
   153  				{
   154  					nodeCount: 3,
   155  				},
   156  				{
   157  					nodeCount: 1,
   158  				},
   159  				{
   160  					nodeCount: 5,
   161  				},
   162  			},
   163  			options: &testSwarmNetworkOptions{
   164  				Timeout: 90 * time.Second,
   165  			},
   166  		},
   167  		{
   168  			name: "inc_dec_node_count",
   169  			steps: []testSwarmNetworkStep{
   170  				{
   171  					nodeCount: 3,
   172  				},
   173  				{
   174  					nodeCount: 5,
   175  				},
   176  				{
   177  					nodeCount: 25,
   178  				},
   179  				{
   180  					nodeCount: 10,
   181  				},
   182  				{
   183  					nodeCount: 4,
   184  				},
   185  			},
   186  			options: &testSwarmNetworkOptions{
   187  				Timeout: 5 * time.Minute,
   188  			},
   189  			disabled: !*longrunning,
   190  		},
   191  		{
   192  			name: "inc_dec_node_count_skip_check",
   193  			steps: []testSwarmNetworkStep{
   194  				{
   195  					nodeCount: 3,
   196  				},
   197  				{
   198  					nodeCount: 5,
   199  				},
   200  				{
   201  					nodeCount: 25,
   202  				},
   203  				{
   204  					nodeCount: 10,
   205  				},
   206  				{
   207  					nodeCount: 4,
   208  				},
   209  			},
   210  			options: &testSwarmNetworkOptions{
   211  				Timeout:   5 * time.Minute,
   212  				SkipCheck: true,
   213  			},
   214  			disabled: !*longrunning,
   215  		},
   216  	} {
   217  		if tc.disabled {
   218  			continue
   219  		}
   220  		t.Run(tc.name, func(t *testing.T) {
   221  			testSwarmNetwork(t, tc.options, tc.steps...)
   222  		})
   223  	}
   224  }
   225  
   226  // testSwarmNetworkStep is the configuration
   227  // for the state of the simulation network.
   228  type testSwarmNetworkStep struct {
   229  	// number of swarm nodes that must be in the Up state
   230  	nodeCount int
   231  }
   232  
   233  // file represents the file uploaded on a particular node.
   234  type file struct {
   235  	addr   storage.Address
   236  	data   string
   237  	nodeID enode.ID
   238  }
   239  
   240  // check represents a reference to a file that is retrieved
   241  // from a particular node.
   242  type check struct {
   243  	key    string
   244  	nodeID enode.ID
   245  }
   246  
   247  // testSwarmNetworkOptions contains optional parameters for running
   248  // testSwarmNetwork.
   249  type testSwarmNetworkOptions struct {
   250  	Timeout   time.Duration
   251  	SkipCheck bool
   252  }
   253  
   254  // testSwarmNetwork is a helper function used for testing different
   255  // static and dynamic Swarm network simulations.
   256  // It is responsible for:
   257  //  - Setting up a Swarm network simulation, and updates the number of nodes within the network on every step according to steps.
   258  //  - Uploading a unique file to every node on every step.
   259  //  - May wait for Kademlia on every node to be healthy.
   260  //  - Checking if a file is retrievable from all nodes.
   261  func testSwarmNetwork(t *testing.T, o *testSwarmNetworkOptions, steps ...testSwarmNetworkStep) {
   262  
   263  	if o == nil {
   264  		o = new(testSwarmNetworkOptions)
   265  	}
   266  
   267  	sim := simulation.New(map[string]simulation.ServiceFunc{
   268  		"swarm": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) {
   269  			config := api.NewConfig()
   270  
   271  			dir, err := ioutil.TempDir("", "swarm-network-test-node")
   272  			if err != nil {
   273  				return nil, nil, err
   274  			}
   275  			cleanup = func() {
   276  				err := os.RemoveAll(dir)
   277  				if err != nil {
   278  					log.Error("cleaning up swarm temp dir", "err", err)
   279  				}
   280  			}
   281  
   282  			config.Path = dir
   283  
   284  			privkey, err := crypto.GenerateKey()
   285  			if err != nil {
   286  				return nil, cleanup, err
   287  			}
   288  
   289  			config.Init(privkey)
   290  			config.DeliverySkipCheck = o.SkipCheck
   291  			config.Port = ""
   292  
   293  			swarm, err := NewSwarm(config, nil)
   294  			if err != nil {
   295  				return nil, cleanup, err
   296  			}
   297  			bucket.Store(simulation.BucketKeyKademlia, swarm.bzz.Hive.Kademlia)
   298  			log.Info("new swarm", "bzzKey", config.BzzKey, "baseAddr", fmt.Sprintf("%x", swarm.bzz.BaseAddr()))
   299  			return swarm, cleanup, nil
   300  		},
   301  	})
   302  	defer sim.Close()
   303  
   304  	ctx := context.Background()
   305  	if o.Timeout > 0 {
   306  		var cancel context.CancelFunc
   307  		ctx, cancel = context.WithTimeout(ctx, o.Timeout)
   308  		defer cancel()
   309  	}
   310  
   311  	files := make([]file, 0)
   312  
   313  	for i, step := range steps {
   314  		log.Debug("test sync step", "n", i+1, "nodes", step.nodeCount)
   315  
   316  		change := step.nodeCount - len(sim.UpNodeIDs())
   317  
   318  		if change > 0 {
   319  			_, err := sim.AddNodesAndConnectChain(change)
   320  			if err != nil {
   321  				t.Fatal(err)
   322  			}
   323  		} else if change < 0 {
   324  			_, err := sim.StopRandomNodes(-change)
   325  			if err != nil {
   326  				t.Fatal(err)
   327  			}
   328  		} else {
   329  			t.Logf("step %v: no change in nodes", i)
   330  			continue
   331  		}
   332  
   333  		var checkStatusM sync.Map
   334  		var nodeStatusM sync.Map
   335  		var totalFoundCount uint64
   336  
   337  		result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error {
   338  			nodeIDs := sim.UpNodeIDs()
   339  			rand.Shuffle(len(nodeIDs), func(i, j int) {
   340  				nodeIDs[i], nodeIDs[j] = nodeIDs[j], nodeIDs[i]
   341  			})
   342  			for _, id := range nodeIDs {
   343  				key, data, err := uploadFile(sim.Service("swarm", id).(*Swarm))
   344  				if err != nil {
   345  					return err
   346  				}
   347  				log.Trace("file uploaded", "node", id, "key", key.String())
   348  				files = append(files, file{
   349  					addr:   key,
   350  					data:   data,
   351  					nodeID: id,
   352  				})
   353  			}
   354  
   355  			if *waitKademlia {
   356  				if _, err := sim.WaitTillHealthy(ctx); err != nil {
   357  					return err
   358  				}
   359  			}
   360  
   361  			// File retrieval check is repeated until all uploaded files are retrieved from all nodes
   362  			// or until the timeout is reached.
   363  			for {
   364  				if retrieve(sim, files, &checkStatusM, &nodeStatusM, &totalFoundCount) == 0 {
   365  					return nil
   366  				}
   367  			}
   368  		})
   369  
   370  		if result.Error != nil {
   371  			t.Fatal(result.Error)
   372  		}
   373  		log.Debug("done: test sync step", "n", i+1, "nodes", step.nodeCount)
   374  	}
   375  }
   376  
   377  // uploadFile, uploads a short file to the swarm instance
   378  // using the api.Put method.
   379  func uploadFile(swarm *Swarm) (storage.Address, string, error) {
   380  	b := make([]byte, 8)
   381  	_, err := rand.Read(b)
   382  	if err != nil {
   383  		return nil, "", err
   384  	}
   385  	// File data is very short, but it is ensured that its
   386  	// uniqueness is very certain.
   387  	data := fmt.Sprintf("test content %s %x", time.Now().Round(0), b)
   388  	ctx := context.TODO()
   389  	k, wait, err := swarm.api.Put(ctx, data, "text/plain", false)
   390  	if err != nil {
   391  		return nil, "", err
   392  	}
   393  	if wait != nil {
   394  		err = wait(ctx)
   395  	}
   396  	return k, data, err
   397  }
   398  
   399  // retrieve is the function that is used for checking the availability of
   400  // uploaded files in testSwarmNetwork test helper function.
   401  func retrieve(
   402  	sim *simulation.Simulation,
   403  	files []file,
   404  	checkStatusM *sync.Map,
   405  	nodeStatusM *sync.Map,
   406  	totalFoundCount *uint64,
   407  ) (missing uint64) {
   408  	rand.Shuffle(len(files), func(i, j int) {
   409  		files[i], files[j] = files[j], files[i]
   410  	})
   411  
   412  	var totalWg sync.WaitGroup
   413  	errc := make(chan error)
   414  
   415  	nodeIDs := sim.UpNodeIDs()
   416  
   417  	totalCheckCount := len(nodeIDs) * len(files)
   418  
   419  	for _, id := range nodeIDs {
   420  		if _, ok := nodeStatusM.Load(id); ok {
   421  			continue
   422  		}
   423  		start := time.Now()
   424  		var checkCount uint64
   425  		var foundCount uint64
   426  
   427  		totalWg.Add(1)
   428  
   429  		var wg sync.WaitGroup
   430  
   431  		swarm := sim.Service("swarm", id).(*Swarm)
   432  		for _, f := range files {
   433  
   434  			checkKey := check{
   435  				key:    f.addr.String(),
   436  				nodeID: id,
   437  			}
   438  			if n, ok := checkStatusM.Load(checkKey); ok && n.(int) == 0 {
   439  				continue
   440  			}
   441  
   442  			checkCount++
   443  			wg.Add(1)
   444  			go func(f file, id enode.ID) {
   445  				defer wg.Done()
   446  
   447  				log.Debug("api get: check file", "node", id.String(), "key", f.addr.String(), "total files found", atomic.LoadUint64(totalFoundCount))
   448  
   449  				r, _, _, _, err := swarm.api.Get(context.TODO(), api.NOOPDecrypt, f.addr, "/")
   450  				if err != nil {
   451  					errc <- fmt.Errorf("api get: node %s, key %s, kademlia %s: %v", id, f.addr, swarm.bzz.Hive, err)
   452  					return
   453  				}
   454  				d, err := ioutil.ReadAll(r)
   455  				if err != nil {
   456  					errc <- fmt.Errorf("api get: read response: node %s, key %s: kademlia %s: %v", id, f.addr, swarm.bzz.Hive, err)
   457  					return
   458  				}
   459  				data := string(d)
   460  				if data != f.data {
   461  					errc <- fmt.Errorf("file contend missmatch: node %s, key %s, expected %q, got %q", id, f.addr, f.data, data)
   462  					return
   463  				}
   464  				checkStatusM.Store(checkKey, 0)
   465  				atomic.AddUint64(&foundCount, 1)
   466  				log.Info("api get: file found", "node", id.String(), "key", f.addr.String(), "content", data, "files found", atomic.LoadUint64(&foundCount))
   467  			}(f, id)
   468  		}
   469  
   470  		go func(id enode.ID) {
   471  			defer totalWg.Done()
   472  			wg.Wait()
   473  
   474  			atomic.AddUint64(totalFoundCount, foundCount)
   475  
   476  			if foundCount == checkCount {
   477  				log.Info("all files are found for node", "id", id.String(), "duration", time.Since(start))
   478  				nodeStatusM.Store(id, 0)
   479  				return
   480  			}
   481  			log.Debug("files missing for node", "id", id.String(), "check", checkCount, "found", foundCount)
   482  		}(id)
   483  
   484  	}
   485  
   486  	go func() {
   487  		totalWg.Wait()
   488  		close(errc)
   489  	}()
   490  
   491  	var errCount int
   492  	for err := range errc {
   493  		if err != nil {
   494  			errCount++
   495  		}
   496  		log.Warn(err.Error())
   497  	}
   498  
   499  	log.Info("check stats", "total check count", totalCheckCount, "total files found", atomic.LoadUint64(totalFoundCount), "total errors", errCount)
   500  
   501  	return uint64(totalCheckCount) - atomic.LoadUint64(totalFoundCount)
   502  }