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

     1  package ping
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"time"
     8  
     9  	ggio "github.com/gogo/protobuf/io"
    10  	"github.com/libp2p/go-libp2p/core/host"
    11  	"github.com/libp2p/go-libp2p/core/network"
    12  	"github.com/libp2p/go-libp2p/core/peer"
    13  	"github.com/libp2p/go-libp2p/core/protocol"
    14  	"github.com/rs/zerolog"
    15  
    16  	fnetwork "github.com/onflow/flow-go/network"
    17  	"github.com/onflow/flow-go/network/internal/p2putils"
    18  	"github.com/onflow/flow-go/network/message"
    19  )
    20  
    21  const (
    22  	_  = iota
    23  	kb = 1 << (10 * iota)
    24  )
    25  
    26  const maxPingMessageSize = 5 * kb
    27  
    28  const pingTimeout = time.Second * 60
    29  
    30  // Service handles the outbound and inbound ping requests and response
    31  type Service struct {
    32  	host             host.Host
    33  	pingProtocolID   protocol.ID
    34  	pingInfoProvider fnetwork.PingInfoProvider
    35  	logger           zerolog.Logger
    36  }
    37  
    38  type InfoProvider struct {
    39  	SoftwareVersionFun   func() string
    40  	SealedBlockHeightFun func() (uint64, error)
    41  	HotstuffViewFun      func() (uint64, error)
    42  }
    43  
    44  func (p InfoProvider) SoftwareVersion() string {
    45  	return p.SoftwareVersionFun()
    46  }
    47  func (p InfoProvider) SealedBlockHeight() uint64 {
    48  	height, err := p.SealedBlockHeightFun()
    49  	// if the node is unable to report the latest sealed block height, then report 0 instead of failing the ping
    50  	if err != nil {
    51  		return uint64(0)
    52  	}
    53  	return height
    54  }
    55  
    56  func (p InfoProvider) HotstuffView() uint64 {
    57  	view, err := p.HotstuffViewFun()
    58  	if err != nil {
    59  		return uint64(0)
    60  	}
    61  	return view
    62  }
    63  
    64  func NewPingService(
    65  	h host.Host,
    66  	pingProtocolID protocol.ID,
    67  	logger zerolog.Logger,
    68  	pingProvider fnetwork.PingInfoProvider,
    69  ) *Service {
    70  	ps := &Service{host: h, pingProtocolID: pingProtocolID, pingInfoProvider: pingProvider, logger: logger}
    71  
    72  	h.SetStreamHandler(pingProtocolID, ps.pingHandler)
    73  	return ps
    74  }
    75  
    76  // PingHandler receives the inbound stream for Flow ping protocol and respond back with the PingResponse message
    77  func (ps *Service) pingHandler(s network.Stream) {
    78  
    79  	errCh := make(chan error, 1)
    80  	defer close(errCh)
    81  	timer := time.NewTimer(pingTimeout)
    82  	defer timer.Stop()
    83  
    84  	go func() {
    85  		log := p2putils.StreamLogger(ps.logger, s)
    86  		select {
    87  		case <-timer.C:
    88  			// if read or write took longer than configured timeout, then reset the stream
    89  			log.Error().Msg("ping timeout")
    90  			err := s.Reset()
    91  			if err != nil {
    92  				log.Error().Err(err).Msg("failed to reset stream")
    93  			}
    94  		case err, ok := <-errCh:
    95  			// reset the stream if an error occur while responding to a ping request
    96  			if ok {
    97  				log.Error().Err(err).Msg("ping response failed")
    98  				err := s.Reset()
    99  				if err != nil {
   100  					log.Error().Err(err).Msg("failed to reset stream")
   101  				}
   102  				return
   103  			}
   104  			// if no error, then just close the stream
   105  			err = s.Close()
   106  			if err != nil {
   107  				log.Error().Err(err).Msg("failed to close stream")
   108  			}
   109  		}
   110  	}()
   111  
   112  	pingRequest := &message.PingRequest{}
   113  	// create the reader
   114  	reader := ggio.NewDelimitedReader(s, maxPingMessageSize)
   115  
   116  	// read the request
   117  	err := reader.ReadMsg(pingRequest)
   118  	if err != nil {
   119  		errCh <- err
   120  		return
   121  	}
   122  
   123  	// create the writer
   124  	bufw := bufio.NewWriter(s)
   125  	writer := ggio.NewDelimitedWriter(bufw)
   126  
   127  	// query for the semantic version of the build this node is running
   128  	version := ps.pingInfoProvider.SoftwareVersion()
   129  
   130  	// query for the lastest finalized block height
   131  	blockHeight := ps.pingInfoProvider.SealedBlockHeight()
   132  
   133  	// query for the hotstuff view
   134  	hotstuffView := ps.pingInfoProvider.HotstuffView()
   135  
   136  	// create a PingResponse
   137  	pingResponse := &message.PingResponse{
   138  		Version:      version,
   139  		BlockHeight:  blockHeight,
   140  		HotstuffView: hotstuffView,
   141  	}
   142  
   143  	// send the PingResponse
   144  	err = writer.WriteMsg(pingResponse)
   145  	if err != nil {
   146  		errCh <- err
   147  		return
   148  	}
   149  
   150  	// flush the stream
   151  	err = bufw.Flush()
   152  	if err != nil {
   153  		errCh <- err
   154  		return
   155  	}
   156  }
   157  
   158  // Ping sends a Ping request to the remote node and returns the response, rtt and error if any.
   159  func (ps *Service) Ping(ctx context.Context, peerID peer.ID) (message.PingResponse, time.Duration, error) {
   160  	pingError := func(err error) error {
   161  		return fmt.Errorf("failed to ping peer %s: %w", peerID, err)
   162  	}
   163  
   164  	targetInfo := peer.AddrInfo{ID: peerID}
   165  
   166  	ps.host.ConnManager().Protect(targetInfo.ID, "ping")
   167  	defer ps.host.ConnManager().Unprotect(targetInfo.ID, "ping")
   168  
   169  	// connect to the target node
   170  	err := ps.host.Connect(ctx, targetInfo)
   171  	if err != nil {
   172  		return message.PingResponse{}, -1, pingError(err)
   173  	}
   174  
   175  	// ping the target
   176  	resp, rtt, err := ps.ping(ctx, targetInfo.ID)
   177  	if err != nil {
   178  		return message.PingResponse{}, -1, pingError(err)
   179  	}
   180  
   181  	return resp, rtt, nil
   182  }
   183  
   184  func (ps *Service) ping(ctx context.Context, p peer.ID) (message.PingResponse, time.Duration, error) {
   185  
   186  	// create a done channel to indicate Ping request-response is done or an error has occurred
   187  	done := make(chan error, 1)
   188  	defer close(done)
   189  
   190  	// create a new stream to the remote node
   191  	s, err := ps.host.NewStream(ctx, p, ps.pingProtocolID)
   192  	if err != nil {
   193  		return message.PingResponse{}, -1, fmt.Errorf("failed to create stream: %w", err)
   194  	}
   195  
   196  	// if ping succeeded, close the stream else reset the stream
   197  	go func() {
   198  		log := p2putils.StreamLogger(ps.logger, s)
   199  		select {
   200  		case <-ctx.Done():
   201  			// time expired without a response, log an error and reset the stream
   202  			log.Error().Msg("context timed out on ping to remote node")
   203  			// reset the stream (to cause an error on the remote side as well)
   204  			err := s.Reset()
   205  			if err != nil {
   206  				log.Err(err).Msg("failed to reset stream")
   207  			}
   208  		case <-done:
   209  			// close the stream
   210  			err := s.Close()
   211  			if err != nil {
   212  				log.Err(err).Msg("failed to close stream")
   213  			}
   214  		}
   215  	}()
   216  
   217  	// create the writer
   218  	bufw := bufio.NewWriter(s)
   219  	writer := ggio.NewDelimitedWriter(bufw)
   220  
   221  	// create a ping request
   222  	pingRequest := &message.PingRequest{}
   223  
   224  	// record start of ping
   225  	before := time.Now()
   226  
   227  	// send the request
   228  	err = writer.WriteMsg(pingRequest)
   229  	if err != nil {
   230  		return message.PingResponse{}, -1, err
   231  	}
   232  
   233  	// flush the stream
   234  	err = bufw.Flush()
   235  	if err != nil {
   236  		return message.PingResponse{}, -1, err
   237  	}
   238  
   239  	pingResponse := &message.PingResponse{}
   240  
   241  	// create the reader
   242  	reader := ggio.NewDelimitedReader(s, maxPingMessageSize)
   243  
   244  	// read the ping response
   245  	err = reader.ReadMsg(pingResponse)
   246  	if err != nil {
   247  		return message.PingResponse{}, -1, err
   248  	}
   249  
   250  	rtt := time.Since(before)
   251  	// No error, record the RTT.
   252  	ps.host.Peerstore().RecordLatency(p, rtt)
   253  
   254  	return *pingResponse, rtt, nil
   255  }