gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/download.go (about)

     1  package renter
     2  
     3  import (
     4  	"encoding/hex"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"os"
     9  	"path/filepath"
    10  	"sync"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	"gitlab.com/NebulousLabs/fastrand"
    15  
    16  	"gitlab.com/NebulousLabs/errors"
    17  
    18  	"gitlab.com/SkynetLabs/skyd/build"
    19  	"gitlab.com/SkynetLabs/skyd/skymodules"
    20  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/filesystem/siafile"
    21  )
    22  
    23  type (
    24  	// A download is a file download that has been queued by the renter.
    25  	download struct {
    26  		// Data progress variables.
    27  		atomicDataReceived         uint64 // Incremented as data completes, will stop at 100% file progress.
    28  		atomicTotalDataTransferred uint64 // Incremented as data arrives, includes overdrive, contract negotiation, etc.
    29  
    30  		// Other progress variables.
    31  		chunksRemaining uint64        // Number of chunks whose downloads are incomplete.
    32  		completeChan    chan struct{} // Closed once the download is complete.
    33  		err             error         // Only set if there was an error which prevented the download from completing.
    34  
    35  		// downloadCompleteFunc is a slice of functions which are called when
    36  		// completeChan is closed.
    37  		downloadCompleteFuncs []func(error) error
    38  
    39  		// Timestamp information.
    40  		endTime         time.Time // Set immediately before closing 'completeChan'.
    41  		staticStartTime time.Time // Set immediately when the download object is created.
    42  
    43  		// Basic information about the file/download.
    44  		destination           downloadDestination
    45  		destinationString     string                // The string reported to the user to indicate the download's destination.
    46  		staticDestinationType string                // "memory buffer", "http stream", "file", etc.
    47  		staticLength          uint64                // Length to download starting from the offset.
    48  		staticOffset          uint64                // Offset within the file to start the download.
    49  		staticSiaPath         skymodules.SiaPath    // The path of the siafile at the time the download started.
    50  		staticUID             skymodules.DownloadID // unique identifier for the download
    51  
    52  		staticParams downloadParams
    53  
    54  		// Retrieval settings for the file.
    55  		staticLatencyTarget time.Duration // In milliseconds. Lower latency results in lower total system throughput.
    56  		staticOverdrive     int           // How many extra pieces to download to prevent slow hosts from being a bottleneck.
    57  		staticPriority      uint64        // Downloads with higher priority will complete first.
    58  
    59  		// Utilities.
    60  		staticRenter *Renter    // The renter that was used to create the download.
    61  		mu           sync.Mutex // Unique to the download object.
    62  	}
    63  
    64  	// downloadParams is the set of parameters to use when downloading a file.
    65  	downloadParams struct {
    66  		destination       downloadDestination // The place to write the downloaded data.
    67  		destinationType   string              // "file", "buffer", "http stream", etc.
    68  		destinationString string              // The string to report to the user for the destination.
    69  		disableLocalFetch bool                // Whether or not the file can be fetched from disk if available.
    70  		file              *siafile.Snapshot   // The file to download.
    71  		latencyTarget     time.Duration       // Workers above this latency will be automatically put on standby initially.
    72  		length            uint64              // Length of download. Cannot be 0.
    73  		needsMemory       bool                // Whether new memory needs to be allocated to perform the download.
    74  		offset            uint64              // Offset within the file to start the download. Must be less than the total filesize.
    75  		overdrive         int                 // How many extra pieces to download to prevent slow hosts from being a bottleneck.
    76  		priority          uint64              // Files with a higher priority will be downloaded first.
    77  
    78  		staticMemoryManager *memoryManager
    79  
    80  		// staticSpendingCategory specifies what field to update when we track
    81  		// the amount of money spent from an ephemeral account
    82  		staticSpendingCategory spendingCategory
    83  	}
    84  )
    85  
    86  // managedCancel cancels a download by marking it as failed.
    87  func (d *download) managedCancel() {
    88  	d.managedFail(skymodules.ErrDownloadCancelled)
    89  }
    90  
    91  // managedFail will mark the download as complete, but with the provided error.
    92  // If the download has already failed, the error will be updated to be a
    93  // concatenation of the previous error and the new error.
    94  func (d *download) managedFail(err error) {
    95  	d.mu.Lock()
    96  	defer d.mu.Unlock()
    97  
    98  	// If the download is already complete, extend the error.
    99  	complete := d.staticComplete()
   100  	if complete && d.err != nil {
   101  		return
   102  	} else if complete && d.err == nil {
   103  		d.staticRenter.staticLog.Critical("download is marked as completed without error, but then managedFail was called with err:", err)
   104  		return
   105  	}
   106  
   107  	// Mark the download as complete and set the error.
   108  	d.err = err
   109  	d.markComplete()
   110  }
   111  
   112  // markComplete is a helper method which closes the completeChan and and
   113  // executes the downloadCompleteFuncs. The completeChan should always be closed
   114  // using this method.
   115  func (d *download) markComplete() {
   116  	// Avoid calling markComplete multiple times. In a production build
   117  	// build.Critical won't panic which is fine since we set
   118  	// downloadCompleteFunc to nil after executing them. We still don't want to
   119  	// close the completeChan again though to avoid a crash.
   120  	if d.staticComplete() {
   121  		build.Critical("Can't call markComplete multiple times")
   122  	} else {
   123  		defer close(d.completeChan)
   124  	}
   125  	// Execute the downloadCompleteFuncs before closing the channel. This gives
   126  	// the initiator of the download the nice guarantee that waiting for the
   127  	// completeChan to be closed also means that the downloadCompleteFuncs are
   128  	// done.
   129  	var err error
   130  	for _, f := range d.downloadCompleteFuncs {
   131  		err = errors.Compose(err, f(d.err))
   132  	}
   133  	// Log potential errors.
   134  	if err != nil {
   135  		d.staticRenter.staticLog.Println("Failed to execute at least one downloadCompleteFunc", err)
   136  	}
   137  	// Set downloadCompleteFuncs to nil to avoid executing them multiple times.
   138  	d.downloadCompleteFuncs = nil
   139  }
   140  
   141  // onComplete registers a function to be called when the download is completed.
   142  // This can either mean that the download succeeded or failed. The registered
   143  // functions are executed in the same order as they are registered and waiting
   144  // for the download's completeChan to be closed implies that the registered
   145  // functions were executed.
   146  func (d *download) onComplete(f func(error) error) {
   147  	select {
   148  	case <-d.completeChan:
   149  		if err := f(d.err); err != nil {
   150  			d.staticRenter.staticLog.Println("Failed to execute downloadCompleteFunc", err)
   151  		}
   152  		return
   153  	default:
   154  	}
   155  	d.downloadCompleteFuncs = append(d.downloadCompleteFuncs, f)
   156  }
   157  
   158  // staticComplete is a helper function to indicate whether or not the download
   159  // has completed.
   160  func (d *download) staticComplete() bool {
   161  	select {
   162  	case <-d.completeChan:
   163  		return true
   164  	default:
   165  		return false
   166  	}
   167  }
   168  
   169  // Err returns the error encountered by a download, if it exists.
   170  func (d *download) Err() (err error) {
   171  	d.mu.Lock()
   172  	err = d.err
   173  	d.mu.Unlock()
   174  	return err
   175  }
   176  
   177  // OnComplete registers a function to be called when the download is completed.
   178  // This can either mean that the download succeeded or failed. The registered
   179  // functions are executed in the same order as they are registered and waiting
   180  // for the download's completeChan to be closed implies that the registered
   181  // functions were executed.
   182  func (d *download) OnComplete(f func(error) error) {
   183  	d.mu.Lock()
   184  	defer d.mu.Unlock()
   185  	d.onComplete(f)
   186  }
   187  
   188  // UID returns the unique identifier of the download.
   189  func (d *download) UID() skymodules.DownloadID {
   190  	return d.staticUID
   191  }
   192  
   193  // Download creates a file download using the passed parameters and blocks until
   194  // the download is finished. The download needs to be started by calling the
   195  // returned method.
   196  func (r *Renter) Download(p skymodules.RenterDownloadParameters) (skymodules.DownloadID, func() error, error) {
   197  	if err := r.tg.Add(); err != nil {
   198  		return "", nil, err
   199  	}
   200  	defer r.tg.Done()
   201  	d, err := r.managedDownload(p)
   202  	if err != nil {
   203  		return "", nil, err
   204  	}
   205  	return d.UID(), func() error {
   206  		// Start download.
   207  		if err := d.Start(); err != nil {
   208  			return err
   209  		}
   210  		// Block until the download has completed
   211  		select {
   212  		case <-d.completeChan:
   213  			return d.Err()
   214  		case <-r.tg.StopChan():
   215  			return errors.New("download interrupted by shutdown")
   216  		}
   217  	}, nil
   218  }
   219  
   220  // DownloadAsync creates a file download using the passed parameters without
   221  // blocking until the download is finished. The download needs to be started
   222  // using the method returned by DownloadAsync. DownloadAsync also accepts an
   223  // optional input function which will be registered to be called when the
   224  // download is finished.
   225  func (r *Renter) DownloadAsync(p skymodules.RenterDownloadParameters, f func(error) error) (id skymodules.DownloadID, start func() error, cancel func(), err error) {
   226  	if err := r.tg.Add(); err != nil {
   227  		return "", nil, nil, err
   228  	}
   229  	defer r.tg.Done()
   230  	d, err := r.managedDownload(p)
   231  	if err != nil {
   232  		return "", nil, nil, err
   233  	}
   234  	if f != nil {
   235  		d.onComplete(f)
   236  	}
   237  	return d.UID(), func() error {
   238  		return d.Start()
   239  	}, d.managedCancel, nil
   240  }
   241  
   242  // managedDownload performs a file download using the passed parameters and
   243  // returns the download object and an error that indicates if the download
   244  // setup was successful.
   245  func (r *Renter) managedDownload(p skymodules.RenterDownloadParameters) (_ *download, err error) {
   246  	// Lookup the file associated with the nickname.
   247  	entry, err := r.staticFileSystem.OpenSiaFile(p.SiaPath)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	defer func() {
   252  		err = errors.Compose(err, entry.UpdateAccessTime())
   253  		err = errors.Compose(err, entry.Close())
   254  	}()
   255  
   256  	// Validate download parameters.
   257  	isHTTPResp := p.Httpwriter != nil
   258  	if p.Async && isHTTPResp {
   259  		return nil, errors.New("cannot async download to http response")
   260  	}
   261  	if isHTTPResp && p.Destination != "" {
   262  		return nil, errors.New("destination cannot be specified when downloading to http response")
   263  	}
   264  	if !isHTTPResp && p.Destination == "" {
   265  		return nil, errors.New("destination not supplied")
   266  	}
   267  	if p.Destination != "" && !filepath.IsAbs(p.Destination) {
   268  		return nil, errors.New("destination must be an absolute path")
   269  	}
   270  	if p.Offset == entry.Size() && entry.Size() != 0 {
   271  		return nil, errors.New("offset equals filesize")
   272  	}
   273  	// Sentinel: if length == 0, download the entire file.
   274  	if p.Length == 0 {
   275  		if p.Offset > entry.Size() {
   276  			return nil, errors.New("offset cannot be greater than file size")
   277  		}
   278  		p.Length = entry.Size() - p.Offset
   279  	}
   280  	// Check whether offset and length is valid.
   281  	if p.Offset < 0 || p.Offset+p.Length > entry.Size() {
   282  		return nil, fmt.Errorf("offset and length combination invalid, max byte is at index %d", entry.Size()-1)
   283  	}
   284  
   285  	// Instantiate the correct downloadWriter implementation.
   286  	var dw downloadDestination
   287  	var destinationType string
   288  	if isHTTPResp {
   289  		dw = newDownloadDestinationWriter(p.Httpwriter)
   290  		destinationType = "http stream"
   291  	} else {
   292  		osFile, err := os.OpenFile(p.Destination, os.O_CREATE|os.O_WRONLY, entry.Mode())
   293  		if err != nil {
   294  			return nil, err
   295  		}
   296  		dw = &downloadDestinationFile{
   297  			deps:            r.staticDeps,
   298  			f:               osFile,
   299  			staticChunkSize: int64(entry.ChunkSize()),
   300  		}
   301  		destinationType = "file"
   302  	}
   303  
   304  	// If the destination is a httpWriter, we set the Content-Length in the
   305  	// header.
   306  	if isHTTPResp {
   307  		w, ok := p.Httpwriter.(http.ResponseWriter)
   308  		if ok {
   309  			w.Header().Set("Content-Length", fmt.Sprint(p.Length))
   310  		}
   311  	}
   312  
   313  	// Prepare snapshot.
   314  	snap, err := entry.SnapshotRange(p.SiaPath, p.Offset, p.Length)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	// Create the download object.
   319  	d, err := r.managedNewDownload(downloadParams{
   320  		destination:       dw,
   321  		destinationType:   destinationType,
   322  		destinationString: p.Destination,
   323  		disableLocalFetch: p.DisableDiskFetch,
   324  		file:              snap,
   325  
   326  		latencyTarget: 25e3 * time.Millisecond, // TODO: high default until full latency support is added.
   327  		length:        p.Length,
   328  		needsMemory:   true,
   329  		offset:        p.Offset,
   330  		overdrive:     3, // TODO: moderate default until full overdrive support is added.
   331  		priority:      5, // TODO: moderate default until full priority support is added.
   332  
   333  		staticMemoryManager:    r.staticUserDownloadMemoryManager, // user initiated download
   334  		staticSpendingCategory: categoryDownload,
   335  	})
   336  	if closer, ok := dw.(io.Closer); err != nil && ok {
   337  		// If the destination can be closed we do so.
   338  		return nil, errors.Compose(err, closer.Close())
   339  	} else if err != nil {
   340  		return nil, err
   341  	}
   342  
   343  	// Register some cleanup for when the download is done.
   344  	d.OnComplete(func(_ error) error {
   345  		// close the destination if possible.
   346  		if closer, ok := dw.(io.Closer); ok {
   347  			return closer.Close()
   348  		}
   349  		// sanity check that we close files.
   350  		if destinationType == "file" {
   351  			build.Critical("file wasn't closed after download")
   352  		}
   353  		return nil
   354  	})
   355  
   356  	// Add the download object to the download history if it's not a stream.
   357  	if destinationType != destinationTypeSeekStream {
   358  		r.staticDownloadHistory.callAddDownload(d)
   359  	}
   360  
   361  	// Return the download object
   362  	return d, nil
   363  }
   364  
   365  // managedNewDownload creates and initializes a download based on the provided
   366  // parameters.
   367  func (r *Renter) managedNewDownload(params downloadParams) (*download, error) {
   368  	// Input validation.
   369  	if params.file == nil {
   370  		return nil, errors.New("no file provided when requesting download")
   371  	}
   372  	if params.length < 0 {
   373  		return nil, errors.New("download length must be zero or a positive whole number")
   374  	}
   375  	if params.offset < 0 {
   376  		return nil, errors.New("download offset cannot be a negative number")
   377  	}
   378  	if params.offset+params.length > params.file.Size() {
   379  		return nil, errors.New("download is requesting data past the boundary of the file")
   380  	}
   381  
   382  	// Create the download object.
   383  	d := &download{
   384  		completeChan: make(chan struct{}),
   385  
   386  		staticStartTime: time.Now(),
   387  
   388  		destination:           params.destination,
   389  		destinationString:     params.destinationString,
   390  		staticDestinationType: params.destinationType,
   391  		staticUID:             skymodules.DownloadID(hex.EncodeToString(fastrand.Bytes(16))),
   392  		staticLatencyTarget:   params.latencyTarget,
   393  		staticLength:          params.length,
   394  		staticOffset:          params.offset,
   395  		staticOverdrive:       params.overdrive,
   396  		staticSiaPath:         params.file.SiaPath(),
   397  		staticPriority:        params.priority,
   398  
   399  		staticRenter: r,
   400  		staticParams: params,
   401  	}
   402  
   403  	// Update the endTime of the download when it's done. Also nil out the
   404  	// destination pointer so that the garbage collector does not think any
   405  	// memory is still being used.
   406  	d.onComplete(func(_ error) error {
   407  		d.endTime = time.Now()
   408  		d.destination = nil
   409  		d.staticParams.file = nil
   410  		return nil
   411  	})
   412  
   413  	return d, nil
   414  }
   415  
   416  // Start starts a download previously created with `managedNewDownload`.
   417  func (d *download) Start() error {
   418  	// Nothing more to do for 0-byte files or 0-length downloads.
   419  	if d.staticLength == 0 {
   420  		d.mu.Lock()
   421  		d.markComplete()
   422  		d.mu.Unlock()
   423  		return nil
   424  	}
   425  
   426  	// Determine which chunks to download.
   427  	params := d.staticParams
   428  	minChunk, minChunkOffset := params.file.ChunkIndexByOffset(params.offset)
   429  	maxChunk, maxChunkOffset := params.file.ChunkIndexByOffset(params.offset + params.length)
   430  
   431  	// If the maxChunkOffset is exactly 0 we need to subtract 1 chunk. e.g. if
   432  	// the chunkSize is 100 bytes and we want to download 100 bytes from offset
   433  	// 0, maxChunk would be 1 and maxChunkOffset would be 0. We want maxChunk
   434  	// to be 0 though since we don't actually need any data from chunk 1.
   435  	if maxChunk > 0 && maxChunkOffset == 0 {
   436  		maxChunk--
   437  	}
   438  	// Make sure the requested chunks are within the boundaries.
   439  	if minChunk == params.file.NumChunks() || maxChunk == params.file.NumChunks() {
   440  		return errors.New("download is requesting a chunk that is past the boundary of the file")
   441  	}
   442  
   443  	// For each chunk, assemble a mapping from the contract id to the index of
   444  	// the piece within the chunk that the contract is responsible for.
   445  	chunkMaps := make([]map[string]downloadPieceInfo, maxChunk-minChunk+1)
   446  	for chunkIndex := minChunk; chunkIndex <= maxChunk; chunkIndex++ {
   447  		// Create the map.
   448  		chunkMaps[chunkIndex-minChunk] = make(map[string]downloadPieceInfo)
   449  		// Get the pieces for the chunk.
   450  		pieces := params.file.Pieces(chunkIndex)
   451  		for pieceIndex, pieceSet := range pieces {
   452  			for _, piece := range pieceSet {
   453  				// Sanity check - the same worker should not have two pieces for
   454  				// the same chunk.
   455  				_, exists := chunkMaps[chunkIndex-minChunk][piece.HostPubKey.String()]
   456  				if exists {
   457  					d.staticRenter.staticLog.Println("ERROR: Worker has multiple pieces uploaded for the same chunk.", params.file.SiaPath(), chunkIndex, pieceIndex, piece.HostPubKey.String())
   458  				}
   459  				chunkMaps[chunkIndex-minChunk][piece.HostPubKey.String()] = downloadPieceInfo{
   460  					index: uint64(pieceIndex),
   461  					root:  piece.MerkleRoot,
   462  				}
   463  			}
   464  		}
   465  	}
   466  
   467  	// Queue the downloads for each chunk.
   468  	writeOffset := int64(0) // where to write a chunk within the download destination.
   469  	d.chunksRemaining += maxChunk - minChunk + 1
   470  	for i := minChunk; i <= maxChunk; i++ {
   471  		udc := &unfinishedDownloadChunk{
   472  			destination: params.destination,
   473  			erasureCode: params.file.ErasureCode(),
   474  			masterKey:   params.file.MasterKey(),
   475  
   476  			staticChunkIndex: i,
   477  			staticCacheID:    fmt.Sprintf("%v:%v", d.staticSiaPath, i),
   478  			staticChunkMap:   chunkMaps[i-minChunk],
   479  			staticChunkSize:  params.file.ChunkSize(),
   480  			staticPieceSize:  params.file.PieceSize(),
   481  
   482  			staticSpendingCategory: d.staticParams.staticSpendingCategory,
   483  
   484  			// TODO: 25ms is just a guess for a good default. Really, we want to
   485  			// set the latency target such that slower workers will pick up the
   486  			// later chunks, but only if there's a very strong chance that
   487  			// they'll finish before the earlier chunks finish, so that they do
   488  			// no contribute to low latency.
   489  			//
   490  			// TODO: There is some sane minimum latency that should actually be
   491  			// set based on the number of pieces 'n', and the 'n' fastest
   492  			// workers that we have.
   493  			staticDisableDiskFetch: params.disableLocalFetch,
   494  			staticLatencyTarget:    d.staticLatencyTarget + (25 * time.Duration(i-minChunk)), // Increase target by 25ms per chunk.
   495  			staticNeedsMemory:      params.needsMemory,
   496  			staticPriority:         params.priority,
   497  
   498  			completedPieces:   make([]bool, params.file.ErasureCode().NumPieces()),
   499  			physicalChunkData: make([][]byte, params.file.ErasureCode().NumPieces()),
   500  			pieceUsage:        make([]bool, params.file.ErasureCode().NumPieces()),
   501  
   502  			staticDownload:      d,
   503  			staticMemoryManager: params.staticMemoryManager,
   504  			renterFile:          params.file,
   505  		}
   506  
   507  		// Set the fetchOffset - the offset within the chunk that we start
   508  		// downloading from.
   509  		if i == minChunk {
   510  			udc.staticFetchOffset = minChunkOffset
   511  		} else {
   512  			udc.staticFetchOffset = 0
   513  		}
   514  		// Set the fetchLength - the number of bytes to fetch within the chunk
   515  		// that we start downloading from.
   516  		if i == maxChunk && maxChunkOffset != 0 {
   517  			udc.staticFetchLength = maxChunkOffset - udc.staticFetchOffset
   518  		} else {
   519  			udc.staticFetchLength = params.file.ChunkSize() - udc.staticFetchOffset
   520  		}
   521  		// Set the writeOffset within the destination for where the data should
   522  		// be written.
   523  		udc.staticWriteOffset = writeOffset
   524  		writeOffset += int64(udc.staticFetchLength)
   525  
   526  		// TODO: Currently all chunks are given overdrive. This should probably
   527  		// be changed once the hostdb knows how to measure host speed/latency
   528  		// and once we can assign overdrive dynamically.
   529  		udc.staticOverdrive = params.overdrive
   530  
   531  		// Add this chunk to the chunk heap, and notify the download loop that
   532  		// there is work to do.
   533  		d.staticRenter.managedAddChunkToDownloadHeap(udc)
   534  		select {
   535  		case d.staticRenter.newDownloads <- struct{}{}:
   536  		default:
   537  		}
   538  	}
   539  	return nil
   540  }
   541  
   542  // DownloadByUID returns a single download from the history by it's UID.
   543  func (r *Renter) DownloadByUID(uid skymodules.DownloadID) (skymodules.DownloadInfo, bool) {
   544  	d, exists := r.staticDownloadHistory.callFetchDownload(uid)
   545  	if !exists {
   546  		return skymodules.DownloadInfo{}, false
   547  	}
   548  	d.mu.Lock()
   549  	defer d.mu.Unlock()
   550  	return skymodules.DownloadInfo{
   551  		Destination:     d.destinationString,
   552  		DestinationType: d.staticDestinationType,
   553  		Length:          d.staticLength,
   554  		Offset:          d.staticOffset,
   555  		SiaPath:         d.staticSiaPath,
   556  
   557  		Completed:            d.staticComplete(),
   558  		EndTime:              d.endTime,
   559  		Received:             atomic.LoadUint64(&d.atomicDataReceived),
   560  		StartTime:            d.staticStartTime,
   561  		StartTimeUnix:        d.staticStartTime.UnixNano(),
   562  		TotalDataTransferred: atomic.LoadUint64(&d.atomicTotalDataTransferred),
   563  	}, true
   564  }