packages feed

nats-queue 0.1.1.0 → 0.1.2.0

raw patch · 5 files changed

+363/−137 lines, 5 filesdep +asyncdep +hspecdep +nats-queuedep ~aesondep ~basedep ~containersPVP ok

version bump matches the API change (PVP)

Dependencies added: async, hspec, nats-queue

Dependency ranges changed: aeson, base, containers, dequeue, network, network-uri

API changes (from Hackage documentation)

+ Network.Nats: NatsHost :: String -> Int -> String -> String -> NatsHost
+ Network.Nats: NatsSettings :: [NatsHost] -> (Nats -> (String, Int) -> IO ()) -> (Nats -> String -> IO ()) -> NatsSettings
+ Network.Nats: connectSettings :: NatsSettings -> IO Nats
+ Network.Nats: data NatsHost
+ Network.Nats: data NatsSettings
+ Network.Nats: defaultSettings :: NatsSettings
+ Network.Nats: natsHHost :: NatsHost -> String
+ Network.Nats: natsHPass :: NatsHost -> String
+ Network.Nats: natsHPort :: NatsHost -> Int
+ Network.Nats: natsHUser :: NatsHost -> String
+ Network.Nats: natsHosts :: NatsSettings -> [NatsHost]
+ Network.Nats: natsOnConnect :: NatsSettings -> Nats -> (String, Int) -> IO ()
+ Network.Nats: natsOnDisconnect :: NatsSettings -> Nats -> String -> IO ()
+ Network.Nats: requestMany :: Nats -> String -> ByteString -> Int -> IO [ByteString]
+ Network.Nats.Json: requestMany :: (ToJSON a, FromJSON b) => Nats -> String -> a -> Int -> IO [b]

Files

Network/Nats.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE TemplateHaskell,OverloadedStrings,RecordWildCards,GeneralizedNewtypeDeriving,PatternGuards,DeriveDataTypeable #-}+{-# LANGUAGE TemplateHaskell,OverloadedStrings,RecordWildCards,GeneralizedNewtypeDeriving,PatternGuards,DeriveDataTypeable, ScopedTypeVariables #-}  module Network.Nats (     -- * How to use this module@@ -35,28 +35,37 @@     -- server usually closes the connection when there is an error.          -- * Comparison to API in other languages-    -- |Compared to API in other languages, the Haskell binding is very sparse. It does+    -- |Compared to API in other languages, the Haskell binding does     -- not implement timeouts and automatic unsubscribing, the 'request' call is implemented     -- as a synchronous call.      --     -- The timeouts can be easily implemented using 'System.Timeout' module, automatic unsubscribing-    -- can be easily done in the callback function.+    -- can be done in the callback function.          -- * Error behaviour     -- |The 'connect' function tries to connect to the NATS server. In case of failure it immediately fails.     -- If there is an error during operations, the NATS module tries to reconnect to the server.+    -- When there are more servers, the client immediately tries to connect to the next server. If+    -- that fails, it waits 1s before trying the next server in the NatsSettings list.+    --     -- During the reconnection, the calls 'subscribe' and 'request' will block. The calls     -- 'publish' and 'unsubscribe' silently fail (unsubscribe is handled locally, NATS is a messaging     -- system without guarantees, 'publish' is not guaranteed to succeed anyway).     -- After reconnecting to the server, the module automatically resubscribes to previously subscribed channels.     ---    -- If there is network failure, the nats commands 'subscribe' and 'request' -    -- may fail on a network exception. The 'subscribe'+    -- If there is a network failure, the nats commands 'subscribe' and 'request' +    -- may fail on an IOexception or NatsException. The 'subscribe'     -- command is synchronous, it waits until the server responds with +OK. The commands 'publish'-    -- and 'unsubscribe' are asynchronous, no confirmation from server is required.+    -- and 'unsubscribe' are asynchronous, no confirmation from server is required and they +    -- should not raise an exception.+    --     Nats     , NatsSID     , connect+    , connectSettings+    , NatsHost(..)+    , NatsSettings(..)+    , defaultSettings     -- * Exceptions     , NatsException     -- * Access@@ -65,6 +74,7 @@     , unsubscribe     , publish     , request+    , requestMany     -- * Termination     , disconnect ) where@@ -74,15 +84,18 @@ import Control.Concurrent.MVar import Control.Concurrent import qualified Network.Socket as S-import Network.Socket (SocketOption(KeepAlive), setSocketOption, getAddrInfo, SockAddr(..))+import Network.Socket (SocketOption(KeepAlive, NoDelay), setSocketOption, getAddrInfo, SockAddr(..)) import Control.Monad (forever, replicateM) import Data.Dequeue as D import Control.Applicative ((<$>)) import Data.Typeable import qualified Data.Foldable as FOLD-import Control.Exception (bracket, bracketOnError, throwIO, catch, IOException, AsyncException, Exception)+import Control.Exception (bracket, bracketOnError, throwIO, catch, IOException, AsyncException, Exception, SomeException,+                          catches, Handler(..)) import System.Random (randomRIO) import Data.IORef+import System.Timeout+import Control.Concurrent.Async (concurrently)  import qualified Data.Map.Strict as Map import qualified Data.ByteString.Lazy.Char8 as BL@@ -96,14 +109,22 @@  import qualified Network.URI as URI +-- | How often should we ping the server+pingInterval :: Int+pingInterval = 3000000++-- | Timeout interval for connect operations+timeoutInterval :: Int+timeoutInterval = 1000000+ -- | NATS communication error data NatsException = NatsException String     deriving (Show, Typeable) instance Exception NatsException-            + data NatsConnectionOptions = NatsConnectionOptions {-        natsConnUser :: T.Text-        , natsConnPass :: T.Text+        natsConnUser :: String+        , natsConnPass :: String         , natsConnVerbose :: Bool         , natsConnPedantic :: Bool         , natsConnSslRequired :: Bool@@ -123,10 +144,10 @@      -- | Server information sent usually upon opening a connection to NATS server data NatsServerInfo = NatsServerInfo {-    natsSvrServerId :: T.Text-    , natsSvrVersion :: T.Text-    , natsSvrMaxPayload :: Int-    , natsSvrAuthRequired :: Bool+--     natsSvrServerId :: T.Text+--     , natsSvrVersion :: T.Text+--     , natsSvrMaxPayload :: Int+      natsSvrAuthRequired :: Bool     } deriving (Show)      $(deriveJSON defaultOptions{fieldLabelModifier =(@@ -162,9 +183,7 @@      -- | Control structure representing a connection to NATS server data Nats = Nats {-          natsConnOptions :: NatsConnectionOptions-        , natsHost :: String-        , natsPort :: Int+          natsSettings :: NatsSettings         , natsRuntime :: MVar (Handle, -- Network socket                                FifoQueue, -- FIFO of sent commands waiting for ack                                Bool, -- False if we are disconnected@@ -175,7 +194,31 @@         , natsSubMap :: IORef (Map.Map NatsSID NatsSubscription)     } +-- | Host settings; may have different username/password for each host+data NatsHost = NatsHost {+        natsHHost :: String+      , natsHPort :: Int+      , natsHUser :: String      -- ^ Username for authentication+      , natsHPass :: String  -- ^ Password for authentication+    }+    +-- | Advanced settings for connecting to NATS server+data NatsSettings = NatsSettings {+        natsHosts :: [NatsHost]+      , natsOnConnect :: Nats -> (String, Int) -> IO ()  +        -- ^ Called when a client has successfully connected. This callback is called synchronously+        --   before the processing of incoming messages begins.+      , natsOnDisconnect :: Nats -> String -> IO () +        -- ^ Called when a client is disconnected.+    } +defaultSettings :: NatsSettings+defaultSettings = (NatsSettings {+        natsHosts = [(NatsHost "localhost" 4222 "nats" "nats")]+      , natsOnConnect = \_ _ -> (return ())+      , natsOnDisconnect = \_ _ -> (return ())+    })+     -- | Message received by the client from the server data NatsSvrMessage =     NatsSvrMsg { msgSubject::String, msgSid::NatsSID, msgText::BS.ByteString, msgReply::Maybe String}@@ -192,6 +235,7 @@ subjectToStr (Subject str) = str  makeSubject :: String -> Subject+makeSubject ""        = error "Empty subject" makeSubject str     | any (<=' ') str = error $ "Subject contains incorrect characters: " ++ str     | otherwise       = Subject str@@ -223,11 +267,9 @@      -- | Decode NATS server message; result is message + payload (payload is 'undefined' in NatsSvrMsg) decodeMessage :: BS.ByteString -> Maybe (NatsSvrMessage, Maybe Int)-decodeMessage line = decodeMessage_ mid mpayload+decodeMessage line = decodeMessage_ mid (BS.drop 1 mrest)     where -        (mid, mpayload) = (BS.takeWhile (\x -> x/=' ' && x/='\r') line, -                             BS.drop 1 $ BS.dropWhile (\x -> x/=' ' && x/='\r') line)-+        (mid, mrest) = BS.span (\x -> x/=' ' && x/='\r') line         decodeMessage_ :: BS.ByteString -> BS.ByteString -> Maybe (NatsSvrMessage, Maybe Int)         decodeMessage_ "PING" _ = Just (NatsSvrPing, Nothing)         decodeMessage_ "PONG" _ = Just (NatsSvrPong, Nothing)@@ -237,8 +279,7 @@             info <- AE.decode $ BL.fromChunks [msg]             return $ (NatsSvrInfo info, Nothing)         decodeMessage_ "MSG" msg = do-            let fields = BS.split ' ' msg-            case (map BS.unpack fields) of+            case (map BS.unpack (BS.words msg)) of                  [subj, sid, len] -> return (NatsSvrMsg subj (read sid) undefined Nothing, Just $ read len)                  [subj, sid, reply, len] -> return (NatsSvrMsg subj (read sid) undefined (Just $ reply), Just $ read len)                  _ -> fail ""@@ -265,7 +306,9 @@         (S.socket (S.addrFamily serveraddr) S.Stream S.defaultProtocol)         (S.sClose)         (\sock -> do+                         setSocketOption sock KeepAlive 1+            setSocketOption sock NoDelay 1             let connaddr = case (S.addrAddress serveraddr) of                     SockAddrInet _ haddr -> SockAddrInet (fromInteger $ toInteger port) haddr                     SockAddrInet6 _ finfo haddr scopeid -> SockAddrInet6 (fromInteger $ toInteger port) finfo haddr scopeid@@ -277,7 +320,10 @@         )                  -ensureConnection :: Nats -> Bool -> ((Handle, FifoQueue) -> IO FifoQueue) -> IO ()+ensureConnection :: Nats +    -> Bool -- ^ If true, wait for the connection to become available+    -> ((Handle, FifoQueue) -> IO FifoQueue) -- ^ Action to do when the connection is available+    -> IO () -- Block if we are disconnected ensureConnection nats True f = do     bracketOnError@@ -330,18 +376,32 @@         supportsCallback (NatsClntUnsubscribe {}) = True         supportsCallback _ = False +-- Throw exception if an action does not end in specified time+timeoutThrow :: Int -> IO a -> IO a+timeoutThrow t f = do+    res <- timeout t f+    case res of+         Just x -> return x+         Nothing -> throwIO $ NatsException "Reached timeout"+         _sendMessage :: Handle -> NatsClntMessage -> IO ()-_sendMessage handle cmsg = do-    BL.hPut handle $ makeClntMsg cmsg-    BS.hPut handle "\r\n"+_sendMessage handle cmsg = timeoutThrow timeoutInterval $ do+    let msg = makeClntMsg cmsg+    case () of+       _| BL.length msg < 1024 ->+                BS.hPut handle $ BS.concat $ (BL.toChunks msg) ++ ["\r\n"]+        | True -> do+                BL.hPut handle msg+                BS.hPut handle "\r\n"  -- | Do the authentication handshake if necessary-authenticate :: Nats -> Handle -> IO ()-authenticate nats handle = do+authenticate :: Handle -> String -> String -> IO ()+authenticate handle user password = do     info <- BS.hGetLine handle     case (decodeMessage info) of         Just (NatsSvrInfo (NatsServerInfo {natsSvrAuthRequired=True}), Nothing) -> do-            BL.hPut handle $ makeClntMsg (NatsClntConnect $ natsConnOptions nats)+            let coptions = defaultConnectionOptions{natsConnUser=user, natsConnPass=password}+            BL.hPut handle $ makeClntMsg (NatsClntConnect coptions)             BS.hPut handle "\r\n"             response <- BS.hGetLine handle             case (decodeMessage response) of@@ -352,21 +412,34 @@         _ -> throwIO $ NatsException "Incorrect input from server"              -- | Open and authenticate a connection-prepareConnection :: Nats -> IO ()-prepareConnection nats = do-    handle <- connectToServer (natsHost nats) (natsPort nats)-    authenticate nats handle-    (_, _, _, csig) <- takeMVar (natsRuntime nats)-    putMVar (natsRuntime nats) (handle, D.empty, True, undefined)-    putMVar csig ()+prepareConnection :: Nats -> NatsHost -> IO ()+prepareConnection nats nhost = timeoutThrow timeoutInterval $+    bracketOnError+        (connectToServer (natsHHost nhost) (natsHPort nhost))+        (hClose)+        (\handle -> do+            authenticate handle (natsHUser nhost) (natsHPass nhost)+            csig <- modifyMVar (natsRuntime nats) $ \(_,_,_, csig) ->+                return $ ((handle, D.empty, True, undefined), csig)+            putMVar csig ()+        )  -- | Main thread that reads events from NATS server and reconnects if necessary-connectionThread :: Nats -> IO ()-connectionThread nats = do-    connectionHandler nats -        `catch` errorHandler-        `catch` finalHandler+connectionThread :: Nats+                    -> [NatsHost] -- ^ inifinite list of connections to try+                    -> IO ()+connectionThread _ [] = error "Empty list of connections"+connectionThread nats (thisconn:nextconn) = do+    mnewconnlist <- +        (connectionHandler nats thisconn >> return Nothing) -- connectionHandler never returns...+            `catches` [Handler (\(e :: IOException) -> Just <$> errorHandler e),+                       Handler (\(e :: NatsException) -> Just <$> errorHandler e),+                       Handler (\e -> finalHandler e >> return Nothing)]+    case mnewconnlist of+         Nothing -> return () -- Never happens+         Just newconnlist -> connectionThread nats newconnlist     where+        finalize :: (Show e) => e -> IO ()         finalize e = do             -- Hide existing connection             (handle, queue, _, _) <- takeMVar (natsRuntime nats)@@ -376,70 +449,100 @@             hClose handle             -- Call appropriate callbacks on unfinished calls             FOLD.mapM_ (\f -> f $ Just (T.pack $ show e)) queue+            -- Call user supplied disconnect+            (natsOnDisconnect $ natsSettings nats) nats (show e)             -        errorHandler :: IOException -> IO ()+        errorHandler :: (Show e) => e -> IO [NatsHost]         errorHandler e = do             finalize e-            tryToConnect -            -- Restart-            connectionThread nats+            tryToConnect nextconn             where-                tryToConnect = do-                    threadDelay 5000000-                    prepareConnection nats -                        `catch` ((\_ -> tryToConnect) :: IOException -> IO ())-                        `catch` ((\_ -> tryToConnect) :: NatsException -> IO ())+                tryToConnect connlist@(conn:rest) = do+                    res <- ((prepareConnection nats conn) >> (return $ Just connlist))+                        `catches` [ Handler (\(_ :: IOException) -> return Nothing),+                                    Handler (\(_ :: NatsException) -> return Nothing) ]+                    case res of+                         Just restlist -> return restlist+                         Nothing       -> threadDelay timeoutInterval >> tryToConnect rest+                tryToConnect [] = error "Empty list of connections"                                  -- Handler for exiting the thread         finalHandler :: AsyncException -> IO ()-        finalHandler e = finalize e+        finalHandler e = do+            finalize e -connectionHandler :: Nats -> IO ()-connectionHandler nats = do+pingerThread :: Nats -> IORef (Int, Int) -> IO ()+pingerThread nats pingStatus = forever $ do+    threadDelay pingInterval+    -- todo - kontrola pingu+    ok <- atomicModifyIORef' pingStatus $ \(pings, pongs) -> ((pings+1, pongs), pings - pongs < 2)+    if ok == False+        then throwIO (NatsException "Ping timeouted") +        else return ()+    sendMessage nats True NatsClntPing Nothing+        +-- | Forever read input from a connection and process it+connectionHandler :: Nats -> NatsHost -> IO ()+connectionHandler nats (NatsHost host port _ _) = do     (handle, _, _, _) <- readMVar (natsRuntime nats)     -- Subscribe channels that are supposed to be subscribed     subscriptions <- readIORef (natsSubMap nats)     FOLD.forM_ subscriptions $ \(NatsSubscription subject queue _ sid) ->         sendMessage nats True (NatsClntSubscribe subject sid queue) Nothing+        +    -- Call user function that we are successfully connected+    (natsOnConnect $ natsSettings nats) nats (host, port)+    -- Allocate structures for PING, IORef is probably easiest to manage+    pingStatus <- newIORef (0, 0)+         -- Perform the job-    forever $ -        let-            -- | Pull callback for OK/ERR status from FIFO queue-            popCb (h, queue, x1, x2) = return ((h, newq, x1, x2), item)-                where-                    (item, newq) = D.popFront queue-            handleMessage NatsSvrPing = sendMessage nats True NatsClntPong Nothing-            handleMessage NatsSvrPong = return ()-            handleMessage NatsSvrOK = do-                cb <- modifyMVar (natsRuntime nats) $ popCb-                case cb of-                     Just f -> f Nothing-                     Nothing -> return () -- This should not happen, spurious OK-            handleMessage (NatsSvrError txt) = do-                cb <- modifyMVar (natsRuntime nats) $ popCb-                case cb of-                     Just f -> f $ Just txt-                     Nothing -> putStrLn $ show txt-            handleMessage (NatsSvrInfo (NatsServerInfo {natsSvrAuthRequired=True})) = do-                sendMessage nats True (NatsClntConnect $ natsConnOptions nats) Nothing-            handleMessage (NatsSvrInfo _) = return ()-            handleMessage (NatsSvrMsg {..}) = do-                msubscription <- Map.lookup msgSid <$> readIORef (natsSubMap nats)-                case msubscription of-                     Just subscription -> (subCallback subscription) msgSid msgSubject (BL.fromChunks [msgText]) msgReply-                     -- SID not found in map, force unsubscribe-                     Nothing -> sendMessage nats True (NatsClntUnsubscribe msgSid) Nothing -        in do-            line <- BS.hGetLine handle-            case (decodeMessage line) of-                    Just (msg, Nothing) -> do-                        handleMessage msg-                    Just (msg@(NatsSvrMsg {}), Just paylen) -> do-                        payload <- BS.hGet handle paylen-                        _ <- BS.hGet handle 2 -- CRLF-                        handleMessage msg{msgText=payload}-                    _ -> -                        putStrLn $ "Incorrect message: " ++ (show line)+    _ <- concurrently+            (pingerThread nats pingStatus)+            (connectionHandler' handle nats pingStatus)+    return ()++connectionHandler' :: Handle -> Nats -> IORef (Int, Int) -> IO ()+connectionHandler' handle nats pingStatus = forever $ do+    line <- BS.hGetLine handle+    case decodeMessage line of+        Just (msg, Nothing) -> +            handleMessage msg+        Just (msg@(NatsSvrMsg {}), Just paylen) -> do+            payload <- BS.hGet handle (paylen + 2) -- +2 = CRLF+            handleMessage msg{msgText=BS.take paylen payload}+        _ -> +            putStrLn $ "Incorrect message: " ++ (show line)+            +    where+        -- | Pull callback for OK/ERR status from FIFO queue+        popCb (h, queue, x1, x2) = return ((h, newq, x1, x2), item)+            where+                (item, newq) = D.popFront queue+        handleMessage NatsSvrPing = sendMessage nats True NatsClntPong Nothing+        handleMessage NatsSvrPong = +            atomicModifyIORef' pingStatus $ +                \(pings, pongs) -> ((pings, pongs + 1), ())+            +        handleMessage NatsSvrOK = do+            cb <- modifyMVar (natsRuntime nats) $ popCb+            case cb of+                Just f -> f Nothing+                Nothing -> return () -- This should not happen, spurious OK+        handleMessage (NatsSvrError txt) = do+            cb <- modifyMVar (natsRuntime nats) $ popCb+            case cb of+                Just f -> f $ Just txt+                Nothing -> putStrLn $ show txt+        handleMessage (NatsSvrInfo _) = return ()+        handleMessage (NatsSvrMsg {..}) = do+            msubscription <- Map.lookup msgSid <$> readIORef (natsSubMap nats)+            case msubscription of+                Just subscription -> +                    (subCallback subscription) msgSid msgSubject (BL.fromChunks [msgText]) msgReply+                    `catch`+                        (\(e :: SomeException) -> putStrLn $ (show e))+                -- SID not found in map, force unsubscribe+                Nothing -> sendMessage nats True (NatsClntUnsubscribe msgSid) Nothing                           -- | Connect to a NATS server     connect :: String -- ^ URI with format: nats:\/\/user:password\@localhost:4222@@ -459,26 +562,36 @@                                           takeWhile (\x -> x /= '@') $ drop 1 $ dropWhile (\x -> x /= ':') uriUserInfo                                           )                 Nothing -> error "Missing hostname section"+    connectSettings defaultSettings{+            natsHosts=[(NatsHost host port user password)]+        } +-- | Connect to NATS server using custom settings+connectSettings :: NatsSettings -> IO Nats                +connectSettings settings = do     csig <- newEmptyMVar     mruntime <- newMVar (undefined, undefined, False, csig)     mthreadid <- newEmptyMVar      nextsid <- newIORef 1     submap <- newIORef Map.empty-    let opts = defaultConnectionOptions{natsConnUser=T.pack user, natsConnPass=T.pack password}      let nats = Nats{-        natsConnOptions=opts -        , natsHost=host -        , natsPort=port -        , natsRuntime=mruntime -        , natsThreadId=mthreadid -        , natsNextSid=nextsid -        , natsSubMap=submap+              natsSettings=settings+            , natsRuntime=mruntime +            , natsThreadId=mthreadid +            , natsNextSid=nextsid +            , natsSubMap=submap         }-    prepareConnection nats-    threadid <- forkIO $ connectionThread nats+        hosts = (natsHosts settings)+    +    -- Try to connect to all until one succeeds+    connhost <- tryUntilSuccess hosts $ prepareConnection nats+    threadid <- forkIO $ connectionThread nats (connhost:(cycle hosts))     putMVar mthreadid threadid     return nats+    where+        tryUntilSuccess [a] f = f a >> return a+        tryUntilSuccess (a:rest) f = (f a >> return a) `catch` (\(_ :: SomeException) -> tryUntilSuccess rest f)+        tryUntilSuccess [] _ = error "Empty list"      -- | Subscribe to a channel, optionally specifying queue group  subscribe :: Nats @@ -486,22 +599,26 @@     -> (Maybe String) -- ^ Queue     -> MsgCallback -- ^ Callback     -> IO NatsSID -- ^ SID of subscription-subscribe nats subject queue cb = do-    let ssubject = makeSubject subject-    let squeue = makeSubject `fmap` queue-    mvar <- newEmptyMVar :: IO (MVar (Maybe T.Text))-    sid <- newNatsSid nats-    sendMessage nats True (NatsClntSubscribe ssubject sid squeue) $ Just $ \err -> do-        case err of-            Just _ -> return ()-            Nothing -> atomicModifyIORef' (natsSubMap nats) $ \ioref ->-                (Map.insert sid (NatsSubscription{subSubject=ssubject, subQueue=squeue, subCallback=cb, subSid=sid}) ioref, ()) -        putMVar mvar err-    merr <- takeMVar mvar-    case merr of-         Just err -> throwIO $ NatsException $ T.unpack err-         Nothing -> return $ sid+subscribe nats subject queue cb = +    let+        ssubject = makeSubject subject+        squeue = makeSubject `fmap` queue+        addToSubTable sid = atomicModifyIORef' (natsSubMap nats) $ \submap ->+                (Map.insert sid (NatsSubscription{subSubject=ssubject, subQueue=squeue, subCallback=cb, subSid=sid}) submap, ()) +    in do+        mvar <- newEmptyMVar :: IO (MVar (Maybe T.Text))+        sid <- newNatsSid nats+        sendMessage nats True (NatsClntSubscribe ssubject sid squeue) $ Just $ \err -> do+            case err of+                Nothing -> addToSubTable sid+                Just _ -> return ()+            putMVar mvar err +        merr <- takeMVar mvar+        case merr of+            Just err -> throwIO $ NatsException $ T.unpack err+            Nothing -> return $ sid+ -- | Unsubscribe from a channel unsubscribe :: Nats      -> NatsSID @@ -511,9 +628,11 @@     atomicModifyIORef' (natsSubMap nats) $ \ioref -> (Map.delete sid ioref, ())     -- Unsubscribe from server, ignore errors     sendMessage nats False (NatsClntUnsubscribe sid) Nothing-        `catch` ((\_ -> return ()) :: IOException -> IO ())+        `catches` [ Handler (\(_ :: IOException) -> return ()),+                    Handler (\(_ :: NatsException) -> return ()) ]+             --- | Synchronous request/response communication+-- | Synchronous request/response communication to obtain one message request :: Nats      -> String             -- ^ Subject     -> BL.ByteString      -- ^ Request@@ -526,7 +645,7 @@                 _ <- tryPutMVar mvar (Right response)                 return ()             ) -            (\sid -> unsubscribe nats sid)+            (unsubscribe nats)             (\_ -> do                 sendMessage nats True (NatsClntPublish (makeSubject subject) (Just $ makeSubject inbox) body) $ Just $ \merr -> do                     case merr of@@ -538,15 +657,43 @@                      Right res -> return $ res             ) +-- | Synchronous request/response for obtaining many messages in certain timespan+requestMany :: Nats+    -> String              -- ^ Subject+    -> BL.ByteString       -- ^ Body+    -> Int                 -- ^ Timeout in microseconds+    -> IO [BL.ByteString]+requestMany nats subject body time = do+    result <- newIORef []+    inbox <- newInbox+    bracket +        (subscribe nats inbox Nothing $ \_ _ response _ ->+                atomicModifyIORef result $ \old -> (response:old, ())+        )+        (unsubscribe nats)+        (\_ -> do+            publish' nats subject (Just inbox) body+            threadDelay time+        )+    reverse <$> readIORef result+             -- | Publish a message publish :: Nats      -> String -- ^ Subject     -> BL.ByteString -- ^ Data     -> IO ()-publish nats subject body = do+publish nats subject body = publish' nats subject Nothing body+    +publish' :: Nats +    -> String -- ^ Subject+    -> Maybe String+    -> BL.ByteString -- ^ Data+    -> IO ()+publish' nats subject inbox body = do     -- Ignore errors - messages can get lost-    sendMessage nats False (NatsClntPublish (makeSubject subject) Nothing body) Nothing-        `catch` ((\_ -> return()) :: IOException -> IO ())+    sendMessage nats False (NatsClntPublish (makeSubject subject) (makeSubject <$> inbox) body) Nothing+        `catches` [ Handler (\(_ :: IOException) -> return ()),+                    Handler (\(_ :: NatsException) -> return ()) ]      -- | Disconnect from a NATS server disconnect :: Nats -> IO ()
Network/Nats/Json.hs view
@@ -4,11 +4,14 @@ module Network.Nats.Json (     subscribe   , publish+  , requestMany ) where  import Network.Nats (Nats, NatsSID) import qualified Network.Nats as N import qualified Data.Aeson as AE+import Data.Maybe (catMaybes)+import Control.Applicative ((<$>))  -- | Publish a message publish :: AE.ToJSON a =>@@ -37,3 +40,14 @@         cb sid subj msg repl             | Just body <- AE.decode msg = jcallback sid subj body repl             | True                       = return () -- Ignore when there is an error decoding++requestMany :: (AE.ToJSON a, AE.FromJSON b) =>+    Nats+    -> String              -- ^ Subject+    -> a                   -- ^ Body+    -> Int                 -- ^ Timeout in microseconds+    -> IO [b]+requestMany nats subject body time = +   decodeAndFilter <$> N.requestMany nats subject (AE.encode body) time+    where+        decodeAndFilter = catMaybes . map AE.decode
nats-queue.cabal view
@@ -1,5 +1,5 @@ name:                nats-queue-version:             0.1.1.0+version:             0.1.2.0 synopsis:            Haskell API for NATS messaging system description:              This library is a Haskell driver for NATS <http://nats.io>. @@ -14,8 +14,7 @@ maintainer:          palkovsky.ondrej@gmail.com category:            Network build-type:          Simple-cabal-version:       >=1.8-+cabal-version:       >=1.10  source-repository head   type:     git@@ -23,14 +22,36 @@  library   exposed-modules:   Network.Nats, Network.Nats.Json-  build-depends:     base==4.*, network>=2.6, dequeue, random, network-uri>=2.6,-                     containers >= 0.5, bytestring, text, aeson >= 0.7+  build-depends:     base == 4.*+                   , network >=2.6+                   , dequeue+                   , random+                   , network-uri >= 2.6+                   , containers  >= 0.5+                   , bytestring+                   , text+                   , aeson >= 0.7+                   , async+                      ghc-options:       -Wall-  extensions:        TemplateHaskell-                     OverloadedStrings-                     RecordWildCards-                     GeneralizedNewtypeDeriving-                     PatternGuards-                     DeriveDataTypeable-                     Rank2Types-                     +  +  default-language:  Haskell2010++test-suite spec+  main-is:         Spec.hs+  other-modules:   Network.NatsSpec+  type:            exitcode-stdio-1.0+  hs-source-dirs:  test+  default-language:  Haskell2010++  build-depends:     base+                   , nats-queue+                   , hspec+                   , network+                   , dequeue+                   , random+                   , network-uri+                   , containers+                   , bytestring+                   , text+                   , aeson
+ test/Network/NatsSpec.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.NatsSpec where++import Network.Nats+import Test.Hspec+import Data.IORef+import Control.Concurrent (threadDelay)+import Data.ByteString.Lazy ()++natsUrl =  "nats://nats:nats@localhost:4222"++-- fakeSettings = defaultSettings {+--         natsHosts=[(NatsHost "127.0.0.1" 5222 "nats" "nats"), +--                    (NatsHost "127.0.0.1" 5223 "nats" "nats"), +--                    (NatsHost "127.0.0.1" 5224 "nats" "nats")],+--         natsOnConnect=onconnect,+--         natsOnDisconnect=ondisconnect+--     }++spec :: Spec+spec = describe "Client" $ do+    it ("connects and disconnects to " ++ natsUrl) $ do+        nats <- connect natsUrl+        disconnect nats+    +    it "sends and receives message" $ do+        nats <- connect natsUrl+        mcount <- newIORef 0+        sid <- subscribe nats "nats.test" Nothing $ \_ _ _ _ -> do+            atomicModifyIORef mcount (\x -> (x+1, ()))+        publish nats "nats.test" "Hello"+        threadDelay 100000+        unsubscribe nats sid+        publish nats "nats.test" "Hello2"+        threadDelay 100000+        disconnect nats+        newcount <- readIORef mcount+        newcount `shouldBe` 1+++main :: IO ()+main = hspec spec
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}