forked from commercialhaskell/stack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathExecute.hs
More file actions
154 lines (143 loc) · 5.82 KB
/
Execute.hs
File metadata and controls
154 lines (143 loc) · 5.82 KB
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
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE RecordWildCards #-}
-- Concurrent execution with dependencies. Types currently hard-coded for needs
-- of stack, but could be generalized easily.
module Control.Concurrent.Execute
( ActionType (..)
, ActionId (..)
, ActionContext (..)
, Action (..)
, Concurrency(..)
, runActions
) where
import Control.Concurrent.STM (retry)
import Stack.Prelude
import Data.List (sortBy)
import qualified Data.Set as Set
data ActionType
= ATBuild
-- ^ Action for building a package's library and executables. If
-- 'taskAllInOne' is 'True', then this will also build benchmarks
-- and tests. It is 'False' when then library's benchmarks or
-- test-suites have cyclic dependencies.
| ATBuildFinal
-- ^ Task for building the package's benchmarks and test-suites.
-- Requires that the library was already built.
| ATRunTests
-- ^ Task for running the package's test-suites.
| ATRunBenchmarks
-- ^ Task for running the package's benchmarks.
deriving (Show, Eq, Ord)
data ActionId = ActionId !PackageIdentifier !ActionType
deriving (Show, Eq, Ord)
data Action = Action
{ actionId :: !ActionId
, actionDeps :: !(Set ActionId)
, actionDo :: !(ActionContext -> IO ())
, actionConcurrency :: !Concurrency
}
data Concurrency = ConcurrencyAllowed | ConcurrencyDisallowed
deriving (Eq)
data ActionContext = ActionContext
{ acRemaining :: !(Set ActionId)
-- ^ Does not include the current action
, acDownstream :: [Action]
-- ^ Actions which depend on the current action
, acConcurrency :: !Concurrency
-- ^ Whether this action may be run concurrently with others
}
data ExecuteState = ExecuteState
{ esActions :: TVar [Action]
, esExceptions :: TVar [SomeException]
, esInAction :: TVar (Set ActionId)
, esCompleted :: TVar Int
, esKeepGoing :: Bool
}
data ExecuteException
= InconsistentDependencies
deriving Typeable
instance Exception ExecuteException
instance Show ExecuteException where
show InconsistentDependencies =
"Inconsistent dependencies were discovered while executing your build plan. This should never happen, please report it as a bug to the stack team."
runActions :: Int -- ^ threads
-> Bool -- ^ keep going after one task has failed
-> [Action]
-> (TVar Int -> TVar (Set ActionId) -> IO ()) -- ^ progress updated
-> IO [SomeException]
runActions threads keepGoing actions0 withProgress = do
es <- ExecuteState
<$> newTVarIO (sortActions actions0)
<*> newTVarIO []
<*> newTVarIO Set.empty
<*> newTVarIO 0
<*> pure keepGoing
_ <- async $ withProgress (esCompleted es) (esInAction es)
if threads <= 1
then runActions' es
else replicateConcurrently_ threads $ runActions' es
readTVarIO $ esExceptions es
-- | Sort actions such that those that can't be run concurrently are at
-- the end.
sortActions :: [Action] -> [Action]
sortActions = sortBy (compareConcurrency `on` actionConcurrency)
where
-- NOTE: Could derive Ord. However, I like to make this explicit so
-- that changes to the datatype must consider how it's affecting
-- this.
compareConcurrency ConcurrencyAllowed ConcurrencyDisallowed = LT
compareConcurrency ConcurrencyDisallowed ConcurrencyAllowed = GT
compareConcurrency _ _ = EQ
runActions' :: ExecuteState -> IO ()
runActions' ExecuteState {..} =
loop
where
breakOnErrs inner = do
errs <- readTVar esExceptions
if null errs || esKeepGoing
then inner
else return $ return ()
withActions inner = do
as <- readTVar esActions
if null as
then return $ return ()
else inner as
loop = join $ atomically $ breakOnErrs $ withActions $ \as ->
case break (Set.null . actionDeps) as of
(_, []) -> do
inAction <- readTVar esInAction
if Set.null inAction
then do
unless esKeepGoing $
modifyTVar esExceptions (toException InconsistentDependencies:)
return $ return ()
else retry
(xs, action:ys) -> do
inAction <- readTVar esInAction
case actionConcurrency action of
ConcurrencyAllowed -> return ()
ConcurrencyDisallowed -> unless (Set.null inAction) retry
let as' = xs ++ ys
remaining = Set.union
(Set.fromList $ map actionId as')
inAction
writeTVar esActions as'
modifyTVar esInAction (Set.insert $ actionId action)
return $ mask $ \restore -> do
eres <- try $ restore $ actionDo action ActionContext
{ acRemaining = remaining
, acDownstream = downstreamActions (actionId action) as'
, acConcurrency = actionConcurrency action
}
atomically $ do
modifyTVar esInAction (Set.delete $ actionId action)
modifyTVar esCompleted (+1)
case eres of
Left err -> modifyTVar esExceptions (err:)
Right () ->
let dropDep a = a { actionDeps = Set.delete (actionId action) $ actionDeps a }
in modifyTVar esActions $ map dropDep
restore loop
downstreamActions :: ActionId -> [Action] -> [Action]
downstreamActions aid = filter (\a -> aid `Set.member` actionDeps a)