github.com/Synthesix/Sia@v1.3.3-0.20180413141344-f863baeed3ca/modules/renter/renter.go (about)

     1  // Package renter is responsible for uploading and downloading files on the sia
     2  // network.
     3  package renter
     4  
     5  // NOTE: Some of the concurrency patterns in the renter are fairly complex. A
     6  // lot of this has been cleaned up, though some shadows of previous demons still
     7  // remain. Be careful when working with anything that involves concurrency.
     8  
     9  // TODO: Allow the 'baseMemory' to be set by the user.
    10  
    11  // TODO: The repair loop currently receives new upload jobs through a channel.
    12  // The download loop has a better model, a heap that can be pushed to and popped
    13  // from concurrently without needing complex channel communication. Migrating
    14  // the renter to this model should clean up some of the places where uploading
    15  // bottlenecks, and reduce the amount of channel-ninjitsu required to make the
    16  // uploading function.
    17  
    18  import (
    19  	"errors"
    20  	"reflect"
    21  	"strings"
    22  	"sync"
    23  
    24  	"github.com/Synthesix/Sia/build"
    25  	"github.com/Synthesix/Sia/modules"
    26  	"github.com/Synthesix/Sia/modules/renter/contractor"
    27  	"github.com/Synthesix/Sia/modules/renter/hostdb"
    28  	"github.com/Synthesix/Sia/persist"
    29  	siasync "github.com/Synthesix/Sia/sync"
    30  	"github.com/Synthesix/Sia/types"
    31  
    32  	"github.com/NebulousLabs/threadgroup"
    33  )
    34  
    35  var (
    36  	errNilContractor = errors.New("cannot create renter with nil contractor")
    37  	errNilCS         = errors.New("cannot create renter with nil consensus set")
    38  	errNilGateway    = errors.New("cannot create hostdb with nil gateway")
    39  	errNilHdb        = errors.New("cannot create renter with nil hostdb")
    40  	errNilTpool      = errors.New("cannot create renter with nil transaction pool")
    41  )
    42  
    43  var (
    44  	// priceEstimationScope is the number of hosts that get queried by the
    45  	// renter when providing price estimates. Especially for the 'Standard'
    46  	// variable, there should be congruence with the number of contracts being
    47  	// used in the renter allowance.
    48  	priceEstimationScope = build.Select(build.Var{
    49  		Standard: int(50),
    50  		Dev:      int(12),
    51  		Testing:  int(4),
    52  	}).(int)
    53  )
    54  
    55  // A hostDB is a database of hosts that the renter can use for figuring out who
    56  // to upload to, and download from.
    57  type hostDB interface {
    58  	// ActiveHosts returns the list of hosts that are actively being selected
    59  	// from.
    60  	ActiveHosts() []modules.HostDBEntry
    61  
    62  	// AllHosts returns the full list of hosts known to the hostdb, sorted in
    63  	// order of preference.
    64  	AllHosts() []modules.HostDBEntry
    65  
    66  	// AverageContractPrice returns the average contract price of a host.
    67  	AverageContractPrice() types.Currency
    68  
    69  	// Close closes the hostdb.
    70  	Close() error
    71  
    72  	// Host returns the HostDBEntry for a given host.
    73  	Host(types.SiaPublicKey) (modules.HostDBEntry, bool)
    74  
    75  	// RandomHosts returns a set of random hosts, weighted by their estimated
    76  	// usefulness / attractiveness to the renter. RandomHosts will not return
    77  	// any offline or inactive hosts.
    78  	RandomHosts(int, []types.SiaPublicKey) []modules.HostDBEntry
    79  
    80  	// ScoreBreakdown returns a detailed explanation of the various properties
    81  	// of the host.
    82  	ScoreBreakdown(modules.HostDBEntry) modules.HostScoreBreakdown
    83  
    84  	// EstimateHostScore returns the estimated score breakdown of a host with the
    85  	// provided settings.
    86  	EstimateHostScore(modules.HostDBEntry) modules.HostScoreBreakdown
    87  }
    88  
    89  // A hostContractor negotiates, revises, renews, and provides access to file
    90  // contracts.
    91  type hostContractor interface {
    92  	// SetAllowance sets the amount of money the contractor is allowed to
    93  	// spend on contracts over a given time period, divided among the number
    94  	// of hosts specified. Note that contractor can start forming contracts as
    95  	// soon as SetAllowance is called; that is, it may block.
    96  	SetAllowance(modules.Allowance) error
    97  
    98  	// Allowance returns the current allowance
    99  	Allowance() modules.Allowance
   100  
   101  	// Close closes the hostContractor.
   102  	Close() error
   103  
   104  	// Contracts returns the contracts formed by the contractor.
   105  	Contracts() []modules.RenterContract
   106  
   107  	// ContractByID returns the contract associated with the file contract id.
   108  	ContractByID(types.FileContractID) (modules.RenterContract, bool)
   109  
   110  	// ContractUtility returns the utility field for a given contract, along
   111  	// with a bool indicating if it exists.
   112  	ContractUtility(types.FileContractID) (modules.ContractUtility, bool)
   113  
   114  	// CurrentPeriod returns the height at which the current allowance period
   115  	// began.
   116  	CurrentPeriod() types.BlockHeight
   117  
   118  	// PeriodSpending returns the amount spent on contracts during the current
   119  	// billing period.
   120  	PeriodSpending() modules.ContractorSpending
   121  
   122  	// Editor creates an Editor from the specified contract ID, allowing the
   123  	// insertion, deletion, and modification of sectors.
   124  	Editor(types.FileContractID, <-chan struct{}) (contractor.Editor, error)
   125  
   126  	// IsOffline reports whether the specified host is considered offline.
   127  	IsOffline(types.FileContractID) bool
   128  
   129  	// Downloader creates a Downloader from the specified contract ID,
   130  	// allowing the retrieval of sectors.
   131  	Downloader(types.FileContractID, <-chan struct{}) (contractor.Downloader, error)
   132  
   133  	// ResolveID returns the most recent renewal of the specified ID.
   134  	ResolveID(types.FileContractID) types.FileContractID
   135  
   136  	// SetRateLimits sets the bandwidth limits for connections created by the
   137  	// contractor and its submodules.
   138  	SetRateLimits(int64, int64, uint64)
   139  }
   140  
   141  // A trackedFile contains metadata about files being tracked by the Renter.
   142  // Tracked files are actively repaired by the Renter. By default, files
   143  // uploaded by the user are tracked, and files that are added (via loading a
   144  // .sia file) are not.
   145  type trackedFile struct {
   146  	// location of original file on disk
   147  	RepairPath string
   148  }
   149  
   150  // A Renter is responsible for tracking all of the files that a user has
   151  // uploaded to Sia, as well as the locations and health of these files.
   152  //
   153  // TODO: Separate the workerPool to have its own mutex. The workerPool doesn't
   154  // interfere with any of the other fields in the renter, should be fine for it
   155  // to have a separate mutex, that way operations on the worker pool don't block
   156  // operations on other parts of the struct. If we're going to do it that way,
   157  // might make sense to split the worker pool off into it's own struct entirely
   158  // the same way that we split of the memoryManager entirely.
   159  type Renter struct {
   160  	// File management.
   161  	//
   162  	// tracking contains a list of files that the user intends to maintain. By
   163  	// default, files loaded through sharing are not maintained by the user.
   164  	files    map[string]*file
   165  	tracking map[string]trackedFile // Map from nickname to metadata.
   166  
   167  	// Download management. The heap has a separate mutex because it is always
   168  	// accessed in isolation.
   169  	downloadHeapMu sync.Mutex         // Used to protect the downloadHeap.
   170  	downloadHeap   *downloadChunkHeap // A heap of priority-sorted chunks to download.
   171  	newDownloads   chan struct{}      // Used to notify download loop that new downloads are available.
   172  
   173  	// Download history. The history list has its own mutex because it is always
   174  	// accessed in isolation.
   175  	//
   176  	// TODO: Currently the download history doesn't include repair-initiated
   177  	// downloads, and instead only contains user-initiated downlods.
   178  	downloadHistory   []*download
   179  	downloadHistoryMu sync.Mutex
   180  
   181  	// Upload management.
   182  	uploadHeap uploadHeap
   183  
   184  	// List of workers that can be used for uploading and/or downloading.
   185  	memoryManager *memoryManager
   186  	workerPool    map[types.FileContractID]*worker
   187  
   188  	// Cache the last price estimation result.
   189  	lastEstimation modules.RenterPriceEstimation
   190  
   191  	// Utilities.
   192  	chunkCache     map[string][]byte
   193  	cmu            *sync.Mutex
   194  	cs             modules.ConsensusSet
   195  	deps           modules.Dependencies
   196  	g              modules.Gateway
   197  	hostContractor hostContractor
   198  	hostDB         hostDB
   199  	log            *persist.Logger
   200  	persistDir     string
   201  	mu             *siasync.RWMutex
   202  	tg             threadgroup.ThreadGroup
   203  	tpool          modules.TransactionPool
   204  }
   205  
   206  // Close closes the Renter and its dependencies
   207  func (r *Renter) Close() error {
   208  	r.tg.Stop()
   209  	r.hostDB.Close()
   210  	return r.hostContractor.Close()
   211  }
   212  
   213  // PriceEstimation estimates the cost in siacoins of performing various storage
   214  // and data operations.
   215  //
   216  // TODO: Make this function line up with the actual settings in the renter.
   217  // Perhaps even make it so it uses the renter's actual contracts if it has any.
   218  func (r *Renter) PriceEstimation() modules.RenterPriceEstimation {
   219  	id := r.mu.RLock()
   220  	lastEstimation := r.lastEstimation
   221  	r.mu.RUnlock(id)
   222  	if !reflect.DeepEqual(lastEstimation, modules.RenterPriceEstimation{}) {
   223  		return lastEstimation
   224  	}
   225  
   226  	// Grab hosts to perform the estimation.
   227  	hosts := r.hostDB.RandomHosts(priceEstimationScope, nil)
   228  
   229  	// Check if there are zero hosts, which means no estimation can be made.
   230  	if len(hosts) == 0 {
   231  		return modules.RenterPriceEstimation{}
   232  	}
   233  
   234  	// Add up the costs for each host.
   235  	var totalContractCost types.Currency
   236  	var totalDownloadCost types.Currency
   237  	var totalStorageCost types.Currency
   238  	var totalUploadCost types.Currency
   239  	for _, host := range hosts {
   240  		totalContractCost = totalContractCost.Add(host.ContractPrice)
   241  		totalDownloadCost = totalDownloadCost.Add(host.DownloadBandwidthPrice)
   242  		totalStorageCost = totalStorageCost.Add(host.StoragePrice)
   243  		totalUploadCost = totalUploadCost.Add(host.UploadBandwidthPrice)
   244  	}
   245  
   246  	// Convert values to being human-scale.
   247  	totalDownloadCost = totalDownloadCost.Mul(modules.BytesPerTerabyte)
   248  	totalStorageCost = totalStorageCost.Mul(modules.BlockBytesPerMonthTerabyte)
   249  	totalUploadCost = totalUploadCost.Mul(modules.BytesPerTerabyte)
   250  
   251  	// Factor in redundancy.
   252  	totalStorageCost = totalStorageCost.Mul64(3) // TODO: follow file settings?
   253  	totalUploadCost = totalUploadCost.Mul64(3)   // TODO: follow file settings?
   254  
   255  	// Perform averages.
   256  	totalContractCost = totalContractCost.Div64(uint64(len(hosts)))
   257  	totalDownloadCost = totalDownloadCost.Div64(uint64(len(hosts)))
   258  	totalStorageCost = totalStorageCost.Div64(uint64(len(hosts)))
   259  	totalUploadCost = totalUploadCost.Div64(uint64(len(hosts)))
   260  
   261  	// Take the average of the host set to estimate the overall cost of the
   262  	// contract forming.
   263  	totalContractCost = totalContractCost.Mul64(uint64(priceEstimationScope))
   264  
   265  	// Add the cost of paying the transaction fees for the first contract.
   266  	_, feePerByte := r.tpool.FeeEstimation()
   267  	totalContractCost = totalContractCost.Add(feePerByte.Mul64(1000).Mul64(uint64(priceEstimationScope)))
   268  
   269  	est := modules.RenterPriceEstimation{
   270  		FormContracts:        totalContractCost,
   271  		DownloadTerabyte:     totalDownloadCost,
   272  		StorageTerabyteMonth: totalStorageCost,
   273  		UploadTerabyte:       totalUploadCost,
   274  	}
   275  
   276  	id = r.mu.Lock()
   277  	r.lastEstimation = est
   278  	r.mu.Unlock(id)
   279  
   280  	return est
   281  }
   282  
   283  // SetSettings will update the settings for the renter.
   284  func (r *Renter) SetSettings(s modules.RenterSettings) error {
   285  	// Set allowance.
   286  	err := r.hostContractor.SetAllowance(s.Allowance)
   287  	if err != nil {
   288  		return err
   289  	}
   290  	// Set ratelimit
   291  	if s.MaxDownloadSpeed < 0 || s.MaxUploadSpeed < 0 {
   292  		return errors.New("download/upload rate limit can't be below 0")
   293  	}
   294  	if s.MaxDownloadSpeed == 0 && s.MaxUploadSpeed == 0 {
   295  		r.hostContractor.SetRateLimits(0, 0, 0)
   296  	} else {
   297  		// TODO: In the future we might want the user to be able to configure
   298  		// the packetSize using the API. For now the sane default is 16kib if
   299  		// the user wants to limit the connection.
   300  		r.hostContractor.SetRateLimits(s.MaxDownloadSpeed, s.MaxUploadSpeed, 4*4096)
   301  	}
   302  
   303  	r.managedUpdateWorkerPool()
   304  	return nil
   305  }
   306  
   307  // ActiveHosts returns an array of hostDB's active hosts
   308  func (r *Renter) ActiveHosts() []modules.HostDBEntry { return r.hostDB.ActiveHosts() }
   309  
   310  // AllHosts returns an array of all hosts
   311  func (r *Renter) AllHosts() []modules.HostDBEntry { return r.hostDB.AllHosts() }
   312  
   313  // Host returns the host associated with the given public key
   314  func (r *Renter) Host(spk types.SiaPublicKey) (modules.HostDBEntry, bool) { return r.hostDB.Host(spk) }
   315  
   316  // ScoreBreakdown returns the score breakdown
   317  func (r *Renter) ScoreBreakdown(e modules.HostDBEntry) modules.HostScoreBreakdown {
   318  	return r.hostDB.ScoreBreakdown(e)
   319  }
   320  
   321  // EstimateHostScore returns the estimated host score
   322  func (r *Renter) EstimateHostScore(e modules.HostDBEntry) modules.HostScoreBreakdown {
   323  	return r.hostDB.EstimateHostScore(e)
   324  }
   325  
   326  // Contracts returns an array of host contractor's contracts
   327  func (r *Renter) Contracts() []modules.RenterContract { return r.hostContractor.Contracts() }
   328  
   329  // CurrentPeriod returns the host contractor's current period
   330  func (r *Renter) CurrentPeriod() types.BlockHeight { return r.hostContractor.CurrentPeriod() }
   331  
   332  // ContractUtility returns the utility field for a given contract, along
   333  // with a bool indicating if it exists.
   334  func (r *Renter) ContractUtility(id types.FileContractID) (modules.ContractUtility, bool) {
   335  	return r.hostContractor.ContractUtility(id)
   336  }
   337  
   338  // PeriodSpending returns the host contractor's period spending
   339  func (r *Renter) PeriodSpending() modules.ContractorSpending { return r.hostContractor.PeriodSpending() }
   340  
   341  // Settings returns the host contractor's allowance
   342  func (r *Renter) Settings() modules.RenterSettings {
   343  	return modules.RenterSettings{
   344  		Allowance: r.hostContractor.Allowance(),
   345  	}
   346  }
   347  
   348  // ProcessConsensusChange returns the process consensus change
   349  func (r *Renter) ProcessConsensusChange(cc modules.ConsensusChange) {
   350  	id := r.mu.Lock()
   351  	r.lastEstimation = modules.RenterPriceEstimation{}
   352  	r.mu.Unlock(id)
   353  }
   354  
   355  // validateSiapath checks that a Siapath is a legal filename.
   356  // ../ is disallowed to prevent directory traversal, and paths must not begin
   357  // with / or be empty.
   358  func validateSiapath(siapath string) error {
   359  	if siapath == "" {
   360  		return ErrEmptyFilename
   361  	}
   362  	if siapath == ".." {
   363  		return errors.New("siapath cannot be ..")
   364  	}
   365  	if siapath == "." {
   366  		return errors.New("siapath cannot be .")
   367  	}
   368  	// check prefix
   369  	if strings.HasPrefix(siapath, "/") {
   370  		return errors.New("siapath cannot begin with /")
   371  	}
   372  	if strings.HasPrefix(siapath, "../") {
   373  		return errors.New("siapath cannot begin with ../")
   374  	}
   375  	if strings.HasPrefix(siapath, "./") {
   376  		return errors.New("siapath connot begin with ./")
   377  	}
   378  	for _, pathElem := range strings.Split(siapath, "/") {
   379  		if pathElem == "." || pathElem == ".." {
   380  			return errors.New("siapath cannot contain . or .. elements")
   381  		}
   382  	}
   383  	return nil
   384  }
   385  
   386  // Enforce that Renter satisfies the modules.Renter interface.
   387  var _ modules.Renter = (*Renter)(nil)
   388  
   389  // NewCustomRenter initializes a renter and returns it.
   390  func NewCustomRenter(g modules.Gateway, cs modules.ConsensusSet, tpool modules.TransactionPool, hdb hostDB, hc hostContractor, persistDir string, deps modules.Dependencies) (*Renter, error) {
   391  	if g == nil {
   392  		return nil, errNilGateway
   393  	}
   394  	if cs == nil {
   395  		return nil, errNilCS
   396  	}
   397  	if tpool == nil {
   398  		return nil, errNilTpool
   399  	}
   400  	if hc == nil {
   401  		return nil, errNilContractor
   402  	}
   403  	if hdb == nil && build.Release != "testing" {
   404  		return nil, errNilHdb
   405  	}
   406  
   407  	r := &Renter{
   408  		files:    make(map[string]*file),
   409  		tracking: make(map[string]trackedFile),
   410  
   411  		// Making newDownloads a buffered channel means that most of the time, a
   412  		// new download will trigger an unnecessary extra iteration of the
   413  		// download heap loop, searching for a chunk that's not there. This is
   414  		// preferable to the alternative, where in rare cases the download heap
   415  		// will miss work altogether.
   416  		newDownloads: make(chan struct{}, 1),
   417  		downloadHeap: new(downloadChunkHeap),
   418  
   419  		uploadHeap: uploadHeap{
   420  			activeChunks: make(map[uploadChunkID]struct{}),
   421  			newUploads:   make(chan struct{}, 1),
   422  		},
   423  
   424  		workerPool: make(map[types.FileContractID]*worker),
   425  
   426  		chunkCache:     make(map[string][]byte),
   427  		cmu:            new(sync.Mutex),
   428  		cs:             cs,
   429  		deps:           deps,
   430  		g:              g,
   431  		hostDB:         hdb,
   432  		hostContractor: hc,
   433  		persistDir:     persistDir,
   434  		mu:             siasync.New(modules.SafeMutexDelay, 1),
   435  		tpool:          tpool,
   436  	}
   437  	r.memoryManager = newMemoryManager(defaultMemory, r.tg.StopChan())
   438  
   439  	// Load all saved data.
   440  	if err := r.initPersist(); err != nil {
   441  		return nil, err
   442  	}
   443  
   444  	// Subscribe to the consensus set.
   445  	err := cs.ConsensusSetSubscribe(r, modules.ConsensusChangeRecent, r.tg.StopChan())
   446  	if err != nil {
   447  		return nil, err
   448  	}
   449  
   450  	// Spin up the workers for the work pool.
   451  	r.managedUpdateWorkerPool()
   452  	go r.threadedDownloadLoop()
   453  	go r.threadedUploadLoop()
   454  
   455  	// Kill workers on shutdown.
   456  	r.tg.OnStop(func() error {
   457  		id := r.mu.RLock()
   458  		for _, worker := range r.workerPool {
   459  			close(worker.killChan)
   460  		}
   461  		r.mu.RUnlock(id)
   462  		return nil
   463  	})
   464  
   465  	return r, nil
   466  }
   467  
   468  // New returns an initialized renter.
   469  func New(g modules.Gateway, cs modules.ConsensusSet, wallet modules.Wallet, tpool modules.TransactionPool, persistDir string) (*Renter, error) {
   470  	hdb, err := hostdb.New(g, cs, persistDir)
   471  	if err != nil {
   472  		return nil, err
   473  	}
   474  	hc, err := contractor.New(cs, wallet, tpool, hdb, persistDir)
   475  	if err != nil {
   476  		return nil, err
   477  	}
   478  
   479  	return NewCustomRenter(g, cs, tpool, hdb, hc, persistDir, modules.ProdDependencies)
   480  }