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  }