{-# LANGUAGE CPP #-} -- | In a multithreaded environment, running actions on a regularly scheduled -- background thread can dramatically improve performance. -- For example, web servers need to return the current time with each HTTP response. -- For a high-volume server, it's much faster for a dedicated thread to run every -- second, and write the current time to a shared 'IORef', than it is for each -- request to make its own call to 'getCurrentTime'. -- -- But for a low-volume server, whose request frequency is less than once per -- second, that approach will result in /more/ calls to 'getCurrentTime' than -- necessary, and worse, kills idle GC. -- -- This library solves that problem by allowing you to define actions which will -- either be performed by a dedicated thread, or, in times of low volume, will -- be executed by the calling thread. -- -- Example usage: -- -- @ -- import "Data.Time" -- import "Control.AutoUpdate" -- -- getTime <- 'mkAutoUpdate' 'defaultUpdateSettings' -- { 'updateAction' = 'Data.Time.Clock.getCurrentTime' -- , 'updateFreq' = 1000000 -- The default frequency, once per second -- } -- currentTime <- getTime -- @ -- -- For more examples, . module Control.AutoUpdate ( -- * Type UpdateSettings , defaultUpdateSettings -- * Accessors , updateAction , updateFreq , updateSpawnThreshold -- * Creation , mkAutoUpdate , mkAutoUpdateWithModify ) where #if __GLASGOW_HASKELL__ < 709 import Control.Applicative ((<*>)) #endif import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.MVar (newEmptyMVar, putMVar, readMVar, takeMVar, tryPutMVar) import Control.Exception (SomeException, catch, mask_, throw, try) import Control.Monad (void) import Data.IORef (newIORef, readIORef, writeIORef) -- | Default value for creating an 'UpdateSettings'. -- -- @since 0.1.0 defaultUpdateSettings :: UpdateSettings () defaultUpdateSettings = UpdateSettings { updateFreq = 1000000 , updateSpawnThreshold = 3 , updateAction = return () } -- | Settings to control how values are updated. -- -- This should be constructed using 'defaultUpdateSettings' and record -- update syntax, e.g.: -- -- @ -- let settings = 'defaultUpdateSettings' { 'updateAction' = 'Data.Time.Clock.getCurrentTime' } -- @ -- -- @since 0.1.0 data UpdateSettings a = UpdateSettings { updateFreq :: Int -- ^ Microseconds between update calls. Same considerations as -- 'threadDelay' apply. -- -- Default: 1 second (1000000) -- -- @since 0.1.0 , updateSpawnThreshold :: Int -- ^ NOTE: This value no longer has any effect, since worker threads are -- dedicated instead of spawned on demand. -- -- Previously, this determined how many times the data must be requested -- before we decide to spawn a dedicated thread. -- -- Default: 3 -- -- @since 0.1.0 , updateAction :: IO a -- ^ Action to be performed to get the current value. -- -- Default: does nothing. -- -- @since 0.1.0 } -- | Generate an action which will either read from an automatically -- updated value, or run the update action in the current thread. -- -- @since 0.1.0 mkAutoUpdate :: UpdateSettings a -> IO (IO a) mkAutoUpdate us = mkAutoUpdateHelper us Nothing -- | Generate an action which will either read from an automatically -- updated value, or run the update action in the current thread if -- the first time or the provided modify action after that. -- -- @since 0.1.4 mkAutoUpdateWithModify :: UpdateSettings a -> (a -> IO a) -> IO (IO a) mkAutoUpdateWithModify us f = mkAutoUpdateHelper us (Just f) mkAutoUpdateHelper :: UpdateSettings a -> Maybe (a -> IO a) -> IO (IO a) mkAutoUpdateHelper us updateActionModify = do -- A baton to tell the worker thread to generate a new value. needsRunning <- newEmptyMVar -- The initial response variable. Response variables allow the requesting -- thread to block until a value is generated by the worker thread. responseVar0 <- newEmptyMVar -- The current value, if available. We start off with a Left value -- indicating no value is available, and the above-created responseVar0 to -- give a variable to block on. currRef <- newIORef $ Left responseVar0 -- This is used to set a value in the currRef variable when the worker -- thread exits. In reality, that value should never be used, since the -- worker thread exiting only occurs if an async exception is thrown, which -- should only occur if there are no references to needsRunning left. -- However, this handler will make error messages much clearer if there's a -- bug in the implementation. let fillRefOnExit f = do eres <- try f case eres of Left e -> writeIORef currRef $ error $ "Control.AutoUpdate.mkAutoUpdate: worker thread exited with exception: " ++ show (e :: SomeException) Right () -> writeIORef currRef $ error $ "Control.AutoUpdate.mkAutoUpdate: worker thread exited normally, " ++ "which should be impossible due to usage of infinite loop" -- fork the worker thread immediately. Note that we mask async exceptions, -- but *not* in an uninterruptible manner. This will allow a -- BlockedIndefinitelyOnMVar exception to still be thrown, which will take -- down this thread when all references to the returned function are -- garbage collected, and therefore there is no thread that can fill the -- needsRunning MVar. -- -- Note that since we throw away the ThreadId of this new thread and never -- calls myThreadId, normal async exceptions can never be thrown to it, -- only RTS exceptions. mask_ $ void $ forkIO $ fillRefOnExit $ do -- This infinite loop makes up out worker thread. It takes an a -- responseVar value where the next value should be putMVar'ed to for -- the benefit of any requesters currently blocked on it. let loop responseVar maybea = do -- block until a value is actually needed takeMVar needsRunning -- new value requested, so run the updateAction a <- catchSome $ maybe (updateAction us) id (updateActionModify <*> maybea) -- we got a new value, update currRef and lastValue writeIORef currRef $ Right a putMVar responseVar a -- delay until we're needed again threadDelay $ updateFreq us -- delay's over. create a new response variable and set currRef -- to use it, so that the next requester will block on that -- variable. Then loop again with the updated response -- variable. responseVar' <- newEmptyMVar writeIORef currRef $ Left responseVar' loop responseVar' (Just a) -- Kick off the loop, with the initial responseVar0 variable. loop responseVar0 Nothing return $ do mval <- readIORef currRef case mval of Left responseVar -> do -- no current value, force the worker thread to run... void $ tryPutMVar needsRunning () -- and block for the result from the worker readMVar responseVar -- we have a current value, use it Right val -> return val -- | Turn a runtime exception into an impure exception, so that all 'IO' -- actions will complete successfully. This simply defers the exception until -- the value is forced. catchSome :: IO a -> IO a catchSome act = Control.Exception.catch act $ \e -> return $ throw (e :: SomeException)