packages feed

distributed-process-execution (empty) → 0.1.0

raw patch · 14 files changed

+2852/−0 lines, 14 filesdep +HUnitdep +QuickCheckdep +ansi-terminalsetup-changed

Dependencies added: HUnit, QuickCheck, ansi-terminal, base, binary, bytestring, containers, data-accessor, deepseq, derive, distributed-process, distributed-process-client-server, distributed-process-execution, distributed-process-extras, distributed-process-supervisor, distributed-static, fingertree, ghc-prim, hashable, mtl, network, network-transport, network-transport-tcp, rematch, stm, template-haskell, test-framework, test-framework-hunit, test-framework-quickcheck2, time, transformers, uniplate, unordered-containers

Files

+ LICENCE view
@@ -0,0 +1,30 @@+Copyright Tim Watson, 2012-2013.++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of the author nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.lhs view
@@ -0,0 +1,3 @@+#!/usr/bin/env runhaskell+> import Distribution.Simple+> main = defaultMain
+ distributed-process-execution.cabal view
@@ -0,0 +1,146 @@+name:           distributed-process-execution+version:        0.1.0+cabal-version:  >=1.8+build-type:     Simple+license:        BSD3+license-file:   LICENCE+stability:      experimental+Copyright:      Tim Watson 2012 - 2013+Author:         Tim Watson+Maintainer:     watson.timothy@gmail.com+Stability:      experimental+Homepage:       http://github.com/haskell-distributed/distributed-process-execution+Bug-Reports:    http://github.com/haskell-distributed/distributed-process-execution/issues+synopsis:       Execution Framework for The Cloud Haskell Application Platform+description:    +		The Execution Framework provides tools for load regulation, workload shedding and remote hand-off.+		The currently implementation provides only a subset of the plumbing required, comprising tools+		for event management, mailbox buffering and message routing.+category:       Control+tested-with:    GHC == 7.4.2 GHC == 7.6.2+data-dir:       ""++source-repository head+  type:      git+  location:  https://github.com/haskell-distributed/distributed-process-execution++flag perf+  description: Build with profiling enabled+  default: False++library+  build-depends:+                   base >= 4.4 && < 5,+                   data-accessor >= 0.2.2.3,+                   distributed-process >= 0.5.2 && < 0.6,+                   distributed-process-extras >= 0.1.1 && < 0.2,+                   distributed-process-supervisor >= 0.1.1 && < 0.2,+                   distributed-process-client-server >= 0.1.1 && < 0.2,+                   binary >= 0.6.3.0 && < 0.8,+                   deepseq >= 1.3.0.1 && < 1.4,+                   mtl,+                   containers >= 0.4 && < 0.6,+                   hashable >= 1.2.0.5 && < 1.3,+                   unordered-containers >= 0.2.3.0 && < 0.3,+                   fingertree < 0.2,+                   stm >= 2.4 && < 2.5,+                   time > 1.4 && < 1.5,+                   transformers+  if impl(ghc <= 7.5)+    Build-Depends:   template-haskell == 2.7.0.0,+                     derive == 2.5.5,+                     uniplate == 1.6.12,+                     ghc-prim+  extensions:      CPP+  hs-source-dirs:   src+  ghc-options:      -Wall+  exposed-modules:+                   Control.Distributed.Process.Execution,+                   Control.Distributed.Process.Execution.EventManager,+                   Control.Distributed.Process.Execution.Exchange,+                   Control.Distributed.Process.Execution.Mailbox+  other-modules:+                   Control.Distributed.Process.Execution.Exchange.Broadcast,+                   Control.Distributed.Process.Execution.Exchange.Internal,+                   Control.Distributed.Process.Execution.Exchange.Router+++test-suite ExchangeTests+  type:            exitcode-stdio-1.0+--  x-uses-tf:       true+  build-depends:+                   base >= 4.4 && < 5,+                   ansi-terminal >= 0.5 && < 0.7,+                   containers,+                   hashable,+                   unordered-containers >= 0.2.3.0 && < 0.3,+                   distributed-process >= 0.5.2 && < 0.6,+                   distributed-process-execution,+                   distributed-process-extras >= 0.1.1 && < 0.2,+                   distributed-static,+                   bytestring,+                   data-accessor,+                   fingertree < 0.2,+                   network-transport >= 0.4 && < 0.5,+                   deepseq >= 1.3.0.1 && < 1.4,+                   mtl,+                   network-transport-tcp >= 0.4 && < 0.5,+                   binary >= 0.6.3.0 && < 0.8,+                   network >= 2.3 && < 2.7,+                   HUnit >= 1.2 && < 2,+                   stm >= 2.3 && < 2.5,+                   time > 1.4 && < 1.5,+                   test-framework >= 0.6 && < 0.9,+                   test-framework-hunit,+                   QuickCheck >= 2.4,+                   test-framework-quickcheck2,+                   transformers,+                   rematch >= 0.2.0.0,+                   ghc-prim+  hs-source-dirs:+                   tests+  ghc-options:     -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind -eventlog+  extensions:      CPP+  main-is:         TestExchange.hs+  other-modules:   TestUtils+++test-suite MailboxTests+  type:            exitcode-stdio-1.0+--  x-uses-tf:       true+  build-depends:+                   base >= 4.4 && < 5,+                   ansi-terminal >= 0.5 && < 0.7,+                   containers,+                   hashable,+                   unordered-containers >= 0.2.3.0 && < 0.3,+                   distributed-process >= 0.5.2 && < 0.6,+                   distributed-process-execution,+                   distributed-process-extras >= 0.1.1 && < 0.2,+                   distributed-static,+                   bytestring,+                   data-accessor,+                   fingertree < 0.2,+                   network-transport >= 0.4 && < 0.5,+                   deepseq >= 1.3.0.1 && < 1.4,+                   mtl,+                   network-transport-tcp >= 0.4 && < 0.5,+                   binary >= 0.6.3.0 && < 0.8,+                   network >= 2.3 && < 2.7,+                   HUnit >= 1.2 && < 2,+                   stm >= 2.3 && < 2.5,+                   time > 1.4 && < 1.5,+                   test-framework >= 0.6 && < 0.9,+                   test-framework-hunit,+                   QuickCheck >= 2.4,+                   test-framework-quickcheck2,+                   transformers,+                   rematch >= 0.2.0.0,+                   ghc-prim+  hs-source-dirs:+                   tests+  ghc-options:     -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind -eventlog+  extensions:      CPP+  main-is:         TestMailbox.hs+  other-modules:   MailboxTestFilters+                   TestUtils
+ src/Control/Distributed/Process/Execution.hs view
@@ -0,0 +1,44 @@+-----------------------------------------------------------------------------+-- |+-- Module      :  Control.Distributed.Process.Execution+-- Copyright   :  (c) Tim Watson 2013 - 2014+-- License     :  BSD3 (see the file LICENSE)+--+-- Maintainer  :  Tim Watson <watson.timothy@gmail.com>+-- Stability   :  experimental+-- Portability :  non-portable (requires concurrency)+--+-- [Inter-Process Traffic Management]+--+-- The /Execution Framework/ provides tools for load regulation, workload+-- shedding and remote hand-off. The currently implementation provides only+-- a subset of the plumbing required, comprising tools for event management,+-- mailbox buffering and message routing.+--+-----------------------------------------------------------------------------++module Control.Distributed.Process.Execution+  ( -- * Mailbox Buffering+    module Control.Distributed.Process.Execution.Mailbox+    -- * Message Exchanges+  , module Control.Distributed.Process.Execution.Exchange+  ) where++import Control.Distributed.Process.Execution.Exchange hiding (startSupervised)+import Control.Distributed.Process.Execution.Mailbox hiding (startSupervised, post)++{-++Load regulation requires that we apply limits to various parts of the system.+The manner in which they're applied may vary, but the mechanisms are limited+to:++1. rejecting the activity/request+2. accepting the activity immediately+3. blocking some or all requestors+4. blocking some (or all) activities+5. terminiating some (or all) activities++-}++
+ src/Control/Distributed/Process/Execution/EventManager.hs view
@@ -0,0 +1,153 @@+{-# LANGUAGE DeriveDataTypeable        #-}+{-# LANGUAGE DeriveGeneric             #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE RankNTypes                #-}+{-# LANGUAGE RecordWildCards           #-}+{-# LANGUAGE ScopedTypeVariables       #-}+{-# LANGUAGE TemplateHaskell           #-}+{-# LANGUAGE ImpredicativeTypes        #-}++-----------------------------------------------------------------------------+-- |+-- Module      :  Control.Distributed.Process.Execution.EventManager+-- Copyright   :  (c) Well-Typed / Tim Watson+-- License     :  BSD3 (see the file LICENSE)+--+-- Maintainer  :  Tim Watson <watson.timothy@gmail.com>+-- Stability   :  experimental+-- Portability :  non-portable (requires concurrency)+--+-- [Overview]+--+-- The /EventManager/ is a parallel/concurrent event handling tool, built on+-- top of the /Exchange API/. Arbitrary events are published to the event+-- manager using 'notify', and are broadcast simulataneously to a set of+-- registered /event handlers/.+--+-- [Defining and Registering Event Handlers]+--+-- Event handlers are defined as @Serializable m => s -> m -> Process s@,+-- i.e., an expression taking an initial state, an arbitrary @Serializable@+-- event/message and performing an action in the @Process@ monad that evaluates+-- to a new state.+--+-- See "Control.Distributed.Process.Execution.Exchange".+--+-----------------------------------------------------------------------------++module Control.Distributed.Process.Execution.EventManager+  ( EventManager+  , start+  , startSupervised+  , startSupervisedRef+  , notify+  , addHandler+  , addMessageHandler+  ) where++import Control.Distributed.Process hiding (Message, link)+import qualified Control.Distributed.Process as P (Message)+import Control.Distributed.Process.Execution.Exchange+  ( Exchange+  , Message(..)+  , post+  , broadcastExchange+  , broadcastExchangeT+  , broadcastClient+  )+import qualified Control.Distributed.Process.Execution.Exchange as Exchange+  ( startSupervised+  )+import Control.Distributed.Process.Extras.Internal.Primitives+import Control.Distributed.Process.Extras.Internal.Unsafe+  ( InputStream+  , matchInputStream+  )+import Control.Distributed.Process.Supervisor (SupervisorPid)+import Control.Distributed.Process.Serializable hiding (SerializableDict)+import Data.Binary+import Data.Typeable (Typeable)+import GHC.Generics++{- notes++Event manager is implemented over a simple BroadcastExchange. We eschew the+complexities of identifying handlers and allowing them to be removed/deleted+or monitored, since we avoid running them in the exchange process. Instead,+each handler runs as an independent process, leaving handler management up+to the user and allowing all the usual process managemnet techniques (e.g.,+registration, supervision, etc) to be utilised instead.++-}++-- | Opaque handle to an Event Manager.+--+newtype EventManager = EventManager { ex :: Exchange }+  deriving (Typeable, Generic)+instance Binary EventManager where++instance Resolvable EventManager where+  resolve = resolve . ex++-- | Start a new /Event Manager/ process and return an opaque handle+-- to it.+start :: Process EventManager+start = broadcastExchange >>= return . EventManager++startSupervised :: SupervisorPid -> Process EventManager+startSupervised sPid = do+  ex <- broadcastExchangeT >>= \t -> Exchange.startSupervised t sPid+  return $ EventManager ex++startSupervisedRef :: SupervisorPid -> Process (ProcessId, P.Message)+startSupervisedRef sPid = do+  ex <- startSupervised sPid+  Just pid <- resolve ex+  return (pid, unsafeWrapMessage ex)++-- | Broadcast an event to all registered handlers.+notify :: Serializable a => EventManager -> a -> Process ()+notify em msg = post (ex em) msg++-- | Add a new event handler. The handler runs in its own process,+-- which is spawned locally on behalf of the caller.+addHandler :: forall s a. Serializable a+           => EventManager+           -> (s -> a -> Process s)+           -> Process s+           -> Process ProcessId+addHandler m h s =+  spawnLocal $ newHandler (ex m) (\s' m' -> handleMessage m' (h s')) s++-- | As 'addHandler', but operates over a raw @Control.Distributed.Process.Message@.+addMessageHandler :: forall s.+                     EventManager+                  -> (s -> P.Message -> Process (Maybe s))+                  -> Process s+                  -> Process ProcessId+addMessageHandler m h s = spawnLocal $ newHandler (ex m) h s++newHandler :: forall s .+              Exchange+           -> (s -> P.Message -> Process (Maybe s))+           -> Process s+           -> Process ()+newHandler ex handler initState = do+  linkTo ex+  is <- broadcastClient ex+  listen is handler =<< initState++listen :: forall s . InputStream Message+       -> (s -> P.Message -> Process (Maybe s))+       -> s+       -> Process ()+listen inStream handler state = do+  receiveWait [ matchInputStream inStream ] >>= handleEvent inStream handler state+  where+    handleEvent is h s p = do+      r <- h s (payload p)+      let s2 = case r of+                 Nothing -> s+                 Just s' -> s'+      listen is h s2+
+ src/Control/Distributed/Process/Execution/Exchange.hs view
@@ -0,0 +1,149 @@+-----------------------------------------------------------------------------+-- |+-- Module      :  Control.Distributed.Process.Execution.Exchange+-- Copyright   :  (c) Tim Watson 2012 - 2014+-- License     :  BSD3 (see the file LICENSE)+--+-- Maintainer  :  Tim Watson <watson.timothy@gmail.com>+-- Stability   :  experimental+-- Portability :  non-portable (requires concurrency)+--+-- [Message Exchanges]+--+-- The concept of a /message exchange/ is borrowed from the world of+-- messaging and enterprise integration. The /exchange/ acts like a kind of+-- mailbox, accepting inputs from /producers/ and forwarding these messages+-- to one or more /consumers/, depending on the implementation's semantics.+--+-- This module provides some basic types of message exchange and exposes an API+-- for defining your own custom /exchange types/.+--+-- [Broadcast Exchanges]+--+-- The broadcast exchange type, started via 'broadcastExchange', forward their+-- inputs to all registered consumers (as the name suggests). This exchange type+-- is highly optimised for local (intra-node) traffic and provides two different+-- kinds of client binding, one which causes messages to be delivered directly+-- to the client's mailbox (viz 'bindToBroadcaster'), the other providing a+-- separate stream of messages that can be obtained using the @expect@ and+-- @receiveX@ family of messaging primitives (and thus composed with other forms+-- of input selection, such as typed channels and selective reads on the process+-- mailbox).+--+-- /Important:/ When a @ProcessId@ is registered via 'bindToBroadcaster', only+-- the payload of the 'Message' (i.e., the underlying @Serializable@ datum) is+-- forwarded to the consumer, /not/ the whole 'Message' itself.+--+-- [Router Exchanges]+--+-- The /router/ API provides a means to selectively route messages to one or+-- more clients, depending on the content of the 'Message'. Two modes of binding+-- (and client selection) are provided out of the box, one of which matches the+-- message 'key', the second of which matches on a name and value from the+-- 'headers'. Alternative mechanisms for content based routing can be derived+-- by modifying the 'BindingSelector' expression passed to 'router'+--+-- See 'messageKeyRouter' and 'headerContentRouter' for the built-in routing+-- exchanges, and 'router' for the extensible routing API.+--+-- [Custom Exchange Types]+--+-- Both the /broadcast/ and /router/ exchanges are implemented as custom+-- /exchange types/. The mechanism for defining custom exchange behaviours+-- such as these is very simple. Raw exchanges are started by evaluating+-- 'startExchange' with a specific 'ExchangeType' record. This type is+-- parameterised by the internal /state/ it holds, and defines two API callbacks+-- in its 'configureEx' and 'routeEx' fields. The former is evaluated whenever a+-- client process evaluates 'configureExchange', the latter whenever a client+-- evaluates 'post' or 'postMessage'. The 'configureEx' callback takes a raw+-- @Message@ (from "Control.Distributed.Process") and is responsible for+-- decoding the message and updating its own state (if required). It is via+-- this callback that custom exchange types can receive information about+-- clients and handle it in their own way. The 'routeEx' callback is evaluated+-- with the exchange type's own internal state and the 'Message' originally+-- sent to the exchange process (via 'post') and is responsible for delivering+-- the message to its clients in whatever way makes sense for that exchange+-- type.+--+-----------------------------------------------------------------------------++module Control.Distributed.Process.Execution.Exchange+  ( -- * Fundamental API+    Exchange()+  , Message(..)+    -- * Starting/Running an Exchange+  , startExchange+  , startSupervised+  , startSupervisedRef+  , runExchange+    -- * Client Facing API+  , post+  , postMessage+  , configureExchange+  , createMessage+    -- * Broadcast Exchange+  , broadcastExchange+  , broadcastExchangeT+  , broadcastClient+  , bindToBroadcaster+  , BroadcastExchange+    -- * Routing (Content Based)+  , HeaderName+  , Binding(..)+  , Bindable+  , BindingSelector+  , RelayType(..)+    -- * Starting a Router+  , router+  , supervisedRouter+    -- * Routing (Publishing) API+  , route+  , routeMessage+    -- * Routing via message/binding keys+  , messageKeyRouter+  , bindKey+    -- * Routing via message headers+  , headerContentRouter+  , bindHeader+    -- * Defining Custom Exchange Types+  , ExchangeType(..)+  , applyHandlers+  ) where++import Control.Distributed.Process.Execution.Exchange.Broadcast+  ( broadcastExchange+  , broadcastExchangeT+  , broadcastClient+  , bindToBroadcaster+  , BroadcastExchange+  )+import Control.Distributed.Process.Execution.Exchange.Internal+  ( Exchange()+  , Message(..)+  , ExchangeType(..)+  , startExchange+  , startSupervised+  , startSupervisedRef+  , runExchange+  , post+  , postMessage+  , configureExchange+  , createMessage+  , applyHandlers+  )+import Control.Distributed.Process.Execution.Exchange.Router+  ( HeaderName+  , Binding(..)+  , Bindable+  , BindingSelector+  , RelayType(..)+  , router+  , supervisedRouter+  , route+  , routeMessage+  , messageKeyRouter+  , bindKey+  , headerContentRouter+  , bindHeader+  )+
+ src/Control/Distributed/Process/Execution/Exchange/Broadcast.hs view
@@ -0,0 +1,338 @@+{-# LANGUAGE DeriveDataTypeable    #-}+{-# LANGUAGE DeriveGeneric         #-}+{-# LANGUAGE StandaloneDeriving    #-}+{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE PatternGuards         #-}+{-# LANGUAGE RecordWildCards       #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE EmptyDataDecls        #-}+{-# LANGUAGE TemplateHaskell       #-}+{-# LANGUAGE ImpredicativeTypes    #-}+{-# LANGUAGE UndecidableInstances  #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- | An exchange type that broadcasts all incomings 'Post' messages.+module Control.Distributed.Process.Execution.Exchange.Broadcast+  (+    broadcastExchange+  , broadcastExchangeT+  , broadcastClient+  , bindToBroadcaster+  , BroadcastExchange+  ) where++import Control.Concurrent.STM (STM, atomically)+import Control.Concurrent.STM.TChan+  ( TChan+  , newBroadcastTChanIO+  , dupTChan+  , readTChan+  , writeTChan+  )+import Control.DeepSeq (NFData)+import Control.Distributed.Process+  ( Process+  , MonitorRef+  , ProcessMonitorNotification(..)+  , ProcessId+  , SendPort+  , processNodeId+  , getSelfPid+  , getSelfNode+  , liftIO+  , newChan+  , sendChan+  , unsafeSend+  , unsafeSendChan+  , receiveWait+  , match+  , matchIf+  , die+  , handleMessage+  , Match+  )+import qualified Control.Distributed.Process as P+import Control.Distributed.Process.Serializable()+import Control.Distributed.Process.Execution.Exchange.Internal+  ( startExchange+  , configureExchange+  , Message(..)+  , Exchange(..)+  , ExchangeType(..)+  , applyHandlers+  )+import Control.Distributed.Process.Extras.Internal.Types+  ( Channel+  , ServerDisconnected(..)+  )+import Control.Distributed.Process.Extras.Internal.Unsafe -- see [note: pcopy]+  ( PCopy+  , pCopy+  , pUnwrap+  , matchChanP+  , InputStream(Null)+  , newInputStream+  )+import Control.Monad (forM_, void)+import Data.Accessor+  ( Accessor+  , accessor+  , (^:)+  )+import Data.Binary+import qualified Data.Foldable as Foldable (toList)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Typeable (Typeable)+import GHC.Generics++-- newtype RoutingTable r =+--  RoutingTable { routes :: (Map String (Map ProcessId r)) }++-- [note: BindSTM, BindPort and safety]+-- We keep these two /bind types/ separate, since only one of them+-- is truly serializable. The risk of unifying them is that at some+-- later time a maintainer might not realise that BindSTM cannot be+-- sent over the wire due to our use of PCopy.+--++data BindPort = BindPort { portClient :: !ProcessId+                         , portSend   :: !(SendPort Message)+                         } deriving (Typeable, Generic)+instance Binary BindPort where+instance NFData BindPort where++data BindSTM =+    BindSTM  { stmClient :: !ProcessId+             , stmSend   :: !(SendPort (PCopy (InputStream Message)))+             } deriving (Typeable)+{-  | forall r. (Routable r) =>+    BindR    { client :: !ProcessId+             , key    :: !String+             , chanC  :: !r+             }+  deriving (Typeable, Generic)+-}++data OutputStream =+    WriteChan (SendPort Message)+  | WriteSTM  (Message -> STM ())+--  | WriteP    ProcessId+  | NoWrite+  deriving (Typeable)++data Binding = Binding { outputStream :: !OutputStream+                       , inputStream  :: !(InputStream Message)+                       }+             | PidBinding !ProcessId+  deriving (Typeable)++data BindOk = BindOk+  deriving (Typeable, Generic)+instance Binary BindOk where+instance NFData BindOk where++data BindFail = BindFail !String+  deriving (Typeable, Generic)+instance Binary BindFail where+instance NFData BindFail where++data BindPlease = BindPlease+  deriving (Typeable, Generic)+instance Binary BindPlease where+instance NFData BindPlease where++type BroadcastClients = Map ProcessId Binding+data BroadcastEx =+  BroadcastEx { _routingTable   :: !BroadcastClients+              , channel         :: !(TChan Message)+              }++type BroadcastExchange = ExchangeType BroadcastEx++--------------------------------------------------------------------------------+-- Starting/Running the Exchange                                              --+--------------------------------------------------------------------------------++-- | Start a new /broadcast exchange/ and return a handle to the exchange.+broadcastExchange :: Process Exchange+broadcastExchange = broadcastExchangeT >>= startExchange++-- | The 'ExchangeType' of a broadcast exchange. Can be combined with the+-- @startSupervisedRef@ and @startSupervised@ APIs.+--+broadcastExchangeT :: Process BroadcastExchange+broadcastExchangeT = do+  ch <- liftIO newBroadcastTChanIO+  return $ ExchangeType { name        = "BroadcastExchange"+                        , state       = BroadcastEx Map.empty ch+                        , configureEx = apiConfigure+                        , routeEx     = apiRoute+                        }++--------------------------------------------------------------------------------+-- Client Facing API                                                          --+--------------------------------------------------------------------------------++-- | Create a binding to the given /broadcast exchange/ for the calling process+-- and return an 'InputStream' that can be used in the @expect@ and+-- @receiveWait@ family of messaging primitives. This form of client interaction+-- helps avoid cluttering the caller's mailbox with 'Message' data, since the+-- 'InputChannel' provides a separate input stream (in a similar fashion to+-- a typed channel).+-- Example:+--+-- > is <- broadcastClient ex+-- > msg <- receiveWait [ matchInputStream is ]+-- > handleMessage (payload msg)+--+broadcastClient :: Exchange -> Process (InputStream Message)+broadcastClient ex@Exchange{..} = do+  myNode <- getSelfNode+  us     <- getSelfPid+  if processNodeId pid == myNode -- see [note: pcopy]+     then do (sp, rp) <- newChan+             configureExchange ex $ pCopy (BindSTM us sp)+             mRef <- P.monitor pid+             P.finally (receiveWait [ matchChanP rp+                                    , handleServerFailure mRef ])+                       (P.unmonitor mRef)+     else do (sp, rp) <- newChan :: Process (Channel Message)+             configureExchange ex $ BindPort us sp+             mRef <- P.monitor pid+             P.finally (receiveWait [+                           match (\(_ :: BindOk)   -> return $ newInputStream $ Left rp)+                         , match (\(f :: BindFail) -> die f)+                         , handleServerFailure mRef+                         ])+                       (P.unmonitor mRef)++-- | Bind the calling process to the given /broadcast exchange/. For each+-- 'Message' the exchange receives, /only the payload will be sent/+-- to the calling process' mailbox.+--+-- Example:+--+-- (producer)+-- > post ex "Hello"+--+-- (consumer)+-- > bindToBroadcaster ex+-- > expect >>= liftIO . putStrLn+--+bindToBroadcaster :: Exchange -> Process ()+bindToBroadcaster ex@Exchange{..} = do+  us <- getSelfPid+  configureExchange ex $ (BindPlease, us)++--------------------------------------------------------------------------------+-- Exchage Definition/State & API Handlers                                    --+--------------------------------------------------------------------------------++apiRoute :: BroadcastEx -> Message -> Process BroadcastEx+apiRoute ex@BroadcastEx{..} msg = do+  liftIO $ atomically $ writeTChan channel msg+  forM_ (Foldable.toList _routingTable) $ routeToClient msg+  return ex+  where+    routeToClient m (PidBinding p)  = P.forward (payload m) p+    routeToClient m b@(Binding _ _) = writeToStream (outputStream b) m++-- TODO: implement unbind!!?++apiConfigure :: BroadcastEx -> P.Message -> Process BroadcastEx+apiConfigure ex msg = do+  -- for unsafe / non-serializable message passing hacks, see [note: pcopy]+  applyHandlers ex msg $ [ \m -> handleMessage m (handleBindPort ex)+                         , \m -> handleBindSTM ex m+                         , \m -> handleMessage m (handleBindPlease ex)+                         , \m -> handleMessage m (handleMonitorSignal ex)+                         , (const $ return $ Just ex)+                         ]+  where+    handleBindPlease ex' (BindPlease, p) = do+      case lookupBinding ex' p of+        Nothing -> return $ (routingTable ^: Map.insert p (PidBinding p)) ex'+        Just _  -> return ex'++    handleMonitorSignal bx (ProcessMonitorNotification _ p _) =+      return $ (routingTable ^: Map.delete p) bx++    handleBindSTM ex'@BroadcastEx{..} msg' = do+      bind' <- pUnwrap msg' :: Process (Maybe BindSTM) -- see [note: pcopy]+      case bind' of+        Nothing -> return Nothing+        Just s  -> do+          let binding = lookupBinding ex' (stmClient s)+          case binding of+            Nothing -> createBinding ex' s >>= \ex'' -> handleBindSTM ex'' msg'+            Just b  -> sendBinding (stmSend s) b >> return (Just ex')++    createBinding bEx'@BroadcastEx{..} BindSTM{..} = do+      void $ P.monitor stmClient+      nch <- liftIO $ atomically $ dupTChan channel+      let istr = newInputStream $ Right (readTChan nch)+      let ostr = NoWrite -- we write to our own channel, not the broadcast+      let bnd = Binding ostr istr+      return $ (routingTable ^: Map.insert stmClient bnd) bEx'++    sendBinding sp' bs = unsafeSendChan sp' $ pCopy (inputStream bs)++    handleBindPort :: BroadcastEx -> BindPort -> Process BroadcastEx+    handleBindPort x@BroadcastEx{..} BindPort{..} = do+      let binding = lookupBinding x portClient+      case binding of+        Just _  -> unsafeSend portClient (BindFail "DuplicateBinding") >> return x+        Nothing -> do+          let istr = Null+          let ostr = WriteChan portSend+          let bound = Binding ostr istr+          void $ P.monitor portClient+          unsafeSend portClient BindOk+          return $ (routingTable ^: Map.insert portClient bound) x++    lookupBinding BroadcastEx{..} k = Map.lookup k $ _routingTable++{- [note: pcopy]++We rely on risky techniques here, in order to allow for sharing useful+data that is not really serializable. For Cloud Haskell generally, this is+a bad idea, since we want message passing to work both locally and in a+distributed setting. In this case however, what we're really attempting is+an optimisation, since we only use unsafe PCopy based techniques when dealing+with exchange clients residing on our (local) node.++The PCopy mechanism is defined in the (aptly named) "Unsafe" module.++-}++-- TODO: move handleServerFailure into Primitives.hs++writeToStream :: OutputStream -> Message -> Process ()+writeToStream (WriteChan sp) = sendChan sp  -- see [note: safe remote send]+writeToStream (WriteSTM stm) = liftIO . atomically . stm+writeToStream NoWrite        = const $ return ()+{-# INLINE writeToStream #-}++{- [note: safe remote send]++Although we go to great lengths here to avoid serialization and/or copying+overheads, there are some activities for which we prefer to play it safe.+Chief among these is delivering messages to remote clients. Thankfully, our+unsafe @sendChan@ primitive will crash the caller/sender if there are any+encoding problems, however it is only because we /know/ for certain that+our recipient is remote, that we've chosen to write via a SendPort in the+first place! It makes sense therefore, to use the safe @sendChan@ operation+here, since for a remote call we /cannot/ avoid the overhead of serialization+anyway.++-}++handleServerFailure :: MonitorRef -> Match (InputStream Message)+handleServerFailure mRef =+  matchIf (\(ProcessMonitorNotification r _ _) -> r == mRef)+          (\(ProcessMonitorNotification _ _ d) -> die $ ServerDisconnected d)++routingTable :: Accessor BroadcastEx BroadcastClients+routingTable = accessor _routingTable (\r e -> e { _routingTable = r })+
+ src/Control/Distributed/Process/Execution/Exchange/Internal.hs view
@@ -0,0 +1,276 @@+{-# LANGUAGE DeriveDataTypeable    #-}+{-# LANGUAGE DeriveGeneric         #-}+{-# LANGUAGE StandaloneDeriving    #-}+{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE PatternGuards         #-}+{-# LANGUAGE RecordWildCards       #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE EmptyDataDecls        #-}+{-# LANGUAGE TemplateHaskell       #-}+{-# LANGUAGE ImpredicativeTypes    #-}+{-# LANGUAGE UndecidableInstances  #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- | Internal Exchange Implementation+module Control.Distributed.Process.Execution.Exchange.Internal+  ( Exchange(..)+  , Message(..)+  , ExchangeType(..)+  , startExchange+  , startSupervised+  , startSupervisedRef+  , runExchange+  , post+  , postMessage+  , configureExchange+  , createMessage+  , applyHandlers+  ) where++import Control.Concurrent.MVar (MVar, takeMVar, putMVar, newEmptyMVar)+import Control.DeepSeq (NFData)+import Control.Distributed.Process+  ( Process+  , ProcessMonitorNotification(..)+  , ProcessId+  , liftIO+  , spawnLocal+  , unsafeWrapMessage+  )+import qualified Control.Distributed.Process as P (Message, link)+import Control.Distributed.Process.Serializable hiding (SerializableDict)+import Control.Distributed.Process.Extras.Internal.Types+  ( Resolvable(..)+  )+import Control.Distributed.Process.Extras.Internal.Primitives+  ( Linkable(..)+  )+import Control.Distributed.Process.ManagedProcess+  ( channelControlPort+  , handleControlChan+  , handleInfo+  , handleRaw+  , continue+  , defaultProcess+  , InitHandler+  , InitResult(..)+  , ProcessAction+  , ProcessDefinition(..)+  , ControlChannel+  , ControlPort+  )+import qualified Control.Distributed.Process.ManagedProcess as MP+  ( chanServe+  )+import Control.Distributed.Process.ManagedProcess.UnsafeClient+  ( sendControlMessage+  )+import Control.Distributed.Process.Supervisor (SupervisorPid)+import Control.Distributed.Process.Extras.Time (Delay(Infinity))+import Data.Binary+import Data.Typeable (Typeable)+import GHC.Generics+import Prelude hiding (drop)++{- [design notes]++Messages are sent to exchanges and forwarded to clients. An exchange+is parameterised by its routing mechanism, which is responsible for+maintaining its own client state and selecting the clients to which+messages are forwarded.++-}++-- | Opaque handle to an exchange.+--+data Exchange = Exchange { pid   :: !ProcessId+                         , cchan :: !(ControlPort ControlMessage)+                         , xType :: !String+                         } deriving (Typeable, Generic, Eq)+instance Binary Exchange where+instance Show Exchange where+  show Exchange{..} = (xType ++ ":" ++ (show pid))++instance Resolvable Exchange where+  resolve = return . Just . pid++{-+instance Observable Exchange MonitorRef ProcessMonitorNotification where+  observe   = P.monitor . pid+  unobserve = P.unmonitor+  observableFrom ref (ProcessMonitorNotification ref' _ r) =+    return $ if ref' == ref then Just r else Nothing+-}++instance Linkable Exchange where+  linkTo = P.link . pid++-- we communicate with exchanges using control channels+sendCtrlMsg :: Exchange -> ControlMessage -> Process ()+sendCtrlMsg Exchange{..} = sendControlMessage cchan++-- | Messages sent to an exchange can optionally provide a routing+-- key and a list of (key, value) headers in addition to the underlying+-- payload.+data Message =+  Message { key     :: !String  -- ^ a /routing key/ for the payload+          , headers :: ![(String, String)] -- ^ arbitrary key-value headers+          , payload :: !P.Message  -- ^ the underlying @Message@ payload+          } deriving (Typeable, Generic, Show)+instance Binary Message where+instance NFData Message where++data ControlMessage =+    Configure !P.Message+  | Post      !Message+    deriving (Typeable, Generic)+instance Binary ControlMessage where+instance NFData ControlMessage where++-- | Different exchange types are defined using record syntax.+-- The 'configureEx' and 'routeEx' API functions are called during the exchange+-- lifecycle when incoming traffic arrives. Configuration messages are+-- completely arbitrary types and the exchange type author is entirely+-- responsible for decoding them. Messages posted to the exchange (see the+-- 'Message' data type) are passed to the 'routeEx' API function along with the+-- exchange type's own internal state. Both API functions return a new+-- (potentially updated) state and run in the @Process@ monad.+--+data ExchangeType s =+  ExchangeType { name        :: String+               , state       :: s+               , configureEx :: s -> P.Message -> Process s+               , routeEx     :: s -> Message -> Process s+               }++--------------------------------------------------------------------------------+-- Starting/Running an Exchange                                               --+--------------------------------------------------------------------------------++-- | Starts an /exchange process/ with the given 'ExchangeType'.+startExchange :: forall s. ExchangeType s -> Process Exchange+startExchange = doStart Nothing++-- | Starts an exchange as part of a supervision tree.+--+-- Example:+-- > childSpec = toChildStart $ startSupervisedRef exType+--+startSupervisedRef :: forall s . ExchangeType s+                   -> SupervisorPid+                   -> Process (ProcessId, P.Message)+startSupervisedRef t s = do+  ex <- startSupervised t s+  return (pid ex, unsafeWrapMessage ex)++-- | Starts an exchange as part of a supervision tree.+--+-- Example:+-- > childSpec = toChildStart $ startSupervised exType+--+startSupervised :: forall s . ExchangeType s+                -> SupervisorPid+                -> Process Exchange+startSupervised t s = doStart (Just s) t++doStart :: Maybe SupervisorPid -> ExchangeType s -> Process Exchange+doStart mSp t = do+  cchan <- liftIO $ newEmptyMVar+  spawnLocal (maybeLink mSp >> runExchange t cchan) >>= \pid -> do+    cc <- liftIO $ takeMVar cchan+    return $ Exchange pid cc (name t)+  where+    maybeLink Nothing   = return ()+    maybeLink (Just p') = P.link p'++runExchange :: forall s.+               ExchangeType s+            -> MVar (ControlPort ControlMessage)+            -> Process ()+runExchange t tc = MP.chanServe t exInit (processDefinition t tc)++exInit :: forall s. InitHandler (ExchangeType s) (ExchangeType s)+exInit t = return $ InitOk t Infinity++--------------------------------------------------------------------------------+-- Client Facing API                                                          --+--------------------------------------------------------------------------------++-- | Posts an arbitrary 'Serializable' datum to an /exchange/. The raw datum is+-- wrapped in the 'Message' data type, with its 'key' set to @""@ and its+-- 'headers' to @[]@.+post :: Serializable a => Exchange -> a -> Process ()+post ex msg = postMessage ex $ Message "" [] (unsafeWrapMessage msg)++-- | Posts a 'Message' to an /exchange/.+postMessage :: Exchange -> Message -> Process ()+postMessage ex msg = msg `seq` sendCtrlMsg ex $ Post msg++-- | Sends an arbitrary 'Serializable' datum to an /exchange/, for use as a+-- configuration change - see 'configureEx' for details.+configureExchange :: Serializable m => Exchange -> m -> Process ()+configureExchange e m = sendCtrlMsg e $ Configure (unsafeWrapMessage m)++-- | Utility for creating a 'Message' datum from its 'key', 'headers' and+-- 'payload'.+createMessage :: Serializable m => String -> [(String, String)] -> m -> Message+createMessage k h m = Message k h $ unsafeWrapMessage m++-- | Utility for custom exchange type authors - evaluates a set of primitive+-- message handlers from left to right, returning the first which evaluates+-- to @Just a@, or the initial @e@ value if all the handlers yield @Nothing@.+applyHandlers :: a+              -> P.Message+              -> [P.Message -> Process (Maybe a)]+              -> Process a+applyHandlers e _ []     = return e+applyHandlers e m (f:fs) = do+  r <- f m+  case r of+    Nothing -> applyHandlers e m fs+    Just r' -> return r'++--------------------------------------------------------------------------------+-- Process Definition/State & API Handlers                                    --+--------------------------------------------------------------------------------++processDefinition :: forall s.+                     ExchangeType s+                  -> MVar (ControlPort ControlMessage)+                  -> ControlChannel ControlMessage+                  -> Process (ProcessDefinition (ExchangeType s))+processDefinition _ tc cc = do+  liftIO $ putMVar tc $ channelControlPort cc+  return $+    defaultProcess {+        apiHandlers  = [ handleControlChan cc handleControlMessage ]+      , infoHandlers = [ handleInfo handleMonitor+                       , handleRaw convertToCC+                       ]+      } :: Process (ProcessDefinition (ExchangeType s))++handleMonitor :: forall s.+                 ExchangeType s+              -> ProcessMonitorNotification+              -> Process (ProcessAction (ExchangeType s))+handleMonitor ex m = do+  handleControlMessage ex (Configure (unsafeWrapMessage m))++convertToCC :: forall s.+               ExchangeType s+            -> P.Message+            -> Process (ProcessAction (ExchangeType s))+convertToCC ex msg = do+  liftIO $ putStrLn "convert to cc"+  handleControlMessage ex (Post $ Message "" [] msg)++handleControlMessage :: forall s.+                        ExchangeType s+                     -> ControlMessage+                     -> Process (ProcessAction (ExchangeType s))+handleControlMessage ex@ExchangeType{..} cm =+  let action = case cm of+                 Configure msg -> configureEx state msg+                 Post      msg -> routeEx     state msg+  in action >>= \s -> continue $ ex { state = s }+
+ src/Control/Distributed/Process/Execution/Exchange/Router.hs view
@@ -0,0 +1,250 @@+{-# LANGUAGE DeriveDataTypeable    #-}+{-# LANGUAGE DeriveGeneric         #-}+{-# LANGUAGE StandaloneDeriving    #-}+{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE PatternGuards         #-}+{-# LANGUAGE RecordWildCards       #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE EmptyDataDecls        #-}+{-# LANGUAGE TemplateHaskell       #-}+{-# LANGUAGE ImpredicativeTypes    #-}+{-# LANGUAGE UndecidableInstances  #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- | A simple API for /routing/, using a custom exchange type.+module Control.Distributed.Process.Execution.Exchange.Router+  ( -- * Types+    HeaderName+  , Binding(..)+  , Bindable+  , BindingSelector+  , RelayType(..)+    -- * Starting a Router+  , router+  , supervisedRouter+  , supervisedRouterRef+    -- * Client (Publishing) API+  , route+  , routeMessage+    -- * Routing via message/binding keys+  , messageKeyRouter+  , bindKey+    -- * Routing via message headers+  , headerContentRouter+  , bindHeader+  ) where++import Control.DeepSeq (NFData)+import Control.Distributed.Process+  ( Process+  , ProcessMonitorNotification(..)+  , ProcessId+  , monitor+  , handleMessage+  , unsafeWrapMessage+  )+import qualified Control.Distributed.Process as P+import Control.Distributed.Process.Serializable (Serializable)+import Control.Distributed.Process.Execution.Exchange.Internal+  ( startExchange+  , startSupervised+  , configureExchange+  , Message(..)+  , Exchange+  , ExchangeType(..)+  , post+  , postMessage+  , applyHandlers+  )+import Control.Distributed.Process.Extras.Internal.Primitives+  ( deliver+  , Resolvable(..)+  )+import Control.Distributed.Process.Supervisor (SupervisorPid)+import Data.Binary+import Data.Foldable (forM_)+import Data.Hashable+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as Map+import Data.HashSet (HashSet)+import qualified Data.HashSet as Set+import Data.Typeable (Typeable)+import GHC.Generics++type HeaderName = String++-- | The binding key used by the built-in key and header based+-- routers.+data Binding =+    BindKey    { bindingKey :: !String }+  | BindHeader { bindingKey :: !String+               , headerName :: !HeaderName+               }+  | BindNone+  deriving (Typeable, Generic, Eq, Show)+instance Binary Binding where+instance NFData Binding where+instance Hashable Binding where++-- | Things that can be used as binding keys in a router.+class (Hashable k, Eq k, Serializable k) => Bindable k+instance (Hashable k, Eq k, Serializable k) => Bindable k++-- | Used to convert a 'Message' into a 'Bindable' routing key.+type BindingSelector k = (Message -> Process k)++-- | Given to a /router/ to indicate whether clients should+-- receive 'Message' payloads only, or the whole 'Message' object+-- itself.+data RelayType = PayloadOnly | WholeMessage++data State k = State { bindings  :: !(HashMap k (HashSet ProcessId))+                     , selector  :: !(BindingSelector k)+                     , relayType :: !RelayType+                     }++type Router k = ExchangeType (State k)++--------------------------------------------------------------------------------+-- Starting/Running the Exchange                                              --+--------------------------------------------------------------------------------++-- | A router that matches on a 'Message' 'key'. To bind a client @Process@ to+-- such an exchange, use the 'bindKey' function.+messageKeyRouter :: RelayType -> Process Exchange+messageKeyRouter t = router t matchOnKey -- (return . BindKey . key)+  where+    matchOnKey :: Message -> Process Binding+    matchOnKey m = return $ BindKey (key m)++-- | A router that matches on a specific (named) header. To bind a client+-- @Process@ to such an exchange, use the 'bindHeader' function.+headerContentRouter :: RelayType -> HeaderName -> Process Exchange+headerContentRouter t n = router t (checkHeaders n)+  where+    checkHeaders hn Message{..} = do+      case Map.lookup hn (Map.fromList headers) of+        Nothing -> return BindNone+        Just hv -> return $ BindHeader hn hv++-- | Defines a /router/ exchange. The 'BindingSelector' is used to construct+-- a binding (i.e., an instance of the 'Bindable' type @k@) for each incoming+-- 'Message'. Such bindings are matched against bindings stored in the exchange.+-- Clients of a /router/ exchange are identified by a binding, mapped to+-- one or more 'ProcessId's.+--+-- The format of the bindings, nature of their storage and mechanism for+-- submitting new bindings is implementation dependent (i.e., will vary by+-- exchange type). For example, the 'messageKeyRouter' and 'headerContentRouter'+-- implementations both use the 'Binding' data type, which can represent a+-- 'Message' key or a 'HeaderName' and content. As with all custom exchange+-- types, bindings should be submitted by evaluating 'configureExchange' with+-- a suitable data type.+--+router :: (Bindable k) => RelayType -> BindingSelector k -> Process Exchange+router t s = routerT t s >>= startExchange++supervisedRouterRef :: Bindable k+                    => RelayType+                    -> BindingSelector k+                    -> SupervisorPid+                    -> Process (ProcessId, P.Message)+supervisedRouterRef t sel spid = do+  ex <- supervisedRouter t sel spid+  Just pid <- resolve ex+  return (pid, unsafeWrapMessage ex)++-- | Defines a /router/ that can be used in a supervision tree.+supervisedRouter :: Bindable k+                 => RelayType+                 -> BindingSelector k+                 -> SupervisorPid+                 -> Process Exchange+supervisedRouter t sel spid =+  routerT t sel >>= \t' -> startSupervised t' spid++routerT :: Bindable k+        => RelayType+        -> BindingSelector k+        -> Process (Router k)+routerT t s = do+  return $ ExchangeType { name        = "Router"+                        , state       = State Map.empty s t+                        , configureEx = apiConfigure+                        , routeEx     = apiRoute+                        }++--------------------------------------------------------------------------------+-- Client Facing API                                                          --+--------------------------------------------------------------------------------++-- | Add a binding (for the calling process) to a 'messageKeyRouter' exchange.+bindKey :: String -> Exchange -> Process ()+bindKey k ex = do+  self <- P.getSelfPid+  configureExchange ex (self, BindKey k)++-- | Add a binding (for the calling process) to a 'headerContentRouter' exchange.+bindHeader :: HeaderName -> String -> Exchange -> Process ()+bindHeader n v ex = do+  self <- P.getSelfPid+  configureExchange ex (self, BindHeader v n)++-- | Send a 'Serializable' message to the supplied 'Exchange'. The given datum+-- will be converted to a 'Message', with the 'key' set to @""@ and the+-- 'headers' to @[]@.+--+-- The routing behaviour will be dependent on the choice of 'BindingSelector'+-- given when initialising the /router/.+route :: Serializable m => Exchange -> m -> Process ()+route = post++-- | Send a 'Message' to the supplied 'Exchange'.+-- The routing behaviour will be dependent on the choice of 'BindingSelector'+-- given when initialising the /router/.+routeMessage :: Exchange -> Message -> Process ()+routeMessage = postMessage++--------------------------------------------------------------------------------+-- Exchage Definition/State & API Handlers                                    --+--------------------------------------------------------------------------------++apiRoute :: forall k. Bindable k+         => State k+         -> Message+         -> Process (State k)+apiRoute st@State{..} msg = do+  binding <- selector msg+  case Map.lookup binding bindings of+    Nothing -> return st+    Just bs -> forM_ bs (fwd relayType msg) >> return st+  where+    fwd WholeMessage m = deliver m+    fwd PayloadOnly  m = P.forward (payload m)++-- TODO: implement 'unbind' ???+-- TODO: apiConfigure currently leaks memory if clients die (we don't cleanup)++apiConfigure :: forall k. Bindable k+             => State k+             -> P.Message+             -> Process (State k)+apiConfigure st msg = do+  applyHandlers st msg $ [ \m -> handleMessage m (createBinding st)+                         , \m -> handleMessage m (handleMonitorSignal st)+                         ]+  where+    createBinding s@State{..} (pid, bind) = do+      case Map.lookup bind bindings of+        Nothing -> do _ <- monitor pid+                      return $ s { bindings = newBind bind pid bindings }+        Just ps -> return $ s { bindings = addBind bind pid bindings ps }++    newBind b p bs = Map.insert b (Set.singleton p) bs+    addBind b' p' bs' ps = Map.insert b' (Set.insert p' ps) bs'++    handleMonitorSignal s@State{..} (ProcessMonitorNotification _ p _) =+      let bs  = bindings+          bs' = Map.foldlWithKey' (\a k v -> Map.insert k (Set.delete p v) a) bs bs+      in return $ s { bindings = bs' }+
+ src/Control/Distributed/Process/Execution/Mailbox.hs view
@@ -0,0 +1,744 @@+{-# LANGUAGE DeriveDataTypeable   #-}+{-# LANGUAGE DeriveGeneric        #-}+{-# LANGUAGE StandaloneDeriving   #-}+{-# LANGUAGE ScopedTypeVariables  #-}+{-# LANGUAGE PatternGuards        #-}+{-# LANGUAGE RecordWildCards      #-}+{-# LANGUAGE FlexibleInstances    #-}+{-# LANGUAGE EmptyDataDecls       #-}+{-# LANGUAGE TemplateHaskell      #-}+{-# LANGUAGE ImpredicativeTypes   #-}++-----------------------------------------------------------------------------+-- |+-- Module      :  Control.Distributed.Process.Execution.Mailbox+-- 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)+--+-- Generic process that acts as an external mailbox and message buffer.+--+-- [Overview]+--+-- For use when rate limiting is not possible (or desired), this module+-- provides a /buffer process/ that receives mail via its 'post' API, buffers+-- the received messages and delivers them when its /owning process/ asks for+-- them. A mailbox has to be started with a maximum buffer size - the so called+-- /limit/ - and will discard messages once its internal storage reaches this+-- user defined threshold.+--+-- The usual behaviour of the /buffer process/ is to accumulate messages in+-- its internal memory. When a client evaluates 'notify', the buffer will+-- send a 'NewMail' message to the (real) mailbox of its owning process as+-- soon as it has any message(s) ready to deliver. If the buffer already+-- contains undelivered mail, the 'NewMail' message will be dispatched+-- immediately.+--+-- When the owning process wishes to receive mail, evaluating 'deliver' (from+-- any process) will cause the buffer to send its owner a 'Delivery' message+-- containing the accumulated messages and additional information about the+-- number of messages it is delivering, the number of messages dropped since+-- the last delivery and a handle for the mailbox (so that processes can have+-- multiple mailboxes if required, and distinguish between them).+--+-- [Overflow Handling]+--+-- A mailbox handles overflow - when the number of messages it is holding+-- reaches the limit - differently depending on the 'BufferType' selected+-- when it starts. The @Queue@ buffer will, once the limit is reached, drop+-- older messages first (i.e., the head of the queue) to make space for+-- newer ones. The @Ring@ buffer works similarly, but blocks new messages+-- so as to preserve existing ones instead. Finally, the @Stack@ buffer will+-- drop the last (i.e., most recently received) message to make room for new+-- mail.+--+-- Mailboxes can be /resized/ by evaluating 'resize' with a new value for the+-- limit. If the new limit is older that the current/previous one, messages+-- are dropped as though the mailbox had previously seen a volume of mail+-- equal to the difference (in size) between the limits. In this situation,+-- the @Queue@ will drop as many older messages as neccessary to come within+-- the limit, whilst the other two buffer types will drop as many newer messages+-- as needed.+--+-- [Ordering Guarantees]+--+-- When messages are delivered to the owner, they arrive as a list of raw+-- @Message@ entries, given in descending age order (i.e., eldest first).+-- Whilst this approximates the FIFO ordering a process' mailbox would usually+-- offer, the @Stack@ buffer will appear to offer no ordering at all, since+-- it always deletes the most recent message(s). The @Queue@ and @Ring@ buffers+-- will maintain a more queue-like (i.e., FIFO) view of received messages,+-- with the obvious constraint the newer or older data might have been deleted.+--+-- [Post API and Relaying]+--+-- For messages to be properly handled by the mailbox, they can either be sent+-- via the 'post' API or directly to the 'Mailbox'. Messages sent directly to+-- the mailbox will still be handled via the internal buffers and subjected to+-- the mailbox limits. The 'post' API is really just a means to ensure that+-- the conversion from @Serializable a -> Message@ is done in the caller's+-- process and uses the safe @wrapMessage@ variant.+--+-- [Acknowledgements]+--+-- This API is based on the work of Erlang programmers Fred Hebert and+-- Geoff Cant, its design closely mirroring that of the the /pobox/ library+-- application.+--+-----------------------------------------------------------------------------+module Control.Distributed.Process.Execution.Mailbox+  (+    -- * Creating, Starting, Configuring and Running a Mailbox+    Mailbox()+  , startMailbox+  , startSupervised+  , startSupervisedMailbox+  , createMailbox+  , resize+  , statistics+  , monitor+  , Limit+  , BufferType(..)+  , MailboxStats(..)+    -- * Posting Mail+  , post+    -- * Obtaining Mail and Notifications+  , notify+  , deliver+  , active+  , NewMail(..)+  , Delivery(..)+  , FilterResult(..)+  , acceptEverything+  , acceptMatching+    -- * Remote Table+  , __remoteTable+  ) where++import Control.Concurrent.STM (atomically)+import Control.Concurrent.STM.TChan+  ( TChan+  , newBroadcastTChanIO+  , dupTChan+  , readTChan+  , writeTChan+  )+import Control.Distributed.Process hiding (call, monitor)+import qualified Control.Distributed.Process as P (monitor)+import Control.Distributed.Process.Closure+  ( remotable+  , mkStaticClosure+  )+import Control.Distributed.Process.Serializable hiding (SerializableDict)+import Control.Distributed.Process.Extras.Internal.Types+  ( ExitReason(..)+  , Resolvable(..)+  , Routable(..)+  , Linkable(..)+  )+import Control.Distributed.Process.ManagedProcess+  ( call+  , sendControlMessage+  , channelControlPort+  , handleControlChan+  , handleInfo+  , handleRaw+  , continue+  , defaultProcess+  , UnhandledMessagePolicy(..)+  , InitHandler+  , InitResult(..)+  , ProcessAction+  , ProcessDefinition(..)+  , ControlChannel+  , ControlPort+  )+import qualified Control.Distributed.Process.ManagedProcess as MP+  ( chanServe+  )+import Control.Distributed.Process.ManagedProcess.Server+  ( stop+  )+import Control.Distributed.Process.ManagedProcess.Server.Restricted as Restricted+  ( getState+  , Result+  , RestrictedProcess+  )+import qualified Control.Distributed.Process.ManagedProcess.Server.Restricted as Restricted+  ( handleCall+  , reply+  )+import Control.Distributed.Process.Supervisor (SupervisorPid)+import Control.Distributed.Process.Extras.Time+import Control.Exception (SomeException)+import Data.Accessor+  ( Accessor+  , accessor+  , (^:)+  , (.>)+  , (^=)+  , (^.)+  )+import Data.Binary+import qualified Data.Foldable as Foldable+import Data.Sequence+  ( Seq+  , ViewL(EmptyL, (:<))+  , ViewR(EmptyR, (:>))+  , (<|)+  , (|>)+  )+import qualified Data.Sequence as Seq+import Data.Typeable (Typeable)++import GHC.Generics++#if ! MIN_VERSION_base(4,6,0)+import Prelude hiding (catch, drop)+#else+import Prelude hiding (drop)+#endif++--------------------------------------------------------------------------------+-- Types                                                                      --+--------------------------------------------------------------------------------++-- external client/configuration API++-- | Opaque handle to a mailbox.+--+data Mailbox = Mailbox { pid   :: !ProcessId+                       , cchan :: !(ControlPort ControlMessage)+                       } deriving (Typeable, Generic, Eq)+instance Binary Mailbox where+instance Show Mailbox where+  show = ("Mailbox:" ++) . show . pid++instance Linkable Mailbox where+  linkTo = link . pid++instance Resolvable Mailbox where+  resolve = return . Just . pid++instance Routable Mailbox where+  sendTo       = post+  unsafeSendTo = post++sendCtrlMsg :: Mailbox+            -> ControlMessage+            -> Process ()+sendCtrlMsg Mailbox{..} = sendControlMessage cchan++-- | Describes the different types of buffer.+--+data BufferType =+    Queue -- ^ FIFO buffer, limiter drops the eldest message (queue head)+  | Stack -- ^ unordered buffer, limiter drops the newest (top) message+  | Ring  -- ^ FIFO buffer, limiter refuses (i.e., drops) new messages+  deriving (Typeable, Eq, Show)++-- TODO: re-implement this process in terms of a limiter expression, i.e.,+--+-- data Limit s = Accept s | Block s+--+-- limit :: forall s. Closure (Message {- new mail -} -> Process (Limit s))++-- | Represents the maximum number of messages the internal buffer can hold.+--+type Limit = Integer++-- | A @Closure@ used to filter messages in /active/ mode.+--+type Filter = Closure (Message -> Process FilterResult)++-- | Marker message indicating to the owning process that mail has arrived.+--+data NewMail = NewMail !Mailbox !Integer+  deriving (Typeable, Generic, Show)+instance Binary NewMail where++-- | Mail delivery.+--+data Delivery = Delivery { box          :: Mailbox -- ^ handle to the sending mailbox+                         , messages     :: [Message] -- ^ list of raw messages+                         , count        :: Integer -- ^ number of messages delivered+                         , totalDropped :: Integer -- ^ total dropped/skipped messages+                         }+  deriving (Typeable, Generic)+instance Binary Delivery where++-- TODO: keep running totals and send them with the stats...++-- | Bundle of statistics data, available on request via+-- the 'mailboxStats' API call.+--+data MailboxStats =+  MailboxStats { pendingMessages :: Integer+               , droppedMessages :: Integer+               , currentLimit    :: Limit+               , owningProcess   :: ProcessId+               } deriving (Typeable, Generic, Show)+instance Binary MailboxStats where++-- internal APIs++data Post = Post !Message+  deriving (Typeable, Generic)+instance Binary Post where++data StatsReq = StatsReq+  deriving (Typeable, Generic)+instance Binary StatsReq where++data FilterResult = Keep | Skip | Send+  deriving (Typeable, Generic)+instance Binary FilterResult++data Mode =+    Active !Filter -- ^ Send all buffered messages (or wait until one arrives)+  | Notify  -- ^ Send a notification once messages are ready to be received+  | Passive -- ^ Accumulate messages in the buffer, dropping them if necessary+  deriving (Typeable, Generic)+instance Binary Mode where+instance Show Mode where+  show (Active _) = "Active"+  show Notify     = "Notify"+  show Passive    = "Passive"++data ControlMessage =+    Resize !Integer+  | SetActiveMode !Mode+  deriving (Typeable, Generic)+instance Binary ControlMessage where++class Buffered a where+  tag    :: a -> BufferType+  push   :: Message -> a -> a+  pop    :: a -> Maybe (Message, a)+  adjust :: Limit -> a -> a+  drop   :: Integer -> a -> a++data BufferState =+  BufferState { _mode    :: Mode+              , _bufferT :: BufferType+              , _limit   :: Limit+              , _size    :: Integer+              , _dropped :: Integer+              , _owner   :: ProcessId+              , ctrlChan :: ControlPort ControlMessage+              }++defaultState :: BufferType+             -> Limit+             -> ProcessId+             -> ControlPort ControlMessage+             -> BufferState+defaultState bufferT limit' pid cc =+  BufferState { _mode    = Passive+              , _bufferT = bufferT+              , _limit   = limit'+              , _size    = 0+              , _dropped = 0+              , _owner   = pid+              , ctrlChan = cc+              }++data State = State { _buffer :: Seq Message+                   , _state  :: BufferState+                   }++instance Buffered State where+  tag q  = _bufferT $ _state q++  -- see note [buffer enqueue/dequeue semantics]+  push m = (state .> size ^: (+1)) . (buffer ^: (m <|))++  -- see note [buffer enqueue/dequeue semantics]+  pop q = maybe Nothing+                (\(s' :> a) -> Just (a, ( (buffer ^= s')+                                        . (state .> size ^: (1-))+                                        $ q))) $ getR (q ^. buffer)++  adjust sz q = (state .> limit ^= sz) $ maybeDrop+    where+      maybeDrop+        | size' <- (q ^. state ^. size),+          size' > sz = (state .> size ^= sz) $ drop (size' - sz) q+        | otherwise  = q++  -- see note [buffer drop semantics]+  drop n q+    | n > 1     = drop (n - 1) $ drop 1 q+    | isQueue q = dropR q+    | otherwise = dropL q+    where+      dropR q' = maybe q' (\(s' :> _) -> dropOne q' s') $ getR (q' ^. buffer)+      dropL q' = maybe q' (\(_ :< s') -> dropOne q' s') $ getL (q' ^. buffer)+      dropOne q' s = ( (buffer ^= s)+                     . (state .> size ^: (\n' -> n' - 1))+                     . (state .> dropped ^: (+1))+                     $ q' )++{- note [buffer enqueue/dequeue semantics]+If we choose to add a message to the buffer, it is always+added to the left hand side of the sequence. This gives+FIFO (enqueue to tail) semantics for queues, LIFO (push+new head) semantics for stacks when dropping messages - note+that dequeueing will always take the eldest (RHS) message,+regardless of the buffer type - and queue-like semantics for+the ring buffer.++We /always/ take the eldest message each time we dequeue,+in an attempt to maintain something approaching FIFO order+when processing the mailbox, for all data structures. Where+we do not achieve this is dropping messages, since the different+buffer types drop messages either on the right (eldest) or left+(youngest).++-- note [buffer drop semantics]++The "stack buffer", when full, only ever attempts to drop the+youngest (leftmost) message, such that it guarantees no ordering+at all, but that is enforced by the code calling 'drop' rather+than the data structure itself. The ring buffer behaves similarly,+since it rejects new messages altogether, which in practise means+dropping from the LHS.++-}++--------------------------------------------------------------------------------+-- Starting/Running a Mailbox                                                 --+--------------------------------------------------------------------------------++-- | Start a mailbox for the calling process.+--+-- > create = getSelfPid >>= start+--+createMailbox :: BufferType -> Limit -> Process Mailbox+createMailbox buffT maxSz =+  getSelfPid >>= \self -> startMailbox self buffT maxSz++-- | Start a mailbox for the supplied @ProcessId@.+--+-- > start = spawnLocal $ run+--+startMailbox :: ProcessId -> BufferType -> Limit -> Process Mailbox+startMailbox = doStartMailbox Nothing++-- | As 'startMailbox', but suitable for use in supervisor child specs.+-- This variant is for use when you want to access to the underlying+-- 'Mailbox' handle in your supervised child refs. See supervisor's+-- @ChildRef@ data type for more information.+--+-- Example:+-- > childSpec = toChildStart $ startSupervised pid bufferType mboxLimit+--+-- See "Control.Distributed.Process.Supervisor"+--+startSupervised :: ProcessId+                -> BufferType+                -> Limit+                -> SupervisorPid+                -> Process (ProcessId, Message)+startSupervised p b l s = do+  mb <- startSupervisedMailbox p b l s+  return (pid mb, unsafeWrapMessage mb)++-- | As 'startMailbox', but suitable for use in supervisor child specs.+--+-- Example:+-- > childSpec = toChildStart $ startSupervisedMailbox pid bufferType mboxLimit+--+-- See "Control.Distributed.Process.Supervisor"+--+startSupervisedMailbox :: ProcessId+                       -> BufferType+                       -> Limit+                       -> SupervisorPid+                       -> Process Mailbox+startSupervisedMailbox p b l s = doStartMailbox (Just s) p b l++doStartMailbox :: Maybe SupervisorPid+               -> ProcessId+               -> BufferType+               -> Limit+               -> Process Mailbox+doStartMailbox mSp p b l = do+  bchan <- liftIO $ newBroadcastTChanIO+  rchan <- liftIO $ atomically $ dupTChan bchan+  spawnLocal (maybeLink mSp >> runMailbox bchan p b l) >>= \pid -> do+    cc <- liftIO $ atomically $ readTChan rchan+    return $ Mailbox pid cc+  where+    maybeLink Nothing   = return ()+    maybeLink (Just p') = link p'++-- | Run the mailbox server loop.+--+runMailbox :: TChan (ControlPort ControlMessage)+           -> ProcessId+           -> BufferType+           -> Limit+           -> Process ()+runMailbox tc pid buffT maxSz = do+  link pid+  tc' <- liftIO $ atomically $ dupTChan tc+  MP.chanServe (pid, buffT, maxSz) (mboxInit tc') (processDefinition pid tc)++--------------------------------------------------------------------------------+-- Mailbox Initialisation/Startup                                             --+--------------------------------------------------------------------------------++mboxInit :: TChan (ControlPort ControlMessage)+         -> InitHandler (ProcessId, BufferType, Limit) State+mboxInit tc (pid, buffT, maxSz) = do+  cc <- liftIO $ atomically $ readTChan tc+  return $ InitOk (State Seq.empty $ defaultState buffT maxSz pid cc) Infinity++--------------------------------------------------------------------------------+-- Client Facing API                                                          --+--------------------------------------------------------------------------------++-- | Monitor a mailbox.+--+monitor :: Mailbox -> Process MonitorRef+monitor = P.monitor . pid++-- | Instructs the mailbox to send a 'NewMail' signal as soon as any mail is+-- available for delivery. Once the signal is sent, it will not be resent, even+-- when further mail arrives, until 'notify' is called again.+--+-- NB: signals are /only/ delivered to the mailbox's owning process.+--+notify :: Mailbox -> Process ()+notify mb = sendCtrlMsg mb $ SetActiveMode Notify++-- | Instructs the mailbox to send a 'Delivery' as soon as any mail is+-- available, or immediately (if the buffer already contains data).+--+-- NB: signals are /only/ delivered to the mailbox's owning process.+--+active :: Mailbox -> Filter -> Process ()+active mb f = sendCtrlMsg mb $ SetActiveMode $ Active f++-- | Alters the mailbox's /limit/ - this might cause messages to be dropped!+--+resize :: Mailbox -> Integer -> Process ()+resize mb sz = sendCtrlMsg mb $ Resize sz++-- | Posts a message to someone's mailbox.+--+post :: Serializable a => Mailbox -> a -> Process ()+post Mailbox{..} m = send pid (Post $ wrapMessage m)++-- | Obtain statistics (from/to anywhere) about a mailbox.+--+statistics :: Mailbox -> Process MailboxStats+statistics mb = call mb StatsReq++--------------------------------------------------------------------------------+-- PRIVATE Filter Implementation(s)                                           --+--------------------------------------------------------------------------------++everything :: Message -> Process FilterResult+everything _ = return Keep++matching :: Closure (Message -> Process FilterResult)+         -> Message+         -> Process FilterResult+matching predicate msg = do+  pred' <- unClosure predicate :: Process (Message -> Process FilterResult)+  res   <- handleMessage msg pred'+  case res of+    Nothing -> return Skip+    Just fr -> return fr++--------------------------------------------------------------------------------+-- Process Definition/State & API Handlers                                    --+--------------------------------------------------------------------------------++processDefinition :: ProcessId+                  -> TChan (ControlPort ControlMessage)+                  -> ControlChannel ControlMessage+                  -> Process (ProcessDefinition State)+processDefinition pid tc cc = do+  liftIO $ atomically $ writeTChan tc $ channelControlPort cc+  return $ defaultProcess { apiHandlers = [+                               handleControlChan     cc handleControlMessages+                             , Restricted.handleCall handleGetStats+                             ]+                          , infoHandlers = [ handleInfo handlePost+                                           , handleRaw  handleRawInputs ]+                          , unhandledMessagePolicy = DeadLetter pid+                          } :: Process (ProcessDefinition State)++handleControlMessages :: State+                      -> ControlMessage+                      -> Process (ProcessAction State)+handleControlMessages st cm+  | (SetActiveMode new) <- cm = activateMode st new+  | (Resize sz')        <- cm = continue $ adjust sz' st+  | otherwise                 = stop $ ExitOther "IllegalState"+  where+    activateMode :: State -> Mode -> Process (ProcessAction State)+    activateMode st' new+      | sz <- (st ^. state ^. size)+      , sz == 0           = continue $ updated st' new+      | otherwise         = do+          let updated' = updated st' new+          case new of+            Notify     -> sendNotification updated' >> continue updated'+            (Active _) -> sendMail updated' >>= continue+            Passive    -> {- shouldn't happen! -} die $ "IllegalState"++    updated s m = (state .> mode ^= m) s++handleGetStats :: StatsReq -> RestrictedProcess State (Result MailboxStats)+handleGetStats _ = Restricted.reply . (^. stats) =<< getState++handleRawInputs :: State -> Message -> Process (ProcessAction State)+handleRawInputs st msg = handlePost st (Post msg)++handlePost :: State -> Post -> Process (ProcessAction State)+handlePost st (Post msg) = do+  let st' = insert msg st+  continue . (state .> mode ^= Passive) =<< forwardIfNecessary st'+  where+    forwardIfNecessary s+      | Notify   <- currentMode = sendNotification s >> return s+      | Active _ <- currentMode = sendMail s+      | otherwise               = return s++    currentMode = st ^. state ^. mode++--------------------------------------------------------------------------------+-- Accessors, State/Stats Management & Utilities                              --+--------------------------------------------------------------------------------++sendNotification :: State -> Process ()+sendNotification st = do+    pid <- getSelfPid+    send ownerPid $ NewMail (Mailbox pid cchan) pending+  where+    ownerPid = st ^. state ^. owner+    pending  = st ^. state ^. size+    cchan    = ctrlChan (st ^. state)++type Count = Integer+type Skipped = Integer++sendMail :: State -> Process State+sendMail st = do+    let Active f = st ^. state ^. mode+    unCl <- catch (unClosure f >>= return . Just)+                  (\(_ :: SomeException) -> return Nothing)+    case unCl of+      Nothing -> return st -- TODO: Logging!?+      Just f' -> do+        (st', cnt, skipped, msgs) <- applyFilter f' st+        us <- getSelfPid+        send ownerPid $ Delivery { box          = Mailbox us (ctrlChan $ st ^. state)+                                 , messages     = Foldable.toList msgs+                                 , count        = cnt+                                 , totalDropped = skipped + droppedMsgs+                                 }+        return $ ( (state .> dropped ^= 0)+                 . (state .> size ^: ((cnt + skipped) -))+                 $ st' )+  where+    applyFilter f s = filterMessages f (s, 0, 0, Seq.empty)++    filterMessages :: (Message -> Process FilterResult)+                   -> (State, Count, Skipped, Seq Message)+                   -> Process (State, Count, Skipped, Seq Message)+    filterMessages f accIn@(buff, cnt, drp, acc) = do+      case pop buff of+        Nothing         -> return accIn+        Just (m, buff') -> do+          res <- f m+          case res of+            Keep -> filterMessages f (buff', cnt + 1, drp, acc |> m)+            Skip -> filterMessages f (buff', cnt, drp + 1, acc)+            Send -> return accIn++    ownerPid    = st ^. state ^. owner+    droppedMsgs = st ^. state ^. dropped++insert :: Message -> State -> State+insert msg st@(State _ BufferState{..}) =+  if _size /= _limit+     then push msg st+     else case _bufferT of+            Ring -> (state .> dropped ^: (+1)) st+            _    -> push msg $ drop 1 st++isQueue :: State -> Bool+isQueue = (== Queue) . _bufferT . _state++isStack :: State -> Bool+isStack = (== Stack) . _bufferT . _state++getR :: Seq a -> Maybe (ViewR a)+getR s =+  case Seq.viewr s of+    EmptyR -> Nothing+    a      -> Just a++getL :: Seq a -> Maybe (ViewL a)+getL s =+  case Seq.viewl s of+    EmptyL -> Nothing+    a      -> Just a++mode :: Accessor BufferState Mode+mode = accessor _mode (\m st -> st { _mode = m })++bufferType :: Accessor BufferState BufferType+bufferType = accessor _bufferT (\t st -> st { _bufferT = t })++limit :: Accessor BufferState Limit+limit = accessor _limit (\l st -> st { _limit = l })++size :: Accessor BufferState Integer+size = accessor _size (\s st -> st { _size = s })++dropped :: Accessor BufferState Integer+dropped = accessor _dropped (\d st -> st { _dropped = d })++owner :: Accessor BufferState ProcessId+owner = accessor _owner (\o st -> st { _owner = o })++buffer :: Accessor State (Seq Message)+buffer = accessor _buffer (\b qb -> qb { _buffer = b })++state :: Accessor State BufferState+state = accessor _state (\s qb -> qb { _state = s })++stats :: Accessor State MailboxStats+stats = accessor getStats (\_ s -> s) -- TODO: use a READ ONLY accessor for this+  where+    getStats (State _ (BufferState _ _ lm sz dr op _)) = MailboxStats sz dr lm op++$(remotable ['everything, 'matching])++-- | A /do-nothing/ filter that accepts all messages (i.e., returns @Keep@+-- for any input).+acceptEverything :: Closure (Message -> Process FilterResult)+acceptEverything = $(mkStaticClosure 'everything)++-- | A filter that takes a @Closure (Message -> Process FilterResult)@ holding+-- the filter function and applies it remotely (i.e., in the mailbox's own+-- managed process).+--+acceptMatching :: Closure (Closure (Message -> Process FilterResult)+                           -> Message -> Process FilterResult)+acceptMatching = $(mkStaticClosure 'matching)++-- | Instructs the mailbox to deliver all pending messages to the owner.+--+deliver :: Mailbox -> Process ()+deliver mb = active mb acceptEverything+
+ tests/MailboxTestFilters.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE TemplateHaskell     #-}+{-# LANGUAGE ScopedTypeVariables #-}++module MailboxTestFilters where++import Control.Distributed.Process+import Control.Distributed.Process.Execution.Mailbox (FilterResult(..))+import Control.Monad (forM)++#if ! MIN_VERSION_base(4,6,0)+import Prelude hiding (catch, drop)+#else+import Prelude hiding (drop)+#endif+import Data.Maybe (catMaybes)+import Control.Distributed.Process.Closure (remotable, mkClosure, mkStaticClosure)++filterInputs :: (String, Int, Bool) -> Message -> Process FilterResult+filterInputs (s, i, b) msg = do+  rs <- forM [ \m -> handleMessageIf m (\s' -> s' == s) (\_ -> return Keep)+             , \m -> handleMessageIf m (\i' -> i' == i) (\_ -> return Keep)+             , \m -> handleMessageIf m (\b' -> b' == b) (\_ -> return Keep)+             ] $ \h -> h msg+  if (length (catMaybes rs) > 0)+    then return Keep+    else return Skip++filterEvens :: Message -> Process FilterResult+filterEvens m = do+  matched <- handleMessage m (\(i :: Int) -> do+                               if even i then return Keep else return Skip)+  case matched of+    Just fr -> return fr+    _       -> return Skip++$(remotable ['filterInputs, 'filterEvens])++intFilter :: Closure (Message -> Process FilterResult)+intFilter = $(mkStaticClosure 'filterEvens)++myFilter :: (String, Int, Bool) -> Closure (Message -> Process FilterResult)+myFilter = $(mkClosure 'filterInputs)+
+ tests/TestExchange.hs view
@@ -0,0 +1,186 @@+{-# LANGUAGE BangPatterns          #-}+{-# LANGUAGE TemplateHaskell       #-}+{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module Main where++import Control.Distributed.Process hiding (monitor)+import Control.Distributed.Process.Node+import Control.Distributed.Process.Execution.EventManager hiding (start)+import qualified Control.Distributed.Process.Extras+import Control.Distributed.Process.Execution.Exchange+import Control.Distributed.Process.Extras.Internal.Types+import Control.Distributed.Process.Extras.Internal.Primitives+import qualified Control.Distributed.Process.Execution.EventManager as EventManager+  ( start+  )+import Control.Monad (void, forM, forever)+import Control.Rematch (equalTo)++#if ! MIN_VERSION_base(4,6,0)+import Prelude hiding (catch, drop)+#else+import Prelude hiding (drop)+#endif+import qualified Network.Transport as NT+import Test.Framework as TF (testGroup, Test)+import Test.Framework.Providers.HUnit+import TestUtils++testKeyBasedRouting :: TestResult Bool -> Process ()+testKeyBasedRouting result = do+  (sp, rp) <- newChan :: Process (Channel Int)+  rex <- messageKeyRouter PayloadOnly++  -- Since the /router/ doesn't offer a syncrhonous start+  -- option, we use spawnSignalled to get the same effect,+  -- making it more likely (though it's not guaranteed) that+  -- the spawned process will be bound to the routing exchange+  -- prior to our evaluating 'routeMessage' below.+  void $ spawnSignalled (bindKey "foobar" rex) $ const $ do+    receiveWait [ match (\(s :: Int) -> sendChan sp s) ]++  routeMessage rex (createMessage "foobar" [] (123 :: Int))+  stash result . (== (123 :: Int)) =<< receiveChan rp++testMultipleRoutes :: TestResult () -> Process ()+testMultipleRoutes result = do+  stash result ()    -- we don't rely on the test result for assertions...+  (sp, rp) <- newChan+  rex <- messageKeyRouter PayloadOnly+  let recv = receiveWait [+          match (\(s :: String) -> getSelfPid >>= \us -> sendChan sp (us, Left s))+        , match (\(i :: Int) -> getSelfPid >>= \us -> sendChan sp (us, Right i))+        ]++  us <- getSelfPid+  p1 <- spawnSignalled (link us >> bindKey "abc" rex) (const $ forever recv)+  p2 <- spawnSignalled (link us >> bindKey "def" rex) (const $ forever recv)+  p3 <- spawnSignalled (link us >> bindKey "abc" rex) (const $ forever recv)++  -- publish 2 messages with the routing-key set to 'abc'+  routeMessage rex (createMessage "abc" [] "Hello")+  routeMessage rex (createMessage "abc" [] (123 :: Int))++  -- route another message with the 'abc' value a header (should be ignored)+  routeMessage rex (createMessage "" [("abc", "abc")] "Goodbye")++  received <- forM (replicate (2 * 3) us) (const $ receiveChanTimeout 1000 rp)++  -- all bindings for 'abc' fired correctly+  received `shouldContain` Just (p1, Left "Hello")+  received `shouldContain` Just (p3, Left "Hello")+  received `shouldContain` Just (p1, Right (123 :: Int))+  received `shouldContain` Just (p3, Right (123 :: Int))++  -- however the bindings for 'def' never fired+  received `shouldContain` Nothing+  received `shouldNotContain` Just (p2, Left "Hello")+  received `shouldNotContain` Just (p2, Right (123 :: Int))++  -- none of the bindings should have examined the headers!+  received `shouldNotContain` Just (p1, Left "Goodbye")+  received `shouldNotContain` Just (p2, Left "Goodbye")+  received `shouldNotContain` Just (p3, Left "Goodbye")++testHeaderBasedRouting :: TestResult () -> Process ()+testHeaderBasedRouting result = do+  stash result ()  -- we don't rely on the test result for assertions...+  (sp, rp) <- newChan+  rex <- headerContentRouter PayloadOnly "x-name"+  let recv = const $ forever $ receiveWait [+          match (\(s :: String) -> getSelfPid >>= \us -> sendChan sp (us, Left s))+        , match (\(i :: Int) -> getSelfPid >>= \us -> sendChan sp (us, Right i))+        ]++  us <- getSelfPid+  p1 <- spawnSignalled (link us >> bindHeader "x-name" "yellow" rex) recv+  p2 <- spawnSignalled (link us >> bindHeader "x-name" "red"    rex) recv+  _  <- spawnSignalled (link us >> bindHeader "x-type" "fast"   rex) recv++  -- publish 2 messages with the routing-key set to 'abc'+  routeMessage rex (createMessage "" [("x-name", "yellow")] "Hello")+  routeMessage rex (createMessage "" [("x-name", "yellow")] (123 :: Int))+  routeMessage rex (createMessage "" [("x-name", "red")]    (456 :: Int))+  routeMessage rex (createMessage "" [("x-name", "red")]    (789 :: Int))+  routeMessage rex (createMessage "" [("x-type", "fast")]   "Goodbye")++  -- route another message with the 'abc' value a header (should be ignored)+  routeMessage rex (createMessage "" [("abc", "abc")] "FooBar")++  received <- forM (replicate 5 us) (const $ receiveChanTimeout 1000 rp)++  -- all bindings fired correctly+  received `shouldContain` Just (p1, Left "Hello")+  received `shouldContain` Just (p1, Right (123 :: Int))+  received `shouldContain` Just (p2, Right (456 :: Int))+  received `shouldContain` Just (p2, Right (789 :: Int))+  received `shouldContain` Nothing++  -- simple check that no other bindings have fired+  length received `shouldBe` equalTo (5 :: Int)++testSimpleEventHandling :: TestResult Bool -> Process ()+testSimpleEventHandling result = do+  (sp, rp) <- newChan+  (sigStart, recvStart) <- newChan+  em <- EventManager.start+  Just pid <- resolve em+  void $ monitor pid++  -- Note that in our init (state) function, we write a "start signal"+  -- here; Without a start signal, the message sent to the event manager+  -- (via notify) would race with the addHandler registration.+  pid' <- addHandler em (myHandler sp) (sendChan sigStart ())+  link pid'++  () <- receiveChan recvStart++  notify em ("hello", "event", "manager") -- cast message+  r <- receiveTimeout 100000000 [+      matchChan rp return+    , match (\(ProcessMonitorNotification _ _ _) -> die "ServerDied")+    ]+  case r of+    Just ("hello", "event", "manager") -> stash result True+    _                                  -> stash result False++myHandler :: SendPort (String, String, String)+          -> ()+          -> (String, String, String)+          -> Process ()+myHandler sp s m@(_, _, _) = sendChan sp m >> return s++myRemoteTable :: RemoteTable+myRemoteTable =+  Control.Distributed.Process.Extras.__remoteTable initRemoteTable++tests :: NT.Transport  -> IO [Test]+tests transport = do+  localNode <- newLocalNode transport myRemoteTable+  return [+        testGroup "Event Manager"+        [+          testCase "Simple Event Handlers"+          (delayedAssertion "Expected the handler to run"+           localNode True testSimpleEventHandling)+        ]++      , testGroup "Router"+        [+          testCase "Direct Key Routing"+          (delayedAssertion "Expected the sole matching route to run"+           localNode True testKeyBasedRouting)+        , testCase "Key Based Selective Routing"+          (delayedAssertion "Expected only the matching routes to run"+           localNode () testMultipleRoutes)+        , testCase "Header Based Selective Routing"+          (delayedAssertion "Expected only the matching routes to run"+           localNode () testHeaderBasedRouting)+        ]+    ]++main :: IO ()+main = testMain $ tests+
+ tests/TestMailbox.hs view
@@ -0,0 +1,256 @@+{-# LANGUAGE BangPatterns        #-}+{-# LANGUAGE TemplateHaskell     #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main where++import Control.Distributed.Process+import Control.Distributed.Process.Node+import qualified Control.Distributed.Process.Extras (__remoteTable)+import qualified Control.Distributed.Process.Execution.Mailbox (__remoteTable)+import Control.Distributed.Process.Execution.Mailbox+import Control.Distributed.Process.Extras.Time+import Control.Distributed.Process.Extras.Timer++import Control.Rematch (equalTo)++#if ! MIN_VERSION_base(4,6,0)+import Prelude hiding (catch, drop)+#else+import Prelude hiding (drop)+#endif++import Data.Maybe (catMaybes)++import Test.Framework as TF (testGroup, Test)+import Test.Framework.Providers.HUnit+import TestUtils+import qualified MailboxTestFilters (__remoteTable)+import MailboxTestFilters (myFilter, intFilter)++import qualified Network.Transport as NT++-- TODO: This whole test suite would be much better off using QuickCheck.+-- The test-framework driver however, doesn't have the API support we'd need+-- to wire in our tests, so we'll have to write a compatibility layer.+-- That should probably go into (or beneath) the C.D.P.P.Test module.++allBuffersShouldRespectFIFOOrdering :: BufferType -> TestResult Bool -> Process ()+allBuffersShouldRespectFIFOOrdering buffT result = do+  let [a, b, c] = ["a", "b", "c"]+  mbox <- createAndSend buffT [a, b, c]+  active mbox acceptEverything+  Just Delivery { messages = msgs } <- receiveTimeout (after 2 Seconds)+                                                         [ match return ]+  let [ma', mb', mc'] = msgs+  Just a' <- unwrapMessage ma' :: Process (Maybe String)+  Just b' <- unwrapMessage mb' :: Process (Maybe String)+  Just c' <- unwrapMessage mc' :: Process (Maybe String)+  let values = [a', b', c']+  stash result $ values == [a, b, c]+--  if values /= [a, b, c]+--     then liftIO $ putStrLn $ "unexpected " ++ ((show buffT) ++ (" values: " ++ (show values)))+--     else return ()++resizeShouldRespectOrdering :: BufferType+           -> TestResult [String]+           -> Process ()+resizeShouldRespectOrdering buffT result = do+  let [a, b, c, d, e] = ["a", "b", "c", "d", "e"]+  mbox <- createAndSend buffT [a, b, c, d, e]+  resize mbox (3 :: Integer)++  active mbox acceptEverything+  Just Delivery{ messages = msgs } <- receiveTimeout (after 2 Seconds) [ match return ]++  let [mc', md', me'] = msgs+  Just c' <- unwrapMessage mc' :: Process (Maybe String)+  Just d' <- unwrapMessage md' :: Process (Maybe String)+  Just e' <- unwrapMessage me' :: Process (Maybe String)+  let values = [c', d', e']+  stash result $ values++bufferLimiting :: BufferType -> TestResult (Integer, [Maybe String]) -> Process ()+bufferLimiting buffT result = do+  let msgs = ["a", "b", "c", "d", "e", "f", "g"]+  mbox <- createMailboxAndPost buffT 4 msgs++  MailboxStats{ pendingMessages = pending'+              , droppedMessages = dropped'+              , currentLimit    = limit' } <- statistics mbox+  pending' `shouldBe` equalTo 4+  dropped' `shouldBe` equalTo 3+  limit'   `shouldBe` equalTo 4++  active mbox acceptEverything+  Just Delivery{ messages = recvd+               , totalDropped = skipped } <- receiveTimeout (after 5 Seconds)+                                                            [ match return ]+  seen <- mapM unwrapMessage recvd+  stash result (skipped, seen)++mailboxIsInitiallyPassive :: TestResult Bool -> Process ()+mailboxIsInitiallyPassive result = do+  mbox <- createMailbox Stack (6 :: Integer)+  mapM_ (post mbox) ([1..5] :: [Int])+  Nothing <- receiveTimeout (after 3 Seconds) [ matchAny return ]+  notify mbox+  inbound <- receiveTimeout (after 3 Seconds) [ match return ]+  case inbound of+    Just (NewMail _ _) -> stash result True+    Nothing            -> stash result False++complexMailboxFiltering :: (String, Int, Bool)+                        -> TestResult (String, Int, Bool)+                        -> Process ()+complexMailboxFiltering inputs@(s', i', b') result = do+  mbox <- createMailbox Stack (10 :: Integer)+  post mbox s'+  post mbox i'+  post mbox b'+  waitForMailboxReady mbox 3++  active mbox $ myFilter inputs+  Just Delivery{ messages = [m1, m2, m3]+               , totalDropped = _ } <- receiveTimeout (after 5 Seconds)+                                                      [ match return ]+  Just s <- unwrapMessage m1 :: Process (Maybe String)+  Just i <- unwrapMessage m2 :: Process (Maybe Int)+  Just b <- unwrapMessage m3 :: Process (Maybe Bool)+  stash result $ (s, i, b)++dropDuringFiltering :: TestResult Bool -> Process ()+dropDuringFiltering result = do+  let rng = [1..50] :: [Int]+  mbox <- createMailbox Stack (50 :: Integer)+  mapM_ (post mbox) rng++  waitForMailboxReady mbox 50+  active mbox $ intFilter++  Just Delivery{ messages = msgs } <- receiveTimeout (after 5 Seconds)+                                                     [ match return ]+  seen <- mapM unwrapMessage msgs+  stash result $ (catMaybes seen) == (filter even rng)++mailboxHandleReUse :: TestResult Bool -> Process ()+mailboxHandleReUse result = do+  mbox <- createMailbox Queue (1 :: Limit)+  post mbox "abc"++  notify mbox+  Just (NewMail mbox' _) <- receiveTimeout (after 2 Seconds)+                                           [ match return ]+  deliver mbox'+  _ <- expect :: Process Delivery+  stash result True++createAndSend :: BufferType -> [String] -> Process Mailbox+createAndSend buffT msgs = createMailboxAndPost buffT 10 msgs++createMailboxAndPost :: BufferType -> Limit -> [String] -> Process Mailbox+createMailboxAndPost buffT maxSz msgs = do+  (cc, cp) <- newChan+  mbox <- createMailbox buffT maxSz+  spawnLocal $ mapM_ (post mbox) msgs >> sendChan cc ()+  () <- receiveChan cp+  waitForMailboxReady mbox $ min (toInteger (length msgs)) maxSz+  return mbox++waitForMailboxReady :: Mailbox -> Integer -> Process ()+waitForMailboxReady mbox sz = do+  sleep $ seconds 1+  notify mbox+  m <- receiveWait [+            matchIf (\(NewMail mbox' sz') -> mbox == mbox' && sz' >= sz)+                    (\_ -> return True)+          , match (\(NewMail _ _) -> return False)+          , matchAny (\_ -> return False)+          ]+  case m of+    True  -> return ()+    False -> waitForMailboxReady mbox sz++myRemoteTable :: RemoteTable+myRemoteTable =+  Control.Distributed.Process.Execution.Mailbox.__remoteTable $+  Control.Distributed.Process.Extras.__remoteTable $+  MailboxTestFilters.__remoteTable initRemoteTable++tests :: NT.Transport  -> IO [Test]+tests transport = do+  {- verboseCheckWithResult stdArgs -}+  localNode <- newLocalNode transport myRemoteTable+  return [+        testGroup "Dequeue/Pop Ordering"+        [+          testCase "Queue Ordering"+          (delayedAssertion+           "Expected the Queue to offer FIFO ordering"+           localNode True (allBuffersShouldRespectFIFOOrdering Queue))+	   +        , testCase "Stack Ordering"+          (delayedAssertion+           "Expected the Queue to offer FIFO ordering"+           localNode True (allBuffersShouldRespectFIFOOrdering Stack))+        , testCase "Ring Ordering"+           (delayedAssertion+            "Expected the Queue to offer FIFO ordering"+            localNode True (allBuffersShouldRespectFIFOOrdering Ring))+        ]+      , testGroup "Resize & Ordering"+        [+          testCase "Queue Drops Eldest"+          (delayedAssertion+           "expected c, d, e"+           localNode ["c", "d", "e"] $ resizeShouldRespectOrdering Queue)+        , testCase "Stack Drops Youngest"+          (delayedAssertion+           "expected a, b, c"+           localNode ["a", "b", "c"] $ resizeShouldRespectOrdering Stack)+        , testCase "Ring Drops Youngest"+          (delayedAssertion+           "expected a, b, c"+           localNode ["a", "b", "c"] $ resizeShouldRespectOrdering Ring)+        ]+      , testGroup "Buffer Limits & Discarded Messages"+        [+          testCase "Queue Drops Eldest and Enqueues New"+          (delayedAssertion+           "expected d, e, f, g"+           localNode ((3 :: Integer), map Just ["d", "e", "f", "g"]) $ bufferLimiting Queue)+        , testCase "Stack Drops Youngest And Pushes New"+          (delayedAssertion+           "expected a, b, c, g"+           localNode ((3 :: Integer), map Just ["a", "b", "c", "g"]) $ bufferLimiting Stack)+        , testCase "Ring Rejects New Entries"+          (delayedAssertion+           "expected a, b, c, d"+           localNode ((3 :: Integer), map Just ["a", "b", "c", "d"]) $ bufferLimiting Ring)+        ]+      , testGroup "Notification, Activation and Delivery"+        [+          testCase "Mailbox is initially Passive"+           (delayedAssertion+            "Expected the Mailbox to remain passive until told otherwise"+            localNode True mailboxIsInitiallyPassive)+        , testCase "Mailbox Notifications include usable control channel"+           (delayedAssertion+            "Expected traffic to be relayed directly to us"+            localNode True mailboxHandleReUse)+        , testCase "Complex Filtering Rules"+           (delayedAssertion+            "Expected the relevant filters to accept our data"+            localNode inputs (complexMailboxFiltering inputs))+        , testCase "Filter out unwanted messages"+           (delayedAssertion+            "Expected only even numbers to be sent delivered"+            localNode True dropDuringFiltering)+        ]+    ]+  where+    inputs = ("hello", 10 :: Int, True)++main :: IO ()+main = testMain $ tests+
+ tests/TestUtils.hs view
@@ -0,0 +1,234 @@+{-# LANGUAGE DeriveDataTypeable        #-}+{-# LANGUAGE TemplateHaskell           #-}+{-# LANGUAGE DeriveGeneric             #-}++module TestUtils+  ( TestResult+    -- ping !+  , Ping(Ping)+  , ping+  , shouldBe+  , shouldMatch+  , shouldContain+  , shouldNotContain+  , shouldExitWith+  , expectThat+  -- test process utilities+  , TestProcessControl+  , startTestProcess+  , runTestProcess+  , testProcessGo+  , testProcessStop+  , testProcessReport+  , delayedAssertion+  , assertComplete+  , waitForExit+  -- logging+  , Logger()+  , newLogger+  , putLogMsg+  , stopLogger+  -- runners+  , mkNode+  , tryRunProcess+  , testMain+  , stash+  ) where++#if ! MIN_VERSION_base(4,6,0)+import Prelude hiding (catch)+#endif+import Control.Concurrent+  ( ThreadId+  , myThreadId+  , forkIO+  )+import Control.Concurrent.STM+  ( TQueue+  , newTQueueIO+  , readTQueue+  , writeTQueue+  )+import Control.Concurrent.MVar+  ( MVar+  , newEmptyMVar+  , takeMVar+  , putMVar+  )++import Control.Distributed.Process+import Control.Distributed.Process.Node+import Control.Distributed.Process.Serializable()+import Control.Distributed.Process.Extras.Time+import Control.Distributed.Process.Extras.Timer+import Control.Distributed.Process.Extras.Internal.Types+import Control.Exception (SomeException)+import qualified Control.Exception as Exception+import Control.Monad (forever)+import Control.Monad.STM (atomically)+import Control.Rematch hiding (match)+import Control.Rematch.Run+import Test.HUnit (Assertion, assertFailure)+import Test.HUnit.Base (assertBool)+import Test.Framework (Test, defaultMain)+import Control.DeepSeq++import Network.Transport.TCP+import qualified Network.Transport as NT++import Data.Binary+import Data.Typeable+import GHC.Generics++--expect :: a -> Matcher a -> Process ()+--expect a m = liftIO $ Rematch.expect a m++expectThat :: a -> Matcher a -> Process ()+expectThat a matcher = case res of+  MatchSuccess -> return ()+  (MatchFailure msg) -> liftIO $ assertFailure msg+  where res = runMatch matcher a++shouldBe :: a -> Matcher a -> Process ()+shouldBe = expectThat++shouldContain :: (Show a, Eq a) => [a] -> a -> Process ()+shouldContain xs x = expectThat xs $ hasItem (equalTo x)++shouldNotContain :: (Show a, Eq a) => [a] -> a -> Process ()+shouldNotContain xs x = expectThat xs $ isNot (hasItem (equalTo x))++shouldMatch :: a -> Matcher a -> Process ()+shouldMatch = expectThat++shouldExitWith :: (Addressable a) => a -> DiedReason -> Process ()+shouldExitWith a r = do+  _ <- resolve a+  d <- receiveWait [ match (\(ProcessMonitorNotification _ _ r') -> return r') ]+  d `shouldBe` equalTo r++waitForExit :: MVar ExitReason+            -> Process (Maybe ExitReason)+waitForExit exitReason = do+    -- we *might* end up blocked here, so ensure the test doesn't jam up!+  self <- getSelfPid+  tref <- killAfter (within 10 Seconds) self "testcast timed out"+  tr <- liftIO $ takeMVar exitReason+  cancelTimer tref+  case tr of+    ExitNormal -> return Nothing+    other      -> return $ Just other++mkNode :: String -> IO LocalNode+mkNode port = do+  Right (transport1, _) <- createTransportExposeInternals+                                    "127.0.0.1" port defaultTCPParameters+  newLocalNode transport1 initRemoteTable++-- | Run the supplied @testProc@ using an @MVar@ to collect and assert+-- against its result. Uses the supplied @note@ if the assertion fails.+delayedAssertion :: (Eq a) => String -> LocalNode -> a ->+                    (TestResult a -> Process ()) -> Assertion+delayedAssertion note localNode expected testProc = do+  result <- newEmptyMVar+  _ <- forkProcess localNode $ testProc result+  assertComplete note result expected++-- | Takes the value of @mv@ (using @takeMVar@) and asserts that it matches @a@+assertComplete :: (Eq a) => String -> MVar a -> a -> IO ()+assertComplete msg mv a = do+  b <- takeMVar mv+  assertBool msg (a == b)++-- synchronised logging++data Logger = Logger { _tid :: ThreadId, msgs :: TQueue String }++-- | Create a new Logger.+-- Logger uses a 'TQueue' to receive and process messages on a worker thread.+newLogger :: IO Logger+newLogger = do+  tid <- liftIO $ myThreadId+  q <- liftIO $ newTQueueIO+  _ <- forkIO $ logger q+  return $ Logger tid q+  where logger q' = forever $ do+        msg <- atomically $ readTQueue q'+        putStrLn msg++-- | Send a message to the Logger+putLogMsg :: Logger -> String -> Process ()+putLogMsg logger msg = liftIO $ atomically $ writeTQueue (msgs logger) msg++-- | Stop the worker thread for the given Logger+stopLogger :: Logger -> IO ()+stopLogger = (flip Exception.throwTo) Exception.ThreadKilled . _tid++-- | Given a @builder@ function, make and run a test suite on a single transport+testMain :: (NT.Transport -> IO [Test]) -> IO ()+testMain builder = do+  Right (transport, _) <- createTransportExposeInternals+                                    "127.0.0.1" "10501" defaultTCPParameters+  testData <- builder transport+  defaultMain testData++-- | Runs a /test process/ around the supplied @proc@, which is executed+-- whenever the outer process loop receives a 'Go' signal.+runTestProcess :: Process () -> Process ()+runTestProcess proc = do+  ctl <- expect+  case ctl of+    Stop -> return ()+    Go -> proc >> runTestProcess proc+    Report p -> receiveWait [matchAny (\m -> forward m p)] >> runTestProcess proc++-- | Starts a test process on the local node.+startTestProcess :: Process () -> Process ProcessId+startTestProcess proc =+   spawnLocal $ do+     getSelfPid >>= register "test-process"+     runTestProcess proc++-- | Control signals used to manage /test processes/+data TestProcessControl = Stop | Go | Report ProcessId+  deriving (Typeable, Generic)++instance Binary TestProcessControl where++-- | A mutable cell containing a test result.+type TestResult a = MVar a++-- | Stashes a value in our 'TestResult' using @putMVar@+stash :: TestResult a -> a -> Process ()+stash mvar x = liftIO $ putMVar mvar x++-- | Tell a /test process/ to stop (i.e., 'terminate')+testProcessStop :: ProcessId -> Process ()+testProcessStop pid = send pid Stop++-- | Tell a /test process/ to continue executing+testProcessGo :: ProcessId -> Process ()+testProcessGo pid = send pid Go++-- | A simple @Ping@ signal+data Ping = Ping+  deriving (Typeable, Generic, Eq, Show)++instance Binary Ping where+instance NFData Ping where++ping :: ProcessId -> Process ()+ping pid = send pid Ping+++tryRunProcess :: LocalNode -> Process () -> IO ()+tryRunProcess node p = do+  tid <- liftIO myThreadId+  runProcess node $ catch p (\e -> liftIO $ Exception.throwTo tid (e::SomeException))++-- | Tell a /test process/ to send a report (message)+-- back to the calling process+testProcessReport :: ProcessId -> Process ()+testProcessReport pid = do+   self <- getSelfPid+   send pid $ Report self