github.com/ethersphere/bee/v2@v2.2.0/pkg/puller/puller.go (about) 1 // Copyright 2020 The Swarm Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package puller provides protocol-orchestrating functionality 6 // over the pullsync protocol. It pulls chunks from other nodes 7 // and reacts to changes in network configuration. 8 package puller 9 10 import ( 11 "context" 12 "errors" 13 "fmt" 14 "maps" 15 "math" 16 "sync" 17 "time" 18 19 "github.com/ethersphere/bee/v2/pkg/log" 20 "github.com/ethersphere/bee/v2/pkg/p2p" 21 "github.com/ethersphere/bee/v2/pkg/puller/intervalstore" 22 "github.com/ethersphere/bee/v2/pkg/pullsync" 23 "github.com/ethersphere/bee/v2/pkg/rate" 24 "github.com/ethersphere/bee/v2/pkg/storage" 25 "github.com/ethersphere/bee/v2/pkg/storer" 26 "github.com/ethersphere/bee/v2/pkg/swarm" 27 "github.com/ethersphere/bee/v2/pkg/topology" 28 ratelimit "golang.org/x/time/rate" 29 ) 30 31 // loggerName is the tree path name of the logger for this package. 32 const loggerName = "puller" 33 34 var errCursorsLength = errors.New("cursors length mismatch") 35 36 const ( 37 DefaultHistRateWindow = time.Minute * 15 38 39 IntervalPrefix = "sync_interval" 40 recalcPeersDur = time.Minute * 5 41 42 maxChunksPerSecond = 1000 // roughly 4 MB/s 43 44 maxPODelta = 2 // the lowest level of proximity order (of peers) subtracted from the storage radius allowed for chunk syncing. 45 ) 46 47 type Options struct { 48 Bins uint8 49 } 50 51 type Puller struct { 52 base swarm.Address 53 54 topology topology.Driver 55 radius storer.RadiusChecker 56 statestore storage.StateStorer 57 syncer pullsync.Interface 58 blockLister p2p.Blocklister 59 60 metrics metrics 61 logger log.Logger 62 63 syncPeers map[string]*syncPeer // index is bin, map key is peer address 64 syncPeersMtx sync.Mutex 65 intervalMtx sync.Mutex 66 67 cancel func() 68 69 wg sync.WaitGroup 70 71 bins uint8 // how many bins do we support 72 73 rate *rate.Rate // rate of historical syncing 74 75 start sync.Once 76 77 limiter *ratelimit.Limiter 78 } 79 80 func New( 81 addr swarm.Address, 82 stateStore storage.StateStorer, 83 topology topology.Driver, 84 reserveState storer.RadiusChecker, 85 pullSync pullsync.Interface, 86 blockLister p2p.Blocklister, 87 logger log.Logger, 88 o Options, 89 ) *Puller { 90 bins := swarm.MaxBins 91 if o.Bins != 0 { 92 bins = o.Bins 93 } 94 p := &Puller{ 95 base: addr, 96 statestore: stateStore, 97 topology: topology, 98 radius: reserveState, 99 syncer: pullSync, 100 metrics: newMetrics(), 101 logger: logger.WithName(loggerName).Register(), 102 syncPeers: make(map[string]*syncPeer), 103 bins: bins, 104 blockLister: blockLister, 105 rate: rate.New(DefaultHistRateWindow), 106 cancel: func() { /* Noop, since the context is initialized in the Start(). */ }, 107 limiter: ratelimit.NewLimiter(ratelimit.Every(time.Second/maxChunksPerSecond), maxChunksPerSecond), 108 } 109 110 return p 111 } 112 113 func (p *Puller) Start(ctx context.Context) { 114 p.start.Do(func() { 115 cctx, cancel := context.WithCancel(ctx) 116 p.cancel = cancel 117 118 p.wg.Add(1) 119 go p.manage(cctx) 120 }) 121 } 122 123 func (p *Puller) SyncRate() float64 { 124 return p.rate.Rate() 125 } 126 127 func (p *Puller) manage(ctx context.Context) { 128 defer p.wg.Done() 129 130 c, unsubscribe := p.topology.SubscribeTopologyChange() 131 defer unsubscribe() 132 133 p.logger.Info("warmup period complete, starting worker") 134 135 var prevRadius uint8 136 137 onChange := func() { 138 p.syncPeersMtx.Lock() 139 defer p.syncPeersMtx.Unlock() 140 141 newRadius := p.radius.StorageRadius() 142 143 // reset all intervals below the new radius to resync: 144 // 1. previously evicted chunks 145 // 2. previously ignored chunks due to a higher radius 146 if newRadius < prevRadius { 147 for _, peer := range p.syncPeers { 148 p.disconnectPeer(peer.address) 149 } 150 if err := p.resetIntervals(prevRadius); err != nil { 151 p.logger.Debug("reset lower sync radius failed", "error", err) 152 } 153 p.logger.Debug("radius decrease", "old_radius", prevRadius, "new_radius", newRadius) 154 } 155 prevRadius = newRadius 156 157 // peersDisconnected is used to mark and prune peers that are no longer connected. 158 peersDisconnected := maps.Clone(p.syncPeers) 159 160 _ = p.topology.EachConnectedPeerRev(func(addr swarm.Address, po uint8) (stop, jumpToNext bool, err error) { 161 if _, ok := p.syncPeers[addr.ByteString()]; !ok { 162 p.syncPeers[addr.ByteString()] = newSyncPeer(addr, p.bins, po) 163 } 164 delete(peersDisconnected, addr.ByteString()) 165 return false, false, nil 166 }, topology.Select{}) 167 168 for _, peer := range peersDisconnected { 169 p.disconnectPeer(peer.address) 170 } 171 172 p.recalcPeers(ctx, newRadius) 173 } 174 175 tick := time.NewTicker(recalcPeersDur) 176 defer tick.Stop() 177 178 for { 179 180 onChange() 181 182 select { 183 case <-ctx.Done(): 184 return 185 case <-tick.C: 186 case <-c: 187 } 188 } 189 } 190 191 // disconnectPeer cancels all existing syncing and removes the peer entry from the syncing map. 192 // Must be called under lock. 193 func (p *Puller) disconnectPeer(addr swarm.Address) { 194 loggerV2 := p.logger.V(2).Register() 195 196 loggerV2.Debug("disconnecting peer", "peer_address", addr) 197 if peer, ok := p.syncPeers[addr.ByteString()]; ok { 198 peer.mtx.Lock() 199 peer.stop() 200 peer.mtx.Unlock() 201 } 202 delete(p.syncPeers, addr.ByteString()) 203 } 204 205 // recalcPeers starts or stops syncing process for peers per bin depending on the current sync radius. 206 // Must be called under lock. 207 func (p *Puller) recalcPeers(ctx context.Context, storageRadius uint8) { 208 var wg sync.WaitGroup 209 for _, peer := range p.syncPeers { 210 wg.Add(1) 211 p.wg.Add(1) 212 go func(peer *syncPeer) { 213 defer p.wg.Done() 214 defer wg.Done() 215 if err := p.syncPeer(ctx, peer, storageRadius); err != nil { 216 p.logger.Debug("sync peer failed", "peer_address", peer.address, "error", err) 217 } 218 }(peer) 219 } 220 wg.Wait() 221 } 222 223 func (p *Puller) syncPeer(ctx context.Context, peer *syncPeer, storageRadius uint8) error { 224 peer.mtx.Lock() 225 defer peer.mtx.Unlock() 226 227 if peer.cursors == nil { 228 cursors, epoch, err := p.syncer.GetCursors(ctx, peer.address) 229 if err != nil { 230 return fmt.Errorf("could not get cursors from peer %s: %w", peer.address, err) 231 } 232 peer.cursors = cursors 233 234 storedEpoch, err := p.getPeerEpoch(peer.address) 235 if err != nil { 236 return fmt.Errorf("retrieve epoch for peer %s: %w", peer.address, err) 237 } 238 239 if storedEpoch != epoch { 240 // cancel all bins 241 peer.stop() 242 243 p.logger.Debug("peer epoch change detected, resetting past synced intervals", "stored_epoch", storedEpoch, "new_epoch", epoch, "peer_address", peer.address) 244 245 err = p.resetPeerIntervals(peer.address) 246 if err != nil { 247 return fmt.Errorf("reset intervals for peer %s: %w", peer.address, err) 248 } 249 err = p.setPeerEpoch(peer.address, epoch) 250 if err != nil { 251 return fmt.Errorf("set epoch for peer %s: %w", peer.address, err) 252 } 253 } 254 } 255 256 if len(peer.cursors) != int(p.bins) { 257 return errCursorsLength 258 } 259 260 /* 261 The syncing behavior diverges for peers outside and within the storage radius. 262 For neighbor peers, we sync ALL bins greater than or equal to the storage radius. 263 For peers with PO lower than the storage radius, we must sync ONLY the bin that is the PO. 264 For peers peer with PO lower than the storage radius and even lower than the allowed minimum threshold, 265 no syncing is done. 266 */ 267 268 if peer.po >= storageRadius { 269 270 // cancel all bins lower than the storage radius 271 for bin := uint8(0); bin < storageRadius; bin++ { 272 peer.cancelBin(bin) 273 } 274 275 // sync all bins >= storage radius 276 for bin, cur := range peer.cursors { 277 if bin >= int(storageRadius) && !peer.isBinSyncing(uint8(bin)) { 278 p.syncPeerBin(ctx, peer, uint8(bin), cur) 279 } 280 } 281 282 } else if storageRadius-peer.po <= maxPODelta { 283 // cancel all non-po bins, if any 284 for bin := uint8(0); bin < p.bins; bin++ { 285 if bin != peer.po { 286 peer.cancelBin(bin) 287 } 288 } 289 // sync PO bin only 290 if !peer.isBinSyncing(peer.po) { 291 p.syncPeerBin(ctx, peer, peer.po, peer.cursors[peer.po]) 292 } 293 } else { 294 peer.stop() 295 } 296 297 return nil 298 } 299 300 // syncPeerBin will start historical and live syncing for the peer for a particular bin. 301 // Must be called under syncPeer lock. 302 func (p *Puller) syncPeerBin(parentCtx context.Context, peer *syncPeer, bin uint8, cursor uint64) { 303 loggerV2 := p.logger.V(2).Register() 304 305 ctx, cancel := context.WithCancel(parentCtx) 306 peer.setBinCancel(cancel, bin) 307 308 sync := func(isHistorical bool, address swarm.Address, start uint64) { 309 p.metrics.SyncWorkerCounter.Inc() 310 311 defer p.wg.Done() 312 defer peer.wg.Done() 313 defer p.metrics.SyncWorkerCounter.Dec() 314 315 var err error 316 317 for { 318 if isHistorical { // override start with the next interval if historical syncing 319 start, err = p.nextPeerInterval(address, bin) 320 if err != nil { 321 p.metrics.SyncWorkerErrCounter.Inc() 322 p.logger.Error(err, "syncWorker nextPeerInterval failed, quitting") 323 return 324 } 325 326 // historical sync has caught up to the cursor, exit 327 if start > cursor { 328 return 329 } 330 } 331 332 select { 333 case <-ctx.Done(): 334 loggerV2.Debug("syncWorker context cancelled", "peer_address", address, "bin", bin) 335 return 336 default: 337 } 338 339 p.metrics.SyncWorkerIterCounter.Inc() 340 341 syncStart := time.Now() 342 top, count, err := p.syncer.Sync(ctx, address, bin, start) 343 344 if top == math.MaxUint64 { 345 p.metrics.MaxUintErrCounter.Inc() 346 p.logger.Error(nil, "syncWorker max uint64 encountered, quitting", "peer_address", address, "bin", bin, "from", start, "topmost", top) 347 return 348 } 349 350 if err != nil { 351 p.metrics.SyncWorkerErrCounter.Inc() 352 if errors.Is(err, p2p.ErrPeerNotFound) { 353 p.logger.Debug("syncWorker interval failed, quitting", "error", err, "peer_address", address, "bin", bin, "cursor", cursor, "start", start, "topmost", top) 354 return 355 } 356 loggerV2.Debug("syncWorker interval failed", "error", err, "peer_address", address, "bin", bin, "cursor", cursor, "start", start, "topmost", top) 357 } 358 359 _ = p.limiter.WaitN(ctx, count) 360 361 if isHistorical { 362 p.metrics.SyncedCounter.WithLabelValues("historical").Add(float64(count)) 363 p.rate.Add(count) 364 } else { 365 p.metrics.SyncedCounter.WithLabelValues("live").Add(float64(count)) 366 } 367 368 // pulled at least one chunk 369 if top >= start { 370 if err := p.addPeerInterval(address, bin, start, top); err != nil { 371 p.metrics.SyncWorkerErrCounter.Inc() 372 p.logger.Error(err, "syncWorker could not persist interval for peer, quitting", "peer_address", address) 373 return 374 } 375 loggerV2.Debug("syncWorker pulled", "bin", bin, "start", start, "topmost", top, "isHistorical", isHistorical, "duration", time.Since(syncStart), "peer_address", address) 376 start = top + 1 377 } 378 } 379 } 380 381 if cursor > 0 { 382 peer.wg.Add(1) 383 p.wg.Add(1) 384 go sync(true, peer.address, cursor) 385 } 386 387 peer.wg.Add(1) 388 p.wg.Add(1) 389 go sync(false, peer.address, cursor+1) 390 } 391 392 func (p *Puller) Close() error { 393 p.logger.Info("shutting down") 394 p.cancel() 395 cc := make(chan struct{}) 396 go func() { 397 defer close(cc) 398 p.wg.Wait() 399 }() 400 select { 401 case <-cc: 402 case <-time.After(10 * time.Second): 403 p.logger.Warning("shut down timeout, some goroutines may still be running") 404 } 405 406 return nil 407 } 408 409 func (p *Puller) addPeerInterval(peer swarm.Address, bin uint8, start, end uint64) (err error) { 410 p.intervalMtx.Lock() 411 defer p.intervalMtx.Unlock() 412 413 peerStreamKey := peerIntervalKey(peer, bin) 414 i, err := p.getOrCreateInterval(peer, bin) 415 if err != nil { 416 return err 417 } 418 419 i.Add(start, end) 420 421 return p.statestore.Put(peerStreamKey, i) 422 } 423 424 func (p *Puller) getPeerEpoch(peer swarm.Address) (uint64, error) { 425 p.intervalMtx.Lock() 426 defer p.intervalMtx.Unlock() 427 428 var epoch uint64 429 err := p.statestore.Get(peerEpochKey(peer), &epoch) 430 if err != nil { 431 if errors.Is(err, storage.ErrNotFound) { 432 return 0, nil 433 } 434 return 0, err 435 } 436 437 return epoch, nil 438 } 439 440 func (p *Puller) setPeerEpoch(peer swarm.Address, epoch uint64) error { 441 p.intervalMtx.Lock() 442 defer p.intervalMtx.Unlock() 443 444 return p.statestore.Put(peerEpochKey(peer), epoch) 445 } 446 447 func (p *Puller) resetPeerIntervals(peer swarm.Address) (err error) { 448 p.intervalMtx.Lock() 449 defer p.intervalMtx.Unlock() 450 451 for bin := uint8(0); bin < p.bins; bin++ { 452 err = errors.Join(err, p.statestore.Delete(peerIntervalKey(peer, bin))) 453 } 454 455 return 456 } 457 458 func (p *Puller) resetIntervals(oldRadius uint8) (err error) { 459 p.intervalMtx.Lock() 460 defer p.intervalMtx.Unlock() 461 462 var deleteKeys []string 463 464 for bin := uint8(0); bin < p.bins; bin++ { 465 err = errors.Join(err, 466 p.statestore.Iterate(binIntervalKey(bin), func(key, _ []byte) (stop bool, err error) { 467 468 po := swarm.Proximity(addressFromKey(key).Bytes(), p.base.Bytes()) 469 470 // 1. for neighbor peers, only reset the bins below the current radius 471 // 2. for non-neighbor peers, we must reset the entire history 472 if po >= oldRadius { 473 if bin < oldRadius { 474 deleteKeys = append(deleteKeys, string(key)) 475 } 476 } else { 477 deleteKeys = append(deleteKeys, string(key)) 478 } 479 return false, nil 480 }), 481 ) 482 } 483 484 for _, k := range deleteKeys { 485 err = errors.Join(err, p.statestore.Delete(k)) 486 } 487 488 return err 489 } 490 491 func (p *Puller) nextPeerInterval(peer swarm.Address, bin uint8) (uint64, error) { 492 p.intervalMtx.Lock() 493 defer p.intervalMtx.Unlock() 494 495 i, err := p.getOrCreateInterval(peer, bin) 496 if err != nil { 497 return 0, err 498 } 499 500 start, _, _ := i.Next(0) 501 return start, nil 502 } 503 504 // Must be called underlock. 505 func (p *Puller) getOrCreateInterval(peer swarm.Address, bin uint8) (*intervalstore.Intervals, error) { 506 // check that an interval entry exists 507 key := peerIntervalKey(peer, bin) 508 itv := &intervalstore.Intervals{} 509 if err := p.statestore.Get(key, itv); err != nil { 510 if errors.Is(err, storage.ErrNotFound) { 511 // key interval values are ALWAYS > 0 512 itv = intervalstore.NewIntervals(1) 513 if err := p.statestore.Put(key, itv); err != nil { 514 return nil, err 515 } 516 } else { 517 return nil, err 518 } 519 } 520 return itv, nil 521 } 522 523 func peerEpochKey(peer swarm.Address) string { 524 return fmt.Sprintf("%s_epoch_%s", IntervalPrefix, peer.ByteString()) 525 } 526 527 func peerIntervalKey(peer swarm.Address, bin uint8) string { 528 return fmt.Sprintf("%s_%03d_%s", IntervalPrefix, bin, peer.ByteString()) 529 } 530 531 func binIntervalKey(bin uint8) string { 532 return fmt.Sprintf("%s_%03d", IntervalPrefix, bin) 533 } 534 535 func addressFromKey(key []byte) swarm.Address { 536 addr := key[len(fmt.Sprintf("%s_%03d_", IntervalPrefix, 0)):] 537 return swarm.NewAddress(addr) 538 } 539 540 type syncPeer struct { 541 address swarm.Address 542 binCancelFuncs map[uint8]func() // slice of context cancel funcs for historical sync. index is bin 543 po uint8 544 cursors []uint64 545 546 mtx sync.Mutex 547 wg sync.WaitGroup 548 } 549 550 func newSyncPeer(addr swarm.Address, bins, po uint8) *syncPeer { 551 return &syncPeer{ 552 address: addr, 553 binCancelFuncs: make(map[uint8]func(), bins), 554 po: po, 555 } 556 } 557 558 // called when peer disconnects or on shutdown, cleans up ongoing sync operations 559 func (p *syncPeer) stop() { 560 for bin, c := range p.binCancelFuncs { 561 c() 562 delete(p.binCancelFuncs, bin) 563 } 564 p.wg.Wait() 565 } 566 567 func (p *syncPeer) setBinCancel(cf func(), bin uint8) { 568 p.binCancelFuncs[bin] = cf 569 } 570 571 func (p *syncPeer) cancelBin(bin uint8) { 572 if c, ok := p.binCancelFuncs[bin]; ok { 573 c() 574 delete(p.binCancelFuncs, bin) 575 } 576 } 577 578 func (p *syncPeer) isBinSyncing(bin uint8) bool { 579 _, ok := p.binCancelFuncs[bin] 580 return ok 581 }