memcache 0.1.0.1 → 0.2.0.0
raw patch · 13 files changed
+1276/−1010 lines, 13 filesdep +data-default-classdep −vector-algorithms
Dependencies added: data-default-class
Dependencies removed: vector-algorithms
Files
- CHANGELOG.md +24/−6
- Database/Memcache/Client.hs +311/−82
- Database/Memcache/Cluster.hs +197/−115
- Database/Memcache/Errors.hs +55/−52
- Database/Memcache/Protocol.hs +0/−256
- Database/Memcache/SASL.hs +31/−15
- Database/Memcache/Server.hs +55/−22
- Database/Memcache/Socket.hs +423/−0
- Database/Memcache/Types.hs +68/−46
- Database/Memcache/Wire.hs +0/−381
- TODO.md +8/−4
- memcache.cabal +27/−20
- test/Full.hs +77/−11
CHANGELOG.md view
@@ -1,23 +1,41 @@-# 0.1.X.X+# 0.2.0.0 - May 27th, 2016 +* Big design change to reduce code duplication (`Protocol` module gone).+* Remove `Options` type - just fixed configuration for now.+* Design change also allows proper retry handling on operation failure - we+ retry an operation against the same server, but after N consecutive failures,+ we mark the server as dead and don't try using it again until M seconds has+ passed.+* Simplify exception hierachy - just one type `MemcacheError` now for+* Remove `defaultOptions` and `defaultServerSpec`, will revist usefulness.+ exceptions.+* Remove many `Typeable` instances.+* Support better testing with a mock Memcached server.+* Fix bug in socket handling - detected EOF properly.+* Greatly improve documentation.+* Use `data-default-class` for defaults of servers and options.++# 0.1.0.1 - February 26th, 2016+ * Consistent usage of 'memcached' instead of 'memcache'.-* Document `Database.Memcache.Client`+* Document `Database.Memcache.Client`. * Add inline pragmas in appropriate places.+* Fix bug handling fragmented IP packets (Alfredo Di Napoli). -# 0.1.0.0+# 0.1.0.0 - May 18th, 2015 -* First proper release (although still lots of rough edges!)+* First proper release (although still lots of rough edges!). * Filled out `Data.Memcache.Client` to a complete API. * Integrated cluster and authentication handling. * Better error and exception handling. * Fix compilation under GHC 7.10. -# 0.0.1+# 0.0.1 - May 5th, 2015 * Initial (incomplete) support for a cluster of memcached servers. * Fixed compilation under GHC 7.4. -# 0.0.0+# 0.0.0 - August 23rd, 2013 * Initial release. Support for a single server.
Database/Memcache/Client.hs view
@@ -1,147 +1,376 @@--- | A memcached client. Supports the binary protocol (only) and SASL--- authentication.------ A client can connect to a single memcached server or a cluster of them. In--- the later case, consistent hashing is used to route requests to the--- appropriate server.------ Expected return values (like misses) are returned as part of the return--- type, while unexpected errors are thrown as exceptions.+{-# LANGUAGE CPP #-}++{-|+Module : Database.Memcache.Client+Description : Memcached Client+Copyright : (c) David Terei, 2016+License : BSD+Maintainer : code@davidterei.com+Stability : stable+Portability : GHC++A Memcached client. Memcached is an in-memory key-value store typically used as+a distributed and shared cache. Clients connect to a group of Memcached servers+and perform out-of-band caching for things like SQL results, rendered pages, or+third-party APIs.++A client can connect to a single Memcached server or a cluster of them. In the+later case, consistent hashing is used to route requests to the appropriate+server. The /binary/ Memcached protocol is used and /SASL authentication/ is+supported.++Expected return values (like misses) are returned as part of the return type,+while unexpected errors are thrown as exceptions. Exceptions are either of type+'MemcacheError' or an 'IO' exception thrown by the network.++We support the following logic for handling failure in operatins:++* __Timeouts__: we timeout any operation that takes too long and consider it+ failed.+* __Retry__: on operation failure (timeout, network error) we close the+ connection and rety the operation, doing this up to a configurable+ maximum.++* __Failover__: when an operation against a server in a cluster fails all+ retries, we mark that server as dead and use the remaining+ servers in the cluster to handle all operations. After a+ configurable period of time as passed, we consider the server+ alive again and try to use it. This can lead to consistency+ issues (stale data), but is usually fine for caching purposes+ and is the common approach in Memcached clients.++Some of this behavior can be configured through the 'Options' data type. We+also have the following concepts exposed by Memcached:++ [@version@] Each value has a 'Version' associated with it. This is simply a+ numeric, monontonically increasing value. The version field+ allows for a primitive version of 'cas' to be implemented.++ [@expiration@] Each value pair has an 'Expiration' associated with it. Once a+ a value expires, it will no longer be returned from the cache+ until a new value for that key is set. Expirations come in two+ forms, the first form interprets the expiration value as the+ number of seconds in the future at which the value should be+ considered expired. For example, an expiration of @3600@+ expires the value in 1 hour. When the value of the expiration+ is greater than 30 days however (@2592000@), the expiration+ field is instead interpretted as a UNIX timestamp (the number+ of seconds since epoch).++ [@flags@] Each value can have a small amount of fixed metadata associated+ with it beyond the value itself, these are the 'Flags'.++Usage is roughly as follows:++> module Main where+>+> import qualified Database.Memcache.Client as M+>+> main = do+> -- use default values: connects to localhost:11211+> mc <- M.newClient M.def M.def+>+> -- store and then retrieve a key-value pair+> M.set mc "key" "value" 0 0+> v' <- M.get mc "key"+> case v' of+> Nothing -> putStrLn "Miss!"+> Just (v, _, _) -> putStrLn $ "Hit: " + show v+-} module Database.Memcache.Client (- -- * Cluster and connection handling- newClient, Client, ServerSpec(..), defaultServerSpec, Options(..),- defaultOptions, Authentication(..), Username, Password, quit,+ -- * Client creation+ newClient, Client, ServerSpec(..), Options(..),+ Authentication(..), Username, Password, def,+ quit, - -- * Get operations+ -- * Operations++ -- ** Get operations get, gat, touch, - -- * Set operations- set, set', add, replace,- - -- * Delete operations- delete, flush,- - -- * Modify operations+ -- ** Set operations+ set, cas, add, replace,++ -- ** Modify operations increment, decrement, append, prepend, - -- * Information operations- version, stats, P.StatResults,+ -- ** Delete operations+ delete, flush, - -- * Error handling- MemcacheError(..), ClientError(..)+ -- ** Information operations+ StatResults, stats, version,++ -- * Errors+ MemcacheError(..), Status(..), ClientError(..), ProtocolError(..) ) where import Database.Memcache.Cluster import Database.Memcache.Errors-import qualified Database.Memcache.Protocol as P import Database.Memcache.Server-import Database.Memcache.Types+import Database.Memcache.Socket+import Database.Memcache.Types hiding (cas) -import Control.Monad+#if __GLASGOW_HASKELL__ < 710+import Control.Applicative+#endif+import Control.Exception (handle, throwIO, SomeException)+import Control.Monad (forM_, void, when)+import Data.Default.Class import Data.Word import Data.ByteString (ByteString)+import qualified Data.ByteString as B (null) --- | A memcached client, connected to a collection of memcached servers.+-- | A Memcached client, connected to a collection of Memcached servers. type Client = Cluster --- | Establish a new connection to a group of memcached servers.+-- | Establish a new connection to a group of Memcached servers. newClient :: [ServerSpec] -> Options -> IO Client newClient = newCluster --- | Gracefully close a connection to a memcached cluster.+-- | Gracefully close a connection to a Memcached cluster. quit :: Cluster -> IO ()-quit c = void $ allOp (Just ()) c $ \s -> P.quit s+quit c = void $ allOp' c serverQuit+ where+ serverQuit :: Server -> IO ()+ serverQuit s = handle consumeError $ do+ let msg = emptyReq { reqOp = ReqQuit Quiet }+ withSocket s $ \sock -> send sock msg+ close s -keyedOp' :: Cluster -> Key -> (Server -> IO (Maybe a)) -> IO (Maybe a)-keyedOp' = keyedOp (Just Nothing)-{-# INLINE keyedOp' #-}+ consumeError :: SomeException -> IO ()+ consumeError _ = return () --- | Retrieve the value for the given key from memcache.+-- | Retrieve the value for the given key from Memcached. get :: Cluster -> Key -> IO (Maybe (Value, Flags, Version))-get c k = keyedOp' c k $ \s -> P.get s k-{-# INLINE get #-}+get c k = do+ let msg = emptyReq { reqOp = ReqGet Loud NoKey k }+ r <- keyedOp c k msg+ (v, f) <- case resOp r of+ ResGet Loud v f -> return (v, f)+ _ -> throwIO $ wrongOp r "GET"+ case resStatus r of+ NoError -> return $ Just (v, f, resCas r)+ ErrKeyNotFound -> return Nothing+ rs -> throwStatus rs --- | Get-and-touch: Retrieve the value for the given key from memcache, and--- also update the stored key-value pairs expiration time at the server.+-- | Get-and-touch: Retrieve the value for the given key from Memcached, and+-- also update the stored key-value pairs expiration time at the server. Use an+-- expiration value of @0@ to store forever. gat :: Cluster -> Key -> Expiration -> IO (Maybe (Value, Flags, Version))-gat c k e = keyedOp' c k $ \s -> P.gat s k e-{-# INLINE gat #-}+gat c k e = do+ let msg = emptyReq { reqOp = ReqGAT Loud NoKey k (SETouch e) }+ r <- keyedOp c k msg+ (v, f) <- case resOp r of+ ResGAT Loud v f -> return (v, f)+ _ -> throwIO $ wrongOp r "GAT"+ case resStatus r of+ NoError -> return $ Just (v, f, resCas r)+ ErrKeyNotFound -> return Nothing+ rs -> throwStatus rs -- | Update the expiration time of a stored key-value pair, returning its--- version identifier.+-- version identifier. Use an expiration value of @0@ to store forever. touch :: Cluster -> Key -> Expiration -> IO (Maybe Version)-touch c k e = keyedOp' c k $ \s -> P.touch s k e-{-# INLINE touch #-}+touch c k e = do+ let msg = emptyReq { reqOp = ReqTouch k (SETouch e) }+ r <- keyedOp c k msg+ when (resOp r /= ResTouch) $ throwIO $ wrongOp r "TOUCH"+ case resStatus r of+ NoError -> return $ Just (resCas r)+ ErrKeyNotFound -> return Nothing+ rs -> throwStatus rs -- | Store a new (or overwrite exisiting) key-value pair, returning its version--- identifier.+-- identifier. Use an expiration value of @0@ to store forever. set :: Cluster -> Key -> Value -> Flags -> Expiration -> IO Version-set c k v f e = keyedOp (Just 0) c k $ \s -> P.set s k v f e-{-# INLINE set #-}+set c k v f e = do+ let msg = emptyReq { reqOp = ReqSet Loud k v (SESet f e) }+ r <- keyedOp c k msg+ when (resOp r /= ResSet Loud) $ throwIO $ wrongOp r "SET"+ case resStatus r of+ NoError -> return $ resCas r+ rs -> throwStatus rs -- | Store a key-value pair, but only if the version specified by the client--- matches the version of the key-value pair at the server. The version+-- matches the Version of the key-value pair at the server. The version -- identifier of the stored key-value pair is returned, or if the version match--- fails, 'Nothing' is returned.-set' :: Cluster -> Key -> Value -> Flags -> Expiration -> Version -> IO (Maybe Version)-set' c k v f e ver = keyedOp' c k $ \s -> P.set' s k v f e ver-{-# INLINE set' #-}+-- fails, @Nothing@ is returned. Use an expiration value of @0@ to store+-- forever.+cas :: Cluster -> Key -> Value -> Flags -> Expiration -> Version -> IO (Maybe Version)+cas c k v f e ver = do+ let msg = emptyReq { reqOp = ReqSet Loud k v (SESet f e), reqCas = ver }+ r <- keyedOp c k msg+ when (resOp r /= ResSet Loud) $ throwIO $ wrongOp r "SET"+ case resStatus r of+ NoError -> return $ Just (resCas r)+ ErrKeyNotFound -> return Nothing -- CAS: key doesn't exist+ ErrKeyExists -> return Nothing -- CAS: version doesn't match+ rs -> throwStatus rs -- | Store a new key-value pair, returning it's version identifier. If the--- key-value pair already exists, then fail (return 'Nothing').+-- key-value pair already exists, then fail (return 'Nothing'). Use an+-- expiration value of @0@ to store forever. add :: Cluster -> Key -> Value -> Flags -> Expiration -> IO (Maybe Version)-add c k v f e = keyedOp' c k $ \s -> P.add s k v f e-{-# INLINE add #-}+add c k v f e = do+ let msg = emptyReq { reqOp = ReqAdd Loud k v (SESet f e) }+ r <- keyedOp c k msg+ when (resOp r /= ResAdd Loud) $ throwIO $ wrongOp r "ADD"+ case resStatus r of+ NoError -> return $ Just (resCas r)+ ErrKeyExists -> return Nothing+ rs -> throwStatus rs -- | Update the value of an existing key-value pair, returning it's new version -- identifier. If the key doesn't already exist, the fail and return Nothing.+-- Use an expiration value of @0@ to store forever. replace :: Cluster -> Key -> Value -> Flags -> Expiration -> Version -> IO (Maybe Version)-replace c k v f e ver = keyedOp' c k $ \s -> P.replace s k v f e ver-{-# INLINE replace #-}---- | Delete a key-value pair at the server, returning true if successful.-delete :: Cluster -> Key -> Version -> IO Bool-delete c k ver = keyedOp (Just False) c k $ \s -> P.delete s k ver-{-# INLINE delete #-}+replace c k v f e ver = do+ let msg = emptyReq { reqOp = ReqReplace Loud k v (SESet f e), reqCas = ver }+ r <- keyedOp c k msg+ when (resOp r /= ResReplace Loud) $ throwIO $ wrongOp r "REPLACE"+ case resStatus r of+ NoError -> return $ Just (resCas r)+ -- replace only applies to an existing key...+ ErrKeyNotFound -> return Nothing+ -- version specified and doesn't match key...+ ErrKeyExists -> return Nothing+ rs -> throwStatus rs -- | Increment a numeric value stored against a key, returning the incremented--- value and the version identifier of the key-value pair.+-- value and the version identifier of the key-value pair. Use an expiration+-- value of @0@ to store forever. increment :: Cluster -> Key -> Initial -> Delta -> Expiration -> Version -> IO (Maybe (Word64, Version))-increment c k i d e ver = keyedOp' c k $ \s -> P.increment s k i d e ver-{-# INLINE increment #-}+increment c k i d e ver = do+ let msg = emptyReq { reqOp = ReqIncrement Loud k (SEIncr i d e), reqCas = ver }+ r <- keyedOp c k msg+ n <- case resOp r of+ ResIncrement Loud n -> return n+ _ -> throwIO $ wrongOp r "INCREMENT"+ case resStatus r of+ NoError -> return $ Just (n, resCas r)+ ErrKeyNotFound -> return Nothing+ ErrKeyExists -> return Nothing+ rs -> throwStatus rs -- | Decrement a numeric value stored against a key, returning the decremented--- value and the version identifier of the key-value pair.+-- value and the version identifier of the key-value pair. Use an expiration+-- value of @0@ to store forever. decrement :: Cluster -> Key -> Initial -> Delta -> Expiration -> Version -> IO (Maybe (Word64, Version))-decrement c k i d e ver = keyedOp' c k $ \s -> P.decrement s k i d e ver-{-# INLINE decrement #-}+decrement c k i d e ver = do+ let msg = emptyReq { reqOp = ReqDecrement Loud k (SEIncr i d e), reqCas = ver }+ r <- keyedOp c k msg+ n <- case resOp r of+ ResDecrement Loud n -> return n+ _ -> throwIO $ wrongOp r "DECREMENT"+ case resStatus r of+ NoError -> return $ Just (n, resCas r)+ ErrKeyNotFound -> return Nothing+ ErrKeyExists -> return Nothing+ rs -> throwStatus rs -- | Append a value to an existing key-value pair, returning the new version -- identifier of the key-value pair when successful. append :: Cluster -> Key -> Value -> Version -> IO (Maybe Version)-append c k v ver = keyedOp' c k $ \s -> P.append s k v ver-{-# INLINE append #-}+append c k v ver = do+ let msg = emptyReq { reqOp = ReqAppend Loud k v, reqCas = ver }+ r <- keyedOp c k msg+ when (resOp r /= ResAppend Loud) $ throwIO $ wrongOp r "APPEND"+ case resStatus r of+ NoError -> return $ Just (resCas r)+ ErrKeyNotFound -> return Nothing+ rs -> throwStatus rs -- | Prepend a value to an existing key-value pair, returning the new version -- identifier of the key-value pair when successful. prepend :: Cluster -> Key -> Value -> Version -> IO (Maybe Version)-prepend c k v ver = keyedOp' c k $ \s -> P.prepend s k v ver-{-# INLINE prepend #-}+prepend c k v ver = do+ let msg = emptyReq { reqOp = ReqPrepend Loud k v, reqCas = ver }+ r <- keyedOp c k msg+ when (resOp r /= ResPrepend Loud) $ throwIO $ wrongOp r "PREPEND"+ case resStatus r of+ NoError -> return $ Just (resCas r)+ ErrKeyNotFound -> return Nothing+ rs -> throwStatus rs --- | Remove (delete) all currently stored key-value pairs from the cluster.+-- | Delete a key-value pair at the server, returning true if successful.+delete :: Cluster -> Key -> Version -> IO Bool+delete c k ver = do+ let msg = emptyReq { reqOp = ReqDelete Loud k, reqCas = ver }+ r <- keyedOp c k msg+ when (resOp r /= ResDelete Loud) $ throwIO $ wrongOp r "DELETE"+ case resStatus r of+ NoError -> return True+ -- delete only applies to an existing key...+ ErrKeyNotFound -> return False+ -- version specified and doesn't match key...+ ErrKeyExists -> return False+ rs -> throwStatus rs++-- | Remove (delete) all currently stored key-value pairs from the cluster. The+-- expiration value can be used to cause this flush to occur in the future+-- rather than immediately. flush :: Cluster -> Maybe Expiration -> IO ()-flush c e = void $ allOp (Just ()) c $ \s -> P.flush s e-{-# INLINE flush #-}+flush c e = do+ let msg = emptyReq { reqOp = ReqFlush Loud (SETouch <$> e) }+ results <- allOp c msg+ forM_ results $ \(_, r) -> do+ when (resOp r /= ResFlush Loud) $ throwIO $ wrongOp r "FLUSH"+ case resStatus r of+ NoError -> return ()+ rs -> throwStatus rs +-- | StatResults are a list of key-value pairs.+type StatResults = [(ByteString, ByteString)]+ -- | Return statistics on the stored key-value pairs at each server in the--- cluster.-stats :: Cluster -> Maybe Key -> IO [(Server, Maybe P.StatResults)]-stats c key = allOp Nothing c $ \s -> P.stats s key-{-# INLINE stats #-}+-- cluster. The optional key can be used to select a different set of+-- statistics from the server than the default. Most Memcached servers support+-- @"items"@, @"slabs"@ or @"settings"@.+stats :: Cluster -> Maybe Key -> IO [(Server, Maybe StatResults)]+stats c key = allOp' c serverStats+ where+ msg :: Request+ msg = emptyReq { reqOp = ReqStat key } --- | Version returns the version string of the memcached cluster. We just query+ serverStats :: Server -> IO (Maybe StatResults)+ serverStats s = withSocket s $ \sock -> do+ send sock msg+ recvAllStats sock []++ recvAllStats :: Socket -> StatResults -> IO (Maybe StatResults)+ recvAllStats s xs = do+ r <- recv s+ (k, v) <- case resOp r of+ ResStat k v -> return (k, v)+ _ -> throwIO $ wrongOp r "STATS"+ case resStatus r of+ NoError | B.null k && B.null v -> return $ Just xs+ | otherwise -> recvAllStats s $ (k, v):xs+ ErrKeyNotFound -> return Nothing+ rs -> throwStatus rs++-- | Version returns the version string of the Memcached cluster. We just query -- one server and assume all servers in the cluster are the same version. version :: Cluster -> IO ByteString-version c = anyOp Nothing c $ \s -> P.version s-{-# INLINE version #-}+version c = do+ let msg = emptyReq { reqOp = ReqVersion }+ r <- anyOp c msg+ v <- case resOp r of+ ResVersion v -> return v+ _ -> throwIO $ wrongOp r "VERSION"+ case resStatus r of+ NoError -> return v+ rs -> throwStatus rs++-- | Noop sends a Non-operation command to the specified Memcached server. We+-- leave it blanked out, but here for documentation purposes of the full+-- protocol.+-- noop :: Server -> IO ()+-- noop c = do+-- let msg = emptyReq { reqOp = ReqNoop }+-- r <- sendRecv c msg+-- when (resOp r /= ResNoop) $ throwIncorrectRes r "NOOP"+-- case resStatus r of+-- NoError -> return ()+-- rs -> throwStatus rs
Database/Memcache/Cluster.hs view
@@ -1,154 +1,236 @@-{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-} --- | Handles a group of connections to different memcached servers.+{-|+Module : Database.Memcache.Cluster+Description : Cluster Handling+Copyright : (c) David Terei, 2016+License : BSD+Maintainer : code@davidterei.com+Stability : stable+Portability : GHC++Handles a group of connections to different Memcached servers.++We use consistent hashing to choose which server to route a request to. On an+error, we mark the server as failed and remove it temporarialy from the set of+servers available.+-} module Database.Memcache.Cluster (- Cluster, newCluster,- ServerSpec(..), defaultServerSpec,- Options(..), defaultOptions,- keyedOp, anyOp, allOp+ -- * Cluster+ Cluster, ServerSpec(..), Options(..), newCluster,++ -- * Operations+ Retries, keyedOp, anyOp, allOp, allOp' ) where import Database.Memcache.Errors-import Database.Memcache.Server (Server(..), newServer)-import Database.Memcache.Types (Authentication(..), Key)--import qualified Control.Exception as E+import Database.Memcache.Server+import Database.Memcache.Types +import Control.Concurrent (threadDelay)+import Control.Exception (handle, throwIO, SomeException)+import Data.Default.Class+import Data.Fixed (Milli) import Data.Hashable (hash)+import Data.IORef import Data.Maybe (fromMaybe) import Data.List (sort)+import Data.Time.Clock (NominalDiffTime)+import Data.Time.Clock.POSIX (getPOSIXTime) import qualified Data.Vector as V--- import qualified Data.Vector.Mutable as MV- import Network.Socket (HostName, PortNumber)+import System.Timeout --- | ServerSpec specifies a server configuration to connect to.+-- | Number of times to retry an operation before considering it failed.+type Retries = Int++-- | ServerSpec specifies a server configuration for connection. data ServerSpec = ServerSpec {+ -- | Hostname of server to connect to. ssHost :: HostName,+ -- | Port number server is running on. ssPort :: PortNumber,+ -- | Authentication values to use for SASL authentication with this+ -- server. ssAuth :: Authentication- }+ } deriving (Eq, Show) --- | Provides a default value for a server cconnection config.-defaultServerSpec :: ServerSpec-defaultServerSpec = ServerSpec {- ssHost = "localhost",- ssPort = 11211,- ssAuth = NoAuth- }+instance Default ServerSpec where+ def = ServerSpec "localhost" 11211 NoAuth --- | Options specifies how a memcached cluster should be configured.+instance Default [ServerSpec] where+ def = [def]++-- | Options specifies how a Memcached cluster should be configured. data Options = Options {- optsCmdFailure :: !FailureMode,- optsServerFailure :: !FailureMode,- optsServerRetries :: !Int- }+ -- | Number of times to retry an operation on failure. If consecutive+ -- failures exceed this value for a server, we mark it as down and+ -- failover to a different server for the next operation.+ --+ -- Default is 2.+ optsServerRetries :: Retries,+ -- | After an operation has failed, how long to wait before retrying it+ -- while still within the 'optsServerRetries' count?+ --+ -- Default is 200ms.+ optsFailRetryDelay :: Milli,+ -- | How long to wait after a server has been marked down, before+ -- trying to use it again.+ --+ -- Default is 1500ms.+ optsDeadRetryDelay :: Milli,+ -- | How long to wait for an operation to complete before considering+ -- it failed.+ --+ -- Default is 750ms.+ optsServerTimeout :: Milli+ -- TODO: socket_timeout+ -- TODO: failover+ -- TODO: expires_in+ -- TODO: namespace+ -- TODO: compress+ -- TODO: compress_min_size+ -- TODO: compress_max_size+ -- TODO: value_max_bytes+ } deriving (Eq, Show) --- | Provides recommended default for a cluster Options.-defaultOptions :: Options-defaultOptions = Options {- optsCmdFailure = FailToError,- optsServerFailure = FailToError,- optsServerRetries = 2- }+instance Default Options where+ def = Options {+ optsServerRetries = 2,+ optsFailRetryDelay = 200,+ optsDeadRetryDelay = 1500,+ optsServerTimeout = 750+ } --- | A memcached cluster client.+-- | Memcached cluster. data Cluster = Cluster {- servers :: V.Vector Server,- cmdFailureMode :: !FailureMode,- _serverFailureMode :: !FailureMode,- serverRetries :: {-# UNPACK #-} !Int- } deriving Show+ cServers :: V.Vector Server, --- | Establish a new connection to a group of memcached servers.+ -- See 'Options' for description of these values.++ cRetries :: {-# UNPACK #-} !Int,+ cFailDelay :: {-# UNPACK #-} !Int, -- ^ microseconds+ cDeadDelay :: !NominalDiffTime,+ cTimeout :: {-# UNPACK #-} !Int -- ^ microseconds+ } deriving (Eq, Show)++-- | Establish a new connection to a group of Memcached servers. newCluster :: [ServerSpec] -> Options -> IO Cluster+newCluster [] _ = throwIO $ ClientError NoServersReady newCluster hosts Options{..} = do s <- mapM (\ServerSpec{..} -> newServer ssHost ssPort ssAuth) hosts- return $ Cluster (V.fromList $ sort s) optsCmdFailure optsServerFailure- optsServerRetries+ return $+ Cluster {+ cServers = (V.fromList $ sort s),+ cRetries = optsServerRetries ,+ cFailDelay = fromEnum optsFailRetryDelay,+ cDeadDelay = fromRational $ toRational optsDeadRetryDelay / 1000,+ cTimeout = fromEnum optsServerTimeout + } +-- | Check if server is alive.+serverAlive :: NominalDiffTime -> Server -> IO Bool+{-# INLINE serverAlive #-}+serverAlive deadDelay s = do+ t <- readIORef (failed s)+ if t == 0+ then return True+ else do+ t' <- getPOSIXTime+ if (t' - t) < deadDelay+ then return False+ else do+ writeIORef (failed s) 0+ return True+ -- | Figure out which server to talk to for this key. I.e., the distribution -- method. We use consistent hashing based on the CHORD approach.-getServerForKey :: Cluster -> Key -> Server-getServerForKey c k =+getServerForKey :: Cluster -> Key -> IO (Maybe Server)+{-# INLINE getServerForKey #-}+getServerForKey c k = do let hashedKey = hash k- searchFun svr = sid svr < hashedKey- in fromMaybe (V.last $ servers c) $ V.find searchFun (servers c)---- | Run a memcached operation against a server that maps to the key given in--- the cluster.-keyedOp :: forall a. Maybe a -> Cluster -> Key -> (Server -> IO a) -> IO a-keyedOp def c k = serverOp def c (getServerForKey c k)+ searchF s = sid s < hashedKey+ servers' <- V.filterM (serverAlive $ cDeadDelay c) $ cServers c+ return $ if V.null servers'+ then Nothing+ else Just $ fromMaybe (V.last servers') (V.find searchF servers') --- | Run a memcached operation against any single server in the cluster.-anyOp :: forall a. Maybe a -> Cluster -> (Server -> IO a) -> IO a-anyOp def c = serverOp def c (V.head . servers $ c)+-- | Run a Memcached operation against a particular server, handling any+-- failures that occur, retrying the specified number of times.+serverOp :: Cluster -> Server -> Request -> IO Response+{-# INLINE serverOp #-}+serverOp c s req = retryOp c s $ sendRecv s req --- | Run a memcached operation against all servers in the cluster.-allOp :: forall a. Maybe a -> Cluster -> (Server -> IO a) -> IO [(Server, a)]-allOp def c m = do- res <- V.forM (servers c) (\s -> serverOp def c s m)- return $ V.toList $ V.zip (servers c) res+-- | Run a Memcached operation against a particular server, handling any+-- failures that occur, retrying the specified number of times.+keyedOp :: Cluster -> Key -> Request -> IO Response+{-# INLINE keyedOp #-}+keyedOp c k req = do+ s' <- getServerForKey c k+ case s' of+ Just s -> serverOp c s req+ Nothing -> throwIO $ ClientError NoServersReady --- Server down handling modes:--- * Failover to next server -- this command + all others to failed server--- * Failover to next server -- fail current command but all future to failed--- server are moved over--- * Throw error -- on this command + all subsequent ones to this server--- * Throw error -- on this command but silently ignore future commands to--- this server------ Failure Matrix: When command fails:--- * S = server commands are meant to go to.--- * S' = failover server for S (i.e., next in ring)------ Mode | Current Command | Future Commands | Makes Sense?--- ---------------------------------------------------------- 1 | exception | silently drop | Yes--- 2 | exception | failover | Yes--- 3 | exception | exception | Yes--- 4 | silently drop | silently drop | Yes--- 5 | failover | failover | Yes--- 6 | failover | exception | No--- 7 | failover | silently drop | No--- --- Other option here is, is this recursive or a one-hop failover?----data FailureMode = FailSilent | FailToBackup | FailToError- deriving (Eq, Show)+-- | Run a Memcached operation against any single server in the cluster,+-- handling any failures that occur, retrying the specified number of times.+anyOp :: Cluster -> Request -> IO Response+{-# INLINE anyOp #-}+anyOp c req = do+ servers' <- V.filterM (serverAlive $ cDeadDelay c) $ cServers c+ if V.null servers'+ then throwIO $ ClientError NoServersReady+ else serverOp c (V.head servers') req --- | Run a memcached operation against a particular server, handling any--- failures that occur.-serverOp :: forall a. Maybe a -> Cluster -> Server -> (Server -> IO a) -> IO a-serverOp def c s m = go $ serverRetries c- where+-- | Run a Memcached operation against all servers in the cluster, handling any+-- failures that occur, retrying the specified number of times.+allOp :: Cluster -> Request -> IO [(Server, Response)]+{-# INLINE allOp #-}+allOp c req = do+ servers' <- V.filterM (serverAlive $ cDeadDelay c) $ cServers c+ if V.null servers'+ then throwIO $ ClientError NoServersReady+ else do+ res <- V.forM servers' $ \s -> serverOp c s req+ return $ V.toList $ V.zip servers' res - go attempt =- m s `E.catches`- [ E.Handler $ handleMemErrors (attempt - 1)- , E.Handler $ handleAllErrors (attempt - 1)- ]+-- | Run a Memcached operation against all servers in the cluster, handling any+-- failures that occur, retrying the specified number of times. Similar to+-- 'anyOp' but allows more flexible interaction with the 'Server' than a single+-- request and response.+allOp' :: Cluster -> (Server -> IO a) -> IO [(Server, a)]+{-# INLINE allOp' #-}+allOp' c op = do+ servers' <- V.filterM (serverAlive $ cDeadDelay c) $ cServers c+ if V.null servers'+ then throwIO $ ClientError NoServersReady+ else do+ res <- V.forM servers' $ \s -> retryOp c s (op s)+ return $ V.toList $ V.zip servers' res - cmdError err | cmdFailureMode c == FailSilent- = maybe (E.throwIO err) return def- | cmdFailureMode c == FailToBackup- = undefined -- XXX: Implement- | otherwise -- FailToError- = E.throwIO err- - -- These errors are thrown outside the resource-pool and so don't destroy- -- the connection. This is desired as the connection should still be fine.- handleMemErrors :: Int -> MemcacheError -> IO a- handleMemErrors 0 err = cmdError err -- XXX: Mark as failed!- handleMemErrors atmp MemErrStoreFailed = go atmp- handleMemErrors atmp MemErrUnknownCmd = go atmp- handleMemErrors _ err = E.throwIO err+-- | Run an IO operation multiple times if an exception is thrown, marking the+-- server as dead if it fails more than the allowed number of retries.+retryOp :: forall a. Cluster -> Server -> IO a -> IO a+{-# INLINE retryOp #-}+retryOp Cluster{..} s op = go cRetries+ where+ go :: Int -> IO a+ {-# INLINE go #-}+ go !n = handle (handleErrs $ n - 1) $ do+ mr <- timeout cTimeout op+ case mr of+ Just r -> return r+ Nothing -> close s >> throwIO (ClientError Timeout) - -- All other exception types are thrown inside the resource-pool and so- -- cause it to destroy the connection, which is desired as we've had some- -- wire-level error occur.- handleAllErrors :: Int -> E.SomeException -> IO a- handleAllErrors 0 err = cmdError err -- XXX: Mark as failed!- handleAllErrors atmp _ = go atmp+ handleErrs :: Int -> SomeException -> IO a+ {-# INLINE handleErrs #-}+ handleErrs 0 err = do t <- getPOSIXTime+ writeIORef (failed s) t+ throwIO err+ handleErrs n _ = do+ threadDelay cFailDelay+ go n
Database/Memcache/Errors.hs view
@@ -1,12 +1,26 @@ {-# LANGUAGE DeriveDataTypeable #-} --- | Memcached related errors and exception handling.+{-|+Module : Database.Memcache.Errors+Description : Errors Handling+Copyright : (c) David Terei, 2016+License : BSD+Maintainer : code@davidterei.com+Stability : stable+Portability : GHC++Memcached related errors and exception handling.+-} module Database.Memcache.Errors (+ -- * Error types MemcacheError(..),- statusToError,+ Status(..),+ ClientError(..),+ ProtocolError(..),++ -- * Error creation throwStatus,- throwIncorrectRes,- ClientError(..)+ wrongOp ) where import Database.Memcache.Types@@ -14,61 +28,50 @@ import Control.Exception import Data.Typeable --- XXX: What to do about ProtocolError and IncorrectResponse exceptions? Should--- we expose these types to user or map them to ClientError?---- | Exceptions that may be thrown by Memcache. These are expected error codes--- returned by a memcached server.+-- | All exceptions that a Memcached client may throw. data MemcacheError- = MemErrNoKey- | MemErrKeyExists- | MemErrValueTooLarge- | MemErrInvalidArgs- | MemErrStoreFailed- | MemErrValueNonNumeric- | MemErrUnknownCmd- | MemErrOutOfMemory- | MemErrAuthFail+ -- | Memcached operation error.+ = OpError Status+ -- | Error occuring on client side.+ | ClientError ClientError+ -- | Errors occurring communicating with Memcached server.+ | ProtocolError ProtocolError deriving (Eq, Show, Typeable) instance Exception MemcacheError --- | Convert a status to an error. Note, not all status's are errors and so--- this is a partial function!-statusToError :: Status -> MemcacheError-{-# INLINE statusToError #-}-statusToError NoError = error "statusToError: called on NoError"-statusToError ErrKeyNotFound = MemErrNoKey-statusToError ErrKeyExists = MemErrKeyExists-statusToError ErrValueTooLarge = MemErrValueTooLarge-statusToError ErrInvalidArgs = MemErrInvalidArgs-statusToError ErrItemNotStored = MemErrStoreFailed-statusToError ErrValueNonNumeric = MemErrValueNonNumeric-statusToError ErrUnknownCommand = MemErrUnknownCmd-statusToError ErrOutOfMemory = MemErrOutOfMemory-statusToError SaslAuthFail = MemErrAuthFail-statusToError SaslAuthContinue = error "statusToError: called on SaslAuthContinue"+-- | Errors that occur on the client.+data ClientError+ -- | All servers are currently marked failed.+ = NoServersReady+ -- | Timeout occurred sending request to server.+ | Timeout+ deriving (Eq, Show, Typeable) --- | Convert a status to an exception. Note, not all status's are errors and so--- this is not a complete function!+-- | Errors related to Memcached protocol and bytes on the wire.+data ProtocolError+ -- | Received an unknown response packet.+ = UnknownPkt { protocolError :: String }+ -- | Unknown Memcached operation.+ | UnknownOp { protocolError :: String }+ -- | Unknown Memcached status field value.+ | UnknownStatus { protocolError :: String }+ -- | Unexpected length of a Memcached field (extras, key, or value).+ | BadLength { protocolError :: String }+ -- | Response packet is for a different operation than expected.+ | WrongOp { protocolError :: String }+ -- | Network socket closed without receiving enough bytes.+ | UnexpectedEOF { protocolError :: String }+ deriving (Eq, Show, Typeable)++-- | Convert a status to 'MemcacheError' exception. throwStatus :: Status -> IO a-{-# INLINE throwStatus #-}-throwStatus = throwIO . statusToError+throwStatus s = throwIO $ OpError s --- | Throw an IncorrectResponse exception for a wrong received response.-throwIncorrectRes :: Response -> String -> IO a-{-# INLINE throwIncorrectRes #-}-throwIncorrectRes r msg = throwIO- IncorrectResponse {- increspMessage = "Expected " ++ msg ++ " response! Got: " ++ show (resOp r),- increspActual = r+-- | Create a properly formatted 'WrongOp' protocol error.+wrongOp :: Response -> String -> MemcacheError+wrongOp r msg = ProtocolError $+ WrongOp {+ protocolError = "Expected " ++ msg ++ "! Got: " ++ show (resOp r) }---- | Errors that occur between the client and server in communicating. These--- are unexpected exceptions, such as network failures or garbage data.-data ClientError- = NotEnoughBytes- deriving (Eq, Show, Typeable)--instance Exception ClientError
− Database/Memcache/Protocol.hs
@@ -1,256 +0,0 @@-{-# LANGUAGE ScopedTypeVariables #-}---- | A raw, low level interface to the memcached protocol.------ The various operations are represented in full as they appear at the--- protocol level and so aren't generally well suited for application use.--- Instead, applications should use Database.Memcache.Client which presents a--- higher level API suited for application use.-module Database.Memcache.Protocol (- get, gat, touch,- set, set', add, replace,- delete,- increment, decrement,- append, prepend,- StatResults, stats,- flush,- noop, version, quit- ) where--import Database.Memcache.Errors-import Database.Memcache.Server-import Database.Memcache.Types-import Database.Memcache.Wire--import qualified Control.Exception as E-import Control.Monad-import Data.ByteString (ByteString)-import qualified Data.ByteString as B-import Data.Word-import qualified Network.Socket as N---- XXX: Structure Vs. Args?--- i.e., --- replace :: Server -> Key -> Value -> Flags -> Expiration -> Version -> IO (Maybe Version)--- vs.--- replace :: Server -> Request -> IO (Maybe Version)------ Request--- { key :: Key--- , value :: Value--- , flags :: Flags--- , exp :: Expiration--- , ver :: Version--- }------ Using a structure would allow easy defaults...-----get :: Server -> Key -> IO (Maybe (Value, Flags, Version))-get c k = do- let msg = emptyReq { reqOp = ReqGet Loud NoKey k }- r <- sendRecv c msg- (v, f) <- case resOp r of- ResGet Loud v f -> return (v, f)- _ -> throwIncorrectRes r "GET"- case resStatus r of- NoError -> return $ Just (v, f, resCas r)- ErrKeyNotFound -> return Nothing- rs -> throwStatus rs--gat :: Server -> Key -> Expiration -> IO (Maybe (Value, Flags, Version))-gat c k e = do- let msg = emptyReq { reqOp = ReqGAT Loud NoKey k (SETouch e) }- r <- sendRecv c msg- (v, f) <- case resOp r of- ResGAT Loud v f -> return (v, f)- _ -> throwIncorrectRes r "GAT"- case resStatus r of- NoError -> return $ Just (v, f, resCas r)- ErrKeyNotFound -> return Nothing- rs -> throwStatus rs--touch :: Server -> Key -> Expiration -> IO (Maybe Version)-touch c k e = do- let msg = emptyReq { reqOp = ReqTouch k (SETouch e) }- r <- sendRecv c msg- when (resOp r /= ResTouch) $ throwIncorrectRes r "TOUCH"- case resStatus r of- NoError -> return $ Just (resCas r)- ErrKeyNotFound -> return Nothing- rs -> throwStatus rs--set :: Server -> Key -> Value -> Flags -> Expiration -> IO Version-set c k v f e = do- let msg = emptyReq { reqOp = ReqSet Loud k v (SESet f e) }- r <- sendRecv c msg- when (resOp r /= ResSet Loud) $ throwIncorrectRes r "SET"- case resStatus r of- NoError -> return $ resCas r- rs -> throwStatus rs---- XXX: Use a return type like: Return = OK Version | NotFound | NotVersion?-set' :: Server -> Key -> Value -> Flags -> Expiration -> Version -> IO (Maybe Version)-set' c k v f e ver = do- let msg = emptyReq { reqOp = ReqSet Loud k v (SESet f e), reqCas = ver }- r <- sendRecv c msg- when (resOp r /= ResSet Loud) $ throwIncorrectRes r "SET"- case resStatus r of- NoError -> return $ Just (resCas r)- -- version specified and key doesn't exist...- ErrKeyNotFound -> return Nothing- -- version specified and doesn't match key...- ErrKeyExists -> return Nothing- rs -> throwStatus rs--add :: Server -> Key -> Value -> Flags -> Expiration -> IO (Maybe Version)-add c k v f e = do- let msg = emptyReq { reqOp = ReqAdd Loud k v (SESet f e) }- r <- sendRecv c msg- when (resOp r /= ResAdd Loud) $ throwIncorrectRes r "ADD"- case resStatus r of- NoError -> return $ Just (resCas r)- ErrKeyExists -> return Nothing- rs -> throwStatus rs--replace :: Server -> Key -> Value -> Flags -> Expiration -> Version -> IO (Maybe Version)-replace c k v f e ver = do- let msg = emptyReq { reqOp = ReqReplace Loud k v (SESet f e), reqCas = ver }- r <- sendRecv c msg- when (resOp r /= ResReplace Loud) $ throwIncorrectRes r "REPLACE"- case resStatus r of- NoError -> return $ Just (resCas r)- -- replace only applies to an existing key...- ErrKeyNotFound -> return Nothing- -- version specified and doesn't match key...- ErrKeyExists -> return Nothing- rs -> throwStatus rs--delete :: Server -> Key -> Version -> IO Bool-delete c k ver = do- let msg = emptyReq { reqOp = ReqDelete Loud k, reqCas = ver }- r <- sendRecv c msg- when (resOp r /= ResDelete Loud) $ throwIncorrectRes r "DELETE"- case resStatus r of- NoError -> return True- -- delete only applies to an existing key...- ErrKeyNotFound -> return False- -- version specified and doesn't match key...- ErrKeyExists -> return False- rs -> throwStatus rs--increment :: Server -> Key -> Initial -> Delta -> Expiration -> Version -> IO (Maybe (Word64, Version))-increment c k i d e ver = do- let msg = emptyReq { reqOp = ReqIncrement Loud k (SEIncr i d e), reqCas = ver }- r <- sendRecv c msg- n <- case resOp r of- ResIncrement Loud n -> return n- _ -> throwIncorrectRes r "INCREMENT"- case resStatus r of- NoError -> return $ Just (n, resCas r)- ErrKeyNotFound -> return Nothing- ErrKeyExists -> return Nothing- rs -> throwStatus rs--decrement :: Server -> Key -> Initial -> Delta -> Expiration -> Version -> IO (Maybe (Word64, Version))-decrement c k i d e ver = do- let msg = emptyReq { reqOp = ReqDecrement Loud k (SEIncr i d e), reqCas = ver }- r <- sendRecv c msg- n <- case resOp r of- ResDecrement Loud n -> return n- _ -> throwIncorrectRes r "DECREMENT"- case resStatus r of- NoError -> return $ Just (n, resCas r)- ErrKeyNotFound -> return Nothing- ErrKeyExists -> return Nothing- rs -> throwStatus rs--append :: Server -> Key -> Value -> Version -> IO (Maybe Version)-append c k v ver = do- let msg = emptyReq { reqOp = ReqAppend Loud k v, reqCas = ver }- r <- sendRecv c msg- when (resOp r /= ResAppend Loud) $ throwIncorrectRes r "APPEND"- case resStatus r of- NoError -> return $ Just (resCas r)- ErrKeyNotFound -> return Nothing- rs -> throwStatus rs--prepend :: Server -> Key -> Value -> Version -> IO (Maybe Version)-prepend c k v ver = do- let msg = emptyReq { reqOp = ReqPrepend Loud k v, reqCas = ver }- r <- sendRecv c msg- when (resOp r /= ResPrepend Loud) $ throwIncorrectRes r "PREPEND"- case resStatus r of- NoError -> return $ Just (resCas r)- ErrKeyNotFound -> return Nothing- rs -> throwStatus rs--flush :: Server -> Maybe Expiration -> IO ()-flush c e = do- let e' = SETouch `fmap` e- msg = emptyReq { reqOp = ReqFlush Loud e' }- r <- sendRecv c msg- when (resOp r /= ResFlush Loud) $ throwIncorrectRes r "FLUSH"- case resStatus r of- NoError -> return ()- rs -> throwStatus rs--noop :: Server -> IO ()-noop c = do- let msg = emptyReq { reqOp = ReqNoop }- r <- sendRecv c msg- when (resOp r /= ResNoop) $ throwIncorrectRes r "NOOP"- case resStatus r of- NoError -> return ()- rs -> throwStatus rs--version :: Server -> IO ByteString-version c = do- let msg = emptyReq { reqOp = ReqVersion }- r <- sendRecv c msg- v <- case resOp r of- ResVersion v -> return v- _ -> throwIncorrectRes r "VERSION"- case resStatus r of- NoError -> return v- rs -> throwStatus rs---- | StatResults are a list of key-value pairs.-type StatResults = [(ByteString, ByteString)]---- XXX: Should this be Maybe? Does wrong key return error or just empty--- results?-stats :: Server -> Maybe Key -> IO (Maybe StatResults)-stats c key = withSocket c $ \s -> do- let msg = emptyReq { reqOp = ReqStat key }- send s msg- getAllStats s []- where- getAllStats s xs = do- r <- recv s- (k, v) <- case resOp r of- ResStat k v -> return (k, v)- _ -> throwIncorrectRes r "STATS"- case resStatus r of- NoError | B.null k && B.null v -> return $ Just xs- | otherwise -> getAllStats s $ (k, v):xs- ErrKeyNotFound -> return Nothing- rs -> throwStatus rs--quit :: Server -> IO ()-quit c = do- -- TODO: not clear if waiting for a reply matters- withSocket c $ \s -> sendClose s `E.catch` consumeError- close c- where- consumeError (_ ::E.SomeException) = return ()- sendClose s = do- let msg = emptyReq { reqOp = ReqQuit Loud }- send s msg- N.shutdown s N.ShutdownSend- r <- recv s- when (resOp r /= ResQuit Loud) $ throwIncorrectRes r "QUIT"- case resStatus r of- NoError -> return ()- rs -> throwStatus rs-
Database/Memcache/SASL.hs view
@@ -1,53 +1,69 @@ {-# OPTIONS_GHC -fno-warn-unused-binds #-} --- | SASL authentication support for memcache.+{-|+Module : Database.Memcache.SASL+Description : SASL Authentication+Copyright : (c) David Terei, 2016+License : BSD+Maintainer : code@davidterei.com+Stability : stable+Portability : GHC++SASL authentication support for Memcached.+-} module Database.Memcache.SASL (- authenticate, Authentication(..), Username, Password+ -- * Types+ Authentication(..), Username, Password,++ -- * Operations+ authenticate ) where import Database.Memcache.Errors+import Database.Memcache.Socket import Database.Memcache.Types-import Database.Memcache.Wire -import qualified Control.Exception as E (onException)+import Control.Exception (throwIO) import Control.Monad import Data.ByteString.Char8 as B8 (ByteString, pack, singleton) import Data.Monoid-import Network.Socket (Socket) -- | Perform SASL authentication with the server. authenticate :: Socket -> Authentication -> IO ()+{-# INLINE authenticate #-}+authenticate _ NoAuth = return ()+authenticate s (Auth u p) = saslAuthPlain s u p -- NOTE: For correctness really should check that PLAIN auth is supported first -- but we'll just assume it is as that's all mainline and other implementations -- support and one exception is nearly as good as another.-authenticate _ NoAuth = return ()-authenticate s (Auth u p) = saslAuthPlain s u p - -- | Perform SASL PLAIN authentication. saslAuthPlain :: Socket -> Username -> Password -> IO ()+{-# INLINE saslAuthPlain #-} saslAuthPlain s u p = do let credentials = singleton '\0' <> u <> singleton '\0' <> p msg = emptyReq { reqOp = ReqSASLStart (B8.pack "PLAIN") credentials } send s msg- r <- recv s `E.onException` throwStatus SaslAuthFail- when (resOp r /= ResSASLStart) $ throwIncorrectRes r "SASL_START"+ r <- recv s+ when (resOp r /= ResSASLStart) $+ throwIO $ wrongOp r "SASL_START" case resStatus r of NoError -> return ()- rs -> throwStatus rs+ rs -> throwIO $ OpError rs -- | List available SASL authentication methods. We could call this but as we--- only support PLAIN as does the memcached server, we simply assume PLAIN+-- only support PLAIN as does the Memcached server, we simply assume PLAIN -- authentication is supprted and try that. saslListMechs :: Socket -> IO B8.ByteString+{-# INLINE saslListMechs #-} saslListMechs s = do let msg = emptyReq { reqOp = ReqSASLList } send s msg r <- recv s v <- case resOp r of ResSASLList v -> return v- _ -> throwIncorrectRes r "SASL_LIST"+ _ -> throwIO $ wrongOp r "SASL_LIST" case resStatus r of- NoError -> return v- rs -> throwStatus rs+ NoError -> return v+ rs -> throwIO $ OpError rs
Database/Memcache/Server.hs view
@@ -1,19 +1,38 @@--- | Handles the connections between a memcached client and a single server.+{-# LANGUAGE RecordWildCards #-}++{-|+Module : Database.Memcache.Server+Description : Server Handling+Copyright : (c) David Terei, 2016+License : BSD+Maintainer : code@davidterei.com+Stability : stable+Portability : GHC++Handles the connections between a Memcached client and a single server.++Memcached expected errors (part of protocol) are returned in the Response,+unexpected errors (e.g., network failure) are thrown as exceptions. While+the Server datatype supports a `failed` and `failedAt` flag for managing+retries, it's up to consumers to use this.+-} module Database.Memcache.Server (- Server(sid), newServer, sendRecv, withSocket, close+ -- * Server+ Server(sid, failed), newServer, sendRecv, withSocket, close ) where import Database.Memcache.SASL-import Database.Memcache.Types-import Database.Memcache.Wire+import Database.Memcache.Socket import Control.Exception import Data.Hashable+import Data.IORef import Data.Pool import Data.Time.Clock (NominalDiffTime)+import Data.Time.Clock.POSIX (POSIXTime) import Network.BSD (getProtocolNumber, getHostByName, hostAddress)-import Network.Socket (HostName, PortNumber(..), Socket)+import Network.Socket (HostName, PortNumber(..)) import qualified Network.Socket as S -- Connection pool constants.@@ -24,40 +43,51 @@ sCONNECTIONS = 1 sKEEPALIVE = 300 --- | A memcached server connection.+-- | Memcached server connection. data Server = Server {- sid :: {-# UNPACK #-} !Int,- pool :: Pool Socket,- _addr :: !HostName,- _port :: !PortNumber,- _auth :: !Authentication,- failed :: !Bool+ -- | ID of server for consistent hashing.+ sid :: {-# UNPACK #-} !Int,+ -- | Connection pool to server.+ pool :: Pool Socket,+ -- | Hostname of server.+ addr :: !HostName,+ -- | Port number of server.+ port :: !PortNumber,+ -- | Credentials for server.+ auth :: !Authentication,+ -- | When did the server fail? 0 if it is alive.+ failed :: IORef POSIXTime -- TODO: -- weight :: Double -- tansport :: Transport (UDP vs. TCP) -- poolLim :: Int (pooled connection limit) -- cnxnBuf :: IORef ByteString- } deriving Show+ } +instance Show Server where+ show Server{..} =+ "Server [" ++ show sid ++ "] " ++ addr ++ ":" ++ show port+ instance Eq Server where (==) x y = sid x == sid y instance Ord Server where compare x y = compare (sid x) (sid y) --- | Create a new memcached connection.+-- | Create a new Memcached server connection. newServer :: HostName -> PortNumber -> Authentication -> IO Server newServer host port auth = do+ fat <- newIORef 0 pSock <- createPool connectSocket releaseSocket sSTRIPES sKEEPALIVE sCONNECTIONS return Server- { sid = serverHash- , pool = pSock- , _addr = host- , _port = port- , _auth = auth- , failed = False+ { sid = serverHash+ , pool = pSock+ , addr = host+ , port = port+ , auth = auth+ , failed = fat } where serverHash = hash (host, fromEnum port)@@ -78,19 +108,22 @@ releaseSocket = S.close --- | Send and receive a single request/response pair to the memcached server.+-- | Send and receive a single request/response pair to the Memcached server. sendRecv :: Server -> Request -> IO Response-sendRecv svr msg = withResource (pool svr) $ \s -> do+{-# INLINE sendRecv #-}+sendRecv svr msg = withSocket svr $ \s -> do send s msg recv s -- | Run a function with access to an server socket for using 'send' and -- 'recv'. withSocket :: Server -> (Socket -> IO a) -> IO a+{-# INLINE withSocket #-} withSocket svr = withResource $ pool svr -- | Close the server connection. If you perform another operation after this, -- the connection will be re-established. close :: Server -> IO ()+{-# INLINE close #-} close srv = destroyAllResources $ pool srv
+ Database/Memcache/Socket.hs view
@@ -0,0 +1,423 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}++{-|+Module : Database.Memcache.Socket+Description : Connection Handling+Copyright : (c) David Terei, 2016+License : BSD+Maintainer : code@davidterei.com+Stability : stable+Portability : GHC++Handles a single Memcached connection, sending and receiving requests.+-}+module Database.Memcache.Socket (+ -- * Types+ Socket, Request(..), Response(..),++ -- * Operations+ send, recv,++ -- * Serialization / Deserialization+ szRequest, szResponse, dzHeader, dzResponse+ ) where++-- FIXME: Wire works with lazy bytestrings but we receive strict bytestrings+-- from the network...++import Database.Memcache.Errors+import Database.Memcache.Types++import Blaze.ByteString.Builder+#if __GLASGOW_HASKELL__ < 710+import Control.Applicative+#endif+import Control.Exception (throw, throwIO)+import Control.Monad+import Data.Binary.Get+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Data.Monoid+import Data.Word+import Network.Socket (Socket, isConnected, isReadable)+import qualified Network.Socket.ByteString as N++-- | Send a request to the Memcached server.+send :: Socket -> Request -> IO ()+{-# INLINE send #-}+send s m = N.sendAll s (toByteString $ szRequest m)++-- | Retrieve a single response from the Memcached server.+-- FIXME: read into buffer to minimize read syscalls+recv :: Socket -> IO Response+{-# INLINE recv #-}+recv s = do+ header <- recvAll mEMCACHE_HEADER_SIZE mempty+ let h = runGet (dzHeader PktResponse) (L.fromChunks [header])+ if bodyLen h > 0+ then do+ let bytesToRead = fromIntegral $ bodyLen h+ body <- recvAll bytesToRead mempty+ return $ dzResponse h (L.fromChunks [body])+ else return $ dzResponse h L.empty+ where+ recvAll :: Int -> Builder -> IO B.ByteString+ recvAll 0 !acc = return $! toByteString acc+ recvAll !n !acc = do+ canRead <- isSocketActive s+ if canRead+ then do+ buf <- N.recv s n+ case B.length buf of+ 0 -> throwIO errEOF+ bl | bl == n ->+ return $! (toByteString $! acc <> fromByteString buf)+ bl -> recvAll (n - bl) (acc <> fromByteString buf)+ else throwIO errEOF+ + errEOF :: MemcacheError+ errEOF = ProtocolError UnexpectedEOF { protocolError = "" }++-- | Check whether we can still operate on this socket or not.+isSocketActive :: Socket -> IO Bool+{-# INLINE isSocketActive #-}+isSocketActive s = (&&) <$> isConnected s <*> isReadable s++-- | Serialize a response to a ByteString Builder.+szResponse :: Response -> Builder+szResponse res =+ fromWord8 0x81+ <> fromWord8 c+ <> fromWord16be (fromIntegral keyl)+ <> fromWord8 (fromIntegral extl)+ <> fromWord8 0+ <> fromWord16be 0+ <> fromWord32be (fromIntegral $ extl + keyl + vall)+ <> fromWord32be (resOpaque res)+ <> fromWord64be (resCas res)+ <> ext'+ <> key'+ <> val'+ where+ (c, k', v', ext', extl) = szOpResponse (resOp res)+ (keyl, key') = case k' of+ Just k -> (B.length k, fromByteString k)+ Nothing -> (0, mempty)+ (vall, val') = case v' of+ Just v -> (B.length v, fromByteString v)+ Nothing -> (0, mempty)++szOpResponse :: OpResponse -> (Word8, Maybe Key, Maybe Value, Builder, Int)+szOpResponse o = case o of+ ResGet Loud v f -> (0x00, Nothing, Just v, fromWord32be f, 4)+ ResGet Quiet v f -> (0x09, Nothing, Just v, fromWord32be f, 4)+ ResGetK Loud k v f -> (0x0C, Just k, Just v, fromWord32be f, 4)+ ResGetK Quiet k v f -> (0x0D, Just k, Just v, fromWord32be f, 4)+ ResSet Loud -> (0x01, Nothing, Nothing, mempty, 0)+ ResSet Quiet -> (0x11, Nothing, Nothing, mempty, 0)+ ResAdd Loud -> (0x02, Nothing, Nothing, mempty, 0)+ ResAdd Quiet -> (0x12, Nothing, Nothing, mempty, 0)+ ResReplace Loud -> (0x03, Nothing, Nothing, mempty, 0)+ ResReplace Quiet -> (0x13, Nothing, Nothing, mempty, 0)+ ResDelete Loud -> (0x04, Nothing, Nothing, mempty, 0)+ ResDelete Quiet -> (0x14, Nothing, Nothing, mempty, 0)+ ResIncrement Loud f -> (0x05, Nothing, Nothing, fromWord64be f, 8)+ ResIncrement Quiet f -> (0x15, Nothing, Nothing, fromWord64be f, 8)+ ResDecrement Loud f -> (0x06, Nothing, Nothing, fromWord64be f, 8)+ ResDecrement Quiet f -> (0x16, Nothing, Nothing, fromWord64be f, 8)+ ResAppend Loud -> (0x0E, Nothing, Nothing, mempty, 0)+ ResAppend Quiet -> (0x19, Nothing, Nothing, mempty, 0)+ ResPrepend Loud -> (0x0F, Nothing, Nothing, mempty, 0)+ ResPrepend Quiet -> (0x1A, Nothing, Nothing, mempty, 0)+ ResTouch -> (0x1C, Nothing, Nothing, mempty, 0)+ ResGAT Loud v f -> (0x1D, Nothing, Just v, fromWord32be f, 4)+ ResGAT Quiet v f -> (0x1E, Nothing, Just v, fromWord32be f, 4)+ ResGATK Loud k v f -> (0x23, Just k, Just v, fromWord32be f, 4)+ ResGATK Quiet k v f -> (0x24, Just k, Just v, fromWord32be f, 4)+ ResFlush Loud -> (0x08, Nothing, Nothing, mempty, 0)+ ResFlush Quiet -> (0x18, Nothing, Nothing, mempty, 0)+ ResNoop -> (0x0A, Nothing, Nothing, mempty, 0)+ ResVersion v -> (0x0B, Nothing, Just v, mempty, 0)+ ResStat k v -> (0x10, Just k, Just v, mempty, 0)+ ResQuit Loud -> (0x07, Nothing, Nothing, mempty, 0)+ ResQuit Quiet -> (0x17, Nothing, Nothing, mempty, 0)+ ResSASLList v -> (0x20, Nothing, Just v, mempty, 0)+ ResSASLStart -> (0x21, Nothing, Nothing, mempty, 0)+ ResSASLStep -> (0x22, Nothing, Nothing, mempty, 0)++-- | Serialize a request to a ByteString Builder.+szRequest :: Request -> Builder+szRequest req =+ fromWord8 0x80+ <> fromWord8 c+ <> fromWord16be (fromIntegral keyl)+ <> fromWord8 (fromIntegral extl)+ <> fromWord8 0+ <> fromWord16be 0+ <> fromWord32be (fromIntegral $ extl + keyl + vall)+ <> fromWord32be (reqOpaque req)+ <> fromWord64be (reqCas req)+ <> ext'+ <> key'+ <> val'+ where+ (c, k', v', ext', extl) = szOpRequest (reqOp req)+ (keyl, key') = case k' of+ Just k -> (B.length k, fromByteString k)+ Nothing -> (0, mempty)+ (vall, val') = case v' of+ Just v -> (B.length v, fromByteString v)+ Nothing -> (0, mempty)++-- Extract needed info from an OpRequest for serialization.+-- FIXME: Make sure this is optimized well (no tuple, boxing, unboxing, inlined)+szOpRequest :: OpRequest -> (Word8, Maybe Key, Maybe Value, Builder, Int)+szOpRequest o = case o of+ -- FIXME: make sure this isn't a thunk! (c)+ ReqGet q k key -> let c | q == Loud && k == NoKey = 0x00+ | q == Loud && k == IncludeKey = 0x0C+ | k == NoKey = 0x09 -- Quiet+ | otherwise = 0x0D -- Quiet && IncludeKey+ in (c, Just key, Nothing, mempty, 0)++ ReqSet Loud key v e -> (0x01, Just key, Just v, szSESet e, 8)+ ReqSet Quiet key v e -> (0x11, Just key, Just v, szSESet e, 8)+ ReqAdd Loud key v e -> (0x02, Just key, Just v, szSESet e, 8)+ ReqAdd Quiet key v e -> (0x12, Just key, Just v, szSESet e, 8)+ ReqReplace Loud key v e -> (0x03, Just key, Just v, szSESet e, 8)+ ReqReplace Quiet key v e -> (0x13, Just key, Just v, szSESet e, 8)+ ReqDelete Loud key -> (0x04, Just key, Nothing, mempty, 0)+ ReqDelete Quiet key -> (0x14, Just key, Nothing, mempty, 0)+ ReqIncrement Loud key e -> (0x05, Just key, Nothing, szSEIncr e, 20)+ ReqIncrement Quiet key e -> (0x15, Just key, Nothing, szSEIncr e, 20)+ ReqDecrement Loud key e -> (0x06, Just key, Nothing, szSEIncr e, 20)+ ReqDecrement Quiet key e -> (0x16, Just key, Nothing, szSEIncr e, 20)+ ReqAppend Loud key v -> (0x0E, Just key, Just v, mempty, 0)+ ReqAppend Quiet key v -> (0x19, Just key, Just v, mempty, 0)+ ReqPrepend Loud key v -> (0x0F, Just key, Just v, mempty, 0)+ ReqPrepend Quiet key v -> (0x1A, Just key, Just v, mempty, 0)+ ReqTouch key e -> (0x1C, Just key, Nothing, szSETouch e, 4)+ -- FIXME: beware allocation.+ ReqGAT q k key e -> let c | q == Quiet && k == IncludeKey = 0x24+ | q == Quiet && k == NoKey = 0x1E+ | k == IncludeKey = 0x23+ | otherwise = 0x1D+ in (c, Just key, Nothing, szSETouch e, 4)+ ReqFlush Loud (Just e) -> (0x08, Nothing, Nothing, szSETouch e, 4)+ ReqFlush Quiet (Just e) -> (0x18, Nothing, Nothing, szSETouch e, 4)+ ReqFlush Loud Nothing -> (0x08, Nothing, Nothing, mempty, 0)+ ReqFlush Quiet Nothing -> (0x18, Nothing, Nothing, mempty, 0)+ ReqNoop -> (0x0A, Nothing, Nothing, mempty, 0)+ ReqVersion -> (0x0B, Nothing, Nothing, mempty, 0)+ ReqStat key -> (0x10, key, Nothing, mempty, 0)+ ReqQuit Loud -> (0x07, Nothing, Nothing, mempty, 0)+ ReqQuit Quiet -> (0x17, Nothing, Nothing, mempty, 0)+ ReqSASLList -> (0x20, Nothing, Nothing, mempty, 0)+ ReqSASLStart key v -> (0x21, Just key, Just v, mempty, 0)+ ReqSASLStep key v -> (0x22, Just key, Just v, mempty, 0)++ where+ szSESet (SESet f e) = fromWord32be f <> fromWord32be e+ szSEIncr (SEIncr i d e) = fromWord64be i <> fromWord64be d <> fromWord32be e+ szSETouch (SETouch e) = fromWord32be e++-- | Deserialize a Header from a ByteString.+dzHeader :: PktType -> Get Header+{-# INLINE dzHeader #-}+dzHeader pkt = do+ m <- getWord8+ case pkt of+ PktResponse -> when (m /= 0x81) $+ throw $ ProtocolError UnknownPkt { protocolError = show m }+ PktRequest -> when (m /= 0x80) $+ throw $ ProtocolError UnknownPkt { protocolError = show m }+ o <- getWord8+ kl <- getWord16be+ el <- getWord8+ skip 1 -- unused data type field+ st <- dzStatus+ bl <- getWord32be+ opq <- getWord32be+ ver <- getWord64be+ return Header {+ op = o,+ keyLen = kl,+ extraLen = el,+ status = st,+ bodyLen = bl,+ opaque = opq,+ cas = ver+ }++-- | Deserialize a Response body.+dzResponse :: Header -> L.ByteString -> Response+dzResponse h = runGet $+ case op h of+ 0x00 -> dzGetResponse h $ ResGet Loud+ 0x09 -> dzGetResponse h $ ResGet Quiet+ 0x1D -> dzGetResponse h $ ResGAT Loud+ 0x1E -> dzGetResponse h $ ResGAT Quiet+ 0x0C -> dzGetKResponse h $ ResGetK Loud+ 0x0D -> dzGetKResponse h $ ResGetK Quiet+ 0x23 -> dzGetKResponse h $ ResGATK Loud+ 0x24 -> dzGetKResponse h $ ResGATK Quiet+ 0x05 -> dzNumericResponse h $ ResIncrement Loud+ 0x15 -> dzNumericResponse h $ ResIncrement Quiet+ 0x06 -> dzNumericResponse h $ ResDecrement Loud+ 0x16 -> dzNumericResponse h $ ResDecrement Quiet+ 0x01 -> dzGenericResponse h $ ResSet Loud+ 0x11 -> dzGenericResponse h $ ResSet Quiet+ 0x02 -> dzGenericResponse h $ ResAdd Loud+ 0x12 -> dzGenericResponse h $ ResAdd Quiet+ 0x03 -> dzGenericResponse h $ ResReplace Loud+ 0x13 -> dzGenericResponse h $ ResReplace Quiet+ 0x04 -> dzGenericResponse h $ ResDelete Loud+ 0x14 -> dzGenericResponse h $ ResDelete Quiet+ 0x0E -> dzGenericResponse h $ ResAppend Loud+ 0x19 -> dzGenericResponse h $ ResAppend Quiet+ 0x0F -> dzGenericResponse h $ ResPrepend Loud+ 0x1A -> dzGenericResponse h $ ResPrepend Quiet+ 0x1C -> dzGenericResponse h ResTouch+ 0x07 -> dzGenericResponse h $ ResQuit Loud+ 0x17 -> dzGenericResponse h $ ResQuit Quiet+ 0x08 -> dzGenericResponse h $ ResFlush Loud+ 0x18 -> dzGenericResponse h $ ResFlush Quiet+ 0x0A -> dzGenericResponse h ResNoop+ 0x10 -> dzKeyValueResponse h ResStat+ 0x0B -> dzValueResponse h ResVersion+ -- SASL+ 0x20 -> dzValueResponse h ResSASLList+ 0x21 -> dzGenericResponse h ResSASLStart+ 0x22 -> dzGenericResponse h ResSASLStep++ _ -> throw $ ProtocolError UnknownOp { protocolError = show (op h) }++-- | Deserialize the body of a Response that contains nothing.+dzGenericResponse :: Header -> OpResponse -> Get Response+dzGenericResponse h o = do+ skip (fromIntegral $ bodyLen h)+ chkLength 0 (extraLen h) "Extra"+ chkLength 0 (keyLen h) "Key"+ chkLength 0 (bodyLen h) "Body"+ return Res {+ resOp = o,+ resStatus = status h,+ resOpaque = opaque h,+ resCas = cas h+ }++-- | Deserialize the body of a Get Response (Extras [flags] & Value).+dzGetResponse :: Header -> (Value -> Flags -> OpResponse) -> Get Response+dzGetResponse h o = do+ e <- if status h == NoError && el == 4+ then getWord32be+ else skip el >> return 0+ v <- getByteString vl+ chkLength 4 el "Extra"+ chkLength 0 (keyLen h) "Key"+ return Res {+ resOp = o v e,+ resStatus = status h,+ resOpaque = opaque h,+ resCas = cas h+ }+ where+ el = fromIntegral $ extraLen h+ vl = fromIntegral (bodyLen h) - el+ +-- | Deserialize the body of a GetK Response (Extras [flags] & Key & Value).+dzGetKResponse :: Header -> (Key -> Value -> Flags -> OpResponse) -> Get Response+dzGetKResponse h o = do+ e <- if status h == NoError && el == 4+ then getWord32be+ else skip el >> return 0+ k <- getByteString kl+ v <- getByteString vl+ chkLength 4 el "Extra"+ -- FIXME: check strictness ($!)+ return Res {+ resOp = o k v e,+ resStatus = status h,+ resOpaque = opaque h,+ resCas = cas h+ }+ where+ el = fromIntegral $ extraLen h+ kl = fromIntegral $ keyLen h+ vl = fromIntegral (bodyLen h) - el - kl++-- | Deserialize the body of a Incr/Decr Response (Value [Word64]).+dzNumericResponse :: Header -> (Word64 -> OpResponse) -> Get Response+dzNumericResponse h o = do+ v <- if status h == NoError && bodyLen h == 8+ then getWord64be+ else skip (fromIntegral $ bodyLen h) >> return 0+ chkLength 0 (extraLen h) "Extra"+ chkLength 0 (keyLen h) "Key"+ chkLength 8 (bodyLen h) "Body"+ return Res {+ resOp = o v,+ resStatus = status h,+ resOpaque = opaque h,+ resCas = cas h+ }++-- | Deserialize the body of a general response that just has a value (no key+-- or extras).+dzValueResponse :: Header -> (Value -> OpResponse) -> Get Response+dzValueResponse h o = do+ v <- getByteString (fromIntegral $ bodyLen h)+ chkLength 0 (extraLen h) "Extra"+ chkLength 0 (keyLen h) "Key"+ return Res {+ resOp = o v,+ resStatus = status h,+ resOpaque = opaque h,+ resCas = cas h+ }++-- | Deserialize the body of a general response that just has a key and value+-- (no extras).+dzKeyValueResponse :: Header -> (Key -> Value -> OpResponse) -> Get Response+dzKeyValueResponse h o = do+ k <- getByteString kl+ v <- getByteString vl+ chkLength 0 (extraLen h) "Extra"+ return Res {+ resOp = o k v,+ resStatus = status h,+ resOpaque = opaque h,+ resCas = cas h+ }+ where+ kl = fromIntegral $ keyLen h+ vl = fromIntegral (bodyLen h) - kl++-- | Deserialize a Response status code.+dzStatus :: Get Status+dzStatus = do+ st <- getWord16be+ return $ case st of+ 0x00 -> NoError+ 0x01 -> ErrKeyNotFound+ 0x02 -> ErrKeyExists+ 0x03 -> ErrValueTooLarge+ 0x04 -> ErrInvalidArgs+ 0x05 -> ErrItemNotStored+ 0x06 -> ErrValueNonNumeric+ 0x81 -> ErrUnknownCommand+ 0x82 -> ErrOutOfMemory+ 0x20 -> SaslAuthFail+ 0x21 -> SaslAuthContinue+ _ -> throw $ ProtocolError UnknownStatus { protocolError = show st }++-- | Check the length of a header field is as expected, throwing a+-- ProtocolError exception if it is not.+chkLength :: (Eq a, Show a) => a -> a -> String -> Get ()+{-# INLINE chkLength #-}+chkLength expected l msg = when (l /= expected) $+ return $ throw $ ProtocolError BadLength { protocolError =+ msg ++ " length expected " ++ show expected ++ " got " ++ show l+ }+
Database/Memcache/Types.hs view
@@ -1,26 +1,41 @@-{-# LANGUAGE DeriveDataTypeable #-}+{-|+Module : Database.Memcache.Types+Description : Memcached Types+Copyright : (c) David Terei, 2016+License : BSD+Maintainer : code@davidterei.com+Stability : stable+Portability : GHC --- | Stores the various types needed by memcache. Mostly concerned with the--- representation of the protocol.+Stores the various types needed by Memcached. Mostly concerned with the+representation of the protocol.+-} module Database.Memcache.Types (+ -- * SASL Authentication Authentication(..), Username, Password,- Q(..), K(..), Key, Value, Extras, Initial, Delta, Expiration, Flags, Version,- mEMCACHE_HEADER_SIZE, Header(..),++ -- * Fields & Values+ Q(..), K(..), Key, Value, Extras, Initial, Delta, Expiration, Flags,+ Version, Status(..),++ -- * Header+ Header(..), mEMCACHE_HEADER_SIZE, PktType(..),++ -- * Requests Request(..), OpRequest(..), SESet(..), SEIncr(..), SETouch(..), emptyReq,- Response(..), OpResponse(..), Status(..),- ProtocolError(..), IncorrectResponse(..)++ -- * Responses+ Response(..), OpResponse(..), emptyRes ) where -import Control.Exception import Data.ByteString (ByteString)-import Data.Typeable import Data.Word -- | SASL Authentication information for a server. data Authentication = Auth { username :: !Username, password :: !Password } | NoAuth- deriving (Show, Eq)+ deriving (Eq, Show) -- | Username for authentication. type Username = ByteString@@ -49,8 +64,22 @@ mEMCACHE_HEADER_SIZE :: Int mEMCACHE_HEADER_SIZE = 24 -data Q = Loud | Quiet deriving (Eq, Show, Typeable)-data K = NoKey | IncludeKey deriving (Eq, Show, Typeable)+-- | Memcached packet header (for both 'Request' and 'Response').+data Header = Header {+ op :: Word8,+ keyLen :: Word16,+ extraLen :: Word8,+ status :: Status,+ bodyLen :: Word32,+ opaque :: Word32,+ cas :: Version+ } deriving (Eq, Show)++data PktType = PktRequest | PktResponse+ deriving (Eq, Show)++data Q = Loud | Quiet deriving (Eq, Show)+data K = NoKey | IncludeKey deriving (Eq, Show) type Key = ByteString type Value = ByteString type Extras = ByteString@@ -60,7 +89,6 @@ type Flags = Word32 type Version = Word64 --- XXX: Which ones care about version? (Encode?) data OpRequest = ReqGet Q K Key | ReqSet Q Key Value SESet@@ -81,18 +109,19 @@ | ReqSASLList | ReqSASLStart Key Value -- key: auth method, value: auth data | ReqSASLStep Key Value -- key: auth method, value: auth data (continued)- deriving (Eq, Show, Typeable)+ deriving (Eq, Show) -data SESet = SESet Flags Expiration deriving (Eq, Show, Typeable)-data SEIncr = SEIncr Initial Delta Expiration deriving (Eq, Show, Typeable)-data SETouch = SETouch Expiration deriving (Eq, Show, Typeable)+data SESet = SESet Flags Expiration deriving (Eq, Show)+data SEIncr = SEIncr Initial Delta Expiration deriving (Eq, Show)+data SETouch = SETouch Expiration deriving (Eq, Show) data Request = Req { reqOp :: OpRequest, reqOpaque :: Word32, reqCas :: Version- } deriving (Eq, Show, Typeable)+ } deriving (Eq, Show) +-- | Noop request. emptyReq :: Request emptyReq = Req { reqOp = ReqNoop, reqOpaque = 0, reqCas = 0 } @@ -115,54 +144,47 @@ | ResVersion Value | ResStat Key Value | ResQuit Q- | ResSASLList Value+ | ResSASLList Value | ResSASLStart | ResSASLStep- deriving (Eq, Show, Typeable)+ deriving (Eq, Show) +-- | The status (success or error) of a Memcached operation returned in a+-- 'Response'. data Status+ -- | Operation successful. = NoError -- All+ -- | Key not found. | ErrKeyNotFound -- Get, GAT, Touch, Replace, Del, Inc, Dec, App, Pre, Set (key not there and version specified...)+ -- | Key exists when not expected. | ErrKeyExists -- Add, (version): Set, Rep, Del, Inc, Dec, App, Pre+ -- | Value too large to store at server. | ErrValueTooLarge -- Set, Add, Rep, Pre, App+ -- | Invalid arguments for operation. | ErrInvalidArgs -- All+ -- | Key-Value pair not stored at server (internal error). | ErrItemNotStored -- ?+ -- | Value not numeric when increment or decrement requested. | ErrValueNonNumeric -- Incr, Decr+ -- | Server doesn't know requested command. | ErrUnknownCommand -- All+ -- | Server out of memory. | ErrOutOfMemory -- All+ -- | SASL authentication failed. | SaslAuthFail -- SASL+ -- | SASL authentication requires more steps. | SaslAuthContinue -- SASL- deriving (Eq, Show, Typeable)+ deriving (Eq, Show) +-- | Memcached response packet. data Response = Res { resOp :: OpResponse, resStatus :: Status, resOpaque :: Word32, resCas :: Version- } deriving (Eq, Show, Typeable)--data Header = Header {- op :: Word8,- keyLen :: Word16,- extraLen :: Word8,- status :: Status,- bodyLen :: Word32,- opaque :: Word32,- cas :: Version- } deriving (Eq, Show, Typeable)--data ProtocolError = ProtocolError {- protocolMessage :: String,- protocolHeader :: Maybe Header,- protocolParams :: [String]- } deriving (Eq, Show, Typeable)--instance Exception ProtocolError--data IncorrectResponse = IncorrectResponse {- increspMessage :: String,- increspActual :: Response- } deriving (Eq, Show, Typeable)+ } deriving (Eq, Show) -instance Exception IncorrectResponse+-- | Noop response.+emptyRes :: Response+emptyRes = Res { resOp = ResNoop, resStatus = NoError, resOpaque = 0, resCas = 0 }
− Database/Memcache/Wire.hs
@@ -1,381 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE BangPatterns #-}---- | Deals with serializing and parsing memcached requests and responses.-module Database.Memcache.Wire (- send, recv,- szRequest, szRequest',- dzResponse, dzResponse', dzHeader, dzHeader', dzBody, dzBody'- ) where---- XXX: Wire works with lazy bytestrings but we receive strict bytestrings from--- the network...--import Database.Memcache.Errors-import Database.Memcache.Types--import Blaze.ByteString.Builder-#if __GLASGOW_HASKELL__ < 710-import Control.Applicative-#endif-import Control.Exception-import Control.Monad-import Data.Binary.Get-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as L-import Data.Monoid-import Data.Word-import Network.Socket (Socket, isConnected, isReadable)-import qualified Network.Socket.ByteString as N---- | Send a request to the memcached server.-send :: Socket -> Request -> IO ()-send s m = N.sendAll s (toByteString $ szRequest m)---- | Retrieve a single response from the memcached server.--- TODO: read into buffer to minimize read syscalls-recv :: Socket -> IO Response-recv s = do- header <- recvAll mEMCACHE_HEADER_SIZE mempty- let h = dzHeader' (L.fromChunks [header])- if bodyLen h > 0- then do- let bytesToRead = fromIntegral $ bodyLen h- body <- recvAll bytesToRead mempty- return $ dzBody' h (L.fromChunks [body])- else return $ dzBody' h L.empty- where- recvAll :: Int -> Builder -> IO B.ByteString- recvAll 0 !acc = return $! toByteString acc- recvAll !n !acc = do- canRead <- isSocketActive s- if canRead- then do- buf <- N.recv s n- let bufLen = B.length buf- if bufLen == n- then return $! (toByteString $! acc <> fromByteString buf)- else recvAll (max 0 (n - bufLen)) (acc <> fromByteString buf)- else throwIO NotEnoughBytes---- | Check whether we can still operate on this socket or not.-isSocketActive :: Socket -> IO Bool-isSocketActive s = (&&) <$> isConnected s <*> isReadable s---- | Serialize a request to a ByteString.-szRequest' :: Request -> L.ByteString-szRequest' = toLazyByteString . szRequest---- | Serialize a request to a ByteString Builder.-szRequest :: Request -> Builder-szRequest req =- fromWord8 0x80- <> fromWord8 c- <> fromWord16be (fromIntegral keyl)- <> fromWord8 (fromIntegral extl)- <> fromWord8 0- <> fromWord16be 0- <> fromWord32be (fromIntegral $ extl + keyl + vall)- <> fromWord32be (reqOpaque req)- <> fromWord64be (reqCas req)- <> ext'- <> key'- <> val'- where- (c, k', v', ext', extl) = getCodeKeyValue (reqOp req)- (keyl, key') = case k' of- Just k -> (B.length k, fromByteString k)- Nothing -> (0, mempty)- (vall, val') = case v' of- Just v -> (B.length v, fromByteString v)- Nothing -> (0, mempty)---- Extract needed info from an OpRequest for serialization.--- XXX: Make sure this is optimized well (no tuple, boxing, unboxing, inlined)-getCodeKeyValue :: OpRequest -> (Word8, Maybe Key, Maybe Value, Builder, Int)-getCodeKeyValue o = case o of- -- XXX: make sure this isn't a thunk! (c)- ReqGet q k key -> let c | q == Loud && k == NoKey = 0x00- | q == Loud && k == IncludeKey = 0x0C- | k == NoKey = 0x09 -- Quiet- | otherwise = 0x0D -- Quiet && IncludeKey- in (c, Just key, Nothing, mempty, 0)-- ReqSet Loud key v e -> (0x01, Just key, Just v, szSESet e, 8)- ReqSet Quiet key v e -> (0x11, Just key, Just v, szSESet e, 8)-- ReqAdd Loud key v e -> (0x02, Just key, Just v, szSESet e, 8)- ReqAdd Quiet key v e -> (0x12, Just key, Just v, szSESet e, 8)-- ReqReplace Loud key v e -> (0x03, Just key, Just v, szSESet e, 8)- ReqReplace Quiet key v e -> (0x13, Just key, Just v, szSESet e, 8)-- ReqDelete Loud key -> (0x04, Just key, Nothing, mempty, 0)- ReqDelete Quiet key -> (0x14, Just key, Nothing, mempty, 0)-- ReqIncrement Loud key e -> (0x05, Just key, Nothing, szSEIncr e, 20)- ReqIncrement Quiet key e -> (0x15, Just key, Nothing, szSEIncr e, 20)-- ReqDecrement Loud key e -> (0x06, Just key, Nothing, szSEIncr e, 20)- ReqDecrement Quiet key e -> (0x16, Just key, Nothing, szSEIncr e, 20)-- ReqAppend Loud key v -> (0x0E, Just key, Just v, mempty, 0)- ReqAppend Quiet key v -> (0x19, Just key, Just v, mempty, 0)-- ReqPrepend Loud key v -> (0x0F, Just key, Just v, mempty, 0)- ReqPrepend Quiet key v -> (0x1A, Just key, Just v, mempty, 0)-- ReqTouch key e -> (0x1C, Just key, Nothing, szSETouch e, 4)-- -- XXX: beware allocation.- ReqGAT q k key e -> let c | q == Quiet && k == IncludeKey = 0x24- | q == Quiet && k == NoKey = 0x1E- | k == IncludeKey = 0x23- | otherwise = 0x1D- in (c, Just key, Nothing, szSETouch e, 4)-- ReqFlush Loud (Just e) -> (0x08, Nothing, Nothing, szSETouch e, 4)- ReqFlush Quiet (Just e) -> (0x18, Nothing, Nothing, szSETouch e, 4)- ReqFlush Loud Nothing -> (0x08, Nothing, Nothing, mempty, 0)- ReqFlush Quiet Nothing -> (0x18, Nothing, Nothing, mempty, 0)- ReqNoop -> (0x0A, Nothing, Nothing, mempty, 0)- ReqVersion -> (0x0B, Nothing, Nothing, mempty, 0)- ReqStat key -> (0x10, key, Nothing, mempty, 0)- ReqQuit Loud -> (0x07, Nothing, Nothing, mempty, 0)- ReqQuit Quiet -> (0x17, Nothing, Nothing, mempty, 0)-- ReqSASLList -> (0x20, Nothing, Nothing, mempty, 0)- ReqSASLStart key v -> (0x21, Just key, Just v, mempty, 0)- ReqSASLStep key v -> (0x22, Just key, Just v, mempty, 0)-- where- szSESet (SESet f e) = fromWord32be f <> fromWord32be e- szSEIncr (SEIncr i d e) = fromWord64be i <> fromWord64be d <> fromWord32be e- szSETouch (SETouch e) = fromWord32be e---- | Deserialize a Response from a ByteString.-dzResponse' :: L.ByteString -> Response-dzResponse' = runGet dzResponse---- | Deserialize a Response.-dzResponse :: Get Response-dzResponse = do- h <- dzHeader- dzBody h---- | Deserialize a Header from a ByteString.-dzHeader' :: L.ByteString -> Header-dzHeader' = runGet dzHeader---- | Deserialize a Header.-dzHeader :: Get Header-dzHeader = do- m <- getWord8- when (m /= 0x81) $ throw- ProtocolError {- protocolMessage = "Bad magic value for a response packet",- protocolHeader = Nothing,- protocolParams = [show m]- }- o <- getWord8- kl <- getWord16be- el <- getWord8- skip 1 -- unused data type field- st <- dzStatus- bl <- getWord32be- opq <- getWord32be- ver <- getWord64be- return Header {- op = o,- keyLen = kl,- extraLen = el,- status = st,- bodyLen = bl,- opaque = opq,- cas = ver- }---- | Deserialize a Response body from a ByteString.-dzBody' :: Header -> L.ByteString -> Response-dzBody' h = runGet (dzBody h)---- | Deserialize a Response body.-dzBody :: Header -> Get Response-dzBody h =- case op h of- 0x00 -> dzGetResponse h $ ResGet Loud- 0x09 -> dzGetResponse h $ ResGet Quiet- 0x1D -> dzGetResponse h $ ResGAT Loud- 0x1E -> dzGetResponse h $ ResGAT Quiet- 0x0C -> dzGetKResponse h $ ResGetK Loud- 0x0D -> dzGetKResponse h $ ResGetK Quiet- 0x23 -> dzGetKResponse h $ ResGATK Loud- 0x24 -> dzGetKResponse h $ ResGATK Quiet- 0x05 -> dzNumericResponse h $ ResIncrement Loud- 0x15 -> dzNumericResponse h $ ResIncrement Quiet- 0x06 -> dzNumericResponse h $ ResDecrement Loud- 0x16 -> dzNumericResponse h $ ResDecrement Quiet- 0x01 -> dzGenericResponse h $ ResSet Loud- 0x11 -> dzGenericResponse h $ ResSet Quiet- 0x02 -> dzGenericResponse h $ ResAdd Loud- 0x12 -> dzGenericResponse h $ ResAdd Quiet- 0x03 -> dzGenericResponse h $ ResReplace Loud- 0x13 -> dzGenericResponse h $ ResReplace Quiet- 0x04 -> dzGenericResponse h $ ResDelete Loud- 0x14 -> dzGenericResponse h $ ResDelete Quiet- 0x0E -> dzGenericResponse h $ ResAppend Loud- 0x19 -> dzGenericResponse h $ ResAppend Quiet- 0x0F -> dzGenericResponse h $ ResPrepend Loud- 0x1A -> dzGenericResponse h $ ResPrepend Quiet- 0x1C -> dzGenericResponse h ResTouch- 0x07 -> dzGenericResponse h $ ResQuit Loud- 0x17 -> dzGenericResponse h $ ResQuit Quiet- 0x08 -> dzGenericResponse h $ ResFlush Loud- 0x18 -> dzGenericResponse h $ ResFlush Quiet- 0x0A -> dzGenericResponse h ResNoop- 0x10 -> dzKeyValueResponse h ResStat- 0x0B -> dzValueResponse h ResVersion- -- SASL- 0x20 -> dzValueResponse h ResSASLList- 0x21 -> dzGenericResponse h ResSASLStart- 0x22 -> dzGenericResponse h ResSASLStep-- _ -> throw ProtocolError {- protocolMessage = "Unknown operation type",- protocolHeader = Just h,- protocolParams = [show $ op h]- }---- | Deserialize the body of a Response that contains nothing.-dzGenericResponse :: Header -> OpResponse -> Get Response-dzGenericResponse h o = do- skip (fromIntegral $ bodyLen h)- chkLength h 0 (extraLen h) "Extra length expected to be zero"- chkLength h 0 (keyLen h) "Key length expected to be zero"- chkLength h 0 (bodyLen h) "Body length expected to be zero"- return Res {- resOp = o,- resStatus = status h,- resOpaque = opaque h,- resCas = cas h- }---- | Deserialize the body of a Get Response (Extras [flags] & Value).-dzGetResponse :: Header -> (Value -> Flags -> OpResponse) -> Get Response-dzGetResponse h o = do- e <- if status h == NoError && el == 4- then getWord32be- else skip el >> return 0- v <- getByteString vl- chkLength h 4 el "Extra length expected to be four"- chkLength h 0 (keyLen h) "Key length expected to be zero"- return Res {- resOp = o v e,- resStatus = status h,- resOpaque = opaque h,- resCas = cas h- }- where- el = fromIntegral $ extraLen h- vl = fromIntegral (bodyLen h) - el- --- | Deserialize the body of a GetK Response (Extras [flags] & Key & Value).-dzGetKResponse :: Header -> (Key -> Value -> Flags -> OpResponse) -> Get Response-dzGetKResponse h o = do- e <- if status h == NoError && el == 4- then getWord32be- else skip el >> return 0- k <- getByteString kl- v <- getByteString vl- chkLength h 4 el "Extra length expected to be four"- -- XXX: check strictness ($!)- return Res {- resOp = o k v e,- resStatus = status h,- resOpaque = opaque h,- resCas = cas h- }- where- el = fromIntegral $ extraLen h- kl = fromIntegral $ keyLen h- vl = fromIntegral (bodyLen h) - el - kl---- | Deserialize the body of a Incr/Decr Response (Value [Word64]).-dzNumericResponse :: Header -> (Word64 -> OpResponse) -> Get Response-dzNumericResponse h o = do- v <- if status h == NoError && bodyLen h == 8- then getWord64be- else skip (fromIntegral $ bodyLen h) >> return 0- chkLength h 0 (extraLen h) "Extra length expected to be zero"- chkLength h 0 (keyLen h) "Key length expected to be zero"- chkLength h 8 (bodyLen h) "body length expected to be eight"- return Res {- resOp = o v,- resStatus = status h,- resOpaque = opaque h,- resCas = cas h- }---- | Deserialize the body of a general response that just has a value (no key--- or extras).-dzValueResponse :: Header -> (Value -> OpResponse) -> Get Response-dzValueResponse h o = do- v <- getByteString (fromIntegral $ bodyLen h)- chkLength h 0 (extraLen h) "Extra length expected to be zero"- chkLength h 0 (keyLen h) "Key length expected to be zero"- return Res {- resOp = o v,- resStatus = status h,- resOpaque = opaque h,- resCas = cas h- }---- | Deserialize the body of a general response that just has a key and value--- (no extras).-dzKeyValueResponse :: Header -> (Key -> Value -> OpResponse) -> Get Response-dzKeyValueResponse h o = do- k <- getByteString kl- v <- getByteString vl- chkLength h 0 (extraLen h) "Extra length expected to be zero"- return Res {- resOp = o k v,- resStatus = status h,- resOpaque = opaque h,- resCas = cas h- }- where- kl = fromIntegral $ keyLen h- vl = fromIntegral (bodyLen h) - kl---- | Deserialize a Response status code.-dzStatus :: Get Status-dzStatus = do- st <- getWord16be- return $ case st of- 0x00 -> NoError- 0x01 -> ErrKeyNotFound- 0x02 -> ErrKeyExists- 0x03 -> ErrValueTooLarge- 0x04 -> ErrInvalidArgs- 0x05 -> ErrItemNotStored- 0x06 -> ErrValueNonNumeric- 0x81 -> ErrUnknownCommand- 0x82 -> ErrOutOfMemory- 0x20 -> SaslAuthFail- 0x21 -> SaslAuthContinue- _ -> throw ProtocolError {- protocolMessage = "Unknown status type",- protocolHeader = Nothing,- protocolParams = [show st]- }---- | Check the length of a header field is as expected, throwing a--- ProtocolError exception if it is not.-chkLength :: (Eq a, Show a) => Header -> a -> a -> String -> Get ()-chkLength h expected l msg =- when (l /= expected) $ return $ throw ProtocolError {- protocolMessage = msg,- protocolHeader = Just h,- protocolParams = [show l]- }-
TODO.md view
@@ -1,15 +1,18 @@ ## ToDo -Required:-* Connection error handling-* Timeouts- Optional:+* Differentiate between two expiration modes?+* Simpler interface?+* Is System.Timeout still slow? Switch to Warp/Snap timeout handler?+* Is NominalDiffTime slow? Switch to unix epoch time?+* Configurable fail-over mode * Multi-get * Generic multi operation support * Customizable server sharding -- mod & virtual servers Nice-to-have:+* Smarter connection handling to minimize system calls (buffered)+* Server weights * Asynchronous support * Customizable -- timeout, max connection retries, hash algorithm * Max value validation@@ -21,4 +24,5 @@ Maybe: * Typeclass for serialization * Monad / Typeclass for memcache+* Automatic serialization / deserialization
memcache.cabal view
@@ -1,15 +1,19 @@ name: memcache-version: 0.1.0.1+version: 0.2.0.0 homepage: https://github.com/dterei/memcache-hs bug-reports: https://github.com/dterei/memcache-hs/issues synopsis: A memcached client library. description: - A client library for a memcached cluster.+ A client library for a memcached cluster. Memcached is an in-memory key-value+ store typically used as a distributed and shared cache. Clients connect to a+ group of memcached servers and perform out-of-band caching for things like+ SQL results, rendered pages, or third-party APIs. . It supports the binary memcached protocol and SASL authentication. No support for the ASCII protocol is provided. It supports connecting to a single, or a cluster of memcached servers. When connecting to a cluser, consistent hashing- is used for routing requests to the appropriate server.+ is used for routing requests to the appropriate server. Timeouts, retrying+ failed operations, and failover to a different server are all supported. . Complete coverage of the memcached protocol is provided except for multi-get and other pipelined operations.@@ -18,7 +22,7 @@ . > import qualified Database.Memcache.Client as M > - > mc <- M.newClient [M.ServerSpec "localhost" 11211 M.NoAuth] M.defaultOptions+ > mc <- M.newClient [M.ServerSpec "localhost" 11211 M.NoAuth] M.def > M.set mc "key" "value" 0 0 > v <- M.get mc "key" .@@ -49,29 +53,27 @@ Database.Memcache.Client Database.Memcache.Cluster Database.Memcache.Errors- Database.Memcache.Protocol Database.Memcache.SASL Database.Memcache.Server+ Database.Memcache.Socket Database.Memcache.Types- Database.Memcache.Wire build-depends:- base < 5,- binary >= 0.6.2.0,- blaze-builder >= 0.3.1.0,- bytestring >= 0.9.2.1,- hashable >= 1.2.0.3,- network >= 2.4,- resource-pool >= 0.2.1.0,- vector >= 0.7,- vector-algorithms >= 0.5,- time >= 1.4+ base < 5,+ binary >= 0.6.2.0,+ blaze-builder >= 0.3.1.0,+ bytestring >= 0.9.2.1,+ data-default-class >= 0.1.0,+ hashable >= 1.2.0.3,+ network >= 2.4,+ resource-pool >= 0.2.1.0,+ vector >= 0.7,+ time >= 1.4 default-language: Haskell2010 other-extensions: BangPatterns, CPP, DeriveDataTypeable, FlexibleInstances,- OverloadedStrings, RecordWildCards, ScopedTypeVariables ghc-options: -Wall -fwarn-tabs@@ -103,11 +105,16 @@ hs-source-dirs: test main-is: Full.hs build-depends:- base < 5,- bytestring >= 0.9.2.1,- memcache+ base < 5,+ binary >= 0.6.2.0,+ blaze-builder >= 0.3.1.0,+ bytestring >= 0.9.2.1,+ memcache,+ network >= 2.4 default-language: Haskell2010 other-extensions:+ BangPatterns,+ CPP, OverloadedStrings ghc-options: -fwarn-tabs
test/Full.hs view
@@ -1,33 +1,64 @@ {-# LANGUAGE OverloadedStrings #-} --- | Our testsuite, which we run expecting there to be a local memcache server+-- | Our testsuite, which we run expecting there to be a local Memcached server -- running on `localhost:11211`. module Main where +import MockServer+ import qualified Database.Memcache.Client as M+import Database.Memcache.Errors+import Database.Memcache.Socket+import Database.Memcache.Types -import Control.Monad+import Blaze.ByteString.Builder+import Control.Concurrent+import Control.Exception+import Control.Monad+import Data.Binary.Get import qualified Data.ByteString.Char8 as BC-import System.Exit+import qualified Network.Socket as N+import qualified Network.Socket.ByteString as N+import System.Exit+import System.IO main :: IO () main = do- c <- M.newClient [M.ServerSpec "localhost" 11211 M.NoAuth] M.defaultOptions- getTest c+ putStr "GET: "+ getTest+ putStrLn "PASSED"+ putStr "DELETE: "+ deleteTest+ putStrLn "PASSED"+ putStr "RETRY: "+ retryTest+ putStrLn "PASSED"+ putStr "TIMEOUT1: "+ timeoutTest+ putStrLn "PASSED"+ putStr "TIMEOUT2: "+ timeoutRetryTest+ putStrLn "PASSED" exitSuccess -getTest :: M.Client -> IO ()-getTest c = do+getTest :: IO ()+getTest = withMCServer False res $ do+ c <- M.newClient M.def M.def void $ M.set c (BC.pack "key") (BC.pack "world") 0 0 Just (v', _, _) <- M.get c "key" when (v' /= "world") $ do putStrLn $ "bad value returned! " ++ show v' exitFailure + where+ res = [ MR $ emptyRes { resOp = ResSet Loud }+ , MR $ emptyRes { resOp = ResGet Loud "world" 0 }+ ] -deleteTest :: M.Client -> IO ()-deleteTest c = do- v1 <- M.set c "key" "world" 0 0- v2 <- M.set c "key" "world22" 0 0+deleteTest :: IO ()+deleteTest = withMCServer False res $ do+ c <- M.newClient M.def M.def+ v1 <- M.set c "key" "world" 0 0+ v2 <- M.set c "key" "world2" 0 0 when (v1 == v2) $ do putStrLn $ "bad versions! " ++ show v1 ++ ", " ++ show v2 exitFailure@@ -35,4 +66,39 @@ unless r $ do putStrLn "delete failed!" exitFailure+ where+ res = [ MR $ emptyRes { resOp = ResSet Loud, resCas = 1 }+ , MR $ emptyRes { resOp = ResSet Loud, resCas = 2 }+ , MR $ emptyRes { resOp = ResDelete Loud }+ ]++retryTest :: IO ()+retryTest = withMCServer False res $ do+ c <- M.newClient M.def M.def+ void $ M.set c (BC.pack "key") (BC.pack "world") 0 0+ where+ res = [ CloseConnection+ , MR $ emptyRes { resOp = ResSet Loud }+ ]++timeoutTest :: IO ()+timeoutTest = withMCServer True res $ do+ c <- M.newClient M.def M.def+ void $ M.set c (BC.pack "key") (BC.pack "world") 0 0+ r <- try $ M.get c "key" + case r of+ Left (ClientError Timeout) -> return ()+ Left _ -> putStrLn "unexpected exception!" >> exitFailure+ Right _ -> putStrLn "no timeout occured!" >> exitFailure+ where+ res = [ MR $ emptyRes { resOp = ResSet Loud } ]++timeoutRetryTest :: IO ()+timeoutRetryTest = withMCServer False res $ do+ c <- M.newClient M.def M.def+ void $ M.set c (BC.pack "key") (BC.pack "world") 0 0+ where+ res = [ DelayMS 800 Noop+ , MR $ emptyRes { resOp = ResSet Loud }+ ]