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

     1  package renter
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"sync"
     7  	"sync/atomic"
     8  	"time"
     9  
    10  	"github.com/NebulousLabs/Sia/crypto"
    11  	"github.com/NebulousLabs/Sia/modules"
    12  
    13  	"github.com/NebulousLabs/errors"
    14  )
    15  
    16  // downloadPieceInfo contains all the information required to download and
    17  // recover a piece of a chunk from a host. It is a value in a map where the key
    18  // is the file contract id.
    19  type downloadPieceInfo struct {
    20  	index uint64
    21  	root  crypto.Hash
    22  }
    23  
    24  // unfinishedDownloadChunk contains a chunk for a download that is in progress.
    25  //
    26  // TODO: Currently, if a standby worker is needed, all of the standby workers
    27  // are added and the first one that is available will pick up the slack. But,
    28  // depending on the situation, we may only want to add a handful of workers to
    29  // make sure that a fast / optimal worker is initially able to pick up the
    30  // slack. This could potentially be streamlined by turning the standby array
    31  // into a standby heap, and then having some general scoring system for figuring
    32  // out how useful a worker is, and then having some threshold that a worker
    33  // needs to be pulled from standby to work on the download. That threshold
    34  // should go up every time that a worker fails, to make sure that if you have
    35  // repeated failures, you keep pulling in the fresh workers instead of getting
    36  // stuck and always rejecting all the standby workers.
    37  type unfinishedDownloadChunk struct {
    38  	// Fetch + Write instructions - read only or otherwise thread safe.
    39  	destination downloadDestination // Where to write the recovered logical chunk.
    40  	erasureCode modules.ErasureCoder
    41  	masterKey   crypto.TwofishKey
    42  
    43  	// Fetch + Write instructions - read only or otherwise thread safe.
    44  	staticChunkIndex  uint64                       // Required for deriving the encryption keys for each piece.
    45  	staticCacheID     string                       // Used to uniquely identify a chunk in the chunk cache.
    46  	staticChunkMap    map[string]downloadPieceInfo // Maps from host PubKey to the info for the piece associated with that host
    47  	staticChunkSize   uint64
    48  	staticFetchLength uint64 // Length within the logical chunk to fetch.
    49  	staticFetchOffset uint64 // Offset within the logical chunk that is being downloaded.
    50  	staticPieceSize   uint64
    51  	staticWriteOffset int64 // Offset within the writer to write the completed data.
    52  
    53  	// Fetch + Write instructions - read only or otherwise thread safe.
    54  	staticLatencyTarget time.Duration
    55  	staticNeedsMemory   bool // Set to true if memory was not pre-allocated for this chunk.
    56  	staticOverdrive     int
    57  	staticPriority      uint64
    58  
    59  	// Download chunk state - need mutex to access.
    60  	failed            bool      // Indicates if the chunk has been marked as failed.
    61  	physicalChunkData [][]byte  // Used to recover the logical data.
    62  	pieceUsage        []bool    // Which pieces are being actively fetched.
    63  	piecesCompleted   int       // Number of pieces that have successfully completed.
    64  	piecesRegistered  int       // Number of pieces that workers are actively fetching.
    65  	recoveryComplete  bool      // Whether or not the recovery has completed and the chunk memory released.
    66  	workersRemaining  int       // Number of workers still able to fetch the chunk.
    67  	workersStandby    []*worker // Set of workers that are able to work on this download, but are not needed unless other workers fail.
    68  
    69  	// Memory management variables.
    70  	memoryAllocated uint64
    71  
    72  	// The download object, mostly to update download progress.
    73  	download *download
    74  	mu       sync.Mutex
    75  
    76  	// Caching related fields
    77  	staticStreamCache *streamCache
    78  }
    79  
    80  // fail will set the chunk status to failed. The physical chunk memory will be
    81  // wiped and any memory allocation will be returned to the renter. The download
    82  // as a whole will be failed as well.
    83  func (udc *unfinishedDownloadChunk) fail(err error) {
    84  	udc.failed = true
    85  	udc.recoveryComplete = true
    86  	for i := range udc.physicalChunkData {
    87  		udc.physicalChunkData[i] = nil
    88  	}
    89  	udc.download.managedFail(fmt.Errorf("chunk %v failed: %v", udc.staticChunkIndex, err))
    90  	udc.destination = nil
    91  }
    92  
    93  // managedCleanUp will check if the download has failed, and if not it will add
    94  // any standby workers which need to be added. Calling managedCleanUp too many
    95  // times is not harmful, however missing a call to managedCleanUp can lead to
    96  // dealocks.
    97  func (udc *unfinishedDownloadChunk) managedCleanUp() {
    98  	// Check if the chunk is newly failed.
    99  	udc.mu.Lock()
   100  	if udc.workersRemaining+udc.piecesCompleted < udc.erasureCode.MinPieces() && !udc.failed {
   101  		udc.fail(errors.New("not enough workers to continue download"))
   102  	}
   103  	// Return any excess memory.
   104  	udc.returnMemory()
   105  
   106  	// Nothing to do if the chunk has failed.
   107  	if udc.failed {
   108  		udc.mu.Unlock()
   109  		return
   110  	}
   111  
   112  	// Check whether standby workers are required.
   113  	chunkComplete := udc.piecesCompleted >= udc.erasureCode.MinPieces()
   114  	desiredPiecesRegistered := udc.erasureCode.MinPieces() + udc.staticOverdrive - udc.piecesCompleted
   115  	standbyWorkersRequired := !chunkComplete && udc.piecesRegistered < desiredPiecesRegistered
   116  	if !standbyWorkersRequired {
   117  		udc.mu.Unlock()
   118  		return
   119  	}
   120  
   121  	// Assemble a list of standby workers, release the udc lock, and then queue
   122  	// the chunk into the workers. The lock needs to be released early because
   123  	// holding the udc lock and the worker lock at the same time is a deadlock
   124  	// risk (they interact with eachother, call functions on eachother).
   125  	var standbyWorkers []*worker
   126  	for i := 0; i < len(udc.workersStandby); i++ {
   127  		standbyWorkers = append(standbyWorkers, udc.workersStandby[i])
   128  	}
   129  	udc.workersStandby = udc.workersStandby[:0] // Workers have been taken off of standby.
   130  	udc.mu.Unlock()
   131  	for i := 0; i < len(standbyWorkers); i++ {
   132  		standbyWorkers[i].managedQueueDownloadChunk(udc)
   133  	}
   134  }
   135  
   136  // managedRemoveWorker will decrement a worker from the set of remaining workers
   137  // in the udc. After a worker has been removed, the udc needs to be cleaned up.
   138  func (udc *unfinishedDownloadChunk) managedRemoveWorker() {
   139  	udc.mu.Lock()
   140  	udc.workersRemaining--
   141  	udc.mu.Unlock()
   142  	udc.managedCleanUp()
   143  }
   144  
   145  // returnMemory will check on the status of all the workers and pieces, and
   146  // determine how much memory is safe to return to the renter. This should be
   147  // called each time a worker returns, and also after the chunk is recovered.
   148  func (udc *unfinishedDownloadChunk) returnMemory() {
   149  	// The maximum amount of memory is the pieces completed plus the number of
   150  	// workers remaining.
   151  	maxMemory := uint64(udc.workersRemaining+udc.piecesCompleted) * udc.staticPieceSize
   152  	// If enough pieces have completed, max memory is the number of registered
   153  	// pieces plus the number of completed pieces.
   154  	if udc.piecesCompleted >= udc.erasureCode.MinPieces() {
   155  		// udc.piecesRegistered is guaranteed to be at most equal to the number
   156  		// of overdrive pieces, meaning it will be equal to or less than
   157  		// initialMemory.
   158  		maxMemory = uint64(udc.piecesCompleted+udc.piecesRegistered) * udc.staticPieceSize
   159  	}
   160  	// If the chunk recovery has completed, the maximum number of pieces is the
   161  	// number of registered.
   162  	if udc.recoveryComplete {
   163  		maxMemory = uint64(udc.piecesRegistered) * udc.staticPieceSize
   164  	}
   165  	// Return any memory we don't need.
   166  	if uint64(udc.memoryAllocated) > maxMemory {
   167  		udc.download.memoryManager.Return(udc.memoryAllocated - maxMemory)
   168  		udc.memoryAllocated = maxMemory
   169  	}
   170  }
   171  
   172  // threadedRecoverLogicalData will take all of the pieces that have been
   173  // downloaded and encode them into the logical data which is then written to the
   174  // underlying writer for the download.
   175  func (udc *unfinishedDownloadChunk) threadedRecoverLogicalData() error {
   176  	// Ensure cleanup occurs after the data is recovered, whether recovery
   177  	// succeeds or fails.
   178  	defer udc.managedCleanUp()
   179  
   180  	// Recover the pieces into the logical chunk data.
   181  	//
   182  	// TODO: Might be some way to recover into the downloadDestination instead
   183  	// of creating a buffer and then writing that.
   184  	recoverWriter := new(bytes.Buffer)
   185  	err := udc.erasureCode.Recover(udc.physicalChunkData, udc.staticChunkSize, recoverWriter)
   186  	if err != nil {
   187  		udc.mu.Lock()
   188  		udc.fail(err)
   189  		udc.mu.Unlock()
   190  		return errors.AddContext(err, "unable to recover chunk")
   191  	}
   192  	// Clear out the physical chunk pieces, we do not need them anymore.
   193  	for i := range udc.physicalChunkData {
   194  		udc.physicalChunkData[i] = nil
   195  	}
   196  
   197  	// Get recovered data
   198  	recoveredData := recoverWriter.Bytes()
   199  
   200  	// Add the chunk to the cache.
   201  	if udc.download.staticDestinationType == destinationTypeSeekStream {
   202  		// We only cache streaming chunks since browsers and media players tend
   203  		// to only request a few kib at once when streaming data. That way we can
   204  		// prevent scheduling the same chunk for download over and over.
   205  		udc.staticStreamCache.Add(udc.staticCacheID, recoveredData)
   206  	}
   207  
   208  	// Write the bytes to the requested output.
   209  	start := udc.staticFetchOffset
   210  	end := udc.staticFetchOffset + udc.staticFetchLength
   211  	_, err = udc.destination.WriteAt(recoveredData[start:end], udc.staticWriteOffset)
   212  	if err != nil {
   213  		udc.mu.Lock()
   214  		udc.fail(err)
   215  		udc.mu.Unlock()
   216  		return errors.AddContext(err, "unable to write to download destination")
   217  	}
   218  	recoverWriter = nil
   219  
   220  	// Now that the download has completed and been flushed from memory, we can
   221  	// release the memory that was used to store the data. Call 'cleanUp' to
   222  	// trigger the memory cleanup along with some extra checks that everything
   223  	// is consistent.
   224  	udc.mu.Lock()
   225  	udc.recoveryComplete = true
   226  	udc.mu.Unlock()
   227  
   228  	// Update the download and signal completion of this chunk.
   229  	udc.download.mu.Lock()
   230  	defer udc.download.mu.Unlock()
   231  	udc.download.chunksRemaining--
   232  	atomic.AddUint64(&udc.download.atomicDataReceived, udc.staticFetchLength)
   233  	if udc.download.chunksRemaining == 0 {
   234  		// Download is complete, send out a notification and close the
   235  		// destination writer.
   236  		udc.download.endTime = time.Now()
   237  		close(udc.download.completeChan)
   238  		err := udc.download.destination.Close()
   239  		udc.download.destination = nil
   240  		return err
   241  	}
   242  	return nil
   243  }