Skip to content

Commit 7e4c4c7

Browse files
committed
Move worker restart ref into WorkerThread
1 parent 5978cad commit 7e4c4c7

7 files changed

Lines changed: 183 additions & 146 deletions

File tree

ghcide-test/exe/ShakeRestartTests.hs

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
module ShakeRestartTests (tests) where
55

66
import Control.Concurrent.STM
7-
import Data.IORef
8-
import Data.IORef.Extra (atomicModifyIORef'_)
97
import Development.IDE.Core.Shake
108
import Development.IDE.Graph (newKey)
119
import Language.LSP.VFS
@@ -20,37 +18,18 @@ tests = testGroup "shake restart merging"
2018
newestVFSModified vfs1 VFSUnmodified @?= vfs1
2119
newestVFSModified VFSUnmodified vfs1 @?= vfs1
2220

23-
, testCase "mergePendingRestart Nothing" $ do
24-
let p = PendingRestart VFSUnmodified [] ["reason"] [] []
25-
if mergePendingRestart p Nothing == p
26-
then pure ()
27-
else assertFailure "merging with nothing should get new"
28-
29-
, testCase "mergePendingRestart Just" $ do
21+
, testCase "<>" $ do
3022
done1 <- newEmptyTMVarIO
3123
done2 <- newEmptyTMVarIO
3224
let key1 = newKey ("1" :: String)
3325
key2 = newKey ("2" :: String)
3426
p1 = PendingRestart VFSUnmodified [pure [key1]] ["r1"] [] [done1]
3527
p2 = PendingRestart VFSUnmodified [pure [key2]] ["r2"] [] [done2]
36-
merged = mergePendingRestart p1 (Just p2)
28+
merged = p1 <> p2
3729

3830
pendingRestartReasons merged @?= ["r1", "r2"]
3931
keys <- sequence $ reverse $ pendingRestartActionBetweenSessions merged
4032
concat keys @?= [key2, key1]
41-
42-
, testCase "RestartSlot coalescing" $ do
43-
slot <- newRestartSlot
44-
let p1 = PendingRestart VFSUnmodified [] ["r1"] [] []
45-
p2 = PendingRestart VFSUnmodified [] ["r2"] [] []
46-
47-
atomicModifyIORef'_ (queuedRestart slot) $ Just . mergePendingRestart p1
48-
atomicModifyIORef'_ (queuedRestart slot) $ Just . mergePendingRestart p2
49-
50-
res <- atomicModifyIORef' (queuedRestart slot) (Nothing,)
51-
case res of
52-
Nothing -> assertFailure "Should have a pending restart"
53-
Just p -> pendingRestartReasons p @?= ["r2", "r1"]
5433
]
5534

5635
instance Eq VFSModified where

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import Data.HashMap.Strict (HashMap)
9090
import Data.HashSet (HashSet)
9191
import qualified Data.HashSet as Set
9292
import Database.SQLite.Simple
93+
import qualified Development.IDE.Core.Shake as Shake
9394
import Development.IDE.Core.Tracing (withTrace)
9495
import Development.IDE.Core.WorkerThread
9596
import Development.IDE.Session.Dependency
@@ -136,6 +137,7 @@ data Log
136137
| LogLookupSessionCache !FilePath
137138
| LogTime !String
138139
| LogSessionGhc Ghc.Log
140+
| LogShake Shake.Log
139141
deriving instance Show Log
140142

141143
instance Pretty Log where
@@ -209,6 +211,7 @@ instance Pretty Log where
209211
LogSessionGhc msg -> pretty msg
210212
LogSessionLoadingChanged ->
211213
"Session Loading config changed, reloading the full session."
214+
LogShake msg -> pretty msg
212215

213216
-- | Bump this version number when making changes to the format of the data stored in hiedb
214217
hiedbDataVersion :: String
@@ -633,7 +636,7 @@ newSessionState = do
633636
-- components mapping to the same hie.yaml file are mapped to the same
634637
-- HscEnv which is updated as new components are discovered.
635638

636-
loadSessionWithOptions :: Recorder (WithPriority Log) -> SessionLoadingOptions -> FilePath -> TaskQueue (IO ()) -> IO (Action IdeGhcSession)
639+
loadSessionWithOptions :: Recorder (WithPriority Log) -> SessionLoadingOptions -> FilePath -> WorkerTasks STM (IO ()) -> IO (Action IdeGhcSession)
637640
loadSessionWithOptions recorder SessionLoadingOptions{..} rootDir que = do
638641
let toAbsolutePath = toAbsolute rootDir -- see Note [Root Directory]
639642

@@ -663,7 +666,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} rootDir que = do
663666
-- see Note [Serializing runs in separate thread]
664667
-- Start the 'getOptionsLoop' if the queue is empty
665668
liftIO $ atomically $
666-
Extra.whenM (isEmptyTaskQueue que) $ do
669+
Extra.whenM (nullWorkerTasks que) $ do
667670
let newSessionLoadingOptions = SessionLoadingOptions
668671
{ findCradle = cradleLoc
669672
, ..
@@ -683,7 +686,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} rootDir que = do
683686
, sessionLoadingOptions = newSessionLoadingOptions
684687
}
685688

686-
writeTaskQueue que (runReaderT (getOptionsLoop recorder sessionShake sessionState knownTargetsVar) sessionEnv)
689+
addWorkerTask que (runReaderT (getOptionsLoop recorder sessionShake sessionState knownTargetsVar) sessionEnv)
687690

688691
-- Each one of deps will be registered as a FileSystemWatcher in the GhcSession action
689692
-- so that we can get a workspace/didChangeWatchedFiles notification when a dep changes.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ import Development.IDE.Core.Preprocessor
7575
import Development.IDE.Core.ProgressReporting (progressUpdate)
7676
import Development.IDE.Core.RuleTypes
7777
import Development.IDE.Core.Shake
78-
import Development.IDE.Core.WorkerThread (writeTaskQueue)
78+
import Development.IDE.Core.WorkerThread (WorkerTasks (..))
7979
import Development.IDE.Core.Tracing (withTrace)
8080
import qualified Development.IDE.GHC.Compat as Compat
8181
import qualified Development.IDE.GHC.Compat as GHC
@@ -942,7 +942,7 @@ indexHieFile se mod_summary srcPath !hash hf = do
942942
-- hiedb doesn't use the Haskell src, so we clear it to avoid unnecessarily keeping it around
943943
let !hf' = hf{hie_hs_src = mempty}
944944
modifyTVar' indexPending $ HashMap.insert srcPath hash
945-
writeTaskQueue indexQueue $ \withHieDb -> do
945+
addWorkerTask indexQueue $ \withHieDb -> do
946946
-- We are now in the worker thread
947947
-- Check if a newer index of this file has been scheduled, and if so skip this one
948948
newerScheduled <- atomically $ do

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ typecheckParentsAction recorder nfp = do
304304
setSomethingModified :: VFSModified -> IdeState -> T.Text -> IO [Key] -> IO ()
305305
setSomethingModified vfs state reason actionBetweenSession = do
306306
-- Update database to remove any files that might have been renamed/deleted
307-
atomically $ writeTaskQueue (indexQueue $ hiedbWriter $ shakeExtras state) (\withHieDb -> withHieDb deleteMissingRealFiles)
307+
let indexQueue' = indexQueue $ hiedbWriter $ shakeExtras state
308+
atomically $ addWorkerTask indexQueue' (\withHieDb -> withHieDb deleteMissingRealFiles)
308309
void $ restartShakeSession (shakeExtras state) vfs reason [] actionBetweenSession
309310

310311
registerFileWatches :: [String] -> LSP.LspT Config IO Bool

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

Lines changed: 69 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ module Development.IDE.Core.Shake(
2626
IdeState, shakeSessionInit, shakeExtras, shakeDb, rootDir,
2727
ShakeExtras(..), getShakeExtras, getShakeExtrasRules,
2828
KnownTargets(..), Target(..), toKnownFiles, unionKnownTargets, mkKnownTargets,
29-
IdeRule, IdeResult, RestartQueue,
29+
IdeRule, IdeResult,
3030
GetModificationTime(GetModificationTime, GetModificationTime_, missingFileDiagnostics),
3131
shakeOpen, shakeShut,
32-
withRestartWorker, newRestartSlot,
32+
newRestartSlot,
3333
shakeEnqueue,
3434
newSession,
3535
use, useNoFile, uses, useWithStaleFast, useWithStaleFast', delayedAction,
@@ -76,14 +76,14 @@ module Development.IDE.Core.Shake(
7676
RestartSlot(..),
7777
addPersistentRule,
7878
newestVFSModified,
79-
mergePendingRestart,
8079
garbageCollectDirtyKeys,
8180
garbageCollectDirtyKeysOlderThan,
8281
Log(..),
8382
VFSModified(..), getClientConfigAction,
8483
ThreadQueue(..),
8584
runWithSignal,
86-
askShake
85+
askShake,
86+
processPendingRestart
8787
) where
8888

8989
import Control.Concurrent.Async
@@ -149,7 +149,6 @@ import Development.IDE.GHC.Compat (NameCache,
149149
initNameCache,
150150
knownKeyNames)
151151
#endif
152-
import Data.IORef.Extra (atomicModifyIORef'_)
153152
import qualified Data.Text.Encoding as T
154153
import Development.IDE.GHC.Orphans ()
155154
import Development.IDE.Graph hiding (ShakeValue,
@@ -192,10 +191,7 @@ import qualified StmContainers.Map as STM
192191
import System.FilePath hiding (makeRelative)
193192
import System.IO.Unsafe (unsafePerformIO)
194193
import System.Time.Extra
195-
import UnliftIO (IORef,
196-
MonadUnliftIO (withRunInIO),
197-
atomicModifyIORef',
198-
newIORef)
194+
import UnliftIO (MonadUnliftIO (..))
199195

200196

201197
data Log
@@ -275,16 +271,15 @@ data HieDbWriter
275271
-- | Actions to queue up on the index worker thread
276272
-- The inner `(HieDb -> IO ()) -> IO ()` wraps `HieDb -> IO ()`
277273
-- with (currently) retry functionality
278-
type IndexQueue = TaskQueue (((HieDb -> IO ()) -> IO ()) -> IO ())
279-
type RestartQueue = TaskQueue (IO ())
280-
type LoaderQueue = TaskQueue (IO ())
281-
282-
283-
data ThreadQueue = ThreadQueue {
284-
tIndexQueue :: IndexQueue
285-
, tRestartSlot :: RestartSlot
286-
, tLoaderQueue :: LoaderQueue
287-
}
274+
type IndexQueue = WorkerTasks STM (((HieDb -> IO ()) -> IO ()) -> IO ())
275+
type RestartRef = WorkerTasks STM PendingRestart
276+
type LoaderQueue = WorkerTasks STM (IO ())
277+
278+
data ThreadQueue = ThreadQueue
279+
{ tIndexQueue :: IndexQueue
280+
, tRestartSlot :: RestartSlot
281+
, tLoaderQueue :: LoaderQueue
282+
}
288283

289284
-- Note [Semantic Tokens Cache Location]
290285
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -824,23 +819,21 @@ data PendingRestart = PendingRestart
824819
, pendingRestartDoneSignals :: ![TMVar ()]
825820
}
826821

827-
newestVFSModified :: VFSModified -> VFSModified -> VFSModified
828-
newestVFSModified VFSUnmodified old = old
829-
newestVFSModified new@(VFSModified _) _ = new
830-
831-
mergePendingRestart :: PendingRestart -> Maybe PendingRestart -> PendingRestart
832-
mergePendingRestart new Nothing = new
833-
mergePendingRestart new (Just old) = PendingRestart
822+
instance Semigroup PendingRestart where
823+
new <> old = PendingRestart
834824
{ pendingRestartVFS = newestVFSModified (pendingRestartVFS new) (pendingRestartVFS old)
835825
, pendingRestartReasons = pendingRestartReasons new ++ pendingRestartReasons old
836826
, pendingRestartActions = pendingRestartActions new ++ pendingRestartActions old
837827
, pendingRestartActionBetweenSessions = pendingRestartActionBetweenSessions new ++ pendingRestartActionBetweenSessions old
838828
, pendingRestartDoneSignals = pendingRestartDoneSignals new ++ pendingRestartDoneSignals old
839829
}
840830

831+
newestVFSModified :: VFSModified -> VFSModified -> VFSModified
832+
newestVFSModified VFSUnmodified old = old
833+
newestVFSModified new@(VFSModified _) _ = new
834+
841835
data RestartSlot = RestartSlot
842-
{ queuedRestart :: IORef (Maybe PendingRestart)
843-
, restartSignal :: MVar ()
836+
{ restartRef :: WorkerTasks STM PendingRestart
844837
, lastRestartBarrier :: TVar (TMVar ())
845838
-- ^ A barrier that is filled when the most recent shake restart completes.
846839
--
@@ -849,77 +842,62 @@ data RestartSlot = RestartSlot
849842
-- restart can then wait on this.
850843
}
851844

852-
newRestartSlot :: IO RestartSlot
853-
newRestartSlot = do
845+
newRestartSlot :: RestartRef -> IO RestartSlot
846+
newRestartSlot queuedRestart = do
854847
initialBarrier <- newTMVarIO () -- starts filled (no pending restart)
855-
RestartSlot <$> newIORef Nothing <*> newEmptyMVar <*> newTVarIO initialBarrier
848+
RestartSlot <$> pure queuedRestart <*> newTVarIO initialBarrier
856849

857850
-- | Restart the current 'ShakeSession' with the given system actions.
858851
--
859852
-- Any actions running in the current session will be aborted, but actions added
860853
-- via 'shakeEnqueue' will be requeued.
861854
shakeRestart :: IdeState -> VFSModified -> T.Text -> [DelayedAction ()] -> IO [Key] -> IO ()
862855
shakeRestart IdeState{..} vfs reason acts ioActionBetweenShakeSession = do
863-
restartDone <- newEmptyTMVarIO
864-
let slot = restartSlot shakeExtras
865-
-- Publish this restart's barrier, that dependents LSP requests can wait on.
866-
atomically $ writeTVar (lastRestartBarrier slot) restartDone
867-
atomicModifyIORef'_ (queuedRestart slot) $ Just . mergePendingRestart PendingRestart
868-
{ pendingRestartVFS = vfs
869-
, pendingRestartActionBetweenSessions = [ioActionBetweenShakeSession]
870-
, pendingRestartReasons = [reason]
871-
, pendingRestartActions = acts
872-
, pendingRestartDoneSignals = [restartDone]
873-
}
874-
void $ tryPutMVar (restartSignal slot) ()
875-
-- Block until the restart (including ioActionBetweenShakeSession) completes.
876-
-- This preserves the invariant from the original synchronous shakeRestart:
877-
-- callers (e.g. the session loader) must not proceed until their
878-
-- between-session actions have run, otherwise downstream rules can observe
879-
-- stale results (see Note at Session.hs restartSession call site).
880-
atomically $ readTMVar restartDone
881-
882-
-- | Run a worker that asynchronously processes shake restart requests. Will
883-
-- only ever queue upto 1 additional restart, accumulating data while processing
884-
-- any restart.
885-
withRestartWorker :: MVar IdeState -> IO r -> IO r
886-
withRestartWorker ideMVar action = do
887-
let restartWorkerAction = do
888-
ide@IdeState{..} <- readMVar ideMVar
889-
forever (processPendingRestart (shakeRecorder shakeExtras) ide)
890-
`catch` \(e :: SomeException) ->
891-
case fromException e of
892-
Just AsyncCancelled -> throwIO e
893-
_ -> logWith (shakeRecorder shakeExtras) Error (LogRestartWorkerException e)
894-
895-
withAsync restartWorkerAction $ \_ -> action
896-
897-
processPendingRestart :: Recorder (WithPriority Log) -> IdeState -> IO ()
898-
processPendingRestart recorder IdeState{..} = do
899-
takeMVar (restartSignal (restartSlot shakeExtras))
900-
pendingRestart <- atomicModifyIORef' (queuedRestart (restartSlot shakeExtras)) (Nothing,)
901-
void $ forM pendingRestart $ \PendingRestart {..} -> do
902-
flip finally (atomically $ traverse (flip tryPutTMVar ()) (reverse pendingRestartDoneSignals)) $ do
903-
let sessionAction runner = do
904-
(stopTime,()) <- duration $ logErrorAfter 10 $ cancelShakeSession runner
905-
keys <- fmap concat (sequence (reverse pendingRestartActionBetweenSessions))
906-
-- it is every important to update the dirty keys after we enter the critical section
907-
-- see Note [Housekeeping rule cache and dirty key outside of hls-graph]
908-
atomically $ modifyTVar' (dirtyKeys shakeExtras) $ \x -> foldl' (flip insertKeySet) x keys
909-
res <- shakeDatabaseProfile shakeDb
910-
backlog <- readTVarIO $ dirtyKeys shakeExtras
911-
queue <- atomicallyNamed "actionQueue - peek" $ peekInProgress $ actionQueue shakeExtras
912-
913-
-- this log is required by tests
914-
logWith recorder Debug $ LogBuildSessionRestart (reverse pendingRestartReasons) queue backlog stopTime res
915-
916-
withMVar' shakeSession sessionAction $ \() ->
917-
-- It is crucial to be masked here, otherwise we can get killed
918-
-- between spawning the new thread and updating shakeSession.
919-
-- See https://github.com/haskell/ghcide/issues/79
920-
(,()) <$> newSession recorder shakeExtras pendingRestartVFS shakeDb
921-
(reverse pendingRestartActions)
922-
(reverse pendingRestartReasons)
856+
restartDone <- newEmptyTMVarIO
857+
let RestartSlot {..} = restartSlot shakeExtras
858+
-- Publish this restart's barrier, that dependents LSP requests can wait on.
859+
atomically $ do
860+
writeTVar lastRestartBarrier restartDone
861+
addWorkerTask restartRef $ PendingRestart
862+
{ pendingRestartVFS = vfs
863+
, pendingRestartActionBetweenSessions = [ioActionBetweenShakeSession]
864+
, pendingRestartReasons = [reason]
865+
, pendingRestartActions = acts
866+
, pendingRestartDoneSignals = [restartDone]
867+
}
868+
869+
processPendingRestart :: Recorder (WithPriority Log) -> MVar IdeState -> PendingRestart -> IO ()
870+
processPendingRestart recorder ideMVar pendingRestart = do
871+
processPendingRestart' recorder ideMVar pendingRestart
872+
`catch` \(e :: SomeException) ->
873+
case fromException e of
874+
Just AsyncCancelled -> throwIO e
875+
_ -> logWith recorder Error (LogRestartWorkerException e)
876+
877+
processPendingRestart' :: Recorder (WithPriority Log) -> MVar IdeState -> PendingRestart -> IO ()
878+
processPendingRestart' recorder ideMVar PendingRestart{..} = do
879+
IdeState{..} <- readMVar ideMVar
880+
flip finally (atomically $ traverse (flip tryPutTMVar ()) (reverse pendingRestartDoneSignals)) $ do
881+
let sessionAction runner = do
882+
(stopTime,()) <- duration $ logErrorAfter 10 $ cancelShakeSession runner
883+
keys <- fmap concat (sequence (reverse pendingRestartActionBetweenSessions))
884+
-- it is every important to update the dirty keys after we enter the critical section
885+
-- see Note [Housekeeping rule cache and dirty key outside of hls-graph]
886+
atomically $ modifyTVar' (dirtyKeys shakeExtras) $ \x -> foldl' (flip insertKeySet) x keys
887+
res <- shakeDatabaseProfile shakeDb
888+
backlog <- readTVarIO $ dirtyKeys shakeExtras
889+
queue <- atomicallyNamed "actionQueue - peek" $ peekInProgress $ actionQueue shakeExtras
890+
891+
-- this log is required by tests
892+
logWith recorder Debug $ LogBuildSessionRestart (reverse pendingRestartReasons) queue backlog stopTime res
893+
894+
withMVar' shakeSession sessionAction $ \() ->
895+
-- It is crucial to be masked here, otherwise we can get killed
896+
-- between spawning the new thread and updating shakeSession.
897+
-- See https://github.com/haskell/ghcide/issues/79
898+
(,()) <$> newSession recorder shakeExtras pendingRestartVFS shakeDb
899+
(reverse pendingRestartActions)
900+
(reverse pendingRestartReasons)
923901
where
924902
logErrorAfter :: Seconds -> IO () -> IO ()
925903
logErrorAfter seconds action = flip withAsync (const action) $ do

0 commit comments

Comments
 (0)