packages feed

hedis 0.13.1 → 0.14.0

raw patch · 19 files changed

+2214/−1154 lines, 19 filesdep +containersdep ~asyncdep ~basedep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependencies added: containers

Dependency ranges changed: async, base, bytestring, mtl

API changes (from Hackage documentation)

- Database.Redis: data ConnectTimeout
- Database.Redis.Core.Internal: Env :: Connection -> IORef Reply -> RedisEnv
- Database.Redis.Sentinel: data ConnectTimeout
+ Database.Redis: ClusterNodesResponse :: [ClusterNodesResponseEntry] -> ClusterNodesResponse
+ Database.Redis: ClusterNodesResponseEntry :: ByteString -> ByteString -> Integer -> [ByteString] -> Maybe ByteString -> Integer -> Integer -> Integer -> ByteString -> [ClusterNodesResponseSlotSpec] -> ClusterNodesResponseEntry
+ Database.Redis: ClusterNodesResponseSingleSlot :: Integer -> ClusterNodesResponseSlotSpec
+ Database.Redis: ClusterNodesResponseSlotImporting :: Integer -> ByteString -> ClusterNodesResponseSlotSpec
+ Database.Redis: ClusterNodesResponseSlotMigrating :: Integer -> ByteString -> ClusterNodesResponseSlotSpec
+ Database.Redis: ClusterNodesResponseSlotRange :: Integer -> Integer -> ClusterNodesResponseSlotSpec
+ Database.Redis: ClusterSlotsNode :: ByteString -> Int -> ByteString -> ClusterSlotsNode
+ Database.Redis: ClusterSlotsResponse :: [ClusterSlotsResponseEntry] -> ClusterSlotsResponse
+ Database.Redis: ClusterSlotsResponseEntry :: Int -> Int -> ClusterSlotsNode -> [ClusterSlotsNode] -> ClusterSlotsResponseEntry
+ Database.Redis: [clusterNodesResponseConfigEpoch] :: ClusterNodesResponseEntry -> Integer
+ Database.Redis: [clusterNodesResponseEntries] :: ClusterNodesResponse -> [ClusterNodesResponseEntry]
+ Database.Redis: [clusterNodesResponseLinkState] :: ClusterNodesResponseEntry -> ByteString
+ Database.Redis: [clusterNodesResponseMasterId] :: ClusterNodesResponseEntry -> Maybe ByteString
+ Database.Redis: [clusterNodesResponseNodeFlags] :: ClusterNodesResponseEntry -> [ByteString]
+ Database.Redis: [clusterNodesResponseNodeId] :: ClusterNodesResponseEntry -> ByteString
+ Database.Redis: [clusterNodesResponseNodeIp] :: ClusterNodesResponseEntry -> ByteString
+ Database.Redis: [clusterNodesResponseNodePort] :: ClusterNodesResponseEntry -> Integer
+ Database.Redis: [clusterNodesResponsePingSent] :: ClusterNodesResponseEntry -> Integer
+ Database.Redis: [clusterNodesResponsePongReceived] :: ClusterNodesResponseEntry -> Integer
+ Database.Redis: [clusterNodesResponseSlots] :: ClusterNodesResponseEntry -> [ClusterNodesResponseSlotSpec]
+ Database.Redis: [clusterSlotsNodeID] :: ClusterSlotsNode -> ByteString
+ Database.Redis: [clusterSlotsNodeIP] :: ClusterSlotsNode -> ByteString
+ Database.Redis: [clusterSlotsNodePort] :: ClusterSlotsNode -> Int
+ Database.Redis: [clusterSlotsResponseEntries] :: ClusterSlotsResponse -> [ClusterSlotsResponseEntry]
+ Database.Redis: [clusterSlotsResponseEntryEndSlot] :: ClusterSlotsResponseEntry -> Int
+ Database.Redis: [clusterSlotsResponseEntryMaster] :: ClusterSlotsResponseEntry -> ClusterSlotsNode
+ Database.Redis: [clusterSlotsResponseEntryReplicas] :: ClusterSlotsResponseEntry -> [ClusterSlotsNode]
+ Database.Redis: [clusterSlotsResponseEntryStartSlot] :: ClusterSlotsResponseEntry -> Int
+ Database.Redis: clusterGetKeysInSlot :: RedisCtx m f => Integer -> Integer -> m (f [ByteString])
+ Database.Redis: clusterNodes :: RedisCtx m f => m (f ClusterNodesResponse)
+ Database.Redis: clusterSetSlotImporting :: RedisCtx m f => Integer -> ByteString -> m (f Status)
+ Database.Redis: clusterSetSlotMigrating :: RedisCtx m f => Integer -> ByteString -> m (f Status)
+ Database.Redis: clusterSetSlotNode :: RedisCtx m f => Integer -> ByteString -> m (f Status)
+ Database.Redis: clusterSetSlotStable :: RedisCtx m f => Integer -> m (f Status)
+ Database.Redis: clusterSlots :: RedisCtx m f => m (f ClusterSlotsResponse)
+ Database.Redis: command :: RedisCtx m f => m (f [CommandInfo])
+ Database.Redis: connectCluster :: ConnectInfo -> IO Connection
+ Database.Redis: data ClusterNodesResponse
+ Database.Redis: data ClusterNodesResponseEntry
+ Database.Redis: data ClusterNodesResponseSlotSpec
+ Database.Redis: data ClusterSlotsNode
+ Database.Redis: data ClusterSlotsResponse
+ Database.Redis: data ClusterSlotsResponseEntry
+ Database.Redis: data HashSlot
+ Database.Redis: keyToSlot :: ByteString -> HashSlot
+ Database.Redis: newtype ConnectTimeout
+ Database.Redis.Core.Internal: ClusteredEnv :: IO ShardMap -> Connection -> RedisEnv
+ Database.Redis.Core.Internal: NonClusteredEnv :: Connection -> IORef Reply -> RedisEnv
+ Database.Redis.Core.Internal: [connection] :: RedisEnv -> Connection
+ Database.Redis.Core.Internal: [refreshAction] :: RedisEnv -> IO ShardMap
+ Database.Redis.Sentinel: ClusterNodesResponse :: [ClusterNodesResponseEntry] -> ClusterNodesResponse
+ Database.Redis.Sentinel: ClusterNodesResponseEntry :: ByteString -> ByteString -> Integer -> [ByteString] -> Maybe ByteString -> Integer -> Integer -> Integer -> ByteString -> [ClusterNodesResponseSlotSpec] -> ClusterNodesResponseEntry
+ Database.Redis.Sentinel: ClusterNodesResponseSingleSlot :: Integer -> ClusterNodesResponseSlotSpec
+ Database.Redis.Sentinel: ClusterNodesResponseSlotImporting :: Integer -> ByteString -> ClusterNodesResponseSlotSpec
+ Database.Redis.Sentinel: ClusterNodesResponseSlotMigrating :: Integer -> ByteString -> ClusterNodesResponseSlotSpec
+ Database.Redis.Sentinel: ClusterNodesResponseSlotRange :: Integer -> Integer -> ClusterNodesResponseSlotSpec
+ Database.Redis.Sentinel: ClusterSlotsNode :: ByteString -> Int -> ByteString -> ClusterSlotsNode
+ Database.Redis.Sentinel: ClusterSlotsResponse :: [ClusterSlotsResponseEntry] -> ClusterSlotsResponse
+ Database.Redis.Sentinel: ClusterSlotsResponseEntry :: Int -> Int -> ClusterSlotsNode -> [ClusterSlotsNode] -> ClusterSlotsResponseEntry
+ Database.Redis.Sentinel: [clusterNodesResponseConfigEpoch] :: ClusterNodesResponseEntry -> Integer
+ Database.Redis.Sentinel: [clusterNodesResponseEntries] :: ClusterNodesResponse -> [ClusterNodesResponseEntry]
+ Database.Redis.Sentinel: [clusterNodesResponseLinkState] :: ClusterNodesResponseEntry -> ByteString
+ Database.Redis.Sentinel: [clusterNodesResponseMasterId] :: ClusterNodesResponseEntry -> Maybe ByteString
+ Database.Redis.Sentinel: [clusterNodesResponseNodeFlags] :: ClusterNodesResponseEntry -> [ByteString]
+ Database.Redis.Sentinel: [clusterNodesResponseNodeId] :: ClusterNodesResponseEntry -> ByteString
+ Database.Redis.Sentinel: [clusterNodesResponseNodeIp] :: ClusterNodesResponseEntry -> ByteString
+ Database.Redis.Sentinel: [clusterNodesResponseNodePort] :: ClusterNodesResponseEntry -> Integer
+ Database.Redis.Sentinel: [clusterNodesResponsePingSent] :: ClusterNodesResponseEntry -> Integer
+ Database.Redis.Sentinel: [clusterNodesResponsePongReceived] :: ClusterNodesResponseEntry -> Integer
+ Database.Redis.Sentinel: [clusterNodesResponseSlots] :: ClusterNodesResponseEntry -> [ClusterNodesResponseSlotSpec]
+ Database.Redis.Sentinel: [clusterSlotsNodeID] :: ClusterSlotsNode -> ByteString
+ Database.Redis.Sentinel: [clusterSlotsNodeIP] :: ClusterSlotsNode -> ByteString
+ Database.Redis.Sentinel: [clusterSlotsNodePort] :: ClusterSlotsNode -> Int
+ Database.Redis.Sentinel: [clusterSlotsResponseEntries] :: ClusterSlotsResponse -> [ClusterSlotsResponseEntry]
+ Database.Redis.Sentinel: [clusterSlotsResponseEntryEndSlot] :: ClusterSlotsResponseEntry -> Int
+ Database.Redis.Sentinel: [clusterSlotsResponseEntryMaster] :: ClusterSlotsResponseEntry -> ClusterSlotsNode
+ Database.Redis.Sentinel: [clusterSlotsResponseEntryReplicas] :: ClusterSlotsResponseEntry -> [ClusterSlotsNode]
+ Database.Redis.Sentinel: [clusterSlotsResponseEntryStartSlot] :: ClusterSlotsResponseEntry -> Int
+ Database.Redis.Sentinel: clusterGetKeysInSlot :: RedisCtx m f => Integer -> Integer -> m (f [ByteString])
+ Database.Redis.Sentinel: clusterNodes :: RedisCtx m f => m (f ClusterNodesResponse)
+ Database.Redis.Sentinel: clusterSetSlotImporting :: RedisCtx m f => Integer -> ByteString -> m (f Status)
+ Database.Redis.Sentinel: clusterSetSlotMigrating :: RedisCtx m f => Integer -> ByteString -> m (f Status)
+ Database.Redis.Sentinel: clusterSetSlotNode :: RedisCtx m f => Integer -> ByteString -> m (f Status)
+ Database.Redis.Sentinel: clusterSetSlotStable :: RedisCtx m f => Integer -> m (f Status)
+ Database.Redis.Sentinel: clusterSlots :: RedisCtx m f => m (f ClusterSlotsResponse)
+ Database.Redis.Sentinel: command :: RedisCtx m f => m (f [CommandInfo])
+ Database.Redis.Sentinel: connectCluster :: ConnectInfo -> IO Connection
+ Database.Redis.Sentinel: data ClusterNodesResponse
+ Database.Redis.Sentinel: data ClusterNodesResponseEntry
+ Database.Redis.Sentinel: data ClusterNodesResponseSlotSpec
+ Database.Redis.Sentinel: data ClusterSlotsNode
+ Database.Redis.Sentinel: data ClusterSlotsResponse
+ Database.Redis.Sentinel: data ClusterSlotsResponseEntry
+ Database.Redis.Sentinel: data HashSlot
+ Database.Redis.Sentinel: keyToSlot :: ByteString -> HashSlot
+ Database.Redis.Sentinel: newtype ConnectTimeout
- Database.Redis: auth :: ByteString -> Redis (Either Reply Status)
+ Database.Redis: auth :: RedisCtx m f => ByteString -> m (f Status)
- Database.Redis: withCheckedConnect :: (MonadMask m, MonadIO m) => ConnectInfo -> (Connection -> m c) -> m c
+ Database.Redis: withCheckedConnect :: ConnectInfo -> (Connection -> IO c) -> IO c
- Database.Redis.Sentinel: auth :: ByteString -> Redis (Either Reply Status)
+ Database.Redis.Sentinel: auth :: RedisCtx m f => ByteString -> m (f Status)
- Database.Redis.Sentinel: withCheckedConnect :: (MonadMask m, MonadIO m) => ConnectInfo -> (Connection -> m c) -> m c
+ Database.Redis.Sentinel: withCheckedConnect :: ConnectInfo -> (Connection -> IO c) -> IO c

Files

CHANGELOG view
@@ -1,5 +1,9 @@ # Changelog for Hedis +## 0.14.0++* PR #157. Clustering support+ ## 0.13.1  * PR #158. Upgrade to Redis 6.0.9 & Fix auth test
hedis.cabal view
@@ -1,5 +1,5 @@ name:               hedis-version:            0.13.1+version:            0.14.0 synopsis:     Client library for the Redis datastore: supports full command set,     pipelining.@@ -77,6 +77,7 @@                     bytestring-lexing >= 0.5,                     exceptions,                     unordered-containers,+                    containers,                     text,                     deepseq,                     mtl >= 2,@@ -94,6 +95,10 @@       semigroups >= 0.11 && < 0.19    other-modules:    Database.Redis.Core,+                    Database.Redis.Connection,+                    Database.Redis.Cluster,+                    Database.Redis.Cluster.HashSlot,+                    Database.Redis.Cluster.Command,                     Database.Redis.ProtocolPipelining,                     Database.Redis.Protocol,                     Database.Redis.PubSub,@@ -101,7 +106,8 @@                     Database.Redis.Types                     Database.Redis.Commands,                     Database.Redis.ManualCommands,-                    Database.Redis.URL+                    Database.Redis.URL,+                    Database.Redis.ConnectionContext  benchmark hedis-benchmark     default-language: Haskell2010@@ -122,8 +128,35 @@     default-language: Haskell2010     type: exitcode-stdio-1.0     hs-source-dirs: test-    main-is: Test.hs+    main-is: Main.hs     other-modules: PubSubTest+                   Tests+    build-depends:+        base == 4.*,+        bytestring >= 0.10,+        hedis,+        HUnit,+        async,+        stm,+        text,+        mtl == 2.*,+        test-framework,+        test-framework-hunit,+        time+    -- We use -O0 here, since GHC takes *very* long to compile so many constants+    ghc-options: -O0 -Wall -rtsopts -fno-warn-unused-do-bind+    if flag(dev)+      ghc-options: -Werror+    if flag(dev)+      ghc-prof-options: -auto-all++test-suite hedis-test-cluster+    default-language: Haskell2010+    type: exitcode-stdio-1.0+    hs-source-dirs: test+    main-is: ClusterMain.hs+    other-modules: PubSubTest+                   Tests     build-depends:         base == 4.*,         bytestring >= 0.10,
src/Database/Redis.hs view
@@ -23,7 +23,7 @@     -- @     --     -- Send commands to the server:-    -- +    --     -- @     -- {-\# LANGUAGE OverloadedStrings \#-}     -- ...@@ -114,7 +114,7 @@     --  The Redis Scripting website (<http://redis.io/commands/eval>)     --  documents the exact semantics of the scripting commands and value     --  conversion.-    +     -- ** Automatic Pipelining     -- |Commands are automatically pipelined as much as possible. For example,     --  in the above \"hello world\" example, all four commands are pipelined.@@ -130,7 +130,7 @@     --  sent only when at least one reply has been received. That means, command     --  functions may block until there are less than 1000 outstanding replies.     ---    +     -- ** Error Behavior     -- |     --  [Operations against keys holding the wrong kind of value:] Outside of a@@ -155,7 +155,7 @@     --    sure it is not left in an unusable state, e.g. closed or inside a     --    transaction.     ---    +     -- * The Redis Monad     Redis(), runRedis,     unRedis, reRedis,@@ -164,23 +164,23 @@     -- * Connection     Connection, ConnectError(..), connect, checkedConnect, disconnect,     withConnect, withCheckedConnect,-    ConnectInfo(..), defaultConnectInfo, parseConnectInfo,+    ConnectInfo(..), defaultConnectInfo, parseConnectInfo, connectCluster,     PortID(..),      -- * Commands     module Database.Redis.Commands,-    +     -- * Transactions     module Database.Redis.Transactions,-    +     -- * Pub\/Sub     module Database.Redis.PubSub,      -- * Low-Level Command API     sendRequest,     Reply(..), Status(..), RedisResult(..), ConnectionLostException(..),-    ConnectTimeout(..)-    +    ConnectTimeout(..),+     -- |[Solution to Exercise]     --     --  Type of 'expire' inside a transaction:@@ -191,15 +191,28 @@     --     --  > lindex :: ByteString -> Integer -> Redis (Either Reply ByteString)     --+    HashSlot, keyToSlot ) where  import Database.Redis.Core+import Database.Redis.Connection+    ( runRedis+    , connectCluster+    , defaultConnectInfo+    , ConnectInfo(..)+    , disconnect+    , checkedConnect+    , connect+    , ConnectError(..)+    , Connection(..)+    , withConnect+    , withCheckedConnect)+import Database.Redis.ConnectionContext(PortID(..), ConnectionLostException(..), ConnectTimeout(..)) import Database.Redis.PubSub import Database.Redis.Protocol-import Database.Redis.ProtocolPipelining-    (PortID(..), ConnectionLostException(..), ConnectTimeout(..)) import Database.Redis.Transactions import Database.Redis.Types import Database.Redis.URL  import Database.Redis.Commands+import Database.Redis.Cluster.HashSlot(HashSlot, keyToSlot)
+ src/Database/Redis/Cluster.hs view
@@ -0,0 +1,412 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE ViewPatterns #-}+module Database.Redis.Cluster+  ( Connection(..)+  , NodeRole(..)+  , NodeConnection(..)+  , Node(..)+  , ShardMap(..)+  , HashSlot+  , Shard(..)+  , connect+  , disconnect+  , requestPipelined+  , nodes+) where++import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as Char8+import qualified Data.IORef as IOR+import Data.List(nub, sortBy, find)+import Data.Map(fromListWith, assocs)+import Data.Function(on)+import Control.Exception(Exception, throwIO, BlockedIndefinitelyOnMVar(..), catches, Handler(..))+import Control.Concurrent.MVar(MVar, newMVar, readMVar, modifyMVar, modifyMVar_)+import Control.Monad(zipWithM, when, replicateM)+import Database.Redis.Cluster.HashSlot(HashSlot, keyToSlot)+import qualified Database.Redis.ConnectionContext as CC+import qualified Data.HashMap.Strict as HM+import qualified Data.IntMap.Strict as IntMap+import           Data.Typeable+import qualified Scanner+import System.IO.Unsafe(unsafeInterleaveIO)++import Database.Redis.Protocol(Reply(Error), renderRequest, reply)+import qualified Database.Redis.Cluster.Command as CMD++-- This module implements a clustered connection whilst maintaining+-- compatibility with the original Hedis codebase. In particular it still+-- performs implicit pipelining using `unsafeInterleaveIO` as the single node+-- codebase does. To achieve this each connection carries around with it a+-- pipeline of commands. Every time `sendRequest` is called the command is+-- added to the pipeline and an IO action is returned which will, upon being+-- evaluated, execute the entire pipeline. If the pipeline is already executed+-- then it just looks up it's response in the executed pipeline.++-- | A connection to a redis cluster, it is compoesed of a map from Node IDs to+-- | 'NodeConnection's, a 'Pipeline', and a 'ShardMap'+data Connection = Connection (HM.HashMap NodeID NodeConnection) (MVar Pipeline) (MVar ShardMap) CMD.InfoMap++-- | A connection to a single node in the cluster, similar to 'ProtocolPipelining.Connection'+data NodeConnection = NodeConnection CC.ConnectionContext (IOR.IORef (Maybe B.ByteString)) NodeID++instance Eq NodeConnection where+    (NodeConnection _ _ id1) == (NodeConnection _ _ id2) = id1 == id2++instance Ord NodeConnection where+    compare (NodeConnection _ _ id1) (NodeConnection _ _ id2) = compare id1 id2++data PipelineState =+      -- Nothing in the pipeline has been evaluated yet so nothing has been+      -- sent+      Pending [[B.ByteString]]+      -- This pipeline has been executed, the replies are contained within it+    | Executed [Reply]+      -- We're in a MULTI-EXEC transaction. All commands in the transaction+      -- should go to the same node, but we won't know what node that is until+      -- we see a command with a key. We're storing these transactions and will+      -- send them all together when we see an EXEC.+    | TransactionPending [[B.ByteString]]+-- A pipeline has an MVar for the current state, this state is actually always+-- `Pending` because the first thing the implementation does when executing a+-- pipeline is to take the current pipeline state out of the MVar and replace+-- it with a new `Pending` state. The executed state is held on to by the+-- replies within it.++newtype Pipeline = Pipeline (MVar PipelineState)++data NodeRole = Master | Slave deriving (Show, Eq, Ord)++type Host = String+type Port = Int+type NodeID = B.ByteString+data Node = Node NodeID NodeRole Host Port deriving (Show, Eq, Ord)++type MasterNode = Node+type SlaveNode = Node+data Shard = Shard MasterNode [SlaveNode] deriving (Show, Eq, Ord)++newtype ShardMap = ShardMap (IntMap.IntMap Shard) deriving (Show)++newtype MissingNodeException = MissingNodeException [B.ByteString] deriving (Show, Typeable)+instance Exception MissingNodeException++newtype UnsupportedClusterCommandException = UnsupportedClusterCommandException [B.ByteString] deriving (Show, Typeable)+instance Exception UnsupportedClusterCommandException++newtype CrossSlotException = CrossSlotException [[B.ByteString]] deriving (Show, Typeable)+instance Exception CrossSlotException++connect :: [CMD.CommandInfo] -> MVar ShardMap -> Maybe Int -> IO Connection+connect commandInfos shardMapVar timeoutOpt = do+        shardMap <- readMVar shardMapVar+        stateVar <- newMVar $ Pending []+        pipelineVar <- newMVar $ Pipeline stateVar+        nodeConns <- nodeConnections shardMap+        return $ Connection nodeConns pipelineVar shardMapVar (CMD.newInfoMap commandInfos) where+    nodeConnections :: ShardMap -> IO (HM.HashMap NodeID NodeConnection)+    nodeConnections shardMap = HM.fromList <$> mapM connectNode (nub $ nodes shardMap)+    connectNode :: Node -> IO (NodeID, NodeConnection)+    connectNode (Node n _ host port) = do+        ctx <- CC.connect host (CC.PortNumber $ toEnum port) timeoutOpt+        ref <- IOR.newIORef Nothing+        return (n, NodeConnection ctx ref n)++disconnect :: Connection -> IO ()+disconnect (Connection nodeConnMap _ _ _) = mapM_ disconnectNode (HM.elems nodeConnMap) where+    disconnectNode (NodeConnection nodeCtx _ _) = CC.disconnect nodeCtx++-- Add a request to the current pipeline for this connection. The pipeline will+-- be executed implicitly as soon as any result returned from this function is+-- evaluated.+requestPipelined :: IO ShardMap -> Connection -> [B.ByteString] -> IO Reply+requestPipelined refreshAction conn@(Connection _ pipelineVar shardMapVar _) nextRequest = modifyMVar pipelineVar $ \(Pipeline stateVar) -> do+    (newStateVar, repliesIndex) <- hasLocked $ modifyMVar stateVar $ \case+        Pending requests | isMulti nextRequest -> do+            replies <- evaluatePipeline shardMapVar refreshAction conn requests+            s' <- newMVar $ TransactionPending [nextRequest]+            return (Executed replies, (s', 0))+        Pending requests | length requests > 1000 -> do+            replies <- evaluatePipeline shardMapVar refreshAction conn (nextRequest:requests)+            return (Executed replies, (stateVar, length requests))+        Pending requests ->+            return (Pending (nextRequest:requests), (stateVar, length requests))+        TransactionPending requests ->+            if isExec nextRequest then do+              replies <- evaluateTransactionPipeline shardMapVar refreshAction conn (nextRequest:requests)+              return (Executed replies, (stateVar, length requests))+            else+              return (TransactionPending (nextRequest:requests), (stateVar, length requests))+        e@(Executed _) -> do+            s' <- newMVar $+                    if isMulti nextRequest then+                        TransactionPending [nextRequest]+                    else+                        Pending [nextRequest]+            return (e, (s', 0))+    evaluateAction <- unsafeInterleaveIO $ do+        replies <- hasLocked $ modifyMVar newStateVar $ \case+            Executed replies ->+                return (Executed replies, replies)+            Pending requests-> do+                replies <- evaluatePipeline shardMapVar refreshAction conn requests+                return (Executed replies, replies)+            TransactionPending requests-> do+                replies <- evaluateTransactionPipeline shardMapVar refreshAction conn requests+                return (Executed replies, replies)+        return $ replies !! repliesIndex+    return (Pipeline newStateVar, evaluateAction)++isMulti :: [B.ByteString] -> Bool+isMulti ("MULTI" : _) = True+isMulti _ = False++isExec :: [B.ByteString] -> Bool+isExec ("EXEC" : _) = True+isExec _ = False++data PendingRequest = PendingRequest Int [B.ByteString]+data CompletedRequest = CompletedRequest Int [B.ByteString] Reply++rawRequest :: PendingRequest -> [B.ByteString]+rawRequest (PendingRequest _ r) =  r++responseIndex :: CompletedRequest -> Int+responseIndex (CompletedRequest i _ _) = i++rawResponse :: CompletedRequest -> Reply+rawResponse (CompletedRequest _ _ r) = r++-- The approach we take here is similar to that taken by the redis-py-cluster+-- library, which is described at https://redis-py-cluster.readthedocs.io/en/master/pipelines.html+--+-- Essentially we group all the commands by node (based on the current shardmap)+-- and then execute a pipeline for each node (maintaining the order of commands+-- on a per node basis but not between nodes). Once we've done this, if any of+-- the commands have resulted in a MOVED error we refresh the shard map, then+-- we run through all the responses and retry any MOVED or ASK errors. This retry+-- step is not pipelined, there is a request per error. This is probably+-- acceptable in most cases as these errors should only occur in the case of+-- cluster reconfiguration events, which should be rare.+evaluatePipeline :: MVar ShardMap -> IO ShardMap -> Connection -> [[B.ByteString]] -> IO [Reply]+evaluatePipeline shardMapVar refreshShardmapAction conn requests = do+        shardMap <- hasLocked $ readMVar shardMapVar+        requestsByNode <- getRequestsByNode shardMap+        resps <- concat <$> mapM (uncurry executeRequests) requestsByNode+        when (any (moved . rawResponse) resps) refreshShardMapVar+        retriedResps <- mapM (retry 0) resps+        return $ map rawResponse $ sortBy (on compare responseIndex) retriedResps+  where+    getRequestsByNode :: ShardMap -> IO [(NodeConnection, [PendingRequest])]+    getRequestsByNode shardMap = do+        commandsWithNodes <- zipWithM (requestWithNodes shardMap) (reverse [0..(length requests - 1)]) requests+        return $ assocs $ fromListWith (++) (mconcat commandsWithNodes)+    requestWithNodes :: ShardMap -> Int -> [B.ByteString] -> IO [(NodeConnection, [PendingRequest])]+    requestWithNodes shardMap index request = do+        nodeConns <- nodeConnectionForCommand conn shardMap request+        return $ (, [PendingRequest index request]) <$> nodeConns+    executeRequests :: NodeConnection -> [PendingRequest] -> IO [CompletedRequest]+    executeRequests nodeConn nodeRequests = do+        replies <- requestNode nodeConn $ map rawRequest nodeRequests+        return $ zipWith (curry (\(PendingRequest i r, rep) -> CompletedRequest i r rep)) nodeRequests replies+    retry :: Int -> CompletedRequest -> IO CompletedRequest+    retry retryCount (CompletedRequest index request thisReply) = do+        retryReply <- head <$> retryBatch shardMapVar refreshShardmapAction conn retryCount [request] [thisReply]+        return (CompletedRequest index request retryReply)+    refreshShardMapVar :: IO ()+    refreshShardMapVar = hasLocked $ modifyMVar_ shardMapVar (const refreshShardmapAction)++-- Retry a batch of requests if any of the responses is a redirect instruction.+-- If multiple requests are passed in they're assumed to be a MULTI..EXEC+-- transaction and will all be retried.+retryBatch :: MVar ShardMap -> IO ShardMap -> Connection -> Int -> [[B.ByteString]] -> [Reply] -> IO [Reply]+retryBatch shardMapVar refreshShardmapAction conn retryCount requests replies =+    -- The last reply will be the `EXEC` reply containing the redirection, if+    -- there is one.+    case last replies of+        (Error errString) | B.isPrefixOf "MOVED" errString -> do+            let (Connection _ _ _ infoMap) = conn+            keys <- mconcat <$> mapM (requestKeys infoMap) requests+            hashSlot <- hashSlotForKeys (CrossSlotException requests) keys+            nodeConn <- nodeConnForHashSlot shardMapVar conn (MissingNodeException (head requests)) hashSlot+            requestNode nodeConn requests+        (askingRedirection -> Just (host, port)) -> do+            shardMap <- hasLocked $ readMVar shardMapVar+            let maybeAskNode = nodeConnWithHostAndPort shardMap conn host port+            case maybeAskNode of+                Just askNode -> tail <$> requestNode askNode (["ASKING"] : requests)+                Nothing -> case retryCount of+                    0 -> do+                        _ <- hasLocked $ modifyMVar_ shardMapVar (const refreshShardmapAction)+                        retryBatch shardMapVar refreshShardmapAction conn (retryCount + 1) requests replies+                    _ -> throwIO $ MissingNodeException (head requests)+        _ -> return replies++-- Like `evaluateOnPipeline`, except we expect to be able to run all commands+-- on a single shard. Failing to meet this expectation is an error.+evaluateTransactionPipeline :: MVar ShardMap -> IO ShardMap -> Connection -> [[B.ByteString]] -> IO [Reply]+evaluateTransactionPipeline shardMapVar refreshShardmapAction conn requests' = do+    let requests = reverse requests'+    let (Connection _ _ _ infoMap) = conn+    keys <- mconcat <$> mapM (requestKeys infoMap) requests+    -- In cluster mode Redis expects commands in transactions to all work on the+    -- same hashslot. We find that hashslot here.+    -- We could be more permissive and allow transactions that touch multiple+    -- hashslots, as long as those hashslots are on the same node. This allows+    -- a new failure case though: if some of the transactions hashslots are+    -- moved to a different node we could end up in a situation where some of+    -- the commands in a transaction are applied and some are not. Better to+    -- fail early.+    hashSlot <- hashSlotForKeys (CrossSlotException requests) keys+    nodeConn <- nodeConnForHashSlot shardMapVar conn (MissingNodeException (head requests)) hashSlot+    resps <- requestNode nodeConn requests+    -- The Redis documentation has the following to say on the effect of+    -- resharding on multi-key operations:+    --+    --     Multi-key operations may become unavailable when a resharding of the+    --     hash slot the keys belong to is in progress.+    --+    --     More specifically, even during a resharding the multi-key operations+    --     targeting keys that all exist and all still hash to the same slot+    --     (either the source or destination node) are still available.+    --+    --     Operations on keys that don't exist or are - during the resharding -+    --     split between the source and destination nodes, will generate a+    --     -TRYAGAIN error. The client can try the operation after some time,+    --     or report back the error.+    --+    --     https://redis.io/topics/cluster-spec#multiple-keys-operations+    --+    -- An important take-away here is that MULTI..EXEC transactions can fail+    -- with a redirect in which case we need to repeat the full transaction on+    -- the node we're redirected too.+    --+    -- A second important takeway is that MULTI..EXEC transactions might+    -- temporarily fail during resharding with a -TRYAGAIN error. We can only+    -- make arbitrary decisions about how long to paus before the retry and how+    -- often to retry, so instead we'll propagate the error to the library user+    -- and let them decide how they would like to handle the error.+    when (any moved resps)+      (hasLocked $ modifyMVar_ shardMapVar (const refreshShardmapAction))+    retriedResps <- retryBatch shardMapVar refreshShardmapAction conn 0 requests resps+    return retriedResps++nodeConnForHashSlot :: Exception e => MVar ShardMap -> Connection -> e -> HashSlot -> IO NodeConnection+nodeConnForHashSlot shardMapVar conn exception hashSlot = do+    let (Connection nodeConns _ _ _) = conn+    (ShardMap shardMap) <- hasLocked $ readMVar shardMapVar+    node <-+        case IntMap.lookup (fromEnum hashSlot) shardMap of+            Nothing -> throwIO exception+            Just (Shard master _) -> return master+    case HM.lookup (nodeId node) nodeConns of+        Nothing -> throwIO exception+        Just nodeConn' -> return nodeConn'++hashSlotForKeys :: Exception e => e -> [B.ByteString] -> IO HashSlot+hashSlotForKeys exception keys =+    case nub (keyToSlot <$> keys) of+        -- If none of the commands contain a key we can send them to any+        -- node. Let's pick the first one.+        [] -> return 0+        [hashSlot] -> return hashSlot+        _ -> throwIO $ exception++requestKeys :: CMD.InfoMap -> [B.ByteString] -> IO [B.ByteString]+requestKeys infoMap request =+    case CMD.keysForRequest infoMap request of+        Nothing -> throwIO $ UnsupportedClusterCommandException request+        Just k -> return k++askingRedirection :: Reply -> Maybe (Host, Port)+askingRedirection (Error errString) = case Char8.words errString of+    ["ASK", _, hostport] -> case Char8.split ':' hostport of+       [host, portString] -> case Char8.readInt portString of+         Just (port,"") -> Just (Char8.unpack host, port)+         _ -> Nothing+       _ -> Nothing+    _ -> Nothing+askingRedirection _ = Nothing++moved :: Reply -> Bool+moved (Error errString) = case Char8.words errString of+    "MOVED":_ -> True+    _ -> False+moved _ = False+++nodeConnWithHostAndPort :: ShardMap -> Connection -> Host -> Port -> Maybe NodeConnection+nodeConnWithHostAndPort shardMap (Connection nodeConns _ _ _) host port = do+    node <- nodeWithHostAndPort shardMap host port+    HM.lookup (nodeId node) nodeConns++nodeConnectionForCommand :: Connection -> ShardMap -> [B.ByteString] -> IO [NodeConnection]+nodeConnectionForCommand conn@(Connection nodeConns _ _ infoMap) (ShardMap shardMap) request =+    case request of+        ("FLUSHALL" : _) -> allNodes+        ("FLUSHDB" : _) -> allNodes+        ("QUIT" : _) -> allNodes+        ("UNWATCH" : _) -> allNodes+        _ -> do+            keys <- requestKeys infoMap request+            hashSlot <- hashSlotForKeys (CrossSlotException [request]) keys+            node <- case IntMap.lookup (fromEnum hashSlot) shardMap of+                Nothing -> throwIO $ MissingNodeException request+                Just (Shard master _) -> return master+            maybe (throwIO $ MissingNodeException request) (return . return) (HM.lookup (nodeId node) nodeConns)+    where+        allNodes =+            case allMasterNodes conn (ShardMap shardMap) of+                Nothing -> throwIO $ MissingNodeException request+                Just allNodes' -> return allNodes'++allMasterNodes :: Connection -> ShardMap -> Maybe [NodeConnection]+allMasterNodes (Connection nodeConns _ _ _) (ShardMap shardMap) =+    mapM (flip HM.lookup nodeConns . nodeId) masterNodes+  where+    masterNodes = (\(Shard master _) -> master) <$> nub (IntMap.elems shardMap)++requestNode :: NodeConnection -> [[B.ByteString]] -> IO [Reply]+requestNode (NodeConnection ctx lastRecvRef _) requests = do+    mapM_ (sendNode . renderRequest) requests+    _ <- CC.flush ctx+    replicateM (length requests) recvNode++    where++    sendNode :: B.ByteString -> IO ()+    sendNode = CC.send ctx+    recvNode :: IO Reply+    recvNode = do+        maybeLastRecv <- IOR.readIORef lastRecvRef+        scanResult <- case maybeLastRecv of+            Just lastRecv -> Scanner.scanWith (CC.recv ctx) reply lastRecv+            Nothing -> Scanner.scanWith (CC.recv ctx) reply B.empty++        case scanResult of+          Scanner.Fail{}       -> CC.errConnClosed+          Scanner.More{}    -> error "Hedis: parseWith returned Partial"+          Scanner.Done rest' r -> do+            IOR.writeIORef lastRecvRef (Just rest')+            return r++nodes :: ShardMap -> [Node]+nodes (ShardMap shardMap) = concatMap snd $ IntMap.toList $ fmap shardNodes shardMap where+    shardNodes :: Shard -> [Node]+    shardNodes (Shard master slaves) = master:slaves+++nodeWithHostAndPort :: ShardMap -> Host -> Port -> Maybe Node+nodeWithHostAndPort shardMap host port = find (\(Node _ _ nodeHost nodePort) -> port == nodePort && host == nodeHost) (nodes shardMap)++nodeId :: Node -> NodeID+nodeId (Node theId _ _ _) = theId++hasLocked :: IO a -> IO a+hasLocked action =+  action `catches`+  [ Handler $ \exc@BlockedIndefinitelyOnMVar -> throwIO exc+  ]
+ src/Database/Redis/Cluster/Command.hs view
@@ -0,0 +1,163 @@+{-# LANGUAGE OverloadedStrings, RecordWildCards #-}+module Database.Redis.Cluster.Command where++import Data.Char(toLower)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as Char8+import qualified Data.HashMap.Strict as HM+import Database.Redis.Types(RedisResult(decode))+import Database.Redis.Protocol(Reply(..))++data Flag+    = Write+    | ReadOnly+    | DenyOOM+    | Admin+    | PubSub+    | NoScript+    | Random+    | SortForScript+    | Loading+    | Stale+    | SkipMonitor+    | Asking+    | Fast+    | MovableKeys+    | Other BS.ByteString deriving (Show, Eq)+++data AritySpec = Required Integer | MinimumRequired Integer deriving (Show)++data LastKeyPositionSpec = LastKeyPosition Integer | UnlimitedKeys Integer deriving (Show)++newtype InfoMap = InfoMap (HM.HashMap String CommandInfo)++-- Represents the result of the COMMAND command, which returns information+-- about the position of keys in a request+data CommandInfo = CommandInfo+    { name :: BS.ByteString+    , arity :: AritySpec+    , flags :: [Flag]+    , firstKeyPosition :: Integer+    , lastKeyPosition :: LastKeyPositionSpec+    , stepCount :: Integer+    } deriving (Show)++instance RedisResult CommandInfo where+    decode (MultiBulk (Just+        [ Bulk (Just commandName)+        , Integer aritySpec+        , MultiBulk (Just replyFlags)+        , Integer firstKeyPos+        , Integer lastKeyPos+        , Integer replyStepCount])) = do+            parsedFlags <- mapM parseFlag replyFlags+            lastKey <- parseLastKeyPos+            return $ CommandInfo+                { name = commandName+                , arity = parseArity aritySpec+                , flags = parsedFlags+                , firstKeyPosition = firstKeyPos+                , lastKeyPosition = lastKey+                , stepCount = replyStepCount+                } where+        parseArity int = case int of+            i | i >= 0 -> Required i+            i -> MinimumRequired $ abs i+        parseFlag :: Reply -> Either Reply Flag+        parseFlag (SingleLine flag) = return $ case flag of+            "write" -> Write+            "readonly" -> ReadOnly+            "denyoom" -> DenyOOM+            "admin" -> Admin+            "pubsub" -> PubSub+            "noscript" -> NoScript+            "random" -> Random+            "sort_for_script" -> SortForScript+            "loading" -> Loading+            "stale" -> Stale+            "skip_monitor" -> SkipMonitor+            "asking" -> Asking+            "fast" -> Fast+            "movablekeys" -> MovableKeys+            other -> Other other+        parseFlag bad = Left bad+        parseLastKeyPos :: Either Reply LastKeyPositionSpec+        parseLastKeyPos = return $ case lastKeyPos of+            i | i < 0 -> UnlimitedKeys (-i - 1)+            i -> LastKeyPosition i++    decode e = Left e++newInfoMap :: [CommandInfo] -> InfoMap+newInfoMap = InfoMap . HM.fromList . map (\c -> (Char8.unpack $ name c, c))++keysForRequest :: InfoMap -> [BS.ByteString] -> Maybe [BS.ByteString]+keysForRequest _ ["DEBUG", "OBJECT", key] =+    -- `COMMAND` output for `DEBUG` would let us believe it doesn't have any+    -- keys, but the `DEBUG OBJECT` subcommand does.+    Just [key]+keysForRequest _ ["QUIT"] =+    -- The `QUIT` command is not listed in the `COMMAND` output.+    Just []+keysForRequest (InfoMap infoMap) request@(command:_) = do+    info <- HM.lookup (map toLower $ Char8.unpack command) infoMap+    keysForRequest' info request+keysForRequest _ [] = Nothing++keysForRequest' :: CommandInfo -> [BS.ByteString] -> Maybe [BS.ByteString]+keysForRequest' info request+    | isMovable info =+        parseMovable request+    | stepCount info == 0 =+        Just []+    | otherwise = do+        let possibleKeys = case lastKeyPosition info of+                LastKeyPosition end -> take (fromEnum $ 1 + end - firstKeyPosition info) $ drop (fromEnum $ firstKeyPosition info) request+                UnlimitedKeys end ->+                    drop (fromEnum $ firstKeyPosition info) $+                       take (length request - fromEnum end) request+        return $ takeEvery (fromEnum $ stepCount info) possibleKeys++isMovable :: CommandInfo -> Bool+isMovable CommandInfo{..} = MovableKeys `elem` flags++parseMovable :: [BS.ByteString] -> Maybe [BS.ByteString]+parseMovable ("SORT":key:_) = Just [key]+parseMovable ("EVAL":_:rest) = readNumKeys rest+parseMovable ("EVALSHA":_:rest) = readNumKeys rest+parseMovable ("ZUNIONSTORE":_:rest) = readNumKeys rest+parseMovable ("ZINTERSTORE":_:rest) = readNumKeys rest+parseMovable ("XREAD":rest) = readXreadKeys rest+parseMovable ("XREADGROUP":"GROUP":_:_:rest) = readXreadgroupKeys rest+parseMovable _ = Nothing++readXreadKeys :: [BS.ByteString] -> Maybe [BS.ByteString]+readXreadKeys ("COUNT":_:rest) = readXreadKeys rest+readXreadKeys ("BLOCK":_:rest) = readXreadKeys rest+readXreadKeys ("STREAMS":rest) = Just $ take (length rest `div` 2) rest+readXreadKeys _ = Nothing++readXreadgroupKeys :: [BS.ByteString] -> Maybe [BS.ByteString]+readXreadgroupKeys ("COUNT":_:rest) = readXreadKeys rest+readXreadgroupKeys ("BLOCK":_:rest) = readXreadKeys rest+readXreadgroupKeys ("NOACK":rest) = readXreadKeys rest+readXreadgroupKeys ("STREAMS":rest) = Just $ take (length rest `div` 2) rest+readXreadgroupKeys _ = Nothing++readNumKeys :: [BS.ByteString] -> Maybe [BS.ByteString]+readNumKeys (rawNumKeys:rest) = do+    numKeys <- readMaybe (Char8.unpack rawNumKeys)+    return $ take numKeys rest+readNumKeys _ = Nothing+-- takeEvery 1 [1,2,3,4,5] ->[1,2,3,4,5]+-- takeEvery 2 [1,2,3,4,5] ->[1,3,5]+-- takeEvery 3 [1,2,3,4,5] ->[1,4]+takeEvery :: Int -> [a] -> [a]+takeEvery _ [] = []+takeEvery n (x:xs) = x : takeEvery n (drop (n-1) xs)++readMaybe :: Read a => String -> Maybe a+readMaybe s = case reads s of+                  [(val, "")] -> Just val+                  _           -> Nothing
+ src/Database/Redis/Cluster/HashSlot.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+module Database.Redis.Cluster.HashSlot(HashSlot, keyToSlot) where++import Data.Bits((.&.), xor, shiftL)+import qualified Data.ByteString.Char8 as Char8+import qualified Data.ByteString as BS+import Data.Word(Word8, Word16)++newtype HashSlot = HashSlot Word16 deriving (Num, Eq, Ord, Real, Enum, Integral, Show)++numHashSlots :: Word16+numHashSlots = 16384++-- | Compute the hashslot associated with a key+keyToSlot :: BS.ByteString -> HashSlot+keyToSlot = HashSlot . (.&.) (numHashSlots - 1) . crc16 . findSubKey++-- | Find the section of a key to compute the slot for.+findSubKey :: BS.ByteString -> BS.ByteString+findSubKey key = case Char8.break (=='{') key of+  (whole, "") -> whole+  (_, xs) -> case Char8.break (=='}') (Char8.tail xs) of+    ("", _) -> key+    (subKey, _) -> subKey++crc16 :: BS.ByteString -> Word16+crc16 = BS.foldl (crc16Update 0x1021) 0++-- Taken from crc16 package+crc16Update :: Word16  -- ^ polynomial+            -> Word16 -- ^ initial crc+            -> Word8 -- ^ data byte+            -> Word16 -- ^ new crc+crc16Update poly crc b = +  foldl crc16UpdateBit newCrc [1 :: Int .. 8]+  where +    newCrc = crc `xor` shiftL (fromIntegral b :: Word16) 8+    crc16UpdateBit crc' _ =+      if (crc' .&. 0x8000) /= 0x0000+          then shiftL crc' 1 `xor` poly+          else shiftL crc' 1+ 
src/Database/Redis/Commands.hs view
@@ -261,7 +261,20 @@ xdel, -- |Delete messages from a stream. Since Redis 5.0.0 xtrim, -- |Set the upper bound for number of messages in a stream. Since Redis 5.0.0 inf, -- |Constructor for `inf` Redis argument values-+ClusterNodesResponse(..),+ClusterNodesResponseEntry(..),+ClusterNodesResponseSlotSpec(..),+clusterNodes,+ClusterSlotsResponse(..),+ClusterSlotsResponseEntry(..),+ClusterSlotsNode(..),+clusterSlots,+clusterSetSlotNode,+clusterSetSlotStable,+clusterSetSlotImporting,+clusterSetSlotMigrating,+clusterGetKeysInSlot,+command -- * Unimplemented Commands -- |These commands are not implemented, as of now. Library --  users can implement these or other commands from@@ -306,7 +319,7 @@ import Data.ByteString (ByteString) import Database.Redis.ManualCommands import Database.Redis.Types-import Database.Redis.Core+import Database.Redis.Core(sendRequest, RedisCtx)  ttl     :: (RedisCtx m f)@@ -1080,4 +1093,3 @@     -> ByteString -- ^ member     -> m (f Bool) sismember key member = sendRequest (["SISMEMBER"] ++ [encode key] ++ [encode member] )-
+ src/Database/Redis/Connection.hs view
@@ -0,0 +1,239 @@+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+module Database.Redis.Connection where++import Control.Exception+import qualified Control.Monad.Catch as Catch+import Control.Monad.IO.Class(liftIO, MonadIO)+import Control.Monad(when)+import Control.Concurrent.MVar(MVar, newMVar)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as Char8+import Data.Functor(void)+import qualified Data.IntMap.Strict as IntMap+import Data.Pool(Pool, withResource, createPool, destroyAllResources)+import Data.Typeable+import qualified Data.Time as Time+import Network.TLS (ClientParams)+import qualified Network.Socket as NS+import qualified Data.HashMap.Strict as HM++import qualified Database.Redis.ProtocolPipelining as PP+import Database.Redis.Core(Redis, runRedisInternal, runRedisClusteredInternal)+import Database.Redis.Protocol(Reply(..))+import Database.Redis.Cluster(ShardMap(..), Node, Shard(..))+import qualified Database.Redis.Cluster as Cluster+import qualified Database.Redis.ConnectionContext as CC+--import qualified Database.Redis.Cluster.Pipeline as ClusterPipeline+import Database.Redis.Commands+    ( ping+    , select+    , auth+    , clusterSlots+    , command+    , ClusterSlotsResponse(..)+    , ClusterSlotsResponseEntry(..)+    , ClusterSlotsNode(..))++--------------------------------------------------------------------------------+-- Connection+--++-- |A threadsafe pool of network connections to a Redis server. Use the+--  'connect' function to create one.+data Connection+    = NonClusteredConnection (Pool PP.Connection)+    | ClusteredConnection (MVar ShardMap) (Pool Cluster.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           :: NS.HostName+    , connectPort           :: CC.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    :: Time.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'.+    , connectTimeout        :: Maybe Time.NominalDiffTime+    -- ^ Optional timeout until connection to Redis gets+    --   established. 'ConnectTimeoutException' gets thrown if no socket+    --   get connected in this interval of time.+    , connectTLSParams      :: Maybe ClientParams+    -- ^ Optional TLS parameters. TLS will be enabled if this is provided.+    } deriving Show++data ConnectError = ConnectAuthError Reply+                  | ConnectSelectError Reply+    deriving (Eq, Show, Typeable)++instance Exception ConnectError++-- |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+--  connectTimeout        = Nothing         -- Don't add timeout logic+--  connectTLSParams      = Nothing         -- Do not use TLS+-- @+--+defaultConnectInfo :: ConnectInfo+defaultConnectInfo = ConnInfo+    { connectHost           = "localhost"+    , connectPort           = CC.PortNumber 6379+    , connectAuth           = Nothing+    , connectDatabase       = 0+    , connectMaxConnections = 50+    , connectMaxIdleTime    = 30+    , connectTimeout        = Nothing+    , connectTLSParams      = Nothing+    }++createConnection :: ConnectInfo -> IO PP.Connection+createConnection ConnInfo{..} = do+    let timeoutOptUs =+          round . (1000000 *) <$> connectTimeout+    conn <- PP.connect connectHost connectPort timeoutOptUs+    conn' <- case connectTLSParams of+               Nothing -> return conn+               Just tlsParams -> PP.enableTLS tlsParams conn+    PP.beginReceiving conn'++    runRedisInternal conn' $ do+        -- AUTH+        case connectAuth of+            Nothing   -> return ()+            Just pass -> do+              resp <- auth pass+              case resp of+                Left r -> liftIO $ throwIO $ ConnectAuthError r+                _      -> return ()+        -- SELECT+        when (connectDatabase /= 0) $ do+          resp <- select connectDatabase+          case resp of+              Left r -> liftIO $ throwIO $ ConnectSelectError r+              _      -> return ()+    return conn'++-- |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 cInfo@ConnInfo{..} = NonClusteredConnection <$>+    createPool (createConnection cInfo) PP.disconnect 1 connectMaxIdleTime connectMaxConnections++-- |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++-- |Destroy all idle resources in the pool.+disconnect :: Connection -> IO ()+disconnect (NonClusteredConnection pool) = destroyAllResources pool+disconnect (ClusteredConnection _ pool) = destroyAllResources pool++-- | Memory bracket around 'connect' and 'disconnect'.+withConnect :: (Catch.MonadMask m, MonadIO m) => ConnectInfo -> (Connection -> m c) -> m c+withConnect connInfo = Catch.bracket (liftIO $ connect connInfo) (liftIO . disconnect)++-- | Memory bracket around 'checkedConnect' and 'disconnect'+withCheckedConnect :: ConnectInfo -> (Connection -> IO c) -> IO c+withCheckedConnect connInfo = bracket (checkedConnect connInfo) disconnect++-- |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 (NonClusteredConnection pool) redis =+  withResource pool $ \conn -> runRedisInternal conn redis+runRedis (ClusteredConnection _ pool) redis =+    withResource pool $ \conn -> runRedisClusteredInternal conn (refreshShardMap conn) redis++newtype ClusterConnectError = ClusterConnectError Reply+    deriving (Eq, Show, Typeable)++instance Exception ClusterConnectError++-- |Constructs a 'ShardMap' of connections to clustered nodes. The argument is+-- a 'ConnectInfo' for any node in the cluster+--+-- Some Redis commands are currently not supported in cluster mode+-- - CONFIG, AUTH+-- - SCAN+-- - MOVE, SELECT+-- - PUBLISH, SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, RESET+connectCluster :: ConnectInfo -> IO Connection+connectCluster bootstrapConnInfo = do+    conn <- createConnection bootstrapConnInfo+    slotsResponse <- runRedisInternal conn clusterSlots+    shardMapVar <- case slotsResponse of+        Left e -> throwIO $ ClusterConnectError e+        Right slots -> do+            shardMap <- shardMapFromClusterSlotsResponse slots+            newMVar shardMap+    commandInfos <- runRedisInternal conn command+    case commandInfos of+        Left e -> throwIO $ ClusterConnectError e+        Right infos -> do+            pool <- createPool (Cluster.connect infos shardMapVar Nothing) Cluster.disconnect 1 (connectMaxIdleTime bootstrapConnInfo) (connectMaxConnections bootstrapConnInfo)+            return $ ClusteredConnection shardMapVar pool++shardMapFromClusterSlotsResponse :: ClusterSlotsResponse -> IO ShardMap+shardMapFromClusterSlotsResponse ClusterSlotsResponse{..} = ShardMap <$> foldr mkShardMap (pure IntMap.empty)  clusterSlotsResponseEntries where+    mkShardMap :: ClusterSlotsResponseEntry -> IO (IntMap.IntMap Shard) -> IO (IntMap.IntMap Shard)+    mkShardMap ClusterSlotsResponseEntry{..} accumulator = do+        accumulated <- accumulator+        let master = nodeFromClusterSlotNode True clusterSlotsResponseEntryMaster+        let replicas = map (nodeFromClusterSlotNode False) clusterSlotsResponseEntryReplicas+        let shard = Shard master replicas+        let slotMap = IntMap.fromList $ map (, shard) [clusterSlotsResponseEntryStartSlot..clusterSlotsResponseEntryEndSlot]+        return $ IntMap.union slotMap accumulated+    nodeFromClusterSlotNode :: Bool -> ClusterSlotsNode -> Node+    nodeFromClusterSlotNode isMaster ClusterSlotsNode{..} =+        let hostname = Char8.unpack clusterSlotsNodeIP+            role = if isMaster then Cluster.Master else Cluster.Slave+        in+            Cluster.Node clusterSlotsNodeID role hostname (toEnum clusterSlotsNodePort)++refreshShardMap :: Cluster.Connection -> IO ShardMap+refreshShardMap (Cluster.Connection nodeConns _ _ _) = do+    let (Cluster.NodeConnection ctx _ _) = head $ HM.elems nodeConns+    pipelineConn <- PP.fromCtx ctx+    _ <- PP.beginReceiving pipelineConn+    slotsResponse <- runRedisInternal pipelineConn clusterSlots+    case slotsResponse of+        Left e -> throwIO $ ClusterConnectError e+        Right slots -> shardMapFromClusterSlotsResponse slots
+ src/Database/Redis/ConnectionContext.hs view
@@ -0,0 +1,162 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+module Database.Redis.ConnectionContext (+    ConnectionContext(..)+  , ConnectTimeout(..)+  , ConnectionLostException(..)+  , PortID(..)+  , connect+  , disconnect+  , send+  , recv+  , errConnClosed+  , enableTLS+  , flush+  , ioErrorToConnLost+) where++import           Control.Concurrent (threadDelay)+import           Control.Concurrent.Async (race)+import Control.Monad(when)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as LB+import qualified Data.IORef as IOR+import Control.Concurrent.MVar(newMVar, readMVar, swapMVar)+import Control.Exception(bracketOnError, Exception, throwIO, try)+import           Data.Typeable+import Data.Functor(void)+import qualified Network.Socket as NS+import qualified Network.TLS as TLS+import System.IO(Handle, hSetBinaryMode, hClose, IOMode(..), hFlush, hIsOpen)+import System.IO.Error(catchIOError)++data ConnectionContext = NormalHandle Handle | TLSContext TLS.Context++instance Show ConnectionContext where+    show (NormalHandle _) = "NormalHandle"+    show (TLSContext _) = "TLSContext"++data Connection = Connection+    { ctx :: ConnectionContext+    , lastRecvRef :: IOR.IORef (Maybe B.ByteString) }++instance Show Connection where+    show Connection{..} = "Connection{ ctx = " ++ show ctx ++ ", lastRecvRef = IORef}"++data ConnectPhase+  = PhaseUnknown+  | PhaseResolve+  | PhaseOpenSocket+  deriving (Show)++newtype ConnectTimeout = ConnectTimeout ConnectPhase+  deriving (Show, Typeable)++instance Exception ConnectTimeout++data ConnectionLostException = ConnectionLost deriving Show+instance Exception ConnectionLostException++data PortID = PortNumber NS.PortNumber+            | UnixSocket String+            deriving (Eq, Show)++connect :: NS.HostName -> PortID -> Maybe Int -> IO ConnectionContext+connect hostName portId timeoutOpt =+  bracketOnError hConnect hClose $ \h -> do+    hSetBinaryMode h True+    return $ NormalHandle h+  where+        hConnect = do+          phaseMVar <- newMVar PhaseUnknown+          let doConnect = hConnect' phaseMVar+          case timeoutOpt of+            Nothing -> doConnect+            Just micros -> do+              result <- race doConnect (threadDelay micros)+              case result of+                Left h -> return h+                Right () -> do+                  phase <- readMVar phaseMVar+                  errConnectTimeout phase+        hConnect' mvar = bracketOnError createSock NS.close $ \sock -> do+          NS.setSocketOption sock NS.KeepAlive 1+          void $ swapMVar mvar PhaseResolve+          void $ swapMVar mvar PhaseOpenSocket+          NS.socketToHandle sock ReadWriteMode+          where+            createSock = case portId of+              PortNumber portNumber -> do+                addrInfo <- getHostAddrInfo hostName portNumber+                connectSocket addrInfo+              UnixSocket addr -> bracketOnError+                (NS.socket NS.AF_UNIX NS.Stream NS.defaultProtocol)+                NS.close+                (\sock -> NS.connect sock (NS.SockAddrUnix addr) >> return sock)++getHostAddrInfo :: NS.HostName -> NS.PortNumber -> IO [NS.AddrInfo]+getHostAddrInfo hostname port =+  NS.getAddrInfo (Just hints) (Just hostname) (Just $ show port)+  where+    hints = NS.defaultHints+      { NS.addrSocketType = NS.Stream }++errConnectTimeout :: ConnectPhase -> IO a+errConnectTimeout phase = throwIO $ ConnectTimeout phase++connectSocket :: [NS.AddrInfo] -> IO NS.Socket+connectSocket [] = error "connectSocket: unexpected empty list"+connectSocket (addr:rest) = tryConnect >>= \case+  Right sock -> return sock+  Left err   -> if null rest+                then throwIO err+                else connectSocket rest+  where+    tryConnect :: IO (Either IOError NS.Socket)+    tryConnect = bracketOnError createSock NS.close $ \sock ->+      try (NS.connect sock $ NS.addrAddress addr) >>= \case+      Right () -> return (Right sock)+      Left err -> NS.close sock >> return (Left err)+      where+        createSock = NS.socket (NS.addrFamily addr)+                               (NS.addrSocketType addr)+                               (NS.addrProtocol addr)++send :: ConnectionContext -> B.ByteString -> IO ()+send (NormalHandle h) requestData =+      ioErrorToConnLost (B.hPut h requestData)+send (TLSContext ctx) requestData =+        ioErrorToConnLost (TLS.sendData ctx (LB.fromStrict requestData))++recv :: ConnectionContext -> IO B.ByteString+recv (NormalHandle h) = ioErrorToConnLost $ B.hGetSome h 4096+recv (TLSContext ctx) = TLS.recvData ctx+++ioErrorToConnLost :: IO a -> IO a+ioErrorToConnLost a = a `catchIOError` const errConnClosed++errConnClosed :: IO a+errConnClosed = throwIO ConnectionLost+++enableTLS :: TLS.ClientParams -> ConnectionContext -> IO ConnectionContext+enableTLS tlsParams (NormalHandle h) = do+  ctx <- TLS.contextNew h tlsParams+  TLS.handshake ctx+  return $ TLSContext ctx+enableTLS _ c@(TLSContext _) = return c++disconnect :: ConnectionContext -> IO ()+disconnect (NormalHandle h) = do+  open <- hIsOpen h+  when open $ hClose h+disconnect (TLSContext ctx) = do+  TLS.bye ctx+  TLS.contextClose ctx++flush :: ConnectionContext -> IO ()+flush (NormalHandle h) = hFlush h+flush (TLSContext c) = TLS.contextFlush c
src/Database/Redis/Core.hs view
@@ -3,34 +3,27 @@     DeriveDataTypeable, StandaloneDeriving #-}  module Database.Redis.Core (-    Connection(..), ConnectError(..), connect, checkedConnect, disconnect,-    withConnect, withCheckedConnect,-    ConnectInfo(..), defaultConnectInfo,-    Redis(), runRedis, unRedis, reRedis,+    Redis(), unRedis, reRedis,     RedisCtx(..), MonadRedis(..),     send, recv, sendRequest,-    auth, select, ping+    runRedisInternal,+    runRedisClusteredInternal,+    RedisEnv(..), ) where  import Prelude #if __GLASGOW_HASKELL__ < 710 import Control.Applicative #endif-import Control.Exception import Control.Monad.Reader-import qualified Control.Monad.Catch as Catch import qualified Data.ByteString as B import Data.IORef-import Data.Pool-import Data.Time-import Data.Typeable-import qualified Network.Socket as NS-import Network.TLS (ClientParams) import Database.Redis.Core.Internal import Database.Redis.Protocol import qualified Database.Redis.ProtocolPipelining as PP import Database.Redis.Types-+import Database.Redis.Cluster(ShardMap)+import qualified Database.Redis.Cluster as Cluster  -------------------------------------------------------------------------------- -- The Redis Monad@@ -44,29 +37,21 @@ 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 RedisCtx Redis (Either Reply) where+    returnDecode = return . decode+ 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@@ -82,11 +67,17 @@ 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)+  r <- runReaderT redis (NonClusteredEnv conn ref)   -- Evaluate last reply to keep lazy IO inside runRedis.   readIORef ref >>= (`seq` return ())   return r +runRedisClusteredInternal :: Cluster.Connection -> IO ShardMap -> Redis a -> IO a+runRedisClusteredInternal connection refreshShardmapAction (Redis redis) = do+    r <- runReaderT redis (ClusteredEnv refreshShardmapAction connection)+    r `seq` return ()+    return r+ setLastReply :: Reply -> ReaderT RedisEnv IO () setLastReply r = do   ref <- asks envLastReply@@ -118,161 +109,11 @@     => [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+        env <- ask+        case env of+            NonClusteredEnv{..} -> do+                r <- liftIO $ PP.request envConn (renderRequest req)+                setLastReply r+                return r+            ClusteredEnv{..} -> liftIO $ Cluster.requestPipelined refreshAction connection req     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           :: NS.HostName-    , connectPort           :: PP.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'.-    , connectTimeout        :: Maybe NominalDiffTime-    -- ^ Optional timeout until connection to Redis gets-    --   established. 'ConnectTimeoutException' gets thrown if no socket-    --   get connected in this interval of time.-    , connectTLSParams      :: Maybe ClientParams-    -- ^ Optional TLS parameters. TLS will be enabled if this is provided.-    } deriving Show--data ConnectError = ConnectAuthError Reply-                  | ConnectSelectError Reply-    deriving (Eq, Show, Typeable)--instance Exception ConnectError---- |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---  connectTimeout        = Nothing         -- Don't add timeout logic---  connectTLSParams      = Nothing         -- Do not use TLS--- @----defaultConnectInfo :: ConnectInfo-defaultConnectInfo = ConnInfo-    { connectHost           = "localhost"-    , connectPort           = PP.PortNumber 6379-    , connectAuth           = Nothing-    , connectDatabase       = 0-    , connectMaxConnections = 50-    , connectMaxIdleTime    = 30-    , connectTimeout        = Nothing-    , connectTLSParams      = Nothing-    }---- |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-        let timeoutOptUs =-              round . (1000000 *) <$> connectTimeout-        conn <- PP.connect connectHost connectPort timeoutOptUs-        conn' <- case connectTLSParams of-                   Nothing -> return conn-                   Just tlsParams -> PP.enableTLS tlsParams conn-        PP.beginReceiving conn'--        runRedisInternal conn' $ do-            -- AUTH-            case connectAuth of-                Nothing   -> return ()-                Just pass -> do-                  resp <- auth pass-                  case resp of-                    Left r -> liftIO $ throwIO $ ConnectAuthError r-                    _      -> return ()-            -- SELECT-            when (connectDatabase /= 0) $ do-              resp <- select connectDatabase-              case resp of-                  Left r -> liftIO $ throwIO $ ConnectSelectError r-                  _      -> return ()-        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---- |Destroy all idle resources in the pool.-disconnect :: Connection -> IO ()-disconnect (Conn pool) = destroyAllResources pool---- | Memory bracket around 'connect' and 'disconnect'. -withConnect :: (Catch.MonadMask m, MonadIO m) => ConnectInfo -> (Connection -> m c) -> m c-withConnect connInfo = Catch.bracket (liftIO $ connect connInfo) (liftIO . disconnect)---- | Memory bracket around 'checkedConnect' and 'disconnect'-withCheckedConnect :: (Catch.MonadMask m, MonadIO m) => ConnectInfo -> (Connection -> m c) -> m c-withCheckedConnect connInfo = Catch.bracket (liftIO $ checkedConnect connInfo) (liftIO . disconnect)---- 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"] )
src/Database/Redis/Core/Internal.hs view
@@ -10,6 +10,7 @@ import Data.IORef import Database.Redis.Protocol import qualified Database.Redis.ProtocolPipelining as PP+import qualified Database.Redis.Cluster as Cluster  -- |Context for normal command execution, outside of transactions. Use --  'runRedis' to run actions of this type.@@ -22,8 +23,9 @@ #if __GLASGOW_HASKELL__ > 711 deriving instance MonadFail Redis #endif-data RedisEnv =-  Env-    { envConn :: PP.Connection-    , envLastReply :: IORef Reply-    }+data RedisEnv+    = NonClusteredEnv { envConn :: PP.Connection, envLastReply :: IORef Reply }+    | ClusteredEnv+        { refreshAction :: IO Cluster.ShardMap+        , connection :: Cluster.Connection+        }
src/Database/Redis/ManualCommands.hs view
@@ -4,13 +4,16 @@  import Prelude hiding (min, max) import Data.ByteString (ByteString, empty, append)-import Data.Maybe (maybeToList)+import qualified Data.ByteString.Char8 as Char8+import qualified Data.ByteString as BS+import Data.Maybe (maybeToList, catMaybes) #if __GLASGOW_HASKELL__ < 808 import Data.Semigroup ((<>)) #endif import Database.Redis.Core import Database.Redis.Protocol import Database.Redis.Types+import qualified Database.Redis.Cluster.Command as CMD   objectRefcount@@ -1215,3 +1218,181 @@  inf :: RealFloat a => a inf = 1 / 0++auth+    :: RedisCtx m f+    => ByteString -- ^ password+    -> m (f 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"] )++data ClusterNodesResponse = ClusterNodesResponse+    { clusterNodesResponseEntries :: [ClusterNodesResponseEntry]+    } deriving (Show, Eq)++data ClusterNodesResponseEntry = ClusterNodesResponseEntry { clusterNodesResponseNodeId :: ByteString+    , clusterNodesResponseNodeIp :: ByteString+    , clusterNodesResponseNodePort :: Integer+    , clusterNodesResponseNodeFlags :: [ByteString]+    , clusterNodesResponseMasterId :: Maybe ByteString+    , clusterNodesResponsePingSent :: Integer+    , clusterNodesResponsePongReceived :: Integer+    , clusterNodesResponseConfigEpoch :: Integer+    , clusterNodesResponseLinkState :: ByteString+    , clusterNodesResponseSlots :: [ClusterNodesResponseSlotSpec]+    } deriving (Show, Eq)++data ClusterNodesResponseSlotSpec+    = ClusterNodesResponseSingleSlot Integer+    | ClusterNodesResponseSlotRange Integer Integer+    | ClusterNodesResponseSlotImporting Integer ByteString+    | ClusterNodesResponseSlotMigrating Integer ByteString deriving (Show, Eq)+++instance RedisResult ClusterNodesResponse where+    decode r@(Bulk (Just bulkData)) = maybe (Left r) Right $ do+        infos <- mapM parseNodeInfo $ Char8.lines bulkData+        return $ ClusterNodesResponse infos where+            parseNodeInfo :: ByteString -> Maybe ClusterNodesResponseEntry+            parseNodeInfo line = case Char8.words line of+              (nodeId : hostNamePort : flags : masterNodeId : pingSent : pongRecv : epoch : linkState : slots) ->+                case Char8.split ':' hostNamePort of+                  [hostName, port] -> ClusterNodesResponseEntry <$> pure nodeId+                                               <*> pure hostName+                                               <*> readInteger port+                                               <*> pure (Char8.split ',' flags)+                                               <*> pure (readMasterNodeId masterNodeId)+                                               <*> readInteger pingSent+                                               <*> readInteger pongRecv+                                               <*> readInteger epoch+                                               <*> pure linkState+                                               <*> (pure . catMaybes $ map readNodeSlot slots)+                  _ -> Nothing+              _ -> Nothing+            readInteger :: ByteString -> Maybe Integer+            readInteger = fmap fst . Char8.readInteger++            readMasterNodeId :: ByteString -> Maybe ByteString+            readMasterNodeId "-"    = Nothing+            readMasterNodeId nodeId = Just nodeId++            readNodeSlot :: ByteString -> Maybe ClusterNodesResponseSlotSpec+            readNodeSlot slotSpec = case '[' `Char8.elem` slotSpec of+                True -> readSlotImportMigrate slotSpec+                False -> case '-' `Char8.elem` slotSpec of+                    True -> readSlotRange slotSpec+                    False -> ClusterNodesResponseSingleSlot <$> readInteger slotSpec+            readSlotImportMigrate :: ByteString -> Maybe ClusterNodesResponseSlotSpec+            readSlotImportMigrate slotSpec = case BS.breakSubstring "->-" slotSpec of+                (_, "") -> case BS.breakSubstring "-<-" slotSpec of+                    (_, "") -> Nothing+                    (leftPart, rightPart) -> ClusterNodesResponseSlotImporting+                        <$> (readInteger $ Char8.drop 1 leftPart)+                        <*> (pure $ BS.take (BS.length rightPart - 1) rightPart)+                (leftPart, rightPart) -> ClusterNodesResponseSlotMigrating+                    <$> (readInteger $ Char8.drop 1 leftPart)+                    <*> (pure $ BS.take (BS.length rightPart - 1) rightPart)+            readSlotRange :: ByteString -> Maybe ClusterNodesResponseSlotSpec+            readSlotRange slotSpec = case BS.breakSubstring "-" slotSpec of+                (_, "") -> Nothing+                (leftPart, rightPart) -> ClusterNodesResponseSlotRange+                    <$> readInteger leftPart+                    <*> (readInteger $ BS.drop 1 rightPart)++    decode r = Left r++clusterNodes+    :: (RedisCtx m f)+    => m (f ClusterNodesResponse)+clusterNodes = sendRequest $ ["CLUSTER", "NODES"]++data ClusterSlotsResponse = ClusterSlotsResponse { clusterSlotsResponseEntries :: [ClusterSlotsResponseEntry] } deriving (Show)++data ClusterSlotsNode = ClusterSlotsNode+    { clusterSlotsNodeIP :: ByteString+    , clusterSlotsNodePort :: Int+    , clusterSlotsNodeID :: ByteString+    } deriving (Show)++data ClusterSlotsResponseEntry = ClusterSlotsResponseEntry+    { clusterSlotsResponseEntryStartSlot :: Int+    , clusterSlotsResponseEntryEndSlot :: Int+    , clusterSlotsResponseEntryMaster :: ClusterSlotsNode+    , clusterSlotsResponseEntryReplicas :: [ClusterSlotsNode]+    } deriving (Show)++instance RedisResult ClusterSlotsResponse where+    decode (MultiBulk (Just bulkData)) = do+        clusterSlotsResponseEntries <- mapM decode bulkData+        return ClusterSlotsResponse{..}+    decode a = Left a++instance RedisResult ClusterSlotsResponseEntry where+    decode (MultiBulk (Just+        ((Integer startSlot):(Integer endSlot):masterData:replicas))) = do+            clusterSlotsResponseEntryMaster <- decode masterData+            clusterSlotsResponseEntryReplicas <- mapM decode replicas+            let clusterSlotsResponseEntryStartSlot = fromInteger startSlot+            let clusterSlotsResponseEntryEndSlot = fromInteger endSlot+            return ClusterSlotsResponseEntry{..}+    decode a = Left a++instance RedisResult ClusterSlotsNode where+    decode (MultiBulk (Just ((Bulk (Just clusterSlotsNodeIP)):(Integer port):(Bulk (Just clusterSlotsNodeID)):_))) = Right ClusterSlotsNode{..}+        where clusterSlotsNodePort = fromInteger port+    decode a = Left a+++clusterSlots+    :: (RedisCtx m f)+    => m (f ClusterSlotsResponse)+clusterSlots = sendRequest $ ["CLUSTER", "SLOTS"]++clusterSetSlotImporting+    :: (RedisCtx m f)+    => Integer+    -> ByteString+    -> m (f Status)+clusterSetSlotImporting slot sourceNodeId = sendRequest $ ["CLUSTER", "SETSLOT", (encode slot), "IMPORTING", sourceNodeId]++clusterSetSlotMigrating+    :: (RedisCtx m f)+    => Integer+    -> ByteString+    -> m (f Status)+clusterSetSlotMigrating slot destinationNodeId = sendRequest $ ["CLUSTER", "SETSLOT", (encode slot), "MIGRATING", destinationNodeId]++clusterSetSlotStable+    :: (RedisCtx m f)+    => Integer+    -> m (f Status)+clusterSetSlotStable slot = sendRequest $ ["CLUSTER", "SETSLOT", "STABLE", (encode slot)]++clusterSetSlotNode+    :: (RedisCtx m f)+    => Integer+    -> ByteString+    -> m (f Status)+clusterSetSlotNode slot node = sendRequest ["CLUSTER", "SETSLOT", (encode slot), "NODE", node]++clusterGetKeysInSlot+    :: (RedisCtx m f)+    => Integer+    -> Integer+    -> m (f [ByteString])+clusterGetKeysInSlot slot count = sendRequest ["CLUSTER", "GETKEYSINSLOT", (encode slot), (encode count)]++command :: (RedisCtx m f) => m (f [CMD.CommandInfo])+command = sendRequest ["COMMAND"]
src/Database/Redis/ProtocolPipelining.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -18,39 +16,23 @@ -- module Database.Redis.ProtocolPipelining (   Connection,-  connect, enableTLS, beginReceiving, disconnect, request, send, recv, flush,-  ConnectionLostException(..),-  ConnectTimeout(..),-  PortID(..)+  connect, enableTLS, beginReceiving, disconnect, request, send, recv, flush, fromCtx ) where  import           Prelude-import           Control.Concurrent (threadDelay)-import           Control.Concurrent.Async (race)-import           Control.Concurrent.MVar-import           Control.Exception import           Control.Monad import qualified Scanner import qualified Data.ByteString as S-import qualified Data.ByteString.Lazy as L import           Data.IORef-import           Data.Typeable import qualified Network.Socket as NS import qualified Network.TLS as TLS-import           System.IO-import           System.IO.Error import           System.IO.Unsafe  import           Database.Redis.Protocol--data PortID = PortNumber NS.PortNumber-            | UnixSocket String-            deriving (Eq, Show)--data ConnectionContext = NormalHandle Handle | TLSContext TLS.Context+import qualified Database.Redis.ConnectionContext as CC  data Connection = Conn-  { connCtx        :: ConnectionContext -- ^ Connection socket-handle.+  { connCtx        :: CC.ConnectionContext -- ^ Connection socket-handle.   , connReplies    :: IORef [Reply] -- ^ Reply thunks for unsent requests.   , connPending    :: IORef [Reply]     -- ^ Reply thunks for requests "in the pipeline". Refers to the same list as@@ -61,92 +43,22 @@     --   length connPending  - pendingCount = length connReplies   } -data ConnectionLostException = ConnectionLost-  deriving (Show, Typeable) -instance Exception ConnectionLostException--data ConnectPhase-  = PhaseUnknown-  | PhaseResolve-  | PhaseOpenSocket-  deriving (Show)--data ConnectTimeout = ConnectTimeout ConnectPhase-  deriving (Show, Typeable)--instance Exception ConnectTimeout--getHostAddrInfo :: NS.HostName -> NS.PortNumber -> IO [NS.AddrInfo]-getHostAddrInfo hostname port = do-  NS.getAddrInfo (Just hints) (Just hostname) (Just $ show port)-  where-    hints = NS.defaultHints-      { NS.addrSocketType = NS.Stream }--connectSocket :: [NS.AddrInfo] -> IO NS.Socket-connectSocket [] = error "connectSocket: unexpected empty list"-connectSocket (addr:rest) = tryConnect >>= \case-  Right sock -> return sock-  Left err   -> if null rest-                then throwIO err-                else connectSocket rest-  where-    tryConnect :: IO (Either IOError NS.Socket)-    tryConnect = bracketOnError createSock NS.close $ \sock -> do-      try (NS.connect sock $ NS.addrAddress addr) >>= \case-        Right () -> return (Right sock)-        Left err -> NS.close sock >> return (Left err)-      where-        createSock = NS.socket (NS.addrFamily addr)-                               (NS.addrSocketType addr)-                               (NS.addrProtocol addr)+fromCtx :: CC.ConnectionContext -> IO Connection+fromCtx ctx = Conn ctx <$> newIORef [] <*> newIORef [] <*> newIORef 0 -connect :: NS.HostName -> PortID -> Maybe Int -> IO Connection-connect hostName portId timeoutOpt =-  bracketOnError hConnect hClose $ \h -> do-    hSetBinaryMode h True+connect :: NS.HostName -> CC.PortID -> Maybe Int -> IO Connection+connect hostName portId timeoutOpt = do+    connCtx <- CC.connect hostName portId timeoutOpt     connReplies <- newIORef []     connPending <- newIORef []     connPendingCnt <- newIORef 0-    let connCtx = NormalHandle h     return Conn{..}-  where-        hConnect = do-          phaseMVar <- newMVar PhaseUnknown-          let doConnect = hConnect' phaseMVar-          case timeoutOpt of-            Nothing -> doConnect-            Just micros -> do-              result <- race doConnect (threadDelay micros)-              case result of-                Left h -> return h-                Right () -> do-                  phase <- readMVar phaseMVar-                  errConnectTimeout phase-        hConnect' mvar = bracketOnError createSock NS.close $ \sock -> do-          NS.setSocketOption sock NS.KeepAlive 1-          void $ swapMVar mvar PhaseResolve-          void $ swapMVar mvar PhaseOpenSocket-          NS.socketToHandle sock ReadWriteMode-          where-            createSock = case portId of-              PortNumber portNumber -> do-                addrInfo <- getHostAddrInfo hostName portNumber-                connectSocket addrInfo-              UnixSocket addr -> bracketOnError-                (NS.socket NS.AF_UNIX NS.Stream NS.defaultProtocol)-                NS.close-                (\sock -> NS.connect sock (NS.SockAddrUnix addr) >> return sock)  enableTLS :: TLS.ClientParams -> Connection -> IO Connection enableTLS tlsParams conn@Conn{..} = do-  case connCtx of-    NormalHandle h -> do-      ctx <- TLS.contextNew h tlsParams-      TLS.handshake ctx-      return $ conn { connCtx = TLSContext ctx }-    TLSContext _ -> return conn+    newCtx <- CC.enableTLS tlsParams connCtx+    return conn{connCtx = newCtx}  beginReceiving :: Connection -> IO () beginReceiving conn = do@@ -155,25 +67,13 @@   writeIORef (connPending conn) rs  disconnect :: Connection -> IO ()-disconnect Conn{..} = do-  case connCtx of-    NormalHandle h -> do-      open <- hIsOpen h-      when open $ hClose h-    TLSContext ctx -> do-      TLS.bye ctx-      TLS.contextClose ctx+disconnect Conn{..} = CC.disconnect connCtx  -- |Write the request to the socket output buffer, without actually sending. --  The 'Handle' is 'hFlush'ed when reading replies from the 'connCtx'. send :: Connection -> S.ByteString -> IO () send Conn{..} s = do-  case connCtx of-    NormalHandle h ->-      ioErrorToConnLost $ S.hPut h s--    TLSContext ctx ->-      ioErrorToConnLost $ TLS.sendData ctx (L.fromStrict s)+  CC.send connCtx s    -- Signal that we expect one more reply from Redis.   n <- atomicModifyIORef' connPendingCnt $ \n -> let n' = n+1 in (n', n')@@ -196,10 +96,7 @@ -- for the multithreaded pub/sub code, the sending thread needs to explicitly flush the subscription -- change requests. flush :: Connection -> IO ()-flush Conn{..} =-  case connCtx of-    NormalHandle h -> hFlush h-    TLSContext ctx -> TLS.contextFlush ctx+flush Conn{..} = CC.flush connCtx  -- |Send a request and receive the corresponding reply request :: Connection -> S.ByteString -> IO Reply@@ -227,7 +124,7 @@         previous `seq` return ()         scanResult <- Scanner.scanWith readMore reply rest         case scanResult of-          Scanner.Fail{}       -> errConnClosed+          Scanner.Fail{}       -> CC.errConnClosed           Scanner.More{}    -> error "Hedis: parseWith returned Partial"           Scanner.Done rest' r -> do             -- r is the same as 'head' of 'connPending'. Since we just@@ -240,17 +137,6 @@       rs <- unsafeInterleaveIO (go rest' r)       return (r:rs) -    readMore = ioErrorToConnLost $ do+    readMore = CC.ioErrorToConnLost $ do       flush conn-      case connCtx of-        NormalHandle h -> S.hGetSome h 4096-        TLSContext ctx -> TLS.recvData ctx--ioErrorToConnLost :: IO a -> IO a-ioErrorToConnLost a = a `catchIOError` const errConnClosed--errConnClosed :: IO a-errConnClosed = throwIO ConnectionLost--errConnectTimeout :: ConnectPhase -> IO a-errConnectTimeout phase = throwIO $ ConnectTimeout phase+      CC.recv connCtx
src/Database/Redis/PubSub.hs view
@@ -38,6 +38,7 @@ #endif import qualified Data.HashMap.Strict as HM import qualified Database.Redis.Core as Core+import qualified Database.Redis.Connection as Connection import qualified Database.Redis.ProtocolPipelining as PP import Database.Redis.Protocol (Reply(..), renderRequest) import Database.Redis.Types@@ -90,7 +91,7 @@ instance Monoid (Cmd Subscribe a) where   mempty = DoNothing   mappend = (<>)-    + instance Semigroup (Cmd Unsubscribe a) where   (<>) DoNothing x = x   (<>) x DoNothing = x@@ -181,7 +182,7 @@     -> PubSub unsubscribe cs = mempty{ unsubs = Cmd cs } --- |Listen for messages published to channels matching the given patterns +-- |Listen for messages published to channels matching the given patterns --  (<http://redis.io/commands/psubscribe>). psubscribe     :: [ByteString] -- ^ pattern@@ -189,7 +190,7 @@ psubscribe []       = mempty psubscribe ps = mempty{ psubs = Cmd ps } --- |Stop listening for messages posted to channels matching the given patterns +-- |Stop listening for messages posted to channels matching the given patterns --  (<http://redis.io/commands/punsubscribe>). punsubscribe     :: [ByteString] -- ^ pattern@@ -199,11 +200,11 @@ -- |Listens to published messages on subscribed channels and channels matching --  the subscribed patterns. For documentation on the semantics of Redis --  Pub\/Sub see <http://redis.io/topics/pubsub>.---  ---  The given callback function is called for each received message. +--+--  The given callback function is called for each received message. --  Subscription changes are triggered by the returned 'PubSub'. To keep --  subscriptions unchanged, the callback can return 'mempty'.---  +-- --  Example: Subscribe to the \"news\" channel indefinitely. -- --  @@@ -560,13 +561,13 @@ -- and then create a Haskell thread bound to each capability each calling 'pubSubForever' in a loop. -- This will create one network connection per controller/capability and allow you to -- register separate channels and callbacks for each controller, spreading the load across the capabilities.-pubSubForever :: Core.Connection -- ^ The connection pool+pubSubForever :: Connection.Connection -- ^ The connection pool               -> PubSubController -- ^ The controller which keeps track of all subscriptions and handlers               -> IO () -- ^ This action is executed once Redis acknowledges that all the subscriptions in                        -- the controller are now subscribed.  You can use this after an exception (such as                        -- 'ConnectionLost') to signal that all subscriptions are now reactivated.               -> IO ()-pubSubForever (Core.Conn pool) ctrl onInitialLoad = withResource pool $ \rawConn -> do+pubSubForever (Connection.NonClusteredConnection pool) ctrl onInitialLoad = withResource pool $ \rawConn -> do     -- get initial subscriptions and write them into the queue.     atomically $ do       let loop = tryReadTBQueue (sendChanges ctrl) >>=@@ -597,6 +598,7 @@           (Right (Left err)) -> throwIO err           (Left (Left err)) -> throwIO err           _ -> return ()  -- should never happen, since threads exit only with an error+pubSubForever (Connection.ClusteredConnection _ _) _ _ = undefined   ------------------------------------------------------------------------------
src/Database/Redis/URL.hs view
@@ -11,8 +11,8 @@ #if __GLASGOW_HASKELL__ < 808 import Data.Monoid ((<>)) #endif-import Database.Redis.Core (ConnectInfo(..), defaultConnectInfo)-import Database.Redis.ProtocolPipelining+import Database.Redis.Connection (ConnectInfo(..), defaultConnectInfo)+import qualified Database.Redis.ConnectionContext as CC import Network.HTTP.Base import Network.URI (parseURI, uriPath, uriScheme) import Text.Read (readMaybe)@@ -59,7 +59,7 @@         { connectHost = if null h             then connectHost defaultConnectInfo             else h-        , connectPort = maybe (connectPort defaultConnectInfo) (PortNumber . fromIntegral) (port uriAuth)+        , connectPort = maybe (connectPort defaultConnectInfo) (CC.PortNumber . fromIntegral) (port uriAuth)         , connectAuth = C8.pack <$> password uriAuth         , connectDatabase = db         }
+ test/ClusterMain.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import qualified Test.Framework as Test+import Database.Redis+import Tests++main :: IO ()+main = do+    -- We're looking for the cluster on a non-default port to support running+    -- this test in parallel witht the regular non-cluster tests. To quickly+    -- spin up a cluster on this port using docker you can run:+    --+    --     docker run -e "IP=0.0.0.0" -p 7000-7010:7000-7010 grokzen/redis-cluster:5.0.6+    conn <- connectCluster defaultConnectInfo { connectPort = PortNumber 7000 }+    Test.defaultMain (tests conn)++tests :: Connection -> [Test.Test]+tests conn = map ($conn) $ concat+    [ testsMisc, testsKeys, testsStrings, [testHashes], testsLists, testsSets, [testHyperLogLog]+    , testsZSets, [testTransaction], [testScripting]+    , testsConnection, testsServer, [testSScan, testHScan, testZScan], [testZrangelex]+    , [testXAddRead, testXReadGroup, testXRange, testXpending, testXClaim, testXInfo, testXDel, testXTrim]+      -- should always be run last as connection gets closed after it+    , [testQuit]+    ]++testsServer :: [Test]+testsServer =+    [testBgrewriteaof, testFlushall, testSlowlog, testDebugObject]++testsConnection :: [Test]+testsConnection = [ testConnectAuthUnexpected, testConnectDb+                  , testConnectDbUnexisting, testEcho, testPing+                  ]++testsKeys :: [Test]+testsKeys = [ testKeys, testExpireAt, testSortCluster, testGetType, testObject ]++testSortCluster :: Test+testSortCluster = testCase "sort" $ do+    lpush "{same}ids"     ["1","2","3"]                      >>=? 3+    sort "{same}ids" defaultSortOpts                         >>=? ["1","2","3"]+    sortStore "{same}ids" "{same}anotherKey" defaultSortOpts >>=? 3+    let opts = defaultSortOpts { sortOrder = Desc, sortAlpha = True+                               , sortLimit = (1,2)+                               , sortBy    = Nothing+                               , sortGet   = [] }+    sort "{same}ids" opts >>=? ["2", "1"]
+ test/Main.hs view
@@ -0,0 +1,34 @@+module Main (main) where++import qualified Test.Framework as Test+import Database.Redis+import Tests+import PubSubTest++main :: IO ()+main = do+    conn <- connect defaultConnectInfo+    Test.defaultMain (tests conn)++tests :: Connection -> [Test.Test]+tests conn = map ($conn) $ concat+    [ testsMisc, testsKeys, testsStrings, [testHashes], testsLists, testsSets, [testHyperLogLog]+    , testsZSets, [testPubSub], [testTransaction], [testScripting]+    , testsConnection, testsServer, [testScans, testSScan, testHScan, testZScan], [testZrangelex]+    , [testXAddRead, testXReadGroup, testXRange, testXpending, testXClaim, testXInfo, testXDel, testXTrim]+    , testPubSubThreaded+      -- should always be run last as connection gets closed after it+    , [testQuit]+    ]++testsServer :: [Test]+testsServer =+    [testServer, testBgrewriteaof, testFlushall, testInfo, testConfig+    ,testSlowlog, testDebugObject]++testsConnection :: [Test]+testsConnection = [ testConnectAuth, testConnectAuthUnexpected, testConnectDb+                  , testConnectDbUnexisting, testEcho, testPing, testSelect ]++testsKeys :: [Test]+testsKeys = [ testKeys, testKeysNoncluster, testExpireAt, testSort, testGetType, testObject ]
− test/Test.hs
@@ -1,806 +0,0 @@-{-# LANGUAGE CPP, OverloadedStrings, RecordWildCards, LambdaCase #-}-module Main (main) where--#if __GLASGOW_HASKELL__ < 710-import Control.Applicative-import Data.Monoid (mappend)-#endif-import qualified Control.Concurrent.Async as Async-import Control.Exception (try)-import Control.Concurrent-import Control.Monad-import Control.Monad.Trans-import qualified Data.List as L-import Data.Time-import Data.Time.Clock.POSIX-import qualified Test.Framework as Test (Test, defaultMain)-import qualified Test.Framework.Providers.HUnit as Test (testCase)-import qualified Test.HUnit as HUnit--import Database.Redis-import PubSubTest----------------------------------------------------------------------------------- Main and helpers----main :: IO ()-main = do-    conn <- connect defaultConnectInfo-    Test.defaultMain (tests conn)--type Test = Connection -> Test.Test--testCase :: String -> Redis () -> Test-testCase name r conn = Test.testCase name $ do-    withTimeLimit 0.5 $ runRedis conn $ flushdb >>=? Ok >> r-  where-    withTimeLimit limit act = do-        start <- getCurrentTime-        _ <- act-        deltaT <-fmap (`diffUTCTime` start) getCurrentTime-        when (deltaT > limit) $-            putStrLn $ name ++ ": " ++ show deltaT--(>>=?) :: (Eq a, Show a) => Redis (Either Reply a) -> a -> Redis ()-redis >>=? expected = do-    a <- redis-    liftIO $ case a of-        Left reply   -> HUnit.assertFailure $ "Redis error: " ++ show reply-        Right actual -> expected HUnit.@=? actual--assert :: Bool -> Redis ()-assert = liftIO . HUnit.assert----------------------------------------------------------------------------------- Tests----tests :: Connection -> [Test.Test]-tests conn = map ($conn) $ concat-    [ testsMisc, testsKeys, testsStrings, [testHashes], testsLists, testsSets, [testHyperLogLog]-    , testsZSets, [testPubSub], [testTransaction], [testScripting]-    , testsConnection, testsServer, [testScans], [testZrangelex]-    , [testXAddRead, testXReadGroup, testXRange, testXpending, testXClaim, testXInfo, testXDel, testXTrim]-    , testPubSubThreaded-      -- should always be run last as connection gets closed after it-    , [testQuit]-    ]----------------------------------------------------------------------------------- Miscellaneous----testsMisc :: [Test]-testsMisc =-    [ testConstantSpacePipelining, testForceErrorReply, testPipelining-    , testEvalReplies-    ]--testConstantSpacePipelining :: Test-testConstantSpacePipelining = testCase "constant-space pipelining" $ do-    -- This testcase should not exceed the maximum heap size, as set in-    -- the run-test.sh script.-    replicateM_ 100000 ping-    -- If the program didn't crash, pipelining takes constant memory.-    assert True--testForceErrorReply :: Test-testForceErrorReply = testCase "force error reply" $ do-    set "key" "value" >>= \case-      Left _ -> error "impossible"-      _ -> return ()-    -- key is not a hash -> wrong kind of value-    reply <- hkeys "key"-    assert $ case reply of-        Left (Error _) -> True-        _              -> False--testPipelining :: Test-testPipelining = testCase "pipelining" $ do-    let n = 100-    tPipe <- deltaT $ do-        pongs <- replicateM n ping-        assert $ pongs == replicate n (Right Pong)--    tNoPipe <- deltaT $ replicateM_ n (ping >>=? Pong)-    -- pipelining should at least be twice as fast.-    assert $ tNoPipe / tPipe > 2-  where-    deltaT redis = do-        start <- liftIO $ getCurrentTime-        _ <- redis-        liftIO $ fmap (`diffUTCTime` start) getCurrentTime--testEvalReplies :: Test-testEvalReplies conn = testCase "eval unused replies" go conn-  where-    go = do-      _ignored <- set "key" "value"-      (liftIO $ do-         threadDelay $ 10 ^ (5 :: Int)-         mvar <- newEmptyMVar-         _ <--           (Async.wait =<< Async.async (runRedis conn (get "key"))) >>= putMVar mvar-         takeMVar mvar) >>=?-        Just "value"----------------------------------------------------------------------------------- Keys----testsKeys :: [Test]-testsKeys = [ testKeys, testExpireAt, testSort, testGetType, testObject ]--testKeys :: Test-testKeys = testCase "keys" $ do-    set "key" "value"     >>=? Ok-    get "key"             >>=? Just "value"-    exists "key"          >>=? True-    keys "*"              >>=? ["key"]-    randomkey             >>=? Just "key"-    move "key" 13         >>=? True-    select 13             >>=? Ok-    expire "key" 1        >>=? True-    pexpire "key" 1000    >>=? True-    ttl "key" >>= \case-      Left _ -> error "error"-      Right t -> do-        assert $ t `elem` [0..1]-        pttl "key" >>= \case-          Left _ -> error "error"-          Right pt -> do-            assert $ pt `elem` [990..1000]-            persist "key"         >>=? True-            dump "key" >>= \case-              Left _ -> error "impossible"-              Right s -> do-                restore "key'" 0 s    >>=? Ok-                rename "key" "key'"   >>=? Ok-                renamenx "key'" "key" >>=? True-                del ["key"]           >>=? 1-                select 0              >>=? Ok--testExpireAt :: Test-testExpireAt = testCase "expireat" $ do-    set "key" "value"             >>=? Ok-    t <- ceiling . utcTimeToPOSIXSeconds <$> liftIO getCurrentTime-    let expiry = t+1-    expireat "key" expiry         >>=? True-    pexpireat "key" (expiry*1000) >>=? True--testSort :: Test-testSort = testCase "sort" $ do-    lpush "ids"     ["1","2","3"]                >>=? 3-    sort "ids" defaultSortOpts                   >>=? ["1","2","3"]-    sortStore "ids" "anotherKey" defaultSortOpts >>=? 3-    mset-         [("weight_1","1")-         ,("weight_2","2")-         ,("weight_3","3")-         ,("object_1","foo")-         ,("object_2","bar")-         ,("object_3","baz")-         ] >>= \case-      Left _ -> error "error"-      _ -> return ()-    let opts = defaultSortOpts { sortOrder = Desc, sortAlpha = True-                               , sortLimit = (1,2)-                               , sortBy    = Just "weight_*"-                               , sortGet   = ["#", "object_*"] }-    sort "ids" opts >>=? ["2", "bar", "1", "foo"]---testGetType :: Test-testGetType = testCase "getType" $ do-    getType "key"     >>=? None-    forM_ ts $ \(setKey, typ) -> do-        setKey-        getType "key" >>=? typ-        del ["key"]   >>=? 1-  where-    ts = [ (set "key" "value"                         >>=? Ok,   String)-         , (hset "key" "field" "value"                >>=? 1,    Hash)-         , (lpush "key" ["value"]                     >>=? 1,    List)-         , (sadd "key" ["member"]                     >>=? 1,    Set)-         , (zadd "key" [(42,"member"),(12.3,"value")] >>=? 2,    ZSet)-         ]--testObject :: Test-testObject = testCase "object" $ do-    set "key" "value"    >>=? Ok-    objectRefcount "key" >>=? 1-    objectEncoding "key" >>= \case-      Left _ -> error "error"-      _ -> return ()-    objectIdletime "key" >>=? 0----------------------------------------------------------------------------------- Strings----testsStrings :: [Test]-testsStrings = [testStrings, testBitops]--testStrings :: Test-testStrings = testCase "strings" $ do-    setnx "key" "value"               >>=? True-    getset "key" "hello"              >>=? Just "value"-    append "key" "world"              >>=? 10-    strlen "key"                      >>=? 10-    setrange "key" 0 "hello"          >>=? 10-    getrange "key" 0 4                >>=? "hello"-    mset [("k1","v1"), ("k2","v2")]   >>=? Ok-    msetnx [("k1","v1"), ("k2","v2")] >>=? False-    mget ["key"]                      >>=? [Just "helloworld"]-    setex "key" 1 "42"                >>=? Ok-    psetex "key" 1000 "42"            >>=? Ok-    decr "key"                        >>=? 41-    decrby "key" 1                    >>=? 40-    incr "key"                        >>=? 41-    incrby "key" 1                    >>=? 42-    incrbyfloat "key" 1               >>=? 43-    del ["key"]                       >>=? 1-    setbit "key" 42 "1"               >>=? 0-    getbit "key" 42                   >>=? 1-    bitcount "key"                    >>=? 1-    bitcountRange "key" 0 (-1)        >>=? 1--testBitops :: Test-testBitops = testCase "bitops" $ do-    set "k1" "a"               >>=? Ok-    set "k2" "b"               >>=? Ok-    bitopAnd "k3" ["k1", "k2"] >>=? 1-    bitopOr "k3" ["k1", "k2"]  >>=? 1-    bitopXor "k3" ["k1", "k2"] >>=? 1-    bitopNot "k3" "k1"         >>=? 1----------------------------------------------------------------------------------- Hashes----testHashes :: Test-testHashes = testCase "hashes" $ do-    hset "key" "field" "another" >>=? 1-    hset "key" "field" "another" >>=? 0-    hset "key" "field" "value"   >>=? 0-    hsetnx "key" "field" "value" >>=? False-    hexists "key" "field"        >>=? True-    hlen "key"                   >>=? 1-    hget "key" "field"           >>=? Just "value"-    hmget "key" ["field", "-"]   >>=? [Just "value", Nothing]-    hgetall "key"                >>=? [("field","value")]-    hkeys "key"                  >>=? ["field"]-    hvals "key"                  >>=? ["value"]-    hdel "key" ["field"]         >>=? 1-    hmset "key" [("field","40")] >>=? Ok-    hincrby "key" "field" 2      >>=? 42-    hincrbyfloat "key" "field" 2 >>=? 44----------------------------------------------------------------------------------- Lists----testsLists :: [Test]-testsLists =-    [testLists, testBpop]--testLists :: Test-testLists = testCase "lists" $ do-    lpushx "notAKey" "-"          >>=? 0-    rpushx "notAKey" "-"          >>=? 0-    lpush "key" ["value"]         >>=? 1-    lpop "key"                    >>=? Just "value"-    rpush "key" ["value"]         >>=? 1-    rpop "key"                    >>=? Just "value"-    rpush "key" ["v2"]            >>=? 1-    linsertBefore "key" "v2" "v1" >>=? 2-    linsertAfter "key" "v2" "v3"  >>=? 3-    lindex "key" 0                >>=? Just "v1"-    lrange "key" 0 (-1)           >>=? ["v1", "v2", "v3"]-    lset "key" 1 "v2"             >>=? Ok-    lrem "key" 0 "v2"             >>=? 1-    llen "key"                    >>=? 2-    ltrim "key" 0 1               >>=? Ok--testBpop :: Test-testBpop = testCase "blocking push/pop" $ do-    lpush "key" ["v3","v2","v1"] >>=? 3-    blpop ["key"] 1              >>=? Just ("key","v1")-    brpop ["key"] 1              >>=? Just ("key","v3")-    rpush "k1" ["v1","v2"]       >>=? 2-    brpoplpush "k1" "k2" 1       >>=? Just "v2"-    rpoplpush "k1" "k2"          >>=? Just "v1"----------------------------------------------------------------------------------- Sets----testsSets :: [Test]-testsSets = [testSets, testSetAlgebra]--testSets :: Test-testSets = testCase "sets" $ do-    sadd "set" ["member"]       >>=? 1-    sismember "set" "member"    >>=? True-    scard "set"                 >>=? 1-    smembers "set"              >>=? ["member"]-    srandmember "set"           >>=? Just "member"-    spop "set"                  >>=? Just "member"-    srem "set" ["member"]       >>=? 0-    smove "set" "set'" "member" >>=? False-    _ <- sadd "set" ["member1", "member2"]-    (fmap L.sort <$> spopN "set" 2) >>=? ["member1", "member2"]-    _ <- sadd "set" ["member1", "member2"]-    (fmap L.sort <$> srandmemberN "set" 2) >>=? ["member1", "member2"]--testSetAlgebra :: Test-testSetAlgebra = testCase "set algebra" $ do-    sadd "s1" ["member"]          >>=? 1-    sdiff ["s1", "s2"]            >>=? ["member"]-    sunion ["s1", "s2"]           >>=? ["member"]-    sinter ["s1", "s2"]           >>=? []-    sdiffstore "s3" ["s1", "s2"]  >>=? 1-    sunionstore "s3" ["s1", "s2"] >>=? 1-    sinterstore "s3" ["s1", "s2"] >>=? 0----------------------------------------------------------------------------------- Sorted Sets----testsZSets :: [Test]-testsZSets = [testZSets, testZStore]--testZSets :: Test-testZSets = testCase "sorted sets" $ do-    zadd "key" [(1,"v1"),(2,"v2"),(40,"v3")]          >>=? 3-    zcard "key"                                       >>=? 3-    zscore "key" "v3"                                 >>=? Just 40-    zincrby "key" 2 "v3"                              >>=? 42--    zrank "key" "v1"                                  >>=? Just 0-    zrevrank "key" "v1"                               >>=? Just 2-    zcount "key" 10 100                               >>=? 1--    zrange "key" 0 1                                  >>=? ["v1","v2"]-    zrevrange "key" 0 1                               >>=? ["v3","v2"]-    zrangeWithscores "key" 0 1                        >>=? [("v1",1),("v2",2)]-    zrevrangeWithscores "key" 0 1                     >>=? [("v3",42),("v2",2)]-    zrangebyscore "key" 0.5 1.5                       >>=? ["v1"]-    zrangebyscoreWithscores "key" 0.5 1.5             >>=? [("v1",1)]-    zrangebyscoreWithscores "key" (-inf) inf          >>=? [("v1",1.0),("v2",2.0),("v3",42.0)]-    zrangebyscoreLimit "key" 0.5 2.5 0 1              >>=? ["v1"]-    zrangebyscoreWithscoresLimit "key" 0.5 2.5 0 1    >>=? [("v1",1)]-    zrevrangebyscore "key" 1.5 0.5                    >>=? ["v1"]-    zrevrangebyscoreWithscores "key" 1.5 0.5          >>=? [("v1",1)]-    zrevrangebyscoreLimit "key" 2.5 0.5 0 1           >>=? ["v2"]-    zrevrangebyscoreWithscoresLimit "key" 2.5 0.5 0 1 >>=? [("v2",2)]--    zrem "key" ["v2"]                                 >>=? 1-    zremrangebyscore "key" 10 100                     >>=? 1-    zremrangebyrank "key" 0 0                         >>=? 1--testZStore :: Test-testZStore = testCase "zunionstore/zinterstore" $ do-    zadd "k1" [(1, "v1"), (2, "v2")] >>= \case-      Left _ -> error "error"-      _ -> return ()-    zadd "k2" [(2, "v2"), (3, "v3")] >>= \case-      Left _ -> error "error"-      _ -> return ()-    zinterstore "newkey" ["k1","k2"] Sum                >>=? 1-    zinterstoreWeights "newkey" [("k1",1),("k2",2)] Max >>=? 1-    zunionstore "newkey" ["k1","k2"] Sum                >>=? 3-    zunionstoreWeights "newkey" [("k1",1),("k2",2)] Min >>=? 3----------------------------------------------------------------------------------- HyperLogLog-----testHyperLogLog :: Test-testHyperLogLog = testCase "hyperloglog" $ do-  -- test creation-  pfadd "hll1" ["a"] >>= \case-      Left _ -> error "error"-      _ -> return ()-  pfcount ["hll1"] >>=? 1-  -- test cardinality-  pfadd "hll1" ["a"] >>= \case-      Left _ -> error "error"-      _ -> return ()-  pfcount ["hll1"] >>=? 1-  pfadd "hll1" ["b", "c", "foo", "bar"] >>= \case-      Left _ -> error "error"-      _ -> return ()-  pfcount ["hll1"] >>=? 5-  -- test merge-  pfadd "hll2" ["1", "2", "3"] >>= \case-      Left _ -> error "error"-      _ -> return ()-  pfadd "hll3" ["4", "5", "6"] >>= \case-      Left _ -> error "error"-      _ -> return ()-  pfmerge "hll4" ["hll2", "hll3"] >>= \case-      Left _ -> error "error"-      _ -> return ()-  pfcount ["hll4"] >>=? 6-  -- test union cardinality-  pfcount ["hll2", "hll3"] >>=? 6----------------------------------------------------------------------------------- Pub/Sub----testPubSub :: Test-testPubSub conn = testCase "pubSub" go conn-  where-    go = do-        -- producer-        asyncProducer <- liftIO $ Async.async $ do-            runRedis conn $ do-                let t = 10^(5 :: Int)-                liftIO $ threadDelay t-                publish "chan1" "hello" >>=? 1-                liftIO $ threadDelay t-                publish "chan2" "world" >>=? 1-            return ()--        -- consumer-        pubSub (subscribe ["chan1"]) $ \msg -> do-            -- ready for a message-            case msg of-                Message{..} -> return-                    (unsubscribe [msgChannel] `mappend` psubscribe ["chan*"])-                PMessage{..} -> return (punsubscribe [msgPattern])--        pubSub (subscribe [] `mappend` psubscribe []) $ \_ -> do-            liftIO $ HUnit.assertFailure "no subs: should return immediately"-            undefined-        liftIO $ Async.wait asyncProducer------------------------------------------------------------------------------------ Transaction----testTransaction :: Test-testTransaction = testCase "transaction" $ do-    watch ["k1", "k2"] >>=? Ok-    unwatch            >>=? Ok-    set "foo" "foo" >>= \case-      Left _ -> error "error"-      _ -> return ()-    set "bar" "bar" >>= \case-      Left _ -> error "error"-      _ -> return ()-    foobar <- multiExec $ do-        foo <- get "foo"-        bar <- get "bar"-        return $ (,) <$> foo <*> bar-    assert $ foobar == TxSuccess (Just "foo", Just "bar")------------------------------------------------------------------------------------ Scripting----testScripting :: Test-testScripting conn = testCase "scripting" go conn-  where-    go = do-        let script    = "return {false, 42}"-            scriptRes = (False, 42 :: Integer)-        scriptLoad script >>= \case-          Left _ -> error "error"-          Right scriptHash -> do-            eval script [] []                       >>=? scriptRes-            evalsha scriptHash [] []                >>=? scriptRes-            scriptExists [scriptHash, "notAScript"] >>=? [True, False]-            scriptFlush                             >>=? Ok-            -- start long running script from another client-            configSet "lua-time-limit" "100"        >>=? Ok-            evalFinished <- liftIO newEmptyMVar-            asyncScripting <- liftIO $ Async.async $ runRedis conn $ do-                -- we must pattern match to block the thread-                (eval "while true do end" [] []-                    :: Redis (Either Reply Integer)) >>= \case-                    Left _ -> return ()-                    _ -> error "impossible"-                liftIO (putMVar evalFinished ())-                return ()-            liftIO (threadDelay 500000) -- 0.5s-            scriptKill                              >>=? Ok-            () <- liftIO (takeMVar evalFinished)-            liftIO $ Async.wait asyncScripting-            return ()----------------------------------------------------------------------------------- Connection----testsConnection :: [Test]-testsConnection = [ testConnectAuth, testConnectAuthUnexpected, testConnectDb-                  , testConnectDbUnexisting, testEcho, testPing, testSelect ]--testConnectAuth :: Test-testConnectAuth = testCase "connect/auth" $ do-    configSet "requirepass" "pass" >>=? Ok-    liftIO $ do-        c <- checkedConnect defaultConnectInfo { connectAuth = Just "pass" }-        runRedis c (ping >>=? Pong)-    auth "pass"                    >>=? Ok-    configSet "requirepass" ""     >>=? Ok--testConnectAuthUnexpected :: Test-testConnectAuthUnexpected = testCase "connect/auth/unexpected" $ do-    liftIO $ do-        res <- try $ void $ checkedConnect connInfo-        HUnit.assertEqual "" err res--    where connInfo = defaultConnectInfo { connectAuth = Just "pass" }-          err = Left $ ConnectAuthError $-                  Error "ERR AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?"--testConnectDb :: Test-testConnectDb = testCase "connect/db" $ do-    set "connect" "value" >>=? Ok-    liftIO $ void $ do-        c <- checkedConnect defaultConnectInfo { connectDatabase = 1 }-        runRedis c (get "connect" >>=? Nothing)--testConnectDbUnexisting :: Test-testConnectDbUnexisting = testCase "connect/db/unexisting" $ do-    liftIO $ do-        res <- try $ void $ checkedConnect connInfo-        case res of-          Left (ConnectSelectError _) -> return ()-          _ -> HUnit.assertFailure $-                  "Expected ConnectSelectError, got " ++ show res--    where connInfo = defaultConnectInfo { connectDatabase = 100 }--testEcho :: Test-testEcho = testCase "echo" $-    echo ("value" ) >>=? "value"--testPing :: Test-testPing = testCase "ping" $ ping >>=? Pong--testQuit :: Test-testQuit = testCase "quit" $ quit >>=? Ok--testSelect :: Test-testSelect = testCase "select" $ do-    select 13 >>=? Ok-    select 0 >>=? Ok------------------------------------------------------------------------------------ Server----testsServer :: [Test]-testsServer =-    [testServer, testBgrewriteaof, testFlushall, testInfo, testConfig-    ,testSlowlog, testDebugObject]--testServer :: Test-testServer = testCase "server" $ do-    time >>= \case-      Right (_,_) -> return ()-      Left _ -> error "error"-    slaveof "no" "one" >>=? Ok-    return ()--testBgrewriteaof :: Test-testBgrewriteaof = testCase "bgrewriteaof/bgsave/save" $ do-    save >>=? Ok-    bgsave >>= \case-      Right (Status _) -> return ()-      _ -> error "error"-    -- Redis needs time to finish the bgsave-    liftIO $ threadDelay (10^(5 :: Int))-    bgrewriteaof >>= \case-      Right (Status _) -> return ()-      _ -> error "error"-    return ()--testConfig :: Test-testConfig = testCase "config/auth" $ do-    configGet "requirepass"        >>=? [("requirepass", "")]-    configSet "requirepass" "pass" >>=? Ok-    auth "pass"                    >>=? Ok-    configSet "requirepass" ""     >>=? Ok--testFlushall :: Test-testFlushall = testCase "flushall/flushdb" $ do-    flushall >>=? Ok-    flushdb  >>=? Ok--testInfo :: Test-testInfo = testCase "info/lastsave/dbsize" $ do-    info >>= \case-      Left _ -> error "error"-      _ -> return ()-    lastsave >>= \case-      Left _ -> error "error"-      _ -> return ()-    dbsize          >>=? 0-    configResetstat >>=? Ok--testSlowlog :: Test-testSlowlog = testCase "slowlog" $ do-    slowlogReset >>=? Ok-    slowlogGet 5 >>=? []-    slowlogLen   >>=? 0--testDebugObject :: Test-testDebugObject = testCase "debugObject/debugSegfault" $ do-    set "key" "value" >>=? Ok-    debugObject "key" >>= \case-      Left _ -> error "error"-      _ -> return ()-    return ()--testScans :: Test-testScans = testCase "scans" $ do-    set "key" "value"       >>=? Ok-    scan cursor0            >>=? (cursor0, ["key"])-    scanOpts cursor0 sOpts1 >>=? (cursor0, ["key"])-    scanOpts cursor0 sOpts2 >>=? (cursor0, [])-    sadd "set" ["1"]        >>=? 1-    sscan "set" cursor0     >>=? (cursor0, ["1"])-    hset "hash" "k" "v"     >>=? 1-    hscan "hash" cursor0    >>=? (cursor0, [("k", "v")])-    zadd "zset" [(42, "2")] >>=? 1-    zscan "zset" cursor0    >>=? (cursor0, [("2", 42)])-    where sOpts1 = defaultScanOpts { scanMatch = Just "k*" }-          sOpts2 = defaultScanOpts { scanMatch = Just "not*"}--testZrangelex ::Test-testZrangelex = testCase "zrangebylex" $ do-    let testSet = [(10, "aaa"), (10, "abb"), (10, "ccc"), (10, "ddd")]-    zadd "zrangebylex" testSet                          >>=? 4-    zrangebylex "zrangebylex" (Incl "aaa") (Incl "bbb") >>=? ["aaa","abb"]-    zrangebylex "zrangebylex" (Excl "aaa") (Excl "ddd") >>=? ["abb","ccc"]-    zrangebylex "zrangebylex" Minr Maxr                 >>=? ["aaa","abb","ccc","ddd"]-    zrangebylexLimit "zrangebylex" Minr Maxr 2 1        >>=? ["ccc"]--testXAddRead ::Test-testXAddRead = testCase "xadd/xread" $ do-    xadd "somestream" "123" [("key", "value"), ("key2", "value2")]-    xadd "otherstream" "456" [("key1", "value1")]-    xaddOpts "thirdstream" "*" [("k", "v")] (Maxlen 1)-    xaddOpts "thirdstream" "*" [("k", "v")] (ApproxMaxlen 1)-    xread [("somestream", "0"), ("otherstream", "0")] >>=? Just [-        XReadResponse {-            stream = "somestream",-            records = [StreamsRecord{recordId = "123-0", keyValues = [("key", "value"), ("key2", "value2")]}]-        },-        XReadResponse {-            stream = "otherstream",-            records = [StreamsRecord{recordId = "456-0", keyValues = [("key1", "value1")]}]-        }]-    xlen "somestream" >>=? 1--testXReadGroup ::Test-testXReadGroup = testCase "XGROUP */xreadgroup/xack" $ do-    xadd "somestream" "123" [("key", "value")]-    xgroupCreate "somestream" "somegroup" "0"-    xreadGroup "somegroup" "consumer1" [("somestream", ">")] >>=? Just [-        XReadResponse {-            stream = "somestream",-            records = [StreamsRecord{recordId = "123-0", keyValues = [("key", "value")]}]-        }]-    xack "somestream" "somegroup" ["123-0"] >>=? 1-    xreadGroup "somegroup" "consumer1" [("somestream", ">")] >>=? Nothing-    xgroupSetId "somestream" "somegroup" "0" >>=? Ok-    xgroupDelConsumer "somestream" "somegroup" "consumer1" >>=? 0-    xgroupDestroy "somestream" "somegroup" >>=? True--testXRange ::Test-testXRange = testCase "xrange/xrevrange" $ do-    xadd "somestream" "121" [("key1", "value1")]-    xadd "somestream" "122" [("key2", "value2")]-    xadd "somestream" "123" [("key3", "value3")]-    xadd "somestream" "124" [("key4", "value4")]-    xrange "somestream" "122" "123" Nothing >>=? [-        StreamsRecord{recordId = "122-0", keyValues = [("key2", "value2")]},-        StreamsRecord{recordId = "123-0", keyValues = [("key3", "value3")]}-        ]-    xrevRange "somestream" "123" "122" Nothing >>=? [-        StreamsRecord{recordId = "123-0", keyValues = [("key3", "value3")]},-        StreamsRecord{recordId = "122-0", keyValues = [("key2", "value2")]}-        ]--testXpending ::Test-testXpending = testCase "xpending" $ do-    xadd "somestream" "121" [("key1", "value1")]-    xadd "somestream" "122" [("key2", "value2")]-    xadd "somestream" "123" [("key3", "value3")]-    xadd "somestream" "124" [("key4", "value4")]-    xgroupCreate "somestream" "somegroup" "0"-    xreadGroup "somegroup" "consumer1" [("somestream", ">")]-    xpendingSummary "somestream" "somegroup" Nothing >>=? XPendingSummaryResponse {-        numPendingMessages = 4,-        smallestPendingMessageId = "121-0",-        largestPendingMessageId = "124-0",-        numPendingMessagesByconsumer = [("consumer1", 4)]-    }-    detail <- xpendingDetail "somestream" "somegroup" "121" "121" 10 Nothing-    liftIO $ case detail of-        Left reply   -> HUnit.assertFailure $ "Redis error: " ++ show reply-        Right [XPendingDetailRecord{..}] -> do-            messageId HUnit.@=? "121-0"-        Right bad -> HUnit.assertFailure $ "Unexpectedly got " ++ show bad--testXClaim ::Test-testXClaim =-  testCase "xclaim" $ do-    xadd "somestream" "121" [("key1", "value1")] >>=? "121-0"-    xadd "somestream" "122" [("key2", "value2")] >>=? "122-0"-    xgroupCreate "somestream" "somegroup" "0" >>=? Ok-    xreadGroupOpts-      "somegroup"-      "consumer1"-      [("somestream", ">")]-      (defaultXreadOpts {recordCount = Just 2}) >>=?-      Just-        [ XReadResponse-            { stream = "somestream"-            , records =-                [ StreamsRecord-                    {recordId = "121-0", keyValues = [("key1", "value1")]}-                , StreamsRecord-                    {recordId = "122-0", keyValues = [("key2", "value2")]}-                ]-            }-        ]-    xclaim "somestream" "somegroup" "consumer2" 0 defaultXClaimOpts ["121-0"] >>=?-      [StreamsRecord {recordId = "121-0", keyValues = [("key1", "value1")]}]-    xclaimJustIds-      "somestream"-      "somegroup"-      "consumer2"-      0-      defaultXClaimOpts-      ["122-0"] >>=?-      ["122-0"]--testXInfo ::Test-testXInfo = testCase "xinfo" $ do-    xadd "somestream" "121" [("key1", "value1")]-    xadd "somestream" "122" [("key2", "value2")]-    xgroupCreate "somestream" "somegroup" "0"-    xreadGroupOpts "somegroup" "consumer1" [("somestream", ">")] (defaultXreadOpts { recordCount = Just 2})-    consumerInfos <- xinfoConsumers "somestream" "somegroup"-    liftIO $ case consumerInfos of-        Left reply -> HUnit.assertFailure $ "Redis error: " ++ show reply-        Right [XInfoConsumersResponse{..}] -> do-            xinfoConsumerName HUnit.@=? "consumer1"-            xinfoConsumerNumPendingMessages HUnit.@=? 2-        Right bad -> HUnit.assertFailure $ "Unexpectedly got " ++ show bad-    xinfoGroups "somestream" >>=? [-        XInfoGroupsResponse{-            xinfoGroupsGroupName = "somegroup",-            xinfoGroupsNumConsumers = 1,-            xinfoGroupsNumPendingMessages = 2,-            xinfoGroupsLastDeliveredMessageId = "122-0"-        }]-    xinfoStream "somestream" >>=? XInfoStreamResponse-        { xinfoStreamLength = 2-        , xinfoStreamRadixTreeKeys = 1-        , xinfoStreamRadixTreeNodes = 2-        , xinfoStreamNumGroups = 1-        , xinfoStreamLastEntryId = "122-0"-        , xinfoStreamFirstEntry = StreamsRecord-            { recordId = "121-0"-            , keyValues = [("key1", "value1")]-            }-        , xinfoStreamLastEntry = StreamsRecord-            { recordId = "122-0"-            , keyValues = [("key2", "value2")]-            }-        }--testXDel ::Test-testXDel = testCase "xdel" $ do-    xadd "somestream" "121" [("key1", "value1")]-    xadd "somestream" "122" [("key2", "value2")]-    xdel "somestream" ["122"] >>=? 1-    xlen "somestream" >>=? 1--testXTrim ::Test-testXTrim = testCase "xtrim" $ do-    xadd "somestream" "121" [("key1", "value1")]-    xadd "somestream" "122" [("key2", "value2")]-    xadd "somestream" "123" [("key3", "value3")]-    xadd "somestream" "124" [("key4", "value4")]-    xadd "somestream" "125" [("key5", "value5")]-    xtrim "somestream" (Maxlen 2) >>=? 3
+ test/Tests.hs view
@@ -0,0 +1,788 @@+{-# LANGUAGE CPP, OverloadedStrings, RecordWildCards, LambdaCase #-}+module Tests where++#if __GLASGOW_HASKELL__ < 710+import Control.Applicative+import Data.Monoid (mappend)+#endif+import qualified Control.Concurrent.Async as Async+import Control.Exception (try)+import Control.Concurrent+import Control.Monad+import Control.Monad.Trans+import qualified Data.List as L+import Data.Time+import Data.Time.Clock.POSIX+import qualified Test.Framework as Test (Test)+import qualified Test.Framework.Providers.HUnit as Test (testCase)+import qualified Test.HUnit as HUnit++import Database.Redis++------------------------------------------------------------------------------+-- helpers+--+type Test = Connection -> Test.Test++testCase :: String -> Redis () -> Test+testCase name r conn = Test.testCase name $ do+    withTimeLimit 0.5 $ runRedis conn $ flushdb >>=? Ok >> r+  where+    withTimeLimit limit act = do+        start <- getCurrentTime+        _ <- act+        deltaT <-fmap (`diffUTCTime` start) getCurrentTime+        when (deltaT > limit) $+            putStrLn $ name ++ ": " ++ show deltaT++(>>=?) :: (Eq a, Show a) => Redis (Either Reply a) -> a -> Redis ()+redis >>=? expected = do+    a <- redis+    liftIO $ case a of+        Left reply   -> HUnit.assertFailure $ "Redis error: " ++ show reply+        Right actual -> expected HUnit.@=? actual++assert :: Bool -> Redis ()+assert = liftIO . HUnit.assert++------------------------------------------------------------------------------+-- Miscellaneous+--+testsMisc :: [Test]+testsMisc =+    [ testConstantSpacePipelining, testForceErrorReply, testPipelining+    , testEvalReplies+    ]++testConstantSpacePipelining :: Test+testConstantSpacePipelining = testCase "constant-space pipelining" $ do+    -- This testcase should not exceed the maximum heap size, as set in+    -- the run-test.sh script.+    replicateM_ 100000 ping+    -- If the program didn't crash, pipelining takes constant memory.+    assert True++testForceErrorReply :: Test+testForceErrorReply = testCase "force error reply" $ do+    set "key" "value" >>= \case+      Left _ -> error "impossible"+      _ -> return ()+    -- key is not a hash -> wrong kind of value+    reply <- hkeys "key"+    assert $ case reply of+        Left (Error _) -> True+        _              -> False++testPipelining :: Test+testPipelining = testCase "pipelining" $ do+    let n = 100+    tPipe <- deltaT $ do+        pongs <- replicateM n ping+        assert $ pongs == replicate n (Right Pong)++    tNoPipe <- deltaT $ replicateM_ n (ping >>=? Pong)+    -- pipelining should at least be twice as fast.+    assert $ tNoPipe / tPipe > 2+  where+    deltaT redis = do+        start <- liftIO $ getCurrentTime+        _ <- redis+        liftIO $ fmap (`diffUTCTime` start) getCurrentTime++testEvalReplies :: Test+testEvalReplies conn = testCase "eval unused replies" go conn+  where+    go = do+      _ <- liftIO $ runRedis conn $ set "key" "value"+      result <- liftIO $ do+         threadDelay $ 10 ^ (5 :: Int)+         mvar <- newEmptyMVar+         _ <-+           (Async.wait =<< Async.async (runRedis conn (get "key"))) >>= putMVar mvar+         takeMVar mvar+      pure result >>=? Just "value"++------------------------------------------------------------------------------+-- Keys+--+testKeys :: Test+testKeys = testCase "keys" $ do+    set "{same}key" "value"     >>=? Ok+    get "{same}key"             >>=? Just "value"+    exists "{same}key"          >>=? True+    expire "{same}key" 1        >>=? True+    pexpire "{same}key" 1000    >>=? True+    ttl "{same}key" >>= \case+      Left _ -> error "error"+      Right t -> do+        assert $ t `elem` [0..1]+        pttl "{same}key" >>= \case+          Left _ -> error "error"+          Right pt -> do+            assert $ pt `elem` [990..1000]+            persist "{same}key"         >>=? True+            dump "{same}key" >>= \case+              Left _ -> error "impossible"+              Right s -> do+                restore "{same}key'" 0 s          >>=? Ok+                rename "{same}key" "{same}key'"   >>=? Ok+                renamenx "{same}key'" "{same}key" >>=? True+                del ["{same}key"]                 >>=? 1++testKeysNoncluster :: Test+testKeysNoncluster = testCase "keysNoncluster" $ do+    set "key" "value"     >>=? Ok+    keys "*"              >>=? ["key"]+    randomkey             >>=? Just "key"+    move "key" 13         >>=? True+    select 13             >>=? Ok+    get "key"             >>=? Just "value"+    select 0              >>=? Ok++testExpireAt :: Test+testExpireAt = testCase "expireat" $ do+    set "key" "value"             >>=? Ok+    t <- ceiling . utcTimeToPOSIXSeconds <$> liftIO getCurrentTime+    let expiry = t+1+    expireat "key" expiry         >>=? True+    pexpireat "key" (expiry*1000) >>=? True++testSort :: Test+testSort = testCase "sort" $ do+    lpush "ids"     ["1","2","3"]                >>=? 3+    sort "ids" defaultSortOpts                   >>=? ["1","2","3"]+    sortStore "ids" "anotherKey" defaultSortOpts >>=? 3+    mset+         [("weight_1","1")+         ,("weight_2","2")+         ,("weight_3","3")+         ,("object_1","foo")+         ,("object_2","bar")+         ,("object_3","baz")+         ] >>= \case+      Left _ -> error "error"+      _ -> return ()+    let opts = defaultSortOpts { sortOrder = Desc, sortAlpha = True+                               , sortLimit = (1,2)+                               , sortBy    = Just "weight_*"+                               , sortGet   = ["#", "object_*"] }+    sort "ids" opts >>=? ["2", "bar", "1", "foo"]+++testGetType :: Test+testGetType = testCase "getType" $ do+    getType "key"     >>=? None+    forM_ ts $ \(setKey, typ) -> do+        setKey+        getType "key" >>=? typ+        del ["key"]   >>=? 1+  where+    ts = [ (set "key" "value"                         >>=? Ok,   String)+         , (hset "key" "field" "value"                >>=? 1,    Hash)+         , (lpush "key" ["value"]                     >>=? 1,    List)+         , (sadd "key" ["member"]                     >>=? 1,    Set)+         , (zadd "key" [(42,"member"),(12.3,"value")] >>=? 2,    ZSet)+         ]++testObject :: Test+testObject = testCase "object" $ do+    set "key" "value"    >>=? Ok+    objectRefcount "key" >>=? 1+    objectEncoding "key" >>= \case+      Left _ -> error "error"+      _ -> return ()+    objectIdletime "key" >>=? 0++------------------------------------------------------------------------------+-- Strings+--+testsStrings :: [Test]+testsStrings = [testStrings, testBitops]++testStrings :: Test+testStrings = testCase "strings" $ do+    setnx "key" "value"                           >>=? True+    getset "key" "hello"                          >>=? Just "value"+    append "key" "world"                          >>=? 10+    strlen "key"                                  >>=? 10+    setrange "key" 0 "hello"                      >>=? 10+    getrange "key" 0 4                            >>=? "hello"+    mset [("{same}k1","v1"), ("{same}k2","v2")]   >>=? Ok+    msetnx [("{same}k1","v1"), ("{same}k2","v2")] >>=? False+    mget ["key"]                                  >>=? [Just "helloworld"]+    setex "key" 1 "42"                            >>=? Ok+    psetex "key" 1000 "42"                        >>=? Ok+    decr "key"                                    >>=? 41+    decrby "key" 1                                >>=? 40+    incr "key"                                    >>=? 41+    incrby "key" 1                                >>=? 42+    incrbyfloat "key" 1                           >>=? 43+    del ["key"]                                   >>=? 1+    setbit "key" 42 "1"                           >>=? 0+    getbit "key" 42                               >>=? 1+    bitcount "key"                                >>=? 1+    bitcountRange "key" 0 (-1)                    >>=? 1++testBitops :: Test+testBitops = testCase "bitops" $ do+    set "{same}k1" "a"                           >>=? Ok+    set "{same}k2" "b"                           >>=? Ok+    bitopAnd "{same}k3" ["{same}k1", "{same}k2"] >>=? 1+    bitopOr "{same}k3" ["{same}k1", "{same}k2"]  >>=? 1+    bitopXor "{same}k3" ["{same}k1", "{same}k2"] >>=? 1+    bitopNot "{same}k3" "{same}k1"               >>=? 1++------------------------------------------------------------------------------+-- Hashes+--+testHashes :: Test+testHashes = testCase "hashes" $ do+    hset "key" "field" "another" >>=? 1+    hset "key" "field" "another" >>=? 0+    hset "key" "field" "value"   >>=? 0+    hsetnx "key" "field" "value" >>=? False+    hexists "key" "field"        >>=? True+    hlen "key"                   >>=? 1+    hget "key" "field"           >>=? Just "value"+    hmget "key" ["field", "-"]   >>=? [Just "value", Nothing]+    hgetall "key"                >>=? [("field","value")]+    hkeys "key"                  >>=? ["field"]+    hvals "key"                  >>=? ["value"]+    hdel "key" ["field"]         >>=? 1+    hmset "key" [("field","40")] >>=? Ok+    hincrby "key" "field" 2      >>=? 42+    hincrbyfloat "key" "field" 2 >>=? 44++------------------------------------------------------------------------------+-- Lists+--+testsLists :: [Test]+testsLists =+    [testLists, testBpop]++testLists :: Test+testLists = testCase "lists" $ do+    lpushx "notAKey" "-"          >>=? 0+    rpushx "notAKey" "-"          >>=? 0+    lpush "key" ["value"]         >>=? 1+    lpop "key"                    >>=? Just "value"+    rpush "key" ["value"]         >>=? 1+    rpop "key"                    >>=? Just "value"+    rpush "key" ["v2"]            >>=? 1+    linsertBefore "key" "v2" "v1" >>=? 2+    linsertAfter "key" "v2" "v3"  >>=? 3+    lindex "key" 0                >>=? Just "v1"+    lrange "key" 0 (-1)           >>=? ["v1", "v2", "v3"]+    lset "key" 1 "v2"             >>=? Ok+    lrem "key" 0 "v2"             >>=? 1+    llen "key"                    >>=? 2+    ltrim "key" 0 1               >>=? Ok++testBpop :: Test+testBpop = testCase "blocking push/pop" $ do+    lpush "{same}key" ["v3","v2","v1"] >>=? 3+    blpop ["{same}key"] 1              >>=? Just ("{same}key","v1")+    brpop ["{same}key"] 1              >>=? Just ("{same}key","v3")+    rpush "{same}k1" ["v1","v2"]       >>=? 2+    brpoplpush "{same}k1" "{same}k2" 1 >>=? Just "v2"+    rpoplpush "{same}k1" "{same}k2"    >>=? Just "v1"++------------------------------------------------------------------------------+-- Sets+--+testsSets :: [Test]+testsSets = [testSets, testSetAlgebra]++testSets :: Test+testSets = testCase "sets" $ do+    sadd "set" ["member"]       >>=? 1+    sismember "set" "member"    >>=? True+    scard "set"                 >>=? 1+    smembers "set"              >>=? ["member"]+    srandmember "set"           >>=? Just "member"+    spop "set"                  >>=? Just "member"+    srem "set" ["member"]       >>=? 0+    smove "{same}set" "{same}set'" "member" >>=? False+    _ <- sadd "set" ["member1", "member2"]+    (fmap L.sort <$> spopN "set" 2) >>=? ["member1", "member2"]+    _ <- sadd "set" ["member1", "member2"]+    (fmap L.sort <$> srandmemberN "set" 2) >>=? ["member1", "member2"]++testSetAlgebra :: Test+testSetAlgebra = testCase "set algebra" $ do+    sadd "{same}s1" ["member"]                      >>=? 1+    sdiff ["{same}s1", "{same}s2"]                  >>=? ["member"]+    sunion ["{same}s1", "{same}s2"]                 >>=? ["member"]+    sinter ["{same}s1", "{same}s2"]                 >>=? []+    sdiffstore "{same}s3" ["{same}s1", "{same}s2"]  >>=? 1+    sunionstore "{same}s3" ["{same}s1", "{same}s2"] >>=? 1+    sinterstore "{same}s3" ["{same}s1", "{same}s2"] >>=? 0++------------------------------------------------------------------------------+-- Sorted Sets+--+testsZSets :: [Test]+testsZSets = [testZSets, testZStore]++testZSets :: Test+testZSets = testCase "sorted sets" $ do+    zadd "key" [(1,"v1"),(2,"v2"),(40,"v3")]          >>=? 3+    zcard "key"                                       >>=? 3+    zscore "key" "v3"                                 >>=? Just 40+    zincrby "key" 2 "v3"                              >>=? 42++    zrank "key" "v1"                                  >>=? Just 0+    zrevrank "key" "v1"                               >>=? Just 2+    zcount "key" 10 100                               >>=? 1++    zrange "key" 0 1                                  >>=? ["v1","v2"]+    zrevrange "key" 0 1                               >>=? ["v3","v2"]+    zrangeWithscores "key" 0 1                        >>=? [("v1",1),("v2",2)]+    zrevrangeWithscores "key" 0 1                     >>=? [("v3",42),("v2",2)]+    zrangebyscore "key" 0.5 1.5                       >>=? ["v1"]+    zrangebyscoreWithscores "key" 0.5 1.5             >>=? [("v1",1)]+    zrangebyscoreWithscores "key" (-inf) inf          >>=? [("v1",1.0),("v2",2.0),("v3",42.0)]+    zrangebyscoreLimit "key" 0.5 2.5 0 1              >>=? ["v1"]+    zrangebyscoreWithscoresLimit "key" 0.5 2.5 0 1    >>=? [("v1",1)]+    zrevrangebyscore "key" 1.5 0.5                    >>=? ["v1"]+    zrevrangebyscoreWithscores "key" 1.5 0.5          >>=? [("v1",1)]+    zrevrangebyscoreLimit "key" 2.5 0.5 0 1           >>=? ["v2"]+    zrevrangebyscoreWithscoresLimit "key" 2.5 0.5 0 1 >>=? [("v2",2)]++    zrem "key" ["v2"]                                 >>=? 1+    zremrangebyscore "key" 10 100                     >>=? 1+    zremrangebyrank "key" 0 0                         >>=? 1++testZStore :: Test+testZStore = testCase "zunionstore/zinterstore" $ do+    zadd "{same}k1" [(1, "v1"), (2, "v2")] >>= \case+      Left _ -> error "error"+      _ -> return ()+    zadd "{same}k2" [(2, "v2"), (3, "v3")] >>= \case+      Left _ -> error "error"+      _ -> return ()+    zinterstore "{same}newkey" ["{same}k1","{same}k2"] Sum                >>=? 1+    zinterstoreWeights "{same}newkey" [("{same}k1",1),("{same}k2",2)] Max >>=? 1+    zunionstore "{same}newkey" ["{same}k1","{same}k2"] Sum                >>=? 3+    zunionstoreWeights "{same}newkey" [("{same}k1",1),("{same}k2",2)] Min >>=? 3++------------------------------------------------------------------------------+-- HyperLogLog+--++testHyperLogLog :: Test+testHyperLogLog = testCase "hyperloglog" $ do+  -- test creation+  pfadd "hll1" ["a"] >>= \case+      Left _ -> error "error"+      _ -> return ()+  pfcount ["hll1"] >>=? 1+  -- test cardinality+  pfadd "hll1" ["a"] >>= \case+      Left _ -> error "error"+      _ -> return ()+  pfcount ["hll1"] >>=? 1+  pfadd "hll1" ["b", "c", "foo", "bar"] >>= \case+      Left _ -> error "error"+      _ -> return ()+  pfcount ["hll1"] >>=? 5+  -- test merge+  pfadd "{same}hll2" ["1", "2", "3"] >>= \case+      Left _ -> error "error"+      _ -> return ()+  pfadd "{same}hll3" ["4", "5", "6"] >>= \case+      Left _ -> error "error"+      _ -> return ()+  pfmerge "{same}hll4" ["{same}hll2", "{same}hll3"] >>= \case+      Left _ -> error "error"+      _ -> return ()+  pfcount ["{same}hll4"] >>=? 6+  -- test union cardinality+  pfcount ["{same}hll2", "{same}hll3"] >>=? 6++------------------------------------------------------------------------------+-- Pub/Sub+--+testPubSub :: Test+testPubSub conn = testCase "pubSub" go conn+  where+    go = do+        -- producer+        asyncProducer <- liftIO $ Async.async $ do+            runRedis conn $ do+                let t = 10^(5 :: Int)+                liftIO $ threadDelay t+                publish "chan1" "hello" >>=? 1+                liftIO $ threadDelay t+                publish "chan2" "world" >>=? 1+            return ()++        -- consumer+        pubSub (subscribe ["chan1"]) $ \msg -> do+            -- ready for a message+            case msg of+                Message{..} -> return+                    (unsubscribe [msgChannel] `mappend` psubscribe ["chan*"])+                PMessage{..} -> return (punsubscribe [msgPattern])++        pubSub (subscribe [] `mappend` psubscribe []) $ \_ -> do+            liftIO $ HUnit.assertFailure "no subs: should return immediately"+            undefined+        liftIO $ Async.wait asyncProducer+++------------------------------------------------------------------------------+-- Transaction+--+testTransaction :: Test+testTransaction = testCase "transaction" $ do+    watch ["{same}k1", "{same}k2"] >>=? Ok+    unwatch            >>=? Ok+    set "{same}foo" "foo" >>= \case+      Left _ -> error "error"+      _ -> return ()+    set "{same}bar" "bar" >>= \case+      Left _ -> error "error"+      _ -> return ()+    foobar <- multiExec $ do+        foo <- get "{same}foo"+        bar <- get "{same}bar"+        return $ (,) <$> foo <*> bar+    assert $ foobar == TxSuccess (Just "foo", Just "bar")+++------------------------------------------------------------------------------+-- Scripting+--+testScripting :: Test+testScripting conn = testCase "scripting" go conn+  where+    go = do+        let script    = "return {false, 42}"+            scriptRes = (False, 42 :: Integer)+        scriptLoad script >>= \case+          Left _ -> error "error"+          Right scriptHash -> do+            eval script [] []                       >>=? scriptRes+            evalsha scriptHash [] []                >>=? scriptRes+            scriptExists [scriptHash, "notAScript"] >>=? [True, False]+            scriptFlush                             >>=? Ok+            -- start long running script from another client+            configSet "lua-time-limit" "100"        >>=? Ok+            evalFinished <- liftIO newEmptyMVar+            asyncScripting <- liftIO $ Async.async $ runRedis conn $ do+                -- we must pattern match to block the thread+                (eval "while true do end" [] []+                    :: Redis (Either Reply Integer)) >>= \case+                    Left _ -> return ()+                    _ -> error "impossible"+                liftIO (putMVar evalFinished ())+                return ()+            liftIO (threadDelay 500000) -- 0.5s+            scriptKill                              >>=? Ok+            () <- liftIO (takeMVar evalFinished)+            liftIO $ Async.wait asyncScripting+            return ()++------------------------------------------------------------------------------+-- Connection+--+testConnectAuth :: Test+testConnectAuth = testCase "connect/auth" $ do+    configSet "requirepass" "pass" >>=? Ok+    liftIO $ do+        c <- checkedConnect defaultConnectInfo { connectAuth = Just "pass" }+        runRedis c (ping >>=? Pong)+    auth "pass"                    >>=? Ok+    configSet "requirepass" ""     >>=? Ok++testConnectAuthUnexpected :: Test+testConnectAuthUnexpected = testCase "connect/auth/unexpected" $ do+    liftIO $ do+        res <- try $ void $ checkedConnect connInfo+        HUnit.assertEqual "" err res++    where connInfo = defaultConnectInfo { connectAuth = Just "pass" }+          err = Left $ ConnectAuthError $+                  Error "ERR AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?"++testConnectDb :: Test+testConnectDb = testCase "connect/db" $ do+    set "connect" "value" >>=? Ok+    liftIO $ void $ do+        c <- checkedConnect defaultConnectInfo { connectDatabase = 1 }+        runRedis c (get "connect" >>=? Nothing)++testConnectDbUnexisting :: Test+testConnectDbUnexisting = testCase "connect/db/unexisting" $ do+    liftIO $ do+        res <- try $ void $ checkedConnect connInfo+        case res of+          Left (ConnectSelectError _) -> return ()+          _ -> HUnit.assertFailure $+                  "Expected ConnectSelectError, got " ++ show res++    where connInfo = defaultConnectInfo { connectDatabase = 100 }++testEcho :: Test+testEcho = testCase "echo" $+    echo ("value" ) >>=? "value"++testPing :: Test+testPing = testCase "ping" $ ping >>=? Pong++testQuit :: Test+testQuit = testCase "quit" $ quit >>=? Ok++testSelect :: Test+testSelect = testCase "select" $ do+    select 13 >>=? Ok+    select 0 >>=? Ok+++------------------------------------------------------------------------------+-- Server+--+testServer :: Test+testServer = testCase "server" $ do+    time >>= \case+      Right (_,_) -> return ()+      Left _ -> error "error"+    slaveof "no" "one" >>=? Ok+    return ()++testBgrewriteaof :: Test+testBgrewriteaof = testCase "bgrewriteaof/bgsave/save" $ do+    save >>=? Ok+    bgsave >>= \case+      Right (Status _) -> return ()+      _ -> error "error"+    -- Redis needs time to finish the bgsave+    liftIO $ threadDelay (10^(5 :: Int))+    bgrewriteaof >>= \case+      Right (Status _) -> return ()+      _ -> error "error"+    return ()++testConfig :: Test+testConfig = testCase "config/auth" $ do+    configGet "requirepass"        >>=? [("requirepass", "")]+    configSet "requirepass" "pass" >>=? Ok+    auth "pass"                    >>=? Ok+    configSet "requirepass" ""     >>=? Ok++testFlushall :: Test+testFlushall = testCase "flushall/flushdb" $ do+    flushall >>=? Ok+    flushdb  >>=? Ok++testInfo :: Test+testInfo = testCase "info/lastsave/dbsize" $ do+    info >>= \case+      Left _ -> error "error"+      _ -> return ()+    lastsave >>= \case+      Left _ -> error "error"+      _ -> return ()+    dbsize          >>=? 0+    configResetstat >>=? Ok++testSlowlog :: Test+testSlowlog = testCase "slowlog" $ do+    slowlogReset >>=? Ok+    slowlogGet 5 >>=? []+    slowlogLen   >>=? 0++testDebugObject :: Test+testDebugObject = testCase "debugObject/debugSegfault" $ do+    set "key" "value" >>=? Ok+    debugObject "key" >>= \case+      Left _ -> error "error"+      _ -> return ()+    return ()++testScans :: Test+testScans = testCase "scans" $ do+    set "key" "value"       >>=? Ok+    scan cursor0            >>=? (cursor0, ["key"])+    scanOpts cursor0 sOpts1 >>=? (cursor0, ["key"])+    scanOpts cursor0 sOpts2 >>=? (cursor0, [])+    where sOpts1 = defaultScanOpts { scanMatch = Just "k*" }+          sOpts2 = defaultScanOpts { scanMatch = Just "not*"}++testSScan :: Test+testSScan = testCase "sscan" $ do+    sadd "set" ["1"]        >>=? 1+    sscan "set" cursor0     >>=? (cursor0, ["1"])++testHScan :: Test+testHScan = testCase "hscan" $ do+    hset "hash" "k" "v"     >>=? 1+    hscan "hash" cursor0    >>=? (cursor0, [("k", "v")])++testZScan :: Test+testZScan = testCase "zscan" $ do+    zadd "zset" [(42, "2")] >>=? 1+    zscan "zset" cursor0    >>=? (cursor0, [("2", 42)])++testZrangelex ::Test+testZrangelex = testCase "zrangebylex" $ do+    let testSet = [(10, "aaa"), (10, "abb"), (10, "ccc"), (10, "ddd")]+    zadd "zrangebylex" testSet                          >>=? 4+    zrangebylex "zrangebylex" (Incl "aaa") (Incl "bbb") >>=? ["aaa","abb"]+    zrangebylex "zrangebylex" (Excl "aaa") (Excl "ddd") >>=? ["abb","ccc"]+    zrangebylex "zrangebylex" Minr Maxr                 >>=? ["aaa","abb","ccc","ddd"]+    zrangebylexLimit "zrangebylex" Minr Maxr 2 1        >>=? ["ccc"]++testXAddRead ::Test+testXAddRead = testCase "xadd/xread" $ do+    xadd "{same}somestream" "123" [("key", "value"), ("key2", "value2")]+    xadd "{same}otherstream" "456" [("key1", "value1")]+    xaddOpts "{same}thirdstream" "*" [("k", "v")] (Maxlen 1)+    xaddOpts "{same}thirdstream" "*" [("k", "v")] (ApproxMaxlen 1)+    xread [("{same}somestream", "0"), ("{same}otherstream", "0")] >>=? Just [+        XReadResponse {+            stream = "{same}somestream",+            records = [StreamsRecord{recordId = "123-0", keyValues = [("key", "value"), ("key2", "value2")]}]+        },+        XReadResponse {+            stream = "{same}otherstream",+            records = [StreamsRecord{recordId = "456-0", keyValues = [("key1", "value1")]}]+        }]+    xlen "{same}somestream" >>=? 1++testXReadGroup ::Test+testXReadGroup = testCase "XGROUP */xreadgroup/xack" $ do+    xadd "somestream" "123" [("key", "value")]+    xgroupCreate "somestream" "somegroup" "0"+    xreadGroup "somegroup" "consumer1" [("somestream", ">")] >>=? Just [+        XReadResponse {+            stream = "somestream",+            records = [StreamsRecord{recordId = "123-0", keyValues = [("key", "value")]}]+        }]+    xack "somestream" "somegroup" ["123-0"] >>=? 1+    xreadGroup "somegroup" "consumer1" [("somestream", ">")] >>=? Nothing+    xgroupSetId "somestream" "somegroup" "0" >>=? Ok+    xgroupDelConsumer "somestream" "somegroup" "consumer1" >>=? 0+    xgroupDestroy "somestream" "somegroup" >>=? True++testXRange ::Test+testXRange = testCase "xrange/xrevrange" $ do+    xadd "somestream" "121" [("key1", "value1")]+    xadd "somestream" "122" [("key2", "value2")]+    xadd "somestream" "123" [("key3", "value3")]+    xadd "somestream" "124" [("key4", "value4")]+    xrange "somestream" "122" "123" Nothing >>=? [+        StreamsRecord{recordId = "122-0", keyValues = [("key2", "value2")]},+        StreamsRecord{recordId = "123-0", keyValues = [("key3", "value3")]}+        ]+    xrevRange "somestream" "123" "122" Nothing >>=? [+        StreamsRecord{recordId = "123-0", keyValues = [("key3", "value3")]},+        StreamsRecord{recordId = "122-0", keyValues = [("key2", "value2")]}+        ]++testXpending ::Test+testXpending = testCase "xpending" $ do+    xadd "somestream" "121" [("key1", "value1")]+    xadd "somestream" "122" [("key2", "value2")]+    xadd "somestream" "123" [("key3", "value3")]+    xadd "somestream" "124" [("key4", "value4")]+    xgroupCreate "somestream" "somegroup" "0"+    xreadGroup "somegroup" "consumer1" [("somestream", ">")]+    xpendingSummary "somestream" "somegroup" Nothing >>=? XPendingSummaryResponse {+        numPendingMessages = 4,+        smallestPendingMessageId = "121-0",+        largestPendingMessageId = "124-0",+        numPendingMessagesByconsumer = [("consumer1", 4)]+    }+    detail <- xpendingDetail "somestream" "somegroup" "121" "121" 10 Nothing+    liftIO $ case detail of+        Left reply   -> HUnit.assertFailure $ "Redis error: " ++ show reply+        Right [XPendingDetailRecord{..}] -> do+            messageId HUnit.@=? "121-0"+        Right bad -> HUnit.assertFailure $ "Unexpectedly got " ++ show bad++testXClaim ::Test+testXClaim =+  testCase "xclaim" $ do+    xadd "somestream" "121" [("key1", "value1")] >>=? "121-0"+    xadd "somestream" "122" [("key2", "value2")] >>=? "122-0"+    xgroupCreate "somestream" "somegroup" "0" >>=? Ok+    xreadGroupOpts+      "somegroup"+      "consumer1"+      [("somestream", ">")]+      (defaultXreadOpts {recordCount = Just 2}) >>=?+      Just+        [ XReadResponse+            { stream = "somestream"+            , records =+                [ StreamsRecord+                    {recordId = "121-0", keyValues = [("key1", "value1")]}+                , StreamsRecord+                    {recordId = "122-0", keyValues = [("key2", "value2")]}+                ]+            }+        ]+    xclaim "somestream" "somegroup" "consumer2" 0 defaultXClaimOpts ["121-0"] >>=?+      [StreamsRecord {recordId = "121-0", keyValues = [("key1", "value1")]}]+    xclaimJustIds+      "somestream"+      "somegroup"+      "consumer2"+      0+      defaultXClaimOpts+      ["122-0"] >>=?+      ["122-0"]++testXInfo ::Test+testXInfo = testCase "xinfo" $ do+    xadd "somestream" "121" [("key1", "value1")]+    xadd "somestream" "122" [("key2", "value2")]+    xgroupCreate "somestream" "somegroup" "0"+    xreadGroupOpts "somegroup" "consumer1" [("somestream", ">")] (defaultXreadOpts { recordCount = Just 2})+    consumerInfos <- xinfoConsumers "somestream" "somegroup"+    liftIO $ case consumerInfos of+        Left reply -> HUnit.assertFailure $ "Redis error: " ++ show reply+        Right [XInfoConsumersResponse{..}] -> do+            xinfoConsumerName HUnit.@=? "consumer1"+            xinfoConsumerNumPendingMessages HUnit.@=? 2+        Right bad -> HUnit.assertFailure $ "Unexpectedly got " ++ show bad+    xinfoGroups "somestream" >>=? [+        XInfoGroupsResponse{+            xinfoGroupsGroupName = "somegroup",+            xinfoGroupsNumConsumers = 1,+            xinfoGroupsNumPendingMessages = 2,+            xinfoGroupsLastDeliveredMessageId = "122-0"+        }]+    xinfoStream "somestream" >>=? XInfoStreamResponse+        { xinfoStreamLength = 2+        , xinfoStreamRadixTreeKeys = 1+        , xinfoStreamRadixTreeNodes = 2+        , xinfoStreamNumGroups = 1+        , xinfoStreamLastEntryId = "122-0"+        , xinfoStreamFirstEntry = StreamsRecord+            { recordId = "121-0"+            , keyValues = [("key1", "value1")]+            }+        , xinfoStreamLastEntry = StreamsRecord+            { recordId = "122-0"+            , keyValues = [("key2", "value2")]+            }+        }++testXDel ::Test+testXDel = testCase "xdel" $ do+    xadd "somestream" "121" [("key1", "value1")]+    xadd "somestream" "122" [("key2", "value2")]+    xdel "somestream" ["122"] >>=? 1+    xlen "somestream" >>=? 1++testXTrim ::Test+testXTrim = testCase "xtrim" $ do+    xadd "somestream" "121" [("key1", "value1")]+    xadd "somestream" "122" [("key2", "value2")]+    xadd "somestream" "123" [("key3", "value3")]+    xadd "somestream" "124" [("key4", "value4")]+    xadd "somestream" "125" [("key5", "value5")]+    xtrim "somestream" (Maxlen 2) >>=? 3