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 }