packages feed

distributed-process-extras (empty) → 0.1.0

raw patch · 17 files changed

+2259/−0 lines, 17 filesdep +HUnitdep +QuickCheckdep +ansi-terminalsetup-changed

Dependencies added: HUnit, QuickCheck, ansi-terminal, base, binary, containers, deepseq, derive, distributed-process, distributed-process-extras, distributed-process-tests, fingertree, ghc-prim, hashable, mtl, 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-extras.cabal view
@@ -0,0 +1,112 @@+name:           distributed-process-extras+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-extras+Bug-Reports:    https://cloud-haskell.atlassian.net+synopsis:       Cloud Haskell Extras+description:    Supporting library, providing common types and utilities used by the+                various components that make up the distributed-process-platform package.+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-extras++flag perf+  description: Build with profiling enabled+  default: False++library+  build-depends:+                   base >= 4.4 && < 5,+                   distributed-process >= 0.5.2 && < 0.6,+                   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.Extras,+                   Control.Distributed.Process.Extras.Call,+                   Control.Distributed.Process.Extras.Time,+                   Control.Distributed.Process.Extras.Timer,+                   Control.Distributed.Process.Extras.UnsafePrimitives,+                   Control.Concurrent.Utils,+                   Control.Distributed.Process.Extras.Internal.Containers.MultiMap,+                   Control.Distributed.Process.Extras.Internal.Primitives,+                   Control.Distributed.Process.Extras.Internal.Types,+                   Control.Distributed.Process.Extras.Internal.Queue.SeqQ,+                   Control.Distributed.Process.Extras.Internal.Queue.PriorityQ+                   Control.Distributed.Process.Extras.Internal.Unsafe++test-suite InternalQueueTests+  type:            exitcode-stdio-1.0+  x-uses-tf:       true+  build-depends:+                   base >= 4.4 && < 5,+                   ansi-terminal >= 0.5 && < 0.6,+                   distributed-process >= 0.5.2 && < 0.6,+                   distributed-process-extras,+                   distributed-process-tests >= 0.4.1 && < 0.5,+                   HUnit >= 1.2 && < 2,+                   test-framework >= 0.6 && < 0.9,+                   test-framework-hunit,+                   QuickCheck >= 2.4,+                   test-framework-quickcheck2,+                   rematch >= 0.2.0.0,+                   ghc-prim+  hs-source-dirs:+                   tests+  ghc-options:     -Wall -rtsopts+  extensions:      CPP+  main-is:         TestQueues.hs+  cpp-options:     -DTESTING++test-suite TimerTests+  type:            exitcode-stdio-1.0+  x-uses-tf:       true+  build-depends:+                   base >= 4.4 && < 5,+                   ansi-terminal >= 0.5 && < 0.6,+                   deepseq >= 1.3.0.1 && < 1.4,+                   distributed-process >= 0.5.2 && < 0.6,+                   distributed-process-extras,+                   distributed-process-tests >= 0.4.1 && < 0.5,+                   network-transport >= 0.4 && < 0.5,+                   network-transport-tcp >= 0.4 && < 0.5,+                   HUnit >= 1.2 && < 2,+                   test-framework >= 0.6 && < 0.9,+                   test-framework-hunit,+                   QuickCheck >= 2.4,+                   test-framework-quickcheck2,+                   rematch >= 0.2.0.0,+                   ghc-prim+  hs-source-dirs:+                   tests+  ghc-options:     -Wall -rtsopts+  extensions:      CPP+  main-is:         TestTimer.hs+  cpp-options:     -DTESTING
+ src/Control/Concurrent/Utils.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables   #-}++module Control.Concurrent.Utils+  ( Lock()+  , Exclusive(..)+  , Synchronised(..)+  , withLock+  ) where++import Control.Distributed.Process+  ( Process+  )+import qualified Control.Distributed.Process as Process (catch)+import Control.Exception (SomeException, throw)+import qualified Control.Exception as Exception (catch)+import Control.Concurrent.MVar+  ( MVar+  , tryPutMVar+  , newMVar+  , takeMVar+  )+import Control.Monad.IO.Class (MonadIO, liftIO)++newtype Lock = Lock { mvar :: MVar () }++class Exclusive a where+  new          :: IO a+  acquire      :: (MonadIO m) => a -> m ()+  release      :: (MonadIO m) => a -> m ()++instance Exclusive Lock where+  new       = return . Lock =<< newMVar ()+  acquire   = liftIO . takeMVar . mvar+  release l = liftIO (tryPutMVar (mvar l) ()) >> return ()++class Synchronised e m where+  synchronised :: (Exclusive e, Monad m) => e -> m b -> m b++  synchronized :: (Exclusive e, Monad m) => e -> m b -> m b+  synchronized = synchronised++instance Synchronised Lock IO where+  synchronised = withLock++instance Synchronised Lock Process where+  synchronised = withLockP++withLockP :: (Exclusive e) => e -> Process a -> Process a+withLockP excl act = do+  Process.catch (do { liftIO $ acquire excl+                    ; result <- act+                    ; liftIO $ release excl+                    ; return result+                    })+                (\(e :: SomeException) -> (liftIO $ release excl) >> throw e)++withLock :: (Exclusive e) => e -> IO a -> IO a+withLock excl act = do+  Exception.catch (do { acquire excl+                      ; result <- act+                      ; release excl+                      ; return result+                      })+                  (\(e :: SomeException) -> release excl >> throw e)
+ src/Control/Distributed/Process/Extras.hs view
@@ -0,0 +1,124 @@+{- | [Cloud Haskell Extras]++[Evaluation Strategies and Support for NFData]++When sending messages to a local process (i.e., intra-node), the default+approach is to encode (i.e., serialise) the message /anyway/, just to+ensure that no unevaluated thunks are passed to the receiver.+In distributed-process, you must explicitly choose to use /unsafe/ primitives+that do nothing to ensure evaluation, since this might cause an error in the+receiver which would be difficult to debug. Using @NFData@, it is possible+to force evaluation, but there is no way to ensure that both the @NFData@+and @Binary@ instances do so in the same way (i.e., to the same depth, etc)+therefore automatic use of @NFData@ is not possible in distributed-process.++By contrast, distributed-process-platform makes extensive use of @NFData@+to force evaluation (and avoid serialisation overheads during intra-node+communication), via the @NFSerializable@ type class. This does nothing to+fix the potential disparity between @NFData@ and @Binary@ instances, so you+should verify that your data is being handled as expected (e.g., by sticking+to strict fields, or some such) and bear in mind that things could go wrong.++The @UnsafePrimitives@ module in /this/ library will force evaluation before+calling the @UnsafePrimitives@ in distributed-process, which - if you've+vetted everything correctly - should provide a bit more safety, whilst still+keeping performance at an acceptable level.++Users of the various service and utility models (such as @ManagedProcess@ and+the @Service@ and @Task@ APIs) should consult the sub-system specific+documentation for instructions on how to utilise these features.++IMPORTANT NOTICE: Despite the apparent safety of forcing evaluation before+sending, we /still/ cannot make any actual guarantees about the evaluation+semantics of these operations, and therefore the /unsafe/ moniker will remain+in place, in one form or another, for all functions and modules that use them.++[Error/Exception Handling]++It is /important/ not to be too general when catching exceptions in+cloud haskell application, because asynchonous exceptions provide cloud haskell+with its process termination mechanism. Two exception types in particular,+signal the instigator's intention to stop a process immediately, which are+raised (i.e., thrown) in response to the @kill@ and @exit@ primitives provided+by the base distributed-process package.++You should generally try to keep exception handling code to the lowest (i.e.,+most specific) scope possible. If you wish to trap @exit@ signals, use the+various flavours of @catchExit@ primitive from distributed-process.++-}+module Control.Distributed.Process.Extras+  (+    -- * Exported Types+    Addressable+  , sendToRecipient+  , Resolvable(..)+  , Routable(..)+  , Linkable(..)+  , Killable(..)+  , NFSerializable+  , Recipient(..)+  , Shutdown(..)+  , ExitReason(..)+  , CancelWait(..)+  , ServerDisconnected(..)+  , Channel+  , Tag+  , TagPool++    -- * Primitives overriding those in distributed-process+  , monitor+  , module Control.Distributed.Process.Extras.UnsafePrimitives++    -- * Utilities and Extended Primitives+  , spawnSignalled+  , spawnLinkLocal+  , spawnMonitorLocal+  , linkOnFailure+  , times+  , isProcessAlive+  , matchCond+  , deliver+  , awaitExit+  , awaitResponse++    -- * Call/Tagging support+  , newTagPool+  , getTag++    -- * Registration and Process Lookup+  , whereisOrStart+  , whereisOrStartRemote++    -- remote call table+  , __remoteTable+  ) where++import Control.Distributed.Process (RemoteTable)+import Control.Distributed.Process.Extras.Internal.Types+  ( NFSerializable+  , sendToRecipient+  , NFSerializable+  , Recipient(..)+  , Shutdown(..)+  , ExitReason(..)+  , CancelWait(..)+  , ServerDisconnected(..)+  , Channel+  , Tag+  , TagPool+  , newTagPool+  , getTag+  )+import Control.Distributed.Process.Extras.UnsafePrimitives+import Control.Distributed.Process.Extras.Internal.Primitives hiding (__remoteTable)+import qualified Control.Distributed.Process.Extras.Internal.Primitives (__remoteTable)+import qualified Control.Distributed.Process.Extras.Internal.Types      (__remoteTable)++-- remote table++__remoteTable :: RemoteTable -> RemoteTable+__remoteTable =+  Control.Distributed.Process.Extras.Internal.Primitives.__remoteTable .+  Control.Distributed.Process.Extras.Internal.Types.__remoteTable+
+ src/Control/Distributed/Process/Extras/Call.hs view
@@ -0,0 +1,243 @@+{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-}++-----------------------------------------------------------------------------+-- |+-- Module      :  Control.Distributed.Process.Extras.Call+-- Copyright   :  (c) Parallel Scientific (Jeff Epstein) 2012+-- License     :  BSD3 (see the file LICENSE)+--+-- Maintainers :  Jeff Epstein, Tim Watson+-- Stability   :  experimental+-- Portability :  non-portable (requires concurrency)+--+-- This module provides a facility for Remote Procedure Call (rpc) style+-- interactions with Cloud Haskell processes.+--+-- Clients make synchronous calls to a running process (i.e., server) using the+-- 'callAt', 'callTimeout' and 'multicall' functions. Processes acting as the+-- server are constructed using Cloud Haskell's 'receive' family of primitives+-- and the 'callResponse' family of functions in this module.+-----------------------------------------------------------------------------++module Control.Distributed.Process.Extras.Call+  ( -- client API+    callAt+  , callTimeout+  , multicall+    -- server API+  , callResponse+  , callResponseIf+  , callResponseDefer+  , callResponseDeferIf+  , callForward+  , callResponseAsync+  ) where++import Control.Distributed.Process+import Control.Distributed.Process.Serializable (Serializable)+import Control.Monad (forM, forM_, join)+import Data.List (delete)+import qualified Data.Map as M+import Data.Maybe (listToMaybe)+import Data.Binary (Binary,get,put)+import Data.Typeable (Typeable)++import Control.Distributed.Process.Extras hiding (monitor, send)+import Control.Distributed.Process.Extras.Time++----------------------------------------------+-- * Multicall+----------------------------------------------++-- | Sends a message of type a to the given process, to be handled by a+-- corresponding callResponse... function, which will send back a message of+-- type b. The tag is per-process unique identifier of the transaction. If the+-- timeout expires or the target process dies, Nothing will be returned.+callTimeout :: (Serializable a, Serializable b)+            => ProcessId -> a -> Tag -> Timeout -> Process (Maybe b)+callTimeout pid msg tag time =+  do res <- multicall [pid] msg tag time+     return $ join (listToMaybe res)++-- | Like 'callTimeout', but with no timeout.+-- Returns Nothing if the target process dies.+callAt :: (Serializable a, Serializable b)+       => ProcessId -> a -> Tag -> Process (Maybe b)+callAt pid msg tag = callTimeout pid msg tag infiniteWait++-- | Like 'callTimeout', but sends the message to multiple+-- recipients and collects the results.+multicall :: forall a b.(Serializable a, Serializable b)+             => [ProcessId] -> a -> Tag -> Timeout -> Process [Maybe b]+multicall nodes msg tag time =+  do caller <- getSelfPid+     receiver <- spawnLocal $+         do receiver_pid <- getSelfPid+            mon_caller <- monitor caller+            () <- expect+            monitortags <- forM nodes monitor+            forM_ nodes $ \node -> send node (Multicall, node,+                                              receiver_pid, tag, msg)+            maybeTimeout time tag receiver_pid+            results <- recv nodes monitortags mon_caller+            send caller (MulticallResponse,tag,results)+     mon_receiver <- monitor receiver+     send receiver ()+     receiveWait [+         matchIf (\(MulticallResponse,mtag,_) -> mtag == tag)+                 (\(MulticallResponse,_,val) -> return val),+         matchIf (\(ProcessMonitorNotification ref _pid reason)+                  -> ref == mon_receiver && reason /= DiedNormal)+                 (\_ -> error "multicall: unexpected termination of worker")+       ]+  where+    recv nodes' monitortags mon_caller = do+      resultmap <- recv1 mon_caller+                         (nodes', monitortags, M.empty) :: Process (M.Map ProcessId b)+      return $ ordered nodes' resultmap++    ordered []     _ = []+    ordered (x:xs) m = M.lookup x m : ordered xs m++    recv1 _   ([],_,results) = return results+    recv1 _   (_,[],results) = return results+    recv1 ref (nodesleft,monitortagsleft,results) =+          receiveWait [+              matchIf (\(ProcessMonitorNotification ref' _ _)+                         -> ref' == ref)+                      (\_ -> return Nothing)+            , matchIf (\(ProcessMonitorNotification ref' pid reason) ->+                      ref' `elem` monitortagsleft &&+                      pid `elem` nodesleft+                      && reason /= DiedNormal)+                      (\(ProcessMonitorNotification ref' pid _reason) ->+                        return $ Just (delete pid nodesleft,+                                       delete ref' monitortagsleft, results))+            , matchIf (\(MulticallResponse, mtag, _, _) -> mtag == tag)+                      (\(MulticallResponse, _, responder, msgx) ->+                        return $ Just (delete responder nodesleft,+                                       monitortagsleft,+                                       M.insert responder (msgx :: b) results))+            , matchIf (\(TimeoutNotification mtag) -> mtag == tag )+                      (\_ -> return Nothing)+          ]+          >>= maybe (return results) (recv1 ref)++data MulticallResponseType a =+         MulticallAccept+       | MulticallForward ProcessId a+       | MulticallReject deriving Eq++callResponseImpl :: (Serializable a,Serializable b)+                 => (a -> MulticallResponseType c) ->+                    (a -> (b -> Process())-> Process c) -> Match c+callResponseImpl cond proc =+  matchIf (\(Multicall,_responder,_,_,msg) ->+            case cond msg of+              MulticallReject -> False+              _               -> True)+          (\wholemsg@(Multicall,responder,sender,tag,msg) ->+            case cond msg of+              -- TODO: sender should get a ProcessMonitorNotification if+              -- our target dies, or we should link to it (?)+              MulticallForward target ret -> send target wholemsg >> return ret+              -- TODO: use `die Reason` when issue #110 is resolved+              MulticallReject -> error "multicallResponseImpl: Indecisive condition"+              MulticallAccept ->+                let resultSender tosend =+                      send sender (MulticallResponse,+                                   tag::Tag,+                                   responder::ProcessId,+                                   tosend)+                in proc msg resultSender)++-- | Produces a Match that can be used with the 'receiveWait' family of+-- message-receiving functions. @callResponse@ will respond to a message of+-- type a sent by 'callTimeout', and will respond with a value of type b.+callResponse :: (Serializable a,Serializable b)+                => (a -> Process (b,c)) -> Match c+callResponse = callResponseIf (const True)++callResponseDeferIf  :: (Serializable a,Serializable b)+                     => (a -> Bool)+                     -> (a -> (b -> Process()) -> Process c)+                     -> Match c+callResponseDeferIf cond =+  callResponseImpl (\msg ->+                     if cond msg+                     then MulticallAccept+                     else MulticallReject)++callResponseDefer  :: (Serializable a,Serializable b)+                   => (a -> (b -> Process())-> Process c) -> Match c+callResponseDefer = callResponseDeferIf (const True)++-- | Produces a Match that can be used with the 'receiveWait' family of+-- message-receiving functions. When calllForward receives a message of type+-- from from 'callTimeout' (and similar), it will forward the message to another+-- process, who will be responsible for responding to it. It is the user's+-- responsibility to ensure that the forwarding process is linked to the+-- destination process, so that if it fails, the sender will be notified.+callForward :: Serializable a => (a -> (ProcessId, c)) -> Match c+callForward proc =+   callResponseImpl+     (\msg -> let (pid, ret) = proc msg+              in MulticallForward pid ret )+     (\_ sender ->+       (sender::(() -> Process ())) `mention`+          error "multicallForward: Indecisive condition")++-- | The message handling code is started in a separate thread. It's not+-- automatically linked to the calling thread, so if you want it to be+-- terminated when the message handling thread dies, you'll need to call+-- link yourself.+callResponseAsync :: (Serializable a,Serializable b)+                  => (a -> Maybe c) -> (a -> Process b) -> Match c+callResponseAsync cond proc =+   callResponseImpl+         (\msg ->+            case cond msg of+              Nothing -> MulticallReject+              Just _ -> MulticallAccept)+         (\msg sender ->+            do _ <- spawnLocal $ -- TODO linkOnFailure to spawned procss+                 do val <- proc msg+                    sender val+               case cond msg of+                 Nothing -> error "multicallResponseAsync: Indecisive condition"+                 Just ret -> return ret )++callResponseIf :: (Serializable a,Serializable b)+               => (a -> Bool) -> (a -> Process (b,c)) -> Match c+callResponseIf cond proc =+    callResponseImpl+             (\msg ->+                 case cond msg of+                   True -> MulticallAccept+                   False -> MulticallReject)+             (\msg sender ->+                 do (tosend,toreturn) <- proc msg+                    sender tosend+                    return toreturn)++maybeTimeout :: Timeout -> Tag -> ProcessId -> Process ()+maybeTimeout Nothing _ _ = return ()+maybeTimeout (Just time) tag p = timeout time tag p++----------------------------------------------+-- * Private types+----------------------------------------------++mention :: a -> b -> b+mention _a b = b++data Multicall = Multicall+       deriving (Typeable)+instance Binary Multicall where+       get = return Multicall+       put _ = return ()+data MulticallResponse = MulticallResponse+       deriving (Typeable)+instance Binary MulticallResponse where+       get = return MulticallResponse+       put _ = return ()
+ src/Control/Distributed/Process/Extras/Internal/Containers/MultiMap.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE RecordWildCards            #-}+{-# LANGUAGE ExistentialQuantification  #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE UndecidableInstances       #-}++module Control.Distributed.Process.Extras.Internal.Containers.MultiMap+  ( MultiMap+  , Insertable+  , empty+  , insert+  , member+  , lookup+  , filter+  , filterWithKey+  , toList+  ) where++import qualified Data.Foldable as Foldable++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.Foldable (Foldable(..))+import Prelude hiding (lookup, filter, pred)++-- | Class of things that can be inserted in a map or+-- a set (of mapped values), for which instances of+-- @Eq@ and @Hashable@ must be present.+--+class (Eq a, Hashable a) => Insertable a+instance (Eq a, Hashable a) => Insertable a++-- | Opaque type of MultiMaps.+data MultiMap k v = M { hmap :: !(HashMap k (HashSet v)) }++-- instance Foldable++instance Foldable (MultiMap k) where+  foldr f = foldrWithKey (const f)++empty :: MultiMap k v+empty = M $ Map.empty++insert :: forall k v. (Insertable k, Insertable v)+       => k -> v -> MultiMap k v -> MultiMap k v+insert k' v' M{..} =+  case Map.lookup k' hmap of+    Nothing -> M $ Map.insert k' (Set.singleton v') hmap+    Just s  -> M $ Map.insert k' (Set.insert v' s) hmap+{-# INLINE insert #-}++member :: (Insertable k) => k -> MultiMap k a -> Bool+member k = Map.member k . hmap++lookup :: (Insertable k) => k -> MultiMap k v -> Maybe [v]+lookup k M{..} = maybe Nothing (Just . Foldable.toList) $ Map.lookup k hmap+{-# INLINE lookup #-}++filter :: forall k v. (Insertable k)+       => (v -> Bool)+       -> MultiMap k v+       -> MultiMap k v+filter p M{..} = M $ Map.foldlWithKey' (matchOn p) hmap hmap+  where+    matchOn pred acc key valueSet =+      Map.insert key (Set.filter pred valueSet) acc+{-# INLINE filter #-}++filterWithKey :: forall k v. (Insertable k)+              => (k -> v -> Bool)+              -> MultiMap k v+              -> MultiMap k v+filterWithKey p M{..} = M $ Map.foldlWithKey' (matchOn p) hmap hmap+  where+    matchOn pred acc key valueSet =+      Map.insert key (Set.filter (pred key) valueSet) acc+{-# INLINE filterWithKey #-}++-- | /O(n)/ Reduce this map by applying a binary operator to all+-- elements, using the given starting value (typically the+-- right-identity of the operator).+foldrWithKey :: (k -> v -> a -> a) -> a -> MultiMap k v -> a+foldrWithKey f a M{..} =+  let wrap = \k' v' acc' -> f k' v' acc'+  in Map.foldrWithKey (\k v acc -> Set.foldr (wrap k) acc v) a hmap+{-# INLINE foldrWithKey #-}++toList :: MultiMap k v -> [(k, v)]+toList M{..} = Map.foldlWithKey' explode [] hmap+  where+    explode xs k vs = Set.foldl' (\ys v -> ((k, v):ys)) xs vs+{-# INLINE toList #-}+
+ src/Control/Distributed/Process/Extras/Internal/Primitives.hs view
@@ -0,0 +1,290 @@+{-# LANGUAGE DeriveDataTypeable    #-}+{-# LANGUAGE DeriveGeneric         #-}+{-# LANGUAGE StandaloneDeriving    #-}+{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE TemplateHaskell       #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances     #-}++-----------------------------------------------------------------------------+-- |+-- Module      :  Control.Distributed.Process.Extras.Internal.Primitives+-- Copyright   :  (c) Tim Watson 2013, Parallel Scientific (Jeff Epstein) 2012+-- License     :  BSD3 (see the file LICENSE)+--+-- Maintainers :  Jeff Epstein, Tim Watson+-- Stability   :  experimental+-- Portability :  non-portable (requires concurrency)+--+-- This module provides a set of additional primitives that add functionality+-- to the basic Cloud Haskell APIs.+-----------------------------------------------------------------------------++module Control.Distributed.Process.Extras.Internal.Primitives+  ( -- * General Purpose Process Addressing+    Addressable+  , Routable(..)+  , Resolvable(..)+  , Linkable(..)+  , Killable(..)++    -- * Spawning and Linking+  , spawnSignalled+  , spawnLinkLocal+  , spawnMonitorLocal+  , linkOnFailure++    -- * Registered Processes+  , whereisRemote+  , whereisOrStart+  , whereisOrStartRemote++    -- * Selective Receive/Matching+  , matchCond+  , awaitResponse++    -- * General Utilities+  , times+  , monitor+  , awaitExit+  , isProcessAlive+  , forever'+  , deliver++    -- * Remote Table+  , __remoteTable+  ) where++import Control.Concurrent (myThreadId, throwTo)+import Control.Distributed.Process hiding (monitor)+import qualified Control.Distributed.Process as P (monitor)+import Control.Distributed.Process.Closure (seqCP, remotable, mkClosure)+import Control.Distributed.Process.Serializable (Serializable)+import Control.Distributed.Process.Extras.Internal.Types+  ( Addressable+  , Linkable(..)+  , Killable(..)+  , Resolvable(..)+  , Routable(..)+  , RegisterSelf(..)+  , ExitReason(ExitOther)+  , whereisRemote+  )+import Control.Monad (void)+import Data.Maybe (isJust, fromJust)++-- utility++-- | Monitor any @Resolvable@ object.+--+monitor :: Resolvable a => a -> Process (Maybe MonitorRef)+monitor addr = do+  mPid <- resolve addr+  case mPid of+    Nothing -> return Nothing+    Just p  -> return . Just =<< P.monitor p++awaitExit :: Resolvable a => a -> Process ()+awaitExit addr = do+  mPid <- resolve addr+  case mPid of+    Nothing -> return ()+    Just p  -> do+      mRef <- P.monitor p+      receiveWait [+          matchIf (\(ProcessMonitorNotification r p' _) -> r == mRef && p == p')+                  (\_ -> return ())+        ]++deliver :: (Addressable a, Serializable m) => m -> a -> Process ()+deliver = flip sendTo++isProcessAlive :: ProcessId -> Process Bool+isProcessAlive pid = getProcessInfo pid >>= \info -> return $ info /= Nothing++-- | Apply the supplied expression /n/ times+times :: Int -> Process () -> Process ()+n `times` proc = runP proc n+  where runP :: Process () -> Int -> Process ()+        runP _ 0 = return ()+        runP p n' = p >> runP p (n' - 1)++-- | Like 'Control.Monad.forever' but sans space leak+forever' :: Monad m => m a -> m b+forever' a = let a' = a >> a' in a'+{-# INLINE forever' #-}++-- spawning, linking and generic server startup++-- | Spawn a new (local) process. This variant takes an initialisation+-- action and a secondary expression from the result of the initialisation+-- to @Process ()@. The spawn operation synchronises on the completion of the+-- @before@ action, such that the calling process is guaranteed to only see+-- the newly spawned @ProcessId@ once the initialisation has successfully+-- completed.+spawnSignalled :: Process a -> (a -> Process ()) -> Process ProcessId+spawnSignalled before after = do+  (sigStart, recvStart) <- newChan+  (pid, mRef) <- spawnMonitorLocal $ do+    initProc <- before+    sendChan sigStart ()+    after initProc+  receiveWait [+      matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mRef)+              (\(ProcessMonitorNotification _ _ dr) -> die $ ExitOther (show dr))+    , matchChan recvStart (\() -> return pid)+    ]++-- | Node local version of 'Control.Distributed.Process.spawnLink'.+-- Note that this is just the sequential composition of 'spawn' and 'link'.+-- (The "Unified" semantics that underlies Cloud Haskell does not even support+-- a synchronous link operation)+spawnLinkLocal :: Process () -> Process ProcessId+spawnLinkLocal p = do+  pid <- spawnLocal p+  link pid+  return pid++-- | Like 'spawnLinkLocal', but monitors the spawned process.+--+spawnMonitorLocal :: Process () -> Process (ProcessId, MonitorRef)+spawnMonitorLocal p = do+  pid <- spawnLocal p+  ref <- P.monitor pid+  return (pid, ref)++-- | CH's 'link' primitive, unlike Erlang's, will trigger when the target+-- process dies for any reason. This function has semantics like Erlang's:+-- it will trigger 'ProcessLinkException' only when the target dies abnormally.+--+linkOnFailure :: ProcessId -> Process ()+linkOnFailure them = do+  us <- getSelfPid+  tid <- liftIO $ myThreadId+  void $ spawnLocal $ do+    callerRef <- P.monitor us+    calleeRef <- P.monitor them+    reason <- receiveWait [+             matchIf (\(ProcessMonitorNotification mRef _ _) ->+                       mRef == callerRef) -- nothing left to do+                     (\_ -> return DiedNormal)+           , matchIf (\(ProcessMonitorNotification mRef' _ _) ->+                       mRef' == calleeRef)+                     (\(ProcessMonitorNotification _ _ r') -> return r')+         ]+    case reason of+      DiedNormal -> return ()+      _ -> liftIO $ throwTo tid (ProcessLinkException us reason)++-- | Returns the pid of the process that has been registered+-- under the given name. This refers to a local, per-node registration,+-- not @global@ registration. If that name is unregistered, a process+-- is started. This is a handy way to start per-node named servers.+--+whereisOrStart :: String -> Process () -> Process ProcessId+whereisOrStart name proc =+  do mpid <- whereis name+     case mpid of+       Just pid -> return pid+       Nothing ->+         do caller <- getSelfPid+            pid <- spawnLocal $+                 do self <- getSelfPid+                    register name self+                    send caller (RegisterSelf,self)+                    () <- expect+                    proc+            ref <- P.monitor pid+            ret <- receiveWait+               [ matchIf (\(ProcessMonitorNotification aref _ _) -> ref == aref)+                         (\(ProcessMonitorNotification _ _ _) -> return Nothing),+                 matchIf (\(RegisterSelf,apid) -> apid == pid)+                         (\(RegisterSelf,_) -> return $ Just pid)+               ]+            case ret of+              Nothing -> whereisOrStart name proc+              Just somepid ->+                do unmonitor ref+                   send somepid ()+                   return somepid++registerSelf :: (String, ProcessId) -> Process ()+registerSelf (name,target) =+  do self <- getSelfPid+     register name self+     send target (RegisterSelf, self)+     () <- expect+     return ()++$(remotable ['registerSelf])++-- | A remote equivalent of 'whereisOrStart'. It deals with the+-- node registry on the given node, and the process, if it needs to be started,+-- will run on that node. If the node is inaccessible, Nothing will be returned.+--+whereisOrStartRemote :: NodeId -> String -> Closure (Process ()) -> Process (Maybe ProcessId)+whereisOrStartRemote nid name proc =+     do mRef <- monitorNode nid+        whereisRemoteAsync nid name+        res <- receiveWait+          [ matchIf (\(WhereIsReply label _) -> label == name)+                    (\(WhereIsReply _ mPid) -> return (Just mPid)),+            matchIf (\(NodeMonitorNotification aref _ _) -> aref == mRef)+                    (\(NodeMonitorNotification _ _ _) -> return Nothing)+          ]+        case res of+           Nothing -> return Nothing+           Just (Just pid) -> unmonitor mRef >> return (Just pid)+           Just Nothing ->+              do self <- getSelfPid+                 sRef <- spawnAsync nid ($(mkClosure 'registerSelf) (name,self) `seqCP` proc)+                 ret <- receiveWait [+                      matchIf (\(NodeMonitorNotification ref _ _) -> ref == mRef)+                              (\(NodeMonitorNotification _ _ _) -> return Nothing),+                      matchIf (\(DidSpawn ref _) -> ref==sRef )+                              (\(DidSpawn _ pid) ->+                                  do pRef <- P.monitor pid+                                     receiveWait+                                       [ matchIf (\(RegisterSelf, apid) -> apid == pid)+                                                 (\(RegisterSelf, _) -> do unmonitor pRef+                                                                           send pid ()+                                                                           return $ Just pid),+                                         matchIf (\(NodeMonitorNotification aref _ _) -> aref == mRef)+                                                 (\(NodeMonitorNotification _aref _ _) -> return Nothing),+                                         matchIf (\(ProcessMonitorNotification ref _ _) -> ref==pRef)+                                                 (\(ProcessMonitorNotification _ _ _) -> return Nothing)+                                       ] )+                      ]+                 unmonitor mRef+                 case ret of+                   Nothing -> whereisOrStartRemote nid name proc+                   Just pid -> return $ Just pid++-- advanced messaging/matching++-- | An alternative to 'matchIf' that allows both predicate and action+-- to be expressed in one parameter.+matchCond :: (Serializable a) => (a -> Maybe (Process b)) -> Match b+matchCond cond =+   let v n = (isJust n, fromJust n)+       res = v . cond+    in matchIf (fst . res) (snd . res)++-- | Safe (i.e., monitored) waiting on an expected response/message.+awaitResponse :: Addressable a+              => a+              -> [Match (Either ExitReason b)]+              -> Process (Either ExitReason b)+awaitResponse addr matches = do+  mPid <- resolve addr+  case mPid of+    Nothing -> return $ Left $ ExitOther "UnresolvedAddress"+    Just p  -> do+      mRef <- P.monitor p+      receiveWait ((matchRef mRef):matches)+  where+    matchRef :: MonitorRef -> Match (Either ExitReason b)+    matchRef r = matchIf (\(ProcessMonitorNotification r' _ _) -> r == r')+                         (\(ProcessMonitorNotification _ _ d) -> do+                             return (Left (ExitOther (show d))))+
+ src/Control/Distributed/Process/Extras/Internal/Queue/PriorityQ.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE BangPatterns       #-}+{-# LANGUAGE StandaloneDeriving #-}+module Control.Distributed.Process.Extras.Internal.Queue.PriorityQ where++-- NB: we might try this with a skewed binomial heap at some point,+-- but for now, we'll use this module from the fingertree package+import qualified Data.PriorityQueue.FingerTree as PQ+import Data.PriorityQueue.FingerTree (PQueue)++newtype PriorityQ k a = PriorityQ { q :: PQueue k a }++{-# INLINE empty #-}+empty :: Ord k => PriorityQ k v+empty = PriorityQ $ PQ.empty++{-# INLINE isEmpty #-}+isEmpty :: Ord k => PriorityQ k v -> Bool+isEmpty = PQ.null . q++{-# INLINE singleton #-}+singleton :: Ord k => k -> a -> PriorityQ k a+singleton !k !v = PriorityQ $ PQ.singleton k v++{-# INLINE enqueue #-}+enqueue :: Ord k => k -> v -> PriorityQ k v -> PriorityQ k v+enqueue !k !v p = PriorityQ (PQ.add k v $ q p)++{-# INLINE dequeue #-}+dequeue :: Ord k => PriorityQ k v -> Maybe (v, PriorityQ k v)+dequeue p = maybe Nothing (\(v, pq') -> Just (v, pq')) $+              case (PQ.minView (q p)) of+                Nothing     -> Nothing+                Just (v, q') -> Just (v, PriorityQ $ q')++{-# INLINE peek #-}+peek :: Ord k => PriorityQ k v -> Maybe v+peek p = maybe Nothing (\(v, _) -> Just v) $ dequeue p+
+ src/Control/Distributed/Process/Extras/Internal/Queue/SeqQ.hs view
@@ -0,0 +1,66 @@+-----------------------------------------------------------------------------+-- |+-- Module      :  Control.Distributed.Process.Extras.Internal.Queue.SeqQ+-- Copyright   :  (c) Tim Watson 2012 - 2013+-- License     :  BSD3 (see the file LICENSE)+--+-- Maintainer  :  Tim Watson <watson.timothy@gmail.com>+-- Stability   :  experimental+--+-- A simple FIFO queue implementation backed by @Data.Sequence@.+-----------------------------------------------------------------------------++module Control.Distributed.Process.Extras.Internal.Queue.SeqQ+  ( SeqQ+  , empty+  , isEmpty+  , singleton+  , enqueue+  , dequeue+  , peek+  )+  where++import Data.Sequence+  ( Seq+  , ViewR(..)+  , (<|)+  , viewr+  )+import qualified Data.Sequence as Seq (empty, singleton, null)++newtype SeqQ a = SeqQ { q :: Seq a }+  deriving (Show)++instance Eq a => Eq (SeqQ a) where+  a == b = (q a) == (q b)++{-# INLINE empty #-}+empty :: SeqQ a+empty = SeqQ Seq.empty++isEmpty :: SeqQ a -> Bool+isEmpty = Seq.null . q++{-# INLINE singleton #-}+singleton :: a -> SeqQ a+singleton = SeqQ . Seq.singleton++{-# INLINE enqueue #-}+enqueue :: SeqQ a -> a -> SeqQ a+enqueue s a = SeqQ $ a <| q s++{-# INLINE dequeue #-}+dequeue :: SeqQ a -> Maybe (a, SeqQ a)+dequeue s = maybe Nothing (\(s' :> a) -> Just (a, SeqQ s')) $ getR s++{-# INLINE peek #-}+peek :: SeqQ a -> Maybe a+peek s = maybe Nothing (\(_ :> a) -> Just a) $ getR s++getR :: SeqQ a -> Maybe (ViewR a)+getR s =+  case (viewr (q s)) of+    EmptyR -> Nothing+    a      -> Just a+
+ src/Control/Distributed/Process/Extras/Internal/Types.hs view
@@ -0,0 +1,277 @@+{-# LANGUAGE DeriveDataTypeable     #-}+{-# LANGUAGE DeriveGeneric          #-}+{-# LANGUAGE StandaloneDeriving     #-}+{-# LANGUAGE TemplateHaskell        #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# LANGUAGE OverlappingInstances   #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables   #-}++-- | Types used throughout the Extras package+--+module Control.Distributed.Process.Extras.Internal.Types+  ( -- * Tagging+    Tag+  , TagPool+  , newTagPool+  , getTag+    -- * Addressing+  , Linkable(..)+  , Killable(..)+  , Resolvable(..)+  , Routable(..)+  , Addressable+  , sendToRecipient+  , Recipient(..)+  , RegisterSelf(..)+    -- * Interactions+  , whereisRemote+  , resolveOrDie+  , CancelWait(..)+  , Channel+  , Shutdown(..)+  , ExitReason(..)+  , ServerDisconnected(..)+  , NFSerializable+    -- remote table+  , __remoteTable+  ) where++import Control.Concurrent.MVar+  ( MVar+  , newMVar+  , modifyMVar+  )+import Control.DeepSeq (NFData, ($!!))+import Control.Distributed.Process hiding (send)+import qualified Control.Distributed.Process as P+  ( send+  , unsafeSend+  , unsafeNSend+  )+import Control.Distributed.Process.Closure+  ( remotable+  , mkClosure+  , functionTDict+  )+import Control.Distributed.Process.Serializable++import Data.Binary+import Data.Typeable (Typeable)+import GHC.Generics++--------------------------------------------------------------------------------+-- API                                                                        --+--------------------------------------------------------------------------------++-- | Introduces a class that brings NFData into scope along with Serializable,+-- such that we can force evaluation. Intended for use with the UnsafePrimitives+-- module (which wraps "Control.Distributed.Process.UnsafePrimitives"), and+-- guarantees evaluatedness in terms of @NFData@. Please note that we /cannot/+-- guarantee that an @NFData@ instance will behave the same way as a @Binary@+-- one with regards evaluation, so it is still possible to introduce unexpected+-- behaviour by using /unsafe/ primitives in this way.+--+class (NFData a, Serializable a) => NFSerializable a+instance (NFData a, Serializable a) => NFSerializable a++-- | Tags provide uniqueness for messages, so that they can be+-- matched with their response.+type Tag = Int++-- | Generates unique 'Tag' for messages and response pairs.+-- Each process that depends, directly or indirectly, on+-- the call mechanisms in "Control.Distributed.Process.Global.Call"+-- should have at most one TagPool on which to draw unique message+-- tags.+type TagPool = MVar Tag++-- | Create a new per-process source of unique+-- message identifiers.+newTagPool :: Process TagPool+newTagPool = liftIO $ newMVar 0++-- | Extract a new identifier from a 'TagPool'.+getTag :: TagPool -> Process Tag+getTag tp = liftIO $ modifyMVar tp (\tag -> return (tag+1,tag))++-- | Wait cancellation message.+data CancelWait = CancelWait+    deriving (Eq, Show, Typeable, Generic)+instance Binary CancelWait where+instance NFData CancelWait where++-- | Simple representation of a channel.+type Channel a = (SendPort a, ReceivePort a)++-- | Used internally in whereisOrStart. Sent as (RegisterSelf,ProcessId).+data RegisterSelf = RegisterSelf+  deriving (Typeable, Generic)+instance Binary RegisterSelf where+instance NFData RegisterSelf where++-- | A ubiquitous /shutdown signal/ that can be used+-- to maintain a consistent shutdown/stop protocol for+-- any process that wishes to handle it.+data Shutdown = Shutdown+  deriving (Typeable, Generic, Show, Eq)+instance Binary Shutdown where+instance NFData Shutdown where++-- | Provides a /reason/ for process termination.+data ExitReason =+    ExitNormal        -- ^ indicates normal exit+  | ExitShutdown      -- ^ normal response to a 'Shutdown'+  | ExitOther !String -- ^ abnormal (error) shutdown+  deriving (Typeable, Generic, Eq, Show)+instance Binary ExitReason where+instance NFData ExitReason where++-- | A simple means of mapping to a receiver.+data Recipient =+    Pid !ProcessId+  | Registered !String+  | RemoteRegistered !String !NodeId+--  | ProcReg !ProcessId !String+--  | RemoteProcReg NodeId String+--  | GlobalReg String+  deriving (Typeable, Generic, Show, Eq)+instance Binary Recipient where++-- useful exit reasons++-- | Given when a server is unobtainable.+data ServerDisconnected = ServerDisconnected !DiedReason+  deriving (Typeable, Generic)+instance Binary ServerDisconnected where+instance NFData ServerDisconnected where++$(remotable ['whereis])++-- | A synchronous version of 'whereis', this relies on 'call'+-- to perform the relevant monitoring of the remote node.+whereisRemote :: NodeId -> String -> Process (Maybe ProcessId)+whereisRemote node name =+  call $(functionTDict 'whereis) node ($(mkClosure 'whereis) name)++sendToRecipient :: (Serializable m) => Recipient -> m -> Process ()+sendToRecipient (Pid p) m                = P.send p m+sendToRecipient (Registered s) m         = nsend s m+sendToRecipient (RemoteRegistered s n) m = nsendRemote n s m++unsafeSendToRecipient :: (NFSerializable m) => Recipient -> m -> Process ()+unsafeSendToRecipient (Pid p) m                = P.unsafeSend p $!! m+unsafeSendToRecipient (Registered s) m         = P.unsafeNSend s $!! m+unsafeSendToRecipient (RemoteRegistered s n) m = nsendRemote n s m++baseAddressableErrorMessage :: (Routable a) => a -> String+baseAddressableErrorMessage _ = "CannotResolveAddressable"++-- | Class of things to which a @Process@ can /link/ itself.+class Linkable a where+  -- | Create a /link/ with the supplied object.+  linkTo :: a -> Process ()++-- | Class of things that can be resolved to a 'ProcessId'.+--+class Resolvable a where+  -- | Resolve the reference to a process id, or @Nothing@ if resolution fails+  resolve :: a -> Process (Maybe ProcessId)++-- | Class of things that can be killed (or instructed to exit).+class Killable a where+  killProc :: a -> String -> Process ()+  exitProc :: (Serializable m) => a -> m -> Process ()++instance Killable ProcessId where+  killProc = kill+  exitProc = exit++instance Resolvable r => Killable r where+  killProc r s = resolve r >>= maybe (return ()) (flip kill $ s)+  exitProc r m = resolve r >>= maybe (return ()) (flip exit $ m)++-- | Provides a unified API for addressing processes.+--+class Routable a where+  -- | Send a message to the target asynchronously+  sendTo  :: (Serializable m) => a -> m -> Process ()++  -- | Send some @NFData@ message to the target asynchronously,+  -- forcing evaluation (i.e., @deepseq@) beforehand.+  unsafeSendTo :: (NFSerializable m) => a -> m -> Process ()++  -- | Unresolvable @Addressable@ Message+  unresolvableMessage :: a -> String+  unresolvableMessage = baseAddressableErrorMessage++instance (Resolvable a) => Routable a where+  sendTo a m = do+    mPid <- resolve a+    maybe (die (unresolvableMessage a))+          (\p -> P.send p m)+          mPid++  unsafeSendTo a m = do+    mPid <- resolve a+    maybe (die (unresolvableMessage a))+          (\p -> P.unsafeSend p $!! m)+          mPid++  -- | Unresolvable Addressable Message+  unresolvableMessage = baseAddressableErrorMessage++instance Resolvable Recipient where+  resolve (Pid                p) = return (Just p)+  resolve (Registered         n) = whereis n+  resolve (RemoteRegistered s n) = whereisRemote n s++instance Routable Recipient where+  sendTo = sendToRecipient+  unsafeSendTo = unsafeSendToRecipient++  unresolvableMessage (Pid                p) = unresolvableMessage p+  unresolvableMessage (Registered         n) = unresolvableMessage n+  unresolvableMessage (RemoteRegistered s n) = unresolvableMessage (n, s)++instance Resolvable ProcessId where+  resolve p = return (Just p)++instance Routable ProcessId where+  sendTo                 = P.send+  unsafeSendTo pid msg   = P.unsafeSend pid $!! msg+  unresolvableMessage p  = "CannotResolvePid[" ++ (show p) ++ "]"++instance Resolvable String where+  resolve = whereis++instance Routable String where+  sendTo                = nsend+  unsafeSendTo name msg = P.unsafeNSend name $!! msg+  unresolvableMessage s = "CannotResolveRegisteredName[" ++ s ++ "]"++instance Resolvable (NodeId, String) where+  resolve (nid, pname) = whereisRemote nid pname++instance Routable (NodeId, String) where+  sendTo  (nid, pname) msg   = nsendRemote nid pname msg+  unsafeSendTo               = sendTo -- because serialisation *must* take place+  unresolvableMessage (n, s) =+    "CannotResolveRemoteRegisteredName[name: " ++ s ++ ", node: " ++ (show n) ++ "]"++instance Routable (Message -> Process ()) where+  sendTo f       = f . wrapMessage+  unsafeSendTo f = f . unsafeWrapMessage++class (Resolvable a, Routable a) => Addressable a+instance (Resolvable a, Routable a) => Addressable a++-- TODO: this probably belongs somewhere other than in ..Types.+-- | resolve the Resolvable or die with specified msg plus details of what didn't resolve+resolveOrDie  :: (Routable a, Resolvable a) => a -> String -> Process ProcessId+resolveOrDie resolvable failureMsg = do+  result <- resolve resolvable+  case result of+    Nothing -> die $ failureMsg ++ " " ++ unresolvableMessage resolvable+    Just pid -> return pid
+ src/Control/Distributed/Process/Extras/Internal/Unsafe.hs view
@@ -0,0 +1,128 @@+{-# LANGUAGE DeriveDataTypeable  #-}+{-# LANGUAGE DeriveGeneric       #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving  #-}++-- | If you don't know exactly what this module is for and precisely+-- how to use the types within, you should move on, quickly!+--+module Control.Distributed.Process.Extras.Internal.Unsafe+  ( -- * Copying non-serializable data+    PCopy()+  , pCopy+  , matchP+  , matchChanP+  , pUnwrap+    -- * Arbitrary (unmanaged) message streams+  , InputStream(Null)+  , newInputStream+  , matchInputStream+  , readInputStream+  , InvalidBinaryShim(..)+  ) where++import Control.Concurrent.STM (STM, atomically)+import Control.Distributed.Process+  ( matchAny+  , matchChan+  , matchSTM+  , match+  , handleMessage+  , receiveChan+  , liftIO+  , die+  , Match+  , ReceivePort+  , Message+  , Process+  )+import Control.Distributed.Process.Serializable (Serializable)+import Data.Binary+import Control.DeepSeq (NFData)+import Data.Typeable (Typeable)+import GHC.Generics++data InvalidBinaryShim = InvalidBinaryShim+  deriving (Typeable, Show, Eq)++-- NB: PCopy is a shim, allowing us to copy a pointer to otherwise+-- non-serializable data directly to another local process'+-- mailbox with no serialisation or even deepseq evaluation+-- required. We disallow remote queries (i.e., from other nodes)+-- and thus the Binary instance below is never used (though it's+-- required by the type system) and will in fact generate errors if+-- you attempt to use it at runtime. In other words, if you attempt+-- to make a @Message@ out of this, you'd better make sure you're+-- calling @unsafeCreateUnencodedMessage@, otherwise /BOOM/! You have+-- been warned.+--+data PCopy a = PCopy !a+  deriving (Typeable, Generic)+instance (NFData a) => NFData (PCopy a) where++instance (Typeable a) => Binary (PCopy a) where+  put _ = error "InvalidBinaryShim"+  get   = error "InvalidBinaryShim"++-- | Wrap any @Typeable@ datum in a @PCopy@. We hide the constructor to+-- discourage arbitrary uses of the type, since @PCopy@ is a specialised+-- and potentially dangerous construct.+pCopy :: (Typeable a) => a -> PCopy a+pCopy = PCopy++-- | Matches on @PCopy m@ and returns the /m/ within.+-- This potentially allows us to bypass serialization (and the type constraints+-- it enforces) for local message passing (i.e., with @UnencodedMessage@ data),+-- since PCopy is just a shim.+matchP :: (Typeable m) => Match (Maybe m)+matchP = matchAny pUnwrap++-- | Given a raw @Message@, attempt to unwrap a @Typeable@ datum from+-- an enclosing @PCopy@ wrapper.+pUnwrap :: (Typeable m) => Message -> Process (Maybe m)+pUnwrap m = handleMessage m (\(PCopy m' :: PCopy m) -> return m')++-- | Matches on a @TypedChannel (PCopy a)@.+matchChanP :: (Typeable m) => ReceivePort (PCopy m) -> Match m+matchChanP rp = matchChan rp (\(PCopy m' :: PCopy m) -> return m')++-- | A generic input channel that can be read from in the same fashion+-- as a typed channel (i.e., @ReceivePort@). To read from an input stream+-- in isolation, see 'readInputStream'. To compose an 'InputStream' with+-- reads on a process' mailbox (and/or typed channels), see 'matchInputStream'.+--+data InputStream a = ReadChan (ReceivePort a) | ReadSTM (STM a) | Null+  deriving (Typeable)++data NullInputStream = NullInputStream+  deriving (Typeable, Generic, Show, Eq)+instance Binary NullInputStream where+instance NFData NullInputStream where++-- [note: InputStream]+-- InputStream wraps either a ReceivePort or an arbitrary STM action. Used+-- internally when we want to allow internal clients to completely bypass+-- regular messaging primitives (which is rare but occaisionally useful),+-- the type (only, minus its constructors) is exposed to users of some+-- @Exchange@ APIs.++-- | Create a new 'InputStream'.+newInputStream :: forall a. (Typeable a)+               => Either (ReceivePort a) (STM a)+               -> InputStream a+newInputStream (Left rp)   = ReadChan rp+newInputStream (Right stm) = ReadSTM stm++-- | Read from an 'InputStream'. This is a blocking operation.+readInputStream :: (Serializable a) => InputStream a -> Process a+readInputStream (ReadChan rp) = receiveChan rp+readInputStream (ReadSTM stm) = liftIO $ atomically stm+readInputStream Null          = die $ NullInputStream++-- | Constructs a @Match@ for a given 'InputChannel'.+matchInputStream :: InputStream a -> Match a+matchInputStream (ReadChan rp) = matchChan rp return+matchInputStream (ReadSTM stm) = matchSTM stm return+matchInputStream Null          = match (\NullInputStream -> do+                                           error "NullInputStream")+
+ src/Control/Distributed/Process/Extras/Time.hs view
@@ -0,0 +1,258 @@+{-# LANGUAGE DeriveDataTypeable  #-}+{-# LANGUAGE DeriveGeneric       #-}++-----------------------------------------------------------------------------+-- |+-- Module      :  Control.Distributed.Process.Extras.Time+-- Copyright   :  (c) Tim Watson, Jeff Epstein, Alan Zimmerman+-- License     :  BSD3 (see the file LICENSE)+--+-- Maintainer  :  Tim Watson+-- Stability   :  experimental+-- Portability :  non-portable (requires concurrency)+--+-- This module provides facilities for working with time delays and timeouts.+-- The type 'Timeout' and the 'timeout' family of functions provide mechanisms+-- for working with @threadDelay@-like behaviour that operates on microsecond+-- values.+--+-- The 'TimeInterval' and 'TimeUnit' related functions provide an abstraction+-- for working with various time intervals, whilst the 'Delay' type provides a+-- corrolary to 'timeout' that works with these.+-----------------------------------------------------------------------------++module Control.Distributed.Process.Extras.Time+  ( -- * Time interval handling+    microSeconds+  , milliSeconds+  , seconds+  , minutes+  , hours+  , asTimeout+  , after+  , within+  , timeToMicros+  , TimeInterval+  , TimeUnit(..)+  , Delay(..)++  -- * Conversion To/From NominalDiffTime+  , timeIntervalToDiffTime+  , diffTimeToTimeInterval+  , diffTimeToDelay+  , delayToDiffTime+  , microsecondsToNominalDiffTime++    -- * (Legacy) Timeout Handling+  , Timeout+  , TimeoutNotification(..)+  , timeout+  , infiniteWait+  , noWait+  ) where++import Control.Concurrent (threadDelay)+import Control.DeepSeq (NFData)+import Control.Distributed.Process+import Control.Distributed.Process.Extras.Internal.Types+import Control.Monad (void)+import Data.Binary+import Data.Ratio ((%))+import Data.Time.Clock+import Data.Typeable (Typeable)++import GHC.Generics++--------------------------------------------------------------------------------+-- API                                                                        --+--------------------------------------------------------------------------------++-- | Defines the time unit for a Timeout value+data TimeUnit = Days | Hours | Minutes | Seconds | Millis | Micros+    deriving (Typeable, Generic, Eq, Show)++instance Binary TimeUnit where+instance NFData TimeUnit where++-- | A time interval.+data TimeInterval = TimeInterval TimeUnit Int+    deriving (Typeable, Generic, Eq, Show)++instance Binary TimeInterval where+instance NFData TimeInterval where++-- | Represents either a delay of 'TimeInterval', an infinite wait or no delay+-- (i.e., non-blocking).+data Delay = Delay TimeInterval | Infinity | NoDelay+    deriving (Typeable, Generic, Eq, Show)++instance Binary Delay where+instance NFData Delay where++-- | Represents a /timeout/ in terms of microseconds, where 'Nothing' stands for+-- infinity and @Just 0@, no-delay.+type Timeout = Maybe Int++-- | Send to a process when a timeout expires.+data TimeoutNotification = TimeoutNotification Tag+       deriving (Typeable)++instance Binary TimeoutNotification where+  get = fmap TimeoutNotification $ get+  put (TimeoutNotification n) = put n++-- time interval/unit handling++-- | converts the supplied @TimeInterval@ to microseconds+asTimeout :: TimeInterval -> Int+asTimeout (TimeInterval u v) = timeToMicros u v++-- | Convenience for making timeouts; e.g.,+--+-- > receiveTimeout (after 3 Seconds) [ match (\"ok" -> return ()) ]+--+after :: Int -> TimeUnit -> Int+after n m = timeToMicros m n++-- | Convenience for making 'TimeInterval'; e.g.,+--+-- > let ti = within 5 Seconds in .....+--+within :: Int -> TimeUnit -> TimeInterval+within n m = TimeInterval m n++-- | given a number, produces a @TimeInterval@ of microseconds+microSeconds :: Int -> TimeInterval+microSeconds = TimeInterval Micros++-- | given a number, produces a @TimeInterval@ of milliseconds+milliSeconds :: Int -> TimeInterval+milliSeconds = TimeInterval Millis++-- | given a number, produces a @TimeInterval@ of seconds+seconds :: Int -> TimeInterval+seconds = TimeInterval Seconds++-- | given a number, produces a @TimeInterval@ of minutes+minutes :: Int -> TimeInterval+minutes = TimeInterval Minutes++-- | given a number, produces a @TimeInterval@ of hours+hours :: Int -> TimeInterval+hours = TimeInterval Hours++-- TODO: is timeToMicros efficient enough?++-- | converts the supplied @TimeUnit@ to microseconds+{-# INLINE timeToMicros #-}+timeToMicros :: TimeUnit -> Int -> Int+timeToMicros Micros  us   = us+timeToMicros Millis  ms   = ms  * (10 ^ (3 :: Int)) -- (1000µs == 1ms)+timeToMicros Seconds secs = timeToMicros Millis  (secs * milliSecondsPerSecond)+timeToMicros Minutes mins = timeToMicros Seconds (mins * secondsPerMinute)+timeToMicros Hours   hrs  = timeToMicros Minutes (hrs  * minutesPerHour)+timeToMicros Days    days = timeToMicros Hours   (days * hoursPerDay)++{-# INLINE hoursPerDay #-}+hoursPerDay :: Int+hoursPerDay = 60++{-# INLINE minutesPerHour #-}+minutesPerHour :: Int+minutesPerHour = 60++{-# INLINE secondsPerMinute #-}+secondsPerMinute :: Int+secondsPerMinute = 60++{-# INLINE milliSecondsPerSecond #-}+milliSecondsPerSecond :: Int+milliSecondsPerSecond = 1000++{-# INLINE microSecondsPerSecond #-}+microSecondsPerSecond :: Int+microSecondsPerSecond = 1000000++-- timeouts/delays (microseconds)++-- | Constructs an inifinite 'Timeout'.+infiniteWait :: Timeout+infiniteWait = Nothing++-- | Constructs a no-wait 'Timeout'+noWait :: Timeout+noWait = Just 0++-- | Sends the calling process @TimeoutNotification tag@ after @time@ microseconds+timeout :: Int -> Tag -> ProcessId -> Process ()+timeout time tag p =+  void $ spawnLocal $+               do liftIO $ threadDelay time+                  send p (TimeoutNotification tag)++-- Converting to/from Data.Time.Clock NominalDiffTime++-- | given a @TimeInterval@, provide an equivalent @NominalDiffTim@+timeIntervalToDiffTime :: TimeInterval -> NominalDiffTime+timeIntervalToDiffTime ti = microsecondsToNominalDiffTime (fromIntegral $ asTimeout ti)++-- | given a @NominalDiffTim@@, provide an equivalent @TimeInterval@+diffTimeToTimeInterval :: NominalDiffTime -> TimeInterval+diffTimeToTimeInterval dt = microSeconds $ (fromIntegral (round (dt * 1000000) :: Integer))++-- | given a @Delay@, provide an equivalent @NominalDiffTim@+delayToDiffTime :: Delay -> NominalDiffTime+delayToDiffTime (Delay ti) = timeIntervalToDiffTime ti+delayToDiffTime Infinity   = error "trying to convert Delay.Infinity to a NominalDiffTime"+delayToDiffTime (NoDelay)  = microsecondsToNominalDiffTime 0++-- | given a @NominalDiffTim@@, provide an equivalent @Delay@+diffTimeToDelay :: NominalDiffTime -> Delay+diffTimeToDelay dt = Delay $ diffTimeToTimeInterval dt++-- | Create a 'NominalDiffTime' from a number of microseconds.+microsecondsToNominalDiffTime :: Integer -> NominalDiffTime+microsecondsToNominalDiffTime x = fromRational (x % (fromIntegral microSecondsPerSecond))++-- tenYearsAsMicroSeconds :: Integer+-- tenYearsAsMicroSeconds = 10 * 365 * 24 * 60 * 60 * 1000000++-- | Allow @(+)@ and @(-)@ operations on @TimeInterval@s+instance Num TimeInterval where+  t1 + t2 = microSeconds $ asTimeout t1 + asTimeout t2+  t1 - t2 = microSeconds $ asTimeout t1 - asTimeout t2+  _ * _ = error "trying to multiply two TimeIntervals"+  abs t = microSeconds $ abs (asTimeout t)+  signum t = if (asTimeout t) == 0+              then 0+              else if (asTimeout t) < 0 then -1+                                        else 1+  fromInteger _ = error "trying to call fromInteger for a TimeInterval. Cannot guess units"++-- | Allow @(+)@ and @(-)@ operations on @Delay@s+instance Num Delay where+  NoDelay     + x          = x+  Infinity    + _          = Infinity+  x           + NoDelay    = x+  _           + Infinity   = Infinity+  (Delay t1 ) + (Delay t2) = Delay (t1 + t2)++  NoDelay     - x          = x+  Infinity    - _          = Infinity+  x           - NoDelay    = x+  _           - Infinity   = Infinity+  (Delay t1 ) - (Delay t2) = Delay (t1 - t2)++  _ * _ = error "trying to multiply two Delays"++  abs NoDelay   = NoDelay+  abs Infinity  = Infinity+  abs (Delay t) = Delay (abs t)++  signum (NoDelay) = 0+  signum Infinity  = 1+  signum (Delay t) = Delay (signum t)++  fromInteger 0 = NoDelay+  fromInteger _ = error "trying to call fromInteger for a Delay. Cannot guess units"+
+ src/Control/Distributed/Process/Extras/Timer.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric      #-}+{-# LANGUAGE PatternGuards      #-}+{-# LANGUAGE TemplateHaskell    #-}++-----------------------------------------------------------------------------+-- |+-- Module      :  Control.Distributed.Process.Extras.Timer+-- Copyright   :  (c) Tim Watson 2012+-- License     :  BSD3 (see the file LICENSE)+--+-- Maintainer  :  Tim Watson <watson.timothy@gmail.com>+-- Stability   :  experimental+-- Portability :  non-portable (requires concurrency)+--+-- Provides an API for running code or sending messages, either after some+-- initial delay or periodically, and for cancelling, re-setting and/or+-- flushing pending /timers/.+-----------------------------------------------------------------------------++module Control.Distributed.Process.Extras.Timer+  (+    TimerRef+  , Tick(Tick)+  , sleep+  , sleepFor+  , sendAfter+  , runAfter+  , exitAfter+  , killAfter+  , startTimer+  , ticker+  , periodically+  , resetTimer+  , cancelTimer+  , flushTimer+  ) where++import Control.DeepSeq (NFData)+import Control.Distributed.Process hiding (send)+import Control.Distributed.Process.Serializable+import Control.Distributed.Process.Extras.UnsafePrimitives (send)+import Control.Distributed.Process.Extras.Internal.Types (NFSerializable)+import Control.Distributed.Process.Extras.Time+import Data.Binary+import Data.Typeable (Typeable)+import Prelude hiding (init)++import GHC.Generics++-- | an opaque reference to a timer+type TimerRef = ProcessId++-- | cancellation message sent to timers+data TimerConfig = Reset | Cancel+    deriving (Typeable, Generic, Eq, Show)+instance Binary TimerConfig where+instance NFData TimerConfig where++-- | represents a 'tick' event that timers can generate+data Tick = Tick+    deriving (Typeable, Generic, Eq, Show)+instance Binary Tick where+instance NFData Tick where++data SleepingPill = SleepingPill+    deriving (Typeable, Generic, Eq, Show)+instance Binary SleepingPill where+instance NFData SleepingPill where++--------------------------------------------------------------------------------+-- API                                                                        --+--------------------------------------------------------------------------------++-- | blocks the calling Process for the specified TimeInterval. Note that this+-- function assumes that a blocking receive is the most efficient approach to+-- acheiving this, however the runtime semantics (particularly with regards+-- scheduling) should not differ from threadDelay in practise.+sleep :: TimeInterval -> Process ()+sleep t =+  let ms = asTimeout t in do+  _ <- receiveTimeout ms [matchIf (\SleepingPill -> True)+                                  (\_ -> return ())]+  return ()++-- | Literate way of saying @sleepFor 3 Seconds@.+sleepFor :: Int -> TimeUnit -> Process ()+sleepFor i u = sleep (within i u)++-- | starts a timer which sends the supplied message to the destination+-- process after the specified time interval.+sendAfter :: (NFSerializable a)+          => TimeInterval+          -> ProcessId+          -> a+          -> Process TimerRef+sendAfter t pid msg = runAfter t proc+  where proc = do { send pid msg }++-- | runs the supplied process action(s) after @t@ has elapsed+runAfter :: TimeInterval -> Process () -> Process TimerRef+runAfter t p = spawnLocal $ runTimer t p True++-- | calls @exit pid reason@ after @t@ has elapsed+exitAfter :: (Serializable a)+             => TimeInterval+             -> ProcessId+             -> a+             -> Process TimerRef+exitAfter delay pid reason = runAfter delay $ exit pid reason++-- | kills the specified process after @t@ has elapsed+killAfter :: TimeInterval -> ProcessId -> String -> Process TimerRef+killAfter delay pid why = runAfter delay $ kill pid why++-- | starts a timer that repeatedly sends the supplied message to the destination+-- process each time the specified time interval elapses. To stop messages from+-- being sent in future, 'cancelTimer' can be called.+startTimer :: (NFSerializable a)+           => TimeInterval+           -> ProcessId+           -> a+           -> Process TimerRef+startTimer t pid msg = periodically t (send pid msg)++-- | runs the supplied process action(s) repeatedly at intervals of @t@+periodically :: TimeInterval -> Process () -> Process TimerRef+periodically t p = spawnLocal $ runTimer t p False++-- | resets a running timer. Note: Cancelling a timer does not guarantee that+-- all its messages are prevented from being delivered to the target process.+-- Also note that resetting an ongoing timer (started using the 'startTimer' or+-- 'periodically' functions) will only cause the current elapsed period to time+-- out, after which the timer will continue running. To stop a long-running+-- timer permanently, you should use 'cancelTimer' instead.+resetTimer :: TimerRef -> Process ()+resetTimer = (flip send) Reset++-- | permanently cancels a timer+cancelTimer :: TimerRef -> Process ()+cancelTimer = (flip send) Cancel++-- | cancels a running timer and flushes any viable timer messages from the+-- process' message queue. This function should only be called by the process+-- expecting to receive the timer's messages!+flushTimer :: (Serializable a, Eq a) => TimerRef -> a -> Delay -> Process ()+flushTimer ref ignore t = do+    mRef <- monitor ref+    cancelTimer ref+    performFlush mRef t+    return ()+  where performFlush mRef Infinity  = receiveWait $ filters mRef+        performFlush mRef NoDelay   = performFlush mRef (Delay $ microSeconds 0)+        performFlush mRef (Delay i) = receiveTimeout (asTimeout i) (filters mRef) >> return ()+        filters mRef = [+                matchIf (\x -> x == ignore)+                        (\_ -> return ())+              , matchIf (\(ProcessMonitorNotification mRef' _ _) -> mRef == mRef')+                        (\_ -> return ()) ]++-- | sets up a timer that sends 'Tick' repeatedly at intervals of @t@+ticker :: TimeInterval -> ProcessId -> Process TimerRef+ticker t pid = startTimer t pid Tick++--------------------------------------------------------------------------------+-- Implementation                                                             --+--------------------------------------------------------------------------------++-- runs the timer process+runTimer :: TimeInterval -> Process () -> Bool -> Process ()+runTimer t proc cancelOnReset = do+    cancel <- expectTimeout (asTimeout t)+    -- say $ "cancel = " ++ (show cancel) ++ "\n"+    case cancel of+        Nothing     -> runProc cancelOnReset+        Just Cancel -> return ()+        Just Reset  -> if cancelOnReset then return ()+                                        else runTimer t proc cancelOnReset+  where runProc True  = proc+        runProc False = proc >> runTimer t proc cancelOnReset
+ src/Control/Distributed/Process/Extras/UnsafePrimitives.hs view
@@ -0,0 +1,63 @@+-----------------------------------------------------------------------------+-- |+-- Module      :  Control.Distributed.Process.Extras.UnsafePrimitives+-- Copyright   :  (c) Tim Watson 2013+-- License     :  BSD3 (see the file LICENSE)+--+-- Maintainer  :  Tim Watson <watson.timothy@gmail.com>+-- Stability   :  experimental+-- Portability :  non-portable (requires concurrency)+--+-- [Unsafe Messaging Primitives Using NFData]+--+-- This module mirrors "Control.Distributed.Process.UnsafePrimitives", but+-- attempts to provide a bit more safety by forcing evaluation before sending.+-- This is handled using @NFData@, by means of the @NFSerializable@ type class.+--+-- Note that we /still/ cannot guarantee that both the @NFData@ and @Binary@+-- instances will evaluate your data the same way, therefore these primitives+-- still have certain risks and potential side effects. Use with caution.+--+-----------------------------------------------------------------------------+module Control.Distributed.Process.Extras.UnsafePrimitives+  ( send+  , nsend+  , sendToAddr+  , sendChan+  , wrapMessage+  ) where++import Control.DeepSeq (($!!))+import Control.Distributed.Process+  ( Process+  , ProcessId+  , SendPort+  , Message+  )+import Control.Distributed.Process.Extras.Internal.Types+  ( NFSerializable+  , Addressable+  , Resolvable(..)+  )+import qualified Control.Distributed.Process.UnsafePrimitives as Unsafe++send :: NFSerializable m => ProcessId -> m -> Process ()+send pid msg = Unsafe.send pid $!! msg++nsend :: NFSerializable a => String -> a -> Process ()+nsend name msg = Unsafe.nsend name $!! msg++sendToAddr :: (Addressable a, NFSerializable m) => a -> m -> Process ()+sendToAddr addr msg = do+  mPid <- resolve addr+  case mPid of+    Nothing -> return ()+    Just p  -> send p msg++sendChan :: (NFSerializable m) => SendPort m -> m -> Process ()+sendChan port msg = Unsafe.sendChan port $!! msg++-- | Create an unencoded @Message@ for any @Serializable@ type.+wrapMessage :: NFSerializable a => a -> Message+wrapMessage msg = Unsafe.wrapMessage $!! msg+
+ tests/TestQueues.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE PatternGuards  #-}+module Main where++import qualified Control.Distributed.Process.Extras.Internal.Queue.SeqQ as FIFO+import Control.Distributed.Process.Extras.Internal.Queue.SeqQ ( SeqQ )+import qualified Control.Distributed.Process.Extras.Internal.Queue.PriorityQ as PQ++import Control.Rematch hiding (on)+import Control.Rematch.Run+import Data.Function (on)+import Data.List+import Test.Framework as TF (defaultMain, testGroup, Test)+import Test.Framework.Providers.HUnit+import Test.Framework.Providers.QuickCheck2 (testProperty)+import Test.HUnit (Assertion, assertFailure)++import Prelude++expectThat :: a -> Matcher a -> Assertion+expectThat a matcher = case res of+  MatchSuccess -> return ()+  (MatchFailure msg) -> assertFailure msg+  where res = runMatch matcher a++-- NB: these tests/properties are not meant to be complete, but rather+-- they exercise the small number of behaviours that we actually use!++-- TODO: some laziness vs. strictness tests, with error/exception checking++prop_pq_ordering :: [Int] -> Bool+prop_pq_ordering xs =+    let xs' = map (\x -> (x, show x)) xs+        q   = foldl (\q' x -> PQ.enqueue (fst x) (snd x) q') PQ.empty xs'+        ys  = drain q []+        zs  = [snd x | x <- reverse $ sortBy (compare `on` fst) xs']+        -- the sorted list should match the stuff we drained back out+    in zs == ys+  where+    drain q xs'+      | True <- PQ.isEmpty q = xs'+      | otherwise =+          let Just (x, q') = PQ.dequeue q in drain q' (x:xs')++prop_fifo_enqueue :: Int -> Int -> Int -> Bool+prop_fifo_enqueue a b c =+  let q1           = foldl FIFO.enqueue FIFO.empty [a,b,c]+      Just (a', q2) = FIFO.dequeue q1+      Just (b', q3) = FIFO.dequeue q2+      Just (c', q4) = FIFO.dequeue q3+      Nothing       = FIFO.dequeue q4+  in q4 `seq` [a',b',c'] == [a,b,c]  -- why seq here? to shut the compiler up.++prop_enqueue_empty :: String -> Bool+prop_enqueue_empty s =+  let q            = FIFO.enqueue FIFO.empty s+      Just (_, q') = FIFO.dequeue q+  in (FIFO.isEmpty q') == ((FIFO.isEmpty q) == False)++tests :: [TF.Test]+tests = [+     testGroup "Priority Queue Tests" [+        -- testCase "New Queue Should Be Empty"+        --   (expect (PQ.isEmpty $ PQ.empty) $ equalTo True),+        -- testCase "Singleton Queue Should Contain One Element"+        --   (expect (PQ.dequeue $ (PQ.singleton 1 "hello") :: PriorityQ Int String) $+        --      equalTo $ (Just ("hello", PQ.empty)) :: Maybe (PriorityQ Int String)),+        -- testCase "Dequeue Empty Queue Should Be Nothing"+        --   (expect (Q.isEmpty $ PQ.dequeue $+        --              (PQ.empty :: PriorityQ Int ())) $ equalTo True),+        testProperty "Enqueue/Dequeue should respect Priority order"+            prop_pq_ordering+     ],+     testGroup "FIFO Queue Tests" [+        testCase "New Queue Should Be Empty"+          (expectThat (FIFO.isEmpty $ FIFO.empty) $ equalTo True),+        testCase "Singleton Queue Should Contain One Element"+          (expectThat (FIFO.dequeue $ FIFO.singleton "hello") $+             equalTo $ Just ("hello", FIFO.empty)),+        testCase "Dequeue Empty Queue Should Be Nothing"+          (expectThat (FIFO.dequeue $ (FIFO.empty :: SeqQ ())) $+            is (Nothing :: Maybe ((), SeqQ ()))),+        testProperty "Enqueue/Dequeue should respect FIFO order"+            prop_fifo_enqueue,+        testProperty "Enqueue/Dequeue should respect isEmpty"+            prop_enqueue_empty+     ]+   ]++main :: IO ()+main = defaultMain tests+
+ tests/TestTimer.hs view
@@ -0,0 +1,196 @@+module Main where++#if ! MIN_VERSION_base(4,6,0)+import Prelude hiding (catch)+#endif+import Control.Monad (forever)+import Control.Concurrent.MVar+  ( newEmptyMVar+  , putMVar+  , takeMVar+  , withMVar+  )+import qualified Network.Transport as NT (Transport)+import Network.Transport.TCP()+import Control.DeepSeq (NFData)+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.Tests.Internal.Utils++import Test.Framework (Test, testGroup, defaultMain)+import Test.Framework.Providers.HUnit (testCase)+import Network.Transport.TCP+import qualified Network.Transport as NT++import GHC.Generics++-- orphan instance+instance NFData Ping where++testSendAfter :: TestResult Bool -> Process ()+testSendAfter result =+  let delay = seconds 1 in do+  sleep $ seconds 10+  pid <- getSelfPid+  _ <- sendAfter delay pid Ping+  hdInbox <- receiveTimeout (asTimeout (seconds 2)) [+                      match (\m@(Ping) -> return m)+                    ]+  case hdInbox of+      Just Ping -> stash result True+      Nothing   -> stash result False++testRunAfter :: TestResult Bool -> Process ()+testRunAfter result =+  let delay = seconds 2 in do++  parentPid <- getSelfPid+  _ <- spawnLocal $ do+    _ <- runAfter delay $ send parentPid Ping+    return ()++  msg <- expectTimeout ((asTimeout delay) * 4)+  case msg of+      Just Ping -> stash result True+      Nothing   -> stash result False+  return ()++testCancelTimer :: TestResult Bool -> Process ()+testCancelTimer result = do+  let delay = milliSeconds 50+  pid <- periodically delay noop+  ref <- monitor pid++  sleep $ seconds 1+  cancelTimer pid++  _ <- receiveWait [+          match (\(ProcessMonitorNotification ref' pid' _) ->+                  stash result $ ref == ref' && pid == pid')+        ]++  return ()++testPeriodicSend :: TestResult Bool -> Process ()+testPeriodicSend result = do+  let delay = milliSeconds 100+  self <- getSelfPid+  ref <- ticker delay self+  listener 0 ref+  liftIO $ putMVar result True+  where listener :: Int -> TimerRef -> Process ()+        listener n tRef | n > 10    = cancelTimer tRef+                        | otherwise = waitOne >> listener (n + 1) tRef+        -- get a single tick, blocking indefinitely+        waitOne :: Process ()+        waitOne = do+            Tick <- expect+            return ()++testTimerReset :: TestResult Int -> Process ()+testTimerReset result = do+  let delay = seconds 10+  counter <- liftIO $ newEmptyMVar++  listenerPid <- spawnLocal $ do+      stash counter 0+      -- we continually listen for 'ticks' and increment counter for each+      forever $ do+        Tick <- expect+        liftIO $ withMVar counter (\n -> (return (n + 1)))++  -- this ticker will 'fire' every 10 seconds+  ref <- ticker delay listenerPid++  sleep $ seconds 2+  resetTimer ref++  -- at this point, the timer should be back to roughly a 5 second count down+  -- so our few remaining cycles no ticks ought to make it to the listener+  -- therefore we kill off the timer and the listener now and take the count+  cancelTimer ref+  kill listenerPid "stop!"++  -- how many 'ticks' did the listener observer? (hopefully none!)+  count <- liftIO $ takeMVar counter+  liftIO $ putMVar result count++testTimerFlush :: TestResult Bool -> Process ()+testTimerFlush result = do+  let delay = seconds 1+  self <- getSelfPid+  ref  <- ticker delay self++  -- sleep so we *should* have a message in our 'mailbox'+  sleep $ milliSeconds 2++  -- flush it out if it's there+  flushTimer ref Tick (Delay $ seconds 3)++  m <- expectTimeout 10+  case m of+      Nothing   -> stash result True+      Just Tick -> stash result False++testSleep :: TestResult Bool -> Process ()+testSleep r = do+  sleep $ seconds 20+  stash r True++--------------------------------------------------------------------------------+-- Utilities and Plumbing                                                     --+--------------------------------------------------------------------------------++tests :: LocalNode  -> [Test]+tests localNode = [+    testGroup "Timer Tests" [+        testCase "testSendAfter"+                 (delayedAssertion+                  "expected Ping within 1 second"+                  localNode True testSendAfter)+      , testCase "testRunAfter"+                 (delayedAssertion+                  "expecting run (which pings parent) within 2 seconds"+                  localNode True testRunAfter)+      , testCase "testCancelTimer"+                 (delayedAssertion+                  "expected cancelTimer to exit the timer process normally"+                  localNode True testCancelTimer)+      , testCase "testPeriodicSend"+                 (delayedAssertion+                  "expected ten Ticks to have been sent before exiting"+                  localNode True testPeriodicSend)+      , testCase "testTimerReset"+                 (delayedAssertion+                  "expected no Ticks to have been sent before resetting"+                  localNode 0 testTimerReset)+      , testCase "testTimerFlush"+                 (delayedAssertion+                  "expected all Ticks to have been flushed"+                  localNode True testTimerFlush)+      , testCase "testSleep"+                 (delayedAssertion+                  "why am I not seeing a delay!?"+                  localNode True testTimerFlush)+      ]+  ]++timerTests :: NT.Transport -> IO [Test]+timerTests transport = do+  localNode <- newLocalNode transport initRemoteTable+  let testData = tests localNode+  return testData++-- | 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++main :: IO ()+main = testMain $ timerTests