packages feed

zeromq4-clone-pattern-0.1.0.0: src/System/ZMQ4/Patterns/Clone/Internal.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RecordWildCards #-}
module System.ZMQ4.Patterns.Clone.Internal (
  -- * Shared
  Message(..)
, iSTATE_REQUEST

  -- * Server
, server

  -- * Client
, client
) where

import Control.Concurrent.Async (Async, waitBoth, uninterruptibleCancel)
import Control.Concurrent.MVar
import Control.Concurrent.STM

import Control.Monad (forever, void, when)
import Control.Monad.Catch
import Control.Monad.IO.Class (liftIO)

import Data.Binary
import Data.ByteString (ByteString)
import Data.Word (Word64)
import qualified Data.ByteString.Lazy as BL

import System.ZMQ4.Monadic

-- | Actual message sent between server and client.
data Message a = Message {
    messageVersion :: {-# UNPACK #-} !Word64
  , messageValue :: !a
  } deriving (Eq, Show)

instance Binary a => Binary (Message a) where
    put Message{..} = do
        put messageVersion
        put messageValue

    get = do
        messageVersion <- get
        messageValue <- get
        return $! Message{..}

-- | Message sent by a client to request a state snapshot
iSTATE_REQUEST :: ByteString
iSTATE_REQUEST = "STATE_REQUEST?"

-- | A server publishing messages to a client.
--
-- This function will start serving messages on the
-- current thread.
--
-- It will send all objects that are pushed on the
-- channel to a PUB socket.
--
-- It will listen for snapshot requests on a ROUTER
-- socket.
--
-- The server will automatically keep the last sent
-- object cached, and use at as a snapshot. Sequencing
-- is automatically handled using a 'Word64' sequence
-- counter.
server :: Binary a
       => String    -- ^ Bind address of the PUB socket
       -> String    -- ^ Bind address of the ROUTER socket
       -> MVar a    -- ^ Channel of incoming messages
       -> IO ()
server !pubAddr !routerAddr !chan = runZMQ $ do
    ready <- liftIO newEmptyMVar
    routerC <- liftIO . atomically $ newTVar Nothing

    withAsync (publisher ready routerC) $ \t1 ->
        withAsync (router ready routerC) $ \t2 ->
            void . liftIO $ waitBoth t1 t2
  where
    publisher :: forall z void. MVar () -> TVar (Maybe ByteString) -> ZMQ z void
    publisher ready routerC = do
        -- bind socket
        pub <- socket Pub
        bind pub pubAddr

        -- wait for router
        () <- liftIO $ takeMVar ready

        -- handle all messages
        let next :: Word64 -> ZMQ z void
            next !seq' = do a <- liftIO $ takeMVar chan
                            let !env = Message seq' a
                                !msg = BL.toStrict (encode env)
                                !jmsg = Just msg
                            liftIO . atomically $ writeTVar routerC jmsg
                            send pub [] msg
                            next (seq'+1)

        next 0

    router :: forall z void. MVar () -> TVar (Maybe ByteString) -> ZMQ z void
    router ready routerC = do
        -- bind socket
        rout <- socket Router
        bind rout routerAddr

        -- send ready
        liftIO $ putMVar ready ()

        -- handle all messages
        let getSnapshot :: IO ByteString
            getSnapshot = liftIO . atomically $ readTVar routerC >>= \case
                            Nothing -> retry
                            Just x -> return x

        forever $ do
            receiveMulti rout >>= \case
                [identity', req] -> when (req == iSTATE_REQUEST) $ do
                        snapshot <- liftIO getSnapshot
                        sendMulti rout [identity', snapshot]
                _ -> return ()

-- | A client receiving messages from a server.
--
-- This function will start reading messages on the
-- current thread.
--
-- It will connect to the server's PUB and ROUTER
-- ports. It will backlog messages from the PUB
-- socket, while requesting the initial state snapshot
-- from the ROUTER socket. Afterwards, it will forever
-- read from the PUB socket, and process all
-- in-sequence updates.
--
-- Note that this pattern is not 100% reliable. Messages might be dropped
-- between the initial state request and the first update.
client :: Binary a
       => String    -- ^ Address of the server's PUB socket
       -> String    -- ^ Address of the server's ROUTER socket
       -> MVar a    -- ^ CHannel where incoming messsages will be written to
       -> IO ()
client !pubAddr !routerAddr !chan = runZMQ $ do
    -- connect to both sockets
    sub <- socket Sub
    connect sub pubAddr
    subscribe sub ""

    dealer <- socket Dealer
    connect dealer routerAddr

    -- get inital snapshot
    send dealer [] iSTATE_REQUEST
    msg <- receive dealer
    let !env = decode (BL.fromStrict msg)
        !a = messageValue env
    liftIO $ putMVar chan a

    -- get remaining updates
    forever $ do
        msg' <- receive sub
        let !env' = decode (BL.fromStrict msg')
            !a' = messageValue env'

        when (messageVersion env' > messageVersion env) $
            liftIO $ putMVar chan a'


-- | Helper function
withAsync :: ZMQ z a -> (Async a -> ZMQ z b) -> ZMQ z b
withAsync action inner = mask $ \restore -> do
    a <- async (restore action)
    restore (inner a) `finally` liftIO (uninterruptibleCancel a)