github.com/avahowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/renter/hostdb/weightedlist_test.go (about)

     1  package hostdb
     2  
     3  import (
     4  	"crypto/rand"
     5  	"errors"
     6  	"math/big"
     7  	"strconv"
     8  	"testing"
     9  
    10  	"github.com/NebulousLabs/Sia/modules"
    11  	"github.com/NebulousLabs/Sia/types"
    12  )
    13  
    14  // fakeAddr returns a modules.NetAddress to be used in a HostEntry. Such
    15  // addresses are needed in order to satisfy the HostDB's "1 host per IP" rule.
    16  func fakeAddr(n uint8) modules.NetAddress {
    17  	return modules.NetAddress("127.0.0." + strconv.Itoa(int(n)) + ":1")
    18  }
    19  
    20  // uniformTreeVerification checks that everything makes sense in the tree given
    21  // the number of entries that the tree is supposed to have and also given that
    22  // every entropy has the same weight.
    23  func uniformTreeVerification(hdb *HostDB, numEntries int) error {
    24  	// Check that the weight of the hostTree is what is expected.
    25  	expectedWeight := hdb.hostTree.hostEntry.Weight.Mul64(uint64(numEntries))
    26  	if hdb.hostTree.weight.Cmp(expectedWeight) != 0 {
    27  		return errors.New("expected weight is incorrect")
    28  	}
    29  
    30  	// Check that the length of activeHosts and the count of hostTree are
    31  	// consistent.
    32  	if len(hdb.activeHosts) != numEntries {
    33  		return errors.New("unexpected number of active hosts")
    34  	}
    35  
    36  	// Select many random hosts and do naive statistical analysis on the
    37  	// results.
    38  	if !testing.Short() {
    39  		// Pull a bunch of random hosts and count how many times we pull each
    40  		// host.
    41  		selectionMap := make(map[modules.NetAddress]int)
    42  		expected := 100
    43  		for i := 0; i < expected*numEntries; i++ {
    44  			entries := hdb.RandomHosts(1, nil)
    45  			if len(entries) == 0 {
    46  				return errors.New("no hosts")
    47  			}
    48  			selectionMap[entries[0].NetAddress]++
    49  		}
    50  
    51  		// See if each host was selected enough times.
    52  		errorBound := 64 // Pretty large, but will still detect if something is seriously wrong.
    53  		for _, count := range selectionMap {
    54  			if count < expected-errorBound || count > expected+errorBound {
    55  				return errors.New("error bound was breached")
    56  			}
    57  		}
    58  	}
    59  
    60  	// Try removing an re-adding all hosts.
    61  	var removedEntries []*hostEntry
    62  	for {
    63  		if hdb.hostTree.weight.IsZero() {
    64  			break
    65  		}
    66  		randWeight, err := rand.Int(rand.Reader, hdb.hostTree.weight.Big())
    67  		if err != nil {
    68  			break
    69  		}
    70  		node, err := hdb.hostTree.nodeAtWeight(types.NewCurrency(randWeight))
    71  		if err != nil {
    72  			break
    73  		}
    74  		node.removeNode()
    75  		delete(hdb.activeHosts, node.hostEntry.NetAddress)
    76  
    77  		// remove the entry from the hostdb so it won't be selected as a
    78  		// repeat.
    79  		removedEntries = append(removedEntries, node.hostEntry)
    80  	}
    81  	for _, entry := range removedEntries {
    82  		hdb.insertNode(entry)
    83  	}
    84  	return nil
    85  }
    86  
    87  // TestWeightedList inserts and removes nodes in a semi-random manner and
    88  // verifies that the tree stays consistent through the adjustments.
    89  func TestWeightedList(t *testing.T) {
    90  	if testing.Short() {
    91  		t.SkipNow()
    92  	}
    93  
    94  	// Create a hostdb and 3 equal entries to insert.
    95  	hdb := &HostDB{
    96  		activeHosts: make(map[modules.NetAddress]*hostNode),
    97  		allHosts:    make(map[modules.NetAddress]*hostEntry),
    98  		scanPool:    make(chan *hostEntry, scanPoolSize),
    99  	}
   100  
   101  	// Create a bunch of host entries of equal weight.
   102  	var dbe modules.HostDBEntry
   103  	firstInsertions := 64
   104  	for i := 0; i < firstInsertions; i++ {
   105  		dbe.NetAddress = fakeAddr(uint8(i))
   106  		entry := hostEntry{
   107  			HostDBEntry: dbe,
   108  			Weight:      types.NewCurrency64(10),
   109  		}
   110  		hdb.insertNode(&entry)
   111  	}
   112  	err := uniformTreeVerification(hdb, firstInsertions)
   113  	if err != nil {
   114  		t.Error(err)
   115  	}
   116  
   117  	// Remove a few hosts and check that the tree is still in order.
   118  	removals := 12
   119  	// Keep a map of what we've removed so far.
   120  	removedMap := make(map[uint8]struct{})
   121  	for i := 0; i < removals; i++ {
   122  		// Try numbers until we roll a number that's not been removed yet.
   123  		var randInt uint8
   124  		for {
   125  			randBig, err := rand.Int(rand.Reader, big.NewInt(int64(firstInsertions)))
   126  			if err != nil {
   127  				t.Fatal(err)
   128  			}
   129  			randInt = uint8(randBig.Int64())
   130  			_, exists := removedMap[randInt]
   131  			if !exists {
   132  				break
   133  			}
   134  		}
   135  
   136  		// Remove the entry and add it to the list of removed entries
   137  		err := hdb.removeHost(fakeAddr(randInt))
   138  		if err != nil {
   139  			t.Fatal(err)
   140  		}
   141  		removedMap[randInt] = struct{}{}
   142  	}
   143  	err = uniformTreeVerification(hdb, firstInsertions-removals)
   144  	if err != nil {
   145  		t.Error(err)
   146  	}
   147  
   148  	// Do some more insertions.
   149  	secondInsertions := 64
   150  	for i := firstInsertions; i < firstInsertions+secondInsertions; i++ {
   151  		dbe.NetAddress = fakeAddr(uint8(i))
   152  		entry := hostEntry{
   153  			HostDBEntry: dbe,
   154  			Weight:      types.NewCurrency64(10),
   155  		}
   156  		hdb.insertNode(&entry)
   157  	}
   158  	err = uniformTreeVerification(hdb, firstInsertions-removals+secondInsertions)
   159  	if err != nil {
   160  		t.Error(err)
   161  	}
   162  }
   163  
   164  // TestVariedWeights runs broad statistical tests on selecting hosts with
   165  // multiple different weights.
   166  func TestVariedWeights(t *testing.T) {
   167  	if testing.Short() {
   168  		t.SkipNow()
   169  	}
   170  	hdb := &HostDB{
   171  		activeHosts: make(map[modules.NetAddress]*hostNode),
   172  		allHosts:    make(map[modules.NetAddress]*hostEntry),
   173  		scanPool:    make(chan *hostEntry, scanPoolSize),
   174  	}
   175  
   176  	// insert i hosts with the weights 0, 1, ..., i-1. 100e3 selections will be made
   177  	// per weight added to the tree, the total number of selections necessary
   178  	// will be tallied up as hosts are created.
   179  	var dbe modules.HostDBEntry
   180  	hostCount := 5
   181  	expectedPerWeight := int(10e3)
   182  	selections := 0
   183  	for i := 0; i < hostCount; i++ {
   184  		dbe.NetAddress = fakeAddr(uint8(i))
   185  		entry := hostEntry{
   186  			HostDBEntry: dbe,
   187  			Weight:      types.NewCurrency64(uint64(i)),
   188  		}
   189  		hdb.insertNode(&entry)
   190  		selections += i * expectedPerWeight
   191  	}
   192  
   193  	// Perform many random selections, noting which host was selected each
   194  	// time.
   195  	selectionMap := make(map[string]int)
   196  	for i := 0; i < selections; i++ {
   197  		randEntry := hdb.RandomHosts(1, nil)
   198  		if len(randEntry) == 0 {
   199  			t.Fatal("no hosts!")
   200  		}
   201  		node, exists := hdb.activeHosts[randEntry[0].NetAddress]
   202  		if !exists {
   203  			t.Fatal("can't find randomly selected node in tree")
   204  		}
   205  		selectionMap[node.hostEntry.Weight.String()]++
   206  	}
   207  
   208  	// Check that each host was selected an expected number of times. An error
   209  	// will be reported if the host of 0 weight is ever selected.
   210  	acceptableError := 0.2
   211  	for weight, timesSelected := range selectionMap {
   212  		intWeight, err := strconv.Atoi(weight)
   213  		if err != nil {
   214  			t.Fatal(err)
   215  		}
   216  
   217  		expectedSelected := float64(intWeight * expectedPerWeight)
   218  		if float64(expectedSelected)*acceptableError > float64(timesSelected) || float64(expectedSelected)/acceptableError < float64(timesSelected) {
   219  			t.Error("weighted list not selecting in a uniform distribution based on weight")
   220  			t.Error(expectedSelected)
   221  			t.Error(timesSelected)
   222  		}
   223  	}
   224  }
   225  
   226  // TestRepeatInsert inserts 2 hosts with the same address.
   227  func TestRepeatInsert(t *testing.T) {
   228  	if testing.Short() {
   229  		t.SkipNow()
   230  	}
   231  	hdb := &HostDB{
   232  		activeHosts: make(map[modules.NetAddress]*hostNode),
   233  		allHosts:    make(map[modules.NetAddress]*hostEntry),
   234  		scanPool:    make(chan *hostEntry, scanPoolSize),
   235  	}
   236  
   237  	var dbe modules.HostDBEntry
   238  	dbe.NetAddress = fakeAddr(0)
   239  	entry1 := hostEntry{
   240  		HostDBEntry: dbe,
   241  		Weight:      types.NewCurrency64(1),
   242  	}
   243  	entry2 := entry1
   244  	hdb.insertNode(&entry1)
   245  
   246  	entry2.Weight = types.NewCurrency64(100)
   247  	hdb.insertNode(&entry2)
   248  	if len(hdb.activeHosts) != 1 {
   249  		t.Error("insterting the same entry twice should result in only 1 entry in the hostdb")
   250  	}
   251  }
   252  
   253  // TestNodeAtWeight tests the nodeAtWeight method.
   254  func TestNodeAtWeight(t *testing.T) {
   255  	// create hostTree
   256  	h1 := new(hostEntry)
   257  	h1.NetAddress = "foo"
   258  	h1.Weight = baseWeight
   259  	ht := createNode(nil, h1)
   260  
   261  	// overweight
   262  	_, err := ht.nodeAtWeight(baseWeight.Mul64(2))
   263  	if err != errOverweight {
   264  		t.Errorf("expected %v, got %v", errOverweight, err)
   265  	}
   266  
   267  	h, err := ht.nodeAtWeight(baseWeight)
   268  	if err != nil {
   269  		t.Error(err)
   270  	} else if h.hostEntry != h1 {
   271  		t.Errorf("nodeAtWeight returned wrong node: expected %v, got %v", h1, h.hostEntry)
   272  	}
   273  }
   274  
   275  // TestRandomHosts probes the RandomHosts function.
   276  func TestRandomHosts(t *testing.T) {
   277  	// Create the hostdb.
   278  	hdb := bareHostDB()
   279  
   280  	// Empty.
   281  	if hosts := hdb.RandomHosts(1, nil); len(hosts) != 0 {
   282  		t.Errorf("empty hostdb returns %v hosts: %v", len(hosts), hosts)
   283  	}
   284  
   285  	// Insert 3 hosts to be selected.
   286  	var dbe modules.HostDBEntry
   287  	dbe.NetAddress = fakeAddr(1)
   288  	entry1 := hostEntry{
   289  		HostDBEntry: dbe,
   290  		Weight:      types.NewCurrency64(1),
   291  	}
   292  	dbe.NetAddress = fakeAddr(2)
   293  	entry2 := hostEntry{
   294  		HostDBEntry: dbe,
   295  		Weight:      types.NewCurrency64(2),
   296  	}
   297  	dbe.NetAddress = fakeAddr(3)
   298  	entry3 := hostEntry{
   299  		HostDBEntry: dbe,
   300  		Weight:      types.NewCurrency64(3),
   301  	}
   302  	hdb.insertNode(&entry1)
   303  	hdb.insertNode(&entry2)
   304  	hdb.insertNode(&entry3)
   305  
   306  	if len(hdb.activeHosts) != 3 {
   307  		t.Error("wrong number of hosts")
   308  	}
   309  	if hdb.hostTree.weight.Cmp(types.NewCurrency64(6)) != 0 {
   310  		t.Error("unexpected weight at initialization")
   311  		t.Error(hdb.hostTree.weight)
   312  	}
   313  
   314  	// Grab 1 random host.
   315  	randHosts := hdb.RandomHosts(1, nil)
   316  	if len(randHosts) != 1 {
   317  		t.Error("didn't get 1 hosts")
   318  	}
   319  
   320  	// Grab 2 random hosts.
   321  	randHosts = hdb.RandomHosts(2, nil)
   322  	if len(randHosts) != 2 {
   323  		t.Error("didn't get 2 hosts")
   324  	}
   325  	if randHosts[0].NetAddress == randHosts[1].NetAddress {
   326  		t.Error("doubled up")
   327  	}
   328  
   329  	// Grab 3 random hosts.
   330  	randHosts = hdb.RandomHosts(3, nil)
   331  	if len(randHosts) != 3 {
   332  		t.Error("didn't get 3 hosts")
   333  	}
   334  	if randHosts[0].NetAddress == randHosts[1].NetAddress || randHosts[0].NetAddress == randHosts[2].NetAddress || randHosts[1].NetAddress == randHosts[2].NetAddress {
   335  		t.Error("doubled up")
   336  	}
   337  
   338  	// Grab 4 random hosts. 3 should be returned.
   339  	randHosts = hdb.RandomHosts(4, nil)
   340  	if len(randHosts) != 3 {
   341  		t.Error("didn't get 3 hosts")
   342  	}
   343  	if randHosts[0].NetAddress == randHosts[1].NetAddress || randHosts[0].NetAddress == randHosts[2].NetAddress || randHosts[1].NetAddress == randHosts[2].NetAddress {
   344  		t.Error("doubled up")
   345  	}
   346  
   347  	// Ask for 3 hosts that are not in randHosts. No hosts should be
   348  	// returned.
   349  	uniqueHosts := hdb.RandomHosts(3, []modules.NetAddress{
   350  		randHosts[0].NetAddress,
   351  		randHosts[1].NetAddress,
   352  		randHosts[2].NetAddress,
   353  	})
   354  	if len(uniqueHosts) != 0 {
   355  		t.Error("didn't get 0 hosts")
   356  	}
   357  
   358  	// Ask for 3 hosts, blacklisting non-existent hosts. 3 should be returned.
   359  	randHosts = hdb.RandomHosts(3, []modules.NetAddress{"foo", "bar", "baz"})
   360  	if len(randHosts) != 3 {
   361  		t.Error("didn't get 3 hosts")
   362  	}
   363  	if randHosts[0].NetAddress == randHosts[1].NetAddress || randHosts[0].NetAddress == randHosts[2].NetAddress || randHosts[1].NetAddress == randHosts[2].NetAddress {
   364  		t.Error("doubled up")
   365  	}
   366  
   367  }