gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/workerjobhassector_test.go (about) 1 package renter 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "testing" 8 "time" 9 10 "gitlab.com/NebulousLabs/fastrand" 11 "gitlab.com/SkynetLabs/skyd/build" 12 "gitlab.com/SkynetLabs/skyd/siatest/dependencies" 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 19 // TestHasSectorJobBatchCallNext makes sure that multiple has sector jobs are 20 // batched together correctly. 21 func TestHasSectorJobBatchCallNext(t *testing.T) { 22 t.Parallel() 23 24 // Create queue and job. 25 queue := jobHasSectorQueue{ 26 availabilityMetrics: newAvailabilityMetrics(availabilityMetricsDefaultHalfLife), 27 jobGenericQueue: newJobGenericQueue(&worker{}), 28 } 29 jhs := &jobHasSector{ 30 jobGeneric: jobGeneric{ 31 staticQueue: queue, 32 staticCtx: context.Background(), 33 }, 34 staticSpan: testSpan(), 35 } 36 37 // add jobs 38 for i := 0; i < int(hasSectorBatchSize)+1; i++ { 39 if !queue.callAdd(jhs) { 40 t.Fatal("job wasn't added") 41 } 42 } 43 44 // call callNext 3 times. 45 next1 := queue.callNext() 46 next2 := queue.callNext() 47 next3 := queue.callNext() 48 49 // the first should contain hasSectorBatchSize jobs, the second one 1 job 50 // and the third one should be nil. 51 if l := len(next1.(*jobHasSectorBatch).staticJobs); l != int(hasSectorBatchSize) { 52 t.Fatal("wrong size", l, hasSectorBatchSize) 53 } 54 if len(next2.(*jobHasSectorBatch).staticJobs) != 1 { 55 t.Fatal("wrong size") 56 } 57 if next3 != nil { 58 t.Fatal("should be nil") 59 } 60 } 61 62 // TestHasSectorJobQueueUpdateAvailabilityMetrics is a unit that verifies the HS 63 // job queue correctly updates the availability metrics 64 func TestHasSectorJobQueueUpdateAvailabilityMetrics(t *testing.T) { 65 t.Parallel() 66 67 // mock a worker 68 w := mockWorker(0) 69 hsq := w.staticJobHasSectorQueue 70 71 // assert basic case 72 hsq.callUpdateAvailabilityMetrics(1, 2, 1) 73 ar := hsq.callAvailabilityRate(1) 74 if ar != .5 { 75 t.Fatal("bad") 76 } 77 78 // assert updates take effect 79 hsq.callUpdateAvailabilityMetrics(1, 1, 0) 80 ar = hsq.callAvailabilityRate(1) 81 if ar != float64(1)/float64(3) { 82 t.Fatal("bad") 83 } 84 85 // assert min rate is returned of 0 metrics 86 ar = hsq.callAvailabilityRate(2) 87 if ar != jobHasSectorQueueMinAvailabilityRate { 88 t.Fatal("bad") 89 } 90 91 // assert min rate is returned if 0 available 92 hsq.callUpdateAvailabilityMetrics(2, 1, 0) 93 ar = hsq.callAvailabilityRate(2) 94 if ar != jobHasSectorQueueMinAvailabilityRate { 95 t.Fatal("bad") 96 } 97 98 // assert correct rate is returned if available 99 hsq.callUpdateAvailabilityMetrics(2, 1, 1) 100 ar = hsq.callAvailabilityRate(2) 101 if ar != .5 { 102 t.Fatal("bad") 103 } 104 } 105 106 // TestHasSectorJobQueueAvailabilityRate is a unit that verifies the HS job 107 // queue correctly returns the availability rate 108 func TestHasSectorJobQueueAvailabilityRate(t *testing.T) { 109 if testing.Short() { 110 t.SkipNow() 111 } 112 t.Parallel() 113 114 // create a new worker tester 115 wt, err := newWorkerTester(t.Name()) 116 if err != nil { 117 t.Fatal(err) 118 } 119 defer func() { 120 err := wt.Close() 121 if err != nil { 122 t.Fatal(err) 123 } 124 }() 125 w := wt.worker 126 127 // assert the min availability rate on a new queue 128 randomNumPieces := fastrand.Intn(64) + 1 129 if w.staticJobHasSectorQueue.callAvailabilityRate(randomNumPieces) != jobHasSectorQueueMinAvailabilityRate { 130 t.Fatal("unexpected") 131 } 132 133 // create a two roots, add one to the host 134 randomData := fastrand.Bytes(int(modules.SectorSize)) 135 randomRoot := crypto.MerkleRoot(randomData) 136 sectorData := fastrand.Bytes(int(modules.SectorSize)) 137 sectorRoot := crypto.MerkleRoot(sectorData) 138 err = wt.host.AddSector(sectorRoot, sectorData) 139 if err != nil { 140 t.Fatal(err) 141 } 142 143 // add a job where the host is supposed to find one root out of two 144 roots := []crypto.Hash{sectorRoot, randomRoot} 145 responseChan := make(chan *jobHasSectorResponse, 1) 146 jhs := w.newJobHasSector(context.Background(), responseChan, randomNumPieces, roots...) 147 added := w.staticJobHasSectorQueue.callAdd(jhs) 148 if !added { 149 t.Fatal("unexpected") 150 } 151 152 // check whether the availability rate is correct 153 if err := build.Retry(10, 10*time.Millisecond, func() error { 154 availabilityRate := w.staticJobHasSectorQueue.callAvailabilityRate(randomNumPieces) 155 if availabilityRate != .5 { 156 return fmt.Errorf("unexpected availability rate %v != .5", availabilityRate) 157 } 158 return nil 159 }); err != nil { 160 t.Fatal(err) 161 } 162 163 // add a job where the host won't find any root 164 roots = []crypto.Hash{randomRoot, randomRoot, randomRoot} 165 responseChan = make(chan *jobHasSectorResponse, 1) 166 jhs = w.newJobHasSector(context.Background(), responseChan, randomNumPieces, roots...) 167 added = w.staticJobHasSectorQueue.callAdd(jhs) 168 if !added { 169 t.Fatal("unexpected") 170 } 171 172 // check whether the availability rate is correct 173 if err := build.Retry(10, 10*time.Millisecond, func() error { 174 availabilityRate := w.staticJobHasSectorQueue.callAvailabilityRate(randomNumPieces) 175 if availabilityRate != .2 { 176 return fmt.Errorf("unexpected availability rate %v != .2", availabilityRate) 177 } 178 return nil 179 }); err != nil { 180 t.Fatal(err) 181 } 182 } 183 184 // TestHasSectorJobExpectedBandwidth is a unit test that verifies our HS job 185 // bandwidth estimates are given in a way we never execute a program and run out 186 // of budget. 187 func TestHasSectorJobExpectedBandwidth(t *testing.T) { 188 if testing.Short() { 189 t.SkipNow() 190 } 191 t.Parallel() 192 193 // create a new worker tester 194 wt, err := newWorkerTester(t.Name()) 195 if err != nil { 196 t.Fatal(err) 197 } 198 defer func() { 199 err := wt.Close() 200 if err != nil { 201 t.Fatal(err) 202 } 203 }() 204 w := wt.worker 205 pt := wt.staticPriceTable().staticPriceTable 206 207 // numPacketsRequiredForSectors is a helper function that executes a HS 208 // program with the given amount of sectors and returns the amount of 209 // packets needed to cover both the upload and download bandwidth of the 210 // program. 211 numPacketsRequiredForSectors := func(numSectors int) (uint64, uint64) { 212 // build sectors 213 sectors := make([]crypto.Hash, numSectors) 214 for i := 0; i < numSectors; i++ { 215 sectors[i] = crypto.Hash{1, 2, 3} 216 } 217 218 // build program 219 pb := modules.NewProgramBuilder(&pt, 0) 220 for _, sector := range sectors { 221 pb.AddHasSectorInstruction(sector) 222 } 223 p, data := pb.Program() 224 cost, _, _ := pb.Cost(true) 225 226 // build job 227 jhs := new(jobHasSector) 228 jhs.staticSectors = sectors 229 jhs.staticNumPieces = skymodules.RenterDefaultNumPieces 230 231 // build a batch from the job for comparison 232 jhsb := *&jobHasSectorBatch{ 233 staticJobs: []*jobHasSector{ 234 jhs, 235 }, 236 } 237 238 // calculate cost 239 ulBandwidth, dlBandwidth := jhs.callExpectedBandwidth() 240 bandwidthCost, bandwidthRefund := mdmBandwidthCost(pt, ulBandwidth, dlBandwidth) 241 cost = cost.Add(bandwidthCost) 242 243 // cost of batch should match. 244 ulb, dlb := jhsb.callExpectedBandwidth() 245 if ulb != ulBandwidth || dlb != dlBandwidth { 246 t.Fatal("batch bandwidth doesn't match job bandwidth") 247 } 248 249 // execute the program 250 _, limit, err := w.managedExecuteProgram(p, data, types.FileContractID{}, categoryDownload, cost, bandwidthRefund) 251 if err != nil { 252 t.Fatal(err) 253 } 254 255 return limit.Downloaded() / 1460, limit.Uploaded() / 1460 256 } 257 258 // expect 1 root to only require a single packet on both up and download 259 dl, ul := numPacketsRequiredForSectors(1) 260 if dl != 1 || ul != 1 { 261 t.Fatal("unexpected") 262 } 263 264 // expect 12 roots to not exceed the threshold (which is at 13) on download 265 dl, ul = numPacketsRequiredForSectors(12) 266 if dl != 1 || ul != 1 { 267 t.Fatal("unexpected") 268 } 269 270 // expect 13 roots to push us over the threshold, and require an extra 271 // packet on download 272 dl, ul = numPacketsRequiredForSectors(13) 273 if dl != 2 || ul != 1 { 274 t.Fatal("unexpected") 275 } 276 277 // expect 16 roots to not exceed the threshold (which is at 17) on upload 278 dl, ul = numPacketsRequiredForSectors(16) 279 if dl != 2 || ul != 1 { 280 t.Fatal("unexpected") 281 } 282 283 // expect 17 roots to push us over the threshold, and require an extra 284 // packet on upload 285 dl, ul = numPacketsRequiredForSectors(17) 286 if dl != 2 || ul != 2 { 287 t.Fatal("unexpected") 288 } 289 } 290 291 // TestAvailabilityMetrics is a unit test for AvailabilityMetrics 292 func TestAvailabilityMetrics(t *testing.T) { 293 t.Parallel() 294 295 // verify we have the expected amount of buckets 296 metrics := newAvailabilityMetrics(100 * time.Second) 297 if len(metrics.buckets) != availabilityMetricsNumBuckets { 298 t.Fatal("bad") 299 } 300 301 // verify the piecesToBuckets slice against this hardcoded slice 302 expected := []int{-1, 0, 1, 2, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15} 303 if len(metrics.piecesToBuckets) != len(expected) { 304 t.Fatal("bad") 305 } 306 for i := range expected { 307 if metrics.piecesToBuckets[i] != expected[i] { 308 t.Fatal("bad") 309 } 310 } 311 312 // manually verify some bucket indices 313 bucketIndex := metrics.piecesToBuckets[1] 314 if bucketIndex != 0 { 315 t.Fatal("bad", bucketIndex) 316 } 317 bucketIndex = metrics.piecesToBuckets[10] 318 if bucketIndex != 5 { 319 t.Fatal("bad") 320 } 321 bucketIndex = metrics.piecesToBuckets[30] 322 if bucketIndex != 10 { 323 t.Fatal("bad") 324 } 325 bucketIndex = metrics.piecesToBuckets[96] 326 if bucketIndex != 15 { 327 t.Fatal("bad") 328 } 329 330 // assert we're returning the last bucket if the num pieces is larger than 331 // what we support, which is 116 num pieces with the current defaults 332 if metrics.bucket(999) != metrics.buckets[bucketIndex] { 333 t.Fatal("bad") 334 } 335 336 // assert the bucket has no datapoints yet 337 bucket := metrics.bucket(10) 338 if bucket.totalAvailable != 0 || bucket.totalLookups != 0 { 339 t.Fatal("bad") 340 } 341 342 // update metrics and assert the correct bucket got updated 343 metrics.updateMetrics(10, 3, 2) 344 bucket = metrics.bucket(10) 345 if bucket.totalAvailable != 2 || bucket.totalLookups != 3 { 346 t.Fatal("bad") 347 } 348 349 // assert all other buckets have not been updated 350 for b := 0; b < availabilityMetricsNumBuckets; b++ { 351 bucketIndex = metrics.piecesToBuckets[10] 352 if b == bucketIndex { 353 continue 354 } 355 bucket := metrics.buckets[b] 356 if bucket.totalAvailable != 0 || bucket.totalLookups != 0 { 357 t.Fatal("bad") 358 } 359 } 360 } 361 362 // TestHasSectorJobWithdrawal verifies that executing a has-sector job withdraws 363 // the right amount of tokens from the local EA balance. 364 func TestHasSectorJobWithdrawal(t *testing.T) { 365 if testing.Short() { 366 t.SkipNow() 367 } 368 t.Parallel() 369 370 wt, err := newWorkerTesterCustomDependency(t.Name(), &dependencies.DependencyDisableWorker{}, skymodules.SkydProdDependencies) 371 if err != nil { 372 t.Fatal(err) 373 } 374 defer func() { 375 if err := wt.Close(); err != nil { 376 t.Fatal(err) 377 } 378 }() 379 380 // Get a price table and refill the account manually. 381 wt.staticUpdatePriceTable() 382 wt.managedRefillAccount() 383 384 // Loop that performs upload jobs. 385 go func() { 386 for range time.NewTicker(100 * time.Millisecond).C { 387 if wt.managedHasUploadJob() { 388 wt.externLaunchSerialJob(wt.managedPerformUploadChunkJob) 389 } 390 } 391 }() 392 393 // Upload some data. 394 r := wt.rt.renter 395 sup := skymodules.SkyfileUploadParameters{ 396 BaseChunkRedundancy: 2, 397 Filename: "test", 398 SiaPath: skymodules.RandomSkynetFilePath(), 399 } 400 data := bytes.NewReader(fastrand.Bytes(10)) 401 sl, err := r.UploadSkyfile(context.Background(), sup, skymodules.NewSkyfileReader(data, sup)) 402 if err != nil { 403 t.Fatal(err) 404 } 405 406 // Get the pricetable. 407 pt := wt.staticPriceTable() 408 409 // Get the balance. 410 wt.staticAccount.mu.Lock() 411 balanceBefore := wt.staticAccount.balance 412 wt.staticAccount.mu.Unlock() 413 414 // Read the entry 415 respChan := make(chan *jobHasSectorResponse) 416 jhs := wt.newJobHasSector(context.Background(), respChan, 1, sl.MerkleRoot()) 417 if !wt.externLaunchAsyncJob(jhs) { 418 t.Fatal("job wasn't launched") 419 } 420 resp := <-respChan 421 if resp.staticErr != nil { 422 t.Fatal(resp.staticErr) 423 } 424 425 // Get the balance after. 426 wt.staticAccount.mu.Lock() 427 balanceAfter := wt.staticAccount.balance 428 wt.staticAccount.mu.Unlock() 429 430 // Compute the expected cost. 431 pb := modules.NewProgramBuilder(&pt.staticPriceTable, 0) 432 pb.AddHasSectorInstruction(sl.MerkleRoot()) 433 cost, _, _ := pb.Cost(true) 434 435 // Add the expected bandwidth cost. 436 // NOTE: We use 1460 here because we know that that's the actual 437 // bandwidth we are using. readRegistryJobExpectedBandwidth will 438 // slightly overestimate the bandwidth by using 1500 and then issue a 439 // refund for 40 bytes. 440 bandwidthCost := modules.MDMBandwidthCost(pt.staticPriceTable, 1460, 1460) 441 cost = cost.Add(bandwidthCost) 442 443 // Make sure the delta of the account matches the cost. 444 balanceDelta := balanceBefore.Sub(balanceAfter) 445 if !balanceDelta.Equals(cost) { 446 t.Fatal("delta doesn't match cost", balanceDelta, cost) 447 } 448 449 // Same test again but for non-existent root. 450 respChan = make(chan *jobHasSectorResponse) 451 jhs = wt.newJobHasSector(context.Background(), respChan, 1, crypto.Hash{}) 452 if !wt.externLaunchAsyncJob(jhs) { 453 t.Fatal("job wasn't launched") 454 } 455 resp = <-respChan 456 if resp.staticErr != nil { 457 t.Fatal(resp.staticErr) 458 } 459 460 // Get the balance after. 461 wt.staticAccount.mu.Lock() 462 balanceBefore = balanceAfter 463 balanceAfter = wt.staticAccount.balance 464 wt.staticAccount.mu.Unlock() 465 466 // Compute the expected cost. 467 pb = modules.NewProgramBuilder(&pt.staticPriceTable, 0) 468 pb.AddHasSectorInstruction(crypto.Hash{}) 469 cost, _, _ = pb.Cost(true) 470 471 // Add the expected bandwidth cost. 472 // NOTE: We use 1460 here because we know that that's the actual 473 // bandwidth we are using. callExpectedBandwidth will slightly 474 // overestimate the bandwidth by using 1500 and then issue a refund for 475 // 40 bytes. 476 bandwidthCost = modules.MDMBandwidthCost(pt.staticPriceTable, 1460, 1460) 477 cost = cost.Add(bandwidthCost) 478 479 // Make sure the delta of the account matches the cost. 480 balanceDelta = balanceBefore.Sub(balanceAfter) 481 if !balanceDelta.Equals(cost) { 482 t.Fatal("delta doesn't match cost", balanceDelta, cost) 483 } 484 } 485 486 // TestHasSectorJobWithdrawal verifies that executing a batched has-sector job 487 // withdraws the right amount of tokens from the local EA balance. 488 func TestHasSectorBatchJobWithdrawal(t *testing.T) { 489 if testing.Short() { 490 t.SkipNow() 491 } 492 t.Parallel() 493 494 wt, err := newWorkerTesterCustomDependency(t.Name(), &dependencies.DependencyDisableWorker{}, skymodules.SkydProdDependencies) 495 if err != nil { 496 t.Fatal(err) 497 } 498 defer func() { 499 if err := wt.Close(); err != nil { 500 t.Fatal(err) 501 } 502 }() 503 504 // Get a price table and refill the account manually. 505 wt.staticUpdatePriceTable() 506 wt.managedRefillAccount() 507 508 // Loop that performs upload jobs. 509 go func() { 510 for range time.NewTicker(100 * time.Millisecond).C { 511 if wt.managedHasUploadJob() { 512 wt.externLaunchSerialJob(wt.managedPerformUploadChunkJob) 513 } 514 } 515 }() 516 517 // Upload some data. 518 r := wt.rt.renter 519 sup := skymodules.SkyfileUploadParameters{ 520 BaseChunkRedundancy: 2, 521 Filename: "test", 522 SiaPath: skymodules.RandomSkynetFilePath(), 523 } 524 data := bytes.NewReader(fastrand.Bytes(10)) 525 sl, err := r.UploadSkyfile(context.Background(), sup, skymodules.NewSkyfileReader(data, sup)) 526 if err != nil { 527 t.Fatal(err) 528 } 529 530 // Get the pricetable. 531 pt := wt.staticPriceTable() 532 533 // Get the balance. 534 wt.staticAccount.mu.Lock() 535 balanceBefore := wt.staticAccount.balance 536 wt.staticAccount.mu.Unlock() 537 538 // Read the entry 539 respChan := make(chan *jobHasSectorResponse) 540 jhs := wt.newJobHasSector(context.Background(), respChan, 1, sl.MerkleRoot()) 541 var jhsb jobHasSectorBatch 542 for i := 0; i < hasSectorBatchSize; i++ { 543 jhsb.staticJobs = append(jhsb.staticJobs, jhs) 544 } 545 if !wt.externLaunchAsyncJob(jhsb) { 546 t.Fatal("job wasn't launched") 547 } 548 resp := <-respChan 549 if resp.staticErr != nil { 550 t.Fatal(resp.staticErr) 551 } 552 553 // Get the balance after. 554 wt.staticAccount.mu.Lock() 555 balanceAfter := wt.staticAccount.balance 556 wt.staticAccount.mu.Unlock() 557 558 // Compute the expected cost. 559 pb := modules.NewProgramBuilder(&pt.staticPriceTable, 0) 560 for range jhsb.staticJobs { 561 pb.AddHasSectorInstruction(sl.MerkleRoot()) 562 } 563 cost, _, _ := pb.Cost(true) 564 565 // Add the expected bandwidth cost. 566 // NOTE: We use 1460 and 2920 here because we know that that's the 567 // actual bandwidth we are using. callExpectedBandwidth will slightly 568 // overestimate the bandwidth by using 1500 and 3000 and then issue a 569 // refund for the difference. 570 bandwidthCost := modules.MDMBandwidthCost(pt.staticPriceTable, 1460, 2920) 571 cost = cost.Add(bandwidthCost) 572 573 // Make sure the delta of the account matches the cost. 574 balanceDelta := balanceBefore.Sub(balanceAfter) 575 if !balanceDelta.Equals(cost) { 576 t.Fatal("delta doesn't match cost", balanceDelta, cost) 577 } 578 }