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