Skip to content

Commit 1ef2453

Browse files
committed
[glean-lsp] Async requests and cancellation
1 parent 51a3564 commit 1ef2453

3 files changed

Lines changed: 182 additions & 32 deletions

File tree

glean/lsp/Data/ConcurrentCache.hs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{-
2+
Derived from Data.ConcurrentCache in static-ls
3+
4+
Copyright (c) 2023 Joseph Sumabat
5+
-}
6+
7+
module Data.ConcurrentCache (
8+
ConcurrentCache,
9+
new,
10+
flush,
11+
remove,
12+
insert
13+
) where
14+
15+
import Control.Monad
16+
import Control.Monad.IO.Class
17+
import Control.Monad.IO.Unlift
18+
import Data.Hashable
19+
import Data.HashMap.Strict (HashMap)
20+
import qualified Data.HashMap.Strict as HashMap
21+
import UnliftIO.Exception
22+
import UnliftIO.IORef
23+
import UnliftIO.MVar
24+
25+
newtype ConcurrentCache k v = ConcurrentCache {
26+
map :: IORef (HashMap k (MVar (Maybe v)))
27+
-- k -> missing => computation for k not cached
28+
-- k -> MVar empty => computation for k in progress
29+
-- k -> MVar (Just v) => computation for k cached with value v
30+
-- k -> MVar Nothing => computation for k failed, try again
31+
}
32+
33+
new :: (MonadIO m) => m (ConcurrentCache k v)
34+
new = do
35+
map <- newIORef HashMap.empty
36+
pure ConcurrentCache {map}
37+
38+
flush :: (MonadIO m) => ConcurrentCache k v -> m ()
39+
flush cache = writeIORef cache.map HashMap.empty
40+
41+
remove :: (Hashable k, MonadIO m) => k -> ConcurrentCache k v -> m ()
42+
remove k cache = do
43+
atomicModifyIORef' cache.map $ \m -> do
44+
(HashMap.delete k m, ())
45+
46+
-- | Perform a computation and insert its result into the cache. If
47+
-- multiple threads try to insert a computation for the same key, one
48+
-- will perform its computation while the others wait. If the
49+
-- computation throws, another thread will try its computation.
50+
insert :: (Hashable k, MonadUnliftIO m) => k -> ConcurrentCache k v -> m v -> m v
51+
insert k cache act = mask $ \restore -> do
52+
var <- newEmptyMVar
53+
existed <- atomicModifyIORef' cache.map $ \m -> do
54+
case HashMap.lookup k m of
55+
Nothing -> (HashMap.insert k var m, Nothing)
56+
Just otherVar -> (m, Just otherVar)
57+
case existed of
58+
Nothing -> do
59+
v <-
60+
(restore act) `onException` do
61+
remove k cache
62+
putMVar var Nothing
63+
putMVar var (Just v)
64+
pure v
65+
Just otherVar ->
66+
join $ restore $ do
67+
v <- readMVar otherVar
68+
case v of
69+
Just v -> return $ return v
70+
Nothing ->
71+
-- try again; the key was removed from the cache
72+
return $ insert k cache act

glean/lsp/Glean/LSP.hs

Lines changed: 109 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
{-# LANGUAGE ApplicativeDo #-}
1+
{-# LANGUAGE ApplicativeDo, RecursiveDo #-}
22
module Glean.LSP (main) where
33

44
import Control.Monad
55
import Control.Monad.Catch
6+
import Control.Monad.Fix
67
import Control.Monad.IO.Class
78
import Control.Monad.IO.Unlift
89
import Control.Monad.Trans.Class
@@ -23,6 +24,7 @@ import qualified Language.LSP.Protocol.Message as LSP
2324
import qualified Language.LSP.Protocol.Types as LSP
2425
import Options.Applicative
2526
import Thrift.Protocol
27+
import UnliftIO.Async
2628
import UnliftIO.Exception
2729
import UnliftIO.IORef
2830
import Util.Log.Text
@@ -37,10 +39,10 @@ import qualified Glean.Glass.Types as Glass
3739
import qualified Glean.Glass.Handler.Documents as Glass.Handler
3840
import qualified Glean.Glass.Handler.Symbols as Glass.Handler
3941

42+
import Data.ConcurrentCache as ConcurrentCache
4043
import Data.Path as Path
4144

4245
{- TODO / ideas
43-
- concurrent requests and cancellation
4446
- go to decl / go to impl / go to type def?
4547
- documentSymbols:
4648
- make hierarchical
@@ -103,14 +105,13 @@ data LspEnv = LspEnv {
103105
options :: LspOptions,
104106
wsRoot :: AbsPath,
105107
glass :: Glass.Env,
106-
-- TODO: use a better symbol cache so that if two threads simultaneously
107-
-- try to fetch symbols for a file we should only make a single Glass request.
108-
symbolCache :: IORef (HashMap RelPath Glass.DocumentSymbolIndex)
108+
symbolCache :: ConcurrentCache RelPath Glass.DocumentSymbolIndex,
109+
requests :: IORef (HashMap LSP.SomeLspId (Async ()))
109110
}
110111

111112
newtype GleanLspM a =
112113
GleanLspM { unGleanLspM :: ReaderT (IORef (Maybe LspEnv)) (LspM LspConfig) a }
113-
deriving (Monad, Applicative, Functor, MonadIO, MonadThrow, MonadUnliftIO)
114+
deriving (Monad, Applicative, Functor, MonadIO, MonadThrow, MonadUnliftIO, MonadFix)
114115

115116
deriving instance LSP.MonadLsp LspConfig GleanLspM
116117

@@ -130,6 +131,77 @@ getGleanLspEnv = GleanLspM $ do
130131
Nothing -> throwIO $ GleanLspException "not initialized"
131132
Just env -> return env
132133

134+
-- -----------------------------------------------------------------------------
135+
-- Executing requests
136+
137+
-- | Perform a request asynchronously. It can subsequently be
138+
-- cancelled by 'cancelRequest', which will send the appropriate
139+
-- @RequestCancelled@ response back to the client if the request was
140+
-- still in progress at the time of cancellation.
141+
asyncRequest ::
142+
forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .
143+
(LSP.TRequestMessage m ->
144+
GleanLspM (Either (LSP.TResponseError m) (LSP.MessageResult m))) ->
145+
LSP.Handler GleanLspM m
146+
asyncRequest act = \msg respond -> mdo
147+
env <- getGleanLspEnv
148+
m <- readIORef env.requests
149+
liftIO $ logInfo $ "asyncRequest: " <>
150+
Text.pack (show (HashMap.size m)) <> " in progress"
151+
let
152+
register =
153+
atomicModifyIORef env.requests $ -- not strict due to mdo
154+
\m -> (HashMap.insert (LSP.SomeLspId msg._id) async m, ())
155+
unregister =
156+
atomicModifyIORef' env.requests $
157+
\m -> (HashMap.delete (LSP.SomeLspId msg._id) m, ())
158+
register
159+
async <- UnliftIO.Exception.mask_ $ asyncWithUnmask $ \unmask -> do
160+
r <- UnliftIO.Exception.trySyncOrAsync $ unmask $ act msg
161+
-- we want to catch everything, including async cancellation
162+
unregister
163+
case r of
164+
Left (ex :: SomeException)
165+
| Just AsyncCancelled <- fromException ex ->
166+
respond $ Left $ responseCancelled "cancelled by client"
167+
| otherwise -> respond $ Left $ responseException ex
168+
Right result -> respond result
169+
return ()
170+
171+
-- | Cancel a request by LspId
172+
cancelRequest ::
173+
forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .
174+
LSP.LspId m ->
175+
GleanLspM ()
176+
cancelRequest id = do
177+
env <- getGleanLspEnv
178+
m <- readIORef env.requests
179+
case HashMap.lookup (LSP.SomeLspId id) m of
180+
Nothing -> logWarning $ "cancelRequest: not found"
181+
Just async -> cancel async
182+
183+
responseCancelled ::
184+
forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .
185+
Text ->
186+
LSP.TResponseError m
187+
responseCancelled msg =
188+
LSP.TResponseError {
189+
_code = LSP.InL LSP.LSPErrorCodes_RequestCancelled,
190+
_message = msg,
191+
_xdata = Nothing
192+
}
193+
194+
responseException ::
195+
forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .
196+
SomeException ->
197+
LSP.TResponseError m
198+
responseException ex =
199+
LSP.TResponseError {
200+
_code = LSP.InL LSP.LSPErrorCodes_RequestFailed,
201+
_message = Text.pack (show ex),
202+
_xdata = Nothing
203+
}
204+
133205
-- -----------------------------------------------------------------------------
134206
-- Setup & initialisation
135207

@@ -144,8 +216,9 @@ initServer glass options envRef serverConfig _msg = do
144216
runExceptT $ do
145217
wsRoot <- ExceptT $ LSP.runLspT serverConfig getWsRoot
146218
wsRoot <- filePathToAbs wsRoot
147-
symbolCache <- newIORef HashMap.empty
148-
writeIORef envRef (Just LspEnv { options, glass, wsRoot, symbolCache })
219+
symbolCache <- ConcurrentCache.new
220+
requests <- newIORef HashMap.empty
221+
writeIORef envRef (Just LspEnv { options, glass, wsRoot, symbolCache, requests })
149222
liftIO $ logInfo $ "wsRoot: " <> Text.pack (Path.toFilePath wsRoot)
150223
pure serverConfig
151224
where
@@ -201,7 +274,7 @@ serverDef glass options = do
201274
, handleReferencesRequest
202275
-- , handleRenameRequest
203276
-- , handlePrepareRenameRequest
204-
-- , handleCancelNotification
277+
, handleCancelNotification
205278
, handleDidOpen
206279
-- , handleDidChange
207280
-- , handleDidSave
@@ -259,6 +332,15 @@ handleInitialized =
259332
LSP.notificationHandler LSP.SMethod_Initialized $
260333
pure $ pure ()
261334

335+
handleCancelNotification :: LSP.Handlers GleanLspM
336+
handleCancelNotification =
337+
LSP.notificationHandler LSP.SMethod_CancelRequest $ \req -> do
338+
let id = case req._params._id of
339+
LSP.InL i -> LSP.IdInt i
340+
LSP.InR t -> LSP.IdString t
341+
liftIO $ logInfo $ "cancel: " <> Text.pack (show id)
342+
cancelRequest id
343+
262344
handleDidOpen :: LSP.Handlers GleanLspM
263345
handleDidOpen =
264346
LSP.notificationHandler LSP.SMethod_TextDocumentDidOpen $ \message -> do
@@ -274,52 +356,53 @@ handleDidClose =
274356

275357
handleDocumentSymbols :: LSP.Handlers GleanLspM
276358
handleDocumentSymbols =
277-
LSP.requestHandler LSP.SMethod_TextDocumentDocumentSymbol $ \req res ->
359+
LSP.requestHandler LSP.SMethod_TextDocumentDocumentSymbol $ asyncRequest $ \req ->
278360
logTimed ("documentSymbols: " <> req._params._textDocument._uri.getUri) $ do
279361
let params = req._params
280362
path <- uriToAbsPath params._textDocument._uri
281363
syms <- getDocumentSymbols path
282364
liftIO $ logInfo $ "symbols: " <> Text.pack (show (length syms))
283-
res $ Right $ LSP.InR $ LSP.InL syms
365+
return $ Right $ LSP.InR $ LSP.InL syms
284366

285367
handleDefinitionRequest :: LSP.Handlers GleanLspM
286368
handleDefinitionRequest =
287-
LSP.requestHandler LSP.SMethod_TextDocumentDefinition $ \req resp -> do
369+
LSP.requestHandler LSP.SMethod_TextDocumentDefinition $ asyncRequest $ \req -> do
288370
let params = req._params
289371
logTimed ("definition: " <> params._textDocument._uri.getUri <>
290372
Text.pack (show req._params._position)) $ do
291373
path <- uriToAbsPath params._textDocument._uri
292374
defs <- getDefinition path params._position
293-
resp $ Right . LSP.InR $ LSP.InL defs
375+
return $ Right . LSP.InR $ LSP.InL defs
294376

295377
handleSetTrace :: LSP.Handlers GleanLspM
296378
handleSetTrace = LSP.notificationHandler LSP.SMethod_SetTrace $ \_ -> pure ()
297379

298380
handleTextDocumentHoverRequest :: LSP.Handlers GleanLspM
299381
handleTextDocumentHoverRequest =
300-
LSP.requestHandler LSP.SMethod_TextDocumentHover $ \req resp -> do
382+
LSP.requestHandler LSP.SMethod_TextDocumentHover $ asyncRequest $ \req -> do
301383
let hoverParams = req._params
302384
logTimed ("hover: " <> hoverParams._textDocument._uri.getUri <>
303385
Text.pack (show hoverParams._position)) $ do
304386
path <- uriToAbsPath hoverParams._textDocument._uri
305387
hover <- retrieveHover path hoverParams._position
306-
resp $ Right $ LSP.maybeToNull hover
388+
return $ Right $ LSP.maybeToNull hover
307389

308390
handleReferencesRequest :: LSP.Handlers GleanLspM
309391
handleReferencesRequest =
310-
LSP.requestHandler LSP.SMethod_TextDocumentReferences $ \req res -> do
392+
LSP.requestHandler LSP.SMethod_TextDocumentReferences $ asyncRequest $ \req -> do
311393
let params = req._params
312394
logTimed ("references: " <> params._textDocument._uri.getUri <>
313395
Text.pack (show params._position)) $ do
314396
path <- uriToAbsPath params._textDocument._uri
315397
refs <- findRefs path params._position
316-
res $ Right $ LSP.InL refs
398+
return $ Right $ LSP.InL refs
317399

318400
handleWorkspaceSymbol :: LSP.Handlers GleanLspM
319-
handleWorkspaceSymbol = LSP.requestHandler LSP.SMethod_WorkspaceSymbol $ \req res -> do
320-
-- https://hackage.haskell.org/package/lsp-types-1.6.0.0/docs/Language-LSP-Types.html#t:WorkspaceSymbolParams
321-
symbols <- symbolSearch req._params._query
322-
res $ Right . LSP.InL $ symbols
401+
handleWorkspaceSymbol =
402+
LSP.requestHandler LSP.SMethod_WorkspaceSymbol $ asyncRequest $ \req ->
403+
logTimed ("search: " <> req._params._query) $ do
404+
symbols <- symbolSearch req._params._query
405+
return $ Right . LSP.InL $ symbols
323406

324407
-- -----------------------------------------------------------------------------
325408
-- Glean / Glass stuff
@@ -354,27 +437,20 @@ getSymbols path includeRefs = do
354437
getSymbolsCached :: AbsPath -> GleanLspM Glass.DocumentSymbolIndex
355438
getSymbolsCached path = do
356439
env <- getGleanLspEnv
357-
cache <- readIORef env.symbolCache
358440
let relPath = Path.makeRelative env.wsRoot path
359-
case HashMap.lookup relPath cache of
360-
Just symbols -> return symbols
361-
Nothing -> do
362-
symbols <- getSymbols relPath True {- includeRefs -}
363-
atomicModifyIORef' env.symbolCache $ \old ->
364-
(HashMap.insert relPath symbols old, ())
365-
return symbols
441+
ConcurrentCache.insert relPath env.symbolCache $
442+
getSymbols relPath True {- includeRefs -}
366443

367444
flushSymbolCache :: GleanLspM ()
368445
flushSymbolCache = do
369446
env <- getGleanLspEnv
370-
writeIORef env.symbolCache HashMap.empty
447+
ConcurrentCache.flush env.symbolCache
371448

372449
removeCachedSymbols :: AbsPath -> GleanLspM ()
373450
removeCachedSymbols path = do
374451
env <- getGleanLspEnv
375452
let relPath = Path.makeRelative env.wsRoot path
376-
atomicModifyIORef' env.symbolCache $ \cache ->
377-
(HashMap.delete relPath cache, ())
453+
ConcurrentCache.remove relPath env.symbolCache
378454

379455
findSymbol ::
380456
LSP.Position ->
@@ -588,3 +664,4 @@ logTimed msg io = do
588664
liftIO $ logInfo $ msg <> ": " <>
589665
Text.pack (showTime t) <> ", " <> Text.pack (showAllocs b)
590666
return a
667+

glean/lsp/glean-lsp.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ executable glean-lsp
6565
ghc-options: -main-is Glean.LSP
6666
other-modules:
6767
Data.Path
68+
Data.ConcurrentCache
6869
build-depends:
6970
aeson >= 2.0.3 && < 2.3,
7071
base >=4.11.1 && <4.19,

0 commit comments

Comments
 (0)