@@ -0,0 +1,156 @@
1+{-# LANGUAGE BangPatterns #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+-- | A common problem is the desire to have an action run at a scheduled
4+-- interval, but only if it is needed. For example, instead of having
5+-- every web request result in a new @getCurrentTime@ call, we'd like to
6+-- have a single worker thread run every second, updating an @IORef@.
7+-- However, if the request frequency is less than once per second, this is
8+-- a pessimization, and worse, kills idle GC.
10+-- This library allows you to define actions which will either be
11+-- performed by a dedicated thread or, in times of low volume, will be
12+-- executed by the calling thread.
13+module Control.AutoUpdate
15+ , defaultUpdateSettings
17+ , updateSpawnThreshold
22+import Control.Concurrent (ThreadId, forkIO, killThread, myThreadId,
24+import Control.Exception (Exception, assert, fromException, handle,
26+import Control.Monad (forever, join)
27+import Data.IORef (IORef, atomicModifyIORef, newIORef)
28+import Data.Typeable (Typeable)
30+-- | Default value for creating an @UpdateSettings@.
33+defaultUpdateSettings :: UpdateSettings ()
34+defaultUpdateSettings = UpdateSettings
35+ { updateFreq = 1000000
36+ , updateSpawnThreshold = 3
37+ , updateAction = return ()
40+-- | Settings to control how values are updated.
42+-- This should be constructed using @defaultUpdateSettings@ and record
43+-- update syntax, e.g.:
46+-- let set = defaultUpdateSettings { updateAction = getCurrentTime }
50+data UpdateSettings a = UpdateSettings
52+ -- ^ Microseconds between update calls. Same considerations as
53+ -- @threadDelay@ apply.
55+ -- Default: 1 second (1000000)
58+ , updateSpawnThreshold :: Int
59+ -- ^ How many times the data must be requested before we decide to
60+ -- spawn a dedicated thread.
65+ , updateAction :: IO a
66+ -- ^ Action to be performed to get the current value.
68+ -- Default: does nothing.
73+data Status a = AutoUpdated
76+ -- Number of times used since last updated.
77+ {-# UNPACK #-} !ThreadId
81+ -- Number of times used since we started/switched
82+ -- off manual updates.
84+-- | A value which will be updated, either via a dedicated thread or when
85+-- requested. Request the current value with @getCurrent@.
88+data AutoUpdate a = AutoUpdate
89+-- We use bang patterns on all arguments to ensure no bottoms leak in.
90+ { auFreq :: {-# UNPACK #-} !Int
91+ , auSpawnThreadhold :: {-# UNPACK #-} !Int
92+ , auAction :: !(IO ())
93+ , auStatus :: !(IORef (Status a))
96+-- | Generate an action which will either read from an automatically
97+-- updated value, or run the update action in the current thread.
100+mkAutoUpdate :: UpdateSettings a -> IO (IO a)
101+mkAutoUpdate (UpdateSettings !f !t !a) = do
102+ istatus <- newIORef $ ManualUpdates 0
103+ return $! getCurrent f t a istatus
105+data Action a = Return a | Manual | Spawn
107+-- | Get the current value, either fed from an auto-update thread, or
108+-- computed manually in the current thread.
111+getCurrent :: Int -- ^ frequency
112+ -> Int -- ^ spawn threshold
113+ -> IO a -- ^ internal update action
114+ -> IORef (Status a) -- ^ mutable state
116+getCurrent freq spawnThreshold action istatus = do
117+ ea <- atomicModifyIORef istatus $ \status ->
119+ AutoUpdated a cnt tid -> (AutoUpdated a (succ cnt) tid, Return a)
120+ ManualUpdates i -> (ManualUpdates (succ i), if i > spawnThreshold then Spawn else Manual)
122+ Return a -> return a
127+ join $ atomicModifyIORef istatus $ \status ->
129+ AutoUpdated _ cnt old -> (AutoUpdated a cnt tid, throwTo old Replaced)
130+ ManualUpdates cnt -> (AutoUpdated a cnt tid, return ())
133+ spawn = handle onErr $ forever $ do
136+ let stop = throwIO Replaced
137+ join $ atomicModifyIORef istatus $ \status ->
139+ AutoUpdated _ cnt tid
140+ | cnt >= 1 -> (AutoUpdated a 0 tid, return ())
141+ | otherwise -> (ManualUpdates 0, stop)
142+ ManualUpdates i -> assert False (ManualUpdates i, stop)
144+ case fromException ex of
145+ Just Replaced -> return ()
148+ atomicModifyIORef istatus $ \status ->
150+ AutoUpdated _ _ tid' | tid == tid' -> (ManualUpdates 0, ())
154+data Replaced = Replaced
155+ deriving (Show, Typeable)
156+instance Exception Replaced