packages feed

network-rpca-0.0.1: src/Network/RPCA/Channel.hs

-- | A channel carries RPCs to a remote server
module Network.RPCA.Channel(
    ChannelStatus(..)
  , ErrorCode(..)
  , Channel
  , networkChannel
  , rpc
  , rpcAsync
  ) where

import Control.Concurrent.STM
import GHC.Conc
import Control.Timeout
import Control.Monad (when)

import Text.Printf (printf)

import Data.Word
import Data.Maybe (isJust, fromJust)

import Network.Socket hiding (send, sendTo, recv, recvFrom)

import qualified Data.ByteString as BS

import qualified Data.Map as Map
import qualified Data.Sequence as Seq

import Codec.Libevent.Class

import Network.RPCA.Structs
import Network.RPCA.Util
import qualified Network.RPCA.Connection as C

-- | These are the various states that a channel can be in
data ChannelStatus = Down  -- ^ channel is down. Requests will be enqueued
                   | Lame  -- ^ remote end has signaled that it's shutting down
                   | Connecting  -- ^ a connection is being attempted.
                                 --   Requests will be enqueued
                   | Up  -- ^ channel is ready. Requests will be sent to the transport

-- | This just factors out some common code from @rpc@ and @rpcAsync@
genericRPCCallback :: (TaggedStructure a)
                   => ((Either ErrorCode a) -> IO ())
                   -> Rpcreply
                   -> BS.ByteString
                   -> IO ()
genericRPCCallback cont reply payload =
 let
   replyCode = toEnum $ fromIntegral $ rpcreply_reply_code reply
 in
   if replyCode == ErrNone
      then case deserialise payload of
                Left _ -> cont $ Left ErrReplyPayloadParseFailed
                Right x -> cont $ Right x
      else cont $ Left replyCode

-- | Perform an asyncronous RPC call
rpcAsync :: (Channel c, TaggedStructure a, TaggedStructure b)
         => c  -- ^ the channel
         -> String  -- ^ the method name
         -> a  -- ^ the request arguments
         -> ((Either ErrorCode b) -> IO ())  -- ^ callback
         -> Float  -- ^ timeout
         -> IO ()
rpcAsync channel method request cb timeout = do
  nqueue channel (rpcrequestEmpty { rpcrequest_method = method }) (serialise request) (Just timeout) $ genericRPCCallback cb

-- | Perform a syncronous RPC
rpc :: (Channel c, TaggedStructure a, TaggedStructure b)
    => c  -- ^ the channel
    -> String  -- ^ the method name
    -> a  -- ^ request arguments
    -> Float -- ^ timeout
    -> IO (Either ErrorCode b)
rpc channel method request timeout = do
  result <- atomically newEmptyTMVar
  nqueue channel (rpcrequestEmpty {rpcrequest_method = method }) (serialise request) Nothing $ genericRPCCallback (atomically . putTMVar result)
  atomically (readTMVar result) >>= return

-- | This is a channel over which RPCs can be submitted
class Channel c where
  -- | Add an RPC to the outbound queue
  nqueue :: c  -- ^ the channel
         -> Rpcrequest  -- ^ the request - the id and service is filled in by the channel
         -> BS.ByteString  -- ^ payload
         -> Maybe Float  -- ^ timeout in seconds
         -> (Rpcreply -> BS.ByteString -> IO ())  -- ^ callback
         -> IO ()

  -- | Get the current status of the channel
  channelStatus :: c -> TVar ChannelStatus

-- | A simple network channel which tries to maintain a connection to a
--   hostname:port pair.
data NetworkChannel = NetworkChannel
  { ncid :: TVar Word32  -- ^ the next RPC id
  , ncservice :: String  -- ^ the target service name
  , nchost :: String  -- ^ the target host
  , ncport :: Int  -- ^ the target port number
  , ncoutq :: TVar (Seq.Seq (BS.ByteString, BS.ByteString))
    -- ^ requests waiting for a connection
  , ncdispatch :: TVar (Map.Map Word32 (Rpcreply -> BS.ByteString -> IO ()))
    -- ^ the dispatch table for incomming replies. Maps the RPC id to the
    --   handler function
  , nctimeouts :: TVar (Map.Map Word32 TimeoutTag)
    -- ^ maps RPC id to the timeout tag that we can use to cancel the timeout
    --   when the reply comes in
  , ncstatus :: TVar ChannelStatus  -- ^ the status of this channel
  , ncdead :: TVar Bool  -- ^ if set, the channel is to be shutdown
  , ncconn :: TVar C.Connection }

-- | This is the "reading" thread for the network connection. If it throws an
--   exception the connection is shutdown and we end up in closeConnection
readAction :: Socket -> NetworkChannel -> IO ()
readAction socket nc = do
  atomically $ writeTVar (ncstatus nc) Connecting
  hostaddr <- inet_addr $ nchost nc
  print "Connecting"
  connect socket (SockAddrInet (PortNum $ htons $ fromIntegral $ ncport nc) hostaddr)

  -- Set the status and drain the outbound queue
  (q, conn) <- atomically (do
    writeTVar (ncstatus nc) Up  -- TODO: maybe Lame?
    q <- readTVar $ ncoutq nc
    writeTVar (ncoutq nc) Seq.empty
    conn <- readTVar $ ncconn nc
    return (q, conn))

  atomically $ mapM (C.writePacket conn) $ seqToList q

  readReply nc socket

-- | This also runs in the read thread of the connection and loops forever,
--   reading replies from the network and processing them.
readReply :: NetworkChannel -> Socket -> IO ()
readReply nc socket = do
  (a, payload) <- C.readPacket socket
  let mibreply = inboundreplyDeserialiseBS a
  case mibreply of
    Left _ -> error "Protocol error"
    Right ibreply -> do
      -- Lookup the id in the dispatch table, removing it if found. Also,
      -- possibly cancel a timeout linked to the RPC
      mcb <- atomically (do
        dispatch <- readTVar $ ncdispatch nc
        timeouts <- readTVar $ nctimeouts nc

        let id = inboundreply_id ibreply
        let mtimeoutTag = Map.lookup id timeouts

        -- If we have a timeout, cancel it and remove it from our map
        when (isJust mtimeoutTag) (do
          cancelTimeout $ fromJust mtimeoutTag
          writeTVar (nctimeouts nc) $ Map.delete id timeouts)

        case Map.lookup id dispatch of
             Nothing -> return Nothing
             Just cb -> do
               writeTVar (ncdispatch nc) $ Map.delete id dispatch
               return $ Just cb)
      case mcb of
           Nothing -> printf "RPC reply id:%d\n" $ ((fromIntegral $ inboundreply_id ibreply) :: Int)
           Just _ -> return ()

      case mcb of
           Nothing -> printf "Unknown RPC reply" >> readReply nc socket
           Just cb -> do
             case inboundreply_rpc ibreply of
               Nothing -> error "No RPC reply in reply"
               Just rpcreply -> do
                 cb rpcreply payload
                 readReply nc socket

-- | This is called by the Connection when the connection fails. If this
--   channel has been shutdown then we have nothing else to do as the
--   Connection will close the socket and kill the threads. Otherwise, we
--   sleep, make a new socket and retry the connection
closeAction :: NetworkChannel -> IO ()
closeAction nc = do
  (dead, cbs) <- atomically (do
    writeTVar (ncstatus nc) Down
    cbs <- readTVar (ncdispatch nc) >>= return . Map.elems
    timeoutTags <- readTVar (nctimeouts nc) >>= return . Map.elems
    mapM_ cancelTimeout timeoutTags
    writeTVar (ncdispatch nc) Map.empty
    writeTVar (nctimeouts nc) Map.empty
    dead <- readTVar $ ncdead nc
    return (dead, cbs))

  -- fail all enqueued RPCs
  mapM_ (abortRPC ErrTransportFailed) cbs

  if dead
     then return ()
     else do
       threadDelay 1000000
       sock <- socket AF_INET Stream 0
       setSocketOption sock NoDelay 1
       conn <- atomically (do
         conn <- C.new sock $ closeAction nc
         writeTVar (ncconn nc) conn
         return conn)
       C.forkThreads conn $ readAction sock nc

-- | Create a new networkChannel
networkChannel :: String  -- ^ service name
               -> String  -- ^ hostname
               -> Int  -- ^ port number
               -> IO NetworkChannel
networkChannel service host port = do
  sock <- socket AF_INET Stream 0
  setSocketOption sock NoDelay 1
  c <- atomically (do ncid <- newTVar 0
                      ncdispatch <- newTVar Map.empty
                      nctimeouts <- newTVar Map.empty
                      ncstatus <- newTVar Down
                      ncoutq <- newTVar Seq.empty
                      ncdead <- newTVar False
                      ncconn <- newTVar undefined
                      let c = NetworkChannel ncid service host port ncoutq ncdispatch nctimeouts ncstatus ncdead ncconn
                      conn <- C.new sock $ closeAction c
                      writeTVar ncconn conn
                      return c)
  conn <- atomically $ readTVar $ ncconn c
  C.forkThreads conn $ readAction sock c
  return c

-- | Run the given callback with a constructued Rpcreply object which has the
--   correct error code for the given error.
abortRPC :: ErrorCode  -- ^ the error code to give
         -> (Rpcreply -> BS.ByteString -> IO ())  -- ^ the callback
         -> IO ()
abortRPC err cb = do
  -- make up the reply with the correct error code and run the callback
  let rpcreply = rpcreplyEmpty { rpcreply_reply_code = fromIntegral $ fromEnum err }
  cb rpcreply BS.empty

handleTimeout :: NetworkChannel  -- ^ the channel which contains the request
              -> Word32  -- ^ the id which has timed out
              -> IO ()
handleTimeout nc id = do
  -- remove our timeout tag from the map of timeouts and return the callback
  mcb <- atomically (do
    updateTVar (Map.delete id) $ nctimeouts nc
    updateTVar (Map.delete id) (ncdispatch nc) >>= return . Map.lookup id)
  case mcb of
       Nothing -> return ()
       Just cb -> abortRPC ErrTimeout cb

instance Channel NetworkChannel where
  channelStatus = ncstatus

  nqueue nc rpcreq payload mtimeout cb = do
    -- Get an ID number for this RPC
    id <- atomically $ updateTVar ((+) 1) $ ncid nc

    -- If we have a timeout, do the IO preparation now
    timeoutTag <-
      case mtimeout of
           Nothing -> return Nothing
           Just timeout -> addTimeoutAtomic timeout (handleTimeout nc id) >>= return . Just

    -- Update all the bookkeeping
    atomically (do
      let rpcreq' = rpcreq { rpcrequest_service = ncservice nc }
      updateTVar (Map.insert id cb) $ ncdispatch nc
      -- if we have a timeout, run the STM action to enable it and record the
      -- tag in case we want to cancel it.
      when (isJust timeoutTag) (do
        tag <- fromJust timeoutTag
        updateTVar (Map.insert id tag) $ nctimeouts nc
        return ())

      -- build the outgoing request
      let obreq = outboundrequestEmpty { outboundrequest_id = id
                                       , outboundrequest_rpc = Just rpcreq' }
          obreqbytes = outboundrequestSerialiseBS obreq

      -- depending on the channel status we either enqueue the request here or
      -- in the Connection. (If the Connection is still connecting etc, then
      -- enqueuing in the Connection will result in it getting dropped if the
      -- connection fails etc)
      status <- readTVar $ channelStatus nc
      case status of
        Up -> do
          conn <- readTVar $ ncconn nc
          C.writePacket conn (obreqbytes, payload)
        _ -> do
          updateTVar ((Seq.<|) (obreqbytes, payload)) $ ncoutq nc
          return ())