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 }