{-# LANGUAGE StandaloneDeriving, DeriveDataTypeable, RecursiveDo,
             BangPatterns, UnboxedTuples #-}

-- |
-- Module      :  Happstack.Util.TimeOut
-- Copyright   :  (c) Happstack.com, 2009; (c) HAppS.org, 2005
-- License     :  BSD3
-- Portability :  uses mdo
-- Timeout implementation for performing operations in the IO monad
-- with a timeout added. Both using Maybe and exceptions to handle
-- timeouts are supported.
-- Timeouts can be implemented in GHC with either a global handler
-- or a per-timeout thread which sleeps until the timeout. The latter
-- is used in this module. Blocking on foreign calls can cause
-- problems as GHC has no way of interrupting such threads.
-- The module provides a slightly slower alternative implementation
-- which returns even if the computation has blocked on a foreign
-- call. This should not be an issue unless -threaded is used.
-- The timeouts are currently limited to a maximum of about
-- 2000 seconds. This is a feature of threadDelay, but
-- supporting longer timeouts is certainly possible if
-- that is desirable.
-- For nested timeouts there are different ways to implement them:
-- a) attach an id to the exception so that the catch knows wether it may catch
--    this timout exception. I've choosen this because overhead is only passing
--    and incrementing an integer value. A integer wrap araound is possible but
--    too unlikely to happen to make me worry about it
-- b) start a new workiing and killing thread so that if the original thread
--   was run within withTimeOut itself it catches the exception and not an inner
--   timout. (this is done in withSafeTimeOut, for another reason though)
-- c) keep throwing exceptions until the the withTimeOut function kills the
--   killing thread. But consider sequence (forever (timeOut threadDelay 10sec) )
--   In this case the exception will be called and the next timOut may be entered
--   before the second Exception has been thrown
-- All exceptions but the internal TimeOutExceptionI are rethrown in the calling thread
module Happstack.Util.TimeOut 
    (withTimeOut, withTimeOutMaybe,
     withSafeTimeOut, withSafeTimeOutMaybe,
     TimeOutException(..), second
    ) where

import Control.Concurrent
import qualified Control.Concurrent.MVar.Strict as SM
import Control.Exception.Extensible as E
import Data.Typeable(Typeable)
import Data.Maybe
import System.IO.Unsafe (unsafePerformIO)
import Control.Monad (when)

import Happstack.Util.Concurrent

type TimeOutTId = Int -- must be distinct within a thread only 

{-# NOINLINE timeOutIdState #-}
timeOutIdState :: SM.MVar TimeOutTId
timeOutIdState = unsafePerformIO $ SM.newMVar minBound

nextTimeOutId :: IO TimeOutTId
nextTimeOutId = 
  SM.modifyMVar timeOutIdState $ \a ->
      let nid = nextId a in return (nid `seq` (nid,nid))

  where nextId !i | i == maxBound = minBound
        nextId !i = i + 1

data TimeOutExceptionI = TimeOutExceptionI !TimeOutTId -- internal exception, should only be used within this module 

data TimeOutException = TimeOutException -- that's the exception the user may catch 

instance Show TimeOutExceptionI where show _ = error "this TimeOutExceptionI should have been caught within this module"
instance E.Exception TimeOutExceptionI

deriving instance Show TimeOutException
instance E.Exception TimeOutException

throw' :: Exception exception => exception -> b
throw' = throw

throwTo' :: Exception e => ThreadId -> e -> IO ()
throwTo' = E.throwTo

catch' :: Exception e => IO a -> (e -> IO a) -> IO a
catch' = E.catch

try' :: IO a -> IO (Either SomeException a) -- give a type signature for try 
try' = E.try

-- module internal function 
catchTimeOutI :: TimeOutTId -> IO a -> IO a -> IO a
catchTimeOutI toId op handler =
  op `catch'` (\e@(TimeOutExceptionI i) -> if i == toId then handler  else throw' e)

-- | This handler returns @Nothing@ if the timeout occurs and @Just a@ if computation 
-- returns @a@.
withTimeOutMaybe :: Int -> IO a -> IO (Maybe a)
withTimeOutMaybe tout op = do 
  toId <- nextTimeOutId
  wtid <- myThreadId
  ktid <- fork ( do threadDelay tout 
                    throwTo' wtid (TimeOutExceptionI toId)
  catchTimeOutI toId (fmap Just (op >>= \r -> killThread ktid >> return  r)) (return Nothing)

-- | This is the normal timeout handler. It throws a TimeOutException exception,
-- if the timeout occurs.
withTimeOut :: Int -> IO a -> IO a
withTimeOut tout op = maybeToEx =<< withTimeOutMaybe tout op

maybeToEx :: (Monad m) => Maybe t -> m t  
maybeToEx (Just r) = return r
maybeToEx Nothing = throw' TimeOutException

-- | Like timeOut, but additionally it works even if the computation is blocking
-- async exceptions (explicitely or by a blocking FFI call). This consumes
-- more resources than timeOut, but is still quite fast.
withSafeTimeOut :: Int -> IO a -> IO a
withSafeTimeOut tout op = maybeToEx =<< withSafeTimeOutMaybe tout op

-- | Like withTimeOutMaybe, but handles the operation blocking exceptions like withSafeTimeOut
-- does.
withSafeTimeOutMaybe :: Int -> IO a -> IO (Maybe a)
withSafeTimeOutMaybe tout op = mdo
  mv <- newEmptyMVar
  wt <- fork $ do 
          t <- try' op
          case t of
            Left e -> tryPutMVar mv (Left e)
            Right r -> tryPutMVar mv (Right (Just r))
          killThread kt
  kt <- fork $ do 
          threadDelay tout
          e <- tryPutMVar mv (Right Nothing)
          when e $ killThread wt
  eitherToEx =<< takeMVar mv
  where eitherToEx (Left e) = throw' e
        eitherToEx (Right r) = return r

-- | Constant representing one second.
second :: Int
second = 1000000