gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/worker_test.go (about) 1 package renter 2 3 import ( 4 "context" 5 "fmt" 6 "path/filepath" 7 "strings" 8 "sync" 9 "testing" 10 "time" 11 12 "gitlab.com/NebulousLabs/errors" 13 "gitlab.com/NebulousLabs/fastrand" 14 "gitlab.com/NebulousLabs/threadgroup" 15 "gitlab.com/SkynetLabs/skyd/build" 16 "gitlab.com/SkynetLabs/skyd/siatest/dependencies" 17 "gitlab.com/SkynetLabs/skyd/skymodules" 18 "go.sia.tech/siad/modules" 19 "go.sia.tech/siad/types" 20 ) 21 22 // workerTester is a helper type which contains a renter, host and worker that 23 // communicates with that host. 24 type workerTester struct { 25 rt *renterTester 26 host modules.Host 27 *worker 28 } 29 30 // newWorkerTester creates a new worker for testing. 31 func newWorkerTester(name string) (*workerTester, error) { 32 return newWorkerTesterCustomDependency(name, skymodules.SkydProdDependencies, modules.ProdDependencies) 33 } 34 35 // newWorkerTesterCustomDependency creates a new worker for testing with a 36 // custom depency. 37 func newWorkerTesterCustomDependency(name string, renterDeps skymodules.SkydDependencies, hostDeps modules.Dependencies) (*workerTester, error) { 38 // Create the renter. 39 rt, err := newRenterTesterWithDependency(filepath.Join(name, "renter"), renterDeps) 40 if err != nil { 41 return nil, err 42 } 43 44 // Set an allowance. 45 err = rt.renter.staticHostContractor.SetAllowance(skymodules.DefaultAllowance) 46 if err != nil { 47 return nil, err 48 } 49 50 // Add a host. 51 host, err := rt.addCustomHost(filepath.Join(rt.dir, "host"), hostDeps) 52 if err != nil { 53 return nil, err 54 } 55 56 // Wait for worker to show up. 57 var w *worker 58 err = build.Retry(100, 100*time.Millisecond, func() error { 59 _, err := rt.miner.AddBlock() 60 if err != nil { 61 return err 62 } 63 rt.renter.staticWorkerPool.callUpdate(rt.renter) 64 workers := rt.renter.staticWorkerPool.callWorkers() 65 if len(workers) != 1 { 66 return fmt.Errorf("expected %v workers but got %v", 1, len(workers)) 67 } 68 w = workers[0] 69 return nil 70 }) 71 if err != nil { 72 return nil, err 73 } 74 75 if !renterDeps.Disrupt("DisableWorkerLoop") { 76 // Schedule a price table update for a brand new one. 77 w.staticSchedulePriceTableUpdate(false) 78 79 // Wait for the price table to be updated. 80 err = build.Retry(100, 100*time.Millisecond, func() error { 81 pt := w.staticPriceTable() 82 if pt.staticUpdateTime.Before(time.Now()) { 83 return errors.New("price table not updated") 84 } 85 return nil 86 }) 87 if err != nil { 88 return nil, err 89 } 90 } 91 92 if !(renterDeps.Disrupt("DisableFunding") || renterDeps.Disrupt("DisableWorkerLoop") || renterDeps.Disrupt("DisableCommitPaymentIntent")) { 93 // Wait until the worker is done with its maintenance tasks. 94 err = build.Retry(100, 100*time.Millisecond, func() error { 95 if !w.managedMaintenanceSucceeded() { 96 return errors.New("worker not ready with maintenance") 97 } 98 return nil 99 }) 100 if err != nil { 101 return nil, err 102 } 103 } 104 105 // Wait for the price table to be updated. 106 // 107 // NOTE: all dependencies which disable updating the pricetable or 108 // refilling the account on purpose need to make sure to disrupt on the 109 // following keywords. 110 err = build.Retry(100, 100*time.Millisecond, func() error { 111 pt := w.staticPriceTable() 112 if pt.staticUpdateTime.Before(time.Now()) { 113 return errors.New("price table not updated") 114 } 115 return nil 116 }) 117 if err != nil && !renterDeps.Disrupt("DisablePriceTableUpdatedCheck") { 118 return nil, err 119 } 120 121 // block until worker has funded the EA before starting the tests. 122 err = build.Retry(100, 100*time.Millisecond, func() error { 123 if w.staticAccount.managedAvailableBalance().IsZero() { 124 return errors.New("balance is zero") 125 } 126 return nil 127 }) 128 if err != nil && !renterDeps.Disrupt("DisableBalanceIsZeroCheck") && !renterDeps.Disrupt("DisableCommitPaymentIntent") { 129 return nil, err 130 } 131 132 return &workerTester{ 133 rt: rt, 134 host: host, 135 worker: w, 136 }, nil 137 } 138 139 // Close closes the renter and host. 140 func (wt *workerTester) Close() error { 141 var err1, err2 error 142 var wg sync.WaitGroup 143 144 // Kill the worker first to verify that all of the worker's background 145 // threads are stopped by merely killing the worker and not the whole 146 // renter. 147 wt.worker.managedKill() 148 149 wg.Add(2) 150 go func() { 151 err1 = wt.rt.Close() 152 wg.Done() 153 }() 154 go func() { 155 err2 = wt.host.Close() 156 wg.Done() 157 }() 158 wg.Wait() 159 return errors.Compose(err1, err2) 160 } 161 162 // TestNewWorkerTester creates a new worker 163 func TestNewWorkerTester(t *testing.T) { 164 if testing.Short() { 165 t.SkipNow() 166 } 167 t.Parallel() 168 169 wt, err := newWorkerTester(t.Name()) 170 if err != nil { 171 t.Fatal(err) 172 } 173 if err := wt.Close(); err != nil { 174 t.Fatal(err) 175 } 176 } 177 178 // TestReadOffsetCorruptProof tests that ReadOffset jobs correctly verify the 179 // merkle proof returned by the host and reject data that doesn't match said 180 // proof. 181 func TestReadOffsetCorruptedProof(t *testing.T) { 182 if testing.Short() { 183 t.SkipNow() 184 } 185 t.Parallel() 186 187 deps := dependencies.NewDependencyCorruptMDMOutput() 188 wt, err := newWorkerTesterCustomDependency(t.Name(), skymodules.SkydProdDependencies, deps) 189 if err != nil { 190 t.Fatal(err) 191 } 192 defer func() { 193 if err := wt.Close(); err != nil { 194 t.Fatal(err) 195 } 196 }() 197 198 backup := skymodules.UploadedBackup{ 199 Name: "foo", 200 CreationDate: types.CurrentTimestamp(), 201 Size: 10, 202 UploadProgress: 0, 203 } 204 205 // Upload a snapshot to fill the first sector of the contract. 206 err = wt.UploadSnapshot(context.Background(), backup, fastrand.Bytes(int(backup.Size))) 207 if err != nil { 208 t.Fatal(err) 209 } 210 // Download the first sector partially and then fully since both actions 211 // require different proofs. 212 _, err = wt.ReadOffset(context.Background(), categorySnapshotDownload, 0, modules.SectorSize/2) 213 if err != nil { 214 t.Fatal(err) 215 } 216 _, err = wt.ReadOffset(context.Background(), categorySnapshotDownload, 0, modules.SectorSize) 217 if err != nil { 218 t.Fatal(err) 219 } 220 221 // Do it again but this time corrupt the output to make sure the proof 222 // doesn't match. 223 deps.Fail() 224 _, err = wt.ReadOffset(context.Background(), categorySnapshotDownload, 0, modules.SectorSize/2) 225 if err == nil || !strings.Contains(err.Error(), "verifying proof failed") { 226 t.Fatal(err) 227 } 228 229 // Retry since the worker might be on a cooldown. 230 err = build.Retry(100, 100*time.Millisecond, func() error { 231 deps.Fail() 232 _, err = wt.ReadOffset(context.Background(), categorySnapshotDownload, 0, modules.SectorSize) 233 if err == nil || !strings.Contains(err.Error(), "verifying proof failed") { 234 return fmt.Errorf("unexpected error %v", err) 235 } 236 return nil 237 }) 238 if err != nil { 239 t.Fatal(err) 240 } 241 } 242 243 // TestManagedAsyncReady is a unit test that probes the 'managedAsyncReady' 244 // function on the worker 245 func TestManagedAsyncReady(t *testing.T) { 246 w := new(worker) 247 w.initJobHasSectorQueue() 248 jrs := NewJobReadStats() 249 w.initJobReadQueue(jrs) 250 w.initJobLowPrioReadQueue(jrs) 251 w.initJobReadRegistryQueue() 252 w.initJobUpdateRegistryQueue() 253 254 timeInFuture := time.Now().Add(time.Hour) 255 timeInPast := time.Now().Add(-time.Hour) 256 257 // ensure pt is considered valid 258 w.newPriceTable() 259 w.staticPriceTable().staticExpiryTime = timeInFuture 260 261 // ensure the worker has a maintenancestate, by default it will pass 262 w.newMaintenanceState() 263 264 // verify worker is considered async ready 265 if !w.managedAsyncReady() { 266 t.Fatal("unexpected") 267 } 268 269 // tweak the price table to make it not ready 270 badWorkerPriceTable := w 271 badWorkerPriceTable.staticPriceTable().staticExpiryTime = timeInPast 272 if badWorkerPriceTable.managedAsyncReady() { 273 t.Fatal("unexpected") 274 } 275 276 // tweak the maintenancestate making it non ready 277 badWorkerMaintenanceState := w 278 badWorkerMaintenanceState.staticMaintenanceState.cooldownUntil = timeInFuture 279 if badWorkerMaintenanceState.managedAsyncReady() { 280 t.Fatal("unexpected") 281 } 282 } 283 284 // TestJobQueueInitialEstimate verifies the initial time estimates are set on 285 // both the HS and RJ queues right after performing the pricetable update for 286 // the first time. 287 func TestJobQueueInitialEstimate(t *testing.T) { 288 if testing.Short() { 289 t.SkipNow() 290 } 291 t.Parallel() 292 293 wt, err := newWorkerTester(t.Name()) 294 if err != nil { 295 t.Fatal(err) 296 } 297 defer func() { 298 if err := wt.Close(); err != nil { 299 t.Fatal(err) 300 } 301 }() 302 w := wt.worker 303 304 // verify it has set the initial estimates on both queues 305 if w.staticJobHasSectorQueue.callExpectedJobTime() == 0 { 306 t.Fatal("unexpected") 307 } 308 if w.staticJobReadQueue.staticStats.callExpectedJobTime(fastrand.Uint64n(1<<24)) == 0 { 309 t.Fatal("unexpected") 310 } 311 } 312 313 // TestWorkerOfflineHost verifies that we do not create a worker for hosts that 314 // are offline and kill off workers for hosts that went offline. 315 func TestWorkerOfflineHost(t *testing.T) { 316 if testing.Short() { 317 t.SkipNow() 318 } 319 t.Parallel() 320 321 // create a dependency that allows interrupting host scans, simulating the 322 // behaviour of a host going offline 323 deps := dependencies.NewDependencyInterruptHostScan() 324 deps.Disable() 325 326 // create a worker tester with that dependency 327 wt, err := newWorkerTesterCustomDependency(t.Name(), deps, skymodules.SkydProdDependencies) 328 if err != nil { 329 t.Fatal(err) 330 } 331 defer func() { 332 if err := wt.Close(); err != nil { 333 t.Fatal(err) 334 } 335 }() 336 337 // assert the worker pool has a worker 338 // 339 // NOTE: this is redundant because the worker tester will have verified this 340 // already, we check it anyway here to ensure this check takes place 341 err = build.Retry(100, 100*time.Millisecond, func() error { 342 workers := wt.rt.renter.staticWorkerPool.callWorkers() 343 if len(workers) == 0 { 344 return errors.New("no workers in pool") 345 } 346 return nil 347 }) 348 if err != nil { 349 t.Fatal(err) 350 } 351 352 // assert the worker gets removed from the pool if its host appears offline 353 deps.Enable() 354 err = build.Retry(600, 100*time.Millisecond, func() error { 355 workers := wt.rt.renter.staticWorkerPool.callWorkers() 356 if len(workers) != 0 { 357 wt.rt.renter.staticWorkerPool.callUpdate(wt.rt.renter) 358 return errors.New("worker not removed") 359 } 360 return nil 361 }) 362 if err != nil { 363 t.Fatal(err) 364 } 365 366 // assert the worker gets re-added to the pool if its host comes online 367 deps.Disable() 368 err = build.Retry(600, 100*time.Millisecond, func() error { 369 workers := wt.rt.renter.staticWorkerPool.callWorkers() 370 if len(workers) == 0 { 371 wt.rt.renter.staticWorkerPool.callUpdate(wt.rt.renter) 372 return errors.New("no workers in pool") 373 } 374 return nil 375 }) 376 if err != nil { 377 t.Fatal(err) 378 } 379 } 380 381 // TestWorkerSpending is a unit test that verifies several actions and whether 382 // or not those actions' spending are properly reflected in the contract header. 383 func TestWorkerSpending(t *testing.T) { 384 if testing.Short() { 385 t.SkipNow() 386 } 387 t.Parallel() 388 389 // Create a worker that's not running its worker loop. 390 wt, err := newWorkerTesterCustomDependency(t.Name(), &dependencies.DependencyDisableWorker{}, modules.ProdDependencies) 391 if err != nil { 392 t.Fatal(err) 393 } 394 defer func() { 395 // Ignore threadgroup stopped error since we are manually closing the 396 // threadgroup of the worker. 397 if err := wt.Close(); err != nil && !errors.Contains(err, threadgroup.ErrStopped) { 398 t.Fatal(err) 399 } 400 }() 401 w := wt.worker 402 403 // getRenterContract is a helper function that fetches the contract 404 getRenterContract := func() skymodules.RenterContract { 405 host := w.staticHostPubKey 406 rc, found := w.staticRenter.staticHostContractor.ContractByPublicKey(host) 407 if !found { 408 t.Fatal("unexpected") 409 } 410 return rc 411 } 412 rc := getRenterContract() 413 414 // Assert the initial spending metrics are all zero 415 if !rc.FundAccountSpending.IsZero() || !rc.MaintenanceSpending.Sum().IsZero() || !rc.UploadSpending.IsZero() { 416 t.Fatal("unexpected") 417 } 418 419 // Get a price table and verify whether the spending cost is reflected in 420 // the spending metrics. 421 wt.staticUpdatePriceTable() 422 rc = getRenterContract() 423 pt := wt.staticPriceTable().staticPriceTable 424 if !rc.MaintenanceSpending.UpdatePriceTableCost.Equals(pt.UpdatePriceTableCost) { 425 t.Fatal("unexpected") 426 } 427 428 // Manually refill the account and verify whether the spending costs are 429 // reflected in the spending metrics. 430 w.managedRefillAccount() 431 rc = getRenterContract() 432 if !rc.MaintenanceSpending.FundAccountCost.Equals(pt.FundAccountCost) || rc.FundAccountSpending.IsZero() { 433 t.Fatal("unexpected") 434 } 435 436 // Manually sync the account balance and verify whether the spending costs 437 // are reflected in the spending metrics. 438 w.externSyncAccountBalanceToHost(false) 439 rc = getRenterContract() 440 if !rc.MaintenanceSpending.FundAccountCost.Equals(pt.AccountBalanceCost) { 441 t.Fatal("unexpected") 442 } 443 444 // Verify the sum is equal to the cost of the 3 RPCs we've just performed. 445 if !rc.MaintenanceSpending.Sum().Equals(pt.AccountBalanceCost.Add(pt.UpdatePriceTableCost).Add(pt.FundAccountCost)) { 446 t.Fatal("unexpected") 447 } 448 449 // Upload a snapshot and verify whether the spending metrics reflect the 450 // upload. 451 uploadSnapshotRespChan := make(chan *jobUploadSnapshotResponse) 452 jus := &jobUploadSnapshot{ 453 staticSiaFileData: fastrand.Bytes(100), 454 staticResponseChan: uploadSnapshotRespChan, 455 jobGeneric: newJobGeneric(context.Background(), w.staticJobUploadSnapshotQueue, skymodules.UploadedBackup{UID: [16]byte{3, 2, 1}}), 456 } 457 w.externLaunchSerialJob(jus.callExecute) 458 select { 459 case <-uploadSnapshotRespChan: 460 case <-time.After(time.Minute): 461 t.Fatal("unexpected timeout") 462 } 463 rc = getRenterContract() 464 if rc.UploadSpending.IsZero() { 465 t.Fatal("unexpected") 466 } 467 }