Fix race condition in shakeRun (#80)

* Fix race condition in shakeRun

fixes #79 which also contains a detailed description of the issue.

* Factor out async exception logic into withMVar'
This commit is contained in:
Moritz Kiefer 2019-09-17 14:28:03 +02:00 committed by GitHub
parent eba0185710
commit 819bd4242c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 37 additions and 20 deletions

View File

@ -216,7 +216,7 @@ type IdeRule k v =
-- mappings from @(FilePath, k)@ to @RuleResult k@.
data IdeState = IdeState
{shakeDb :: ShakeDatabase
,shakeAbort :: Var (IO ()) -- close whoever was running last
,shakeAbort :: MVar (IO ()) -- close whoever was running last
,shakeClose :: IO ()
,shakeExtras :: ShakeExtras
,shakeProfileDir :: Maybe FilePath
@ -298,7 +298,7 @@ shakeOpen eventer logger shakeProfileDir (IdeReportProgress reportProgress) opts
, shakeProgress = if reportProgress then lspShakeProgress eventer else const (pure ())
}
rules
shakeAbort <- newVar $ return ()
shakeAbort <- newMVar $ return ()
shakeDb <- shakeDb
return IdeState{..}
@ -336,31 +336,47 @@ shakeProfile :: IdeState -> FilePath -> IO ()
shakeProfile IdeState{..} = shakeProfileDatabase shakeDb
shakeShut :: IdeState -> IO ()
shakeShut IdeState{..} = withVar shakeAbort $ \stop -> do
shakeShut IdeState{..} = withMVar shakeAbort $ \stop -> do
-- Shake gets unhappy if you try to close when there is a running
-- request so we first abort that.
stop
shakeClose
-- | This is a variant of withMVar where the first argument is run unmasked and if it throws
-- an exception, the previous value is restored while the second argument is executed masked.
withMVar' :: MVar a -> (a -> IO b) -> (b -> IO (a, c)) -> IO c
withMVar' var unmasked masked = mask $ \restore -> do
a <- takeMVar var
b <- restore (unmasked a) `onException` putMVar var a
(a', c) <- masked b
putMVar var a'
pure c
-- | Spawn immediately. If you are already inside a call to shakeRun that will be aborted with an exception.
shakeRun :: IdeState -> [Action a] -> IO (IO [a])
-- FIXME: If there is already a shakeRun queued up and waiting to send me a kill, I should probably
-- not even start, which would make issues with async exceptions less problematic.
shakeRun IdeState{shakeExtras=ShakeExtras{..}, ..} acts = modifyVar shakeAbort $ \stop -> do
(stopTime,_) <- duration stop
logDebug logger $ T.pack $ "Starting shakeRun (aborting the previous one took " ++ showDuration stopTime ++ ")"
bar <- newBarrier
start <- offsetTime
thread <- forkFinally (shakeRunDatabaseProfile shakeProfileDir shakeDb acts) $ \res -> do
runTime <- start
let res' = case res of
Left e -> "exception: " <> displayException e
Right _ -> "completed"
logDebug logger $ T.pack $
"Finishing shakeRun (took " ++ showDuration runTime ++ ", " ++ res' ++ ")"
signalBarrier bar res
-- important: we send an async exception to the thread, then wait for it to die, before continuing
return (do killThread thread; void $ waitBarrier bar, either throwIO return =<< waitBarrier bar)
shakeRun IdeState{shakeExtras=ShakeExtras{..}, ..} acts =
withMVar'
shakeAbort
(\stop -> do
(stopTime,_) <- duration stop
logDebug logger $ T.pack $ "Starting shakeRun (aborting the previous one took " ++ showDuration stopTime ++ ")"
bar <- newBarrier
start <- offsetTime
pure (start, bar))
-- It is crucial to be masked here, otherwise we can get killed
-- between spawning the new thread and updating shakeAbort.
-- See https://github.com/digital-asset/ghcide/issues/79
(\(start, bar) -> do
thread <- forkFinally (shakeRunDatabaseProfile shakeProfileDir shakeDb acts) $ \res -> do
runTime <- start
let res' = case res of
Left e -> "exception: " <> displayException e
Right _ -> "completed"
logDebug logger $ T.pack $
"Finishing shakeRun (took " ++ showDuration runTime ++ ", " ++ res' ++ ")"
signalBarrier bar res
-- important: we send an async exception to the thread, then wait for it to die, before continuing
pure (killThread thread >> void (waitBarrier bar), either throwIO return =<< waitBarrier bar))
getDiagnostics :: IdeState -> IO [FileDiagnostic]
getDiagnostics IdeState{shakeExtras = ShakeExtras{diagnostics}} = do

View File

@ -143,6 +143,7 @@ suggestAction contents Diagnostic{_range=_range@Range{..},..}
suggestAction _ _ = []
topOfHoleFitsMarker :: T.Text
topOfHoleFitsMarker =
#if MIN_GHC_API_VERSION(8,6,0)
"Valid hole fits include"