unix sockets to support multiple clients

This commit is contained in:
Dmitry Zuikov 2023-09-28 05:23:41 +03:00
parent 00800e0867
commit 902125da75
4 changed files with 273 additions and 72 deletions

View File

@ -1,3 +1,23 @@
TODO: test-unix-sockets-multiple-clients TODO: test-unix-sockets-multiple-clients
Протестировать, работают ли Unix сокеты Протестировать, работают ли Unix сокеты
в режиме с многими клиентами. в режиме с многими клиентами.
TODO: hbs2-peer-storage-unix-socket-rpc
RPC специфичное для операций со сторейджем.
Команды соответствуют методам Storage.
Синхронная обёртка, т.е некий тайпкласс,
который говорит, как в команды протокола
добавлять уникальный нонс, и обёртка,
которая после отправки запроса ждёт
ответа с заданным нонсом.
Хорошо бы типизировать, т.е с одной стороны
не хочется делать разные типы, это вроде бы
один протокол.
С другой стороны, непонятно, как это
типизировать тогда.

View File

@ -308,7 +308,7 @@ instance ( MonadIO m
when allowed do when allowed do
sendTo pipe (To peer_e) (From me) (AnyMessage @(Encoded e) @e proto (encode msg)) sendTo pipe (To peer_e) (From me) (AnyMessage @(Encoded e) @e proto (encode msg))
trace $ "REQUEST: after sendTo" <+> viaShow peer_e -- trace $ "REQUEST: after sendTo" <+> viaShow peer_e
instance ( Typeable (EventHandler e p (PeerM e IO)) instance ( Typeable (EventHandler e p (PeerM e IO))

View File

@ -1,3 +1,4 @@
{-# Language TemplateHaskell #-}
module HBS2.Net.Messaging.Unix where module HBS2.Net.Messaging.Unix where
import HBS2.Prelude.Plated import HBS2.Prelude.Plated
@ -14,6 +15,8 @@ import Data.ByteString.Lazy qualified as LBS
import Data.Function import Data.Function
import Data.Functor import Data.Functor
import Data.Hashable import Data.Hashable
import Data.HashMap.Strict qualified as HashMap
import Data.HashMap.Strict (HashMap)
import Data.List qualified as List import Data.List qualified as List
import Network.ByteOrder hiding (ByteString) import Network.ByteOrder hiding (ByteString)
import Network.Socket import Network.Socket
@ -21,11 +24,28 @@ import Network.Socket.ByteString
import Control.Concurrent.STM.TQueue (flushTQueue) import Control.Concurrent.STM.TQueue (flushTQueue)
import Data.Set (Set) import Data.Set (Set)
import Data.Set qualified as Set import Data.Set qualified as Set
import Lens.Micro.Platform
import UnliftIO import UnliftIO
import Control.Concurrent (myThreadId)
data UNIX = UNIX data UNIX = UNIX
deriving (Eq,Ord,Show,Generic) deriving (Eq,Ord,Show,Generic)
type PeerUnixAddr = String
instance HasPeer UNIX where
newtype instance Peer UNIX = PeerUNIX { _fromPeerUnix :: PeerUnixAddr}
deriving stock (Eq,Ord,Show,Generic)
deriving newtype (Pretty)
instance IsString (Peer UNIX) where
fromString = PeerUNIX
instance Hashable (Peer UNIX) where
hashWithSalt salt (PeerUNIX p) = hashWithSalt salt p
{- HLINT ignore "Use newtype instead of data" -} {- HLINT ignore "Use newtype instead of data" -}
data MessagingUnixOpts = data MessagingUnixOpts =
MUWatchdog Int MUWatchdog Int
@ -40,13 +60,14 @@ data MessagingUnix =
, msgUnixRetryTime :: Timeout 'Seconds , msgUnixRetryTime :: Timeout 'Seconds
, msgUnixSelf :: Peer UNIX , msgUnixSelf :: Peer UNIX
, msgUnixOpts :: Set MessagingUnixOpts , msgUnixOpts :: Set MessagingUnixOpts
, msgUnixInbox :: TQueue ByteString , msgUnixSendTo :: TVar (HashMap (Peer UNIX) (TQueue ByteString))
, msgUnixRecv :: TQueue (From UNIX, ByteString) , msgUnixRecv :: TQueue (From UNIX, ByteString)
, msgUnixLast :: TVar TimeSpec , msgUnixLast :: TVar TimeSpec
, msgUnixAccepts :: TVar Int , msgUnixAccepts :: TVar Int
, msgSockets :: TVar (HashMap (Peer UNIX) Socket)
} }
makeLenses 'PeerUNIX
newMessagingUnix :: MonadIO m newMessagingUnix :: MonadIO m
=> Bool => Bool
@ -57,7 +78,7 @@ newMessagingUnix :: MonadIO m
newMessagingUnix server tsec path = do newMessagingUnix server tsec path = do
newMessagingUnixOpts mempty server tsec path newMessagingUnixOpts mempty server tsec path
newMessagingUnixOpts :: MonadIO m newMessagingUnixOpts :: (MonadIO m)
=> [MessagingUnixOpts] => [MessagingUnixOpts]
-> Bool -> Bool
-> Timeout 'Seconds -> Timeout 'Seconds
@ -65,39 +86,23 @@ newMessagingUnixOpts :: MonadIO m
-> m MessagingUnix -> m MessagingUnix
newMessagingUnixOpts opts server tsec path = do newMessagingUnixOpts opts server tsec path = do
let sa = SockAddrUnix path
now <- getTimeCoarse now <- getTimeCoarse
MessagingUnix path MessagingUnix path
server server
tsec tsec
(PeerUNIX sa) (PeerUNIX path)
(Set.fromList opts) (Set.fromList opts)
<$> liftIO newTQueueIO <$> liftIO (newTVarIO mempty)
<*> liftIO newTQueueIO <*> liftIO newTQueueIO
<*> liftIO (newTVarIO now) <*> liftIO (newTVarIO now)
<*> liftIO (newTVarIO 0) <*> liftIO (newTVarIO 0)
<*> liftIO (newTVarIO mempty)
instance HasPeer UNIX where
newtype instance Peer UNIX = PeerUNIX {fromPeerUnix :: SockAddr}
deriving stock (Eq,Ord,Show,Generic)
deriving newtype (Pretty)
instance IsString (Peer UNIX) where
fromString p = PeerUNIX (SockAddrUnix p)
-- FIXME: fix-code-dup?
instance Hashable (Peer UNIX) where
hashWithSalt salt p = case fromPeerUnix p of
SockAddrInet pn h -> hashWithSalt salt (4, fromIntegral pn, h)
SockAddrInet6 pn _ h _ -> hashWithSalt salt (6, fromIntegral pn, h)
SockAddrUnix s -> hashWithSalt salt ("unix", s)
data ReadTimeoutException = ReadTimeoutException deriving (Show, Typeable) data ReadTimeoutException = ReadTimeoutException deriving (Show, Typeable)
instance Exception ReadTimeoutException instance Exception ReadTimeoutException
runMessagingUnix :: MonadUnliftIO m => MessagingUnix -> m () runMessagingUnix :: MonadUnliftIO m => MessagingUnix -> m ()
runMessagingUnix env = do runMessagingUnix env = do
@ -118,12 +123,9 @@ runMessagingUnix env = do
void $ allocate (pure sock) (`shutdown` ShutdownBoth) void $ allocate (pure sock) (`shutdown` ShutdownBoth)
liftIO $ bind sock $ SockAddrUnix (msgUnixSockPath env) liftIO $ bind sock $ SockAddrUnix (msgUnixSockPath env)
liftIO $ listen sock 1 liftIO $ listen sock 5
let doFork = Set.member MUFork (msgUnixOpts env) let withSession = void . async . runResourceT
let withSession | doFork = void . async . runResourceT
| otherwise = void . runResourceT
watchdog <- async $ do watchdog <- async $ do
@ -149,14 +151,31 @@ runMessagingUnix env = do
run <- async $ forever $ runResourceT do run <- async $ forever $ runResourceT do
(so, sa) <- liftIO $ accept sock (so, sa) <- liftIO $ accept sock
atomically $ modifyTVar (msgUnixAccepts env) succ
-- FIXME: fixing-unix-sockets
-- Вот тут: нумеруем клиентов, в PeerAddr ставим
-- строку или номер.
peerNum <- atomically $ do
n <- readTVar (msgUnixAccepts env)
modifyTVar (msgUnixAccepts env) succ
pure n
withSession do withSession do
ti <- liftIO myThreadId
let that = msgUnixSelf env & over fromPeerUnix (<> "#" <> show peerNum)
void $ allocate ( createQueues env that ) dropQueuesFor
void $ allocate (pure so) close void $ allocate (pure so) close
writer <- async $ forever do writer <- async $ forever do
msg <- liftIO . atomically $ readTQueue (msgUnixInbox env) mq <- atomically $ readTVar (msgUnixSendTo env) <&> HashMap.lookup that
maybe1 mq none $ \q -> do
msg <- liftIO . atomically $ readTQueue q
let len = fromIntegral $ LBS.length msg :: Int let len = fromIntegral $ LBS.length msg :: Int
liftIO $ sendAll so $ bytestring32 (fromIntegral len) liftIO $ sendAll so $ bytestring32 (fromIntegral len)
liftIO $ sendAll so $ LBS.toStrict msg liftIO $ sendAll so $ LBS.toStrict msg
@ -166,10 +185,18 @@ runMessagingUnix env = do
link writer link writer
fix \next -> do fix \next -> do
-- FIXME: timeout-hardcode me <- liftIO myThreadId
let mq = Just (msgUnixRecv env)
frameLen <- liftIO $ recv so 4 <&> word32 <&> fromIntegral frameLen <- liftIO $ recv so 4 <&> word32 <&> fromIntegral
frame <- liftIO $ recv so frameLen frame <- liftIO $ recv so frameLen
atomically $ writeTQueue (msgUnixRecv env) (From (PeerUNIX sa), LBS.fromStrict frame)
let s = if msgUnixServer env then "S-" else "C-"
maybe1 mq none $ \q -> do
atomically $ writeTQueue q (From that, LBS.fromStrict frame)
now <- getTimeCoarse now <- getTimeCoarse
atomically $ writeTVar (msgUnixLast env) now atomically $ writeTVar (msgUnixLast env) now
next next
@ -183,12 +210,16 @@ runMessagingUnix env = do
runClient = liftIO $ forever $ handleAny logAndRetry $ runResourceT do runClient = liftIO $ forever $ handleAny logAndRetry $ runResourceT do
let sa = SockAddrUnix (msgUnixSockPath env)
let p = msgUnixSockPath env
let who = PeerUNIX p
createQueues env who
sock <- liftIO $ socket AF_UNIX Stream defaultProtocol sock <- liftIO $ socket AF_UNIX Stream defaultProtocol
void $ allocate (pure sock) close void $ allocate (pure sock) close
let sa = SockAddrUnix (msgUnixSockPath env)
let attemptConnect = do let attemptConnect = do
result <- liftIO $ try $ connect sock $ SockAddrUnix (msgUnixSockPath env) result <- liftIO $ try $ connect sock $ SockAddrUnix (msgUnixSockPath env)
case result of case result of
@ -200,14 +231,29 @@ runMessagingUnix env = do
attemptConnect attemptConnect
reader <- async $ forever do -- TODO: create-queues!
reader <- async $ do
forever do
let q = msgUnixRecv env
-- Read response from server -- Read response from server
frameLen <- liftIO $ recv sock 4 <&> word32 <&> fromIntegral frameLen <- liftIO $ recv sock 4 <&> word32 <&> fromIntegral
frame <- liftIO $ recv sock frameLen frame <- liftIO $ recv sock frameLen
atomically $ writeTQueue (msgUnixRecv env) (From (PeerUNIX sa), LBS.fromStrict frame)
-- сообщения кому? **МНЕ**
-- сообщения от кого? от **КОГО-ТО**
atomically $ writeTQueue q (From who, LBS.fromStrict frame)
forever do forever do
msg <- liftIO . atomically $ readTQueue (msgUnixInbox env)
-- Мы клиент. Шлём кому? **ЕМУ**, на том конце трубы.
-- У нас один контрагент, имя сокета (файла) == адрес пира.
-- Как в TCP порт сервиса (а отвечает тот с другого порта)
mq <- atomically $ readTVar (msgUnixSendTo env) <&> HashMap.lookup who
maybe1 mq none $ \q -> do
msg <- liftIO . atomically $ readTQueue q
let len = fromIntegral $ LBS.length msg :: Int let len = fromIntegral $ LBS.length msg :: Int
liftIO $ sendAll sock $ bytestring32 (fromIntegral len) liftIO $ sendAll sock $ bytestring32 (fromIntegral len)
liftIO $ sendAll sock $ LBS.toStrict msg liftIO $ sendAll sock $ LBS.toStrict msg
@ -218,22 +264,68 @@ runMessagingUnix env = do
warn $ "MessagingUnix. client seems gone. restaring server" <+> pretty (msgUnixSelf env) warn $ "MessagingUnix. client seems gone. restaring server" <+> pretty (msgUnixSelf env)
err (viaShow e) err (viaShow e)
atomically $ writeTVar (msgUnixAccepts env) 0 atomically $ writeTVar (msgUnixAccepts env) 0
liftIO $ atomically $ void $ flushTQueue (msgUnixInbox env)
liftIO $ atomically $ void $ flushTQueue (msgUnixRecv env) liftIO $ atomically $ void $ flushTQueue (msgUnixRecv env)
dropQueues
pause (msgUnixRetryTime env) pause (msgUnixRetryTime env)
logAndRetry :: SomeException -> IO () logAndRetry :: SomeException -> IO ()
logAndRetry e = do logAndRetry e = do
warn $ "MessagingUnix. runClient failed, probably server is gone. Retrying:" <+> pretty (msgUnixSelf env) warn $ "MessagingUnix. runClient failed, probably server is gone. Retrying:" <+> pretty (msgUnixSelf env)
err (viaShow e) err (viaShow e)
dropQueues
pause (msgUnixRetryTime env) pause (msgUnixRetryTime env)
dropQueues :: MonadIO m => m ()
dropQueues = do
-- liftIO $ atomically $ modifyTVar (msgUnixRecvFrom env) mempty
liftIO $ atomically $ modifyTVar (msgUnixSendTo env) mempty
-- мы не дропаем обратную очередь (принятые сообщения), потому,
-- что нет смысла. она живёт столько, сколько живёт клиент
-- очередь отправки мы удаляем, потому, что этого клиента
-- мы больше никогда не увидим, ведь они разделяются на уровне
-- сокетов и больше никак.
dropQueuesFor :: MonadIO m => Peer UNIX -> m ()
dropQueuesFor who = liftIO do
atomically do
modifyTVar (msgUnixSendTo env) (HashMap.delete who)
-- modifyTVar (msgUnixRecvFrom env) (HashMap.delete who)
createQueues :: MonadIO m => MessagingUnix -> Peer UNIX -> m (Peer UNIX)
createQueues env who = liftIO do
atomically $ do
sHere <- readTVar (msgUnixSendTo env) <&> HashMap.member who
if sHere then do
pure False
else do
sendToQ <- newTQueue
modifyTVar (msgUnixSendTo env) (HashMap.insert who sendToQ)
pure True
pure who
instance Messaging MessagingUnix UNIX ByteString where instance Messaging MessagingUnix UNIX ByteString where
sendTo bus (To _) _ msg = liftIO do sendTo bus (To who) (From me) msg = liftIO do
atomically $ writeTQueue (msgUnixInbox bus) msg
createQueues bus who
-- FIXME: handle-no-queue-for-rcpt-situation-1
mq <- atomically $ readTVar (msgUnixSendTo bus) <&> HashMap.lookup who
maybe1 mq none $ \q -> do
atomically $ writeTQueue q msg
receive bus _ = liftIO do receive bus _ = liftIO do
atomically $ readTQueue (msgUnixRecv bus) <&> List.singleton let q = msgUnixRecv bus
atomically $ peekTQueue q >> flushTQueue q

View File

@ -8,6 +8,8 @@ import HBS2.Net.Messaging.Unix
import HBS2.Actors.Peer import HBS2.Actors.Peer
import HBS2.OrDie import HBS2.OrDie
import HBS2.System.Logger.Simple
import Codec.Serialise import Codec.Serialise
import Control.Monad.Reader import Control.Monad.Reader
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
@ -18,11 +20,8 @@ import System.FilePath.Posix
import System.IO import System.IO
import System.IO.Temp import System.IO.Temp
import UnliftIO.Async import UnliftIO.Async
import UnliftIO qualified as UIO
import UnliftIO (TVar)
debug :: (MonadIO m) => Doc ann -> m ()
debug p = liftIO $ hPrint stderr p
data PingPong e = Ping Int data PingPong e = Ping Int
| Pong Int | Pong Int
@ -38,20 +37,72 @@ instance HasProtocol UNIX (PingPong UNIX) where
decode = either (const Nothing) Just . deserialiseOrFail decode = either (const Nothing) Just . deserialiseOrFail
encode = serialise encode = serialise
pingPongHandler :: forall e m . ( MonadIO m pingPongHandlerS :: forall e m . ( MonadIO m
, Response e (PingPong e) m , Response e (PingPong e) m
, HasProtocol e (PingPong e) , HasProtocol e (PingPong e)
, Pretty (Peer e)
) )
=> Int => TVar [(Peer e, PingPong e)]
-> Int
-> PingPong e -> PingPong e
-> m () -> m ()
pingPongHandler n = \case pingPongHandlerS tv n msg = do
Ping c -> debug ("Ping" <+> pretty c) >> response (Pong @e c) that <- thatPeer (Proxy @(PingPong e))
UIO.atomically $ UIO.modifyTVar tv ((that,msg):)
case msg of
Ping c -> do
debug ("S: Ping" <+> pretty c <+> "from" <+> pretty that ) >> response (Pong @e c)
Pong _ -> pure ()
pingPongHandler1 :: forall e m . ( MonadIO m
, Response e (PingPong e) m
, HasProtocol e (PingPong e)
)
=> TVar [PingPong e]
-> Int
-> PingPong e
-> m ()
pingPongHandler1 t n msg = do
UIO.atomically $ UIO.modifyTVar t (msg:)
case msg of
Ping c -> pure ()
Pong c -> pure ()
-- Pong c | c < n -> debug ("C1: Pong" <+> pretty c) >> response (Ping @e (succ c))
-- | otherwise -> pure ()
pingPongHandler2 :: forall e m . ( MonadIO m
, Response e (PingPong e) m
, HasProtocol e (PingPong e)
)
=> TVar [PingPong e]
-> Int
-> PingPong e
-> m ()
pingPongHandler2 t n msg = do
UIO.atomically $ UIO.modifyTVar t (msg:)
case msg of
Ping c -> pure ()
Pong c -> pure ()
-- Pong c | c < n -> debug ("C2: Pong" <+> pretty c) >> response (Ping @e (succ c))
-- | otherwise -> pure ()
Pong c | c < n -> debug ("Pong" <+> pretty c) >> response (Ping @e (succ c))
| otherwise -> pure ()
data PPEnv = data PPEnv =
PPEnv PPEnv
@ -66,6 +117,7 @@ newtype PingPongM m a = PingPongM { fromPingPong :: ReaderT PPEnv m a }
, Applicative , Applicative
, Monad , Monad
, MonadIO , MonadIO
, MonadUnliftIO
, MonadReader PPEnv , MonadReader PPEnv
, MonadTrans , MonadTrans
) )
@ -84,6 +136,14 @@ instance HasTimeLimits UNIX (PingPong UNIX) IO where
main :: IO () main :: IO ()
main = do main = do
setLogging @DEBUG (logPrefix "[debug] ")
setLogging @INFO (logPrefix "")
setLogging @ERROR (logPrefix "[err] ")
setLogging @WARN (logPrefix "[warn] ")
setLogging @NOTICE (logPrefix "[notice] ")
setLogging @TRACE (logPrefix "[trace] ")
liftIO $ hSetBuffering stdout LineBuffering liftIO $ hSetBuffering stdout LineBuffering
liftIO $ hSetBuffering stderr LineBuffering liftIO $ hSetBuffering stderr LineBuffering
@ -91,25 +151,54 @@ main = do
let soname = tmp </> "unix.socket" let soname = tmp </> "unix.socket"
server <- newMessagingUnix True 1.0 soname server <- newMessagingUnixOpts [MUFork] True 1.0 soname
client <- newMessagingUnix False 1.0 soname client1 <- newMessagingUnix False 1.0 soname
client2 <- newMessagingUnix False 1.0 soname
m1 <- async $ runMessagingUnix server m1 <- async $ runMessagingUnix server
m2 <- async $ runMessagingUnix client m2 <- async $ runMessagingUnix client1
m3 <- async $ runMessagingUnix client2
trs <- UIO.newTVarIO []
tr1 <- UIO.newTVarIO []
tr2 <- UIO.newTVarIO []
p1 <- async $ runPingPong server do p1 <- async $ runPingPong server do
runProto @UNIX runProto @UNIX
[ makeResponse (pingPongHandler 100000) [ makeResponse (pingPongHandlerS trs 2)
] ]
p2 <- async $ runPingPong client do -- p2 <- async $ pause @'Seconds 300
request (msgUnixSelf server) (Ping @UNIX 0) p2 <- async $ runPingPong client1 do
runProto @UNIX -- pause @'Seconds 0.25
[ makeResponse (pingPongHandler 100000) -- request (msgUnixSelf server) (Ping @UNIX 0)
l <- async $ runProto @UNIX
[ makeResponse (pingPongHandler1 tr1 10)
] ]
link l
forM_ [1..10] $ \n-> request (msgUnixSelf server) (Ping @UNIX n)
wait l
(_,r) <- liftIO $ waitAnyCatchCancel [m1,m2,p1,p2] -- p3 <- async $ pause @'Seconds 300
p3 <- async $ do
runPingPong client2 do
l <- async $ runProto @UNIX
[ makeResponse (pingPongHandler2 tr2 200)
]
link l
forM_ (take 10 [10000000..]) $ \n-> request (msgUnixSelf server) (Ping @UNIX n)
wait l
-- p4 <- async do
pause @'Seconds 10
UIO.readTVarIO trs >>= print . vcat . fmap (\(a,b) -> pretty (a, show b))
UIO.readTVarIO tr1 >>= print
UIO.readTVarIO tr2 >>= print
cancel m1
(_,r) <- liftIO $ waitAnyCatchCancel [m1,m2,m3,p1,p2,p3]
debug (viaShow r) debug (viaShow r)