File: Resource.hs

package info (click to toggle)
haskell-shake 0.13.2%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 888 kB
  • ctags: 127
  • sloc: haskell: 6,388; makefile: 35; ansic: 25; sh: 2
file content (170 lines) | stat: -rw-r--r-- 8,311 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
{-# LANGUAGE RecordWildCards #-}

module Development.Shake.Resource(
    Resource, newResourceIO, newThrottleIO, acquireResource, releaseResource
    ) where

import Development.Shake.Errors
import General.Base
import Data.Function
import System.IO.Unsafe
import Control.Arrow
import Control.Monad


{-# NOINLINE resourceIds #-}
resourceIds :: Var Int
resourceIds = unsafePerformIO $ newVar 0

resourceId :: IO Int
resourceId = modifyVar resourceIds $ \i -> let j = i + 1 in j `seq` return (j, j)


-- | A type representing an external resource which the build system should respect. There
--   are two ways to create 'Resource's in Shake:
--
-- * 'Development.Shake.newResource' creates a finite resource, stopping too many actions running
--   simultaneously.
--
-- * 'Development.Shake.newThrottle' creates a throttled resource, stopping too many actions running
--   over a short time period.
--
--   These resources are used with 'Development.Shake.withResource' when defining rules. Typically only
--   system commands (such as 'Development.Shake.cmd') should be run inside 'Development.Shake.withResource',
--   not commands such as 'Development.Shake.need'.
--
--   Be careful that the actions run within 'Development.Shake.withResource' do not themselves require further
--   resources, or you may get a \"thread blocked indefinitely in an MVar operation\" exception.
--   If an action requires multiple resources, use 'Development.Shake.withResources' to avoid deadlock.
data Resource = Resource
    {resourceOrd :: Int
        -- ^ Key used for Eq/Ord operations. To make withResources work, we require newResourceIO < newThrottleIO
    ,resourceShow :: String
        -- ^ String used for Show
    ,acquireResource :: Int -> IO (Maybe (IO ()))
        -- ^ Try to acquire a resource. Returns Nothing to indicate you have acquired with no blocking, or Just act to
        --   say after act completes (which will block) then you will have the resource.
    ,releaseResource :: Int -> IO ()
        -- ^ You should only ever releaseResource that you obtained with acquireResource.
    }

instance Show Resource where show = resourceShow
instance Eq Resource where (==) = (==) `on` resourceOrd
instance Ord Resource where compare = compare `on` resourceOrd


---------------------------------------------------------------------
-- FINITE RESOURCES

-- | (number available, queue of people with how much they want and a barrier to signal when it is allocated to them)
type Finite = Var (Int, [(Int,Barrier ())])

-- | A version of 'Development.Shake.newResource' that runs in IO, and can be called before calling 'Development.Shake.shake'.
--   Most people should use 'Development.Shake.newResource' instead.
newResourceIO :: String -> Int -> IO Resource
newResourceIO name mx = do
    when (mx < 0) $
        error $ "You cannot create a resource named " ++ name ++ " with a negative quantity, you used " ++ show mx
    key <- resourceId
    var <- newVar (mx, [])
    return $ Resource (negate key) shw (acquire var) (release var)
    where
        shw = "Resource " ++ name

        acquire :: Finite -> Int -> IO (Maybe (IO ()))
        acquire var want
            | want < 0 = error $ "You cannot acquire a negative quantity of " ++ shw ++ ", requested " ++ show want
            | want > mx = error $ "You cannot acquire more than " ++ show mx ++ " of " ++ shw ++ ", requested " ++ show want
            | otherwise = modifyVar var $ \(available,waiting) ->
                if want <= available then
                    return ((available - want, waiting), Nothing)
                else do
                    bar <- newBarrier
                    return ((available, waiting ++ [(want,bar)]), Just $ waitBarrier bar)

        release :: Finite -> Int -> IO ()
        release var i = modifyVar_ var $ \(available,waiting) -> f (available+i) waiting
            where
                f i ((wi,wa):ws) | wi <= i = signalBarrier wa () >> f (i-wi) ws
                                 | otherwise = do (i,ws) <- f i ws; return (i,(wi,wa):ws)
                f i [] = return (i, [])


---------------------------------------------------------------------
-- THROTTLE RESOURCES

data Throttle = Throttle
    {throttleLock :: Lock
        -- people queue up to grab from replenish, full means no one is queued
    ,throttleVal :: Var (Either (Barrier ()) [(Time, Int)])
        -- either someone waiting for resources, or the time to wait until before N resources become available
        -- anyone who puts a Barrier in the Left must be holding the Lock
    ,throttleTime :: IO Time
    }

-- | A version of 'Development.Shake.newThrottle' that runs in IO, and can be called before calling 'Development.Shake.shake'.
--   Most people should use 'Development.Shake.newResource' instead.
newThrottleIO :: String -> Int -> Double -> IO Resource
newThrottleIO name count period_ = do
    when (count < 0) $
        error $ "You cannot create a throttle named " ++ name ++ " with a negative quantity, you used " ++ show count
    key <- resourceId
    lock <- newLock
    time <- offsetTime
    rep <- newVar $ Right [(0, count)]
    let s = Throttle lock rep time
    return $ Resource key shw (acquire s) (release s)
    where
        period = fromRational $ toRational period_
        shw = "Throttle " ++ name

        release :: Throttle -> Int -> IO ()
        release Throttle{..} n = do
            t <- throttleTime
            modifyVar_ throttleVal $ \v -> case v of
                Left b -> signalBarrier b () >> return (Right [(t+period, n)])
                Right ts -> return $ Right $ ts ++ [(t+period, n)]

        acquire :: Throttle -> Int -> IO (Maybe (IO ()))
        acquire Throttle{..} want
            | want < 0 = error $ "You cannot acquire a negative quantity of " ++ shw ++ ", requested " ++ show want
            | want > count = error $ "You cannot acquire more than " ++ show count ++ " of " ++ shw ++ ", requested " ++ show want
            | otherwise = do
                let grab t vs = do
                        let (a,b) = span ((<= t) . fst) vs
                        -- renormalise for clock skew, nothing can ever be > t+period away
                        return (sum $ map snd a, map (first $ min $ t+period) b)
                let push i vs = [(0,i) | i > 0] ++ vs

                -- attempt to grab without locking
                res <- withLockTry throttleLock $ do
                    modifyVar throttleVal $ \v -> case v of
                        Right vs -> do
                            t <- throttleTime
                            (got,vs) <- grab t vs
                            if got >= want then
                                return (Right $ push (got - want) vs, True)
                             else
                                return (Right $ push got vs, False)
                        _ -> return (v, False)
                if res == Just True then
                    return Nothing
                 else
                    return $ Just $ withLock throttleLock $ do
                        -- keep trying to acquire more resources until you have everything you need
                        let f want = join $ modifyVar throttleVal $ \v -> case v of
                                Left _ -> err "newThrottle, invariant failed, Left while holding throttleLock"
                                Right vs -> do
                                    t <- throttleTime
                                    (got,vs) <- grab t vs
                                    case vs of
                                        _ | got >= want -> return (Right $ push (got - want) vs, return ())
                                        [] -> do
                                            b <- newBarrier
                                            return (Left b, waitBarrier b >> f (want - got))
                                        (t2,n):vs -> do
                                            -- be robust to clock skew - only ever sleep for 'period' at most and always mark the next as good.
                                            return $ (,) (Right $ (0,n):vs) $ do
                                                sleep $ min period (t2-t)
                                                f $ want - got
                        f want