module LibP2P.Protocol.Ping
(
pingProtocolId
, PingError (..)
, PingResult (..)
, PingSession
, handlePing
, PingLimiter
, newPingLimiter
, handlePingLimited
, sendPing
, openPingSession
, ping
, pingWithTimeout
, closePingSession
, withPingSession
, registerPingHandler
, pingSize
, pingTimeoutMicros
, maxPingStreamsPerPeer
) where
import Control.Concurrent.MVar (MVar, newMVar, withMVar)
import Control.Concurrent.STM
( TVar
, atomically
, modifyTVar'
, newTVarIO
, readTVar
, writeTVar
)
import Control.Exception (SomeException, catch, finally, try)
import Control.Monad (unless)
import Data.ByteString (ByteString)
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import Data.Time.Clock (NominalDiffTime, diffUTCTime, getCurrentTime)
import Crypto.Random (getRandomBytes)
import LibP2P.Crypto.PeerId (PeerId)
import LibP2P.MultistreamSelect.Negotiation
( StreamIO (..)
, negotiateInitiator
, NegotiationResult (..)
, readExactBounded
)
import LibP2P.Switch.Connection (newStream)
import LibP2P.Switch.Types
( Connection (..)
, Switch (..)
)
import System.Timeout (timeout)
pingProtocolId :: Text
pingProtocolId :: Text
pingProtocolId = Text
"/ipfs/ping/1.0.0"
pingSize :: Int
pingSize :: Int
pingSize = Int
32
pingTimeoutMicros :: Int
pingTimeoutMicros :: Int
pingTimeoutMicros = Int
10000000
maxPingStreamsPerPeer :: Int
maxPingStreamsPerPeer :: Int
maxPingStreamsPerPeer = Int
2
data PingError
= PingTimeout
| PingMismatch
| PingStreamError !String
deriving (Int -> PingError -> ShowS
[PingError] -> ShowS
PingError -> [Char]
(Int -> PingError -> ShowS)
-> (PingError -> [Char])
-> ([PingError] -> ShowS)
-> Show PingError
forall a.
(Int -> a -> ShowS) -> (a -> [Char]) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> PingError -> ShowS
showsPrec :: Int -> PingError -> ShowS
$cshow :: PingError -> [Char]
show :: PingError -> [Char]
$cshowList :: [PingError] -> ShowS
showList :: [PingError] -> ShowS
Show, PingError -> PingError -> Bool
(PingError -> PingError -> Bool)
-> (PingError -> PingError -> Bool) -> Eq PingError
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: PingError -> PingError -> Bool
== :: PingError -> PingError -> Bool
$c/= :: PingError -> PingError -> Bool
/= :: PingError -> PingError -> Bool
Eq)
data PingResult = PingResult
{ PingResult -> NominalDiffTime
pingRTT :: !NominalDiffTime
} deriving (Int -> PingResult -> ShowS
[PingResult] -> ShowS
PingResult -> [Char]
(Int -> PingResult -> ShowS)
-> (PingResult -> [Char])
-> ([PingResult] -> ShowS)
-> Show PingResult
forall a.
(Int -> a -> ShowS) -> (a -> [Char]) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> PingResult -> ShowS
showsPrec :: Int -> PingResult -> ShowS
$cshow :: PingResult -> [Char]
show :: PingResult -> [Char]
$cshowList :: [PingResult] -> ShowS
showList :: [PingResult] -> ShowS
Show, PingResult -> PingResult -> Bool
(PingResult -> PingResult -> Bool)
-> (PingResult -> PingResult -> Bool) -> Eq PingResult
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: PingResult -> PingResult -> Bool
== :: PingResult -> PingResult -> Bool
$c/= :: PingResult -> PingResult -> Bool
/= :: PingResult -> PingResult -> Bool
Eq)
handlePing :: StreamIO -> PeerId -> IO ()
handlePing :: StreamIO -> PeerId -> IO ()
handlePing StreamIO
stream PeerId
_remotePeerId = IO ()
echoLoop IO () -> IO () -> IO ()
forall a b. IO a -> IO b -> IO a
`finally` StreamIO -> IO ()
closeQuietly StreamIO
stream
where
echoLoop :: IO ()
echoLoop = do
result <- StreamIO -> Int -> Int -> IO (Either [Char] ByteString)
readExactBounded StreamIO
stream Int
pingSize Int
pingSize IO (Either [Char] ByteString)
-> (SomeException -> IO (Either [Char] ByteString))
-> IO (Either [Char] ByteString)
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`catch`
(\(SomeException
_ :: SomeException) -> Either [Char] ByteString -> IO (Either [Char] ByteString)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([Char] -> Either [Char] ByteString
forall a b. a -> Either a b
Left [Char]
"stream closed"))
case result of
Left [Char]
_ -> () -> IO ()
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
Right ByteString
payload -> do
StreamIO -> ByteString -> IO ()
streamWrite StreamIO
stream ByteString
payload
IO ()
echoLoop
newtype PingLimiter = PingLimiter (TVar (Map.Map PeerId Int))
newPingLimiter :: IO PingLimiter
newPingLimiter :: IO PingLimiter
newPingLimiter = TVar (Map PeerId Int) -> PingLimiter
PingLimiter (TVar (Map PeerId Int) -> PingLimiter)
-> IO (TVar (Map PeerId Int)) -> IO PingLimiter
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Map PeerId Int -> IO (TVar (Map PeerId Int))
forall a. a -> IO (TVar a)
newTVarIO Map PeerId Int
forall k a. Map k a
Map.empty
handlePingLimited :: PingLimiter -> StreamIO -> PeerId -> IO ()
handlePingLimited :: PingLimiter -> StreamIO -> PeerId -> IO ()
handlePingLimited (PingLimiter TVar (Map PeerId Int)
countsVar) StreamIO
stream PeerId
peer = do
accepted <- STM Bool -> IO Bool
forall a. STM a -> IO a
atomically (STM Bool -> IO Bool) -> STM Bool -> IO Bool
forall a b. (a -> b) -> a -> b
$ do
counts <- TVar (Map PeerId Int) -> STM (Map PeerId Int)
forall a. TVar a -> STM a
readTVar TVar (Map PeerId Int)
countsVar
let live = Int -> PeerId -> Map PeerId Int -> Int
forall k a. Ord k => a -> k -> Map k a -> a
Map.findWithDefault Int
0 PeerId
peer Map PeerId Int
counts
if live >= maxPingStreamsPerPeer
then pure False
else do
writeTVar countsVar (Map.insert peer (live + 1) counts)
pure True
if accepted
then handlePing stream peer `finally` atomically (modifyTVar' countsVar releaseSlot)
else closeQuietly stream
where
releaseSlot :: Map PeerId Int -> Map PeerId Int
releaseSlot = (Int -> Maybe Int) -> PeerId -> Map PeerId Int -> Map PeerId Int
forall k a. Ord k => (a -> Maybe a) -> k -> Map k a -> Map k a
Map.update (\Int
n -> if Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
1 then Maybe Int
forall a. Maybe a
Nothing else Int -> Maybe Int
forall a. a -> Maybe a
Just (Int
n Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1)) PeerId
peer
data PingSession = PingSession
{ PingSession -> StreamIO
psStream :: !StreamIO
, PingSession -> IORef Bool
psClosed :: !(IORef Bool)
, PingSession -> MVar ()
psLock :: !(MVar ())
}
openPingSession :: Switch -> Connection -> IO (Either PingError PingSession)
openPingSession :: Switch -> Connection -> IO (Either PingError PingSession)
openPingSession Switch
sw Connection
conn = do
streamOrErr <- Switch -> Connection -> IO (Either ResourceError StreamIO)
newStream Switch
sw Connection
conn
case streamOrErr of
Left ResourceError
err ->
Either PingError PingSession -> IO (Either PingError PingSession)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PingError -> Either PingError PingSession
forall a b. a -> Either a b
Left ([Char] -> PingError
PingStreamError ([Char]
"stream reservation failed: " [Char] -> ShowS
forall a. [a] -> [a] -> [a]
++ ResourceError -> [Char]
forall a. Show a => a -> [Char]
show ResourceError
err)))
Right StreamIO
stream -> do
negotiated <- IO NegotiationResult -> IO (Either SomeException NegotiationResult)
forall e a. Exception e => IO a -> IO (Either e a)
try (StreamIO -> [Text] -> IO NegotiationResult
negotiateInitiator StreamIO
stream [Text
pingProtocolId])
case negotiated of
Right (Accepted Text
_) -> do
closedRef <- Bool -> IO (IORef Bool)
forall a. a -> IO (IORef a)
newIORef Bool
False
lock <- newMVar ()
pure (Right (PingSession stream closedRef lock))
Right NegotiationResult
NoProtocol -> do
StreamIO -> IO ()
closeQuietly StreamIO
stream
Either PingError PingSession -> IO (Either PingError PingSession)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PingError -> Either PingError PingSession
forall a b. a -> Either a b
Left ([Char] -> PingError
PingStreamError [Char]
"remote does not support ping"))
Left (SomeException
e :: SomeException) -> do
StreamIO -> IO ()
closeQuietly StreamIO
stream
Either PingError PingSession -> IO (Either PingError PingSession)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PingError -> Either PingError PingSession
forall a b. a -> Either a b
Left ([Char] -> PingError
PingStreamError ([Char]
"ping negotiation failed: " [Char] -> ShowS
forall a. [a] -> [a] -> [a]
++ SomeException -> [Char]
forall a. Show a => a -> [Char]
show SomeException
e)))
ping :: PingSession -> IO (Either PingError PingResult)
ping :: PingSession -> IO (Either PingError PingResult)
ping = Int -> PingSession -> IO (Either PingError PingResult)
pingWithTimeout Int
pingTimeoutMicros
pingWithTimeout :: Int -> PingSession -> IO (Either PingError PingResult)
pingWithTimeout :: Int -> PingSession -> IO (Either PingError PingResult)
pingWithTimeout Int
timeoutUs PingSession
sess = MVar ()
-> (() -> IO (Either PingError PingResult))
-> IO (Either PingError PingResult)
forall a b. MVar a -> (a -> IO b) -> IO b
withMVar (PingSession -> MVar ()
psLock PingSession
sess) ((() -> IO (Either PingError PingResult))
-> IO (Either PingError PingResult))
-> (() -> IO (Either PingError PingResult))
-> IO (Either PingError PingResult)
forall a b. (a -> b) -> a -> b
$ \() -> do
closed <- IORef Bool -> IO Bool
forall a. IORef a -> IO a
readIORef (PingSession -> IORef Bool
psClosed PingSession
sess)
if closed
then pure (Left (PingStreamError "ping session is closed"))
else do
result <- pingOnce timeoutUs (psStream sess)
case result of
Left PingError
err -> do
PingSession -> IO ()
closePingSession PingSession
sess
Either PingError PingResult -> IO (Either PingError PingResult)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PingError -> Either PingError PingResult
forall a b. a -> Either a b
Left PingError
err)
Either PingError PingResult
ok -> Either PingError PingResult -> IO (Either PingError PingResult)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Either PingError PingResult
ok
pingOnce :: Int -> StreamIO -> IO (Either PingError PingResult)
pingOnce :: Int -> StreamIO -> IO (Either PingError PingResult)
pingOnce Int
timeoutUs StreamIO
stream = do
payload <- Int -> IO ByteString
forall byteArray. ByteArray byteArray => Int -> IO byteArray
forall (m :: * -> *) byteArray.
(MonadRandom m, ByteArray byteArray) =>
Int -> m byteArray
getRandomBytes Int
pingSize :: IO ByteString
t0 <- getCurrentTime
outcome <- try $ timeout timeoutUs $ do
streamWrite stream payload
either fail pure =<< readExactBounded stream pingSize pingSize
case outcome of
Left (SomeException
e :: SomeException) ->
Either PingError PingResult -> IO (Either PingError PingResult)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PingError -> Either PingError PingResult
forall a b. a -> Either a b
Left ([Char] -> PingError
PingStreamError ([Char]
"ping I/O failed: " [Char] -> ShowS
forall a. [a] -> [a] -> [a]
++ SomeException -> [Char]
forall a. Show a => a -> [Char]
show SomeException
e)))
Right Maybe ByteString
Nothing -> Either PingError PingResult -> IO (Either PingError PingResult)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PingError -> Either PingError PingResult
forall a b. a -> Either a b
Left PingError
PingTimeout)
Right (Just ByteString
echo)
| ByteString
echo ByteString -> ByteString -> Bool
forall a. Eq a => a -> a -> Bool
/= ByteString
payload -> Either PingError PingResult -> IO (Either PingError PingResult)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PingError -> Either PingError PingResult
forall a b. a -> Either a b
Left PingError
PingMismatch)
| Bool
otherwise -> do
t1 <- IO UTCTime
getCurrentTime
pure (Right (PingResult (diffUTCTime t1 t0)))
closePingSession :: PingSession -> IO ()
closePingSession :: PingSession -> IO ()
closePingSession PingSession
sess = do
alreadyClosed <- IORef Bool -> (Bool -> (Bool, Bool)) -> IO Bool
forall a b. IORef a -> (a -> (a, b)) -> IO b
atomicModifyIORef' (PingSession -> IORef Bool
psClosed PingSession
sess) (\Bool
c -> (Bool
True, Bool
c))
unless alreadyClosed $ closeQuietly (psStream sess)
withPingSession
:: Switch
-> Connection
-> (PingSession -> IO a)
-> IO (Either PingError a)
withPingSession :: forall a.
Switch
-> Connection -> (PingSession -> IO a) -> IO (Either PingError a)
withPingSession Switch
sw Connection
conn PingSession -> IO a
action = do
opened <- Switch -> Connection -> IO (Either PingError PingSession)
openPingSession Switch
sw Connection
conn
case opened of
Left PingError
err -> Either PingError a -> IO (Either PingError a)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PingError -> Either PingError a
forall a b. a -> Either a b
Left PingError
err)
Right PingSession
sess -> (a -> Either PingError a
forall a b. b -> Either a b
Right (a -> Either PingError a) -> IO a -> IO (Either PingError a)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> PingSession -> IO a
action PingSession
sess) IO (Either PingError a) -> IO () -> IO (Either PingError a)
forall a b. IO a -> IO b -> IO a
`finally` PingSession -> IO ()
closePingSession PingSession
sess
sendPing :: Switch -> Connection -> IO (Either PingError PingResult)
sendPing :: Switch -> Connection -> IO (Either PingError PingResult)
sendPing Switch
sw Connection
conn = (PingError -> Either PingError PingResult)
-> (Either PingError PingResult -> Either PingError PingResult)
-> Either PingError (Either PingError PingResult)
-> Either PingError PingResult
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either PingError -> Either PingError PingResult
forall a b. a -> Either a b
Left Either PingError PingResult -> Either PingError PingResult
forall a. a -> a
id (Either PingError (Either PingError PingResult)
-> Either PingError PingResult)
-> IO (Either PingError (Either PingError PingResult))
-> IO (Either PingError PingResult)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Switch
-> Connection
-> (PingSession -> IO (Either PingError PingResult))
-> IO (Either PingError (Either PingError PingResult))
forall a.
Switch
-> Connection -> (PingSession -> IO a) -> IO (Either PingError a)
withPingSession Switch
sw Connection
conn PingSession -> IO (Either PingError PingResult)
ping
registerPingHandler :: Switch -> IO ()
registerPingHandler :: Switch -> IO ()
registerPingHandler Switch
sw = do
limiter <- IO PingLimiter
newPingLimiter
atomically $ do
protos <- readTVar (swProtocols sw)
let handler Connection
conn StreamIO
stream = PingLimiter -> StreamIO -> PeerId -> IO ()
handlePingLimited PingLimiter
limiter StreamIO
stream (Connection -> PeerId
connPeerId Connection
conn)
writeTVar (swProtocols sw) (Map.insert pingProtocolId handler protos)
closeQuietly :: StreamIO -> IO ()
closeQuietly :: StreamIO -> IO ()
closeQuietly StreamIO
stream = StreamIO -> IO ()
streamClose StreamIO
stream IO () -> (SomeException -> IO ()) -> IO ()
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`catch` \(SomeException
_ :: SomeException) -> () -> IO ()
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()