Permalink
Browse files

Fix wire protocol bit flags order. Catch not_master read and write er…

…ror codes and return {failure, not_master}
  • Loading branch information...
1 parent 0a8cc8c commit d6fd289d8813070e0b7446c1a01c0b7483e6322c Tony Hannan committed Nov 27, 2010
Showing with 41 additions and 38 deletions.
  1. +4 −6 README.md
  2. +1 −1 include/mongo_protocol.hrl
  3. +13 −9 src/mongo.erl
  4. +4 −4 src/mongo_protocol.erl
  5. +14 −17 src/mongo_query.erl
  6. +5 −1 src/mongodb_tests.erl
View
@@ -35,22 +35,20 @@ A database operation happens in the context of a connection, database, read-mode
mongo:insert (foo, {x,1, y,2}),
mongo:find (foo, {x,1}) end).
-`safe`, along with `{safe, GetLastErrorParams}` and `unsafe`, are write-modes. Safe mode makes a *getLastError* request after every write in the sequence, and if the reply says it failed then the rest of the sequence is aborted and `mongo:do` returns `{error, {write_failure, Reason}}`. Some possible write failures are: attempting to write to a slave server (connected server must be a master), and attempting to insert a duplicate key that is indexed to be unique. Alternatively, unsafe mode issues every write without a confirmation, so if a write fails you won't know about it and remaining operations will be executed. This is unsafe but faster because you there is no round-trip delay.
+`safe`, along with `{safe, GetLastErrorParams}` and `unsafe`, are write-modes. Safe mode makes a *getLastError* request after every write in the sequence. If the reply says it failed then the rest of the sequence is aborted and `mongo:do` returns `{failure, {write_failure, Reason}}`, or {failure, not_master} if connected to a slave. An example write failure is attempting to insert a duplicate key that is indexed to be unique. Alternatively, unsafe mode issues every write without a confirmation, so if a write fails you won't know about it and remaining operations will be executed. This is unsafe but faster because you there is no round-trip delay.
-`master`, along with `slave_ok`, are read-modes. `master` means every query in the sequence must read fresh data (from a master/primary server). If the connected server is not a master then the first read will fail, the remaining operations will be aborted, and `mongo:do` will return {error, not_master} [1]. `slave_ok` means every query is allowed to read stale data from a slave (master is fine too).
+`master`, along with `slave_ok`, are read-modes. `master` means every query in the sequence must read fresh data (from a master/primary server). If the connected server is not a master then the first read will fail, the remaining operations will be aborted, and `mongo:do` will return {failure, not_master}. `slave_ok` means every query is allowed to read stale data from a slave (master is fine too).
-`Conn` is the connection we send the operations to. If it fails during one of the operations then the remaining operations are aborted and `{error, {connection_failure, Reason}}` is returned.
+`Conn` is the connection we send the operations to. If it fails during one of the operations then the remaining operations are aborted and `{failure, {connection_failure, Reason}}` is returned.
`test` is the name of the database in this example. Collections accessed in the sequence (`foo` in this example) are taken from the database given in the fourth argument. If a collection is missing from the database it will be automatically created upon first access.
If there are no errors in the sequence of operations then the result of the last operation is returned as the result of the entire `mongo:do` command. It is wrapped in an *ok* tuple as in `{ok, Result}` to distinguish it from an error.
-`mongo:find` returns a *cursor* holding the pending list of results, which are accessed using `mongo:next` to get the next result, and `mongo:rest` to get the remaining results. `mongo:rest` also closes the cursor, otherwise you should close the cursor when finished using `mongo:close_cursor`.
+`mongo:find` returns a *cursor* holding the pending list of results, which are accessed using `mongo:next` to get the next result, and `mongo:rest` to get the remaining results. Either one throws `{cursor_expired, Cursor}` if the cursor was idle for more than 10 minutes. This exception is caught by `mongo:do` and returned as `{failure, {cursor_expired, Cursor}}`. `mongo:rest` also closes the cursor, otherwise you should close the cursor when finished using `mongo:close_cursor`.
Finally, you should close the connection when finished using `mongo:disconnect`.
See the [*mongo* module](http://github.com/TonyGen/mongodb-erlang/blob/master/src/mongo.erl) for a description of all operations. A type specification is provided with each operation so you know the expected arguments and results. The spec line also has a comment if it performs a side-effect such as IO and what exceptions it may throw. No comment means it is a pure function. Also, see the [*bson* module](http://github.com/TonyGen/bson-erlang/blob/master/src/bson.erl) in the bson application for details on the document type and its value types.
This driver does not provide helper functions for commands. Use `mongo:command` directly and refer to the [MongoDB documentation](http://www.mongodb.org/display/DOCS/Commands) for how to issue raw commands. A future version will probably include helper functions for the most common commands. In particular, `ensure_index` will be added in the next minor revision (expected in a few days) because it doesn't even use a command but requires updating a meta-collection directly. In the meantime, you should create indexes from the mongo (javascript) shell.
-
-[1] Currently not-master exception will raise an erlang:error instead of being returned as an error from `mongo:do`. This is a bug that will be fixed in the next minor revision expected in a few days.
@@ -46,7 +46,7 @@
-record (getmore, {
collection :: collection(),
- batchsize :: integer(),
+ batchsize = 0 :: batchsize(),
cursorid :: cursorid() }).
-record (reply, {
View
@@ -38,29 +38,31 @@ connect (Address) -> mongo_connect:connect ({Address, 27017}).
disconnect (Conn) -> mongo_connect:close (Conn).
-type action(A) :: fun (() -> A).
-% IO, reads process dict {mongo_action_context, #context{}}, and throws failure()
+% An Action does IO, reads process dict {mongo_action_context, #context{}}, and throws failure()
-type failure() ::
- mongo_connect:failure() |
- write_failure() |
- mongo_cursor:expired().
+ mongo_connect:failure() | % thrown by read and safe write
+ mongo_query:not_master() | % throws by read and safe write
+ write_failure() | % throws by safe write
+ mongo_cursor:expired(). % thrown by cursor next/rest
-record (context, {
write_mode :: write_mode(),
read_mode :: read_mode(),
dbconn :: mongo_connect:dbconnection() }).
--spec do (write_mode(), read_mode(), connection(), db(), action(A)) -> {ok, A} | {error, failure()}. % IO
+-spec do (write_mode(), read_mode(), connection(), db(), action(A)) -> {ok, A} | {failure, failure()}. % IO
% Execute mongo action under given write_mode, read_mode, connection, and db. Return action result or failure.
do (WriteMode, ReadMode, Connection, Database, Action) ->
PrevContext = get (mongo_action_context),
put (mongo_action_context, #context {write_mode = WriteMode, read_mode = ReadMode, dbconn = {Database, Connection}}),
try Action() of
Result -> {ok, Result}
catch
- throw: E = {connection_failure, _, _} -> {error, E};
- throw: E = {write_failure, _, _} -> {error, E};
- throw: E = {cursor_expired, _} -> {error, E}
+ throw: E = {connection_failure, _, _} -> {failure, E};
+ throw: E = not_master -> {failure, E};
+ throw: E = {write_failure, _, _} -> {failure, E};
+ throw: E = {cursor_expired, _} -> {failure, E}
after
case PrevContext of undefined -> erase (mongo_action_context); _ -> put (mongo_action_context, PrevContext) end
end.
@@ -87,7 +89,9 @@ write (Write) ->
Ack = mongo_query:write (Context #context.dbconn, Write, Params),
case bson:lookup (err, Ack) of
{} -> ok; {null} -> ok;
- {String} -> throw ({write_failure, bson:at (code, Ack), String}) end end.
+ {String} -> case bson:at (code, Ack) of
+ 10058 -> throw (not_master);
+ Code -> throw ({write_failure, Code, String}) end end end.
-spec insert (collection(), bson:document()) -> bson:value(). % Action
% Insert document into collection. Return its '_id' value, which is auto-generated if missing.
@@ -61,14 +61,14 @@ put_message (Db, Message, RequestId) -> case Message of
?put_header (?UpdateOpcode),
?put_int32 (0),
(put_cstring (dbcoll (Db, Coll))) /binary,
- (bit (U)):1, (bit (M)):1, 0:30,
+ ?put_bits32 (0,0,0,0,0,0, bit(M), bit(U)),
(put_document (Sel)) /binary,
(put_document (Up)) /binary >>;
#delete {collection = Coll, singleremove = R, selector = Sel} -> <<
?put_header (?DeleteOpcode),
?put_int32 (0),
(put_cstring (dbcoll (Db, Coll))) /binary,
- (bit (R)):1, 0:31,
+ ?put_bits32 (0,0,0,0,0,0,0, bit(R)),
(put_document (Sel)) /binary >>;
#killcursor {cursorids = Cids} -> <<
?put_header (?KillcursorOpcode),
@@ -78,7 +78,7 @@ put_message (Db, Message, RequestId) -> case Message of
#'query' {tailablecursor = TC, slaveok = SOK, nocursortimeout = NCT, awaitdata = AD,
collection = Coll, skip = Skip, batchsize = Batch, selector = Sel, projector = Proj} -> <<
?put_header (?QueryOpcode),
- 0:1, (bit (TC)):1, (bit (SOK)):1, 0:1, (bit (NCT)):1, (bit (AD)):1, 0:1, 0:25,
+ ?put_bits32 (0, 0, bit(AD), bit(NCT), 0, bit(SOK), bit(TC), 0),
(put_cstring (dbcoll (Db, Coll))) /binary,
?put_int32 (Skip),
?put_int32 (Batch),
@@ -95,7 +95,7 @@ put_message (Db, Message, RequestId) -> case Message of
-spec get_reply (binary()) -> {requestid(), reply(), binary()}.
get_reply (<<
?get_header (?ReplyOpcode, ResponseTo),
- CursorNotFound:1, QueryError:1, _:1, AwaitCapable:1, _:28,
+ ?get_bits32 (_,_,_,_, AwaitCapable, _, QueryError, CursorNotFound),
?get_int64 (CursorId),
?get_int32 (StartingFrom),
?get_int32 (NumDocs),
View
@@ -3,15 +3,16 @@
-export_type ([write/0, 'query'/0, command/0]).
-export_type ([getlasterror_request/0, getlasterror_reply/0]).
--export_type ([query_error/0]).
+-export_type ([not_master/0]).
-export ([write/3, write/2, find_one/2, find/2, command/3]).
-include ("mongo_protocol.hrl").
-% CIO means does IO and may throw mongo_connect:failure().
+% QIO means does IO and may throw mongo_connect:failure() or not_master().
--type query_error() :: {query_error, bson:utf8()}.
+-type not_master() :: not_master.
+% Thrown when attempting to query a slave when the query requires master (slaveok = false).
-type write() :: #insert{} | #update{} | #delete{}.
@@ -20,7 +21,7 @@
-type getlasterror_reply() :: bson:document().
% Reply to getlasterror request.
--spec write (mongo_connect:dbconnection(), write(), getlasterror_request()) -> getlasterror_reply(). % CIO
+-spec write (mongo_connect:dbconnection(), write(), getlasterror_request()) -> getlasterror_reply(). % QIO
% Send write and getlasterror request to mongodb over connection and wait for and return getlasterror reply. Bad getlasterror params are ignored.
% Caller is responsible for checking for error in reply; if 'err' field is null then success, otherwise it holds error message string.
write (DbConn, Write, GetlasterrorParams) ->
@@ -37,7 +38,7 @@ write (DbConn, Write) ->
-type command() :: bson:document().
--spec command (mongo_connect:dbconnection(), command(), boolean()) -> bson:document(). % CIO
+-spec command (mongo_connect:dbconnection(), command(), boolean()) -> bson:document(). % QIO
% Send command to mongodb over connection and wait for reply and return it. Boolean arg indicates slave-ok or not. 'bad_command' error if bad command.
command (DbConn, Command, SlaveOk) ->
Query = #'query' {collection = '$cmd', selector = Command, slaveok = SlaveOk},
@@ -49,31 +50,27 @@ command (DbConn, Command, SlaveOk) ->
-type maybe(A) :: {A} | {}.
--spec find_one (mongo_connect:dbconnection(), 'query'()) -> maybe (bson:document()). % CIO
+-spec find_one (mongo_connect:dbconnection(), 'query'()) -> maybe (bson:document()). % QIO
% Send read request to mongodb over connection and wait for reply. Return first result or nothing if empty.
find_one (DbConn, Query) ->
Query1 = Query #'query' {batchsize = -1},
Reply = mongo_connect:call (DbConn, [], Query1),
{0, Docs} = query_reply (Reply), % batchsize negative so cursor should be closed (0)
case Docs of [] -> {}; [Doc | _] -> {Doc} end.
--spec find (mongo_connect:dbconnection(), 'query'()) -> mongo_cursor:cursor(). % CIO
+-spec find (mongo_connect:dbconnection(), 'query'()) -> mongo_cursor:cursor(). % QIO
% Send read request to mongodb over connection and wait for reply of first batch. Return a cursor holding this batch and able to fetch next batch on demand.
find (DbConn, Query) ->
Reply = mongo_connect:call (DbConn, [], Query),
mongo_cursor:cursor (DbConn, Query #'query'.collection, Query #'query'.batchsize, query_reply (Reply)).
--spec query_reply (mongo_protocol:reply()) -> {cursorid(), [bson:document()]}. % CIO
+-spec query_reply (mongo_protocol:reply()) -> {cursorid(), [bson:document()]}. % QIO
% Extract cursorid and results from reply. 'bad_query' error if query error.
query_reply (#reply {
cursornotfound = false, queryerror = QueryError, awaitcapable = _,
cursorid = Cid, startingfrom = _, documents = Docs }) ->
- Error = case {QueryError, Docs} of
- {true, [_ | _]} -> true;
- {false, [Doc | _]} -> case bson:lookup ('$err', Doc) of
- {_} -> true; % first doc error doc (Server should also set QueryError)
- {} -> false end;
- _ -> false end,
- case Error of
- true -> erlang:error ({bad_query, hd (Docs)});
- false -> {Cid, Docs} end.
+ case QueryError of
+ false -> {Cid, Docs};
+ true -> case bson:at (code, hd (Docs)) of
+ 13435 -> throw (not_master);
+ _ -> erlang:error ({bad_query, hd (Docs)}) end end.
@@ -36,6 +36,8 @@ connect_test() ->
Doc1X = bson:update (text, <<"world!!">>, Doc1),
Cursor = mongo_query:find (DbConn, #'query' {collection = foo, selector = {}}),
[Doc0, Doc1X] = mongo_cursor:rest (Cursor),
+ mongo_cursor:close (Cursor),
+ #reply {cursornotfound = true} = mongo_connect:call (DbConn, [], #getmore {collection = foo, cursorid = 2938725639}),
mongo_connect:close (Conn).
% Mongod server must be running on 127.0.0.1:27017
@@ -57,6 +59,8 @@ mongo_test() ->
TeamNames = lists:map (fun (Team) -> {name, bson:at (name, Team)} end, Teams),
TeamNames = mongo:rest (mongo:find (team, {}, {'_id', 0, name, 1})),
BostonTeam = lists:last (Teams),
- {BostonTeam} = mongo:find_one (team, {home, {city, <<"Boston">>, state, <<"MA">>}})
+ {BostonTeam} = mongo:find_one (team, {home, {city, <<"Boston">>, state, <<"MA">>}}),
+ mongo:delete_one (team, {}),
+ 3 = mongo:count (team, {})
end),
mongo:disconnect (Conn).

0 comments on commit d6fd289

Please sign in to comment.