{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ScopedTypeVariables #-} ----------------------------------------------------------------------------- -- | -- Module : Control.Distributed.Process.ManagedProcess.Client -- Copyright : (c) Tim Watson 2012 - 2017 -- License : BSD3 (see the file LICENSE) -- -- Maintainer : Tim Watson -- Stability : experimental -- Portability : non-portable (requires concurrency) -- -- The Client Portion of the /Managed Process/ API. ----------------------------------------------------------------------------- module Control.Distributed.Process.ManagedProcess.Client ( -- * API for client interactions with the process sendControlMessage , shutdown , call , safeCall , tryCall , callTimeout , flushPendingCalls , callAsync , cast , callChan , syncCallChan , syncSafeCallChan , callSTM ) where import Control.Concurrent.STM (atomically, STM) import Control.Distributed.Process hiding (call, finally) import Control.Distributed.Process.Serializable import Control.Distributed.Process.Async hiding (check) import Control.Distributed.Process.ManagedProcess.Internal.Types hiding (liftIO) import qualified Control.Distributed.Process.ManagedProcess.Internal.Types as T import Control.Distributed.Process.Extras.Internal.Types (resolveOrDie) import Control.Distributed.Process.Extras hiding (monitor, sendChan) import Control.Distributed.Process.Extras.Time import Control.Monad.Catch (finally) import Data.Maybe (fromJust) import Prelude hiding (init) -- | Send a control message over a 'ControlPort'. -- sendControlMessage :: Serializable m => ControlPort m -> m -> Process () sendControlMessage cp m = sendChan (unPort cp) (CastMessage m) -- | Send a signal instructing the process to terminate. The /receive loop/ which -- manages the process mailbox will prioritise @Shutdown@ signals higher than -- any other incoming messages, but the server might be busy (i.e., still in the -- process of excuting a handler) at the time of sending however, so the caller -- should not make any assumptions about the timeliness with which the shutdown -- signal will be handled. If responsiveness is important, a better approach -- might be to send an /exit signal/ with 'Shutdown' as the reason. An exit -- signal will interrupt any operation currently underway and force the running -- process to clean up and terminate. shutdown :: ProcessId -> Process () shutdown pid = cast pid Shutdown -- | Make a synchronous call - will block until a reply is received. -- The calling process will exit with 'ExitReason' if the calls fails. -- -- __NOTE: this function does not catch exceptions!__ -- call :: forall s a b . (Addressable s, Serializable a, Serializable b) => s -> a -> Process b call sid msg = initCall sid msg >>= waitResponse Nothing >>= decodeResult where decodeResult (Just (Right r)) = return r decodeResult (Just (Left err)) = die err decodeResult Nothing {- the impossible happened -} = terminate -- | Safe version of 'call' that returns information about the error -- if the operation fails. If the calling process dies (that is, forces itself -- to exit such that an exit signal arises with @ExitOther String@) then -- evaluation will return @Left exitReason@ and the explanation will be -- stashed away as @(ExitOther String)@. -- -- __NOTE: this function does not catch exceptions!__ -- -- The /safety/ of the name, comes from carefully handling situations in which -- the server dies while we're waiting for a reply. Notably, exit signals from -- other processes, kill signals, and both synchronous and asynchronous -- exceptions can still terminate the caller abruptly. To avoid this consider -- masking or evaluating within your own exception handling code. -- safeCall :: forall s a b . (Addressable s, Serializable a, Serializable b) => s -> a -> Process (Either ExitReason b) safeCall s m = do us <- getSelfPid (fmap fromJust (initCall s m >>= waitResponse Nothing) :: Process (Either ExitReason b)) `catchesExit` [(\pid msg -> handleMessageIf msg (weFailed pid us) (return . Left))] where weFailed a b (ExitOther _) = a == b weFailed _ _ _ = False -- | Version of 'safeCall' that returns 'Nothing' if the operation fails. If -- you need information about *why* a call has failed then you should use -- 'safeCall' or combine @catchExit@ and @call@ instead. -- -- __NOTE: this function does not catch exceptions!__ -- -- In fact, this API handles fewer exceptions than it's relative, "safeCall". -- Notably, exit signals, kill signals, and both synchronous and asynchronous -- exceptions can still terminate the caller abruptly. To avoid this consider -- masking or evaluating within your own exception handling code (as mentioned -- above). -- tryCall :: forall s a b . (Addressable s, Serializable a, Serializable b) => s -> a -> Process (Maybe b) tryCall s m = initCall s m >>= waitResponse Nothing >>= decodeResult where decodeResult (Just (Right r)) = return $ Just r decodeResult _ = return Nothing -- | Make a synchronous call, but timeout and return @Nothing@ if a reply -- is not received within the specified time interval. -- -- If the result of the call is a failure (or the call was cancelled) then -- the calling process will exit, with the 'ExitReason' given as the reason. -- If the call times out however, the semantics on the server side are -- undefined, i.e., the server may or may not successfully process the -- request and may (or may not) send a response at a later time. From the -- callers perspective, this is somewhat troublesome, since the call result -- cannot be decoded directly. In this case, the "flushPendingCalls" API /may/ -- be used to attempt to receive the message later on, however this makes -- /no attempt whatsoever/ to guarantee /which/ call response will in fact -- be returned to the caller. In those semantics are unsuited to your -- application, you might choose to @exit@ or @die@ in case of a timeout, -- or alternatively, use the 'callAsync' API and associated @waitTimeout@ -- function (in the /Async API/), which takes a re-usable handle on which -- to wait (with timeouts) multiple times. -- callTimeout :: forall s a b . (Addressable s, Serializable a, Serializable b) => s -> a -> TimeInterval -> Process (Maybe b) callTimeout s m d = initCall s m >>= waitResponse (Just d) >>= decodeResult where decodeResult :: (Serializable b) => Maybe (Either ExitReason b) -> Process (Maybe b) decodeResult Nothing = return Nothing decodeResult (Just (Right result)) = return $ Just result decodeResult (Just (Left reason)) = die reason -- | Attempt to flush out any pending call responses. flushPendingCalls :: forall b . (Serializable b) => TimeInterval -> (b -> Process b) -> Process (Maybe b) flushPendingCalls d proc = receiveTimeout (asTimeout d) [ match (\(CallResponse (m :: b) _) -> proc m) ] -- | Invokes 'call' /out of band/, and returns an /async handle/. -- callAsync :: forall s a b . (Addressable s, Serializable a, Serializable b) => s -> a -> Process (Async b) callAsync server msg = async $ task $ call server msg -- | Sends a /cast/ message to the server identified by @server@. The server -- will not send a response. Like Cloud Haskell's 'send' primitive, cast is -- fully asynchronous and /never fails/ - therefore 'cast'ing to a non-existent -- (e.g., dead) server process will not generate an error. -- cast :: forall a m . (Addressable a, Serializable m) => a -> m -> Process () cast server msg = sendTo server (CastMessage msg :: T.Message m ()) -- | Sends a /channel/ message to the server and returns a @ReceivePort@ on -- which the reponse can be delivered, if the server so chooses (i.e., the -- might ignore the request or crash). callChan :: forall s a b . (Addressable s, Serializable a, Serializable b) => s -> a -> Process (ReceivePort b) callChan server msg = do (sp, rp) <- newChan sendTo server (ChanMessage msg sp :: T.Message a b) return rp -- | A synchronous version of 'callChan'. syncCallChan :: forall s a b . (Addressable s, Serializable a, Serializable b) => s -> a -> Process b syncCallChan server msg = do r <- syncSafeCallChan server msg case r of Left e -> die e Right r' -> return r' -- | A safe version of 'syncCallChan', which returns @Left ExitReason@ if the -- call fails. syncSafeCallChan :: forall s a b . (Addressable s, Serializable a, Serializable b) => s -> a -> Process (Either ExitReason b) syncSafeCallChan server msg = do rp <- callChan server msg awaitResponse server [ matchChan rp (return . Right) ] -- | Manages an rpc-style interaction with a server process, using @STM@ actions -- to read/write data. The server process is monitored for the duration of the -- /call/. The stm write expression is passed the input, and the read expression -- is evaluated and the result given as @Right b@ or @Left ExitReason@ if a -- monitor signal is detected whilst waiting. -- -- Note that the caller will exit (with @ExitOther String@) if the server -- address is un-resolvable. -- -- A note about scheduling and timing guarantees (or lack thereof): It is not -- possibly to guarantee the contents of @ExitReason@ in cases where this API -- fails due to server exits/crashes. We establish a monitor prior to evaluating -- the stm writer action, however @monitor@ is asychronous and we've no way to -- know whether or not the scheduler will allow monitor establishment to proceed -- first, or the stm transaction. As a result, assuming that your server process -- can die/fail/exit on evaluating the read end of the STM write we perform here -- (and we assume this is very likely, since we apply no safety rules and do not -- even worry about serializing thunks passed from the client's thread), it is -- just as likely that in the case of failure you will see a reason such as -- @ExitOther "DiedUnknownId"@ due to the server process crashing before the node -- controller can establish a monitor. -- -- As unpleasant as this is, there's little we can do about it without making -- false assumptions about the runtime. Cloud Haskell's semantics guarantee us -- only that we will see /some/ monitor signal in the event of a failure here. -- To provide a more robust error handling, you can catch/trap failures in the -- server process and return a wrapper reponse datum here instead. This will -- /still/ be subject to the failure modes described above in cases where the -- server process exits abnormally, but that will at least allow the caller to -- differentiate between expected and exceptional failure conditions. -- callSTM :: forall s a b . (Addressable s) => s -> (a -> STM ()) -> STM b -> a -> Process (Either ExitReason b) callSTM server writeAction readAction input = do -- NB: we must establish the monitor before writing, to ensure we have -- a valid ref such that server failure gets reported properly pid <- resolveOrDie server "callSTM: unresolveable address " mRef <- monitor pid liftIO $ atomically $ writeAction input finally (receiveWait [ matchRef mRef , matchSTM readAction (return . Right) ]) (unmonitor mRef) where matchRef :: MonitorRef -> Match (Either ExitReason b) matchRef r = matchIf (\(ProcessMonitorNotification r' _ _) -> r == r') (\(ProcessMonitorNotification _ _ d) -> return (Left (ExitOther (show d))))