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 }