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 }