Skip to content

Commit 3084c7f

Browse files
soulomoonwz1000jhrcekmichaelpj
authored
Stabilize the build system by correctly house keeping the dirtykeys and rule values [flaky test #4185 #4093] (#4190)
The main problem is the out of sync state in the build system. Several states involved, the shake database running result state for key. shake extra's dirty state for key and shake extra's rule values state. To stablize the build system. This PR force some of the updates of these state into a single STM block. 1. collect dirtykeys and ship it to session restart directly. No more invalid removal before session restart. Fixing Flaky test failure result in error of GetLinkable #4093 2. move the dirtykey removal and rules values updating to hls-graph by adding a call back fucntion to RunResult. Properly handle the dirtykeys and rule value state after session start and closely followed by another session restart Fixing ghcide-tests' addDependentFile test #4194 3. properly handle clean up by wrapping the asyncWithCleanUp to refreshDeps --------- Co-authored-by: wz1000 <[email protected]> Co-authored-by: Jan Hrcek <[email protected]> Co-authored-by: Michael Peyton Jones <[email protected]>
1 parent 3822586 commit 3084c7f

File tree

15 files changed

+176
-136
lines changed

15 files changed

+176
-136
lines changed

ghcide/session-loader/Development/IDE/Session.hs

+13-18
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ import qualified Data.HashSet as Set
106106
import Database.SQLite.Simple
107107
import Development.IDE.Core.Tracing (withTrace)
108108
import Development.IDE.Session.Diagnostics (renderCradleError)
109-
import Development.IDE.Types.Shake (WithHieDb)
109+
import Development.IDE.Types.Shake (WithHieDb, toNoFileKey)
110110
import HieDb.Create
111111
import HieDb.Types
112112
import HieDb.Utils
@@ -474,10 +474,9 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
474474
clientConfig <- getClientConfigAction
475475
extras@ShakeExtras{restartShakeSession, ideNc, knownTargetsVar, lspEnv
476476
} <- getShakeExtras
477-
let invalidateShakeCache :: IO ()
478-
invalidateShakeCache = do
477+
let invalidateShakeCache = do
479478
void $ modifyVar' version succ
480-
join $ atomically $ recordDirtyKeys extras GhcSessionIO [emptyFilePath]
479+
return $ toNoFileKey GhcSessionIO
481480

482481
IdeOptions{ optTesting = IdeTesting optTesting
483482
, optCheckProject = getCheckProject
@@ -510,16 +509,16 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
510509
TargetModule _ -> do
511510
found <- filterM (IO.doesFileExist . fromNormalizedFilePath) targetLocations
512511
return [(targetTarget, Set.fromList found)]
513-
hasUpdate <- join $ atomically $ do
512+
hasUpdate <- atomically $ do
514513
known <- readTVar knownTargetsVar
515514
let known' = flip mapHashed known $ \k ->
516515
HM.unionWith (<>) k $ HM.fromList knownTargets
517516
hasUpdate = if known /= known' then Just (unhashed known') else Nothing
518517
writeTVar knownTargetsVar known'
519-
logDirtyKeys <- recordDirtyKeys extras GetKnownTargets [emptyFilePath]
520-
return (logDirtyKeys >> pure hasUpdate)
518+
pure hasUpdate
521519
for_ hasUpdate $ \x ->
522520
logWith recorder Debug $ LogKnownFilesUpdated x
521+
return $ toNoFileKey GetKnownTargets
523522

524523
-- Create a new HscEnv from a hieYaml root and a set of options
525524
let packageSetup :: (Maybe FilePath, NormalizedFilePath, ComponentOptions, FilePath)
@@ -612,18 +611,14 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do
612611
, "If you are using a .cabal file, please ensure that this module is listed in either the exposed-modules or other-modules section"
613612
]
614613

615-
void $ modifyVar' fileToFlags $
616-
Map.insert hieYaml this_flags_map
617-
void $ modifyVar' filesMap $
618-
flip HM.union (HM.fromList (map ((,hieYaml) . fst) $ concatMap toFlagsMap all_targets))
619-
620-
void $ extendKnownTargets all_targets
621-
622-
-- Invalidate all the existing GhcSession build nodes by restarting the Shake session
623-
invalidateShakeCache
624-
614+
void $ modifyVar' fileToFlags $ Map.insert hieYaml this_flags_map
615+
void $ modifyVar' filesMap $ flip HM.union (HM.fromList (map ((,hieYaml) . fst) $ concatMap toFlagsMap all_targets))
625616
-- The VFS doesn't change on cradle edits, re-use the old one.
626-
restartShakeSession VFSUnmodified "new component" []
617+
-- Invalidate all the existing GhcSession build nodes by restarting the Shake session
618+
keys2 <- invalidateShakeCache
619+
restartShakeSession VFSUnmodified "new component" [] $ do
620+
keys1 <- extendKnownTargets all_targets
621+
return [keys1, keys2]
627622

628623
-- Typecheck all files in the project on startup
629624
checkProject <- getCheckProject

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

+8-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import qualified Development.IDE.Core.Shake as Shake
2828
import Development.IDE.Graph
2929
import Development.IDE.Types.Location
3030
import Development.IDE.Types.Options
31+
import Development.IDE.Types.Shake (toKey)
3132
import qualified Focus
3233
import Ide.Logger (Pretty (pretty),
3334
Recorder, WithPriority,
@@ -105,12 +106,12 @@ getFileExistsMapUntracked = do
105106
FileExistsMapVar v <- getIdeGlobalAction
106107
return v
107108

108-
-- | Modify the global store of file exists.
109-
modifyFileExists :: IdeState -> [(NormalizedFilePath, FileChangeType)] -> IO ()
109+
-- | Modify the global store of file exists and return the keys that need to be marked as dirty
110+
modifyFileExists :: IdeState -> [(NormalizedFilePath, FileChangeType)] -> IO [Key]
110111
modifyFileExists state changes = do
111112
FileExistsMapVar var <- getIdeGlobalState state
112113
-- Masked to ensure that the previous values are flushed together with the map update
113-
join $ mask_ $ atomicallyNamed "modifyFileExists" $ do
114+
mask_ $ atomicallyNamed "modifyFileExists" $ do
114115
forM_ changes $ \(f,c) ->
115116
case fromChange c of
116117
Just c' -> STM.focus (Focus.insert c') f var
@@ -119,10 +120,10 @@ modifyFileExists state changes = do
119120
-- flush previous values
120121
let (fileModifChanges, fileExistChanges) =
121122
partition ((== FileChangeType_Changed) . snd) changes
122-
mapM_ (deleteValue (shakeExtras state) GetFileExists . fst) fileExistChanges
123-
io1 <- recordDirtyKeys (shakeExtras state) GetFileExists $ map fst fileExistChanges
124-
io2 <- recordDirtyKeys (shakeExtras state) GetModificationTime $ map fst fileModifChanges
125-
return (io1 <> io2)
123+
keys0 <- concat <$> mapM (deleteValue (shakeExtras state) GetFileExists . fst) fileExistChanges
124+
let keys1 = map (toKey GetFileExists . fst) fileExistChanges
125+
let keys2 = map (toKey GetModificationTime . fst) fileModifChanges
126+
return (keys0 <> keys1 <> keys2)
126127

127128
fromChange :: FileChangeType -> Maybe Bool
128129
fromChange FileChangeType_Created = Just True

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

+19-19
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import Development.IDE.Import.DependencyInformation
4949
import Development.IDE.Types.Diagnostics
5050
import Development.IDE.Types.Location
5151
import Development.IDE.Types.Options
52+
import Development.IDE.Types.Shake (toKey)
5253
import HieDb.Create (deleteMissingRealFiles)
5354
import Ide.Logger (Pretty (pretty),
5455
Priority (Info),
@@ -148,24 +149,24 @@ isInterface :: NormalizedFilePath -> Bool
148149
isInterface f = takeExtension (fromNormalizedFilePath f) `elem` [".hi", ".hi-boot", ".hie", ".hie-boot", ".core"]
149150

150151
-- | Reset the GetModificationTime state of interface files
151-
resetInterfaceStore :: ShakeExtras -> NormalizedFilePath -> STM ()
152+
resetInterfaceStore :: ShakeExtras -> NormalizedFilePath -> STM [Key]
152153
resetInterfaceStore state f = do
153154
deleteValue state GetModificationTime f
154155

155156
-- | Reset the GetModificationTime state of watched files
156157
-- Assumes the list does not include any FOIs
157-
resetFileStore :: IdeState -> [(NormalizedFilePath, LSP.FileChangeType)] -> IO ()
158+
resetFileStore :: IdeState -> [(NormalizedFilePath, LSP.FileChangeType)] -> IO [Key]
158159
resetFileStore ideState changes = mask $ \_ -> do
159160
-- we record FOIs document versions in all the stored values
160161
-- so NEVER reset FOIs to avoid losing their versions
161162
-- FOI filtering is done by the caller (LSP Notification handler)
162-
forM_ changes $ \(nfp, c) -> do
163-
case c of
164-
LSP.FileChangeType_Changed
165-
-- already checked elsewhere | not $ HM.member nfp fois
166-
-> atomically $
167-
deleteValue (shakeExtras ideState) GetModificationTime nfp
168-
_ -> pure ()
163+
fmap concat <$>
164+
forM changes $ \(nfp, c) -> do
165+
case c of
166+
LSP.FileChangeType_Changed
167+
-- already checked elsewhere | not $ HM.member nfp fois
168+
-> atomically $ deleteValue (shakeExtras ideState) GetModificationTime nfp
169+
_ -> pure []
169170

170171

171172
modificationTime :: FileVersion -> Maybe UTCTime
@@ -215,16 +216,18 @@ setFileModified :: Recorder (WithPriority Log)
215216
-> IdeState
216217
-> Bool -- ^ Was the file saved?
217218
-> NormalizedFilePath
219+
-> IO [Key]
218220
-> IO ()
219-
setFileModified recorder vfs state saved nfp = do
221+
setFileModified recorder vfs state saved nfp actionBefore = do
220222
ideOptions <- getIdeOptionsIO $ shakeExtras state
221223
doCheckParents <- optCheckParents ideOptions
222224
let checkParents = case doCheckParents of
223225
AlwaysCheck -> True
224226
CheckOnSave -> saved
225227
_ -> False
226-
join $ atomically $ recordDirtyKeys (shakeExtras state) GetModificationTime [nfp]
227-
restartShakeSession (shakeExtras state) vfs (fromNormalizedFilePath nfp ++ " (modified)") []
228+
restartShakeSession (shakeExtras state) vfs (fromNormalizedFilePath nfp ++ " (modified)") [] $ do
229+
keys<-actionBefore
230+
return (toKey GetModificationTime nfp:keys)
228231
when checkParents $
229232
typecheckParents recorder state nfp
230233

@@ -244,14 +247,11 @@ typecheckParentsAction recorder nfp = do
244247
-- | Note that some keys have been modified and restart the session
245248
-- Only valid if the virtual file system was initialised by LSP, as that
246249
-- independently tracks which files are modified.
247-
setSomethingModified :: VFSModified -> IdeState -> [Key] -> String -> IO ()
248-
setSomethingModified vfs state keys reason = do
250+
setSomethingModified :: VFSModified -> IdeState -> String -> IO [Key] -> IO ()
251+
setSomethingModified vfs state reason actionBetweenSession = do
249252
-- Update database to remove any files that might have been renamed/deleted
250-
atomically $ do
251-
writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) (\withHieDb -> withHieDb deleteMissingRealFiles)
252-
modifyTVar' (dirtyKeys $ shakeExtras state) $ \x ->
253-
foldl' (flip insertKeySet) x keys
254-
void $ restartShakeSession (shakeExtras state) vfs reason []
253+
atomically $ writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) (\withHieDb -> withHieDb deleteMissingRealFiles)
254+
void $ restartShakeSession (shakeExtras state) vfs reason [] actionBetweenSession
255255

256256
registerFileWatches :: [String] -> LSP.LspT Config IO Bool
257257
registerFileWatches globs = do

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

+8-5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import Development.IDE.Plugin.Completions.Types
4040
import Development.IDE.Types.Exports
4141
import Development.IDE.Types.Location
4242
import Development.IDE.Types.Options (IdeTesting (..))
43+
import Development.IDE.Types.Shake (toKey)
4344
import GHC.TypeLits (KnownSymbol)
4445
import Ide.Logger (Pretty (pretty),
4546
Priority (..),
@@ -103,24 +104,26 @@ getFilesOfInterestUntracked = do
103104
OfInterestVar var <- getIdeGlobalAction
104105
liftIO $ readVar var
105106

106-
addFileOfInterest :: IdeState -> NormalizedFilePath -> FileOfInterestStatus -> IO ()
107+
addFileOfInterest :: IdeState -> NormalizedFilePath -> FileOfInterestStatus -> IO [Key]
107108
addFileOfInterest state f v = do
108109
OfInterestVar var <- getIdeGlobalState state
109110
(prev, files) <- modifyVar var $ \dict -> do
110111
let (prev, new) = HashMap.alterF (, Just v) f dict
111112
pure (new, (prev, new))
112-
when (prev /= Just v) $ do
113-
join $ atomically $ recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
113+
if prev /= Just v
114+
then do
114115
logWith (ideLogger state) Debug $
115116
LogSetFilesOfInterest (HashMap.toList files)
117+
return [toKey IsFileOfInterest f]
118+
else return []
116119

117-
deleteFileOfInterest :: IdeState -> NormalizedFilePath -> IO ()
120+
deleteFileOfInterest :: IdeState -> NormalizedFilePath -> IO [Key]
118121
deleteFileOfInterest state f = do
119122
OfInterestVar var <- getIdeGlobalState state
120123
files <- modifyVar' var $ HashMap.delete f
121-
join $ atomically $ recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
122124
logWith (ideLogger state) Debug $
123125
LogSetFilesOfInterest (HashMap.toList files)
126+
return [toKey IsFileOfInterest f]
124127
scheduleGarbageCollection :: IdeState -> IO ()
125128
scheduleGarbageCollection state = do
126129
GarbageCollectVar var <- getIdeGlobalState state

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

+45-21
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ module Development.IDE.Core.Shake(
5757
FileVersion(..),
5858
updatePositionMapping,
5959
updatePositionMappingHelper,
60-
deleteValue, recordDirtyKeys,
60+
deleteValue,
6161
WithProgressFunc, WithIndefiniteProgressFunc,
6262
ProgressEvent(..),
6363
DelayedAction, mkDelayedAction,
@@ -300,6 +300,7 @@ data ShakeExtras = ShakeExtras
300300
:: VFSModified
301301
-> String
302302
-> [DelayedAction ()]
303+
-> IO [Key]
303304
-> IO ()
304305
#if MIN_VERSION_ghc(9,3,0)
305306
,ideNc :: NameCache
@@ -557,26 +558,17 @@ setValues state key file val diags =
557558

558559

559560
-- | Delete the value stored for a given ide build key
561+
-- and return the key that was deleted.
560562
deleteValue
561563
:: Shake.ShakeValue k
562564
=> ShakeExtras
563565
-> k
564566
-> NormalizedFilePath
565-
-> STM ()
566-
deleteValue ShakeExtras{dirtyKeys, state} key file = do
567+
-> STM [Key]
568+
deleteValue ShakeExtras{state} key file = do
567569
STM.delete (toKey key file) state
568-
modifyTVar' dirtyKeys $ insertKeySet (toKey key file)
570+
return [toKey key file]
569571

570-
recordDirtyKeys
571-
:: Shake.ShakeValue k
572-
=> ShakeExtras
573-
-> k
574-
-> [NormalizedFilePath]
575-
-> STM (IO ())
576-
recordDirtyKeys ShakeExtras{dirtyKeys} key file = do
577-
modifyTVar' dirtyKeys $ \x -> foldl' (flip insertKeySet) x (toKey key <$> file)
578-
return $ withEventTrace "recordDirtyKeys" $ \addEvent -> do
579-
addEvent (fromString $ unlines $ "dirty " <> show key : map fromNormalizedFilePath file)
580572

581573
-- | We return Nothing if the rule has not run and Just Failed if it has failed to produce a value.
582574
getValues ::
@@ -759,12 +751,16 @@ delayedAction a = do
759751
-- | Restart the current 'ShakeSession' with the given system actions.
760752
-- Any actions running in the current session will be aborted,
761753
-- but actions added via 'shakeEnqueue' will be requeued.
762-
shakeRestart :: Recorder (WithPriority Log) -> IdeState -> VFSModified -> String -> [DelayedAction ()] -> IO ()
763-
shakeRestart recorder IdeState{..} vfs reason acts =
754+
shakeRestart :: Recorder (WithPriority Log) -> IdeState -> VFSModified -> String -> [DelayedAction ()] -> IO [Key] -> IO ()
755+
shakeRestart recorder IdeState{..} vfs reason acts ioActionBetweenShakeSession =
764756
withMVar'
765757
shakeSession
766758
(\runner -> do
767759
(stopTime,()) <- duration $ logErrorAfter 10 $ cancelShakeSession runner
760+
keys <- ioActionBetweenShakeSession
761+
-- it is every important to update the dirty keys after we enter the critical section
762+
-- see Note [Housekeeping rule cache and dirty key outside of hls-graph]
763+
atomically $ modifyTVar' (dirtyKeys shakeExtras) $ \x -> foldl' (flip insertKeySet) x keys
768764
res <- shakeDatabaseProfile shakeDb
769765
backlog <- readTVarIO $ dirtyKeys shakeExtras
770766
queue <- atomicallyNamed "actionQueue - peek" $ peekInProgress $ actionQueue shakeExtras
@@ -1198,7 +1194,7 @@ defineEarlyCutoff' doDiagnostics cmp key file mbOld mode action = do
11981194
Just (v@(Succeeded _ x), diags) -> do
11991195
ver <- estimateFileVersionUnsafely key (Just x) file
12001196
doDiagnostics (vfsVersion =<< ver) $ Vector.toList diags
1201-
return $ Just $ RunResult ChangedNothing old $ A v
1197+
return $ Just $ RunResult ChangedNothing old (A v) $ return ()
12021198
_ -> return Nothing
12031199
_ ->
12041200
-- assert that a "clean" rule is never a cache miss
@@ -1222,7 +1218,6 @@ defineEarlyCutoff' doDiagnostics cmp key file mbOld mode action = do
12221218
Nothing -> do
12231219
pure (toShakeValue ShakeStale mbBs, staleV)
12241220
Just v -> pure (maybe ShakeNoCutoff ShakeResult mbBs, Succeeded ver v)
1225-
liftIO $ atomicallyNamed "define - write" $ setValues state key file res (Vector.fromList diags)
12261221
doDiagnostics (vfsVersion =<< ver) diags
12271222
let eq = case (bs, fmap decodeShakeValue mbOld) of
12281223
(ShakeResult a, Just (ShakeResult b)) -> cmp a b
@@ -1232,9 +1227,12 @@ defineEarlyCutoff' doDiagnostics cmp key file mbOld mode action = do
12321227
_ -> False
12331228
return $ RunResult
12341229
(if eq then ChangedRecomputeSame else ChangedRecomputeDiff)
1235-
(encodeShakeValue bs) $
1236-
A res
1237-
liftIO $ atomicallyNamed "define - dirtyKeys" $ modifyTVar' dirtyKeys (deleteKeySet $ toKey key file)
1230+
(encodeShakeValue bs)
1231+
(A res) $ do
1232+
-- this hook needs to be run in the same transaction as the key is marked clean
1233+
-- see Note [Housekeeping rule cache and dirty key outside of hls-graph]
1234+
setValues state key file res (Vector.fromList diags)
1235+
modifyTVar' dirtyKeys (deleteKeySet $ toKey key file)
12381236
return res
12391237
where
12401238
-- Highly unsafe helper to compute the version of a file
@@ -1258,6 +1256,32 @@ defineEarlyCutoff' doDiagnostics cmp key file mbOld mode action = do
12581256
-- * creating bogus "file does not exists" diagnostics
12591257
| otherwise = useWithoutDependency (GetModificationTime_ False) fp
12601258

1259+
-- Note [Housekeeping rule cache and dirty key outside of hls-graph]
1260+
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1261+
-- Hls-graph contains its own internal running state for each key in the shakeDatabase.
1262+
-- ShakeExtras contains `state` field (rule result cache) and `dirtyKeys` (keys that became
1263+
-- dirty in between build sessions) that is not visible to the hls-graph
1264+
-- Essentially, we need to keep the rule cache and dirty key and hls-graph's internal state
1265+
-- in sync.
1266+
1267+
-- 1. A dirty key collected in a session should not be removed from dirty keys in the same session.
1268+
-- Since if we clean out the dirty key in the same session,
1269+
-- 1.1. we will lose the chance to dirty its reverse dependencies. Since it only happens during session restart.
1270+
-- 1.2. a key might be marked as dirty in ShakeExtras while it's being recomputed by hls-graph which could lead to it's premature removal from dirtyKeys.
1271+
-- See issue https://github.com/haskell/haskell-language-server/issues/4093 for more details.
1272+
1273+
-- 2. When a key is marked clean in the hls-graph's internal running
1274+
-- state, the rule cache and dirty keys are updated in the same transaction.
1275+
-- otherwise, some situations like the following can happen:
1276+
-- thread 1: hls-graph session run a key
1277+
-- thread 1: defineEarlyCutoff' run the action for the key
1278+
-- thread 1: the action is done, rule cache and dirty key are updated
1279+
-- thread 2: we restart the hls-graph session, thread 1 is killed, the
1280+
-- hls-graph's internal state is not updated.
1281+
-- This is problematic with early cut off because we are having a new rule cache matching the
1282+
-- old hls-graph's internal state, which might case it's reverse dependency to skip the recomputation.
1283+
-- See https://github.com/haskell/haskell-language-server/issues/4194 for more details.
1284+
12611285
traceA :: A v -> String
12621286
traceA (A Failed{}) = "Failed"
12631287
traceA (A Stale{}) = "Stale"

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ otTracedAction key file mode result act
112112
ExitCaseSuccess res -> do
113113
setTag sp "result" (pack $ result $ runValue res)
114114
setTag sp "changed" $ case res of
115-
RunResult x _ _ -> fromString $ show x
115+
RunResult x _ _ _ -> fromString $ show x
116116
endSpan sp)
117117
(\sp -> act (liftIO . setTag sp "diagnostics" . encodeUtf8 . showDiagnostics ))
118118
| otherwise = act (\_ -> return ())

0 commit comments

Comments
 (0)