combined read & write mode into a single access mode. newtyped Action monad. liftDB for monads stacked on top of Action. fetch op

This commit is contained in:
Tony Hannan 2011-07-08 22:13:47 -04:00
parent 9f48c26384
commit 91c88c0a14
5 changed files with 111 additions and 59 deletions

View file

@ -10,7 +10,7 @@ Simple example below. Use with language extension /OvererloadedStrings/.
> >
> main = do > main = do
> pipe <- runIOE $ connect (host "127.0.0.1") > pipe <- runIOE $ connect (host "127.0.0.1")
> e <- access pipe safe Master "baseball" run > e <- access pipe master "baseball" run
> close pipe > close pipe
> print e > print e
> >
@ -53,5 +53,5 @@ import Database.MongoDB.Admin
{- Authors: Tony Hannan <tony@10gen.com> {- Authors: Tony Hannan <tony@10gen.com>
Copyright 2010 10gen Inc. Copyright 2010-11 10gen Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -}

View file

@ -109,7 +109,7 @@ ensureIndex idx = let k = (iColl idx, iName idx) in do
icache <- fetchIndexCache icache <- fetchIndexCache
set <- liftIO (readIORef icache) set <- liftIO (readIORef icache)
unless (S.member k set) $ do unless (S.member k set) $ do
writeMode (Safe []) (createIndex idx) accessMode master (createIndex idx)
liftIO $ writeIORef icache (S.insert k set) liftIO $ writeIORef icache (S.insert k set)
createIndex :: (MonadIO' m) => Index -> Action m () createIndex :: (MonadIO' m) => Index -> Action m ()

View file

@ -26,14 +26,14 @@ import Control.Monad (forM_)
import Control.Applicative ((<$>)) import Control.Applicative ((<$>))
import Data.UString (UString, unpack) import Data.UString (UString, unpack)
import Data.Bson as D (Document, lookup, at, (=:)) import Data.Bson as D (Document, lookup, at, (=:))
import Database.MongoDB.Query (access, safe, MasterOrSlaveOk(SlaveOk), Failure(ConnectionFailure), Command, runCommand) import Database.MongoDB.Query (access, slaveOk, Failure(ConnectionFailure), Command, runCommand)
import Database.MongoDB.Internal.Util (untilSuccess, liftIOE, runIOE, updateAssocs, shuffle) import Database.MongoDB.Internal.Util (untilSuccess, liftIOE, runIOE, updateAssocs, shuffle)
import Data.List as L (lookup, intersect, partition, (\\)) import Data.List as L (lookup, intersect, partition, (\\))
adminCommand :: Command -> Pipe -> IOE Document adminCommand :: Command -> Pipe -> IOE Document
-- ^ Run command against admin database on server connected to pipe. Fail if connection fails. -- ^ Run command against admin database on server connected to pipe. Fail if connection fails.
adminCommand cmd pipe = adminCommand cmd pipe =
liftIOE failureToIOError . ErrorT $ access pipe safe SlaveOk "admin" $ runCommand cmd liftIOE failureToIOError . ErrorT $ access pipe slaveOk "admin" $ runCommand cmd
where where
failureToIOError (ConnectionFailure e) = e failureToIOError (ConnectionFailure e) = e
failureToIOError e = userError $ show e failureToIOError e = userError $ show e

View file

@ -5,7 +5,6 @@ This module is not intended for direct use. Use the high-level interface at "Dat
{-# LANGUAGE RecordWildCards, StandaloneDeriving, OverloadedStrings, FlexibleContexts, TupleSections, TypeSynonymInstances, MultiParamTypeClasses, FlexibleInstances, UndecidableInstances #-} {-# LANGUAGE RecordWildCards, StandaloneDeriving, OverloadedStrings, FlexibleContexts, TupleSections, TypeSynonymInstances, MultiParamTypeClasses, FlexibleInstances, UndecidableInstances #-}
module Database.MongoDB.Internal.Protocol ( module Database.MongoDB.Internal.Protocol (
MasterOrSlaveOk(..),
FullCollection, FullCollection,
-- * Pipe -- * Pipe
Pipe, send, call, Pipe, send, call,
@ -43,13 +42,6 @@ import Control.Monad.Error
import System.IO (hFlush) import System.IO (hFlush)
import Database.MongoDB.Internal.Util (whenJust, hGetN, bitOr, byteStringHex) import Database.MongoDB.Internal.Util (whenJust, hGetN, bitOr, byteStringHex)
-- * MasterOrSlaveOk
data MasterOrSlaveOk =
Master -- ^ connect to master only
| SlaveOk -- ^ connect to a slave, or master if no slave available
deriving (Show, Eq)
-- * Pipe -- * Pipe
type Pipe = Pipeline Response Message type Pipe = Pipeline Response Message

View file

@ -1,10 +1,12 @@
-- | Query and update documents -- | Query and update documents
{-# LANGUAGE OverloadedStrings, RecordWildCards, NamedFieldPuns, TupleSections, FlexibleContexts, FlexibleInstances, UndecidableInstances, MultiParamTypeClasses, GeneralizedNewtypeDeriving, StandaloneDeriving, TypeSynonymInstances, RankNTypes #-} {-# LANGUAGE OverloadedStrings, RecordWildCards, NamedFieldPuns, TupleSections, FlexibleContexts, FlexibleInstances, UndecidableInstances, MultiParamTypeClasses, GeneralizedNewtypeDeriving, StandaloneDeriving, TypeSynonymInstances, TypeFamilies #-}
module Database.MongoDB.Query ( module Database.MongoDB.Query (
-- * Monad -- * Monad
Action, Failure(..), access, Action, access, Failure(..),
AccessMode(..), GetLastError, master, slaveOk, accessMode,
MonadDB(..),
-- * Database -- * Database
Database, allDatabases, useDb, thisDatabase, Database, allDatabases, useDb, thisDatabase,
-- ** Authentication -- ** Authentication
@ -15,7 +17,6 @@ module Database.MongoDB.Query (
Selection(..), Selector, whereJS, Selection(..), Selector, whereJS,
Select(select), Select(select),
-- * Write -- * Write
WriteMode(..), safe, GetLastError, writeMode,
-- ** Insert -- ** Insert
insert, insert_, insertMany, insertMany_, insert, insert_, insertMany, insertMany_,
-- ** Update -- ** Update
@ -23,10 +24,9 @@ module Database.MongoDB.Query (
-- ** Delete -- ** Delete
delete, deleteOne, delete, deleteOne,
-- * Read -- * Read
MasterOrSlaveOk(..), readMode,
-- ** Query -- ** Query
Query(..), QueryOption(..), Projector, Limit, Order, BatchSize, Query(..), QueryOption(..), Projector, Limit, Order, BatchSize,
explain, find, findOne, count, distinct, explain, find, findOne, fetch, count, distinct,
-- *** Cursor -- *** Cursor
Cursor, next, nextN, rest, closeCursor, isCursorClosed, Cursor, next, nextN, rest, closeCursor, isCursorClosed,
-- ** Group -- ** Group
@ -41,12 +41,15 @@ module Database.MongoDB.Query (
import Prelude as X hiding (lookup) import Prelude as X hiding (lookup)
import Data.UString as U (UString, dropWhile, any, tail) import Data.UString as U (UString, dropWhile, any, tail)
import Data.Bson (Document, at, lookup, look, Field(..), (=:), (=?), Label, Value(String,Doc), Javascript, genObjectId) import Data.Bson (Document, at, lookup, look, Field(..), (=:), (=?), Label, Value(String,Doc), Javascript, genObjectId)
import Database.MongoDB.Internal.Protocol (MasterOrSlaveOk(..), Pipe, Notice(..), Request(GetMore), Reply(..), QueryOption(..), ResponseFlag(..), UpdateOption(..), DeleteOption(..), CursorId, FullCollection, Username, Password, pwKey) import Database.MongoDB.Internal.Protocol (Pipe, Notice(..), Request(GetMore), Reply(..), QueryOption(..), ResponseFlag(..), UpdateOption(..), DeleteOption(..), CursorId, FullCollection, Username, Password, pwKey)
import qualified Database.MongoDB.Internal.Protocol as P (send, call, Request(Query)) import qualified Database.MongoDB.Internal.Protocol as P (send, call, Request(Query))
import Database.MongoDB.Internal.Util (MonadIO', loop, liftIOE, true1, (<.>)) import Database.MongoDB.Internal.Util (MonadIO', loop, liftIOE, true1, (<.>))
import Control.Monad.MVar import Control.Monad.MVar
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.Reader import Control.Monad.Reader
import Control.Monad.State (StateT)
import Control.Monad.Writer (WriterT, Monoid)
import Control.Monad.RWS (RWST)
import Control.Applicative (Applicative, (<$>)) import Control.Applicative (Applicative, (<$>))
import Data.Maybe (listToMaybe, catMaybes) import Data.Maybe (listToMaybe, catMaybes)
import Data.Int (Int32) import Data.Int (Int32)
@ -54,8 +57,15 @@ import Data.Word (Word32)
-- * Monad -- * Monad
type Action m = ErrorT Failure (ReaderT Context m) newtype Action m a = Action (ErrorT Failure (ReaderT Context m) a)
-- ^ A monad on top of m (which must be a MonadIO) with access to a 'Context' and may throw a 'Failure' deriving (Functor, Applicative, Monad, MonadIO, MonadMVar, MonadError Failure)
-- ^ A monad on top of m (which must be a MonadIO) that may access the database and may fail with a DB 'Failure'
instance MonadTrans Action where lift = Action . lift . lift
access :: (MonadIO m) => Pipe -> AccessMode -> Database -> Action m a -> m (Either Failure a)
-- ^ Run action against database on server at other end of pipe. Use write mode for any writes and read mode for any reads. Return Left on connection or read/write failure.
access myPipe myAccessMode myDatabase (Action action) = runReaderT (runErrorT action) Context{..}
-- | A connection failure, or a read or write exception like cursor expired or inserting a duplicate key. -- | A connection failure, or a read or write exception like cursor expired or inserting a duplicate key.
-- Note, unexpected data from the server is not a Failure, rather it is a programming error (you should call 'error' in this case) because the client and server are incompatible and requires a programming change. -- Note, unexpected data from the server is not a Failure, rather it is a programming error (you should call 'error' in this case) because the client and server are incompatible and requires a programming change.
@ -64,38 +74,95 @@ data Failure =
| CursorNotFoundFailure CursorId -- ^ Cursor expired because it wasn't accessed for over 10 minutes, or this cursor came from a different server that the one you are currently connected to (perhaps a fail over happen between servers in a replica set) | CursorNotFoundFailure CursorId -- ^ Cursor expired because it wasn't accessed for over 10 minutes, or this cursor came from a different server that the one you are currently connected to (perhaps a fail over happen between servers in a replica set)
| QueryFailure String -- ^ Query failed for some reason as described in the string | QueryFailure String -- ^ Query failed for some reason as described in the string
| WriteFailure ErrorCode String -- ^ Error observed by getLastError after a write, error description is in string | WriteFailure ErrorCode String -- ^ Error observed by getLastError after a write, error description is in string
| DocNotFound Selection -- ^ 'fetch' found no document matching selection
deriving (Show, Eq) deriving (Show, Eq)
type ErrorCode = Int type ErrorCode = Int
-- ^ Error code from getLastError -- ^ Error code from getLastError
instance Error Failure where strMsg = error instance Error Failure where strMsg = error
-- ^ 'fail' is treated the same as 'error'. In other words, don't use it. -- ^ 'fail' is treated the same as a programming 'error'. In other words, don't use it.
-- | Type of reads and writes to perform
data AccessMode =
ReadStaleOk -- Read-only action, reading stale data from a slave is OK.
| UnconfirmedWrites -- Read-write action, slave not OK, every write is fire & forget.
| ConfirmWrites GetLastError -- Read-write action, slave not OK, every write is confirmed with getLastError.
type GetLastError = Document
-- ^ Parameters for getLastError command. For example ["w" =: 2] tells the server to wait for the write to reach at least two servers in replica set before acknowledging. See "http://www.mongodb.org/display/DOCS/Last+Error+Commands" for more options.
master :: AccessMode
-- ^ @'ConfirmWrites' []@
master = ConfirmWrites []
slaveOk :: AccessMode
-- ^ @'ReadStaleOk'@
slaveOk = ReadStaleOk
accessMode :: (Monad m) => AccessMode -> Action m a -> Action m a
-- ^ Run action with given 'AccessMode'
accessMode mode (Action act) = Action $ local (\ctx -> ctx {myAccessMode = mode}) act
readMode :: AccessMode -> ReadMode
readMode ReadStaleOk = StaleOk
readMode _ = Fresh
writeMode :: AccessMode -> WriteMode
writeMode ReadStaleOk = Confirm []
writeMode UnconfirmedWrites = NoConfirm
writeMode (ConfirmWrites z) = Confirm z
-- | Values needed when executing a db operation -- | Values needed when executing a db operation
data Context = Context { data Context = Context {
myPipe :: Pipe, -- | operations read/write to this pipelined TCP connection to a MongoDB server myPipe :: Pipe, -- | operations read/write to this pipelined TCP connection to a MongoDB server
myReadMode :: MasterOrSlaveOk, -- | queries set slaveOk according to this mode myAccessMode :: AccessMode, -- | read/write operation will use this access mode
myWriteMode :: WriteMode, -- | writes will automatically issue a getlasterror when this writeMode is `Safe`
myDatabase :: Database } -- | operations query/update this database myDatabase :: Database } -- | operations query/update this database
access :: (MonadIO m) => Pipe -> WriteMode -> MasterOrSlaveOk -> Database -> Action m a -> m (Either Failure a) myReadMode :: Context -> ReadMode
-- ^ Run action under given context. Return Left on Failure. myReadMode = readMode . myAccessMode
access myPipe myWriteMode myReadMode myDatabase action = runReaderT (runErrorT action) Context{..}
myWriteMode :: Context -> WriteMode
myWriteMode = writeMode . myAccessMode
send :: (MonadIO m) => [Notice] -> Action m () send :: (MonadIO m) => [Notice] -> Action m ()
-- ^ Send notices as a contiguous batch to server with no reply. Throw 'ConnectionFailure' if pipe fails. -- ^ Send notices as a contiguous batch to server with no reply. Throw 'ConnectionFailure' if pipe fails.
send ns = do send ns = Action $ do
pipe <- asks myPipe pipe <- asks myPipe
liftIOE ConnectionFailure $ P.send pipe ns liftIOE ConnectionFailure $ P.send pipe ns
call :: (MonadIO m) => [Notice] -> Request -> Action m (ErrorT Failure IO Reply) call :: (MonadIO m) => [Notice] -> Request -> Action m (ErrorT Failure IO Reply)
-- ^ Send notices and request as a contiguous batch to server and return reply promise, which will block when invoked until reply arrives. This call will throw 'ConnectionFailure' if pipe fails on send, and promise will throw 'ConnectionFailure' if pipe fails on receive. -- ^ Send notices and request as a contiguous batch to server and return reply promise, which will block when invoked until reply arrives. This call will throw 'ConnectionFailure' if pipe fails on send, and promise will throw 'ConnectionFailure' if pipe fails on receive.
call ns r = do call ns r = Action $ do
pipe <- asks myPipe pipe <- asks myPipe
promise <- liftIOE ConnectionFailure $ P.call pipe ns r promise <- liftIOE ConnectionFailure $ P.call pipe ns r
return (liftIOE ConnectionFailure promise) return (liftIOE ConnectionFailure promise)
-- | If you stack a monad on top of 'Action' then make it an instance of this class and use 'liftDB' to execute an DB Actions within it. Instances already exist for simple mtl transformers.
class (Monad m, MonadMVar (BaseMonad m), Applicative (BaseMonad m), Functor (BaseMonad m)) => MonadDB m where
type BaseMonad m :: * -> *
liftDB :: Action (BaseMonad m) a -> m a
instance (MonadMVar m, Applicative m, Functor m) => MonadDB (Action m) where
type BaseMonad (Action m) = m
liftDB = id
instance (MonadDB m, Error e) => MonadDB (ErrorT e m) where
type BaseMonad (ErrorT e m) = BaseMonad m
liftDB = lift . liftDB
instance (MonadDB m) => MonadDB (ReaderT r m) where
type BaseMonad (ReaderT r m) = BaseMonad m
liftDB = lift . liftDB
instance (MonadDB m) => MonadDB (StateT s m) where
type BaseMonad (StateT s m) = BaseMonad m
liftDB = lift . liftDB
instance (MonadDB m, Monoid w) => MonadDB (WriterT w m) where
type BaseMonad (WriterT w m) = BaseMonad m
liftDB = lift . liftDB
instance (MonadDB m, Monoid w) => MonadDB (RWST r w s m) where
type BaseMonad (RWST r w s m) = BaseMonad m
liftDB = lift . liftDB
-- * Database -- * Database
type Database = UString type Database = UString
@ -106,11 +173,11 @@ allDatabases = map (at "name") . at "databases" <$> useDb "admin" (runCommand1 "
thisDatabase :: (Monad m) => Action m Database thisDatabase :: (Monad m) => Action m Database
-- ^ Current database in use -- ^ Current database in use
thisDatabase = asks myDatabase thisDatabase = Action $ asks myDatabase
useDb :: (Monad m) => Database -> Action m a -> Action m a useDb :: (Monad m) => Database -> Action m a -> Action m a
-- ^ Run action against given database -- ^ Run action against given database
useDb = local . \db ctx -> ctx {myDatabase = db} useDb db (Action act) = Action $ local (\ctx -> ctx {myDatabase = db}) act
-- * Authentication -- * Authentication
@ -159,28 +226,16 @@ instance Select Query where
-- * Write -- * Write
-- | Default write-mode is 'Unsafe'
data WriteMode = data WriteMode =
Unsafe -- ^ Submit writes without receiving acknowledgments. Fast. Assumes writes succeed even though they may not. NoConfirm -- ^ Submit writes without receiving acknowledgments. Fast. Assumes writes succeed even though they may not.
| Safe GetLastError -- ^ Receive an acknowledgment after every write, and raise exception if one says the write failed. This is acomplished by sending the getLastError command, with given 'GetLastError' parameters, after every write. | Confirm GetLastError -- ^ Receive an acknowledgment after every write, and raise exception if one says the write failed. This is acomplished by sending the getLastError command, with given 'GetLastError' parameters, after every write.
deriving (Show, Eq) deriving (Show, Eq)
type GetLastError = Document
-- ^ Parameters for getLastError command. For example ["w" =: 2] tells the server to wait for the write to reach at least two servers in replica set before acknowledging. See "http://www.mongodb.org/display/DOCS/Last+Error+Commands" for more options.
safe :: WriteMode
-- ^ Safe []
safe = Safe []
writeMode :: (Monad m) => WriteMode -> Action m a -> Action m a
-- ^ Run action with given 'WriteMode'
writeMode = local . \w ctx -> ctx {myWriteMode = w}
write :: (MonadIO m) => Notice -> Action m () write :: (MonadIO m) => Notice -> Action m ()
-- ^ Send write to server, and if write-mode is 'Safe' then include getLastError request and raise 'WriteFailure' if it reports an error. -- ^ Send write to server, and if write-mode is 'Safe' then include getLastError request and raise 'WriteFailure' if it reports an error.
write notice = asks myWriteMode >>= \mode -> case mode of write notice = Action (asks myWriteMode) >>= \mode -> case mode of
Unsafe -> send [notice] NoConfirm -> send [notice]
Safe params -> do Confirm params -> do
let q = query (("getlasterror" =: (1 :: Int)) : params) "$cmd" let q = query (("getlasterror" =: (1 :: Int)) : params) "$cmd"
Batch _ _ [doc] <- fulfill =<< request [notice] =<< queryRequest False q {limit = 1} Batch _ _ [doc] <- fulfill =<< request [notice] =<< queryRequest False q {limit = 1}
case lookup "err" doc of case lookup "err" doc of
@ -262,13 +317,14 @@ delete' opts (Select sel col) = do
-- * Read -- * Read
readMode :: (Monad m) => MasterOrSlaveOk -> Action m a -> Action m a data ReadMode =
-- ^ Execute action using given read mode. Master = consistent reads, SlaveOk = eventually consistent reads. Fresh -- ^ read from master only
readMode = local . \r ctx -> ctx {myReadMode = r} | StaleOk -- ^ read from slave ok
deriving (Show, Eq)
msOption :: MasterOrSlaveOk -> [QueryOption] readModeOption :: ReadMode -> [QueryOption]
msOption Master = [] readModeOption Fresh = []
msOption SlaveOk = [SlaveOK] readModeOption StaleOk = [SlaveOK]
-- ** Query -- ** Query
@ -314,6 +370,10 @@ findOne q = do
Batch _ _ docs <- fulfill =<< request [] =<< queryRequest False q {limit = 1} Batch _ _ docs <- fulfill =<< request [] =<< queryRequest False q {limit = 1}
return (listToMaybe docs) return (listToMaybe docs)
fetch :: (MonadIO m) => Query -> Action m Document
-- ^ Same as 'findOne' except throw 'DocNotFound' if no match
fetch q = findOne q >>= maybe (throwError $ DocNotFound $ selection q) return
explain :: (MonadIO m) => Query -> Action m Document explain :: (MonadIO m) => Query -> Action m Document
-- ^ Return performance stats of query execution -- ^ Return performance stats of query execution
explain q = do -- same as findOne but with explain set to true explain q = do -- same as findOne but with explain set to true
@ -333,11 +393,11 @@ distinct k (Select sel col) = at "values" <$> runCommand ["distinct" =: col, "ke
queryRequest :: (Monad m) => Bool -> Query -> Action m (Request, Limit) queryRequest :: (Monad m) => Bool -> Query -> Action m (Request, Limit)
-- ^ Translate Query to Protocol.Query. If first arg is true then add special $explain attribute. -- ^ Translate Query to Protocol.Query. If first arg is true then add special $explain attribute.
queryRequest isExplain Query{..} = do queryRequest isExplain Query{..} = do
ctx <- ask ctx <- Action ask
return $ queryRequest' (myReadMode ctx) (myDatabase ctx) return $ queryRequest' (myReadMode ctx) (myDatabase ctx)
where where
queryRequest' mos db = (P.Query{..}, remainingLimit) where queryRequest' rm db = (P.Query{..}, remainingLimit) where
qOptions = msOption mos ++ options qOptions = readModeOption rm ++ options
qFullCollection = db <.> coll selection qFullCollection = db <.> coll selection
qSkip = fromIntegral skip qSkip = fromIntegral skip
(qBatchSize, remainingLimit) = batchSizeRemainingLimit batchSize limit (qBatchSize, remainingLimit) = batchSizeRemainingLimit batchSize limit
@ -383,9 +443,9 @@ fromReply limit Reply{..} = do
CursorNotFound -> throwError (CursorNotFoundFailure rCursorId) CursorNotFound -> throwError (CursorNotFoundFailure rCursorId)
QueryError -> throwError (QueryFailure $ at "$err" $ head rDocuments) QueryError -> throwError (QueryFailure $ at "$err" $ head rDocuments)
fulfill :: (MonadIO m) => DelayedBatch -> ErrorT Failure m Batch fulfill :: (MonadIO m) => DelayedBatch -> Action m Batch
-- ^ Demand and wait for result, raise failure if exception -- ^ Demand and wait for result, raise failure if exception
fulfill = liftIOE id fulfill = Action . liftIOE id
-- *** Cursor -- *** Cursor