Andrea Vezzosi pushed to branch wip/romes/async-rpc at Glasgow Haskell Compiler / GHC
Commits:
-
73b952d2
by Andrea Vezzosi at 2026-10-27T19:56:51+01:00
-
d748ef6a
by Andrea Vezzosi at 2026-10-27T19:56:51+01:00
-
8f45d12a
by Andrea Vezzosi at 2026-10-27T21:59:17+01:00
7 changed files:
- compiler/GHC/Runtime/Interpreter.hs
- compiler/GHC/Runtime/Interpreter/Process.hs
- compiler/GHC/Tc/Gen/Splice.hs
- libraries/ghci/GHCi/Message.hs
- libraries/ghci/GHCi/Message/Async.hs
- libraries/ghci/GHCi/Server.hs
- libraries/ghci/GHCi/Server/Threads.hs
Changes:
| ... | ... | @@ -105,7 +105,7 @@ import GHCi.Run |
| 105 | 105 | import Control.Concurrent
|
| 106 | 106 | import Control.Monad
|
| 107 | 107 | import Control.Monad.IO.Class
|
| 108 | -import Control.Monad.Catch as MC (mask)
|
|
| 108 | +import Control.Monad.Catch as MC (mask, handle, MonadThrow (..))
|
|
| 109 | 109 | import Data.Binary
|
| 110 | 110 | import Data.ByteString (ByteString)
|
| 111 | 111 | import qualified Data.ByteString.Short as SBS
|
| ... | ... | @@ -113,6 +113,7 @@ import qualified GHC.Exts.Heap as Heap |
| 113 | 113 | import GHC.Stack.CCS (CostCentre,CostCentreStack)
|
| 114 | 114 | import System.Directory
|
| 115 | 115 | import System.Process
|
| 116 | +import System.Timeout (timeout)
|
|
| 116 | 117 | import qualified GHC.InfoProv as InfoProv
|
| 117 | 118 | |
| 118 | 119 | import GHC.Builtin.Modules( gHC_PRIM, gHC_PRIMOPWRAPPERS )
|
| ... | ... | @@ -196,14 +197,16 @@ interpCmd interp msg = case interpInstance interp of |
| 196 | 197 | #if defined(HAVE_INTERNAL_INTERPRETER)
|
| 197 | 198 | InternalInterp -> run msg -- Just run it directly
|
| 198 | 199 | #endif
|
| 199 | - ExternalInterp ext -> withExtInterp ext $ \inst ->
|
|
| 200 | + ExternalInterp ext -> withExtInterp interp ext $ \inst ->
|
|
| 200 | 201 | sendMessage inst msg
|
| 201 | 202 | |
| 202 | -withExtInterp :: ExceptionMonad m => ExtInterp -> (forall d. ExtInterpInstance d -> m a) -> m a
|
|
| 203 | -withExtInterp ext action = case ext of
|
|
| 203 | +withExtInterp :: ExceptionMonad m => Interp -> ExtInterp -> (forall d. ExtInterpInstance d -> m a) -> m a
|
|
| 204 | +withExtInterp interp ext action = stopInterpOnPipeClosed $ case ext of
|
|
| 204 | 205 | ExtJS i -> withJSInterp i action
|
| 205 | 206 | ExtWasm i -> withWasmInterp i action
|
| 206 | 207 | ExtIServ i -> withIServ i action
|
| 208 | + where
|
|
| 209 | + stopInterpOnPipeClosed = MC.handle $ \ (e :: PipeClosedException) -> liftIO (stopInterp interp) >> MC.throwM e
|
|
| 207 | 210 | |
| 208 | 211 | withExtInterpStatus :: ExtInterp -> (forall d. ExtInterpStatusVar d -> m a) -> m a
|
| 209 | 212 | withExtInterpStatus ext action = case ext of
|
| ... | ... | @@ -631,18 +634,41 @@ stopInterp interp = case interpInstance interp of |
| 631 | 634 | #if defined(HAVE_INTERNAL_INTERPRETER)
|
| 632 | 635 | InternalInterp -> pure ()
|
| 633 | 636 | #endif
|
| 634 | - ExternalInterp ext -> withExtInterpStatus ext $ \mstate -> do
|
|
| 635 | - MC.mask $ \_restore -> modifyMVar_ mstate $ \state -> do
|
|
| 637 | + ExternalInterp ext -> (maybe (pure ()) throwIO =<<) $ withExtInterpStatus ext $ \mstate -> do
|
|
| 638 | + MC.mask $ \restore -> modifyMVar mstate $ \state -> do
|
|
| 636 | 639 | case state of
|
| 637 | - InterpPending -> pure state -- already stopped
|
|
| 640 | + InterpPending -> pure (state, Nothing) -- already stopped
|
|
| 638 | 641 | InterpRunning i -> do
|
| 639 | 642 | ex <- getProcessExitCode (interpHandle (instProcess i))
|
| 640 | - if isJust ex
|
|
| 641 | - then pure ()
|
|
| 642 | - else sendMessage i Shutdown
|
|
| 643 | - -- We asked the server to shutdown, so we will also shutdown any communication.
|
|
| 644 | - closePipe (interpPipe (instProcess i))
|
|
| 645 | - pure InterpPending
|
|
| 643 | + e <- if isJust ex
|
|
| 644 | + then gracefulShutdown i restore
|
|
| 645 | + else closePipe (interpPipe (instProcess i)) >> pure Nothing
|
|
| 646 | + modifyMVar_ (loader_state (interpLoader interp)) (const $ pure Nothing)
|
|
| 647 | + purgeInterpSymbolCache (interpSymbolCache interp)
|
|
| 648 | + modifyMVar_ (interpStringCache interp) (const $ pure emptyFsEnv)
|
|
| 649 | + pure (InterpPending,e)
|
|
| 650 | + where
|
|
| 651 | + gracefulShutdown i restore = do
|
|
| 652 | + x <- try (timeLimited restore $ sendMessage i Shutdown `finally` closePipe (interpPipe (instProcess i)))
|
|
| 653 | + let ph = interpHandle (instProcess i)
|
|
| 654 | + e <- case x of
|
|
| 655 | + -- happy path: ext. interp. should be shutting down of its own accord.
|
|
| 656 | + Right (Just ()) -> pure Nothing
|
|
| 657 | + -- timeout: ext. interp. couldn't handle Shutdown in time.
|
|
| 658 | + Right Nothing -> do
|
|
| 659 | + terminateProcess ph
|
|
| 660 | + pure Nothing
|
|
| 661 | + -- something went wrong, probably Pipe already closed.
|
|
| 662 | + Left e -> do
|
|
| 663 | + terminateProcess ph
|
|
| 664 | + returnUnexpectedException e
|
|
| 665 | + void $ timeLimited restore $ void $ waitForProcess ph
|
|
| 666 | + pure e
|
|
| 667 | + timeLimited restore m = timeout 1_000_000 (restore m)
|
|
| 668 | + returnUnexpectedException e
|
|
| 669 | + | Just (_ :: PipeClosedException) <- fromException e = return Nothing
|
|
| 670 | + | Just (_ :: IOException) <- fromException e = return Nothing
|
|
| 671 | + | otherwise = pure (Just e)
|
|
| 646 | 672 | |
| 647 | 673 | -- -----------------------------------------------------------------------------
|
| 648 | 674 | {- Note [External GHCi pointers]
|
| ... | ... | @@ -12,13 +12,10 @@ import GHC.Prelude |
| 12 | 12 | import GHC.Runtime.Interpreter.Types
|
| 13 | 13 | import GHCi.Message
|
| 14 | 14 | |
| 15 | -import GHC.IO (catchException)
|
|
| 16 | 15 | import GHC.Utils.Panic
|
| 17 | 16 | import GHC.Utils.Exception as Ex
|
| 18 | 17 | |
| 19 | 18 | import Data.Binary
|
| 20 | -import System.Exit
|
|
| 21 | -import System.Process
|
|
| 22 | 19 | import Control.Concurrent
|
| 23 | 20 | import Data.IORef
|
| 24 | 21 | import Control.Concurrent.STM
|
| ... | ... | @@ -34,7 +31,6 @@ import Control.Concurrent.STM |
| 34 | 31 | sendMessage :: Binary a => ExtInterpInstance d -> Message a -> IO a
|
| 35 | 32 | sendMessage i msg =
|
| 36 | 33 | remoteCall (interpPipe proc) msg
|
| 37 | - `catchException` \(e :: SomeException) -> handleInterpProcessFailure proc e
|
|
| 38 | 34 | where
|
| 39 | 35 | proc = instProcess i
|
| 40 | 36 | |
| ... | ... | @@ -64,7 +60,7 @@ sendTHComputation i msg reqHandler = mask $ \restore -> do |
| 64 | 60 | where
|
| 65 | 61 | proc = instProcess i
|
| 66 | 62 | handleOne mbox doneRef = do
|
| 67 | - handleRemoteTHCall (interpPipe proc) mbox $ \req -> do
|
|
| 63 | + handleRemoteTHCall (interpPipe proc) mbox $ \_setCancel req -> do
|
|
| 68 | 64 | (stop, res) <- reqHandler req
|
| 69 | 65 | writeIORef doneRef stop
|
| 70 | 66 | return res
|
| ... | ... | @@ -77,18 +73,6 @@ sendTHComputation i msg reqHandler = mask $ \restore -> do |
| 77 | 73 | else loop
|
| 78 | 74 | in loop
|
| 79 | 75 | |
| 80 | -handleInterpProcessFailure :: InterpProcess -> SomeException -> IO a
|
|
| 81 | -handleInterpProcessFailure i e = do
|
|
| 82 | - let hdl = interpHandle i
|
|
| 83 | - ex <- getProcessExitCode hdl
|
|
| 84 | - case ex of
|
|
| 85 | - Just (ExitFailure n) ->
|
|
| 86 | - throwIO (InstallationError ("External interpreter terminated (" ++ show n ++ ")"))
|
|
| 87 | - _ -> do
|
|
| 88 | - terminateProcess hdl
|
|
| 89 | - _ <- waitForProcess hdl
|
|
| 90 | - throw e
|
|
| 91 | - |
|
| 92 | 76 | data CancelTHHandler = CancelTHHandler deriving Show
|
| 93 | 77 | instance Exception CancelTHHandler where
|
| 94 | 78 | fromException = asyncExceptionFromException
|
| ... | ... | @@ -1160,7 +1160,7 @@ runRemoteModFinalizers (ThModFinalizers finRefs) = do |
| 1160 | 1160 | runQinTcM $ sequence_ qs
|
| 1161 | 1161 | #endif
|
| 1162 | 1162 | |
| 1163 | - ExternalInterp ext -> withExtInterp ext $ \inst -> do
|
|
| 1163 | + ExternalInterp ext -> withExtInterp interp ext $ \inst -> do
|
|
| 1164 | 1164 | tcg <- getGblEnv
|
| 1165 | 1165 | th_state <- readTcRef (tcg_th_remote_state tcg)
|
| 1166 | 1166 | case th_state of
|
| ... | ... | @@ -1817,7 +1817,7 @@ runTH ty fhv = do |
| 1817 | 1817 | return r
|
| 1818 | 1818 | #endif
|
| 1819 | 1819 | |
| 1820 | - ExternalInterp ext -> withExtInterp ext $ \inst -> do
|
|
| 1820 | + ExternalInterp ext -> withExtInterp interp ext $ \inst -> do
|
|
| 1821 | 1821 | -- Run it on the server. For an overview of how TH works with
|
| 1822 | 1822 | -- Remote GHCi, see Note [Remote Template Haskell] in
|
| 1823 | 1823 | -- libraries/ghci/GHCi/TH.hs.
|
| ... | ... | @@ -27,7 +27,7 @@ module GHCi.Message |
| 27 | 27 | , Pipe(..), mkPipeFromHandles, mkPipeFromContinuations, closePipe
|
| 28 | 28 | , remoteCall, remoteTHCall
|
| 29 | 29 | , handleRemoteCall, handleRemoteTHCall, writeReply
|
| 30 | - , RemoteProcessException(..), PipeReadClosedException(..)
|
|
| 30 | + , RemoteProcessException(..), PipeClosedException(..)
|
|
| 31 | 31 | |
| 32 | 32 | , InboxId(..)
|
| 33 | 33 | , newMailbox
|
| ... | ... | @@ -666,12 +666,12 @@ remoteCall pipe msg = either throwIO (pure . runGet get) |
| 666 | 666 | -- | Handle a single 'Message' request on the interpreter and reply to the host
|
| 667 | 667 | handleRemoteCall :: Pipe
|
| 668 | 668 | -> (Msg -> IO Msg) {-^ Msg hook -}
|
| 669 | - -> (forall a. (Show a, Binary a) => WriteReply a %1 -> Message a -> IO r)
|
|
| 669 | + -> (forall a. (Show a, Binary a) => WriteReply a %1 -> (IO () -> IO ()) -> Message a -> IO r)
|
|
| 670 | 670 | -> IO r
|
| 671 | 671 | handleRemoteCall pipe hook k =
|
| 672 | - handleAsyncReq pipe getMessage Nothing \(WriteReply writer) msg0 -> IO \s0 ->
|
|
| 672 | + handleAsyncReq pipe getMessage Nothing \(WriteReply writer) sa msg0 -> IO \s0 ->
|
|
| 673 | 673 | case hook msg0 |#> s0 of -- unIO for linearity checking
|
| 674 | - (# s1, Msg msg #) -> k (WriteReply \x -> writer (fmap put x)) msg |#> s1
|
|
| 674 | + (# s1, Msg msg #) -> k (WriteReply \x -> writer (fmap put x)) sa msg |#> s1
|
|
| 675 | 675 | |
| 676 | 676 | -- | Send a 'THMessage' request to the host and wait for a reply from the host
|
| 677 | 677 | -- to the interpreter.
|
| ... | ... | @@ -687,9 +687,9 @@ remoteTHCall pipe ii msg = either throwIO (pure . runGet get) |
| 687 | 687 | -- Symmterically to 'remoteTHCall', we always want to specify the 'InboxId' for
|
| 688 | 688 | -- RunTH "reverse requests"
|
| 689 | 689 | handleRemoteTHCall
|
| 690 | - :: Pipe -> InboxId -> (forall a. (Show a, Binary a) => THMessage a -> IO a) -> IO ()
|
|
| 690 | + :: Pipe -> InboxId -> (forall a. (Show a, Binary a) => (IO () -> IO ()) -> THMessage a -> IO a) -> IO ()
|
|
| 691 | 691 | handleRemoteTHCall pipe ii k =
|
| 692 | - handleAsyncReq pipe getTHMessage (Just ii) \writer (THMsg msg) -> IO \s0 ->
|
|
| 693 | - case k msg |#> s0 of -- unIO for linearity checking
|
|
| 692 | + handleAsyncReq pipe getTHMessage (Just ii) \writer sa (THMsg msg) -> IO \s0 ->
|
|
| 693 | + case k sa msg |#> s0 of -- unIO for linearity checking
|
|
| 694 | 694 | (# s1, a #) -> writeReply writer (put a) |#> s1
|
| 695 | 695 |
| ... | ... | @@ -7,7 +7,7 @@ module GHCi.Message.Async |
| 7 | 7 | (
|
| 8 | 8 | -- * Pipes
|
| 9 | 9 | Pipe(..), mkPipeFromHandles, mkPipeFromContinuations
|
| 10 | - , closePipe, PipeReadClosedException(..)
|
|
| 10 | + , closePipe, PipeClosedException(..)
|
|
| 11 | 11 | |
| 12 | 12 | -- * Interface for async bidirectional RPC
|
| 13 | 13 | , remoteAsyncReq, handleAsyncReq
|
| ... | ... | @@ -145,13 +145,13 @@ closePipe p@Pipe{..} = do |
| 145 | 145 | unless writeAlreadyClosed
|
| 146 | 146 | (throwTo (asyncFillTid pipeAsyncMngr) PipeWriteClosedException)
|
| 147 | 147 | |
| 148 | --- | We've closed the pipe read end on this process
|
|
| 149 | -data PipeReadClosedException = PipeReadClosedException deriving Show
|
|
| 150 | -instance Exception PipeReadClosedException
|
|
| 148 | +data PipeClosedException
|
|
| 149 | + = PipeReadClosedException -- ^ We've closed the pipe read end on this process
|
|
| 150 | + |
|
| 151 | + | PipeWriteClosedException -- ^ We've closed the pipe write end on this process
|
|
| 152 | + deriving Show
|
|
| 153 | +instance Exception PipeClosedException
|
|
| 151 | 154 | |
| 152 | --- | We've closed the pipe write end on this process
|
|
| 153 | -data PipeWriteClosedException = PipeWriteClosedException deriving Show
|
|
| 154 | -instance Exception PipeWriteClosedException
|
|
| 155 | 155 | |
| 156 | 156 | --------------------------------------------------------------------------------
|
| 157 | 157 | -- ** Pipe thread-safe read/write
|
| ... | ... | @@ -160,14 +160,19 @@ instance Exception PipeWriteClosedException |
| 160 | 160 | -- and identify whether they are requests or replies.
|
| 161 | 161 | |
| 162 | 162 | writePipe :: Pipe -> IddMsg -> IO ()
|
| 163 | -writePipe Pipe{..} msg = do
|
|
| 164 | - var <- newEmptyTMVarIO
|
|
| 165 | - atomically $ do
|
|
| 163 | +writePipe p msg = do
|
|
| 164 | + var <- atomically $ writePipeAsync p msg
|
|
| 165 | + either throwIO pure =<< atomically (takeTMVar var)
|
|
| 166 | + |
|
| 167 | +writePipeAsync :: Pipe -> IddMsg -> STM (TMVar (Either SomeException ()))
|
|
| 168 | +writePipeAsync Pipe{..} msg = do
|
|
| 169 | + var <- newEmptyTMVar
|
|
| 166 | 170 | readTVar pipeWriteClosed >>= \case
|
| 167 | 171 | True -> throwSTM PipeWriteClosedException
|
| 168 | 172 | False -> pure ()
|
| 169 | 173 | writeTQueue (writeQueue pipeAsyncMngr) (msg, var)
|
| 170 | - either throwIO pure =<< atomically (takeTMVar var)
|
|
| 174 | + pure var
|
|
| 175 | + |
|
| 171 | 176 | |
| 172 | 177 | -- | Only the 'drainPipe' thread should read from the pipe directly. Other
|
| 173 | 178 | -- functions should wait for the matching reply to be delivered asynchronously.
|
| ... | ... | @@ -239,14 +244,18 @@ remoteAsyncReq pipe msg ii = do |
| 239 | 244 | -- Write request
|
| 240 | 245 | writePipe pipe (Request uq ii (runPut msg))
|
| 241 | 246 | `onException`
|
| 242 | - -- the req wasn't done, or no one will be reading the reply:
|
|
| 243 | - -- in either case, drop it from the pending map.
|
|
| 244 | - atomically (modifyTVar pendingReplies (IntMap.delete uq))
|
|
| 247 | + -- the req wasn't done, or no one will be reading the reply:
|
|
| 248 | + -- in either case, drop it from the pending map.
|
|
| 249 | + atomically (modifyTVar pendingReplies (IntMap.delete uq))
|
|
| 245 | 250 | |
| 246 | 251 | -- Block waiting for reply
|
| 247 | 252 | atomically (takeTMVar wv)
|
| 248 | 253 | `onException`
|
| 249 | - atomically (modifyTVar pendingReplies (IntMap.delete uq))
|
|
| 254 | + atomically (do
|
|
| 255 | + modifyTVar pendingReplies (IntMap.delete uq)
|
|
| 256 | + -- we don't wait for Cancel to be written on the pipe,
|
|
| 257 | + -- and won't get a reply: nowhere to report it anyway.
|
|
| 258 | + writePipeAsync pipe (Cancel uq))
|
|
| 250 | 259 | |
| 251 | 260 | -- | Receive a single asynchronous request and send a matching reply
|
| 252 | 261 | -- asynchronously (the handler may fork a computation that eventually replies
|
| ... | ... | @@ -258,7 +267,7 @@ remoteAsyncReq pipe msg ii = do |
| 258 | 267 | -- branch, and if the handler otherwise throws, it is handled by 'handleAsyncReq'.
|
| 259 | 268 | --
|
| 260 | 269 | -- See Note [Exceptions during serialization] about exceptions the 'Put' argument might raise.
|
| 261 | -handleAsyncReq :: Pipe -> Get a -> (Maybe InboxId) -> (WriteReply Put %1 -> a -> IO r) -> IO r
|
|
| 270 | +handleAsyncReq :: Pipe -> Get a -> (Maybe InboxId) -> (WriteReply Put %1 -> (IO () -> IO ()) -> a -> IO r) -> IO r
|
|
| 262 | 271 | handleAsyncReq pipe getit mii k = mask $ \restore -> do
|
| 263 | 272 | let am = pipeAsyncMngr pipe
|
| 264 | 273 | ack <- newEmptyMVar
|
| ... | ... | @@ -274,7 +283,7 @@ handleAsyncReq pipe getit mii k = mask $ \restore -> do |
| 274 | 283 | Nothing -> tryReadTQueue $ requestsQueue am
|
| 275 | 284 | Just ii -> tryReadTQueue =<< getMailbox am ii
|
| 276 | 285 | case taken of
|
| 277 | - Just req -> pure (Just req)
|
|
| 286 | + Just req@(uq,_) -> fetchedRequest am uq >> pure (Just req)
|
|
| 278 | 287 | Nothing | isClosed -> pure Nothing -- empty and closed
|
| 279 | 288 | | otherwise -> retry -- wait for msg
|
| 280 | 289 | |
| ... | ... | @@ -282,9 +291,9 @@ handleAsyncReq pipe getit mii k = mask $ \restore -> do |
| 282 | 291 | Nothing -> throwIO PipeReadClosedException -- can't expect reqs
|
| 283 | 292 | Just (uq, bs) -> do
|
| 284 | 293 | let reply p = do
|
| 285 | - writePipe pipe (Reply uq (bimap toSerializableException runPut p))
|
|
| 294 | + b <- atomically $ doneHandling am uq
|
|
| 295 | + when b $ writePipe pipe (Reply uq (bimap toSerializableException runPut p))
|
|
| 286 | 296 | putMVar ack ()
|
| 287 | - |
|
| 288 | 297 | -- Run the continuation which must reply exactly once with 'writeReply'
|
| 289 | 298 | case runGetOrFail getit bs of
|
| 290 | 299 | Left (_, _, err) -> do
|
| ... | ... | @@ -292,7 +301,7 @@ handleAsyncReq pipe getit mii k = mask $ \restore -> do |
| 292 | 301 | reply (Left (toException exc))
|
| 293 | 302 | throwIO exc
|
| 294 | 303 | Right (_, _, it) ->
|
| 295 | - restore (k (WriteReply reply) it)
|
|
| 304 | + restore (k (WriteReply reply) (setActive am uq) it)
|
|
| 296 | 305 | |
| 297 | 306 | -- If there's an exception before the reply is sent, reply with an error instead.
|
| 298 | 307 | `catch` \(e :: SomeException) -> do
|
| ... | ... | @@ -306,6 +315,7 @@ handleAsyncReq pipe getit mii k = mask $ \restore -> do |
| 306 | 315 | reply (Left e)
|
| 307 | 316 | throwIO e
|
| 308 | 317 | |
| 318 | + |
|
| 309 | 319 | -- | 'handleSyncReq' failed to decode an incoming request payload using the given @'Get' a@
|
| 310 | 320 | newtype DecodeAsyncReqException = DecodeAsyncReqException String deriving Show
|
| 311 | 321 | instance Exception DecodeAsyncReqException
|
| ... | ... | @@ -347,6 +357,28 @@ getMailbox am (InboxId ii) = do |
| 347 | 357 | |
| 348 | 358 | -- ** Async matching "internals" -----------------------------------------------
|
| 349 | 359 | |
| 360 | +-- | Tracks handling status of a received 'Request'. The values are states of a
|
|
| 361 | +-- tiny state machine used to coordinate cancelations.
|
|
| 362 | +--
|
|
| 363 | +-- 'handleAsyncReq' will pop a request from the appropriate queue, setting the
|
|
| 364 | +-- status to `Fetched`. Then one of two things can happen first:
|
|
| 365 | +-- - we receive a 'Cancel', then status is set to `Canceling` so the cancel
|
|
| 366 | +-- action will run when set.
|
|
| 367 | +-- - handler sets a cancel action, then reception of a `Cancel` runs that
|
|
| 368 | +-- action and deletes the RequestStatus.
|
|
| 369 | +--
|
|
| 370 | +-- If RequestStatus is `Canceling` or absent, then a `Reply` won't be sent.
|
|
| 371 | +--
|
|
| 372 | +-- The two possible transition paths:
|
|
| 373 | +-- requestsQueue -> Fetched -> Active cancel -> gone
|
|
| 374 | +-- requestsQueue -> Fetched -> Canceling -> gone
|
|
| 375 | + |
|
| 376 | +data RequestStatus
|
|
| 377 | + = Fetched -- ^ 'Fetched' from a request queue, to pass to handler.
|
|
| 378 | + | Active (IO ()) -- ^ Handler set a cancel action, and has not sent a reply
|
|
| 379 | + -- yet. The cancel action must not block.
|
|
| 380 | + | Canceling -- ^ `Cancel` received while status is still `Fetched`.
|
|
| 381 | + |
|
| 350 | 382 | data AsyncManager = AsyncManager
|
| 351 | 383 | { pendingReply :: !(TVar (IntMap (TMVar (Either SomeException LB.ByteString))))
|
| 352 | 384 | -- ^ When a request is sent, keep track that we're expecting a reply. The
|
| ... | ... | @@ -357,6 +389,9 @@ data AsyncManager = AsyncManager |
| 357 | 389 | -- /without/ a specific 'InboxId', queue it here. A request handler will
|
| 358 | 390 | -- pop the next request from it. (Nothing except the 'drainPipe' thread
|
| 359 | 391 | -- should read from the pipe). Read with @'handleAsyncReq' p get Nothing@.
|
| 392 | + , cancelStatus :: !(TVar (IntMap RequestStatus))
|
|
| 393 | + -- ^ When a `Cancel uq` is received, we lookup `uq` in this map and call the
|
|
| 394 | + -- callback if present.
|
|
| 360 | 395 | , mailboxQueues :: !(TVar (IntMap (TQueue (CorrelationId, LB.ByteString))))
|
| 361 | 396 | -- ^ Requests for a specific 'InboxId' that can be read using
|
| 362 | 397 | -- @'handleAsyncReq' p get (Just <mailboxid>)@.
|
| ... | ... | @@ -389,15 +424,21 @@ type CorrelationId = Int |
| 389 | 424 | -- exclusively.
|
| 390 | 425 | data IddMsg = Request !CorrelationId !(Maybe InboxId) LB.ByteString
|
| 391 | 426 | | Reply !CorrelationId (Either SerializableException LB.ByteString)
|
| 427 | + | Cancel !CorrelationId -- ^ Technically a Request, but no Reply
|
|
| 428 | + -- is expected, the CorrelationId is the
|
|
| 429 | + -- one of the Request being canceled.
|
|
| 392 | 430 | |
| 393 | 431 | instance Binary IddMsg where
|
| 394 | 432 | put (Request i ii bs) = putWord8 0 >> put i >> put ii >> put bs
|
| 395 | 433 | put (Reply i bs) = putWord8 1 >> put i >> put bs
|
| 434 | + put (Cancel i) = putWord8 2 >> put i
|
|
| 435 | + |
|
| 396 | 436 | get = do
|
| 397 | 437 | b <- getWord8
|
| 398 | 438 | case b of
|
| 399 | 439 | 0 -> Request <$> get <*> get <*> get
|
| 400 | 440 | 1 -> Reply <$> get <*> get
|
| 441 | + 2 -> Cancel <$> get
|
|
| 401 | 442 | _ -> fail $ "impossible: Binary IddMsg - " ++ show b
|
| 402 | 443 | |
| 403 | 444 | newAsyncManager :: Pipe -> IO AsyncManager
|
| ... | ... | @@ -405,6 +446,7 @@ newAsyncManager pipe = do |
| 405 | 446 | asyncFreshId <- newIORef 0
|
| 406 | 447 | pendingReply <- newTVarIO IntMap.empty
|
| 407 | 448 | requestsQueue <- newTQueueIO
|
| 449 | + cancelStatus <- newTVarIO IntMap.empty
|
|
| 408 | 450 | mailboxQueues <- newTVarIO IntMap.empty
|
| 409 | 451 | writeQueue <- newTQueueIO
|
| 410 | 452 | |
| ... | ... | @@ -430,41 +472,85 @@ drainPipe :: Pipe -> (forall x. IO x -> IO x) -> IO () |
| 430 | 472 | drainPipe p unmask = catch (unmask $ loop `finally` pipeCloseRead p) replyToPendings where
|
| 431 | 473 | loop = do
|
| 432 | 474 | readPipe p >>= \case
|
| 433 | - -- Receive a reply to a pending notification
|
|
| 434 | - Just (Reply uq bs) -> do
|
|
| 435 | - atomically $ do
|
|
| 436 | - m <- readTVar (pendingReply am)
|
|
| 437 | - case IntMap.lookup uq m of
|
|
| 438 | - Nothing ->
|
|
| 439 | - -- the req was dropped from pending bc there was an exception, and no
|
|
| 440 | - -- one's listening any more. the reply is ignored
|
|
| 441 | - pure ()
|
|
| 442 | - Just wv -> do
|
|
| 443 | - writeTVar (pendingReply am) (IntMap.delete uq m)
|
|
| 444 | - -- signal matching request
|
|
| 445 | - putTMVar wv (bimap (toException . RemoteProcessException) id bs)
|
|
| 446 | - loop
|
|
| 447 | - -- Receive a request and queue it
|
|
| 448 | - Just (Request uq mbi bs) -> do
|
|
| 449 | - atomically $ case mbi of
|
|
| 450 | - Nothing ->
|
|
| 451 | - -- "Global" request
|
|
| 452 | - writeTQueue (requestsQueue am) (uq,bs)
|
|
| 453 | - Just ii -> do
|
|
| 454 | - -- "Specific mailbox" request
|
|
| 455 | - q <- getMailbox am ii
|
|
| 456 | - writeTQueue q (uq,bs)
|
|
| 475 | + Just msg -> do
|
|
| 476 | + handleMsg msg
|
|
| 457 | 477 | loop
|
| 458 | 478 | -- EOF on read handle
|
| 459 | 479 | Nothing -> do
|
| 460 | 480 | -- clean up pipe then exit loop
|
| 461 | 481 | replyToPendings (toException PipeReadClosedException)
|
| 462 | 482 | pure ()
|
| 483 | + handleMsg = \case
|
|
| 484 | + -- Receive a reply to a pending notification
|
|
| 485 | + Reply uq bs -> do
|
|
| 486 | + atomically $ do
|
|
| 487 | + m <- readTVar (pendingReply am)
|
|
| 488 | + case IntMap.lookup uq m of
|
|
| 489 | + Nothing ->
|
|
| 490 | + -- the req was dropped from pending bc there was an exception, and no
|
|
| 491 | + -- one's listening any more. the reply is ignored
|
|
| 492 | + pure ()
|
|
| 493 | + Just wv -> do
|
|
| 494 | + writeTVar (pendingReply am) (IntMap.delete uq m)
|
|
| 495 | + -- signal matching request
|
|
| 496 | + putTMVar wv (bimap (toException . RemoteProcessException) id bs)
|
|
| 497 | + -- Receive a request and queue it
|
|
| 498 | + Request uq mbi bs -> do
|
|
| 499 | + atomically $ case mbi of
|
|
| 500 | + Nothing ->
|
|
| 501 | + -- "Global" request
|
|
| 502 | + writeTQueue (requestsQueue am) (uq,bs)
|
|
| 503 | + Just ii -> do
|
|
| 504 | + -- "Specific mailbox" request
|
|
| 505 | + q <- getMailbox am ii
|
|
| 506 | + writeTQueue q (uq,bs)
|
|
| 507 | + Cancel uq -> cancelRequest am uq
|
|
| 463 | 508 | |
| 464 | 509 | replyToPendings e = atomically $ failPendingReplies p e
|
| 465 | 510 | |
| 466 | 511 | am = pipeAsyncMngr p
|
| 467 | 512 | |
| 513 | +cancelRequest :: AsyncManager -> CorrelationId -> IO ()
|
|
| 514 | +cancelRequest am uq = join . atomically $ do
|
|
| 515 | + cs <- readTVar (cancelStatus am)
|
|
| 516 | + case IntMap.lookup uq cs of
|
|
| 517 | + Nothing -> pure (pure ())
|
|
| 518 | + Just Fetched -> writeTVar (cancelStatus am) (IntMap.insert uq Canceling cs) >> pure (pure ())
|
|
| 519 | + Just (Active cancel) -> writeTVar (cancelStatus am) (IntMap.delete uq cs) >> pure cancel
|
|
| 520 | + Just Canceling -> pure (pure ())
|
|
| 521 | + |
|
| 522 | +-- | Returns @True@ when the handler should write its 'Reply'.
|
|
| 523 | +doneHandling :: AsyncManager -> CorrelationId -> STM Bool
|
|
| 524 | +doneHandling am uq = do
|
|
| 525 | + cs <- readTVar (cancelStatus am)
|
|
| 526 | + case IntMap.lookup uq cs of
|
|
| 527 | + Nothing -> pure False
|
|
| 528 | + Just s -> do
|
|
| 529 | + writeTVar (cancelStatus am) $ IntMap.delete uq cs
|
|
| 530 | + case s of
|
|
| 531 | + Canceling -> pure False
|
|
| 532 | + _ -> pure True
|
|
| 533 | + |
|
| 534 | +setActive :: AsyncManager -> CorrelationId -> IO () -> IO ()
|
|
| 535 | +setActive am uq cancel = join . atomically $ do
|
|
| 536 | + cs <- readTVar (cancelStatus am)
|
|
| 537 | + case IntMap.lookup uq cs of
|
|
| 538 | + Nothing -> pure (pure ()) -- reply already received.
|
|
| 539 | + Just Fetched -> writeTVar (cancelStatus am) (IntMap.insert uq (Active cancel) cs) >> pure (pure ())
|
|
| 540 | + Just (Active _) -> writeTVar (cancelStatus am) (IntMap.insert uq (Active cancel) cs) >> pure (pure ())
|
|
| 541 | + Just Canceling -> pure cancel
|
|
| 542 | + |
|
| 543 | +fetchedRequest :: AsyncManager -> CorrelationId -> STM ()
|
|
| 544 | +fetchedRequest am uq = do
|
|
| 545 | + cs <- readTVar (cancelStatus am)
|
|
| 546 | + case IntMap.lookup uq cs of
|
|
| 547 | + Nothing -> writeTVar (cancelStatus am) (IntMap.insert uq Fetched cs)
|
|
| 548 | + Just Fetched -> error "impossible"
|
|
| 549 | + Just (Active _) -> error "impossible"
|
|
| 550 | + Just Canceling -> error "impossible"
|
|
| 551 | + |
|
| 552 | + |
|
| 553 | + |
|
| 468 | 554 | {-
|
| 469 | 555 | Note [Exceptions during serialization]
|
| 470 | 556 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| ... | ... | @@ -553,7 +639,7 @@ runWriteReply (WriteReply r) = r |
| 553 | 639 | (|#>) (IO fs) s = fs s
|
| 554 | 640 | |
| 555 | 641 | (%>>=) :: (IO a) -> (a -> IO b) %1 -> IO b
|
| 556 | -(%>>=) = \ m f -> IO (\s0 ->
|
|
| 642 | +(%>>=) = \ m -> \ f -> IO (\s0 ->
|
|
| 557 | 643 | case m |#> s0 of -- unIO for linearity checking
|
| 558 | 644 | (# s1, x #) -> f x |#> s1)
|
| 559 | 645 | infixl 1 %>>=
|
| ... | ... | @@ -66,13 +66,13 @@ servWithCustom |
| 66 | 66 | -> IO ()
|
| 67 | 67 | servWithCustom verbose hook pipe restore customHandler = do
|
| 68 | 68 | ref <- newIORef False
|
| 69 | - withThreadManager $ \mngr ->
|
|
| 69 | + withThreadManager $ \mngr -> do
|
|
| 70 | 70 | loop ref mngr `finally` closePipe pipe
|
| 71 | 71 | where
|
| 72 | 72 | loop doneRef mngr = do
|
| 73 | 73 | when verbose $ trace "reading pipe..."
|
| 74 | 74 | |
| 75 | - handleRemoteCall pipe hook $ \wr (msg :: Message a) ->
|
|
| 75 | + handleRemoteCall pipe hook $ \wr setCancel (msg :: Message a) ->
|
|
| 76 | 76 | |
| 77 | 77 | discardCtrlC %>>= \_ ->
|
| 78 | 78 | when verbose (trace ("msg: " ++ (show msg))) %>>= \_ ->
|
| ... | ... | @@ -82,9 +82,11 @@ servWithCustom verbose hook pipe restore customHandler = do |
| 82 | 82 | Shutdown -> writeIORef doneRef True >>
|
| 83 | 83 | reply () %>>= writeReply wr
|
| 84 | 84 | RunTH ii st q ty loc -> spawnFinally mngr (runWriteReply wr)
|
| 85 | - (wrapRunTH ii (runTH pipe ii st q ty loc))
|
|
| 85 | + (restore $ wrapRunTH ii (runTH pipe ii st q ty loc))
|
|
| 86 | + (setCancel . void . forkIO . killThread)
|
|
| 86 | 87 | RunModFinalizers ii st qs -> spawnFinally mngr (runWriteReply wr)
|
| 87 | - (wrapRunTH ii (runModFinalizerRefs pipe ii st qs))
|
|
| 88 | + (restore $ wrapRunTH ii (runModFinalizerRefs pipe ii st qs))
|
|
| 89 | + (setCancel . void . forkIO . killThread)
|
|
| 88 | 90 | _other -> run msg >>= reply %>>= writeReply wr
|
| 89 | 91 | |
| 90 | 92 | isDone <- readIORef doneRef
|
| ... | ... | @@ -41,8 +41,8 @@ withThreadManager k = do |
| 41 | 41 | |
| 42 | 42 | -- | Linear and resource-safe because 'ThreadManager' is guaranteed to wait or
|
| 43 | 43 | -- cancel all threads.
|
| 44 | -spawnFinally :: ThreadManager -> (Either SomeException a -> IO ()) %1 -> IO a -> IO ()
|
|
| 45 | -spawnFinally ThreadManager{..} = unsafe_linear \fin -> \action -> void $
|
|
| 44 | +spawnFinally :: ThreadManager -> (Either SomeException a -> IO ()) %1 -> IO a -> (ThreadId -> IO b) -> IO b
|
|
| 45 | +spawnFinally ThreadManager{..} = unsafe_linear \fin -> \action -> \k ->
|
|
| 46 | 46 | forkFinally
|
| 47 | 47 | ( mask $ \unmask -> do
|
| 48 | 48 | tid <- myThreadId
|
| ... | ... | @@ -57,6 +57,7 @@ spawnFinally ThreadManager{..} = unsafe_linear \fin -> \action -> void $ |
| 57 | 57 | else throwIO ThreadKilled
|
| 58 | 58 | )
|
| 59 | 59 | ( \ea -> fin ea )
|
| 60 | + >>= k
|
|
| 60 | 61 | |
| 61 | 62 | unsafe_linear :: (a -> b) %1 -> (a %1 -> b)
|
| 62 | 63 | unsafe_linear = Unsafe.unsafeCoerce (Unsafe.unsafeCoerce :: (a -> b) -> (a %1 -> b)) |