hedis-0.9.7: src/Database/Redis/Core.hs
{-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving, RecordWildCards,
MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances, CPP #-}
module Database.Redis.Core (
Connection(..), connect, checkedConnect,
ConnectInfo(..), defaultConnectInfo,
Redis(), runRedis, unRedis, reRedis,
RedisCtx(..), MonadRedis(..),
send, recv, sendRequest,
auth, select, ping
) where
import Prelude
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
#endif
import Control.Monad.Reader
import qualified Data.ByteString as B
import Data.IORef
import Data.Pool
import Data.Time
import Network
import Database.Redis.Protocol
import qualified Database.Redis.ProtocolPipelining as PP
import Database.Redis.Types
--------------------------------------------------------------------------------
-- The Redis Monad
--
-- |Context for normal command execution, outside of transactions. Use
-- 'runRedis' to run actions of this type.
--
-- In this context, each result is wrapped in an 'Either' to account for the
-- possibility of Redis returning an 'Error' reply.
newtype Redis a = Redis (ReaderT RedisEnv IO a)
deriving (Monad, MonadIO, Functor, Applicative)
data RedisEnv = Env { envConn :: PP.Connection, envLastReply :: IORef Reply }
-- |This class captures the following behaviour: In a context @m@, a command
-- will return its result wrapped in a \"container\" of type @f@.
--
-- Please refer to the Command Type Signatures section of this page for more
-- information.
class (MonadRedis m) => RedisCtx m f | m -> f where
returnDecode :: RedisResult a => Reply -> m (f a)
instance RedisCtx Redis (Either Reply) where
returnDecode = return . decode
class (Monad m) => MonadRedis m where
liftRedis :: Redis a -> m a
instance MonadRedis Redis where
liftRedis = id
-- |Interact with a Redis datastore specified by the given 'Connection'.
--
-- Each call of 'runRedis' takes a network connection from the 'Connection'
-- pool and runs the given 'Redis' action. Calls to 'runRedis' may thus block
-- while all connections from the pool are in use.
runRedis :: Connection -> Redis a -> IO a
runRedis (Conn pool) redis =
withResource pool $ \conn -> runRedisInternal conn redis
-- |Deconstruct Redis constructor.
--
-- 'unRedis' and 'reRedis' can be used to define instances for
-- arbitrary typeclasses.
--
-- WARNING! These functions are considered internal and no guarantee
-- is given at this point that they will not break in future.
unRedis :: Redis a -> ReaderT RedisEnv IO a
unRedis (Redis r) = r
-- |Reconstruct Redis constructor.
reRedis :: ReaderT RedisEnv IO a -> Redis a
reRedis r = Redis r
-- |Internal version of 'runRedis' that does not depend on the 'Connection'
-- abstraction. Used to run the AUTH command when connecting.
runRedisInternal :: PP.Connection -> Redis a -> IO a
runRedisInternal conn (Redis redis) = do
-- Dummy reply in case no request is sent.
ref <- newIORef (SingleLine "nobody will ever see this")
r <- runReaderT redis (Env conn ref)
-- Evaluate last reply to keep lazy IO inside runRedis.
readIORef ref >>= (`seq` return ())
return r
setLastReply :: Reply -> ReaderT RedisEnv IO ()
setLastReply r = do
ref <- asks envLastReply
lift (writeIORef ref r)
recv :: (MonadRedis m) => m Reply
recv = liftRedis $ Redis $ do
conn <- asks envConn
r <- liftIO (PP.recv conn)
setLastReply r
return r
send :: (MonadRedis m) => [B.ByteString] -> m ()
send req = liftRedis $ Redis $ do
conn <- asks envConn
liftIO $ PP.send conn (renderRequest req)
-- |'sendRequest' can be used to implement commands from experimental
-- versions of Redis. An example of how to implement a command is given
-- below.
--
-- @
-- -- |Redis DEBUG OBJECT command
-- debugObject :: ByteString -> 'Redis' (Either 'Reply' ByteString)
-- debugObject key = 'sendRequest' [\"DEBUG\", \"OBJECT\", key]
-- @
--
sendRequest :: (RedisCtx m f, RedisResult a)
=> [B.ByteString] -> m (f a)
sendRequest req = do
r' <- liftRedis $ Redis $ do
conn <- asks envConn
r <- liftIO $ PP.request conn (renderRequest req)
setLastReply r
return r
returnDecode r'
--------------------------------------------------------------------------------
-- Connection
--
-- |A threadsafe pool of network connections to a Redis server. Use the
-- 'connect' function to create one.
newtype Connection = Conn (Pool PP.Connection)
-- |Information for connnecting to a Redis server.
--
-- It is recommended to not use the 'ConnInfo' data constructor directly.
-- Instead use 'defaultConnectInfo' and update it with record syntax. For
-- example to connect to a password protected Redis server running on localhost
-- and listening to the default port:
--
-- @
-- myConnectInfo :: ConnectInfo
-- myConnectInfo = defaultConnectInfo {connectAuth = Just \"secret\"}
-- @
--
data ConnectInfo = ConnInfo
{ connectHost :: HostName
, connectPort :: PortID
, connectAuth :: Maybe B.ByteString
-- ^ When the server is protected by a password, set 'connectAuth' to 'Just'
-- the password. Each connection will then authenticate by the 'auth'
-- command.
, connectDatabase :: Integer
-- ^ Each connection will 'select' the database with the given index.
, connectMaxConnections :: Int
-- ^ Maximum number of connections to keep open. The smallest acceptable
-- value is 1.
, connectMaxIdleTime :: NominalDiffTime
-- ^ Amount of time for which an unused connection is kept open. The
-- smallest acceptable value is 0.5 seconds. If the @timeout@ value in
-- your redis.conf file is non-zero, it should be larger than
-- 'connectMaxIdleTime'.
} deriving Show
-- |Default information for connecting:
--
-- @
-- connectHost = \"localhost\"
-- connectPort = PortNumber 6379 -- Redis default port
-- connectAuth = Nothing -- No password
-- connectDatabase = 0 -- SELECT database 0
-- connectMaxConnections = 50 -- Up to 50 connections
-- connectMaxIdleTime = 30 -- Keep open for 30 seconds
-- @
--
defaultConnectInfo :: ConnectInfo
defaultConnectInfo = ConnInfo
{ connectHost = "localhost"
, connectPort = PortNumber 6379
, connectAuth = Nothing
, connectDatabase = 0
, connectMaxConnections = 50
, connectMaxIdleTime = 30
}
-- |Constructs a 'Connection' pool to a Redis server designated by the
-- given 'ConnectInfo'. The first connection is not actually established
-- until the first call to the server.
connect :: ConnectInfo -> IO Connection
connect ConnInfo{..} = Conn <$>
createPool create destroy 1 connectMaxIdleTime connectMaxConnections
where
create = do
conn <- PP.connect connectHost connectPort
runRedisInternal conn $ do
-- AUTH
case connectAuth of
Nothing -> return ()
Just pass -> void $ auth pass
-- SELECT
when (connectDatabase /= 0) (void $ select connectDatabase)
return conn
destroy = PP.disconnect
-- |Constructs a 'Connection' pool to a Redis server designated by the
-- given 'ConnectInfo', then tests if the server is actually there.
-- Throws an exception if the connection to the Redis server can't be
-- established.
checkedConnect :: ConnectInfo -> IO Connection
checkedConnect connInfo = do
conn <- connect connInfo
runRedis conn $ void ping
return conn
-- The AUTH command. It has to be here because it is used in 'connect'.
auth
:: B.ByteString -- ^ password
-> Redis (Either Reply Status)
auth password = sendRequest ["AUTH", password]
-- The SELECT command. Used in 'connect'.
select
:: RedisCtx m f
=> Integer -- ^ index
-> m (f Status)
select ix = sendRequest ["SELECT", encode ix]
-- The PING command. Used in 'checkedConnect'.
ping
:: (RedisCtx m f)
=> m (f Status)
ping = sendRequest (["PING"] )