gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/projectchunkworkerset.go (about) 1 package renter 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 "github.com/opentracing/opentracing-go" 10 "gitlab.com/NebulousLabs/fastrand" 11 "gitlab.com/SkynetLabs/skyd/build" 12 "gitlab.com/SkynetLabs/skyd/persist" 13 "gitlab.com/SkynetLabs/skyd/skymodules" 14 "go.sia.tech/siad/crypto" 15 "go.sia.tech/siad/modules" 16 "go.sia.tech/siad/types" 17 18 "gitlab.com/NebulousLabs/errors" 19 ) 20 21 var ( 22 // ErrRootNotFound is returned if all workers were unable to recover the 23 // root 24 ErrRootNotFound = errors.New("workers were unable to recover the data by sector root - all workers failed") 25 26 // ErrSkylinkDeleted is returned if the registry for a resolver skylink 27 // was set to the sentinel value for deleted skylinks. 28 ErrSkylinkDeleted = errors.New("resolver skylink was deleted") 29 30 // ErrProjectTimedOut is returned when the project timed out 31 ErrProjectTimedOut = errors.New("project timed out") 32 33 // pcwsWorkerStateResetTime defines the amount of time that the pcws will 34 // wait before resetting / refreshing the worker state, meaning that all of 35 // the workers will do another round of HasSector queries on the network. 36 pcwsWorkerStateResetTime = build.Select(build.Var{ 37 Dev: time.Hour, 38 Standard: time.Hour * 9, 39 Testing: time.Minute * 5, 40 }).(time.Duration) 41 42 // pcwsHasSectorTimeout defines the amount of time that the pcws will wait 43 // before giving up on receiving a HasSector response from a single worker. 44 // This value is set as a global timeout because different download queries 45 // that have different timeouts will use the same projectChunkWorkerSet. 46 pcwsHasSectorTimeout = build.Select(build.Var{ 47 Dev: time.Minute * 1, 48 Standard: time.Minute * 3, 49 Testing: time.Second * 30, 50 }).(time.Duration) 51 ) 52 53 const ( 54 // maxOverdriveWorkers defines the maximum amount of overdrive workers to 55 // select as part of a worker set 56 maxOverdriveWorkers = 10 57 ) 58 59 // pcwsUnresolvedWorker tracks an unresolved worker that is associated with a 60 // specific projectChunkWorkerSet. The timestamp indicates when the unresolved 61 // worker is expected to have a resolution, and is an estimate based on historic 62 // performance from the worker. 63 type pcwsUnresolvedWorker struct { 64 // The expected time that the worker will resolve. A worker is considered 65 // resolved if the HasSector job has finished. 66 staticExpectedResolvedTime time.Time 67 68 // The worker that is performing the HasSector job. 69 staticWorker *worker 70 } 71 72 // pcwsWorkerResponse contains a worker's response to a HasSector query. There 73 // is a list of piece indices where the worker responded that they had the piece 74 // at that index. There is also an error field that will be set in the event an 75 // error occurred while performing the HasSector query. 76 type pcwsWorkerResponse struct { 77 worker *worker 78 pieceIndices []uint64 79 err error 80 } 81 82 // pcwsWorkerState contains the worker state for a single thread that is 83 // resolving which workers have which pieces. When the projectChunkWorkerSet 84 // resets, it does so by spinning up a new pcwsWorkerState and then replacing 85 // the old worker state with the new worker state. The new worker state will 86 // send out a new round of HasSector queries to the network. 87 type pcwsWorkerState struct { 88 // unresolvedWorkers is the set of workers that are currently running 89 // HasSector programs and have not yet finished. 90 // 91 // A map is used so that workers can be removed from the set in constant 92 // time as they complete their HasSector jobs. 93 unresolvedWorkers map[string]*pcwsUnresolvedWorker 94 95 // ResolvedWorkers is an array that tracks which workers have responded to 96 // HasSector queries and which sectors are available. This array is only 97 // appended to as workers come back, meaning that chunk downloads can track 98 // internally which elements of the array they have already looked at, 99 // saving computational time when updating. 100 resolvedWorkers []pcwsWorkerResponse 101 102 // workerUpdateChans is used by download objects to block until more 103 // information about the unresolved workers is available. All of the worker 104 // update chans will be closed each time an unresolved worker returns a 105 // response (regardless of whether the response is positive or negative). 106 // The array will then be cleared. 107 // 108 // NOTE: Once 'unresolvedWorkers' has a length of zero, any attempt to add a 109 // channel to the set of workerUpdateChans should fail, as there will be no 110 // more updates. This is specific to this particular worker state, the 111 // pcwsWorkerSet as a whole can be reset by replacing the worker state. 112 workerUpdateChans []chan struct{} 113 114 // Utilities. 115 staticDeps skymodules.SkydDependencies 116 staticLog *persist.Logger 117 staticWorkerPool *workerPool 118 mu sync.Mutex 119 } 120 121 // projectChunkWorkerSet is an object that contains a set of workers that can be 122 // used to download a single chunk. The object can be initialized with either a 123 // set of roots (for Skynet downloads) or with a siafile where the host-root 124 // pairs are already known (for traditional renter downloads). 125 // 126 // If the pcws is initialized with only a set of roots, it will immediately spin 127 // up a bunch of worker jobs to locate those roots on the network using 128 // HasSector programs. 129 // 130 // Once the pcws has been initialized, it can be used repeatedly to download 131 // data from the chunk, and it will not need to repeat the network lookups. 132 // Every few hours (pcwsWorkerStateResetTime), it will re-do the lookups to 133 // ensure that it is up-to-date on the best way to download the file. 134 type projectChunkWorkerSet struct { 135 // workerState is a pointer to a single pcwsWorkerState, specifically the 136 // most recent worker state that has launched. The workerState is 137 // responsible for querying the network with HasSector requests and 138 // determining which workers are able to download which pieces of the chunk. 139 // 140 // workerStateLaunchTime indicates when the workerState was launched, which 141 // is used to figure out when the worker state should be refreshed. 142 // 143 // updateInProgress and updateFinishedChan are used to ensure that only one 144 // worker state is being refreshed at a time. Before a workerState refresh 145 // begins, the projectChunkWorkerSet is locked and the updateInProgress 146 // value is set to 'true'. At the same time, a new 'updateFinishedChan' is 147 // created. Then the projectChunkWorkerSet is unlocked. New threads that try 148 // to launch downloads will see that there is an update in progress and will 149 // wait on the 'updateFinishedChan' to close before grabbing the new 150 // workerState. When the new workerState is done being initialized, the 151 // projectChunkWorkerSet is locked and the updateInProgress field is set to 152 // false, the workerState is updated to the new state, and the 153 // updateFinishedChan is closed. 154 updateInProgress bool 155 updateFinishedChan chan struct{} 156 workerState *pcwsWorkerState 157 workerStateLaunchTime time.Time 158 159 // Decoding and decryption information for the chunk. 160 staticChunkIndex uint64 161 staticErasureCoder skymodules.ErasureCoder 162 staticMasterKey crypto.CipherKey 163 staticPieceRoots []crypto.Hash 164 165 // Stats 166 staticBaseSectorDownloadStats *skymodules.DownloadOverdriveStats 167 staticFanoutSectorDownloadStats *skymodules.DownloadOverdriveStats 168 169 // Utilities 170 staticCtx context.Context 171 staticDeps skymodules.SkydDependencies 172 staticWorkerPool *workerPool 173 staticLog *persist.Logger 174 mu sync.Mutex 175 } 176 177 // chunkFetcher is an interface that exposes a download function, the PCWS 178 // implements this interface. 179 type chunkFetcher interface { 180 Download(ctx context.Context, pricePerMS types.Currency, offset, length uint64, lowPrio bool) (chan *downloadResponse, error) 181 } 182 183 // Download will download a range from a chunk. 184 func (pcws *projectChunkWorkerSet) Download(ctx context.Context, pricePerMS types.Currency, offset, length uint64, lowPrio bool) (chan *downloadResponse, error) { 185 return pcws.managedDownload(ctx, pricePerMS, offset, length, lowPrio) 186 } 187 188 // closeUpdateChans will close all of the update chans and clear out the slice. 189 // This will cause any threads waiting for more results from the unresolved 190 // workers to unblock. 191 // 192 // Typically there will be a small number of channels, often 0 and often just 1. 193 func (ws *pcwsWorkerState) closeUpdateChans() { 194 for _, c := range ws.workerUpdateChans { 195 close(c) 196 } 197 ws.workerUpdateChans = nil 198 } 199 200 // registerForWorkerUpdate will create a channel and append it to the list of 201 // update chans in the worker state. When there is more information available 202 // about which worker is the best worker to select, the channel will be closed. 203 func (ws *pcwsWorkerState) registerForWorkerUpdate() <-chan struct{} { 204 // Return a nil channel if there are no more unresolved workers. 205 if len(ws.unresolvedWorkers) == 0 { 206 return nil 207 } 208 209 // Create the channel that will be closed when the set of unresolved workers 210 // has been updated. 211 c := make(chan struct{}) 212 ws.workerUpdateChans = append(ws.workerUpdateChans, c) 213 return c 214 } 215 216 // removeUnresolvedWorker removes an unresolved worker from the worker state. 217 func (ws *pcwsWorkerState) removeUnresolvedWorker(hpk string) { 218 uw, exists := ws.unresolvedWorkers[hpk] 219 if exists { 220 staticPoolUnresolvedWorkers.Put(uw) 221 delete(ws.unresolvedWorkers, hpk) 222 } 223 224 // If the map is empty now, release some memory. 225 if len(ws.unresolvedWorkers) == 0 { 226 ws.unresolvedWorkers = make(map[string]*pcwsUnresolvedWorker) 227 } 228 } 229 230 // managedHandleResponse will handle a HasSector response from a worker, 231 // updating the workerState accordingly. 232 // 233 // The worker's response will be included into the resolvedWorkers even if it is 234 // emptied or errored because the worker selection algorithms in the downloads 235 // may wish to be able to view which workers have failed. This is currently 236 // unused, but certain computational optimizations in the future depend on it. 237 func (ws *pcwsWorkerState) managedHandleResponse(resp *jobHasSectorResponse) { 238 ws.mu.Lock() 239 defer ws.mu.Unlock() 240 241 // Defer closing the update chans to signal we've received and processed an 242 // HS response. 243 defer ws.closeUpdateChans() 244 245 // Delete the worker from the set of unresolved workers. 246 w := resp.staticWorker 247 if w == nil { 248 ws.staticLog.Critical("nil worker provided in resp") 249 } 250 251 // Return the worker to the pool and delete it from the map. 252 ws.removeUnresolvedWorker(w.staticHostPubKeyStr) 253 254 // If the response contained an error, add this worker to the set of 255 // resolved workers as supporting no indices. 256 if resp.staticErr != nil { 257 ws.resolvedWorkers = append(ws.resolvedWorkers, pcwsWorkerResponse{ 258 worker: w, 259 err: resp.staticErr, 260 }) 261 return 262 } 263 264 // Add this worker to the set of resolved workers (even if there are no 265 // indices that the worker can fetch). 266 ws.resolvedWorkers = append(ws.resolvedWorkers, pcwsWorkerResponse{ 267 worker: w, 268 pieceIndices: resp.staticAvailbleIndices, 269 }) 270 } 271 272 // managedRegisterForWorkerUpdate will create a channel and append it to the 273 // list of update chans in the worker state. When there is more information 274 // available about which worker is the best worker to select, the channel will 275 // be closed. 276 func (ws *pcwsWorkerState) managedRegisterForWorkerUpdate() <-chan struct{} { 277 ws.mu.Lock() 278 defer ws.mu.Unlock() 279 return ws.registerForWorkerUpdate() 280 } 281 282 // WaitForResults waits for all workers of the state to resolve up until ctx is 283 // closed. Once the ctx is closed, all available responses are returned. 284 func (ws *pcwsWorkerState) WaitForResults(ctx context.Context) []pcwsWorkerResponse { 285 for { 286 ws.mu.Lock() 287 rw := ws.resolvedWorkers 288 noUnresolvedWorkers := len(ws.unresolvedWorkers) == 0 289 updateChan := ws.registerForWorkerUpdate() 290 ws.mu.Unlock() 291 292 // If there are no more unresolved workers, we are done. 293 if noUnresolvedWorkers { 294 return rw 295 } 296 297 // Otherwise we wait for either an update or the timeout. 298 select { 299 case <-updateChan: 300 continue 301 case <-ctx.Done(): 302 // Timeout reached. Return what we got. 303 return rw 304 } 305 } 306 } 307 308 // managedLaunchWorker will launch a job to determine which sectors of a chunk 309 // are available through that worker. The resulting unresolved worker is 310 // returned so it can be added to the pending worker state. 311 func (pcws *projectChunkWorkerSet) managedLaunchWorker(w *worker, responseChan chan *jobHasSectorResponse, ws *pcwsWorkerState) error { 312 // Check for gouging. 313 cache := w.staticCache() 314 pt := w.staticPriceTable().staticPriceTable 315 numWorkers := pcws.staticWorkerPool.callNumWorkers() 316 317 err := staticPCWSGougingCache.IsGouging(w.staticHostPubKeyStr, pt, cache.staticRenterAllowance, numWorkers, len(pcws.staticPieceRoots)) 318 if err != nil { 319 return errors.AddContext(err, fmt.Sprintf("price gouging for chunk worker set detected in worker %v", w.staticHostPubKeyStr)) 320 } 321 322 // Check whether the worker is on a cooldown. Because the PCWS is cached, we 323 // do not want to exclude this worker if it is on a cooldown, however we do 324 // want to take into consideration the cooldown period when we estimate the 325 // expected resolve time. 326 var coolDownPenalty time.Duration 327 if w.managedOnMaintenanceCooldown() { 328 wms := w.staticMaintenanceState 329 wms.mu.Lock() 330 coolDownPenalty = time.Until(wms.cooldownUntil) 331 wms.mu.Unlock() 332 } 333 334 // Don't bother scheduling a job if the worker doesn't have a valid 335 // price table. 336 if wpt := w.staticPriceTable(); !wpt.staticValid() { 337 return errInvalidPriceTable 338 } 339 340 // Create and launch the job. 341 ctx, cancel := context.WithTimeout(pcws.staticCtx, pcwsHasSectorTimeout) 342 jhs := w.newJobHasSectorWithPostExecutionHook(ctx, responseChan, func(resp *jobHasSectorResponse) { 343 ws.managedHandleResponse(resp) 344 staticPoolJobHasSectorResponse.Put(resp) 345 cancel() 346 }, pcws.staticErasureCoder.NumPieces(), pcws.staticPieceRoots...) 347 348 expectedJobTime, err := w.staticJobHasSectorQueue.callAddWithEstimate(jhs, pcwsHasSectorTimeout) 349 if err != nil { 350 cancel() 351 return errors.AddContext(err, fmt.Sprintf("unable to add has sector job to %v", w.staticHostPubKeyStr)) 352 } 353 expectedResolveTime := expectedJobTime.Add(coolDownPenalty) 354 355 // Create the unresolved worker for this job. 356 uw := staticPoolUnresolvedWorkers.Get() 357 uw.staticWorker = w 358 uw.staticExpectedResolvedTime = expectedResolveTime 359 360 // Add the unresolved worker to the worker state. Technically this doesn't 361 // need to be wrapped in a lock, but that's not obvious from the function 362 // context so we wrap it in a lock anyway. There will be no contention, so 363 // there should be minimal performance overhead. 364 ws.mu.Lock() 365 ws.unresolvedWorkers[w.staticHostPubKeyStr] = uw 366 ws.mu.Unlock() 367 return nil 368 } 369 370 // managedLaunchWorkers will spin up a bunch of jobs to determine which workers 371 // have what pieces for the pcws, and then update the input worker state with 372 // the results. 373 func (pcws *projectChunkWorkerSet) managedLaunchWorkers(ws *pcwsWorkerState) { 374 // Launch all of the HasSector jobs for each worker. A channel is needed to 375 // receive the responses, and the channel needs to be buffered to be equal 376 // in size to the number of queries so that none of the workers sending 377 // reponses get blocked sending down the channel. 378 workers := ws.staticWorkerPool.callWorkers() 379 380 responseChan := make(chan *jobHasSectorResponse, len(workers)) 381 for _, w := range workers { 382 err := pcws.managedLaunchWorker(w, responseChan, ws) 383 if err != nil && !errors.Contains(err, errEstimateAboveMax) { 384 pcws.staticLog.Debugf("failed to launch worker: %v", err) 385 } 386 } 387 } 388 389 // managedWorkerState returns a pointer to the current worker state object 390 func (pcws *projectChunkWorkerSet) managedWorkerState() *pcwsWorkerState { 391 pcws.mu.Lock() 392 defer pcws.mu.Unlock() 393 return pcws.workerState 394 } 395 396 // managedTryUpdateWorkerState will check whether the worker state needs to be 397 // refreshed. If so, it will refresh the worker state. 398 func (pcws *projectChunkWorkerSet) managedTryUpdateWorkerState() error { 399 // The worker state does not need to be refreshed if it is recent or if 400 // there is another refresh currently in progress. 401 pcws.mu.Lock() 402 if pcws.updateInProgress || time.Since(pcws.workerStateLaunchTime) < pcwsWorkerStateResetTime { 403 c := pcws.updateFinishedChan 404 pcws.mu.Unlock() 405 // If there is no update in progress, the channel will already be 406 // closed, and therefore listening on the channel will return 407 // immediately. 408 <-c 409 return nil 410 } 411 // An update is needed. Set the flag that an update is in progress. 412 pcws.updateInProgress = true 413 pcws.updateFinishedChan = make(chan struct{}) 414 pcws.mu.Unlock() 415 416 // Create the new worker state and launch the thread that will create worker 417 // jobs and collect responses from the workers. 418 // 419 // The concurrency here is a bit awkward because jobs cannot be launched 420 // while the pcws lock is held, the workerState of the pcws cannot be set 421 // until all the jobs are launched, and the context for timing out the 422 // worker jobs needs to be created in the same thread that listens for the 423 // responses. Though there are a lot of concurrency patterns at play here, 424 // it was the cleanest thing I could come up with. 425 numWorkers := pcws.staticWorkerPool.callNumWorkers() 426 ws := &pcwsWorkerState{ 427 unresolvedWorkers: make(map[string]*pcwsUnresolvedWorker, numWorkers), 428 resolvedWorkers: make([]pcwsWorkerResponse, 0, numWorkers), 429 430 staticDeps: pcws.staticDeps, 431 staticLog: pcws.staticLog, 432 staticWorkerPool: pcws.staticWorkerPool, 433 } 434 435 // Launch the thread to find the workers for this launch state. 436 pcws.managedLaunchWorkers(ws) 437 438 // Update the pcws so that the workerState in the pcws is the newest worker 439 // state. 440 pcws.mu.Lock() 441 pcws.updateInProgress = false 442 pcws.workerState = ws 443 pcws.workerStateLaunchTime = time.Now() 444 pcws.mu.Unlock() 445 close(pcws.updateFinishedChan) 446 return nil 447 } 448 449 // managedDownload will download a range from a chunk. This call is 450 // asynchronous. It will return as soon as the initial sector download requests 451 // have been sent to the workers. This means that it will block until enough 452 // workers have reported back with HasSector results that the optimal download 453 // request can be made. Where possible, the projectChunkWorkerSet should be 454 // created in advance of the download call, so that the HasSector calls have as 455 // long as possible to complete, reducing the latency of the actual download 456 // call. 457 // 458 // Blocking until all of the piece downloads have been put into job queues 459 // ensures that the workers will account for the bandwidth overheads associated 460 // with these jobs before new downloads are requested. Multiple calls to 461 // 'managedDownload' from the same thread will generally follow the rule that 462 // the first calls will return first. This rule cannot be enforced if the call 463 // to managedDownload returns before the download jobs are queued into the 464 // workers. 465 // 466 // pricePerMS is "price per millisecond". This gives the download code a budget 467 // to spend on faster workers. For example, if a faster set of workers is 468 // expected to trim 100 milliseconds off of the download time, the download code 469 // will select those workers only if the additional expense of using those 470 // workers is less than 100 * pricePerMS. 471 func (pcws *projectChunkWorkerSet) managedDownload(ctx context.Context, pricePerMS types.Currency, offset, length uint64, lowPrio bool) (chan *downloadResponse, error) { 472 // Potentially force a timeout via a disrupt for testing. 473 if pcws.staticDeps.Disrupt("timeoutProjectDownloadByRoot") { 474 return nil, errors.Compose(ErrProjectTimedOut, ErrRootNotFound) 475 } 476 477 // Convenience variables. 478 ec := pcws.staticErasureCoder 479 480 // Start a span for the PDC. 481 _, ctx = opentracing.StartSpanFromContext(ctx, "managedDownload") 482 483 // Depending on the encryption type we might have to download the entire 484 // entire chunk. For the ciphers we support, this will be the case when the 485 // overhead is not zero. This is due to the overhead being a checksum that 486 // has to be verified against the entire piece. 487 // 488 // NOTE: These checks assume that any upload with encryption overhead needs 489 // to be downloaded as full sectors. This feels reasonable because smaller 490 // sectors were not supported when encryption schemes with overhead were 491 // being suggested. 492 if pcws.staticMasterKey.Type().Overhead() != 0 && (offset != 0 || length != modules.SectorSize*uint64(ec.MinPieces())) { 493 return nil, errors.New("invalid request performed - this chunk has encryption overhead and therefore the full chunk must be downloaded") 494 } 495 496 // Refresh the pcws. This will only cause a refresh if one is necessary. 497 err := pcws.managedTryUpdateWorkerState() 498 if err != nil { 499 return nil, errors.AddContext(err, "unable to initiate download") 500 } 501 502 // After refresh, grab the worker state. 503 ws := pcws.managedWorkerState() 504 505 // Determine the offset and length that needs to be downloaded from the 506 // pieces. This is non-trivial because both the network itself and also the 507 // erasure coder have required segment sizes. 508 pieceOffset, pieceLength := GetPieceOffsetAndLen(ec, offset, length) 509 510 // If the pricePerMS is zero, initialize it to 1H to avoid division by zero, 511 // or multiplication by zero, possibly resulting in unwanted side-effects in 512 // the worker selection and/or any other algorithms. 513 if pricePerMS.IsZero() { 514 pricePerMS = types.NewCurrency64(1) 515 } 516 517 // Build static piece indices, chimera workers use this static piece index 518 // array to avoid creating it for every chimera worker over and over again. 519 pieceIndices := make([]uint64, ec.NumPieces()) 520 for i := 0; i < len(pieceIndices); i++ { 521 pieceIndices[i] = uint64(i) 522 } 523 524 // Build the full pdc. 525 pdc := &projectDownloadChunk{ 526 lengthInChunk: length, 527 offsetInChunk: offset, 528 529 pieceOffset: pieceOffset, 530 pieceLength: pieceLength, 531 532 pricePerMS: pricePerMS, 533 534 workerProgressMap: make(map[uint32]workerProgress), 535 536 piecesData: make([][]byte, ec.NumPieces()), 537 piecesProofs: make([][]crypto.Hash, ec.NumPieces()), 538 piecesInfo: make([]pieceInfo, ec.NumPieces()), 539 540 staticIsLowPrio: lowPrio, 541 staticLaunchTime: time.Now(), 542 543 staticBaseSectorDownloadStats: pcws.staticBaseSectorDownloadStats, 544 staticFanoutSectorDownloadStats: pcws.staticFanoutSectorDownloadStats, 545 546 staticPieceIndices: pieceIndices, 547 ctx: ctx, 548 workerResponseChan: make(chan *jobReadResponse), 549 downloadResponseChan: make(chan *downloadResponse, 1), 550 workerSet: pcws, 551 workerState: ws, 552 } 553 554 // Set debug variables on the pdc 555 fastrand.Read(pdc.uid[:]) 556 557 // Launch the download project in a separate go routine 558 go pdc.threadedLaunchProjectDownload() 559 560 return pdc.downloadResponseChan, nil 561 } 562 563 // newPCWSByRoots will create a worker set to download a chunk given just the 564 // set of sector roots associated with the pieces. The hosts that correspond to 565 // the roots will be determined by scanning the network with a large number of 566 // HasSector queries. Once opened, the projectChunkWorkerSet can be used to 567 // initiate many downloads. If it is already known what pieces a worker is 568 // expected to have, it can be provided as a seedWorker. A seedWorker is 569 // considered to be resolved right away. 570 func (r *Renter) newPCWSByRoots(ctx context.Context, roots []crypto.Hash, ec skymodules.ErasureCoder, masterKey crypto.CipherKey, chunkIndex uint64) (*projectChunkWorkerSet, error) { 571 // Check that the number of roots provided is consistent with the erasure 572 // coder provided. 573 // 574 // NOTE: There's a legacy special case where 1-of-N only needs 1 root. 575 if len(roots) != ec.NumPieces() && !(len(roots) == 1 && ec.MinPieces() == 1) { 576 return nil, fmt.Errorf("%v roots provided, but erasure coder specifies %v pieces", len(roots), ec.NumPieces()) 577 } 578 579 // Check if enough roots are known. 580 var knownRoots int 581 for _, root := range roots { 582 if root != (crypto.Hash{}) { 583 knownRoots++ 584 } 585 } 586 if knownRoots < ec.MinPieces() { 587 return nil, fmt.Errorf("only %v roots are known which is smaller than the minimum of %v", knownRoots, ec.MinPieces()) 588 } 589 590 // Check that the given cipher is not nil, if no encryption is required a 591 // plain text cipher key should be passed 592 if masterKey == nil { 593 return nil, errors.New("master key is nil, if no decryption is required pass a plaintext cipher key") 594 } 595 596 // Create the worker set. 597 pcws := &projectChunkWorkerSet{ 598 staticChunkIndex: chunkIndex, 599 staticErasureCoder: ec, 600 staticMasterKey: masterKey, 601 staticPieceRoots: roots, 602 603 staticBaseSectorDownloadStats: r.staticBaseSectorDownloadStats, 604 staticFanoutSectorDownloadStats: r.staticFanoutSectorDownloadStats, 605 606 staticCtx: ctx, 607 staticDeps: r.staticDeps, 608 staticLog: r.staticLog, 609 staticWorkerPool: r.staticWorkerPool, 610 } 611 612 // The worker state is blank, ensure that everything can get started. 613 err := pcws.managedTryUpdateWorkerState() 614 if err != nil { 615 return nil, errors.AddContext(err, "cannot create a new PCWS") 616 } 617 618 // Return the worker set. 619 return pcws, nil 620 }