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  }