packages feed

festung-0.9.1.1: src/Festung/Concurrency/Job.hs

{-# LANGUAGE NamedFieldPuns #-}

module Festung.Concurrency.Job 
  ( Job
  , Command(..)
  , JobStatus(..)
  , newJob
  , newJob_
  , sendMappedCommand
  , sendCommand
  , killJob
  , keepGoing
  , isJobExited
  , isJobRunning
  , onExit
  ) where

import Control.Concurrent
import Control.Exception
import Control.Monad
import Data.Maybe

import Festung.Concurrency.Utils (readAnyMVar)
import Festung.Utils             (eitherUnitToMaybe)


data Job a = Job { chan   :: Chan a
                 , exited :: MVar ()
                 , tid    :: ThreadId
                 }

instance Show (Job a) where
    show _ = "Job"

data Command c r = Command c (MVar r)


data JobStatus = KeepGoing
               | Stop


-- | Folds over all message sent to the job
--
--  Takes the initial state as the first argument.
--  The second argument is the folding function.
--  The last argument is the action to execute on exit.
newJob :: a -> (a -> b -> IO (JobStatus, a)) -> IO (Job b)
newJob init_ f = do
    chan    <- newChan
    exited  <- newEmptyMVar
    running <- newEmptyMVar
    tid     <- forkIO $
        finally (putMVar running () >> consume chan init_)
                (putMVar exited ())
    readMVar running

    return Job { chan = chan, exited = exited, tid = tid }
        where consume chan ini = do
                msg             <- readChan chan
                (status, state) <- mask_ $ f ini msg
                case status of
                    KeepGoing -> consume chan state
                    Stop      -> return ()


-- | Make a function always running
keepGoing :: (a -> IO b) -> (a -> IO (JobStatus, b))
keepGoing = (fmap . fmap) ((,) KeepGoing)


-- | Create a job without any state (just consuming the messages)
newJob_ :: (b -> IO ()) -> IO (Job b)
newJob_ f = newJob () $ const (keepGoing f)


-- | map the command before sending it to the job
--
-- Some job can receive multiple commands, therefore the use a
-- union type:
--
-- @
--    JobCommand = SquareCmd (Command Int Int)
--               | ConcatCmd (Command (String, String) String)
--               ...
-- @
--
-- This gives the ability to map the command with the constructor of the union type
-- before sending it to the job in question.
--
-- This sends the command in question, and waits for the result to be sent back.
--
-- If the job is exited, it returns @Nothing@
sendMappedCommand :: (Command c r -> a) -> Job a -> c -> IO (Maybe r)
sendMappedCommand constructor Job{chan,exited} command = do
    responder <- newEmptyMVar
    writeChan chan $ constructor (Command command responder)
    eitherUnitToMaybe <$> readAnyMVar exited responder



-- | Same as @'sendMappedCommand', but for a job receiving only one command type.
sendCommand :: Job (Command c r) -> c -> IO (Maybe r)
sendCommand = sendMappedCommand id


-- | Stops a running job, and wait for it to stop.
--
-- If the job is not running, this is a noop.
killJob :: Job a -> IO ()
killJob Job{tid,exited} = killThread tid >> readMVar exited


isJobExited :: Job a -> IO Bool
isJobExited Job{exited} = isJust <$> tryReadMVar exited


isJobRunning :: Job a -> IO Bool
isJobRunning job = not <$> isJobExited job


-- | Register a finalizer when the job exits (even after an exception)
onExit :: Job a -> IO () -> IO ()
onExit Job{exited} action = void $ forkIO (readMVar exited >> action)