{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} ----------------------------------------------------------------------------- -- | -- Module : Control.Distributed.Process.ManagedProcess.Server.Restricted -- Copyright : (c) Tim Watson 2012 - 2013 -- License : BSD3 (see the file LICENSE) -- -- Maintainer : Tim Watson <watson.timothy@gmail.com> -- Stability : experimental -- Portability : non-portable (requires concurrency) -- -- A /safe/ variant of the Server Portion of the /Managed Process/ API. Most -- of these operations have the same names as similar operations in the impure -- @Server@ module (re-exported by the primary API in @ManagedProcess@). To -- remove the ambiguity, some combination of either qualification and/or the -- @hiding@ clause will be required. -- -- [Restricted Server Callbacks] -- -- The idea behind this module is to provide /safe/ callbacks, i.e., server -- code that is free from side effects. This safety is enforced by the type -- system via the @RestrictedProcess@ monad. A StateT interface is provided -- for code running in the @RestrictedProcess@ monad, so that server side -- state can be managed safely without resorting to IO (or code running in -- the @Process@ monad). -- ----------------------------------------------------------------------------- module Control.Distributed.Process.ManagedProcess.Server.Restricted ( -- * Exported Types RestrictedProcess , Result(..) , RestrictedAction(..) -- * Creating call/cast protocol handlers , handleCall , handleCallIf , handleCast , handleCastIf , handleInfo , handleExit , handleTimeout -- * Handling Process State , putState , getState , modifyState -- * Handling responses/transitions , reply , noReply , haltNoReply , continue , timeoutAfter , hibernate , stop -- * Utilities , say ) where import Control.Applicative (Applicative) import Control.Distributed.Process hiding (call, say) import qualified Control.Distributed.Process as P (say) import Control.Distributed.Process.Extras (ExitReason(..)) import Control.Distributed.Process.ManagedProcess.Internal.Types import qualified Control.Distributed.Process.ManagedProcess.Server as Server import Control.Distributed.Process.Extras.Time import Control.Distributed.Process.Serializable import Prelude hiding (init) import Control.Monad.IO.Class (MonadIO) import qualified Control.Monad.State as ST ( MonadState , MonadTrans , StateT , get , lift , modify , put , runStateT ) import Data.Typeable -- | Restricted (i.e., pure, free from side effects) execution -- environment for call/cast/info handlers to execute in. -- newtype RestrictedProcess s a = RestrictedProcess { unRestricted :: ST.StateT s Process a } deriving (Functor, Monad, ST.MonadState s, MonadIO, Typeable, Applicative) -- | The result of a 'call' handler's execution. data Result a = Reply a -- ^ reply with the given term | Timeout Delay a -- ^ reply with the given term and enter timeout | Hibernate TimeInterval a -- ^ reply with the given term and hibernate | Stop ExitReason -- ^ stop the process with the given reason deriving (Typeable) -- | The result of a safe 'cast' handler's execution. data RestrictedAction = RestrictedContinue -- ^ continue executing | RestrictedTimeout Delay -- ^ timeout if no messages are received | RestrictedHibernate TimeInterval -- ^ hibernate (i.e., sleep) | RestrictedStop ExitReason -- ^ stop/terminate the server process -------------------------------------------------------------------------------- -- Handling state in RestrictedProcess execution environments -- -------------------------------------------------------------------------------- -- | Log a trace message using the underlying Process's @say@ say :: String -> RestrictedProcess s () say msg = lift . P.say $ msg -- | Get the current process state getState :: RestrictedProcess s s getState = ST.get -- | Put a new process state state putState :: s -> RestrictedProcess s () putState = ST.put -- | Apply the given expression to the current process state modifyState :: (s -> s) -> RestrictedProcess s () modifyState = ST.modify -------------------------------------------------------------------------------- -- Generating replies and state transitions inside RestrictedProcess -- -------------------------------------------------------------------------------- -- | Instructs the process to send a reply and continue running. reply :: forall s r . (Serializable r) => r -> RestrictedProcess s (Result r) reply = return . Reply -- | Continue without giving a reply to the caller - equivalent to 'continue', -- but usable in a callback passed to the 'handleCall' family of functions. noReply :: forall s r . (Serializable r) => Result r -> RestrictedProcess s (Result r) noReply r = return r -- | Halt process execution during a call handler, without paying any attention -- to the expected return type. haltNoReply :: forall s r . (Serializable r) => ExitReason -> RestrictedProcess s (Result r) haltNoReply r = noReply (Stop r) -- | Instructs the process to continue running and receiving messages. continue :: forall s . RestrictedProcess s RestrictedAction continue = return RestrictedContinue -- | Instructs the process loop to wait for incoming messages until 'Delay' -- is exceeded. If no messages are handled during this period, the /timeout/ -- handler will be called. Note that this alters the process timeout permanently -- such that the given @Delay@ will remain in use until changed. timeoutAfter :: forall s. Delay -> RestrictedProcess s RestrictedAction timeoutAfter d = return $ RestrictedTimeout d -- | Instructs the process to /hibernate/ for the given 'TimeInterval'. Note -- that no messages will be removed from the mailbox until after hibernation has -- ceased. This is equivalent to evaluating @liftIO . threadDelay@. -- hibernate :: forall s. TimeInterval -> RestrictedProcess s RestrictedAction hibernate d = return $ RestrictedHibernate d -- | Instructs the process to terminate, giving the supplied reason. If a valid -- 'shutdownHandler' is installed, it will be called with the 'ExitReason' -- returned from this call, along with the process state. stop :: forall s. ExitReason -> RestrictedProcess s RestrictedAction stop r = return $ RestrictedStop r -------------------------------------------------------------------------------- -- Wrapping handler expressions in Dispatcher and DeferredDispatcher -- -------------------------------------------------------------------------------- -- | A version of "Control.Distributed.Process.ManagedProcess.Server.handleCall" -- that takes a handler which executes in 'RestrictedProcess'. -- handleCall :: forall s a b . (Serializable a, Serializable b) => (a -> RestrictedProcess s (Result b)) -> Dispatcher s handleCall = handleCallIf $ Server.state (const True) -- | A version of "Control.Distributed.Process.ManagedProcess.Server.handleCallIf" -- that takes a handler which executes in 'RestrictedProcess'. -- handleCallIf :: forall s a b . (Serializable a, Serializable b) => (Condition s a) -> (a -> RestrictedProcess s (Result b)) -> Dispatcher s handleCallIf cond h = Server.handleCallIf cond (wrapCall h) -- | A version of "Control.Distributed.Process.ManagedProcess.Server.handleCast" -- that takes a handler which executes in 'RestrictedProcess'. -- handleCast :: forall s a . (Serializable a) => (a -> RestrictedProcess s RestrictedAction) -> Dispatcher s handleCast = handleCastIf (Server.state (const True)) -- | A version of "Control.Distributed.Process.ManagedProcess.Server.handleCastIf" -- that takes a handler which executes in 'RestrictedProcess'. -- handleCastIf :: forall s a . (Serializable a) => Condition s a -- ^ predicate that must be satisfied for the handler to run -> (a -> RestrictedProcess s RestrictedAction) -- ^ an action yielding function over the process state and input message -> Dispatcher s handleCastIf cond h = Server.handleCastIf cond (wrapHandler h) -- | A version of "Control.Distributed.Process.ManagedProcess.Server.handleInfo" -- that takes a handler which executes in 'RestrictedProcess'. -- handleInfo :: forall s a. (Serializable a) => (a -> RestrictedProcess s RestrictedAction) -> DeferredDispatcher s -- cast and info look the same to a restricted process handleInfo h = Server.handleInfo (wrapHandler h) handleExit :: forall s a. (Serializable a) => (a -> RestrictedProcess s RestrictedAction) -> ExitSignalDispatcher s handleExit h = Server.handleExit $ \s _ a -> (wrapHandler h) s a handleTimeout :: forall s . (Delay -> RestrictedProcess s RestrictedAction) -> TimeoutHandler s handleTimeout h = \s d -> do (r, s') <- runRestricted s (h d) case r of RestrictedContinue -> Server.continue s' (RestrictedTimeout i) -> Server.timeoutAfter i s' (RestrictedHibernate i) -> Server.hibernate i s' (RestrictedStop r') -> Server.stop r' -------------------------------------------------------------------------------- -- Implementation -- -------------------------------------------------------------------------------- wrapHandler :: forall s a . (Serializable a) => (a -> RestrictedProcess s RestrictedAction) -> s -> a -> Process (ProcessAction s) wrapHandler h s a = do (r, s') <- runRestricted s (h a) case r of RestrictedContinue -> Server.continue s' (RestrictedTimeout i) -> Server.timeoutAfter i s' (RestrictedHibernate i) -> Server.hibernate i s' (RestrictedStop r') -> Server.stop r' wrapCall :: forall s a b . (Serializable a, Serializable b) => (a -> RestrictedProcess s (Result b)) -> s -> a -> Process (ProcessReply b s) wrapCall h s a = do (r, s') <- runRestricted s (h a) case r of (Reply r') -> Server.reply r' s' (Timeout i r') -> Server.timeoutAfter i s' >>= Server.replyWith r' (Hibernate i r') -> Server.hibernate i s' >>= Server.replyWith r' (Stop r'' ) -> Server.stop r'' >>= Server.noReply runRestricted :: s -> RestrictedProcess s a -> Process (a, s) runRestricted state proc = ST.runStateT (unRestricted proc) state -- | TODO MonadTrans instance? lift :: (Monad m) => m a -> t m a lift :: Process a -> RestrictedProcess s a lift p = RestrictedProcess $ ST.lift p