Skip to content

Commit 027d588

Browse files
committed
Clarify hls-graph runtime restart keys
1 parent 6095bcd commit 027d588

5 files changed

Lines changed: 135 additions & 60 deletions

File tree

ghcide/src/Development/IDE/Core/Shake.hs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ import Development.IDE.GHC.Orphans ()
145145
import Development.IDE.Graph hiding (ShakeValue,
146146
action)
147147
import qualified Development.IDE.Graph as Shake
148-
import Development.IDE.Graph.Database (ShakeDatabase,
148+
import Development.IDE.Graph.Database (RuntimeRestartKeys (..),
149+
ShakeDatabase,
149150
instantiateDelayedAction,
150151
shakeComputeToPreserve,
151152
shakeGetActionQueueLength,
@@ -873,17 +874,16 @@ shakeRestart recorder IdeState{..} vfs reason acts ioActionBetweenShakeSession =
873874
prepareRuntimeRestart :: Bool -> [Key] -> IO (RuntimeKeysChanged, RuntimeRestartStats)
874875
prepareRuntimeRestart optRunSubset keys
875876
| optRunSubset = do
876-
(affected, changedKeys, _lookupCount, _) <-
877-
shakeComputeToPreserve shakeDb $ fromListKeySet keys
878-
logErrorAfter 10 $ shakeShutDatabase affected shakeDb
877+
runtimeRestartKeys <- shakeComputeToPreserve shakeDb $ fromListKeySet keys
878+
logErrorAfter 10 $ shakeShutDatabase (restartKillKeys runtimeRestartKeys) shakeDb
879879
surviving <- shakePeekAsyncsDelivers shakeDb
880880
queueCount <- shakeGetActionQueueLength shakeDb
881881
let preserved = fromListKeySet $ map GraphRuntime.deliverKey surviving
882882
pure
883-
( Just (changedKeys, preserved)
883+
( Just (runtimeRestartKeys, preserved)
884884
, RuntimeRestartStats
885885
{ runtimeDirtyCount = length keys
886-
, runtimeAffectedCount = lengthKeySet affected
886+
, runtimeAffectedCount = lengthKeySet (restartKillKeys runtimeRestartKeys)
887887
, runtimePreservedCount = lengthKeySet preserved
888888
, runtimeActionQueueCount = queueCount
889889
, runtimeSurvivingActions = map GraphRuntime.deliverName surviving
@@ -930,7 +930,7 @@ shakeEnqueue ShakeExtras{actionQueue, shakeRecorder} act = do
930930

931931
data VFSModified = VFSUnmodified | VFSModified !VFS
932932

933-
type RuntimeKeysChanged = Maybe (([Key], [Key]), KeySet)
933+
type RuntimeKeysChanged = Maybe (RuntimeRestartKeys, KeySet)
934934

935935
-- | Set up a new 'ShakeSession' with a set of initial actions
936936
-- Will crash if there is an existing 'ShakeSession' running.
@@ -961,8 +961,8 @@ newSession recorder ShakeExtras{..} vfsMod shakeDb acts reason runtimeKeysChange
961961
workRun restore = withSpan "Shake session" $ \otSpan -> do
962962
setTag otSpan "reason" (fromString reason)
963963
setTag otSpan "queue" (fromString $ unlines $ map actionName reenqueued)
964-
whenJust runtimeKeysChanged $ \((_, newKeys), _) ->
965-
setTag otSpan "keys" (BS8.pack $ unlines $ map show newKeys)
964+
whenJust runtimeKeysChanged $ \(runtimeRestartKeys, _) ->
965+
setTag otSpan "keys" (BS8.pack $ unlines $ map show $ restartDirtyKeys runtimeRestartKeys)
966966
res <- try @SomeException $
967967
restore startDatabase
968968
return $ do

hls-graph/src/Development/IDE/Graph/Database.hs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module Development.IDE.Graph.Database(
1616
,shakeGetBuildEdges,
1717
shakeShutDatabase,
1818
shakeGetActionQueueLength,
19+
RuntimeRestartKeys(..),
1920
shakeComputeToPreserve,
2021
-- shakedatabaseRuntimeDep,
2122
shakePeekAsyncsDelivers,
@@ -91,7 +92,7 @@ unvoid = fmap undefined
9192
-- seperate incrementing the step from running the build.
9293
-- Also immediately enqueues upsweep actions for the newly dirty keys.
9394
shakeRunDatabaseForKeysSep
94-
:: Maybe (([Key],[Key]),KeySet) -- ^ Set of keys changed since last run. 'Nothing' means everything has changed
95+
:: Maybe (RuntimeRestartKeys, KeySet) -- ^ Set of keys changed since last run. 'Nothing' means everything has changed
9596
-> ShakeDatabase
9697
-> [Action a]
9798
-> IO (IO [Either SomeException a])
@@ -126,7 +127,7 @@ mkDelayedAction s p a = do
126127
u <- newUnique
127128
return $ DelayedAction (newDirectKey $ hashUnique u) s (toEnum (fromEnum p)) a
128129

129-
shakeComputeToPreserve :: ShakeDatabase -> KeySet -> IO (KeySet, ([Key], [Key]), Int, [Key])
130+
shakeComputeToPreserve :: ShakeDatabase -> KeySet -> IO RuntimeRestartKeys
130131
shakeComputeToPreserve (ShakeDatabase _ _ db) ks = atomically (computeToPreserve db ks)
131132

132133
shakeRunDatabaseForKeys
@@ -146,7 +147,12 @@ shakeRunDatabaseForKeysWithExceptions
146147
-> IO [Either SomeException a]
147148
shakeRunDatabaseForKeysWithExceptions Nothing sdb as2 = join $ shakeRunDatabaseForKeysSep Nothing sdb as2
148149
shakeRunDatabaseForKeysWithExceptions (Just x) sdb as2 =
149-
let y = fromListKeySet x in join $ shakeRunDatabaseForKeysSep (Just (([], toListKeySet y), y)) sdb as2
150+
let y = fromListKeySet x
151+
restartKeys = RuntimeRestartKeys
152+
{ restartKillKeys = y
153+
, restartDirtyKeys = toListKeySet y
154+
}
155+
in join $ shakeRunDatabaseForKeysSep (Just (restartKeys, y)) sdb as2
150156

151157

152158
shakePeekAsyncsDelivers :: ShakeDatabase -> IO [DeliverStatus]

hls-graph/src/Development/IDE/Graph/Internal/Database.hs

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
{-# LANGUAGE RecordWildCards #-}
1010
{-# LANGUAGE TypeFamilies #-}
1111

12-
module Development.IDE.Graph.Internal.Database (compute, newDatabase, incDatabase, build, getDirtySet, getKeysAndVisitAge, AsyncParentKill(..), computeToPreserve, getRunTimeRDeps, spawnAsyncWithDbRegistration) where
12+
module Development.IDE.Graph.Internal.Database (compute, newDatabase, incDatabase, build, getDirtySet, getKeysAndVisitAge, AsyncParentKill(..), RuntimeRestartKeys(..), computeToPreserve, getRunTimeRDeps, spawnAsyncWithDbRegistration) where
1313

1414
import Prelude hiding (unzip)
1515

@@ -38,9 +38,8 @@ import qualified StmContainers.Map as SMap
3838
import System.Time.Extra (duration)
3939
import UnliftIO (MVar, atomically,
4040
newEmptyMVar, putMVar,
41-
takeMVar)
41+
readMVar)
4242

43-
import qualified Data.List as List
4443
import qualified UnliftIO.Exception as UE
4544

4645
#if MIN_VERSION_base(4,19,0)
@@ -65,12 +64,11 @@ newDatabase dataBaseLogger databaseActionQueue databaseExtra databaseRules = do
6564
-- | Increment the step and mark dirty.
6665
-- Assumes that the database is not running a build
6766
-- only some keys are dirty
68-
incDatabase :: Database -> Maybe (([Key], [Key]), KeySet) -> IO KeySet
69-
incDatabase db (Just ((_oldKeys, newKeys), preserves)) = do
67+
incDatabase :: Database -> Maybe (RuntimeRestartKeys, KeySet) -> IO KeySet
68+
incDatabase db (Just (RuntimeRestartKeys{..}, preserves)) = do
7069
atomicallyNamed "incDatabase" $ modifyTVar' (databaseStep db) $ \(Step i) -> Step $ i + 1
71-
forM_ newKeys $ \newKey -> atomically $ SMap.focus updateDirty newKey (databaseValues db)
72-
-- only upsweep the keys that are not preserved
73-
-- atomically $ writeUpsweepQueue (filter (`notMemberKeySet` preserves) oldkeys ++ newKeys) db
70+
forM_ restartDirtyKeys $ \newKey -> atomically $ SMap.focus updateDirty newKey (databaseValues db)
71+
-- Only re-enqueue actions that were not preserved across the restart.
7472
return $ preserves
7573

7674
-- all keys are dirty
@@ -82,10 +80,29 @@ incDatabase db Nothing = do
8280
SMap.focus updateDirty k (databaseValues db)
8381
return $ mempty
8482

85-
computeToPreserve :: Database -> KeySet -> STM (KeySet, ([Key], [Key]), Int, [Key])
86-
computeToPreserve db dirtySet = do
87-
(oldKeys, newKeys, affected) <- transitiveDirtyListBottomUpDiff db (toListKeySet dirtySet) []
88-
pure (affected, (oldKeys, newKeys), length newKeys, [])
83+
data RuntimeRestartKeys = RuntimeRestartKeys
84+
{ restartKillKeys :: !KeySet
85+
-- ^ Keys used to select running runtime actions to stop before the next
86+
-- session starts. This may include rule keys and delayed-action 'DirectKey's.
87+
, restartDirtyKeys :: ![Key]
88+
-- ^ Rule database keys to mark dirty before the next run. In the ghcide
89+
-- restart path this is rule-key-only by construction; the raw hls-graph API
90+
-- does not enforce that invariant by type.
91+
} deriving Show
92+
93+
-- Note [RuntimeRestartKeys]
94+
-- The restart plan intentionally keeps runtime cancellation separate from rule
95+
-- dirtiness. 'restartKillKeys' is consumed by shutdown and may include direct
96+
-- delayed-action keys. 'restartDirtyKeys' is consumed by the rule database and
97+
-- is expected to contain only rule keys that can be marked dirty.
98+
-- For the ghcide restart path, the initial dirty seeds come from rule keys
99+
-- ('toKey'/'toNoFileKey'), so 'restartDirtyKeys' can use the
100+
-- 'databaseRRuntimeDep' closure directly. Direct/root runtime edges are stored
101+
-- separately in 'databaseRRuntimeDepRoot' by 'insertdatabaseRuntimeDep' and are
102+
-- expanded only for 'restartKillKeys'. The raw hls-graph API does not enforce
103+
-- this seed invariant by type.
104+
computeToPreserve :: Database -> KeySet -> STM RuntimeRestartKeys
105+
computeToPreserve = transitiveDirtyKeysBottomUp
89106

90107
updateDirty :: Monad m => Focus.Focus KeyDetails m ()
91108
updateDirty = Focus.adjust $ \(KeyDetails status rdeps) ->
@@ -130,7 +147,7 @@ interpreBuildContinue :: Database -> Key -> (Key, BuildContinue) -> IO (Key, Res
130147
interpreBuildContinue _db _pk (_kid, BCStop k v) = return (k, v)
131148
interpreBuildContinue db _pk (kid, BCContinue Nothing) = builderOneFinal db emptyStack kid
132149
interpreBuildContinue _db _pk (_kid, BCContinue (Just barrier)) =
133-
takeMVar barrier >>= either throwIO pure
150+
readMVar barrier >>= either throwIO pure
134151

135152

136153
builderOne :: Key -> Database -> Stack -> Key -> IO (Key, BuildContinue)
@@ -324,32 +341,22 @@ getRunTimeRDeps db k = SMap.lookup k (databaseRRuntimeDep db)
324341
getDeps :: SMap.Map Key KeySet -> Key -> STM (Maybe KeySet)
325342
getDeps m k = SMap.lookup k m
326343

327-
-- Edges in the reverse-dependency graph go from a child to its parents.
328-
-- We perform a DFS and, after exploring all outgoing edges, cons the node onto
329-
-- the accumulator. This yields children-before-parents order directly.
330-
331-
-- the lefts are keys that are no longer affected, we can try to mark them clean
332-
-- the rights are new affected keys, we need to mark them dirty
333-
transitiveDirtyListBottomUpDiff :: Database -> [Key] -> [Key] -> STM ([Key], [Key], KeySet)
334-
transitiveDirtyListBottomUpDiff database seeds allOldKeys = do
335-
(newKeys, seen) <- cacheTransitiveDirtyListBottomUpDFSWithRootKey database $ fromListKeySet seeds
336-
let oldKeys = filter (`notMemberKeySet` seen) allOldKeys
337-
return (oldKeys, newKeys, seen)
338-
339-
cacheTransitiveDirtyListBottomUpDFSWithRootKey :: Database -> KeySet -> STM ([Key], KeySet)
340-
cacheTransitiveDirtyListBottomUpDFSWithRootKey db@Database{..} seeds = do
341-
(newKeys, seen) <- cacheTransitiveDirtyListBottomUpDFS db seeds
342-
-- we should put pump root keys back to seen
343-
-- for each new key, get its root keys and put them back to seen
344-
-- newKeys is for upsweep, databaseRRuntimeDepRoot only add new root keys which is not needed for upsweep
345-
-- but seen is for thread filtering, we need to make sure all root keys are in seen
346-
(_newKeys, newSeen) <- transitiveDirtyListBottomUpDFS databaseRRuntimeDepRoot seen
344+
transitiveDirtyKeysBottomUp :: Database -> KeySet -> STM RuntimeRestartKeys
345+
transitiveDirtyKeysBottomUp db@Database{..} seeds = do
346+
TransitiveDirtyKeys dirtyKeys seen <- cacheTransitiveDirtyListBottomUpDFS db seeds
347+
-- restartDirtyKeys should contain only rule keys. restartKillKeys also needs
348+
-- the root/direct delayed-action keys, so expand through the root dependency
349+
-- map only for the kill set.
350+
TransitiveDirtyKeys _newKeys newSeen <- transitiveDirtyListBottomUpDFS databaseRRuntimeDepRoot seen
347351
let rootKey = newKey "root"
348-
return $ (List.delete rootKey newKeys, deleteKeySet rootKey newSeen)
352+
pure RuntimeRestartKeys
353+
{ restartDirtyKeys = dirtyKeys
354+
, restartKillKeys = deleteKeySet rootKey newSeen
355+
}
349356

350357

351358

352-
cacheTransitiveDirtyListBottomUpDFS :: Database -> KeySet -> STM ([Key], KeySet)
359+
cacheTransitiveDirtyListBottomUpDFS :: Database -> KeySet -> STM TransitiveDirtyKeys
353360
cacheTransitiveDirtyListBottomUpDFS Database{..} seeds = do
354361
SMap.lookup seeds databaseTransitiveRRuntimeDepCache >>= \case
355362
Just v -> return v
@@ -358,22 +365,25 @@ cacheTransitiveDirtyListBottomUpDFS Database{..} seeds = do
358365
SMap.insert r seeds databaseTransitiveRRuntimeDepCache
359366
return r
360367

361-
transitiveDirtyListBottomUpDFS :: SMap.Map Key KeySet -> KeySet -> STM ([Key], KeySet)
368+
-- Edges in the reverse-dependency graph go from a child to its parents.
369+
-- We perform a DFS and, after exploring all outgoing edges, cons the node onto
370+
-- the accumulator. This yields children-before-parents order directly.
371+
transitiveDirtyListBottomUpDFS :: SMap.Map Key KeySet -> KeySet -> STM TransitiveDirtyKeys
362372
transitiveDirtyListBottomUpDFS database seeds = do
363-
let go1 :: Key -> ([Key], KeySet) -> STM ([Key], KeySet)
364-
go1 x acc@(dirties, seen) = do
373+
let go1 :: Key -> TransitiveDirtyKeys -> STM TransitiveDirtyKeys
374+
go1 x acc@TransitiveDirtyKeys{transitiveDirtySet = seen} = do
365375
if x `memberKeySet` seen
366376
then pure acc
367377
else do
368-
let newAcc = (dirties, insertKeySet x seen)
378+
let newAcc = acc{transitiveDirtySet = insertKeySet x seen}
369379
mnext <- getDeps database x
370-
(newDirties, newSeen) <- foldrM go1 newAcc (maybe mempty toListKeySet mnext)
371-
return (x:newDirties, newSeen)
372-
-- if it is root key, we do not add it to the dirty list
373-
-- since root key is not up for upsweep
374-
-- but it would be in the seen list, so we would kill dirty root key async
380+
childClosure <- foldrM go1 newAcc (maybe mempty toListKeySet mnext)
381+
return childClosure{transitiveDirtyList = x : transitiveDirtyList childClosure}
382+
-- Root keys are filtered out by 'transitiveDirtyKeysBottomUp'
383+
-- for the dirty list, but kept in the set long enough to find
384+
-- runtime roots that need shutdown.
375385
-- traverse all seeds
376-
foldrM go1 ([], mempty) (toListKeySet seeds)
386+
foldrM go1 (TransitiveDirtyKeys [] mempty) (toListKeySet seeds)
377387

378388
-- | Original spawnRefresh using the general pattern
379389
-- inline

hls-graph/src/Development/IDE/Graph/Internal/Types.hs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ data Database = Database {
283283
-- if not in any of the transitive reverse deps of a dirty node, it is clean
284284
-- we can skip clean the threads.
285285
-- this is update right before we query the database for the key result.
286-
databaseTransitiveRRuntimeDepCache :: SMap.Map KeySet ([Key], KeySet),
286+
databaseTransitiveRRuntimeDepCache :: SMap.Map KeySet TransitiveDirtyKeys,
287287
-- ^ this is a cache for transitive reverse deps if we have computed it before
288288
-- and the databaseRRuntimeDep did not change since last time
289289
-- it is very useful for large projects where many files depend on a few common files
@@ -307,6 +307,13 @@ data Database = Database {
307307

308308
}
309309

310+
data TransitiveDirtyKeys = TransitiveDirtyKeys
311+
{ transitiveDirtyList :: ![Key]
312+
-- ^ Dirty keys in children-before-parents order.
313+
, transitiveDirtySet :: !KeySet
314+
-- ^ Same transitive closure as a set, used for membership/filtering.
315+
} deriving Show
316+
310317

311318
---------------------------------------------------------------------
312319
-- | Remove finished asyncs from 'databaseThreads' (non-blocking).

hls-graph/test/ActionSpec.hs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,30 @@ module ActionSpec where
66
import Control.Concurrent (MVar, readMVar)
77
import qualified Control.Concurrent as C
88
import Control.Concurrent.STM
9+
import Control.Exception (SomeException)
10+
import Control.Monad (void)
911
import Control.Monad.IO.Class (MonadIO (..))
1012
import Data.Typeable (Typeable)
1113
import Development.IDE.Graph (RuleResult,
1214
shakeOptions)
1315
import Development.IDE.Graph.Classes (Hashable)
14-
import Development.IDE.Graph.Database (shakeNewDatabase,
16+
import Development.IDE.Graph.Database (RuntimeRestartKeys (..),
17+
mkDelayedAction,
18+
shakeComputeToPreserve,
19+
shakeNewDatabase,
1520
shakeRunDatabase,
16-
shakeRunDatabaseForKeys)
21+
shakeRunDatabaseForKeys,
22+
shakeShutDatabase)
23+
import Development.IDE.Graph.Internal.Action (actionCatch,
24+
actionFinally,
25+
pumpActionThreadReRun)
1726
import Development.IDE.Graph.Internal.Database (build, incDatabase)
1827
import Development.IDE.Graph.Internal.Key
1928
import Development.IDE.Graph.Internal.Types
2029
import Development.IDE.Graph.Rule
2130
import Example
2231
import qualified StmContainers.Map as STM
32+
import System.Timeout (timeout)
2333
import Test.Hspec
2434

2535

@@ -32,6 +42,13 @@ itInThread = it
3242

3343
shakeRunDatabaseFromRight :: ShakeDatabase -> [Action a] -> IO [a]
3444
shakeRunDatabaseFromRight = shakeRunDatabase
45+
46+
waitForRuntimeRootDep :: Database -> Key -> Key -> IO ()
47+
waitForRuntimeRootDep Database{..} child parent =
48+
atomically $ do
49+
deps <- STM.lookup child databaseRRuntimeDepRoot
50+
check $ maybe False (memberKeySet parent) deps
51+
3552
spec :: Spec
3653
spec = do
3754
describe "apply1" $ itInThread "Test build update, Buggy dirty mechanism in hls-graph #4237" $ do
@@ -105,6 +122,41 @@ spec = do
105122
db <- shakeNewDatabase shakeOptions $ addRule $ \(Rule :: Rule ()) _old _mode -> error "boom"
106123
let res = shakeRunDatabaseFromRight db $ pure $ apply1 (Rule @())
107124
res `shouldThrow` anyErrorCall
125+
itInThread "restart kills a delayed action parked behind a caught producer failure" $ do
126+
producerStarted <- C.newEmptyMVar
127+
releaseProducer <- C.newEmptyMVar
128+
producerCaught <- C.newEmptyMVar
129+
waiterFinalized <- C.newEmptyMVar
130+
sdb@(ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $
131+
addRule $ \(Rule :: Rule Int) _old _mode -> do
132+
liftIO $ void $ C.tryPutMVar producerStarted ()
133+
liftIO $ readMVar releaseProducer
134+
error "boom"
135+
producer <- mkDelayedAction "producer" Debug $
136+
actionCatch @SomeException
137+
(void $ apply1 (Rule @Int))
138+
(\_ -> liftIO $ void $ C.tryPutMVar producerCaught ())
139+
waiter <- mkDelayedAction "waiter" Debug $
140+
actionFinally
141+
(do
142+
liftIO $ readMVar producerStarted
143+
void $ apply1 (Rule @Int))
144+
(void $ C.tryPutMVar waiterFinalized ())
145+
146+
_ <- shakeRunDatabaseForKeys Nothing sdb
147+
[ pumpActionThreadReRun sdb producer
148+
, pumpActionThreadReRun sdb waiter
149+
]
150+
let dirtyKey = newKey (Rule @Int)
151+
waitForRuntimeRootDep theDb dirtyKey (uniqueID waiter)
152+
C.putMVar releaseProducer ()
153+
readMVar producerCaught
154+
C.tryReadMVar waiterFinalized >>= (`shouldBe` Nothing)
155+
156+
runtimeRestartKeys <- shakeComputeToPreserve sdb (singletonKeySet dirtyKey)
157+
uniqueID waiter `memberKeySet` restartKillKeys runtimeRestartKeys `shouldBe` True
158+
shakeShutDatabase (restartKillKeys runtimeRestartKeys) sdb
159+
timeout 1000000 (readMVar waiterFinalized) >>= (`shouldBe` Just ())
108160
itInThread "computes a rule with branching dependencies does not invoke phantom dependencies #3423" $ do
109161
cond <- C.newMVar True
110162
count <- C.newMVar 0

0 commit comments

Comments
 (0)