gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/skylinkdatasource.go (about) 1 package renter 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 8 "github.com/opentracing/opentracing-go" 9 "gitlab.com/SkynetLabs/skyd/build" 10 "gitlab.com/SkynetLabs/skyd/skykey" 11 "gitlab.com/SkynetLabs/skyd/skymodules" 12 "go.sia.tech/siad/crypto" 13 "go.sia.tech/siad/modules" 14 "go.sia.tech/siad/types" 15 16 "gitlab.com/NebulousLabs/errors" 17 ) 18 19 var ( 20 // SkylinkDataSourceRequestSize is the size that is suggested by the data 21 // source to be used when reading data from it. 22 SkylinkDataSourceRequestSize = build.Select(build.Var{ 23 Dev: uint64(1 << 18), // 256 KiB 24 Standard: uint64(1 << 20), // 1 MiB 25 Testing: uint64(1 << 9), // 512 B 26 }).(uint64) 27 28 // chunkFetchersMaximumPreload defines the amount of chunk fetchers we 29 // preload when the data source gets created. The remaining chunk fetchers 30 // are lazy loaded when data sections get created, which in turn are also 31 // created before they are being read from, giving those PCWS some buffer to 32 // execute their has sector jobs before being used to download the chunk. 33 chunkFetchersMaximumPreload = build.Select(build.Var{ 34 Dev: 3, 35 Standard: 16, // ~32MiB worth of PCWS 36 Testing: 3, 37 }).(int) 38 ) 39 40 type ( 41 // skylinkDataSource implements streamBufferDataSource on a Skylink. 42 // Notably, it creates a pcws for every single chunk in the Skylink and 43 // keeps them in memory, to reduce latency on seeking through the file. 44 skylinkDataSource struct { 45 // Metadata. 46 staticID skymodules.DataSourceID 47 staticLayout skymodules.SkyfileLayout 48 staticMetadata skymodules.SkyfileMetadata 49 staticRawMetadata []byte 50 staticSkylink skymodules.Skylink 51 52 // staticSkylinkSector will contain the raw data of the sector 53 // referenced by the skylink. That way we can provide proofs for 54 // arbitrary ranges of the sector later. 55 // NOTE: The skylink sector is never decrypted to make sure the 56 // merkle proofs can still be verified against the sector root. 57 staticSkylinkSector []byte 58 59 // staticLayoutOffset is the offset of the layout within the 60 // sector. 61 staticLayoutOffset int 62 63 staticDecryptedSkylinkSector []byte 64 65 // staticChunkFetcherLoaders holds a loader function for every chunk in 66 // the fanout. This loaders creates a PCWS for that chunk, and thus 67 // spins up a bunch of has sector jobs. On initial read, we want to 68 // limit the amount of PCWS created, to avoid unnecessary network load. 69 // That is why we only preload a certain amount, and lazy load the 70 // others when a new data section gets created. 71 // 72 // staticChunkFetchers holds the PCWS after it became available 73 // 74 // staticChunkFetchersAvailable is a channel that gets closed by the 75 // loader after the PCWS got created and its state was updated for the 76 // first time 77 // 78 // staticChunkErrs contains a potential error, this error should be 79 // checked when the chunk fetcher becomes available 80 staticChunkFetcherLoaders []chunkFetcherLoaderFn 81 staticChunkFetchersAvailable []chan struct{} 82 staticChunkFetchers []chunkFetcher 83 staticChunkErrs []error 84 85 // Utilities 86 staticCtx context.Context 87 staticCancelFunc context.CancelFunc 88 } 89 90 // chunkFetcherLoaderFn is a helper type that represents a function that 91 // loads a chunk fetcher. Every chunk in the fanout will have a 92 // corresponding loader function that creates the PCWS for that chunk. 93 chunkFetcherLoaderFn func() 94 ) 95 96 // DataSize implements streamBufferDataSource 97 func (sds *skylinkDataSource) DataSize() uint64 { 98 return sds.staticLayout.Filesize 99 } 100 101 // ID implements streamBufferDataSource 102 func (sds *skylinkDataSource) ID() skymodules.DataSourceID { 103 return sds.staticID 104 } 105 106 // ReadBaseSectorPayload reads data from the data source's base sector payload. 107 // This will return an error when called on anything but a small skyfile. 108 func (sds *skylinkDataSource) ReadBaseSectorPayload(off, length uint64) (*downloadResponse, error) { 109 return sds.managedReadSmallFileSection(off, length) 110 } 111 112 // HasRecursiveFanout returns whether or not the datasource belongs to a skylink 113 // with a recursive fanout. 114 func (sds *skylinkDataSource) HasRecursiveFanout() bool { 115 return sds.staticLayout.HasRecursiveFanout(uint64(sds.staticLayoutOffset)) 116 } 117 118 // Layout implements streamBufferDataSource 119 func (sds *skylinkDataSource) Layout() skymodules.SkyfileLayout { 120 return sds.staticLayout 121 } 122 123 // RawLayout implements streamBufferDataSource 124 func (sds *skylinkDataSource) RawLayout() (skymodules.SkyfileLayout, []byte, []crypto.Hash) { 125 return sds.managedReadLayout() 126 } 127 128 // Metadata implements streamBufferDataSource 129 func (sds *skylinkDataSource) Metadata() skymodules.SkyfileMetadata { 130 return sds.staticMetadata 131 } 132 133 // RawMetadata implements streamBufferDataSource 134 func (sds *skylinkDataSource) RawMetadata() []byte { 135 return sds.staticRawMetadata 136 } 137 138 // RequestSize implements streamBufferDataSource 139 func (sds *skylinkDataSource) RequestSize() uint64 { 140 return SkylinkDataSourceRequestSize 141 } 142 143 // Skylink implements streamBufferDataSource 144 func (sds *skylinkDataSource) Skylink() skymodules.Skylink { 145 return sds.staticSkylink 146 } 147 148 // SilentClose implements streamBufferDataSource 149 func (sds *skylinkDataSource) SilentClose() { 150 // Canceling the context for the data source should be sufficient. As 151 // all child processes (such as the pcws for each chunk) should be using 152 // contexts derived from the sds context. 153 sds.staticCancelFunc() 154 } 155 156 // managedReadSection downloads a section for the given section index from the 157 // data source. It returns a response that can be awaited, 158 func (sds *skylinkDataSource) managedReadSection(ctx context.Context, sectionIndex uint64, pricePerMS types.Currency) (<-chan *downloadResponse, uint64, error) { 159 // Translate input to offset and fetchSize within DataSource. 160 off := sectionIndex * sds.RequestSize() 161 if off >= sds.staticLayout.Filesize { 162 return nil, 0, fmt.Errorf("ReadSection: offset out-of-bounds %v >= %v", off, sds.staticLayout.Filesize) 163 } 164 165 // We tolerate out-of-bounds if at least the offset was within bounds. 166 // That just means we are at the end of the file and there is not a full 167 // section left. 168 fetchSize := sds.RequestSize() 169 if off+fetchSize > sds.staticLayout.Filesize { 170 fetchSize = sds.staticLayout.Filesize - off 171 } 172 173 // If we are dealing with a small skyfile without fanout bytes, we can 174 // simply read from that and return early. 175 if sds.staticLayout.FanoutSize == 0 { 176 dr, err := sds.managedReadSmallFileSection(off, fetchSize) 177 respChan := make(chan *downloadResponse, 1) 178 respChan <- dr 179 close(respChan) 180 return respChan, 0, err 181 } 182 183 // Determine how large each uploaded chunk is. 184 chunkSize := skymodules.ChunkSize(sds.staticLayout.CipherType, uint64(sds.staticLayout.FanoutDataPieces)) 185 186 // The fetchSize should never exceed the size of a chunk. That way we 187 // only need to deal with a single set of proofs per data section and 188 // not multiple. 189 if fetchSize > chunkSize { 190 err := fmt.Errorf("ReadSection: fetchSize > SectorSize %v > %v", fetchSize, modules.SectorSize) 191 build.Critical(err) 192 return nil, 0, err 193 } 194 195 // Determine which chunk the offset is currently in. 196 chunkIndex := off / chunkSize 197 offsetInChunk := off % chunkSize 198 199 // Trigger the loader, this function will only be executed once and 200 // ensures the chunk fetcher is being initialized. Consecutive calls are 201 // essentially a no-op 202 sds.staticChunkFetcherLoaders[chunkIndex]() 203 204 // Wait until the chunk fetcher is ready, and check if there was any 205 // error in initializing the chunk fetcher. 206 select { 207 case <-sds.staticChunkFetchersAvailable[chunkIndex]: 208 case <-sds.staticCtx.Done(): 209 return nil, 0, errors.New("stream fetch aborted because of cancel") 210 } 211 if sds.staticChunkErrs[chunkIndex] != nil { 212 return nil, 0, errors.AddContext(sds.staticChunkErrs[chunkIndex], "unable to start download") 213 } 214 215 // Schedule the download. 216 respChan, err := sds.staticChunkFetchers[chunkIndex].Download(ctx, pricePerMS, offsetInChunk, fetchSize, false) 217 if err != nil { 218 return nil, 0, errors.AddContext(err, "unable to start download") 219 } 220 return respChan, chunkIndex, nil 221 } 222 223 // ReadFanout reads a single piece root from the fanout of the datasource and 224 // returns the proof for that root as well as the offset within the sector. 225 func (sds *skylinkDataSource) ReadFanout(chunkIndex, pieceIndex uint64) ([]byte, []crypto.Hash, uint32, error) { 226 layout := sds.staticLayout 227 228 // Get the offset of where the fanout starts within the base sector. 229 fanoutOff := layout.FanoutOffset(uint64(sds.staticLayoutOffset)) 230 231 // Get the offset of the right piece root within the fanout. If it is 232 // compressed, each chunk only contains 1 piece. So we can ignore the 233 // pieceIndex. 234 chunkOff := fanoutOff + layout.FanoutPiecesPerChunk()*chunkIndex*crypto.HashSize 235 rootOff := chunkOff + pieceIndex*crypto.HashSize 236 237 dr, err := sds.managedReadBaseSector(rootOff, crypto.HashSize) 238 if err != nil { 239 return nil, nil, 0, err 240 } 241 return dr.externDownloadedData.LogicalChunkData[0], dr.externDownloadedData.Proofs[0], uint32(rootOff), nil 242 } 243 244 // ReadSection implements streamBufferDataSource 245 func (sds *skylinkDataSource) ReadSection(ctx context.Context, sectionIndex uint64, pricePerMS types.Currency) (<-chan *downloadResponse, error) { 246 respChan, _, err := sds.managedReadSection(ctx, sectionIndex, pricePerMS) 247 return respChan, err 248 } 249 250 // managedDownloadByRoot will fetch data using the merkle root of that data. 251 func (r *Renter) managedDownloadByRoot(ctx context.Context, root crypto.Hash, offset, length uint64, pricePerMS types.Currency) ([]byte, *downloadedData, *pcwsWorkerState, error) { 252 // Create a context that dies when the function ends, this will cancel all 253 // of the worker jobs that get created by this function. 254 ctx, cancel := context.WithCancel(ctx) 255 defer cancel() 256 257 // Capture the base sector download in a new span. 258 span, ctx := opentracing.StartSpanFromContext(ctx, "managedDownloadByRoot") 259 span.SetTag("root", root) 260 defer span.Finish() 261 262 // Create the pcws for the first chunk. We use a passthrough cipher and 263 // erasure coder. If the base sector is encrypted, we will notice and be 264 // able to decrypt it once we have fully downloaded it and are able to 265 // access the layout. We can make the assumption on the erasure coding being 266 // of 1-N seeing as we currently always upload the basechunk using 1-N 267 // redundancy. 268 ptec := skymodules.NewPassthroughErasureCoder() 269 tpsk, err := crypto.NewSiaKey(crypto.TypePlain, nil) 270 if err != nil { 271 return nil, nil, nil, errors.AddContext(err, "unable to create plain skykey") 272 } 273 pcws, err := r.newPCWSByRoots(ctx, []crypto.Hash{root}, ptec, tpsk, 0) 274 if err != nil { 275 return nil, nil, nil, errors.AddContext(err, "unable to create the worker set for this skylink") 276 } 277 278 // Download the base sector. The base sector contains the metadata, without 279 // it we can't provide a completed data source. 280 // 281 // NOTE: we pass in the provided context here, if the user imposed a timeout 282 // on the download request, this will fire if it takes too long. 283 respChan, err := pcws.managedDownload(ctx, pricePerMS, offset, length, false) 284 if err != nil { 285 return nil, nil, nil, errors.AddContext(err, "unable to start download") 286 } 287 resp := <-respChan 288 dd, err := resp.Data() 289 if err != nil { 290 return nil, nil, nil, errors.AddContext(resp.err, "base sector download did not succeed") 291 } 292 data, err := dd.Recover() 293 return data, dd, pcws.managedWorkerState(), err 294 } 295 296 // managedReadLayout returns the data sources layout, the segment-aligned raw 297 // layout and a proof for the raw layout. 298 func (sds *skylinkDataSource) managedReadLayout() (skymodules.SkyfileLayout, []byte, []crypto.Hash) { 299 // Get offset and length of the layout. 300 layoutOff := sds.staticLayoutOffset 301 layoutLen := skymodules.SkyfileLayoutSize 302 303 // The downloaded data needs to be segment aligned for the proof. 304 layoutOffAligned := layoutOff 305 if mod := layoutOffAligned % crypto.SegmentSize; mod != 0 { 306 layoutOffAligned -= mod 307 } 308 endLayoutOffAligned := layoutOff + layoutLen 309 if mod := endLayoutOffAligned % crypto.SegmentSize; mod != 0 { 310 endLayoutOffAligned += (crypto.SegmentSize - mod) 311 } 312 313 // Get the requested, segment-aligned data. 314 data := sds.staticSkylinkSector[layoutOffAligned:endLayoutOffAligned] 315 316 // Create a range proof for the payload. 317 proofStart := int(layoutOffAligned / crypto.SegmentSize) 318 proofEnd := int(endLayoutOffAligned / crypto.SegmentSize) 319 proof := crypto.MerkleRangeProof(sds.staticSkylinkSector, proofStart, proofEnd) 320 321 // Sanity check the proof. 322 if build.Release == "testing" { 323 if !crypto.VerifyRangeProof(data, proof, proofStart, proofEnd, sds.Skylink().MerkleRoot()) { 324 build.Critical("managedReadLayout: created merkle proof is invalid") 325 } 326 } 327 return sds.staticLayout, data, proof 328 } 329 330 // managedReadBaseSector handles reading a part of a base sector. It returns a 331 // proof for the section that was read. 332 func (sds *skylinkDataSource) managedReadBaseSector(off, fetchSize uint64) (*downloadResponse, error) { 333 if off > modules.SectorSize { 334 return nil, fmt.Errorf("managedReadBaseSectorRange: off can't be greater than sector size: %v > %v", off, modules.SectorSize) 335 } 336 bytesLeft := modules.SectorSize - off 337 if fetchSize > bytesLeft { 338 return nil, fmt.Errorf("managedReadBaseSectorRange: read is out-of-bounds for off %v fetchSize %v and sector size %v", off, fetchSize, modules.SectorSize) 339 } 340 341 // Convenience var. 342 offsetInChunk := off 343 344 // The data we grab needs to be segment aligned so we adjust the offset 345 // and length that we use. 346 offsetInChunkAligned := offsetInChunk 347 if mod := offsetInChunkAligned % crypto.SegmentSize; mod != 0 { 348 offsetInChunkAligned -= mod 349 } 350 endOffsetInChunkAligned := offsetInChunk + fetchSize 351 if mod := endOffsetInChunkAligned % crypto.SegmentSize; mod != 0 { 352 endOffsetInChunkAligned += (crypto.SegmentSize - mod) 353 } 354 355 // Prepare slice for returning the segment-aligned data. We just copy 356 // the raw, decrypted sector data of the range we want to return. 357 data := append([]byte{}, sds.staticDecryptedSkylinkSector[offsetInChunkAligned:endOffsetInChunkAligned]...) 358 359 // Create a range proof for the payload. The range proof is created over 360 // the encrypted data because otherwise the proof doesn't add up to the 361 // root in the skylink. 362 proofStart := int(offsetInChunkAligned / crypto.SegmentSize) 363 proofEnd := int(endOffsetInChunkAligned / crypto.SegmentSize) 364 proof := crypto.MerkleRangeProof(sds.staticSkylinkSector, proofStart, proofEnd) 365 366 // Sanity check the proof. We do this only for unencrypted sectors. For 367 // encrypted ones, the client needs to encrypt the data again for it to 368 // match the proof. 369 if build.Release == "testing" && !skymodules.IsEncryptedLayout(sds.staticLayout) { 370 if !crypto.VerifyRangeProof(data, proof, proofStart, proofEnd, sds.Skylink().MerkleRoot()) { 371 build.Critical("managedReadSmallFileSection: created merkle proof is invalid") 372 } 373 } 374 375 // Create the response. 376 dr := newDownloadResponse(offsetInChunk, fetchSize, skymodules.NewPassthroughErasureCoder(), [][]byte{data}, [][]crypto.Hash{proof}, nil) 377 return dr, nil 378 } 379 380 // managedReadSmallFileSection handles reading a section from the buffered 381 // sector in-memory in case the file is a small one. 382 func (sds *skylinkDataSource) managedReadSmallFileSection(off, fetchSize uint64) (*downloadResponse, error) { 383 if !sds.staticLayout.IsSmallFile() { 384 return nil, errors.New("file is not a small file") 385 } 386 if off > sds.staticLayout.Filesize { 387 return nil, fmt.Errorf("managedReadSmallFileSection: off can't be greater than payload length: %v > %v", off, sds.staticLayout.Filesize) 388 } 389 bytesLeft := sds.staticLayout.Filesize - off 390 if fetchSize > bytesLeft { 391 fetchSize = bytesLeft 392 } 393 394 // Get the offset of the payload within the sector. 395 payloadOff := uint64(sds.staticLayoutOffset + skymodules.SkyfileLayoutSize + len(sds.staticRawMetadata)) 396 return sds.managedReadBaseSector(payloadOff+off, fetchSize) 397 } 398 399 // managedSkylinkDataSource will create a streamBufferDataSource for the data 400 // contained inside of a Skylink. The function will not return until the base 401 // sector and all skyfile metadata has been retrieved. 402 // 403 // NOTE: Skylink data sources are cached and outlive the user's request because 404 // multiple different callers may want to use the same data source. We do have 405 // to pass in a context though to adhere to a possible user-imposed request 406 // timeout. This can be optimized to always create the data source when it was 407 // requested, but we should only do so after gathering some real world feedback 408 // that indicates we would benefit from this. 409 func (r *Renter) managedSkylinkDataSource(ctx context.Context, skylink skymodules.Skylink, pricePerMS types.Currency) (streamBufferDataSource, error) { 410 skylinkSector, dd, _, err := r.managedDownloadByRoot(ctx, skylink.MerkleRoot(), 0, modules.SectorSize, pricePerMS) 411 if err != nil { 412 return nil, errors.AddContext(err, "failed to download baseSector") 413 } 414 415 // Store the skylink sector in the cache. We call this even if the sector 416 // was cached. That way we count the cache hit and refresh the lru. 417 err = r.staticStreamBufferSet.staticCache.Put(skylink.DataSourceID(), baseSectorSectionIndex, dd) 418 if err != nil { 419 return nil, errors.AddContext(err, "failed to update the cache") 420 } 421 422 // Get the offset and fetchsize of the base sector within the full 423 // sector. 424 slOffset, slFetchSize, err := skylink.OffsetAndFetchSize() 425 if err != nil { 426 return nil, errors.AddContext(err, "failed to get offset and fetchsize from skylink") 427 } 428 baseSector := skylinkSector[slOffset : slOffset+slFetchSize] 429 decryptedSkylinkSector := skylinkSector 430 431 // Check if the base sector is encrypted, and attempt to decrypt it. 432 // This will fail if we don't have the decryption key. 433 var fileSpecificSkykey skykey.Skykey 434 if skymodules.IsEncryptedBaseSector(baseSector) { 435 // Deep-copy the sector to avoid decrypting the 436 // original skylinkSector. 437 decryptedSkylinkSector = append([]byte{}, decryptedSkylinkSector...) 438 baseSector = decryptedSkylinkSector[slOffset : slOffset+slFetchSize] 439 fileSpecificSkykey, err = r.managedDecryptBaseSector(baseSector) 440 if err != nil { 441 return nil, errors.AddContext(err, "unable to decrypt skyfile base sector") 442 } 443 } 444 445 // Parse out the metadata of the skyfile. 446 // TODO: (f/u?) it might be better to resolve the parts of the fanout we 447 // need on demand. But that's quite the undertaking by itself. 448 // e.g. if we don't start resolving the recursive fanout right away we 449 // lose the benefit of the workerset, because we will add some latency 450 // later once we actually know what the user wants to download. 451 layout, fanoutBytes, metadata, rawMetadata, _, _, err := r.ParseSkyfileMetadata(baseSector) 452 if err != nil { 453 return nil, errors.AddContext(err, "unable to parse skyfile metadata") 454 } 455 456 // Create the context for the data source - a child of the renter 457 // threadgroup but otherwise independent. 458 dsCtx, cancelFunc := context.WithCancel(r.tg.StopCtx()) 459 460 // Tag the span with its size. We tag it with 64kb, 1mb, 4mb and 10mb as 461 // those are the size increments used by the benchmark tool. This way we can 462 // run the benchmark and then filter the results using these tags. 463 // 464 // NOTE: the sizes used are "exact sizes", meaning they are as close as 465 // possible to their eventual size after taking into account the size of the 466 // metadata. See cmd/skynet-benchmark/dl.go for more info. 467 if span := opentracing.SpanFromContext(ctx); span != nil { 468 switch length := metadata.Length; { 469 case length <= 61e3: 470 span.SetTag("length", "64kb") 471 case length <= 982e3: 472 span.SetTag("length", "1mb") 473 case length <= 3931e3: 474 span.SetTag("length", "4mb") 475 default: 476 span.SetTag("length", "10mb") 477 } 478 479 // Attach the span to the ctx 480 dsCtx = opentracing.ContextWithSpan(dsCtx, span) 481 } 482 483 // If there's a fanout create a PCWS for every chunk. 484 var fanoutChunkFetcherLoaders []chunkFetcherLoaderFn 485 var fanoutChunkFetchersAvailable []chan struct{} 486 var fanoutChunkFetchers []chunkFetcher 487 var fanoutChunkErrs []error 488 var ec skymodules.ErasureCoder 489 if len(fanoutBytes) > 0 { 490 // Derive the fanout key 491 fanoutKey, err := skymodules.DeriveFanoutKey(&layout, fileSpecificSkykey) 492 if err != nil { 493 cancelFunc() 494 return nil, errors.AddContext(err, "unable to derive encryption key") 495 } 496 497 // Create the erasure coder 498 ec, err = skymodules.NewRSSubCode(int(layout.FanoutDataPieces), int(layout.FanoutParityPieces), crypto.SegmentSize) 499 if err != nil { 500 cancelFunc() 501 return nil, errors.AddContext(err, "unable to derive erasure coding settings for fanout") 502 } 503 504 // Create the list of chunks from the fanout. 505 fanoutChunks, err := layout.DecodeFanoutIntoChunks(fanoutBytes) 506 if err != nil { 507 cancelFunc() 508 return nil, errors.AddContext(err, "error parsing skyfile fanout") 509 } 510 511 // Initialize the fanout chunk fetcher loaders. Note that we only 512 // initialize the loaders here and we do not block on initializing the 513 // actual PCWS objects for every chunk. We will preload a certain amount 514 // to improve TTFB, we do not however want to precreate a PCWS for every 515 // chunk on initial read because that might put too much strain on the 516 // network because of the amount of has sector jobs. The loaders will 517 // send a PCWS down the channel when it is ready, the caller (Stream) 518 // can block on that channel. 519 numChunks := len(fanoutChunks) 520 fanoutChunkFetcherLoaders = make([]chunkFetcherLoaderFn, numChunks) 521 fanoutChunkFetchersAvailable = make([]chan struct{}, numChunks) 522 fanoutChunkFetchers = make([]chunkFetcher, numChunks) 523 fanoutChunkErrs = make([]error, numChunks) 524 for i := 0; i < numChunks; i++ { 525 fanoutChunkFetchersAvailable[i] = make(chan struct{}) 526 } 527 528 // Create the PCWS loaders 529 for i, chunk := range fanoutChunks { 530 chunkIndex := uint64(i) 531 chunkCopy := chunk 532 var once sync.Once 533 fanoutChunkFetcherLoaders[i] = func() { 534 once.Do(func() { 535 pcws, err := r.newPCWSByRoots(dsCtx, chunkCopy, ec, fanoutKey, chunkIndex) 536 fanoutChunkErrs[chunkIndex] = err 537 fanoutChunkFetchers[chunkIndex] = pcws 538 close(fanoutChunkFetchersAvailable[chunkIndex]) 539 }) 540 } 541 } 542 543 // Launch a portion of the loaders in a goroutine, this makes it so we 544 // can return the data source faster and we also preload the first 545 // couple of PCWSs, the others are lazy loaded when new data sections 546 // get created. Since we use a `minimumLookahead` there, we will load 547 // the PCWSs before they are used, giving them some time to execute 548 // their has sector jobs prior to being downloaded from. 549 err = r.tg.Launch(func() { 550 for i := 0; i < len(fanoutChunks) && i < chunkFetchersMaximumPreload; i++ { 551 fanoutChunkFetcherLoaders[i]() 552 } 553 }) 554 if err != nil { 555 cancelFunc() 556 return nil, errors.AddContext(err, "unable to launch thread to preload one of the initial chunk fetchers") 557 } 558 } 559 560 sds := &skylinkDataSource{ 561 staticID: skylink.DataSourceID(), 562 staticLayout: layout, 563 staticLayoutOffset: int(slOffset), 564 staticMetadata: metadata, 565 staticRawMetadata: rawMetadata, 566 staticSkylink: skylink, 567 568 staticSkylinkSector: skylinkSector, 569 staticDecryptedSkylinkSector: decryptedSkylinkSector, 570 571 staticChunkFetcherLoaders: fanoutChunkFetcherLoaders, 572 staticChunkFetchersAvailable: fanoutChunkFetchersAvailable, 573 staticChunkFetchers: fanoutChunkFetchers, 574 staticChunkErrs: fanoutChunkErrs, 575 576 staticCtx: dsCtx, 577 staticCancelFunc: cancelFunc, 578 } 579 return sds, nil 580 }