Rodrigo Mesquita pushed to branch wip/romes/async-rpc at Glasgow Haskell Compiler / GHC

Commits:

3 changed files:

Changes:

  • libraries/ghci/GHCi/Message.hs
    ... ... @@ -27,7 +27,7 @@ module GHCi.Message
    27 27
       , Pipe(..), mkPipeFromHandles, mkPipeFromContinuations, closePipe
    
    28 28
       , remoteCall, remoteTHCall
    
    29 29
       , handleRemoteCall, handleRemoteTHCall
    
    30
    -  , RemoteProcessException(..)
    
    30
    +  , RemoteProcessException(..), PipeClosedException(..)
    
    31 31
     
    
    32 32
       , BreakModule
    
    33 33
       , BreakUnitId
    
    ... ... @@ -89,7 +89,6 @@ import qualified GHC.Boot.TH.Syntax as TH
    89 89
     import qualified GHC.Boot.TH.Monad         as TH
    
    90 90
     import System.Exit
    
    91 91
     import System.IO
    
    92
    -import System.IO.Error
    
    93 92
     import Control.Monad
    
    94 93
     import Control.Concurrent.STM
    
    95 94
     import Data.Bifunctor
    
    ... ... @@ -722,38 +721,54 @@ data Pipe = Pipe
    722 721
       , pipeLeftovers   :: !(IORef (Maybe ByteString))
    
    723 722
       , pipeLock        :: !(MVar ()) -- ^ Lock to prevent concurrent writes to the stream
    
    724 723
       , pipeAsyncMngr   :: !AsyncManager
    
    725
    -  , pipeCleanupRW   :: !(IO ())
    
    726
    -    -- ^ Clean up the read/write resources, e.g. closes the handles if 'mkPipeFromHandles'
    
    724
    +  , pipeIsClosed    :: !(TVar Bool)
    
    725
    +    -- ^ After the pipe is closed, requests can no longer be received nor sent
    
    727 726
       }
    
    728 727
     
    
    729 728
     -- | Make a 'Pipe' from a 'Handle' to read and a 'Handle' to write.
    
    730
    --- Must call 'closePipe' at the end to clean up and close the handles.
    
    729
    +-- Must call 'closePipe' at the end to clean up.
    
    731 730
     mkPipeFromHandles :: Handle -> Handle -> IO Pipe
    
    732 731
     mkPipeFromHandles pipeRead pipeWrite = do
    
    733 732
       let getSome = B.hGetSome pipeRead (32*1024)
    
    734 733
           putAll b = do
    
    735 734
             B.hPutBuilder pipeWrite b
    
    736 735
             hFlush pipeWrite
    
    737
    -  mkPipeFromContinuations getSome putAll (hClose pipeRead >> hClose pipeWrite)
    
    736
    +  mkPipeFromContinuations getSome putAll
    
    738 737
     
    
    739
    --- | Make a 'Pipe' from a reader function and a writer function, plus an action
    
    740
    --- to clean up the reader and writer resources. The pipe should be closed with
    
    741
    --- 'closePipe', which will call this clean up action too.
    
    742
    -mkPipeFromContinuations :: IO ByteString -> (B.Builder -> IO ()) -> IO () -> IO Pipe
    
    743
    -mkPipeFromContinuations getSome putAll pipeCleanupRW = mdo
    
    738
    +-- | Make a 'Pipe' from a reader function and a writer function.
    
    739
    +-- The pipe should be closed with 'closePipe'.
    
    740
    +mkPipeFromContinuations :: IO ByteString -> (B.Builder -> IO ()) -> IO Pipe
    
    741
    +mkPipeFromContinuations getSome putAll = mdo
    
    744 742
       pipeLeftovers   <- newIORef Nothing
    
    745 743
       pipeLock        <- newMVar ()
    
    744
    +  pipeIsClosed    <- newTVarIO False
    
    746 745
       mdo
    
    747 746
         pipeAsyncMngr <- newAsyncManager p
    
    748 747
         let p = Pipe { .. }
    
    749 748
         pure p
    
    750 749
     
    
    751
    --- | Cleanly close this pipe, terminating the async manager's drain-read-end thread.
    
    752
    --- This doesn't close the handles
    
    750
    +-- | Cleanly close our end of the Pipe, terminating the async manager's
    
    751
    +-- drain-read-end thread. Calling 'closePipe' on an already closed pipe is a no-op.
    
    752
    +--
    
    753
    +-- If the pipe was created using 'mkPipeFromHandles', the handles must only by
    
    754
    +-- closed after calling 'closePipe'.
    
    755
    +--
    
    756
    +-- The requests we've sent that are pending are all canceled with
    
    757
    +-- 'PipeClosedException' by ourselves.
    
    758
    +--
    
    759
    +-- The remote process does not necessarily detect that this end of the pipe was
    
    760
    +-- closed, but it must handle cleanly and clean up after exceptions caused from
    
    761
    +-- it such as a handle being closed.
    
    753 762
     closePipe :: Pipe -> IO ()
    
    754 763
     closePipe Pipe{..} = do
    
    755
    -  killThread (asyncDrainTid pipeAsyncMngr) -- releases handle lock taken by reading
    
    756
    -  pipeCleanupRW -- hClose won't deadlock now that the drainThread is killed
    
    764
    +
    
    765
    +  isAlreadyClosed <- atomically $ swapTVar pipeIsClosed True
    
    766
    +
    
    767
    +  unless isAlreadyClosed $ do
    
    768
    +
    
    769
    +    -- releases handle lock taken by reading, and cleans up by replying to every
    
    770
    +    -- pending request with 'PipeClosedException'.
    
    771
    +    throwTo (asyncDrainTid pipeAsyncMngr) PipeClosedException
    
    757 772
     
    
    758 773
     -- | Send a 'Message' request to and wait for a reply from the interpreter
    
    759 774
     -- (blocks on the calling side, doesn't necessarily block the interpreter)
    
    ... ... @@ -798,20 +813,28 @@ remoteAsyncReq pipe msg = do
    798 813
     
    
    799 814
       -- Mark pending request
    
    800 815
       uq <- freshReqId (pipeAsyncMngr pipe)
    
    801
    -  wv <- newEmptyMVar
    
    802
    -  atomicModifyIORef' pendingReplies $
    
    803
    -    \m -> (IntMap.insert uq wv m, ())
    
    816
    +  wv <- newEmptyTMVarIO
    
    817
    +
    
    818
    +  isClosed <- atomically $ do
    
    819
    +    isClosed <- readTVar (pipeIsClosed pipe)
    
    820
    +    if isClosed
    
    821
    +      then pure True
    
    822
    +      else do
    
    823
    +        modifyTVar pendingReplies (IntMap.insert uq wv)
    
    824
    +        pure False
    
    825
    +
    
    826
    +  when isClosed $ do
    
    827
    +    throwIO PipeClosedException
    
    804 828
     
    
    805 829
       -- Write request
    
    806 830
       writePipe pipe (Request uq (runPut msg))
    
    807 831
         `onException`
    
    808 832
           -- the req wasn't done, or no one will be reading the reply:
    
    809 833
           -- in either case, drop it from the pending map.
    
    810
    -      atomicModifyIORef' pendingReplies
    
    811
    -        (\m -> (IntMap.delete uq m, ()))
    
    834
    +      atomically (modifyTVar pendingReplies (IntMap.delete uq))
    
    812 835
     
    
    813 836
       -- Block waiting for reply
    
    814
    -  takeMVar wv
    
    837
    +  atomically (takeTMVar wv)
    
    815 838
     
    
    816 839
     -- | Receive a single asynchronous request and send a matching reply
    
    817 840
     -- asynchronously (the handler may fork a computation that eventually replies
    
    ... ... @@ -826,32 +849,40 @@ handleAsyncReq pipe getit k = do
    826 849
       ack <- newEmptyMVar
    
    827 850
     
    
    828 851
       -- Block waiting for request
    
    829
    -  (uq, bs) <- atomically (readTQueue (requestsQueue (pipeAsyncMngr pipe)))
    
    830
    -
    
    831
    -  let reply p = do
    
    832
    -        writePipe pipe (Reply uq (bimap toSerializableException runPut p))
    
    833
    -        putMVar ack ()
    
    834
    -
    
    835
    -  -- Run the continuation which must reply exactly once with 'writeReply'
    
    836
    -  case runGetOrFail getit bs of
    
    837
    -    Left (_, _, err) -> do
    
    838
    -      let exc = DecodeAsyncReqException err
    
    839
    -      reply (Left (toException exc))
    
    840
    -      throwIO exc
    
    841
    -    Right (_, _, it) ->
    
    842
    -      k (WriteReply reply) it
    
    843
    -
    
    844
    -        -- If there's an exception before the reply is sent, reply with an error instead.
    
    845
    -        `catch` \(e :: SomeException) -> do
    
    846
    -
    
    847
    -            tryTakeMVar ack >>= \case
    
    848
    -              Just () ->
    
    849
    -                -- Already replied
    
    850
    -                throwIO e
    
    851
    -              Nothing -> do
    
    852
    -                -- Reply with an error
    
    853
    -                reply (Left e)
    
    854
    -                throwIO e
    
    852
    +  mbreq <- atomically $ do
    
    853
    +    isClosed <- readTVar (pipeIsClosed pipe)
    
    854
    +    isEmpty  <- isEmptyTQueue (requestsQueue (pipeAsyncMngr pipe))
    
    855
    +    if isClosed && isEmpty
    
    856
    +      then return Nothing
    
    857
    +      else Just <$> readTQueue (requestsQueue (pipeAsyncMngr pipe))
    
    858
    +
    
    859
    +  case mbreq of
    
    860
    +    Nothing -> throwIO PipeClosedException
    
    861
    +    Just (uq, bs) -> do
    
    862
    +      let reply p = do
    
    863
    +            writePipe pipe (Reply uq (bimap toSerializableException runPut p))
    
    864
    +            putMVar ack ()
    
    865
    +
    
    866
    +      -- Run the continuation which must reply exactly once with 'writeReply'
    
    867
    +      case runGetOrFail getit bs of
    
    868
    +        Left (_, _, err) -> do
    
    869
    +          let exc = DecodeAsyncReqException err
    
    870
    +          reply (Left (toException exc))
    
    871
    +          throwIO exc
    
    872
    +        Right (_, _, it) ->
    
    873
    +          k (WriteReply reply) it
    
    874
    +
    
    875
    +            -- If there's an exception before the reply is sent, reply with an error instead.
    
    876
    +            `catch` \(e :: SomeException) -> do
    
    877
    +
    
    878
    +                tryTakeMVar ack >>= \case
    
    879
    +                  Just () ->
    
    880
    +                    -- Already replied
    
    881
    +                    throwIO e
    
    882
    +                  Nothing -> do
    
    883
    +                    -- Reply with an error
    
    884
    +                    reply (Left e)
    
    885
    +                    throwIO e
    
    855 886
     
    
    856 887
     -- | An exception that can be thrown on the host process representing an
    
    857 888
     -- expection that happened on the remote one.
    
    ... ... @@ -862,12 +893,16 @@ instance Exception RemoteProcessException
    862 893
     newtype DecodeAsyncReqException = DecodeAsyncReqException String deriving Show
    
    863 894
     instance Exception DecodeAsyncReqException
    
    864 895
     
    
    896
    +-- | We've closed the pipe on our end and we terminate all pending requests with this.
    
    897
    +data PipeClosedException = PipeClosedException deriving Show
    
    898
    +instance Exception PipeClosedException
    
    899
    +
    
    865 900
     -- ** Async matching "internals" -----------------------------------------------
    
    866 901
     
    
    867 902
     data AsyncManager = AsyncManager
    
    868
    -  { pendingReply :: !(IORef (IntMap (MVar (Either SomeException LB.ByteString))))
    
    903
    +  { pendingReply :: !(TVar (IntMap (TMVar (Either SomeException LB.ByteString))))
    
    869 904
         -- ^ When a request is sent, keep track that we're expecting a reply. The
    
    870
    -    -- MVar is signaled when the reply arrives. The exception can originate
    
    905
    +    -- TMVar is signaled when the reply arrives. The exception can originate
    
    871 906
         -- from the host process, or from the remote one (with 'RemoteProcessException').
    
    872 907
       , requestsQueue :: !(TQueue (CorrelationId, LB.ByteString))
    
    873 908
         -- ^ A queue of pending received requests. When a request is received,
    
    ... ... @@ -904,45 +939,58 @@ instance Binary IddMsg where
    904 939
     newAsyncManager :: Pipe -> IO AsyncManager
    
    905 940
     newAsyncManager pipe = do
    
    906 941
       asyncNextReq <- newIORef 0
    
    907
    -  pendingReply  <- newIORef IntMap.empty
    
    942
    +  pendingReply  <- newTVarIO IntMap.empty
    
    908 943
       requestsQueue  <- newTQueueIO
    
    909 944
     
    
    910
    -  mdo
    
    911
    -    -- Fork thread to drain read end.
    
    912
    -    -- (cleaned up on 'closePipe')
    
    913
    -    asyncDrainTid  <- forkIO (drainPipe pipe am)
    
    945
    +  -- Fork thread to drain read end.
    
    946
    +  -- (cleaned up on 'closePipe')
    
    947
    +  asyncDrainTid  <- forkIO (drainPipe pipe)
    
    914 948
     
    
    915
    -    let am = AsyncManager{..}
    
    916
    -    pure am
    
    949
    +  let am = AsyncManager{..}
    
    950
    +  pure am
    
    917 951
     
    
    918 952
     -- | Drains the read end of the Pipe and match 'Reply'es against pending 'Request's.
    
    919 953
     --
    
    920 954
     -- In pratice, we'll have both a thread draining the read end of the host's
    
    921 955
     -- 'Pipe' and a thread draining the read end of the interpreter's 'Pipe', since
    
    922
    --- both processes construct a 'Pipe' and send and receive messages:
    
    923
    -drainPipe :: Pipe -> AsyncManager -> IO ()
    
    924
    -drainPipe p am = flip catch cancelPending $ forever $ do
    
    925
    -  readPipe p >>= \case
    
    926
    -    -- Receive a reply to a pending notification
    
    927
    -    Reply uq bs -> do
    
    928
    -      mwv <- atomicModifyIORef' (pendingReply am) $ \m -> do
    
    929
    -        case IntMap.lookup uq m of
    
    930
    -          Nothing -> (m, Nothing)
    
    931
    -          Just wv -> (IntMap.delete uq m, Just wv)
    
    932
    -      case mwv of
    
    933
    -        Just wv ->
    
    934
    -          putMVar wv (bimap (toException . RemoteProcessException) id bs) -- signal matching request
    
    935
    -        Nothing ->
    
    936
    -          -- the req was dropped from pending bc there was an exception, and no
    
    937
    -          -- one's listening any more. the reply is ignored
    
    938
    -          pure ()
    
    939
    -    -- Receive a request and queue it
    
    940
    -    Request uq bs ->
    
    941
    -      atomically $ writeTQueue (requestsQueue am) (uq,bs)
    
    942
    -  where
    
    943
    -    cancelPending (e::SomeException) = do
    
    944
    -      pendings <- readIORef (pendingReply am)
    
    945
    -      mapM_ (\p -> putMVar p (Left e {-host exception-})) pendings
    
    956
    +-- both processes construct a 'Pipe' and send and receive messages
    
    957
    +drainPipe :: Pipe -> IO ()
    
    958
    +drainPipe p = catch loop replyToPendings where
    
    959
    +  loop = do
    
    960
    +    readPipe p >>= \case
    
    961
    +      -- Receive a reply to a pending notification
    
    962
    +      Just (Reply uq bs) -> do
    
    963
    +        mwv <- atomically $ do
    
    964
    +          m <- readTVar (pendingReply am)
    
    965
    +          case IntMap.lookup uq m of
    
    966
    +            Nothing -> pure Nothing
    
    967
    +            Just wv -> do
    
    968
    +              writeTVar (pendingReply am) (IntMap.delete uq m)
    
    969
    +              pure (Just wv)
    
    970
    +        case mwv of
    
    971
    +          Just wv -> atomically $
    
    972
    +            putTMVar wv (bimap (toException . RemoteProcessException) id bs) -- signal matching request
    
    973
    +          Nothing ->
    
    974
    +            -- the req was dropped from pending bc there was an exception, and no
    
    975
    +            -- one's listening any more. the reply is ignored
    
    976
    +            pure ()
    
    977
    +        loop
    
    978
    +      -- Receive a request and queue it
    
    979
    +      Just (Request uq bs) -> do
    
    980
    +        atomically $ writeTQueue (requestsQueue am) (uq,bs)
    
    981
    +        loop
    
    982
    +      -- EOF on read handle
    
    983
    +      Nothing -> do
    
    984
    +        -- clean up pipe then exit loop
    
    985
    +        replyToPendings (toException PipeClosedException)
    
    986
    +        pure ()
    
    987
    +
    
    988
    +  replyToPendings e = atomically $ do
    
    989
    +    writeTVar (pipeIsClosed p) True
    
    990
    +    pendings <- swapTVar (pendingReply am) IntMap.empty -- take pending
    
    991
    +    mapM_ (\p -> putTMVar p (Left e {-host exception-})) pendings
    
    992
    +
    
    993
    +  am = pipeAsyncMngr p
    
    946 994
     
    
    947 995
     freshReqId :: AsyncManager -> IO CorrelationId
    
    948 996
     freshReqId AsyncManager{asyncNextReq} =
    
    ... ... @@ -960,16 +1008,17 @@ writePipe p@Pipe{..} = withLock p . putAll . execPut . put
    960 1008
     -- | Only the 'drainPipe' thread should read from the pipe directly. Other
    
    961 1009
     -- functions should wait for the matching reply to be delivered asynchronously.
    
    962 1010
     -- (see 'withAsyncReq'). It doesn't lock to read bc it assumes single reader is 'drainPipe'.
    
    963
    -readPipe :: Pipe -> IO IddMsg
    
    1011
    +--
    
    1012
    +-- Returns @Nothing@ on EOF.
    
    1013
    +readPipe :: Pipe -> IO (Maybe IddMsg)
    
    964 1014
     readPipe Pipe{..} = do
    
    965 1015
       leftovers <- readIORef pipeLeftovers
    
    966 1016
       m <- getBin getSome get leftovers
    
    967 1017
       case m of
    
    968
    -    Nothing -> throw $
    
    969
    -      mkIOError eofErrorType "GHCi.Message.readPipe" Nothing Nothing
    
    1018
    +    Nothing -> return Nothing
    
    970 1019
         Just (result, new_leftovers) -> do
    
    971 1020
           writeIORef pipeLeftovers new_leftovers
    
    972
    -      return result
    
    1021
    +      return (Just result)
    
    973 1022
     
    
    974 1023
     getBin
    
    975 1024
       :: (IO ByteString) -> Get a -> Maybe ByteString
    

  • libraries/ghci/GHCi/Server.hs
    ... ... @@ -79,7 +79,7 @@ servWithCustom verbose hook pipe restore customHandler = newIORef False >>= loop
    79 79
             _other -> run msg >>= reply
    
    80 80
     
    
    81 81
         isDone <- readIORef doneRef
    
    82
    -    if isDone then return ()
    
    82
    +    if isDone then closePipe pipe
    
    83 83
                   else loop doneRef
    
    84 84
     
    
    85 85
       handleCustom tag payload = do
    
    ... ... @@ -184,7 +184,7 @@ defaultServerWithCustom customHandler = do
    184 184
     #if defined(wasm32_HOST_ARCH)
    
    185 185
       -- See Note [wasm ghci signal handlers] for details
    
    186 186
       installSignalHandlers $ js_register_signal_handler cb_sig
    
    187
    -  pipe <- mkPipeFromContinuations (recv_buf cb_recv) (send_buf cb_send) (pure ())
    
    187
    +  pipe <- mkPipeFromContinuations (recv_buf cb_recv) (send_buf cb_send)
    
    188 188
     #else
    
    189 189
       when verbose $
    
    190 190
         printf "GHC iserv starting (in: %s; out: %s)\n" (show inh) (show outh)
    

  • testsuite/tests/ghci/custom-external-interpreter-commands/Main.hs
    ... ... @@ -171,12 +171,14 @@ withServer serverExe action = do
    171 171
         , std_out = Inherit
    
    172 172
         , std_err = Inherit
    
    173 173
         }
    
    174
    -  bracket (mkHandles ghcRead ghcWrite >>= uncurry mkPipeFromHandles)
    
    175
    -          (\pipe -> do
    
    174
    +  bracket (mkHandles ghcRead ghcWrite >>= \hds -> (hds,) <$> uncurry mkPipeFromHandles hds)
    
    175
    +          (\((hFromServer, hToServer), pipe) -> do
    
    176 176
                  closePipe pipe
    
    177
    +             hClose hFromServer
    
    178
    +             hClose hToServer
    
    177 179
                  terminateProcess ph
    
    178 180
                  void (waitForProcess ph))
    
    179
    -          (\pipe -> action pipe)
    
    181
    +          (\(_, pipe) -> action pipe)
    
    180 182
       where
    
    181 183
         mkHandles r w = do
    
    182 184
           hR <- fdToHandle r