github.com/NebulousLabs/Sia@v1.3.7/modules/renter/hostdb/hostdb.go (about)

     1  // Package hostdb provides a HostDB object that implements the renter.hostDB
     2  // interface. The blockchain is scanned for host announcements and hosts that
     3  // are found get added to the host database. The database continually scans the
     4  // set of hosts it has found and updates who is online.
     5  package hostdb
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/NebulousLabs/Sia/modules"
    16  	"github.com/NebulousLabs/Sia/modules/renter/hostdb/hosttree"
    17  	"github.com/NebulousLabs/Sia/persist"
    18  	"github.com/NebulousLabs/Sia/types"
    19  	"github.com/NebulousLabs/threadgroup"
    20  )
    21  
    22  var (
    23  	// ErrInitialScanIncomplete is returned whenever an operation is not
    24  	// allowed to be executed before the initial host scan has finished.
    25  	ErrInitialScanIncomplete = errors.New("initial hostdb scan is not yet completed")
    26  	errNilCS                 = errors.New("cannot create hostdb with nil consensus set")
    27  	errNilGateway            = errors.New("cannot create hostdb with nil gateway")
    28  )
    29  
    30  // The HostDB is a database of potential hosts. It assigns a weight to each
    31  // host based on their hosting parameters, and then can select hosts at random
    32  // for uploading files.
    33  type HostDB struct {
    34  	// dependencies
    35  	cs         modules.ConsensusSet
    36  	deps       modules.Dependencies
    37  	gateway    modules.Gateway
    38  	log        *persist.Logger
    39  	mu         sync.RWMutex
    40  	persistDir string
    41  	tg         threadgroup.ThreadGroup
    42  
    43  	// The hostTree is the root node of the tree that organizes hosts by
    44  	// weight. The tree is necessary for selecting weighted hosts at
    45  	// random.
    46  	hostTree *hosttree.HostTree
    47  
    48  	// the scanPool is a set of hosts that need to be scanned. There are a
    49  	// handful of goroutines constantly waiting on the channel for hosts to
    50  	// scan. The scan map is used to prevent duplicates from entering the scan
    51  	// pool.
    52  	initialScanComplete  bool
    53  	initialScanLatencies []time.Duration
    54  	scanList             []modules.HostDBEntry
    55  	scanMap              map[string]struct{}
    56  	scanWait             bool
    57  	scanningThreads      int
    58  
    59  	blockHeight types.BlockHeight
    60  	lastChange  modules.ConsensusChangeID
    61  }
    62  
    63  // New returns a new HostDB.
    64  func New(g modules.Gateway, cs modules.ConsensusSet, persistDir string) (*HostDB, error) {
    65  	// Check for nil inputs.
    66  	if g == nil {
    67  		return nil, errNilGateway
    68  	}
    69  	if cs == nil {
    70  		return nil, errNilCS
    71  	}
    72  	// Create HostDB using production dependencies.
    73  	return NewCustomHostDB(g, cs, persistDir, modules.ProdDependencies)
    74  }
    75  
    76  // NewCustomHostDB creates a HostDB using the provided dependencies. It loads the old
    77  // persistence data, spawns the HostDB's scanning threads, and subscribes it to
    78  // the consensusSet.
    79  func NewCustomHostDB(g modules.Gateway, cs modules.ConsensusSet, persistDir string, deps modules.Dependencies) (*HostDB, error) {
    80  	// Create the HostDB object.
    81  	hdb := &HostDB{
    82  		cs:         cs,
    83  		deps:       deps,
    84  		gateway:    g,
    85  		persistDir: persistDir,
    86  
    87  		scanMap: make(map[string]struct{}),
    88  	}
    89  
    90  	// Create the persist directory if it does not yet exist.
    91  	err := os.MkdirAll(persistDir, 0700)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	// Create the logger.
    97  	logger, err := persist.NewFileLogger(filepath.Join(persistDir, "hostdb.log"))
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	hdb.log = logger
   102  	err = hdb.tg.AfterStop(func() error {
   103  		if err := hdb.log.Close(); err != nil {
   104  			// Resort to println as the logger is in an uncertain state.
   105  			fmt.Println("Failed to close the hostdb logger:", err)
   106  			return err
   107  		}
   108  		return nil
   109  	})
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	// The host tree is used to manage hosts and query them at random.
   115  	hdb.hostTree = hosttree.New(hdb.calculateHostWeight)
   116  
   117  	// Load the prior persistence structures.
   118  	hdb.mu.Lock()
   119  	err = hdb.load()
   120  	hdb.mu.Unlock()
   121  	if err != nil && !os.IsNotExist(err) {
   122  		return nil, err
   123  	}
   124  	err = hdb.tg.AfterStop(func() error {
   125  		hdb.mu.Lock()
   126  		err := hdb.saveSync()
   127  		hdb.mu.Unlock()
   128  		if err != nil {
   129  			hdb.log.Println("Unable to save the hostdb:", err)
   130  			return err
   131  		}
   132  		return nil
   133  	})
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	// Loading is complete, establish the save loop.
   139  	go hdb.threadedSaveLoop()
   140  
   141  	// Don't perform the remaining startup in the presence of a quitAfterLoad
   142  	// disruption.
   143  	if hdb.deps.Disrupt("quitAfterLoad") {
   144  		return hdb, nil
   145  	}
   146  
   147  	// COMPATv1.1.0
   148  	//
   149  	// If the block height has loaded as zero, the most recent consensus change
   150  	// needs to be set to perform a full rescan. This will also help the hostdb
   151  	// to pick up any hosts that it has incorrectly dropped in the past.
   152  	hdb.mu.Lock()
   153  	if hdb.blockHeight == 0 {
   154  		hdb.lastChange = modules.ConsensusChangeBeginning
   155  	}
   156  	hdb.mu.Unlock()
   157  
   158  	err = cs.ConsensusSetSubscribe(hdb, hdb.lastChange, hdb.tg.StopChan())
   159  	if err == modules.ErrInvalidConsensusChangeID {
   160  		// Subscribe again using the new ID. This will cause a triggered scan
   161  		// on all of the hosts, but that should be acceptable.
   162  		hdb.mu.Lock()
   163  		hdb.blockHeight = 0
   164  		hdb.lastChange = modules.ConsensusChangeBeginning
   165  		hdb.mu.Unlock()
   166  		err = cs.ConsensusSetSubscribe(hdb, hdb.lastChange, hdb.tg.StopChan())
   167  	}
   168  	if err != nil {
   169  		return nil, errors.New("hostdb subscription failed: " + err.Error())
   170  	}
   171  	err = hdb.tg.OnStop(func() error {
   172  		cs.Unsubscribe(hdb)
   173  		return nil
   174  	})
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	// Spawn the scan loop during production, but allow it to be disrupted
   180  	// during testing. Primary reason is so that we can fill the hostdb with
   181  	// fake hosts and not have them marked as offline as the scanloop operates.
   182  	if !hdb.deps.Disrupt("disableScanLoop") {
   183  		go hdb.threadedScan()
   184  	} else {
   185  		hdb.initialScanComplete = true
   186  	}
   187  
   188  	return hdb, nil
   189  }
   190  
   191  // ActiveHosts returns a list of hosts that are currently online, sorted by
   192  // weight.
   193  func (hdb *HostDB) ActiveHosts() (activeHosts []modules.HostDBEntry) {
   194  	allHosts := hdb.hostTree.All()
   195  	for _, entry := range allHosts {
   196  		if len(entry.ScanHistory) == 0 {
   197  			continue
   198  		}
   199  		if !entry.ScanHistory[len(entry.ScanHistory)-1].Success {
   200  			continue
   201  		}
   202  		if !entry.AcceptingContracts {
   203  			continue
   204  		}
   205  		activeHosts = append(activeHosts, entry)
   206  	}
   207  	return activeHosts
   208  }
   209  
   210  // AllHosts returns all of the hosts known to the hostdb, including the
   211  // inactive ones.
   212  func (hdb *HostDB) AllHosts() (allHosts []modules.HostDBEntry) {
   213  	return hdb.hostTree.All()
   214  }
   215  
   216  // AverageContractPrice returns the average price of a host.
   217  func (hdb *HostDB) AverageContractPrice() (totalPrice types.Currency) {
   218  	sampleSize := 32
   219  	hosts := hdb.hostTree.SelectRandom(sampleSize, nil)
   220  	if len(hosts) == 0 {
   221  		return totalPrice
   222  	}
   223  	for _, host := range hosts {
   224  		totalPrice = totalPrice.Add(host.ContractPrice)
   225  	}
   226  	return totalPrice.Div64(uint64(len(hosts)))
   227  }
   228  
   229  // Close closes the hostdb, terminating its scanning threads
   230  func (hdb *HostDB) Close() error {
   231  	return hdb.tg.Stop()
   232  }
   233  
   234  // Host returns the HostSettings associated with the specified NetAddress. If
   235  // no matching host is found, Host returns false.
   236  func (hdb *HostDB) Host(spk types.SiaPublicKey) (modules.HostDBEntry, bool) {
   237  	host, exists := hdb.hostTree.Select(spk)
   238  	if !exists {
   239  		return host, exists
   240  	}
   241  	hdb.mu.RLock()
   242  	updateHostHistoricInteractions(&host, hdb.blockHeight)
   243  	hdb.mu.RUnlock()
   244  	return host, exists
   245  }
   246  
   247  // InitialScanComplete returns a boolean indicating if the initial scan of the
   248  // hostdb is completed.
   249  func (hdb *HostDB) InitialScanComplete() (complete bool, err error) {
   250  	if err = hdb.tg.Add(); err != nil {
   251  		return
   252  	}
   253  	defer hdb.tg.Done()
   254  	hdb.mu.Lock()
   255  	defer hdb.mu.Unlock()
   256  	complete = hdb.initialScanComplete
   257  	return
   258  }
   259  
   260  // RandomHosts implements the HostDB interface's RandomHosts() method. It takes
   261  // a number of hosts to return, and a slice of netaddresses to ignore, and
   262  // returns a slice of entries.
   263  func (hdb *HostDB) RandomHosts(n int, excludeKeys []types.SiaPublicKey) ([]modules.HostDBEntry, error) {
   264  	hdb.mu.RLock()
   265  	initialScanComplete := hdb.initialScanComplete
   266  	hdb.mu.RUnlock()
   267  	if !initialScanComplete {
   268  		return []modules.HostDBEntry{}, ErrInitialScanIncomplete
   269  	}
   270  	return hdb.hostTree.SelectRandom(n, excludeKeys), nil
   271  }