on master help sign in
Merge pull request #276 from yesodweb/warp-auto-update

Use auto-update in Warp

commit19181598f97d1b4db66f6862853e16870c13ff6e authorKazu Yamamoto <kazu@iij.ad.jp> dateWed 6 Aug 2014, 12:14 (12 years ago) parent61081b2f4fac7c
12 files changed+255−146
auto-update/Control/AutoUpdate.hs+1−20 auto-update/Control/AutoUpdate/Util.hs+23 auto-update/Control/Reaper.hs+136 auto-update/auto-update.cabal+3−1 auto-update/test/Control/ReaperSpec.hs+35 warp/Network/Wai/Handler/Warp/Date.hs+7−20 warp/Network/Wai/Handler/Warp/FdCache.hs+26−28 warp/Network/Wai/Handler/Warp/MultiMap.hs+1−1 warp/Network/Wai/Handler/Warp/Thread.hs−28 warp/Network/Wai/Handler/Warp/Timeout.hs+20−19 warp/test/ThreadSpec.hs−26 warp/warp.cabal+3−3
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+136
@@ -0,0 +1,136 @@
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+ , reaperEmpty
13+ , mkListAction
14+ ) where
15+
16+import Prelude hiding (null, empty)
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 (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+ , reaperEmpty :: workload
65+ -- ^ An empty workload.
66+ --
67+ -- Default: empty list.
68+ --
69+ -- Since 0.1.1
70+ }
71+
72+-- | Default @ReaperSettings@ value, biased towards having a list of work
73+-- items.
74+--
75+-- Since 0.1.1
76+defaultReaperSettings :: ReaperSettings [item] item
77+defaultReaperSettings = ReaperSettings
78+ { reaperAction = \wl -> return (wl ++)
79+ , reaperDelay = 30000000
80+ , reaperCons = (:)
81+ , reaperNull = Prelude.null
82+ , reaperEmpty = []
83+ }
84+
85+-- | Create a reaper addition function. This funciton can be used to add
86+-- new items to the workload. Spawning of reaper threads will be handled
87+-- for you automatically.
88+--
89+-- Since 0.1.1
90+reaper :: ReaperSettings workload item
91+ -> IO (item -> IO (), IORef (Maybe workload))
92+reaper (ReaperSettings action delay cons null empty) = do
93+ stateRef <- newIORef Nothing
94+ return (update stateRef, stateRef)
95+ where
96+ update stateRef item = mask_ $ join $ atomicModifyIORef' stateRef $ \ms ->
97+ case ms of
98+ Nothing -> (Just $ cons item empty, spawn stateRef)
99+ Just wl -> (Just $ cons item wl, return ())
100+
101+ spawn stateRef = void $ forkIO $ fix $ \loop -> do
102+ threadDelay delay
103+ wl1 <- atomicModifyIORef' stateRef $ \ms ->
104+ case ms of
105+ Nothing -> error "Control.Reaper.reaper.spawn: unexpected Nothing (1)"
106+ Just wl -> (Just empty, wl)
107+ wl2 <- action wl1
108+ join $ atomicModifyIORef' stateRef $ \ms ->
109+ case ms of
110+ Nothing -> error "Control.Reaper.reaper.spawn: unexpected Nothing (2)"
111+ Just wl3 ->
112+ let wl4 = wl2 wl3
113+ in if null wl4
114+ then (Nothing, return ())
115+ else (Just wl4, loop)
116+
117+-- | A helper function for creating 'reaperAction' functions. You would
118+-- provide this function with a function to process a single work item and
119+-- return either a new work item, or @Nothing@ if the work item is
120+-- expired.
121+--
122+-- Since 0.1.1
123+mkListAction :: (item -> IO (Maybe item'))
124+ -> [item]
125+ -> IO ([item'] -> [item'])
126+mkListAction f =
127+ loop id
128+ where
129+ loop front [] = return front
130+ loop front (x:xs) = do
131+ my <- f x
132+ let front' =
133+ case my of
134+ Nothing -> front
135+ Just y -> front . (y:)
136+ 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+35
@@ -0,0 +1,35 @@
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, wlRef) <- 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
34+ Nothing <- readIORef wlRef
35+ return ()
warp/Network/Wai/Handler/Warp/Date.hs+7−20
@@ -8,11 +8,8 @@ module Network.Wai.Handler.Warp.Date (
88 ) where
99
1010import Control.Applicative
11−import Control.Concurrent
12−import Control.Exception
13−import Control.Monad
11+import Control.AutoUpdate (defaultUpdateSettings, updateAction, mkAutoUpdate)
1412import Data.ByteString.Char8
15−import Data.IORef
1613
1714#if WINDOWS
1815import Data.Time
@@ -26,28 +23,18 @@ import System.Posix (epochTime)
2623type GMTDate = ByteString
2724
2825-- | The type of the cache of the Date header value.
29−data DateCache = DateCache (IORef GMTDate)
26+type DateCache = IO GMTDate
3027
3128-- | Creating 'DateCache' and executing the action.
3229withDateCache :: (DateCache -> IO a) -> IO a
33−withDateCache action = bracket initialize
34− (\(t,_) -> killThread t)
35− (\(_,dc) -> action dc)
36−
37−initialize :: IO (ThreadId, DateCache)
38−initialize = do
39− dc <- DateCache <$> (getCurrentGMTDate >>= newIORef)
40− t <- forkIO $ forever $ do
41− threadDelay 1000000
42− update dc
43− return (t, dc)
30+withDateCache action = initialize >>= action
31+
32+initialize :: IO DateCache
33+initialize = mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentGMTDate }
4434
4535-- | Getting 'GMTDate' based on 'DateCache'.
4636getDate :: DateCache -> IO GMTDate
47−getDate (DateCache ref) = readIORef ref
48−
49−update :: DateCache -> IO ()
50−update (DateCache ref) = getCurrentGMTDate >>= writeIORef ref
37+getDate = id
5138
5239getCurrentGMTDate :: IO GMTDate
5340#ifdef WINDOWS
warp/Network/Wai/Handler/Warp/FdCache.hs+26−28
@@ -23,14 +23,14 @@ module Network.Wai.Handler.Warp.FdCache (
2323 ) where
2424
2525import Control.Applicative ((<$>), (<*>))
26−import Control.Concurrent (threadDelay)
27−import Control.Exception (bracket, mask_)
26+import Control.Exception (bracket)
2827import Data.Hashable (hash)
2928import Network.Wai.Handler.Warp.IORef
3029import Network.Wai.Handler.Warp.MultiMap
31−import Network.Wai.Handler.Warp.Thread
3230import System.Posix.IO (openFd, OpenFileFlags(..), defaultFileFlags, OpenMode(ReadOnly), closeFd)
3331import System.Posix.Types (Fd)
32+import Control.Reaper
33+import Data.Maybe (fromMaybe)
3434
3535----------------------------------------------------------------
3636
@@ -68,19 +68,12 @@ type Hash = Int
6868type FdCache = MMap Hash FdEntry
6969
7070-- | Mutable Fd cacher.
71−newtype MutableFdCache = MutableFdCache (IORef FdCache)
71+data MutableFdCache = MutableFdCache
72+ ((Hash, FdEntry) -> IO ())
73+ (IORef (Maybe FdCache))
7274
7375fdCache :: MutableFdCache -> IO FdCache
74−fdCache (MutableFdCache ref) = readIORef ref
75−
76−swapWithNew :: IORef FdCache -> IO FdCache
77−swapWithNew ref = atomicModifyIORef' ref $ \t -> (empty, t)
78−
79−update :: MutableFdCache -> (FdCache -> FdCache) -> IO ()
80−update (MutableFdCache ref) = update' ref
81−
82−update' :: IORef FdCache -> (FdCache -> FdCache) -> IO ()
83−update' ref f = atomicModifyIORef' ref $ \t -> (f t, ())
76+fdCache (MutableFdCache _ ref) = fromMaybe empty <$> readIORef ref
8477
8578look :: MutableFdCache -> FilePath -> Hash -> IO (Maybe FdEntry)
8679look mfc path key = searchWith key check <$> fdCache mfc
@@ -106,17 +99,19 @@ withFdCache duration action = bracket (initialize duration)
10699-- The first argument is a cache duration in second.
107100initialize :: Int -> IO (Maybe MutableFdCache)
108101initialize 0 = return Nothing
109−initialize duration = do
110− ref' <- forkIOwithBreakableForever empty $ \ref -> do
111− threadDelay duration
112− clean ref
113− return (Just (MutableFdCache ref'))
114−
115−clean :: IORef FdCache -> IO ()
116−clean ref = do
117− old <- swapWithNew ref
102+initialize duration = fmap (Just . uncurry MutableFdCache)
103+ $ reaper defaultReaperSettings
104+ { reaperAction = clean
105+ , reaperDelay = duration
106+ , reaperCons = uncurry insert
107+ , reaperNull = isEmpty
108+ , reaperEmpty = empty
109+ }
110+
111+clean :: FdCache -> IO (FdCache -> FdCache)
112+clean old = do
118113 new <- pruneWith old prune
119− update' ref (merge new)
114+ return $ merge new
120115
121116prune :: t -> Some FdEntry -> IO [(t, Some FdEntry)]
122117prune k v@(One (FdEntry _ fd mst)) = status mst >>= prune'
@@ -138,8 +133,11 @@ prune k (Tom ent@(FdEntry _ fd mst) vs) = status mst >>= prune'
138133
139134terminate :: Maybe MutableFdCache -> IO ()
140135terminate Nothing = return ()
141−terminate (Just (MutableFdCache ref)) = mask_ $ do
142− !t <- breakForever ref
136+terminate (Just (MutableFdCache _ ref)) = do
137+ !t <- atomicModifyIORef ref $ \mm ->
138+ case mm of
139+ Nothing -> (Nothing, empty)
140+ Just m -> (Just empty, m)
143141 mapM_ closeIt $ toList t
144142 where
145143 closeIt (_, FdEntry _ fd _) = closeFd fd
@@ -148,12 +146,12 @@ terminate (Just (MutableFdCache ref)) = mask_ $ do
148146
149147-- | Getting 'Fd' and 'Refresh' from the mutable Fd cacher.
150148getFd :: MutableFdCache -> FilePath -> IO (Fd, Refresh)
151−getFd mfc path = look mfc path key >>= getFd'
149+getFd mfc@(MutableFdCache add _) path = look mfc path key >>= getFd'
152150 where
153151 key = hash path
154152 getFd' Nothing = do
155153 ent@(FdEntry _ fd mst) <- newFdEntry path
156− update mfc (insert key ent)
154+ add (key, ent)
157155 return (fd, refresh mst)
158156 getFd' (Just (FdEntry _ fd mst)) = do
159157 refresh mst
warp/Network/Wai/Handler/Warp/MultiMap.hs+1−1
@@ -72,7 +72,7 @@ searchWith xk f (Node _ l k v r) = case compare xk k of
7272----------------------------------------------------------------
7373
7474-- | O(1)
75−isEmpty :: (Eq k, Eq v) => MMap k v -> Bool
75+isEmpty :: MMap k v -> Bool
7676isEmpty Leaf = True
7777isEmpty _ = False
7878
warp/Network/Wai/Handler/Warp/Thread.hsdeleted−28
@@ -1,28 +0,0 @@
1−{-# LANGUAGE DeriveDataTypeable #-}
2−
3−module Network.Wai.Handler.Warp.Thread (
4− forkIOwithBreakableForever
5− , breakForever
6− ) where
7−
8−import Control.Concurrent (forkIO)
9−import Control.Exception (handle, throw, mask_, Exception)
10−import Control.Monad (void, forever)
11−import Data.IORef
12−import Data.Typeable
13−
14−data BreakForever = BreakForever deriving (Show, Typeable)
15−
16−instance Exception BreakForever
17−
18−forkIOwithBreakableForever :: a -> (IORef a -> IO ()) -> IO (IORef a)
19−forkIOwithBreakableForever ini action = do
20− ref <- newIORef ini
21− void . forkIO . handle stopPropagation . forever . mask_ $ action ref
22− return ref
23−
24−stopPropagation :: BreakForever -> IO ()
25−stopPropagation _ = return ()
26−
27−breakForever :: IORef a -> IO a
28−breakForever ref = atomicModifyIORef ref $ \x -> (throw BreakForever, x)
warp/Network/Wai/Handler/Warp/Timeout.hs+20−19
@@ -49,19 +49,19 @@ import GHC.Conc (ThreadId(..))
4949import GHC.Exts (mkWeak#)
5050import GHC.IO (IO (IO))
5151#endif
52−import Control.Concurrent (threadDelay, myThreadId)
52+import Control.Concurrent (myThreadId)
5353import qualified Control.Exception as E
5454import GHC.Weak (Weak (..))
5555import Network.Wai.Handler.Warp.IORef (IORef)
5656import qualified Network.Wai.Handler.Warp.IORef as I
57−import Network.Wai.Handler.Warp.Thread
5857import System.Mem.Weak (deRefWeak)
5958import Data.Typeable (Typeable)
59+import Control.Reaper
6060
6161----------------------------------------------------------------
6262
6363-- | A timeout manager
64−newtype Manager = Manager (IORef [Handle])
64+data Manager = Manager (Handle -> IO ()) (IORef (Maybe [Handle]))
6565
6666-- | An action to be performed on timeout.
6767type TimeoutAction = IO ()
@@ -79,23 +79,21 @@ data State = Active -- Manager turns it to Inactive.
7979-- | Creating timeout manager which works every N micro seconds
8080-- where N is the first argument.
8181initialize :: Int -> IO Manager
82−initialize timeout = do
83− ref' <- forkIOwithBreakableForever [] $ \ref -> do
84− threadDelay timeout
85− old <- I.atomicModifyIORef' ref (\x -> ([], x))
86− merge <- prune old id
87− I.atomicModifyIORef' ref (\new -> (merge new, ()))
88− return $ Manager ref'
82+initialize timeout =
83+ fmap (uncurry Manager) $ reaper defaultReaperSettings
84+ { reaperAction = mkListAction prune
85+ , reaperDelay = timeout
86+ }
8987 where
90− prune [] front = return front
91− prune (m@(Handle onTimeout iactive):rest) front = do
88+ prune m@(Handle onTimeout iactive) = do
9289 state <- I.atomicModifyIORef' iactive (\x -> (inactivate x, x))
9390 case state of
9491 Inactive -> do
9592 onTimeout `E.catch` ignoreAll
96− prune rest front
97− Canceled -> prune rest front
98− _ -> prune rest (front . (:) m)
93+ return Nothing
94+ Canceled -> return Nothing
95+ _ -> return $ Just m
96+
9997 inactivate Active = Inactive
10098 inactivate x = x
10199
@@ -103,8 +101,11 @@ initialize timeout = do
103101
104102-- | Stopping timeout manager.
105103stopManager :: Manager -> IO ()
106−stopManager (Manager ref) = E.mask_ $ do
107− !handles <- breakForever ref
104+stopManager (Manager _ ref) = E.mask_ $ do
105+ handles <- I.atomicModifyIORef ref $ \x ->
106+ case x of
107+ Nothing -> (Nothing, [])
108+ Just y -> (Just [], y)
108109 mapM_ fire handles
109110 where
110111 fire (Handle onTimeout _) = onTimeout `E.catch` ignoreAll
@@ -116,10 +117,10 @@ ignoreAll _ = return ()
116117
117118-- | Registering a timeout action.
118119register :: Manager -> TimeoutAction -> IO Handle
119−register (Manager ref) onTimeout = do
120+register (Manager add _) onTimeout = do
120121 iactive <- I.newIORef Active
121122 let h = Handle onTimeout iactive
122− I.atomicModifyIORef' ref (\x -> (h : x, ()))
123+ add h
123124 return h
124125
125126-- | Registering a timeout action of killing this thread.
warp/test/ThreadSpec.hsdeleted−26
@@ -1,26 +0,0 @@
1−{-# LANGUAGE BangPatterns #-}
2−
3−module ThreadSpec where
4−
5−import Control.Concurrent (threadDelay)
6−import Data.IORef (readIORef, writeIORef, atomicModifyIORef)
7−import Network.Wai.Handler.Warp.Thread
8−import Test.Hspec
9−
10−main :: IO ()
11−main = hspec spec
12−
13−spec :: Spec
14−spec = describe "forkIOwithBreakableForever" $ do
15− it "can be breakable" $ do
16− ref' <- forkIOwithBreakableForever True $ \ref -> do
17− threadDelay 1000
18− !_ <- atomicModifyIORef ref (\x -> (False, x))
19− return ()
20− threadDelay 100000
21− readIORef ref' `shouldReturn` False
22− _ <- breakForever ref'
23− threadDelay 100000
24− writeIORef ref' True
25− threadDelay 100000
26− readIORef ref' `shouldReturn` True
warp/warp.cabal+3−3
@@ -1,5 +1,5 @@
11Name: warp
2−Version: 3.0.0.5
2+Version: 3.0.0.6
33Synopsis: A fast, light-weight web server for WAI applications.
44License: MIT
55License-file: LICENSE
@@ -40,6 +40,7 @@ Flag warp-debug
4040Library
4141 Build-Depends: base >= 3 && < 5
4242 , array
43+ , auto-update >= 0.1.1 && < 0.2
4344 , blaze-builder >= 0.3.3 && < 0.4
4445 , bytestring >= 0.9.1.4
4546 , case-insensitive >= 0.2
@@ -75,7 +76,6 @@ Library
7576 Network.Wai.Handler.Warp.Run
7677 Network.Wai.Handler.Warp.SendFile
7778 Network.Wai.Handler.Warp.Settings
78− Network.Wai.Handler.Warp.Thread
7979 Network.Wai.Handler.Warp.Types
8080 Network.Wai.Handler.Warp.Windows
8181 Paths_warp
@@ -114,13 +114,13 @@ Test-Suite spec
114114 ResponseHeaderSpec
115115 ResponseSpec
116116 RunSpec
117− ThreadSpec
118117 Hs-Source-Dirs: test, .
119118 Type: exitcode-stdio-1.0
120119
121120 Ghc-Options: -Wall
122121 Build-Depends: base >= 4 && < 5
123122 , array
123+ , auto-update
124124 , blaze-builder >= 0.3.3 && < 0.4
125125 , bytestring >= 0.9.1.4
126126 , case-insensitive >= 0.2