cassy (empty) → 0.2.0.1
raw patch · 7 files changed
+1062/−0 lines, 7 filesdep +Thriftdep +aesondep +attoparsecsetup-changed
Dependencies added: Thrift, aeson, attoparsec, base, bytestring, cassandra-thrift, containers, network, stm, syb, text, time, transformers
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- cassy.cabal +79/−0
- src/Database/Cassandra/Basic.hs +259/−0
- src/Database/Cassandra/JSON.hs +308/−0
- src/Database/Cassandra/Pool.hs +206/−0
- src/Database/Cassandra/Types.hs +178/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c)2011, Ozgun Ataman++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Ozgun Ataman nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ cassy.cabal view
@@ -0,0 +1,79 @@+Name: cassy+Version: 0.2.0.1+Synopsis: A high level driver for the Cassandra datastore+License: BSD3+License-file: LICENSE+Author: Ozgun Ataman+Maintainer: ozataman@gmail.com+Category: Data+Build-type: Simple+description:+ The objective is to completely isolate away the thrift layer, providing+ a more idiomatic Haskell experience working with Cassandra.+ .+ Certain parts of the API was inspired by pycassa (Python client) and+ hscassandra (on Hackage).+ .+ A brief explanation of modules:+ . + * /Database.Cassandra.Basic/: Contains a low level, simple+ implementation of Cassandra interaction using the thrift API+ underneath.+ .+ * /Database.Cassandra.JSON/: A higher level API that operates on+ values with ToJSON and FromJSON isntances from the /aeson/+ library. This module has in part been inspired by Bryan+ O\'Sullivan\'s /riak/ client for Haskell.+ .+ * /Database.Cassandra.Pool/: Handles a /pool/ of connections to+ multiple servers in a cluster, splitting the load among them.+ .+ * /Database.Cassandra.Types/: A common set of types used everywhere.+ .+ Potential TODOs include:+ .+ * Support for counters and batch mutators+ .+ * Support for database admin operations+ .+ * Support for composite column types ++-- Extra-source-files: ++Cabal-version: >=1.2++++Library+ hs-source-dirs: src+ Exposed-modules:+ Database.Cassandra.Basic+ Database.Cassandra.JSON+ Database.Cassandra.Pool+ Database.Cassandra.Types+ + -- Packages needed in order to build this package.+ Build-depends:+ base >= 4 && < 5+ , bytestring+ , containers+ , network+ , time+ , transformers+ , stm+ , syb+ , text+ , attoparsec >= 0.10 && < 0.11+ , aeson+ , Thrift >= 0.6+ , cassandra-thrift >= 0.8++ Extensions:+ TypeSynonymInstances+ + -- Modules not exported by this package.+ -- Other-modules: + + -- Extra tools (e.g. alex, hsc2hs, ...) needed to build the source.+ -- Build-tools: +
+ src/Database/Cassandra/Basic.hs view
@@ -0,0 +1,259 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternGuards, NamedFieldPuns, RecordWildCards #-}+++module Database.Cassandra.Basic ++(+ -- * Basic Types+ ColumnFamily(..)+ , Key(..)+ , ColumnName(..)+ , Value(..)+ , Column(..)+ , col+ , Row(..)+ , ConsistencyLevel(..)++ -- * Filtering+ , Selector(..)+ , Order(..)+ , KeySelector(..)+ , KeyRangeType(..)++ -- * Exceptions+ , CassandraException(..)++ -- * Connection+ , CPool+ , Server(..)+ , defServer+ , defServers+ , KeySpace(..)+ , createCassandraPool++ -- * Cassandra Operations+ , getCol+ , get+ , getMulti+ , insert+ , delete++ -- * Utility+ , getTime+ , throwing+) where+++import Control.Exception+import Control.Monad+import Data.ByteString.Lazy (ByteString)+import qualified Database.Cassandra.Thrift.Cassandra_Client as C+import qualified Database.Cassandra.Thrift.Cassandra_Types as T+import Database.Cassandra.Thrift.Cassandra_Types + (ConsistencyLevel(..))+import Data.Map (Map)+import qualified Data.Map as M+import Data.Maybe (mapMaybe)+import Network+import Prelude hiding (catch)++import Database.Cassandra.Pool+import Database.Cassandra.Types+++test = do+ pool <- createCassandraPool [("127.0.0.1", PortNumber 9160)] 3 300 "Keyspace1"+ withPool pool $ \ Cassandra{..} -> do+ let cp = T.ColumnParent (Just "CF1") Nothing+ let sr = Just $ T.SliceRange (Just "") (Just "") (Just False) (Just 100)+ let ks = Just ["eben"]+ let sp = T.SlicePredicate Nothing sr+ C.get_slice (cProto, cProto) "darak" cp sp ONE+ get pool "darak" "CF1" All ONE+ getCol pool "CF1" "darak" "eben" ONE+ insert pool "CF1" "test1" ONE [col "col1" "val1", col "col2" "val2"] + get pool "test1" "CF1" All ONE >>= putStrLn . show+ delete pool "test1" "CF1" (ColNames ["col2"]) ONE+ get pool "test1" "CF1" (Range Nothing Nothing Reversed 1) ONE >>= putStrLn . show+++------------------------------------------------------------------------------+-- | Get a single key-column value+getCol + :: CPool+ -> ColumnFamily + -> Key + -- ^ Row key+ -> ColumnName+ -- ^ Column/SuperColumn name+ -> ConsistencyLevel + -- ^ Read quorum+ -> IO (Either CassandraException Column)+getCol p cf k cn cl = do+ c <- get p cf k (ColNames [cn]) cl+ case c of+ Left e -> return $ Left e+ Right [] -> return $ Left NotFoundException+ Right (x:_) -> return $ Right x+++------------------------------------------------------------------------------+-- | An arbitrary get operation - slice with 'Selector'+get + :: CPool+ -> ColumnFamily + -- ^ in ColumnFamily+ -> Key + -- ^ Row key to get+ -> Selector + -- ^ Slice columns with selector+ -> ConsistencyLevel + -> IO (Either CassandraException Row)+get p cf k s cl = withPool p $ \ Cassandra{..} -> do+ res <- wrapException $ C.get_slice (cProto, cProto) k cp (mkPredicate s) cl+ case res of+ Left e -> return $ Left e+ Right xs -> return $ do+ cs <- mapM castColumn xs+ case cs of+ [] -> Left NotFoundException+ _ -> Right $ cs+ where+ cp = T.ColumnParent (Just cf) Nothing+++------------------------------------------------------------------------------+-- | Do multiple 'get's in one DB hit+getMulti + :: CPool+ -> ColumnFamily + -> KeySelector+ -- ^ A selection of rows to fetch in one hit+ -> Selector + -- ^ Subject to column selector conditions+ -> ConsistencyLevel + -> IO (Either CassandraException (Map ByteString Row))+ -- ^ A Map from Row keys to 'Row's is returned+getMulti p cf ks s cl = withPool p $ \ Cassandra{..} -> do+ case ks of+ Keys xs -> do+ res <- wrapException $ C.multiget_slice (cProto, cProto) xs cp (mkPredicate s) cl+ case res of+ Left e -> return $ Left e+ Right m -> return . Right $ M.mapMaybe f m+ KeyRange {} -> do+ res <- wrapException $ + C.get_range_slices (cProto, cProto) cp (mkPredicate s) (mkKeyRange ks) cl+ case res of+ Left e -> return $ Left e+ Right res' -> return . Right $ collectKeySlices res'+ where+ collectKeySlices :: [T.KeySlice] -> Map ByteString Row+ collectKeySlices ks = M.fromList $ mapMaybe collectKeySlice ks+ where + f (k, Just x) = True+ f _ = False+ g (k, Just x) = (k,x)+++ collectKeySlice (T.KeySlice (Just k) (Just xs)) = + case mapM castColumn xs of+ Left _ -> Nothing+ Right xs' -> Just (k, xs')+ collectKeySlice _ = Nothing++ cp = T.ColumnParent (Just cf) Nothing+ f xs = + case mapM castColumn xs of+ Left _ -> Nothing+ Right xs' -> Just xs'+++------------------------------------------------------------------------------+-- | Insert an entire row into the db.+--+-- This will do as many round-trips as necessary to insert the full row.+insert+ :: CPool+ -> ColumnFamily+ -> Key+ -> ConsistencyLevel+ -> Row+ -> IO (Either CassandraException ())+insert p cf k cl row = withPool p $ \ Cassandra{..} -> do+ let iOne c = do+ c' <- mkThriftCol c+ wrapException $ C.insert (cProto, cProto) k cp c' cl + res <- sequenceE $ map iOne row+ return $ res >> return ()+ where+ cp = T.ColumnParent (Just cf) Nothing+ ++sequenceE :: [IO (Either a b)] -> IO (Either a [b])+sequenceE [] = return (return [])+sequenceE (a:as) = do+ r1 <- a+ rr <- sequenceE as+ return $ liftM2 (:) r1 rr+++------------------------------------------------------------------------------+-- | Delete an entire row, specific columns or a specific sub-set of columns+-- within a SuperColumn.+delete + :: CPool+ -> ColumnFamily+ -- ^ In 'ColumnFamily'+ -> Key+ -- ^ Key to be deleted+ -> Selector+ -- ^ Columns to be deleted+ -> ConsistencyLevel+ -> IO (Either CassandraException ())+delete p cf k s cl = withPool p $ \ Cassandra {..} -> do+ now <- getTime+ wrapException $ case s of+ All -> C.remove (cProto, cProto) k cpAll now cl+ ColNames cs -> forM_ cs $ \c -> do+ C.remove (cProto, cProto) k (cpCol c) now cl+ SupNames sn cs -> forM_ cs $ \c -> do+ C.remove (cProto, cProto) k (cpSCol sn c) now cl+ where+ -- wipe out the entire row+ cpAll = T.ColumnPath (Just cf) Nothing Nothing++ -- just a single column+ cpCol name = T.ColumnPath (Just cf) Nothing (Just name)++ -- scope column by supercol+ cpSCol sc name = T.ColumnPath (Just cf) (Just sc) (Just name)+ +++------------------------------------------------------------------------------+-- | Wrap exceptions into an explicit type+wrapException :: IO a -> IO (Either CassandraException a)+wrapException a = + (a >>= return . Right)+ `catch` (\(T.NotFoundException) -> return $ Left NotFoundException)+ `catch` (\(T.InvalidRequestException e) -> + return . Left . InvalidRequestException $ maybe "" id e)+ `catch` (\T.UnavailableException -> return $ Left UnavailableException)+ `catch` (\T.TimedOutException -> return $ Left TimedOutException)+ `catch` (\(T.AuthenticationException e) -> + return . Left . AuthenticationException $ maybe "" id e)+ `catch` (\(T.AuthorizationException e) -> + return . Left . AuthorizationException $ maybe "" id e)+ `catch` (\T.SchemaDisagreementException -> return $ Left SchemaDisagreementException)+++-------------------------------------------------------------------------------+-- | Make exceptions implicit+throwing :: IO (Either CassandraException a) -> IO a+throwing f = do+ res <- f+ case res of+ Left e -> throw e+ Right a -> return a
+ src/Database/Cassandra/JSON.hs view
@@ -0,0 +1,308 @@+{-# LANGUAGE PatternGuards, + NamedFieldPuns, + OverloadedStrings,+ TypeSynonymInstances, + ScopedTypeVariables,+ RecordWildCards #-}++{-|+ A higher level module for working with Cassandra.+ + Row and Column keys can be any string-like type implementing the+ CKey typeclass. You can add your own types by defining new instances++ Serialization and de-serialization of Column values are taken care of+ automatically using the ToJSON and FromJSON typeclasses.+ + Also, this module currently attempts to reduce verbosity by+ throwing errors instead of returning Either types as in the+ 'Database.Cassandra.Basic' module.++-}++module Database.Cassandra.JSON +( + + -- * Connection+ CPool+ , Server(..)+ , defServer+ , defServers+ , KeySpace(..)+ , createCassandraPool+++ -- * Necessary Types+ , CKey(..)+ , ModifyOperation(..)+ , ColumnFamily(..)+ , ConsistencyLevel(..)+ , CassandraException(..)++ -- * Cassandra Operations+ , get+ , getCol + , insertCol+ , modify+ , modify_+ , delete+++) where++import Control.Exception+import Control.Monad+import Data.Aeson as A+import Data.Aeson.Parser (value)+import qualified Data.Attoparsec as Atto (IResult(..), parse)+import qualified Data.ByteString.Char8 as B+import Data.ByteString.Lazy.Char8 (ByteString)+import qualified Data.ByteString.Lazy.Char8 as LB+import Data.Map (Map)+import qualified Data.Map as M+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Data.Text.Lazy as LT+import qualified Data.Text.Lazy.Encoding as LT+import Network+import Prelude hiding (catch)++import Database.Cassandra.Basic hiding (get, getCol, delete)+import qualified Database.Cassandra.Basic as CB+import Database.Cassandra.Pool+import Database.Cassandra.Types+++-------------------------------------------------------------------------------+---- CKey Typeclass+-------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | A typeclass to enable using any string-like type for row and column keys+class CKey a where+ toBS :: a -> ByteString+ fromBS :: ByteString -> a++instance CKey String where+ toBS = LB.pack+ fromBS = LB.unpack++instance CKey LT.Text where+ toBS = LT.encodeUtf8+ fromBS = LT.decodeUtf8++instance CKey T.Text where+ toBS = toBS . LT.fromChunks . return+ fromBS = T.concat . LT.toChunks . fromBS++instance CKey B.ByteString where+ toBS = LB.fromChunks . return+ fromBS = B.concat . LB.toChunks . fromBS++instance CKey ByteString where+ toBS = id+ fromBS = id++++-------------------------------------------------------------------------------+-- | Convert regular column to a key-value pair+col2val :: (CKey colKey, FromJSON a) => Column -> (colKey, a)+col2val (Column nm val _ _) = (fromBS nm, maybe err id $ unMarshallJSON' val)+ where err = error "Value can't be parsed from JSON."+col2val _ = error "col2val is not implemented for SuperColumns"++++------------------------------------------------------------------------------+-- | Possible outcomes of a modify operation +data ModifyOperation a = + Update a+ | Delete+ | DoNothing+ deriving (Eq,Show,Ord,Read)+++------------------------------------------------------------------------------+-- | A modify function that will fetch a specific column, apply modification+-- function on it and save results back to Cassandra. +--+-- A 'b' side value is returned for computational convenience.+--+-- This is intended to be a workhorse function, in that you should be+-- able to do all kinds of relatively straightforward operations just+-- using this function.+--+-- This method may throw a 'CassandraException' for all exceptions other than+-- 'NotFoundException'.+modify+ :: (CKey rowKey, CKey colKey, ToJSON a, FromJSON a)+ => CPool+ -> ColumnFamily+ -> rowKey+ -> colKey+ -> ConsistencyLevel+ -- ^ Read quorum+ -> ConsistencyLevel+ -- ^ Write quorum+ -> (Maybe a -> IO (ModifyOperation a, b))+ -- ^ Modification function. Called with 'Just' the value if present,+ -- 'Nothing' otherwise.+ -> IO b+ -- ^ Return the decided 'ModifyOperation' and its execution outcome+modify cp cf k cn rcl wcl f = + let+ k' = toBS k+ cn' = toBS cn+ execF prev = do+ (fres, b) <- f prev+ dbres <- case fres of+ (Update a) ->+ insert cp cf k' wcl [col cn' (marshallJSON' a)]+ (Delete) ->+ CB.delete cp cf k' (ColNames [cn']) wcl+ (DoNothing) -> return $ Right ()+ case dbres of+ Left e -> throw e -- Modify op returned error; throw it+ Right _ -> return b+ in do+ res <- CB.getCol cp cf k' cn' rcl+ case res of+ Left NotFoundException -> execF Nothing+ Left e -> throw e+ Right Column{..} -> execF (unMarshallJSON' colVal)+ Right SuperColumn{..} -> throw $ + OperationNotSupported "modify not implemented for SuperColumn"+++------------------------------------------------------------------------------+-- | Same as 'modify' but does not offer a side value.+--+-- This method may throw a 'CassandraException' for all exceptions other than+-- 'NotFoundException'.+modify_+ :: (CKey rowKey, CKey colKey, ToJSON a, FromJSON a)+ => CPool+ -> ColumnFamily+ -> rowKey+ -> colKey+ -> ConsistencyLevel+ -- ^ Read quorum+ -> ConsistencyLevel+ -- ^ Write quorum+ -> (Maybe a -> IO (ModifyOperation a))+ -- ^ Modification function. Called with 'Just' the value if present,+ -- 'Nothing' otherwise.+ -> IO ()+modify_ cp cf k cn rcl wcl f = + let+ f' prev = do+ op <- f prev+ return (op, ())+ in do+ modify cp cf k cn rcl wcl f'+ return ()+++-------------------------------------------------------------------------------+-- Simple insertion function making use of typeclasses+insertCol+ :: (CKey rowKey, CKey colKey, ToJSON a)+ => CPool -> ColumnFamily + -> rowKey+ -> colKey+ -> ConsistencyLevel+ -> a -- ^ Content+ -> IO ()+insertCol cp cf k cn cl a = + throwing $ insert cp cf (toBS k) cl [col (toBS cn) (marshallJSON' a)]+++------------------------------------------------------------------------------+-- | An arbitrary get operation - slice with 'Selector'.+--+-- Internally based on Basic.get. Table is assumed to be a regular+-- ColumnFamily and contents of returned columns are cast into the+-- target type.+get+ :: (CKey rowKey, CKey colKey, FromJSON a)+ => CPool -> ColumnFamily+ -> rowKey+ -> Selector+ -> ConsistencyLevel+ -> IO [(colKey, a)]+get cp cf k s cl = do+ res <- throwing $ CB.get cp cf (toBS k) s cl+ return $ map col2val res+++-------------------------------------------------------------------------------+-- | Get a single column from a single row+getCol+ :: (CKey rowKey, CKey colKey, FromJSON a)+ => CPool -> ColumnFamily+ -> rowKey+ -> colKey+ -> ConsistencyLevel+ -> IO (Maybe a)+getCol cp cf rk ck cl = do+ res <- CB.getCol cp cf (toBS rk) (toBS ck) cl+ case res of+ Left NotFoundException -> return Nothing+ Left e -> throw e+ Right a -> + let (_ :: ByteString, x) = col2val a+ in return $ Just x++++------------------------------------------------------------------------------+-- | Same as the 'delete' in the 'Cassandra.Basic' module, except that+-- it throws an exception rather than returning an explicit Either+-- value.+delete + :: (CKey rowKey)+ => CPool+ -- ^ Cassandra connection+ -> ColumnFamily+ -- ^ In 'ColumnFamily'+ -> rowKey+ -- ^ Key to be deleted+ -> Selector+ -- ^ Columns to be deleted+ -> ConsistencyLevel+ -> IO ()+delete p cf k s cl = throwing $ CB.delete p cf (toBS k) s cl+++------------------------------------------------------------------------------+-- | Lazy 'marshallJSON'+marshallJSON' :: ToJSON a => a -> ByteString+marshallJSON' = LB.fromChunks . return . marshallJSON+++------------------------------------------------------------------------------+-- | Encode JSON +marshallJSON :: ToJSON a => a -> B.ByteString+marshallJSON = B.concat . LB.toChunks . A.encode+++------------------------------------------------------------------------------+-- | Lazy 'unMarshallJSON'+unMarshallJSON' :: FromJSON a => ByteString -> Maybe a+unMarshallJSON' = unMarshallJSON . B.concat . LB.toChunks ++------------------------------------------------------------------------------+-- | Decode JSON +unMarshallJSON :: FromJSON a => B.ByteString -> Maybe a+unMarshallJSON = pJson + where + pJson bs = val+ where+ js = Atto.parse value bs+ val = case js of+ Atto.Done _ r -> case fromJSON r of+ Error e -> error $ "JSON err: " ++ show e+ Success a -> a+ _ -> Nothing
+ src/Database/Cassandra/Pool.hs view
@@ -0,0 +1,206 @@+{-# LANGUAGE PatternGuards, NamedFieldPuns, RecordWildCards #-}+++module Database.Cassandra.Pool where++++import Control.Applicative ((<$>))+import Control.Concurrent.STM+import Control.Exception (SomeException, catch, onException)+import Control.Monad (forM_, forever, join, liftM2, unless, when)+import Control.Monad.IO.Class (liftIO)+import Data.ByteString (ByteString)+import Data.List (partition)+import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime)+import Prelude hiding (catch)+import System.Mem.Weak (addFinalizer)+import System.IO (hClose, Handle(..))+++import qualified Database.Cassandra.Thrift.Cassandra_Client as C+import Thrift.Transport+import Thrift.Transport.Handle+import Thrift.Transport.Framed+import Thrift.Protocol.Binary+import Network++------------------------------------------------------------------------------+-- | A round-robin pool of cassandra connections+type CPool = Pool Cassandra Server+++type Server = (HostName, PortID)+++-- | A localhost server with default configuration+defServer :: Server+defServer = ("127.0.0.1", PortNumber 9160)+++-- | A single localhost server with default configuration+defServers :: [Server]+defServers = [defServer]+++type KeySpace = String+++data Cassandra = Cassandra {+ cHandle :: Handle+ , cFramed :: FramedTransport Handle+ , cProto :: BinaryProtocol (FramedTransport Handle)+}++++-- | Create a pool of connections to a cluster of Cassandra boxes+--+-- Each box in the cluster will get up to n connections. The pool will send+-- queries in round-robin fashion to balance load on each box in the cluster.+createCassandraPool + :: [Server]+ -- ^ List of servers to connect to+ -> Int+ -- ^ Max connections per server (n)+ -> NominalDiffTime+ -- ^ Kill each connection after this many seconds+ -> KeySpace+ -- ^ Each pool operates on a single KeySpace+ -> IO CPool+createCassandraPool servers n maxIdle ks = createPool cr dest n maxIdle servers+ where+ cr :: Server -> IO Cassandra+ cr (host, p) = do+ h <- hOpen (host, p)+ ft <- openFramedTransport h+ let p = BinaryProtocol ft+ C.set_keyspace (p,p) ks+ return $ Cassandra h ft p+ dest h = hClose $ cHandle h+++------------------------------------------------------------------------------+-- Generic pool functionality - might want to factor out one day+--+------------------------------------------------------------------------------++newtype Pool a s = Pool { stripes :: TVar (Ring (Stripe a s)) }+++createPool cr dest n maxIdle servers = do+ when (maxIdle < 0.5) $+ modError "pool " $ "invalid idle time " ++ show maxIdle+ when (n < 1) $+ modError "pool " $ "invalid maximum resource count " ++ show n+ stripes' <- mapM (createStripe cr dest n maxIdle) servers+ -- reaperId <- forkIO $ reaper destroy idleTime localPools+ -- addFinalizer p $ killThread reaperId+ tv <- atomically $ newTVar (mkRing stripes')+ return $ Pool tv++++withPool :: Pool a s -> (a -> IO b) -> IO b+withPool Pool{..} f = do+ Ring{..} <- atomically $ do+ r <- readTVar stripes+ writeTVar stripes $ next r+ return r+ withStripe current f+++data Ring a = Ring {+ current :: !a+ , used :: [a]+ , upcoming :: [a]+ }+++mkRing [] = error "Can't make a ring from empty list"+mkRing (a:as) = Ring a [] as+++next :: Ring a -> Ring a+next Ring{..} + | (n:rest) <- upcoming+ = Ring n (current : used) rest+next Ring{..} + | (n:rest) <- reverse (current : used)+ = Ring n [] rest+++data Stripe a s = Stripe {+ idle :: TVar [Connection a]+ -- ^ FIFO buffer of idle connections+ , inUse :: TVar Int+ -- ^ Set of in-use connections+ , server :: s+ -- ^ Server this strip is connected to+ , create :: s -> IO a+ -- ^ Create action+ , destroy :: (a -> IO ())+ -- ^ Destroy action+ , cxns :: Int+ -- ^ Max connections+ , ttl :: NominalDiffTime+ -- ^ TTL for each connection+ }+++createStripe + :: (s -> IO a)+ -> (a -> IO ())+ -> Int+ -> NominalDiffTime+ -> s+ -> IO (Stripe a s)+createStripe cr dest n maxIdle s = atomically $ do+ idles <- newTVar []+ used <- newTVar 0+ return $ Stripe {+ idle = idles+ , inUse = used+ , server = s+ , create = cr+ , destroy = dest+ , cxns = n+ , ttl = maxIdle+ }+++withStripe :: Stripe a s -> (a -> IO b) -> IO b+withStripe Stripe{..} f = do+ res <- join . atomically $ do+ cs <- readTVar idle+ case cs of+ (Connection{..}:rest) -> writeTVar idle rest >> return (return cxn)+ [] -> do+ used <- readTVar inUse+ when (used == cxns) retry+ writeTVar inUse $! used + 1+ return $ create server + `onException` atomically (modifyTVar_ inUse (subtract 1))+ ret <- f res `onException` (destroy res `onException` return ())+ now <- getCurrentTime+ atomically $ modifyTVar_ idle (Connection res now : ) + return ret++++data Connection a = Connection {+ cxn :: a+ , lastUse :: UTCTime+ }++++modifyTVar_ :: TVar a -> (a -> a) -> STM ()+modifyTVar_ v f = readTVar v >>= \a -> writeTVar v $! f a+++modError :: String -> String -> a+modError func msg =+ error $ "Data.Pool." ++ func ++ ": " ++ msg++
+ src/Database/Cassandra/Types.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE PatternGuards, NamedFieldPuns, RecordWildCards #-}++module Database.Cassandra.Types where++import Control.Monad+import Control.Exception+import Data.Generics+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy.Char8 as LB+import Data.Int (Int32, Int64)+import Data.Time+import Data.Time.Clock.POSIX++import qualified Database.Cassandra.Thrift.Cassandra_Types as C+++-- | A 'Key' range selector to use with 'getMulti'.+data KeySelector = + Keys [Key]+ -- ^ Just a list of keys to get+ | KeyRange KeyRangeType Key Key Int32+ -- ^ A range of keys to get. Remember that RandomPartitioner ranges may not+ -- mean much as keys are randomly assigned to nodes.+ deriving (Show)+++-- | Encodes the Key vs. Token options in the thrift API. +--+-- 'InclusiveRange' ranges are just plain intuitive range queries.+-- 'WrapAround' ranges are also inclusive, but they wrap around the ring.+data KeyRangeType = InclusiveRange | WrapAround+ deriving (Show)+++mkKeyRange (KeyRange ty st end cnt) = case ty of+ InclusiveRange -> C.KeyRange (Just st) (Just end) Nothing Nothing (Just cnt)+ WrapAround -> C.KeyRange Nothing Nothing (Just $ LB.unpack st) (Just $ LB.unpack end) (Just cnt)++-- | A column selector/filter statement for queries.+--+-- Remember that SuperColumns are always fully deserialized, so we don't offer+-- a way to filter columns within a 'SuperColumn'.+data Selector = + All+ -- ^ Return everything in 'Row'+ | ColNames [ColumnName]+ -- ^ Return specific columns or super-columns depending on the 'ColumnFamily'+ | SupNames ColumnName [ColumnName]+ -- ^ When deleting specific columns in a super column+ | Range (Maybe ColumnName) (Maybe ColumnName) Order Int32+ -- ^ Return a range of columns or super-columns+ deriving (Show)+++mkPredicate :: Selector -> C.SlicePredicate+mkPredicate s = + let+ allRange = C.SliceRange (Just "") (Just "") (Just False) (Just 100)+ in case s of+ All -> C.SlicePredicate Nothing (Just allRange)+ ColNames ks -> C.SlicePredicate (Just ks) Nothing+ Range st end ord cnt -> + let+ st' = st `mplus` Just ""+ end' = end `mplus` Just ""+ in C.SlicePredicate Nothing + (Just (C.SliceRange st' end' (Just $ renderOrd ord) (Just cnt)))++++------------------------------------------------------------------------------+-- | Order in a range query+data Order = Regular | Reversed+ deriving (Show)++renderOrd Regular = False+renderOrd Reversed = True+++type ColumnFamily = String+++type Key = ByteString+++type ColumnName = ByteString+++type Value = ByteString+++------------------------------------------------------------------------------+-- | A Column is either a single key-value pair or a SuperColumn with an+-- arbitrary number of key-value pairs+data Column = + SuperColumn ColumnName [Column]+ | Column {+ colKey :: ColumnName+ , colVal :: Value+ , colTS :: Maybe Int64+ -- ^ Last update timestamp; will be overridden during write/update ops+ , colTTL :: Maybe Int32+ -- ^ A TTL after which Cassandra will erase the column+ }+ deriving (Eq,Show,Read,Ord)+++------------------------------------------------------------------------------+-- | A full row is simply a sequence of columns+type Row = [Column]+++------------------------------------------------------------------------------+-- | A short-hand for creating key-value 'Column' values+col :: ByteString -> ByteString -> Column+col k v = Column k v Nothing Nothing+++mkThriftCol :: Column -> IO C.Column+mkThriftCol Column{..} = do+ now <- getTime+ return $ C.Column (Just colKey) (Just colVal) (Just now) colTTL+++castColumn :: C.ColumnOrSuperColumn -> Either CassandraException Column+castColumn x | Just c <- C.f_ColumnOrSuperColumn_column x = castCol c+ | Just c <- C.f_ColumnOrSuperColumn_super_column x = castSuperCol c+castColumn _ = + Left $ ConversionException "Unsupported/unexpected ColumnOrSuperColumn type"+++castCol :: C.Column -> Either CassandraException Column+castCol c + | Just nm <- C.f_Column_name c+ , Just val <- C.f_Column_value c+ , Just ts <- C.f_Column_timestamp c+ , ttl <- C.f_Column_ttl c+ = Right $ Column nm val (Just ts) ttl+castCol _ = Left $ ConversionException "Can't parse Column"+++castSuperCol :: C.SuperColumn -> Either CassandraException Column+castSuperCol c + | Just nm <- C.f_SuperColumn_name c+ , Just cols <- C.f_SuperColumn_columns c+ , Right cols' <- mapM castCol cols+ = Right $ SuperColumn nm cols'+castSuperCol _ = Left $ ConversionException "Can't parse SuperColumn"+++data CassandraException = + NotFoundException+ | InvalidRequestException String+ | UnavailableException+ | TimedOutException+ | AuthenticationException String+ | AuthorizationException String+ | SchemaDisagreementException+ | ConversionException String+ | OperationNotSupported String+ deriving (Eq,Show,Read,Ord,Data,Typeable)+++instance Exception CassandraException+++------------------------------------------------------------------------------+-- | Cassandra is VERY sensitive to its timestamp values. As a convention,+-- timestamps are always in microseconds+getTime :: IO Int64+getTime = do+ t <- getPOSIXTime+ return . fromIntegral . floor $ t * 1000000+