This feels like kind of a long shot, but I wrote a pipe to connect to a database, get a list of databases on the server, connect to each one, then perform a query on each one (a user count), then print those counts. Unfortunately this is about as much as I can simplify it from my real example. I'm using pipes-4.1.0, pipes-safe-2.0.2, and mysql-simple-0.2.2.4. Here is the code:
{-# 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
This works fine. However, let's say in one of those databases, the user table is named users instead of user, therefore mysql-simple will throw an exception when that query is run. I want to catch that error inline, and just return 0 users for those queries, but keep going. Things I've tried:
(queryProducer db "select count(*) from user" () `PS.catchAll` (\e -> (liftIO $ putStrLn "failure") >> yield (Only 0))) >-> P.map unOnly
This doesn't work. Sometimes it will print failure and yield a 0, only to immediately terminate on exception. I thought maybe it is because I broke out of queryProducer with the exception, and I should call it again so I tried this recursive version:
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)
But this also fails. However sometimes it will actually perform a several queries, printing out failure a few times and yielding a few 0's before terminating with an exception again. I'm really confused about why this is happening.
According to the async library, exceptions should be send up into the thread that the pipe is running in, so it doesn't seem like it could be a threading problem.
In case the implementation of my queryProducer matters, it is modeled after the pipes-postgresql query function, generalized to Producer' so I can embed it in other combinators. Below mysql-simple, in the mysql library there is a throw which throws a ConnectionError if your sql doesn't make sense, that percolates all the way up through this function.
{-# 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
I also attempted to use EitherT to try and catch exceptions since that seems to be the way it was done in the past in pipes. But the documentation for that in pipes' tutorial disappeared between 3 and 4 making me wonder if that technique is still recommended or not. Unfortunately I could not get it to work because the way I am using queryProducer instead of singular await/yields, I'm not sure how to structure it.
Based on Gabe's comment, I fixed my queryProducer function by making sure the query cannot happen until the link function has fired.
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
I've tested this and it seems to work.
Related
I have written an application in Haskell that does the following:
Recursively list a directory,
Parse the JSON files from the directory list,
Look for matching key-value pairs, and
Return filenames where matches have been found.
My first version of this application was the simplest, naive version I could write, but I noticed that space usage seemed to increase monotonically.
As a result, I switched to conduit, and now my primary functionality looks like this:
conduitFilesFilter :: ProjectFilter -> Path Abs Dir -> IO [Path Abs File]
conduitFilesFilter projFilter dirname' = do
(_, allFiles) <- listDirRecur dirname'
C.runConduit $
C.yieldMany allFiles
.| C.filterMC (filterMatchingFile projFilter)
.| C.sinkList
Now my application has bounded memory usage but it's still quite slow. Out of this, I have two questions.
1)
I used stack new to generate the skeleton to create this application and it by default uses the ghc options -threaded -rtsopts -with-rtsopts=-N.
The surprising thing (to me) is that the application uses all processors available to it (about 40 in the target machine) when I actually go to run it. However, I didn't write any part of the application to be run in parallel (I considered it, actually).
What's running in parallel?
2)
Additionally, most of the JSON files are really large (10mb) and there are probably 500k of them to be traversed. This means my program is very slow as a result of all the Aeson-decoding. My idea was to run my filterMatchingFile part in parallel, but looking at the stm-conduit library, I can't see an obvious way to run this middle action in parallel across a handful of processors.
Can anyone suggest a way to smartly parallelize my function above using stm-conduit or some other means?
Edit
I realized that I could break up my readFile -> decodeObject -> runFilterFunction into separate parts of the conduit and then I could use stm-conduit there with a bounded channel. Maybe I'll give it a shot...
I ran my application with +RTS -s (I reconfigured it to -N4) and I see the following:
115,961,554,600 bytes allocated in the heap
35,870,639,768 bytes copied during GC
56,467,720 bytes maximum residency (681 sample(s))
1,283,008 bytes maximum slop
145 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 108716 colls, 108716 par 76.915s 20.571s 0.0002s 0.0266s
Gen 1 681 colls, 680 par 0.530s 0.147s 0.0002s 0.0009s
Parallel GC work balance: 14.99% (serial 0%, perfect 100%)
TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4)
SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.001s ( 0.007s elapsed)
MUT time 34.813s ( 42.938s elapsed)
GC time 77.445s ( 20.718s elapsed)
EXIT time 0.000s ( 0.010s elapsed)
Total time 112.260s ( 63.672s elapsed)
Alloc rate 3,330,960,996 bytes per MUT second
Productivity 31.0% of total user, 67.5% of total elapsed
gc_alloc_block_sync: 188614
whitehole_spin: 0
gen[0].sync: 33
gen[1].sync: 811204
From your program description, there is no reason for it to have increasing memory usage. I think it was accidental memory leak from missed lazy computation. This can be easily detected by heap profiling: https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/profiling.html#hp2ps-rendering-heap-profiles-to-postscript. Other possible reason is that runtime does not release all memory back to OS. Until some threshold, it will keep holding to memory proportional to the largest file processed. This may look as a memory leak if tracked through process RSS size.
-A32m option increases nursery size. It lets your program allocate more memory before garbage collection is triggered. Stats shows that very little memory is retained during GC, so less often it happens, more time program spends doing actual work.
Prompted by Michael Snoyman on Haskell Cafe, who pointed out that my first version was not truly taking advantage of Conduit's streaming capabilities, I rewrote my Conduit version of the application (without using stm-conduit). This was a large improvement: my first Conduit version was operating over all data and I didn't realize this.
I also increased the nursery size and this increased my productivity by doing garbage collection less frequently.
My revised function ended up looking like this:
module Search where
import Conduit ((.|))
import qualified Conduit as C
import Control.Monad
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Resource (MonadResource)
import qualified Data.ByteString as B
import Data.List (isPrefixOf)
import Data.Maybe (fromJust, isJust)
import System.Path.NameManip (guess_dotdot, absolute_path)
import System.FilePath (addTrailingPathSeparator, normalise)
import System.Directory (getHomeDirectory)
import Filters
sourceFilesFilter :: (MonadResource m, MonadIO m) => ProjectFilter -> FilePath -> C.ConduitM () String m ()
sourceFilesFilter projFilter dirname' =
C.sourceDirectoryDeep False dirname'
.| parseProject projFilter
parseProject :: (MonadResource m, MonadIO m) => ProjectFilter -> C.ConduitM FilePath String m ()
parseProject (ProjectFilter filterFunc) = do
C.awaitForever go
where
go path' = do
bytes <- liftIO $ B.readFile path'
let isProj = validProject bytes
when (isJust isProj) $ do
let proj' = fromJust isProj
when (filterFunc proj') $ C.yield path'
My main just runs the conduit and prints those that pass the filter:
mainStreamingConduit :: IO ()
mainStreamingConduit = do
options <- getRecord "Search JSON Files"
let filterFunc = makeProjectFilter options
searchDir <- absolutize (searchPath options)
itExists <- doesDirectoryExist searchDir
case itExists of
False -> putStrLn "Search Directory does not exist" >> exitWith (ExitFailure 1)
True -> C.runConduitRes $ sourceFilesFilter filterFunc searchDir .| C.mapM_ (liftIO . putStrLn)
I run it like this (without the stats, typically):
stack exec search-json -- --searchPath $FILES --name NAME +RTS -s -A32m -n4m
Without increasing nursery size, I get a productivity around 30%. With the above, however, it looks like this:
72,308,248,744 bytes allocated in the heap
733,911,752 bytes copied during GC
7,410,520 bytes maximum residency (8 sample(s))
863,480 bytes maximum slop
187 MB total memory in use (27 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 580 colls, 580 par 2.731s 0.772s 0.0013s 0.0105s
Gen 1 8 colls, 7 par 0.163s 0.044s 0.0055s 0.0109s
Parallel GC work balance: 35.12% (serial 0%, perfect 100%)
TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4)
SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.001s ( 0.006s elapsed)
MUT time 26.155s ( 31.602s elapsed)
GC time 2.894s ( 0.816s elapsed)
EXIT time -0.003s ( 0.008s elapsed)
Total time 29.048s ( 32.432s elapsed)
Alloc rate 2,764,643,665 bytes per MUT second
Productivity 90.0% of total user, 97.5% of total elapsed
gc_alloc_block_sync: 3494
whitehole_spin: 0
gen[0].sync: 15527
gen[1].sync: 177
I'd still like to figure out how to parallelize the filterProj . parseJson . readFile part, but for now I'm satisfied with this.
I figured out how to run this application using stm-conduit with some help from the Haskell wiki on parallelism and a Stack Overflow answer that talks about waiting for threads to end before main exits.
The way it works is that I create a channel that holds all of the filenames to be operated on. Then, I fork a bunch of threads that each runs a Conduit with the filepath-channel as a Source. I track all of the child threads and wait for them to finish.
Maybe this solution will be useful for someone else?
Not all of my lower-level filter functions are present, but the gist of it is that I have a Conduit that tests some JSON. If it passes, then it yields the FilePath.
Here's my main in entirety:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Conduit ((.|))
import qualified Conduit as C
import Control.Concurrent
import Control.Monad (forM_)
import Control.Monad.IO.Class (liftIO)
import Control.Concurrent.STM
import Control.Monad.Trans.Resource (register)
import qualified Data.Conduit.TMChan as STMChan
import Data.Maybe (isJust, fromJust)
import qualified Data.Text as T
import Options.Generic
import System.Directory (doesDirectoryExist)
import System.Exit
import Search
data Commands =
Commands { searchPath :: String
, par :: Maybe Int
, project :: Maybe T.Text
, revision :: Maybe T.Text
} deriving (Generic, Show)
instance ParseRecord Commands
makeProjectFilter :: Commands -> ProjectFilter
makeProjectFilter options =
let stdFilts = StdProjectFilters
(ProjName <$> project options)
(Revision <$> revision options)
in makeProjectFilters stdFilts
main :: IO ()
main = do
options <- getRecord "Search JSON Files"
-- Would user like to run in parallel?
let runner = if isJust $ par options
then mainSTMConduit (fromJust $ par options)
else mainStreamingConduit
-- necessary things to search files: search path, filters to use, search dir exists
let filterFunc = makeProjectFilter options
searchDir <- absolutize (searchPath options)
itExists <- doesDirectoryExist searchDir
-- Run it if it exists
case itExists of
False -> putStrLn "Search Directory does not exist" >> exitWith (ExitFailure 1)
True -> runner filterFunc searchDir
-- Single-threaded version with bounded memory usage
mainStreamingConduit :: ProjectFilter -> FilePath -> IO ()
mainStreamingConduit filterFunc searchDir = do
C.runConduitRes $
sourceFilesFilter filterFunc searchDir .| C.mapM_C (liftIO . putStrLn)
-- Multiple-threaded version of this program using channels from `stm-conduit`
mainSTMConduit :: Int -> ProjectFilter -> FilePath -> IO ()
mainSTMConduit nrWorkers filterFunc searchDir = do
children <- newMVar []
inChan <- atomically $ STMChan.newTBMChan 16
_ <- forkIO . C.runResourceT $ do
_ <- register $ atomically $ STMChan.closeTBMChan inChan
C.runConduitRes $ C.sourceDirectoryDeep False searchDir .| STMChan.sinkTBMChan inChan True
forM_ [1..nrWorkers] (\_ -> forkChild children $ runConduitChan inChan filterFunc)
waitForChildren children
return ()
runConduitChan :: STMChan.TBMChan FilePath -> ProjectFilter -> IO ()
runConduitChan inChan filterFunc = do
C.runConduitRes $
STMChan.sourceTBMChan inChan
.| parseProject filterFunc
.| C.mapM_C (liftIO . putStrLn)
waitForChildren :: MVar [MVar ()] -> IO ()
waitForChildren children = do
cs <- takeMVar children
case cs of
[] -> return ()
m:ms -> do
putMVar children ms
takeMVar m
waitForChildren children
forkChild :: MVar [MVar ()] -> IO () -> IO ThreadId
forkChild children io = do
mvar <- newEmptyMVar
childs <- takeMVar children
putMVar children (mvar:childs)
forkFinally io (\_ -> putMVar mvar ())
Note: I'm using stm-conduit 3.0.0 with conduit 1.12.1, which is why I needed to include the boolean argument:
STMChan.sinkTBMChan inChan True
In version 4.0.0 of stm-conduit, this function automatically closes the channel, so the boolean argument has been removed.
I want to parse all json files in a given directory into a data type Result.
So i have a decode function
decodeResult :: Data.ByteString.Lazy.ByteString -> Maybe Result
I began with Data.Text.Lazy.IO to load file into Lazy ByteString,
import qualified Data.Text.Lazy.IO as T
import qualified Data.Text.Lazy.Encoding as T
getFileContent :: FilePath -> IO B.ByteString
getFileContent path = T.encodeUtf8 `fmap` T.readFile path
It compiled, but I ran into Too many files opened problem, so I thought maybe I should use withFile.
import System.IO
import qualified Data.ByteString.Lazy as B
import qualified Data.Text.Lazy.IO as T
import qualified Data.Text.Lazy.Encoding as T
getFileContent :: FilePath -> IO (Maybe Result)
getFileContent path = withFile path ReadMode $ \hnd -> do
content <- T.hGetContents hnd
return $ (decodeAnalytic . T.encodeUtf8) content
loadAllResults :: FilePath -> IO [Result]
loadAllResults path = do
paths <- listDirectory path
results <- sequence $ fmap getFileContent (fmap (path ++ ) $ filter (endswith ".json") paths)
return $ catMaybes results
In this version, the lazy io seems never got evaluated, it always return empty list. But If i print content inside getFileContent function, then everything seems work correctly.
getFileContent :: FilePath -> IO (Maybe Result)
getFileContent path = withFile path ReadMode $ \hnd -> do
content <- T.hGetContents hnd
print content
return $ (decodeAnalytic . T.encodeUtf8) content
So I am not sure what am I missing, should I use conduit for this type of things?
Generally speaking I would recommend using a streaming library for parsing arbitrarily sized data like a JSON file. However, in the specific case of parsing JSON with aeson, the concerns of overrunning memory are not as significant IMO, since the aeson library itself will ultimately represent the entire file in memory as a Value type. So given that, you may choose to simply use strict bytestring I/O. I've given an example of using both conduit and strict I/O for parsing a JSON value. (I think the conduit version exists in some libraries already, I'm not sure.)
#!/usr/bin/env stack
{- stack --resolver lts-7.14 --install-ghc runghc
--package aeson --package conduit-extra
-}
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (FromJSON, Result (..), eitherDecodeStrict',
fromJSON, json, Value)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Conduit (ConduitM, runConduitRes, (.|))
import Data.Conduit.Attoparsec (sinkParser)
import Data.Conduit.Binary (sourceFile)
sinkFromJSON :: (MonadThrow m, FromJSON a) => ConduitM ByteString o m a
sinkFromJSON = do
value <- sinkParser json
case fromJSON value of
Error e -> throwM $ userError e
Success x -> return x
readJSONFile :: (MonadIO m, FromJSON a) => FilePath -> m a
readJSONFile fp = liftIO $ runConduitRes $ sourceFile fp .| sinkFromJSON
-- Or using strict I/O
readJSONFileStrict :: (MonadIO m, FromJSON a) => FilePath -> m a
readJSONFileStrict fp = liftIO $ do
bs <- B.readFile fp
case eitherDecodeStrict' bs of
Left e -> throwM $ userError e
Right x -> return x
main :: IO ()
main = do
x <- readJSONFile "test.json"
y <- readJSONFileStrict "test.json"
print (x :: Value)
print (y :: Value)
EDIT Forgot to mention: I'd strongly recommend against using textual I/O for reading your JSON files. JSON files should be encoded with UTF-8, while the textual I/O functions will use whatever your system settings specify for character encoding. Relying on Data.ByteString.readFile and similar is more reliable. I went into more detail in a recent blog post.
Short version
Same question as in here, but within a generic MonadResource instance rather than an explicit ResourceT m.
Long version
How would you define a catch function such that:
import Control.Exception (Exception, IOException)
import Control.Monad.Trans.Resource (MonadResource, runResourceT)
catch :: (MonadResource m, Exception e) -> m () -> (e -> m ()) -> m ()
catch = undefined
-- 'a' and 'b' are functions from an external library,
-- so I can't actually change their implementation
a, b :: MonadResource m => m ()
a = -- Something that might throw IO exceptions
b = -- Something that might throw IO exceptions
main :: IO ()
main = runResourceT $ do
a `catch` \(e :: IOException) -> -- Exception handling
b `catch` \(e :: IOException) -> -- Exception handling
The problems I run into are:
In Control.Exception, catch only works on bare IOs ;
In Control.Exception.Lifted, catch requires an instance of MonadBaseControl, which MonadResource is unfortunately not (and I wonder why) ;
MonadResource implies MonadThrow which defines a monadThrow function without its 'catch' equivalent (and I wonder why) ;
It looks like the only way to handle IO exceptions is to exit the ResourceT layer, and this bothers me: I'd like to be able to handle exceptions locally without travelling through the monad transformers stack.
For information, in my real code, a and b are actually the http function from Network.HTTP.Conduit.
Thank you for your insights.
Minimal code with the problem
Compilable with ghc --make example.hs with http-conduit library installed:
{-# LANGUAGE FlexibleContexts, ScopedTypeVariables #-}
import Control.Exception.Lifted (IOException, catch)
import Control.Monad.Base (liftBase)
import Control.Monad.Error (MonadError(..), runErrorT)
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Trans.Resource (MonadResource, runResourceT)
import Data.Conduit
import Data.Conduit.List (consume)
import Data.Conduit.Text (decode, utf8)
import Data.Text (Text)
import Network.HTTP.Client
import Network.HTTP.Conduit (http)
main :: IO ()
main = do
result <- runErrorT $ runResourceT f
putStrLn $ "OK: " ++ show result
f :: (MonadBaseControl IO m, MonadResource m, MonadError String m) => m [Text]
f = do
req <- liftBase $ parseUrl "http://uri-that-does-not-exist.abc"
manager <- liftBase $ newManager defaultManagerSettings
response <- (http req manager `catch` \(e :: IOException) -> throwError $ show e)
response $$+- decode utf8 =$ consume
When executed, this program ends in error with the following output:
InternalIOException getAddrInfo: does not exist (Name or service not known)
http does not throw IOException, it throws HttpException and InternalIOException is one of the latter's constructors.
You should either catch HttpException or SomeException in case you want to catch all exceptions.
The type you need,
a, b :: MonadResource m, MonadBaseControl IO m => m ()
Is a special case of the type you currently have
a, b :: MonadResource m => m ()
as the only difference is the extra class constraint. You are free to make the type signatures in your code less general than they would be by default; therefore, changing the signatures of a and b should be enough.
If I understand your problem correctly, there is no problem with using lifted-base. Although the type of a and b only use the constraint MonadResource m, it doesn't mean you can't use them on a monad that has other additional properties. For example, if you perform your computation inside ResourceT, it satisfies the constraint for a and b, and you can also use anything from Control.Exception.Lifted:
-- ...
import Control.Exception.Lifted
-- 'a' and 'b' are functions from an external library,
-- so I can't actually change their implementation
a, b :: MonadResource m => m ()
a = undefined -- Something that might throw IO exceptions
b = undefined -- Something that might throw IO exceptions
main :: IO ()
main = runResourceT $ do
a `catch` \(e :: IOException) -> undefined -- Exception handling
b `catch` \(e :: IOException) -> undefined -- Exception handling
If you alter the type signature of catch to include MonadCatch from the exceptions package, then it would be trivial:
import Control.Monad.Trans.Resource (MonadResource, runResourceT)
import Control.Monad.Catch (catch)
a, b :: MonadResource m => m ()
a = …
b = …
main :: IO ()
main = runResourceT $ do
a `catch` \e -> …
b `catch` \e -> …
Note that this does not require any changes for a nor b.
Also, both duplode and Petr Pudlák have pointed out that you are free to make the monad for catch to be as specific as you like, because doing so does not require any cooperation from a or b. So any of these solutions will work.
I'm using hedis and trying to handle the case of the server going dead. According to the documentation:
Connection to the server lost:
In case of a lost connection, command functions throw a ConnectionLostException. It can only be caught outside of runRedis.
So I would assume I want to catch the ConnectionLostException. However, whilst I can seem to catch it correctly, it seems to also bubble up to the top, and I'm not sure why. Here's some code (just running in GHCI):
:set -XOverloadedStrings
import Database.Redis
import Control.Exception
conn <- connect defaultConnectInfo
runRedis conn $ ping
Now, if I kill the redis server between making the connection and running the command, I get the result I expect:
⟨interactive⟩: ConnectionLost
*** Exception: ConnectionLost
So instead I try to do the following (I added >>= evaluate in order to attempt to force evaluation of the error, but it made no difference):
let tryR = try :: IO a -> IO (Either ConnectionLostException a)
tryR . (>>= evaluate) . runRedis conn $ ping
This gives me:
Left Con: ConnectionLost
nectionLost
So I'm getting the Left result as expected, but halfway through the exception is also presumably being caught and displayed by GHCI. Is this a problem with things not being evaluated?
Like John hinted, there seems to be somthing that prints this message to stderr.
Consider this example:
{-# LANGUAGE OverloadedStrings #-}
import Control.Concurrent (threadDelay)
import Control.Exception
import Control.Monad
import Database.Redis
import System.Mem
tryR :: IO a -> IO (Either ConnectionLostException a)
tryR = try
main :: IO ()
main = do
conn <- connect defaultConnectInfo
loop conn
putStrLn $ "exiting gracefully after counting up some numbers"
performGC
forM_ [1..10] $ \i -> do
print i
threadDelay 10000 -- 0.05 seconds
where
loop conn = do
e <- tryR . (>>= evaluate) . runRedis conn $ ping
case e of
Right x -> do print x
threadDelay 1000000
loop conn
Left err -> do putStrLn $ "tryR caught exception: " ++ show err
It prints:
Right Pong
Right Pong <-------------- after this I Ctrl-C the redis server
tryR caught exception: ConnectionLost
exiting gracefully after counting up some numbers
1
test: ConnectionLost
2
3
4
5
6
7
8
9
10
This looks like something in the stack is printing this test: ConnectionLost (or test.hs: ConnectionLost if you use GHCI/runghc) asynchronously.
If that's GHC, that is probably a bug, but chances are high it is done by hedis or one of its dependencies (I haven't found it in hedis itself yet).
Apologies in advance for my Haskell inexperience. I am writing a little wrapper for a Redis instance for a learning project. So far Yesod has been an absolute wonder. With very little Haskell experience I got browserId Auth working, and I'm inserting records into Redis successfully and quickly.
I'm stuck figuring out how to get the Redis responses converted into JSON and sent back. Here is a working, un-scaffold, app that shows getting a static RepJson or a RepPlain with Redis info (App is called LRedis):
{-# LANGUAGE OverloadedStrings, TemplateHaskell, TypeFamilies,
MultiParamTypeClasses, QuasiQuotes #-}
import Yesod
import Data.Text
import Data.Text.Encoding
import Data.ByteString.UTF8
import Database.Redis
import qualified Data.ByteString.Lazy as L
data LRedis = LRedis
instance Yesod LRedis where
mkYesod "LRedis" [parseRoutes|
/ HomeR GET
/learnJson LearnJsonR GET
/redisWorks RedisWorksR GET
|]
getHomeR :: Handler RepHtml
getHomeR = do
defaultLayout[whamlet|
<p>Hi this is a headless API thing.
|]
getLearnJsonR :: Handler RepJson
getLearnJsonR = do
jsonToRepJson $ object [("json", ("ftw"::Text))]
getRedisWorksR :: Handler RepPlain
getRedisWorksR = do
conn <- liftIO $ connect defaultConnectInfo
liftIO $ runRedis conn $ do
result <- Database.Redis.get (fromString "hello")
case result of
Left e -> return $ RepPlain "Error"
Right mAnswer -> do
case mAnswer of
Nothing -> return $ RepPlain "Not found."
Just x -> return $ RepPlain (toContent x)
main :: IO()
main = do
warpDebug 3000 $ LRedis
Again, that is all working. It will return the string stored in "hello" in redis if you curl /redisWorks, or it will return JSON if you curl /learnJson, but I want to give the redis answer as JSON, not a plain string. I thought I could just naively combine the two, like:
getRedisJsonR :: Handler RepJson
getRedisJsonR = do
conn <- liftIO $ connect defaultConnectInfo
liftIO $ runRedis conn $ do
result <- Database.Redis.get (fromString "hello")
case result of
Left e -> jsonToRepJson $ object [("response", ("error"::Text))]
Right mAnswer -> do
case mAnswer of
Nothing -> jsonToRepJson $ object [("response", ("Nothing"::Text))]
Just x -> jsonToRepJson $ object [("response", ((decodeUtf8 x)::Text))]
But after adding the route /redisJson RedisJsonR GET it fails with this compilation error:
Couldn't match expected type `Redis a0'
with actual type `GHandler sub0 master0 RepJson'
In the expression:
jsonToRepJson $ object [("response", ("error" :: Text))]
In a case alternative:
Left e -> jsonToRepJson $ object [("response", ("error" :: Text))]
In a stmt of a 'do' block:
case result of {
Left e -> jsonToRepJson $ object [("response", ("error" :: Text))]
Right mAnswer
-> do { case mAnswer of {
Nothing -> ...
Just x -> ... } } }
It seems like its telling me I need to do something different with the result in case of an error, but I don't know what that would be, or why it's necessary given the RepPlain version is working.
Is there an example of getting the results from Redis into JSON within Yesod?
Is there just something simple I'm doing wrong with IO or something?
Convenient link to Hedis docs: http://hackage.haskell.org/package/hedis Thank you for helping me with this. Sorry again if it turns out to be super simple.
getRedisJsonR :: Handler RepJson
getRedisJsonR = do
conn <- liftIO $ connect defaultConnectInfo
res <- liftIO $ runRedis conn $ do
result <- Database.Redis.get (fromString "hello")
case result of
Left e -> return $ jsonToRepJson $ object [("response", ("error"::Text))]
Right mAnswer -> do
case mAnswer of
Nothing -> return $ jsonToRepJson $ object [("response", ("Nothing"::Text))]
Just x -> return $ jsonToRepJson $ object [("response", ((decodeUtf8 x)::Text))]
res
I actually don't trust myself to explain why this works and your original code doesn't -- I've been programming in Haskell on a daily basis for just under three months, so I have a developing gut sense for what will work but I'm really not there yet on the theory side, especially when it comes to stacked monads, which I think is what we're dealing with here (either Redis is on top of Handler or vice versa, and liftIO is facilitating the stacking).
Hopefully someone else can weigh in -- seems like a great concrete example to illustrate some monad concepts.