Andrea Vezzosi pushed to branch wip/romes/async-rpc at Glasgow Haskell Compiler / GHC
Commits:
-
3e76e7c9
by Andrea Vezzosi at 2026-10-30T13:04:38+01:00
3 changed files:
- compiler/GHC/Runtime/Interpreter/Process.hs
- libraries/ghci/GHCi/Message.hs
- libraries/ghci/GHCi/Message/Async.hs
Changes:
| ... | ... | @@ -4,6 +4,7 @@ module GHC.Runtime.Interpreter.Process |
| 4 | 4 | Message(..)
|
| 5 | 5 | , sendMessage
|
| 6 | 6 | , sendTHComputation
|
| 7 | + , sendMessageWithResponse
|
|
| 7 | 8 | )
|
| 8 | 9 | where
|
| 9 | 10 | |
| ... | ... | @@ -16,9 +17,7 @@ import GHC.Utils.Panic |
| 16 | 17 | import GHC.Utils.Exception as Ex
|
| 17 | 18 | |
| 18 | 19 | import Data.Binary
|
| 19 | -import Control.Concurrent
|
|
| 20 | 20 | import Data.IORef
|
| 21 | -import Control.Concurrent.STM
|
|
| 22 | 21 | |
| 23 | 22 | -- -----------------------------------------------------------------------------
|
| 24 | 23 | -- Top-level Message API
|
| ... | ... | @@ -34,29 +33,27 @@ sendMessage i msg = |
| 34 | 33 | where
|
| 35 | 34 | proc = instProcess i
|
| 36 | 35 | |
| 36 | +sendMessageWithResponse :: Binary a => ExtInterpInstance c -> Message a -> (IO a -> IO b) -> IO b
|
|
| 37 | +sendMessageWithResponse i msg k =
|
|
| 38 | + remoteCallWithResponse (interpPipe proc) msg k
|
|
| 39 | + where
|
|
| 40 | + proc = instProcess i
|
|
| 41 | + |
|
| 37 | 42 | -- | Send a message with a TH computation to the ext. interpreter (like
|
| 38 | 43 | -- 'RunTH', or 'RunModFinalizers') and handle 'THMessage' requests from the
|
| 39 | --- ext. interpreter to us in the meantime.
|
|
| 40 | ---
|
|
| 41 | --- This function blocks until the TH computation returns a value, but spawns in
|
|
| 42 | --- the background a thread to handle the 'THMessage's until the TH computation
|
|
| 43 | --- is fully done.
|
|
| 44 | +-- ext. interpreter to us until done, then wait for the response.
|
|
| 44 | 45 | sendTHComputation
|
| 45 | 46 | :: Binary a
|
| 46 | 47 | => ExtInterpInstance d
|
| 47 | 48 | -> (InboxId -> Message a)
|
| 48 | 49 | -> (forall b. Binary b => THMessage b -> IO (Bool, b))
|
| 49 | 50 | -- ^ Handle one message. Return True to end the loop.
|
| 50 | - -- This handler must never uninterrutibly mask an expression which may get
|
|
| 51 | - -- stuck. If it does, then 'sendTHComputation' will also get uninterruptibly stuck.
|
|
| 52 | 51 | -> IO a
|
| 53 | -sendTHComputation i msg reqHandler = mask $ \restore -> do
|
|
| 54 | - -- inline "withAsync/wait"
|
|
| 55 | - var <- newEmptyTMVarIO
|
|
| 56 | - mbox <- newMailbox (pipeAsyncMngr (interpPipe proc))
|
|
| 57 | - ht <- forkFinally (restore (handlerLoop mbox)) (atomically . putTMVar var)
|
|
| 58 | - restore (sendMessage i (msg mbox) <* atomically (readTMVar var))
|
|
| 59 | - `onException` uninterruptibleMask_ (throwTo ht CancelTHHandler)
|
|
| 52 | +sendTHComputation i msg reqHandler =
|
|
| 53 | + withNewMailbox (pipeAsyncMngr (interpPipe proc)) $ \ mbox ->
|
|
| 54 | + sendMessageWithResponse i (msg mbox) $ \ waitRes -> do
|
|
| 55 | + handlerLoop mbox
|
|
| 56 | + waitRes
|
|
| 60 | 57 | where
|
| 61 | 58 | proc = instProcess i
|
| 62 | 59 | handleOne mbox doneRef = do
|
| ... | ... | @@ -25,13 +25,12 @@ module GHCi.Message |
| 25 | 25 | , getMessage, putMessage, getTHMessage, putTHMessage
|
| 26 | 26 | |
| 27 | 27 | , Pipe(..), mkPipeFromHandles, mkPipeFromContinuations, closePipe
|
| 28 | - , remoteCall, remoteTHCall
|
|
| 28 | + , remoteCall, remoteCallWithResponse, remoteTHCall
|
|
| 29 | 29 | , handleRemoteCall, handleRemoteTHCall, writeReply
|
| 30 | 30 | , RemoteProcessException(..), PipeClosedException(..)
|
| 31 | 31 | |
| 32 | 32 | , InboxId(..)
|
| 33 | - , newMailbox
|
|
| 34 | - , closeMailbox
|
|
| 33 | + , withNewMailbox
|
|
| 35 | 34 | |
| 36 | 35 | , BreakModule
|
| 37 | 36 | , BreakUnitId
|
| ... | ... | @@ -660,8 +659,12 @@ serializeBCOs rbcos = parMap doChunk (chunkList 100 rbcos) |
| 660 | 659 | -- | Send a 'Message' request to and wait for a reply from the interpreter
|
| 661 | 660 | -- (blocks on the calling side, doesn't necessarily block the interpreter)
|
| 662 | 661 | remoteCall :: Binary a => Pipe -> Message a -> IO a
|
| 663 | -remoteCall pipe msg = either throwIO (pure . runGet get)
|
|
| 664 | - =<< remoteAsyncReq pipe (putMessage msg) Nothing
|
|
| 662 | +remoteCall pipe msg = remoteCallWithResponse pipe msg id
|
|
| 663 | + |
|
| 664 | +remoteCallWithResponse :: Binary a => Pipe -> Message a -> (IO a -> IO b) -> IO b
|
|
| 665 | +remoteCallWithResponse pipe msg k =
|
|
| 666 | + remoteAsyncReqWithResponse pipe (putMessage msg) Nothing $ \ m ->
|
|
| 667 | + k $ either throwIO (pure . runGet get) =<< m
|
|
| 665 | 668 | |
| 666 | 669 | -- | Handle a single 'Message' request on the interpreter and reply to the host
|
| 667 | 670 | handleRemoteCall :: Pipe
|
| ... | ... | @@ -11,12 +11,14 @@ module GHCi.Message.Async |
| 11 | 11 | |
| 12 | 12 | -- * Interface for async bidirectional RPC
|
| 13 | 13 | , remoteAsyncReq, handleAsyncReq
|
| 14 | + , remoteAsyncReqWithResponse
|
|
| 14 | 15 | , RemoteProcessException(..), DecodeAsyncReqException(..)
|
| 15 | 16 | |
| 16 | 17 | -- ** Mailboxes
|
| 17 | 18 | , InboxId(..)
|
| 18 | 19 | , newMailbox
|
| 19 | 20 | , closeMailbox
|
| 21 | + , withNewMailbox
|
|
| 20 | 22 | |
| 21 | 23 | -- ** Async matching internals
|
| 22 | 24 | , AsyncManager(..), CorrelationId
|
| ... | ... | @@ -247,24 +249,44 @@ newPendingReply pipe k = do |
| 247 | 249 | --
|
| 248 | 250 | -- See Note [Exceptions during serialization] about exceptions the 'Put' argument might raise.
|
| 249 | 251 | remoteAsyncReq :: Pipe -> Put -> (Maybe InboxId) -> IO (Either SomeException LB.ByteString)
|
| 250 | -remoteAsyncReq pipe msg ii = newPendingReply pipe $ \ uq wv ->
|
|
| 251 | - -- We might send a Cancel even if the request never got sent, but it will just
|
|
| 252 | - -- get ignored in that case.
|
|
| 253 | - handle (\e -> sendCancelMaybe uq e >> throwIO e) $ do
|
|
| 252 | +remoteAsyncReq pipe msg ii = remoteAsyncReqWithResponse pipe msg ii id
|
|
| 254 | 253 | |
| 255 | - -- Write request
|
|
| 256 | - writePipe pipe (Request uq ii (runPut msg))
|
|
| 257 | 254 | |
| 258 | - -- Block waiting for reply
|
|
| 259 | - atomically (takeTMVar wv)
|
|
| 255 | +remoteAsyncReqWithResponse :: Pipe
|
|
| 256 | + -> Put
|
|
| 257 | + -> Maybe InboxId
|
|
| 258 | + -> (IO (Either SomeException LB.ByteString) -> IO b)
|
|
| 259 | + -> IO b
|
|
| 260 | +remoteAsyncReqWithResponse pipe msg ii k = newPendingReply pipe $ \ uq wv -> do
|
|
| 261 | + -- We use an MVar to only Cancel the request if the response did not get read by `k`.
|
|
| 262 | + readRes <- newEmptyMVar
|
|
| 263 | + -- We might send a Cancel even if the request never got sent, but it will just
|
|
| 264 | + -- get ignored in that case.
|
|
| 265 | + --
|
|
| 266 | + -- Waiting for confirmation that the request got sent before "arming" the
|
|
| 267 | + -- cancelation requires more collaboration from `fillPipe`, because
|
|
| 268 | + -- writePipe's takeMVar is interruptible. Could be done if we generalize the
|
|
| 269 | + -- type of `writeQueue` to `TQueue (IddMsg, (Either SomeException ()) -> STM ())`.
|
|
| 270 | + cancelOnErrorMaybe uq readRes $ do
|
|
| 271 | + -- Write request
|
|
| 272 | + writePipe pipe (Request uq ii (runPut msg))
|
|
| 273 | + |
|
| 274 | + k $ do
|
|
| 275 | + -- Block waiting for reply
|
|
| 276 | + atomically (takeTMVar wv) <* putMVar readRes ()
|
|
| 260 | 277 | where
|
| 261 | - sendCancelMaybe uq e
|
|
| 262 | - | Just PipeWriteClosedException <- fromException e = pure ()
|
|
| 263 | - | otherwise = sendCancel uq
|
|
| 264 | - sendCancel uq = void . atomically $ do
|
|
| 265 | - -- we don't wait for Cancel to be written on the pipe,
|
|
| 266 | - -- and won't get a reply: nowhere to report it anyway.
|
|
| 267 | - writePipeAsync pipe (Cancel uq)
|
|
| 278 | + cancelOnErrorMaybe :: CorrelationId -> MVar () -> IO a -> IO a
|
|
| 279 | + cancelOnErrorMaybe uq readRes = handle (\ e -> sendCancelMaybe e >> throwIO e) where
|
|
| 280 | + sendCancelMaybe = \case
|
|
| 281 | + e | Just PipeWriteClosedException <- fromException e ->
|
|
| 282 | + pure ()
|
|
| 283 | + | otherwise ->
|
|
| 284 | + tryTakeMVar readRes >>= \case
|
|
| 285 | + Just () -> pure ()
|
|
| 286 | + Nothing -> void . atomically $ do
|
|
| 287 | + -- we don't wait for Cancel to be written on the pipe,
|
|
| 288 | + -- and won't get a reply: nowhere to report it anyway.
|
|
| 289 | + writePipeAsync pipe (Cancel uq)
|
|
| 268 | 290 | |
| 269 | 291 | -- | Receive a single asynchronous request and send a matching reply
|
| 270 | 292 | -- asynchronously (the handler may fork a computation that eventually replies
|
| ... | ... | @@ -352,6 +374,9 @@ closeMailbox :: AsyncManager -> InboxId -> IO () |
| 352 | 374 | closeMailbox AsyncManager{..} (InboxId ii) =
|
| 353 | 375 | atomically (modifyTVar mailboxQueues (IntMap.delete ii))
|
| 354 | 376 | |
| 377 | +withNewMailbox :: AsyncManager -> (InboxId -> IO a) -> IO a
|
|
| 378 | +withNewMailbox am = bracket (newMailbox am) (closeMailbox am)
|
|
| 379 | + |
|
| 355 | 380 | -- | Get or create a new mailbox for this 'InboxId'
|
| 356 | 381 | getMailbox :: AsyncManager -> InboxId -> STM (TQueue (CorrelationId, LB.ByteString))
|
| 357 | 382 | getMailbox am (InboxId ii) = do
|