gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/workersubscription.go (about) 1 package renter 2 3 import ( 4 "context" 5 "encoding/hex" 6 "fmt" 7 "io" 8 "sync" 9 "sync/atomic" 10 "time" 11 "unsafe" 12 13 "gitlab.com/NebulousLabs/errors" 14 "gitlab.com/NebulousLabs/fastrand" 15 "gitlab.com/NebulousLabs/siamux" 16 "gitlab.com/NebulousLabs/threadgroup" 17 "gitlab.com/SkynetLabs/skyd/build" 18 "gitlab.com/SkynetLabs/skyd/skymodules" 19 "gitlab.com/SkynetLabs/skyd/skymodules/gouging" 20 "go.sia.tech/siad/modules" 21 "go.sia.tech/siad/types" 22 ) 23 24 // TODO: (f/u) cooldown testing 25 26 var ( 27 // initialSubscriptionBudget is the initial budget withdrawn for a 28 // subscription. After using up 50% of it, the worker refills the budget 29 // again to match the initial budget. 30 initialSubscriptionBudget = modules.DefaultMaxEphemeralAccountBalance.Div64(10) // 10% of the max 31 32 // subscriptionCooldownResetInterval is the time after which we consider an 33 // ongoing subscription to be healthy enough to reset the consecutive 34 // failures. 35 subscriptionCooldownResetInterval = build.Select(build.Var{ 36 Testing: time.Second * 5, 37 Dev: time.Minute, 38 Standard: time.Hour, 39 }).(time.Duration) 40 41 // subscriptionLoopInterval is the interval after which the subscription 42 // loop checks for work when it's idle. Idle means the staticWakeChan isn't 43 // signaling new work. 44 subscriptionLoopInterval = time.Second 45 46 // stopSubscriptionGracePeriod is the period of time we wait after signaling 47 // the host that we want to stop the subscription. All the incoming 48 // bandwidth within this period will be accounted for correctly and help us 49 // to keep our EA balance expectations in sync with the host's. 50 stopSubscriptionGracePeriod = build.Select(build.Var{ 51 Testing: time.Second, 52 Dev: 3 * time.Second, 53 Standard: 3 * time.Second, 54 }).(time.Duration) 55 56 // priceTableRetryInterval is the interval the subscription loop waits for 57 // the maintenance to update the price table before checking again. 58 priceTableRetryInterval = time.Second 59 ) 60 61 // minSubscriptionVersion is the min version required for a host to support the 62 // subscription protocol. 63 const minSubscriptionVersion = "1.5.5" 64 65 // minSubscribeByRIDVersion is the min version required for a host to support 66 // the subscribe-by-entryid protocol upgrade. 67 const minSubscribeByRIDVersion = "1.5.8" 68 69 type ( 70 // subscriptionInfos contains all of the registry subscription related 71 // information of a worker. 72 subscriptionInfos struct { 73 // subscriptions is the map of subscriptions that the worker is supposed 74 // to subscribe to. The worker might not be subscribed to these values 75 // at all times due to interruptions but it will try to resubscribe as 76 // soon as possible. 77 // The worker will also try to unsubscribe from all subscriptions that 78 // it currently has which it is not supposed to be subscribed to. 79 subscriptions map[modules.RegistryEntryID]*subscription 80 81 // staticWakeChan is a channel to tell the subscription loop that more 82 // work is available. 83 staticWakeChan chan struct{} 84 85 // stats 86 atomicExtensions uint64 87 88 // cooldown 89 cooldownUntil time.Time 90 consecutiveFailures uint64 91 92 // staticManager manages the subscriptions across workers. 93 staticManager subscriptionManager 94 95 // utility fields 96 mu sync.Mutex 97 } 98 99 // subscription is a struct that provides additional information around a 100 // subscription. 101 subscription struct { 102 staticRequest *skymodules.SubscriptionRequest 103 104 // subscribe indicates whether the subscription should be kept active. 105 // If it is 'true', the worker will try to resubscribe if the session is 106 // interrupted. 107 subscribe bool 108 109 // subscribed is closed as soon as the corresponding entry is subscribed 110 // to and indicates that the worker is actively listening for updates. 111 // It's also closed when a subscription is deleted from the map due to 112 // no longer being necessary. 113 subscribed chan struct{} 114 115 // latestRV is kept up-to-date with the latest known value for a 116 // subscribed entry and should only be checked if 'subscribed' is 117 // closed. It may be 'nil' even though a subscription is active in case 118 // the host doesn't know the subscribed entry. If the host does know, 119 // the initial value should be set before closing 'subscribed'. 120 latestRV *modules.SignedRegistryValue 121 } 122 123 // notificationHandler is a helper type that contains some information 124 // relevant to notification pricing and updating price tables. 125 notificationHandler struct { 126 staticStream io.Closer // stream of the main subscription thread 127 staticWorker *worker 128 129 staticPTUpdateChan chan struct{} // used by notification thread to signal subscription thread to update the price table 130 staticPTUpdatedChan chan struct{} // used by subscription thread to signal notification thread that price table update is done 131 132 notificationCost types.Currency 133 mu sync.Mutex 134 } 135 ) 136 137 // newSubscription creates a new subscription. 138 func newSubscription(request *skymodules.SubscriptionRequest) *subscription { 139 return &subscription{ 140 staticRequest: request, 141 subscribed: make(chan struct{}), 142 subscribe: true, 143 } 144 } 145 146 // active returns 'true' if the subscription is currently active. That means the 147 // subscribed channel was closed after a successful subscription request. 148 func (sub *subscription) active() bool { 149 select { 150 case <-sub.subscribed: 151 return true 152 default: 153 } 154 return false 155 } 156 157 // managedHandleRegistryEntry is called by managedHandleNotification to handle a 158 // notification about an updated registry entry. 159 func (nh *notificationHandler) managedHandleRegistryEntry(stream siamux.Stream, budget *modules.RPCBudget, limit *modules.BudgetLimit) (err error) { 160 w := nh.staticWorker 161 subInfo := w.staticSubscriptionInfo 162 163 // Add a limit to the stream. 164 err = stream.SetLimit(limit) 165 if err != nil { 166 return errors.AddContext(err, "failed to set limit on notification stream") 167 } 168 169 // Withdraw notification cost. 170 nh.mu.Lock() 171 ok := budget.Withdraw(nh.notificationCost) 172 nh.mu.Unlock() 173 if !ok { 174 return errors.New("failed to withdraw notification cost") 175 } 176 177 // Read the update. 178 var sneu modules.RPCRegistrySubscriptionNotificationEntryUpdate 179 err = modules.RPCRead(stream, &sneu) 180 if err != nil { 181 return errors.AddContext(err, "failed to read entry update") 182 } 183 184 // Starting here we close the main subscription stream if an error happens 185 // because it will be the host trying to cheat us. 186 defer func() { 187 if err != nil { 188 err = errors.Compose(err, nh.staticStream.Close()) 189 } 190 }() 191 192 // Verify the signature. 193 err = sneu.Entry.Verify(sneu.PubKey.ToPublicKey()) 194 if err != nil { 195 return errors.AddContext(err, "failed to verify signature") 196 } 197 198 // Check if the host was trying to cheat us with an outdated entry. 199 rid := modules.DeriveRegistryEntryID(sneu.PubKey, sneu.Entry.Tweak) 200 err = w.managedCheckHostCheating(rid, &sneu.Entry, false) 201 if err != nil { 202 return errors.AddContext(err, "host provided outdated entry") 203 } 204 205 // Check if the host sent us an update we are not subsribed to. This might 206 // not seem bad, but the host might want to spam us with valid entries that 207 // we are not interested in simply to have us pay for bandwidth. 208 subInfo.mu.Lock() 209 sub, exists := subInfo.subscriptions[modules.DeriveRegistryEntryID(sneu.PubKey, sneu.Entry.Tweak)] 210 if !exists { 211 subInfo.mu.Unlock() 212 return fmt.Errorf("subscription not found") 213 } 214 var shouldUpdate bool 215 if sub.latestRV != nil { 216 shouldUpdate, _ = sub.latestRV.ShouldUpdateWith(&sneu.Entry.RegistryValue, w.staticHostPubKey) 217 if !shouldUpdate { 218 subInfo.mu.Unlock() 219 return fmt.Errorf("host sent an outdated revision %v >= %v", sub.latestRV.Revision, sneu.Entry.Revision) 220 } 221 } 222 223 // Update the subscription. 224 sub.latestRV = &sneu.Entry 225 subInfo.mu.Unlock() 226 subInfo.staticManager.Notify(w.staticHostPubKey, []skymodules.SubscriptionRequest{ 227 { 228 EntryID: modules.DeriveRegistryEntryID(sneu.PubKey, sneu.Entry.Tweak), 229 PubKey: &sneu.PubKey, 230 Tweak: &sneu.Entry.Tweak, 231 }, 232 }, sneu) 233 return nil 234 } 235 236 // managedHandleSubscriptionSuccess is called by managedHandleNotification to 237 // handle a subscription success notification. 238 func (nh *notificationHandler) managedHandleSubscriptionSuccess(stream siamux.Stream, limit *modules.BudgetLimit) error { 239 // Tell the subscription thread to update the limits using the new price 240 // table. 241 select { 242 case <-nh.staticWorker.staticTG.StopChan(): 243 return nil // shutdown 244 case nh.staticPTUpdateChan <- struct{}{}: 245 } 246 // Wait for the subscription thread to be done updating the limits. 247 select { 248 case <-nh.staticWorker.staticTG.StopChan(): 249 return nil // shutdown 250 case <-nh.staticPTUpdatedChan: 251 } 252 // Since this stream uses the new costs we set the limit after updating 253 // the costs. 254 err := stream.SetLimit(limit) 255 if err != nil { 256 return errors.AddContext(err, "extension 'ok' was received but failed to update limit on stream") 257 } 258 return nil 259 } 260 261 // managedHandleNotification handles incoming notifications from the host. It 262 // verifies notifications and updates the worker's internal state accordingly. 263 // Since it's registered as a handle which is called in a separate goroutine it 264 // doesn't return an error. 265 func (nh *notificationHandler) managedHandleNotification(stream siamux.Stream, budget *modules.RPCBudget, limit *modules.BudgetLimit) { 266 w := nh.staticWorker 267 // Close the stream when done. 268 defer func() { 269 if err := stream.Close(); err != nil { 270 w.staticRenter.staticLog.Print("managedHandleNotification: failed to close stream: ", err) 271 } 272 }() 273 274 // The stream should have a sane deadline. 275 err := stream.SetDeadline(time.Now().Add(defaultNewStreamTimeout)) 276 if err != nil { 277 w.staticRenter.staticLog.Print("managedHandleNotification: failed to set deadlien on stream: ", err) 278 return 279 } 280 281 // Read the notification type. 282 var snt modules.RPCRegistrySubscriptionNotificationType 283 err = modules.RPCRead(stream, &snt) 284 if err != nil { 285 w.staticRenter.staticLog.Print("managedHandleNotification: failed to read notification type: ", err) 286 return 287 } 288 289 // Handle the notification. 290 switch snt.Type { 291 case modules.SubscriptionResponseSubscriptionSuccess: 292 if err := nh.managedHandleSubscriptionSuccess(stream, limit); err != nil { 293 w.staticRenter.staticLog.Print("managedHAndleSubscriptionSuccess:", err) 294 } 295 return 296 case modules.SubscriptionResponseRegistryValue: 297 if err := nh.managedHandleRegistryEntry(stream, budget, limit); err != nil { 298 w.staticRenter.staticLog.Print("managedHandleRegistryEntry:", err) 299 } 300 return 301 default: 302 } 303 304 // TODO: (f/u) Punish the host by adding a subscription cooldown. 305 w.staticRenter.staticLog.Print("managedHandleNotification: unknown notification type") 306 if err := nh.staticStream.Close(); err != nil { 307 w.staticRenter.staticLog.Debugln("managedHandleNotification: failed to close subscription:", err) 308 } 309 } 310 311 // managedClearSubscription replaces the channels of all subscriptions of the 312 // subInfo. This unblocks anyone waiting for a subscription to be established. 313 func (subInfo *subscriptionInfos) managedClearSubscriptions() { 314 subInfo.mu.Lock() 315 defer subInfo.mu.Unlock() 316 for _, sub := range subInfo.subscriptions { 317 // Replace channels. 318 select { 319 case <-sub.subscribed: 320 default: 321 close(sub.subscribed) 322 } 323 sub.subscribed = make(chan struct{}) 324 } 325 } 326 327 // managedIncrementCooldown increments the subscription cooldown. 328 func (subInfo *subscriptionInfos) managedIncrementCooldown() { 329 subInfo.mu.Lock() 330 defer subInfo.mu.Unlock() 331 332 // If the last cooldown ended a while ago, we reset the consecutive 333 // failures. 334 if time.Now().Sub(subInfo.cooldownUntil) > subscriptionCooldownResetInterval { 335 subInfo.consecutiveFailures = 0 336 } 337 338 // Increment the cooldown. 339 subInfo.cooldownUntil = cooldownUntil(subInfo.consecutiveFailures) 340 subInfo.consecutiveFailures++ 341 } 342 343 // managedOnCooldown returns whether the subscription cooldown is active and its 344 // remaining time. 345 func (subInfo *subscriptionInfos) managedOnCooldown() (time.Duration, bool) { 346 subInfo.mu.Lock() 347 defer subInfo.mu.Unlock() 348 return time.Until(subInfo.cooldownUntil), time.Now().Before(subInfo.cooldownUntil) 349 } 350 351 // managedSubscriptionDiff returns the difference between the desired 352 // subscriptions and the active subscriptions. It also returns a slice of 353 // channels which need to be closed when the corresponding desired subscription 354 // was established. 355 func (subInfo *subscriptionInfos) managedSubscriptionDiff() (toSubscribe, toUnsubscribe []skymodules.SubscriptionRequest, subChans []chan struct{}) { 356 subInfo.mu.Lock() 357 defer subInfo.mu.Unlock() 358 for sid, sub := range subInfo.subscriptions { 359 if !sub.subscribe && !sub.active() { 360 // Delete the subscription. We are neither supposed to subscribe 361 // to it nor are we subscribed to it. 362 delete(subInfo.subscriptions, sid) 363 // Close its channel. 364 close(sub.subscribed) 365 } else if sub.active() && !sub.subscribe { 366 // Unsubscribe from the entry. 367 toUnsubscribe = append(toUnsubscribe, *sub.staticRequest) 368 } else if !sub.active() && sub.subscribe { 369 // Subscribe and remember the channel to close it later. 370 toSubscribe = append(toSubscribe, *sub.staticRequest) 371 subChans = append(subChans, sub.subscribed) 372 } 373 } 374 return 375 } 376 377 // managedExtendSubscriptionPeriod extends the ongoing subscription with a host 378 // and adjusts the deadline on the stream. 379 func (w *worker) managedExtendSubscriptionPeriod(stream siamux.Stream, budget *modules.RPCBudget, limit *modules.BudgetLimit, oldDeadline time.Time, oldPT *modules.RPCPriceTable, nh *notificationHandler) (*modules.RPCPriceTable, time.Time, error) { 380 subInfo := w.staticSubscriptionInfo 381 382 // Get a pricetable that is valid until the new deadline. 383 newDeadline := oldDeadline.Add(modules.SubscriptionPeriod) 384 newPT := w.managedPriceTableForSubscription(time.Until(newDeadline)) 385 if newPT == nil { 386 return nil, time.Time{}, threadgroup.ErrStopped // shutdown 387 } 388 389 // Try extending the subscription. 390 err := modules.RPCExtendSubscription(stream, newPT) 391 if err != nil { 392 return nil, time.Time{}, errors.AddContext(err, "failed to extend subscription") 393 } 394 395 // Wait for "ok". 396 select { 397 case <-w.staticTG.StopChan(): 398 return nil, time.Time{}, threadgroup.ErrStopped 399 case <-time.After(time.Until(newDeadline)): 400 return nil, time.Time{}, errors.New("never received the 'ok' response for extending the subscription") 401 case <-nh.staticPTUpdateChan: 402 } 403 404 // Update limit and notification cost. 405 limit.UpdateCosts(newPT.DownloadBandwidthCost, newPT.UploadBandwidthCost) 406 nh.mu.Lock() 407 nh.notificationCost = newPT.SubscriptionNotificationCost 408 nh.mu.Unlock() 409 410 // Tell the notification goroutine that the limit was updated. 411 select { 412 case <-w.staticTG.StopChan(): 413 return nil, time.Time{}, threadgroup.ErrStopped 414 case <-time.After(time.Until(newDeadline)): 415 return nil, time.Time{}, errors.New("notification thread never read the update signal") 416 case nh.staticPTUpdatedChan <- struct{}{}: 417 } 418 419 // Count the number of active subscriptions. 420 var nSubs uint64 421 for _, sub := range subInfo.subscriptions { 422 if sub.active() { 423 nSubs++ 424 } 425 } 426 427 // Withdraw from budget. 428 if !budget.Withdraw(modules.MDMSubscriptionMemoryCost(newPT, nSubs)) { 429 return nil, time.Time{}, errors.New("failed to withdraw subscription extension cost from budget") 430 } 431 432 // Set the stream deadline to the new subscription deadline. 433 err = stream.SetDeadline(newDeadline) 434 if err != nil { 435 return nil, time.Time{}, errors.AddContext(err, "failed to set stream deadlien to subscription deadline") 436 } 437 438 // Increment stats for extending the subscription. 439 atomic.AddUint64(&subInfo.atomicExtensions, 1) 440 return newPT, newDeadline, nil 441 } 442 443 // managedRefillSubscription refills the subscription up until expectedBudget. 444 func (w *worker) managedRefillSubscription(stream siamux.Stream, pt *modules.RPCPriceTable, expectedBudget types.Currency, budget *modules.RPCBudget) (err error) { 445 fundAmt := expectedBudget.Sub(budget.Remaining()) 446 447 // Track the withdrawal. 448 w.accountSyncMu.Lock() 449 defer w.accountSyncMu.Unlock() 450 w.staticAccount.managedTrackWithdrawal(fundAmt) 451 452 // Defer a commit. 453 defer func() { 454 w.staticAccount.managedCommitWithdrawal(categorySubscription, fundAmt, types.ZeroCurrency, err) 455 }() 456 457 // Fund the subscription. 458 err = w.managedFundSubscription(stream, pt, fundAmt) 459 if err != nil { 460 return errors.AddContext(err, "failed to fund subscription") 461 } 462 463 // Success. Add the funds to the budget and signal to the account 464 // that the withdrawal was successful. 465 budget.Deposit(fundAmt) 466 return nil 467 } 468 469 // managedSubscriptionCleanup cleans up a subscription by signalling the host 470 // that we would like to stop the subscription and resetting the subscription 471 // related fields in the subscription info. 472 func (w *worker) managedSubscriptionCleanup(stream siamux.Stream, subscriber string) (err error) { 473 subInfo := w.staticSubscriptionInfo 474 475 // Close the stream gracefully. 476 err = modules.RPCStopSubscription(stream) 477 478 // After signalling to shut down the subscription, we wait for a short 479 // grace period to allow for incoming streams which were already read by 480 // the siamux but did not have the handler called upon them yet. This 481 // makes sure that our bandwidth expectations don't drift apart from the 482 // host's. We want to always wait for this even upon shutdown to make 483 // sure we refund our account correctly. 484 time.Sleep(stopSubscriptionGracePeriod) 485 486 // Close the handler. 487 err = errors.Compose(err, w.staticRenter.staticMux.CloseListener(subscriber)) 488 489 // Clear the active subscriptions at the end of this method. 490 subInfo.managedClearSubscriptions() 491 return err 492 } 493 494 // managedUnsubscribeFromRVs unsubscribes the worker from multiple ongoing 495 // subscriptions. 496 func (w *worker) managedUnsubscribeFromRVs(stream siamux.Stream, toUnsubscribe []skymodules.SubscriptionRequest) error { 497 subInfo := w.staticSubscriptionInfo 498 // Unsubscribe. 499 err := rpcUnsubscribeFromRVs(stream, toUnsubscribe, w.staticCache().staticHostVersion) 500 if err != nil { 501 return errors.AddContext(err, "failed to unsubscribe from registry values") 502 } 503 // Reset the subscription's channel to signal that it's no longer 504 // active. 505 subInfo.mu.Lock() 506 defer subInfo.mu.Unlock() 507 for _, req := range toUnsubscribe { 508 sub, exists := subInfo.subscriptions[req.EntryID] 509 if !exists { 510 err = errors.New("managedSubscriptionLoop: missing subscription - subscriptions should only be deleted in this thread so this shouldn't be the case") 511 build.Critical(err) 512 return err 513 } 514 sub.subscribed = make(chan struct{}) 515 } 516 return nil 517 } 518 519 // managedCheckHostCheating is a helper method that checks a registry entry 520 // against the cache to determine whether the host is cheating or not. It also 521 // updates the cache accordingly. 522 func (w *worker) managedCheckHostCheating(rid modules.RegistryEntryID, srv *modules.SignedRegistryValue, overwrite bool) error { 523 // Check if we have an entry in the cache already. 524 ce, exists := w.staticRegistryCache.Get(rid) 525 if !exists { 526 // If not we update the cache and are done. If srv is nil, 527 // that's also fine. 528 if srv != nil { 529 w.staticRegistryCache.Set(rid, *srv, false) 530 } 531 return nil 532 } 533 534 // The host has returned a revision in the past. If it returns 'nil' now 535 // that's a lie. 536 if srv == nil { 537 return errHostCheating 538 } 539 540 // If it is cached, check if the host's entry is better than our own. 541 better, err := ce.ShouldUpdateWith(&srv.RegistryValue, w.staticHostPubKey) 542 543 // If it is better, the host isn't cheating. So we update the cache. 544 if better && err == nil { 545 w.staticRegistryCache.Set(rid, *srv, false) 546 return nil 547 } 548 549 // If it is not better, we expect it to be at least equal to our own. 550 sameRevNum := errors.Contains(err, modules.ErrSameRevNum) 551 if sameRevNum && ce.IsPrimaryEntry(w.staticHostPubKey) == srv.IsPrimaryEntry(w.staticHostPubKey) { 552 return nil 553 } 554 555 // The revision the host provided is worse than the one we already know 556 // it had. The host is cheating us. We force update the cache to reset 557 // our knowledge of what we think is the host's most recent revision if 558 // overwrite is specified. 559 w.staticRegistryCache.Set(rid, *srv, overwrite) 560 return errors.Compose(errHostCheating, err) 561 } 562 563 // managedSubscribeToRVs subscribes the workers to multiple registry values. 564 func (w *worker) managedSubscribeToRVs(stream siamux.Stream, toSubscribe []skymodules.SubscriptionRequest, subChans []chan struct{}, budget *modules.RPCBudget, pt *modules.RPCPriceTable) error { 565 subInfo := w.staticSubscriptionInfo 566 // Subscribe. 567 rvs, err := rpcSubscribeToRVs(stream, toSubscribe, w.staticCache().staticHostVersion) 568 if err != nil { 569 return errors.AddContext(err, "failed to subscribe to registry values") 570 } 571 // Check that the initial values are not outdated and update the cache. 572 for _, rv := range rvs { 573 rid := modules.DeriveRegistryEntryID(rv.PubKey, rv.Entry.Tweak) 574 errCheating := w.managedCheckHostCheating(rid, &rv.Entry, false) 575 if errCheating != nil { 576 return errors.AddContext(errCheating, "managedSubscribeToRVs: host is cheating") 577 } 578 } 579 // Withdraw from budget. 580 if !budget.Withdraw(modules.MDMSubscribeCost(pt, uint64(len(rvs)), uint64(len(toSubscribe)))) { 581 return errors.New("failed to withdraw subscription payment from budget") 582 } 583 // Update the subscriptions with the received values. 584 subInfo.mu.Lock() 585 for _, rv := range rvs { 586 subInfo.subscriptions[modules.DeriveRegistryEntryID(rv.PubKey, rv.Entry.Tweak)].latestRV = &rv.Entry 587 } 588 // Close the channels to signal that the subscription is done. 589 for _, c := range subChans { 590 close(c) 591 } 592 subInfo.mu.Unlock() 593 // Tell the subscription manager. 594 subInfo.staticManager.Notify(w.staticHostPubKey, toSubscribe, rvs...) 595 return nil 596 } 597 598 // managedSubscriptionLoop handles an existing subscription session. It will add 599 // subscriptions, remove subscriptions, fund the subscription and extend it 600 // indefinitely. 601 func (w *worker) managedSubscriptionLoop(stream siamux.Stream, pt *modules.RPCPriceTable, deadline time.Time, budget *modules.RPCBudget, expectedBudget types.Currency, subscriber string) (err error) { 602 if w.staticRenter.staticDeps.Disrupt("InterruptSubscriptionLoop") { 603 return errors.New("interrupted") 604 } 605 606 // Set the bandwidth limiter on the stream. 607 limit := modules.NewBudgetLimit(budget, pt.DownloadBandwidthCost, pt.UploadBandwidthCost) 608 err = stream.SetLimit(limit) 609 if err != nil { 610 return errors.AddContext(err, "failed to set bandwidth limiter on the stream") 611 } 612 613 // Register the handler. This can happen after beginning the subscription 614 // since we are not expecting any notifications yet. 615 nh := ¬ificationHandler{ 616 staticStream: stream, 617 staticWorker: w, 618 staticPTUpdateChan: make(chan struct{}), 619 staticPTUpdatedChan: make(chan struct{}), 620 notificationCost: pt.SubscriptionNotificationCost, 621 } 622 err = w.staticRenter.staticMux.NewListenerSerial(subscriber, func(stream siamux.Stream) { 623 nh.managedHandleNotification(stream, budget, limit) 624 }) 625 if err != nil { 626 return errors.AddContext(err, "failed to register listener") 627 } 628 629 // Register some cleanup. 630 defer func() { 631 err = errors.Compose(err, w.managedSubscriptionCleanup(stream, subscriber)) 632 }() 633 634 // Set the stream deadline to the subscription deadline. 635 err = stream.SetDeadline(deadline) 636 if err != nil { 637 return errors.AddContext(err, "failed to set stream deadlien to subscription deadline") 638 } 639 640 for { 641 // If the budget is half empty, fund it. 642 if budget.Remaining().Cmp(expectedBudget.Div64(2)) < 0 { 643 err = w.managedRefillSubscription(stream, pt, expectedBudget, budget) 644 if err != nil { 645 return err 646 } 647 } 648 649 // If the subscription period is halfway over, extend it. 650 if time.Until(deadline) < modules.SubscriptionPeriod/2 { 651 pt, deadline, err = w.managedExtendSubscriptionPeriod(stream, budget, limit, deadline, pt, nh) 652 if err != nil { 653 return err 654 } 655 } 656 657 // Create a diff between the active subscriptions and the desired 658 // ones. 659 subInfo := w.staticSubscriptionInfo 660 toSubscribe, toUnsubscribe, subChans := subInfo.managedSubscriptionDiff() 661 662 // Unsubscribe from unnecessary subscriptions. 663 if len(toUnsubscribe) > 0 { 664 err = w.managedUnsubscribeFromRVs(stream, toUnsubscribe) 665 if err != nil { 666 return err 667 } 668 } 669 670 // Subscribe to any missing values. 671 if len(toSubscribe) > 0 { 672 err = w.managedSubscribeToRVs(stream, toSubscribe, subChans, budget, pt) 673 if err != nil { 674 return err 675 } 676 } 677 678 // Wait until some time passed or until there is new work. 679 ctx, cancel := context.WithTimeout(context.Background(), subscriptionLoopInterval) 680 select { 681 case <-w.staticTG.StopChan(): 682 cancel() 683 return threadgroup.ErrStopped // shutdown 684 case <-ctx.Done(): 685 // continue right away since the timer is drained. 686 cancel() 687 continue 688 case <-subInfo.staticWakeChan: 689 cancel() 690 } 691 } 692 } 693 694 // managedPriceTableForSubscription will fetch a price table that is valid for 695 // the provided duration. If the current price table of the worker isn't valid 696 // for that long, it will change its update time to trigger an update. 697 func (w *worker) managedPriceTableForSubscription(duration time.Duration) *modules.RPCPriceTable { 698 for { 699 // Check for shutdown. 700 select { 701 case _ = <-w.staticTG.StopChan(): 702 w.staticRenter.staticLog.Debugln("managedPriceTableForSubscription: abort due to shutdown") 703 return nil // shutdown 704 default: 705 } 706 707 // Get most recent price table. 708 pt := w.staticPriceTable() 709 710 // Check for gouging. 711 allowance := w.staticRenter.staticHostContractor.Allowance() 712 if err := gouging.CheckSubscription(allowance, pt.staticPriceTable); err != nil { 713 w.staticRenter.staticLog.Debugf("WARN: worker %v failed subscription gouging: %v", w.staticHostPubKeyStr, err) 714 sleepTime := time.Until(pt.staticUpdateTime) 715 if sleepTime < priceTableRetryInterval { 716 sleepTime = priceTableRetryInterval 717 } 718 // Wait a bit before checking again. 719 select { 720 case <-w.staticRenter.tg.StopChan(): 721 return nil // shutdown 722 case <-time.After(sleepTime): 723 continue // check next price table 724 } 725 } 726 727 // If the price table is valid, return it. 728 if pt.staticValidFor(duration) { 729 return &pt.staticPriceTable 730 } 731 732 // NOTE: The price table is not valid for the subsription. This 733 // theoretically should not happen a lot. 734 // The reason why it shouldn't happen often is that a price table is 735 // valid for rpcPriceGuaranteePeriod. That period is 10 minutes in 736 // production and gets renewed every 5 minutes. So we should always have 737 // a price table that is at least valid for another 5 minutes. The 738 // SubscriptionPeriod also happens to be 5 minutes but we renew 2.5 739 // minutes before it ends. 740 w.staticRenter.staticLog.Debugf("managedPriceTableForSubscription: pt not ready yet for worker %v", w.staticHostPubKeyStr) 741 742 // Trigger an update by setting the update time to now and calling 743 // 'staticWake'. 744 newPT := *pt 745 newPT.staticUpdateTime = time.Time{} 746 oldPT := (*workerPriceTable)(atomic.SwapPointer(&w.atomicPriceTable, unsafe.Pointer(&newPT))) 747 w.staticWake() 748 749 // The old table's UID should be the same. Otherwise we just swapped out 750 // a new table and need to try again. This condition can be false when 751 // pricetable got updated between now and when we fetched it at the 752 // beginning of this iteration. 753 if oldPT.staticPriceTable.UID != pt.staticPriceTable.UID { 754 w.staticSetPriceTable(oldPT) // set back to the old one 755 continue 756 } 757 758 // Wait a bit before checking again. 759 select { 760 case _ = <-w.staticTG.StopChan(): 761 w.staticRenter.staticLog.Debugln("managedPriceTableForSubscription: abort due to shutdown") 762 return nil // shutdown 763 case <-time.After(priceTableRetryInterval): 764 } 765 } 766 } 767 768 // managedBeginSubscription begins a subscription on a new stream and returns 769 // it. 770 func (w *worker) managedBeginSubscription(initialBudget types.Currency, fundAcc modules.AccountID, subscriber types.Specifier) (_ siamux.Stream, err error) { 771 stream, err := w.staticNewStream() 772 if err != nil { 773 return nil, errors.AddContext(err, "managedBeginSubscription: failed to create stream") 774 } 775 defer func() { 776 if err != nil { 777 err = errors.Compose(err, stream.Close()) 778 } 779 }() 780 bh, _ := w.managedSyncInfo() 781 return stream, modules.RPCBeginSubscription(stream, w.staticHostPubKey, &w.staticPriceTable().staticPriceTable, w.staticAccount.staticID, w.staticAccount.staticSecretKey, initialBudget, bh, subscriber) 782 } 783 784 // managedFundSubscription pays the host to increase the subscription budget. 785 func (w *worker) managedFundSubscription(stream siamux.Stream, pt *modules.RPCPriceTable, fundAmt types.Currency) error { 786 return modules.RPCFundSubscription(stream, w.staticHostPubKey, w.staticAccount.staticID, w.staticAccount.staticSecretKey, pt.HostBlockHeight, fundAmt) 787 } 788 789 // threadedSubscriptionLoop is the main subscription loop. It opens a 790 // subscription with the host and then calls managedSubscriptionLoop to keep the 791 // subscription alive. If the subscription dies, threadedSubscriptionLoop will 792 // start it again. 793 func (w *worker) threadedSubscriptionLoop() { 794 if err := w.staticTG.Add(); err != nil { 795 return 796 } 797 defer w.staticTG.Done() 798 799 // No need to run loop if the host doesn't support it. 800 if build.VersionCmp(w.staticCache().staticHostVersion, minSubscriptionVersion) < 0 { 801 return 802 } 803 804 // Disable loop if necessary. 805 if w.staticRenter.staticDeps.Disrupt("DisableSubscriptionLoop") { 806 return 807 } 808 809 // Convenience var. 810 subInfo := w.staticSubscriptionInfo 811 812 for { 813 // Clear potential subscriptions before establishing a new loop. 814 subInfo.managedClearSubscriptions() 815 816 // Check for shutdown 817 select { 818 case <-w.staticTG.StopChan(): 819 return // shutdown 820 default: 821 } 822 823 // Nothing to do if there are no subscriptions. 824 subInfo.mu.Lock() 825 nSubs := len(subInfo.subscriptions) 826 subInfo.mu.Unlock() 827 if nSubs == 0 { 828 select { 829 case <-subInfo.staticWakeChan: 830 // Wait for work 831 case <-w.staticTG.StopChan(): 832 return // shutdown 833 } 834 // Check if we got work after waking up. 835 subInfo.mu.Lock() 836 nSubs = len(subInfo.subscriptions) 837 subInfo.mu.Unlock() 838 if nSubs == 0 { 839 continue 840 } 841 } 842 843 // If the worker is on a cooldown, block until it is over before trying 844 // to establish a new subscriptoin session. 845 if w.managedOnMaintenanceCooldown() { 846 cooldownTime := w.callStatus().MaintenanceCoolDownTime 847 w.staticTG.Sleep(cooldownTime) 848 continue // try again 849 } 850 if cooldownTime, onCooldown := subInfo.managedOnCooldown(); onCooldown { 851 w.staticTG.Sleep(cooldownTime) 852 continue // try again 853 } 854 855 // Get a valid price table. 856 pt := w.managedPriceTableForSubscription(modules.SubscriptionPeriod) 857 if pt == nil { 858 return // shutdown 859 } 860 861 // Compute the initial deadline. 862 deadline := time.Now().Add(modules.SubscriptionPeriod) 863 864 // Set the initial budget. 865 initialBudget := initialSubscriptionBudget 866 budget := modules.NewBudget(initialBudget) 867 868 // Track the withdrawal. 869 w.accountSyncMu.Lock() 870 w.staticAccount.managedTrackWithdrawal(initialBudget) 871 872 // Prepare a unique handler for the host to subscribe to. 873 var subscriber types.Specifier 874 fastrand.Read(subscriber[:]) 875 subscriberStr := hex.EncodeToString(subscriber[:]) 876 877 // Begin the subscription session. 878 stream, err := w.managedBeginSubscription(initialBudget, w.staticAccount.staticID, subscriber) 879 if err != nil { 880 // Mark withdrawal as failed. 881 w.staticAccount.managedCommitWithdrawal(categorySubscription, initialBudget, types.ZeroCurrency, err) 882 w.accountSyncMu.Unlock() 883 884 // Log error and increment cooldown. 885 w.staticRenter.staticLog.Printf("Worker %v: failed to begin subscription: %v", w.staticHostPubKeyStr, err) 886 subInfo.managedIncrementCooldown() 887 continue 888 } 889 890 // Mark withdrawal as successful. 891 w.staticAccount.managedCommitWithdrawal(categorySubscription, initialBudget, types.ZeroCurrency, nil) 892 w.accountSyncMu.Unlock() 893 894 // Run the subscription. The error is checked after closing the handler 895 // and the refund. 896 errSubscription := w.managedSubscriptionLoop(stream, pt, deadline, budget, initialBudget, subscriberStr) 897 898 // Commit the refund. 899 w.staticAccount.managedCommitRefund(categorySubscription, budget.Remaining()) 900 901 // Handle the error. 902 w.staticHandleError(errSubscription) 903 904 // Check the error. 905 if errors.Contains(errSubscription, threadgroup.ErrStopped) { 906 return // shutdown 907 } 908 if errSubscription != nil { 909 w.staticRenter.staticLog.Printf("Worker %v: subscription got interrupted: %v", w.staticHostPubKeyStr, errSubscription) 910 subInfo.managedIncrementCooldown() 911 continue 912 } 913 } 914 } 915 916 // UpdateSubscriptions updates the entries the worker is subscribed to. 917 func (w *worker) UpdateSubscriptions(requests ...skymodules.SubscriptionRequest) { 918 subInfo := w.staticSubscriptionInfo 919 subInfo.mu.Lock() 920 defer subInfo.mu.Unlock() 921 922 // Create a map of the values we should subscribe to. 923 requestMap := make(map[modules.RegistryEntryID]*skymodules.SubscriptionRequest) 924 for i := range requests { 925 req := requests[i] 926 requestMap[req.EntryID] = &req 927 } 928 929 // Check the subscriptions we already have. 930 for sid, sub := range subInfo.subscriptions { 931 // Change the subscribe field of existing subscriptions depending on 932 // whether we want it or not. 933 _, wanted := requestMap[sid] 934 sub.subscribe = wanted 935 936 // Remove the sid from the requestMap since we handled it. 937 delete(requestMap, sid) 938 } 939 940 // For the remaining requests we create new entries. 941 for sid, req := range requestMap { 942 sub := newSubscription(req) 943 subInfo.subscriptions[sid] = sub 944 } 945 946 // Notify the worker. 947 select { 948 case subInfo.staticWakeChan <- struct{}{}: 949 default: 950 } 951 } 952 953 // rpcUnsubscribeToRVs executes the unsubscribe-protocol on the given stream. 954 func rpcUnsubscribeFromRVs(stream siamux.Stream, toUnsubscribe []skymodules.SubscriptionRequest, hostVersion string) error { 955 if build.VersionCmp(hostVersion, minSubscribeByRIDVersion) < 0 { 956 requests := make([]modules.RPCRegistrySubscriptionRequest, 0, len(toUnsubscribe)) 957 for _, ts := range toUnsubscribe { 958 if !ts.HasKeyAndTweak() { 959 continue // can't subscribe by entry id on this host 960 } 961 requests = append(requests, modules.RPCRegistrySubscriptionRequest{ 962 PubKey: *ts.PubKey, 963 Tweak: *ts.Tweak, 964 }) 965 } 966 return modules.RPCUnsubscribeFromRVs(stream, requests) 967 } 968 requests := make([]modules.RPCRegistrySubscriptionByRIDRequest, 0, len(toUnsubscribe)) 969 for _, ts := range toUnsubscribe { 970 requests = append(requests, modules.RPCRegistrySubscriptionByRIDRequest{ 971 EntryID: ts.EntryID, 972 }) 973 } 974 return modules.RPCUnsubscribeFromRVsByRID(stream, requests) 975 } 976 977 // rpcSubscribeToRVs executes the subscribe-protocol on the given stream. 978 func rpcSubscribeToRVs(stream siamux.Stream, toSubscribe []skymodules.SubscriptionRequest, hostVersion string) ([]modules.RPCRegistrySubscriptionNotificationEntryUpdate, error) { 979 if build.VersionCmp(hostVersion, minSubscribeByRIDVersion) < 0 { 980 requests := make([]modules.RPCRegistrySubscriptionRequest, 0, len(toSubscribe)) 981 for _, ts := range toSubscribe { 982 if !ts.HasKeyAndTweak() { 983 continue // can't subscribe by entry id on this host 984 } 985 requests = append(requests, modules.RPCRegistrySubscriptionRequest{ 986 PubKey: *ts.PubKey, 987 Tweak: *ts.Tweak, 988 }) 989 } 990 return modules.RPCSubscribeToRVs(stream, requests) 991 } 992 requests := make([]modules.RPCRegistrySubscriptionByRIDRequest, 0, len(toSubscribe)) 993 for _, ts := range toSubscribe { 994 requests = append(requests, modules.RPCRegistrySubscriptionByRIDRequest{ 995 EntryID: ts.EntryID, 996 }) 997 } 998 return modules.RPCSubscribeToRVsByRID(stream, requests) 999 }