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 }