on master help sign in
Control.Reaper
commit875089646b81c89dfdfebc9467b1b807a6d84e83 authorMichael Snoyman <michael@snoyman.com> dateMon 4 Aug 2014, 10:05 (12 years ago) parent5108cb1
5 files changed+190−21
auto-update/Control/AutoUpdate.hs+1−20 auto-update/Control/AutoUpdate/Util.hs+23 auto-update/Control/Reaper.hs+130 auto-update/auto-update.cabal+3−1 auto-update/test/Control/ReaperSpec.hs+33
auto-update/Control/AutoUpdate.hs+1−20
@@ -1,6 +1,5 @@
11{-# LANGUAGE BangPatterns #-}
22{-# LANGUAGE DeriveDataTypeable #-}
3−{-# LANGUAGE CPP #-}
43
54-- | A common problem is the desire to have an action run at a scheduled
65-- interval, but only if it is needed. For example, instead of having
@@ -21,6 +20,7 @@ module Control.AutoUpdate
2120 , mkAutoUpdate
2221 ) where
2322
23+import Control.AutoUpdate.Util (atomicModifyIORef')
2424import Control.Concurrent (ThreadId, forkIO, myThreadId, threadDelay)
2525import Control.Exception (Exception, SomeException
2626 ,assert, fromException, handle,throwIO, throwTo)
@@ -28,25 +28,6 @@ import Control.Monad (forever, join)
2828import Data.IORef (IORef, newIORef)
2929import Data.Typeable (Typeable)
3030
31−#ifndef MIN_VERSION_base
32−#define MIN_VERSION_base(x,y,z) 1
33−#endif
34−
35−#if MIN_VERSION_base(4,6,0)
36−import Data.IORef (atomicModifyIORef')
37−#else
38−import Data.IORef (atomicModifyIORef)
39−-- | Strict version of 'atomicModifyIORef'. This forces both the value stored
40−-- in the 'IORef' as well as the value returned.
41−atomicModifyIORef' :: IORef a -> (a -> (a,b)) -> IO b
42−atomicModifyIORef' ref f = do
43− c <- atomicModifyIORef ref
44− (\x -> let (a, b) = f x -- Lazy application of "f"
45− in (a, a `seq` b)) -- Lazy application of "seq"
46− -- The following forces "a `seq` b", so it also forces "f x".
47− c `seq` return c
48−#endif
49−
5031-- | Default value for creating an @UpdateSettings@.
5132--
5233-- Since 0.1.0
auto-update/Control/AutoUpdate/Util.hsnew file+23
@@ -0,0 +1,23 @@
1+{-# LANGUAGE CPP #-}
2+module Control.AutoUpdate.Util
3+ ( atomicModifyIORef'
4+ ) where
5+
6+#ifndef MIN_VERSION_base
7+#define MIN_VERSION_base(x,y,z) 1
8+#endif
9+
10+#if MIN_VERSION_base(4,6,0)
11+import Data.IORef (atomicModifyIORef')
12+#else
13+import Data.IORef (atomicModifyIORef)
14+-- | Strict version of 'atomicModifyIORef'. This forces both the value stored
15+-- in the 'IORef' as well as the value returned.
16+atomicModifyIORef' :: IORef a -> (a -> (a,b)) -> IO b
17+atomicModifyIORef' ref f = do
18+ c <- atomicModifyIORef ref
19+ (\x -> let (a, b) = f x -- Lazy application of "f"
20+ in (a, a `seq` b)) -- Lazy application of "seq"
21+ -- The following forces "a `seq` b", so it also forces "f x".
22+ c `seq` return c
23+#endif
auto-update/Control/Reaper.hsnew file+130
@@ -0,0 +1,130 @@
1+-- | This module provides the ability to create reapers: dedicated cleanup
2+-- threads. These threads will automatically spawn and die based on the
3+-- presence of a workload to process on.
4+module Control.Reaper
5+ ( ReaperSettings
6+ , reaper
7+ , defaultReaperSettings
8+ , reaperAction
9+ , reaperDelay
10+ , reaperCons
11+ , reaperNull
12+ , mkListAction
13+ ) where
14+
15+import Prelude hiding (null)
16+import Data.Monoid (Monoid, mempty)
17+import Control.Monad (join, void)
18+import Data.Function (fix)
19+import Control.AutoUpdate.Util (atomicModifyIORef')
20+import Control.Concurrent (forkIO, threadDelay)
21+import Data.IORef (newIORef)
22+import Control.Exception (mask_)
23+import qualified Prelude
24+
25+-- | Settings for creating a reaper. This type has two parameters:
26+-- @workload@ gives the entire workload, whereas @item@ gives an
27+-- individual piece of the queue. A common approach is to have @workload@
28+-- be a list of @item@s. This is encouraged by 'defaultReaperSettings' and
29+-- 'mkListAction'.
30+--
31+-- Since 0.1.1
32+data ReaperSettings workload item = ReaperSettings
33+ { reaperAction :: workload -> IO (workload -> workload)
34+ -- ^ The action to perform on a workload. The result of this is a
35+ -- \"workload modifying\" function. In the common case of using lists,
36+ -- the result should be a difference list that prepends the remaining
37+ -- workload to the temporary workload. For help with setting up such
38+ -- an action, see 'mkListAction'.
39+ --
40+ -- Default: do nothing with the workload, and then prepend it to the
41+ -- temporary workload. This is incredibly useless; you should
42+ -- definitely override this default.
43+ --
44+ -- Since 0.1.1
45+ , reaperDelay :: {-# UNPACK #-} !Int
46+ -- ^ Number of microseconds to delay between calls of 'reaperAction'.
47+ --
48+ -- Default: 30 seconds.
49+ --
50+ -- Since 0.1.1
51+ , reaperCons :: item -> workload -> workload
52+ -- ^ Add an item onto a workload.
53+ --
54+ -- Default: list consing.
55+ --
56+ -- Since 0.1.1
57+ , reaperNull :: workload -> Bool
58+ -- ^ Check if a workload is empty, in which case the worker thread
59+ -- will shut down.
60+ --
61+ -- Default: 'Prelude.null'.
62+ --
63+ -- Since 0.1.1
64+ }
65+
66+-- | Default @ReaperSettings@ value, biased towards having a list of work
67+-- items.
68+--
69+-- Since 0.1.1
70+defaultReaperSettings :: ReaperSettings [item] item
71+defaultReaperSettings = ReaperSettings
72+ { reaperAction = \wl -> return (wl ++)
73+ , reaperDelay = 30000000
74+ , reaperCons = (:)
75+ , reaperNull = Prelude.null
76+ }
77+
78+-- | Create a reaper addition function. This funciton can be used to add
79+-- new items to the workload. Spawning of reaper threads will be handled
80+-- for you automatically.
81+--
82+-- Since 0.1.1
83+reaper :: Monoid workload
84+ => ReaperSettings workload item
85+ -> IO (item -> IO ())
86+reaper (ReaperSettings action delay cons null) = do
87+ stateRef <- newIORef Nothing
88+ return (update stateRef)
89+ where
90+ update stateRef item = mask_ $ join $ atomicModifyIORef' stateRef $ \ms ->
91+ case ms of
92+ Nothing -> (Just $ cons item mempty, spawn stateRef)
93+ Just wl -> (Just $ cons item wl, return ())
94+
95+ spawn stateRef = void $ forkIO $ fix $ \loop -> do
96+ threadDelay delay
97+ wl1 <- atomicModifyIORef' stateRef $ \ms ->
98+ case ms of
99+ Nothing -> error "Control.Reaper.reaper.spawn: unexpected Nothing (1)"
100+ Just wl -> (Just mempty, wl)
101+ wl2 <- action wl1
102+ join $ atomicModifyIORef' stateRef $ \ms ->
103+ case ms of
104+ Nothing -> error "Control.Reaper.reaper.spawn: unexpected Nothing (2)"
105+ Just wl3 ->
106+ let wl4 = wl2 wl3
107+ in if null wl4
108+ then (Nothing, return ())
109+ else (Just wl4, loop)
110+
111+-- | A helper function for creating 'reaperAction' functions. You would
112+-- provide this function with a function to process a single work item and
113+-- return either a new work item, or @Nothing@ if the work item is
114+-- expired.
115+--
116+-- Since 0.1.1
117+mkListAction :: (item -> IO (Maybe item'))
118+ -> [item]
119+ -> IO ([item'] -> [item'])
120+mkListAction f =
121+ loop id
122+ where
123+ loop front [] = return front
124+ loop front (x:xs) = do
125+ my <- f x
126+ let front' =
127+ case my of
128+ Nothing -> front
129+ Just y -> front . (y:)
130+ loop front' xs
auto-update/auto-update.cabal+3−1
@@ -1,5 +1,5 @@
11name: auto-update
2−version: 0.1.0.0
2+version: 0.1.1.0
33synopsis: Efficiently run periodic, on-demand actions
44description:
55 A common problem is the desire to have an action run at a scheduled interval, but only if it is needed. For example, instead of having every web request result in a new @getCurrentTime@ call, we'd like to have a single worker thread run every second, updating an @IORef@. However, if the request frequency is less than once per second, this is a pessimization, and worse, kills idle GC.
@@ -17,6 +17,8 @@ cabal-version: >=1.10
1717
1818library
1919 exposed-modules: Control.AutoUpdate
20+ Control.Reaper
21+ other-modules: Control.AutoUpdate.Util
2022 build-depends: base >= 4 && < 5
2123 default-language: Haskell2010
2224
auto-update/test/Control/ReaperSpec.hsnew file+33
@@ -0,0 +1,33 @@
1+module Control.ReaperSpec (spec) where
2+
3+import Control.Reaper
4+import Control.Concurrent
5+import Test.Hspec
6+import Test.Hspec.QuickCheck
7+import Data.IORef
8+
9+type Item = (Int, IORef Int)
10+
11+spec :: Spec
12+spec = prop "works" $ \is -> do
13+ addItem <- reaper defaultReaperSettings
14+ { reaperAction = mkListAction $ \(i, ref) -> do
15+ modifyIORef ref succ
16+ return $ if i > 1
17+ then Just (pred i, ref)
18+ else Nothing
19+ , reaperDelay = 1000
20+ }
21+
22+ let mkTest i = do
23+ ref <- newIORef 0
24+ let expected = (abs i `mod` 10) + 1
25+ addItem (expected, ref)
26+ return (expected, ref)
27+ tests <- mapM mkTest is
28+
29+ let test (expected, ref) = do
30+ actual <- readIORef ref
31+ actual `shouldBe` (expected :: Int)
32+ threadDelay 100000
33+ mapM_ test tests