这感觉就像一个长镜头,但我写了一个连接到数据库的管道,获取服务器上的数据库列表,连接到每个数据库,然后对每个数据库执行查询(用户数),然后打印那些计数。不幸的是,这与我可以从我的真实例子中简化它一样多。我使用的是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的方式,我不知道如何构造它。
答案 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
我已经对此进行了测试,似乎有效。