packages feed

distributed-process-platform-0.1.0: src/Control/Distributed/Process/Platform/Async.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  Control.Distributed.Process.Platform.Async
-- 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)
--
-- The /async/ APIs provided by distributed-process-platform provide means
-- for spawning asynchronous operations, waiting for their results, cancelling
-- them and various other utilities. The two primary implementation are
-- @AsyncChan@ which provides a handle which is scoped to the calling process,
-- and @AsyncSTM@, whose async mechanism can be used by (i.e., shared across)
-- multiple local processes, though its handles cannot be serialised.
--
-- Both abstractions can run asynchronous operations on remote nodes. The STM
-- based implementation provides a slightly richer API. The API defined in
-- /this/ module only supports a subset of operations on async handles,
-- and (specifically) does not support mixing handles initialised via
-- different implementations.
--
-- [Asynchronous Operations]
--
-- There is an implicit contract for async workers; Workers must exit
-- normally (i.e., should not call the 'exit', 'die' or 'terminate'
-- Cloud Haskell primitives), otherwise the 'AsyncResult' will end up being
-- @AsyncFailed DiedException@ instead of containing the result.
--
-- See "Control.Distributed.Process.Platform.Async.AsyncSTM",
-- "Control.Distributed.Process.Platform.Async.AsyncChan",
-- "Control.Distributed.Platform.Task",
-- "Control.Distributed.Platform.Task.Execution".
-----------------------------------------------------------------------------

module Control.Distributed.Process.Platform.Async
 ( -- * Exported Types
    Async(..)
  , AsyncRef
  , AsyncTask(..)
  , AsyncResult(..)
  -- * Spawning asynchronous operations
  , async
  , asyncLinked
  , asyncSTM
  , asyncLinkedSTM
  , asyncChan
  , asyncLinkedChan
  , task
  , remoteTask
  , monitorAsync
  -- * Cancelling asynchronous operations
  , cancel
  , cancelWait
  , cancelWith
  , cancelKill
    -- * Querying for results
  , poll
  , check
  , wait
    -- * Waiting with timeouts
  , waitTimeout
  , waitCancelTimeout
  , waitCheckTimeout
  ) where

import Control.Distributed.Process
import Control.Distributed.Process.Serializable
  ( Serializable
  , SerializableDict
  )
import Control.Distributed.Process.Platform.Async.Types
  ( Async(..)
  , AsyncRef
  , AsyncTask(..)
  , AsyncResult(..)
  )
import qualified Control.Distributed.Process.Platform.Async.AsyncSTM as AsyncSTM
import qualified Control.Distributed.Process.Platform.Async.AsyncChan as AsyncChan
import Control.Distributed.Process.Platform.Time
import Data.Maybe
  ( fromMaybe
  )

--------------------------------------------------------------------------------
-- API                                                                        --
--------------------------------------------------------------------------------

-- | Spawn an 'AsyncTask' and return the 'Async' handle to it.
-- See 'asyncSTM'.
async :: (Serializable a) => Process a -> Process (Async a)
async t = asyncSTM (AsyncTask t)

-- | Spawn an 'AsyncTask' (linked to the calling process) and
-- return the 'Async' handle to it.
-- See 'asyncSTM'.
asyncLinked :: (Serializable a) => Process a -> Process (Async a)
asyncLinked p = AsyncSTM.newAsync AsyncSTM.asyncLinked (AsyncTask p)

-- | Spawn an 'AsyncTask' and return the 'Async' handle to it.
-- Uses the STM implementation, whose handles can be read by other
-- processes, though they're not @Serializable@.
--
-- See 'Control.Distributed.Process.Platform.Async.AsyncSTM'.
asyncSTM :: (Serializable a) => AsyncTask a -> Process (Async a)
asyncSTM = AsyncSTM.newAsync AsyncSTM.async

-- | Spawn an 'AsyncTask' (linked to the calling process) and return the
-- 'Async' handle to it. Uses the STM based implementation, whose handles
-- can be read by other processes, though they're not @Serializable@.
--
-- See 'Control.Distributed.Process.Platform.Async.AsyncSTM'.
asyncLinkedSTM :: (Serializable a) => AsyncTask a -> Process (Async a)
asyncLinkedSTM = AsyncSTM.newAsync AsyncSTM.asyncLinked

-- | Spawn an 'AsyncTask' and return the 'Async' handle to it.
-- Uses a channel based implementation, whose handles can only be read once,
-- and only by the calling process.
--
-- See 'Control.Distributed.Process.Platform.Async.AsyncChan'.
asyncChan :: (Serializable a) => AsyncTask a -> Process (Async a)
asyncChan = AsyncChan.newAsync AsyncChan.async

-- | Linked version of 'asyncChan'.
--
-- See 'Control.Distributed.Process.Platform.Async.AsyncChan'.
asyncLinkedChan :: (Serializable a) => AsyncTask a -> Process (Async a)
asyncLinkedChan = AsyncChan.newAsync AsyncChan.asyncLinked

-- | Wraps a regular @Process a@ as an 'AsyncTask'.
task :: Process a -> AsyncTask a
task = AsyncTask

-- | Wraps the components required and builds a remote 'AsyncTask'.
remoteTask :: Static (SerializableDict a)
              -> NodeId
              -> Closure (Process a)
              -> AsyncTask a
remoteTask = AsyncRemoteTask

-- | Given an 'Async' handle, monitor the worker process.
monitorAsync :: Async a -> Process MonitorRef
monitorAsync = monitor . asyncWorker

-- | Check whether an 'Async' handle has completed yet. The status of the
-- action is encoded in the returned 'AsyncResult'. If the action has not
-- completed, the result will be 'AsyncPending', or one of the other
-- constructors otherwise. This function does not block waiting for the result.
-- Use 'wait' or 'waitTimeout' if you need blocking/waiting semantics.
{-# INLINE poll #-}
poll :: (Serializable a) => Async a -> Process (AsyncResult a)
poll = hPoll

-- | Like 'poll' but returns 'Nothing' if @(poll hAsync) == AsyncPending@.
-- See 'poll'.
check :: (Serializable a) => Async a -> Process (Maybe (AsyncResult a))
check hAsync = poll hAsync >>= \r -> case r of
  AsyncPending -> return Nothing
  ar           -> return (Just ar)

-- | Wait for an asynchronous operation to complete or timeout. This variant
-- returns the 'AsyncResult' itself, which will be 'AsyncPending' if the
-- result has not been made available, otherwise one of the other constructors.
{-# INLINE waitCheckTimeout #-}
waitCheckTimeout :: (Serializable a) =>
                    TimeInterval -> Async a -> Process (AsyncResult a)
waitCheckTimeout t hAsync =
  waitTimeout t hAsync >>= return . fromMaybe (AsyncPending)

-- | Wait for an asynchronous action to complete, and return its
-- value. The result (which can include failure and/or cancellation) is
-- encoded by the 'AsyncResult' type.
{-# INLINE wait #-}
wait :: Async a -> Process (AsyncResult a)
wait = hWait

-- | Wait for an asynchronous operation to complete or timeout. Returns
-- @Nothing@ if the 'AsyncResult' does not change from @AsyncPending@ within
-- the specified delay, otherwise @Just asyncResult@ is returned. If you want
-- to wait/block on the 'AsyncResult' without the indirection of @Maybe@ then
-- consider using 'wait' or 'waitCheckTimeout' instead.
{-# INLINE waitTimeout #-}
waitTimeout :: (Serializable a) =>
               TimeInterval -> Async a -> Process (Maybe (AsyncResult a))
waitTimeout = flip hWaitTimeout

-- | Wait for an asynchronous operation to complete or timeout. If it times out,
-- then 'cancelWait' the async handle instead.
--
waitCancelTimeout :: (Serializable a)
                  => TimeInterval
                  -> Async a
                  -> Process (AsyncResult a)
waitCancelTimeout t hAsync = do
  r <- waitTimeout t hAsync
  case r of
    Nothing -> cancelWait hAsync
    Just ar -> return ar

-- | Cancel an asynchronous operation. Cancellation is asynchronous in nature.
-- To wait for cancellation to complete, use 'cancelWait' instead. The notes
-- about the asynchronous nature of 'cancelWait' apply here also.
--
-- See 'Control.Distributed.Process'
{-# INLINE cancel #-}
cancel :: Async a -> Process ()
cancel = hCancel

-- | Cancel an asynchronous operation and wait for the cancellation to complete.
-- Because of the asynchronous nature of message passing, the instruction to
-- cancel will race with the asynchronous worker, so it is /entirely possible/
-- that the 'AsyncResult' returned will not necessarily be 'AsyncCancelled'. For
-- example, the worker may complete its task after this function is called, but
-- before the cancellation instruction is acted upon.
--
-- If you wish to stop an asychronous operation /immediately/ (with caveats)
-- then consider using 'cancelWith' or 'cancelKill' instead.
--
{-# INLINE cancelWait #-}
cancelWait :: (Serializable a) => Async a -> Process (AsyncResult a)
cancelWait hAsync = cancel hAsync >> wait hAsync

-- | Cancel an asynchronous operation immediately.
-- This operation is performed by sending an /exit signal/ to the asynchronous
-- worker, which leads to the following semantics:
--
--     1. If the worker already completed, this function has no effect.
--
--     2. The worker might complete after this call, but before the signal arrives.
--
--     3. The worker might ignore the exit signal using @catchExit@.
--
-- In case of (3), this function has no effect. You should use 'cancel'
-- if you need to guarantee that the asynchronous task is unable to ignore
-- the cancellation instruction.
--
-- You should also consider that when sending exit signals to a process, the
-- definition of 'immediately' is somewhat vague and a scheduler might take
-- time to handle the request, which can lead to situations similar to (1) as
-- listed above, if the scheduler to which the calling process' thread is bound
-- decides to GC whilst another scheduler on which the worker is running is able
-- to continue.
--
-- See 'Control.Distributed.Process.exit'
{-# INLINE cancelWith #-}
cancelWith :: (Serializable b) => b -> Async a -> Process ()
cancelWith reason = (flip exit) reason . asyncWorker

-- | Like 'cancelWith' but sends a @kill@ instruction instead of an exit signal.
--
-- See 'Control.Distributed.Process.kill'
{-# INLINE cancelKill #-}
cancelKill :: String -> Async a -> Process ()
cancelKill reason = (flip kill) reason . asyncWorker