From 86e13726afc9983b4f61271b4db962986f6703ba Mon Sep 17 00:00:00 2001 From: Jeremy Hood Date: Thu, 10 Nov 2011 16:52:30 -0500 Subject: [PATCH 1/3] added the ability to turn off auto reconnect in emongo_pool; other minor bug fixes --- src/emongo.erl | 10 +++++++--- src/emongo_pool.erl | 31 +++++++++++++++++++++---------- src/emongo_sup.erl | 7 +++++-- t/007-performance.t | 6 ------ 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/emongo.erl b/src/emongo.erl index db86e36..11eeae5 100644 --- a/src/emongo.erl +++ b/src/emongo.erl @@ -26,7 +26,7 @@ -module(emongo). -behaviour(gen_server). --export([pools/0, oid/0, add_pool/5, del_pool/1]). +-export([pools/0, oid/0, add_pool/5, add_pool/6, del_pool/1]). -export([fold_all/6, find_all/2, find_all/3, find_all/4, @@ -85,6 +85,9 @@ oid() -> add_pool(PoolId, Host, Port, Database, Size) -> emongo_sup:start_pool(PoolId, Host, Port, Database, Size). +add_pool(PoolId, Host, Port, Database, Size, AutoReconnect) -> + emongo_sup:start_pool(PoolId, Host, Port, Database, Size, AutoReconnect). + del_pool(PoolId) -> emongo_sup:stop_pool(PoolId). @@ -509,8 +512,9 @@ fam_options([{update, _} | Options], OptDoc) -> fam_options(Options, OptDoc); % update is a param to find_and_modify/5 fam_options([{new, _}=Opt | Options], OptDoc) -> fam_options(Options, [opt(Opt) | OptDoc]); -fam_options([{fields, _}=Opt | Options], OptDoc) -> - fam_options(Options, [opt(Opt) | OptDoc]); +fam_options([{fields, Fields} | Options], OptDoc) -> + NewOpt = [{Field, 1} || Field <- Fields], + fam_options(Options, [opt({fields, NewOpt}) | OptDoc]); fam_options([{upsert, _}=Opt | Options], OptDoc) -> fam_options(Options, [opt(Opt) | OptDoc]); fam_options([_ | Options], OptDoc) -> diff --git a/src/emongo_pool.erl b/src/emongo_pool.erl index a4b1451..b46616d 100644 --- a/src/emongo_pool.erl +++ b/src/emongo_pool.erl @@ -6,7 +6,7 @@ -behaviour(gen_server). %% API --export([start_link/5, pid/1, pid/2]). +-export([start_link/6, pid/1, pid/2]). -deprecated([pid/1]). @@ -24,6 +24,7 @@ port, database, size, + auto_reconnect, active=true, poll=none, conn_pid=pqueue:new(), @@ -42,8 +43,8 @@ %% public api %% %%%%%%%%%%%%%%%% -start_link(PoolId, Host, Port, Database, Size) -> - gen_server:start_link(?MODULE, [PoolId, Host, Port, Database, Size], []). +start_link(PoolId, Host, Port, Database, Size, AutoReconnect) -> + gen_server:start_link(?MODULE, [PoolId, Host, Port, Database, Size, AutoReconnect], []). pid(Pid) -> gen_server:call(Pid, pid). @@ -62,18 +63,21 @@ pid(Pid, RequestCount) -> %% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- -init([PoolId, Host, Port, Database, Size]) -> +init([PoolId, Host, Port, Database, Size, AutoReconnect]) -> process_flag(trap_exit, true), Pool0 = #pool{id = PoolId, host = Host, port = Port, database = unicode:characters_to_binary(Database), - size = Size + size = Size, + auto_reconnect = AutoReconnect }, - {noreply, Pool} = handle_info(?poll(), Pool0), - {ok, Pool}. + case handle_info(?poll(), Pool0) of + {noreply, Pool} -> {ok, Pool}; + {stop, Reason, _NewState} -> {stop, Reason} + end. %%-------------------------------------------------------------------- %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | @@ -115,19 +119,21 @@ handle_info({'EXIT', Pid, Reason}, #pool{conn_pid=Pids}=State) -> [State#pool.id, Reason]), Pids1 = pqueue:filter(fun(Item) -> Item =/= Pid end, Pids), - {noreply, State#pool{conn_pid = Pids1, active=false}}; + NewState = State#pool{conn_pid = Pids1, active=false}, + check_return_state(NewState); handle_info(?poll(), State) -> erlang:send_after(?POLL_INTERVAL, self(), poll), NewState = do_open_connections(State), - {noreply, NewState}; + check_return_state(NewState); handle_info(?poll_timeout(Pid, ReqId, Tag), #pool{poll={Tag, _}}=State) -> case catch emongo_server:recv(Pid, ReqId, 0, Tag) of #response{} -> {noreply, State#pool{active=true, poll=none}}; _ -> - {noreply, State#pool{active=false, poll=none}} + NewState = State#pool{active=false, poll=none}, + check_return_state(NewState) end; handle_info({Tag, _}, #pool{poll={Tag, TimerRef}}=State) -> @@ -211,3 +217,8 @@ do_poll(Pool) -> _ -> Pool#pool{active=false} end. + +check_return_state(#pool{active=false, auto_reconnect=false}=State) -> + {stop, connection_error, State}; +check_return_state(State) -> + {noreply, State}. diff --git a/src/emongo_sup.erl b/src/emongo_sup.erl index 97645ad..8fa5616 100644 --- a/src/emongo_sup.erl +++ b/src/emongo_sup.erl @@ -3,7 +3,7 @@ -behaviour(supervisor). -export([start_link/0, pools/0, worker_pid/2, worker_pid/3]). --export([start_pool/5, stop_pool/1]). +-export([start_pool/5, start_pool/6, stop_pool/1]). -export([start_router/2, stop_router/1]). -deprecated([worker_pid/2]). @@ -33,8 +33,11 @@ stop_router(BalId) -> start_pool(PoolId, Host, Port, Database, Size) -> + start_pool(PoolId, Host, Port, Database, Size, true). + +start_pool(PoolId, Host, Port, Database, Size, AutoReconnect) -> supervisor:start_child(?MODULE, {PoolId, - {emongo_pool, start_link, [PoolId, Host, Port, Database, Size]}, + {emongo_pool, start_link, [PoolId, Host, Port, Database, Size, AutoReconnect]}, permanent, 10000, worker, [emongo_pool] }). diff --git a/t/007-performance.t b/t/007-performance.t index 44be02e..8bf438c 100644 --- a/t/007-performance.t +++ b/t/007-performance.t @@ -94,12 +94,6 @@ block_until_done(X) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -ensure_started(App) -> - case application:start(App) of - ok -> ok; - {error, {already_started, App}} -> ok - end. - cur_time_ms() -> {MegaSec, Sec, MicroSec} = erlang:now(), MegaSec * 1000000000 + Sec * 1000 + erlang:round(MicroSec / 1000). From f5cc296abc31463119d4f9d6a66896eaf1024175 Mon Sep 17 00:00:00 2001 From: Jeremy Hood Date: Thu, 19 Apr 2012 09:30:12 -0400 Subject: [PATCH 2/3] Back to version based on Jacob's --- .gitignore | 2 +- Emakefile | 12 +- Makefile | 39 +- README.markdown | 148 ++++--- debian/changelog | 12 - debian/compat | 1 - debian/control | 13 - debian/copyright | 1 - debian/rules | 5 - ebin/emongo.appup | 30 -- include/emongo.hrl | 32 +- priv/erlang-emongo.spec | 65 ---- priv/example.config | 8 +- src/emongo.app.src | 11 - src/emongo.erl | 830 +++++++++++++++++++++++----------------- src/emongo_app.erl | 32 +- src/emongo_bson.erl | 35 +- src/emongo_packet.erl | 48 +-- src/emongo_pool.erl | 224 ----------- src/emongo_router.erl | 191 --------- src/emongo_server.erl | 171 --------- src/emongo_sup.erl | 89 ----- src/pqueue.erl | 107 ------ t/001-load.t | 10 +- t/002-bson.t | 285 +++++++------- t/003-find.t | 115 +++--- t/004-cond-exprs.t | 212 +++++----- t/005-drop.t | 21 - t/006-multiup.t | 25 -- t/007-performance.t | 107 ------ t/pqueue_test.erl | 69 ---- 31 files changed, 912 insertions(+), 2038 deletions(-) delete mode 100644 debian/changelog delete mode 100644 debian/compat delete mode 100644 debian/control delete mode 100644 debian/copyright delete mode 100755 debian/rules delete mode 100644 ebin/emongo.appup delete mode 100644 priv/erlang-emongo.spec delete mode 100644 src/emongo.app.src delete mode 100644 src/emongo_pool.erl delete mode 100644 src/emongo_router.erl delete mode 100644 src/emongo_server.erl delete mode 100644 src/emongo_sup.erl delete mode 100644 src/pqueue.erl delete mode 100644 t/005-drop.t delete mode 100644 t/006-multiup.t delete mode 100644 t/007-performance.t delete mode 100644 t/pqueue_test.erl diff --git a/.gitignore b/.gitignore index 7d3bff3..c8b3877 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ ebin/*.beam -ebin/*.app +.eunit diff --git a/Emakefile b/Emakefile index 4311581..d59c341 100644 --- a/Emakefile +++ b/Emakefile @@ -1,5 +1,7 @@ -{"src/*", [ - debug_info, - {i, "include/"}, - {outdir, "ebin/"} -]}. \ No newline at end of file +% -*- mode: erlang -*- + +{[ 'src/*' ], + [ {i, "include"}, + {outdir, "ebin"}, + debug_info ] +}. \ No newline at end of file diff --git a/Makefile b/Makefile index 7d300d3..ce7b429 100644 --- a/Makefile +++ b/Makefile @@ -1,36 +1,13 @@ -PKGNAME=emongo -ROOTDIR=`erl -eval 'io:format("~s~n", [code:root_dir()])' -s init stop -noshell` -LIBDIR=$(shell erl -eval 'io:format("~s~n", [code:lib_dir()])' -s init stop -noshell) -# get application vsn from app file -VERSION=$(shell erl -pa ebin/ -eval 'application:load(${PKGNAME}), {ok, Vsn} = application:get_key(${PKGNAME}, vsn), io:format("~s~n", [Vsn])' -s init stop -noshell) +all: emake -all: src +emake: + erl -make -src: FORCE - @erl -make - @cp src/${PKGNAME}.app.src ebin/${PKGNAME}.app - -test: src +test: emake prove t/*.t -clean: - rm -rf erl_crash.dump *.boot *.rel *.script ebin/*.beam ebin/emongo.app - -package: clean - @mkdir $(PKGNAME)-$(VERSION)/ && cp -rf ebin include Emakefile Makefile priv README.markdown src t $(PKGNAME)-$(VERSION) - @COPYFILE_DISABLE=true tar zcf $(PKGNAME)-$(VERSION).tgz $(PKGNAME)-$(VERSION) - @rm -rf $(PKGNAME)-$(VERSION)/ - -install: src - @mkdir -p $(DESTDIR)/$(LIBDIR)/$(PKGNAME)-$(VERSION)/ebin - @mkdir -p $(DESTDIR)/$(LIBDIR)/$(PKGNAME)-$(VERSION)/include - for i in ebin/*.beam include/*.hrl ebin/*.app; do install $$i $(DESTDIR)/$(LIBDIR)/$(PKGNAME)-$(VERSION)/$$i ; done +check: emake + @./rebar eunit skip_deps=true - -plt: src - @dialyzer --check_plt -q -r . -I include/ - -check: src - @dialyzer --src -r . -I include/ - -FORCE: \ No newline at end of file +clean: + rm -rf $(wildcard ebin/*.beam) erl_crash.dump .eunit/ diff --git a/README.markdown b/README.markdown index 4c70541..1b5c547 100644 --- a/README.markdown +++ b/README.markdown @@ -1,20 +1,19 @@ #### The goal of emongo is to be stable, fast and easy to use. -## Compile and install +## Build make - sudo make install - + ## Start emongo application:start(emongo). - + ## Connecting to mongodb #### Option 1 - Config file example.config: - + [{emongo, [ {pools, [ {pool1, [ @@ -25,7 +24,7 @@ example.config: ]} ]} ]}]. - + specify the config file path when starting Erlang erl -config priv/example @@ -33,17 +32,17 @@ specify the config file path when starting Erlang start the application application:start(emongo). - + #### Option 2 - Add pool start the app and then add as many pools as you like application:start(emongo). emongo:add_pool(pool1, "localhost", 27017, "testdatabase", 1). - + ## API Type Reference -__PoolName__ = atom() +__PoolId__ = atom() __Host__ = string() __Port__ = integer() __Database__ = string() @@ -53,44 +52,42 @@ __Selector__ = Document __Document__ = [{Key, Val}] __Key__ = string() | atom() | binary() | integer() __Val__ = float() | string() | binary() | Document | {array, [term()]} | {binary, BinSubType, binary()} | {oid, binary()} | {oid, string()} | bool() | now() | datetime() | undefined | {regexp, string(), string()} | integer() -__BinSubType__ = integer() +__BinSubType__ = integer() ## Add Pool - emongo:add_pool(PoolName, Host, Port, Database, PoolSize) -> ok + emongo:add_pool(PoolId, Host, Port, Database, PoolSize) -> ok ## Insert -__PoolName__ = atom() +__PoolId__ = atom() __CollectionName__ = string() __Document__ = [{Key, Val}] -__Documents__ = [Document] - - emongo:insert(PoolName, CollectionName, Document) -> ok - emongo:insert(PoolName, CollectionName, Documents) -> ok +__Documents__ = [Document] + emongo:insert(PoolId, CollectionName, Document) -> ok + emongo:insert(PoolId, CollectionName, Documents) -> ok + ### Examples %% insert a single document with two fields into the "collection" collection emongo:insert(test, "collection", [{"field1", "value1"}, {"field2", "value2"}]). - + %% insert two documents, each with a single field into the "collection" collection - emongo:insert(test, "collection", [[{"document1_field1", "value1"}], [{"document2_field1", "value1"}]]). + emongo:insert(test, "collection", [[{"document1_field1", "value1"}], [{"document2_field1", "value1"}]]). ## Update -__PoolName__ = atom() +__PoolId__ = atom() __CollectionName__ = string() __Selector__ = Document __Document__ = [{Key, Val}] __Upsert__ = true | false (insert a new document if the selector does not match an existing document) -__MultiUpdate__ = true | false (if all documents matching selector should be updated) - - %% by default upsert == false and multiupdate == false - emongo:update(PoolName, CollectionName, Selector, Document) -> ok - emongo:update(PoolName, CollectionName, Selector, Document, Upsert) -> ok - emongo:update(PoolName, CollectionName, Selector, Document, Upsert, MultiUpdate) -> ok + %% by default upsert == false + emongo:update(PoolId, CollectionName, Selector, Document) -> ok + emongo:update(PoolId, CollectionName, Selector, Document, Upsert) -> ok + ### Examples %% update the document that matches "field1" == "value1" @@ -98,18 +95,18 @@ __MultiUpdate__ = true | false (if all documents matching selector should be upd ## Delete -__PoolName__ = atom() +__PoolId__ = atom() __CollectionName__ = string() -__Selector__ = Document +__Selector__ = Document %% delete all documents in a collection - emongo:delete(PoolName, CollectionName) -> ok - + emongo:delete(PoolId, CollectionName) -> ok + %% delete all documents in a collection that match a selector - emongo:delete(PoolName, CollectionName, Selector) -> ok - + emongo:delete(PoolId, CollectionName, Selector) -> ok + ## Find - + __Options__ = {timeout, Timeout} | {limit, Limit} | {offset, Offset} | {orderby, Orderby} | {fields, Fields} | response_options __Timeout__ = integer (timeout in milliseconds) __Limit__ = integer @@ -118,12 +115,12 @@ __Orderby__ = [{Key, Direction}] __Direction__ = 1 (Asc) | -1 (Desc) __Fields__ = [Key] = specifies a list of fields to return in the result set __response_options__ = return #response{header, response_flag, cursor_id, offset, limit, documents} -__Result__ = [Document] | response() - - emongo:find_all(PoolName, CollectionName) -> Result - emongo:find_all(PoolName, CollectionName, Selector) -> Result - emongo:find_all(PoolName, CollectionName, Selector, Options) -> Result - +__Result__ = [Document] | response() + + emongo:find(PoolId, CollectionName) -> Result + emongo:find(PoolId, CollectionName, Selector) -> Result + emongo:find(PoolId, CollectionName, Selector, Options) -> Result + ### Examples __limit, offset, timeout, orderby, fields__ @@ -132,81 +129,66 @@ __limit, offset, timeout, orderby, fields__ %% limit the number of results to 100 and offset the first document 10 documents from the beginning %% return documents in ascending order, sorted by the value of field1 %% limit the fields in the return documents to field1 (the _id field is always included in the results) - emongo:find_all(test, "collection", [{"field1", 1}], [{limit, 100}, {offset, 10}, {timeout, 5000}, {orderby, [{"field1", asc}]}, {fields, ["field1"]}]). - + emongo:find(test, "collection", [{"field1", 1}], [{limit, 100}, {offset, 10}, {timeout, 5000}, {orderby, [{"field1", asc}]}, {fields, ["field1"]}]). + __great than, less than, great than or equal, less than or equal__ %% find documents where field1 is greater than 5 and less than 10 - emongo:find_all(test, "collection", [{"field1", [{gt, 5}, {lt, 10}]}]). - + emongo:find(test, "collection", [{"field1", [{gt, 5}, {lt, 10}]}]). + %% find documents where field1 is greater than or equal to 5 and less than or equal to 10 - emongo:find_all(test, "collection", [{"field1", [{gte, 5}, {lte, 10}]}]). - + emongo:find(test, "collection", [{"field1", [{gte, 5}, {lte, 10}]}]). + %% find documents where field1 is greater than 5 and less than 10 - emongo:find_all(test, "collection", [{"field1", [{'>', 5}, {'<', 10}]}]). - + emongo:find(test, "collection", [{"field1", [{'>', 5}, {'<', 10}]}]). + %% find documents where field1 is greater than or equal to 5 and less than or equal to 10 - emongo:find_all(test, "collection", [{"field1", [{'>=', 5}, {'=<', 10}]}]). - + emongo:find(test, "collection", [{"field1", [{'>=', 5}, {'=<', 10}]}]). + __not equal__ %% find documents where field1 is not equal to 5 or 10 - emongo:find_all(test, "collection", [{"field1", [{ne, 5}, {ne, 10}]}]). - + emongo:find(test, "collection", [{"field1", [{ne, 5}, {ne, 10}]}]). + %% find documents where field1 is not equal to 5 - emongo:find_all(test, "collection", [{"field1", [{'=/=', 5}]}]). - + emongo:find(test, "collection", [{"field1", [{'=/=', 5}]}]). + %% find documents where field1 is not equal to 5 - emongo:find_all(test, "collection", [{"field1", [{'/=', 5}]}]). - + emongo:find(test, "collection", [{"field1", [{'/=', 5}]}]). + __in__ %% find documents where the value of field1 is one of the values in the list [1,2,3,4,5] - emongo:find_all(test, "collection", [{"field1", [{in, [1,2,3,4,5]}]}]). + emongo:find(test, "collection", [{"field1", [{in, [1,2,3,4,5]}]}]). __not in__ - + %% find documents where the value of field1 is NOT one of the values in the list [1,2,3,4,5] - emongo:find_all(test, "collection", [{"field1", [{nin, [1,2,3,4,5]}]}]). - + emongo:find(test, "collection", [{"field1", [{nin, [1,2,3,4,5]}]}]). + __all__ %% find documents where the value of field1 is an array and contains all of the values in the list [1,2,3,4,5] - emongo:find_all(test, "collection", [{"field1", [{all, [1,2,3,4,5]}]}]). - + emongo:find(test, "collection", [{"field1", [{all, [1,2,3,4,5]}]}]). + __size__ %% find documents where the value of field1 is an array of size 10 - emongo:find_all(test, "collection", [{"field1", [{size, 10}]}]). - + emongo:find(test, "collection", [{"field1", [{size, 10}]}]). + __exists__ %% find documents where field1 exists - emongo:find_all(test, "collection", [{"field1", [{exists, true}]}]). - + emongo:find(test, "collection", [{"field1", [{exists, true}]}]). + __where__ %% find documents where the value of field1 is greater than 10 - emongo:find_all(test, "collection", [{where, "this.field1 > 10"}]). - + emongo:find(test, "collection", [{where, "this.field1 > 10"}]). + __nested queries__ - %% find documents with an address field containing a sub-document + %% find documents with an address field containing a sub-document %% with street equal to "Maple Drive". %% ie: [{"address", [{"street", "Maple Drive"}, {"zip", 94114}] - emongo:find_all(test, "people", [{"address.street", "Maple Drive"}]). - -## Drop database - - %% drop current database - emongo:drop_database(PoolName) -> ok - -## Tests - -Ensure you have [etap](https://github.com/ngerakines/etap). - - git clone https://github.com/ngerakines/etap.git - cd etap && make && cd .. - export ERL_LIBS="etap" - - make test + emongo:find(test, "people", [{"address.street", "Maple Drive"}]). \ No newline at end of file diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index ded2c07..0000000 --- a/debian/changelog +++ /dev/null @@ -1,12 +0,0 @@ -emongo (0.2.2-1) unstable; urgency=low - - * Added unique index support to ensure_index. - - -- Oleg Smirnov Wed, 17 Aug 2011 19:25:16 +0300 - -emongo (0.2.1-1) unstable; urgency=low - - * initial prerelease - - -- Oleg Smirnov Sat, 06 Aug 2011 14:58:22 +0300 - diff --git a/debian/compat b/debian/compat deleted file mode 100644 index 7f8f011..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -7 diff --git a/debian/control b/debian/control deleted file mode 100644 index 3802da5..0000000 --- a/debian/control +++ /dev/null @@ -1,13 +0,0 @@ -Source: emongo -Section: libs -Priority: extra -Maintainer: Oleg Smirnov -Build-Depends: debhelper (>= 7.3~), erlang-base -Standards-Version: 3.9.1 -Homepage: https://github.com/master/emongo - -Package: emongo -Architecture: any -Depends: erlang -Suggests: mongodb -Description: the most Emo of mongo drivers diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 14f8a9e..0000000 --- a/debian/copyright +++ /dev/null @@ -1 +0,0 @@ -First debianized by Oleg Smirnov on Sat, 06 Aug 2011 14:58:22 +0300. diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 328323e..0000000 --- a/debian/rules +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/make -f -%: - dh $@ - -override_dh_auto_test: diff --git a/ebin/emongo.appup b/ebin/emongo.appup deleted file mode 100644 index c6b184d..0000000 --- a/ebin/emongo.appup +++ /dev/null @@ -1,30 +0,0 @@ -{"0.2.2", - [{"0.0.5", [{load_module, emongo}, - {load_module, emongo_packet}]}], - [{"0.0.5", [{load_module, emongo}, - {load_module, emongo_packet}]}], -}. -{"0.0.5", - [{"0.0.4", [ - {add_module, pqueue}, - {load_module, emongo_sup}, - {load_module, emongo_server}, - {load_module, emongo_router}, - {load_module, emongo_bson}, - {load_module, emongo}, - {update, emongo_pool, {advanced, []}}, - {delete_module, emongo_collection} - ]} - ], - [{"0.0.4", [ - {delete_module, pqueue}, - {load_module, emongo_sup}, - {load_module, emongo_server}, - {load_module, emongo_router}, - {load_module, emongo_bson}, - {load_module, emongo}, - {load_module, emongo_pool}, - {add_module, emongo_collection} - ]} - ] -}. diff --git a/include/emongo.hrl b/include/emongo.hrl index 55a152b..3b6fa0a 100644 --- a/include/emongo.hrl +++ b/include/emongo.hrl @@ -1,15 +1,22 @@ +-include_lib("emongo_public.hrl"). + +-record(pool, {id, host, port, database, size=1, conn_pids=queue:new(), req_id=1}). -record(header, {message_length, request_id, response_to, op_code}). --record(response, { - header, - response_flag, - cursor_id, - offset, - limit, - documents, - pool_id - }). -record(emo_query, {opts=[], offset=0, limit=0, q=[], field_selector=[]}). +-define(IS_DOCUMENT(Doc), (is_list(Doc) andalso (Doc == [] orelse (is_tuple(hd(Doc)) andalso tuple_size(hd(Doc)) == 2)))). +-define(IS_LIST_OF_DOCUMENTS(Docs), ( + is_list(Docs) andalso ( + Docs == [] orelse ( + is_list(hd(Docs)) andalso ( + hd(Docs) == [] orelse ( + is_tuple(hd(hd(Docs))) andalso + tuple_size(hd(hd(Docs))) == 2 + ) + ) + ) + ))). + -define(TIMEOUT, 5000). -define(OP_REPLY, 1). @@ -20,10 +27,3 @@ -define(OP_GET_MORE, 2005). -define(OP_DELETE, 2006). -define(OP_KILL_CURSORS, 2007). - --define(TAILABLE_CURSOR, 2). --define(SLAVE_OK, 4). --define(OPLOG, 8). --define(NO_CURSOR_TIMEOUT, 16). - --define(DUPLICATE_KEY_ERROR, 11000). diff --git a/priv/erlang-emongo.spec b/priv/erlang-emongo.spec deleted file mode 100644 index 514c3cb..0000000 --- a/priv/erlang-emongo.spec +++ /dev/null @@ -1,65 +0,0 @@ -%define realname emongo -Name: erlang-%{realname} -Version: 0.2.1 -Release: 1%{?dist} -Summary: the most Emo of mongo drivers -Group: Development/Languages -License: MIT -URL: https://github.com/master/emongo -Source0: http://cloud.github.com/downloads/master/%{realname}/%{realname}-%{version}.tar.gz -BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) -BuildRequires: erlang -%if 0%{?el4}%{?el5}%{?fc11} -Requires: erlang -%else -Requires: erlang-erts -Requires: erlang-kernel -Requires: erlang-stdlib -%endif -Provides: %{realname} = %{version}-%{release} - - -%description -the most Emo of mongo drivers - - -%prep -%setup -q -n %{realname}-%{version} - - -%build -make %{?_smp_mflags} - -%install -rm -rf $RPM_BUILD_ROOT -mkdir -p $RPM_BUILD_ROOT%{_libdir}/erlang/lib/%{realname}-%{version}/ebin -mkdir -p $RPM_BUILD_ROOT%{_libdir}/erlang/lib/%{realname}-%{version}/include -for i in ebin/*.beam include/*.hrl ebin/*.app ebin/*.appup; do install $i $RPM_BUILD_ROOT%{_libdir}/erlang/lib/%{realname}-%{version}/$i ; done - -%clean -rm -rf $RPM_BUILD_ROOT - -%files -%defattr(-,root,root,-) -%doc README.markdown -%dir %{_libdir}/erlang/lib/%{realname}-%{version} -%dir %{_libdir}/erlang/lib/%{realname}-%{version}/ebin -%dir %{_libdir}/erlang/lib/%{realname}-%{version}/include -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/emongo.app -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/emongo_app.beam -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/emongo.appup -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/emongo.beam -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/emongo_bson.beam -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/emongo_packet.beam -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/emongo_pool.beam -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/emongo_router.beam -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/emongo_server.beam -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/emongo_sup.beam -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/pqueue.beam -%{_libdir}/erlang/lib/%{realname}-%{version}/include/emongo.hrl - - -%changelog - -* Thu Mar 24 2011 Oleg Smirnov 0.2.1-1 -- Initial package diff --git a/priv/example.config b/priv/example.config index c326de8..c5d41e5 100644 --- a/priv/example.config +++ b/priv/example.config @@ -5,12 +5,6 @@ {host, "localhost"}, {port, 27017}, {database, "testdatabase"} - ]}, - {test2, [ - {size, 10}, - {host, "localhost"}, - {port, 27017}, - {database, "testdatabase"} ]} ]} -]}]. +]}]. \ No newline at end of file diff --git a/src/emongo.app.src b/src/emongo.app.src deleted file mode 100644 index 3ab183a..0000000 --- a/src/emongo.app.src +++ /dev/null @@ -1,11 +0,0 @@ -{application, emongo, [ - {description, "Erlang MongoDB Driver"}, - {vsn, "0.2.2"}, - {modules, [ - emongo, emongo_app, emongo_sup, emongo_bson, emongo_packet, - emongo_server, emongo_pool, emongo_router, pqueue - ]}, - {registered, [emongo_sup, emongo]}, - {mod, {emongo_app, []}}, - {applications, [kernel, stdlib]} -]}. diff --git a/src/emongo.erl b/src/emongo.erl index 11eeae5..3e120ca 100644 --- a/src/emongo.erl +++ b/src/emongo.erl @@ -1,7 +1,4 @@ %% Copyright (c) 2009 Jacob Vorreuter -%% Jacob Perkins -%% Belyaev Dmitry -%% François de Metz %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation @@ -26,43 +23,29 @@ -module(emongo). -behaviour(gen_server). --export([pools/0, oid/0, add_pool/5, add_pool/6, del_pool/1]). - --export([fold_all/6, - find_all/2, find_all/3, find_all/4, - find_one/3, find_one/4, - find_and_modify/5]). - --export([insert/3, update/4, update/5, update/6, delete/2, delete/3]). - --export([ensure_index/3, ensure_index/4, count/2, count/3, distinct/3, - distinct/4]). - --export([dec2hex/1, hex2dec/1]). - --export([sequence/2, synchronous/0, no_response/0, - find_all_seq/3, fold_all_seq/5, - insert_seq/3, update_seq/6, delete_seq/3]). - --export([update_sync/5, update_sync/6, delete_sync/3, insert_sync/3]). - --export([drop_database/1]). - --deprecated([update_sync/5, delete_sync/3]). - -%% internal -export([start_link/0, init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3]). + +-export([pools/0, oid/0, oid_generation_time/1, add_pool/5, remove_pool/1, + auth/3, find_all/2, find_all/3, find_all/4, + get_more/4, get_more/5, find_one/3, find_one/4, kill_cursors/2, + insert/3, insert_sync/3, insert_sync/4, update/4, update/5, + update_all/4, update_sync/4, update_sync/5, update_sync/6, + update_all_sync/4, update_all_sync/5, delete/2, delete/3, + delete_sync/2, delete_sync/3, delete_sync/4, ensure_index/3, count/2, + count/3, count/4, find_and_modify/4, find_and_modify/5, dec2hex/1, + hex2dec/1]). -include("emongo.hrl"). --record(state, {oid_index, hashed_hostn}). +-record(state, {pools, oid_index, hashed_hostn}). %%==================================================================== %% Types %%==================================================================== %% pool_id() = atom() %% collection() = string() +%% response() = {response, header, response_flag, cursor_id, offset, limit, documents} %% documents() = [document()] %% document() = [{term(), term()}] @@ -74,282 +57,334 @@ %% Description: Starts the server %%-------------------------------------------------------------------- start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). pools() -> - emongo_sup:pools(). + gen_server:call(?MODULE, pools, infinity). oid() -> - gen_server:call(?MODULE, oid, infinity). + gen_server:call(?MODULE, oid, infinity). -add_pool(PoolId, Host, Port, Database, Size) -> - emongo_sup:start_pool(PoolId, Host, Port, Database, Size). - -add_pool(PoolId, Host, Port, Database, Size, AutoReconnect) -> - emongo_sup:start_pool(PoolId, Host, Port, Database, Size, AutoReconnect). +oid_generation_time({oid, Oid}) -> + oid_generation_time(Oid); +oid_generation_time(Oid) when is_binary(Oid) andalso size(Oid) =:= 12 -> + <> = Oid, + UnixTime. -del_pool(PoolId) -> - emongo_sup:stop_pool(PoolId). +add_pool(PoolId, Host, Port, Database, Size) -> + gen_server:call(?MODULE, {add_pool, PoolId, Host, Port, Database, Size}, infinity). +remove_pool(PoolId) -> + gen_server:call(?MODULE, {remove_pool, PoolId}). %%------------------------------------------------------------------------------ -%% sequences of operations +%% authenticate %%------------------------------------------------------------------------------ - -sequence(_PoolId, []) -> - ok; -sequence(PoolId, Sequence) -> - {Pid, Database, ReqId} = get_pid_pool(PoolId, length(Sequence)), - sequence(Sequence, Pid, Database, ReqId). - - -sequence([Operation|Tail], Pid, Database, ReqId) -> - Result = Operation(Pid, Database, ReqId), - case Tail of - [] -> Result; - _ -> sequence(Tail, Pid, Database, ReqId + 1) +auth(PoolId, User, Pass) -> + % Authentication is a two step process. First be must run getnonce command to get + % nonce that we are going to use in step two. We need to authenticate also every + % connection. + Pools = pools(), + {Pool, _} = get_pool(PoolId, Pools), + PoolPids = queue:to_list(Pool#pool.conn_pids), + + F = fun(Pid) -> + case getnonce(Pid, Pool) of + error -> + throw(getnonce); + Nonce -> + do_auth(Nonce, Pid, Pool, User, Pass) + end + end, + lists:foreach(F, PoolPids). + +do_auth(Nonce, Pid, Pool, User, Pass) -> + Hash = emongo:dec2hex(erlang:md5(User ++ ":mongo:" ++ Pass)), + Digest = emongo:dec2hex(erlang:md5(binary_to_list(Nonce) ++ User ++ Hash)), + Query = #emo_query{q=[{<<"authenticate">>, 1}, {<<"user">>, User}, {<<"nonce">>, Nonce}, {<<"key">>, Digest}], limit=1}, + Packet = emongo_packet:do_query(Pool#pool.database, "$cmd", Pool#pool.req_id, Query), + + Resp = emongo_conn:send_recv(Pid, Pool#pool.req_id, Packet, ?TIMEOUT), + case lists:nth(1, Resp#response.documents) of + [{<<"ok">>, 1.0}] -> + {ok, authenticated}; + L -> + case lists:keyfind(<<"errmsg">>, 1, L) of + false -> + throw(no_error_message); + {<<"errmsg">>, Error} -> + throw(Error) + end end. +getnonce(Pid, Pool) -> + Query1 = #emo_query{q=[{<<"getnonce">>, 1}], limit=1}, + Packet = emongo_packet:do_query(Pool#pool.database, "$cmd", Pool#pool.req_id, Query1), + Resp1 = emongo_conn:send_recv(Pid, Pool#pool.req_id, Packet, ?TIMEOUT), + case lists:keyfind(<<"nonce">>, 1, lists:nth(1, Resp1#response.documents)) of + false -> + error; + {<<"nonce">>, Nonce} -> + Nonce + end. -synchronous() -> - synchronous(?TIMEOUT). - -synchronous(Timeout) -> - [fun(_, _, _) -> ok end, - fun(Pid, Database, ReqId) -> - PacketGetLastError = emongo_packet:get_last_error(Database, ReqId), - Resp = emongo_server:send_recv(Pid, ReqId, PacketGetLastError, Timeout), - Resp#response.documents - end]. +%%------------------------------------------------------------------------------ +%% find +%%------------------------------------------------------------------------------ +%find(PoolId, Collection) -> +% find(PoolId, Collection, [], [{timeout, ?TIMEOUT}]). -no_response() -> - []. +%find(PoolId, Collection, Selector) when ?IS_DOCUMENT(Selector) -> +% find(PoolId, Collection, Selector, [{timeout, ?TIMEOUT}]); +%%% this function has been deprecated +%find(PoolId, Collection, Query) when is_record(Query, emo_query) -> +% {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), +% Packet = emongo_packet:do_query(Pool#pool.database, Collection, Pool#pool.req_id, Query), +% emongo_conn:send_recv(Pid, Pool#pool.req_id, Packet, ?TIMEOUT). %% @spec find(PoolId, Collection, Selector, Options) -> Result -%% PoolId = atom() -%% Collection = string() -%% Selector = document() -%% Options = [Option] -%% Option = {timeout, Timeout} | {limit, Limit} | {offset, Offset} | {orderby, Orderby} | {fields, Fields} | explain -%% Timeout = integer (timeout in milliseconds) -%% Limit = integer -%% Offset = integer -%% Orderby = [{Key, Direction}] -%% Key = string() | binary() | atom() | integer() -%% Direction = asc | desc -%% Fields = [Field] -%% Field = string() | binary() | atom() | integer() = specifies a field to return in the result set -%% response_options = return {response, header, response_flag, cursor_id, offset, limit, documents} -%% Result = documents() | response() +%% PoolId = atom() +%% Collection = string() +%% Selector = document() +%% Options = [Option] +%% Option = {timeout, Timeout} | {limit, Limit} | {offset, Offset} | {orderby, Orderby} | {fields, Fields} | response_options +%% Timeout = integer (timeout in milliseconds) +%% Limit = integer +%% Offset = integer +%% Orderby = [{Key, Direction}] +%% Key = string() | binary() | atom() | integer() +%% Direction = asc | desc +%% Fields = [Field] +%% Field = string() | binary() | atom() | integer() = specifies a field to return in the result set +%% response_options = return {response, header, response_flag, cursor_id, offset, limit, documents} +%% Result = documents() | response() +find(PoolId, Collection, Selector, Options) when ?IS_DOCUMENT(Selector), is_list(Options) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Query = create_query(Options, Selector), + Packet = emongo_packet:do_query(Pool#pool.database, Collection, Pool#pool.req_id, Query), + Resp = emongo_conn:send_recv(Pid, Pool#pool.req_id, Packet, proplists:get_value(timeout, Options, ?TIMEOUT)), + case lists:member(response_options, Options) of + true -> Resp; + false -> Resp#response.documents + end. %%------------------------------------------------------------------------------ %% find_all %%------------------------------------------------------------------------------ find_all(PoolId, Collection) -> - find_all(PoolId, Collection, [], []). - -find_all(PoolId, Collection, Selector) -> - find_all(PoolId, Collection, Selector, []). + find_all(PoolId, Collection, [], [{timeout, ?TIMEOUT}]). -find_all(PoolId, Collection, Selector, Options) -> - sequence(PoolId, find_all_seq(Collection, Selector, Options)). +find_all(PoolId, Collection, Selector) when ?IS_DOCUMENT(Selector) -> + find_all(PoolId, Collection, Selector, [{timeout, ?TIMEOUT}]). +find_all(PoolId, Collection, Selector, Options) when ?IS_DOCUMENT(Selector), is_list(Options) -> + Resp = find(PoolId, Collection, Selector, [response_options|Options]), + find_all(PoolId, Collection, Selector, Options, Resp). -find_all_seq(Collection, Selector, Options) -> - [Fun0,Fun1] = fold_all_seq(fun(I, A) -> [I | A] end, [], Collection, Selector, Options), +find_all(_PoolId, _Collection, _Selector, Options, Resp) when is_record(Resp, response), Resp#response.cursor_id == 0 -> + case lists:member(response_options, Options) of + true -> Resp; + false -> Resp#response.documents + end; - [Fun0, - fun(Pid, Database, ReqId) -> - lists:reverse(Fun1(Pid, Database, ReqId)) - end]. +find_all(PoolId, Collection, Selector, Options, Resp) when is_record(Resp, response) -> + Resp1 = get_more(PoolId, Collection, Resp#response.cursor_id, proplists:get_value(timeout, Options, ?TIMEOUT)), + Documents = lists:append(Resp#response.documents, Resp1#response.documents), + find_all(PoolId, Collection, Selector, Options, Resp1#response{documents=Documents}). %%------------------------------------------------------------------------------ -%% find_and_modify +%% find_one %%------------------------------------------------------------------------------ -find_and_modify(PoolId, Collection, Selector, Update, Options) -> - Selector1 = transform_selector(Selector), - Collection1 = unicode:characters_to_binary(Collection), - OptionsDoc = fam_options(Options, [{<<"query">>, Selector1}, - {<<"update">>, Update}]), - Query = #emo_query{q=[{<<"findandmodify">>, Collection1} | OptionsDoc], - limit=1}, - {Pid, Database, ReqId} = get_pid_pool(PoolId, 1), - Packet = emongo_packet:do_query(Database, "$cmd", - ReqId, Query), - Resp = emongo_server:send_recv(Pid, ReqId, Packet, - proplists:get_value(timeout, Options, ?TIMEOUT)), - case lists:member(response_options, Options) of - true -> Resp; - false -> Resp#response.documents - end. +find_one(PoolId, Collection, Selector) when ?IS_DOCUMENT(Selector) -> + find_one(PoolId, Collection, Selector, [{timeout, ?TIMEOUT}]). + +find_one(PoolId, Collection, Selector, Options) when ?IS_DOCUMENT(Selector), is_list(Options) -> + Options1 = [{limit, 1} | lists:keydelete(limit, 1, Options)], + find(PoolId, Collection, Selector, Options1). %%------------------------------------------------------------------------------ -%% fold_all +%% get_more %%------------------------------------------------------------------------------ -fold_all(F, Value, PoolId, Collection, Selector, Options) -> - sequence(PoolId, fold_all_seq(F, Value, Collection, Selector, Options)). - - -fold_all_seq(F, Value, Collection, Selector, Options) -> - Timeout = proplists:get_value(timeout, Options, ?TIMEOUT), - Query = create_query(Options, Selector), - [fun(_, _, _) -> ok end, - fun(Pid, Database, ReqId) -> - Packet = emongo_packet:do_query(Database, Collection, ReqId, Query), - Resp = emongo_server:send_recv(Pid, ReqId, Packet, Timeout), - - NewValue = fold_documents(F, Value, Resp), - case Query#emo_query.limit of - 0 -> - fold_more(F, NewValue, Collection, Resp#response{documents=[]}, Timeout); - _ -> - kill_cursor(Resp#response.pool_id, Resp#response.cursor_id), - NewValue - end - end]. +get_more(PoolId, Collection, CursorID, Timeout) -> + get_more(PoolId, Collection, CursorID, 0, Timeout). -fold_more(_F, Value, _Collection, #response{cursor_id=0}, _Timeout) -> - Value; - -fold_more(F, Value, Collection, #response{pool_id=PoolId, cursor_id=CursorID}, Timeout) -> - {Pid, Database, ReqId} = get_pid_pool(PoolId, 2), - Packet = emongo_packet:get_more(Database, Collection, ReqId, 0, CursorID), - Resp1 = emongo_server:send_recv(Pid, ReqId, Packet, Timeout), - - NewValue = fold_documents(F, Value, Resp1), - fold_more(F, NewValue, Collection, Resp1#response{documents=[]}, Timeout). +get_more(PoolId, Collection, CursorID, NumToReturn, Timeout) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet = emongo_packet:get_more(Pool#pool.database, Collection, Pool#pool.req_id, NumToReturn, CursorID), + emongo_conn:send_recv(Pid, Pool#pool.req_id, Packet, Timeout). %%------------------------------------------------------------------------------ -%% find_one +%% kill_cursors %%------------------------------------------------------------------------------ -find_one(PoolId, Collection, Selector) -> - find_one(PoolId, Collection, Selector, []). +kill_cursors(PoolId, CursorID) when is_integer(CursorID) -> + kill_cursors(PoolId, [CursorID]); -find_one(PoolId, Collection, Selector, Options) -> - Options1 = [{limit, 1} | lists:keydelete(limit, 1, Options)], - find_all(PoolId, Collection, Selector, Options1). +kill_cursors(PoolId, CursorIDs) when is_list(CursorIDs) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet = emongo_packet:kill_cursors(Pool#pool.req_id, CursorIDs), + emongo_conn:send(Pid, Pool#pool.req_id, Packet). %%------------------------------------------------------------------------------ %% insert %%------------------------------------------------------------------------------ -insert(PoolId, Collection, Documents) -> - sequence(PoolId, insert_seq(Collection, Documents, no_response())). - -insert_seq(Collection, [[_|_]|_]=Documents, Next) -> - [fun(Pid, Database, ReqId) -> - Packet = emongo_packet:insert(Database, Collection, ReqId, Documents), - emongo_server:send(Pid, Packet) - end | Next]; -insert_seq(Collection, Document, Next) -> - insert_seq(Collection, [Document], Next). +insert(PoolId, Collection, Document) when ?IS_DOCUMENT(Document) -> + insert(PoolId, Collection, [Document]); +insert(PoolId, Collection, Documents) when ?IS_LIST_OF_DOCUMENTS(Documents) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet = emongo_packet:insert(Pool#pool.database, Collection, Pool#pool.req_id, Documents), + emongo_conn:send(Pid, Pool#pool.req_id, Packet). +%%------------------------------------------------------------------------------ +%% insert_sync that runs db.$cmd.findOne({getlasterror: 1}); +%%------------------------------------------------------------------------------ insert_sync(PoolId, Collection, Documents) -> - sequence(PoolId, insert_seq(Collection, Documents, synchronous())). + insert_sync(PoolId, Collection, Documents, []). + +insert_sync(PoolId, Collection, DocumentsIn, Options) -> + Documents = if + ?IS_LIST_OF_DOCUMENTS(DocumentsIn) -> DocumentsIn; + ?IS_DOCUMENT(DocumentsIn) -> [DocumentsIn] + end, + + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet1 = emongo_packet:insert(Pool#pool.database, Collection, Pool#pool.req_id, Documents), + sync_command({Pid, Pool}, Packet1, Options). %%------------------------------------------------------------------------------ %% update %%------------------------------------------------------------------------------ -update(PoolId, Collection, Selector, Document) -> - update(PoolId, Collection, Selector, Document, false). +update(PoolId, Collection, Selector, Document) when ?IS_DOCUMENT(Selector), ?IS_DOCUMENT(Document) -> + update(PoolId, Collection, Selector, Document, false). -update(PoolId, Collection, Selector, Document, Upsert) -> - update(PoolId, Collection, Selector, Document, Upsert, false). +update(PoolId, Collection, Selector, Document, Upsert) when ?IS_DOCUMENT(Selector), ?IS_DOCUMENT(Document) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet = emongo_packet:update(Pool#pool.database, Collection, Pool#pool.req_id, Upsert, false, Selector, Document), + emongo_conn:send(Pid, Pool#pool.req_id, Packet). -update(PoolId, Collection, Selector, Document, Upsert, MultiUpdate) -> - sequence(PoolId, update_seq(Collection, Selector, Document, Upsert, - MultiUpdate, no_response())). +%%------------------------------------------------------------------------------ +%% update_all +%%------------------------------------------------------------------------------ +update_all(PoolId, Collection, Selector, Document) when ?IS_DOCUMENT(Selector), ?IS_DOCUMENT(Document) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet = emongo_packet:update(Pool#pool.database, Collection, Pool#pool.req_id, false, true, Selector, Document), + emongo_conn:send(Pid, Pool#pool.req_id, Packet). -update_seq(Collection, Selector, Document, Upsert, MultiUpdate, Next) -> - [fun(Pid, Database, ReqId) -> - Packet = emongo_packet:update(Database, Collection, ReqId, Upsert, - MultiUpdate, - transform_selector(Selector), Document), - emongo_server:send(Pid, Packet) - end | Next]. +%%------------------------------------------------------------------------------ +%% update_sync that runs db.$cmd.findOne({getlasterror: 1}); +%%------------------------------------------------------------------------------ +update_sync(PoolId, Collection, Selector, Document) when ?IS_DOCUMENT(Selector), ?IS_DOCUMENT(Document) -> + update_sync(PoolId, Collection, Selector, Document, false). +update_sync(PoolId, Collection, Selector, Document, Upsert) when ?IS_DOCUMENT(Selector), ?IS_DOCUMENT(Document) -> + update_sync(PoolId, Collection, Selector, Document, Upsert, []). -update_sync(PoolId, Collection, Selector, Document, Upsert) -> - update_sync(PoolId, Collection, Selector, Document, Upsert, false). +update_sync(PoolId, Collection, Selector, Document, Upsert, Options) when ?IS_DOCUMENT(Selector), ?IS_DOCUMENT(Document) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet1 = emongo_packet:update(Pool#pool.database, Collection, Pool#pool.req_id, Upsert, false, Selector, Document), + sync_command({Pid, Pool}, Packet1, Options). -update_sync(PoolId, Collection, Selector, Document, Upsert, MultiUpdate) -> - sequence(PoolId, update_seq(Collection, Selector, Document, Upsert, MultiUpdate, synchronous())). +%%------------------------------------------------------------------------------ +%% update_all_sync that runs db.$cmd.findOne({getlasterror: 1}); +%%------------------------------------------------------------------------------ +update_all_sync(PoolId, Collection, Selector, Document) when ?IS_DOCUMENT(Selector), ?IS_DOCUMENT(Document) -> + update_all_sync(PoolId, Collection, Selector, Document, []). + +update_all_sync(PoolId, Collection, Selector, Document, Options) when ?IS_DOCUMENT(Selector), ?IS_DOCUMENT(Document) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet1 = emongo_packet:update(Pool#pool.database, Collection, Pool#pool.req_id, false, true, Selector, Document), + % We could check <<"n">> as the update_sync(...) functions do, but + % update_all_sync(...) isn't targeting a specific number of documents, so 0 + % updates is legitimate. + sync_command({Pid, Pool}, Packet1, Options). %%------------------------------------------------------------------------------ %% delete %%------------------------------------------------------------------------------ delete(PoolId, Collection) -> - delete(PoolId, Collection, []). + delete(PoolId, Collection, []). delete(PoolId, Collection, Selector) -> - sequence(PoolId, delete_seq(Collection, Selector, no_response())). - - -delete_seq(Collection, Selector, Next) -> - [fun(Pid, Database, ReqId) -> - Packet = emongo_packet:delete(Database, Collection, ReqId, transform_selector(Selector)), - emongo_server:send(Pid, Packet) - end | Next]. + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet = emongo_packet:delete(Pool#pool.database, Collection, Pool#pool.req_id, transform_selector(Selector)), + emongo_conn:send(Pid, Pool#pool.req_id, Packet). +%%------------------------------------------------------------------------------ +%% delete_sync that runs db.$cmd.findOne({getlasterror: 1}); +%%------------------------------------------------------------------------------ +delete_sync(PoolId, Collection) -> + delete_sync(PoolId, Collection, []). delete_sync(PoolId, Collection, Selector) -> - sequence(PoolId, delete_seq(Collection, Selector, synchronous())). + delete_sync(PoolId, Collection, Selector, []). +delete_sync(PoolId, Collection, Selector, Options) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet1 = emongo_packet:delete(Pool#pool.database, Collection, Pool#pool.req_id, transform_selector(Selector)), + sync_command({Pid, Pool}, Packet1, Options). %%------------------------------------------------------------------------------ %% ensure index %%------------------------------------------------------------------------------ -%% ensure_index(pool, "collection", [{"fieldname1", 1}, {"fieldname2", -1}]). -ensure_index(PoolId, Collection, Keys) -> - ensure_index(PoolId, Collection, Keys, false). - -ensure_index(PoolId, Collection, Keys, Unique) -> - {Pid, Database, ReqId} = get_pid_pool(PoolId, 1), - Packet = emongo_packet:ensure_index(Database, Collection, ReqId, Keys, Unique), - emongo_server:send(Pid, Packet). - - -count(PoolId, Collection) -> count(PoolId, Collection, []). +ensure_index(PoolId, Collection, Keys) when ?IS_DOCUMENT(Keys)-> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Packet = emongo_packet:ensure_index(Pool#pool.database, Collection, Pool#pool.req_id, Keys), + emongo_conn:send(Pid, Pool#pool.req_id, Packet). +%%------------------------------------------------------------------------------ +%% count +%%------------------------------------------------------------------------------ +count(PoolId, Collection) -> + count(PoolId, Collection, [], []). count(PoolId, Collection, Selector) -> - {Pid, Database, ReqId} = get_pid_pool(PoolId, 2), - Q = [{<<"count">>, Collection}, {<<"ns">>, Database}, - {<<"query">>, transform_selector(Selector)}], - Query = #emo_query{q=Q, limit=1}, - Packet = emongo_packet:do_query(Database, "$cmd", ReqId, Query), - case emongo_server:send_recv(Pid, ReqId, Packet, ?TIMEOUT) of - #response{documents=[[{<<"n">>,Count}|_]]} -> - round(Count); - _ -> - undefined - end. - - -distinct(PoolId, Collection, Key) -> distinct(PoolId, Collection, Key, []). - -distinct(PoolId, Collection, Key, Selector) -> - {Pid, Database, ReqId} = get_pid_pool(PoolId, 2), - Q = [{<<"distinct">>, Collection}, {<<"key">>, Key}, {<<"ns">>, Database}, - {<<"query">>, transform_selector(Selector)}], - Query = #emo_query{q=Q, limit=1}, - Packet = emongo_packet:do_query(Database, "$cmd", ReqId, Query), - case emongo_server:send_recv(Pid, ReqId, Packet, ?TIMEOUT) of - #response{documents=[[{<<"values">>, {array, Vals}} | _]]} -> - Vals; - _ -> - undefined - end. + count(PoolId, Collection, Selector, []). + +count(PoolId, Collection, Selector, Options) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Query = create_query([{<<"count">>, Collection}, {limit, 1} | Options], + Selector), + Packet = emongo_packet:do_query(Pool#pool.database, "$cmd", Pool#pool.req_id, + Query), + case emongo_conn:send_recv(Pid, Pool#pool.req_id, Packet, ?TIMEOUT) of + #response{documents=[[{<<"n">>,Count}|_]]} -> + round(Count); + _ -> + undefined + end. %%------------------------------------------------------------------------------ -%% drop database +%% find_and_modify %%------------------------------------------------------------------------------ -drop_database(PoolId) -> - {Pid, Database, ReqId} = get_pid_pool(PoolId, 1), - Query = #emo_query{q=[{<<"dropDatabase">>, 1}], limit=1}, - Packet = emongo_packet:do_query(Database, "$cmd", ReqId, Query), - emongo_server:send(Pid, ReqId, Packet). +find_and_modify(PoolId, Collection, Selector, Update) -> + find_and_modify(PoolId, Collection, Selector, Update, []). + +find_and_modify(PoolId, Collection, Selector, Update, Options) + when ?IS_DOCUMENT(Selector), ?IS_DOCUMENT(Update), is_list(Options) -> + {Pid, Pool} = gen_server:call(?MODULE, {pid, PoolId}, infinity), + Collection1 = to_binary(Collection), + Selector1 = transform_selector(Selector), + Fields = proplists:get_value(fields, Options, []), + FieldSelector = convert_fields(Fields), + Options1 = proplists:delete(fields, Options), + Options2 = [{to_binary(Opt), Val} || {Opt, Val} <- Options1], + Query = #emo_query{q = [{<<"findandmodify">>, Collection1}, + {<<"query">>, Selector1}, + {<<"update">>, Update}, + {<<"fields">>, FieldSelector} + | Options2], + limit = 1}, + Packet = emongo_packet:do_query(Pool#pool.database, "$cmd", Pool#pool.req_id, + Query), + Resp = emongo_conn:send_recv(Pid, Pool#pool.req_id, Packet, + proplists:get_value(timeout, Options, ?TIMEOUT)), + case lists:member(response_options, Options) of + true -> Resp; + false -> Resp#response.documents + end. + +%drop_collection(PoolId, Collection) when is_atom(PoolId), is_list(Collection) -> %%==================================================================== %% gen_server callbacks @@ -363,9 +398,11 @@ drop_database(PoolId) -> %% Description: Initiates the server %%-------------------------------------------------------------------- init(_) -> - {ok, HN} = inet:gethostname(), - <> = erlang:md5(HN), - {ok, #state{oid_index=1, hashed_hostn=HashedHN}}. + process_flag(trap_exit, true), + Pools = initialize_pools(), + {ok, HN} = inet:gethostname(), + <> = erlang:md5(HN), + {ok, #state{pools=Pools, oid_index=1, hashed_hostn=HashedHN}}. %%-------------------------------------------------------------------- %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | @@ -376,12 +413,60 @@ init(_) -> %% {stop, Reason, State} %% Description: Handling call messages %%-------------------------------------------------------------------- +handle_call(pools, _From, State) -> + {reply, State#state.pools, State}; + handle_call(oid, _From, State) -> - {Total_Wallclock_Time, _} = erlang:statistics(wall_clock), - Front = Total_Wallclock_Time rem 16#ffffffff, - <<_:20/binary,PID:2/binary,_/binary>> = term_to_binary(self()), - Index = State#state.oid_index rem 16#ffffff, - {reply, <>, State#state{oid_index = State#state.oid_index + 1}}; + {MegaSecs, Secs, _} = now(), + UnixTime = MegaSecs * 1000000 + Secs, + <<_:20/binary,PID:2/binary,_/binary>> = term_to_binary(self()), + Index = State#state.oid_index rem 16#ffffff, + {reply, <>, State#state{oid_index = State#state.oid_index + 1}}; + +handle_call({add_pool, PoolId, Host, Port, Database, Size}, _From, #state{pools=Pools}=State) -> + {Result, Pools1} = + case proplists:is_defined(PoolId, Pools) of + true -> + Pool = proplists:get_value(PoolId, Pools), + Pool1 = do_open_connections(Pool), + {ok, [{PoolId, Pool1}|proplists:delete(PoolId, Pools)]}; + false -> + Pool = #pool{ + id=PoolId, + host=Host, + port=Port, + database=Database, + size=Size + }, + Pool1 = do_open_connections(Pool), + {ok, [{PoolId, Pool1}|Pools]} + end, + {reply, Result, State#state{pools=Pools1}}; + +handle_call({remove_pool, PoolId}, _From, #state{pools=Pools}=State) -> + {Result, Pools1} = + case proplists:is_defined(PoolId, Pools) of + true -> + {ok, lists:keydelete(PoolId, 1, Pools)}; + false -> + {not_found, Pools} + end, + {reply, Result, State#state{pools=Pools1}}; + +handle_call({pid, PoolId}, _From, #state{pools=Pools}=State) -> + case get_pool(PoolId, Pools) of + undefined -> + {reply, {undefined, undefined}, State}; + {Pool, Others} -> + case queue:out(Pool#pool.conn_pids) of + {{value, Pid}, Q2} -> + Pool1 = Pool#pool{conn_pids = queue:in(Pid, Q2), req_id = ((Pool#pool.req_id)+1)}, + Pools1 = [{PoolId, Pool1}|Others], + {reply, {Pid, Pool}, State#state{pools=Pools1}}; + {empty, _} -> + {reply, {undefined, Pool}, State} + end + end; handle_call(_, _From, State) -> {reply, {error, invalid_call}, State}. @@ -400,8 +485,29 @@ handle_cast(_Msg, State) -> %% {stop, Reason, State} %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- -handle_info(_Info, State) -> - {noreply, State}. +handle_info({'EXIT', Pid, {emongo_conn, PoolId, Error}}, #state{pools=Pools}=State) -> + io:format("EXIT ~p, {emongo_conn, ~p, ~p} in ~p~n", + [Pid, PoolId, Error, ?MODULE]), + State1 = + case get_pool(PoolId, Pools) of + undefined -> + State; + {Pool, Others} -> + Pids1 = queue:filter(fun(Item) -> Item =/= Pid end, Pool#pool.conn_pids), + Pool1 = Pool#pool{conn_pids = Pids1}, + case do_open_connections(Pool1) of + {error, _Reason} -> + Pools1 = Others; + Pool2 -> + Pools1 = [{PoolId, Pool2}|Others] + end, + State#state{pools=Pools1} + end, + {noreply, State1}; + +handle_info(Info, State) -> + io:format("WARNING: unrecognized message in ~p: ~p~n", [?MODULE, Info]), + {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) -> void() @@ -423,155 +529,158 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -get_pid_pool(PoolId, RequestCount) -> - case emongo_sup:worker_pid(PoolId, emongo_sup:pools(), RequestCount) of - undefined -> throw(emongo_busy); - Val -> Val - end. - - -fold_documents(F, Value, Resp) -> - try - lists:foldl(F, Value, Resp#response.documents) - catch - Class:Exception -> - kill_cursor(Resp#response.pool_id, Resp#response.cursor_id), - erlang:Class(Exception) - end. - - -kill_cursor(_, 0) -> - ok; -kill_cursor(PoolId, CursorID) -> - {Pid, _Database, ReqId} = get_pid_pool(PoolId, 1), - Packet = emongo_packet:kill_cursors(ReqId, [CursorID]), - emongo_server:send(Pid, ReqId, Packet). - +initialize_pools() -> + case application:get_env(emongo, pools) of + undefined -> + []; + {ok, Pools} -> + [begin + Pool = #pool{ + id = PoolId, + size = proplists:get_value(size, Props, 1), + host = proplists:get_value(host, Props, "localhost"), + port = proplists:get_value(port, Props, 27017), + database = proplists:get_value(database, Props, "test") + }, + {PoolId, do_open_connections(Pool)} + end || {PoolId, Props} <- Pools] + end. + +do_open_connections(#pool{conn_pids=Pids, size=Size}=Pool) -> + case queue:len(Pids) < Size of + true -> + case emongo_conn:start_link(Pool#pool.id, Pool#pool.host, Pool#pool.port) of + {error, Reason} -> + throw({error, Reason}); + Pid -> + do_open_connections(Pool#pool{conn_pids = queue:in(Pid, Pids)}) + end; + false -> + Pool + end. + +get_pool(PoolId, Pools) -> + get_pool(PoolId, Pools, []). + +get_pool(_, [], _) -> + undefined; + +get_pool(PoolId, [{PoolId, Pool}|Tail], Others) -> + {Pool, lists:append(Tail, Others)}; + +get_pool(PoolId, [Pool|Tail], Others) -> + get_pool(PoolId, Tail, [Pool|Others]). dec2hex(Dec) -> - dec2hex(<<>>, Dec). + dec2hex(<<>>, Dec). dec2hex(N, <>) -> - dec2hex(<>, Rem); + dec2hex(<>, Rem); dec2hex(N,<<>>) -> - N. + N. hex2dec(Hex) when is_list(Hex) -> - hex2dec(list_to_binary(Hex)); + hex2dec(list_to_binary(Hex)); hex2dec(Hex) -> - hex2dec(<<>>, Hex). + hex2dec(<<>>, Hex). hex2dec(N,<>) -> - hex2dec(<>, Rem); + hex2dec(<>, Rem); hex2dec(N,<<>>) -> - N. + N. create_query(Options, Selector) -> - Selector1 = transform_selector(Selector), - create_query(Options, #emo_query{}, Selector1, []). + Selector1 = transform_selector(Selector), + create_query(Options, #emo_query{}, Selector1, []). create_query([], QueryRec, QueryDoc, []) -> - QueryRec#emo_query{q=QueryDoc}; + QueryRec#emo_query{q=QueryDoc}; create_query([], QueryRec, [], OptDoc) -> - QueryRec#emo_query{q=(OptDoc ++ [{<<"$query">>, [{none, none}]}])}; + QueryRec#emo_query{q=OptDoc}; create_query([], QueryRec, QueryDoc, OptDoc) -> - QueryRec#emo_query{q=(OptDoc ++ [{<<"$query">>, QueryDoc}])}; + QueryRec#emo_query{q=(OptDoc ++ [{<<"query">>, QueryDoc}])}; create_query([{limit, Limit}|Options], QueryRec, QueryDoc, OptDoc) -> - QueryRec1 = QueryRec#emo_query{limit=Limit}, - create_query(Options, QueryRec1, QueryDoc, OptDoc); + QueryRec1 = QueryRec#emo_query{limit=Limit}, + create_query(Options, QueryRec1, QueryDoc, OptDoc); create_query([{offset, Offset}|Options], QueryRec, QueryDoc, OptDoc) -> - QueryRec1 = QueryRec#emo_query{offset=Offset}, - create_query(Options, QueryRec1, QueryDoc, OptDoc); + QueryRec1 = QueryRec#emo_query{offset=Offset}, + create_query(Options, QueryRec1, QueryDoc, OptDoc); create_query([{orderby, Orderby}|Options], QueryRec, QueryDoc, OptDoc) -> - Orderby1 = [{Key, case Dir of desc -> -1; _ -> 1 end}|| {Key, Dir} <- Orderby], - OptDoc1 = [{<<"$orderby">>, Orderby1}|OptDoc], - create_query(Options, QueryRec, QueryDoc, OptDoc1); + Orderby1 = [{Key, case Dir of desc -> -1; _ -> 1 end}|| {Key, Dir} <- Orderby], + OptDoc1 = [{<<"orderby">>, Orderby1}|OptDoc], + create_query(Options, QueryRec, QueryDoc, OptDoc1); create_query([{fields, Fields}|Options], QueryRec, QueryDoc, OptDoc) -> - QueryRec1 = QueryRec#emo_query{field_selector=[{Field, 1} || Field <- Fields]}, - create_query(Options, QueryRec1, QueryDoc, OptDoc); + QueryRec1 = QueryRec#emo_query{field_selector=convert_fields(Fields)}, + create_query(Options, QueryRec1, QueryDoc, OptDoc); -create_query([explain | Options], QueryRec, QueryDoc, OptDoc) -> - create_query(Options, QueryRec, QueryDoc, [{<<"$explain">>,true}|OptDoc]); +create_query([Opt|Options], QueryRec, QueryDoc, OptDoc) when is_integer(Opt) -> + QueryRec1 = QueryRec#emo_query{opts=[Opt|QueryRec#emo_query.opts]}, + create_query(Options, QueryRec1, QueryDoc, OptDoc); + +create_query([{<<_/binary>>, _} = NV | Options], QueryRec, QueryDoc, OptDoc) -> + create_query(Options, QueryRec, QueryDoc, [NV | OptDoc]); create_query([_|Options], QueryRec, QueryDoc, OptDoc) -> - create_query(Options, QueryRec, QueryDoc, OptDoc). - -fam_options([], OptDoc) -> OptDoc; -fam_options([{sort, _}=Opt | Options], OptDoc) -> - fam_options(Options, [opt(Opt) | OptDoc]); -fam_options([{remove, _}=Opt | Options], OptDoc) -> - fam_options(Options, [opt(Opt) | OptDoc]); -fam_options([{update, _} | Options], OptDoc) -> - fam_options(Options, OptDoc); % update is a param to find_and_modify/5 -fam_options([{new, _}=Opt | Options], OptDoc) -> - fam_options(Options, [opt(Opt) | OptDoc]); -fam_options([{fields, Fields} | Options], OptDoc) -> - NewOpt = [{Field, 1} || Field <- Fields], - fam_options(Options, [opt({fields, NewOpt}) | OptDoc]); -fam_options([{upsert, _}=Opt | Options], OptDoc) -> - fam_options(Options, [opt(Opt) | OptDoc]); -fam_options([_ | Options], OptDoc) -> - fam_options(Options, OptDoc). - -opt({Atom, Val}) when is_atom(Atom) -> - {list_to_binary(atom_to_list(Atom)), Val}. + create_query(Options, QueryRec, QueryDoc, OptDoc). transform_selector(Selector) -> - transform_selector(Selector, []). + transform_selector(Selector, []). transform_selector([], Acc) -> - lists:reverse(Acc); + lists:reverse(Acc); transform_selector([{where, Val}|Tail], Acc) when is_list(Val) -> - transform_selector(Tail, [{<<"$where">>, Val}|Acc]); + transform_selector(Tail, [{<<"$where">>, Val}|Acc]); transform_selector([{Key, [{_,_}|_]=Vals}|Tail], Acc) -> - Vals1 = - [case Operator of - O when O == '>'; O == gt -> - {<<"$gt">>, Val}; - O when O == '<'; O == lt -> - {<<"$lt">>, Val}; - O when O == '>='; O == gte -> - {<<"$gte">>, Val}; - O when O == '=<'; O == lte -> - {<<"$lte">>, Val}; - O when O == '=/='; O == '/='; O == ne -> - {<<"$ne">>, Val}; - in when is_list(Val) -> - {<<"$in">>, {array, Val}}; - nin when is_list(Val) -> - {<<"$nin">>, {array, Val}}; - mod when is_list(Val), length(Val) == 2 -> - {<<"$mod">>, {array, Val}}; - all when is_list(Val) -> - {<<"$all">>, {array, Val}}; - size when is_integer(Val) -> - {<<"$size">>, Val}; - exists when is_boolean(Val) -> - {<<"$exists">>, Val}; - _ -> - {Operator, Val} - end || {Operator, Val} <- Vals], - transform_selector(Tail, [{Key, Vals1}|Acc]); + Vals1 = + [case Operator of + O when O == '>'; O == gt -> + {<<"$gt">>, Val}; + O when O == '<'; O == lt -> + {<<"$lt">>, Val}; + O when O == '>='; O == gte -> + {<<"$gte">>, Val}; + O when O == '=<'; O == lte -> + {<<"$lte">>, Val}; + O when O == '=/='; O == '/='; O == ne -> + {<<"$ne">>, Val}; + in when is_list(Val) -> + {<<"$in">>, {array, Val}}; + nin when is_list(Val) -> + {<<"$nin">>, {array, Val}}; + mod when is_list(Val), length(Val) == 2 -> + {<<"$mod">>, {array, Val}}; + all when is_list(Val) -> + {<<"$all">>, {array, Val}}; + size when is_integer(Val) -> + {<<"$size">>, Val}; + exists when is_boolean(Val) -> + {<<"$exists">>, Val}; + near when is_list(Val) -> + {<<"$near">>, {array, Val}}; + _ -> + {Operator, Val} + end || {Operator, Val} <- Vals], + transform_selector(Tail, [{Key, Vals1}|Acc]); transform_selector([Other|Tail], Acc) -> - transform_selector(Tail, [Other|Acc]). + transform_selector(Tail, [Other|Acc]). -dec0($a) -> 10; -dec0($b) -> 11; -dec0($c) -> 12; -dec0($d) -> 13; -dec0($e) -> 14; -dec0($f) -> 15; -dec0(X) -> X - $0. +dec0($a) -> 10; +dec0($b) -> 11; +dec0($c) -> 12; +dec0($d) -> 13; +dec0($e) -> 14; +dec0($f) -> 15; +dec0(X) -> X - $0. hex0(10) -> $a; hex0(11) -> $b; @@ -580,3 +689,28 @@ hex0(13) -> $d; hex0(14) -> $e; hex0(15) -> $f; hex0(I) -> $0 + I. + +sync_command({Pid, Pool}, Packet1, Options) -> + Query1 = #emo_query{q=[{<<"getlasterror">>, 1}], limit=1}, + Packet2 = emongo_packet:do_query(Pool#pool.database, "$cmd", Pool#pool.req_id, Query1), + Resp = emongo_conn:send_sync(Pid, Pool#pool.req_id, Packet1, Packet2, ?TIMEOUT), + case lists:member(response_options, Options) of + true -> Resp; + false -> get_sync_result(Resp) + end. + +get_sync_result(#response{documents = [Doc]}) -> + case lists:keysearch(<<"err">>, 1, Doc) of + {value, {_, undefined}} -> ok; + {value, {_, Msg}} -> {error, Msg}; + _ -> {error, {invalid_error_message, Doc}} + end; +get_sync_result(Resp) -> + {error, {invalid_response, Resp}}. + +to_binary(V) when is_binary(V) -> V; +to_binary(V) when is_list(V) -> list_to_binary(V); +to_binary(V) when is_atom(V) -> list_to_binary(atom_to_list(V)). + +convert_fields(Fields) -> + [{Field, 1} || Field <- Fields]. diff --git a/src/emongo_app.erl b/src/emongo_app.erl index b749ae2..b216dc5 100644 --- a/src/emongo_app.erl +++ b/src/emongo_app.erl @@ -21,35 +21,17 @@ %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR %% OTHER DEALINGS IN THE SOFTWARE. -module(emongo_app). - -behaviour(application). --include("emongo.hrl"). - --export([start/2, stop/1, initialize_pools/0]). +-export([start/2, stop/1]). start(_, _) -> - {ok, Pid} = emongo_sup:start_link(), - % Pools must be initialized after emongo_sup is started instead of in - % emongo:init, because emongo_server_sup instances are dynamically added - % to the emongo_sup supervisor, which also supervises emongo gen_server. - % (otherwise get a deadlock where emongo is waiting on emongo_sup, which - % is waiting on emongo) - initialize_pools(), - {ok, Pid}. + emongo:start_link(). + %supervisor:start_link({local, ?MODULE}, ?MODULE, []). stop(_) -> ok. -initialize_pools() -> - F = fun({PoolId, Props}) -> - Host = proplists:get_value(host, Props, "localhost"), - Port = proplists:get_value(port, Props, 27017), - Database = proplists:get_value(database, Props, "test"), - Size = proplists:get_value(size, Props, 1), - emongo:add_pool(PoolId, Host, Port, Database, Size) - end, - - case application:get_env(emongo, pools) of - undefined -> ok; - {ok, Pools} -> lists:foreach(F, Pools) - end. +%init(_) -> +% {ok, {{one_for_one, 10, 10}, [ +% {emongo, {emongo, start_link, []}, permanent, 5000, worker, [emongo]} +% ]}}. diff --git a/src/emongo_bson.erl b/src/emongo_bson.erl index 7ae0048..4ab8fac 100644 --- a/src/emongo_bson.erl +++ b/src/emongo_bson.erl @@ -31,21 +31,13 @@ encode([{_,_}|_]=List) when is_list(List) -> Bin = iolist_to_binary([encode_key_value(Key, Val) || {Key, Val} <- List]), <<(size(Bin)+5):32/little-signed, Bin/binary, 0:8>>. -encode_key_value(none, none) -> - <<>>; - %% FLOAT encode_key_value(Key, Val) when is_float(Val) -> Key1 = encode_key(Key), <<1, Key1/binary, 0, Val:64/little-signed-float>>; %% STRING -%% binary string must be already in utf8 -encode_key_value(Key, Val) when is_binary(Val) -> - Key1 = encode_key(Key), - <<2, Key1/binary, 0, (byte_size(Val)+1):32/little-signed, Val/binary, 0:8>>; - -encode_key_value(Key, Val) when Val == [] orelse (is_list(Val) andalso length(Val) > 0 andalso is_integer(hd(Val))) -> +encode_key_value(Key, Val) when is_binary(Val) orelse Val == [] orelse (is_list(Val) andalso length(Val) > 0 andalso is_integer(hd(Val))) -> Key1 = encode_key(Key), case unicode:characters_to_binary(Val) of {error, Bin, RestData} -> @@ -84,11 +76,6 @@ encode_key_value(Key, {binary, SubType, Val}) when is_integer(SubType), is_binar encode_key_value(Key, {oid, HexString}) when is_list(HexString) -> encode_key_value(Key, {oid, emongo:hex2dec(HexString)}); -%% UNDEFINED -encode_key_value(Key, undefined) -> - Key1 = encode_key(Key), - <<6, Key1/binary, 0>>; - encode_key_value(Key, {oid, OID}) when is_binary(OID) -> Key1 = encode_key(Key), <<7, Key1/binary, 0, OID/binary>>; @@ -119,8 +106,8 @@ encode_key_value(Key, {datetime, Val}) -> encode_key_value(Key, {{Year, Month, Day}, {Hour, Min, Secs}}) when is_integer(Year), is_integer(Month), is_integer(Day), is_integer(Hour), is_integer(Min), is_integer(Secs) -> encode_key_value(Key, {datetime, {{Year, Month, Day}, {Hour, Min, Secs}}}); -%% NULL -encode_key_value(Key, null) -> +%% VOID +encode_key_value(Key, undefined) -> Key1 = encode_key(Key), <<10, Key1/binary, 0>>; @@ -142,7 +129,7 @@ encode_key_value(Key, Val) when is_integer(Val) -> <<18, Key1/binary, 0, Val:64/little-signed>>; encode_key_value(Key, Val) -> - exit({oh_balls, Key, Val}). + exit({emongo_bson_encode_error, Key, Val}). encode_key(Key) when is_binary(Key) -> Key; @@ -211,10 +198,6 @@ decode_value(5, <<_Size:32/little-signed, 2:8/little, BinSize:32/little-signed, decode_value(5, <>) -> {{binary, SubType, BinData}, Tail}; -%% VOID -decode_value(6, Tail) -> - {undefined, Tail}; - %% OID decode_value(7, <>) -> {{oid, OID}, Tail}; @@ -236,19 +219,15 @@ decode_value(9, <>) -> %% VOID decode_value(10, Tail) -> - {null, Tail}; + {undefined, Tail}; %% INT decode_value(16, <>) -> {Int, Tail}; -%% Timestamp -decode_value(17, <>) -> - {{timestamp, Inc, Timestamp}, Tail}; - %% LONG decode_value(18, <>) -> {Int, Tail}; -decode_value(Type, Tail) -> - exit({emongo_unknown_type, Type, Tail}). +decode_value(Type, Value) -> + exit({emongo_bson_decode_error, Type, Value}). diff --git a/src/emongo_packet.erl b/src/emongo_packet.erl index d4b96b6..736602f 100644 --- a/src/emongo_packet.erl +++ b/src/emongo_packet.erl @@ -22,40 +22,23 @@ %% OTHER DEALINGS IN THE SOFTWARE. -module(emongo_packet). --export([update/7, insert/4, do_query/4, get_more/5, +-export([update/7, insert/4, do_query/4, get_more/5, delete/4, kill_cursors/2, msg/2, decode_response/1, - ensure_index/5, get_last_error/2, server_status/2]). + ensure_index/4]). -include("emongo.hrl"). -get_last_error(Database, ReqId) -> - %%Query = #emo_query{q=[{<<"getlasterror">>, 1}], limit=1}, - %%do_query(Database, "$cmd", ReqId, Query). - DatabaseLength = byte_size(Database), - <<(57+DatabaseLength):32/little-signed, ReqId:32/little-signed, 0:32, - ?OP_QUERY:32/little-signed, 0:32, Database/binary, ".$cmd", 0, 0:32, 1:32/little-signed, - %% Encoded document - 23:32/little-signed, 16, "getlasterror", 0, 1:32/little-signed, 0>>. - -server_status(Database, ReqId) -> - %%Query = #emo_query{q=[{<<"serverStatus">>, 1}], limit=1}, - %%do_query(Database, "$cmd", ReqId, Query). - DatabaseLength = byte_size(Database), - <<(57+DatabaseLength):32/little-signed, ReqId:32/little-signed, 0:32, - ?OP_QUERY:32/little-signed, 0:32, Database/binary, ".$cmd", 0, 0:32, 1:32/little-signed, - %% Encoded document - 23:32/little-signed, 16, "serverStatus", 0, 1:32/little-signed, 0>>. - -update(Database, Collection, ReqID, Upsert, MultiUpdate, Selector, Document) -> +update(Database, Collection, ReqID, Upsert, Multi, Selector, Document) -> FullName = unicode:characters_to_binary([Database, ".", Collection]), EncodedSelector = emongo_bson:encode(Selector), EncodedDocument = emongo_bson:encode(Document), BinUpsert = if Upsert == true -> 1; true -> 0 end, - BinMultiUpdate = if MultiUpdate == true -> 2#10; true -> 0 end, - OptsSum = BinUpsert + BinMultiUpdate, - Message = <<0:32, FullName/binary, 0, OptsSum:32/little-signed, EncodedSelector/binary, EncodedDocument/binary>>, + BinMulti = if Multi == true -> 1; true -> 0 end, + Flags = (BinMulti bsl 1) bor BinUpsert, + Message = <<0:32, FullName/binary, 0, Flags:32/little-signed, EncodedSelector/binary, EncodedDocument/binary>>, Length = byte_size(Message), - <<(Length+16):32/little-signed, ReqID:32/little-signed, 0:32, ?OP_UPDATE:32/little-signed, Message/binary>>. + <<(Length+16):32/little-signed, ReqID:32/little-signed, 0:32, + ?OP_UPDATE:32/little-signed, Message/binary>>. insert(Database, Collection, ReqID, Documents) -> FullName = unicode:characters_to_binary([Database, ".", Collection]), @@ -95,13 +78,12 @@ delete(Database, Collection, ReqID, Selector) -> Length = byte_size(Message), <<(Length+16):32/little-signed, ReqID:32/little-signed, 0:32, ?OP_DELETE:32/little-signed, Message/binary>>. -ensure_index(Database, Collection, ReqID, Keys, Unique) -> +ensure_index(Database, Collection, ReqID, Keys) -> FullName = unicode:characters_to_binary([Database, ".system.indexes"]), Selector = [ {<<"name">>, index_name(Keys, <<>>)}, {<<"ns">>, unicode:characters_to_binary([Database, ".", Collection])}, - {<<"key">>, Keys}, - {<<"unique">>, Unique}], + {<<"key">>, Keys}], EncodedDocument = emongo_bson:encode(Selector), Message = <<0:32, FullName/binary, 0, EncodedDocument/binary>>, Length = byte_size(Message), @@ -131,10 +113,7 @@ decode_response(<> = Message, Resp = #response{ - header = #header{message_length = Length, - request_id = ReqID, - response_to = RespTo, - op_code = Op}, + header = {header, Length, ReqID, RespTo, Op}, response_flag = RespFlag, cursor_id = CursorID, offset = StartingFrom, @@ -143,9 +122,10 @@ decode_response(< - undefined. + undefined. index_name([], Bin) -> Bin; index_name([{Key, Val}|Tail], Bin) -> diff --git a/src/emongo_pool.erl b/src/emongo_pool.erl deleted file mode 100644 index b46616d..0000000 --- a/src/emongo_pool.erl +++ /dev/null @@ -1,224 +0,0 @@ -%%%------------------------------------------------------------------- -%%% Description : emongo pool supervisor -%%%------------------------------------------------------------------- --module(emongo_pool). - --behaviour(gen_server). - -%% API --export([start_link/6, pid/1, pid/2]). - --deprecated([pid/1]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --include("emongo.hrl"). - --define(POLL_INTERVAL, 10000). --define(POLL_TIMEOUT, 9000). - --record(pool, {id, - host, - port, - database, - size, - auto_reconnect, - active=true, - poll=none, - conn_pid=pqueue:new(), - req_id=1}). - -%% messages --define(pid(RequestCount), {pid, RequestCount}). --define(poll(), poll). --define(poll_timeout(Pid, ReqId, Tag), {poll_timeout, Pid, ReqId, Tag}). - -%% to be removed next release --define(old_pid(), pid). - - -%%%%%%%%%%%%%%%% -%% public api %% -%%%%%%%%%%%%%%%% - -start_link(PoolId, Host, Port, Database, Size, AutoReconnect) -> - gen_server:start_link(?MODULE, [PoolId, Host, Port, Database, Size, AutoReconnect], []). - -pid(Pid) -> - gen_server:call(Pid, pid). - -pid(Pid, RequestCount) -> - gen_server:call(Pid, {pid, RequestCount}). - -%%%%%%%%%%%%%%%%%%%%%%%%%% -%% gen_server callbacks %% -%%%%%%%%%%%%%%%%%%%%%%%%%% - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([PoolId, Host, Port, Database, Size, AutoReconnect]) -> - process_flag(trap_exit, true), - - Pool0 = #pool{id = PoolId, - host = Host, - port = Port, - database = unicode:characters_to_binary(Database), - size = Size, - auto_reconnect = AutoReconnect - }, - - case handle_info(?poll(), Pool0) of - {noreply, Pool} -> {ok, Pool}; - {stop, Reason, _NewState} -> {stop, Reason} - end. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- -handle_call(?old_pid(), _From, #pool{active=true}=State) -> - {Reply, NewState} = get_pid(State, 1), - {reply, Reply, NewState}; - -handle_call(?pid(RequestCount), _From, #pool{active=true}=State) -> - {Reply, NewState} = get_pid(State, RequestCount), - {reply, Reply, NewState}; - -handle_call(_Request, _From, State) -> - {reply, undefined, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast(_Msg, State) -> - {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info({'EXIT', Pid, Reason}, #pool{conn_pid=Pids}=State) -> - error_logger:error_msg("Pool ~p deactivated by worker death: ~p~n", - [State#pool.id, Reason]), - - Pids1 = pqueue:filter(fun(Item) -> Item =/= Pid end, Pids), - NewState = State#pool{conn_pid = Pids1, active=false}, - check_return_state(NewState); - -handle_info(?poll(), State) -> - erlang:send_after(?POLL_INTERVAL, self(), poll), - NewState = do_open_connections(State), - check_return_state(NewState); - -handle_info(?poll_timeout(Pid, ReqId, Tag), #pool{poll={Tag, _}}=State) -> - case catch emongo_server:recv(Pid, ReqId, 0, Tag) of - #response{} -> - {noreply, State#pool{active=true, poll=none}}; - _ -> - NewState = State#pool{active=false, poll=none}, - check_return_state(NewState) - end; - -handle_info({Tag, _}, #pool{poll={Tag, TimerRef}}=State) -> - _Time = erlang:cancel_timer(TimerRef), - %%io:format("polling ~p success: ~p~n", [State#pool.id, Time]), - {noreply, State#pool{active=true, poll=none}}; - -handle_info(Info, State) -> - error_logger:info_msg("Pool ~p unknown message: ~p~n", - [State#pool.id, Info]), - - {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, _State) -> - ok. - -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(OldVsn, State, _Extra) -> - error_logger:info_msg("emongo_pool:code_change(~p, ...)~n", [OldVsn]), - - State1 = case queue:is_queue(State#pool.conn_pid) of - false -> - State; - true -> - State#pool{conn_pid = queue2pqueue(State#pool.conn_pid, pqueue:new())} - end, - - {ok, State1}. - -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- -queue2pqueue(Queue, PQueue) -> - case queue:out(Queue) of - {empty, _} -> - PQueue; - {{value, Item}, NewQueue} -> - queue2pqueue(NewQueue, pqueue:push(1, Item, PQueue)) - end. - -get_pid(#pool{database=Database, conn_pid=Pids, req_id=ReqId}=State, RequestCount) -> - case pqueue:pop(Pids) of - {Pid, Q2} -> - NewState = State#pool{conn_pid=pqueue:push(RequestCount, Pid, Q2), - req_id=(ReqId + RequestCount)}, - {{Pid, Database, ReqId}, NewState}; - empty -> - {undefined, State} - end. - -do_open_connections(#pool{conn_pid=Pids, size=Size}=Pool) -> - case pqueue:size(Pids) < Size of - true -> - case emongo_server:start_link(Pool#pool.id, Pool#pool.host, Pool#pool.port) of - {error, _Reason} -> - Pool#pool{active=false}; - {ok, Pid} -> - do_open_connections(Pool#pool{conn_pid = pqueue:push(1, Pid, Pids)}) - end; - false -> - do_poll(Pool) - end. - -do_poll(Pool) -> - case get_pid(Pool, 2) of - {{Pid, Database, ReqId}, NewPool} -> - PacketLast = emongo_packet:get_last_error(Database, ReqId), - Tag = emongo_server:send_recv_nowait(Pid, ReqId, PacketLast), - TimerRef = erlang:send_after(?POLL_TIMEOUT, self(), ?poll_timeout(Pid, ReqId, Tag)), - NewPool#pool{poll={Tag, TimerRef}}; - _ -> - Pool#pool{active=false} - end. - -check_return_state(#pool{active=false, auto_reconnect=false}=State) -> - {stop, connection_error, State}; -check_return_state(State) -> - {noreply, State}. diff --git a/src/emongo_router.erl b/src/emongo_router.erl deleted file mode 100644 index 87effd7..0000000 --- a/src/emongo_router.erl +++ /dev/null @@ -1,191 +0,0 @@ -%%%------------------------------------------------------------------- -%%% Description : balancer for emongo_pool connections -%%%------------------------------------------------------------------- --module(emongo_router). - --behaviour(gen_server). - -%% API --export([start_link/2, pid/1, pid/2]). - --deprecated([pid/1]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {id, - active, - passive, - timer=undefined - }). - --define(POOL_ID(BalancerId, PoolIdx), {BalancerId, PoolIdx}). --define(RECHECK_TIME, 9500). - -%% messages --define(pid(RequestCount), {pid, RequestCount}). - -%% to be removed next release --define(old_pid(), pid). - -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -start_link(BalId, Pools) -> - gen_server:start_link(?MODULE, [BalId, Pools], []). - - -pid(BalancerPid) -> - pid(BalancerPid, 1). - - -pid(BalancerPid, RequestCount) -> - gen_server:call(BalancerPid, {pid, RequestCount}). -%%==================================================================== -%% gen_server callbacks -%%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([BalId, Pools]) -> - self() ! {init, Pools}, - {ok, #state{id = BalId, - active = [], - passive = []}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messagesp -%%-------------------------------------------------------------------- -handle_call(?old_pid(), _From, State) -> - {Pid, NewState} = get_pid(State, emongo_sup:pools(), 1), - {reply, Pid, NewState}; - -handle_call(?pid(RequestCount), _From, State) -> - {Pid, NewState} = get_pid(State, emongo_sup:pools(), RequestCount), - {reply, Pid, NewState}; - -handle_call(stop_children, _, #state{id=BalId, active=Active, passive=Passive}=State) -> - Fun = fun(PoolIdx) -> - emongo_sup:stop_pool(?POOL_ID(BalId, PoolIdx)), - false - end, - lists:foreach(Fun, Passive), - lists:foreach(Fun, Active), - - {reply, ok, State#state{active=[], passive=[]}}; - -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast(_Msg, State) -> - {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info({init, Pools}, #state{id=BalId, active=Active}=State) -> - Fun = fun({Host, Port, Database, Size}, {PoolIdx, PoolList}) -> - case emongo_sup:start_pool(?POOL_ID(BalId, PoolIdx), - Host, Port, Database, Size) of - {ok, _} -> - ok; - {error, {already_started, _}} -> - ok - end, - {PoolIdx + 1, [PoolIdx | PoolList]} - end, - {_, PoolList} = lists:foldl(Fun, {1, Active}, Pools), - {noreply, State#state{active=lists:sort(PoolList)}}; - -handle_info(recheck, State) -> - {noreply, activate(State, [], emongo_sup:pools())}; - -handle_info(_Info, State) -> - {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, _State) -> - ok. - -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- - -get_pid(#state{id=BalId, active=Active, passive=Passive, timer=Timer}=State, Pools, RequestCount) -> - case Active of - [PoolIdx | Active2] -> - case emongo_sup:worker_pid(?POOL_ID(BalId, PoolIdx), Pools, RequestCount) of - undefined -> - error_logger:info_msg("pool ~p is disabled!~n", [?POOL_ID(BalId, PoolIdx)]), - - get_pid(State#state{active=Active2, - passive=[PoolIdx | Passive], - timer=set_timer(Timer) - }, Pools, RequestCount); - Pid -> - {Pid, State} - end; - [] -> - {undefined, State} - end. - - -set_timer(undefined) -> - erlang:send_after(?RECHECK_TIME, self(), recheck); -set_timer(TimerRef) -> - TimerRef. - - -activate(#state{passive=[], timer=_TimerRef}=State, [], _) -> - State#state{timer=undefined}; - -activate(#state{passive=[]}=State, Passive, _) -> - State#state{passive=Passive, timer=erlang:send_after(?RECHECK_TIME, self(), recheck)}; - -activate(#state{id=BalId, active=Active, passive=[PoolIdx | Passive]}=State, Acc, Pools) -> - case emongo_sup:worker_pid(?POOL_ID(BalId, PoolIdx), Pools, 0) of - undefined -> - activate(State#state{passive=Passive}, [PoolIdx | Acc], Pools); - _ -> - error_logger:info_msg("pool ~p is enabled!~n", [?POOL_ID(BalId, PoolIdx)]), - activate(State#state{active=lists:umerge([PoolIdx], Active), passive=Passive}, Acc, Pools) - end. diff --git a/src/emongo_server.erl b/src/emongo_server.erl deleted file mode 100644 index 638d927..0000000 --- a/src/emongo_server.erl +++ /dev/null @@ -1,171 +0,0 @@ --module(emongo_server). - --behaviour(gen_server). - --include("emongo.hrl"). - --export([start_link/3]). - --export([send/3, send/2, send_recv/4]). --export([send_recv_nowait/3, recv/4]). - --deprecated([send/3]). - -%% gen_server --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {pool_id, socket, requests, leftover}). - -%% messages --define(abort(ReqId), {abort, ReqId}). --define(send(Packet), {send, Packet}). --define(send_recv(ReqId, Packet, From), - {send_recv, ReqID, Packet, From}). - -%% to be removed next release --define(old_send(ReqId, Packet), {send, ReqId, Packet}). - - -start_link(PoolId, Host, Port) -> - gen_server:start_link(?MODULE, [PoolId, Host, Port], []). - - -send(Pid, _ReqID, Packet) -> - send(Pid, Packet). - -send(Pid, Packet) -> - gen_server:cast(Pid, ?send(Packet)). - - -send_recv_nowait(Pid, ReqID, Packet) -> - Tag = make_ref(), - gen_server:cast(Pid, ?send_recv(ReqID, Packet, {self(), Tag})), - Tag. - - -recv(Pid, ReqID, 0, Tag) -> - Pid ! ?abort(ReqID), - receive - {Tag, Resp} -> - Documents = emongo_bson:decode(Resp#response.documents), - Resp#response{documents=Documents} - after 0 -> - exit(emongo_timeout) - end; - -recv(Pid, ReqID, Timeout, Tag) -> - receive - {Tag, Resp} -> - Documents = emongo_bson:decode(Resp#response.documents), - Resp#response{documents=Documents} - after Timeout -> - recv(Pid, ReqID, 0, Tag) - end. - - -send_recv(Pid, ReqID, Packet, Timeout) -> - Tag = send_recv_nowait(Pid, ReqID, Packet), - recv(Pid, ReqID, Timeout, Tag). - - -%% gen_server %% - -init([PoolId, Host, Port]) -> - case gen_tcp:connect(Host, Port, [binary, {active, true}, {nodelay, true}], ?TIMEOUT) of - {ok, Socket} -> - {ok, #state{pool_id=PoolId, socket=Socket, requests=[], leftover = <<>>}}; - {error, Reason} -> - {stop, {failed_to_open_socket, Reason}} - end. - - -handle_call(_Request, _From, State) -> - {reply, undefined, State}. - - -handle_cast(?send_recv(ReqID, Packet, From), State) -> - case is_aborted(ReqID) of - true -> - {noreply, State}; - _ -> - gen_tcp:send(State#state.socket, Packet), - State1 = State#state{requests=[{ReqID, From} | State#state.requests]}, - {noreply, State1} - end; - -handle_cast(?old_send(_ReqId, Packet), State) -> - gen_tcp:send(State#state.socket, Packet), - {noreply, State}; - -handle_cast(?send(Packet), State) -> - gen_tcp:send(State#state.socket, Packet), - {noreply, State}. - - -handle_info(?abort(ReqId), #state{requests=Requests}=State) -> - State1 = State#state{requests=lists:keydelete(ReqId, 1, Requests)}, - {noreply, State1}; - -handle_info({tcp, _Socket, Data}, State) -> - Leftover = <<(State#state.leftover)/binary, Data/binary>>, - {noreply, process_bin(State#state{leftover= <<>>}, Leftover)}; - -handle_info({tcp_closed, _Socket}, _State) -> - exit(tcp_closed); - -handle_info({tcp_error, _Socket, Reason}, _State) -> - exit({tcp_error, Reason}). - - -terminate(_, State) -> gen_tcp:close(State#state.socket). - - -code_change(_Old, State, _Extra) -> {ok, State}. - -%% internal - - -process_bin(State, <<>>) -> - State; - -process_bin(State, Bin) -> - case emongo_packet:decode_response(Bin) of - undefined -> - State#state{leftover=Bin}; - - {Resp, Tail} -> - ResponseTo = (Resp#response.header)#header.response_to, - - case lists:keytake(ResponseTo, 1, State#state.requests) of - false -> - cleanup_cursor(Resp, ResponseTo, State), - process_bin(State, Tail); - - {value, {_, From}, Requests} -> - case is_aborted(ResponseTo) of - false -> - gen_server:reply( - From, - Resp#response{pool_id=State#state.pool_id}); - true -> - cleanup_cursor(Resp, ResponseTo, State) - end, - process_bin(State#state{requests=Requests}, Tail) - end - end. - - -is_aborted(ReqId) -> - receive - ?abort(ReqId) -> - true - after 0 -> - false - end. - -cleanup_cursor(#response{cursor_id=0}, _, _) -> - ok; -cleanup_cursor(#response{cursor_id=CursorID}, ReqId, State) -> - Packet = emongo_packet:kill_cursors(ReqId, [CursorID]), - gen_tcp:send(State#state.socket, Packet). diff --git a/src/emongo_sup.erl b/src/emongo_sup.erl deleted file mode 100644 index 8fa5616..0000000 --- a/src/emongo_sup.erl +++ /dev/null @@ -1,89 +0,0 @@ --module(emongo_sup). - --behaviour(supervisor). - --export([start_link/0, pools/0, worker_pid/2, worker_pid/3]). --export([start_pool/5, start_pool/6, stop_pool/1]). --export([start_router/2, stop_router/1]). - --deprecated([worker_pid/2]). - -%% supervisor exports --export([init/1]). - -%%%%%%%%%%%%%%%% -%% public api %% -%%%%%%%%%%%%%%%% - -start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -start_router(BalId, Pools) -> - supervisor:start_child(?MODULE, - {BalId, - {emongo_router, start_link, [BalId, Pools]}, - permanent, 10000, worker, [emongo_router] - }). - -stop_router(BalId) -> - case [Pid || {PoolId, Pid, _, [emongo_router]} <- supervisor:which_children(?MODULE), PoolId =:= BalId] of - [Pid] -> - gen_server:call(Pid, stop_children), - stop_pool(BalId) - end. - - -start_pool(PoolId, Host, Port, Database, Size) -> - start_pool(PoolId, Host, Port, Database, Size, true). - -start_pool(PoolId, Host, Port, Database, Size, AutoReconnect) -> - supervisor:start_child(?MODULE, {PoolId, - {emongo_pool, start_link, [PoolId, Host, Port, Database, Size, AutoReconnect]}, - permanent, 10000, worker, [emongo_pool] - }). - - -stop_pool(PoolPid) when is_pid(PoolPid) -> - case [PoolId || {PoolId, Pid,_,_} <- supervisor:which_children(?MODULE), Pid =:= PoolPid] of - [PoolId] -> stop_pool(PoolId); - _ -> {error, not_found} - end; - -stop_pool(PoolId) -> - supervisor:terminate_child(?MODULE, PoolId), - supervisor:delete_child(?MODULE, PoolId). - - -pools() -> - [{Id, Pid, Module} || {Id, Pid, _, [Module]} - <- supervisor:which_children(?MODULE), Module == emongo_pool]. - -worker_pid(PoolId, Pools) -> - worker_pid(PoolId, Pools, 1). - - -worker_pid(PoolPid, Pools, RequestCount) when is_pid(PoolPid) -> - case lists:keyfind(PoolPid, 2, Pools) of - {_, Pid, Module} -> - Module:pid(Pid, RequestCount); - _ -> - undefined - end; - -worker_pid(PoolId, Pools, RequestCount) -> - case lists:keyfind(PoolId, 1, Pools) of - {_, Pid, Module} -> - Module:pid(Pid, RequestCount); - _ -> - undefined - end. - - -%%%%%%%%%%%%%%%%%%%%%%%%%% -%% supervisor callbacks %% -%%%%%%%%%%%%%%%%%%%%%%%%%% - -init(_) -> - {ok, {{one_for_one, 10, 10}, [ - {emongo, {emongo, start_link, []}, - permanent, 5000, worker, [emongo]} - ]}}. diff --git a/src/pqueue.erl b/src/pqueue.erl deleted file mode 100644 index ab6feea..0000000 --- a/src/pqueue.erl +++ /dev/null @@ -1,107 +0,0 @@ -%% Copyright (c) 2010 Belyaev Dmitry -%% -%% Permission is hereby granted, free of charge, to any person obtaining a copy -%% of this software and associated documentation files (the "Software"), to deal -%% in the Software without restriction, including without limitation the rights -%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the Software is -%% furnished to do so, subject to the following conditions: -%% -%% The above copyright notice and this permission notice shall be included in -%% all copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -%% THE SOFTWARE. - -%% Is pairing heap a better realization? --module(pqueue). - --export([ - new/0, - size/1, - push/3, - pop/1, - filter/2 - ]). - --type tail() :: {integer(), [term()]}. - --record(pqueue, {size :: integer(), - head :: [term()], - tails :: [tail()]}). - --spec new() -> #pqueue{}. -new() -> - #pqueue{size = 0, head = [], tails = []}. - - --spec size(#pqueue{}) -> integer(). -size(#pqueue{size = Size}) -> - Size. - - --spec push(integer(), term(), #pqueue{}) -> #pqueue{}. -push(Priority, Item, #pqueue{size = Size, head = Head, tails = Tails}) -> - #pqueue{size = Size + 1, head = Head, tails = push_item(Tails, Priority, Item)}. - - --spec pop(#pqueue{}) -> {term(), #pqueue{}} | empty. -pop(#pqueue{size = Size, head = [Item | Head], tails = Tails}) -> - {Item, #pqueue{size = Size - 1, head = Head, tails = Tails}}; - -pop(#pqueue{size = Size, head = [], tails = [{_, Tail} | Tails]}) -> - [Item | Head] = lists:reverse(Tail), - {Item, #pqueue{size = Size - 1, head = Head, tails = Tails}}; - -pop(_) -> - empty. - - --spec filter(fun((term()) -> boolean()), #pqueue{}) -> #pqueue{}. -filter(_, #pqueue{size = 0} = Q) -> - Q; -filter(Fun, #pqueue{head = Head, tails = Tails}) -> - NewHead = [I || I <- Head, Fun(I)], - NewTails = filter_tails(Fun, Tails), - - NewSize = length(Head) + lists:sum([length(Items) || {_, Items} <- NewTails]), - #pqueue{size = NewSize, head = NewHead, tails = NewTails}. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec push_item([tail()], integer(), term()) -> [tail()]. -push_item([], Priority, Item) -> - [{Priority, [Item]}]; - -push_item([{Priority, Items} | Tails], Priority, Item) -> - [{Priority, [Item | Items]} | Tails]; - -push_item([{LowerP, _} = LowerI | Tails], Priority, Item) - when LowerP < Priority -> - [LowerI | push_item(Tails, Priority - LowerP, Item)]; - -push_item([{HigherP, Items} | Tails], Priority, Item) -> - [{Priority, [Item]}, {HigherP - Priority, Items} | Tails]. - - --spec filter_tails(fun((term()) -> boolean()), [tail()]) -> [tail()]. -filter_tails(_, []) -> - []; -filter_tails(Fun, [{Priority, Items} | Tails]) -> - case [I || I <- Items, Fun(I)] of - [] -> - filter_tails(Fun, add_priority(Priority, Tails)); - NewItems -> - [{Priority, NewItems} | filter_tails(Fun, Tails)] - end. - - --spec add_priority(integer(), [tail()]) -> [tail()]. -add_priority(_, []) -> - []; -add_priority(Priority, [{NextP, Items} | Tails]) -> - [{NextP + Priority, Items} | Tails]. diff --git a/t/001-load.t b/t/001-load.t index de3f44f..b15bf5a 100644 --- a/t/001-load.t +++ b/t/001-load.t @@ -1,12 +1,12 @@ -#!/usr/bin/env escript +#!/usr/local/bin/escript %% -*- erlang -*- %%! -pa ebin -sasl errlog_type error -boot start_sasl -noshell -config priv/example main(_) -> - etap:plan(unknown), + etap:plan(unknown), error_logger:tty(false), - etap:ok(application:start(emongo) == ok, "application 'emongo' started ok"), + etap_application:start_ok(emongo, "application 'emongo' started ok"), - etap:is(length(emongo:pools()), 2, "two pools exist in state"), + etap:is(length(emongo:pools()), 1, "one pool exists in state"), - etap:end_tests(). + etap:end_tests(). \ No newline at end of file diff --git a/t/002-bson.t b/t/002-bson.t index cb95f7a..621af01 100644 --- a/t/002-bson.t +++ b/t/002-bson.t @@ -1,153 +1,142 @@ -#!/usr/bin/env escript +#!/usr/local/bin/escript %% -*- erlang -*- %%! -pa ebin -sasl errlog_type error -boot start_sasl -noshell -config priv/example main(_) -> - etap:plan(unknown), + etap:plan(unknown), error_logger:tty(false), - etap:ok(application:start(emongo) == ok, "application 'emongo' started ok"), - - %% 1) data_number - (fun() -> - Val = 1.1, - BinVal = <>, - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_number encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_number decodes ok"), - ok - end)(), - - %% 2) data_string - (fun() -> - Val = "abc", - Val1 = unicode:characters_to_binary(Val), - BinVal = <<(byte_size(Val1)+1):32/little-signed, Val1/binary, 0:8>>, - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_string encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, list_to_binary(Val)}], "data_string decodes ok"), - ok - end)(), - - %% 3) data_object - (fun() -> - Val = [{"b", "c"}], - BinVal = emongo_bson:encode(Val), - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_object encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, [{<<"b">>, <<"c">>}]}], "data_object decodes ok"), - ok - end)(), - - %% 4) data_array - (fun() -> - Val = {array, ["a", "b", "c"]}, - BinVal = emongo_bson:encode([{0, "a"}, {1, "b"}, {2, "c"}]), - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_array encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, {array, [<<"a">>, <<"b">>, <<"c">>]}}], "data_array decodes ok"), - ok - end)(), - - %% 5) data_binary - (fun() -> - Val = {binary, 2, <<"abc">>}, - BinVal = <<7:32/little-signed, 2:8, 3:32/little-signed, <<"abc">>/binary>>, - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_binary encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_binary decodes ok"), - ok - end)(), - - %% 6) data_undefined - (fun() -> - Val = undefined, - BinVal = <<>>, - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_undefined encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_null decodes ok"), - ok - end)(), - - %% 7) data_oid - (fun() -> - Val1 = {oid, <<255,255,255,255,255,255,255,255,255,255,255,255>>}, - BinVal1 = <<255,255,255,255,255,255,255,255,255,255,255,255>>, - Size1 = size(BinVal1) + 8, - Encoded1 = emongo_bson:encode([{<<"a">>, Val1}]), - etap:is(Encoded1, <>, "data_oid dec encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded1)), [{<<"a">>, Val1}], "data_oid decodes ok"), - - Val2 = {oid, "ffffffffffffffffffffffff"}, - BinVal2 = emongo:hex2dec("ffffffffffffffffffffffff"), - Size2 = size(BinVal2) + 8, - Encoded2 = emongo_bson:encode([{<<"a">>, Val2}]), - etap:is(Encoded2, <>, "data_oid hex encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded2)), [{<<"a">>, Val1}], "data_oid decodes ok"), - - ok - end)(), - - %% 8) data_boolean - (fun() -> - Val = true, - BinVal = <<1:8>>, - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_boolean encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_boolean decodes ok"), - ok - end)(), - - %% 9) data_date - (fun() -> - {MegaSecs,Secs,MicroSecs} = Val = now(), - Secs1 = (MegaSecs * 1000000) + Secs, - Epoch = Secs1 * 1000 + trunc(MicroSecs / 1000), - BinVal = <>, - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_date encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, {MegaSecs,Secs,erlang:trunc(MicroSecs / 1000) * 1000}}], "data_date decodes ok"), - ok - end)(), - - %% 10) data_null - (fun() -> - Val = null, - BinVal = <<>>, - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_null encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_null decodes ok"), - ok - end)(), - - %% 16) data_int - (fun() -> - Val = 11, - BinVal = <>, - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_int encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_int decodes ok"), - ok - end)(), - - %% 18) data_long - (fun() -> - Val = 5275387038659964208, - BinVal = <>, - Size = size(BinVal) + 8, - Encoded = emongo_bson:encode([{<<"a">>, Val}]), - etap:is(Encoded, <>, "data_number encodes ok"), - etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_number decodes ok"), - ok - end)(), - - etap:end_tests(). + etap_application:start_ok(emongo, "application 'emongo' started ok"), + + %% 1) data_number + (fun() -> + Val = 1.1, + BinVal = <>, + Size = size(BinVal) + 8, + Encoded = emongo_bson:encode([{<<"a">>, Val}]), + etap:is(Encoded, <>, "data_number encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_number decodes ok"), + ok + end)(), + + %% 2) data_string + (fun() -> + Val = "abc", + Val1 = unicode:characters_to_binary(Val), + BinVal = <<(byte_size(Val1)+1):32/little-signed, Val1/binary, 0:8>>, + Size = size(BinVal) + 8, + Encoded = emongo_bson:encode([{<<"a">>, Val}]), + etap:is(Encoded, <>, "data_string encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, list_to_binary(Val)}], "data_string decodes ok"), + ok + end)(), + + %% 3) data_object + (fun() -> + Val = [{"b", "c"}], + BinVal = emongo_bson:encode(Val), + Size = size(BinVal) + 8, + Encoded = emongo_bson:encode([{<<"a">>, Val}]), + etap:is(Encoded, <>, "data_object encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, [{<<"b">>, <<"c">>}]}], "data_object decodes ok"), + ok + end)(), + + %% 4) data_array + (fun() -> + Val = {array, ["a", "b", "c"]}, + BinVal = emongo_bson:encode([{0, "a"}, {1, "b"}, {2, "c"}]), + Size = size(BinVal) + 8, + Encoded = emongo_bson:encode([{<<"a">>, Val}]), + etap:is(Encoded, <>, "data_array encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, {array, [<<"a">>, <<"b">>, <<"c">>]}}], "data_array decodes ok"), + ok + end)(), + + %% 5) data_binary + (fun() -> + Val = {binary, 2, <<"abc">>}, + BinVal = <<7:32/little-signed, 2:8, 3:32/little-signed, <<"abc">>/binary>>, + Size = size(BinVal) + 8, + Encoded = emongo_bson:encode([{<<"a">>, Val}]), + etap:is(Encoded, <>, "data_binary encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_binary decodes ok"), + ok + end)(), + + %% 7) data_oid + (fun() -> + Val1 = {oid, <<255,255,255,255,255,255,255,255,255,255,255,255>>}, + BinVal1 = <<255,255,255,255,255,255,255,255,255,255,255,255>>, + Size1 = size(BinVal1) + 8, + Encoded1 = emongo_bson:encode([{<<"a">>, Val1}]), + etap:is(Encoded1, <>, "data_oid dec encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded1)), [{<<"a">>, Val1}], "data_oid decodes ok"), + + Val2 = {oid, "ffffffffffffffffffffffff"}, + BinVal2 = emongo:hex2dec("ffffffffffffffffffffffff"), + Size2 = size(BinVal2) + 8, + Encoded2 = emongo_bson:encode([{<<"a">>, Val2}]), + etap:is(Encoded2, <>, "data_oid hex encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded2)), [{<<"a">>, Val1}], "data_oid decodes ok"), + + ok + end)(), + + %% 8) data_boolean + (fun() -> + Val = true, + BinVal = <<1:8>>, + Size = size(BinVal) + 8, + Encoded = emongo_bson:encode([{<<"a">>, Val}]), + etap:is(Encoded, <>, "data_boolean encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_boolean decodes ok"), + ok + end)(), + + %% 9) data_date + (fun() -> + {MegaSecs,Secs,MicroSecs} = Val = now(), + Secs1 = (MegaSecs * 1000000) + Secs, + Epoch = Secs1 * 1000 + trunc(MicroSecs / 1000), + BinVal = <>, + Size = size(BinVal) + 8, + Encoded = emongo_bson:encode([{<<"a">>, Val}]), + etap:is(Encoded, <>, "data_date encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, {MegaSecs,Secs,erlang:trunc(MicroSecs / 1000) * 1000}}], "data_date decodes ok"), + ok + end)(), + + %% 10) data_null + (fun() -> + Val = undefined, + BinVal = <<>>, + Size = size(BinVal) + 8, + Encoded = emongo_bson:encode([{<<"a">>, Val}]), + etap:is(Encoded, <>, "data_null encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_null decodes ok"), + ok + end)(), + + %% 16) data_int + (fun() -> + Val = 11, + BinVal = <>, + Size = size(BinVal) + 8, + Encoded = emongo_bson:encode([{<<"a">>, Val}]), + etap:is(Encoded, <>, "data_int encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_int decodes ok"), + ok + end)(), + + %% 18) data_long + (fun() -> + Val = 5275387038659964208, + BinVal = <>, + Size = size(BinVal) + 8, + Encoded = emongo_bson:encode([{<<"a">>, Val}]), + etap:is(Encoded, <>, "data_number encodes ok"), + etap:is(hd(emongo_bson:decode(Encoded)), [{<<"a">>, Val}], "data_number decodes ok"), + ok + end)(), + + etap:end_tests(). \ No newline at end of file diff --git a/t/003-find.t b/t/003-find.t index 80737ef..4e3e97d 100644 --- a/t/003-find.t +++ b/t/003-find.t @@ -1,65 +1,64 @@ -#!/usr/bin/env escript +#!/usr/local/bin/escript %% -*- erlang -*- %%! -pa ebin -sasl errlog_type error -boot start_sasl -noshell -config priv/example main(_) -> - etap:plan(unknown), + etap:plan(unknown), error_logger:tty(false), - etap:ok(application:start(emongo) == ok, "application 'emongo' started ok"), + etap_application:start_ok(emongo, "application 'emongo' started ok"), - emongo:delete(test1, "sushi"), - etap:is(emongo:find_all(test1, "sushi"), [], "sushi collection is empty"), + emongo:delete(test1, "sushi"), + etap:is(emongo:find(test1, "sushi"), [], "sushi collection is empty"), + + [emongo:insert(test1, "sushi", [{<<"rolls">>, I}, {<<"uni">>, <<"nigiri">>}]) || I <- lists:seq(1, 1000)], + + All = emongo:find_all(test1, "sushi", [], [{orderby, [{"rolls", asc}]}]), + etap:is(length(All), 1000, "inserted documents into sushi collection"), + + (fun() -> + OrderOK = lists:foldl( + fun(Item, I) -> + case Item of + [_, {<<"rolls">>, I}, _] -> I + 1; + _ -> -1 + end + end, 1, All), + etap:is(OrderOK, 1001, "sushi collection returned in correct order"), + ok + end)(), + + %% LIMIT && ORDERBY + (fun() -> + Docs = emongo:find(test1, "sushi", [], [{limit, 1}, {orderby, [{"rolls", asc}]}, {fields, [<<"rolls">>]}]), + etap:is(length(Docs), 1, "limit returns one document"), + etap:is(length(hd(Docs)), 2, "correct number of fields"), + etap:is(lists:nth(2, hd(Docs)), {<<"rolls">>, 1}, "returned correct document from limit query"), + ok + end)(), + + %% LIMIT && OFFSET && ORDERBY + (fun() -> + Docs = emongo:find(test1, "sushi", [], [{limit, 1}, {offset, 1}, {orderby, [{"rolls", desc}]}]), + etap:is(length(Docs), 1, "limit returns one document"), + etap:is(lists:nth(2, hd(Docs)), {<<"rolls">>, 999}, "returned correct document from offset query"), + ok + end)(), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{"rolls", 100}], [{orderby, [{"rolls", desc}]}]), + etap:is(proplists:get_value(<<"rolls">>, hd(Docs)), 100, "query returned correct value"), + ok + end)(), + + %% NESTED QUERIES + [emongo:insert(test1, "sushi", [{<<"seaweed">>, [{<<"sheets">>, I}]}]) || I <- lists:seq(1,10)], + + (fun() -> + Docs = emongo:find(test1, "sushi", [{"seaweed.sheets", 5}]), + etap:is(length(Docs), 1, "correct number of results from nested query"), + etap:is(proplists:get_value(<<"seaweed">>, hd(Docs)), [{<<"sheets">>, 5}], "correct result returned"), + ok + end)(), + - [emongo:insert(test1, "sushi", [{<<"rolls">>, I}, {<<"uni">>, <<"nigiri">>}]) || I <- lists:seq(1, 1000)], - - All = emongo:find_all(test1, "sushi", [], [{orderby, [{"rolls", asc}]}]), - etap:is(length(All), 1000, "inserted documents into sushi collection"), - - (fun() -> - OrderOK = lists:foldl( - fun(Item, I) -> - case Item of - [_, {<<"rolls">>, I}, _] -> I + 1; - _ -> -1 - end - end, 1, All), - etap:is(OrderOK, 1001, "sushi collection returned in correct order"), - ok - end)(), - - %% LIMIT && ORDERBY - (fun() -> - Docs = emongo:find_all(test1, "sushi", [], [{limit, 1}, {orderby, [{"rolls", asc}]}, {fields, [<<"rolls">>]}]), - etap:is(length(Docs), 1, "limit returns one document"), - etap:is(length(hd(Docs)), 2, "correct number of fields"), - etap:is(lists:nth(2, hd(Docs)), {<<"rolls">>, 1}, "returned correct document from limit query"), - ok - end)(), - - %% LIMIT && OFFSET && ORDERBY - (fun() -> - Docs = emongo:find_all(test1, "sushi", [], [{limit, 1}, {offset, 1}, {orderby, [{"rolls", desc}]}]), - etap:is(length(Docs), 1, "limit returns one document"), - etap:is(lists:nth(2, hd(Docs)), {<<"rolls">>, 999}, "returned correct document from offset query"), - ok - end)(), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{"rolls", 100}], [{orderby, [{"rolls", desc}]}]), - etap:is(proplists:get_value(<<"rolls">>, hd(Docs)), 100, "query returned correct value"), - ok - end)(), - - %% NESTED QUERIES - [emongo:insert(test1, "sushi", [{<<"seaweed">>, [{<<"sheets">>, I}]}]) || I <- lists:seq(1,10)], - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{"seaweed.sheets", 5}]), - etap:is(length(Docs), 1, "correct number of results from nested query"), - etap:is(proplists:get_value(<<"seaweed">>, hd(Docs)), [{<<"sheets">>, 5}], "correct result returned"), - ok - end)(), - - emongo:delete(test1, "sushi"), - - etap:end_tests(). + etap:end_tests(). \ No newline at end of file diff --git a/t/004-cond-exprs.t b/t/004-cond-exprs.t index 56b37b7..0bbe127 100644 --- a/t/004-cond-exprs.t +++ b/t/004-cond-exprs.t @@ -1,112 +1,110 @@ -#!/usr/bin/env escript +#!/usr/local/bin/escript %% -*- erlang -*- %%! -pa ebin -sasl errlog_type error -boot start_sasl -noshell -config priv/example main(_) -> - etap:plan(unknown), + etap:plan(unknown), error_logger:tty(false), - etap:ok(application:start(emongo) == ok, "application 'emongo' started ok"), - - emongo:delete(test1, "sushi"), - etap:is(emongo:find_all(test1, "sushi"), [], "sushi collection is empty"), - - [emongo:insert(test1, "sushi", [{<<"rolls">>, I}]) || I <- lists:seq(1, 50)], - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"rolls">>, [{gt, 45}]}], [orderby, [{"rolls", asc}]]), - etap:is(length(Docs), 5, "correct number of results from gt query"), - etap:is(lists:sort([I || [_, {_, I}] <- Docs]), [46,47,48,49,50], "correct results from gt query"), - ok - end)(), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"rolls">>, [{lt, 5}]}], [orderby, [{"rolls", asc}]]), - etap:is(length(Docs), 4, "correct number of results from lt query"), - etap:is(lists:sort([I || [_, {_, I}] <- Docs]), [1, 2, 3, 4], "correct results from lt query"), - ok - end)(), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"rolls">>, [{gte, 45}]}], [orderby, [{"rolls", asc}]]), - etap:is(length(Docs), 6, "correct number of results from gte query"), - etap:is(lists:sort([I || [_, {_, I}] <- Docs]), [45,46,47,48,49,50], "correct results from gte query"), - ok - end)(), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"rolls">>, [{lte, 5}]}], [orderby, [{"rolls", asc}]]), - etap:is(length(Docs), 5, "correct number of results from lte query"), - etap:is(lists:sort([I || [_, {_, I}] <- Docs]), [1, 2, 3, 4, 5], "correct results from lte query"), - ok - end)(), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"rolls">>, [{ne, 1}, {ne, 50}]}], [orderby, [{"rolls", asc}]]), - etap:is(length(Docs), 48, "correct number of results from ne query"), - etap:is(lists:sort([I || [_, {_, I}] <- Docs]), lists:seq(2, 49), "correct results from ne query"), - ok - end)(), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"rolls">>, [{in, [1,2,3,4,5]}]}], [orderby, [{"rolls", asc}]]), - etap:is(length(Docs), 5, "correct number of results from in query"), - etap:is(lists:sort([I || [_, {_, I}] <- Docs]), [1,2,3,4,5], "correct results from in query"), - ok - end)(), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"rolls">>, [{nin, [1,2,3,4,5]}]}], [orderby, [{"rolls", asc}]]), - etap:is(length(Docs), 45, "correct number of results from nin query"), - etap:is(lists:sort([I || [_, {_, I}] <- Docs]), lists:seq(6,50), "correct results from nin query"), - ok - end)(), - - [emongo:insert(test1, "sushi", [{<<"maki">>, {array, [I,I+1,I+2]}}]) || I <- lists:seq(1, 10)], - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"maki">>, [{all, [2,3]}]}], [orderby, [{"maki", asc}]]), - etap:is(length(Docs), 2, "correct number of results from all query"), - etap:is(lists:sort([I || [_, {_, I}] <- Docs]), [{array, [1,2,3]}, {array, [2,3,4]}], "correct results from all query"), - ok - end)(), - - emongo:insert(test1, "sushi", [{<<"maki">>, {array, [1,2,3,4,5]}}]), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"maki">>, [{size, 5}]}], [orderby, [{"maki", asc}]]), - etap:is(length(Docs), 1, "correct number of results from size query"), - etap:is(lists:sort([I || [_, {_, I}] <- Docs]), [{array, [1,2,3,4,5]}], "correct results from size query"), - ok - end)(), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"maki">>, [{exists, true}]}], [orderby, [{"maki", asc}]]), - etap:is(length(Docs), 11, "correct number of results from exists query"), - ok - end)(), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{<<"maki">>, [{exists, false}]}], [orderby, [{"maki", asc}]]), - etap:is(length(Docs), 50, "correct number of results from exists query"), - ok - end)(), - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{where, "this.rolls > 45"}], [orderby, [{"rolls", asc}]]), - etap:is(length(Docs), 5, "correct number of results from where query"), - etap:is(lists:sort([I || [_, {_, I}] <- Docs]), [46,47,48,49,50], "correct results from where query"), - ok - end)(), - - [emongo:insert(test1, "sushi", [{<<"seaweed">>, [{<<"sheets">>, I}]}]) || I <- lists:seq(1,10)], - - (fun() -> - Docs = emongo:find_all(test1, "sushi", [{"seaweed.sheets", [{in, [3,4,5]}]}]), - etap:is(length(Docs), 3, "correct number of results from nested query"), - etap:is(lists:sort([I || [_, {<<"seaweed">>, [{<<"sheets">>, I}]}] <- Docs]), [3,4,5], "correct results from where query"), - ok - end)(), - - emongo:delete(test1, "sushi"), - - etap:end_tests(). + etap_application:start_ok(emongo, "application 'emongo' started ok"), + + emongo:delete(test1, "sushi"), + etap:is(emongo:find(test1, "sushi"), [], "sushi collection is empty"), + + [emongo:insert(test1, "sushi", [{<<"rolls">>, I}]) || I <- lists:seq(1, 50)], + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"rolls">>, [{gt, 45}]}], [orderby, [{"rolls", asc}]]), + etap:is(length(Docs), 5, "correct number of results from gt query"), + etap:is([I || [_, {_, I}] <- Docs], [46,47,48,49,50], "correct results from gt query"), + ok + end)(), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"rolls">>, [{lt, 5}]}], [orderby, [{"rolls", asc}]]), + etap:is(length(Docs), 4, "correct number of results from lt query"), + etap:is([I || [_, {_, I}] <- Docs], [1, 2, 3, 4], "correct results from lt query"), + ok + end)(), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"rolls">>, [{gte, 45}]}], [orderby, [{"rolls", asc}]]), + etap:is(length(Docs), 6, "correct number of results from gte query"), + etap:is([I || [_, {_, I}] <- Docs], [45,46,47,48,49,50], "correct results from gte query"), + ok + end)(), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"rolls">>, [{lte, 5}]}], [orderby, [{"rolls", asc}]]), + etap:is(length(Docs), 5, "correct number of results from lte query"), + etap:is([I || [_, {_, I}] <- Docs], [1, 2, 3, 4, 5], "correct results from lte query"), + ok + end)(), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"rolls">>, [{ne, 1}, {ne, 50}]}], [orderby, [{"rolls", asc}]]), + etap:is(length(Docs), 48, "correct number of results from ne query"), + etap:is([I || [_, {_, I}] <- Docs], lists:seq(2, 49), "correct results from ne query"), + ok + end)(), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"rolls">>, [{in, [1,2,3,4,5]}]}], [orderby, [{"rolls", asc}]]), + etap:is(length(Docs), 5, "correct number of results from in query"), + etap:is([I || [_, {_, I}] <- Docs], [1,2,3,4,5], "correct results from in query"), + ok + end)(), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"rolls">>, [{nin, [1,2,3,4,5]}]}], [orderby, [{"rolls", asc}]]), + etap:is(length(Docs), 45, "correct number of results from nin query"), + etap:is([I || [_, {_, I}] <- Docs], lists:seq(6,50), "correct results from nin query"), + ok + end)(), + + [emongo:insert(test1, "sushi", [{<<"maki">>, {array, [I,I+1,I+2]}}]) || I <- lists:seq(1, 10)], + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"maki">>, [{all, [2,3]}]}], [orderby, [{"maki", asc}]]), + etap:is(length(Docs), 2, "correct number of results from all query"), + etap:is([I || [_, {_, I}] <- Docs], [{array, [1,2,3]}, {array, [2,3,4]}], "correct results from all query"), + ok + end)(), + + emongo:insert(test1, "sushi", [{<<"maki">>, {array, [1,2,3,4,5]}}]), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"maki">>, [{size, 5}]}], [orderby, [{"maki", asc}]]), + etap:is(length(Docs), 1, "correct number of results from size query"), + etap:is([I || [_, {_, I}] <- Docs], [{array, [1,2,3,4,5]}], "correct results from size query"), + ok + end)(), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"maki">>, [{exists, true}]}], [orderby, [{"maki", asc}]]), + etap:is(length(Docs), 11, "correct number of results from exists query"), + ok + end)(), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{<<"maki">>, [{exists, false}]}], [orderby, [{"maki", asc}]]), + etap:is(length(Docs), 50, "correct number of results from exists query"), + ok + end)(), + + (fun() -> + Docs = emongo:find(test1, "sushi", [{where, "this.rolls > 45"}], [orderby, [{"rolls", asc}]]), + etap:is(length(Docs), 5, "correct number of results from where query"), + etap:is([I || [_, {_, I}] <- Docs], [46,47,48,49,50], "correct results from where query"), + ok + end)(), + + [emongo:insert(test1, "sushi", [{<<"seaweed">>, [{<<"sheets">>, I}]}]) || I <- lists:seq(1,10)], + + (fun() -> + Docs = emongo:find(test1, "sushi", [{"seaweed.sheets", [{in, [3,4,5]}]}]), + etap:is(length(Docs), 3, "correct number of results from nested query"), + etap:is([I || [_, {<<"seaweed">>, [{<<"sheets">>, I}]}] <- Docs], [3,4,5], "correct results from where query"), + ok + end)(), + + etap:end_tests(). \ No newline at end of file diff --git a/t/005-drop.t b/t/005-drop.t deleted file mode 100644 index e127511..0000000 --- a/t/005-drop.t +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env escript -%% -*- erlang -*- -%%! -pa ebin -sasl errlog_type error -boot start_sasl -noshell -config priv/example - -main(_) -> - etap:plan(unknown), - error_logger:tty(false), - etap:ok(application:start(emongo) == ok, "application 'emongo' started ok"), - - [emongo:insert(test1, "sushi", [{<<"rolls">>, I}]) || I <- lists:seq(1, 50)], - [emongo:insert(test1, "sushi2", [{<<"rolls">>, I}]) || I <- lists:seq(1, 50)], - - ok = emongo:drop_database(test1), - - emongo:insert(test1, "sushi", [{<<"rolls">>, 1}]), - etap:is(length(emongo:find_all(test1, "sushi")), 1, "There is 1 doc"), - etap:is(length(emongo:find_all(test1, "sushi2")), 0, "there is 0 doc"), - - emongo:delete(test1, "sushi"), - - etap:end_tests(). diff --git a/t/006-multiup.t b/t/006-multiup.t deleted file mode 100644 index 1a22869..0000000 --- a/t/006-multiup.t +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env escript -%% -*- erlang -*- -%%! -pa ebin -sasl errlog_type error -boot start_sasl -noshell -config priv/example - -main(_) -> - etap:plan(unknown), - error_logger:tty(false), - etap:ok(application:start(emongo) == ok, "application 'emongo' started ok"), - - emongo:delete(test1, "sushi"), - etap:is(emongo:find_all(test1, "sushi"), [], "sushi collection is empty"), - - [emongo:insert(test1, "sushi", [{<<"rolls">>, I}]) || I <- lists:seq(1, 50)], - - (fun() -> - emongo:update(test1, "sushi", [{<<"rolls">>, [{gt, 45}]}], [{<<"$set">>, [{<<"rolls">>, 100}]}], false, true), - Docs = emongo:find_all(test1, "sushi", [{<<"rolls">>, 100}], []), - etap:is(length(Docs), 5, "correct number of results after multiupdate"), - etap:is([I || [_, {_, I}] <- Docs], [100,100,100,100,100], "correct results after multiupdate"), - ok - end)(), - - emongo:delete(test1, "sushi"), - - etap:end_tests(). diff --git a/t/007-performance.t b/t/007-performance.t deleted file mode 100644 index 8bf438c..0000000 --- a/t/007-performance.t +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env escript -%% -*- erlang -*- -%%! -pa ebin -sasl errlog_type error -boot start_sasl -noshell -config priv/example - --define(NUM_PROCESSES, 500). --define(NUM_TESTS_PER_PID, 10). --define(POOL, test2). --define(COLL, <<"sushi">>). --define(TIMEOUT, 10000). --define(OUT(Format, Data), io:format(Format ++ "\n", Data)). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -main(_) -> - etap:plan(unknown), - error_logger:tty(false), - etap:ok(application:start(emongo) == ok, "application 'emongo' started ok"), - emongo:delete(?POOL, ?COLL, []), - (fun() -> - test_performance() - end)(), - emongo:delete(?POOL, ?COLL, []), - etap:end_tests(). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -test_performance() -> - ?OUT("Testing performance", []), - Start = cur_time_ms(), - try - start_processes(?NUM_PROCESSES), - block_until_done(?NUM_PROCESSES) - after - % Clean up in case something failed. - emongo:delete_sync(?POOL, ?COLL, []) - end, - End = cur_time_ms(), - ?OUT("Test passed in ~p ms\n", [End - Start]). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -start_processes(X) when X =< 0 -> ok; -start_processes(X) -> - Pid = self(), - proc_lib:spawn(fun() -> - run_tests(Pid, X, ?NUM_TESTS_PER_PID) - end), - start_processes(X - 1). - -run_tests(Pid, _, Y) when Y =< 0 -> - Pid ! done; -run_tests(Pid, X, Y) -> - Num = (X bsl 16) bor Y, % Make up a unique number for this run - try - IRes = emongo:insert_sync(?POOL, ?COLL, [{"_id", Num}]), - ok = check_result(IRes), - %etap:is(check_result(IRes), ok, "insert_sync ok"), - - FMRes = emongo:find_and_modify(?POOL, ?COLL, [{"_id", Num}], - [{<<"$set">>, [{<<"fm">>, Num}]}], [{new, true}]), - [[{<<"value">>, [{<<"_id">>, Num}, {<<"fm">>, Num}]}, {<<"ok">>, 1.0}]] = - FMRes, - %etap:is(FMRes, [[{<<"value">>, [{<<"_id">>, Num}, {<<"fm">>, Num}]}, - % {<<"ok">>, 1.0}]], "find_all ok"), - - URes = emongo:update_sync(?POOL, ?COLL, [{"_id", Num}], - [{<<"$set">>, [{<<"us">>, Num}]}], false), - ok = check_result(URes), - %etap:is(check_result(URes), ok, "update_sync ok"), - - FARes = emongo:find_all(?POOL, ?COLL, [{"_id", Num}]), - [[{<<"_id">>, Num}, {<<"fm">>, Num}, {<<"us">>, Num}]] = FARes, - %etap:is(FARes, [[{<<"_id">>, Num}, {<<"fm">>, Num}, {<<"us">>, Num}]], - % "find_all ok"), - - DRes = emongo:delete_sync(?POOL, ?COLL, [{"_id", Num}]), - ok = check_result(DRes) - %etap:is(check_result(DRes), ok, "delete_sync ok") - catch _:E -> - ?OUT("Exception occurred for test ~.16b: ~p\n~p\n", - [Num, E, erlang:get_stacktrace()]), - throw(test_failed) - end, - run_tests(Pid, X, Y - 1). - -block_until_done(X) when X =< 0 -> ok; -block_until_done(X) -> - receive done -> ok - after ?TIMEOUT -> - ?OUT("No response\n", []), - throw(test_failed) - end, - block_until_done(X - 1). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -cur_time_ms() -> - {MegaSec, Sec, MicroSec} = erlang:now(), - MegaSec * 1000000000 + Sec * 1000 + erlang:round(MicroSec / 1000). - -check_result([List]) when is_list(List) -> check_result_int(List); -check_result(_) -> {error, invalid_result}. - -check_result_int([]) -> {error, result_unknown}; -check_result_int([{<<"err">>, null} | _]) -> ok; -check_result_int([{<<"err">>, Error} | _]) -> {error, Error}; -check_result_int([_ | Rest]) -> check_result_int(Rest). diff --git a/t/pqueue_test.erl b/t/pqueue_test.erl deleted file mode 100644 index a62e704..0000000 --- a/t/pqueue_test.erl +++ /dev/null @@ -1,69 +0,0 @@ --module(pqueue_test). - --compile(export_all). - -test(Data, F) -> - {Time1, Sorted1} = timer:tc(lists, sort, [Data]), - {Time21, Pushed1} = timer:tc(lists, foldl, [fun({P, I}, Q) -> - pqueue:push(P, I, Q) - end, pqueue:new(), Data]), - - Sorted = [I || {_, V} = I <- Sorted1, F(V)], - Pushed = pqueue:filter(F, Pushed1), - - {Time22, _} = timer:tc(?MODULE, pop_items, [Pushed]), - Time2 = Time21 + Time22, - - io:format("Time sorted: ~p~nTime pushed: ~p~nDivide: ~p~n", [Time1, Time2, Time1/Time2]), - - LenS = length(Sorted), - LenP = pqueue:size(Pushed), - - LenS = LenP, - io:format("Length: ~p~n", [LenS]), - - lists:foldl(fun({_, I}, Q) -> - {I, NQ} = pqueue:pop(Q), - NQ - end, Pushed, Sorted). - -test() -> - Len = 10000, - Dev = 10, - Fun = fun(I) -> I rem 3 =/= 0 end, - - Data = [{random:uniform(Dev), I} || I <- lists:seq(1, Len)], - test(Data, Fun). - -test2() -> - Queue0 = push_items([{1, 1}, {1, 2}, {5, 3}, {2, 4}], pqueue:new()), - 4 = pqueue:size(Queue0), - - Queue1 = ensure_items([1, 2, 4], Queue0), - 1 = pqueue:size(Queue1), - - Queue2 = push_items([{2, 5}, {3, 6}], Queue1), - 3 = pqueue:size(Queue2), - - Queue3 = ensure_items([5, 3, 6], Queue2), - - Queue3 = pqueue:new(). - -push_items(Data, Queue) -> - lists:foldl(fun({P, I}, Q) -> pqueue:push(P, I, Q) end, - Queue, - Data). - -ensure_items(Items, Queue) -> - lists:foldl(fun(I, Q) -> {I, NQ} = pqueue:pop(Q), NQ end, - Queue, - Items). - -pop_items(Queue) -> - case pqueue:size(Queue) of - 0 -> - ok; - _ -> - {_Item, NewQueue} = pqueue:pop(Queue), - pop_items(NewQueue) - end. From a484d6c1f8787a543a7b29d21919748151029e47 Mon Sep 17 00:00:00 2001 From: Jeremy Hood Date: Thu, 19 Apr 2012 09:30:16 -0400 Subject: [PATCH 3/3] Back to version based on Jacob's --- ebin/emongo.app | 14 ++++ include/emongo_public.hrl | 12 +++ rebar | Bin 0 -> 112021 bytes src/emongo_conn.erl | 152 ++++++++++++++++++++++++++++++++++++++ test/emongo_test.erl | 113 ++++++++++++++++++++++++++++ 5 files changed, 291 insertions(+) create mode 100644 ebin/emongo.app create mode 100644 include/emongo_public.hrl create mode 100755 rebar create mode 100644 src/emongo_conn.erl create mode 100644 test/emongo_test.erl diff --git a/ebin/emongo.app b/ebin/emongo.app new file mode 100644 index 0000000..174397a --- /dev/null +++ b/ebin/emongo.app @@ -0,0 +1,14 @@ +{application, emongo, [ + {description, "Erlang MongoDB Driver"}, + {vsn, "0.2"}, + {modules, [ + emongo, + emongo_app, + emongo_bson, + emongo_conn, + emongo_packet + ]}, + {registered, []}, + {mod, {emongo_app, []}}, + {applications, [kernel, stdlib, sasl]} +]}. diff --git a/include/emongo_public.hrl b/include/emongo_public.hrl new file mode 100644 index 0000000..df98e44 --- /dev/null +++ b/include/emongo_public.hrl @@ -0,0 +1,12 @@ +-ifndef(EMONGO_PUBLIC). + +-record(response, {header, response_flag, cursor_id, offset, limit, documents}). + +% Additional options that can be passed to emongo:find() +-define(TAILABLE_CURSOR, 2). +-define(SLAVE_OK, 4). +-define(OPLOG, 8). +-define(NO_CURSOR_TIMEOUT, 16). + +-define(EMONGO_PUBLIC, true). +-endif. diff --git a/rebar b/rebar new file mode 100755 index 0000000000000000000000000000000000000000..1cac23b9e2aefef305864f9916e4b1b2d3dd1160 GIT binary patch literal 112021 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

mGiZ@ERHT?``>DFfVJ{-Psu21AFn7|) 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�D#0~JwMJadL}=Mc9gtT&{@U%rcdkSV|pNy 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 literal 0 HcmV?d00001 diff --git a/src/emongo_conn.erl b/src/emongo_conn.erl new file mode 100644 index 0000000..01b5bfc --- /dev/null +++ b/src/emongo_conn.erl @@ -0,0 +1,152 @@ +%% Copyright (c) 2009 Jacob Vorreuter +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +-module(emongo_conn). + +-export([start_link/3, init/4, send/3, send_sync/5, send_recv/4]). + +-record(request, {req_id, requestor}). +-record(state, {pool_id, socket, requests}). + +-include("emongo.hrl"). + +start_link(PoolId, Host, Port) -> + proc_lib:start_link(?MODULE, init, [PoolId, Host, Port, self()]). + +init(PoolId, Host, Port, Parent) -> + Socket = open_socket(Host, Port), + proc_lib:init_ack(Parent, self()), + loop(#state{pool_id=PoolId, socket=Socket, requests=[]}, <<>>). + +send(Pid, ReqID, Packet) -> + case gen:call(Pid, '$emongo_conn_send', {ReqID, Packet}) of + {ok, Result} -> Result; + {error, Reason} -> exit(Reason) + end. + +send_sync(Pid, ReqID, Packet1, Packet2, Timeout) -> + try + {ok, Resp} = gen:call(Pid, '$emongo_conn_send_sync', {ReqID, Packet1, Packet2}, Timeout), + Documents = emongo_bson:decode(Resp#response.documents), + Resp#response{documents=Documents} + catch + exit:timeout-> + %Clear the state from the timed out call + gen:call(Pid, '$emongo_recv_timeout', ReqID, Timeout), + exit(timeout) + end. + +send_recv(Pid, ReqID, Packet, Timeout) -> + try + {ok, Resp} = gen:call(Pid, '$emongo_conn_send_recv', {ReqID, Packet}, Timeout), + Documents = emongo_bson:decode(Resp#response.documents), + Resp#response{documents=Documents} + catch + exit:timeout-> + %Clear the state from the timed out call + gen:call(Pid, '$emongo_recv_timeout', ReqID, Timeout), + exit(timeout) + end. + +loop(State, Leftover) -> + {NewState, NewLeftover} = try + Socket = State#state.socket, + receive + {'$emongo_conn_send', {From, Mref}, {_ReqID, Packet}} -> + gen_tcp:send(Socket, Packet), + gen:reply({From, Mref}, ok), + {State, Leftover}; + {'$emongo_conn_send_sync', {From, Mref}, {ReqID, Packet1, Packet2}} -> + % Packet2 is the packet containing getlasterror. + % Send both packets in the same TCP packet for performance reasons. It's + % about 3 times faster. + gen_tcp:send(Socket, <>), + Request = #request{req_id=ReqID, requestor={From, Mref}}, + State1 = State#state{requests=[{ReqID, Request}|State#state.requests]}, + {State1, Leftover}; + {'$emongo_conn_send_recv', {From, Mref}, {ReqID, Packet}} -> + gen_tcp:send(Socket, Packet), + Request = #request{req_id=ReqID, requestor={From, Mref}}, + State1 = State#state{requests=[{ReqID, Request}|State#state.requests]}, + {State1, Leftover}; + {'$emongo_recv_timeout', {From, Mref}, ReqID} -> + case lists:keytake(ReqID, 1, State#state.requests) of + false -> + gen:reply({From, Mref}, ok), + {State, Leftover}; + {value, _, Others} -> + gen:reply({From, Mref}, ok), + %Loop again, but drop any leftovers to + %prevent the loop response processing + %from getting out of sync and causing all + %subsequent calls to send_recv to fail. + %loop(State#state{requests=Others}, <<>>) + + % Leave Leftover there because it could be from a different + % request than the one timing out. This Pid is still in the + % pool and can still be used by other processes. If the + % data gets out of sync, the socket needs to be closed and + % reopened. + {State#state{requests=Others}, Leftover} + end; + {tcp, Socket, Data} -> + {_NewState, _NewLeftover} = + process_bin(State, <>); + {tcp_closed, Socket} -> + exit(tcp_closed); + {tcp_error, Socket, Reason} -> + exit(Reason) + end + catch _:Error -> + % The exit message has to include the pool_id and follow a format the emongo + % module expects so this process can be restarted. + exit({?MODULE, State#state.pool_id, Error}) + end, + loop(NewState, NewLeftover). + +open_socket(Host, Port) -> + case gen_tcp:connect(Host, Port, [binary, {active, true}]) of + {ok, Sock} -> + Sock; + {error, Reason} -> + exit({failed_to_open_socket, Reason}) + end. + +process_bin(State, <<>>) -> + {State, <<>>}; +process_bin(State, Bin) -> + case emongo_packet:decode_response(Bin) of + undefined -> + {State, Bin}; + {Resp, Tail} -> + ResponseTo = (Resp#response.header)#header.response_to, + NewState = case lists:keytake(ResponseTo, 1, State#state.requests) of + false -> + State; + {value, {_ReqID, Request}, Others} -> + gen:reply(Request#request.requestor, Resp), + State#state{requests=Others} + end, + % Continue processing Tail in case there's another complete message + % in it. + process_bin(NewState, Tail) + end. diff --git a/test/emongo_test.erl b/test/emongo_test.erl new file mode 100644 index 0000000..13d3a99 --- /dev/null +++ b/test/emongo_test.erl @@ -0,0 +1,113 @@ +-module(emongo_test). +-include_lib("eunit/include/eunit.hrl"). +-compile(export_all). + +-define(NUM_PROCESSES, 500). +-define(NUM_TESTS_PER_PID, 10). +-define(POOL, pool1). +-define(COLL, <<"test">>). +-define(TIMEOUT, 5000). +-define(OUT(F, D), ?debugFmt(F, D)). + +setup() -> + ensure_started(sasl), + ensure_started(emongo), + emongo:add_pool(?POOL, "localhost", 27017, "testdatabase", 10), + ok. + +cleanup(_) -> + ok. + +run_test_() -> + [{setup, + fun setup/0, + fun cleanup/1, + [ + fun test_performance/0 + ] + }]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +test_performance() -> + ?OUT("Testing performance.", []), + emongo:delete_sync(?POOL, ?COLL), + Start = cur_time_ms(), + try + start_processes(?NUM_PROCESSES), + block_until_done(?NUM_PROCESSES) + after + % Clean up in case something failed. + emongo:delete_sync(?POOL, ?COLL) + end, + End = cur_time_ms(), + ?OUT("Test passed in ~p ms\n", [End - Start]). + +start_processes(X) when X =< 0 -> ok; +start_processes(X) -> + Pid = self(), + proc_lib:spawn(fun() -> + run_tests(Pid, X, ?NUM_TESTS_PER_PID) + end), + start_processes(X - 1). + +run_tests(Pid, _, Y) when Y =< 0 -> + Pid ! done; +run_tests(Pid, X, Y) -> + Num = (X bsl 16) bor Y, % Make up a unique number for this run + try + IRes = emongo:insert_sync(?POOL, ?COLL, [{"_id", Num}], [response_options]), + ok = check_result("insert_sync", IRes, 0), + + [FMRes] = emongo:find_and_modify(?POOL, ?COLL, [{"_id", Num}], + [{<<"$set">>, [{<<"fm">>, Num}]}], [{new, true}]), + FMVal = proplists:get_value(<<"value">>, FMRes), + [{<<"_id">>, Num}, {<<"fm">>, Num}] = FMVal, + + URes = emongo:update_sync(?POOL, ?COLL, [{"_id", Num}], + [{<<"$set">>, [{<<"us">>, Num}]}], false, [response_options]), + ok = check_result("update_sync", URes, 1), + + FARes = emongo:find_all(?POOL, ?COLL, [{"_id", Num}]), + [[{<<"_id">>, Num}, {<<"fm">>, Num}, {<<"us">>, Num}]] = FARes, + + DRes = emongo:delete_sync(?POOL, ?COLL, [{"_id", Num}], [response_options]), + ok = check_result("delete_sync", DRes, 1) + catch _:E -> + ?OUT("Exception occurred for test ~.16b: ~p\n~p\n", + [Num, E, erlang:get_stacktrace()]), + throw(test_failed) + end, + run_tests(Pid, X, Y - 1). + +block_until_done(X) when X =< 0 -> ok; +block_until_done(X) -> + receive done -> ok + after ?TIMEOUT -> + ?OUT("No response\n", []), + throw(test_failed) + end, + block_until_done(X - 1). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +ensure_started(App) -> + case application:start(App) of + ok -> ok; + {error, {already_started, App}} -> ok + end. + +cur_time_ms() -> + {MegaSec, Sec, MicroSec} = erlang:now(), + MegaSec * 1000000000 + Sec * 1000 + erlang:round(MicroSec / 1000). + +check_result(Desc, + {response, _,_,_,_,_, [List]}, + ExpectedN) when is_list(List) -> + {_, Err} = lists:keyfind(<<"err">>, 1, List), + {_, N} = lists:keyfind(<<"n">>, 1, List), + if Err == undefined, N == ExpectedN -> ok; + true -> + ?OUT("Unexpected result for ~p: Err = ~p; N = ~p", [Desc, Err, N]), + throw({error, invalid_db_response}) + end.