gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/churnlimiter.go (about)

     1  package contractor
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"sync"
     7  
     8  	"gitlab.com/SkynetLabs/skyd/build"
     9  	"gitlab.com/SkynetLabs/skyd/skymodules"
    10  	"go.sia.tech/siad/types"
    11  
    12  	"gitlab.com/NebulousLabs/errors"
    13  )
    14  
    15  // contractScoreAndUtil combines a contract with its host's score and an updated
    16  // utility.
    17  type contractScoreAndUtil struct {
    18  	contract skymodules.RenterContract
    19  	score    types.Currency
    20  	util     skymodules.ContractUtility
    21  }
    22  
    23  // churnLimiter keeps track of the aggregate number of bytes stored in contracts
    24  // marked !GFR (AKA churned contracts) in the current period.
    25  type churnLimiter struct {
    26  	// remainingChurnBudget is the number of bytes that the churnLimiter will
    27  	// allow to be churned in contracts at the present moment. Note that this
    28  	// value may be negative.
    29  	remainingChurnBudget int
    30  
    31  	// aggregateCurrentPeriodChurn is the aggregate size of files stored in contracts
    32  	// churned in the current period.
    33  	aggregateCurrentPeriodChurn uint64
    34  
    35  	mu               sync.Mutex
    36  	staticContractor *Contractor
    37  }
    38  
    39  // churnLimiterPersist is the persisted state of a churnLimiter.
    40  type churnLimiterPersist struct {
    41  	AggregateCurrentPeriodChurn uint64 `json:"aggregatecurrentperiodchurn"`
    42  	RemainingChurnBudget        int    `json:"remainingchurnbudget"`
    43  }
    44  
    45  // managedMaxPeriodChurn returns the MaxPeriodChurn of the churnLimiter.
    46  func (cl *churnLimiter) managedMaxPeriodChurn() uint64 {
    47  	return cl.staticContractor.Allowance().MaxPeriodChurn
    48  }
    49  
    50  // callPersistData returns the churnLimiterPersist corresponding to this
    51  // churnLimiter's state
    52  func (cl *churnLimiter) callPersistData() churnLimiterPersist {
    53  	cl.mu.Lock()
    54  	defer cl.mu.Unlock()
    55  	return churnLimiterPersist{cl.aggregateCurrentPeriodChurn, cl.remainingChurnBudget}
    56  }
    57  
    58  // newChurnLimiterFromPersist creates a new churnLimiter using persisted state.
    59  func newChurnLimiterFromPersist(contractor *Contractor, persistData churnLimiterPersist) *churnLimiter {
    60  	return &churnLimiter{
    61  		staticContractor:            contractor,
    62  		aggregateCurrentPeriodChurn: persistData.AggregateCurrentPeriodChurn,
    63  		remainingChurnBudget:        persistData.RemainingChurnBudget,
    64  	}
    65  }
    66  
    67  // newChurnLimiter returns a new churnLimiter.
    68  func newChurnLimiter(contractor *Contractor) *churnLimiter {
    69  	return &churnLimiter{staticContractor: contractor}
    70  }
    71  
    72  // ChurnStatus returns the current period's aggregate churn and the max churn
    73  // per period.
    74  func (c *Contractor) ChurnStatus() skymodules.ContractorChurnStatus {
    75  	aggregateChurn, maxChurn := c.staticChurnLimiter.managedAggregateAndMaxChurn()
    76  	return skymodules.ContractorChurnStatus{
    77  		AggregateCurrentPeriodChurn: aggregateChurn,
    78  		MaxPeriodChurn:              maxChurn,
    79  	}
    80  }
    81  
    82  // callResetAggregateChurn resets the aggregate churn for this period. This
    83  // method must be called at the beginning of every new period.
    84  func (cl *churnLimiter) callResetAggregateChurn() {
    85  	cl.mu.Lock()
    86  	cl.staticContractor.staticLog.Println("Aggregate Churn for last period: ", cl.aggregateCurrentPeriodChurn)
    87  	cl.aggregateCurrentPeriodChurn = 0
    88  	cl.mu.Unlock()
    89  }
    90  
    91  // callNotifyChurnedContract adds the size of this contract's files to the aggregate
    92  // churn in this period. Must be called when contracts are marked !GFR.
    93  func (cl *churnLimiter) callNotifyChurnedContract(contract skymodules.RenterContract) {
    94  	size := contract.Transaction.FileContractRevisions[0].NewFileSize
    95  	if size == 0 {
    96  		return
    97  	}
    98  	maxPeriodChurn := cl.managedMaxPeriodChurn()
    99  
   100  	cl.mu.Lock()
   101  	defer cl.mu.Unlock()
   102  
   103  	cl.aggregateCurrentPeriodChurn += size
   104  	cl.remainingChurnBudget -= int(size)
   105  	cl.staticContractor.staticLog.Debugf("Increasing aggregate churn by %d to %d (MaxPeriodChurn: %d)", size, cl.aggregateCurrentPeriodChurn, maxPeriodChurn)
   106  	cl.staticContractor.staticLog.Debugf("Remaining churn budget: %d", cl.remainingChurnBudget)
   107  }
   108  
   109  // callBumpChurnBudget increases the churn budget by a fraction of the max churn
   110  // budget per period. Used when new blocks are processed.
   111  func (cl *churnLimiter) callBumpChurnBudget(numBlocksAdded int, period types.BlockHeight) {
   112  	// Don't add to churn budget when there is no period, since no allowance is
   113  	// set yet.
   114  	if period == types.BlockHeight(0) {
   115  		return
   116  	}
   117  	maxPeriodChurn := cl.managedMaxPeriodChurn()
   118  	maxChurnBudget := cl.managedMaxChurnBudget()
   119  	refillBudget := cl.staticContractor.staticDeps.Disrupt("RefillChurnBudget")
   120  	cl.mu.Lock()
   121  	defer cl.mu.Unlock()
   122  
   123  	// Increase churn budget as a multiple of the period budget per block. This
   124  	// let's the remainingChurnBudget increase more quickly.
   125  	budgetIncrease := numBlocksAdded * int(maxPeriodChurn/uint64(period))
   126  	cl.remainingChurnBudget += budgetIncrease
   127  	if refillBudget || cl.remainingChurnBudget > maxChurnBudget {
   128  		cl.remainingChurnBudget = maxChurnBudget
   129  	}
   130  	cl.staticContractor.staticLog.Debugf("Updated churn budget: %d", cl.remainingChurnBudget)
   131  }
   132  
   133  // managedMaxChurnBudget returns the max allowed value for remainingChurnBudget.
   134  func (cl *churnLimiter) managedMaxChurnBudget() int {
   135  	// Do not let churn budget to build up to maxPeriodChurn to avoid using entire
   136  	// period budget at once (except in special circumstances).
   137  	return int(cl.managedMaxPeriodChurn() / 2)
   138  }
   139  
   140  // managedProcessSuggestedUpdates processes suggested utility updates. It
   141  // prevents contracts from being marked as !GFR if the churn limit has been
   142  // reached. The inputs are assumed to be contracts that have passed all critical
   143  // utility checks. The result is the list of updates which need to be applied.
   144  func (cl *churnLimiter) managedProcessSuggestedUpdates(queue []contractScoreAndUtil) map[types.FileContractID]skymodules.ContractUtility {
   145  	sort.Slice(queue, func(i, j int) bool {
   146  		return queue[i].score.Cmp(queue[j].score) < 0
   147  	})
   148  
   149  	var queuedContract contractScoreAndUtil
   150  	contractUpdates := make(map[types.FileContractID]skymodules.ContractUtility)
   151  	for len(queue) > 0 {
   152  		queuedContract, queue = queue[0], queue[1:]
   153  
   154  		// Churn a contract if it went from GFR in the previous util
   155  		// (queuedContract.contract.Utility) to !GFR in the suggested util
   156  		// (queuedContract.util) and the churnLimit has not been reached.
   157  		turnedNotGFR := queuedContract.contract.Utility.GoodForRenew && !queuedContract.util.GoodForRenew
   158  		churningThisContract := turnedNotGFR && cl.managedCanChurnContract(queuedContract.contract)
   159  		if turnedNotGFR && !churningThisContract {
   160  			cl.staticContractor.staticLog.Debugln("Avoiding churn on contract: ", queuedContract.contract.ID)
   161  			currentBudget, periodBudget := cl.managedChurnBudget()
   162  			cl.staticContractor.staticLog.Debugf("Remaining Churn Budget: %d. Remaining Period Budget: %d", currentBudget, periodBudget)
   163  			queuedContract.util.GoodForRenew = true
   164  		}
   165  
   166  		if churningThisContract {
   167  			cl.staticContractor.staticLog.Println("Churning contract for bad score: ", queuedContract.contract.ID, queuedContract.score)
   168  
   169  			// If we are churning this contract, mark it as churned.
   170  			// That way the churn is considered in the next
   171  			// iteration.
   172  			cl.callNotifyChurnedContract(queuedContract.contract)
   173  		}
   174  
   175  		// Remember contract for updating.
   176  		contractUpdates[queuedContract.contract.ID] = queuedContract.util
   177  	}
   178  	return contractUpdates
   179  }
   180  
   181  // managedChurnBudget returns the current remaining churn budget, and the remaining
   182  // budget for the period.
   183  func (cl *churnLimiter) managedChurnBudget() (int, int) {
   184  	maxPeriodChurn := cl.managedMaxPeriodChurn()
   185  	cl.mu.Lock()
   186  	defer cl.mu.Unlock()
   187  	return cl.remainingChurnBudget, int(maxPeriodChurn) - int(cl.aggregateCurrentPeriodChurn)
   188  }
   189  
   190  // managedAggregateAndMaxChurn returns the aggregate churn for the current period,
   191  // and the maximum churn allowed per period.
   192  func (cl *churnLimiter) managedAggregateAndMaxChurn() (uint64, uint64) {
   193  	maxPeriodChurn := cl.managedMaxPeriodChurn()
   194  	cl.mu.Lock()
   195  	defer cl.mu.Unlock()
   196  	return cl.aggregateCurrentPeriodChurn, maxPeriodChurn
   197  }
   198  
   199  // managedCanChurnContract returns true if and only if the churnLimiter can
   200  // churn the contract right now, given its current budget.
   201  func (cl *churnLimiter) managedCanChurnContract(contract skymodules.RenterContract) bool {
   202  	size := contract.Transaction.FileContractRevisions[0].NewFileSize
   203  	maxPeriodChurn := cl.managedMaxPeriodChurn()
   204  	maxChurnBudget := cl.managedMaxChurnBudget()
   205  	cl.mu.Lock()
   206  	defer cl.mu.Unlock()
   207  
   208  	// Allow any size contract to be churned if the current budget is the max
   209  	// budget. This allows large contracts to be churned if there is enough budget
   210  	// remaining for the period, even if the contract is larger than the
   211  	// maxChurnBudget.
   212  	fitsInCurrentBudget := (cl.remainingChurnBudget-int(size) >= 0) || (cl.remainingChurnBudget == maxChurnBudget)
   213  	fitsInPeriodBudget := (int(maxPeriodChurn) - int(cl.aggregateCurrentPeriodChurn) - int(size)) >= 0
   214  
   215  	// If there has been no churn in this period, allow any size contract to be
   216  	// churned.
   217  	fitsInPeriodBudget = fitsInPeriodBudget || (cl.aggregateCurrentPeriodChurn == 0)
   218  
   219  	return fitsInPeriodBudget && fitsInCurrentBudget
   220  }
   221  
   222  // managedMarkContractUtility checks an active contract in the contractor and
   223  // figures out whether the contract is useful for uploading, and whether the
   224  // contract should be renewed.
   225  func (c *Contractor) managedMarkContractUtility(contract skymodules.RenterContract, minScoreGFR, minScoreGFU types.Currency, endHeight types.BlockHeight) (contractScoreAndUtil, utilityUpdateStatus, error) {
   226  	// Acquire contract.
   227  	sc, ok := c.staticContracts.Acquire(contract.ID)
   228  	if !ok {
   229  		return contractScoreAndUtil{}, noUpdate, errors.New("managedMarkContractUtility: Unable to acquire contract")
   230  	}
   231  	defer c.staticContracts.Return(sc)
   232  
   233  	// Get latest metadata.
   234  	u := sc.Metadata().Utility
   235  
   236  	// If the utility is locked, do nothing.
   237  	if u.Locked {
   238  		return contractScoreAndUtil{}, noUpdate, nil
   239  	}
   240  
   241  	// Get host from hostdb and check that it's not filtered.
   242  	host, u, needsUpdate := c.managedHostInHostDBCheck(contract)
   243  	if needsUpdate {
   244  		return contractScoreAndUtil{contract: contract, score: types.ZeroCurrency, util: u}, necessaryUtilityUpdate, nil
   245  	}
   246  
   247  	sb, err := c.staticHDB.ScoreBreakdown(host.HostDBEntry)
   248  	if err != nil {
   249  		c.staticLog.Println("Unable to get ScoreBreakdown for", host.PublicKey.String(), "got err:", err)
   250  		return contractScoreAndUtil{}, noUpdate, nil // it may just be this host that has an issue.
   251  	}
   252  
   253  	// Do critical contract checks and update the utility if any checks fail.
   254  	u, utilityUpdateStatus := c.managedUtilityChecks(sc.Metadata(), host, sb, minScoreGFU, minScoreGFR, endHeight)
   255  
   256  	// Sanity check
   257  	if utilityUpdateStatus == noUpdate && contract.Utility != u {
   258  		build.Critical("managedMarkContractUtility: status was 'noUpdate' but utility changed")
   259  	}
   260  
   261  	return contractScoreAndUtil{
   262  		contract: contract,
   263  		score:    sb.Score,
   264  		util:     u,
   265  	}, utilityUpdateStatus, nil
   266  }
   267  
   268  // managedMarkContractsUtility checks every active contract in the contractor and
   269  // figures out whether the contract is useful for uploading, and whether the
   270  // contract should be renewed.
   271  func (c *Contractor) managedMarkContractsUtility(wantedHosts uint64, endHeight types.BlockHeight) error {
   272  	minScoreGFR, minScoreGFU, err := c.managedFindMinAllowedHostScores(wantedHosts)
   273  	if err != nil {
   274  		return err
   275  	}
   276  
   277  	// Queue for possible contracts to churn. Passed to churnLimiter for final
   278  	// judgment.
   279  	suggestedUpdateQueue := make([]contractScoreAndUtil, 0)
   280  
   281  	// These updates are always applied.
   282  	necessaryUpdateQueue := make([]contractScoreAndUtil, 0)
   283  
   284  	// Sort the updates into suggested and necessary ones.
   285  	for _, contract := range c.staticContracts.ViewAll() {
   286  		update, uus, err := c.managedMarkContractUtility(contract, minScoreGFR, minScoreGFU, endHeight)
   287  		if err != nil {
   288  			return err
   289  		}
   290  
   291  		switch uus {
   292  		case noUpdate:
   293  		case suggestedUtilityUpdate:
   294  			suggestedUpdateQueue = append(suggestedUpdateQueue, update)
   295  		case necessaryUtilityUpdate:
   296  			necessaryUpdateQueue = append(necessaryUpdateQueue, update)
   297  		default:
   298  			err := fmt.Errorf("undefined checkHostScore utilityUpdateStatus %v %v", uus, contract.ID)
   299  			c.staticLog.Critical(err)
   300  		}
   301  	}
   302  
   303  	// Apply the necessary updates. These always have to be applied so we
   304  	// apply them separately.
   305  	for _, nu := range necessaryUpdateQueue {
   306  		if nu.contract.Utility.GoodForRenew && !nu.util.GoodForRenew {
   307  			c.staticChurnLimiter.callNotifyChurnedContract(nu.contract)
   308  		}
   309  	}
   310  
   311  	// Filter the suggested updates through the churn limiter.
   312  	contractUpdates := c.staticChurnLimiter.managedProcessSuggestedUpdates(suggestedUpdateQueue)
   313  
   314  	// Merge the necessary updates in.
   315  	for _, nu := range necessaryUpdateQueue {
   316  		_, exists := contractUpdates[nu.contract.ID]
   317  		if exists {
   318  			build.Critical("managedMarkContractsUtility: there shouldn't be both a suggested as well as a necessary update for a single contract")
   319  		}
   320  		contractUpdates[nu.contract.ID] = nu.util
   321  	}
   322  
   323  	// Get all contracts and apply the updates to them in-memory only.
   324  	contractsAfterUpdates := c.Contracts()
   325  	for i, contract := range contractsAfterUpdates {
   326  		utility, exists := contractUpdates[contract.ID]
   327  		if exists {
   328  			contractsAfterUpdates[i].Utility = utility
   329  		}
   330  	}
   331  
   332  	// Limit the number of active contracts.
   333  	c.managedLimitGFUHosts(contractsAfterUpdates, contractUpdates, wantedHosts)
   334  
   335  	// Apply the suggested updates.
   336  	for id, utility := range contractUpdates {
   337  		// We already called callNotifyChurnedContract for the necessary
   338  		// updates and managedProcessSuggestedUpdates called it for
   339  		// suggested updates IFF they were churned. So we can ignore
   340  		// that here and pass in 'true'.
   341  		if err := c.managedAcquireAndUpdateContractUtility(id, utility, true); err != nil {
   342  			return errors.AddContext(err, "managedMarkContractsUtility: failed to apply update to contract")
   343  		}
   344  	}
   345  	return nil
   346  }