github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/scoring/internal/subscriptionCache.go (about)

     1  package internal
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/libp2p/go-libp2p/core/peer"
     7  	"github.com/rs/zerolog"
     8  	"go.uber.org/atomic"
     9  
    10  	"github.com/onflow/flow-go/model/flow"
    11  	"github.com/onflow/flow-go/module"
    12  	herocache "github.com/onflow/flow-go/module/mempool/herocache/backdata"
    13  	"github.com/onflow/flow-go/module/mempool/herocache/backdata/heropool"
    14  	"github.com/onflow/flow-go/module/mempool/stdmap"
    15  )
    16  
    17  // SubscriptionRecordCache manages the subscription records of peers in a network.
    18  // It uses a currentCycle counter to track the update cycles of the cache, ensuring the relevance of subscription data.
    19  type SubscriptionRecordCache struct {
    20  	c *stdmap.Backend
    21  
    22  	// currentCycle is an atomic counter used to track the update cycles of the subscription cache.
    23  	// It plays a critical role in maintaining the cache's data relevance and coherence.
    24  	// Each increment of currentCycle represents a new update cycle, signifying the cache's transition to a new state
    25  	// where only the most recent and relevant subscriptions are maintained. This design choice ensures that the cache
    26  	// does not retain stale or outdated subscription information, thereby reflecting the dynamic nature of peer
    27  	// subscriptions in the network. It is incremented every time the subscription cache is updated, either with new
    28  	// topic subscriptions or other update operations.
    29  	// The currentCycle is incremented atomically and externally by calling the MoveToNextUpdateCycle() function.
    30  	// This is called by the module that uses the subscription provider cache signaling that whatever updates it has
    31  	// made to the cache so far can be considered out-of-date, and the new updates to the cache records should
    32  	// overwrite the old ones.
    33  	currentCycle atomic.Uint64
    34  }
    35  
    36  // NewSubscriptionRecordCache creates a new subscription cache with the given size limit.
    37  // Args:
    38  // - sizeLimit: the size limit of the cache.
    39  // - logger: the logger to use for logging.
    40  // - collector: the metrics collector to use for collecting metrics.
    41  func NewSubscriptionRecordCache(sizeLimit uint32,
    42  	logger zerolog.Logger,
    43  	collector module.HeroCacheMetrics) *SubscriptionRecordCache {
    44  	backData := herocache.NewCache(sizeLimit,
    45  		herocache.DefaultOversizeFactor,
    46  		heropool.LRUEjection,
    47  		logger.With().Str("mempool", "subscription-records").Logger(),
    48  		collector)
    49  
    50  	return &SubscriptionRecordCache{
    51  		c:            stdmap.NewBackend(stdmap.WithBackData(backData)),
    52  		currentCycle: *atomic.NewUint64(0),
    53  	}
    54  }
    55  
    56  // GetSubscribedTopics returns the list of topics a peer is subscribed to.
    57  // Returns:
    58  // - []string: the list of topics the peer is subscribed to.
    59  // - bool: true if there is a record for the peer, false otherwise.
    60  func (s *SubscriptionRecordCache) GetSubscribedTopics(pid peer.ID) ([]string, bool) {
    61  	e, ok := s.c.ByID(entityIdOf(pid))
    62  	if !ok {
    63  		return nil, false
    64  	}
    65  	return e.(SubscriptionRecordEntity).Topics, true
    66  }
    67  
    68  // MoveToNextUpdateCycle moves the subscription cache to the next update cycle.
    69  // A new update cycle is started when the subscription cache is first created, and then every time the subscription cache
    70  // is updated. The update cycle is used to keep track of the last time the subscription cache was updated. It is used to
    71  // implement a notion of time in the subscription cache.
    72  // When the update cycle is moved forward, it means that all the updates made to the subscription cache so far are
    73  // considered out-of-date, and the new updates to the cache records should overwrite the old ones.
    74  // The expected behavior is that the update cycle is moved forward by the module that uses the subscription provider once
    75  // per each update on the "entire" cache (and not per each update on a single record).
    76  // In other words, assume a cache with 3 records: A, B, and C. If the module updates record A, then record B, and then
    77  // record C, the module should move the update cycle forward only once after updating record C, and then update record A
    78  // B, and C again. If the module moves the update cycle forward after updating record A, then again after updating
    79  // record B, and then again after updating record C, the cache will be in an inconsistent state.
    80  // Returns:
    81  // - uint64: the current update cycle.
    82  func (s *SubscriptionRecordCache) MoveToNextUpdateCycle() uint64 {
    83  	s.currentCycle.Inc()
    84  	return s.currentCycle.Load()
    85  }
    86  
    87  // AddWithInitTopicForPeer appends a topic to the list of topics a peer is subscribed to. If the peer is not subscribed to any
    88  // topics yet, a new record is created.
    89  // If the last update cycle is older than the current cycle, the list of topics for the peer is first cleared, and then
    90  // the topic is added to the list. This is to ensure that the list of topics for a peer is always up to date.
    91  // Args:
    92  // - pid: the peer id of the peer.
    93  // - topic: the topic to add.
    94  // Returns:
    95  // - []string: the list of topics the peer is subscribed to after the update.
    96  // - error: an error if the update failed; any returned error is an irrecoverable error and indicates a bug or misconfiguration.
    97  // Implementation must be thread-safe.
    98  func (s *SubscriptionRecordCache) AddWithInitTopicForPeer(pid peer.ID, topic string) ([]string, error) {
    99  	entityId := entityIdOf(pid)
   100  	initLogic := func() flow.Entity {
   101  		return SubscriptionRecordEntity{
   102  			entityId:         entityId,
   103  			PeerID:           pid,
   104  			Topics:           make([]string, 0),
   105  			LastUpdatedCycle: s.currentCycle.Load(),
   106  		}
   107  	}
   108  	var rErr error
   109  	adjustLogic := func(entity flow.Entity) flow.Entity {
   110  		record, ok := entity.(SubscriptionRecordEntity)
   111  		if !ok {
   112  			// sanity check
   113  			// This should never happen, because the cache only contains SubscriptionRecordEntity entities.
   114  			panic(fmt.Sprintf("invalid entity type, expected SubscriptionRecordEntity type, got: %T", entity))
   115  		}
   116  
   117  		currentCycle := s.currentCycle.Load()
   118  		if record.LastUpdatedCycle > currentCycle {
   119  			// sanity check
   120  			// This should never happen, because the update cycle must be moved forward before adding a topic.
   121  			panic(fmt.Sprintf("invalid last updated cycle, expected <= %d, got: %d", currentCycle, record.LastUpdatedCycle))
   122  		}
   123  		if record.LastUpdatedCycle < currentCycle {
   124  			// This record was not updated in the current cycle, so we can wipe its topics list (topic list is only
   125  			// valid for the current cycle).
   126  			record.Topics = make([]string, 0)
   127  		}
   128  		// check if the topic already exists; if it does, we do not need to update the record.
   129  		for _, t := range record.Topics {
   130  			if t == topic {
   131  				// topic already exists
   132  				return record
   133  			}
   134  		}
   135  		record.LastUpdatedCycle = currentCycle
   136  		record.Topics = append(record.Topics, topic)
   137  
   138  		// Return the adjusted record.
   139  		return record
   140  	}
   141  	adjustedEntity, adjusted := s.c.AdjustWithInit(entityId, adjustLogic, initLogic)
   142  	if rErr != nil {
   143  		return nil, fmt.Errorf("failed to adjust record with error: %w", rErr)
   144  	}
   145  	if !adjusted {
   146  		return nil, fmt.Errorf("failed to adjust record, entity not found")
   147  	}
   148  
   149  	return adjustedEntity.(SubscriptionRecordEntity).Topics, nil
   150  }
   151  
   152  // entityIdOf converts a peer ID to a flow ID by taking the hash of the peer ID.
   153  // This is used to convert the peer ID in a notion that is compatible with HeroCache.
   154  // This is not a protocol-level conversion, and is only used internally by the cache, MUST NOT be exposed outside the cache.
   155  // Args:
   156  // - peerId: the peer ID of the peer in the GossipSub protocol.
   157  // Returns:
   158  // - flow.Identifier: the flow ID of the peer.
   159  func entityIdOf(pid peer.ID) flow.Identifier {
   160  	return flow.MakeID(pid)
   161  }