github.com/koko1123/flow-go-1@v0.29.6/network/p2p/scoring/subscription_provider.go (about)

     1  package scoring
     2  
     3  import (
     4  	"sync"
     5  
     6  	"github.com/libp2p/go-libp2p/core/peer"
     7  	"github.com/rs/zerolog"
     8  	"go.uber.org/atomic"
     9  
    10  	"github.com/koko1123/flow-go-1/network/p2p"
    11  )
    12  
    13  // SubscriptionProvider provides a list of topics a peer is subscribed to.
    14  type SubscriptionProvider struct {
    15  	logger zerolog.Logger
    16  	tp     p2p.TopicProvider
    17  
    18  	// allTopics is a list of all topics in the pubsub network
    19  	// TODO: we should add an expiry time to this cache and clean up the cache periodically
    20  	// to avoid leakage of stale topics.
    21  	peersByTopic         sync.Map // map[topic]peers
    22  	peersByTopicUpdating sync.Map // whether a goroutine is already updating the list of peers for a topic
    23  
    24  	// allTopics is a list of all topics in the pubsub network that this node is subscribed to.
    25  	allTopicsLock   sync.RWMutex // protects allTopics
    26  	allTopics       []string     // list of all topics in the pubsub network that this node has subscribed to.
    27  	allTopicsUpdate atomic.Bool  // whether a goroutine is already updating the list of topics.
    28  }
    29  
    30  func NewSubscriptionProvider(logger zerolog.Logger, tp p2p.TopicProvider) *SubscriptionProvider {
    31  	return &SubscriptionProvider{
    32  		logger:    logger.With().Str("module", "subscription_provider").Logger(),
    33  		tp:        tp,
    34  		allTopics: make([]string, 0),
    35  	}
    36  }
    37  
    38  // GetSubscribedTopics returns all the subscriptions of a peer within the pubsub network.
    39  // Note that the current node can only see peer subscriptions to topics that it has also subscribed to
    40  // e.g., if current node has subscribed to topics A and B, and peer1 has subscribed to topics A, B, and C,
    41  // then GetSubscribedTopics(peer1) will return A and B. Since this node has not subscribed to topic C,
    42  // it will not be able to query for other peers subscribed to topic C.
    43  func (s *SubscriptionProvider) GetSubscribedTopics(pid peer.ID) []string {
    44  	topics := s.getAllTopics()
    45  
    46  	// finds the topics that this peer is subscribed to.
    47  	subscriptions := make([]string, 0)
    48  	for _, topic := range topics {
    49  		peers := s.getPeersByTopic(topic)
    50  		for _, p := range peers {
    51  			if p == pid {
    52  				subscriptions = append(subscriptions, topic)
    53  			}
    54  		}
    55  	}
    56  
    57  	return subscriptions
    58  }
    59  
    60  // getAllTopics returns all the topics in the pubsub network that this node (peer) has subscribed to.
    61  // Note that this method always returns the cached version of the subscribed topics while querying the
    62  // pubsub network for the list of topics in a goroutine. Hence, the first call to this method always returns an empty
    63  // list.
    64  func (s *SubscriptionProvider) getAllTopics() []string {
    65  	go func() {
    66  		// TODO: refactor this to a component manager worker once we have a startable libp2p node.
    67  		if updateInProgress := s.allTopicsUpdate.CompareAndSwap(false, true); updateInProgress {
    68  			// another goroutine is already updating the list of topics
    69  			return
    70  		}
    71  
    72  		allTopics := s.tp.GetTopics()
    73  		s.atomicUpdateAllTopics(allTopics)
    74  
    75  		// remove the update flag
    76  		s.allTopicsUpdate.Store(false)
    77  
    78  		s.logger.Trace().Msgf("all topics updated: %v", allTopics)
    79  	}()
    80  
    81  	s.allTopicsLock.RLock()
    82  	defer s.allTopicsLock.RUnlock()
    83  	return s.allTopics
    84  }
    85  
    86  // getPeersByTopic returns all the peers subscribed to a topic.
    87  // Note that this method always returns the cached version of the subscribed peers while querying the
    88  // pubsub network for the list of topics in a goroutine. Hence, the first call to this method always returns an empty
    89  // list.
    90  // As this method is injected into GossipSub, it is vital that it never block the caller, otherwise it causes a
    91  // deadlock on the GossipSub.
    92  // Also note that, this peer itself should be subscribed to the topic, otherwise, it cannot find the list of peers
    93  // subscribed to the topic in the pubsub network due to an inherent limitation of GossipSub.
    94  func (s *SubscriptionProvider) getPeersByTopic(topic string) []peer.ID {
    95  	go func() {
    96  		// TODO: refactor this to a component manager worker once we have a startable libp2p node.
    97  		if _, updateInProgress := s.peersByTopicUpdating.LoadOrStore(topic, true); updateInProgress {
    98  			// another goroutine is already updating the list of peers for this topic
    99  			return
   100  		}
   101  
   102  		subscribedPeers := s.tp.ListPeers(topic)
   103  		s.peersByTopic.Store(topic, subscribedPeers)
   104  
   105  		// remove the update flag
   106  		s.peersByTopicUpdating.Delete(topic)
   107  
   108  		s.logger.Trace().Str("topic", topic).Msgf("peers by topic updated: %v", subscribedPeers)
   109  	}()
   110  
   111  	peerId, ok := s.peersByTopic.Load(topic)
   112  	if !ok {
   113  		return make([]peer.ID, 0)
   114  	}
   115  	return peerId.([]peer.ID)
   116  }
   117  
   118  // atomicUpdateAllTopics updates the list of all topics in the pubsub network that this node has subscribed to.
   119  func (s *SubscriptionProvider) atomicUpdateAllTopics(allTopics []string) {
   120  	s.allTopicsLock.Lock()
   121  	s.allTopics = allTopics
   122  	s.allTopicsLock.Unlock()
   123  }