gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/uploadheap.go (about) 1 package renter 2 3 // TODO: replace managedRefreshHostsAndWorkers with structural updates to the 4 // worker pool. The worker pool should maintain the map of hosts that 5 // managedRefreshHostsAndWorkers builds every call, and the contractor should 6 // work with the worker pool to instantly notify the worker pool of any changes 7 // to the set of contracts. 8 9 import ( 10 "container/heap" 11 "io/ioutil" 12 "math" 13 "os" 14 "path/filepath" 15 "sort" 16 "strings" 17 "sync" 18 "time" 19 20 "gitlab.com/NebulousLabs/errors" 21 "gitlab.com/NebulousLabs/fastrand" 22 23 "gitlab.com/SiaPrime/SiaPrime/build" 24 "gitlab.com/SiaPrime/SiaPrime/modules" 25 "gitlab.com/SiaPrime/SiaPrime/modules/renter/siafile" 26 "gitlab.com/SiaPrime/SiaPrime/types" 27 ) 28 29 // repairTarget is a helper type for telling the repair heap what type of 30 // files/chunks to target for repair 31 type repairTarget int 32 33 // targetStuckChunks tells the repair loop to target stuck chunks for repair and 34 // targetUnstuckChunks tells the repair loop to target unstuck chunks for repair 35 const ( 36 targetError repairTarget = iota 37 targetStuckChunks 38 targetUnstuckChunks 39 targetBackupChunks 40 ) 41 42 // uploadChunkHeap is a bunch of priority-sorted chunks that need to be either 43 // uploaded or repaired. 44 type uploadChunkHeap []*unfinishedUploadChunk 45 46 // Implementation of heap.Interface for uploadChunkHeap. 47 func (uch uploadChunkHeap) Len() int { return len(uch) } 48 func (uch uploadChunkHeap) Less(i, j int) bool { 49 // If only chunk i is high priority, return true to prioritize it. 50 if uch[i].priority && !uch[j].priority { 51 return true 52 } 53 // If only chunk j is high priority, return false to prioritize it. 54 if !uch[i].priority && uch[j].priority { 55 return false 56 } 57 // If the chunks have the same stuck status, check which chunk has the worse 58 // health. A higher health is a worse health 59 if uch[i].stuck == uch[j].stuck { 60 return uch[i].health > uch[j].health 61 } 62 // If chunk i is stuck, return true to prioritize it. 63 if uch[i].stuck { 64 return true 65 } 66 // Chunk j is stuck, return false to prioritize it. 67 return false 68 } 69 func (uch uploadChunkHeap) Swap(i, j int) { uch[i], uch[j] = uch[j], uch[i] } 70 func (uch *uploadChunkHeap) Push(x interface{}) { *uch = append(*uch, x.(*unfinishedUploadChunk)) } 71 func (uch *uploadChunkHeap) Pop() interface{} { 72 old := *uch 73 n := len(old) 74 x := old[n-1] 75 *uch = old[:n-1] 76 return x 77 } 78 79 // reset clears the uploadChunkHeap and makes sure all the files belonging to 80 // the chunks are closed 81 func (uch *uploadChunkHeap) reset() (err error) { 82 for _, c := range *uch { 83 err = errors.Compose(err, c.fileEntry.Close()) 84 } 85 *uch = uploadChunkHeap{} 86 return err 87 } 88 89 // uploadHeap contains a priority-sorted heap of all the chunks being uploaded 90 // to the renter, along with some metadata. 91 type uploadHeap struct { 92 heap uploadChunkHeap 93 94 // heapChunks is a map containing all the chunks that are currently in the 95 // heap. Chunks are added and removed from the map when chunks are pushed 96 // and popped off the heap 97 // 98 // repairingChunks is a map containing all the chunks are that currently 99 // assigned to workers and are being repaired/worked on. 100 repairingChunks map[uploadChunkID]*unfinishedUploadChunk 101 stuckHeapChunks map[uploadChunkID]*unfinishedUploadChunk 102 unstuckHeapChunks map[uploadChunkID]*unfinishedUploadChunk 103 104 // Control channels 105 newUploads chan struct{} 106 repairNeeded chan struct{} 107 stuckChunkFound chan struct{} 108 stuckChunkSuccess chan struct{} 109 110 mu sync.Mutex 111 } 112 113 // managedExists checks if a chunk currently exists in the upload heap. A chunk 114 // exists in the upload heap if it exists in any of the heap's tracking maps 115 func (uh *uploadHeap) managedExists(id uploadChunkID) bool { 116 uh.mu.Lock() 117 defer uh.mu.Unlock() 118 _, existsUnstuckHeap := uh.unstuckHeapChunks[id] 119 _, existsRepairing := uh.repairingChunks[id] 120 _, existsStuckHeap := uh.stuckHeapChunks[id] 121 return existsUnstuckHeap || existsRepairing || existsStuckHeap 122 } 123 124 // managedLen will return the length of the heap 125 func (uh *uploadHeap) managedLen() int { 126 uh.mu.Lock() 127 uhLen := uh.heap.Len() 128 uh.mu.Unlock() 129 return uhLen 130 } 131 132 // managedMarkRepairDone removes the chunk from the repairingChunks map of the 133 // uploadHeap. It also performs a sanity check that the chunk was in the map, 134 // this is to ensure that we are adding and removing the chunks as expected 135 func (uh *uploadHeap) managedMarkRepairDone(id uploadChunkID) { 136 uh.mu.Lock() 137 defer uh.mu.Unlock() 138 _, ok := uh.repairingChunks[id] 139 if !ok { 140 build.Critical("Chunk is not in the repair map, this means it was removed prematurely or was never added") 141 } 142 delete(uh.repairingChunks, id) 143 } 144 145 // managedNumStuckChunks returns the number of stuck chunks in the heap 146 func (uh *uploadHeap) managedNumStuckChunks() int { 147 uh.mu.Lock() 148 defer uh.mu.Unlock() 149 return len(uh.stuckHeapChunks) 150 } 151 152 // managedPush will try and add a chunk to the upload heap. If the chunk is 153 // added it will return true otherwise it will return false 154 func (uh *uploadHeap) managedPush(uuc *unfinishedUploadChunk) bool { 155 // Grab chunk stuck status 156 uuc.mu.Lock() 157 chunkStuck := uuc.stuck 158 uuc.mu.Unlock() 159 160 // Check if chunk is in any of the heap maps 161 uh.mu.Lock() 162 defer uh.mu.Unlock() 163 unstuckUUC, existsUnstuckHeap := uh.unstuckHeapChunks[uuc.id] 164 repairingUUC, existsRepairing := uh.repairingChunks[uuc.id] 165 stuckUUC, existsStuckHeap := uh.stuckHeapChunks[uuc.id] 166 167 // If the added chunk has a sourceReader and the existing one doesn't, replace 168 // them. 169 if uuc.sourceReader != nil && (existsUnstuckHeap || existsRepairing || existsStuckHeap) { 170 // Get the existing chunk. 171 var existingUUC *unfinishedUploadChunk 172 if existsStuckHeap { 173 existingUUC = stuckUUC 174 } else if existsRepairing { 175 existingUUC = repairingUUC 176 } else if existsUnstuckHeap { 177 existingUUC = unstuckUUC 178 } 179 // Cancel the chunk. 180 existingUUC.cancelMU.Lock() 181 existingUUC.canceled = true 182 existingUUC.cancelMU.Unlock() 183 // Wait for all workers to finish ongoing work on that chunk and try to push 184 // the new chunk again. This happens in a separate thread to avoid holding the 185 // uploadHeap lock while waiting. 186 go func() { 187 existingUUC.cancelWG.Wait() 188 uh.managedPush(uuc) 189 }() 190 return true // It's not pushed yet but it is guranteed to be pushed eventually. 191 } 192 193 // Check if the chunk can be added to the heap 194 canAddStuckChunk := chunkStuck && !existsStuckHeap && !existsRepairing && len(uh.stuckHeapChunks) < maxStuckChunksInHeap 195 canAddUnstuckChunk := !chunkStuck && !existsUnstuckHeap && !existsRepairing 196 if canAddStuckChunk { 197 uh.stuckHeapChunks[uuc.id] = uuc 198 heap.Push(&uh.heap, uuc) 199 return true 200 } else if canAddUnstuckChunk { 201 uh.unstuckHeapChunks[uuc.id] = uuc 202 heap.Push(&uh.heap, uuc) 203 return true 204 } 205 return false 206 } 207 208 // managedPop will pull a chunk off of the upload heap and return it. 209 func (uh *uploadHeap) managedPop() (uc *unfinishedUploadChunk) { 210 uh.mu.Lock() 211 if len(uh.heap) > 0 { 212 uc = heap.Pop(&uh.heap).(*unfinishedUploadChunk) 213 delete(uh.unstuckHeapChunks, uc.id) 214 delete(uh.stuckHeapChunks, uc.id) 215 if _, exists := uh.repairingChunks[uc.id]; exists { 216 build.Critical("There should not be a chunk in the heap that can be popped that is currently being repaired") 217 } 218 uh.repairingChunks[uc.id] = uc 219 } 220 uh.mu.Unlock() 221 return uc 222 } 223 224 // managedReset will reset the slice and maps within the heap to free up memory. 225 func (uh *uploadHeap) managedReset() error { 226 uh.mu.Lock() 227 defer uh.mu.Unlock() 228 uh.unstuckHeapChunks = make(map[uploadChunkID]*unfinishedUploadChunk) 229 uh.stuckHeapChunks = make(map[uploadChunkID]*unfinishedUploadChunk) 230 return uh.heap.reset() 231 } 232 233 // managedBuildUnfinishedChunk will pull out a single unfinished chunk of a file. 234 func (r *Renter) managedBuildUnfinishedChunk(entry *siafile.SiaFileSetEntry, chunkIndex uint64, hosts map[string]struct{}, hostPublicKeys map[string]types.SiaPublicKey, priority bool, offline, goodForRenew map[string]bool) (*unfinishedUploadChunk, error) { 235 // Copy entry 236 entryCopy, err := entry.CopyEntry() 237 if err != nil { 238 r.log.Println("WARN: unable to copy siafile entry:", err) 239 return nil, errors.AddContext(err, "unable to copy file entry when trying to build the unfinished chunk") 240 } 241 if entryCopy == nil { 242 build.Critical("nil file entry return from CopyEntry, and no error should have been returned") 243 return nil, errors.New("CopyEntry returned a nil copy") 244 } 245 stuck, err := entry.StuckChunkByIndex(chunkIndex) 246 if err != nil { 247 r.log.Println("WARN: unable to get 'stuck' status:", err) 248 return nil, errors.AddContext(err, "unable to get 'stuck' status") 249 } 250 uuc := &unfinishedUploadChunk{ 251 fileEntry: entryCopy, 252 253 id: uploadChunkID{ 254 fileUID: entry.UID(), 255 index: chunkIndex, 256 }, 257 258 index: chunkIndex, 259 length: entry.ChunkSize(), 260 offset: int64(chunkIndex * entry.ChunkSize()), 261 priority: priority, 262 263 // memoryNeeded has to also include the logical data, and also 264 // include the overhead for encryption. 265 // 266 // TODO / NOTE: If we adjust the file to have a flexible encryption 267 // scheme, we'll need to adjust the overhead stuff too. 268 // 269 // TODO: Currently we request memory for all of the pieces as well 270 // as the minimum pieces, but we perhaps don't need to request all 271 // of that. 272 memoryNeeded: entry.PieceSize()*uint64(entry.ErasureCode().NumPieces()+entry.ErasureCode().MinPieces()) + uint64(entry.ErasureCode().NumPieces())*entry.MasterKey().Type().Overhead(), 273 minimumPieces: entry.ErasureCode().MinPieces(), 274 piecesNeeded: entry.ErasureCode().NumPieces(), 275 stuck: stuck, 276 277 physicalChunkData: make([][]byte, entry.ErasureCode().NumPieces()), 278 279 pieceUsage: make([]bool, entry.ErasureCode().NumPieces()), 280 unusedHosts: make(map[string]struct{}, len(hosts)), 281 } 282 283 // Every chunk can have a different set of unused hosts. 284 for host := range hosts { 285 uuc.unusedHosts[host] = struct{}{} 286 } 287 288 // Iterate through the pieces of all chunks of the file and mark which 289 // hosts are already in use for a particular chunk. As you delete hosts 290 // from the 'unusedHosts' map, also increment the 'piecesCompleted' value. 291 pieces, err := entry.Pieces(chunkIndex) 292 if err != nil { 293 r.log.Println("failed to get pieces for building incomplete chunks", err) 294 if err := entry.SetStuck(chunkIndex, true); err != nil { 295 r.log.Printf("failed to set chunk %v stuck: %v", chunkIndex, err) 296 } 297 return nil, errors.AddContext(err, "error trying to get the pieces for the chunk") 298 } 299 for pieceIndex, pieceSet := range pieces { 300 for _, piece := range pieceSet { 301 hpk := piece.HostPubKey.String() 302 goodForRenew, exists2 := goodForRenew[hpk] 303 offline, exists := offline[hpk] 304 if !exists || !exists2 || !goodForRenew || offline { 305 // This piece cannot be counted towards redudnacy if the host is 306 // offline, is marked no good for renew, or is not available in 307 // the lookup maps. 308 continue 309 } 310 311 // Mark the chunk set based on the pieces in this contract. 312 _, exists = uuc.unusedHosts[piece.HostPubKey.String()] 313 redundantPiece := uuc.pieceUsage[pieceIndex] 314 if exists && !redundantPiece { 315 uuc.pieceUsage[pieceIndex] = true 316 uuc.piecesCompleted++ 317 delete(uuc.unusedHosts, piece.HostPubKey.String()) 318 } else if exists { 319 // This host has a piece, but it is the same piece another 320 // host has. We should still remove the host from the 321 // unusedHosts since one host having multiple pieces of a 322 // chunk might lead to unexpected issues. e.g. if a host 323 // has multiple pieces and another host with redundant 324 // pieces goes offline, we end up with false redundancy 325 // reporting. 326 delete(uuc.unusedHosts, piece.HostPubKey.String()) 327 } 328 } 329 } 330 // Now that we have calculated the completed pieces for the chunk we can 331 // calculate the health of the chunk to avoid a call to ChunkHealth 332 uuc.health = 1 - (float64(uuc.piecesCompleted-uuc.minimumPieces) / float64(uuc.piecesNeeded-uuc.minimumPieces)) 333 return uuc, nil 334 } 335 336 // managedBuildUnfinishedChunks will pull all of the unfinished chunks out of a 337 // file. 338 // 339 // NOTE: each unfinishedUploadChunk needs its own SiaFileSetEntry. This is due 340 // to the SiaFiles being removed from memory. Since the renter does not keep the 341 // SiaFiles in memory the unfinishedUploadChunks need to close the SiaFile when 342 // they are done and so cannot share a SiaFileSetEntry as the first chunk to 343 // finish would then close the Entry and consequentially impact the remaining 344 // chunks. 345 func (r *Renter) managedBuildUnfinishedChunks(entry *siafile.SiaFileSetEntry, hosts map[string]struct{}, target repairTarget, offline, goodForRenew map[string]bool) []*unfinishedUploadChunk { 346 // If we don't have enough workers for the file, don't repair it right now. 347 minPieces := entry.ErasureCode().MinPieces() 348 r.staticWorkerPool.mu.RLock() 349 workerPoolLen := len(r.staticWorkerPool.workers) 350 r.staticWorkerPool.mu.RUnlock() 351 if workerPoolLen < minPieces { 352 // There are not enough workers for the chunk to reach minimum 353 // redundancy. Check if the allowance has enough hosts for the chunk to 354 // reach minimum redundancy 355 r.log.Debugln("Not building any chunks from file as there are not enough workers") 356 allowance := r.hostContractor.Allowance() 357 // Only perform this check when we are looking for unstuck chunks. This 358 // will prevent log spam from repeatedly logging to the user the issue 359 // with the file after marking the chunks as stuck 360 if allowance.Hosts < uint64(minPieces) && target == targetUnstuckChunks { 361 // There are not enough hosts in the allowance for the file to reach 362 // minimum redundancy. Mark all chunks as stuck 363 r.log.Printf("WARN: allownace had insufficient hosts for chunk to reach minimum redundancy, have %v need %v for file %v", allowance.Hosts, minPieces, entry.SiaFilePath()) 364 if err := entry.SetAllStuck(true); err != nil { 365 r.log.Println("WARN: unable to mark all chunks as stuck:", err) 366 } 367 } 368 return nil 369 } 370 371 // Assemble chunk indexes, stuck Loop should only be adding stuck chunks and 372 // the repair loop should only be adding unstuck chunks 373 var chunkIndexes []uint64 374 for i := uint64(0); i < entry.NumChunks(); i++ { 375 stuck, err := entry.StuckChunkByIndex(i) 376 if err != nil { 377 r.log.Debugln("failed to get 'stuck' status of entry:", err) 378 continue 379 } 380 if (target == targetStuckChunks) == stuck { 381 chunkIndexes = append(chunkIndexes, i) 382 } 383 } 384 385 // Sanity check that we have chunk indices to go through 386 if len(chunkIndexes) == 0 { 387 r.log.Println("WARN: no chunk indices gathered, can't add chunks to heap") 388 return nil 389 } 390 391 // Build a map of host public keys. We assume that all entrys are the same. 392 pks := make(map[string]types.SiaPublicKey) 393 for _, pk := range entry.HostPublicKeys() { 394 pks[string(pk.Key)] = pk 395 } 396 397 // Assemble the set of chunks. 398 newUnfinishedChunks := make([]*unfinishedUploadChunk, 0, len(chunkIndexes)) 399 for _, index := range chunkIndexes { 400 // Sanity check: fileUID should not be the empty value. 401 if entry.UID() == "" { 402 build.Critical("empty string for file UID") 403 } 404 405 // Create unfinishedUploadChunk 406 chunk, err := r.managedBuildUnfinishedChunk(entry, uint64(index), hosts, pks, false, offline, goodForRenew) 407 if err != nil { 408 r.log.Debugln("Error when building an unfinished chunk:", err) 409 continue 410 } 411 newUnfinishedChunks = append(newUnfinishedChunks, chunk) 412 } 413 414 // Iterate through the set of newUnfinishedChunks and remove any that are 415 // completed or are not downloadable. 416 incompleteChunks := newUnfinishedChunks[:0] 417 for _, chunk := range newUnfinishedChunks { 418 // Check the chunk status. A chunk is repairable if it can be fully 419 // downloaded, or if the source file is available on disk. We also check 420 // if the chunk needs repair, which is only true if more than a certain 421 // amount of redundancy is missing. We only repair above a certain 422 // threshold of missing redundancy to minimize the amount of repair work 423 // that gets triggered by host churn. 424 // 425 // While a file could be on disk as long as !os.IsNotExist(err), for the 426 // purposes of repairing a file is only considered on disk if it can be 427 // accessed without error. If there is an error accessing the file then 428 // it is likely that we can not read the file in which case it can not 429 // be used for repair. 430 _, err := os.Stat(chunk.fileEntry.LocalPath()) 431 onDisk := err == nil 432 repairable := chunk.health <= 1 || onDisk 433 needsRepair := chunk.health >= RepairThreshold 434 435 // Add chunk to list of incompleteChunks if it is incomplete and 436 // repairable or if we are targeting stuck chunks 437 if needsRepair && (repairable || target == targetStuckChunks) { 438 incompleteChunks = append(incompleteChunks, chunk) 439 continue 440 } 441 442 // If a chunk is not able to be repaired, mark it as stuck. 443 if !repairable { 444 r.log.Println("Marking chunk", chunk.id, "as stuck due to not being repairable") 445 err = r.managedSetStuckAndClose(chunk, true) 446 if err != nil { 447 r.log.Debugln("WARN: unable to set chunk stuck status and close:", err) 448 } 449 continue 450 } 451 452 // Close entry of completed chunk 453 err = r.managedSetStuckAndClose(chunk, false) 454 if err != nil { 455 r.log.Debugln("WARN: unable to set chunk stuck status and close:", err) 456 } 457 } 458 return incompleteChunks 459 } 460 461 // managedAddChunksToHeap will add chunks to the upload heap one directory at a 462 // time until the directory heap is empty or the uploadheap is full. It does 463 // this by popping directories off the directory heap and adding the chunks from 464 // that directory to the upload heap. If the worst health directory found is 465 // sufficiently healthy then we return. 466 func (r *Renter) managedAddChunksToHeap(hosts map[string]struct{}) (map[modules.SiaPath]struct{}, error) { 467 siaPaths := make(map[modules.SiaPath]struct{}) 468 prevHeapLen := r.uploadHeap.managedLen() 469 // Loop until the upload heap has maxUploadHeapChunks in it or the directory 470 // heap is empty 471 for r.uploadHeap.managedLen() < maxUploadHeapChunks && r.directoryHeap.managedLen() > 0 { 472 select { 473 case <-r.tg.StopChan(): 474 return siaPaths, errors.New("renter shutdown before we could finish adding chunks to heap") 475 default: 476 } 477 478 // Pop an explored directory off of the directory heap 479 dir, err := r.managedNextExploredDirectory() 480 if err != nil { 481 r.log.Println("WARN: error getting explored directory:", err) 482 // Reset the directory heap to try and help address the error 483 r.directoryHeap.managedReset() 484 return siaPaths, err 485 } 486 487 // Sanity Check if directory was returned 488 if dir == nil { 489 r.log.Debugln("no more chunks added to the upload heap because there are no more directories") 490 return siaPaths, nil 491 } 492 493 // Grab health and siaPath of the directory 494 dir.mu.Lock() 495 dirHealth := dir.health 496 dirSiaPath := dir.siaPath 497 dir.mu.Unlock() 498 499 // If the directory that was just popped is healthy then return 500 if dirHealth < RepairThreshold { 501 r.log.Debugln("no more chunks added to the upload heap because directory popped is healthy") 502 return siaPaths, nil 503 } 504 505 // Add chunks from the directory to the uploadHeap. 506 r.managedBuildChunkHeap(dirSiaPath, hosts, targetUnstuckChunks) 507 508 // Check to see if we are still adding chunks 509 heapLen := r.uploadHeap.managedLen() 510 if heapLen == prevHeapLen { 511 // No more chunks added to the uploadHeap from the worst health 512 // directory. This means that the worse health chunks are already in 513 // the heap or are currently being repaired, so return. This can be 514 // the case in new uploads or repair loop iterations triggered from 515 // bubble 516 r.log.Debugln("no more chunks added to the upload heap") 517 return siaPaths, nil 518 } 519 chunksAdded := heapLen - prevHeapLen 520 prevHeapLen = heapLen 521 522 // Since we added chunks from this directory, track the siaPath 523 // 524 // NOTE: we only want to remember each siaPath once which is why we use 525 // a map. We Don't check if the siaPath is already in the map because 526 // another thread could have added the directory back to the heap after 527 // we just popped it off. This is the case for new uploads. 528 siaPaths[dirSiaPath] = struct{}{} 529 r.log.Println("Added", chunksAdded, "chunks from", dirSiaPath, "to the upload heap") 530 } 531 532 return siaPaths, nil 533 } 534 535 // managedBuildAndPushRandomChunk randomly selects a file and builds the 536 // unfinished chunks, then randomly adds chunksToAdd chunks to the upload heap 537 func (r *Renter) managedBuildAndPushRandomChunk(files []*siafile.SiaFileSetEntry, chunksToAdd int, hosts map[string]struct{}, target repairTarget, offline, goodForRenew map[string]bool) { 538 // Sanity check that there are files 539 if len(files) == 0 { 540 return 541 } 542 543 // Create random indices for files 544 p := fastrand.Perm(len(files)) 545 for i := 0; i < chunksToAdd && i < len(files); i++ { 546 // Grab random file 547 file := files[p[i]] 548 549 // Build the unfinished stuck chunks from the file 550 unfinishedUploadChunks := r.managedBuildUnfinishedChunks(file, hosts, target, offline, goodForRenew) 551 552 // Sanity check that there are stuck chunks 553 if len(unfinishedUploadChunks) == 0 { 554 continue 555 } 556 557 // Add random stuck chunks to the upload heap and set its stuckRepair field 558 // to true 559 randChunkIndex := fastrand.Intn(len(unfinishedUploadChunks)) 560 randChunk := unfinishedUploadChunks[randChunkIndex] 561 randChunk.stuckRepair = true 562 if !r.uploadHeap.managedPush(randChunk) { 563 // Chunk wasn't added to the heap. Close the file 564 r.log.Debugln("WARN: stuck chunk", randChunk.id, "wasn't added to heap") 565 err := randChunk.fileEntry.Close() 566 if err != nil { 567 r.log.Println("WARN: unable to close file:", err) 568 } 569 } 570 unfinishedUploadChunks = append(unfinishedUploadChunks[:randChunkIndex], unfinishedUploadChunks[randChunkIndex+1:]...) 571 // Close the unused unfinishedUploadChunks 572 for _, chunk := range unfinishedUploadChunks { 573 err := chunk.fileEntry.Close() 574 if err != nil { 575 r.log.Println("WARN: unable to close file:", err) 576 } 577 } 578 } 579 return 580 } 581 582 // managedBuildAndPushChunks builds the unfinished upload chunks and adds them 583 // to the upload heap 584 // 585 // NOTE: the files submitted to this function should all be from the same 586 // directory 587 func (r *Renter) managedBuildAndPushChunks(files []*siafile.SiaFileSetEntry, hosts map[string]struct{}, target repairTarget, offline, goodForRenew map[string]bool) { 588 // Sanity check that at least one file was provided 589 if len(files) == 0 { 590 build.Critical("managedBuildAndPushChunks called without providing any files") 591 return 592 } 593 594 // Loop through the whole set of files and get a list of chunks and build a 595 // temporary heap 596 var unfinishedChunkHeap uploadChunkHeap 597 var worstIgnoredHealth float64 598 dirHeapHealth := r.directoryHeap.managedPeekHealth() 599 for _, file := range files { 600 // For normal repairs check if file is a worse health than the directory 601 // heap 602 fileHealth := file.Metadata().CachedHealth 603 if fileHealth < dirHeapHealth && target == targetUnstuckChunks { 604 worstIgnoredHealth = math.Max(worstIgnoredHealth, fileHealth) 605 continue 606 } 607 608 // Build unfinished chunks from file and add them to the temp heap if 609 // they are a worse health than the directory heap 610 unfinishedUploadChunks := r.managedBuildUnfinishedChunks(file, hosts, target, offline, goodForRenew) 611 for i := 0; i < len(unfinishedUploadChunks); i++ { 612 chunk := unfinishedUploadChunks[i] 613 // Check to see the chunk is already in the upload heap 614 if r.uploadHeap.managedExists(chunk.id) { 615 // Close the file entry 616 err := chunk.fileEntry.Close() 617 if err != nil { 618 r.log.Println("WARN: unable to close file:", err) 619 } 620 // Since the chunk is already in the heap we do not need to 621 // track the health of the chunk 622 continue 623 } 624 625 // For normal repairs check if chunk has a worse health than the 626 // directory heap 627 if chunk.health < dirHeapHealth && target == targetUnstuckChunks { 628 // Track the health 629 worstIgnoredHealth = math.Max(worstIgnoredHealth, chunk.health) 630 // Close the file entry 631 err := chunk.fileEntry.Close() 632 if err != nil { 633 r.log.Println("WARN: unable to close file:", err) 634 } 635 continue 636 } 637 638 // Add chunk to temp heap 639 heap.Push(&unfinishedChunkHeap, chunk) 640 641 // Check if temp heap is growing too large. We want to restrict it 642 // to twice the size of the max upload heap size. This restriction 643 // should be applied to all repairs to prevent excessive memory 644 // usage. 645 if len(unfinishedChunkHeap) < maxUploadHeapChunks*2 { 646 continue 647 } 648 649 // Pop of the worst half of the heap 650 var chunksToKeep []*unfinishedUploadChunk 651 for len(unfinishedChunkHeap) > maxUploadHeapChunks { 652 chunksToKeep = append(chunksToKeep, heap.Pop(&unfinishedChunkHeap).(*unfinishedUploadChunk)) 653 } 654 655 // Check health of next chunk 656 chunk = heap.Pop(&unfinishedChunkHeap).(*unfinishedUploadChunk) 657 worstIgnoredHealth = math.Max(worstIgnoredHealth, chunk.health) 658 // Close the file entry 659 err := chunk.fileEntry.Close() 660 if err != nil { 661 r.log.Println("WARN: unable to close file:", err) 662 } 663 664 // Reset temp heap to release memory 665 err = unfinishedChunkHeap.reset() 666 if err != nil { 667 r.log.Println("WARN: error resetting the temporary upload heap:", err) 668 } 669 670 // Add worst chunks back to heap 671 for _, chunk := range chunksToKeep { 672 heap.Push(&unfinishedChunkHeap, chunk) 673 } 674 675 // Make sure chunksToKeep is zeroed out in memory 676 chunksToKeep = []*unfinishedUploadChunk{} 677 } 678 } 679 680 // We now have a temporary heap of the worst chunks in the directory that 681 // are also worse than any other chunk in the directory heap. Now we try and 682 // add as many chunks as we can to the uploadHeap 683 for len(unfinishedChunkHeap) > 0 && (r.uploadHeap.managedLen() < maxUploadHeapChunks || target == targetBackupChunks) { 684 // Add chunk to the uploadHeap 685 chunk := heap.Pop(&unfinishedChunkHeap).(*unfinishedUploadChunk) 686 if !r.uploadHeap.managedPush(chunk) { 687 // We don't track the health of this chunk since the only reason it 688 // wouldn't be added to the heap is if it is already in the heap or 689 // is currently being repaired. Close the file. 690 err := chunk.fileEntry.Close() 691 if err != nil { 692 r.log.Println("WARN: unable to close file:", err) 693 } 694 } 695 } 696 697 // Check if there are still chunks left in the temp heap. If so check the 698 // health of the next chunk 699 if len(unfinishedChunkHeap) > 0 { 700 chunk := heap.Pop(&unfinishedChunkHeap).(*unfinishedUploadChunk) 701 worstIgnoredHealth = math.Max(worstIgnoredHealth, chunk.health) 702 // Close the chunk's file 703 err := chunk.fileEntry.Close() 704 if err != nil { 705 r.log.Println("WARN: unable to close file:", err) 706 } 707 } 708 709 // We are done with the temporary heap so reset it to help release the 710 // memory 711 err := unfinishedChunkHeap.reset() 712 if err != nil { 713 r.log.Println("WARN: error resetting the temporary upload heap:", err) 714 } 715 716 // Check if we were adding backup chunks, if so return here as backups are 717 // not added to the directory heap 718 if target == targetBackupChunks { 719 return 720 } 721 722 // Check if we should add the directory back to the directory heap 723 if worstIgnoredHealth < RepairThreshold { 724 return 725 } 726 727 // All files submitted are from the same directory so use the first one to 728 // get the directory siapath 729 dirSiaPath, err := r.staticFileSet.SiaPath(files[0]).Dir() 730 if err != nil { 731 r.log.Println("WARN: unable to get directory SiaPath and add directory back to directory heap:", err) 732 return 733 } 734 735 // Since directory is being added back as explored we only need to set the 736 // health as that is what will be used for sorting in the directory heap. 737 // 738 // The aggregate health is set to 'worstIgnoredHealth' as well. In the event 739 // that the directory gets added as unexplored because another copy of the 740 // unexplored directory exists on the directory heap, we need to make sure 741 // that the worst known health is represented in the aggregate value. 742 d := &directory{ 743 aggregateHealth: worstIgnoredHealth, 744 health: worstIgnoredHealth, 745 explored: true, 746 siaPath: dirSiaPath, 747 } 748 // Add the directory to the heap. If there is a conflict because the 749 // directory is already in the heap (for example, added by another thread or 750 // process), then the worst of the values between this dir and the one 751 // that's already in the dir will be used, to ensure that the repair loop 752 // will prioritize all bad value files. 753 r.directoryHeap.managedPush(d) 754 } 755 756 // managedBuildChunkHeap will iterate through all of the files in the renter and 757 // construct a chunk heap. 758 // 759 // TODO: accept an input to indicate how much room is in the heap 760 // 761 // TODO: Explore whether there is a way to perform the task below without 762 // opening a full file entry for each file in the directory. 763 func (r *Renter) managedBuildChunkHeap(dirSiaPath modules.SiaPath, hosts map[string]struct{}, target repairTarget) { 764 // Get Directory files 765 var fileinfos []os.FileInfo 766 var err error 767 if target == targetBackupChunks { 768 fileinfos, err = ioutil.ReadDir(dirSiaPath.SiaDirSysPath(r.staticBackupsDir)) 769 } else { 770 fileinfos, err = ioutil.ReadDir(dirSiaPath.SiaDirSysPath(r.staticFilesDir)) 771 } 772 if err != nil { 773 r.log.Println("WARN: could not read directory:", err) 774 return 775 } 776 // Build files from fileinfos 777 var files []*siafile.SiaFileSetEntry 778 for _, fi := range fileinfos { 779 // skip sub directories and non siafiles 780 ext := filepath.Ext(fi.Name()) 781 if fi.IsDir() || ext != modules.SiaFileExtension { 782 continue 783 } 784 785 // Open SiaFile 786 siaPath, err := dirSiaPath.Join(strings.TrimSuffix(fi.Name(), ext)) 787 if err != nil { 788 r.log.Println("WARN: could not create siaPath:", err) 789 continue 790 } 791 var file *siafile.SiaFileSetEntry 792 if target == targetBackupChunks { 793 file, err = r.staticBackupFileSet.Open(siaPath) 794 } else { 795 file, err = r.staticFileSet.Open(siaPath) 796 } 797 if err != nil { 798 r.log.Println("WARN: could not open siafile:", err) 799 continue 800 } 801 802 // For stuck chunk repairs, check to see if file has stuck chunks 803 if target == targetStuckChunks && file.NumStuckChunks() == 0 { 804 // Close unneeded files 805 err := file.Close() 806 if err != nil { 807 r.log.Println("WARN: Could not close file:", err) 808 } 809 continue 810 } 811 // For normal repairs, ignore files that don't have any unstuck chunks 812 // or are healthy. 813 // 814 // We can used the cached value of health because it is updated during 815 // bubble. Since the repair loop operates off of the metadata 816 // information updated by bubble this cached health is accurate enough 817 // to use in order to determine if a file has any chunks that need 818 // repair 819 ignore := file.NumChunks() == file.NumStuckChunks() || file.Metadata().CachedHealth < RepairThreshold 820 if target == targetUnstuckChunks && ignore { 821 err := file.Close() 822 if err != nil { 823 r.log.Println("WARN: Could not close file:", err) 824 } 825 continue 826 } 827 828 files = append(files, file) 829 } 830 831 // Check if any files were selected from directory 832 if len(files) == 0 { 833 r.log.Debugln("No files pulled from `", dirSiaPath, "` to build the repair heap") 834 return 835 } 836 837 // If there are more files than there is room in the heap, sort the files by 838 // health and only use the required number of files to build the heap. In 839 // the absolute worst case, each file will be only contributing one chunk to 840 // the heap, so this shortcut will not be missing any important chunks. This 841 // shortcut will also not be used for directories that have fewer than 842 // 'maxUploadHeapChunks' files in them, minimzing the impact of this code in 843 // the typical case. 844 // 845 // This check only applies to normal repairs. Stuck repairs have their own 846 // way of managing the number of chunks added to the heap and backup chunks 847 // should always be added. 848 // 849 // v1.4.1 Benchmark: on a computer with an SSD, the time to sort 6,000 files 850 // is less than 50 milliseconds, while the time to process 250 files with 40 851 // chunks each using 'managedBuildAndPushChunks' is several seconds. Even in 852 // the worst case, where we are sorting 251 files with 1 chunk each, there 853 // is not much slowdown compared to skipping the sort, because the sort is 854 // so fast. 855 if len(files) > maxUploadHeapChunks && target == targetUnstuckChunks { 856 // Sort so that the highest health chunks will be first in the array. 857 // Higher health values equal worse health for the file, and we want to 858 // focus on the worst files. 859 sort.Slice(files, func(i, j int) bool { 860 return files[i].Metadata().CachedHealth > files[j].Metadata().CachedHealth 861 }) 862 for i := maxUploadHeapChunks; i < len(files); i++ { 863 err := files[i].Close() 864 if err != nil { 865 r.log.Println("WARN: Could not close file:", err) 866 } 867 } 868 files = files[:maxUploadHeapChunks] 869 } 870 871 // Build the unfinished upload chunks and add them to the upload heap 872 offline, goodForRenew, _ := r.managedContractUtilityMaps() 873 switch target { 874 case targetBackupChunks: 875 r.log.Debugln("Attempting to add backup chunks to heap") 876 r.managedBuildAndPushChunks(files, hosts, target, offline, goodForRenew) 877 case targetStuckChunks: 878 r.log.Debugln("Attempting to add stuck chunk to heap") 879 r.managedBuildAndPushRandomChunk(files, maxStuckChunksInHeap, hosts, target, offline, goodForRenew) 880 case targetUnstuckChunks: 881 r.log.Debugln("Attempting to add chunks to heap") 882 r.managedBuildAndPushChunks(files, hosts, target, offline, goodForRenew) 883 default: 884 r.log.Println("WARN: repair target not recognized", target) 885 } 886 887 // Close all files 888 for _, file := range files { 889 err := file.Close() 890 if err != nil { 891 r.log.Println("WARN: Could not close file:", err) 892 } 893 } 894 } 895 896 // managedPrepareNextChunk takes the next chunk from the chunk heap and prepares 897 // it for upload. Preparation includes blocking until enough memory is 898 // available, fetching the logical data for the chunk (either from the disk or 899 // from the network), erasure coding the logical data into the physical data, 900 // and then finally passing the work onto the workers. 901 func (r *Renter) managedPrepareNextChunk(uuc *unfinishedUploadChunk, hosts map[string]struct{}) error { 902 // Grab the next chunk, loop until we have enough memory, update the amount 903 // of memory available, and then spin up a thread to asynchronously handle 904 // the rest of the chunk tasks. 905 if !r.memoryManager.Request(uuc.memoryNeeded, memoryPriorityLow) { 906 return errors.New("couldn't request memory") 907 } 908 // Fetch the chunk in a separate goroutine, as it can take a long time and 909 // does not need to bottleneck the repair loop. 910 go r.threadedFetchAndRepairChunk(uuc) 911 return nil 912 } 913 914 // managedRefreshHostsAndWorkers will reset the set of hosts and the set of 915 // workers for the renter. 916 // 917 // TODO: This function can be removed entirely if the worker pool is made to 918 // keep a list of hosts. Then instead of passing around the hosts as a parameter 919 // the cached value in the worker pool can be used instead. Using the cached 920 // value in the worker pool is more accurate anyway because the hosts field will 921 // match the set of workers that we have. Doing it the current way means there 922 // can be drift between the set of workers and the set of hosts we are using to 923 // build out the chunk heap. 924 func (r *Renter) managedRefreshHostsAndWorkers() map[string]struct{} { 925 // Grab the current set of contracts and use them to build a list of hosts 926 // that are currently active. The hosts are assembled into a map where the 927 // key is the String() representation of the host's SiaPublicKey. 928 // 929 // TODO / NOTE: This code can be removed once files store the HostPubKey 930 // of the hosts they are using, instead of just the FileContractID. 931 currentContracts := r.hostContractor.Contracts() 932 hosts := make(map[string]struct{}) 933 for _, contract := range currentContracts { 934 hosts[contract.HostPublicKey.String()] = struct{}{} 935 } 936 // Refresh the worker pool as well. 937 r.staticWorkerPool.callUpdate() 938 return hosts 939 } 940 941 // managedRepairLoop works through the uploadheap repairing chunks. The repair 942 // loop will continue until the renter stops, there are no more chunks, or the 943 // number of chunks in the uploadheap has dropped below the minUploadHeapSize 944 func (r *Renter) managedRepairLoop(hosts map[string]struct{}) error { 945 // smallRepair indicates whether or not the repair loop should process all 946 // of the chunks in the heap instead of just processing down to the minimum 947 // heap size. We want to process all of the chunks if the rest of the 948 // directory heap is in good health and there are no more chunks that could 949 // be added to the heap. 950 smallRepair := r.directoryHeap.managedPeekHealth() < RepairThreshold 951 952 // Limit the amount of time spent in each iteration of the repair loop so 953 // that changes to the directory heap take effect sooner rather than later. 954 repairBreakTime := time.Now().Add(maxRepairLoopTime) 955 956 // Work through the heap repairing chunks until heap is empty for 957 // smallRepairs or heap drops below minUploadHeapSize for larger repairs, or 958 // until the total amount of time spent in one repair iteration has elapsed. 959 for r.uploadHeap.managedLen() >= minUploadHeapSize || smallRepair || time.Now().After(repairBreakTime) { 960 select { 961 case <-r.tg.StopChan(): 962 // Return if the renter has shut down. 963 return errors.New("Repair loop interrupted because renter is shutting down") 964 default: 965 } 966 967 // Return if the renter is not online. 968 if !r.g.Online() { 969 return errors.New("repair loop returned early due to the renter been offline") 970 } 971 972 // Check if there is work by trying to pop off the next chunk from the 973 // heap. 974 nextChunk := r.uploadHeap.managedPop() 975 if nextChunk == nil { 976 // The heap is empty so reset it to free memory and return. 977 r.uploadHeap.managedReset() 978 return nil 979 } 980 981 // Make sure we have enough workers for this chunk to reach minimum 982 // redundancy. 983 r.staticWorkerPool.mu.RLock() 984 availableWorkers := len(r.staticWorkerPool.workers) 985 r.staticWorkerPool.mu.RUnlock() 986 if availableWorkers < nextChunk.minimumPieces { 987 // If the chunk is not stuck, check whether there are enough hosts 988 // in the allowance to support the chunk. 989 if !nextChunk.stuck { 990 // There are not enough available workers for the chunk to reach 991 // minimum redundancy. Check if the allowance has enough hosts 992 // for the chunk to reach minimum redundancy 993 allowance := r.hostContractor.Allowance() 994 if allowance.Hosts < uint64(nextChunk.minimumPieces) { 995 // There are not enough hosts in the allowance for this 996 // chunk to reach minimum redundancy. Log an error, set the 997 // chunk as stuck, and close the file 998 r.log.Printf("WARN: allownace had insufficient hosts for chunk to reach minimum redundancy, have %v need %v for chunk %v", allowance.Hosts, nextChunk.minimumPieces, nextChunk.id) 999 err := nextChunk.fileEntry.SetStuck(nextChunk.index, true) 1000 if err != nil { 1001 r.log.Debugln("WARN: unable to mark chunk as stuck:", err, nextChunk.id) 1002 } 1003 } 1004 } 1005 1006 // There are enough hosts set in the allowance so this is a 1007 // temporary issue with available workers, just ignore the chunk 1008 // for now and close the file 1009 err := nextChunk.fileEntry.Close() 1010 if err != nil { 1011 r.log.Debugln("WARN: unable to close file:", err, nextChunk.fileEntry.SiaFilePath()) 1012 } 1013 // Remove the chunk from the repairingChunks map 1014 r.uploadHeap.managedMarkRepairDone(nextChunk.id) 1015 continue 1016 } 1017 1018 // Perform the work. managedPrepareNextChunk will block until 1019 // enough memory is available to perform the work, slowing this 1020 // thread down to using only the resources that are available. 1021 err := r.managedPrepareNextChunk(nextChunk, hosts) 1022 if err != nil { 1023 // An error was return which means the renter was unable to allocate 1024 // memory for the repair. Since that is not an issue with the file 1025 // we will just close the chunk file entry instead of marking it as 1026 // stuck 1027 r.log.Debugln("WARN: unable to prepare next chunk without issues", err, nextChunk.id) 1028 err = nextChunk.fileEntry.Close() 1029 if err != nil { 1030 r.log.Debugln("WARN: unable to close file:", err, nextChunk.fileEntry.SiaFilePath()) 1031 } 1032 // Remove the chunk from the repairingChunks map 1033 r.uploadHeap.managedMarkRepairDone(nextChunk.id) 1034 continue 1035 } 1036 } 1037 return nil 1038 } 1039 1040 // threadedUploadAndRepair is a background thread that maintains a queue of 1041 // chunks to repair. This thread attempts to prioritize repairing files and 1042 // chunks with the lowest health, and attempts to keep heavy throughput 1043 // sustained for data upload as long as there is at least one chunk in need of 1044 // upload or repair. 1045 func (r *Renter) threadedUploadAndRepair() { 1046 err := r.tg.Add() 1047 if err != nil { 1048 return 1049 } 1050 defer r.tg.Done() 1051 1052 // Perpetual loop to scan for more files and add chunks to the uploadheap. 1053 // The loop assumes that the heap has already been initialized (either at 1054 // startup, or after sleeping) and does checks to see whether there is any 1055 // work required. If there is not any work required, the loop will sleep 1056 // until woken up. If there is work required, the loop will begin to process 1057 // the chunks and directories in the repair heaps. 1058 // 1059 // After 'repairLoopResetFrequency', the repair loop will be reset. This 1060 // adds a layer of robustness in case the repair loop gets stuck or can't 1061 // work through the full heap quickly because the user keeps uploading new 1062 // files and keeping a minimum number of chunks in the repair heap. 1063 resetTime := time.Now().Add(repairLoopResetFrequency) 1064 for { 1065 // Return if the renter has shut down. 1066 select { 1067 case <-r.tg.StopChan(): 1068 return 1069 default: 1070 } 1071 1072 // Wait until the renter is online to proceed. This function will return 1073 // 'false' if the renter has shut down before being online. 1074 if !r.managedBlockUntilOnline() { 1075 return 1076 } 1077 // Refresh the worker set. 1078 hosts := r.managedRefreshHostsAndWorkers() 1079 1080 // If enough time has elapsed to trigger a directory reset, reset the 1081 // directory. 1082 if time.Now().After(resetTime) { 1083 resetTime = time.Now().Add(repairLoopResetFrequency) 1084 r.directoryHeap.managedReset() 1085 err = r.managedPushUnexploredDirectory(modules.RootSiaPath()) 1086 if err != nil { 1087 r.log.Println("WARN: error re-initializing the directory heap:", err) 1088 } 1089 } 1090 1091 // Add any chunks from the backup heap that need to be repaired. This 1092 // needs to be handled separately because currently the filesystem for 1093 // storing system files and chunks such as those related to snapshot 1094 // backups is different from the siafileset that stores non-system files 1095 // and chunks. 1096 heapLen := r.uploadHeap.managedLen() 1097 r.managedBuildChunkHeap(modules.RootSiaPath(), hosts, targetBackupChunks) 1098 numBackupchunks := r.uploadHeap.managedLen() - heapLen 1099 if numBackupchunks > 0 { 1100 r.log.Println("Added", numBackupchunks, "backup chunks to the upload heap") 1101 } 1102 1103 // Check if there is work to do. If the filesystem is healthy and the 1104 // heap is empty, there is no work to do and the thread should block 1105 // until there is work to do. 1106 if r.uploadHeap.managedLen() == 0 && r.directoryHeap.managedPeekHealth() < RepairThreshold { 1107 // TODO: This has a tiny window where it might be dumping out chunks 1108 // that need health, if the upload call is appending to the 1109 // directory heap because there is a new upload. 1110 // 1111 // I believe that a good fix for this would be to change the upload 1112 // heap so that it performs a rapid bubble before trying to insert 1113 // the chunks into the heap. Then, even if a reset is triggered, 1114 // because a rapid bubble has already completed updating the health 1115 // of the root dir, it will be considered fairly. 1116 r.directoryHeap.managedReset() 1117 1118 // If the file system is healthy then block until there is a new 1119 // upload or there is a repair that is needed. 1120 select { 1121 case <-r.uploadHeap.newUploads: 1122 r.log.Debugln("repair loop triggered by new upload channel") 1123 case <-r.uploadHeap.repairNeeded: 1124 r.log.Debugln("repair loop triggered by repair needed channel") 1125 case <-r.tg.StopChan(): 1126 return 1127 } 1128 1129 err = r.managedPushUnexploredDirectory(modules.RootSiaPath()) 1130 if err != nil { 1131 // If there is an error initializing the directory heap log 1132 // the error. We don't want to sleep here as we were trigger 1133 // to repair chunks so we don't want to delay the repair if 1134 // there are chunks in the upload heap already. 1135 r.log.Println("WARN: error re-initializing the directory heap:", err) 1136 } 1137 1138 // Continue here to force the code to re-check for backups, to 1139 // re-block until it's online, and to refresh the worker pool. 1140 continue 1141 } 1142 1143 // Add chunks to heap. 1144 dirSiaPaths := make(map[modules.SiaPath]struct{}) 1145 dirSiaPaths, err = r.managedAddChunksToHeap(hosts) 1146 if err != nil { 1147 // Log the error but don't sleep as there are potentially chunks in 1148 // the heap from new uploads. If the heap is empty the next check 1149 // will catch that and handle it as an error 1150 r.log.Debugln("WARN: error adding chunks to the heap:", err) 1151 } 1152 1153 // There are benign edge cases where the heap will be empty after chunks 1154 // have been added. For example, if a chunk has gotten more healthy 1155 // since the last health check due to one of its hosts coming back 1156 // online. In these cases, the best course of action is to proceed with 1157 // the repair and move on to the next directories in the directory heap. 1158 // The repair loop will return immediately if it is given little or no 1159 // work but it can see that there is more work that it could be given. 1160 1161 r.log.Debugln("Executing an upload and repair cycle, uploadHeap has", r.uploadHeap.managedLen(), "chunks in it") 1162 err = r.managedRepairLoop(hosts) 1163 if err != nil { 1164 // If there was an error with the repair loop sleep for a little bit 1165 // and then try again. Here we do not skip to the next iteration as 1166 // we want to call bubble on the impacted directories 1167 r.log.Println("WARN: there was an error in the repair loop:", err) 1168 select { 1169 case <-time.After(uploadAndRepairErrorSleepDuration): 1170 case <-r.tg.StopChan(): 1171 return 1172 } 1173 } 1174 1175 // Call threadedBubbleMetadata to update the filesystem. 1176 for dirSiaPath := range dirSiaPaths { 1177 // We call bubble in a go routine so that it is not a bottle neck 1178 // for the repair loop iterations. This however can lead to some 1179 // additional unneeded cycles of the repair loop as a result of when 1180 // these bubbles reach root. This cycles however will be handled and 1181 // can be seen in the logs. 1182 go r.threadedBubbleMetadata(dirSiaPath) 1183 } 1184 } 1185 }