在管道中捕获异常而不终止它

时间:2014-03-19 19:39:54

标签: exception haskell haskell-pipes

这感觉就像一个长镜头,但我写了一个连接到数据库的管道,获取服务器上的数据库列表,连接到每个数据库,然后对每个数据库执行查询(用户数),然后打印那些计数。不幸的是,这与我可以从我的真实例子中简化它一样多。我使用的是pipe-4.1.0,pipes-safe-2.0.2和mysql-simple-0.2.2.4。这是代码:

{-# LANGUAGE RankNTypes, OverloadedStrings #-}

import Pipes
import qualified Pipes.Safe as PS
import qualified Pipes.Prelude as P
import Database.MySQL.Simple

import qualified Data.Text as T

import Control.Monad.Catch as MC

import Control.Monad (forever)

import Database.MySQL.Simple.QueryParams
import Database.MySQL.Simple.QueryResults

data DBName = DBName T.Text deriving Show

-- connect to a database and use a table.
mydb :: T.Text -> ConnectInfo
mydb = undefined

-- Quirk of (mysql|postgresql)-simple libraries
unOnly (Only a) = a

queryProducer :: (MonadIO m, QueryParams params, QueryResults r) => Connection -> Query -> params -> Pipes.Producer' r m ()
queryProducer = undefined

myDBNames :: (PS.MonadSafe m, MonadIO m) => Producer DBName m ()
myDBNames = PS.bracket (liftIO $ connect $ mydb "sometable") (liftIO . close) $ \db ->
  queryProducer db "show databases" () >-> P.map (DBName . unOnly)

-- I realize this is inefficient, one step at a time.
connectToDB :: (PS.MonadSafe m, MonadIO m) => Pipe DBName Connection m ()
connectToDB = forever $ do
      (DBName dbname) <- await
      PS.bracket
        (liftIO . connect . mydb $ dbname)
        (liftIO . close)
        yield

userCount :: (PS.MonadCatch m, MonadIO m) => Pipe Connection Int m ()
userCount = forever $ do
  db <- await
  queryProducer db "select count(*) from user" () >-> P.map unOnly

main :: IO ()
main = PS.runSafeT $ runEffect $ myDBNames >-> P.tee P.print >-> connectToDB >-> userCount >-> P.print

这很好用。但是,假设在其中一个数据库中,用户表被命名为users而不是user,因此mysql-simple将在运行该查询时抛出异常。我希望内联捕获该错误,并为这些查询返回0个用户,但继续。我尝试过的事情:

(queryProducer db "select count(*) from user" () `PS.catchAll` (\e -> (liftIO $ putStrLn "failure") >> yield (Only 0))) >-> P.map unOnly 

这不起作用。有时会打印失败并产生0,只会立即终止异常。我想也许是因为我打破了查询生成器的异常,我应该再次调用它,所以我尝试了这个递归版本:

thequery db >-> P.map unOnly
where
  thequery db = queryProducer db "select count(*) from user" () `PS.catchAll` (\e -> (liftIO $ putStrLn "failure") >> yield (Only 0) >> thequery db)

但这也失败了。但是有时它实际上会执行几次查询,几次打印失败并在再次终止异常之前产生几个0。我真的很困惑为什么会这样。

根据异步库,异常应该发送到运行管道的线程中,因此它似乎不是一个线程问题。

如果我的queryProducer的实现很重要,它是在pipes-postgresql查询函数之后建模的,一般化为Producer'所以我可以将它嵌入到其他组合器中。在mysql-simple下面,在mysql库中有一个throw会抛出一个ConnectionError,如果你的sql没有意义,那就会一直渗透到这个函数中。

{-# LANGUAGE RankNTypes #-}

import qualified Control.Concurrent.Async as Async
import qualified Control.Concurrent.STM as STM
import qualified Database.MySQL.Simple as My
import Database.MySQL.Simple.QueryParams
import Database.MySQL.Simple.QueryResults
import qualified Pipes
import qualified Pipes.Concurrent as Pipes

--------------------------------------------------------------------------------
-- | Convert a query to a 'Producer' of rows.
--
-- For example,
--
-- > pg <- connectToMysql
-- > query pg "SELECT * FROM widgets WHERE ID = ?" (Only widgetId) >-> print
--
-- Will select all widgets for a given @widgetId@, and then print each row to
-- standard output.
queryProducer 
    :: (MonadIO m, QueryResults r, QueryParams params)
    => My.Connection -> My.Query -> params -> Pipes.Producer' r m ()
queryProducer c q p = do
    (o, i, seal) <- liftIO (Pipes.spawn' Pipes.Single)
    worker <- liftIO $ Async.async $ do
        My.fold c q p () (const $ void . STM.atomically . Pipes.send o)
        STM.atomically seal
    liftIO $ Async.link worker
    Pipes.fromInput i

我还尝试使用EitherT尝试捕获异常,因为这似乎是过去在管道中完成的方式。但管道教程中的文档在3到4之间消失了,让我想知道是否仍然推荐这种技术。不幸的是我无法让它工作,因为我使用queryProducer而不是单数await / yield的方式,我不知道如何构造它。

1 个答案:

答案 0 :(得分:2)

根据Gabe的评论,我修复了queryProducer函数,确保在链接函数被触发之前不能进行查询。

query :: (MonadIO m, QueryResults r, QueryParams params) => My.Connection -> My.Query -> params -> Pipes.Producer' r m ()
query c q p = do
    (o, i, seal) <- liftIO (Pipes.spawn' Pipes.Single)
    mvar <- liftIO $ newEmptyMVar
    worker <- liftIO $ Async.async $ do
        takeMVar mvar
        My.fold c q p () (const $ void . STM.atomically . Pipes.send o)
        STM.atomically seal
    liftIO $ Async.link worker
    liftIO $ putMVar mvar ()
    Pipes.fromInput i

我已经对此进行了测试,似乎有效。