Skip to content

Commit 46beafa

Browse files
committed
Ensure notification side-effects are immediately visible
1 parent 224e74f commit 46beafa

3 files changed

Lines changed: 48 additions & 32 deletions

File tree

ghcide-test/exe/ShakeRestartTests.hs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
module ShakeRestartTests (tests) where
55

6-
import Control.Concurrent.STM
6+
import qualified Data.Map.Lazy as Map
77
import Development.IDE.Core.Shake
8-
import Development.IDE.Graph (newKey)
8+
import Language.LSP.Protocol.Types (Uri (..), toNormalizedUri)
99
import Language.LSP.VFS
1010
import Test.Tasty
1111
import Test.Tasty.HUnit
@@ -18,18 +18,22 @@ tests = testGroup "shake restart merging"
1818
newestVFSModified vfs1 VFSUnmodified @?= vfs1
1919
newestVFSModified VFSUnmodified vfs1 @?= vfs1
2020

21-
, testCase "<>" $ do
22-
done1 <- newEmptyTMVarIO
23-
done2 <- newEmptyTMVarIO
24-
let key1 = newKey ("1" :: String)
25-
key2 = newKey ("2" :: String)
26-
p1 = PendingRestart VFSUnmodified [pure [key1]] ["r1"] [] [done1]
27-
p2 = PendingRestart VFSUnmodified [pure [key2]] ["r2"] [] [done2]
28-
merged = p1 <> p2
21+
, testCase "<> appends reasons in chronological order" $ do
22+
let p1 = PendingRestart VFSUnmodified mempty ["r1"] [] []
23+
p2 = PendingRestart VFSUnmodified mempty ["r2"] [] []
24+
pendingRestartReasons (p1 <> p2) @?= ["r1", "r2"]
2925

30-
pendingRestartReasons merged @?= ["r1", "r2"]
31-
keys <- sequence $ reverse $ pendingRestartActionBetweenSessions merged
32-
concat keys @?= [key2, key1]
26+
, testCase "<> takes VFS from the right operand" $ do
27+
let olderUri = toNormalizedUri (Uri "older")
28+
newerUri = toNormalizedUri (Uri "newer")
29+
unforced = error "VFS payload should not be forced by Map.keys"
30+
olderVfs = VFSModified (VFS (Map.singleton olderUri unforced))
31+
newerVfs = VFSModified (VFS (Map.singleton newerUri unforced))
32+
older = PendingRestart olderVfs mempty ["older"] [] []
33+
newer = PendingRestart newerVfs mempty ["newer"] [] []
34+
case pendingRestartVFS (older <> newer) of
35+
VFSModified (VFS m) -> Map.keys m @?= [newerUri]
36+
VFSUnmodified -> assertFailure "expected VFSModified"
3337
]
3438

3539
instance Eq VFSModified where

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

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,7 @@ import Data.Hashable
114114
import qualified Data.HashMap.Strict as HMap
115115
import Data.HashSet (HashSet)
116116
import qualified Data.HashSet as HSet
117-
import Data.List.Extra (foldl', partition,
118-
takeEnd)
117+
import Data.List.Extra (partition, takeEnd)
119118
import qualified Data.Map.Strict as Map
120119
import Data.Maybe
121120
import qualified Data.SortedList as SL
@@ -814,20 +813,22 @@ delayedAction a = do
814813
liftIO $ shakeEnqueue extras a
815814

816815
data PendingRestart = PendingRestart
817-
{ pendingRestartVFS :: !VFSModified
818-
, pendingRestartActionBetweenSessions :: ![IO [Key]]
819-
, pendingRestartReasons :: ![T.Text]
820-
, pendingRestartActions :: ![DelayedActionInternal]
821-
, pendingRestartDoneSignals :: ![TMVar ()]
816+
{ pendingRestartVFS :: !VFSModified
817+
, pendingRestartDirtyKeys :: !KeySet
818+
, pendingRestartReasons :: ![T.Text]
819+
, pendingRestartActions :: ![DelayedActionInternal]
820+
, pendingRestartDoneSignals :: ![TMVar ()]
822821
}
823822

823+
-- | TODO(crtschin): This isn't commutative because of VFS ordering. Make this a
824+
-- proper function.
824825
instance Semigroup PendingRestart where
825-
new <> old = PendingRestart
826-
{ pendingRestartVFS = newestVFSModified (pendingRestartVFS new) (pendingRestartVFS old)
827-
, pendingRestartReasons = pendingRestartReasons new ++ pendingRestartReasons old
828-
, pendingRestartActions = pendingRestartActions new ++ pendingRestartActions old
829-
, pendingRestartActionBetweenSessions = pendingRestartActionBetweenSessions new ++ pendingRestartActionBetweenSessions old
830-
, pendingRestartDoneSignals = pendingRestartDoneSignals new ++ pendingRestartDoneSignals old
826+
older <> newer = PendingRestart
827+
{ pendingRestartVFS = newestVFSModified (pendingRestartVFS newer) (pendingRestartVFS older)
828+
, pendingRestartDirtyKeys = pendingRestartDirtyKeys older <> pendingRestartDirtyKeys newer
829+
, pendingRestartReasons = pendingRestartReasons older ++ pendingRestartReasons newer
830+
, pendingRestartActions = pendingRestartActions older ++ pendingRestartActions newer
831+
, pendingRestartDoneSignals = pendingRestartDoneSignals older ++ pendingRestartDoneSignals newer
831832
}
832833

833834
newestVFSModified :: VFSModified -> VFSModified -> VFSModified
@@ -857,12 +858,20 @@ shakeRestart :: IdeState -> VFSModified -> T.Text -> [DelayedAction ()] -> IO [K
857858
shakeRestart IdeState{..} vfs reason acts ioActionBetweenShakeSession = do
858859
restartDone <- newEmptyTMVarIO
859860
let RestartSlot {..} = restartSlot shakeExtras
860-
-- Publish this restart's barrier, that dependents LSP requests can wait on.
861+
-- Run the between-session action here (not on the worker) so notification
862+
-- side effects are visible by the time the LSP notification handler returns.
863+
--
864+
-- If not here, then it would be hard to determine exactly where to place
865+
-- these side-effects when shake restarts are merged. While these side-effects
866+
-- do need to occur here, the keys they invalidate need to propagate to the
867+
-- worker so it can be used during the concrete restart.
868+
-- See Note [Housekeeping rule cache and dirty key outside of hls-graph].
869+
!newDirty <- fromListKeySet <$> ioActionBetweenShakeSession
861870
atomically $ do
862871
writeTVar lastRestartBarrier restartDone
863872
addWorkerTask restartRef $ PendingRestart
864873
{ pendingRestartVFS = vfs
865-
, pendingRestartActionBetweenSessions = [ioActionBetweenShakeSession]
874+
, pendingRestartDirtyKeys = newDirty
866875
, pendingRestartReasons = [reason]
867876
, pendingRestartActions = acts
868877
, pendingRestartDoneSignals = [restartDone]
@@ -882,10 +891,11 @@ processPendingRestart' recorder ideMVar PendingRestart{..} = do
882891
flip finally (atomically $ traverse (flip tryPutTMVar ()) (reverse pendingRestartDoneSignals)) $ do
883892
let sessionAction runner = do
884893
(stopTime,()) <- duration $ logErrorAfter 10 $ cancelShakeSession runner
885-
keys <- fmap concat (sequence (reverse pendingRestartActionBetweenSessions))
886-
-- it is every important to update the dirty keys after we enter the critical section
887-
-- see Note [Housekeeping rule cache and dirty key outside of hls-graph]
888-
atomically $ modifyTVar' (dirtyKeys shakeExtras) $ \x -> foldl' (flip insertKeySet) x keys
894+
-- Apply dirty keys after the dying session's work thread is dead
895+
-- (cancelShakeSession is synchronous). This is the only placement
896+
-- immune to concern 1.2 in
897+
-- Note [Housekeeping rule cache and dirty key outside of hls-graph]
898+
atomically $ modifyTVar' (dirtyKeys shakeExtras) (<> pendingRestartDirtyKeys)
889899
res <- shakeDatabaseProfile shakeDb
890900
backlog <- readTVarIO $ dirtyKeys shakeExtras
891901
queue <- atomicallyNamed "actionQueue - peek" $ peekInProgress $ actionQueue shakeExtras

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ withWorkerQueue
7777
-> ContT () IO (WorkerTasks STM a)
7878
withWorkerQueue = withWorkerTasks workerTaskQueue
7979

80+
-- | Similar to @withWorkerQueue@, but facilitates squashing actions using some
81+
-- @Semigroup@ semantics.
8082
withWorkerRef
8183
:: Semigroup a
8284
=> Recorder (WithPriority LogWorkerThread)

0 commit comments

Comments
 (0)