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