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 }