memcache 0.0.0 → 0.0.1
raw patch · 12 files changed
+484/−134 lines, 12 filesdep +hashabledep +resource-pooldep +timesetup-changed
Dependencies added: hashable, resource-pool, time, vector, vector-algorithms
Files
- Database/Memcache/Client.hs +71/−1
- Database/Memcache/Cluster.hs +135/−0
- Database/Memcache/Errors.hs +15/−2
- Database/Memcache/Protocol.hs +63/−51
- Database/Memcache/SASL.hs +8/−10
- Database/Memcache/Server.hs +97/−46
- Database/Memcache/Wire.hs +3/−0
- README.md +46/−14
- Setup.hs +7/−0
- Setup.lhs +0/−3
- memcache.cabal +31/−3
- test/Full.hs +8/−4
Database/Memcache/Client.hs view
@@ -1,3 +1,73 @@ -- | A memcache client.-module Database.Memcache.Client where+module Database.Memcache.Client (+ get, gat, touch,+ set, set', add, replace,+ delete,+ increment, decrement,+ append, prepend,+ flush, version, stats, quit+ ) where++import Database.Memcache.Cluster+import qualified Database.Memcache.Protocol as P+import Database.Memcache.Server+import Database.Memcache.Types++import Control.Monad+import Data.Word+import Data.ByteString (ByteString)++keyedOp' :: Cluster -> Key -> (Server -> IO (Maybe a)) -> IO (Maybe a)+keyedOp' = keyedOp (Just Nothing)++get :: Cluster -> Key -> IO (Maybe (Value, Flags, Version))+get c k = keyedOp' c k $ \s -> P.get s k++gat :: Cluster -> Key -> Expiration -> IO (Maybe (Value, Flags, Version))+gat c k e = keyedOp' c k $ \s -> P.gat s k e++touch :: Cluster -> Key -> Expiration -> IO (Maybe Version)+touch c k e = keyedOp' c k $ \s -> P.touch s k e++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++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++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++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++delete :: Cluster -> Key -> Version -> IO Bool+delete c k ver = keyedOp (Just False) c k $ \s -> P.delete s k ver++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++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++append :: Cluster -> Key -> Value -> Version -> IO (Maybe Version)+append c k v ver = keyedOp' c k $ \s -> P.append s k v ver++prepend :: Cluster -> Key -> Value -> Version -> IO (Maybe Version)+prepend c k v ver = keyedOp' c k $ \s -> P.prepend s k v ver++flush :: Cluster -> Maybe Expiration -> IO ()+flush c e = void $ allOp (Just ()) c $ \s -> P.flush s e++stats :: Cluster -> Maybe Key -> IO ([(Server, Maybe P.StatResults)])+stats c key = do+ allOp Nothing c $ \s -> P.stats s key++quit :: Cluster -> IO ()+quit c = void $ allOp (Just ()) c $ \s -> P.quit s++-- | 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
+ Database/Memcache/Cluster.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE RecordWildCards #-}++-- | Handles a group of connections to different memcache servers.+module Database.Memcache.Cluster (+ Cluster, newMemcacheCluster, Options(..), defaultOptions, keyedOp,+ anyOp, allOp+ ) where++import Database.Memcache.Errors+import Database.Memcache.Server (Server(..), newServer)+import Database.Memcache.Types (Key)++import qualified Control.Exception as E++import Data.Hashable (hash)+import Data.Maybe (fromMaybe)+import Data.List (sort)+import qualified Data.Vector as V+import qualified Data.Vector.Mutable as MV++import Network.Socket (HostName, PortNumber)++data Options = Options {+ optsCmdFailure :: !FailureMode,+ optsServerFailure :: !FailureMode,+ optsServerRetries :: !Int+ }++defaultOptions :: Options+defaultOptions = Options {+ optsCmdFailure = FailToError,+ optsServerFailure = FailToError,+ optsServerRetries = 2+ }++-- | A memcached cluster.+data Cluster = Cluster {+ servers :: V.Vector Server,+ cmdFailureMode :: !FailureMode,+ _serverFailureMode :: !FailureMode,+ serverRetries :: {-# UNPACK #-} !Int+ } deriving Show++-- | Establish a new connection to a group of memcached servers.+newMemcacheCluster :: [(HostName, PortNumber)] -> Options -> IO Cluster+newMemcacheCluster hosts Options{..} = do+ s <- mapM (uncurry newServer) hosts+ return $ Cluster (V.fromList $ sort s) optsCmdFailure optsServerFailure+ optsServerRetries++-- | 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 =+ let hashedKey = hash k+ searchFun svr = sid svr < hashedKey+ in fromMaybe (V.last $ servers c) $ V.find searchFun (servers c)++-- | Run a memcache 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)++-- | Run a memcache 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 memcache 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++-- 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 memcache 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++ go attempt =+ m s `E.catches`+ [ E.Handler $ handleMemErrors (attempt - 1)+ , E.Handler $ handleAllErrors (attempt - 1)+ ]++ 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++ -- 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+
Database/Memcache/Errors.hs view
@@ -5,7 +5,8 @@ MemcacheError(..), statusToError, throwStatus,- throwIncorrectRes+ throwIncorrectRes,+ ClientError(..) ) where import Database.Memcache.Types@@ -13,7 +14,11 @@ import Control.Exception import Data.Typeable --- | Exceptions that may be thrown by Memcache.+-- 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. data MemcacheError = MemErrNoKey | MemErrKeyExists@@ -54,4 +59,12 @@ increspMessage = "Expected " ++ msg ++ " response! Got: " ++ show (resOp r), increspActual = 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 view
@@ -1,7 +1,9 @@+{-# LANGUAGE ScopedTypeVariables #-}+ -- | A raw, low level interface to the memcache protocol. -- -- The various operations are represented in full as they appear at the--- protocol level and so aren't generaly well suited for application use.+-- 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 (@@ -10,22 +12,40 @@ delete, increment, decrement, append, prepend,- flush, noop, version, stats, quit+ StatResults, stats,+ flush,+ noop, version, quit ) where -import Control.Concurrent.MVar import Database.Memcache.Errors import Database.Memcache.Server import Database.Memcache.Types -import Control.Exception+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 -get :: Connection -> Key -> IO (Maybe (Value, Flags, Version))+-- 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@@ -35,11 +55,9 @@ case resStatus r of NoError -> return $ Just (v, f, resCas r) ErrKeyNotFound -> return Nothing- -- XXX: Exception Vs. Either? _ -> throwStatus r --- XXX: Maybe collapse data structures into single...-gat :: Connection -> Key -> Expiration -> IO (Maybe (Value, Flags, Version))+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@@ -51,7 +69,7 @@ ErrKeyNotFound -> return Nothing _ -> throwStatus r -touch :: Connection -> Key -> Expiration -> IO (Maybe Version)+touch :: Server -> Key -> Expiration -> IO (Maybe Version) touch c k e = do let msg = emptyReq { reqOp = ReqTouch k (SETouch e) } r <- sendRecv c msg@@ -61,9 +79,7 @@ ErrKeyNotFound -> return Nothing _ -> throwStatus r -----set :: Connection -> Key -> Value -> Flags -> Expiration -> IO Version+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@@ -72,7 +88,8 @@ NoError -> return $ resCas r _ -> throwStatus r -set' :: Connection -> Key -> Value -> Flags -> Expiration -> Version -> IO (Maybe Version)+-- 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@@ -85,7 +102,7 @@ ErrKeyExists -> return Nothing _ -> throwStatus r -add :: Connection -> Key -> Value -> Flags -> Expiration -> IO (Maybe Version)+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@@ -95,8 +112,7 @@ ErrKeyExists -> return Nothing _ -> throwStatus r --- XXX: Structure Vs. Args?-replace :: Connection -> Key -> Value -> Flags -> Expiration -> Version -> IO (Maybe Version)+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@@ -109,9 +125,7 @@ ErrKeyExists -> return Nothing _ -> throwStatus r -----delete :: Connection -> Key -> Version -> IO Bool+delete :: Server -> Key -> Version -> IO Bool delete c k ver = do let msg = emptyReq { reqOp = ReqDelete Loud k, reqCas = ver } r <- sendRecv c msg@@ -124,9 +138,7 @@ ErrKeyExists -> return False _ -> throwStatus r -----increment :: Connection -> Key -> Initial -> Delta -> Expiration -> Version -> IO (Maybe (Word64, Version))+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@@ -137,10 +149,9 @@ NoError -> return $ Just (n, resCas r) ErrKeyNotFound -> return Nothing ErrKeyExists -> return Nothing- -- XXX: Exception or Nothing for nonnumeric status? _ -> throwStatus r -decrement :: Connection -> Key -> Initial -> Delta -> Expiration -> Version -> IO (Maybe (Word64, Version))+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@@ -149,16 +160,11 @@ _ -> throwIncorrectRes r "DECREMENT" case resStatus r of NoError -> return $ Just (n, resCas r)- -- XXX: Should differentiate, use custom sum, NOT either. ErrKeyNotFound -> return Nothing ErrKeyExists -> return Nothing- -- XXX: Exception or Nothing for nonnumeric status? _ -> throwStatus r ------- XXX: Maybe? perhaps should be either so I can indicate why...-append :: Connection -> Key -> Value -> Version -> IO (Maybe Version)+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@@ -166,10 +172,9 @@ case resStatus r of NoError -> return $ Just (resCas r) ErrKeyNotFound -> return Nothing- ErrKeyExists -> return Nothing _ -> throwStatus r -prepend :: Connection -> Key -> Value -> Version -> IO (Maybe Version)+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@@ -177,12 +182,9 @@ case resStatus r of NoError -> return $ Just (resCas r) ErrKeyNotFound -> return Nothing- ErrKeyExists -> return Nothing _ -> throwStatus r -----flush :: Connection -> Maybe Expiration -> IO ()+flush :: Server -> Maybe Expiration -> IO () flush c e = do let e' = maybe Nothing (\xp -> Just (SETouch xp)) e msg = emptyReq { reqOp = ReqFlush Loud e' }@@ -192,7 +194,7 @@ NoError -> return () _ -> throwStatus r -noop :: Connection -> IO ()+noop :: Server -> IO () noop c = do let msg = emptyReq { reqOp = ReqNoop } r <- sendRecv c msg@@ -201,7 +203,7 @@ NoError -> return () _ -> throwStatus r -version :: Connection -> IO ByteString+version :: Server -> IO ByteString version c = do let msg = emptyReq { reqOp = ReqVersion } r <- sendRecv c msg@@ -212,8 +214,13 @@ NoError -> return v _ -> throwStatus r -stats :: Connection -> Maybe Key -> IO (Maybe [(ByteString, ByteString)])-stats c key = withMVar (conn c) $ \s -> do+-- | 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 []@@ -229,15 +236,20 @@ ErrKeyNotFound -> return Nothing _ -> throwStatus r -quit :: Connection -> IO ()--- XXX: close can throw, need to handle...-quit c = withMVar (conn c) $ \s -> finally (N.close 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 ()- _ -> throwStatus r+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 ()+ _ -> throwStatus r
Database/Memcache/SASL.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-unused-binds #-} -- | SASL authentication support for memcached.@@ -11,28 +10,27 @@ import Database.Memcache.Types import Control.Monad-import Data.ByteString-import qualified Data.ByteString.Char8 as BC+import Data.ByteString.Char8 as B8 (ByteString, pack, singleton) import Data.Monoid -- | Username for authentication.-type Username = ByteString+type Username = B8.ByteString -- | Password for authentication.-type Password = ByteString+type Password = B8.ByteString -- | Perform SASL authentication with the server.-authenticate :: Connection -> Username -> Password -> IO Bool+authenticate :: Server -> Username -> Password -> IO Bool -- 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 = saslAuthPlain -- | Perform SASL PLAIN authentication.-saslAuthPlain :: Connection -> Username -> Password -> IO Bool+saslAuthPlain :: Server -> Username -> Password -> IO Bool saslAuthPlain c u p = do- let credentials = singleton 0 <> u <> singleton 0 <> p- msg = emptyReq { reqOp = ReqSASLStart (BC.pack "PLAIN") credentials }+ let credentials = singleton '\0' <> u <> singleton '\0' <> p+ msg = emptyReq { reqOp = ReqSASLStart (B8.pack "PLAIN") credentials } r <- sendRecv c msg when (resOp r /= ResSASLStart) $ throwIncorrectRes r "SASL_START" case resStatus r of@@ -43,7 +41,7 @@ -- | List available SASL authentication methods. We could call this but as we -- only support PLAIN as does the memcached server, we simply assume PLAIN -- authentication is supprted and try that.-saslListMechs :: Connection -> IO ByteString+saslListMechs :: Server -> IO B8.ByteString saslListMechs c = do let msg = emptyReq { reqOp = ReqSASLList } r <- sendRecv c msg
Database/Memcache/Server.hs view
@@ -1,69 +1,120 @@--- | Handles the connections between a memcache client and the various servers--- that make up the cluster.+-- | Handles the connections between a memcache client and a single server.+ module Database.Memcache.Server (- Connection(..), newMemcacheClient, send, sendRecv, recv+ Server(sid, failed), newServer, sendRecv,+ withSocket, send, recv, close ) where -import Control.Concurrent.MVar+import Database.Memcache.Errors import Database.Memcache.Types import Database.Memcache.Wire import Blaze.ByteString.Builder import Control.Exception+import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as L+import Data.Hashable+import Data.Pool+import Data.Time.Clock (NominalDiffTime) import Network.BSD (getProtocolNumber, getHostByName, hostAddress)-import Network.Socket hiding (send, recv)+import Network.Socket (HostName, PortNumber(..), Socket)+import qualified Network.Socket as S import qualified Network.Socket.ByteString as N --- | A Memcache connection handle.--- XXX: Should make abstract-data Connection = Conn {- conn :: MVar Socket- }+-- Connection pool constants.+-- TODO: make configurable+sSTRIPES, sCONNECTIONS :: Int+sKEEPALIVE :: NominalDiffTime+sSTRIPES = 1+sCONNECTIONS = 1+sKEEPALIVE = 300 --- | Establish a new connection to a memcache backend.-newMemcacheClient :: HostName -> PortNumber -> IO Connection-newMemcacheClient h p = do- s <- connectTo h p- m <- newMVar s- setSocketOption s KeepAlive 1- setSocketOption s NoDelay 1- return (Conn m)+-- | A memcached server connection.+data Server = Server {+ sid :: {-# UNPACK #-} !Int,+ pool :: Pool Socket,+ _addr :: HostName,+ _port :: PortNumber,+ failed :: Bool+ -- TODO: + -- weight :: Double+ -- auth :: Authentication+ -- tansport :: Transport (UDP vs. TCP)+ -- poolLim :: Int (pooled connection limit)+ -- cnxnBuf :: IORef ByteString+ } deriving Show --- | Connect to a host. (Internal, socket version of connectTo).-connectTo :: HostName -> PortNumber -> IO Socket-connectTo host port = do- proto <- getProtocolNumber "tcp"- bracketOnError- (socket AF_INET Stream proto)- (close)- (\sock -> do- h <- getHostByName host- connect sock (SockAddrInet port (hostAddress h))- return sock- ) --- | Send a request to the memcache cluster.-send :: Socket -> Request -> IO ()--- XXX: catch errors and rethrow as MemcacheErrors?-send s m = N.sendAll s (toByteString $ szRequest m)+instance Eq Server where+ (==) x y = (sid x) == (sid y) --- | Send a receieve a single request/response pair to the memcache cluster.-sendRecv :: Connection -> Request -> IO Response-sendRecv c m = withMVar (conn c) $ \s -> do- send s m+instance Ord Server where+ compare x y = compare (sid x) (sid y)++-- | Create a new memcached connection.+newServer :: HostName -> PortNumber -> IO Server+newServer host port = do+ pSock <- createPool connectSocket releaseSocket+ sSTRIPES sKEEPALIVE sCONNECTIONS+ return $ Server+ { sid = serverHash+ , pool = pSock+ , _addr = host+ , _port = port+ , failed = False+ }+ where+ serverHash = hash (host, let PortNum p = port in p)+ connectSocket = do+ proto <- getProtocolNumber "tcp"+ bracketOnError+ (S.socket S.AF_INET S.Stream proto)+ (releaseSocket)+ (\s -> do+ h <- getHostByName host+ S.connect s (S.SockAddrInet port $ hostAddress h)+ S.setSocketOption s S.KeepAlive 1+ S.setSocketOption s S.NoDelay 1+ return s+ )++ releaseSocket s = S.close s++-- | 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+ send s msg recv s --- | Retrieve a single response from the memcache cluster.+-- | Run a function with access to an server socket for using 'send' and+-- 'recv'.+withSocket :: Server -> (Socket -> IO a) -> IO a+withSocket svr = withResource (pool svr)++-- | 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- -- XXX: recv may return less.- header <- N.recv s mEMCACHE_HEADER_SIZE+ header <- recvAll mEMCACHE_HEADER_SIZE let h = dzHeader' (L.fromChunks [header])- if (bodyLen h > 0) then do- body <- N.recv s (fromIntegral $ bodyLen h)- return $ dzBody' h (L.fromChunks [body])- else- return $ dzBody' h L.empty+ if (bodyLen h > 0)+ then do body <- recvAll (fromIntegral $ bodyLen h)+ return $ dzBody' h (L.fromChunks [body])+ else return $ dzBody' h L.empty+ where+ recvAll n = do+ buf <- N.recv s n+ if B.length buf == n+ then return buf+ else throwIO NotEnoughBytes++-- | Close the server connection. If you perform another operation after this,+-- the connection will be re-established.+close :: Server -> IO ()+close srv = destroyAllResources (pool srv)
Database/Memcache/Wire.hs view
@@ -4,6 +4,9 @@ dzResponse, dzResponse', dzHeader, dzHeader', dzBody, dzBody' ) where +-- XXX: Wire works with lazy bytestrings but we receive strict bytestrings from+-- the network...+ import Database.Memcache.Types import Control.Exception
README.md view
@@ -1,5 +1,7 @@ # memcache: Haskell Memcache Client +[](https://hackage.haskell.org/package/memcache) [](https://travis-ci.org/dterei/memcache-hs)+ This library provides a client interface to a Memcache cluster. It is aimed at full binary protocol support, ease of use and speed. @@ -7,28 +9,58 @@ This library is BSD-licensed. -## To do+## Tools -* Timeouts-* Ring / CHORD support-* Thread-safe?+This library also includes a few tools for manipulating and+experimenting with memcached servers. -* mutli(-get)+* `OpGen` -- A load generator for memcached. Doesn't collect timing+ statistics, other tools like+ [mutilate](https://github.com/leverich/mutilate) already do that+ very well. This tool is useful in conjunction with mutilate.+* `Loader` -- A tool to load random data of a certain size into a+ memcache server. Useful for priming a server for testing. -* Connection drop / reconnect resiliance?-* Asynchronous support?-* Connection pooling?-* Tweaking? (e.g., drop in hash algorithm?, timeout, max connection- retries...)-* Max value validation...?+## Architecture Notes -* Optimizations? http://code.google.com/p/spymemcached/wiki/Optimizations+We're relying on `Data.Pool` for thread safety right now, which is+fine but is a blocking API in that when we grab a socket+(`withResource`) we are blocking any other requests being sent over+that connection until we get a response. That is, we can't pipeline. -* Typeclass for serialization?-* Monad / Typeclass for memcache?+Now, use of multiple connections through the pool abstraction is an+easy way to solve this and perhaps the right approach. But, could also+implement own pool abstraction that allowed pipelining. This wouldn't+be a pool abstraction so much as just round-robbining over multiple+connections for performance. +Either way, a pool is fine for now.++## ToDo++Required:+* Connection error handling+* SASL -- tie in with cluster management+* Timeouts+* Pull cluster and server creation into client++Optional:+* Multi-get+* Generic multi operation support+* Customizable server sharding -- mod & virtual servers++Nice-to-have:+* Asynchronous support+* Customizable -- timeout, max connection retries, hash algorithm+* Max value validation+* Optimizations -- http://code.google.com/p/spymemcached/wiki/Optimizations * UDP * ASCII+* Server error handling mode where we return misses and ignore sets++Maybe:+* Typeclass for serialization+* Monad / Typeclass for memcache ## Other clients
+ Setup.hs view
@@ -0,0 +1,7 @@+module Main (main) where++import Distribution.Simple++main :: IO ()+main = defaultMain+
− Setup.lhs
@@ -1,3 +0,0 @@-#!/usr/bin/env runhaskell-> import Distribution.Simple-> main = defaultMain
memcache.cabal view
@@ -1,5 +1,5 @@ name: memcache-version: 0.0.0+version: 0.0.1 homepage: https://github.com/dterei/memcache-hs bug-reports: https://github.com/dterei/memcache-hs/issues synopsis: A memcached client library.@@ -10,7 +10,7 @@ license-file: LICENSE author: David Terei <code@davidterei.com> maintainer: David Terei <code@davidterei.com>-copyright: 2013 David Terei.+copyright: 2015 David Terei. category: Database build-type: Simple cabal-version: >= 1.8@@ -20,6 +20,7 @@ library exposed-modules: Database.Memcache.Client+ Database.Memcache.Cluster Database.Memcache.Errors Database.Memcache.Protocol Database.Memcache.SASL@@ -32,11 +33,38 @@ binary >= 0.6.2.0, blaze-builder >= 0.3.1.0, bytestring >= 0.9.2.1,- network >= 2.4+ hashable >= 1.2.0.3,+ network >= 2.4,+ resource-pool >= 0.2.1.0,+ vector >= 0.7,+ vector-algorithms >= 0.5,+ time >= 1.4 ghc-options: -Wall if impl(ghc >= 6.8) ghc-options: -fwarn-tabs++-- executable opgen+-- hs-source-dirs: tools+-- main-is: OpGen.hs+-- ghc-options: -threaded+-- build-depends:+-- async,+-- base < 5,+-- bytestring,+-- memcache,+-- network+--+-- executable load+-- hs-source-dirs: tools+-- main-is: Loader.hs+-- ghc-options: -threaded+-- build-depends:+-- async,+-- base < 5,+-- bytestring,+-- memcache,+-- network test-suite full type: exitcode-stdio-1.0
test/Full.hs view
@@ -1,9 +1,12 @@ {-# LANGUAGE OverloadedStrings #-} +-- | Our testsuite, which we run expecting there to be a local memcache server+-- running on `localhost:11211`. module Main where import qualified Database.Memcache.Protocol as M-import Database.Memcache.Server+import qualified Database.Memcache.Server as M+import qualified Database.Memcache.SASL as M import Control.Monad import qualified Data.ByteString.Char8 as BC@@ -11,11 +14,12 @@ main :: IO () main = do- c <- newMemcacheClient "localhost" 11211+ c <- M.newServer "localhost" 11211+ -- M.authenticate c "user" "pass" getTest c exitSuccess -getTest :: Connection -> IO ()+getTest :: M.Server -> IO () getTest c = do v <- M.set c (BC.pack "key") (BC.pack "world") 0 0 Just (v', _, _) <- M.get c "key"@@ -23,7 +27,7 @@ putStrLn $ "bad value returned! " ++ show v' exitFailure -deleteTest :: Connection -> IO ()+deleteTest :: M.Server -> IO () deleteTest c = do v1 <- M.set c "key" "world" 0 0 v2 <- M.set c "key" "world22" 0 0