| ... |
... |
@@ -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 %>>=
|