conduit icon indicating copy to clipboard operation
conduit copied to clipboard

Question: how to differentiate between normal and abnormal termination?

Open edsko opened this issue 8 years ago • 2 comments

The main primitive for exception handling in conduit (I think) is

bracketP :: MonadResource m  
         => IO a    
         -> (a -> IO ())    
         -> (a -> ConduitM i o m r) 
         -> ConduitM i o m r     

but this function does not allow us to differentiate between normal and abnormal termination, which sometimes affects the way we want to cleanup a resource. For example, in a conduit that wraps a database transaction, we might want to commit on regular termination but abort when an exception arises. Ideally we'd have something like

bracketPE :: MonadResource m     
          => IO a   
          -> (Maybe Exception -> a -> IO ())    
          -> (a -> ConduitM i o m r)    
          -> ConduitM i o m r    

or something along those lines, so that the cleanup handler can distinguish between these two cases. However, I don't know how to implement this, especially since MonadResource does not require MonadMask. I ended up implementing

type IsReleased = Bool

bracketPE :: forall i o m r a. MonadResource m
          => IO a         -- ^ Allocation
          -> (a -> IO ()) -- ^ Finalize (cleanup on normal termination)
          -> (a -> IO ()) -- ^ Abort (cleanup on abnormal termination)
          -> (a -> ConduitM i o m r) -- ^ Body
          -> ConduitM i o m r
bracketPE alloc finalize abort k =
    bracketP acq rel body
  where
    acq :: IO (a, IORef IsReleased)
    acq = (,) <$> alloc <*> newIORef False

    rel :: (a, IORef IsReleased) -> IO ()
    rel (a, ref) = do
        isReleased <- readIORef ref
        unless isReleased $ abort a

    body :: (a, IORef IsReleased) -> ConduitM i o m r
    body (a, ref) = do
        r <- k a
        liftIO $ uninterruptibleMask_ $ finalize a >> writeIORef ref True
        return r

which is almost the same (it doesn't provide the actual exception, but it's good enough for my current purposes). But I don't know if there is a more idiomatic way of doing this (not tto mention that it's hard to get this kind of code right, so I'm not 100% convinced there's no flaw in my above implementation).

For completeness, here's how I would write it if I had a MonadMask instance:

bracketE :: MonadMask m
         => m a         -- ^ Allocation
         -> (a -> m ()) -- ^ Finanlize (cleanup on normal termination)
         -> (a -> m ()) -- ^ Abort (cleanup on abnormal termination)
         -> (a -> m r)  -- ^ Body
         -> m r
bracketE alloc finalize abort k =
  mask $ \restore -> do
    a <- alloc
    r <- restore (k a) `onException` abort a
    _ <- finalize a
    return r

edsko avatar Aug 27 '16 05:08 edsko

Actually, this version is somewhat more general, and has the benefit of making it clear in the types what's going on:

-- | Conduit equivalent of 'bracketE'
bracketPE :: forall i o m r r' a. MonadResource m
          => IO a              -- ^ Allocation
          -> (a -> r -> IO r') -- ^ Finalize (cleanup on normal termination)
          -> (a -> IO ())      -- ^ Abort (cleanup on abnormal termination)
          -> (a -> ConduitM i o m r) -- ^ Body
          -> ConduitM i o m r'
bracketPE alloc finalize abort k =
    bracketP acq rel body
  where
    acq :: IO (a, IORef IsReleased)
    acq = (,) <$> alloc <*> newIORef False

    rel :: (a, IORef IsReleased) -> IO ()
    rel (a, ref) = do
        isReleased <- readIORef ref
        unless isReleased $ abort a

    body :: (a, IORef IsReleased) -> ConduitM i o m r'
    body (a, ref) = do
        r <- k a
        liftIO $ uninterruptibleMask_ $ do
          r' <- finalize a r
          writeIORef ref True
          return r'

edsko avatar Aug 27 '16 05:08 edsko

You're right that there's no function for this now. I based the API on the Control.Exception bracket API which doesn't provide this either. I'd be open to a PR to add a more generalized function.

On Sat, Aug 27, 2016, 8:59 AM Edsko de Vries [email protected] wrote:

Actually, this version is somewhat more general, and has the benefit of making it clear in the types what's going on:

-- | Conduit equivalent of 'bracketE'bracketPE :: forall i o m r r' a. MonadResource m

      => IO a              -- ^ Allocation


      -> (a -> r -> IO r') -- ^ Finalize (cleanup on normal termination)


      -> (a -> IO ())      -- ^ Abort (cleanup on abnormal termination)
      -> (a -> ConduitM i o m r) -- ^ Body
      -> ConduitM i o m r'

bracketPE alloc finalize abort k = bracketP acq rel body where acq :: IO (a, IORef IsReleased) acq = (,) <$> alloc <*> newIORef False

rel :: (a, IORef IsReleased) -> IO ()
rel (a, ref) = do
    isReleased <- readIORef ref
    unless isReleased $ abort a

body :: (a, IORef IsReleased) -> ConduitM i o m r'
body (a, ref) = do


    r <- k a
    liftIO $ uninterruptibleMask_ $ do
      r' <- finalize a r
      writeIORef ref True
      return r'

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/snoyberg/conduit/issues/278#issuecomment-242898637, or mute the thread https://github.com/notifications/unsubscribe-auth/AADBB3_T3U3xQY6OPruztya5hxHzf6Zrks5qj9IvgaJpZM4JunZ1 .

snoyberg avatar Aug 28 '16 04:08 snoyberg