concurrent-rpc-0.1.0.0: src/Control/Concurrent/RPC.hs
-----------------------------------------------------------------------------
-- |
-- Module : Control.Concurrent.RPC
-- Copyright : (c) Lars Petersen 2016
-- License : MIT
--
-- Maintainer : info@lars-petersen.net
-- Stability : experimental
--
-- This module offers the single method `newRPC` which creates a typed
-- communication channel. Give the one end of the channel to client threads
-- and the other end to worker threads.
--
-- Also see https://gist.github.com/lpeterse/da7b574da5c7a0dc9794 for an example.
-----------------------------------------------------------------------------
module Control.Concurrent.RPC where
import Control.Exception
import Control.Concurrent.MVar
-- | The client interface for threads invoking /remote procedure calls/.
--
-- * The operation blocks until another thread processed the request.
-- * If the other thread throws an exception during processing, the exception
-- is re-thrown in the thread waiting for the response.
--
-- > type Rocket = String
-- > type Liftoff = Bool
-- >
-- > main = do
-- > (launchRocket, withRocket) <- newRPC :: IO (RPC Rocket Liftoff, WithRPC Rocket Liftoff)
-- > ..
-- > catch
-- > ( launchRocket "Apollo 11" >>= \liftoff-> if liftoff
-- > then print "Houston, we have a liftoff!"
-- > else print "Launch cancelled!"
-- > )
-- > ( \e-> print $ "Houston, we have a problem: " ++ show (e :: IOError) )
type RPC request response = request -> IO response
-- | The interface for threads that serve and process /remote procedure calls/.
--
-- * More than one thread may be used to process requests all using the same
-- interface object. All processing threads will block on `Control.Concurrent.take`
-- on the same `MVar` and only one will be served at a time. Fairness
-- properties of `Control.Concurrent.MVar` apply.
-- * Exceptions thrown by the handler operation will be re-thrown in both
-- the processing thread and the requesting thread.
--
-- > type Rocket = String
-- > type Liftoff = Bool
-- >
-- > main = do
-- > (launchRocket, withRocket) <- newRPC :: IO (RPC Rocket Liftoff, WithRPC Rocket Liftoff)
-- > ..
-- > -- This is the rocket launch site thread. It forever waits for rockets and fires them into space one after the other.
-- > forkIO $ withFile "/dev/null" WriteMode $ \space->
-- > forever $ catch
-- > ( withRocket $ \rocket-> do
-- > weather <- getWeatherCondition
-- > when (isGood weather) $
-- > hPutStrLn space rocket -- The actual launch may throw an IOError!
-- > return weather
-- > )
-- > ( \e-> print "A rocket exploded during launch phase: " ++ (e :: IOError) )
type WithRPC request response = (request -> IO response) -> IO ()
-- | Creates a new request-response communication channel that may be used
-- by arbitrary many requesting and/or processing threads.
--
-- > main :: IO ()
-- > main = do
-- > (rpc, withRpc) <- newRPC
-- > forkIO $ forever $ withRpc $ \request->
-- > response <- doSomethingWith request
-- > return response
-- > response <- rpc request
-- > ..
--
-- * `newRPC` initially creates one empty `Control.Concurrent.MVar.MVar`
-- for queueing requests.
-- * Each call of `rpc` creates a temporary `Control.Concurrent.MVar` for the
-- reponse.
-- * If the handler given to `withRpc` throws an exception, the exception is
-- re-thrown in the `withRpc` thread as well as in the `rpc` thread that
-- issued the call.
-- * If an `rpc` thread that issued a call dies before the request
-- processing has started, the request gets discarded. If processing has
-- already started, the processing will finish and the response gets discarded.
newRPC :: IO (RPC request response, WithRPC request response)
newRPC = do
requestMVar <- newEmptyMVar
return (createRequest requestMVar, processRequest requestMVar)
where
createRequest requestMVar request = do
responseMVar <- newEmptyMVar
putMVar requestMVar (request, responseMVar)
response <- readMVar responseMVar
case response of
Left e -> throwIO (e :: SomeException)
Right result -> return result
processRequest requestMVar process = do
(request, responseMVar) <- takeMVar requestMVar
response <- try (process request)
putMVar responseMVar response
case response of
Left e -> throwIO (e :: SomeException)
Right _ -> return ()